選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

385 行
15 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. -include("rebar.hrl").
  9. -define(PROVIDER, ct).
  10. -define(DEPS, [compile]).
  11. %% ===================================================================
  12. %% Public API
  13. %% ===================================================================
  14. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  15. init(State) ->
  16. Provider = providers:create([{name, ?PROVIDER},
  17. {module, ?MODULE},
  18. {deps, ?DEPS},
  19. {bare, false},
  20. {example, "rebar ct"},
  21. {short_desc, "Run Common Tests."},
  22. {desc, ""},
  23. {opts, ct_opts(State)},
  24. {profiles, [test]}]),
  25. State1 = rebar_state:add_provider(State, Provider),
  26. State2 = rebar_state:add_to_profile(State1, test, test_state(State1)),
  27. {ok, State2}.
  28. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  29. do(State) ->
  30. ?INFO("Running Common Test suites...", []),
  31. {RawOpts, _} = rebar_state:command_parsed_args(State),
  32. Opts = transform_opts(RawOpts),
  33. TestApps = filter_checkouts(rebar_state:project_apps(State)),
  34. ok = create_dirs(Opts),
  35. InDirs = in_dirs(State, RawOpts),
  36. ok = compile_tests(State, TestApps, InDirs),
  37. ok = maybe_cover_compile(State, RawOpts),
  38. CTOpts = resolve_ct_opts(State, Opts),
  39. Verbose = proplists:get_value(verbose, RawOpts, false),
  40. TestDirs = test_dirs(State, TestApps),
  41. Result = run_test([{dir, TestDirs}|CTOpts], Verbose),
  42. ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER),
  43. case Result of
  44. {error, Reason} ->
  45. {error, {?MODULE, Reason}};
  46. ok ->
  47. {ok, State}
  48. end.
  49. -spec format_error(any()) -> iolist().
  50. format_error({failures_running_tests, FailedCount}) ->
  51. io_lib:format("Failures occured running tests: ~p", [FailedCount]);
  52. format_error({error_running_tests, Reason}) ->
  53. io_lib:format("Error running tests: ~p", [Reason]).
  54. run_test(CTOpts, true) ->
  55. handle_results(ct:run_test(CTOpts));
  56. run_test(CTOpts, false) ->
  57. Pid = self(),
  58. LogDir = proplists:get_value(logdir, CTOpts),
  59. erlang:spawn_monitor(fun() ->
  60. {ok, F} = file:open(filename:join([LogDir, "ct.latest.log"]),
  61. [write]),
  62. true = group_leader(F, self()),
  63. Pid ! ct:run_test(CTOpts)
  64. end),
  65. receive Result -> handle_quiet_results(CTOpts, Result) end.
  66. ct_opts(_State) ->
  67. DefaultLogsDir = filename:join([rebar_dir:get_cwd(), "_build", "logs"]),
  68. [{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list
  69. {suite, undefined, "suite", string, help(suite)}, %% comma-seperated list
  70. {group, undefined, "group", string, help(group)}, %% comma-seperated list
  71. {testcase, undefined, "case", string, help(testcase)}, %% comma-seperated list
  72. {spec, undefined, "spec", string, help(spec)}, %% comma-seperated list
  73. {join_specs, undefined, "join_specs", boolean, help(join_specs)}, %% Boolean
  74. {label, undefined, "label", string, help(label)}, %% String
  75. {config, undefined, "config", string, help(config)}, %% comma-seperated list
  76. {userconfig, undefined, "userconfig", string, help(userconfig)}, %% [{CallbackMod, CfgStrings}] | {CallbackMod, CfgStrings}
  77. {allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool
  78. {logdir, undefined, "logdir", {string, DefaultLogsDir}, help(logdir)}, %% dir
  79. {logopts, undefined, "logopts", string, help(logopts)}, %% enum, no_nl | no_src
  80. {verbosity, undefined, "verbosity", string, help(verbosity)}, %% Integer OR [{Category, VLevel}]
  81. {silent_connections, undefined, "silent_connections", string,
  82. help(silent_connections)}, % all OR %% comma-seperated list
  83. {stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% file
  84. {cover, $c, "cover", boolean, help(cover)},
  85. {cover_spec, undefined, "cover_spec", string, help(cover_spec)}, %% file
  86. {cover_stop, undefined, "cover_stop", boolean, help(cover_stop)}, %% Boolean
  87. {event_handler, undefined, "event_handler", string, help(event_handler)}, %% EH | [EH] WHERE EH atom() | {atom(), InitArgs} | {[atom()], InitArgs}
  88. {include, undefined, "include", string, help(include)}, % comma-seperated list
  89. {abort_if_missing_suites, undefined, "abort_if_missing_suites", {boolean, true},
  90. help(abort_if_missing_suites)}, %% boolean
  91. {multiply_timetraps, undefined, "multiply_timetraps", integer,
  92. help(multiply_timetraps)}, %% integer
  93. {scale_timetraps, undefined, "scale_timetraps", boolean, help(scale_timetraps)}, %% Boolean
  94. {create_priv_dir, undefined, "create_priv_dir", string, help(create_priv_dir)}, %% enum: auto_per_run | auto_per_tc | manual_per_tc
  95. {repeat, undefined, "repeat", integer, help(repeat)}, %% integer
  96. {duration, undefined, "duration", string, help(duration)}, % format: HHMMSS
  97. {until, undefined, "until", string, help(until)}, %% format: YYMoMoDD[HHMMSS]
  98. {force_stop, undefined, "force_stop", string, help(force_stop)}, % enum: skip_rest, bool
  99. {basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Booloean
  100. {ct_hooks, undefined, "ct_hooks", string, help(ct_hooks)}, %% List: [CTHModule | {CTHModule, CTHInitArgs}] where CTHModule is atom CthInitArgs is term
  101. {verbose, $v, "verbose", boolean, help(verbose)}
  102. ].
  103. help(dir) ->
  104. "List of additional directories containing test suites";
  105. help(suite) ->
  106. "List of test suites to run";
  107. help(group) ->
  108. "List of test groups to run";
  109. help(testcase) ->
  110. "List of test cases to run";
  111. help(spec) ->
  112. "List of test specs to run";
  113. help(join_specs) ->
  114. ""; %% ??
  115. help(label) ->
  116. "Test label";
  117. help(config) ->
  118. "List of config files";
  119. help(allow_user_terms) ->
  120. ""; %% ??
  121. help(logdir) ->
  122. "Log folder";
  123. help(logopts) ->
  124. ""; %% ??
  125. help(verbosity) ->
  126. "Verbosity";
  127. help(silent_connections) ->
  128. ""; %% ??
  129. help(stylesheet) ->
  130. "Stylesheet to use for test results";
  131. help(cover) ->
  132. "Generate cover data";
  133. help(cover_spec) ->
  134. "Cover file to use";
  135. help(cover_stop) ->
  136. ""; %% ??
  137. help(event_handler) ->
  138. "Event handlers to attach to the runner";
  139. help(include) ->
  140. "Include folder";
  141. help(abort_if_missing_suites) ->
  142. "Abort if suites are missing";
  143. help(multiply_timetraps) ->
  144. ""; %% ??
  145. help(scale_timetraps) ->
  146. ""; %% ??
  147. help(create_priv_dir) ->
  148. ""; %% ??
  149. help(repeat) ->
  150. "How often to repeat tests";
  151. help(duration) ->
  152. "Max runtime (format: HHMMSS)";
  153. help(until) ->
  154. "Run until (format: HHMMSS)";
  155. help(force_stop) ->
  156. "Force stop after time";
  157. help(basic_html) ->
  158. "Show basic HTML";
  159. help(ct_hooks) ->
  160. "";
  161. help(userconfig) ->
  162. "";
  163. help(verbose) ->
  164. "Verbose output".
  165. transform_opts(Opts) ->
  166. transform_opts(Opts, []).
  167. transform_opts([], Acc) -> Acc;
  168. %% drop `cover` and `verbose` so they're not passed as an option to common_test
  169. transform_opts([{cover, _}|Rest], Acc) ->
  170. transform_opts(Rest, Acc);
  171. transform_opts([{verbose, _}|Rest], Acc) ->
  172. transform_opts(Rest, Acc);
  173. transform_opts([{ct_hooks, CtHooks}|Rest], Acc) ->
  174. transform_opts(Rest, [{ct_hooks, parse_term(CtHooks)}|Acc]);
  175. transform_opts([{force_stop, "skip_rest"}|Rest], Acc) ->
  176. transform_opts(Rest, [{force_stop, skip_rest}|Acc]);
  177. transform_opts([{force_stop, _}|Rest], Acc) ->
  178. transform_opts(Rest, [{force_stop, true}|Acc]);
  179. transform_opts([{repeat, Repeat}|Rest], Acc) ->
  180. transform_opts(Rest, [{repeat,
  181. ec_cnv:to_integer(Repeat)}|Acc]);
  182. transform_opts([{create_priv_dir, CreatePrivDir}|Rest], Acc) ->
  183. transform_opts(Rest, [{create_priv_dir,
  184. to_atoms(CreatePrivDir)}|Acc]);
  185. transform_opts([{multiply_timetraps, MultiplyTimetraps}|Rest], Acc) ->
  186. transform_opts(Rest, [{multiply_timetraps,
  187. ec_cnv:to_integer(MultiplyTimetraps)}|Acc]);
  188. transform_opts([{event_handler, EventHandler}|Rest], Acc) ->
  189. transform_opts(Rest, [{event_handler, parse_term(EventHandler)}|Acc]);
  190. transform_opts([{silent_connections, "all"}|Rest], Acc) ->
  191. transform_opts(Rest, [{silent_connections, all}|Acc]);
  192. transform_opts([{silent_connections, SilentConnections}|Rest], Acc) ->
  193. transform_opts(Rest, [{silent_connections,
  194. to_atoms(split_string(SilentConnections))}|Acc]);
  195. transform_opts([{verbosity, Verbosity}|Rest], Acc) ->
  196. transform_opts(Rest, [{verbosity, parse_term(Verbosity)}|Acc]);
  197. transform_opts([{logopts, LogOpts}|Rest], Acc) ->
  198. transform_opts(Rest, [{logopts, to_atoms(split_string(LogOpts))}|Acc]);
  199. transform_opts([{userconfig, UserConfig}|Rest], Acc) ->
  200. transform_opts(Rest, [{userconfig, parse_term(UserConfig)}|Acc]);
  201. transform_opts([{testcase, Testcase}|Rest], Acc) ->
  202. transform_opts(Rest, [{testcase, to_atoms(split_string(Testcase))}|Acc]);
  203. transform_opts([{group, Group}|Rest], Acc) -> % @TODO handle ""
  204. % Input is a list or an atom. It can also be a nested list.
  205. transform_opts(Rest, [{group, parse_term(Group)}|Acc]);
  206. transform_opts([{Key, Val}|Rest], Acc) when is_list(Val) ->
  207. % Default to splitting a string on comma, that works fine for both flat
  208. % lists of which there are many and single-items.
  209. Val1 = case split_string(Val) of
  210. [Val2] ->
  211. Val2;
  212. Val2 ->
  213. Val2
  214. end,
  215. transform_opts(Rest, [{Key, Val1}|Acc]);
  216. transform_opts([{Key, Val}|Rest], Acc) ->
  217. transform_opts(Rest, [{Key, Val}|Acc]).
  218. to_atoms(List) ->
  219. lists:map(fun(X) -> list_to_atom(X) end, List).
  220. split_string(String) ->
  221. string:tokens(String, ",").
  222. parse_term(String) ->
  223. String1 = "[" ++ String ++ "].",
  224. {ok, Tokens, _} = erl_scan:string(String1),
  225. case erl_parse:parse_term(Tokens) of
  226. {ok, [Terms]} ->
  227. Terms;
  228. Term ->
  229. Term
  230. end.
  231. filter_checkouts(Apps) -> filter_checkouts(Apps, []).
  232. filter_checkouts([], Acc) -> lists:reverse(Acc);
  233. filter_checkouts([App|Rest], Acc) ->
  234. AppDir = filename:absname(rebar_app_info:dir(App)),
  235. CheckoutsDir = filename:absname("_checkouts"),
  236. case lists:prefix(CheckoutsDir, AppDir) of
  237. true -> filter_checkouts(Rest, Acc);
  238. false -> filter_checkouts(Rest, [App|Acc])
  239. end.
  240. create_dirs(Opts) ->
  241. LogDir = proplists:get_value(logdir, Opts),
  242. ensure_dir([LogDir]),
  243. ok.
  244. ensure_dir([]) -> ok;
  245. ensure_dir([Dir|Rest]) ->
  246. case ec_file:is_dir(Dir) of
  247. true ->
  248. ok;
  249. false ->
  250. ec_file:mkdir_path(Dir)
  251. end,
  252. ensure_dir(Rest).
  253. in_dirs(State, Opts) ->
  254. %% preserve the override nature of command line opts by only checking
  255. %% `rebar.config` defined additional test dirs if none are defined via
  256. %% command line flag
  257. case proplists:get_value(dir, Opts) of
  258. undefined ->
  259. CTOpts = rebar_state:get(State, ct_opts, []),
  260. proplists:get_value(dir, CTOpts, []);
  261. Dirs -> split_string(Dirs)
  262. end.
  263. test_dirs(State, TestApps) ->
  264. %% we need to add "./ebin" if it exists but only if it's not already
  265. %% due to be added
  266. F = fun(App) -> rebar_app_info:dir(App) =/= rebar_dir:get_cwd() end,
  267. BareEbin = filename:join([rebar_dir:base_dir(State), "ebin"]),
  268. case lists:any(F, TestApps) andalso filelib:is_dir(BareEbin) of
  269. false -> application_dirs(TestApps, []);
  270. true -> [BareEbin|application_dirs(TestApps, [])]
  271. end.
  272. application_dirs([], Acc) -> lists:reverse(Acc);
  273. application_dirs([App|Rest], Acc) ->
  274. application_dirs(Rest, [rebar_app_info:ebin_dir(App)|Acc]).
  275. test_state(State) ->
  276. TestOpts = case rebar_state:get(State, ct_compile_opts, []) of
  277. [] -> [];
  278. Opts -> [{erl_opts, Opts}]
  279. end,
  280. [first_files(State)|TestOpts].
  281. first_files(State) ->
  282. CTFirst = rebar_state:get(State, ct_first_files, []),
  283. {erl_first_files, CTFirst}.
  284. resolve_ct_opts(State, CmdLineOpts) ->
  285. CTOpts = rebar_state:get(State, ct_opts, []),
  286. Opts = lists:ukeymerge(1,
  287. lists:ukeysort(1, CmdLineOpts),
  288. lists:ukeysort(1, CTOpts)),
  289. %% disable `auto_compile` and remove `dir` from the opts
  290. [{auto_compile, false}|lists:keydelete(dir, 1, Opts)].
  291. compile_tests(State, TestApps, InDirs) ->
  292. State1 = replace_src_dirs(State, InDirs),
  293. F = fun(AppInfo) ->
  294. AppDir = rebar_app_info:dir(AppInfo),
  295. S = case rebar_app_info:state(AppInfo) of
  296. undefined ->
  297. C = rebar_config:consult(AppDir),
  298. rebar_state:new(State1, C, AppDir);
  299. AppState ->
  300. AppState
  301. end,
  302. ok = rebar_erlc_compiler:compile(S,
  303. ec_cnv:to_list(rebar_app_info:dir(AppInfo)),
  304. ec_cnv:to_list(rebar_app_info:out_dir(AppInfo)))
  305. end,
  306. lists:foreach(F, TestApps),
  307. compile_bare_tests(State1, TestApps).
  308. compile_bare_tests(State, TestApps) ->
  309. F = fun(App) -> rebar_app_info:dir(App) == rebar_dir:get_cwd() end,
  310. case lists:filter(F, TestApps) of
  311. %% compile just the `test` directory of the base dir
  312. [] -> rebar_erlc_compiler:compile(State,
  313. rebar_dir:get_cwd(),
  314. rebar_dir:base_dir(State));
  315. %% already compiled `./test` so do nothing
  316. _ -> ok
  317. end.
  318. replace_src_dirs(State, InDirs) ->
  319. %% replace any `src_dirs` with just the `test` dir and any `InDirs`
  320. ErlOpts = rebar_state:get(State, erl_opts, []),
  321. StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts),
  322. rebar_state:set(State, erl_opts, [{src_dirs, ["test"|InDirs]}|StrippedOpts]).
  323. maybe_cover_compile(State, Opts) ->
  324. State1 = case proplists:get_value(cover, Opts, false) of
  325. true -> rebar_state:set(State, cover_enabled, true);
  326. false -> State
  327. end,
  328. rebar_prv_cover:maybe_cover_compile(State1).
  329. handle_results([Result]) ->
  330. handle_results(Result);
  331. handle_results([Result|Results]) when is_list(Results) ->
  332. case handle_results(Result) of
  333. ok ->
  334. handle_results(Results);
  335. Error ->
  336. Error
  337. end;
  338. handle_results({error, Reason}) ->
  339. {error, {error_running_tests, Reason}};
  340. handle_results(_) -> ok.
  341. handle_quiet_results(_, {Passed, 0, {0, 0}}) ->
  342. io:format(" All ~p tests passed.~n", [Passed]);
  343. handle_quiet_results(_, {Passed, 0, {UserSkipped, AutoSkipped}}) ->
  344. io:format(" All ~p tests passed. Skipped ~p tests.~n",
  345. [Passed, UserSkipped + AutoSkipped]);
  346. handle_quiet_results(CTOpts, {_, Failed, _}) ->
  347. LogDir = proplists:get_value(logdir, CTOpts),
  348. Index = filename:join([LogDir, "index.html"]),
  349. io:format(" ~p tests failed.~n Results written to ~p.~n", [Failed, Index]);
  350. handle_quiet_results(_CTOpts, {'DOWN', _, _, _, Reason}) ->
  351. handle_results({error, Reason});
  352. handle_quiet_results(_CTOpts, Result) -> handle_results(Result).