You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

370 lines
14 KiB

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