Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

391 строка
14 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, apps).
  39. -spec maybe_cover_compile(rebar_state:t(), [file:name()] | apps) -> ok.
  40. maybe_cover_compile(State, Dirs) ->
  41. case rebar_state:get(State, cover_enabled, false) of
  42. true -> cover_compile(State, Dirs);
  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} = rebar_utils: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. TotalLabel = format("total", MaxLength),
  183. TotalCov = format(calculate_total(Stats), 8),
  184. [io_lib:format("~ts~n~ts~n~ts~n", [Seperator, Header, Seperator]),
  185. lists:map(fun({Mod, Coverage}) ->
  186. Name = format(Mod, MaxLength),
  187. Cov = format(Coverage, 8),
  188. io_lib:format(" | ~ts | ~ts |~n", [Name, Cov])
  189. end, Stats),
  190. io_lib:format("~ts~n", [Seperator]),
  191. io_lib:format(" | ~ts | ~ts |~n", [TotalLabel, TotalCov]),
  192. io_lib:format("~ts~n", [Seperator]),
  193. io_lib:format(" coverage calculated from:~n", []),
  194. lists:map(fun(File) ->
  195. io_lib:format(" ~ts~n", [File])
  196. end, CoverFiles)].
  197. max_length({ModName, _}, Min) ->
  198. Length = length(lists:flatten(ModName)),
  199. case Length > Min of
  200. true -> Length;
  201. false -> Min
  202. end.
  203. header(Width) ->
  204. [" | ", format("module", Width), " | ", format("coverage", 8), " |"].
  205. seperator(Width) ->
  206. [" |--", io_lib:format("~*c", [Width, $-]), "--|------------|"].
  207. format(String, Width) -> io_lib:format("~*.ts", [Width, String]).
  208. calculate_total(Stats) when length(Stats) =:= 0 ->
  209. "0%";
  210. calculate_total(Stats) ->
  211. TotalStats = length(Stats),
  212. TotalCovInt = round(lists:foldl(
  213. fun({_Mod, Coverage, _File}, Acc) ->
  214. Acc + (list_to_integer(string:strip(Coverage, right, $%)) / TotalStats);
  215. ({_Mod, Coverage}, Acc) ->
  216. Acc + (list_to_integer(string:strip(Coverage, right, $%)) / TotalStats)
  217. end, 0, Stats)),
  218. integer_to_list(TotalCovInt) ++ "%".
  219. write_index(State, Coverage) ->
  220. CoverDir = cover_dir(State),
  221. FileName = filename:join([CoverDir, "index.html"]),
  222. {ok, F} = file:open(FileName, [write]),
  223. ok = file:write(F, "<!DOCTYPE HTML><html>\n"
  224. "<head><meta charset=\"utf-8\">"
  225. "<title>Coverage Summary</title></head>\n"
  226. "<body>\n"),
  227. {Aggregate, Rest} = lists:partition(fun({"aggregate", _, _}) -> true; (_) -> false end,
  228. Coverage),
  229. ok = write_index_section(F, Aggregate),
  230. ok = write_index_section(F, Rest),
  231. ok = file:write(F, "</body></html>"),
  232. ok = file:close(F),
  233. io:format(" cover summary written to: ~ts~n", [filename:absname(FileName)]).
  234. write_index_section(_F, []) -> ok;
  235. write_index_section(F, [{Section, DataFile, Mods}|Rest]) ->
  236. %% Write the report
  237. ok = file:write(F, ?FMT("<h1>~s summary</h1>\n", [Section])),
  238. ok = file:write(F, "coverage calculated from:\n<ul>"),
  239. ok = lists:foreach(fun(D) -> ok = file:write(F, io_lib:format("<li>~ts</li>", [D])) end,
  240. DataFile),
  241. ok = file:write(F, "</ul>\n"),
  242. ok = file:write(F, "<table><tr><th>module</th><th>coverage %</th></tr>\n"),
  243. FmtLink =
  244. fun({Mod, Cov, Report}) ->
  245. ?FMT("<tr><td><a href='~ts'>~ts</a></td><td>~ts</td>\n",
  246. [strip_coverdir(Report), Mod, Cov])
  247. end,
  248. lists:foreach(fun(M) -> ok = file:write(F, FmtLink(M)) end, Mods),
  249. ok = file:write(F, ?FMT("<tr><td><strong>Total</strong></td><td>~ts</td>\n",
  250. [calculate_total(Mods)])),
  251. ok = file:write(F, "</table>\n"),
  252. write_index_section(F, Rest).
  253. %% fix for r15b which doesn't put the correct path in the `source` section
  254. %% of `module_info(compile)`
  255. strip_coverdir([]) -> "";
  256. strip_coverdir(File) ->
  257. filename:join(lists:reverse(lists:sublist(lists:reverse(filename:split(File)),
  258. 2))).
  259. cover_compile(State, apps) ->
  260. Apps = filter_checkouts(rebar_state:project_apps(State)),
  261. AppDirs = app_dirs(Apps),
  262. cover_compile(State, lists:filter(fun(D) -> ec_file:is_dir(D) end, AppDirs));
  263. cover_compile(State, Dirs) ->
  264. rebar_utils:update_code(rebar_state:code_paths(State, all_deps), [soft_purge]),
  265. %% start the cover server if necessary
  266. {ok, CoverPid} = start_cover(),
  267. %% redirect cover output
  268. true = redirect_cover_output(State, CoverPid),
  269. lists:foreach(fun(Dir) ->
  270. ?DEBUG("cover compiling ~p", [Dir]),
  271. case catch(cover:compile_beam_directory(Dir)) of
  272. {error, eacces} ->
  273. ?WARN("Directory ~p not readable, modules will not be included in coverage", [Dir]);
  274. {error, enoent} ->
  275. ?WARN("Directory ~p not found", [Dir]);
  276. {'EXIT', {Reason, _}} ->
  277. ?WARN("Cover compilation for directory ~p failed: ~p", [Dir, Reason]);
  278. Results ->
  279. %% print any warnings about modules that failed to cover compile
  280. lists:foreach(fun print_cover_warnings/1, lists:flatten(Results))
  281. end
  282. end, Dirs),
  283. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  284. ok.
  285. app_dirs(Apps) ->
  286. lists:foldl(fun app_ebin_dirs/2, [], Apps).
  287. app_ebin_dirs(App, Acc) ->
  288. [rebar_app_info:ebin_dir(App)|Acc].
  289. filter_checkouts(Apps) -> filter_checkouts(Apps, []).
  290. filter_checkouts([], Acc) -> lists:reverse(Acc);
  291. filter_checkouts([App|Rest], Acc) ->
  292. case rebar_app_info:is_checkout(App) of
  293. true -> filter_checkouts(Rest, Acc);
  294. false -> filter_checkouts(Rest, [App|Acc])
  295. end.
  296. start_cover() ->
  297. case cover:start() of
  298. {ok, Pid} -> {ok, Pid};
  299. {error, {already_started, Pid}} -> {ok, Pid}
  300. end.
  301. redirect_cover_output(State, CoverPid) ->
  302. %% redirect cover console output to file
  303. DataDir = cover_dir(State),
  304. ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
  305. {ok, F} = file:open(filename:join([DataDir, "cover.log"]),
  306. [append]),
  307. group_leader(F, CoverPid).
  308. print_cover_warnings({ok, _}) -> ok;
  309. print_cover_warnings({error, Error}) ->
  310. ?WARN("Cover compilation failed: ~p", [Error]).
  311. write_coverdata(State, Task) ->
  312. DataDir = cover_dir(State),
  313. ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
  314. ExportFile = filename:join([DataDir, atom_to_list(Task) ++ ".coverdata"]),
  315. case cover:export(ExportFile) of
  316. ok ->
  317. ?DEBUG("Cover data written to ~p.", [ExportFile]);
  318. {error, Reason} ->
  319. ?WARN("Cover data export failed: ~p", [Reason])
  320. end.
  321. command_line_opts(State) ->
  322. {Opts, _} = rebar_state:command_parsed_args(State),
  323. Opts.
  324. config_opts(State) ->
  325. rebar_state:get(State, cover_opts, []).
  326. verbose(State) ->
  327. Command = proplists:get_value(verbose, command_line_opts(State), undefined),
  328. Config = proplists:get_value(verbose, config_opts(State), undefined),
  329. case {Command, Config} of
  330. {undefined, undefined} -> false;
  331. {undefined, Verbose} -> Verbose;
  332. {Verbose, _} -> Verbose
  333. end.
  334. cover_dir(State) ->
  335. filename:join([rebar_dir:base_dir(State), "cover"]).
  336. cover_opts(_State) ->
  337. [{reset, $r, "reset", boolean, help(reset)},
  338. {verbose, $v, "verbose", boolean, help(verbose)}].
  339. help(reset) -> "Reset all coverdata.";
  340. help(verbose) -> "Print coverage analysis.".