diff options
-rw-r--r-- | src/rebar.app.src | 1 | ||||
-rw-r--r-- | src/rebar_cover_utils.erl | 261 | ||||
-rw-r--r-- | src/rebar_prv_common_test.erl | 24 | ||||
-rw-r--r-- | src/rebar_prv_cover.erl | 332 | ||||
-rw-r--r-- | src/rebar_prv_eunit.erl | 13 | ||||
-rw-r--r-- | test/rebar_cover_SUITE.erl | 127 |
6 files changed, 491 insertions, 267 deletions
diff --git a/src/rebar.app.src b/src/rebar.app.src index e5f21ea..8c544aa 100644 --- a/src/rebar.app.src +++ b/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, diff --git a/src/rebar_cover_utils.erl b/src/rebar_cover_utils.erl deleted file mode 100644 index 0439b8a..0000000 --- a/src/rebar_cover_utils.erl +++ /dev/null @@ -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)) ++ "%". diff --git a/src/rebar_prv_common_test.erl b/src/rebar_prv_common_test.erl index 1a7e483..032a8a6 100644 --- a/src/rebar_prv_common_test.erl +++ b/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).
\ No newline at end of file +handle_quiet_results(_CTOpts, Result) -> handle_results(Result). diff --git a/src/rebar_prv_cover.erl b/src/rebar_prv_cover.erl new file mode 100644 index 0000000..f3fdb24 --- /dev/null +++ b/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.".
\ No newline at end of file diff --git a/src/rebar_prv_eunit.erl b/src/rebar_prv_eunit.erl index 18cceda..d841f26 100644 --- a/src/rebar_prv_eunit.erl +++ b/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}; diff --git a/test/rebar_cover_SUITE.erl b/test/rebar_cover_SUITE.erl new file mode 100644 index 0000000..c719982 --- /dev/null +++ b/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"])). |