1 覆盖
1.1 简介
模块 cover 提供了一组用于对 Erlang 程序进行覆盖分析的函数,计算每个 可执行行 被执行的次数。
覆盖分析可用于验证测试用例,确保覆盖所有相关代码,在查找代码中的瓶颈时可能会有所帮助。
1.2 覆盖入门
示例
假设应该验证以下程序的测试用例
-module(channel). -behaviour(gen_server). -export([start_link/0,stop/0]). -export([alloc/0,free/1]). % client interface -export([init/1,handle_call/3,terminate/2]). % callback functions start_link() -> gen_server:start_link({local,channel},channel,[],[]). stop() -> gen_server:call(channel,stop). %%%-Client interface functions------------------------------------------- alloc() -> gen_server:call(channel,alloc). free(Channel) -> gen_server:call(channel,{free,Channel}). %%%-gen_server callback functions---------------------------------------- init(_Arg) -> {ok,channels()}. handle_call(stop,Client,Channels) -> {stop,normal,ok,Channels}; handle_call(alloc,Client,Channels) -> {Ch,Channels2} = alloc(Channels), {reply,{ok,Ch},Channels2}; handle_call({free,Channel},Client,Channels) -> Channels2 = free(Channel,Channels), {reply,ok,Channels2}. terminate(_Reason,Channels) -> ok. %%%-Internal functions--------------------------------------------------- channels() -> [ch1,ch2,ch3]. alloc([Channel|Channels]) -> {Channel,Channels}; alloc([]) -> false. free(Channel,Channels) -> [Channel|Channels].
测试用例的实现如下
-module(test). -export([s/0]). s() -> {ok,Pid} = channel:start_link(), {ok,Ch1} = channel:alloc(), ok = channel:free(Ch1), ok = channel:stop().
准备
首先,必须启动 Cover。这将生成一个进程,该进程拥有 Cover 数据库,所有覆盖数据都将存储在其中。
1> cover:start().
{ok,<0.30.0>}
要将其他节点包含在覆盖分析中,请使用 start/1。然后,所有经过 Cover 编译的模块将在所有节点上加载,分析时将汇总所有节点的数据。为简单起见,本示例仅涉及当前节点。
在进行任何分析之前,必须 **使用 Cover 编译** 所涉及的模块。这意味着在将模块编译成二进制文件之前,会向模块添加一些额外的信息,然后该二进制文件将被 加载。模块的源文件不受影响,也不会创建任何 .beam 文件。
2> cover:compile_module(channel).
{ok,channel}
每次调用 Cover 编译的模块 channel 中的函数时,有关该调用的信息都将添加到 Cover 数据库中。运行测试用例
3> test:s().
ok
通过检查 Cover 数据库的内容来执行 Cover 分析。输出由两个参数确定,即 Level 和 Analysis。 Analysis 既可以是 coverage,也可以是 calls,它决定了分析的类型。 Level 既可以是 module,也可以是 function,也可以是 clause,也可以是 line,它决定了分析的级别。
覆盖分析
类型为 coverage 的分析用于找出已执行了多少代码,以及未执行了多少代码。覆盖率由一个元组 {Cov,NotCov} 表示,其中 Cov 是至少执行过一次的可执行行的数量,而 NotCov 是未执行过的可执行行的数量。
如果分析是在模块级别进行的,则结果将作为元组 {Module,{Cov,NotCov}} 给出,它表示整个模块。
4> cover:analyse(channel,coverage,module).
{ok,{channel,{14,1}}}
对于 channel,结果表明模块中的 14 行已覆盖,但有一行未覆盖。
如果分析是在函数级别进行的,则结果将作为元组 {Function,{Cov,NotCov}} 的列表给出,每个函数在模块中都有一个。函数由其模块名称、函数名称和元数指定
5> cover:analyse(channel,coverage,function).
{ok,[{{channel,start_link,0},{1,0}},
{{channel,stop,0},{1,0}},
{{channel,alloc,0},{1,0}},
{{channel,free,1},{1,0}},
{{channel,init,1},{1,0}},
{{channel,handle_call,3},{5,0}},
{{channel,terminate,2},{1,0}},
{{channel,channels,0},{1,0}},
{{channel,alloc,1},{1,1}},
{{channel,free,2},{1,0}}]}
对于 channel,结果表明未覆盖的行位于函数 channel:alloc/1 中。
如果分析是在子句级别进行的,则结果将作为元组 {Clause,{Cov,NotCov}} 的列表给出,每个函数子句在模块中都有一个。子句由其模块名称、函数名称、元数和函数定义中的位置指定
6> cover:analyse(channel,coverage,clause).
{ok,[{{channel,start_link,0,1},{1,0}},
{{channel,stop,0,1},{1,0}},
{{channel,alloc,0,1},{1,0}},
{{channel,free,1,1},{1,0}},
{{channel,init,1,1},{1,0}},
{{channel,handle_call,3,1},{1,0}},
{{channel,handle_call,3,2},{2,0}},
{{channel,handle_call,3,3},{2,0}},
{{channel,terminate,2,1},{1,0}},
{{channel,channels,0,1},{1,0}},
{{channel,alloc,1,1},{1,0}},
{{channel,alloc,1,2},{0,1}},
{{channel,free,2,1},{1,0}}]}
对于 channel,结果表明未覆盖的行位于 channel:alloc/1 的第二个子句中。
最后,如果分析是在行级别进行的,则结果将作为元组 {Line,{Cov,NotCov}} 的列表给出,源代码中的每行可执行行都有一个。行由其模块名称和行号指定。
7> cover:analyse(channel,coverage,line).
{ok,[{{channel,9},{1,0}},
{{channel,12},{1,0}},
{{channel,17},{1,0}},
{{channel,20},{1,0}},
{{channel,25},{1,0}},
{{channel,28},{1,0}},
{{channel,31},{1,0}},
{{channel,32},{1,0}},
{{channel,35},{1,0}},
{{channel,36},{1,0}},
{{channel,39},{1,0}},
{{channel,44},{1,0}},
{{channel,47},{1,0}},
{{channel,49},{0,1}},
{{channel,52},{1,0}}]}
对于 channel,结果表明未覆盖的行是第 49 行。
调用统计信息
类型为 calls 的分析用于找出某些内容被调用的次数,它由一个整数 Calls 表示。
如果分析是在模块级别进行的,则结果将作为元组 {Module,Calls} 给出。这里 Calls 是对模块中函数的总调用次数
8> cover:analyse(channel,calls,module).
{ok,{channel,12}}
对于 channel,结果表明已对模块中的函数进行了总共 12 次调用。
如果分析是在函数级别进行的,则结果将作为元组 {Function,Calls} 的列表给出。这里 Calls 是对每个函数的调用次数
9> cover:analyse(channel,calls,function).
{ok,[{{channel,start_link,0},1},
{{channel,stop,0},1},
{{channel,alloc,0},1},
{{channel,free,1},1},
{{channel,init,1},1},
{{channel,handle_call,3},3},
{{channel,terminate,2},1},
{{channel,channels,0},1},
{{channel,alloc,1},1},
{{channel,free,2},1}]}
对于 channel,结果表明 handle_call/3 是模块中最常调用的函数(三次调用)。所有其他函数都被调用过一次。
如果分析是在子句级别进行的,则结果将作为元组 {Clause,Calls} 的列表给出。这里 Calls 是对每个函数子句的调用次数
10> cover:analyse(channel,calls,clause).
{ok,[{{channel,start_link,0,1},1},
{{channel,stop,0,1},1},
{{channel,alloc,0,1},1},
{{channel,free,1,1},1},
{{channel,init,1,1},1},
{{channel,handle_call,3,1},1},
{{channel,handle_call,3,2},1},
{{channel,handle_call,3,3},1},
{{channel,terminate,2,1},1},
{{channel,channels,0,1},1},
{{channel,alloc,1,1},1},
{{channel,alloc,1,2},0},
{{channel,free,2,1},1}]}
对于 channel,结果表明所有子句都被调用过一次,除了 channel:alloc/1 的第二个子句,它根本没有被调用过。
最后,如果分析是在行级别进行的,则结果将作为元组 {Line,Calls} 的列表给出。这里 Calls 是每行被执行的次数
11> cover:analyse(channel,calls,line).
{ok,[{{channel,9},1},
{{channel,12},1},
{{channel,17},1},
{{channel,20},1},
{{channel,25},1},
{{channel,28},1},
{{channel,31},1},
{{channel,32},1},
{{channel,35},1},
{{channel,36},1},
{{channel,39},1},
{{channel,44},1},
{{channel,47},1},
{{channel,49},0},
{{channel,52},1}]}
对于 channel,结果表明所有行都被执行过一次,除了第 49 行,它根本没有被执行过。
分析到文件
可以使用 cover:analysis_to_file/1 将 channel 的行级调用分析写入文件
12> cover:analyse_to_file(channel).
{ok,"channel.COVER.out"}
该函数会创建 channel.erl 的副本,其中会为每个可执行行指定该行被执行的次数。输出文件名为 channel.COVER.out。
File generated from channel.erl by COVER 2001-05-21 at 11:16:38 **************************************************************************** | -module(channel). | -behaviour(gen_server). | | -export([start_link/0,stop/0]). | -export([alloc/0,free/1]). % client interface | -export([init/1,handle_call/3,terminate/2]). % callback functions | | start_link() -> 1..| gen_server:start_link({local,channel},channel,[],[]). | | stop() -> 1..| gen_server:call(channel,stop). | | %%%-Client interface functions------------------------------------ | | alloc() -> 1..| gen_server:call(channel,alloc). | | free(Channel) -> 1..| gen_server:call(channel,{free,Channel}). | | %%%-gen_server callback functions--------------------------------- | | init(_Arg) -> 1..| {ok,channels()}. | | handle_call(stop,Client,Channels) -> 1..| {stop,normal,ok,Channels}; | | handle_call(alloc,Client,Channels) -> 1..| {Ch,Channels2} = alloc(Channels), 1..| {reply,{ok,Ch},Channels2}; | | handle_call({free,Channel},Client,Channels) -> 1..| Channels2 = free(Channel,Channels), 1..| {reply,ok,Channels2}. | | terminate(_Reason,Channels) -> 1..| ok. | | %%%-Internal functions-------------------------------------------- | | channels() -> 1..| [ch1,ch2,ch3]. | | alloc([Channel|Channels]) -> 1..| {Channel,Channels}; | alloc([]) -> 0..| false. | | free(Channel,Channels) -> 1..| [Channel|Channels].
结论
通过查看分析结果,可以推断出测试用例未涵盖所有通道都被分配的情况,因此应相应地扩展 test.erl。
顺便说一下,当测试用例被修正时,确实应该在 channel 中发现一个错误。
准备就绪后,将停止 Cover 分析,所有经过 Cover 编译的模块都将被 卸载。现在,将像往常一样从当前路径中的 .beam 文件中加载 channel 的代码。
13> code:which(channel). cover_compiled 14> cover:stop(). ok 15> code:which(channel). "./channel.beam"
1.3 杂项
性能
在经过 Cover 编译的模块中执行代码比在常规编译的模块中执行代码更慢,并且占用更多内存。由于 Cover 数据库包含有关每个经过 Cover 编译的模块中每个可执行行的信息,因此性能会随着经过 Cover 编译的模块的大小和数量成比例地下降。
为了提高分析 Cover 结果的性能,可以一次对 analyse 和 analyse_to_file 进行多次调用。您还可以使用 async_analyse_to_file 方便函数。
可执行行
Cover 使用 **可执行行** 的概念,它是包含可执行表达式的代码行,例如匹配或函数调用。空白行或包含注释、函数头或 case 或 receive 语句中的模式的行不可执行。
在下面的示例中,第 2、4、6、8 和 11 行是可执行行
1: is_loaded(Module,Compiled) -> 2: case get_file(Module,Compiled) of 3: {ok,File} -> 4: case code:which(Module) of 5: ?TAG -> 6: {loaded,File}; 7: _ -> 8: unloaded 9: end; 10: false -> 11: false 12: end.
代码加载机制
当模块经过 Cover 编译时,它也会使用 Erlang 的正常代码加载机制加载。这意味着,如果在 Cover 会话期间重新加载经过 Cover 编译的模块(例如使用 c(Module)),它将不再经过 Cover 编译。
使用 cover:is_compiled/1 或 code:which/1 查看模块是否经过 Cover 编译(以及是否已加载)。
停止 Cover 时,将卸载所有经过 Cover 编译的模块。