summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoralisdair sullivan <alisdairsullivan@yahoo.ca>2015-02-01 18:41:28 -0800
committeralisdair sullivan <alisdairsullivan@yahoo.ca>2015-03-03 01:57:48 -0800
commitad8982b931b4adb5616014dff74d47fb69907060 (patch)
tree9762f6527b6f4cae611c03e177e3cd16cc34e669
parent516a4cef25f37699faa5c1bbf7f6bc5b846143f2 (diff)
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
-rw-r--r--src/rebar.app.src1
-rw-r--r--src/rebar_cover_utils.erl261
-rw-r--r--src/rebar_prv_common_test.erl24
-rw-r--r--src/rebar_prv_cover.erl332
-rw-r--r--src/rebar_prv_eunit.erl13
-rw-r--r--test/rebar_cover_SUITE.erl127
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"])).