No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

335 líneas
13 KiB

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