選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

393 行
15 KiB

  1. %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. %% -------------------------------------------------------------------
  4. %%
  5. %% rebar: Erlang Build Tools
  6. %%
  7. %% Copyright (c) 2009, 2010 Dave Smith (dizzyd@dizzyd.com)
  8. %%
  9. %% Permission is hereby granted, free of charge, to any person obtaining a copy
  10. %% of this software and associated documentation files (the "Software"), to deal
  11. %% in the Software without restriction, including without limitation the rights
  12. %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. %% copies of the Software, and to permit persons to whom the Software is
  14. %% furnished to do so, subject to the following conditions:
  15. %%
  16. %% The above copyright notice and this permission notice shall be included in
  17. %% all copies or substantial portions of the Software.
  18. %%
  19. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. %% THE SOFTWARE.
  26. %% -------------------------------------------------------------------
  27. %% @author Dave Smith <dizzyd@dizzyd.com>
  28. %% @doc rebar_eunit supports the following commands:
  29. %% <ul>
  30. %% <li>eunit - runs eunit tests</li>
  31. %% <li>clean - remove .eunit directory</li>
  32. %% </ul>
  33. %% The following Global options are supported:
  34. %% <ul>
  35. %% <li>verbose=1 - show extra output from the eunit test</li>
  36. %% <li>suite="foo"" - runs test/foo_tests.erl</li>
  37. %% </ul>
  38. %% Additionally, for projects that have separate folders for the core
  39. %% implementation, and for the unit tests, then the following
  40. %% <code>rebar.config</code> option can be provided:
  41. %% <code>{eunit_compile_opts, [{src_dirs, ["dir"]}]}.</code>.
  42. %% @copyright 2009, 2010 Dave Smith
  43. %% -------------------------------------------------------------------
  44. -module(rebar_eunit).
  45. -export([eunit/2,
  46. clean/2]).
  47. -include("rebar.hrl").
  48. -define(EUNIT_DIR, ".eunit").
  49. %% ===================================================================
  50. %% Public API
  51. %% ===================================================================
  52. eunit(Config, AppFile) ->
  53. %% Check for app global parameter; this is a comma-delimited list
  54. %% of apps on which we want to run eunit
  55. case rebar_config:get_global(app, undefined) of
  56. undefined ->
  57. %% No app parameter specified, run everything..
  58. ok;
  59. Apps ->
  60. TargetApps = [list_to_atom(A) || A <- string:tokens(Apps, ",")],
  61. ThisApp = rebar_app_utils:app_name(AppFile),
  62. case lists:member(ThisApp, TargetApps) of
  63. true ->
  64. ok;
  65. false ->
  66. ?DEBUG("Skipping eunit on app: ~p\n", [ThisApp]),
  67. throw(ok)
  68. end
  69. end,
  70. %% Make sure ?EUNIT_DIR/ and ebin/ directory exists (tack on dummy module)
  71. ok = filelib:ensure_dir(eunit_dir() ++ "/foo"),
  72. ok = filelib:ensure_dir(ebin_dir() ++ "/foo"),
  73. %% Setup code path prior to compilation so that parse_transforms
  74. %% and the like work properly. Also, be sure to add ebin_dir()
  75. %% to the END of the code path so that we don't have to jump
  76. %% through hoops to access the .app file
  77. CodePath = code:get_path(),
  78. true = code:add_patha(eunit_dir()),
  79. true = code:add_pathz(ebin_dir()),
  80. %% Obtain all the test modules for inclusion in the compile stage.
  81. %% Notice: this could also be achieved with the following
  82. %% rebar.config option: {eunit_compile_opts, [{src_dirs, ["test"]}]}
  83. TestErls = rebar_utils:find_files("test", ".*\\.erl\$"),
  84. %% Copy source files to eunit dir for cover in case they are not directly
  85. %% in src but in a subdirectory of src. Cover only looks in cwd and ../src
  86. %% for source files.
  87. SrcErls = rebar_utils:find_files("src", ".*\\.erl\$"),
  88. ok = rebar_file_utils:cp_r(SrcErls ++ TestErls, ?EUNIT_DIR),
  89. %% Compile erlang code to ?EUNIT_DIR, using a tweaked config
  90. %% with appropriate defines for eunit, and include all the test modules
  91. %% as well.
  92. rebar_erlc_compiler:doterl_compile(eunit_config(Config),
  93. ?EUNIT_DIR, TestErls),
  94. %% Build a list of all the .beams in ?EUNIT_DIR -- use this for cover
  95. %% and eunit testing. Normally you can just tell cover and/or eunit to
  96. %% scan the directory for you, but eunit does a code:purge in conjunction
  97. %% with that scan and causes any cover compilation info to be lost.
  98. %% Filter out "*_tests" modules so eunit won't doubly run them and
  99. %% so cover only calculates coverage on production code.
  100. BeamFiles = [N || N <- rebar_utils:beams(?EUNIT_DIR),
  101. string:str(N, "_tests.beam") =:= 0],
  102. Modules = [rebar_utils:beam_to_mod(?EUNIT_DIR, N) || N <- BeamFiles],
  103. SrcModules = [rebar_utils:erl_to_mod(M) || M <- SrcErls],
  104. cover_init(Config, BeamFiles),
  105. EunitResult = perform_eunit(Config, Modules),
  106. perform_cover(Config, Modules, SrcModules),
  107. case EunitResult of
  108. ok ->
  109. ok;
  110. _ ->
  111. ?ABORT("One or more eunit tests failed.~n", [])
  112. end,
  113. %% Restore code path
  114. true = code:set_path(CodePath),
  115. ok.
  116. clean(_Config, _File) ->
  117. rebar_file_utils:rm_rf(?EUNIT_DIR).
  118. %% ===================================================================
  119. %% Internal functions
  120. %% ===================================================================
  121. eunit_dir() ->
  122. filename:join(rebar_utils:get_cwd(), ?EUNIT_DIR).
  123. ebin_dir() ->
  124. filename:join(rebar_utils:get_cwd(), "ebin").
  125. perform_eunit(Config, Modules) ->
  126. %% suite defined, so only specify the module that relates to the
  127. %% suite (if any)
  128. Suite = rebar_config:get_global(suite, undefined),
  129. EunitOpts = get_eunit_opts(Config),
  130. %% Move down into ?EUNIT_DIR while we run tests so any generated files
  131. %% are created there (versus in the source dir)
  132. Cwd = rebar_utils:get_cwd(),
  133. ok = file:set_cwd(?EUNIT_DIR),
  134. EunitResult = perform_eunit(EunitOpts, Modules, Suite),
  135. %% Return to original working dir
  136. ok = file:set_cwd(Cwd),
  137. EunitResult.
  138. perform_eunit(EunitOpts, Modules, undefined) ->
  139. (catch eunit:test(Modules, EunitOpts));
  140. perform_eunit(EunitOpts, _Modules, Suite) ->
  141. (catch eunit:test(list_to_atom(Suite), EunitOpts)).
  142. get_eunit_opts(Config) ->
  143. %% Enable verbose in eunit if so requested..
  144. BaseOpts = case rebar_config:is_verbose() of
  145. true ->
  146. [verbose];
  147. false ->
  148. []
  149. end,
  150. BaseOpts ++ rebar_config:get_list(Config, eunit_opts, []).
  151. eunit_config(Config) ->
  152. EqcOpts = eqc_opts(),
  153. PropErOpts = proper_opts(),
  154. ErlOpts = rebar_config:get_list(Config, erl_opts, []),
  155. EunitOpts = rebar_config:get_list(Config, eunit_compile_opts, []),
  156. Opts = [{d, 'TEST'}, debug_info] ++
  157. ErlOpts ++ EunitOpts ++ EqcOpts ++ PropErOpts,
  158. Config1 = rebar_config:set(Config, erl_opts, Opts),
  159. FirstErls = rebar_config:get_list(Config1, eunit_first_files, []),
  160. rebar_config:set(Config1, erl_first_files, FirstErls).
  161. eqc_opts() ->
  162. define_if('PROPER', is_lib_avail(is_eqc_avail, eqc,
  163. "eqc.hrl", "QuickCheck")).
  164. proper_opts() ->
  165. define_if('EQC', is_lib_avail(is_proper_avail, proper,
  166. "proper.hrl", "PropEr")).
  167. define_if(Def, true) -> [{d, Def}];
  168. define_if(_Def, false) -> [].
  169. is_lib_avail(DictKey, Mod, Hrl, Name) ->
  170. case erlang:get(DictKey) of
  171. undefined ->
  172. IsAvail = case code:lib_dir(Mod, include) of
  173. {error, bad_name} ->
  174. false;
  175. Dir ->
  176. filelib:is_regular(filename:join(Dir, Hrl))
  177. end,
  178. erlang:put(DictKey, IsAvail),
  179. ?DEBUG("~s availability: ~p\n", [Name, IsAvail]),
  180. IsAvail;
  181. IsAvail ->
  182. IsAvail
  183. end.
  184. perform_cover(Config, BeamFiles, SrcModules) ->
  185. perform_cover(rebar_config:get(Config, cover_enabled, false),
  186. Config, BeamFiles, SrcModules).
  187. perform_cover(false, _Config, _BeamFiles, _SrcModules) ->
  188. ok;
  189. perform_cover(true, Config, BeamFiles, SrcModules) ->
  190. cover_analyze(Config, BeamFiles, SrcModules).
  191. cover_analyze(_Config, [], _SrcModules) ->
  192. ok;
  193. cover_analyze(Config, Modules, SrcModules) ->
  194. Suite = list_to_atom(rebar_config:get_global(suite, "")),
  195. FilteredModules = [M || M <- Modules, M =/= Suite],
  196. %% Generate coverage info for all the cover-compiled modules
  197. Coverage = [cover_analyze_mod(M) || M <- FilteredModules],
  198. %% Write index of coverage info
  199. cover_write_index(lists:sort(Coverage), SrcModules),
  200. %% Write coverage details for each file
  201. lists:foreach(fun({M, _, _}) ->
  202. {ok, _} = cover:analyze_to_file(M, cover_file(M),
  203. [html])
  204. end, Coverage),
  205. Index = filename:join([rebar_utils:get_cwd(), ?EUNIT_DIR, "index.html"]),
  206. ?CONSOLE("Cover analysis: ~s\n", [Index]),
  207. %% Print coverage report, if configured
  208. case rebar_config:get(Config, cover_print_enabled, false) of
  209. true ->
  210. cover_print_coverage(lists:sort(Coverage));
  211. false ->
  212. ok
  213. end.
  214. cover_init(false, _BeamFiles) ->
  215. ok;
  216. cover_init(true, BeamFiles) ->
  217. %% Make sure any previous runs of cover don't unduly influence
  218. cover:reset(),
  219. ?INFO("Cover compiling ~s\n", [rebar_utils:get_cwd()]),
  220. Compiled = [{Beam, cover:compile_beam(Beam)} || Beam <- BeamFiles],
  221. case [Module || {_, {ok, Module}} <- Compiled] of
  222. [] ->
  223. %% No modules compiled successfully...fail
  224. ?ERROR("Cover failed to compile any modules; aborting.~n", []),
  225. ?FAIL;
  226. _ ->
  227. %% At least one module compiled successfully
  228. %% It's not an error for cover compilation to fail partially,
  229. %% but we do want to warn about them
  230. PrintWarning =
  231. fun(Beam, Desc) ->
  232. ?CONSOLE("Cover compilation warning for ~p: ~p",
  233. [Beam, Desc])
  234. end,
  235. _ = [PrintWarning(Beam, Desc) || {Beam, {error, Desc}} <- Compiled],
  236. ok
  237. end;
  238. cover_init(Config, BeamFiles) ->
  239. cover_init(rebar_config:get(Config, cover_enabled, false), BeamFiles).
  240. cover_analyze_mod(Module) ->
  241. case cover:analyze(Module, coverage, module) of
  242. {ok, {Module, {Covered, NotCovered}}} ->
  243. %% Modules that include the eunit header get an implicit
  244. %% test/0 fun, which cover considers a runnable line, but
  245. %% eunit:test(TestRepresentation) never calls. Decrement
  246. %% NotCovered in this case.
  247. align_notcovered_count(Module, Covered, NotCovered,
  248. is_eunitized(Module));
  249. {error, Reason} ->
  250. ?ERROR("Cover analyze failed for ~p: ~p ~p\n",
  251. [Module, Reason, code:which(Module)]),
  252. {0,0}
  253. end.
  254. is_eunitized(Mod) ->
  255. has_eunit_test_fun(Mod) andalso
  256. has_header(Mod, "include/eunit.hrl").
  257. has_eunit_test_fun(Mod) ->
  258. [F || {exports, Funs} <- Mod:module_info(),
  259. {F, 0} <- Funs, F =:= test] =/= [].
  260. has_header(Mod, Header) ->
  261. Mod1 = case code:which(Mod) of
  262. cover_compiled ->
  263. {file, File} = cover:is_compiled(Mod),
  264. File;
  265. non_existing -> Mod;
  266. preloaded -> Mod;
  267. L -> L
  268. end,
  269. {ok, {_, [{abstract_code, {_, AC}}]}} = beam_lib:chunks(Mod1,
  270. [abstract_code]),
  271. [F || {attribute, 1, file, {F, 1}} <- AC,
  272. string:str(F, Header) =/= 0] =/= [].
  273. align_notcovered_count(Module, Covered, NotCovered, false) ->
  274. {Module, Covered, NotCovered};
  275. align_notcovered_count(Module, Covered, NotCovered, true) ->
  276. {Module, Covered, NotCovered - 1}.
  277. cover_write_index(Coverage, SrcModules) ->
  278. {ok, F} = file:open(filename:join([?EUNIT_DIR, "index.html"]), [write]),
  279. ok = file:write(F, "<html><head><title>Coverage Summary</title></head>\n"),
  280. IsSrcCoverage = fun({Mod,_C,_N}) -> lists:member(Mod, SrcModules) end,
  281. {SrcCoverage, TestCoverage} = lists:partition(IsSrcCoverage, Coverage),
  282. cover_write_index_section(F, "Source", SrcCoverage),
  283. cover_write_index_section(F, "Test", TestCoverage),
  284. ok = file:write(F, "</body></html>"),
  285. ok = file:close(F).
  286. cover_write_index_section(_F, _SectionName, []) ->
  287. ok;
  288. cover_write_index_section(F, SectionName, Coverage) ->
  289. %% Calculate total coverage %
  290. {Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
  291. {CAcc + C, NAcc + N}
  292. end, {0, 0}, Coverage),
  293. TotalCoverage = percentage(Covered, NotCovered),
  294. %% Write the report
  295. ok = file:write(F, ?FMT("<body><h1>~s Summary</h1>\n", [SectionName])),
  296. ok = file:write(F, ?FMT("<h3>Total: ~s</h3>\n", [TotalCoverage])),
  297. ok = file:write(F, "<table><tr><th>Module</th><th>Coverage %</th></tr>\n"),
  298. FmtLink =
  299. fun(Module, Cov, NotCov) ->
  300. ?FMT("<tr><td><a href='~s.COVER.html'>~s</a></td><td>~s</td>\n",
  301. [Module, Module, percentage(Cov, NotCov)])
  302. end,
  303. lists:foreach(fun({Module, Cov, NotCov}) ->
  304. ok = file:write(F, FmtLink(Module, Cov, NotCov))
  305. end, Coverage),
  306. ok = file:write(F, "</table>\n").
  307. cover_print_coverage(Coverage) ->
  308. {Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
  309. {CAcc + C, NAcc + N}
  310. end, {0, 0}, Coverage),
  311. TotalCoverage = percentage(Covered, NotCovered),
  312. %% Determine the longest module name for right-padding
  313. Width = lists:foldl(fun({Mod, _, _}, Acc) ->
  314. case length(atom_to_list(Mod)) of
  315. N when N > Acc ->
  316. N;
  317. _ ->
  318. Acc
  319. end
  320. end, 0, Coverage) * -1,
  321. %% Print the output the console
  322. ?CONSOLE("~nCode Coverage:~n", []),
  323. lists:foreach(fun({Mod, C, N}) ->
  324. ?CONSOLE("~*s : ~3s~n",
  325. [Width, Mod, percentage(C, N)])
  326. end, Coverage),
  327. ?CONSOLE("~n~*s : ~s~n", [Width, "Total", TotalCoverage]).
  328. cover_file(Module) ->
  329. filename:join([?EUNIT_DIR, atom_to_list(Module) ++ ".COVER.html"]).
  330. percentage(0, 0) ->
  331. "not executed";
  332. percentage(Cov, NotCov) ->
  333. integer_to_list(trunc((Cov / (Cov + NotCov)) * 100)) ++ "%".