Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

600 righe
23 KiB

10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
10 anni fa
  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([setup_ct/1]).
  10. -include("rebar.hrl").
  11. -include_lib("providers/include/providers.hrl").
  12. -define(PROVIDER, ct).
  13. -define(DEPS, [compile]).
  14. %% ===================================================================
  15. %% Public API
  16. %% ===================================================================
  17. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  18. init(State) ->
  19. Provider = providers:create([{name, ?PROVIDER},
  20. {module, ?MODULE},
  21. {deps, ?DEPS},
  22. {bare, true},
  23. {example, "rebar3 ct"},
  24. {short_desc, "Run Common Tests."},
  25. {desc, "Run Common Tests."},
  26. {opts, ct_opts(State)},
  27. {profiles, [test]}]),
  28. State1 = rebar_state:add_provider(State, Provider),
  29. State2 = rebar_state:add_to_profile(State1, test, test_state(State1)),
  30. {ok, State2}.
  31. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  32. do(State) ->
  33. ?INFO("Running Common Test suites...", []),
  34. rebar_utils:update_code(rebar_state:code_paths(State, all_deps)),
  35. %% Run ct provider prehooks
  36. Providers = rebar_state:providers(State),
  37. Cwd = rebar_dir:get_cwd(),
  38. rebar_hooks:run_all_hooks(Cwd, pre, ?PROVIDER, Providers, State),
  39. try run_test(State) of
  40. {ok, State1} = Result ->
  41. %% Run ct provider posthooks
  42. rebar_hooks:run_all_hooks(Cwd, post, ?PROVIDER, Providers, State1),
  43. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  44. Result;
  45. ?PRV_ERROR(_) = Error ->
  46. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  47. Error
  48. catch
  49. throw:{error, Reason} ->
  50. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
  51. ?PRV_ERROR(Reason)
  52. end.
  53. -spec format_error(any()) -> iolist().
  54. format_error({multiple_dirs_and_suites, Opts}) ->
  55. io_lib:format("Multiple dirs declared alongside suite in opts: ~p", [Opts]);
  56. format_error({bad_dir_or_suite, Opts}) ->
  57. io_lib:format("Bad value for dir or suite in opts: ~p", [Opts]);
  58. format_error({failures_running_tests, {Failed, AutoSkipped}}) ->
  59. io_lib:format("Failures occured running tests: ~b", [Failed+AutoSkipped]);
  60. format_error({error_running_tests, Reason}) ->
  61. io_lib:format("Error running tests: ~p", [Reason]);
  62. format_error(suite_at_project_root) ->
  63. io_lib:format("Test suites can't be located in project root", []);
  64. format_error({error, Reason}) ->
  65. io_lib:format("Unknown error: ~p", [Reason]).
  66. %% ===================================================================
  67. %% Internal functions
  68. %% ===================================================================
  69. run_test(State) ->
  70. case setup_ct(State) of
  71. {error, {no_tests_specified, Opts}} ->
  72. ?WARN("No tests specified in opts: ~p", [Opts]),
  73. {ok, State};
  74. Opts ->
  75. Opts1 = setup_logdir(State, Opts),
  76. ?DEBUG("common test opts: ~p", [Opts1]),
  77. run_test(State, Opts1)
  78. end.
  79. run_test(State, Opts) ->
  80. {RawOpts, _} = rebar_state:command_parsed_args(State),
  81. Result = case proplists:get_value(verbose, RawOpts, false) of
  82. true -> run_test_verbose(Opts);
  83. false -> run_test_quiet(Opts)
  84. end,
  85. ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER),
  86. case Result of
  87. ok -> {ok, State};
  88. Error -> Error
  89. end.
  90. run_test_verbose(Opts) -> handle_results(ct:run_test(Opts)).
  91. run_test_quiet(Opts) ->
  92. Pid = self(),
  93. Ref = erlang:make_ref(),
  94. LogDir = proplists:get_value(logdir, Opts),
  95. {_, Monitor} = erlang:spawn_monitor(fun() ->
  96. {ok, F} = file:open(filename:join([LogDir, "ct.latest.log"]),
  97. [write]),
  98. true = group_leader(F, self()),
  99. Pid ! {Ref, ct:run_test(Opts)}
  100. end),
  101. receive
  102. {Ref, Result} -> handle_quiet_results(Opts, Result);
  103. {'DOWN', Monitor, _, _, Reason} -> handle_results(?PRV_ERROR(Reason))
  104. end.
  105. handle_results(Results) when is_list(Results) ->
  106. Result = lists:foldl(fun sum_results/2, {0, 0, {0,0}}, Results),
  107. handle_results(Result);
  108. handle_results({_, Failed, {_, AutoSkipped}})
  109. when Failed > 0 orelse AutoSkipped > 0 ->
  110. ?PRV_ERROR({failures_running_tests, {Failed, AutoSkipped}});
  111. handle_results({error, Reason}) ->
  112. ?PRV_ERROR({error_running_tests, Reason});
  113. handle_results(_) ->
  114. ok.
  115. sum_results({Passed, Failed, {UserSkipped, AutoSkipped}},
  116. {Passed2, Failed2, {UserSkipped2, AutoSkipped2}}) ->
  117. {Passed+Passed2, Failed+Failed2,
  118. {UserSkipped+UserSkipped2, AutoSkipped+AutoSkipped2}}.
  119. handle_quiet_results(_, {error, _} = Result) ->
  120. handle_results(Result);
  121. handle_quiet_results(CTOpts, Results) when is_list(Results) ->
  122. _ = [format_result(Result) || Result <- Results],
  123. case handle_results(Results) of
  124. ?PRV_ERROR({failures_running_tests, _}) = Error ->
  125. LogDir = proplists:get_value(logdir, CTOpts),
  126. Index = filename:join([LogDir, "index.html"]),
  127. ?CONSOLE("Results written to ~p.", [Index]),
  128. Error;
  129. Other ->
  130. Other
  131. end;
  132. handle_quiet_results(CTOpts, Result) ->
  133. handle_quiet_results(CTOpts, [Result]).
  134. format_result({Passed, 0, {0, 0}}) ->
  135. ?CONSOLE("All ~p tests passed.", [Passed]);
  136. format_result({Passed, Failed, Skipped}) ->
  137. Format = [format_failed(Failed), format_skipped(Skipped),
  138. format_passed(Passed)],
  139. ?CONSOLE("~s", [Format]).
  140. format_failed(0) ->
  141. [];
  142. format_failed(Failed) ->
  143. io_lib:format("Failed ~p tests. ", [Failed]).
  144. format_passed(Passed) ->
  145. io_lib:format("Passed ~p tests. ", [Passed]).
  146. format_skipped({0, 0}) ->
  147. [];
  148. format_skipped({User, Auto}) ->
  149. io_lib:format("Skipped ~p (~p, ~p) tests. ", [User+Auto, User, Auto]).
  150. test_state(State) ->
  151. TestOpts = case rebar_state:get(State, ct_compile_opts, []) of
  152. [] -> [];
  153. Opts -> [{erl_opts, Opts}]
  154. end,
  155. [first_files(State)|TestOpts].
  156. first_files(State) ->
  157. CTFirst = rebar_state:get(State, ct_first_files, []),
  158. {erl_first_files, CTFirst}.
  159. setup_ct(State) ->
  160. Opts = resolve_ct_opts(State),
  161. Opts1 = discover_tests(State, Opts),
  162. copy_and_compile_tests(State, Opts1).
  163. resolve_ct_opts(State) ->
  164. {RawOpts, _} = rebar_state:command_parsed_args(State),
  165. CmdOpts = transform_opts(RawOpts),
  166. CfgOpts = rebar_state:get(State, ct_opts, []),
  167. Merged = lists:ukeymerge(1,
  168. lists:ukeysort(1, CmdOpts),
  169. lists:ukeysort(1, CfgOpts)),
  170. %% make sure `dir` and/or `suite` from command line go in as
  171. %% a pair overriding both `dir` and `suite` from config if
  172. %% they exist
  173. case {proplists:get_value(suite, CmdOpts), proplists:get_value(dir, CmdOpts)} of
  174. {undefined, undefined} -> Merged;
  175. {_Suite, undefined} -> lists:keydelete(dir, 1, Merged);
  176. {undefined, _Dir} -> lists:keydelete(suite, 1, Merged);
  177. {_Suite, _Dir} -> Merged
  178. end.
  179. discover_tests(State, Opts) ->
  180. case proplists:get_value(spec, Opts) of
  181. undefined -> discover_dirs_and_suites(State, Opts);
  182. TestSpec -> discover_testspec(TestSpec, Opts)
  183. end.
  184. discover_dirs_and_suites(State, Opts) ->
  185. case {proplists:get_value(dir, Opts), proplists:get_value(suite, Opts)} of
  186. %% no dirs or suites defined, try using `$APP/test` and `$ROOT/test`
  187. %% as suites
  188. {undefined, undefined} -> test_dirs(State, Opts);
  189. %% no dirs defined
  190. {undefined, _} -> Opts;
  191. %% no suites defined
  192. {_, undefined} -> Opts;
  193. %% a single dir defined, this is ok
  194. {Dirs, Suites} when is_integer(hd(Dirs)), is_list(Suites) -> Opts;
  195. %% still a single dir defined, adjust to make acceptable to ct
  196. {[Dir], Suites} when is_integer(hd(Dir)), is_list(Suites) ->
  197. [{dir, Dir}|lists:keydelete(dir, 1, Opts)];
  198. %% multiple dirs and suites, error now to simplify later steps
  199. {_, _} -> throw({error, {multiple_dirs_and_suites, Opts}})
  200. end.
  201. discover_testspec(_TestSpec, Opts) ->
  202. lists:keydelete(auto_compile, 1, Opts).
  203. copy_and_compile_tests(State, Opts) ->
  204. %% possibly enable cover
  205. {RawOpts, _} = rebar_state:command_parsed_args(State),
  206. State1 = case proplists:get_value(cover, RawOpts, false) of
  207. true -> rebar_state:set(State, cover_enabled, true);
  208. false -> State
  209. end,
  210. copy_and_compile_test_suites(State1, Opts).
  211. copy_and_compile_test_suites(State, Opts) ->
  212. case proplists:get_value(suite, Opts) of
  213. %% no suites, try dirs
  214. undefined -> copy_and_compile_test_dirs(State, Opts);
  215. Suites ->
  216. Dir = proplists:get_value(dir, Opts, undefined),
  217. AllSuites = join(Dir, Suites),
  218. Dirs = find_suite_dirs(AllSuites),
  219. lists:foreach(fun(S) ->
  220. NewPath = copy(State, S),
  221. compile_dir(State, NewPath)
  222. end, Dirs),
  223. NewSuites = lists:map(fun(S) -> retarget_path(State, S) end, AllSuites),
  224. [{suite, NewSuites}|lists:keydelete(suite, 1, Opts)]
  225. end.
  226. copy_and_compile_test_dirs(State, Opts) ->
  227. case proplists:get_value(dir, Opts) of
  228. undefined -> {error, {no_tests_specified, Opts}};
  229. %% dir is a single directory
  230. Dir when is_list(Dir), is_integer(hd(Dir)) ->
  231. NewPath = copy(State, Dir),
  232. [{dir, compile_dir(State, NewPath)}|lists:keydelete(dir, 1, Opts)];
  233. %% dir is a list of directories
  234. Dirs when is_list(Dirs) ->
  235. NewDirs = lists:map(fun(Dir) ->
  236. NewPath = copy(State, Dir),
  237. compile_dir(State, NewPath)
  238. end, Dirs),
  239. [{dir, NewDirs}|lists:keydelete(dir, 1, Opts)]
  240. end.
  241. join(undefined, Suites) -> Suites;
  242. join(Dir, Suites) when is_list(Dir), is_integer(hd(Dir)) ->
  243. lists:map(fun(S) -> filename:join([Dir, S]) end, Suites);
  244. %% multiple dirs or a bad dir argument, try to continue anyways
  245. join(_, Suites) -> Suites.
  246. find_suite_dirs(Suites) ->
  247. AllDirs = lists:map(fun(S) -> filename:dirname(filename:absname(S)) end, Suites),
  248. %% eliminate duplicates
  249. lists:usort(AllDirs).
  250. copy(State, Dir) ->
  251. From = reduce_path(Dir),
  252. case From == rebar_state:dir(State) of
  253. true -> throw({error, suite_at_project_root});
  254. false -> ok
  255. end,
  256. case retarget_path(State, From) of
  257. %% directory lies outside of our project's file structure so
  258. %% don't copy it
  259. From -> From;
  260. Target ->
  261. %% recursively delete any symlinks in the target directory
  262. %% if it exists so we don't smash files in the linked dirs
  263. case ec_file:is_dir(Target) of
  264. true -> remove_links(Target);
  265. false -> ok
  266. end,
  267. case rebar_file_utils:symlink_or_copy(From, Target) of
  268. exists -> Target;
  269. ok -> Target
  270. end
  271. end.
  272. compile_dir(State, Dir) ->
  273. NewState = replace_src_dirs(State, [filename:absname(Dir)]),
  274. ok = rebar_erlc_compiler:compile(NewState, rebar_dir:base_dir(State), Dir),
  275. ok = maybe_cover_compile(State, Dir),
  276. Dir.
  277. retarget_path(State, Path) ->
  278. ProjectApps = rebar_state:project_apps(State),
  279. retarget_path(State, Path, ProjectApps).
  280. %% not relative to any apps in project, check to see it's relative to
  281. %% project root
  282. retarget_path(State, Path, []) ->
  283. case relative_path(reduce_path(Path), rebar_state:dir(State)) of
  284. {ok, NewPath} -> filename:join([rebar_dir:base_dir(State), NewPath]);
  285. %% not relative to project root, don't modify
  286. {error, not_relative} -> Path
  287. end;
  288. %% relative to current app, retarget to the same dir relative to
  289. %% the app's out_dir
  290. retarget_path(State, Path, [App|Rest]) ->
  291. case relative_path(reduce_path(Path), rebar_app_info:dir(App)) of
  292. {ok, NewPath} -> filename:join([rebar_app_info:out_dir(App), NewPath]);
  293. {error, not_relative} -> retarget_path(State, Path, Rest)
  294. end.
  295. relative_path(Target, To) ->
  296. relative_path1(filename:split(filename:absname(Target)),
  297. filename:split(filename:absname(To))).
  298. relative_path1([Part|Target], [Part|To]) -> relative_path1(Target, To);
  299. relative_path1([], []) -> {ok, ""};
  300. relative_path1(Target, []) -> {ok, filename:join(Target)};
  301. relative_path1(_, _) -> {error, not_relative}.
  302. reduce_path(Dir) -> reduce_path([], filename:split(filename:absname(Dir))).
  303. reduce_path([], []) -> filename:nativename("/");
  304. reduce_path(Acc, []) -> filename:join(lists:reverse(Acc));
  305. reduce_path(Acc, ["."|Rest]) -> reduce_path(Acc, Rest);
  306. reduce_path([_|Acc], [".."|Rest]) -> reduce_path(Acc, Rest);
  307. reduce_path([], [".."|Rest]) -> reduce_path([], Rest);
  308. reduce_path(Acc, [Component|Rest]) -> reduce_path([Component|Acc], Rest).
  309. remove_links(Path) ->
  310. IsDir = ec_file:is_dir(Path),
  311. case ec_file:is_symlink(Path) of
  312. true when IsDir ->
  313. delete_dir_link(Path);
  314. false when IsDir ->
  315. lists:foreach(fun(ChildPath) ->
  316. remove_links(ChildPath)
  317. end, dir_entries(Path));
  318. _ -> file:delete(Path)
  319. end.
  320. delete_dir_link(Path) ->
  321. case os:type() of
  322. {unix, _} -> file:delete(Path);
  323. {win32, _} -> file:del_dir(Path)
  324. end.
  325. dir_entries(Path) ->
  326. {ok, SubDirs} = file:list_dir(Path),
  327. [filename:join(Path, SubDir) || SubDir <- SubDirs].
  328. replace_src_dirs(State, Dirs) ->
  329. %% replace any `src_dirs` with the test dirs
  330. ErlOpts = rebar_state:get(State, erl_opts, []),
  331. StrippedErlOpts = filter_src_dirs(ErlOpts),
  332. State1 = rebar_state:set(State, erl_opts, StrippedErlOpts),
  333. State2 = rebar_state:set(State1, src_dirs, []),
  334. rebar_state:set(State2, extra_src_dirs, Dirs).
  335. filter_src_dirs(ErlOpts) ->
  336. lists:filter(fun({src_dirs, _}) -> false; ({extra_src_dirs, _}) -> false; (_) -> true end, ErlOpts).
  337. test_dirs(State, Opts) ->
  338. BareTest = filename:join([rebar_state:dir(State), "test"]),
  339. F = fun(App) -> rebar_app_info:dir(App) == rebar_state:dir(State) end,
  340. TestApps = project_apps(State),
  341. case filelib:is_dir(BareTest) andalso not lists:any(F, TestApps) of
  342. %% `test` dir at root of project is already scheduled to be
  343. %% included or `test` does not exist
  344. false -> application_dirs(TestApps, Opts, []);
  345. %% need to add `test` dir at root to dirs to be included
  346. true -> application_dirs(TestApps, Opts, [BareTest])
  347. end.
  348. project_apps(State) ->
  349. filter_checkouts(rebar_state:project_apps(State)).
  350. filter_checkouts(Apps) -> filter_checkouts(Apps, []).
  351. filter_checkouts([], Acc) -> lists:reverse(Acc);
  352. filter_checkouts([App|Rest], Acc) ->
  353. case rebar_app_info:is_checkout(App) of
  354. true -> filter_checkouts(Rest, Acc);
  355. false -> filter_checkouts(Rest, [App|Acc])
  356. end.
  357. application_dirs([], Opts, []) -> Opts;
  358. application_dirs([], Opts, [Acc]) -> [{dir, Acc}|Opts];
  359. application_dirs([], Opts, Acc) -> [{dir, lists:reverse(Acc)}|Opts];
  360. application_dirs([App|Rest], Opts, Acc) ->
  361. TestDir = filename:join([rebar_app_info:dir(App), "test"]),
  362. case filelib:is_dir(TestDir) of
  363. true -> application_dirs(Rest, Opts, [TestDir|Acc]);
  364. false -> application_dirs(Rest, Opts, Acc)
  365. end.
  366. setup_logdir(State, Opts) ->
  367. Logdir = case proplists:get_value(logdir, Opts) of
  368. undefined -> filename:join([rebar_dir:base_dir(State), "logs"]);
  369. Dir -> Dir
  370. end,
  371. ensure_dir([Logdir]),
  372. [{logdir, Logdir}|lists:keydelete(logdir, 1, Opts)].
  373. ensure_dir([]) -> ok;
  374. ensure_dir([Dir|Rest]) ->
  375. case ec_file:is_dir(Dir) of
  376. true ->
  377. ok;
  378. false ->
  379. ec_file:mkdir_path(Dir)
  380. end,
  381. ensure_dir(Rest).
  382. maybe_cover_compile(State, Dir) ->
  383. {Opts, _} = rebar_state:command_parsed_args(State),
  384. State1 = case proplists:get_value(cover, Opts, false) of
  385. true -> rebar_state:set(State, cover_enabled, true);
  386. false -> State
  387. end,
  388. rebar_prv_cover:maybe_cover_compile(State1, [Dir]).
  389. ct_opts(_State) ->
  390. [{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list
  391. {suite, undefined, "suite", string, help(suite)}, %% comma-seperated list
  392. {group, undefined, "group", string, help(group)}, %% comma-seperated list
  393. {testcase, undefined, "case", string, help(testcase)}, %% comma-seperated list
  394. {spec, undefined, "spec", string, help(spec)}, %% comma-seperated list
  395. {join_specs, undefined, "join_specs", boolean, help(join_specs)}, %% Boolean
  396. {label, undefined, "label", string, help(label)}, %% String
  397. {config, undefined, "config", string, help(config)}, %% comma-seperated list
  398. {userconfig, undefined, "userconfig", string, help(userconfig)}, %% [{CallbackMod, CfgStrings}] | {CallbackMod, CfgStrings}
  399. {allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool
  400. {logdir, undefined, "logdir", string, help(logdir)}, %% dir
  401. {logopts, undefined, "logopts", string, help(logopts)}, %% enum, no_nl | no_src
  402. {verbosity, undefined, "verbosity", string, help(verbosity)}, %% Integer OR [{Category, VLevel}]
  403. {silent_connections, undefined, "silent_connections", string,
  404. help(silent_connections)}, % all OR %% comma-seperated list
  405. {stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% file
  406. {cover, $c, "cover", {boolean, false}, help(cover)},
  407. {cover_spec, undefined, "cover_spec", string, help(cover_spec)}, %% file
  408. {cover_stop, undefined, "cover_stop", boolean, help(cover_stop)}, %% Boolean
  409. {event_handler, undefined, "event_handler", string, help(event_handler)}, %% EH | [EH] WHERE EH atom() | {atom(), InitArgs} | {[atom()], InitArgs}
  410. {include, undefined, "include", string, help(include)}, % comma-seperated list
  411. {abort_if_missing_suites, undefined, "abort_if_missing_suites", {boolean, true},
  412. help(abort_if_missing_suites)}, %% boolean
  413. {multiply_timetraps, undefined, "multiply_timetraps", integer,
  414. help(multiply_timetraps)}, %% integer
  415. {scale_timetraps, undefined, "scale_timetraps", boolean, help(scale_timetraps)}, %% Boolean
  416. {create_priv_dir, undefined, "create_priv_dir", string, help(create_priv_dir)}, %% enum: auto_per_run | auto_per_tc | manual_per_tc
  417. {repeat, undefined, "repeat", integer, help(repeat)}, %% integer
  418. {duration, undefined, "duration", string, help(duration)}, % format: HHMMSS
  419. {until, undefined, "until", string, help(until)}, %% format: YYMoMoDD[HHMMSS]
  420. {force_stop, undefined, "force_stop", string, help(force_stop)}, % enum: skip_rest, bool
  421. {basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Booloean
  422. {ct_hooks, undefined, "ct_hooks", string, help(ct_hooks)}, %% List: [CTHModule | {CTHModule, CTHInitArgs}] where CTHModule is atom CthInitArgs is term
  423. {auto_compile, undefined, "auto_compile", {boolean, false}, help(auto_compile)},
  424. {verbose, $v, "verbose", boolean, help(verbose)}
  425. ].
  426. help(dir) ->
  427. "List of additional directories containing test suites";
  428. help(suite) ->
  429. "List of test suites to run";
  430. help(group) ->
  431. "List of test groups to run";
  432. help(testcase) ->
  433. "List of test cases to run";
  434. help(spec) ->
  435. "List of test specs to run";
  436. help(label) ->
  437. "Test label";
  438. help(config) ->
  439. "List of config files";
  440. help(logdir) ->
  441. "Log folder";
  442. help(verbosity) ->
  443. "Verbosity";
  444. help(stylesheet) ->
  445. "Stylesheet to use for test results";
  446. help(cover) ->
  447. "Generate cover data";
  448. help(cover_spec) ->
  449. "Cover file to use";
  450. help(event_handler) ->
  451. "Event handlers to attach to the runner";
  452. help(include) ->
  453. "Include folder";
  454. help(abort_if_missing_suites) ->
  455. "Abort if suites are missing";
  456. help(repeat) ->
  457. "How often to repeat tests";
  458. help(duration) ->
  459. "Max runtime (format: HHMMSS)";
  460. help(until) ->
  461. "Run until (format: HHMMSS)";
  462. help(force_stop) ->
  463. "Force stop after time";
  464. help(basic_html) ->
  465. "Show basic HTML";
  466. help(verbose) ->
  467. "Verbose output";
  468. help(_) ->
  469. "".
  470. transform_opts(Opts) ->
  471. transform_opts(Opts, []).
  472. transform_opts([], Acc) -> Acc;
  473. %% drop `cover` and `verbose` so they're not passed as an option to common_test
  474. transform_opts([{cover, _}|Rest], Acc) ->
  475. transform_opts(Rest, Acc);
  476. transform_opts([{verbose, _}|Rest], Acc) ->
  477. transform_opts(Rest, Acc);
  478. transform_opts([{ct_hooks, CtHooks}|Rest], Acc) ->
  479. transform_opts(Rest, [{ct_hooks, parse_term(CtHooks)}|Acc]);
  480. transform_opts([{force_stop, "skip_rest"}|Rest], Acc) ->
  481. transform_opts(Rest, [{force_stop, skip_rest}|Acc]);
  482. transform_opts([{force_stop, _}|Rest], Acc) ->
  483. transform_opts(Rest, [{force_stop, true}|Acc]);
  484. transform_opts([{repeat, Repeat}|Rest], Acc) ->
  485. transform_opts(Rest, [{repeat,
  486. ec_cnv:to_integer(Repeat)}|Acc]);
  487. transform_opts([{create_priv_dir, CreatePrivDir}|Rest], Acc) ->
  488. transform_opts(Rest, [{create_priv_dir,
  489. to_atoms(CreatePrivDir)}|Acc]);
  490. transform_opts([{multiply_timetraps, MultiplyTimetraps}|Rest], Acc) ->
  491. transform_opts(Rest, [{multiply_timetraps,
  492. ec_cnv:to_integer(MultiplyTimetraps)}|Acc]);
  493. transform_opts([{event_handler, EventHandler}|Rest], Acc) ->
  494. transform_opts(Rest, [{event_handler, parse_term(EventHandler)}|Acc]);
  495. transform_opts([{silent_connections, "all"}|Rest], Acc) ->
  496. transform_opts(Rest, [{silent_connections, all}|Acc]);
  497. transform_opts([{silent_connections, SilentConnections}|Rest], Acc) ->
  498. transform_opts(Rest, [{silent_connections,
  499. to_atoms(split_string(SilentConnections))}|Acc]);
  500. transform_opts([{verbosity, Verbosity}|Rest], Acc) ->
  501. transform_opts(Rest, [{verbosity, parse_term(Verbosity)}|Acc]);
  502. transform_opts([{logopts, LogOpts}|Rest], Acc) ->
  503. transform_opts(Rest, [{logopts, to_atoms(split_string(LogOpts))}|Acc]);
  504. transform_opts([{userconfig, UserConfig}|Rest], Acc) ->
  505. transform_opts(Rest, [{userconfig, parse_term(UserConfig)}|Acc]);
  506. transform_opts([{testcase, Testcase}|Rest], Acc) ->
  507. transform_opts(Rest, [{testcase, to_atoms(split_string(Testcase))}|Acc]);
  508. transform_opts([{group, Group}|Rest], Acc) -> % @TODO handle ""
  509. % Input is a list or an atom. It can also be a nested list.
  510. transform_opts(Rest, [{group, parse_term(Group)}|Acc]);
  511. transform_opts([{suite, Suite}|Rest], Acc) ->
  512. transform_opts(Rest, [{suite, split_string(Suite)}|Acc]);
  513. transform_opts([{Key, Val}|Rest], Acc) when is_list(Val) ->
  514. % Default to splitting a string on comma, that works fine for both flat
  515. % lists of which there are many and single-items.
  516. Val1 = case split_string(Val) of
  517. [Val2] ->
  518. Val2;
  519. Val2 ->
  520. Val2
  521. end,
  522. transform_opts(Rest, [{Key, Val1}|Acc]);
  523. transform_opts([{Key, Val}|Rest], Acc) ->
  524. transform_opts(Rest, [{Key, Val}|Acc]).
  525. to_atoms(List) ->
  526. lists:map(fun(X) -> list_to_atom(X) end, List).
  527. split_string(String) ->
  528. string:tokens(String, ",").
  529. parse_term(String) ->
  530. String1 = "[" ++ String ++ "].",
  531. {ok, Tokens, _} = erl_scan:string(String1),
  532. case erl_parse:parse_term(Tokens) of
  533. {ok, [Terms]} ->
  534. Terms;
  535. Term ->
  536. Term
  537. end.