瀏覽代碼

generate cover data (via the `{cover_enabled, true}` option in

`rebar.config` or via the `-c\--cover` flag given to the appropriate
task) from the `eunit` and `ct` tasks and add a `cover` task to
write coverage analysis to disk
pull/202/head
alisdair sullivan 10 年之前
父節點
當前提交
ad8982b931
共有 6 個檔案被更改,包括 491 行新增267 行删除
  1. +1
    -0
      src/rebar.app.src
  2. +0
    -261
      src/rebar_cover_utils.erl
  3. +19
    -5
      src/rebar_prv_common_test.erl
  4. +332
    -0
      src/rebar_prv_cover.erl
  5. +12
    -1
      src/rebar_prv_eunit.erl
  6. +127
    -0
      test/rebar_cover_SUITE.erl

+ 1
- 0
src/rebar.app.src 查看文件

@ -25,6 +25,7 @@
{providers, [rebar_prv_as,
rebar_prv_clean,
rebar_prv_cover,
rebar_prv_deps,
rebar_prv_dialyzer,
rebar_prv_do,

+ 0
- 261
src/rebar_cover_utils.erl 查看文件

@ -1,261 +0,0 @@
%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ts=4 sw=4 et
%% -------------------------------------------------------------------
%%
%% rebar: Erlang Build Tools
%%
%% Copyright (c) 2009, 2010 Dave Smith (dizzyd@dizzyd.com)
%% Copyright (c) 2013 Andras Horvath (andras.horvath@erlang-solutions.com)
%%
%% Permission is hereby granted, free of charge, to any person obtaining a copy
%% of this software and associated documentation files (the "Software"), to deal
%% in the Software without restriction, including without limitation the rights
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
%% copies of the Software, and to permit persons to whom the Software is
%% furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
%% THE SOFTWARE.
%% -------------------------------------------------------------------
-module(rebar_cover_utils).
%% for internal use only
-export([init/3,
perform_cover/4,
close/1,
exit/0]).
-include("rebar.hrl").
%% ====================================================================
%% Internal functions
%% ====================================================================
perform_cover(Config, BeamFiles, SrcModules, TargetDir) ->
perform_cover(rebar_state:get(Config, cover_enabled, false),
Config, BeamFiles, SrcModules, TargetDir).
perform_cover(false, _Config, _BeamFiles, _SrcModules, _TargetDir) ->
ok;
perform_cover(true, Config, BeamFiles, SrcModules, TargetDir) ->
analyze(Config, BeamFiles, SrcModules, TargetDir).
close(not_enabled) ->
ok;
close(F) ->
ok = file:close(F).
exit() ->
cover:stop().
init(false, _BeamFiles, _TargetDir) ->
{ok, not_enabled};
init(true, BeamFiles, TargetDir) ->
%% Attempt to start the cover server, then set its group leader to
%% TargetDir/cover.log, so all cover log messages will go there instead of
%% to stdout. If the cover server is already started, we'll kill that
%% server and start a new one in order not to inherit a polluted
%% cover_server state.
{ok, CoverPid} = case whereis(cover_server) of
undefined ->
cover:start();
_ ->
cover:stop(),
cover:start()
end,
{ok, F} = OkOpen = file:open(
filename:join([TargetDir, "cover.log"]),
[write]),
group_leader(F, CoverPid),
?INFO("Cover compiling ~s\n", [rebar_dir:get_cwd()]),
Compiled = [{Beam, cover:compile_beam(Beam)} || Beam <- BeamFiles],
case [Module || {_, {ok, Module}} <- Compiled] of
[] ->
%% No modules compiled successfully...fail
?ERROR("Cover failed to compile any modules; aborting.", []),
?FAIL;
_ ->
%% At least one module compiled successfully
%% It's not an error for cover compilation to fail partially,
%% but we do want to warn about them
PrintWarning =
fun(Beam, Desc) ->
?CONSOLE("Cover compilation warning for ~p: ~p",
[Beam, Desc])
end,
_ = [PrintWarning(Beam, Desc) || {Beam, {error, Desc}} <- Compiled],
OkOpen
end;
init(Config, BeamFiles, TargetDir) ->
init(rebar_state:get(Config, cover_enabled, false), BeamFiles, TargetDir).
analyze(_Config, [], _SrcModules, _TargetDir) ->
ok;
analyze(Config, FilteredModules, SrcModules, TargetDir) ->
%% Generate coverage info for all the cover-compiled modules
Coverage = lists:flatten([analyze_mod(M)
|| M <- FilteredModules,
cover:is_compiled(M) =/= false]),
%% Write index of coverage info
write_index(lists:sort(Coverage), SrcModules, TargetDir),
%% Write coverage details for each file
lists:foreach(
fun({M, _, _}) ->
{ok, _} = cover:analyze_to_file(M,
cover_file(M, TargetDir),
[html])
end, Coverage),
Index = filename:join([rebar_dir:get_cwd(), TargetDir, "index.html"]),
?CONSOLE("Cover analysis: ~s\n", [Index]),
%% Export coverage data, if configured
case rebar_state:get(Config, cover_export_enabled, false) of
true ->
export_coverdata(TargetDir);
false ->
ok
end,
%% Print coverage report, if configured
case rebar_state:get(Config, cover_print_enabled, false) of
true ->
print_coverage(lists:sort(Coverage));
false ->
ok
end.
analyze_mod(Module) ->
case cover:analyze(Module, coverage, module) of
{ok, {Module, {Covered, NotCovered}}} ->
%% Modules that include the eunit header get an implicit
%% test/0 fun, which cover considers a runnable line, but
%% eunit:test(TestRepresentation) never calls. Decrement
%% NotCovered in this case.
[align_notcovered_count(Module, Covered, NotCovered,
is_eunitized(Module))];
{error, Reason} ->
?ERROR("Cover analyze failed for ~p: ~p ~p\n",
[Module, Reason, code:which(Module)]),
[]
end.
is_eunitized(Mod) ->
has_eunit_test_fun(Mod) andalso
has_header(Mod, "include/eunit.hrl").
has_eunit_test_fun(Mod) ->
[F || {exports, Funs} <- Mod:module_info(),
{F, 0} <- Funs, F =:= test] =/= [].
has_header(Mod, Header) ->
Mod1 = case code:which(Mod) of
cover_compiled ->
{file, File} = cover:is_compiled(Mod),
File;
non_existing -> Mod;
preloaded -> Mod;
L -> L
end,
{ok, {_, [{abstract_code, {_, AC}}]}} =
beam_lib:chunks(Mod1, [abstract_code]),
[F || {attribute, 1, file, {F, 1}} <- AC,
string:str(F, Header) =/= 0] =/= [].
align_notcovered_count(Module, Covered, NotCovered, false) ->
{Module, Covered, NotCovered};
align_notcovered_count(Module, Covered, NotCovered, true) ->
{Module, Covered, NotCovered - 1}.
write_index(Coverage, SrcModules, TargetDir) ->
{ok, F} = file:open(filename:join([TargetDir, "index.html"]), [write]),
ok = file:write(F, "<!DOCTYPE HTML><html>\n"
"<head><meta charset=\"utf-8\">"
"<title>Coverage Summary</title></head>\n"
"<body>\n"),
IsSrcCoverage = fun({Mod,_C,_N}) -> lists:member(Mod, SrcModules) end,
{SrcCoverage, TestCoverage} = lists:partition(IsSrcCoverage, Coverage),
write_index_section(F, "Source", SrcCoverage),
write_index_section(F, "Test", TestCoverage),
ok = file:write(F, "</body></html>"),
ok = file:close(F).
write_index_section(_F, _SectionName, []) ->
ok;
write_index_section(F, SectionName, Coverage) ->
%% Calculate total coverage
{Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
{CAcc + C, NAcc + N}
end, {0, 0}, Coverage),
TotalCoverage = percentage(Covered, NotCovered),
%% Write the report
ok = file:write(F, ?FMT("<h1>~s Summary</h1>\n", [SectionName])),
ok = file:write(F, ?FMT("<h3>Total: ~s</h3>\n", [TotalCoverage])),
ok = file:write(F, "<table><tr><th>Module</th><th>Coverage %</th></tr>\n"),
FmtLink =
fun(Module, Cov, NotCov) ->
?FMT("<tr><td><a href='~s.COVER.html'>~s</a></td><td>~s</td>\n",
[Module, Module, percentage(Cov, NotCov)])
end,
lists:foreach(fun({Module, Cov, NotCov}) ->
ok = file:write(F, FmtLink(Module, Cov, NotCov))
end, Coverage),
ok = file:write(F, "</table>\n").
print_coverage(Coverage) ->
{Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
{CAcc + C, NAcc + N}
end, {0, 0}, Coverage),
TotalCoverage = percentage(Covered, NotCovered),
%% Determine the longest module name for right-padding
Width = lists:foldl(fun({Mod, _, _}, Acc) ->
case length(atom_to_list(Mod)) of
N when N > Acc ->
N;
_ ->
Acc
end
end, 0, Coverage) * -1,
%% Print the output the console
?CONSOLE("~nCode Coverage:", []),
lists:foreach(fun({Mod, C, N}) ->
?CONSOLE("~*s : ~4s",
[Width, Mod, percentage(C, N)])
end, Coverage),
?CONSOLE("~n~*s : ~s", [Width, "Total", TotalCoverage]).
cover_file(Module, TargetDir) ->
filename:join([TargetDir, atom_to_list(Module) ++ ".COVER.html"]).
export_coverdata(TargetDir) ->
ExportFile = filename:join(TargetDir, "cover.coverdata"),
case cover:export(ExportFile) of
ok ->
?CONSOLE("Coverdata export: ~s", [ExportFile]);
{error, Reason} ->
?ERROR("Coverdata export failed: ~p", [Reason])
end.
percentage(0, 0) ->
"not executed";
percentage(Cov, NotCov) ->
integer_to_list(trunc((Cov / (Cov + NotCov)) * 100)) ++ "%".

+ 19
- 5
src/rebar_prv_common_test.erl 查看文件

@ -42,10 +42,12 @@ do(State) ->
ok = create_dirs(Opts),
InDirs = in_dirs(State, RawOpts),
ok = compile_tests(State, TestApps, InDirs),
ok = maybe_cover_compile(State, RawOpts),
CTOpts = resolve_ct_opts(State, Opts),
Verbose = proplists:get_value(verbose, Opts, false),
Verbose = proplists:get_value(verbose, RawOpts, false),
TestDirs = test_dirs(State, TestApps),
Result = run_test([{dir, TestDirs}|CTOpts], Verbose),
ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER),
case Result of
{error, Reason} ->
{error, {?MODULE, Reason}};
@ -90,7 +92,8 @@ ct_opts(State) ->
{silent_connections, undefined, "silent_connections", string,
help(silent_connections)}, % all OR %% comma-seperated list
{stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% file
{cover, undefined, "cover", string, help(cover)}, %% file
{cover, $c, "cover", boolean, help(cover)},
{cover_spec, undefined, "cover_spec", string, help(cover_spec)}, %% file
{cover_stop, undefined, "cover_stop", boolean, help(cover_stop)}, %% Boolean
{event_handler, undefined, "event_handler", string, help(event_handler)}, %% EH | [EH] WHERE EH atom() | {atom(), InitArgs} | {[atom()], InitArgs}
{include, undefined, "include", string, help(include)}, % comma-seperated list
@ -138,6 +141,8 @@ help(silent_connections) ->
help(stylesheet) ->
"Stylesheet to use for test results";
help(cover) ->
"Generate cover data";
help(cover_spec) ->
"Cover file to use";
help(cover_stop) ->
""; %% ??
@ -174,8 +179,10 @@ transform_opts(Opts) ->
transform_opts(Opts, []).
transform_opts([], Acc) -> Acc;
%% drop `outdir` so it's not passed to common_test
transform_opts([{outdir, _}|Rest], Acc) ->
%% drop `cover` and `verbose` so they're not passed as an option to common_test
transform_opts([{cover, _}|Rest], Acc) ->
transform_opts(Rest, Acc);
transform_opts([{verbose, _}|Rest], Acc) ->
transform_opts(Rest, Acc);
transform_opts([{ct_hooks, CtHooks}|Rest], Acc) ->
transform_opts(Rest, [{ct_hooks, parse_term(CtHooks)}|Acc]);
@ -344,6 +351,13 @@ replace_src_dirs(State, InDirs) ->
StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts),
rebar_state:set(State, erl_opts, [{src_dirs, ["test"|InDirs]}|StrippedOpts]).
maybe_cover_compile(State, Opts) ->
State1 = case proplists:get_value(cover, Opts, false) of
true -> rebar_state:set(State, cover_enabled, true);
false -> State
end,
rebar_prv_cover:maybe_cover_compile(State1).
handle_results([Result]) ->
handle_results(Result);
handle_results([Result|Results]) when is_list(Results) ->
@ -368,4 +382,4 @@ handle_quiet_results(CTOpts, {_, Failed, _}) ->
io:format(" ~p tests failed.~n Results written to ~p.~n", [Failed, Index]);
handle_quiet_results(_CTOpts, {'DOWN', _, _, _, Reason}) ->
handle_results({error, Reason});
handle_quiet_results(_CTOpts, Result) -> handle_results(Result).
handle_quiet_results(_CTOpts, Result) -> handle_results(Result).

+ 332
- 0
src/rebar_prv_cover.erl 查看文件

@ -0,0 +1,332 @@
%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ts=4 sw=4 et
-module(rebar_prv_cover).
-behaviour(provider).
-export([init/1,
do/1,
maybe_cover_compile/1,
maybe_write_coverdata/2,
format_error/1]).
-include("rebar.hrl").
-define(PROVIDER, cover).
-define(DEPS, []).
%% ===================================================================
%% Public API
%% ===================================================================
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER},
{module, ?MODULE},
{bare, false},
{deps, ?DEPS},
{example, "rebar cover"},
{short_desc, "Perform coverage analysis."},
{desc, ""},
{opts, cover_opts(State)}])),
{ok, State1}.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
{Opts, _} = rebar_state:command_parsed_args(State),
case proplists:get_value(reset, Opts, false) of
true -> reset(State);
false -> analyze(State)
end.
-spec maybe_cover_compile(rebar_state:t()) -> ok.
maybe_cover_compile(State) ->
case rebar_state:get(State, cover_enabled, false) of
true -> cover_compile(State);
false -> ok
end.
-spec maybe_write_coverdata(rebar_state:t(), atom()) -> ok.
maybe_write_coverdata(State, Task) ->
case cover:modules() of
%% no coverdata collected, skip writing anything out
[] -> ok;
_ -> write_coverdata(State, Task)
end.
-spec format_error(any()) -> iolist().
format_error(Reason) ->
io_lib:format("~p", [Reason]).
%% ===================================================================
%% Internal functions
%% ===================================================================
reset(State) ->
?INFO("Resetting collected cover data...", []),
CoverDir = cover_dir(State),
CoverFiles = get_all_coverdata(CoverDir),
F = fun(File) ->
case file:delete(File) of
{error, Reason} ->
?WARN("Error deleting ~p: ~p", [Reason, File]);
_ -> ok
end
end,
ok = lists:foreach(F, CoverFiles),
{ok, State}.
analyze(State) ->
?INFO("Performing cover analysis...", []),
%% figure out what coverdata we have
CoverDir = cover_dir(State),
CoverFiles = get_all_coverdata(CoverDir),
%% start the cover server if necessary
{ok, CoverPid} = start_cover(),
%% redirect cover output
true = redirect_cover_output(State, CoverPid),
%% analyze!
ok = case analyze(State, CoverFiles) of
[] -> ok;
Analysis ->
print_analysis(Analysis, verbose(State)),
write_index(State, Analysis)
end,
{ok, State}.
get_all_coverdata(CoverDir) ->
ok = filelib:ensure_dir(filename:join([CoverDir, "dummy.log"])),
{ok, Files} = file:list_dir(CoverDir),
rebar_utils:filtermap(fun(FileName) ->
case filename:extension(FileName) == ".coverdata" of
true -> {true, filename:join([CoverDir, FileName])};
false -> false
end
end, Files).
analyze(_State, []) ->
?WARN("No coverdata found", []),
[];
analyze(State, CoverFiles) ->
%% reset any existing cover data
ok = cover:reset(),
%% import all coverdata files
ok = lists:foreach(fun(M) -> import(M) end, CoverFiles),
[{"aggregate", CoverFiles, analysis(State, "aggregate")}] ++
analyze(State, CoverFiles, []).
analyze(_State, [], Acc) -> lists:reverse(Acc);
analyze(State, [F|Rest], Acc) ->
%% reset any existing cover data
ok = cover:reset(),
%% extract taskname from the CoverData file
Task = filename:basename(F, ".coverdata"),
%% import task cover data and process it
ok = import(F),
analyze(State, Rest, [{Task, [F], analysis(State, Task)}] ++ Acc).
import(CoverData) ->
case cover:import(CoverData) of
{error, {cant_open_file, F, _Reason}} ->
?WARN("Can't import cover data from ~ts.", [F]),
error;
ok -> ok
end.
analysis(State, Task) ->
Mods = cover:imported_modules(),
lists:map(
fun(Mod) ->
{ok, Answer} = cover:analyze(Mod, coverage, line),
{ok, File} = analyze_to_file(Mod, State, Task),
{Mod, process(Answer), File}
end,
Mods).
analyze_to_file(Mod, State, Task) ->
CoverDir = cover_dir(State),
TaskDir = filename:join([CoverDir, Task]),
ok = filelib:ensure_dir(filename:join([TaskDir, "dummy.html"])),
case code:ensure_loaded(Mod) of
{module, _} ->
cover:analyze_to_file(Mod, mod_to_filename(TaskDir, Mod), [html]);
{error, _} ->
?WARN("Can't load module ~ts.", [Mod]),
{ok, []}
end.
mod_to_filename(TaskDir, M) ->
filename:join([TaskDir, atom_to_list(M) ++ ".html"]).
process(Coverage) -> process(Coverage, {0, 0}).
process([], {0, 0}) ->
"0.0%";
process([], {Cov, Not}) ->
float_to_list((Cov / (Cov + Not)) * 100, [{decimals, 1}]) ++ "%";
%% line 0 is a line added by eunit and never executed so ignore it
process([{{_, 0}, _}|Rest], Acc) -> process(Rest, Acc);
process([{_, {Cov, Not}}|Rest], {Covered, NotCovered}) ->
process(Rest, {Covered + Cov, NotCovered + Not}).
print_analysis(_, false) -> ok;
print_analysis(Analysis, true) ->
{_, CoverFiles, Stats} = lists:keyfind("aggregate", 1, Analysis),
ConsoleStats = [ {atom_to_list(M), C} || {M, C, _} <- Stats ],
Table = format_table(ConsoleStats, CoverFiles),
io:format("~ts", [Table]).
format_table(Stats, CoverFiles) ->
MaxLength = max(lists:foldl(fun max_length/2, 0, Stats), 20),
Header = header(MaxLength),
Seperator = seperator(MaxLength),
[io_lib:format("~ts~n~ts~n~ts~n", [Seperator, Header, Seperator]),
lists:map(fun({Mod, Coverage}) ->
Name = format(Mod, MaxLength),
Cov = format(Coverage, 8),
io_lib:format(" | ~ts | ~ts |~n", [Name, Cov])
end, Stats),
io_lib:format("~ts~n", [Seperator]),
io_lib:format(" coverage calculated from:~n", []),
lists:map(fun(File) ->
io_lib:format(" ~ts~n", [File])
end, CoverFiles)].
max_length({ModName, _}, Min) ->
Length = length(lists:flatten(ModName)),
case Length > Min of
true -> Length;
false -> Min
end.
header(Width) ->
[" | ", format("module", Width), " | ", format("coverage", 8), " |"].
seperator(Width) ->
[" |--", io_lib:format("~*c", [Width, $-]), "--|------------|"].
format(String, Width) -> io_lib:format("~*.ts", [Width, String]).
write_index(State, Coverage) ->
CoverDir = cover_dir(State),
FileName = filename:join([CoverDir, "index.html"]),
{ok, F} = file:open(FileName, [write]),
ok = file:write(F, "<!DOCTYPE HTML><html>\n"
"<head><meta charset=\"utf-8\">"
"<title>Coverage Summary</title></head>\n"
"<body>\n"),
{Aggregate, Rest} = lists:partition(fun({"aggregate", _, _}) -> true; (_) -> false end,
Coverage),
ok = write_index_section(F, Aggregate),
ok = write_index_section(F, Rest),
ok = file:write(F, "</body></html>"),
ok = file:close(F),
io:format(" cover summary written to: ~ts~n", [filename:absname(FileName)]).
write_index_section(_F, []) -> ok;
write_index_section(F, [{Section, DataFile, Mods}|Rest]) ->
%% Write the report
ok = file:write(F, ?FMT("<h1>~s summary</h1>\n", [Section])),
ok = file:write(F, "coverage calculated from:\n<ul>"),
ok = lists:foreach(fun(D) -> ok = file:write(F, io_lib:format("<li>~ts</li>", [D])) end,
DataFile),
ok = file:write(F, "</ul>\n"),
ok = file:write(F, "<table><tr><th>module</th><th>coverage %</th></tr>\n"),
FmtLink =
fun({Mod, Cov, Report}) ->
?FMT("<tr><td><a href='~ts'>~ts</a></td><td>~ts</td>\n",
[strip_coverdir(Report), Mod, Cov])
end,
lists:foreach(fun(M) -> ok = file:write(F, FmtLink(M)) end, Mods),
ok = file:write(F, "</table>\n"),
write_index_section(F, Rest).
strip_coverdir(File) ->
filename:join(lists:reverse(lists:sublist(lists:reverse(filename:split(File)),
2))).
cover_compile(State) ->
%% start the cover server if necessary
{ok, CoverPid} = start_cover(),
%% redirect cover output
true = redirect_cover_output(State, CoverPid),
%% cover compile the modules we just compiled
Apps = filter_checkouts(rebar_state:project_apps(State)),
CompileResult = compile_beam_directories(Apps, []) ++
compile_bare_test_directory(State),
%% print any warnings about modules that failed to cover compile
lists:foreach(fun print_cover_warnings/1, CompileResult).
filter_checkouts(Apps) -> filter_checkouts(Apps, []).
filter_checkouts([], Acc) -> lists:reverse(Acc);
filter_checkouts([App|Rest], Acc) ->
AppDir = filename:absname(rebar_app_info:dir(App)),
CheckoutsDir = filename:absname("_checkouts"),
case lists:prefix(CheckoutsDir, AppDir) of
true -> filter_checkouts(Rest, Acc);
false -> filter_checkouts(Rest, [App|Acc])
end.
compile_beam_directories([], Acc) -> Acc;
compile_beam_directories([App|Rest], Acc) ->
Result = cover:compile_beam_directory(filename:join([rebar_app_info:out_dir(App),
"ebin"])),
compile_beam_directories(Rest, Acc ++ Result).
compile_bare_test_directory(State) ->
case cover:compile_beam_directory(filename:join([rebar_dir:base_dir(State),
"ebin"])) of
%% if directory doesn't exist just return empty result set
{error, enoent} -> [];
Result -> Result
end.
start_cover() ->
case cover:start() of
{ok, Pid} -> {ok, Pid};
{error, {already_started, Pid}} -> {ok, Pid}
end.
redirect_cover_output(State, CoverPid) ->
%% redirect cover console output to file
DataDir = cover_dir(State),
ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
{ok, F} = file:open(filename:join([DataDir, "cover.log"]),
[append]),
group_leader(F, CoverPid).
print_cover_warnings({ok, _}) -> ok;
print_cover_warnings({error, File}) ->
?WARN("Cover compilation of ~p failed, module is not included in cover data.",
[File]).
write_coverdata(State, Task) ->
DataDir = cover_dir(State),
ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
ExportFile = filename:join([DataDir, atom_to_list(Task) ++ ".coverdata"]),
case cover:export(ExportFile) of
ok ->
?DEBUG("Cover data written to ~p.", [ExportFile]),
ok;
{error, Reason} ->
?WARN("Cover data export failed: ~p", [Reason])
end.
verbose(State) ->
{Opts, _} = rebar_state:command_parsed_args(State),
case proplists:get_value(verbose, Opts, missing) of
missing -> rebar_state:get(State, cover_print_enabled, false);
Else -> Else
end.
cover_dir(State) ->
rebar_state:get(State, cover_data_dir, "_cover").
cover_opts(_State) ->
[{reset, $r, "reset", boolean, help(reset)},
{verbose, $v, "verbose", boolean, help(verbose)}].
help(reset) -> "Reset all coverdata.";
help(verbose) -> "Print coverage analysis.".

