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

347 行
14 KiB

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. {ok, State1}.
  27. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  28. do(State) ->
  29. ?INFO("Running Common Test suites...", []),
  30. {RawOpts, _} = rebar_state:command_parsed_args(State),
  31. {InDirs, OutDir} = split_ct_dirs(State, RawOpts),
  32. Opts = transform_opts(RawOpts),
  33. TestApps = filter_checkouts(rebar_state:project_apps(State)),
  34. ok = create_dirs(Opts),
  35. ?DEBUG("Compiling Common Test suites in: ~p", [OutDir]),
  36. lists:foreach(fun(App) ->
  37. AppDir = rebar_app_info:dir(App),
  38. C = rebar_config:consult(AppDir),
  39. S = rebar_state:new(State, C, AppDir),
  40. %% combine `erl_first_files` and `common_test_first_files` and
  41. %% adjust compile opts to include `common_test_compile_opts`
  42. %% and `{src_dirs, "test"}`
  43. TestState = test_state(S, InDirs, OutDir),
  44. ok = rebar_erlc_compiler:compile(TestState, AppDir)
  45. end, TestApps),
  46. ok = maybe_compile_extra_tests(TestApps, State, InDirs, OutDir),
  47. Path = code:get_path(),
  48. true = code:add_patha(OutDir),
  49. CTOpts = resolve_ct_opts(State, Opts, OutDir),
  50. Result = handle_results(ct:run_test(CTOpts)),
  51. true = code:set_path(Path),
  52. case Result of
  53. {error, Reason} ->
  54. {error, {?MODULE, Reason}};
  55. ok ->
  56. {ok, State}
  57. end.
  58. -spec format_error(any()) -> iolist().
  59. format_error({failures_running_tests, FailedCount}) ->
  60. io_lib:format("Failures occured running tests: ~p", [FailedCount]);
  61. format_error({error_running_tests, Reason}) ->
  62. io_lib:format("Error running tests: ~p", [Reason]).
  63. ct_opts(State) ->
  64. DefaultLogsDir = filename:join([rebar_state:dir(State), "logs"]),
  65. [{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list
  66. {outdir, undefined, "outdir", string, help(outdir)}, %% string
  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. ].
  99. help(outdir) ->
  100. "Output directory for compiled modules";
  101. help(dir) ->
  102. "List of additional directories containing test suites";
  103. help(suite) ->
  104. "List of test suites to run";
  105. help(group) ->
  106. "List of test groups to run";
  107. help(testcase) ->
  108. "List of test cases to run";
  109. help(spec) ->
  110. "List of test specs to run";
  111. help(join_specs) ->
  112. ""; %% ??
  113. help(label) ->
  114. "Test label";
  115. help(config) ->
  116. "List of config files";
  117. help(allow_user_terms) ->
  118. ""; %% ??
  119. help(logdir) ->
  120. "Log folder";
  121. help(logopts) ->
  122. ""; %% ??
  123. help(verbosity) ->
  124. "Verbosity";
  125. help(silent_connections) ->
  126. ""; %% ??
  127. help(stylesheet) ->
  128. "Stylesheet to use for test results";
  129. help(cover) ->
  130. "Cover file to use";
  131. help(cover_stop) ->
  132. ""; %% ??
  133. help(event_handler) ->
  134. "Event handlers to attach to the runner";
  135. help(include) ->
  136. "Include folder";
  137. help(abort_if_missing_suites) ->
  138. "Abort if suites are missing";
  139. help(multiply_timetraps) ->
  140. ""; %% ??
  141. help(scale_timetraps) ->
  142. ""; %% ??
  143. help(create_priv_dir) ->
  144. ""; %% ??
  145. help(repeat) ->
  146. "How often to repeat tests";
  147. help(duration) ->
  148. "Max runtime (format: HHMMSS)";
  149. help(until) ->
  150. "Run until (format: HHMMSS)";
  151. help(force_stop) ->
  152. "Force stop after time";
  153. help(basic_html) ->
  154. "Show basic HTML";
  155. help(ct_hooks) ->
  156. "";
  157. help(userconfig) ->
  158. "".
  159. split_ct_dirs(State, RawOpts) ->
  160. %% preserve the override nature of command line opts by only checking
  161. %% `rebar.config` defined additional test dirs if none are defined via
  162. %% command line flag
  163. InDirs = case proplists:get_value(dir, RawOpts) of
  164. undefined ->
  165. CTOpts = rebar_state:get(State, common_test_opts, []),
  166. proplists:get_value(dir, CTOpts, []);
  167. Dirs -> split_string(Dirs)
  168. end,
  169. OutDir = proplists:get_value(outdir, RawOpts, default_test_dir(State)),
  170. {InDirs, OutDir}.
  171. default_test_dir(State) ->
  172. Tmp = rebar_file_utils:system_tmpdir(),
  173. Root = filename:join([rebar_state:dir(State), Tmp]),
  174. Project = filename:basename(rebar_state:dir(State)),
  175. OutDir = filename:join([Root, Project ++ "_rebar3_ct"]),
  176. ok = rebar_file_utils:reset_dir(OutDir),
  177. OutDir.
  178. transform_opts(Opts) ->
  179. transform_opts(Opts, []).
  180. transform_opts([], Acc) -> Acc;
  181. %% drop `outdir` so it's not passed to common_test
  182. transform_opts([{outdir, _}|Rest], Acc) ->
  183. transform_opts(Rest, Acc);
  184. transform_opts([{ct_hooks, CtHooks}|Rest], Acc) ->
  185. transform_opts(Rest, [{ct_hooks, parse_term(CtHooks)}|Acc]);
  186. transform_opts([{force_stop, "skip_rest"}|Rest], Acc) ->
  187. transform_opts(Rest, [{force_stop, skip_rest}|Acc]);
  188. transform_opts([{force_stop, _}|Rest], Acc) ->
  189. transform_opts(Rest, [{force_stop, true}|Acc]);
  190. transform_opts([{repeat, Repeat}|Rest], Acc) ->
  191. transform_opts(Rest, [{repeat,
  192. ec_cnv:to_integer(Repeat)}|Acc]);
  193. transform_opts([{create_priv_dir, CreatePrivDir}|Rest], Acc) ->
  194. transform_opts(Rest, [{create_priv_dir,
  195. to_atoms(CreatePrivDir)}|Acc]);
  196. transform_opts([{multiply_timetraps, MultiplyTimetraps}|Rest], Acc) ->
  197. transform_opts(Rest, [{multiply_timetraps,
  198. ec_cnv:to_integer(MultiplyTimetraps)}|Acc]);
  199. transform_opts([{event_handler, EventHandler}|Rest], Acc) ->
  200. transform_opts(Rest, [{event_handler, parse_term(EventHandler)}|Acc]);
  201. transform_opts([{silent_connections, "all"}|Rest], Acc) ->
  202. transform_opts(Rest, [{silent_connections, all}|Acc]);
  203. transform_opts([{silent_connections, SilentConnections}|Rest], Acc) ->
  204. transform_opts(Rest, [{silent_connections,
  205. to_atoms(split_string(SilentConnections))}|Acc]);
  206. transform_opts([{verbosity, Verbosity}|Rest], Acc) ->
  207. transform_opts(Rest, [{verbosity, parse_term(Verbosity)}|Acc]);
  208. transform_opts([{logopts, LogOpts}|Rest], Acc) ->
  209. transform_opts(Rest, [{logopts, to_atoms(split_string(LogOpts))}|Acc]);
  210. transform_opts([{userconfig, UserConfig}|Rest], Acc) ->
  211. transform_opts(Rest, [{userconfig, parse_term(UserConfig)}|Acc]);
  212. transform_opts([{testcase, Testcase}|Rest], Acc) ->
  213. transform_opts(Rest, [{testcase, to_atoms(split_string(Testcase))}|Acc]);
  214. transform_opts([{group, Group}|Rest], Acc) -> % @TODO handle ""
  215. % Input is a list or an atom. It can also be a nested list.
  216. transform_opts(Rest, [{group, parse_term(Group)}|Acc]);
  217. transform_opts([{Key, Val}|Rest], Acc) when is_list(Val) ->
  218. % Default to splitting a string on comma, that works fine for both flat
  219. % lists of which there are many and single-items.
  220. Val1 = case split_string(Val) of
  221. [Val2] ->
  222. Val2;
  223. Val2 ->
  224. Val2
  225. end,
  226. transform_opts(Rest, [{Key, Val1}|Acc]);
  227. transform_opts([{Key, Val}|Rest], Acc) ->
  228. transform_opts(Rest, [{Key, Val}|Acc]).
  229. to_atoms(List) ->
  230. lists:map(fun(X) -> list_to_atom(X) end, List).
  231. split_string(String) ->
  232. string:tokens(String, ",").
  233. parse_term(String) ->
  234. String1 = "[" ++ String ++ "].",
  235. {ok, Tokens, _} = erl_scan:string(String1),
  236. case erl_parse:parse_term(Tokens) of
  237. {ok, [Terms]} ->
  238. Terms;
  239. Term ->
  240. Term
  241. end.
  242. filter_checkouts(Apps) -> filter_checkouts(Apps, []).
  243. filter_checkouts([], Acc) -> lists:reverse(Acc);
  244. filter_checkouts([App|Rest], Acc) ->
  245. AppDir = filename:absname(rebar_app_info:dir(App)),
  246. CheckoutsDir = filename:absname("_checkouts"),
  247. case lists:prefix(CheckoutsDir, AppDir) of
  248. true -> filter_checkouts(Rest, Acc);
  249. false -> filter_checkouts(Rest, [App|Acc])
  250. end.
  251. create_dirs(Opts) ->
  252. LogDir = proplists:get_value(logdir, Opts),
  253. ensure_dir([LogDir]),
  254. ok.
  255. ensure_dir([]) -> ok;
  256. ensure_dir([Dir|Rest]) ->
  257. case ec_file:is_dir(Dir) of
  258. true ->
  259. ok;
  260. false ->
  261. ec_file:mkdir_path(Dir)
  262. end,
  263. ensure_dir(Rest).
  264. test_state(State, InDirs, OutDir) ->
  265. ErlOpts = rebar_state:get(State, common_test_compile_opts, []) ++
  266. rebar_utils:erl_opts(State),
  267. TestOpts = [{outdir, OutDir}] ++
  268. add_test_dir(ErlOpts, InDirs),
  269. first_files(rebar_state:set(State, erl_opts, TestOpts)).
  270. add_test_dir(Opts, InDirs) ->
  271. %% if no src_dirs are set we have to specify `src` or it won't
  272. %% be built
  273. case proplists:append_values(src_dirs, Opts) of
  274. [] -> [{src_dirs, ["src", "test" | InDirs]} | Opts];
  275. _ -> [{src_dirs, ["test" | InDirs]} | Opts]
  276. end.
  277. first_files(State) ->
  278. BaseFirst = rebar_state:get(State, erl_first_files, []),
  279. CTFirst = rebar_state:get(State, common_test_first_files, []),
  280. rebar_state:set(State, erl_first_files, BaseFirst ++ CTFirst).
  281. resolve_ct_opts(State, CmdLineOpts, OutDir) ->
  282. CTOpts = rebar_state:get(State, common_test_opts, []),
  283. Opts = lists:ukeymerge(1,
  284. lists:ukeysort(1, CmdLineOpts),
  285. lists:ukeysort(1, CTOpts)),
  286. %% rebar has seperate input and output directories whereas `common_test`
  287. %% uses only a single directory so set `dir` to our precompiled `OutDir`
  288. %% and disable `auto_compile`
  289. [{auto_compile, false}, {dir, OutDir}] ++ lists:keydelete(dir, 1, Opts).
  290. maybe_compile_extra_tests(TestApps, State, InDirs, OutDir) ->
  291. F = fun(App) -> rebar_app_info:dir(App) == rebar_dir:get_cwd() end,
  292. case lists:filter(F, TestApps) of
  293. %% compile just the `test` and extra test directories of the base dir
  294. [] ->
  295. ErlOpts = rebar_state:get(State, common_test_compile_opts, []) ++
  296. rebar_utils:erl_opts(State),
  297. TestOpts = [{outdir, OutDir}] ++
  298. [{src_dirs, ["test"|InDirs]}] ++
  299. lists:keydelete(src_dirs, 1, ErlOpts),
  300. TestState = first_files(rebar_state:set(State, erl_opts, TestOpts)),
  301. rebar_erlc_compiler:compile(TestState, rebar_dir:get_cwd());
  302. %% already compiled `./test` so do nothing
  303. _ -> ok
  304. end.
  305. handle_results([Result]) ->
  306. handle_results(Result);
  307. handle_results([Result|Results]) when is_list(Results) ->
  308. case handle_results(Result) of
  309. ok ->
  310. handle_results(Results);
  311. Error ->
  312. Error
  313. end;
  314. handle_results({_, 0, _}) ->
  315. ok;
  316. handle_results({_, FailedCount, _}) ->
  317. {error, {failures_running_tests, FailedCount}};
  318. handle_results({error, Reason}) ->
  319. {error, {error_running_tests, Reason}}.