%% -*- 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_cover_compile/2, maybe_write_coverdata/2, format_error/1]). -include("rebar.hrl"). -define(PROVIDER, cover). -define(DEPS, [lock]). %% =================================================================== %% 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, true}, {deps, ?DEPS}, {example, "rebar3 cover"}, {short_desc, "Perform coverage analysis."}, {desc, "Perform coverage analysis."}, {opts, cover_opts(State)}, {profiles, [test]}])), {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) -> maybe_cover_compile(State, apps). -spec maybe_cover_compile(rebar_state:t(), [file:name()] | apps) -> ok. maybe_cover_compile(State, Dirs) -> case rebar_state:get(State, cover_enabled, false) of true -> cover_compile(State, Dirs); 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) -> %% modules have to be compiled and then cover compiled %% in order for cover data to be reloaded %% this maybe breaks if modules have been deleted %% since code coverage was collected? {ok, S} = rebar_prv_compile:do(State), ok = cover_compile(S, apps), do_analyze(State). do_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} = rebar_utils: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) -> OldPath = code:get_path(), ok = restore_cover_paths(State), Mods = cover:imported_modules(), Analysis = 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), true = rebar_utils:cleanup_code_path(OldPath), Analysis. restore_cover_paths(State) -> lists:foreach(fun(App) -> AppDir = rebar_app_info:out_dir(App), _ = code:add_path(filename:join([AppDir, "ebin"])), _ = code:add_path(filename:join([AppDir, "test"])) end, rebar_state:project_apps(State)), _ = code:add_path(filename:join([rebar_dir:base_dir(State), "test"])), ok. 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, _} -> write_file(Mod, mod_to_filename(TaskDir, Mod)); {error, _} -> ?WARN("Can't load module ~ts.", [Mod]), {ok, []} end. write_file(Mod, FileName) -> case cover:analyze_to_file(Mod, FileName, [html]) of {ok, File} -> {ok, File}; {error, Reason} -> ?WARN("Couldn't write annotated file for module ~p for reason ~p", [Mod, Reason]), {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%"; process([], {Cov, Not}) -> integer_to_list(trunc((Cov / (Cov + Not)) * 100)) ++ "%"; %% 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), Separator = separator(MaxLength), TotalLabel = format("total", MaxLength), TotalCov = format(calculate_total(Stats), 8), [io_lib:format("~ts~n~ts~n~ts~n", [Separator, Header, Separator]), 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", [Separator]), io_lib:format(" | ~ts | ~ts |~n", [TotalLabel, TotalCov]), io_lib:format("~ts~n", [Separator]), 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), " |"]. separator(Width) -> [" |--", io_lib:format("~*c", [Width, $-]), "--|------------|"]. format(String, Width) -> io_lib:format("~*.ts", [Width, String]). calculate_total(Stats) when length(Stats) =:= 0 -> "0%"; calculate_total(Stats) -> TotalStats = length(Stats), TotalCovInt = round(lists:foldl( fun({_Mod, Coverage, _File}, Acc) -> Acc + (list_to_integer(string:strip(Coverage, right, $%)) / TotalStats); ({_Mod, Coverage}, Acc) -> Acc + (list_to_integer(string:strip(Coverage, right, $%)) / TotalStats) end, 0, Stats)), integer_to_list(TotalCovInt) ++ "%". write_index(State, Coverage) -> CoverDir = cover_dir(State), FileName = filename:join([CoverDir, "index.html"]), {ok, F} = file:open(FileName, [write]), ok = file:write(F, "\n" "
" "module | coverage % |
---|---|
~ts | ~ts | \n", [strip_coverdir(Report), Mod, Cov]) end, lists:foreach(fun(M) -> ok = file:write(F, FmtLink(M)) end, Mods), ok = file:write(F, ?FMT("
Total | ~ts | \n", [calculate_total(Mods)])), ok = file:write(F, "