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

600 lines
23 KiB

10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
10 년 전
  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.