+ 12
- 1
src/rebar_prv_eunit.erl 查看文件

@ -40,8 +40,10 @@ do(State) ->
EUnitOpts = resolve_eunit_opts(State, Opts),
TestApps = filter_checkouts(rebar_state:project_apps(State)),
ok = compile_tests(State, TestApps),
ok = maybe_cover_compile(State, Opts),
AppsToTest = test_dirs(State, TestApps),
Result = eunit:test(AppsToTest, EUnitOpts),
ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER),
case handle_results(Result) of
{error, Reason} ->
{error, {?MODULE, Reason}};
@ -56,8 +58,10 @@ format_error({error_running_tests, Reason}) ->
io_lib:format("Error running tests: ~p", [Reason]).
eunit_opts(_State) ->
[{verbose, $v, "verbose", boolean, help(verbose)}].
[{cover, $c, "cover", boolean, help(cover)},
{verbose, $v, "verbose", boolean, help(verbose)}].
help(cover) -> "Generate cover data";
help(verbose) -> "Verbose output".
filter_checkouts(Apps) -> filter_checkouts(Apps, []).
@ -157,6 +161,13 @@ replace_src_dirs(State) ->
StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts),
rebar_state:set(State, erl_opts, [{src_dirs, ["test"]}|StrippedOpts]).
maybe_cover_compile(State, Opts) ->
State1 = case proplists:get_value(cover, Opts, false) of
true -> rebar_state:set(State, cover_enabled, true);
false -> State
end,
rebar_prv_cover:maybe_cover_compile(State1).
handle_results(ok) -> ok;
handle_results(error) ->
{error, unknown_error};

