Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

588 Zeilen
23 KiB

vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
vor 10 Jahren
  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, false},
  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. code:add_pathsa(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(State1, 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, S, 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, Dir, 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, Dir, 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, OutDir) ->
  269. NewState = replace_src_dirs(State, [Dir]),
  270. ok = rebar_erlc_compiler:compile(NewState, rebar_dir:base_dir(State), OutDir),
  271. ok = maybe_cover_compile(State, Dir),
  272. OutDir.
  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. case ec_file:is_dir(Path) of
  307. false -> ok;
  308. true -> remove_links1(Path)
  309. end.
  310. remove_links1(Path) ->
  311. case ec_file:is_symlink(Path) of
  312. true ->
  313. file:delete(Path);
  314. false ->
  315. lists:foreach(fun(ChildPath) ->
  316. remove_links(ChildPath)
  317. end, sub_dirs(Path))
  318. end.
  319. sub_dirs(Path) ->
  320. {ok, SubDirs} = file:list_dir(Path),
  321. [filename:join(Path, SubDir) || SubDir <- SubDirs].
  322. replace_src_dirs(State, Dirs) ->
  323. %% replace any `src_dirs` with the test dirs
  324. ErlOpts = rebar_state:get(State, erl_opts, []),
  325. StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts),
  326. rebar_state:set(State, erl_opts, [{src_dirs, Dirs}|StrippedOpts]).
  327. test_dirs(State, Opts) ->
  328. BareTest = filename:join([rebar_state:dir(State), "test"]),
  329. F = fun(App) -> rebar_app_info:dir(App) == rebar_state:dir(State) end,
  330. TestApps = project_apps(State),
  331. case filelib:is_dir(BareTest) andalso not lists:any(F, TestApps) of
  332. %% `test` dir at root of project is already scheduled to be
  333. %% included or `test` does not exist
  334. false -> application_dirs(TestApps, Opts, []);
  335. %% need to add `test` dir at root to dirs to be included
  336. true -> application_dirs(TestApps, Opts, [BareTest])
  337. end.
  338. project_apps(State) ->
  339. filter_checkouts(rebar_state:project_apps(State)).
  340. filter_checkouts(Apps) -> filter_checkouts(Apps, []).
  341. filter_checkouts([], Acc) -> lists:reverse(Acc);
  342. filter_checkouts([App|Rest], Acc) ->
  343. case rebar_app_info:is_checkout(App) of
  344. true -> filter_checkouts(Rest, Acc);
  345. false -> filter_checkouts(Rest, [App|Acc])
  346. end.
  347. application_dirs([], Opts, []) -> Opts;
  348. application_dirs([], Opts, [Acc]) -> [{dir, Acc}|Opts];
  349. application_dirs([], Opts, Acc) -> [{dir, lists:reverse(Acc)}|Opts];
  350. application_dirs([App|Rest], Opts, Acc) ->
  351. TestDir = filename:join([rebar_app_info:dir(App), "test"]),
  352. case filelib:is_dir(TestDir) of
  353. true -> application_dirs(Rest, Opts, [TestDir|Acc]);
  354. false -> application_dirs(Rest, Opts, Acc)
  355. end.
  356. setup_logdir(State, Opts) ->
  357. Logdir = case proplists:get_value(logdir, Opts) of
  358. undefined -> filename:join([rebar_dir:base_dir(State), "logs"]);
  359. Dir -> Dir
  360. end,
  361. ensure_dir([Logdir]),
  362. [{logdir, Logdir}|lists:keydelete(logdir, 1, Opts)].
  363. ensure_dir([]) -> ok;
  364. ensure_dir([Dir|Rest]) ->
  365. case ec_file:is_dir(Dir) of
  366. true ->
  367. ok;
  368. false ->
  369. ec_file:mkdir_path(Dir)
  370. end,
  371. ensure_dir(Rest).
  372. maybe_cover_compile(State, Dir) ->
  373. {Opts, _} = rebar_state:command_parsed_args(State),
  374. State1 = case proplists:get_value(cover, Opts, false) of
  375. true -> rebar_state:set(State, cover_enabled, true);
  376. false -> State
  377. end,
  378. rebar_prv_cover:maybe_cover_compile(State1, [Dir]).
  379. ct_opts(_State) ->
  380. [{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list
  381. {suite, undefined, "suite", string, help(suite)}, %% comma-seperated list
  382. {group, undefined, "group", string, help(group)}, %% comma-seperated list
  383. {testcase, undefined, "case", string, help(testcase)}, %% comma-seperated list
  384. {spec, undefined, "spec", string, help(spec)}, %% comma-seperated list
  385. {join_specs, undefined, "join_specs", boolean, help(join_specs)}, %% Boolean
  386. {label, undefined, "label", string, help(label)}, %% String
  387. {config, undefined, "config", string, help(config)}, %% comma-seperated list
  388. {userconfig, undefined, "userconfig", string, help(userconfig)}, %% [{CallbackMod, CfgStrings}] | {CallbackMod, CfgStrings}
  389. {allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool
  390. {logdir, undefined, "logdir", string, help(logdir)}, %% dir
  391. {logopts, undefined, "logopts", string, help(logopts)}, %% enum, no_nl | no_src
  392. {verbosity, undefined, "verbosity", string, help(verbosity)}, %% Integer OR [{Category, VLevel}]
  393. {silent_connections, undefined, "silent_connections", string,
  394. help(silent_connections)}, % all OR %% comma-seperated list
  395. {stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% file
  396. {cover, $c, "cover", {boolean, false}, help(cover)},
  397. {cover_spec, undefined, "cover_spec", string, help(cover_spec)}, %% file
  398. {cover_stop, undefined, "cover_stop", boolean, help(cover_stop)}, %% Boolean
  399. {event_handler, undefined, "event_handler", string, help(event_handler)}, %% EH | [EH] WHERE EH atom() | {atom(), InitArgs} | {[atom()], InitArgs}
  400. {include, undefined, "include", string, help(include)}, % comma-seperated list
  401. {abort_if_missing_suites, undefined, "abort_if_missing_suites", {boolean, true},
  402. help(abort_if_missing_suites)}, %% boolean
  403. {multiply_timetraps, undefined, "multiply_timetraps", integer,
  404. help(multiply_timetraps)}, %% integer
  405. {scale_timetraps, undefined, "scale_timetraps", boolean, help(scale_timetraps)}, %% Boolean
  406. {create_priv_dir, undefined, "create_priv_dir", string, help(create_priv_dir)}, %% enum: auto_per_run | auto_per_tc | manual_per_tc
  407. {repeat, undefined, "repeat", integer, help(repeat)}, %% integer
  408. {duration, undefined, "duration", string, help(duration)}, % format: HHMMSS
  409. {until, undefined, "until", string, help(until)}, %% format: YYMoMoDD[HHMMSS]
  410. {force_stop, undefined, "force_stop", string, help(force_stop)}, % enum: skip_rest, bool
  411. {basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Booloean
  412. {ct_hooks, undefined, "ct_hooks", string, help(ct_hooks)}, %% List: [CTHModule | {CTHModule, CTHInitArgs}] where CTHModule is atom CthInitArgs is term
  413. {auto_compile, undefined, "auto_compile", {boolean, false}, help(auto_compile)},
  414. {verbose, $v, "verbose", boolean, help(verbose)}
  415. ].
  416. help(dir) ->
  417. "List of additional directories containing test suites";
  418. help(suite) ->
  419. "List of test suites to run";
  420. help(group) ->
  421. "List of test groups to run";
  422. help(testcase) ->
  423. "List of test cases to run";
  424. help(spec) ->
  425. "List of test specs to run";
  426. help(label) ->
  427. "Test label";
  428. help(config) ->
  429. "List of config files";
  430. help(logdir) ->
  431. "Log folder";
  432. help(verbosity) ->
  433. "Verbosity";
  434. help(stylesheet) ->
  435. "Stylesheet to use for test results";
  436. help(cover) ->
  437. "Generate cover data";
  438. help(cover_spec) ->
  439. "Cover file to use";
  440. help(event_handler) ->
  441. "Event handlers to attach to the runner";
  442. help(include) ->
  443. "Include folder";
  444. help(abort_if_missing_suites) ->
  445. "Abort if suites are missing";
  446. help(repeat) ->
  447. "How often to repeat tests";
  448. help(duration) ->
  449. "Max runtime (format: HHMMSS)";
  450. help(until) ->
  451. "Run until (format: HHMMSS)";
  452. help(force_stop) ->
  453. "Force stop after time";
  454. help(basic_html) ->
  455. "Show basic HTML";
  456. help(verbose) ->
  457. "Verbose output";
  458. help(_) ->
  459. "".
  460. transform_opts(Opts) ->
  461. transform_opts(Opts, []).
  462. transform_opts([], Acc) -> Acc;
  463. %% drop `cover` and `verbose` so they're not passed as an option to common_test
  464. transform_opts([{cover, _}|Rest], Acc) ->
  465. transform_opts(Rest, Acc);
  466. transform_opts([{verbose, _}|Rest], Acc) ->
  467. transform_opts(Rest, Acc);
  468. transform_opts([{ct_hooks, CtHooks}|Rest], Acc) ->
  469. transform_opts(Rest, [{ct_hooks, parse_term(CtHooks)}|Acc]);
  470. transform_opts([{force_stop, "skip_rest"}|Rest], Acc) ->
  471. transform_opts(Rest, [{force_stop, skip_rest}|Acc]);
  472. transform_opts([{force_stop, _}|Rest], Acc) ->
  473. transform_opts(Rest, [{force_stop, true}|Acc]);
  474. transform_opts([{repeat, Repeat}|Rest], Acc) ->
  475. transform_opts(Rest, [{repeat,
  476. ec_cnv:to_integer(Repeat)}|Acc]);
  477. transform_opts([{create_priv_dir, CreatePrivDir}|Rest], Acc) ->
  478. transform_opts(Rest, [{create_priv_dir,
  479. to_atoms(CreatePrivDir)}|Acc]);
  480. transform_opts([{multiply_timetraps, MultiplyTimetraps}|Rest], Acc) ->
  481. transform_opts(Rest, [{multiply_timetraps,
  482. ec_cnv:to_integer(MultiplyTimetraps)}|Acc]);
  483. transform_opts([{event_handler, EventHandler}|Rest], Acc) ->
  484. transform_opts(Rest, [{event_handler, parse_term(EventHandler)}|Acc]);
  485. transform_opts([{silent_connections, "all"}|Rest], Acc) ->
  486. transform_opts(Rest, [{silent_connections, all}|Acc]);
  487. transform_opts([{silent_connections, SilentConnections}|Rest], Acc) ->
  488. transform_opts(Rest, [{silent_connections,
  489. to_atoms(split_string(SilentConnections))}|Acc]);
  490. transform_opts([{verbosity, Verbosity}|Rest], Acc) ->
  491. transform_opts(Rest, [{verbosity, parse_term(Verbosity)}|Acc]);
  492. transform_opts([{logopts, LogOpts}|Rest], Acc) ->
  493. transform_opts(Rest, [{logopts, to_atoms(split_string(LogOpts))}|Acc]);
  494. transform_opts([{userconfig, UserConfig}|Rest], Acc) ->
  495. transform_opts(Rest, [{userconfig, parse_term(UserConfig)}|Acc]);
  496. transform_opts([{testcase, Testcase}|Rest], Acc) ->
  497. transform_opts(Rest, [{testcase, to_atoms(split_string(Testcase))}|Acc]);
  498. transform_opts([{group, Group}|Rest], Acc) -> % @TODO handle ""
  499. % Input is a list or an atom. It can also be a nested list.
  500. transform_opts(Rest, [{group, parse_term(Group)}|Acc]);
  501. transform_opts([{suite, Suite}|Rest], Acc) ->
  502. transform_opts(Rest, [{suite, split_string(Suite)}|Acc]);
  503. transform_opts([{Key, Val}|Rest], Acc) when is_list(Val) ->
  504. % Default to splitting a string on comma, that works fine for both flat
  505. % lists of which there are many and single-items.
  506. Val1 = case split_string(Val) of
  507. [Val2] ->
  508. Val2;
  509. Val2 ->
  510. Val2
  511. end,
  512. transform_opts(Rest, [{Key, Val1}|Acc]);
  513. transform_opts([{Key, Val}|Rest], Acc) ->
  514. transform_opts(Rest, [{Key, Val}|Acc]).
  515. to_atoms(List) ->
  516. lists:map(fun(X) -> list_to_atom(X) end, List).
  517. split_string(String) ->
  518. string:tokens(String, ",").
  519. parse_term(String) ->
  520. String1 = "[" ++ String ++ "].",
  521. {ok, Tokens, _} = erl_scan:string(String1),
  522. case erl_parse:parse_term(Tokens) of
  523. {ok, [Terms]} ->
  524. Terms;
  525. Term ->
  526. Term
  527. end.