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.

890 rivejä
36 KiB

10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
8 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
10 vuotta sitten
  1. %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. -module(rebar_prv_common_test).
  4. -behaviour(provider).
  5. -export([init/1,
  6. do/1,
  7. format_error/1]).
  8. -ifdef(TEST).
  9. %% exported for test purposes
  10. -export([compile/2, prepare_tests/1, translate_paths/2, maybe_write_coverdata/1]).
  11. -endif.
  12. -include("rebar.hrl").
  13. -include_lib("providers/include/providers.hrl").
  14. -define(PROVIDER, ct).
  15. %% we need to modify app_info state before compile
  16. -define(DEPS, [lock]).
  17. %% ===================================================================
  18. %% Public API
  19. %% ===================================================================
  20. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  21. init(State) ->
  22. Provider = providers:create([{name, ?PROVIDER},
  23. {module, ?MODULE},
  24. {deps, ?DEPS},
  25. {bare, true},
  26. {example, "rebar3 ct"},
  27. {short_desc, "Run Common Tests."},
  28. {desc, "Run Common Tests."},
  29. {opts, ct_opts(State)},
  30. {profiles, [test]}]),
  31. {ok, rebar_state:add_provider(State, Provider)}.
  32. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  33. do(State) ->
  34. setup_name(State),
  35. Tests = prepare_tests(State),
  36. case compile(State, Tests) of
  37. %% successfully compiled apps
  38. {ok, S} ->
  39. {RawOpts, _} = rebar_state:command_parsed_args(S),
  40. case proplists:get_value(compile_only, RawOpts, false) of
  41. true ->
  42. {ok, S};
  43. false ->
  44. do(S, Tests)
  45. end;
  46. %% this should look like a compiler error, not a ct error
  47. Error -> Error
  48. end.
  49. do(State, Tests) ->
  50. ?INFO("Running Common Test suites...", []),
  51. rebar_paths:set_paths([deps, plugins], State),
  52. %% Run ct provider prehooks
  53. Providers = rebar_state:providers(State),
  54. Cwd = rebar_dir:get_cwd(),
  55. %% Run ct provider pre hooks for all project apps and top level project hooks
  56. rebar_hooks:run_project_and_app_hooks(Cwd, pre, ?PROVIDER, Providers, State),
  57. case Tests of
  58. {ok, T} ->
  59. case run_tests(State, T) of
  60. ok ->
  61. %% Run ct provider post hooks for all project apps and top level project hooks
  62. rebar_hooks:run_project_and_app_hooks(Cwd, post, ?PROVIDER, Providers, State),
  63. rebar_paths:set_paths([plugins, deps], State),
  64. symlink_to_last_ct_logs(State),
  65. {ok, State};
  66. Error ->
  67. rebar_paths:set_paths([plugins, deps], State),
  68. symlink_to_last_ct_logs(State),
  69. Error
  70. end;
  71. Error ->
  72. rebar_paths:set_paths([plugins, deps], State),
  73. Error
  74. end.
  75. run_tests(State, Opts) ->
  76. T = translate_paths(State, Opts),
  77. Opts1 = setup_logdir(State, T),
  78. Opts2 = turn_off_auto_compile(Opts1),
  79. ?DEBUG("ct_opts ~p", [Opts2]),
  80. {RawOpts, _} = rebar_state:command_parsed_args(State),
  81. Result = case proplists:get_value(verbose, RawOpts, false) of
  82. true -> run_test_verbose(Opts2);
  83. false -> run_test_quiet(Opts2)
  84. end,
  85. ok = maybe_write_coverdata(State),
  86. Result.
  87. -spec format_error(any()) -> iolist().
  88. format_error({error, Reason}) ->
  89. io_lib:format("Error running tests:~n ~p", [Reason]);
  90. format_error({error_running_tests, Reason}) ->
  91. format_error({error, Reason});
  92. format_error({failures_running_tests, {Failed, AutoSkipped}}) ->
  93. io_lib:format("Failures occurred running tests: ~b", [Failed+AutoSkipped]);
  94. format_error({badconfig, {Msg, {Value, Key}}}) ->
  95. io_lib:format(Msg, [Value, Key]);
  96. format_error({badconfig, Msg}) ->
  97. io_lib:format(Msg, []);
  98. format_error({multiple_errors, Errors}) ->
  99. io_lib:format(lists:concat(["Error running tests:"] ++
  100. lists:map(fun(Error) -> "~n " ++ Error end, Errors)), []);
  101. format_error({error_reading_testspec, Reason}) ->
  102. io_lib:format("Error reading testspec: ~p", [Reason]).
  103. %% ===================================================================
  104. %% Internal functions
  105. %% ===================================================================
  106. %% @doc Tries to make the symlink `_build/<profile>/logs/last` to the `ct_run` directory
  107. %% of the last common test run.
  108. -spec symlink_to_last_ct_logs(rebar_state:t()) -> ok.
  109. symlink_to_last_ct_logs(State) ->
  110. LogDir = filename:join([rebar_dir:base_dir(State), "logs"]),
  111. {ok, Filenames} = file:list_dir(LogDir),
  112. CtRunDirs = lists:filter(fun(S) -> re:run(S, "ct_run", [unicode]) /= nomatch end, Filenames),
  113. NewestDir = lists:last(lists:sort(CtRunDirs)),
  114. Target = filename:join([LogDir, "last"]),
  115. Existing = filename:join([LogDir, NewestDir]),
  116. case rebar_file_utils:symlink_or_copy(Existing, Target) of
  117. ok -> ok;
  118. exists ->
  119. %% in case the symlink already exists we remove it
  120. %% and make a new updated one
  121. rebar_file_utils:rm_rf(Target),
  122. rebar_file_utils:symlink_or_copy(Existing, Target);
  123. Reason -> ?DEBUG("Warning, couldn't make a symlink to ~ts, reason: ~p.", [Target, Reason])
  124. end.
  125. setup_name(State) ->
  126. {Long, Short, Opts} = rebar_dist_utils:find_options(State),
  127. rebar_dist_utils:either(Long, Short, Opts).
  128. prepare_tests(State) ->
  129. %% command line test options
  130. CmdOpts = cmdopts(State),
  131. %% rebar.config test options
  132. CfgOpts = cfgopts(State),
  133. ProjectApps = rebar_state:project_apps(State),
  134. %% prioritize tests to run first trying any command line specified
  135. %% tests falling back to tests specified in the config file finally
  136. %% running a default set if no other tests are present
  137. select_tests(State, ProjectApps, CmdOpts, CfgOpts).
  138. cmdopts(State) ->
  139. {RawOpts, _} = rebar_state:command_parsed_args(State),
  140. %% filter out opts common_test doesn't know about and convert
  141. %% to ct acceptable forms
  142. transform_retry(transform_opts(RawOpts, []), State).
  143. transform_opts([], Acc) -> lists:reverse(Acc);
  144. transform_opts([{dir, Dirs}|Rest], Acc) ->
  145. transform_opts(Rest, [{dir, split_string(Dirs)}|Acc]);
  146. transform_opts([{suite, Suites}|Rest], Acc) ->
  147. transform_opts(Rest, [{suite, split_string(Suites)}|Acc]);
  148. transform_opts([{group, Groups}|Rest], Acc) ->
  149. transform_opts(Rest, [{group, split_string(Groups)}|Acc]);
  150. transform_opts([{testcase, Cases}|Rest], Acc) ->
  151. transform_opts(Rest, [{testcase, split_string(Cases)}|Acc]);
  152. transform_opts([{config, Configs}|Rest], Acc) ->
  153. transform_opts(Rest, [{config, split_string(Configs)}|Acc]);
  154. transform_opts([{spec, Specs}|Rest], Acc) ->
  155. transform_opts(Rest, [{spec, split_string(Specs)}|Acc]);
  156. transform_opts([{include, Includes}|Rest], Acc) ->
  157. transform_opts(Rest, [{include, split_string(Includes)}|Acc]);
  158. transform_opts([{logopts, LogOpts}|Rest], Acc) ->
  159. transform_opts(Rest, [{logopts, lists:map(fun(P) -> list_to_atom(P) end, split_string(LogOpts))}|Acc]);
  160. transform_opts([{force_stop, "true"}|Rest], Acc) ->
  161. transform_opts(Rest, [{force_stop, true}|Acc]);
  162. transform_opts([{force_stop, "false"}|Rest], Acc) ->
  163. transform_opts(Rest, [{force_stop, false}|Acc]);
  164. transform_opts([{force_stop, "skip_rest"}|Rest], Acc) ->
  165. transform_opts(Rest, [{force_stop, skip_rest}|Acc]);
  166. transform_opts([{create_priv_dir, CreatePrivDir}|Rest], Acc) ->
  167. transform_opts(Rest, [{create_priv_dir, list_to_atom(CreatePrivDir)}|Acc]);
  168. %% drop cover from opts, ct doesn't care about it
  169. transform_opts([{cover, _}|Rest], Acc) ->
  170. transform_opts(Rest, Acc);
  171. %% drop verbose from opts, ct doesn't care about it
  172. transform_opts([{verbose, _}|Rest], Acc) ->
  173. transform_opts(Rest, Acc);
  174. %% drop fail_fast from opts, ct doesn't care about it
  175. transform_opts([{fail_fast, _}|Rest], Acc) ->
  176. transform_opts(Rest, Acc);
  177. %% getopt should handle anything else
  178. transform_opts([Opt|Rest], Acc) ->
  179. transform_opts(Rest, [Opt|Acc]).
  180. %% @private only retry if specified and if no other spec
  181. %% is given.
  182. transform_retry(Opts, State) ->
  183. case proplists:get_value(retry, Opts, false) andalso
  184. not is_any_defined([spec,dir,suite], Opts) of
  185. false ->
  186. Opts;
  187. true ->
  188. Path = filename:join([rebar_dir:base_dir(State), "logs", "retry.spec"]),
  189. filelib:is_file(Path) andalso [{spec, Path}|Opts]
  190. end.
  191. split_string(String) ->
  192. rebar_string:lexemes(String, [$,]).
  193. cfgopts(State) ->
  194. case rebar_state:get(State, ct_opts, []) of
  195. Opts when is_list(Opts) ->
  196. ensure_opts(add_hooks(Opts, State), []);
  197. Wrong ->
  198. %% probably a single non list term
  199. ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, ct_opts}}})
  200. end.
  201. ensure_opts([], Acc) -> lists:reverse(Acc);
  202. ensure_opts([{cover, _}|Rest], Acc) ->
  203. ?WARN("Cover specs not supported. See http://www.rebar3.org/docs/running-tests#common-test", []),
  204. ensure_opts(Rest, Acc);
  205. ensure_opts([{auto_compile, _}|Rest], Acc) ->
  206. ?WARN("Auto compile not supported", []),
  207. ensure_opts(Rest, Acc);
  208. ensure_opts([{suite, Suite}|Rest], Acc) when is_integer(hd(Suite)) ->
  209. ensure_opts(Rest, [{suite, Suite}|Acc]);
  210. ensure_opts([{suite, Suite}|Rest], Acc) when is_atom(Suite) ->
  211. ensure_opts(Rest, [{suite, atom_to_list(Suite)}|Acc]);
  212. ensure_opts([{suite, Suites}|Rest], Acc) ->
  213. NewSuites = {suite, lists:map(fun(S) when is_atom(S) -> atom_to_list(S);
  214. (S) when is_list(S) -> S
  215. end,
  216. Suites)},
  217. ensure_opts(Rest, [NewSuites|Acc]);
  218. ensure_opts([{K, V}|Rest], Acc) ->
  219. ensure_opts(Rest, [{K, V}|Acc]);
  220. %% pass through other options, in case of things like config terms
  221. %% in `ct_opts`
  222. ensure_opts([V|Rest], Acc) ->
  223. ensure_opts(Rest, [V|Acc]).
  224. add_hooks(Opts, State) ->
  225. FailFast = case fails_fast(State) of
  226. true -> [cth_fail_fast];
  227. false -> []
  228. end,
  229. case {readable(State), lists:keyfind(ct_hooks, 1, Opts)} of
  230. {false, _} ->
  231. Opts;
  232. {Other, false} ->
  233. [{ct_hooks, [cth_readable_failonly, readable_shell_type(Other),
  234. cth_retry] ++ FailFast} | Opts];
  235. {Other, {ct_hooks, Hooks}} ->
  236. %% Make sure hooks are there once only.
  237. ReadableHooks = [cth_readable_failonly, readable_shell_type(Other),
  238. cth_retry] ++ FailFast,
  239. AllReadableHooks = [cth_readable_failonly, cth_retry, cth_fail_fast,
  240. cth_readable_shell, cth_readable_compact_shell],
  241. NewHooks = (Hooks -- AllReadableHooks) ++ ReadableHooks,
  242. lists:keyreplace(ct_hooks, 1, Opts, {ct_hooks, NewHooks})
  243. end.
  244. readable_shell_type(true) -> cth_readable_shell;
  245. readable_shell_type(compact) -> cth_readable_compact_shell.
  246. select_tests(_, _, _, {error, _} = Error) -> Error;
  247. select_tests(State, ProjectApps, CmdOpts, CfgOpts) ->
  248. %% set application env if sys_config argument is provided
  249. SysConfigs = sys_config_list(CmdOpts, CfgOpts),
  250. Configs = lists:flatmap(fun(Filename) ->
  251. rebar_file_utils:consult_config(State, Filename)
  252. end, SysConfigs),
  253. %% NB: load the applications (from user directories too) to support OTP < 17
  254. %% to our best ability.
  255. rebar_paths:set_paths([deps, plugins], State),
  256. [application:load(Application) || Config <- Configs, {Application, _} <- Config],
  257. rebar_utils:reread_config(Configs, [update_logger]),
  258. Opts = merge_opts(CmdOpts,CfgOpts),
  259. discover_tests(State, ProjectApps, Opts).
  260. %% Merge the option lists from command line and rebar.config:
  261. %%
  262. %% - Options set on the command line will replace the same options if
  263. %% set in rebar.config.
  264. %%
  265. %% - Special care is taken with options that select which tests to
  266. %% run - ANY such option on the command line will replace ALL such
  267. %% options in the config.
  268. %%
  269. %% Note that if 'spec' is given, common_test will ignore all 'dir',
  270. %% 'suite', 'group' and 'case', so there is no need to explicitly
  271. %% remove any options from the command line.
  272. %%
  273. %% All faulty combinations of options are also handled by
  274. %% common_test and are not taken into account here.
  275. merge_opts(CmdOpts0, CfgOpts0) ->
  276. TestSelectOpts = [spec,dir,suite,group,testcase],
  277. CmdOpts = lists:ukeysort(1, CmdOpts0),
  278. CfgOpts1 = lists:ukeysort(1, CfgOpts0),
  279. CfgOpts = case is_any_defined(TestSelectOpts,CmdOpts) of
  280. false ->
  281. CfgOpts1;
  282. true ->
  283. [Opt || Opt={K,_} <- CfgOpts1,
  284. not lists:member(K,TestSelectOpts)]
  285. end,
  286. lists:ukeymerge(1, CmdOpts, CfgOpts).
  287. is_any_defined([Key|Keys],Opts) ->
  288. proplists:is_defined(Key,Opts) orelse is_any_defined(Keys,Opts);
  289. is_any_defined([],_Opts) ->
  290. false.
  291. sys_config_list(CmdOpts, CfgOpts) ->
  292. CmdSysConfigs = split_string(proplists:get_value(sys_config, CmdOpts, "")),
  293. case proplists:get_value(sys_config, CfgOpts, []) of
  294. [H | _]=Configs when is_list(H) ->
  295. Configs ++ CmdSysConfigs;
  296. [] ->
  297. CmdSysConfigs;
  298. Configs ->
  299. [Configs | CmdSysConfigs]
  300. end.
  301. discover_tests(State, ProjectApps, Opts) ->
  302. case is_any_defined([spec,dir,suite],Opts) of
  303. %% no tests defined, try using `$APP/test` and `$ROOT/test` as dirs
  304. false -> {ok, [default_tests(State, ProjectApps)|Opts]};
  305. true -> {ok, Opts}
  306. end.
  307. default_tests(State, ProjectApps) ->
  308. BareTest = filename:join([rebar_state:dir(State), "test"]),
  309. F = fun(App) -> rebar_app_info:dir(App) == rebar_state:dir(State) end,
  310. AppTests = application_dirs(ProjectApps, []),
  311. case filelib:is_dir(BareTest) andalso not lists:any(F, ProjectApps) of
  312. %% `test` dir at root of project is already scheduled to be
  313. %% included or `test` does not exist
  314. false -> {dir, AppTests};
  315. %% need to add `test` dir at root to dirs to be included
  316. true -> {dir, AppTests ++ [BareTest]}
  317. end.
  318. application_dirs([], []) -> [];
  319. application_dirs([], Acc) -> lists:reverse(Acc);
  320. application_dirs([App|Rest], Acc) ->
  321. TestDir = filename:join([rebar_app_info:dir(App), "test"]),
  322. case filelib:is_dir(TestDir) of
  323. true -> application_dirs(Rest, [TestDir|Acc]);
  324. false -> application_dirs(Rest, Acc)
  325. end.
  326. compile(State, {ok, _} = Tests) ->
  327. %% inject `ct_first_files`, `ct_compile_opts` and `include` (from `ct_opts`
  328. %% and command line options) into the applications to be compiled
  329. case inject_ct_state(State, Tests) of
  330. {ok, NewState} -> do_compile(NewState);
  331. Error -> Error
  332. end;
  333. %% maybe compile even in the face of errors?
  334. compile(_State, Error) -> Error.
  335. do_compile(State) ->
  336. {ok, S} = rebar_prv_compile:do(State),
  337. ok = maybe_cover_compile(S),
  338. {ok, S}.
  339. inject_ct_state(State, {ok, Tests}) ->
  340. Apps = rebar_state:project_apps(State),
  341. case inject_ct_state(State, Tests, Apps, []) of
  342. {ok, {NewState, ModdedApps}} ->
  343. test_dirs(NewState, ModdedApps, Tests);
  344. {error, _} = Error -> Error
  345. end.
  346. inject_ct_state(State, Tests, [App|Rest], Acc) ->
  347. case inject(rebar_app_info:opts(App), State, Tests) of
  348. {error, _} = Error -> Error;
  349. NewOpts ->
  350. NewApp = rebar_app_info:opts(App, NewOpts),
  351. inject_ct_state(State, Tests, Rest, [NewApp|Acc])
  352. end;
  353. inject_ct_state(State, Tests, [], Acc) ->
  354. case inject(rebar_state:opts(State), State, Tests) of
  355. {error, _} = Error -> Error;
  356. NewOpts ->
  357. {ok, {rebar_state:opts(State, NewOpts), lists:reverse(Acc)}}
  358. end.
  359. opts(Opts, Key, Default) ->
  360. case rebar_opts:get(Opts, Key, Default) of
  361. Vs when is_list(Vs) -> Vs;
  362. Wrong ->
  363. ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, Key}}})
  364. end.
  365. inject(Opts, State, Tests) -> erl_opts(Opts, State, Tests).
  366. erl_opts(Opts, State, Tests) ->
  367. %% append `ct_compile_opts` to app defined `erl_opts`
  368. ErlOpts = opts(Opts, erl_opts, []),
  369. CTOpts = opts(Opts, ct_compile_opts, []),
  370. case add_transforms(append(CTOpts, ErlOpts), State) of
  371. {error, _} = Error -> Error;
  372. NewErlOpts -> first_files(rebar_opts:set(Opts, erl_opts, NewErlOpts), Tests)
  373. end.
  374. first_files(Opts, Tests) ->
  375. %% append `ct_first_files` to app defined `erl_first_files`
  376. FirstFiles = opts(Opts, erl_first_files, []),
  377. CTFirstFiles = opts(Opts, ct_first_files, []),
  378. case append(CTFirstFiles, FirstFiles) of
  379. {error, _} = Error -> Error;
  380. NewFirstFiles -> include_files(rebar_opts:set(Opts, erl_first_files, NewFirstFiles), Tests)
  381. end.
  382. include_files(Opts, Tests) ->
  383. %% append include dirs from command line and `ct_opts` to app defined
  384. %% `erl_opts`
  385. ErlOpts = opts(Opts, erl_opts, []),
  386. Includes = proplists:get_value(include, Tests, []),
  387. Is = lists:map(fun(I) -> {i, I} end, Includes),
  388. case append(Is, ErlOpts) of
  389. {error, _} = Error -> Error;
  390. NewIncludes -> ct_macro(rebar_opts:set(Opts, erl_opts, NewIncludes))
  391. end.
  392. ct_macro(Opts) ->
  393. ErlOpts = opts(Opts, erl_opts, []),
  394. NewOpts = safe_define_ct_macro(ErlOpts),
  395. rebar_opts:set(Opts, erl_opts, NewOpts).
  396. safe_define_ct_macro(Opts) ->
  397. %% defining a compile macro twice results in an exception so
  398. %% make sure 'COMMON_TEST' is only defined once
  399. case test_defined(Opts) of
  400. true -> Opts;
  401. false -> [{d, 'COMMON_TEST'}|Opts]
  402. end.
  403. test_defined([{d, 'COMMON_TEST'}|_]) -> true;
  404. test_defined([{d, 'COMMON_TEST', true}|_]) -> true;
  405. test_defined([_|Rest]) -> test_defined(Rest);
  406. test_defined([]) -> false.
  407. append({error, _} = Error, _) -> Error;
  408. append(_, {error, _} = Error) -> Error;
  409. append(A, B) -> A ++ B.
  410. add_transforms(CTOpts, State) when is_list(CTOpts) ->
  411. case readable(State) of
  412. false ->
  413. CTOpts;
  414. Other when Other == true; Other == compact ->
  415. ReadableTransform = [{parse_transform, cth_readable_transform}],
  416. (CTOpts -- ReadableTransform) ++ ReadableTransform
  417. end;
  418. add_transforms({error, _} = Error, _State) -> Error.
  419. readable(State) ->
  420. {RawOpts, _} = rebar_state:command_parsed_args(State),
  421. case proplists:get_value(readable, RawOpts) of
  422. "true" -> true;
  423. "false" -> false;
  424. "compact" -> compact;
  425. undefined -> rebar_state:get(State, ct_readable, compact)
  426. end.
  427. fails_fast(State) ->
  428. {RawOpts, _} = rebar_state:command_parsed_args(State),
  429. proplists:get_value(fail_fast, RawOpts) == true.
  430. test_dirs(State, Apps, Opts) ->
  431. case proplists:get_value(spec, Opts) of
  432. undefined ->
  433. case {proplists:get_value(suite, Opts), proplists:get_value(dir, Opts)} of
  434. {Suites, undefined} -> set_compile_dirs(State, Apps, {suite, Suites});
  435. {undefined, Dirs} -> set_compile_dirs(State, Apps, {dir, Dirs});
  436. {Suites, Dir} when is_integer(hd(Dir)) ->
  437. set_compile_dirs(State, Apps, join(Suites, Dir));
  438. {Suites, [Dir]} when is_integer(hd(Dir)) ->
  439. set_compile_dirs(State, Apps, join(Suites, Dir));
  440. {_Suites, _Dirs} -> {error, "Only a single directory may be specified when specifying suites"}
  441. end;
  442. Spec when is_integer(hd(Spec)) ->
  443. spec_test_dirs(State, Apps, [Spec]);
  444. Specs ->
  445. spec_test_dirs(State, Apps, Specs)
  446. end.
  447. spec_test_dirs(State, Apps, Specs0) ->
  448. case get_dirs_from_specs(Specs0) of
  449. {ok,{Specs,SuiteDirs}} ->
  450. {State1,Apps1} = set_compile_dirs1(State, Apps, {dir, SuiteDirs}),
  451. {State2,Apps2} = set_compile_dirs1(State1, Apps1, {spec, Specs}),
  452. [maybe_copy_spec(State2,Apps2,S) || S <- Specs],
  453. {ok, rebar_state:project_apps(State2, Apps2)};
  454. Error ->
  455. Error
  456. end.
  457. join(Suite, Dir) when is_integer(hd(Suite)) ->
  458. {suite, [filename:join([Dir, Suite])]};
  459. join(Suites, Dir) ->
  460. {suite, lists:map(fun(S) -> filename:join([Dir, S]) end, Suites)}.
  461. set_compile_dirs(State, Apps, What) ->
  462. {NewState,NewApps} = set_compile_dirs1(State, Apps, What),
  463. {ok, rebar_state:project_apps(NewState, NewApps)}.
  464. set_compile_dirs1(State, Apps, {dir, Dir}) when is_integer(hd(Dir)) ->
  465. %% single directory
  466. %% insert `Dir` into an app if relative, or the base state if not
  467. %% app relative but relative to the root or not at all if outside
  468. %% project scope
  469. maybe_inject_test_dir(State, [], Apps, Dir);
  470. set_compile_dirs1(State, Apps, {dir, Dirs}) ->
  471. %% multiple directories
  472. F = fun(Dir, {S, A}) -> maybe_inject_test_dir(S, [], A, Dir) end,
  473. lists:foldl(F, {State, Apps}, Dirs);
  474. set_compile_dirs1(State, Apps, {Type, Files}) when Type==spec; Type==suite ->
  475. %% specs or suites with dir component
  476. Dirs = find_file_dirs(Files),
  477. F = fun(Dir, {S, A}) -> maybe_inject_test_dir(S, [], A, Dir) end,
  478. lists:foldl(F, {State, Apps}, Dirs).
  479. find_file_dirs(Files) ->
  480. AllDirs = lists:map(fun(F) -> filename:dirname(filename:absname(F)) end, Files),
  481. %% eliminate duplicates
  482. lists:usort(AllDirs).
  483. maybe_inject_test_dir(State, AppAcc, [App|Rest], Dir) ->
  484. case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
  485. {ok, []} ->
  486. %% normal operation involves copying the entire directory a
  487. %% suite exists in but if the suite is in the app root directory
  488. %% the current compiler tries to compile all subdirs including priv
  489. %% instead copy only files ending in `.erl' and directories
  490. %% ending in `_SUITE_data' into the `_build/PROFILE/lib/APP' dir
  491. ok = copy_bare_suites(Dir, rebar_app_info:out_dir(App)),
  492. Opts = inject_test_dir(rebar_state:opts(State), rebar_app_info:out_dir(App)),
  493. {rebar_state:opts(State, Opts), AppAcc ++ [App]};
  494. {ok, Path} ->
  495. Opts = inject_test_dir(rebar_app_info:opts(App), Path),
  496. {State, AppAcc ++ [rebar_app_info:opts(App, Opts)] ++ Rest};
  497. {error, badparent} ->
  498. maybe_inject_test_dir(State, AppAcc ++ [App], Rest, Dir)
  499. end;
  500. maybe_inject_test_dir(State, AppAcc, [], Dir) ->
  501. case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of
  502. {ok, []} ->
  503. %% normal operation involves copying the entire directory a
  504. %% suite exists in but if the suite is in the root directory
  505. %% that results in a loop as we copy `_build' into itself
  506. %% instead copy only files ending in `.erl' and directories
  507. %% ending in `_SUITE_data' in the `_build/PROFILE/extras' dir
  508. ExtrasDir = filename:join([rebar_dir:base_dir(State), "extras"]),
  509. ok = copy_bare_suites(Dir, ExtrasDir),
  510. Opts = inject_test_dir(rebar_state:opts(State), ExtrasDir),
  511. {rebar_state:opts(State, Opts), AppAcc};
  512. {ok, Path} ->
  513. Opts = inject_test_dir(rebar_state:opts(State), Path),
  514. {rebar_state:opts(State, Opts), AppAcc};
  515. {error, badparent} ->
  516. {State, AppAcc}
  517. end.
  518. copy_bare_suites(From, To) ->
  519. filelib:ensure_dir(filename:join([To, "dummy.txt"])),
  520. SrcFiles = rebar_utils:find_files(From, ".*\\.[e|h]rl\$", false),
  521. DataDirs = lists:filter(fun filelib:is_dir/1,
  522. filelib:wildcard(filename:join([From, "*_SUITE_data"]))),
  523. ok = rebar_file_utils:cp_r(SrcFiles, To),
  524. rebar_file_utils:cp_r(DataDirs, To).
  525. maybe_copy_spec(State, [App|Apps], Spec) ->
  526. case rebar_file_utils:path_from_ancestor(filename:dirname(Spec), rebar_app_info:dir(App)) of
  527. {ok, []} ->
  528. ok = rebar_file_utils:cp_r([Spec],rebar_app_info:out_dir(App));
  529. {ok,_} ->
  530. ok;
  531. {error,badparent} ->
  532. maybe_copy_spec(State, Apps, Spec)
  533. end;
  534. maybe_copy_spec(State, [], Spec) ->
  535. case rebar_file_utils:path_from_ancestor(filename:dirname(Spec), rebar_state:dir(State)) of
  536. {ok, []} ->
  537. ExtrasDir = filename:join([rebar_dir:base_dir(State), "extras"]),
  538. ok = rebar_file_utils:cp_r([Spec],ExtrasDir);
  539. _R ->
  540. ok
  541. end.
  542. inject_test_dir(Opts, Dir) ->
  543. %% append specified test targets to app defined `extra_src_dirs`
  544. ExtraSrcDirs = rebar_opts:get(Opts, extra_src_dirs, []),
  545. rebar_opts:set(Opts, extra_src_dirs, ExtraSrcDirs ++ [Dir]).
  546. get_dirs_from_specs(Specs) ->
  547. case get_tests_from_specs(Specs) of
  548. {ok,Tests} ->
  549. {SpecLists,NodeRunSkipLists} = lists:unzip(Tests),
  550. SpecList = lists:append(SpecLists),
  551. NodeRunSkipList = lists:append(NodeRunSkipLists),
  552. RunList = lists:append([R || {_,R,_} <- NodeRunSkipList]),
  553. DirList = [element(1,R) || R <- RunList],
  554. {ok,{SpecList,DirList}};
  555. {error,Reason} ->
  556. {error,{?MODULE,{error_reading_testspec,Reason}}}
  557. end.
  558. get_tests_from_specs(Specs) ->
  559. _ = ct_testspec:module_info(), % make sure ct_testspec is loaded
  560. case erlang:function_exported(ct_testspec,get_tests,1) of
  561. true ->
  562. ct_testspec:get_tests(Specs);
  563. false ->
  564. case ct_testspec:collect_tests_from_file(Specs,true) of
  565. Tests when is_list(Tests) ->
  566. {ok,[{S,ct_testspec:prepare_tests(R)} || {S,R} <- Tests]};
  567. Error ->
  568. Error
  569. end
  570. end.
  571. translate_paths(State, Opts) ->
  572. case proplists:get_value(spec, Opts) of
  573. undefined ->
  574. case {proplists:get_value(suite, Opts), proplists:get_value(dir, Opts)} of
  575. {_Suites, undefined} -> translate_paths(State, suite, Opts, []);
  576. {undefined, _Dirs} -> translate_paths(State, dir, Opts, []);
  577. %% both dirs and suites are defined, only translate dir paths
  578. _ -> translate_paths(State, dir, Opts, [])
  579. end;
  580. _Specs ->
  581. translate_paths(State, spec, Opts, [])
  582. end.
  583. translate_paths(_State, _Type, [], Acc) -> lists:reverse(Acc);
  584. translate_paths(State, Type, [{Type, Val}|Rest], Acc) when is_integer(hd(Val)) ->
  585. %% single file or dir
  586. translate_paths(State, Type, [{Type, [Val]}|Rest], Acc);
  587. translate_paths(State, Type, [{Type, Files}|Rest], Acc) ->
  588. Apps = rebar_state:project_apps(State),
  589. New = {Type, lists:map(fun(File) -> translate(State, Apps, File) end, Files)},
  590. translate_paths(State, Type, Rest, [New|Acc]);
  591. translate_paths(State, Type, [Test|Rest], Acc) ->
  592. translate_paths(State, Type, Rest, [Test|Acc]).
  593. translate(State, [App|Rest], Path) ->
  594. case rebar_file_utils:path_from_ancestor(Path, rebar_app_info:dir(App)) of
  595. {ok, P} -> filename:join([rebar_app_info:out_dir(App), P]);
  596. {error, badparent} -> translate(State, Rest, Path)
  597. end;
  598. translate(State, [], Path) ->
  599. case rebar_file_utils:path_from_ancestor(Path, rebar_state:dir(State)) of
  600. {ok, P} -> filename:join([rebar_dir:base_dir(State), "extras", P]);
  601. %% not relative, leave as is
  602. {error, badparent} -> Path
  603. end.
  604. setup_logdir(State, Opts) ->
  605. Logdir = case proplists:get_value(logdir, Opts) of
  606. undefined -> filename:join([rebar_dir:base_dir(State), "logs"]);
  607. Dir -> Dir
  608. end,
  609. filelib:ensure_dir(filename:join([Logdir, "dummy.beam"])),
  610. [{logdir, Logdir}|lists:keydelete(logdir, 1, Opts)].
  611. turn_off_auto_compile(Opts) ->
  612. [{auto_compile, false}|lists:keydelete(auto_compile, 1, Opts)].
  613. run_test_verbose(Opts) -> handle_results(ct:run_test(Opts)).
  614. run_test_quiet(Opts) ->
  615. Pid = self(),
  616. Ref = erlang:make_ref(),
  617. LogDir = proplists:get_value(logdir, Opts),
  618. {_, Monitor} = erlang:spawn_monitor(fun() ->
  619. {ok, F} = file:open(filename:join([LogDir, "ct.latest.log"]),
  620. [write]),
  621. true = group_leader(F, self()),
  622. Pid ! {Ref, ct:run_test(Opts)}
  623. end),
  624. receive
  625. {Ref, Result} -> handle_quiet_results(Opts, Result);
  626. {'DOWN', Monitor, _, _, Reason} -> handle_results(?PRV_ERROR(Reason))
  627. end.
  628. handle_results(Results) when is_list(Results) ->
  629. Result = lists:foldl(fun sum_results/2, {0, 0, {0,0}}, Results),
  630. handle_results(Result);
  631. handle_results({_, Failed, {_, AutoSkipped}})
  632. when Failed > 0 orelse AutoSkipped > 0 ->
  633. ?PRV_ERROR({failures_running_tests, {Failed, AutoSkipped}});
  634. handle_results({error, Reason}) ->
  635. ?PRV_ERROR({error_running_tests, Reason});
  636. handle_results(_) ->
  637. ok.
  638. sum_results({Passed, Failed, {UserSkipped, AutoSkipped}},
  639. {Passed2, Failed2, {UserSkipped2, AutoSkipped2}}) ->
  640. {Passed+Passed2, Failed+Failed2,
  641. {UserSkipped+UserSkipped2, AutoSkipped+AutoSkipped2}};
  642. sum_results(_, {error, Reason}) ->
  643. {error, Reason};
  644. sum_results(Unknown, _) ->
  645. {error, Unknown}.
  646. handle_quiet_results(_, {error, _} = Result) ->
  647. handle_results(Result);
  648. handle_quiet_results(CTOpts, Results) when is_list(Results) ->
  649. _ = [format_result(Result) || Result <- Results],
  650. case handle_results(Results) of
  651. ?PRV_ERROR({failures_running_tests, _}) = Error ->
  652. LogDir = proplists:get_value(logdir, CTOpts),
  653. Index = filename:join([LogDir, "index.html"]),
  654. ?CONSOLE("Results written to ~p.", [Index]),
  655. Error;
  656. Other ->
  657. Other
  658. end;
  659. handle_quiet_results(CTOpts, Result) ->
  660. handle_quiet_results(CTOpts, [Result]).
  661. format_result({Passed, 0, {0, 0}}) ->
  662. ?CONSOLE("All ~p tests passed.", [Passed]);
  663. format_result({Passed, Failed, Skipped}) ->
  664. Format = [format_failed(Failed), format_skipped(Skipped),
  665. format_passed(Passed)],
  666. ?CONSOLE("~ts", [Format]);
  667. format_result(_Unknown) ->
  668. %% Happens when CT itself encounters a bug
  669. ok.
  670. format_failed(0) ->
  671. [];
  672. format_failed(Failed) ->
  673. io_lib:format("Failed ~p tests. ", [Failed]).
  674. format_passed(Passed) ->
  675. io_lib:format("Passed ~p tests. ", [Passed]).
  676. format_skipped({0, 0}) ->
  677. [];
  678. format_skipped({User, Auto}) ->
  679. io_lib:format("Skipped ~p (~p, ~p) tests. ", [User+Auto, User, Auto]).
  680. maybe_cover_compile(State) ->
  681. {RawOpts, _} = rebar_state:command_parsed_args(State),
  682. State1 = case proplists:get_value(cover, RawOpts, false) of
  683. true -> rebar_state:set(State, cover_enabled, true);
  684. false -> State
  685. end,
  686. rebar_prv_cover:maybe_cover_compile(State1).
  687. maybe_write_coverdata(State) ->
  688. {RawOpts, _} = rebar_state:command_parsed_args(State),
  689. State1 = case proplists:get_value(cover, RawOpts, false) of
  690. true -> rebar_state:set(State, cover_enabled, true);
  691. false -> State
  692. end,
  693. Name = proplists:get_value(cover_export_name, RawOpts, ?PROVIDER),
  694. rebar_prv_cover:maybe_write_coverdata(State1, Name).
  695. ct_opts(_State) ->
  696. [{dir, undefined, "dir", string, help(dir)}, %% comma-separated list
  697. {suite, undefined, "suite", string, help(suite)}, %% comma-separated list
  698. {group, undefined, "group", string, help(group)}, %% comma-separated list
  699. {testcase, undefined, "case", string, help(testcase)}, %% comma-separated list
  700. {label, undefined, "label", string, help(label)}, %% String
  701. {config, undefined, "config", string, help(config)}, %% comma-separated list
  702. {spec, undefined, "spec", string, help(spec)}, %% comma-separated list
  703. {join_specs, undefined, "join_specs", boolean, help(join_specs)},
  704. {allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool
  705. {logdir, undefined, "logdir", string, help(logdir)}, %% dir
  706. {logopts, undefined, "logopts", string, help(logopts)}, %% comma-separated list
  707. {verbosity, undefined, "verbosity", integer, help(verbosity)}, %% Integer
  708. {cover, $c, "cover", {boolean, false}, help(cover)},
  709. {cover_export_name, undefined, "cover_export_name", string, help(cover_export_name)},
  710. {repeat, undefined, "repeat", integer, help(repeat)}, %% integer
  711. {duration, undefined, "duration", string, help(duration)}, % format: HHMMSS
  712. {until, undefined, "until", string, help(until)}, %% format: YYMoMoDD[HHMMSS]
  713. {force_stop, undefined, "force_stop", string, help(force_stop)}, %% String
  714. {basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Boolean
  715. {stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% String
  716. {decrypt_key, undefined, "decrypt_key", string, help(decrypt_key)}, %% String
  717. {decrypt_file, undefined, "decrypt_file", string, help(decrypt_file)}, %% String
  718. {abort_if_missing_suites, undefined, "abort_if_missing_suites", {boolean, true}, help(abort_if_missing_suites)}, %% Boolean
  719. {multiply_timetraps, undefined, "multiply_timetraps", integer, help(multiple_timetraps)}, %% Integer
  720. {scale_timetraps, undefined, "scale_timetraps", boolean, help(scale_timetraps)},
  721. {create_priv_dir, undefined, "create_priv_dir", string, help(create_priv_dir)},
  722. {include, undefined, "include", string, help(include)},
  723. {readable, undefined, "readable", string, help(readable)},
  724. {verbose, $v, "verbose", boolean, help(verbose)},
  725. {name, undefined, "name", atom, help(name)},
  726. {sname, undefined, "sname", atom, help(sname)},
  727. {setcookie, undefined, "setcookie", atom, help(setcookie)},
  728. {sys_config, undefined, "sys_config", string, help(sys_config)}, %% comma-separated list
  729. {compile_only, undefined, "compile_only", boolean, help(compile_only)},
  730. {retry, undefined, "retry", boolean, help(retry)},
  731. {fail_fast, undefined, "fail_fast", {boolean, false}, help(fail_fast)}
  732. ].
  733. help(compile_only) ->
  734. "Compile modules in the project with the test configuration but do not run the tests";
  735. help(dir) ->
  736. "List of additional directories containing test suites";
  737. help(suite) ->
  738. "List of test suites to run";
  739. help(group) ->
  740. "List of test groups to run";
  741. help(testcase) ->
  742. "List of test cases to run";
  743. help(label) ->
  744. "Test label";
  745. help(config) ->
  746. "List of config files";
  747. help(spec) ->
  748. "List of test specifications";
  749. help(join_specs) ->
  750. "Merge all test specifications and perform a single test run";
  751. help(sys_config) ->
  752. "List of application config files";
  753. help(allow_user_terms) ->
  754. "Allow user defined config values in config files";
  755. help(logdir) ->
  756. "Log folder";
  757. help(logopts) ->
  758. "Options for common test logging";
  759. help(verbosity) ->
  760. "Verbosity";
  761. help(cover) ->
  762. "Generate cover data";
  763. help(cover_export_name) ->
  764. "Base name of the coverdata file to write";
  765. help(repeat) ->
  766. "How often to repeat tests";
  767. help(duration) ->
  768. "Max runtime (format: HHMMSS)";
  769. help(until) ->
  770. "Run until (format: HHMMSS)";
  771. help(force_stop) ->
  772. "Force stop on test timeout (true | false | skip_rest)";
  773. help(basic_html) ->
  774. "Show basic HTML";
  775. help(stylesheet) ->
  776. "CSS stylesheet to apply to html output";
  777. help(decrypt_key) ->
  778. "Path to key for decrypting config";
  779. help(decrypt_file) ->
  780. "Path to file containing key for decrypting config";
  781. help(abort_if_missing_suites) ->
  782. "Abort if suites are missing";
  783. help(multiply_timetraps) ->
  784. "Multiply timetraps";
  785. help(scale_timetraps) ->
  786. "Scale timetraps";
  787. help(create_priv_dir) ->
  788. "Create priv dir (auto_per_run | auto_per_tc | manual_per_tc)";
  789. help(include) ->
  790. "Directories containing additional include files";
  791. help(readable) ->
  792. "Shows test case names and only displays logs to shell on failures (true | compact | false)";
  793. help(verbose) ->
  794. "Verbose output";
  795. help(name) ->
  796. "Gives a long name to the node";
  797. help(sname) ->
  798. "Gives a short name to the node";
  799. help(setcookie) ->
  800. "Sets the cookie if the node is distributed";
  801. help(retry) ->
  802. "Experimental feature. If any specification for previously failing test is found, runs them.";
  803. help(fail_fast) ->
  804. "Experimental feature. If any test fails, the run is aborted. Since common test does not "
  805. "support this natively, we abort the rebar3 run on a failure. This May break CT's disk logging and "
  806. "other rebar3 features.";
  807. help(_) ->
  808. "".