Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

393 Zeilen
15 KiB

vor 14 Jahren
vor 15 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
vor 14 Jahren
  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)) ++ "%".