You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

370 lines
14 KiB

пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 14 година
пре 15 година
пре 15 година
пре 15 година
пре 14 година
пре 14 година
пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 14 година
пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 14 година
пре 15 година
пре 15 година
пре 14 година
пре 15 година
пре 14 година
пре 14 година
пре 14 година
пре 14 година
пре 14 година
пре 15 година
пре 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)) ++ "%".