+ 127
- 0
test/rebar_cover_SUITE.erl 查看文件

@ -0,0 +1,127 @@
-module(rebar_cover_SUITE).
-export([suite/0,
init_per_suite/1,
end_per_suite/1,
init_per_testcase/2,
all/0,
flag_coverdata_written/1,
config_coverdata_written/1,
index_written/1,
config_alt_coverdir/1,
flag_verbose/1,
config_verbose/1]).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("kernel/include/file.hrl").
suite() ->
[].
init_per_suite(Config) ->
Config.
end_per_suite(_Config) ->
ok.
init_per_testcase(_, Config) ->
rebar_test_utils:init_rebar_state(Config, "cover_").
all() ->
[flag_coverdata_written, config_coverdata_written,
index_written,
config_alt_coverdir,
flag_verbose, config_verbose].
flag_coverdata_written(Config) ->
AppDir = ?config(apps, Config),
Name = rebar_test_utils:create_random_name("cover_"),
Vsn = rebar_test_utils:create_random_vsn(),
rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
RebarConfig = [{erl_opts, [{d, some_define}]}],
rebar_test_utils:run_and_check(Config,
RebarConfig,
["eunit", "--cover"],
{ok, [{app, Name}]}),
true = filelib:is_file(filename:join(["_cover", "eunit.coverdata"])).
config_coverdata_written(Config) ->
AppDir = ?config(apps, Config),
Name = rebar_test_utils:create_random_name("cover_"),
Vsn = rebar_test_utils:create_random_vsn(),
rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
RebarConfig = [{erl_opts, [{d, some_define}]}, {cover_enabled, true}],
rebar_test_utils:run_and_check(Config,
RebarConfig,
["eunit"],
{ok, [{app, Name}]}),
true = filelib:is_file(filename:join(["_cover", "eunit.coverdata"])).
index_written(Config) ->
AppDir = ?config(apps, Config),
Name = rebar_test_utils:create_random_name("cover_"),
Vsn = rebar_test_utils:create_random_vsn(),
rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
RebarConfig = [{erl_opts, [{d, some_define}]}],
rebar_test_utils:run_and_check(Config,
RebarConfig,
["do", "eunit", "--cover", ",", "cover"],
{ok, [{app, Name}]}),
true = filelib:is_file(filename:join(["_cover", "index.html"])).
config_alt_coverdir(Config) ->
AppDir = ?config(apps, Config),
Name = rebar_test_utils:create_random_name("cover_"),
Vsn = rebar_test_utils:create_random_vsn(),
rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
CoverDir = filename:join(["coverage", "goes", "here"]),
RebarConfig = [{erl_opts, [{d, some_define}]}, {cover_data_dir, CoverDir}],
rebar_test_utils:run_and_check(Config,
RebarConfig,
["do", "eunit", "--cover", ",", "cover"],
{ok, [{app, Name}]}),
true = filelib:is_file(filename:join([CoverDir, "index.html"])).
flag_verbose(Config) ->
AppDir = ?config(apps, Config),
Name = rebar_test_utils:create_random_name("cover_"),
Vsn = rebar_test_utils:create_random_vsn(),
rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
RebarConfig = [{erl_opts, [{d, some_define}]}],
rebar_test_utils:run_and_check(Config,
RebarConfig,
["do", "eunit", "--cover", ",", "cover", "--verbose"],
{ok, [{app, Name}]}),
true = filelib:is_file(filename:join(["_cover", "index.html"])).
config_verbose(Config) ->
AppDir = ?config(apps, Config),
Name = rebar_test_utils:create_random_name("cover_"),
Vsn = rebar_test_utils:create_random_vsn(),
rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
RebarConfig = [{erl_opts, [{d, some_define}]}, {cover_print_enabled, true}],
rebar_test_utils:run_and_check(Config,
RebarConfig,
["do", "eunit", "--cover", ",", "cover"],
{ok, [{app, Name}]}),
true = filelib:is_file(filename:join(["_cover", "index.html"])).

Loading…
取消
儲存