檢視原始碼 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 資料庫的內容來執行。輸出由兩個參數決定,LevelAnalysisAnalysis 可以是 coveragecalls,並決定分析的類型。Level 可以是 modulefunctionclauseline,並決定分析的層級。

覆蓋率分析

coverage 類型的分析用於找出有多少程式碼已被執行,以及有多少程式碼尚未被執行。覆蓋率以元組 {Cov,NotCov} 表示,其中 Cov 是至少執行過一次的可執行程式碼行數,而 NotCov 是尚未執行的可執行程式碼行數。

如果分析在模組層級進行,則整個模組的結果會以元組 {Module,{Cov,NotCov}} 的形式給出

4> cover:analyse(channel, coverage, module).
{ok,{channel,{14,1}}}

對於 channel,結果顯示模組中有 14 行被覆蓋,但有 1 行未被覆蓋。

如果分析在函式層級進行,結果會以元組列表 {Function,{Cov,NotCov}} 的形式給出,模組中的每個函式各一個。函式由其模組名稱、函式名稱和arity指定

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}} 的形式給出,模組中的每個函式子句各一個。子句由其模組名稱、函式名稱、arity 和在函式定義中的位置指定

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/1channel 的程式碼行層級呼叫分析寫入檔案

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 結果時提高效能,可以一次多次呼叫 analyseanalyse_to_file。您也可以使用 async_analyse_to_file 便利函式。

可執行程式碼行

Cover 使用可執行程式碼行的概念,這是包含可執行表達式(例如匹配或函式呼叫)的程式碼行。空白行或包含註解、函式標頭或 casereceive 陳述式中模式的行不可執行。

在下面的範例中,第 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 來查看模組是否已進行 cover 編譯(且仍已載入)。

當 Cover 停止時,所有經過 cover 編譯的模組都會被卸載。