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

500 lines
19 KiB

  1. %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. -module(rebar_prv_eunit).
  4. -behaviour(provider).
  5. -export([init/1,
  6. do/1,
  7. format_error/1]).
  8. %% exported solely for tests
  9. -export([prepare_tests/1, eunit_opts/1, validate_tests/2]).
  10. -include("rebar.hrl").
  11. -include_lib("providers/include/providers.hrl").
  12. -define(PROVIDER, eunit).
  13. %% we need to modify app_info state before compile
  14. -define(DEPS, [lock]).
  15. %% ===================================================================
  16. %% Public API
  17. %% ===================================================================
  18. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  19. init(State) ->
  20. Provider = providers:create([{name, ?PROVIDER},
  21. {module, ?MODULE},
  22. {deps, ?DEPS},
  23. {bare, true},
  24. {example, "rebar3 eunit"},
  25. {short_desc, "Run EUnit Tests."},
  26. {desc, "Run EUnit Tests."},
  27. {opts, eunit_opts(State)},
  28. {profiles, [test]}]),
  29. State1 = rebar_state:add_provider(State, Provider),
  30. {ok, State1}.
  31. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  32. do(State) ->
  33. Tests = prepare_tests(State),
  34. %% inject `eunit_first_files`, `eunit_compile_opts` and any
  35. %% directories required by tests into the applications
  36. NewState = inject_eunit_state(State, Tests),
  37. case compile(NewState) of
  38. %% successfully compiled apps
  39. {ok, S} -> do(S, Tests);
  40. Error -> Error
  41. end.
  42. do(State, Tests) ->
  43. ?INFO("Performing EUnit tests...", []),
  44. setup_name(State),
  45. rebar_utils:update_code(rebar_state:code_paths(State, all_deps), [soft_purge]),
  46. %% Run eunit provider prehooks
  47. Providers = rebar_state:providers(State),
  48. Cwd = rebar_dir:get_cwd(),
  49. rebar_hooks:run_project_and_app_hooks(Cwd, pre, ?PROVIDER, Providers, State),
  50. case validate_tests(State, Tests) of
  51. {ok, T} ->
  52. case run_tests(State, T) of
  53. {ok, State1} ->
  54. %% Run eunit provider posthooks
  55. rebar_hooks:run_project_and_app_hooks(Cwd, post, ?PROVIDER, Providers, State1),
  56. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  57. {ok, State1};
  58. Error ->
  59. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  60. Error
  61. end;
  62. Error ->
  63. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  64. Error
  65. end.
  66. run_tests(State, Tests) ->
  67. T = translate_paths(State, Tests),
  68. EUnitOpts = resolve_eunit_opts(State),
  69. ?DEBUG("eunit_tests ~p", [T]),
  70. ?DEBUG("eunit_opts ~p", [EUnitOpts]),
  71. Result = eunit:test(T, EUnitOpts),
  72. ok = maybe_write_coverdata(State),
  73. case handle_results(Result) of
  74. {error, Reason} ->
  75. ?PRV_ERROR(Reason);
  76. ok ->
  77. {ok, State}
  78. end.
  79. -spec format_error(any()) -> iolist().
  80. format_error(unknown_error) ->
  81. io_lib:format("Error running tests", []);
  82. format_error({error_running_tests, Reason}) ->
  83. io_lib:format("Error running tests: ~p", [Reason]);
  84. format_error({eunit_test_errors, Errors}) ->
  85. io_lib:format(lists:concat(["Error Running EUnit Tests:"] ++
  86. lists:map(fun(Error) -> "~n " ++ Error end, Errors)), []);
  87. format_error({badconfig, {Msg, {Value, Key}}}) ->
  88. io_lib:format(Msg, [Value, Key]);
  89. format_error({error, Error}) ->
  90. format_error({error_running_tests, Error}).
  91. %% ===================================================================
  92. %% Internal functions
  93. %% ===================================================================
  94. setup_name(State) ->
  95. {Long, Short, Opts} = rebar_dist_utils:find_options(State),
  96. rebar_dist_utils:either(Long, Short, Opts).
  97. prepare_tests(State) ->
  98. %% parse and translate command line tests
  99. CmdTests = resolve_tests(State),
  100. CfgTests = cfg_tests(State),
  101. ProjectApps = rebar_state:project_apps(State),
  102. %% prioritize tests to run first trying any command line specified
  103. %% tests falling back to tests specified in the config file finally
  104. %% running a default set if no other tests are present
  105. select_tests(State, ProjectApps, CmdTests, CfgTests).
  106. resolve_tests(State) ->
  107. {RawOpts, _} = rebar_state:command_parsed_args(State),
  108. Apps = resolve(app, application, RawOpts),
  109. Applications = resolve(application, RawOpts),
  110. Dirs = resolve(dir, RawOpts),
  111. Files = resolve(file, RawOpts),
  112. Modules = resolve(module, RawOpts),
  113. Suites = resolve(suite, module, RawOpts),
  114. Apps ++ Applications ++ Dirs ++ Files ++ Modules ++ Suites.
  115. resolve(Flag, RawOpts) -> resolve(Flag, Flag, RawOpts).
  116. resolve(Flag, EUnitKey, RawOpts) ->
  117. case proplists:get_value(Flag, RawOpts) of
  118. undefined -> [];
  119. Args -> lists:map(fun(Arg) -> normalize(EUnitKey, Arg) end, string:tokens(Args, [$,]))
  120. end.
  121. normalize(Key, Value) when Key == dir; Key == file -> {Key, Value};
  122. normalize(Key, Value) -> {Key, list_to_atom(Value)}.
  123. cfg_tests(State) ->
  124. case rebar_state:get(State, eunit_tests, []) of
  125. Tests when is_list(Tests) ->
  126. lists:map(fun({app, App}) -> {application, App}; (T) -> T end, Tests);
  127. Wrong ->
  128. %% probably a single non list term
  129. ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, eunit_tests}}})
  130. end.
  131. select_tests(_State, _ProjectApps, {error, _} = Error, _) -> Error;
  132. select_tests(_State, _ProjectApps, _, {error, _} = Error) -> Error;
  133. select_tests(State, ProjectApps, [], []) -> {ok, default_tests(State, ProjectApps)};
  134. select_tests(_State, _ProjectApps, [], Tests) -> {ok, Tests};
  135. select_tests(_State, _ProjectApps, Tests, _) -> {ok, Tests}.
  136. default_tests(State, Apps) ->
  137. %% use `{application, App}` for each app in project
  138. AppTests = set_apps(Apps),
  139. %% additional test modules in `test` dir of each app
  140. ModTests = set_modules(Apps, State),
  141. AppTests ++ ModTests.
  142. set_apps(Apps) -> set_apps(Apps, []).
  143. set_apps([], Acc) -> Acc;
  144. set_apps([App|Rest], Acc) ->
  145. AppName = list_to_atom(binary_to_list(rebar_app_info:name(App))),
  146. set_apps(Rest, [{application, AppName}|Acc]).
  147. set_modules(Apps, State) -> set_modules(Apps, State, {[], []}).
  148. set_modules([], State, {AppAcc, TestAcc}) ->
  149. TestSrc = gather_src([filename:join([rebar_state:dir(State), "test"])]),
  150. dedupe_tests({AppAcc, TestAcc ++ TestSrc});
  151. set_modules([App|Rest], State, {AppAcc, TestAcc}) ->
  152. F = fun(Dir) -> filename:join([rebar_app_info:dir(App), Dir]) end,
  153. AppDirs = lists:map(F, rebar_dir:src_dirs(rebar_app_info:opts(App), ["src"])),
  154. AppSrc = gather_src(AppDirs),
  155. TestDirs = [filename:join([rebar_app_info:dir(App), "test"])],
  156. TestSrc = gather_src(TestDirs),
  157. set_modules(Rest, State, {AppSrc ++ AppAcc, TestSrc ++ TestAcc}).
  158. gather_src(Dirs) -> gather_src(Dirs, []).
  159. gather_src([], Srcs) -> Srcs;
  160. gather_src([Dir|Rest], Srcs) ->
  161. gather_src(Rest, Srcs ++ rebar_utils:find_files(Dir, "^[^._].*\\.erl\$", true)).
  162. dedupe_tests({AppMods, TestMods}) ->
  163. %% for each modules in TestMods create a test if there is not a module
  164. %% in AppMods that will trigger it
  165. F = fun(Mod) ->
  166. M = filename:basename(Mod, ".erl"),
  167. MatchesTest = fun(Dir) -> filename:basename(Dir, ".erl") ++ "_tests" == M end,
  168. case lists:any(MatchesTest, AppMods) of
  169. false -> {true, {module, list_to_atom(M)}};
  170. true -> false
  171. end
  172. end,
  173. lists:usort(rebar_utils:filtermap(F, TestMods)).
  174. inject_eunit_state(State, {ok, Tests}) ->
  175. Apps = rebar_state:project_apps(State),
  176. case inject_eunit_state(State, Apps, []) of
  177. {ok, {NewState, ModdedApps}} ->
  178. test_dirs(NewState, ModdedApps, Tests);
  179. {error, _} = Error -> Error
  180. end;
  181. inject_eunit_state(_State, Error) -> Error.
  182. inject_eunit_state(State, [App|Rest], Acc) ->
  183. case inject(rebar_app_info:opts(App)) of
  184. {error, _} = Error -> Error;
  185. NewOpts ->
  186. NewApp = rebar_app_info:opts(App, NewOpts),
  187. inject_eunit_state(State, Rest, [NewApp|Acc])
  188. end;
  189. inject_eunit_state(State, [], Acc) ->
  190. case inject(rebar_state:opts(State)) of
  191. {error, _} = Error -> Error;
  192. NewOpts -> {ok, {rebar_state:opts(State, NewOpts), lists:reverse(Acc)}}
  193. end.
  194. opts(Opts, Key, Default) ->
  195. case rebar_opts:get(Opts, Key, Default) of
  196. Vs when is_list(Vs) -> Vs;
  197. Wrong ->
  198. ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, Key}}})
  199. end.
  200. inject(Opts) -> erl_opts(Opts).
  201. erl_opts(Opts) ->
  202. %% append `eunit_compile_opts` to app defined `erl_opts`
  203. ErlOpts = opts(Opts, erl_opts, []),
  204. EUnitOpts = opts(Opts, eunit_compile_opts, []),
  205. case append(EUnitOpts, ErlOpts) of
  206. {error, _} = Error -> Error;
  207. NewErlOpts -> first_files(rebar_opts:set(Opts, erl_opts, NewErlOpts))
  208. end.
  209. first_files(Opts) ->
  210. %% append `eunit_first_files` to app defined `erl_first_files`
  211. FirstFiles = opts(Opts, erl_first_files, []),
  212. EUnitFirstFiles = opts(Opts, eunit_first_files, []),
  213. case append(EUnitFirstFiles, FirstFiles) of
  214. {error, _} = Error -> Error;
  215. NewFirstFiles -> eunit_macro(rebar_opts:set(Opts, erl_first_files, NewFirstFiles))
  216. end.
  217. eunit_macro(Opts) ->
  218. ErlOpts = opts(Opts, erl_opts, []),
  219. NewOpts = safe_define_eunit_macro(ErlOpts),
  220. rebar_opts:set(Opts, erl_opts, NewOpts).
  221. safe_define_eunit_macro(Opts) ->
  222. %% defining a compile macro twice results in an exception so
  223. %% make sure 'EUNIT' is only defined once
  224. case test_defined(Opts) of
  225. true -> Opts;
  226. false -> [{d, 'EUNIT'}|Opts]
  227. end.
  228. test_defined([{d, 'EUNIT'}|_]) -> true;
  229. test_defined([{d, 'EUNIT', true}|_]) -> true;
  230. test_defined([_|Rest]) -> test_defined(Rest);
  231. test_defined([]) -> false.
  232. append({error, _} = Error, _) -> Error;
  233. append(_, {error, _} = Error) -> Error;
  234. append(A, B) -> A ++ B.
  235. test_dirs(State, Apps, []) -> rebar_state:project_apps(State, Apps);
  236. test_dirs(State, Apps, [{dir, Dir}|Rest]) ->
  237. %% insert `Dir` into an app if relative, or the base state if not
  238. %% app relative but relative to the root or not at all if outside
  239. %% project scope
  240. {NewState, NewApps} = maybe_inject_test_dir(State, [], Apps, Dir),
  241. test_dirs(NewState, NewApps, Rest);
  242. test_dirs(State, Apps, [{file, File}|Rest]) ->
  243. Dir = filename:dirname(File),
  244. {NewState, NewApps} = maybe_inject_test_dir(State, [], Apps, Dir),
  245. test_dirs(NewState, NewApps, Rest);
  246. test_dirs(State, Apps, [_|Rest]) -> test_dirs(State, Apps, Rest).
  247. maybe_inject_test_dir(State, AppAcc, [App|Rest], Dir) ->
  248. case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
  249. {ok, Path} ->
  250. Opts = inject_test_dir(rebar_app_info:opts(App), Path),
  251. {State, AppAcc ++ [rebar_app_info:opts(App, Opts)] ++ Rest};
  252. {error, badparent} ->
  253. maybe_inject_test_dir(State, AppAcc ++ [App], Rest, Dir)
  254. end;
  255. maybe_inject_test_dir(State, AppAcc, [], Dir) ->
  256. case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of
  257. {ok, Path} ->
  258. Opts = inject_test_dir(rebar_state:opts(State), Path),
  259. {rebar_state:opts(State, Opts), AppAcc};
  260. {error, badparent} ->
  261. {State, AppAcc}
  262. end.
  263. inject_test_dir(Opts, Dir) ->
  264. %% append specified test targets to app defined `extra_src_dirs`
  265. ExtraSrcDirs = rebar_dir:extra_src_dirs(Opts),
  266. rebar_opts:set(Opts, extra_src_dirs, ExtraSrcDirs ++ [Dir]).
  267. compile({error, _} = Error) -> Error;
  268. compile(State) ->
  269. case rebar_prv_compile:do(State) of
  270. %% successfully compiled apps
  271. {ok, S} ->
  272. ok = maybe_cover_compile(S),
  273. {ok, S};
  274. %% this should look like a compiler error, not an eunit error
  275. Error -> Error
  276. end.
  277. validate_tests(State, {ok, Tests}) ->
  278. gather_tests(fun(Elem) -> validate(State, Elem) end, Tests, []);
  279. validate_tests(_State, Error) -> Error.
  280. gather_tests(_F, [], Acc) -> {ok, lists:reverse(Acc)};
  281. gather_tests(F, [Test|Rest], Acc) ->
  282. case F(Test) of
  283. ok -> gather_tests(F, Rest, [Test|Acc]);
  284. %% failure mode, switch to gathering errors
  285. {error, Error} -> gather_errors(F, Rest, [Error])
  286. end.
  287. gather_errors(_F, [], Acc) -> ?PRV_ERROR({eunit_test_errors, lists:reverse(Acc)});
  288. gather_errors(F, [Test|Rest], Acc) ->
  289. case F(Test) of
  290. ok -> gather_errors(F, Rest, Acc);
  291. {error, Error} -> gather_errors(F, Rest, [Error|Acc])
  292. end.
  293. validate(State, {application, App}) ->
  294. validate_app(State, App);
  295. validate(State, {dir, Dir}) ->
  296. validate_dir(State, Dir);
  297. validate(State, {file, File}) ->
  298. validate_file(State, File);
  299. validate(State, {module, Module}) ->
  300. validate_module(State, Module);
  301. validate(State, {suite, Module}) ->
  302. validate_module(State, Module);
  303. validate(State, Module) when is_atom(Module) ->
  304. validate_module(State, Module);
  305. validate(State, Path) when is_list(Path) ->
  306. case ec_file:is_dir(Path) of
  307. true -> validate(State, {dir, Path});
  308. false -> validate(State, {file, Path})
  309. end;
  310. %% unrecognized tests should be included. if they're invalid eunit will error
  311. %% and rebar.config may contain arbitrarily complex tests that are effectively
  312. %% unvalidatable
  313. validate(_State, _Test) -> ok.
  314. validate_app(State, AppName) ->
  315. ProjectApps = rebar_state:project_apps(State),
  316. validate_app(State, ProjectApps, AppName).
  317. validate_app(_State, [], AppName) ->
  318. {error, lists:concat(["Application `", AppName, "' not found in project."])};
  319. validate_app(State, [App|Rest], AppName) ->
  320. case AppName == binary_to_atom(rebar_app_info:name(App), unicode) of
  321. true -> ok;
  322. false -> validate_app(State, Rest, AppName)
  323. end.
  324. validate_dir(State, Dir) ->
  325. case ec_file:is_dir(filename:join([rebar_state:dir(State), Dir])) of
  326. true -> ok;
  327. false -> {error, lists:concat(["Directory `", Dir, "' not found."])}
  328. end.
  329. validate_file(State, File) ->
  330. case ec_file:exists(filename:join([rebar_state:dir(State), File])) of
  331. true -> ok;
  332. false -> {error, lists:concat(["File `", File, "' not found."])}
  333. end.
  334. validate_module(_State, Module) ->
  335. case code:which(Module) of
  336. non_existing -> {error, lists:concat(["Module `", Module, "' not found in project."])};
  337. _ -> ok
  338. end.
  339. resolve_eunit_opts(State) ->
  340. {Opts, _} = rebar_state:command_parsed_args(State),
  341. EUnitOpts = rebar_state:get(State, eunit_opts, []),
  342. EUnitOpts1 = case proplists:get_value(verbose, Opts, false) of
  343. true -> set_verbose(EUnitOpts);
  344. false -> EUnitOpts
  345. end,
  346. IsVerbose = lists:member(verbose, EUnitOpts1),
  347. case proplists:get_value(eunit_formatters, EUnitOpts1, not IsVerbose) of
  348. true -> custom_eunit_formatters(EUnitOpts1);
  349. false -> EUnitOpts1
  350. end.
  351. custom_eunit_formatters(Opts) ->
  352. %% If `report` is already set then treat that like `eunit_formatters` is false
  353. case lists:keymember(report, 1, Opts) of
  354. true -> Opts;
  355. false -> [no_tty, {report, {eunit_progress, [colored, profile]}} | Opts]
  356. end.
  357. set_verbose(Opts) ->
  358. %% if `verbose` is already set don't set it again
  359. case lists:member(verbose, Opts) of
  360. true -> Opts;
  361. false -> [verbose] ++ Opts
  362. end.
  363. translate_paths(State, Tests) -> translate_paths(State, Tests, []).
  364. translate_paths(_State, [], Acc) -> lists:reverse(Acc);
  365. translate_paths(State, [{K, _} = Path|Rest], Acc) when K == file; K == dir ->
  366. Apps = rebar_state:project_apps(State),
  367. translate_paths(State, Rest, [translate(State, Apps, Path)|Acc]);
  368. translate_paths(State, [Test|Rest], Acc) ->
  369. translate_paths(State, Rest, [Test|Acc]).
  370. translate(State, [App|Rest], {dir, Dir}) ->
  371. case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
  372. {ok, Path} -> {dir, filename:join([rebar_app_info:out_dir(App), Path])};
  373. {error, badparent} -> translate(State, Rest, {dir, Dir})
  374. end;
  375. translate(State, [App|Rest], {file, FilePath}) ->
  376. Dir = filename:dirname(FilePath),
  377. File = filename:basename(FilePath),
  378. case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
  379. {ok, Path} -> {file, filename:join([rebar_app_info:out_dir(App), Path, File])};
  380. {error, badparent} -> translate(State, Rest, {file, FilePath})
  381. end;
  382. translate(State, [], {dir, Dir}) ->
  383. case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of
  384. {ok, Path} -> {dir, filename:join([rebar_dir:base_dir(State), "extras", Path])};
  385. %% not relative, leave as is
  386. {error, badparent} -> {dir, Dir}
  387. end;
  388. translate(State, [], {file, FilePath}) ->
  389. Dir = filename:dirname(FilePath),
  390. File = filename:basename(FilePath),
  391. case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(State)) of
  392. {ok, Path} -> {file, filename:join([rebar_dir:base_dir(State), "extras", Path, File])};
  393. %% not relative, leave as is
  394. {error, badparent} -> {file, FilePath}
  395. end.
  396. maybe_cover_compile(State) ->
  397. {RawOpts, _} = rebar_state:command_parsed_args(State),
  398. State1 = case proplists:get_value(cover, RawOpts, false) of
  399. true -> rebar_state:set(State, cover_enabled, true);
  400. false -> State
  401. end,
  402. rebar_prv_cover:maybe_cover_compile(State1).
  403. maybe_write_coverdata(State) ->
  404. {RawOpts, _} = rebar_state:command_parsed_args(State),
  405. State1 = case proplists:get_value(cover, RawOpts, false) of
  406. true -> rebar_state:set(State, cover_enabled, true);
  407. false -> State
  408. end,
  409. rebar_prv_cover:maybe_write_coverdata(State1, ?PROVIDER).
  410. handle_results(ok) -> ok;
  411. handle_results(error) ->
  412. {error, unknown_error};
  413. handle_results({error, Reason}) ->
  414. {error, {error_running_tests, Reason}}.
  415. eunit_opts(_State) ->
  416. [{app, undefined, "app", string, help(app)},
  417. {application, undefined, "application", string, help(app)},
  418. {cover, $c, "cover", boolean, help(cover)},
  419. {dir, $d, "dir", string, help(dir)},
  420. {file, $f, "file", string, help(file)},
  421. {module, $m, "module", string, help(module)},
  422. {suite, $s, "suite", string, help(module)},
  423. {verbose, $v, "verbose", boolean, help(verbose)},
  424. {name, undefined, "name", atom, help(name)},
  425. {sname, undefined, "sname", atom, help(sname)},
  426. {setcookie, undefined, "setcookie", atom, help(setcookie)}].
  427. help(app) -> "Comma separated list of application test suites to run. Equivalent to `[{application, App}]`.";
  428. help(cover) -> "Generate cover data. Defaults to false.";
  429. help(dir) -> "Comma separated list of dirs to load tests from. Equivalent to `[{dir, Dir}]`.";
  430. help(file) -> "Comma separated list of files to load tests from. Equivalent to `[{file, File}]`.";
  431. help(module) -> "Comma separated list of modules to load tests from. Equivalent to `[{module, Module}]`.";
  432. help(verbose) -> "Verbose output. Defaults to false.";
  433. help(name) -> "Gives a long name to the node";
  434. help(sname) -> "Gives a short name to the node";
  435. help(setcookie) -> "Sets the cookie if the node is distributed".