您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

598 行
23 KiB

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