Packages

Rebar3 plugin converting .coverdata files to codecov compatible JSON

Current section

Files

Jump to
rebar3_codecov src rebar3_codecov_prv.erl
Raw

src/rebar3_codecov_prv.erl

-module(rebar3_codecov_prv).
-behaviour(provider).
%% API
-export([init/1, do/1, format_error/1]).
-define(NAMESPACE, codecov).
-define(PROVIDER, analyze).
-define(DEPS, [{default, app_discovery}]).
-define(DESC, "Converts .coverdata files to codecov compatible JSON").
-define(JSON_OUT_FILE, "codecov.json").
-define(LCOV_OUT_FILE, "lcov.info").
%% Public API
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
Provider = providers:create([
{name, ?PROVIDER},
{namespace, ?NAMESPACE},
{module, ?MODULE},
{bare, true},
{deps, ?DEPS},
{example, "rebar3 codecov analyze --lcov --json false"},
{opts, [
{lcov, undefined, "lcov", boolean,
"export data in lcov format (false by default)"},
{json, undefined, "json", boolean,
"export data in json format (true by default)"}
]},
{short_desc, ?DESC},
{desc, ?DESC}
]),
{ok, rebar_state:add_provider(State, Provider)}.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
{RawOpts, _} = rebar_state:command_parsed_args(State),
Apps = filter_apps(RawOpts, State),
lists:map(fun(App) ->
add_profile_ebin_path(App, State),
add_app_ebin_path(App)
end, Apps),
AppsInfo = rebar_state:project_apps(State),
Defaults = lists:map(fun(App) ->
EBin = rebar_app_info:ebin_dir(App),
rebar_dir:make_relative_path(EBin, rebar_dir:root_dir(State))
end, AppsInfo),
code:add_paths(Defaults),
Files = lists:flatmap(fun get_coverdata_files/1, AppsInfo),
Data = analyze(Files),
export(Data, State),
{ok, State}.
-spec format_error(any()) -> iolist().
format_error(Reason) ->
io_lib:format("~p", [Reason]).
%% Private API
analyze(Files) ->
try
cover:start(),
lists:map(fun(F) ->
cover:import(F),
rebar_api:info("importing ~s~n", [F])
end,
Files),
Modules = cover:imported_modules(),
{result, Result, _} = cover:analyse(Modules, calls, line),
Result
catch
Error:Reason:Stacktrace ->
rebar_api:abort("~p~n~p~n~p~n",[Error, Reason, Stacktrace])
end.
export(Data, State) ->
Mod2Data = lists:foldl(fun add_cover_line_into_array/2, #{}, Data),
Formats = export_formats(State),
to_json(Mod2Data, Formats),
to_lcov(Mod2Data, Formats).
export_formats(State) ->
{Args, _} = rebar_state:command_parsed_args(State),
LCov = proplists:get_value(lcov, Args, false),
JSON = proplists:get_value(json, Args, true),
#{lcov => LCov, json => JSON}.
to_json(Mod2Data, #{json := true}) ->
rebar_api:info("exporting ~s~n", [?JSON_OUT_FILE]),
JSON = maps:fold(fun format_array_to_list/3, [], Mod2Data),
Binary = jiffy:encode(#{<<"coverage">> => {JSON}}),
file:write_file(?JSON_OUT_FILE, Binary);
to_json(_, _) -> ok.
to_lcov(Mod2Data, #{lcov := true}) ->
rebar_api:info("exporting ~s~n", [?LCOV_OUT_FILE]),
{ok, LCovFile} = file:open(?LCOV_OUT_FILE, [write]),
maps:fold(fun module_to_lcov/3, LCovFile, Mod2Data),
file:close(LCovFile);
to_lcov(_, _) -> ok.
add_cover_line_into_array({{Module, Line}, CallTimes}, Acc) ->
CallsPerLineArray = maps:get(Module, Acc, array:new({default, null})),
Acc#{Module => array:set(Line, CallTimes, CallsPerLineArray)}.
format_array_to_list(Module, CallsPerLineArray, Acc) ->
ListOfCallTimes = array:to_list(CallsPerLineArray),
BinPath = list_to_binary(get_source_path(Module)),
[{BinPath, ListOfCallTimes}|Acc].
module_to_lcov(Module, CallsPerLineArray, LCovFile) ->
%% lcov file format description can be found here:
%% https://manpages.debian.org/stretch/lcov/geninfo.1.en.html#FILES
%% currently this generator creates only the list of execution counts
%% for each instrumented line, which is enough for coveralls service.
io:format(LCovFile, "SF:~s~n", [get_source_path(Module)]),
CallTimes = array:to_orddict(CallsPerLineArray),
InstrumentedLines = [{N, C} || {N, C} <- CallTimes, C =/= null],
ExecutedLines = [{N, C} || {N, C} <- InstrumentedLines, C > 0],
[io:format(LCovFile, "DA:~p,~p~n", [N, C]) || {N, C} <- InstrumentedLines],
io:format(LCovFile, "LH:~p~n", [length(ExecutedLines)]),
io:format(LCovFile, "LF:~p~n", [length(InstrumentedLines)]),
io:format(LCovFile, "end_of_record~n", []),
LCovFile.
get_source_path(Module) when is_atom(Module) ->
Name = atom_to_list(Module)++".erl",
try filelib:wildcard([filename:join(["src/**/", Name])]) of
[P] -> P;
_ ->
Issue = io_lib:format("Failed to calculate the source path of module ~p~n", [Module]),
rebar_api:warn("~s~n", [Issue]),
[]
catch
Error:Reason:Stacktrace ->
Issue = io_lib:format("Failed to calculate the source path of module ~p~n
falling back to ~s", [Module, Name]),
rebar_api:warn("~s~n~p~n~p~n~p~n", [Issue, Error, Reason, Stacktrace]),
Name
end.
add_profile_ebin_path(App, State) ->
ProfileDir = rebar_dir:profile_dir(rebar_state:opts(State), [default, test]),
Build = rebar_dir:make_relative_path(ProfileDir, rebar_dir:root_dir(State)),
Beams = filename:join([Build, "lib/", App, "ebin/"]),
code:add_paths([Beams]).
add_app_ebin_path(App) ->
Wildcard = filename:join(["_build/**/lib", App, "ebin"]),
Paths = filelib:wildcard(Wildcard),
code:add_paths(Paths).
filter_apps(RawOpts, State) ->
RawApps = proplists:get_all_values(app, RawOpts),
Apps = lists:foldl(fun(String, Acc) -> rebar_string:lexemes(String, ",") ++ Acc end, [], RawApps),
case Apps of
[] ->
ProjectApps = rebar_state:project_apps(State),
lists:map(fun(A) -> erlang:binary_to_atom(rebar_app_info:name(A), utf8) end, ProjectApps);
_ -> Apps
end.
get_coverdata_files(AppInfo) ->
Opts = rebar_app_info:opts(AppInfo),
CoverDataPath = case dict:find(codecov_opts, Opts) of
{ok, CodecovOpts} ->
Paths = proplists:get_value(path, CodecovOpts, ["_build/test/cover"]),
lists:map(fun(P) -> filename:join([P, "*.coverdata"]) end, Paths);
_ ->
["_build/test/cover/*.coverdata"]
end,
rebar_api:info("Exporting cover data from ~p~n", [CoverDataPath]),
lists:flatmap(fun filelib:wildcard/1, CoverDataPath).