25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

370 lines
14 KiB

15 년 전
  1. %% -*- tab-width: 4;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 <code>rebar.config</code>
  40. %% option can be provided: <code>{eunit_compile_opts, [{src_dirs, ["dir"]}]}.</code>.
  41. %% @copyright 2009, 2010 Dave Smith
  42. %% -------------------------------------------------------------------
  43. -module(rebar_eunit).
  44. -export([eunit/2]).
  45. -compile([export_all]).
  46. -include("rebar.hrl").
  47. -define(EUNIT_DIR, ".eunit").
  48. %% ===================================================================
  49. %% Public API
  50. %% ===================================================================
  51. eunit(Config, AppFile) ->
  52. %% Check for app global parameter; this is a comma-delimited list
  53. %% of apps on which we want to run eunit
  54. case rebar_config:get_global(app, undefined) of
  55. undefined ->
  56. %% No app parameter specified, run everything..
  57. ok;
  58. Apps ->
  59. TargetApps = [list_to_atom(A) || A <- string:tokens(Apps, ",")],
  60. ThisApp = rebar_app_utils:app_name(AppFile),
  61. case lists:member(ThisApp, TargetApps) of
  62. true ->
  63. ok;
  64. false ->
  65. ?DEBUG("Skipping eunit on app: ~p\n", [ThisApp]),
  66. throw(ok)
  67. end
  68. end,
  69. %% Make sure ?EUNIT_DIR/ and ebin/ directory exists (tack on dummy module)
  70. ok = filelib:ensure_dir(eunit_dir() ++ "/foo"),
  71. ok = filelib:ensure_dir(ebin_dir() ++ "/foo"),
  72. %% Setup code path prior to compilation so that parse_transforms and the like
  73. %% work properly. Also, be sure to add ebin_dir() to the END of the code path
  74. %% so that we don't have to jump through hoops to access the .app file
  75. CodePath = code:get_path(),
  76. true = code:add_patha(eunit_dir()),
  77. true = code:add_pathz(ebin_dir()),
  78. %% Obtain all the test modules for inclusion in the compile stage.
  79. %% Notice: this could also be achieved with the following rebar.config option:
  80. %% {eunit_compile_opts, [{src_dirs, ["test"]}]}
  81. TestErls = rebar_utils:find_files("test", ".*\\.erl\$"),
  82. %% Copy source files to eunit dir for cover in case they are not directly
  83. %% in src but in a subdirectory of src. Cover only looks in cwd and ../src
  84. %% for source files.
  85. SrcErls = rebar_utils:find_files("src", ".*\\.erl\$"),
  86. ok = rebar_file_utils:cp_r(SrcErls ++ TestErls, ?EUNIT_DIR),
  87. %% Compile erlang code to ?EUNIT_DIR, using a tweaked config
  88. %% with appropriate defines for eunit, and include all the test modules
  89. %% as well.
  90. rebar_erlc_compiler:doterl_compile(eunit_config(Config), ?EUNIT_DIR, TestErls),
  91. %% Build a list of all the .beams in ?EUNIT_DIR -- use this for cover
  92. %% and eunit testing. Normally you can just tell cover and/or eunit to
  93. %% scan the directory for you, but eunit does a code:purge in conjunction
  94. %% with that scan and causes any cover compilation info to be lost.
  95. %% Filter out "*_tests" modules so eunit won't doubly run them and
  96. %% so cover only calculates coverage on production code.
  97. BeamFiles = [N || N <- rebar_utils:beams(?EUNIT_DIR),
  98. string:str(N, "_tests.beam") =:= 0],
  99. Modules = [rebar_utils:beam_to_mod(?EUNIT_DIR, N) || N <- BeamFiles],
  100. SrcModules = [rebar_utils:erl_to_mod(M) || M <- SrcErls],
  101. cover_init(Config, BeamFiles),
  102. EunitResult = perform_eunit(Config, Modules),
  103. perform_cover(Config, Modules, SrcModules),
  104. case EunitResult of
  105. ok ->
  106. ok;
  107. _ ->
  108. ?ABORT("One or more eunit tests failed.~n", [])
  109. end,
  110. %% Restore code path
  111. true = code:set_path(CodePath),
  112. ok.
  113. clean(_Config, _File) ->
  114. rebar_file_utils:rm_rf(?EUNIT_DIR).
  115. %% ===================================================================
  116. %% Internal functions
  117. %% ===================================================================
  118. eunit_dir() ->
  119. filename:join(rebar_utils:get_cwd(), ?EUNIT_DIR).
  120. ebin_dir() ->
  121. filename:join(rebar_utils:get_cwd(), "ebin").
  122. perform_eunit(Config, Modules) ->
  123. %% suite defined, so only specify the module that relates to the
  124. %% suite (if any)
  125. Suite = rebar_config:get_global(suite, undefined),
  126. EunitOpts = get_eunit_opts(Config),
  127. %% Move down into ?EUNIT_DIR while we run tests so any generated files
  128. %% are created there (versus in the source dir)
  129. Cwd = rebar_utils:get_cwd(),
  130. ok = file:set_cwd(?EUNIT_DIR),
  131. EunitResult = perform_eunit(EunitOpts, Modules, Suite),
  132. %% Return to original working dir
  133. ok = file:set_cwd(Cwd),
  134. EunitResult.
  135. perform_eunit(EunitOpts, Modules, undefined) ->
  136. (catch eunit:test(Modules, EunitOpts));
  137. perform_eunit(EunitOpts, _Modules, Suite) ->
  138. (catch eunit:test(list_to_atom(Suite), EunitOpts)).
  139. get_eunit_opts(Config) ->
  140. %% Enable verbose in eunit if so requested..
  141. BaseOpts = case rebar_config:is_verbose() of
  142. true ->
  143. [verbose];
  144. false ->
  145. []
  146. end,
  147. BaseOpts ++ rebar_config:get_list(Config, eunit_opts, []).
  148. eunit_config(Config) ->
  149. EqcOpts = case is_quickcheck_avail() of
  150. true ->
  151. [{d, 'EQC'}];
  152. false ->
  153. []
  154. end,
  155. ErlOpts = rebar_config:get_list(Config, erl_opts, []),
  156. EunitOpts = rebar_config:get_list(Config, eunit_compile_opts, []),
  157. Opts = [{d, 'TEST'}, debug_info] ++
  158. ErlOpts ++ EunitOpts ++ EqcOpts,
  159. rebar_config:set(Config, erl_opts, Opts).
  160. is_quickcheck_avail() ->
  161. case erlang:get(is_quickcheck_avail) of
  162. undefined ->
  163. case code:lib_dir(eqc, include) of
  164. {error, bad_name} ->
  165. IsAvail = false;
  166. Dir ->
  167. IsAvail = filelib:is_regular(filename:join(Dir, "eqc.hrl"))
  168. end,
  169. erlang:put(is_quickcheck_avail, IsAvail),
  170. ?DEBUG("Quickcheck availability: ~p\n", [IsAvail]),
  171. IsAvail;
  172. IsAvail ->
  173. IsAvail
  174. end.
  175. perform_cover(Config, BeamFiles, SrcModules) ->
  176. perform_cover(rebar_config:get(Config, cover_enabled, false),
  177. Config, BeamFiles, SrcModules).
  178. perform_cover(false, _Config, _BeamFiles, _SrcModules) ->
  179. ok;
  180. perform_cover(true, Config, BeamFiles, SrcModules) ->
  181. cover_analyze(Config, BeamFiles, SrcModules).
  182. cover_analyze(_Config, [], _SrcModules) ->
  183. ok;
  184. cover_analyze(Config, Modules, SrcModules) ->
  185. Suite = list_to_atom(rebar_config:get_global(suite, "")),
  186. FilteredModules = [M || M <- Modules, M =/= Suite],
  187. %% Generate coverage info for all the cover-compiled modules
  188. Coverage = [cover_analyze_mod(M) || M <- FilteredModules],
  189. %% Write index of coverage info
  190. cover_write_index(lists:sort(Coverage), SrcModules),
  191. %% Write coverage details for each file
  192. lists:foreach(fun({M, _, _}) ->
  193. {ok, _} = cover:analyze_to_file(M, cover_file(M), [html])
  194. end, Coverage),
  195. Index = filename:join([rebar_utils:get_cwd(), ?EUNIT_DIR, "index.html"]),
  196. ?CONSOLE("Cover analysis: ~s\n", [Index]),
  197. %% Print coverage report, if configured
  198. case rebar_config:get(Config, cover_print_enabled, false) of
  199. true ->
  200. cover_print_coverage(lists:sort(Coverage));
  201. false ->
  202. ok
  203. end.
  204. cover_init(false, _BeamFiles) ->
  205. ok;
  206. cover_init(true, BeamFiles) ->
  207. %% Make sure any previous runs of cover don't unduly influence
  208. cover:reset(),
  209. ?INFO("Cover compiling ~s\n", [rebar_utils:get_cwd()]),
  210. Compiled = [{Beam, cover:compile_beam(Beam)} || Beam <- BeamFiles],
  211. case [Module || {_, {ok, Module}} <- Compiled] of
  212. [] ->
  213. %% No modules compiled successfully...fail
  214. ?ERROR("Cover failed to compile any modules; aborting.~n", []),
  215. ?FAIL;
  216. _ ->
  217. %% At least one module compiled successfully
  218. %% It's not an error for cover compilation to fail partially,
  219. %% but we do want to warn about them
  220. _ = [?CONSOLE("Cover compilation warning for ~p: ~p", [Beam, Desc]) || {Beam, {error, Desc}} <- Compiled],
  221. ok
  222. end;
  223. cover_init(Config, BeamFiles) ->
  224. cover_init(rebar_config:get(Config, cover_enabled, false), BeamFiles).
  225. cover_analyze_mod(Module) ->
  226. case cover:analyze(Module, coverage, module) of
  227. {ok, {Module, {Covered, NotCovered}}} ->
  228. %% Modules that include the eunit header get an implicit
  229. %% test/0 fun, which cover considers a runnable line, but
  230. %% eunit:test(TestRepresentation) never calls. Decrement
  231. %% NotCovered in this case.
  232. align_notcovered_count(Module, Covered, NotCovered,
  233. is_eunitized(Module));
  234. {error, Reason} ->
  235. ?ERROR("Cover analyze failed for ~p: ~p ~p\n",
  236. [Module, Reason, code:which(Module)]),
  237. {0,0}
  238. end.
  239. is_eunitized(Mod) ->
  240. has_eunit_test_fun(Mod) andalso
  241. has_header(Mod, "include/eunit.hrl").
  242. has_eunit_test_fun(Mod) ->
  243. [F || {exports, Funs} <- Mod:module_info(),
  244. {F, 0} <- Funs, F =:= test] =/= [].
  245. has_header(Mod, Header) ->
  246. Mod1 = case code:which(Mod) of
  247. cover_compiled ->
  248. {file, File} = cover:is_compiled(Mod),
  249. File;
  250. non_existing -> Mod;
  251. preloaded -> Mod;
  252. L -> L
  253. end,
  254. {ok, {_, [{abstract_code, {_, AC}}]}} = beam_lib:chunks(Mod1, [abstract_code]),
  255. [F || {attribute, 1, file, {F, 1}} <- AC,
  256. string:str(F, Header) =/= 0] =/= [].
  257. align_notcovered_count(Module, Covered, NotCovered, false) ->
  258. {Module, Covered, NotCovered};
  259. align_notcovered_count(Module, Covered, NotCovered, true) ->
  260. {Module, Covered, NotCovered - 1}.
  261. cover_write_index(Coverage, SrcModules) ->
  262. {ok, F} = file:open(filename:join([?EUNIT_DIR, "index.html"]), [write]),
  263. ok = file:write(F, "<html><head><title>Coverage Summary</title></head>\n"),
  264. IsSrcCoverage = fun({Mod,_C,_N}) -> lists:member(Mod, SrcModules) end,
  265. {SrcCoverage, TestCoverage} = lists:partition(IsSrcCoverage, Coverage),
  266. cover_write_index_section(F, "Source", SrcCoverage),
  267. cover_write_index_section(F, "Test", TestCoverage),
  268. ok = file:write(F, "</body></html>"),
  269. ok = file:close(F).
  270. cover_write_index_section(_F, _SectionName, []) ->
  271. ok;
  272. cover_write_index_section(F, SectionName, Coverage) ->
  273. %% Calculate total coverage %
  274. {Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
  275. {CAcc + C, NAcc + N}
  276. end, {0, 0}, Coverage),
  277. TotalCoverage = percentage(Covered, NotCovered),
  278. %% Write the report
  279. ok = file:write(F, ?FMT("<body><h1>~s Summary</h1>\n", [SectionName])),
  280. ok = file:write(F, ?FMT("<h3>Total: ~s</h3>\n", [TotalCoverage])),
  281. ok = file:write(F, "<table><tr><th>Module</th><th>Coverage %</th></tr>\n"),
  282. lists:foreach(fun({Module, Cov, NotCov}) ->
  283. ok = file:write(F, ?FMT("<tr><td><a href='~s.COVER.html'>~s</a></td><td>~s</td>\n",
  284. [Module, Module, percentage(Cov, NotCov)]))
  285. end, Coverage),
  286. ok = file:write(F, "</table>\n").
  287. cover_print_coverage(Coverage) ->
  288. {Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
  289. {CAcc + C, NAcc + N}
  290. end, {0, 0}, Coverage),
  291. TotalCoverage = percentage(Covered, NotCovered),
  292. %% Determine the longest module name for right-padding
  293. Width = lists:foldl(fun({Mod, _, _}, Acc) ->
  294. case length(atom_to_list(Mod)) of
  295. N when N > Acc ->
  296. N;
  297. _ ->
  298. Acc
  299. end
  300. end, 0, Coverage) * -1,
  301. %% Print the output the console
  302. ?CONSOLE("~nCode Coverage:~n", []),
  303. lists:foreach(fun({Mod, C, N}) ->
  304. ?CONSOLE("~*s : ~3s~n",
  305. [Width, Mod, percentage(C, N)])
  306. end, Coverage),
  307. ?CONSOLE("~n~*s : ~s~n", [Width, "Total", TotalCoverage]).
  308. cover_file(Module) ->
  309. filename:join([?EUNIT_DIR, atom_to_list(Module) ++ ".COVER.html"]).
  310. percentage(0, 0) ->
  311. "not executed";
  312. percentage(Cov, NotCov) ->
  313. integer_to_list(trunc((Cov / (Cov + NotCov)) * 100)) ++ "%".