Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

364 рядки
13 KiB

  1. %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. -module(rebar_prv_cover).
  4. -behaviour(provider).
  5. -export([init/1,
  6. do/1,
  7. maybe_cover_compile/1,
  8. maybe_cover_compile/2,
  9. maybe_write_coverdata/2,
  10. format_error/1]).
  11. -include("rebar.hrl").
  12. -define(PROVIDER, cover).
  13. -define(DEPS, [app_discovery]).
  14. %% ===================================================================
  15. %% Public API
  16. %% ===================================================================
  17. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  18. init(State) ->
  19. State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER},
  20. {module, ?MODULE},
  21. {bare, true},
  22. {deps, ?DEPS},
  23. {example, "rebar3 cover"},
  24. {short_desc, "Perform coverage analysis."},
  25. {desc, "Perform coverage analysis."},
  26. {opts, cover_opts(State)},
  27. {profiles, [test]}])),
  28. {ok, State1}.
  29. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  30. do(State) ->
  31. {Opts, _} = rebar_state:command_parsed_args(State),
  32. case proplists:get_value(reset, Opts, false) of
  33. true -> reset(State);
  34. false -> analyze(State)
  35. end.
  36. -spec maybe_cover_compile(rebar_state:t()) -> ok.
  37. maybe_cover_compile(State) ->
  38. maybe_cover_compile(State, []).
  39. -spec maybe_cover_compile(rebar_state:t(), [file:name()]) -> ok.
  40. maybe_cover_compile(State, ExtraDirs) ->
  41. case rebar_state:get(State, cover_enabled, false) of
  42. true -> cover_compile(State, ExtraDirs);
  43. false -> ok
  44. end.
  45. -spec maybe_write_coverdata(rebar_state:t(), atom()) -> ok.
  46. maybe_write_coverdata(State, Task) ->
  47. case cover:modules() of
  48. %% no coverdata collected, skip writing anything out
  49. [] -> ok;
  50. _ -> write_coverdata(State, Task)
  51. end.
  52. -spec format_error(any()) -> iolist().
  53. format_error(Reason) ->
  54. io_lib:format("~p", [Reason]).
  55. %% ===================================================================
  56. %% Internal functions
  57. %% ===================================================================
  58. reset(State) ->
  59. ?INFO("Resetting collected cover data...", []),
  60. CoverDir = cover_dir(State),
  61. CoverFiles = get_all_coverdata(CoverDir),
  62. F = fun(File) ->
  63. case file:delete(File) of
  64. {error, Reason} ->
  65. ?WARN("Error deleting ~p: ~p", [Reason, File]);
  66. _ -> ok
  67. end
  68. end,
  69. ok = lists:foreach(F, CoverFiles),
  70. {ok, State}.
  71. analyze(State) ->
  72. ?INFO("Performing cover analysis...", []),
  73. %% figure out what coverdata we have
  74. CoverDir = cover_dir(State),
  75. CoverFiles = get_all_coverdata(CoverDir),
  76. %% start the cover server if necessary
  77. {ok, CoverPid} = start_cover(),
  78. %% redirect cover output
  79. true = redirect_cover_output(State, CoverPid),
  80. %% analyze!
  81. ok = case analyze(State, CoverFiles) of
  82. [] -> ok;
  83. Analysis ->
  84. print_analysis(Analysis, verbose(State)),
  85. write_index(State, Analysis)
  86. end,
  87. {ok, State}.
  88. get_all_coverdata(CoverDir) ->
  89. ok = filelib:ensure_dir(filename:join([CoverDir, "dummy.log"])),
  90. {ok, Files} = file:list_dir(CoverDir),
  91. rebar_utils:filtermap(fun(FileName) ->
  92. case filename:extension(FileName) == ".coverdata" of
  93. true -> {true, filename:join([CoverDir, FileName])};
  94. false -> false
  95. end
  96. end, Files).
  97. analyze(_State, []) ->
  98. ?WARN("No coverdata found", []),
  99. [];
  100. analyze(State, CoverFiles) ->
  101. %% reset any existing cover data
  102. ok = cover:reset(),
  103. %% import all coverdata files
  104. ok = lists:foreach(fun(M) -> import(M) end, CoverFiles),
  105. [{"aggregate", CoverFiles, analysis(State, "aggregate")}] ++
  106. analyze(State, CoverFiles, []).
  107. analyze(_State, [], Acc) -> lists:reverse(Acc);
  108. analyze(State, [F|Rest], Acc) ->
  109. %% reset any existing cover data
  110. ok = cover:reset(),
  111. %% extract taskname from the CoverData file
  112. Task = filename:basename(F, ".coverdata"),
  113. %% import task cover data and process it
  114. ok = import(F),
  115. analyze(State, Rest, [{Task, [F], analysis(State, Task)}] ++ Acc).
  116. import(CoverData) ->
  117. case cover:import(CoverData) of
  118. {error, {cant_open_file, F, _Reason}} ->
  119. ?WARN("Can't import cover data from ~ts.", [F]),
  120. error;
  121. ok -> ok
  122. end.
  123. analysis(State, Task) ->
  124. OldPath = code:get_path(),
  125. ok = restore_cover_paths(State),
  126. Mods = cover:imported_modules(),
  127. Analysis = lists:map(fun(Mod) ->
  128. {ok, Answer} = cover:analyze(Mod, coverage, line),
  129. {ok, File} = analyze_to_file(Mod, State, Task),
  130. {Mod, process(Answer), File}
  131. end,
  132. Mods),
  133. true = rebar_utils:cleanup_code_path(OldPath),
  134. Analysis.
  135. restore_cover_paths(State) ->
  136. lists:foreach(fun(App) ->
  137. AppDir = rebar_app_info:out_dir(App),
  138. _ = code:add_path(filename:join([AppDir, "ebin"])),
  139. _ = code:add_path(filename:join([AppDir, "test"]))
  140. end, rebar_state:project_apps(State)),
  141. _ = code:add_path(filename:join([rebar_dir:base_dir(State), "test"])),
  142. ok.
  143. analyze_to_file(Mod, State, Task) ->
  144. CoverDir = cover_dir(State),
  145. TaskDir = filename:join([CoverDir, Task]),
  146. ok = filelib:ensure_dir(filename:join([TaskDir, "dummy.html"])),
  147. case code:ensure_loaded(Mod) of
  148. {module, _} ->
  149. write_file(Mod, mod_to_filename(TaskDir, Mod));
  150. {error, _} ->
  151. ?WARN("Can't load module ~ts.", [Mod]),
  152. {ok, []}
  153. end.
  154. write_file(Mod, FileName) ->
  155. case cover:analyze_to_file(Mod, FileName, [html]) of
  156. {ok, File} -> {ok, File};
  157. {error, Reason} ->
  158. ?WARN("Couldn't write annotated file for module ~p for reason ~p", [Mod, Reason]),
  159. {ok, []}
  160. end.
  161. mod_to_filename(TaskDir, M) ->
  162. filename:join([TaskDir, atom_to_list(M) ++ ".html"]).
  163. process(Coverage) -> process(Coverage, {0, 0}).
  164. process([], {0, 0}) ->
  165. "0%";
  166. process([], {Cov, Not}) ->
  167. integer_to_list(trunc((Cov / (Cov + Not)) * 100)) ++ "%";
  168. %% line 0 is a line added by eunit and never executed so ignore it
  169. process([{{_, 0}, _}|Rest], Acc) -> process(Rest, Acc);
  170. process([{_, {Cov, Not}}|Rest], {Covered, NotCovered}) ->
  171. process(Rest, {Covered + Cov, NotCovered + Not}).
  172. print_analysis(_, false) -> ok;
  173. print_analysis(Analysis, true) ->
  174. {_, CoverFiles, Stats} = lists:keyfind("aggregate", 1, Analysis),
  175. ConsoleStats = [ {atom_to_list(M), C} || {M, C, _} <- Stats ],
  176. Table = format_table(ConsoleStats, CoverFiles),
  177. io:format("~ts", [Table]).
  178. format_table(Stats, CoverFiles) ->
  179. MaxLength = max(lists:foldl(fun max_length/2, 0, Stats), 20),
  180. Header = header(MaxLength),
  181. Seperator = seperator(MaxLength),
  182. [io_lib:format("~ts~n~ts~n~ts~n", [Seperator, Header, Seperator]),
  183. lists:map(fun({Mod, Coverage}) ->
  184. Name = format(Mod, MaxLength),
  185. Cov = format(Coverage, 8),
  186. io_lib:format(" | ~ts | ~ts |~n", [Name, Cov])
  187. end, Stats),
  188. io_lib:format("~ts~n", [Seperator]),
  189. io_lib:format(" coverage calculated from:~n", []),
  190. lists:map(fun(File) ->
  191. io_lib:format(" ~ts~n", [File])
  192. end, CoverFiles)].
  193. max_length({ModName, _}, Min) ->
  194. Length = length(lists:flatten(ModName)),
  195. case Length > Min of
  196. true -> Length;
  197. false -> Min
  198. end.
  199. header(Width) ->
  200. [" | ", format("module", Width), " | ", format("coverage", 8), " |"].
  201. seperator(Width) ->
  202. [" |--", io_lib:format("~*c", [Width, $-]), "--|------------|"].
  203. format(String, Width) -> io_lib:format("~*.ts", [Width, String]).
  204. write_index(State, Coverage) ->
  205. CoverDir = cover_dir(State),
  206. FileName = filename:join([CoverDir, "index.html"]),
  207. {ok, F} = file:open(FileName, [write]),
  208. ok = file:write(F, "<!DOCTYPE HTML><html>\n"
  209. "<head><meta charset=\"utf-8\">"
  210. "<title>Coverage Summary</title></head>\n"
  211. "<body>\n"),
  212. {Aggregate, Rest} = lists:partition(fun({"aggregate", _, _}) -> true; (_) -> false end,
  213. Coverage),
  214. ok = write_index_section(F, Aggregate),
  215. ok = write_index_section(F, Rest),
  216. ok = file:write(F, "</body></html>"),
  217. ok = file:close(F),
  218. io:format(" cover summary written to: ~ts~n", [filename:absname(FileName)]).
  219. write_index_section(_F, []) -> ok;
  220. write_index_section(F, [{Section, DataFile, Mods}|Rest]) ->
  221. %% Write the report
  222. ok = file:write(F, ?FMT("<h1>~s summary</h1>\n", [Section])),
  223. ok = file:write(F, "coverage calculated from:\n<ul>"),
  224. ok = lists:foreach(fun(D) -> ok = file:write(F, io_lib:format("<li>~ts</li>", [D])) end,
  225. DataFile),
  226. ok = file:write(F, "</ul>\n"),
  227. ok = file:write(F, "<table><tr><th>module</th><th>coverage %</th></tr>\n"),
  228. FmtLink =
  229. fun({Mod, Cov, Report}) ->
  230. ?FMT("<tr><td><a href='~ts'>~ts</a></td><td>~ts</td>\n",
  231. [strip_coverdir(Report), Mod, Cov])
  232. end,
  233. lists:foreach(fun(M) -> ok = file:write(F, FmtLink(M)) end, Mods),
  234. ok = file:write(F, "</table>\n"),
  235. write_index_section(F, Rest).
  236. %% fix for r15b which doesn't put the correct path in the `source` section
  237. %% of `module_info(compile)`
  238. strip_coverdir([]) -> "";
  239. strip_coverdir(File) ->
  240. filename:join(lists:reverse(lists:sublist(lists:reverse(filename:split(File)),
  241. 2))).
  242. cover_compile(State, ExtraDirs) ->
  243. %% start the cover server if necessary
  244. {ok, CoverPid} = start_cover(),
  245. %% redirect cover output
  246. true = redirect_cover_output(State, CoverPid),
  247. %% cover compile the modules we just compiled
  248. Apps = filter_checkouts(rebar_state:project_apps(State)),
  249. CompileResult = compile_beam_directories(Apps, []) ++
  250. compile_extras(ExtraDirs, []),
  251. %% print any warnings about modules that failed to cover compile
  252. lists:foreach(fun print_cover_warnings/1, CompileResult).
  253. filter_checkouts(Apps) -> filter_checkouts(Apps, []).
  254. filter_checkouts([], Acc) -> lists:reverse(Acc);
  255. filter_checkouts([App|Rest], Acc) ->
  256. case rebar_app_info:is_checkout(App) of
  257. true -> filter_checkouts(Rest, Acc);
  258. false -> filter_checkouts(Rest, [App|Acc])
  259. end.
  260. compile_beam_directories([], Acc) -> Acc;
  261. compile_beam_directories([App|Rest], Acc) ->
  262. Result = cover:compile_beam_directory(filename:join([rebar_app_info:out_dir(App),
  263. "ebin"])),
  264. compile_beam_directories(Rest, Acc ++ Result).
  265. compile_extras([], Acc) -> Acc;
  266. compile_extras([Dir|Rest], Acc) ->
  267. Result = cover:compile_beam_directory(Dir),
  268. compile_extras(Rest, Acc ++ Result).
  269. start_cover() ->
  270. case cover:start() of
  271. {ok, Pid} -> {ok, Pid};
  272. {error, {already_started, Pid}} -> {ok, Pid}
  273. end.
  274. redirect_cover_output(State, CoverPid) ->
  275. %% redirect cover console output to file
  276. DataDir = cover_dir(State),
  277. ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
  278. {ok, F} = file:open(filename:join([DataDir, "cover.log"]),
  279. [append]),
  280. group_leader(F, CoverPid).
  281. print_cover_warnings({ok, _}) -> ok;
  282. print_cover_warnings({error, File}) ->
  283. ?WARN("Cover compilation of ~p failed, module is not included in cover data.",
  284. [File]).
  285. write_coverdata(State, Task) ->
  286. DataDir = cover_dir(State),
  287. ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
  288. ExportFile = filename:join([DataDir, atom_to_list(Task) ++ ".coverdata"]),
  289. case cover:export(ExportFile) of
  290. ok ->
  291. ?DEBUG("Cover data written to ~p.", [ExportFile]);
  292. {error, Reason} ->
  293. ?WARN("Cover data export failed: ~p", [Reason])
  294. end.
  295. command_line_opts(State) ->
  296. {Opts, _} = rebar_state:command_parsed_args(State),
  297. Opts.
  298. config_opts(State) ->
  299. rebar_state:get(State, cover_opts, []).
  300. verbose(State) ->
  301. Command = proplists:get_value(verbose, command_line_opts(State), undefined),
  302. Config = proplists:get_value(verbose, config_opts(State), undefined),
  303. case {Command, Config} of
  304. {undefined, undefined} -> false;
  305. {undefined, Verbose} -> Verbose;
  306. {Verbose, _} -> Verbose
  307. end.
  308. cover_dir(State) ->
  309. filename:join([rebar_dir:base_dir(State), "cover"]).
  310. cover_opts(_State) ->
  311. [{reset, $r, "reset", boolean, help(reset)},
  312. {verbose, $v, "verbose", boolean, help(verbose)}].
  313. help(reset) -> "Reset all coverdata.";
  314. help(verbose) -> "Print coverage analysis.".