%% -*- 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_config: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_utils: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.~n", []), ?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_config: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_utils:get_cwd(), TargetDir, "index.html"]), ?CONSOLE("Cover analysis: ~s\n", [Index]), %% Export coverage data, if configured case rebar_config:get(Config, cover_export_enabled, false) of true -> export_coverdata(TargetDir); false -> ok end, %% Print coverage report, if configured case rebar_config:get(Config, cover_print_enabled, false) of true -> print_coverage(lists:sort(Coverage)); false -> ok end, %% Generate JSON Coverage Data, if configured case rebar_config:get(Config, cover_export_json, false) of true -> export_json_coverage(TargetDir, 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, "\n" "" "Coverage Summary\n" "\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, ""), 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("

~s Summary

\n", [SectionName])), ok = file:write(F, ?FMT("

Total: ~s

\n", [TotalCoverage])), ok = file:write(F, "\n"), FmtLink = fun(Module, Cov, NotCov) -> ?FMT("\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, "
ModuleCoverage %
~s~s
\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:~n", []), lists:foreach(fun({Mod, C, N}) -> ?CONSOLE("~*s : ~4s~n", [Width, Mod, percentage(C, N)]) end, Coverage), ?CONSOLE("~n~*s : ~s~n", [Width, "Total", TotalCoverage]). export_json_coverage(TargetDir,Coverage) -> ?CONSOLE("~nCode Coverage export to json~n", []), lists:foreach(fun(ModuleCoverage) -> export_json_coverage_to_file( TargetDir, ModuleCoverage) end, Coverage). export_json_coverage_to_file(TargetDir, {Module, Covered, NotCovered}) -> {ok, JsonFile} = file:open(json_file(TargetDir, Module), [write]), io:format(JsonFile, "{\"module\":~p,\"covered\":~p,\"not_covered\":~p}", [atom_to_list(Module), Covered, NotCovered]), ok = file:close(JsonFile). json_file(TargetDir, Module) -> filename:join([TargetDir, atom_to_list(Module) ++ ".COVER.json"]). 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~n", [ExportFile]); {error, Reason} -> ?ERROR("Coverdata export failed: ~p~n", [Reason]) end. percentage(0, 0) -> "not executed"; percentage(Cov, NotCov) -> integer_to_list(trunc((Cov / (Cov + NotCov)) * 100)) ++ "%".