您最多选择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).