查看源码 cover - 代码覆盖率分析工具
简介
模块 cover
提供了一组用于 Erlang 程序代码覆盖率分析的函数,用于统计每个可执行行被执行的次数。
代码覆盖率分析可用于验证测试用例,确保覆盖所有相关代码,并有助于查找代码中的瓶颈。
开始使用 Cover
示例
假设需要验证以下程序的测试用例
-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.90.0>}
若要将其他节点包含在覆盖率分析中,请使用 cover: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
,结果显示总共调用了模块中的函数十二次。
如果分析是在函数级别进行的,则结果将作为元组列表 {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:analyse_to_file/1
将 channel
的行级别调用分析写入文件
12> cover:analyse_to_file(channel).
{ok,"channel.COVER.out"}
该函数会创建 channel.erl
的副本,并在其中指定每行可执行代码被执行的次数。输出文件名为 channel.COVER.out
。
File generated from /Users/bjorng/git/otp/channel.erl by COVER 2024-03-20 at 13:25:04
****************************************************************************
| -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,并卸载所有经过 cover 编译的模块。现在,channel
的代码将像往常一样从当前路径中的 .beam
文件加载。
13> code:which(channel).
cover_compiled
14> cover:stop().
ok
15> code:which(channel).
"./channel.beam"
其他
性能
在经过 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 编译的模块。