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.

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