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.

696 lines
28 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  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. %% exported for test purposes, consider private
  9. -export([compile/2, prepare_tests/1, translate_paths/2]).
  10. -include("rebar.hrl").
  11. -include_lib("providers/include/providers.hrl").
  12. -define(PROVIDER, ct).
  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 ct"},
  25. {short_desc, "Run Common Tests."},
  26. {desc, "Run Common Tests."},
  27. {opts, ct_opts(State)},
  28. {profiles, [test]}]),
  29. {ok, rebar_state:add_provider(State, Provider)}.
  30. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  31. do(State) ->
  32. Tests = prepare_tests(State),
  33. case compile(State, Tests) of
  34. %% successfully compiled apps
  35. {ok, S} -> do(S, Tests);
  36. %% this should look like a compiler error, not a ct error
  37. Error -> Error
  38. end.
  39. do(State, Tests) ->
  40. ?INFO("Running Common Test suites...", []),
  41. rebar_utils:update_code(rebar_state:code_paths(State, all_deps), [soft_purge]),
  42. %% Run ct provider prehooks
  43. Providers = rebar_state:providers(State),
  44. Cwd = rebar_dir:get_cwd(),
  45. %% Run ct provider pre hooks for all project apps and top level project hooks
  46. rebar_hooks:run_project_and_app_hooks(Cwd, pre, ?PROVIDER, Providers, State),
  47. case Tests of
  48. {ok, T} ->
  49. case run_tests(State, T) of
  50. ok ->
  51. %% Run ct provider post hooks for all project apps and top level project hooks
  52. rebar_hooks:run_project_and_app_hooks(Cwd, post, ?PROVIDER, Providers, State),
  53. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  54. {ok, State};
  55. Error ->
  56. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  57. Error
  58. end;
  59. Error ->
  60. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  61. Error
  62. end.
  63. run_tests(State, Opts) ->
  64. T = translate_paths(State, Opts),
  65. Opts1 = setup_logdir(State, T),
  66. Opts2 = turn_off_auto_compile(Opts1),
  67. ?DEBUG("ct_opts ~p", [Opts2]),
  68. {RawOpts, _} = rebar_state:command_parsed_args(State),
  69. Result = case proplists:get_value(verbose, RawOpts, false) of
  70. true -> run_test_verbose(Opts2);
  71. false -> run_test_quiet(Opts2)
  72. end,
  73. ok = maybe_write_coverdata(State),
  74. Result.
  75. -spec format_error(any()) -> iolist().
  76. format_error({error, Reason}) ->
  77. io_lib:format("Error running tests:~n ~p", [Reason]);
  78. format_error({error_running_tests, Reason}) ->
  79. format_error({error, Reason});
  80. format_error({failures_running_tests, {Failed, AutoSkipped}}) ->
  81. io_lib:format("Failures occured running tests: ~b", [Failed+AutoSkipped]);
  82. format_error({badconfig, {Msg, {Value, Key}}}) ->
  83. io_lib:format(Msg, [Value, Key]);
  84. format_error({badconfig, Msg}) ->
  85. io_lib:format(Msg, []);
  86. format_error({multiple_errors, Errors}) ->
  87. io_lib:format(lists:concat(["Error running tests:"] ++
  88. lists:map(fun(Error) -> "~n " ++ Error end, Errors)), []).
  89. %% ===================================================================
  90. %% Internal functions
  91. %% ===================================================================
  92. prepare_tests(State) ->
  93. %% command line test options
  94. CmdOpts = cmdopts(State),
  95. %% rebar.config test options
  96. CfgOpts = cfgopts(State),
  97. ProjectApps = rebar_state:project_apps(State),
  98. %% prioritize tests to run first trying any command line specified
  99. %% tests falling back to tests specified in the config file finally
  100. %% running a default set if no other tests are present
  101. select_tests(State, ProjectApps, CmdOpts, CfgOpts).
  102. cmdopts(State) ->
  103. {RawOpts, _} = rebar_state:command_parsed_args(State),
  104. %% filter out opts common_test doesn't know about and convert
  105. %% to ct acceptable forms
  106. transform_opts(RawOpts, []).
  107. transform_opts([], Acc) -> lists:reverse(Acc);
  108. transform_opts([{dir, Dirs}|Rest], Acc) ->
  109. transform_opts(Rest, [{dir, split_string(Dirs)}|Acc]);
  110. transform_opts([{suite, Suites}|Rest], Acc) ->
  111. transform_opts(Rest, [{suite, split_string(Suites)}|Acc]);
  112. transform_opts([{group, Groups}|Rest], Acc) ->
  113. transform_opts(Rest, [{group, split_string(Groups)}|Acc]);
  114. transform_opts([{testcase, Cases}|Rest], Acc) ->
  115. transform_opts(Rest, [{testcase, split_string(Cases)}|Acc]);
  116. transform_opts([{config, Configs}|Rest], Acc) ->
  117. transform_opts(Rest, [{config, split_string(Configs)}|Acc]);
  118. transform_opts([{include, Includes}|Rest], Acc) ->
  119. transform_opts(Rest, [{include, split_string(Includes)}|Acc]);
  120. transform_opts([{logopts, LogOpts}|Rest], Acc) ->
  121. transform_opts(Rest, [{logopts, lists:map(fun(P) -> list_to_atom(P) end, split_string(LogOpts))}|Acc]);
  122. transform_opts([{force_stop, "true"}|Rest], Acc) ->
  123. transform_opts(Rest, [{force_stop, true}|Acc]);
  124. transform_opts([{force_stop, "false"}|Rest], Acc) ->
  125. transform_opts(Rest, [{force_stop, false}|Acc]);
  126. transform_opts([{force_stop, "skip_rest"}|Rest], Acc) ->
  127. transform_opts(Rest, [{force_stop, skip_rest}|Acc]);
  128. transform_opts([{create_priv_dir, CreatePrivDir}|Rest], Acc) ->
  129. transform_opts(Rest, [{create_priv_dir, list_to_atom(CreatePrivDir)}|Acc]);
  130. %% drop cover from opts, ct doesn't care about it
  131. transform_opts([{cover, _}|Rest], Acc) ->
  132. transform_opts(Rest, Acc);
  133. %% drop verbose from opts, ct doesn't care about it
  134. transform_opts([{verbose, _}|Rest], Acc) ->
  135. transform_opts(Rest, Acc);
  136. %% getopt should handle anything else
  137. transform_opts([Opt|Rest], Acc) ->
  138. transform_opts(Rest, [Opt|Acc]).
  139. split_string(String) ->
  140. string:tokens(String, [$,]).
  141. cfgopts(State) ->
  142. case rebar_state:get(State, ct_opts, []) of
  143. Opts when is_list(Opts) ->
  144. ensure_opts(add_hooks(Opts, State), []);
  145. Wrong ->
  146. %% probably a single non list term
  147. ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, ct_opts}}})
  148. end.
  149. ensure_opts([], Acc) -> lists:reverse(Acc);
  150. ensure_opts([{test_spec, _}|Rest], Acc) ->
  151. ?WARN("Test specs not supported. See http://www.rebar3.org/docs/running-tests#common-test", []),
  152. ensure_opts(Rest, Acc);
  153. ensure_opts([{cover, _}|Rest], Acc) ->
  154. ?WARN("Cover specs not supported. See http://www.rebar3.org/docs/running-tests#common-test", []),
  155. ensure_opts(Rest, Acc);
  156. ensure_opts([{auto_compile, _}|Rest], Acc) ->
  157. ?WARN("Auto compile not supported", []),
  158. ensure_opts(Rest, Acc);
  159. ensure_opts([{suite, Suite}|Rest], Acc) when is_integer(hd(Suite)) ->
  160. ensure_opts(Rest, [{suite, Suite}|Acc]);
  161. ensure_opts([{suite, Suite}|Rest], Acc) when is_atom(Suite) ->
  162. ensure_opts(Rest, [{suite, atom_to_list(Suite)}|Acc]);
  163. ensure_opts([{suite, Suites}|Rest], Acc) ->
  164. NewSuites = {suite, lists:map(fun(S) when is_atom(S) -> atom_to_list(S);
  165. (S) when is_list(S) -> S
  166. end,
  167. Suites)},
  168. ensure_opts(Rest, [NewSuites|Acc]);
  169. ensure_opts([{K, V}|Rest], Acc) ->
  170. ensure_opts(Rest, [{K, V}|Acc]);
  171. ensure_opts([V|_Rest], _Acc) ->
  172. ?PRV_ERROR({badconfig, {"Member `~p' of option `~p' must be a 2-tuple", {V, ct_opts}}}).
  173. add_hooks(Opts, State) ->
  174. case {readable(State), lists:keyfind(ct_hooks, 1, Opts)} of
  175. {false, _} ->
  176. Opts;
  177. {true, false} ->
  178. [{ct_hooks, [cth_readable_failonly, cth_readable_shell]} | Opts];
  179. {true, {ct_hooks, Hooks}} ->
  180. %% Make sure hooks are there once only.
  181. ReadableHooks = [cth_readable_failonly, cth_readable_shell],
  182. NewHooks = (Hooks -- ReadableHooks) ++ ReadableHooks,
  183. lists:keyreplace(ct_hooks, 1, Opts, {ct_hooks, NewHooks})
  184. end.
  185. select_tests(_, _, {error, _} = Error, _) -> Error;
  186. select_tests(_, _, _, {error, _} = Error) -> Error;
  187. select_tests(State, ProjectApps, CmdOpts, CfgOpts) ->
  188. Merged = lists:ukeymerge(1,
  189. lists:ukeysort(1, CmdOpts),
  190. lists:ukeysort(1, CfgOpts)),
  191. %% make sure `dir` and/or `suite` from command line go in as
  192. %% a pair overriding both `dir` and `suite` from config if
  193. %% they exist
  194. Opts = case {proplists:get_value(suite, CmdOpts), proplists:get_value(dir, CmdOpts)} of
  195. {undefined, undefined} -> Merged;
  196. {_Suite, undefined} -> lists:keydelete(dir, 1, Merged);
  197. {undefined, _Dir} -> lists:keydelete(suite, 1, Merged);
  198. {_Suite, _Dir} -> Merged
  199. end,
  200. discover_tests(State, ProjectApps, Opts).
  201. discover_tests(State, ProjectApps, Opts) ->
  202. case {proplists:get_value(suite, Opts), proplists:get_value(dir, Opts)} of
  203. %% no dirs or suites defined, try using `$APP/test` and `$ROOT/test`
  204. %% as suites
  205. {undefined, undefined} -> {ok, [default_tests(State, ProjectApps)|Opts]};
  206. {_, _} -> {ok, Opts}
  207. end.
  208. default_tests(State, ProjectApps) ->
  209. BareTest = filename:join([rebar_state:dir(State), "test"]),
  210. F = fun(App) -> rebar_app_info:dir(App) == rebar_state:dir(State) end,
  211. AppTests = application_dirs(ProjectApps, []),
  212. case filelib:is_dir(BareTest) andalso not lists:any(F, ProjectApps) of
  213. %% `test` dir at root of project is already scheduled to be
  214. %% included or `test` does not exist
  215. false -> {dir, AppTests};
  216. %% need to add `test` dir at root to dirs to be included
  217. true -> {dir, AppTests ++ [BareTest]}
  218. end.
  219. application_dirs([], []) -> [];
  220. application_dirs([], Acc) -> lists:reverse(Acc);
  221. application_dirs([App|Rest], Acc) ->
  222. TestDir = filename:join([rebar_app_info:dir(App), "test"]),
  223. case filelib:is_dir(TestDir) of
  224. true -> application_dirs(Rest, [TestDir|Acc]);
  225. false -> application_dirs(Rest, Acc)
  226. end.
  227. compile(State, {ok, _} = Tests) ->
  228. %% inject `ct_first_files`, `ct_compile_opts` and `include` (from `ct_opts`
  229. %% and command line options) into the applications to be compiled
  230. case inject_ct_state(State, Tests) of
  231. {ok, NewState} -> do_compile(NewState);
  232. Error -> Error
  233. end;
  234. %% maybe compile even in the face of errors?
  235. compile(_State, Error) -> Error.
  236. do_compile(State) ->
  237. case rebar_prv_compile:do(State) of
  238. %% successfully compiled apps
  239. {ok, S} ->
  240. ok = maybe_cover_compile(S),
  241. {ok, S};
  242. %% this should look like a compiler error, not an eunit error
  243. Error -> Error
  244. end.
  245. inject_ct_state(State, {ok, Tests}) ->
  246. Apps = rebar_state:project_apps(State),
  247. case inject_ct_state(State, Tests, Apps, []) of
  248. {ok, {NewState, ModdedApps}} ->
  249. test_dirs(NewState, ModdedApps, Tests);
  250. {error, _} = Error -> Error
  251. end;
  252. inject_ct_state(_State, Error) -> Error.
  253. inject_ct_state(State, Tests, [App|Rest], Acc) ->
  254. case inject(rebar_app_info:opts(App), State, Tests) of
  255. {error, _} = Error -> Error;
  256. NewOpts ->
  257. NewApp = rebar_app_info:opts(App, NewOpts),
  258. inject_ct_state(State, Tests, Rest, [NewApp|Acc])
  259. end;
  260. inject_ct_state(State, Tests, [], Acc) ->
  261. case inject(rebar_state:opts(State), State, Tests) of
  262. {error, _} = Error -> Error;
  263. NewOpts ->
  264. {ok, {rebar_state:opts(State, NewOpts), lists:reverse(Acc)}}
  265. end.
  266. opts(Opts, Key, Default) ->
  267. case rebar_opts:get(Opts, Key, Default) of
  268. Vs when is_list(Vs) -> Vs;
  269. Wrong ->
  270. ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, Key}}})
  271. end.
  272. inject(Opts, State, Tests) -> erl_opts(Opts, State, Tests).
  273. erl_opts(Opts, State, Tests) ->
  274. %% append `ct_compile_opts` to app defined `erl_opts`
  275. ErlOpts = opts(Opts, erl_opts, []),
  276. CTOpts = opts(Opts, ct_compile_opts, []),
  277. case add_transforms(append(CTOpts, ErlOpts), State) of
  278. {error, _} = Error -> Error;
  279. NewErlOpts -> first_files(rebar_opts:set(Opts, erl_opts, NewErlOpts), Tests)
  280. end.
  281. first_files(Opts, Tests) ->
  282. %% append `ct_first_files` to app defined `erl_first_files`
  283. FirstFiles = opts(Opts, erl_first_files, []),
  284. CTFirstFiles = opts(Opts, ct_first_files, []),
  285. case append(CTFirstFiles, FirstFiles) of
  286. {error, _} = Error -> Error;
  287. NewFirstFiles -> include_files(rebar_opts:set(Opts, erl_first_files, NewFirstFiles), Tests)
  288. end.
  289. include_files(Opts, Tests) ->
  290. %% append include dirs from command line and `ct_opts` to app defined
  291. %% `erl_opts`
  292. ErlOpts = opts(Opts, erl_opts, []),
  293. Includes = proplists:get_value(include, Tests, []),
  294. Is = lists:map(fun(I) -> {i, I} end, Includes),
  295. case append(Is, ErlOpts) of
  296. {error, _} = Error -> Error;
  297. NewIncludes -> ct_macro(rebar_opts:set(Opts, erl_opts, NewIncludes))
  298. end.
  299. ct_macro(Opts) ->
  300. ErlOpts = opts(Opts, erl_opts, []),
  301. NewOpts = safe_define_ct_macro(ErlOpts),
  302. rebar_opts:set(Opts, erl_opts, NewOpts).
  303. safe_define_ct_macro(Opts) ->
  304. %% defining a compile macro twice results in an exception so
  305. %% make sure 'COMMON_TEST' is only defined once
  306. case test_defined(Opts) of
  307. true -> Opts;
  308. false -> [{d, 'COMMON_TEST'}|Opts]
  309. end.
  310. test_defined([{d, 'COMMON_TEST'}|_]) -> true;
  311. test_defined([{d, 'COMMON_TEST', true}|_]) -> true;
  312. test_defined([_|Rest]) -> test_defined(Rest);
  313. test_defined([]) -> false.
  314. append({error, _} = Error, _) -> Error;
  315. append(_, {error, _} = Error) -> Error;
  316. append(A, B) -> A ++ B.
  317. add_transforms(CTOpts, State) when is_list(CTOpts) ->
  318. case readable(State) of
  319. true ->
  320. ReadableTransform = [{parse_transform, cth_readable_transform}],
  321. (CTOpts -- ReadableTransform) ++ ReadableTransform;
  322. false ->
  323. CTOpts
  324. end;
  325. add_transforms({error, _} = Error, _State) -> Error.
  326. readable(State) ->
  327. {RawOpts, _} = rebar_state:command_parsed_args(State),
  328. case proplists:get_value(readable, RawOpts) of
  329. true -> true;
  330. false -> false;
  331. undefined -> rebar_state:get(State, ct_readable, true)
  332. end.
  333. test_dirs(State, Apps, Opts) ->
  334. case {proplists:get_value(suite, Opts), proplists:get_value(dir, Opts)} of
  335. {Suites, undefined} -> set_compile_dirs(State, Apps, {suite, Suites});
  336. {undefined, Dirs} -> set_compile_dirs(State, Apps, {dir, Dirs});
  337. {Suites, Dir} when is_integer(hd(Dir)) ->
  338. set_compile_dirs(State, Apps, join(Suites, Dir));
  339. {Suites, [Dir]} when is_integer(hd(Dir)) ->
  340. set_compile_dirs(State, Apps, join(Suites, Dir));
  341. {_Suites, _Dirs} -> {error, "Only a single directory may be specified when specifying suites"}
  342. end.
  343. join(Suite, Dir) when is_integer(hd(Suite)) ->
  344. {suite, [filename:join([Dir, Suite])]};
  345. join(Suites, Dir) ->
  346. {suite, lists:map(fun(S) -> filename:join([Dir, S]) end, Suites)}.
  347. set_compile_dirs(State, Apps, {dir, Dir}) when is_integer(hd(Dir)) ->
  348. %% single directory
  349. %% insert `Dir` into an app if relative, or the base state if not
  350. %% app relative but relative to the root or not at all if outside
  351. %% project scope
  352. {NewState, NewApps} = maybe_inject_test_dir(State, [], Apps, Dir),
  353. {ok, rebar_state:project_apps(NewState, NewApps)};
  354. set_compile_dirs(State, Apps, {dir, Dirs}) ->
  355. %% multiple directories
  356. F = fun(Dir, {S, A}) -> maybe_inject_test_dir(S, [], A, Dir) end,
  357. {NewState, NewApps} = lists:foldl(F, {State, Apps}, Dirs),
  358. {ok, rebar_state:project_apps(NewState, NewApps)};
  359. set_compile_dirs(State, Apps, {suite, Suites}) ->
  360. %% suites with dir component
  361. Dirs = find_suite_dirs(Suites),
  362. F = fun(Dir, {S, A}) -> maybe_inject_test_dir(S, [], A, Dir) end,
  363. {NewState, NewApps} = lists:foldl(F, {State, Apps}, Dirs),
  364. {ok, rebar_state:project_apps(NewState, NewApps)}.
  365. find_suite_dirs(Suites) ->
  366. AllDirs = lists:map(fun(S) -> filename:dirname(filename:absname(S)) end, Suites),
  367. %% eliminate duplicates
  368. lists:usort(AllDirs).
  369. maybe_inject_test_dir(State, AppAcc, [App|Rest], Dir) ->
  370. case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
  371. {ok, []} ->
  372. %% normal operation involves copying the entire directory a
  373. %% suite exists in but if the suite is in the app root directory
  374. %% the current compiler tries to compile all subdirs including priv
  375. %% instead copy only files ending in `.erl' and directories
  376. %% ending in `_SUITE_data' into the `_build/PROFILE/lib/APP' dir
  377. ok = copy_bare_suites(Dir, rebar_app_info:out_dir(App)),
  378. Opts = inject_test_dir(rebar_state:opts(State), rebar_app_info:out_dir(App)),
  379. {rebar_state:opts(State, Opts), AppAcc ++ [App]};
  380. {ok, Path} ->
  381. Opts = inject_test_dir(rebar_app_info:opts(App), Path),
  382. {State, AppAcc ++ [rebar_app_info:opts(App, Opts)] ++ Rest};
  383. {error, badparent} ->
  384. maybe_inject_test_dir(State, AppAcc ++ [App], Rest, Dir)
  385. end;
  386. maybe_inject_test_dir(State, AppAcc, [], Dir) ->
  387. case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of
  388. {ok, []} ->
  389. %% normal operation involves copying the entire directory a
  390. %% suite exists in but if the suite is in the root directory
  391. %% that results in a loop as we copy `_build' into itself
  392. %% instead copy only files ending in `.erl' and directories
  393. %% ending in `_SUITE_data' in the `_build/PROFILE/extras' dir
  394. ExtrasDir = filename:join([rebar_dir:base_dir(State), "extras"]),
  395. ok = copy_bare_suites(Dir, ExtrasDir),
  396. Opts = inject_test_dir(rebar_state:opts(State), ExtrasDir),
  397. {rebar_state:opts(State, Opts), AppAcc};
  398. {ok, Path} ->
  399. Opts = inject_test_dir(rebar_state:opts(State), Path),
  400. {rebar_state:opts(State, Opts), AppAcc};
  401. {error, badparent} ->
  402. {State, AppAcc}
  403. end.
  404. copy_bare_suites(From, To) ->
  405. filelib:ensure_dir(filename:join([To, "dummy.txt"])),
  406. SrcFiles = rebar_utils:find_files(From, ".*\\.[e|h]rl\$", false),
  407. DataDirs = lists:filter(fun filelib:is_dir/1,
  408. filelib:wildcard(filename:join([From, "*_SUITE_data"]))),
  409. ok = rebar_file_utils:cp_r(SrcFiles, To),
  410. rebar_file_utils:cp_r(DataDirs, To).
  411. inject_test_dir(Opts, Dir) ->
  412. %% append specified test targets to app defined `extra_src_dirs`
  413. ExtraSrcDirs = rebar_opts:get(Opts, extra_src_dirs, []),
  414. rebar_opts:set(Opts, extra_src_dirs, ExtraSrcDirs ++ [Dir]).
  415. translate_paths(State, Opts) ->
  416. case {proplists:get_value(suite, Opts), proplists:get_value(dir, Opts)} of
  417. {_Suites, undefined} -> translate_suites(State, Opts, []);
  418. {undefined, _Dirs} -> translate_dirs(State, Opts, []);
  419. %% both dirs and suites are defined, only translate dir paths
  420. _ -> translate_dirs(State, Opts, [])
  421. end.
  422. translate_dirs(_State, [], Acc) -> lists:reverse(Acc);
  423. translate_dirs(State, [{dir, Dir}|Rest], Acc) when is_integer(hd(Dir)) ->
  424. %% single dir
  425. Apps = rebar_state:project_apps(State),
  426. translate_dirs(State, Rest, [{dir, translate(State, Apps, Dir)}|Acc]);
  427. translate_dirs(State, [{dir, Dirs}|Rest], Acc) ->
  428. %% multiple dirs
  429. Apps = rebar_state:project_apps(State),
  430. NewDirs = {dir, lists:map(fun(Dir) -> translate(State, Apps, Dir) end, Dirs)},
  431. translate_dirs(State, Rest, [NewDirs|Acc]);
  432. translate_dirs(State, [Test|Rest], Acc) ->
  433. translate_dirs(State, Rest, [Test|Acc]).
  434. translate_suites(_State, [], Acc) -> lists:reverse(Acc);
  435. translate_suites(State, [{suite, Suite}|Rest], Acc) when is_integer(hd(Suite)) ->
  436. %% single suite
  437. Apps = rebar_state:project_apps(State),
  438. translate_suites(State, Rest, [{suite, translate_suite(State, Apps, Suite)}|Acc]);
  439. translate_suites(State, [{suite, Suites}|Rest], Acc) ->
  440. %% multiple suites
  441. Apps = rebar_state:project_apps(State),
  442. NewSuites = {suite, lists:map(fun(Suite) -> translate_suite(State, Apps, Suite) end, Suites)},
  443. translate_suites(State, Rest, [NewSuites|Acc]);
  444. translate_suites(State, [Test|Rest], Acc) ->
  445. translate_suites(State, Rest, [Test|Acc]).
  446. translate_suite(State, Apps, Suite) ->
  447. Dirname = filename:dirname(Suite),
  448. Basename = filename:basename(Suite),
  449. case Dirname of
  450. "." -> Suite;
  451. _ -> filename:join([translate(State, Apps, Dirname), Basename])
  452. end.
  453. translate(State, [App|Rest], Path) ->
  454. case rebar_file_utils:path_from_ancestor(Path, rebar_app_info:dir(App)) of
  455. {ok, P} -> filename:join([rebar_app_info:out_dir(App), P]);
  456. {error, badparent} -> translate(State, Rest, Path)
  457. end;
  458. translate(State, [], Path) ->
  459. case rebar_file_utils:path_from_ancestor(Path, rebar_state:dir(State)) of
  460. {ok, P} -> filename:join([rebar_dir:base_dir(State), "extras", P]);
  461. %% not relative, leave as is
  462. {error, badparent} -> Path
  463. end.
  464. setup_logdir(State, Opts) ->
  465. Logdir = case proplists:get_value(logdir, Opts) of
  466. undefined -> filename:join([rebar_dir:base_dir(State), "logs"]);
  467. Dir -> Dir
  468. end,
  469. filelib:ensure_dir(filename:join([Logdir, "dummy.beam"])),
  470. [{logdir, Logdir}|lists:keydelete(logdir, 1, Opts)].
  471. turn_off_auto_compile(Opts) ->
  472. [{auto_compile, false}|lists:keydelete(auto_compile, 1, Opts)].
  473. run_test_verbose(Opts) -> handle_results(ct:run_test(Opts)).
  474. run_test_quiet(Opts) ->
  475. Pid = self(),
  476. Ref = erlang:make_ref(),
  477. LogDir = proplists:get_value(logdir, Opts),
  478. {_, Monitor} = erlang:spawn_monitor(fun() ->
  479. {ok, F} = file:open(filename:join([LogDir, "ct.latest.log"]),
  480. [write]),
  481. true = group_leader(F, self()),
  482. Pid ! {Ref, ct:run_test(Opts)}
  483. end),
  484. receive
  485. {Ref, Result} -> handle_quiet_results(Opts, Result);
  486. {'DOWN', Monitor, _, _, Reason} -> handle_results(?PRV_ERROR(Reason))
  487. end.
  488. handle_results(Results) when is_list(Results) ->
  489. Result = lists:foldl(fun sum_results/2, {0, 0, {0,0}}, Results),
  490. handle_results(Result);
  491. handle_results({_, Failed, {_, AutoSkipped}})
  492. when Failed > 0 orelse AutoSkipped > 0 ->
  493. ?PRV_ERROR({failures_running_tests, {Failed, AutoSkipped}});
  494. handle_results({error, Reason}) ->
  495. ?PRV_ERROR({error_running_tests, Reason});
  496. handle_results(_) ->
  497. ok.
  498. sum_results({Passed, Failed, {UserSkipped, AutoSkipped}},
  499. {Passed2, Failed2, {UserSkipped2, AutoSkipped2}}) ->
  500. {Passed+Passed2, Failed+Failed2,
  501. {UserSkipped+UserSkipped2, AutoSkipped+AutoSkipped2}}.
  502. handle_quiet_results(_, {error, _} = Result) ->
  503. handle_results(Result);
  504. handle_quiet_results(CTOpts, Results) when is_list(Results) ->
  505. _ = [format_result(Result) || Result <- Results],
  506. case handle_results(Results) of
  507. ?PRV_ERROR({failures_running_tests, _}) = Error ->
  508. LogDir = proplists:get_value(logdir, CTOpts),
  509. Index = filename:join([LogDir, "index.html"]),
  510. ?CONSOLE("Results written to ~p.", [Index]),
  511. Error;
  512. Other ->
  513. Other
  514. end;
  515. handle_quiet_results(CTOpts, Result) ->
  516. handle_quiet_results(CTOpts, [Result]).
  517. format_result({Passed, 0, {0, 0}}) ->
  518. ?CONSOLE("All ~p tests passed.", [Passed]);
  519. format_result({Passed, Failed, Skipped}) ->
  520. Format = [format_failed(Failed), format_skipped(Skipped),
  521. format_passed(Passed)],
  522. ?CONSOLE("~s", [Format]).
  523. format_failed(0) ->
  524. [];
  525. format_failed(Failed) ->
  526. io_lib:format("Failed ~p tests. ", [Failed]).
  527. format_passed(Passed) ->
  528. io_lib:format("Passed ~p tests. ", [Passed]).
  529. format_skipped({0, 0}) ->
  530. [];
  531. format_skipped({User, Auto}) ->
  532. io_lib:format("Skipped ~p (~p, ~p) tests. ", [User+Auto, User, Auto]).
  533. maybe_cover_compile(State) ->
  534. {RawOpts, _} = rebar_state:command_parsed_args(State),
  535. State1 = case proplists:get_value(cover, RawOpts, false) of
  536. true -> rebar_state:set(State, cover_enabled, true);
  537. false -> State
  538. end,
  539. rebar_prv_cover:maybe_cover_compile(State1).
  540. maybe_write_coverdata(State) ->
  541. {RawOpts, _} = rebar_state:command_parsed_args(State),
  542. State1 = case proplists:get_value(cover, RawOpts, false) of
  543. true -> rebar_state:set(State, cover_enabled, true);
  544. false -> State
  545. end,
  546. rebar_prv_cover:maybe_write_coverdata(State1, ?PROVIDER).
  547. ct_opts(_State) ->
  548. [{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list
  549. {suite, undefined, "suite", string, help(suite)}, %% comma-seperated list
  550. {group, undefined, "group", string, help(group)}, %% comma-seperated list
  551. {testcase, undefined, "case", string, help(testcase)}, %% comma-seperated list
  552. {label, undefined, "label", string, help(label)}, %% String
  553. {config, undefined, "config", string, help(config)}, %% comma-seperated list
  554. {allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool
  555. {logdir, undefined, "logdir", string, help(logdir)}, %% dir
  556. {logopts, undefined, "logopts", string, help(logopts)}, %% comma seperated list
  557. {verbosity, undefined, "verbosity", integer, help(verbosity)}, %% Integer
  558. {cover, $c, "cover", {boolean, false}, help(cover)},
  559. {repeat, undefined, "repeat", integer, help(repeat)}, %% integer
  560. {duration, undefined, "duration", string, help(duration)}, % format: HHMMSS
  561. {until, undefined, "until", string, help(until)}, %% format: YYMoMoDD[HHMMSS]
  562. {force_stop, undefined, "force_stop", string, help(force_stop)}, %% String
  563. {basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Boolean
  564. {stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% String
  565. {decrypt_key, undefined, "decrypt_key", string, help(decrypt_key)}, %% String
  566. {decrypt_file, undefined, "decrypt_file", string, help(decrypt_file)}, %% String
  567. {abort_if_missing_suites, undefined, "abort_if_missing_suites", {boolean, true}, help(abort_if_missing_suites)}, %% Boolean
  568. {multiply_timetraps, undefined, "multiply_timetraps", integer, help(multiple_timetraps)}, %% Integer
  569. {scale_timetraps, undefined, "scale_timetraps", boolean, help(scale_timetraps)},
  570. {create_priv_dir, undefined, "create_priv_dir", string, help(create_priv_dir)},
  571. {include, undefined, "include", string, help(include)},
  572. {readable, undefined, "readable", boolean, help(readable)},
  573. {verbose, $v, "verbose", boolean, help(verbose)}
  574. ].
  575. help(dir) ->
  576. "List of additional directories containing test suites";
  577. help(suite) ->
  578. "List of test suites to run";
  579. help(group) ->
  580. "List of test groups to run";
  581. help(testcase) ->
  582. "List of test cases to run";
  583. help(label) ->
  584. "Test label";
  585. help(config) ->
  586. "List of config files";
  587. help(allow_user_terms) ->
  588. "Allow user defined config values in config files";
  589. help(logdir) ->
  590. "Log folder";
  591. help(logopts) ->
  592. "Options for common test logging";
  593. help(verbosity) ->
  594. "Verbosity";
  595. help(cover) ->
  596. "Generate cover data";
  597. help(repeat) ->
  598. "How often to repeat tests";
  599. help(duration) ->
  600. "Max runtime (format: HHMMSS)";
  601. help(until) ->
  602. "Run until (format: HHMMSS)";
  603. help(force_stop) ->
  604. "Force stop on test timeout (true | false | skip_rest)";
  605. help(basic_html) ->
  606. "Show basic HTML";
  607. help(stylesheet) ->
  608. "CSS stylesheet to apply to html output";
  609. help(decrypt_key) ->
  610. "Path to key for decrypting config";
  611. help(decrypt_file) ->
  612. "Path to file containing key for decrypting config";
  613. help(abort_if_missing_suites) ->
  614. "Abort if suites are missing";
  615. help(multiply_timetraps) ->
  616. "Multiply timetraps";
  617. help(scale_timetraps) ->
  618. "Scale timetraps";
  619. help(create_priv_dir) ->
  620. "Create priv dir (auto_per_run | auto_per_tc | manual_per_tc)";
  621. help(include) ->
  622. "Directories containing additional include files";
  623. help(readable) ->
  624. "Shows test case names and only displays logs to shell on failures";
  625. help(verbose) ->
  626. "Verbose output";
  627. help(_) ->
  628. "".