非常教程

Erlang 20参考手册

tools

1.覆盖 | 1. cover

1.1简介

该模块cover为Erlang程序的覆盖率分析提供了一组函数,计算每个executable line执行的次数。

覆盖率分析可用于验证测试用例,确保所有相关代码都已涵盖,并且在查找代码中的瓶颈时可能会有所帮助。

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().

制备

首先,封面必须启动。这产生了一个拥有覆盖数据库的过程,其中所有覆盖数据将被存储。

1> cover:start().
{ok,<0.30.0>}

要在覆盖率分析中包含其他节点,请使用start/1。所有覆盖编译模块将被加载到所有节点上,并且在分析时将汇总所有节点的数据。为了简单起见,这个例子只涉及当前节点。

在进行任何分析之前,涉及的模块必须进行封面编译。这意味着在将模块编译为二进制文件之前,会将一些额外的信息添加到模块中loaded。该模块的源文件不受影响,也没有.beam创建文件。

2> cover:compile_module(channel).
{ok,channel}

每次channel调用Cover编译模块中的函数时,有关该调用的信息都将添加到Cover数据库中。运行测试用例:

3> test:s().
ok

封面分析是通过检查封面数据库的内容来执行的。该输出通过两个参数确定,LevelAnalysisAnalysiscoverage或者calls并确定分析的类型。Level或者是modulefunctionclause,或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没有被执行。

分析到文件

channel可以使用cover:analysis_to_file/1以下命令将行级别调用分析写入文件:

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确实应该发现一个错误。

当封面分析准备就绪后,封面将停止,所有封面编译模块都会被封上unloaded。代码channel现在按照通常的方式从.beam当前路径中的文件加载。

13> code:which(channel).
cover_compiled
14> cover:stop().
ok
15> code:which(channel).
"./channel.beam"

1.3杂项

性能

Cover编译模块中的代码执行速度比定期编译模块更慢,耗费更多的内存。由于Cover数据库包含每个Cover编译模块中每个可执行文件行的信息,因此性能与Cover编译模块的大小和数量成比例地下降。

为了提高性能,当分析结果盖就可以做多次调用analyseanalyse_to_file一次。您也可以使用async_analyse_to_file便利功能。

可执行的行

封面使用可执行行的概念,这是包含可执行表达式(如匹配或函数调用)的代码行。在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/1code:which/1查看一个模块是否被封装编译(并且仍然载入)。

当封面停止时,所有封面编译模块都将被卸载。

Erlang 20

Erlang 是一种通用的面向并发的编程语言,可应付大规模开发活动的程序设计语言和运行环境。

主页 https://www.erlang.org/
源码 https://github.com/erlang/otp
版本 20
发布版本 20.1