|
|
@ -2,12 +2,13 @@ |
|
|
|
%% ex: ts=4 sw=4 et |
|
|
|
|
|
|
|
-module(rebar_prv_common_test). |
|
|
|
|
|
|
|
-behaviour(provider). |
|
|
|
|
|
|
|
-export([init/1, |
|
|
|
do/1, |
|
|
|
format_error/1]). |
|
|
|
%% exported for test purposes, consider private |
|
|
|
-export([setup_ct/1]). |
|
|
|
|
|
|
|
-include("rebar.hrl"). |
|
|
|
-include_lib("providers/include/providers.hrl"). |
|
|
@ -37,54 +38,352 @@ init(State) -> |
|
|
|
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. |
|
|
|
do(State) -> |
|
|
|
?INFO("Running Common Test suites...", []), |
|
|
|
{RawOpts, _} = rebar_state:command_parsed_args(State), |
|
|
|
Opts = transform_opts(RawOpts), |
|
|
|
TestApps = filter_checkouts(rebar_state:project_apps(State)), |
|
|
|
ok = create_dirs(Opts), |
|
|
|
InDirs = in_dirs(State, RawOpts), |
|
|
|
ok = compile_tests(State, TestApps, InDirs), |
|
|
|
case resolve_ct_opts(State, TestApps, Opts) of |
|
|
|
{ok, CTOpts} -> |
|
|
|
run_test(State, RawOpts, CTOpts); |
|
|
|
{error, Reason} -> |
|
|
|
?PRV_ERROR(Reason) |
|
|
|
end. |
|
|
|
|
|
|
|
run_test(State, RawOpts, CTOpts) -> |
|
|
|
ok = maybe_cover_compile(State, RawOpts), |
|
|
|
Verbose = proplists:get_value(verbose, RawOpts, false), |
|
|
|
Result = run_test(CTOpts, Verbose), |
|
|
|
ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER), |
|
|
|
case Result of |
|
|
|
{error, Reason} -> |
|
|
|
{error, {?MODULE, Reason}}; |
|
|
|
ok -> |
|
|
|
{ok, State} |
|
|
|
try |
|
|
|
case setup_ct(State) of |
|
|
|
{error, {no_tests_specified, Opts}} -> |
|
|
|
?WARN("No tests specified in opts: ~p", [Opts]), |
|
|
|
{ok, State}; |
|
|
|
Opts -> |
|
|
|
Opts1 = setup_logdir(State, Opts), |
|
|
|
?DEBUG("common test opts: ~p", [Opts1]), |
|
|
|
run_test(State, Opts1) |
|
|
|
end |
|
|
|
catch error:Reason -> ?PRV_ERROR(Reason) |
|
|
|
end. |
|
|
|
|
|
|
|
-spec format_error(any()) -> iolist(). |
|
|
|
format_error({multiple_dirs_and_suites, Opts}) -> |
|
|
|
io_lib:format("Multiple dirs declared alongside suite in opts: ~p", [Opts]); |
|
|
|
format_error({bad_dir_or_suite, Opts}) -> |
|
|
|
io_lib:format("Bad value for dir or suite in opts: ~p", [Opts]); |
|
|
|
format_error({failures_running_tests, {Failed, AutoSkipped}}) -> |
|
|
|
io_lib:format("Failures occured running tests: ~b", [Failed+AutoSkipped]); |
|
|
|
format_error({error_running_tests, Reason}) -> |
|
|
|
io_lib:format("Error running tests: ~p", [Reason]); |
|
|
|
format_error({error_processing_options, Reason}) -> |
|
|
|
io_lib:format("Error processing options: ~p", [Reason]). |
|
|
|
format_error({error, Reason}) -> |
|
|
|
io_lib:format("Unknown error: ~p", [Reason]). |
|
|
|
|
|
|
|
%% =================================================================== |
|
|
|
%% Internal functions |
|
|
|
%% =================================================================== |
|
|
|
|
|
|
|
run_test(State, Opts) -> |
|
|
|
{RawOpts, _} = rebar_state:command_parsed_args(State), |
|
|
|
Result = case proplists:get_value(verbose, RawOpts, false) of |
|
|
|
true -> run_test(Opts); |
|
|
|
false -> run_test_quiet(Opts) |
|
|
|
end, |
|
|
|
ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER), |
|
|
|
case Result of |
|
|
|
ok -> {ok, State}; |
|
|
|
Error -> Error |
|
|
|
end. |
|
|
|
|
|
|
|
run_test(Opts) -> handle_results(ct:run_test(Opts)). |
|
|
|
|
|
|
|
run_test(CTOpts, true) -> |
|
|
|
handle_results(ct:run_test(CTOpts)); |
|
|
|
run_test(CTOpts, false) -> |
|
|
|
run_test_quiet(Opts) -> |
|
|
|
Pid = self(), |
|
|
|
LogDir = proplists:get_value(logdir, CTOpts), |
|
|
|
LogDir = proplists:get_value(logdir, Opts), |
|
|
|
erlang:spawn_monitor(fun() -> |
|
|
|
{ok, F} = file:open(filename:join([LogDir, "ct.latest.log"]), |
|
|
|
[write]), |
|
|
|
true = group_leader(F, self()), |
|
|
|
Pid ! ct:run_test(CTOpts) |
|
|
|
Pid ! ct:run_test(Opts) |
|
|
|
end), |
|
|
|
receive Result -> handle_quiet_results(CTOpts, Result) end. |
|
|
|
receive Result -> handle_quiet_results(Opts, Result) end. |
|
|
|
|
|
|
|
handle_results(Results) when is_list(Results) -> |
|
|
|
Result = lists:foldl(fun sum_results/2, {0, 0, {0,0}}, Results), |
|
|
|
handle_results(Result); |
|
|
|
handle_results({_, Failed, {_, AutoSkipped}}) |
|
|
|
when Failed > 0 orelse AutoSkipped > 0 -> |
|
|
|
?PRV_ERROR({failures_running_tests, {Failed, AutoSkipped}}); |
|
|
|
handle_results({error, Reason}) -> |
|
|
|
?PRV_ERROR({error_running_tests, Reason}); |
|
|
|
handle_results(_) -> |
|
|
|
ok. |
|
|
|
|
|
|
|
sum_results({Passed, Failed, {UserSkipped, AutoSkipped}}, |
|
|
|
{Passed2, Failed2, {UserSkipped2, AutoSkipped2}}) -> |
|
|
|
{Passed+Passed2, Failed+Failed2, |
|
|
|
{UserSkipped+UserSkipped2, AutoSkipped+AutoSkipped2}}. |
|
|
|
|
|
|
|
handle_quiet_results(_, {error, _} = Result) -> |
|
|
|
handle_results(Result); |
|
|
|
handle_quiet_results(_, {'DOWN', _, _, _, Reason}) -> |
|
|
|
handle_results(?PRV_ERROR(Reason)); |
|
|
|
handle_quiet_results(CTOpts, Results) when is_list(Results) -> |
|
|
|
_ = [format_result(Result) || Result <- Results], |
|
|
|
case handle_results(Results) of |
|
|
|
?PRV_ERROR({failures_running_tests, _}) = Error -> |
|
|
|
LogDir = proplists:get_value(logdir, CTOpts), |
|
|
|
Index = filename:join([LogDir, "index.html"]), |
|
|
|
?CONSOLE("Results written to ~p.", [Index]), |
|
|
|
Error; |
|
|
|
Other -> |
|
|
|
Other |
|
|
|
end; |
|
|
|
handle_quiet_results(CTOpts, Result) -> |
|
|
|
handle_quiet_results(CTOpts, [Result]). |
|
|
|
|
|
|
|
format_result({Passed, 0, {0, 0}}) -> |
|
|
|
?CONSOLE("All ~p tests passed.", [Passed]); |
|
|
|
format_result({Passed, Failed, Skipped}) -> |
|
|
|
Format = [format_failed(Failed), format_skipped(Skipped), |
|
|
|
format_passed(Passed)], |
|
|
|
?CONSOLE("~s", [Format]). |
|
|
|
|
|
|
|
format_failed(0) -> |
|
|
|
[]; |
|
|
|
format_failed(Failed) -> |
|
|
|
io_lib:format("Failed ~p tests. ", [Failed]). |
|
|
|
|
|
|
|
format_passed(Passed) -> |
|
|
|
io_lib:format("Passed ~p tests. ", [Passed]). |
|
|
|
|
|
|
|
format_skipped({0, 0}) -> |
|
|
|
[]; |
|
|
|
format_skipped({User, Auto}) -> |
|
|
|
io_lib:format("Skipped ~p (~p, ~p) tests. ", [User+Auto, User, Auto]). |
|
|
|
|
|
|
|
test_state(State) -> |
|
|
|
TestOpts = case rebar_state:get(State, ct_compile_opts, []) of |
|
|
|
[] -> []; |
|
|
|
Opts -> [{erl_opts, Opts}] |
|
|
|
end, |
|
|
|
[first_files(State)|TestOpts]. |
|
|
|
|
|
|
|
first_files(State) -> |
|
|
|
CTFirst = rebar_state:get(State, ct_first_files, []), |
|
|
|
{erl_first_files, CTFirst}. |
|
|
|
|
|
|
|
setup_ct(State) -> |
|
|
|
Opts = resolve_ct_opts(State), |
|
|
|
Opts1 = discover_tests(State, Opts), |
|
|
|
copy_and_compile_tests(State, Opts1). |
|
|
|
|
|
|
|
resolve_ct_opts(State) -> |
|
|
|
{RawOpts, _} = rebar_state:command_parsed_args(State), |
|
|
|
CmdOpts = transform_opts(RawOpts), |
|
|
|
CfgOpts = rebar_state:get(State, ct_opts, []), |
|
|
|
Merged = lists:ukeymerge(1, |
|
|
|
lists:ukeysort(1, CmdOpts), |
|
|
|
lists:ukeysort(1, CfgOpts)), |
|
|
|
%% make sure `dir` and/or `suite` from command line go in as |
|
|
|
%% a pair overriding both `dir` and `suite` from config if |
|
|
|
%% they exist |
|
|
|
case {proplists:get_value(suite, CmdOpts), proplists:get_value(dir, CmdOpts)} of |
|
|
|
{undefined, undefined} -> Merged; |
|
|
|
{_Suite, undefined} -> lists:keydelete(dir, 1, Merged); |
|
|
|
{undefined, _Dir} -> lists:keydelete(suite, 1, Merged); |
|
|
|
{_Suite, _Dir} -> Merged |
|
|
|
end. |
|
|
|
|
|
|
|
discover_tests(State, Opts) -> |
|
|
|
case proplists:get_value(spec, Opts) of |
|
|
|
undefined -> discover_dirs_and_suites(State, Opts); |
|
|
|
TestSpec -> discover_testspec(TestSpec, Opts) |
|
|
|
end. |
|
|
|
|
|
|
|
discover_dirs_and_suites(State, Opts) -> |
|
|
|
case {proplists:get_value(dir, Opts), proplists:get_value(suite, Opts)} of |
|
|
|
%% no dirs or suites defined, try using `$APP/test` and `$ROOT/test` |
|
|
|
%% as suites |
|
|
|
{undefined, undefined} -> test_dirs(State, Opts); |
|
|
|
%% no dirs defined |
|
|
|
{undefined, _} -> Opts; |
|
|
|
%% no suites defined |
|
|
|
{_, undefined} -> Opts; |
|
|
|
%% a single dir defined, this is ok |
|
|
|
{Dirs, Suites} when is_integer(hd(Dirs)), is_list(Suites) -> Opts; |
|
|
|
%% still a single dir defined, adjust to make acceptable to ct |
|
|
|
{[Dir], Suites} when is_integer(hd(Dir)), is_list(Suites) -> |
|
|
|
[{dir, Dir}|lists:keydelete(dir, 1, Opts)]; |
|
|
|
%% multiple dirs and suites, error now to simplify later steps |
|
|
|
{_, _} -> erlang:error({multiple_dirs_and_suites, Opts}) |
|
|
|
end. |
|
|
|
|
|
|
|
discover_testspec(_TestSpec, Opts) -> |
|
|
|
lists:keydelete(auto_compile, 1, Opts). |
|
|
|
|
|
|
|
copy_and_compile_tests(State, Opts) -> |
|
|
|
%% possibly enable cover |
|
|
|
{RawOpts, _} = rebar_state:command_parsed_args(State), |
|
|
|
State1 = case proplists:get_value(cover, RawOpts, false) of |
|
|
|
true -> rebar_state:set(State, cover_enabled, true); |
|
|
|
false -> State |
|
|
|
end, |
|
|
|
copy_and_compile_test_suites(State1, Opts). |
|
|
|
|
|
|
|
copy_and_compile_test_suites(State, Opts) -> |
|
|
|
case proplists:get_value(suite, Opts) of |
|
|
|
%% no suites, try dirs |
|
|
|
undefined -> copy_and_compile_test_dirs(State, Opts); |
|
|
|
Suites -> |
|
|
|
Dir = proplists:get_value(dir, Opts, undefined), |
|
|
|
AllSuites = join(Dir, Suites), |
|
|
|
Dirs = find_suite_dirs(AllSuites), |
|
|
|
lists:foreach(fun(S) -> |
|
|
|
NewPath = copy(State, S), |
|
|
|
compile_dir(State, NewPath) |
|
|
|
end, Dirs), |
|
|
|
NewSuites = lists:map(fun(S) -> retarget_path(State, S) end, AllSuites), |
|
|
|
[{suite, NewSuites}|lists:keydelete(suite, 1, Opts)] |
|
|
|
end. |
|
|
|
|
|
|
|
copy_and_compile_test_dirs(State, Opts) -> |
|
|
|
case proplists:get_value(dir, Opts) of |
|
|
|
undefined -> {error, {no_tests_specified, Opts}}; |
|
|
|
%% dir is a single directory |
|
|
|
Dir when is_list(Dir), is_integer(hd(Dir)) -> |
|
|
|
NewPath = copy(State, Dir), |
|
|
|
[{dir, compile_dir(State, NewPath)}|lists:keydelete(dir, 1, Opts)]; |
|
|
|
%% dir is a list of directories |
|
|
|
Dirs when is_list(Dirs) -> |
|
|
|
NewDirs = lists:map(fun(Dir) -> |
|
|
|
NewPath = copy(State, Dir), |
|
|
|
compile_dir(State, NewPath) |
|
|
|
end, Dirs), |
|
|
|
[{dir, NewDirs}|lists:keydelete(dir, 1, Opts)] |
|
|
|
end. |
|
|
|
|
|
|
|
join(undefined, Suites) -> Suites; |
|
|
|
join(Dir, Suites) when is_list(Dir), is_integer(hd(Dir)) -> |
|
|
|
lists:map(fun(S) -> filename:join([Dir, S]) end, Suites); |
|
|
|
%% multiple dirs or a bad dir argument, try to continue anyways |
|
|
|
join(_, Suites) -> Suites. |
|
|
|
|
|
|
|
find_suite_dirs(Suites) -> |
|
|
|
AllDirs = lists:map(fun(S) -> filename:dirname(filename:absname(S)) end, Suites), |
|
|
|
%% eliminate duplicates |
|
|
|
lists:usort(AllDirs). |
|
|
|
|
|
|
|
copy(State, Target) -> |
|
|
|
case retarget_path(State, Target) of |
|
|
|
%% directory lies outside of our project's file structure so |
|
|
|
%% don't copy it |
|
|
|
Target -> Target; |
|
|
|
NewTarget -> |
|
|
|
%% unlink the directory if it's a symlink |
|
|
|
case ec_file:is_symlink(NewTarget) of |
|
|
|
true -> ok = ec_file:remove(NewTarget); |
|
|
|
false -> ok |
|
|
|
end, |
|
|
|
ok = ec_file:copy(Target, NewTarget, [recursive]), |
|
|
|
NewTarget |
|
|
|
end. |
|
|
|
|
|
|
|
compile_dir(State, Dir) -> |
|
|
|
NewState = replace_src_dirs(State, [Dir]), |
|
|
|
ok = rebar_erlc_compiler:compile(NewState, rebar_dir:base_dir(State), Dir), |
|
|
|
ok = maybe_cover_compile(State, Dir), |
|
|
|
Dir. |
|
|
|
|
|
|
|
retarget_path(State, Path) -> |
|
|
|
ProjectApps = rebar_state:project_apps(State), |
|
|
|
retarget_path(State, Path, ProjectApps). |
|
|
|
|
|
|
|
%% not relative to any apps in project, check to see it's relative to |
|
|
|
%% project root |
|
|
|
retarget_path(State, Path, []) -> |
|
|
|
case relative_path(reduce_path(Path), rebar_state:dir(State)) of |
|
|
|
{ok, NewPath} -> filename:join([rebar_dir:base_dir(State), NewPath]); |
|
|
|
%% not relative to project root, don't modify |
|
|
|
{error, not_relative} -> Path |
|
|
|
end; |
|
|
|
%% relative to current app, retarget to the same dir relative to |
|
|
|
%% the app's out_dir |
|
|
|
retarget_path(State, Path, [App|Rest]) -> |
|
|
|
case relative_path(reduce_path(Path), rebar_app_info:dir(App)) of |
|
|
|
{ok, NewPath} -> filename:join([rebar_app_info:out_dir(App), NewPath]); |
|
|
|
{error, not_relative} -> retarget_path(State, Path, Rest) |
|
|
|
end. |
|
|
|
|
|
|
|
relative_path(Target, To) -> |
|
|
|
relative_path1(filename:split(filename:absname(Target)), |
|
|
|
filename:split(filename:absname(To))). |
|
|
|
|
|
|
|
relative_path1([Part|Target], [Part|To]) -> relative_path1(Target, To); |
|
|
|
relative_path1([], []) -> {ok, ""}; |
|
|
|
relative_path1(Target, []) -> {ok, filename:join(Target)}; |
|
|
|
relative_path1(_, _) -> {error, not_relative}. |
|
|
|
|
|
|
|
reduce_path(Dir) -> reduce_path([], filename:split(filename:absname(Dir))). |
|
|
|
|
|
|
|
reduce_path([], []) -> filename:nativename("/"); |
|
|
|
reduce_path(Acc, []) -> filename:join(lists:reverse(Acc)); |
|
|
|
reduce_path(Acc, ["."|Rest]) -> reduce_path(Acc, Rest); |
|
|
|
reduce_path([_|Acc], [".."|Rest]) -> reduce_path(Acc, Rest); |
|
|
|
reduce_path([], [".."|Rest]) -> reduce_path([], Rest); |
|
|
|
reduce_path(Acc, [Component|Rest]) -> reduce_path([Component|Acc], Rest). |
|
|
|
|
|
|
|
replace_src_dirs(State, Dirs) -> |
|
|
|
%% replace any `src_dirs` with the test dirs |
|
|
|
ErlOpts = rebar_state:get(State, erl_opts, []), |
|
|
|
StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts), |
|
|
|
rebar_state:set(State, erl_opts, [{src_dirs, Dirs}|StrippedOpts]). |
|
|
|
|
|
|
|
test_dirs(State, Opts) -> |
|
|
|
BareTest = filename:join([rebar_state:dir(State), "test"]), |
|
|
|
F = fun(App) -> rebar_app_info:dir(App) == rebar_state:dir(State) end, |
|
|
|
TestApps = project_apps(State), |
|
|
|
case filelib:is_dir(BareTest) andalso not lists:any(F, TestApps) of |
|
|
|
%% `test` dir at root of project is already scheduled to be |
|
|
|
%% included or `test` does not exist |
|
|
|
false -> application_dirs(TestApps, Opts, []); |
|
|
|
%% need to add `test` dir at root to dirs to be included |
|
|
|
true -> application_dirs(TestApps, Opts, [BareTest]) |
|
|
|
end. |
|
|
|
|
|
|
|
project_apps(State) -> |
|
|
|
filter_checkouts(rebar_state:project_apps(State)). |
|
|
|
|
|
|
|
filter_checkouts(Apps) -> filter_checkouts(Apps, []). |
|
|
|
|
|
|
|
filter_checkouts([], Acc) -> lists:reverse(Acc); |
|
|
|
filter_checkouts([App|Rest], Acc) -> |
|
|
|
case rebar_app_info:is_checkout(App) of |
|
|
|
true -> filter_checkouts(Rest, Acc); |
|
|
|
false -> filter_checkouts(Rest, [App|Acc]) |
|
|
|
end. |
|
|
|
|
|
|
|
application_dirs([], Opts, []) -> Opts; |
|
|
|
application_dirs([], Opts, [Acc]) -> [{dir, Acc}|Opts]; |
|
|
|
application_dirs([], Opts, Acc) -> [{dir, lists:reverse(Acc)}|Opts]; |
|
|
|
application_dirs([App|Rest], Opts, Acc) -> |
|
|
|
TestDir = filename:join([rebar_app_info:dir(App), "test"]), |
|
|
|
case filelib:is_dir(TestDir) of |
|
|
|
true -> application_dirs(Rest, Opts, [TestDir|Acc]); |
|
|
|
false -> application_dirs(Rest, Opts, Acc) |
|
|
|
end. |
|
|
|
|
|
|
|
setup_logdir(State, Opts) -> |
|
|
|
Logdir = case proplists:get_value(logdir, Opts) of |
|
|
|
undefined -> filename:join([rebar_dir:base_dir(State), "logs"]); |
|
|
|
Dir -> Dir |
|
|
|
end, |
|
|
|
ensure_dir([Logdir]), |
|
|
|
[{logdir, Logdir}|lists:keydelete(logdir, 1, Opts)]. |
|
|
|
|
|
|
|
ensure_dir([]) -> ok; |
|
|
|
ensure_dir([Dir|Rest]) -> |
|
|
|
case ec_file:is_dir(Dir) of |
|
|
|
true -> |
|
|
|
ok; |
|
|
|
false -> |
|
|
|
ec_file:mkdir_path(Dir) |
|
|
|
end, |
|
|
|
ensure_dir(Rest). |
|
|
|
|
|
|
|
maybe_cover_compile(State, Dir) -> |
|
|
|
{Opts, _} = rebar_state:command_parsed_args(State), |
|
|
|
State1 = case proplists:get_value(cover, Opts, false) of |
|
|
|
true -> rebar_state:set(State, cover_enabled, true); |
|
|
|
false -> State |
|
|
|
end, |
|
|
|
rebar_prv_cover:maybe_cover_compile(State1, [Dir]). |
|
|
|
|
|
|
|
ct_opts(_State) -> |
|
|
|
DefaultLogsDir = filename:join([rebar_dir:get_cwd(), "_build", "logs"]), |
|
|
|
[{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list |
|
|
|
{suite, undefined, "suite", string, help(suite)}, %% comma-seperated list |
|
|
|
{group, undefined, "group", string, help(group)}, %% comma-seperated list |
|
|
@ -95,13 +394,13 @@ ct_opts(_State) -> |
|
|
|
{config, undefined, "config", string, help(config)}, %% comma-seperated list |
|
|
|
{userconfig, undefined, "userconfig", string, help(userconfig)}, %% [{CallbackMod, CfgStrings}] | {CallbackMod, CfgStrings} |
|
|
|
{allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool |
|
|
|
{logdir, undefined, "logdir", {string, DefaultLogsDir}, help(logdir)}, %% dir |
|
|
|
{logdir, undefined, "logdir", string, help(logdir)}, %% dir |
|
|
|
{logopts, undefined, "logopts", string, help(logopts)}, %% enum, no_nl | no_src |
|
|
|
{verbosity, undefined, "verbosity", string, help(verbosity)}, %% Integer OR [{Category, VLevel}] |
|
|
|
{silent_connections, undefined, "silent_connections", string, |
|
|
|
help(silent_connections)}, % all OR %% comma-seperated list |
|
|
|
{stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% file |
|
|
|
{cover, $c, "cover", boolean, help(cover)}, |
|
|
|
{cover, $c, "cover", {boolean, false}, help(cover)}, |
|
|
|
{cover_spec, undefined, "cover_spec", string, help(cover_spec)}, %% file |
|
|
|
{cover_stop, undefined, "cover_stop", boolean, help(cover_stop)}, %% Boolean |
|
|
|
{event_handler, undefined, "event_handler", string, help(event_handler)}, %% EH | [EH] WHERE EH atom() | {atom(), InitArgs} | {[atom()], InitArgs} |
|
|
@ -118,6 +417,7 @@ ct_opts(_State) -> |
|
|
|
{force_stop, undefined, "force_stop", string, help(force_stop)}, % enum: skip_rest, bool |
|
|
|
{basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Booloean |
|
|
|
{ct_hooks, undefined, "ct_hooks", string, help(ct_hooks)}, %% List: [CTHModule | {CTHModule, CTHInitArgs}] where CTHModule is atom CthInitArgs is term |
|
|
|
{auto_compile, undefined, "auto_compile", {boolean, false}, help(auto_compile)}, |
|
|
|
{verbose, $v, "verbose", boolean, help(verbose)} |
|
|
|
]. |
|
|
|
|
|
|
@ -131,42 +431,26 @@ help(testcase) -> |
|
|
|
"List of test cases to run"; |
|
|
|
help(spec) -> |
|
|
|
"List of test specs to run"; |
|
|
|
help(join_specs) -> |
|
|
|
""; %% ?? |
|
|
|
help(label) -> |
|
|
|
"Test label"; |
|
|
|
help(config) -> |
|
|
|
"List of config files"; |
|
|
|
help(allow_user_terms) -> |
|
|
|
""; %% ?? |
|
|
|
help(logdir) -> |
|
|
|
"Log folder"; |
|
|
|
help(logopts) -> |
|
|
|
""; %% ?? |
|
|
|
help(verbosity) -> |
|
|
|
"Verbosity"; |
|
|
|
help(silent_connections) -> |
|
|
|
""; %% ?? |
|
|
|
help(stylesheet) -> |
|
|
|
"Stylesheet to use for test results"; |
|
|
|
help(cover) -> |
|
|
|
"Generate cover data"; |
|
|
|
help(cover_spec) -> |
|
|
|
"Cover file to use"; |
|
|
|
help(cover_stop) -> |
|
|
|
""; %% ?? |
|
|
|
help(event_handler) -> |
|
|
|
"Event handlers to attach to the runner"; |
|
|
|
help(include) -> |
|
|
|
"Include folder"; |
|
|
|
help(abort_if_missing_suites) -> |
|
|
|
"Abort if suites are missing"; |
|
|
|
help(multiply_timetraps) -> |
|
|
|
""; %% ?? |
|
|
|
help(scale_timetraps) -> |
|
|
|
""; %% ?? |
|
|
|
help(create_priv_dir) -> |
|
|
|
""; %% ?? |
|
|
|
help(repeat) -> |
|
|
|
"How often to repeat tests"; |
|
|
|
help(duration) -> |
|
|
@ -177,12 +461,10 @@ help(force_stop) -> |
|
|
|
"Force stop after time"; |
|
|
|
help(basic_html) -> |
|
|
|
"Show basic HTML"; |
|
|
|
help(ct_hooks) -> |
|
|
|
""; |
|
|
|
help(userconfig) -> |
|
|
|
""; |
|
|
|
help(verbose) -> |
|
|
|
"Verbose output". |
|
|
|
"Verbose output"; |
|
|
|
help(_) -> |
|
|
|
"". |
|
|
|
|
|
|
|
transform_opts(Opts) -> |
|
|
|
transform_opts(Opts, []). |
|
|
@ -255,210 +537,4 @@ parse_term(String) -> |
|
|
|
Terms; |
|
|
|
Term -> |
|
|
|
Term |
|
|
|
end. |
|
|
|
|
|
|
|
filter_checkouts(Apps) -> filter_checkouts(Apps, []). |
|
|
|
|
|
|
|
filter_checkouts([], Acc) -> lists:reverse(Acc); |
|
|
|
filter_checkouts([App|Rest], Acc) -> |
|
|
|
AppDir = filename:absname(rebar_app_info:dir(App)), |
|
|
|
CheckoutsDir = filename:absname("_checkouts"), |
|
|
|
case lists:prefix(CheckoutsDir, AppDir) of |
|
|
|
true -> filter_checkouts(Rest, Acc); |
|
|
|
false -> filter_checkouts(Rest, [App|Acc]) |
|
|
|
end. |
|
|
|
|
|
|
|
create_dirs(Opts) -> |
|
|
|
LogDir = proplists:get_value(logdir, Opts), |
|
|
|
ensure_dir([LogDir]), |
|
|
|
ok. |
|
|
|
|
|
|
|
ensure_dir([]) -> ok; |
|
|
|
ensure_dir([Dir|Rest]) -> |
|
|
|
case ec_file:is_dir(Dir) of |
|
|
|
true -> |
|
|
|
ok; |
|
|
|
false -> |
|
|
|
ec_file:mkdir_path(Dir) |
|
|
|
end, |
|
|
|
ensure_dir(Rest). |
|
|
|
|
|
|
|
in_dirs(State, Opts) -> |
|
|
|
%% preserve the override nature of command line opts by only checking |
|
|
|
%% `rebar.config` defined additional test dirs if none are defined via |
|
|
|
%% command line flag |
|
|
|
case proplists:get_value(dir, Opts) of |
|
|
|
undefined -> |
|
|
|
CTOpts = rebar_state:get(State, ct_opts, []), |
|
|
|
proplists:get_value(dir, CTOpts, []); |
|
|
|
Dirs -> split_string(Dirs) |
|
|
|
end. |
|
|
|
|
|
|
|
test_dirs(State, TestApps) -> |
|
|
|
%% we need to add "./ebin" if it exists but only if it's not already |
|
|
|
%% due to be added |
|
|
|
F = fun(App) -> rebar_app_info:dir(App) =/= rebar_dir:get_cwd() end, |
|
|
|
BareEbin = filename:join([rebar_dir:base_dir(State), "ebin"]), |
|
|
|
case lists:any(F, TestApps) andalso filelib:is_dir(BareEbin) of |
|
|
|
false -> application_dirs(TestApps, []); |
|
|
|
true -> [BareEbin|application_dirs(TestApps, [])] |
|
|
|
end. |
|
|
|
|
|
|
|
application_dirs([], Acc) -> lists:reverse(Acc); |
|
|
|
application_dirs([App|Rest], Acc) -> |
|
|
|
application_dirs(Rest, [rebar_app_info:ebin_dir(App)|Acc]). |
|
|
|
|
|
|
|
test_state(State) -> |
|
|
|
TestOpts = case rebar_state:get(State, ct_compile_opts, []) of |
|
|
|
[] -> []; |
|
|
|
Opts -> [{erl_opts, Opts}] |
|
|
|
end, |
|
|
|
[first_files(State)|TestOpts]. |
|
|
|
|
|
|
|
first_files(State) -> |
|
|
|
CTFirst = rebar_state:get(State, ct_first_files, []), |
|
|
|
{erl_first_files, CTFirst}. |
|
|
|
|
|
|
|
resolve_ct_opts(State, TestApps, CmdLineOpts) -> |
|
|
|
CTOpts = rebar_state:get(State, ct_opts, []), |
|
|
|
Opts = lists:ukeymerge(1, |
|
|
|
lists:ukeysort(1, CmdLineOpts), |
|
|
|
lists:ukeysort(1, CTOpts)), |
|
|
|
TestDirs = test_dirs(State, TestApps), |
|
|
|
try resolve_ct_opts(TestDirs, Opts) of |
|
|
|
Opts2 -> |
|
|
|
%% disable `auto_compile` |
|
|
|
{ok, [{auto_compile, false} | Opts2]} |
|
|
|
catch |
|
|
|
throw:{error, Reason}-> |
|
|
|
{error, {error_processing_options, Reason}} |
|
|
|
end. |
|
|
|
|
|
|
|
resolve_ct_opts(Dirs, Opts) -> |
|
|
|
Opts2 = lists:keydelete(dir, 1, Opts), |
|
|
|
case lists:keytake(suite, 1, Opts2) of |
|
|
|
{value, {suite, Suites}, Opts3} -> |
|
|
|
%% Find full path to suites so that test names are consistent with |
|
|
|
%% names when testing all dirs. |
|
|
|
Suites2 = [resolve_suite(Dirs, Suite) || Suite <- Suites], |
|
|
|
[{suite, Suites2} | Opts3]; |
|
|
|
false -> |
|
|
|
%% No suites, test all dirs. |
|
|
|
[{dir, Dirs} | Opts2] |
|
|
|
end. |
|
|
|
|
|
|
|
resolve_suite(Dirs, Suite) -> |
|
|
|
File = Suite ++ code:objfile_extension(), |
|
|
|
case [Path || Dir <- Dirs, |
|
|
|
Path <- [filename:join(Dir, File)], |
|
|
|
filelib:is_file(Path)] of |
|
|
|
[Suite2] -> |
|
|
|
Suite2; |
|
|
|
[] -> |
|
|
|
throw({error, {unknown_suite, File}}); |
|
|
|
Suites -> |
|
|
|
throw({error, {duplicate_suites, Suites}}) |
|
|
|
end. |
|
|
|
|
|
|
|
compile_tests(State, TestApps, InDirs) -> |
|
|
|
F = fun(AppInfo) -> |
|
|
|
AppDir = rebar_app_info:dir(AppInfo), |
|
|
|
S = case rebar_app_info:state(AppInfo) of |
|
|
|
undefined -> |
|
|
|
C = rebar_config:consult(AppDir), |
|
|
|
rebar_state:new(State, C, AppDir); |
|
|
|
AppState -> |
|
|
|
AppState |
|
|
|
end, |
|
|
|
ok = rebar_erlc_compiler:compile(replace_src_dirs(S, ["test"]), |
|
|
|
ec_cnv:to_list(rebar_app_info:out_dir(AppInfo))) |
|
|
|
end, |
|
|
|
lists:foreach(F, TestApps), |
|
|
|
compile_extra_tests(State, TestApps, InDirs). |
|
|
|
|
|
|
|
%% extra directories containing tests can be passed to ct via the `dir` option |
|
|
|
compile_extra_tests(State, TestApps, InDirs) -> |
|
|
|
F = fun(App) -> rebar_app_info:dir(App) == rebar_dir:get_cwd() end, |
|
|
|
TestDirs = case lists:filter(F, TestApps) of |
|
|
|
%% add `test` to indirs if it exists at the root of the project and |
|
|
|
%% it hasn't already been compiled |
|
|
|
[] -> ["test"|InDirs]; |
|
|
|
%% already compiled `./test` so do nothing |
|
|
|
_ -> InDirs |
|
|
|
end, |
|
|
|
%% symlink each of the extra dirs |
|
|
|
lists:foreach(fun(Dir) -> |
|
|
|
Source = filename:join([rebar_dir:get_cwd(), Dir]), |
|
|
|
Target = filename:join([rebar_dir:base_dir(State), Dir]), |
|
|
|
ok = rebar_file_utils:symlink_or_copy(Source, Target) |
|
|
|
end, TestDirs), |
|
|
|
rebar_erlc_compiler:compile(replace_src_dirs(State, TestDirs), |
|
|
|
rebar_dir:base_dir(State), |
|
|
|
filename:join([rebar_dir:base_dir(State), "ebin"])). |
|
|
|
|
|
|
|
replace_src_dirs(State, Dirs) -> |
|
|
|
%% replace any `src_dirs` with the test dirs |
|
|
|
ErlOpts = rebar_state:get(State, erl_opts, []), |
|
|
|
StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts), |
|
|
|
rebar_state:set(State, erl_opts, [{src_dirs, Dirs}|StrippedOpts]). |
|
|
|
|
|
|
|
maybe_cover_compile(State, Opts) -> |
|
|
|
State1 = case proplists:get_value(cover, Opts, false) of |
|
|
|
true -> rebar_state:set(State, cover_enabled, true); |
|
|
|
false -> State |
|
|
|
end, |
|
|
|
rebar_prv_cover:maybe_cover_compile(State1). |
|
|
|
|
|
|
|
handle_results(Results) when is_list(Results) -> |
|
|
|
Result = lists:foldl(fun sum_results/2, {0, 0, {0,0}}, Results), |
|
|
|
handle_results(Result); |
|
|
|
handle_results({_, Failed, {_, AutoSkipped}}) |
|
|
|
when Failed > 0 orelse AutoSkipped > 0 -> |
|
|
|
{error, {failures_running_tests, {Failed, AutoSkipped}}}; |
|
|
|
handle_results({error, Reason}) -> |
|
|
|
{error, {error_running_tests, Reason}}; |
|
|
|
handle_results(_) -> |
|
|
|
ok. |
|
|
|
|
|
|
|
sum_results({Passed, Failed, {UserSkipped, AutoSkipped}}, |
|
|
|
{Passed2, Failed2, {UserSkipped2, AutoSkipped2}}) -> |
|
|
|
{Passed+Passed2, Failed+Failed2, |
|
|
|
{UserSkipped+UserSkipped2, AutoSkipped+AutoSkipped2}}. |
|
|
|
|
|
|
|
handle_quiet_results(_, {error, _} = Result) -> |
|
|
|
handle_results(Result); |
|
|
|
handle_quiet_results(_, {'DOWN', _, _, _, Reason}) -> |
|
|
|
handle_results({error, Reason}); |
|
|
|
handle_quiet_results(CTOpts, Results) when is_list(Results) -> |
|
|
|
_ = [format_result(Result) || Result <- Results], |
|
|
|
case handle_results(Results) of |
|
|
|
{error, {failures_running_tests, _}} = Error -> |
|
|
|
LogDir = proplists:get_value(logdir, CTOpts), |
|
|
|
Index = filename:join([LogDir, "index.html"]), |
|
|
|
?CONSOLE("Results written to ~p.", [Index]), |
|
|
|
Error; |
|
|
|
Other -> |
|
|
|
Other |
|
|
|
end; |
|
|
|
handle_quiet_results(CTOpts, Result) -> |
|
|
|
handle_quiet_results(CTOpts, [Result]). |
|
|
|
|
|
|
|
format_result({Passed, 0, {0, 0}}) -> |
|
|
|
?CONSOLE("All ~p tests passed.", [Passed]); |
|
|
|
format_result({Passed, Failed, Skipped}) -> |
|
|
|
Format = [format_failed(Failed), format_skipped(Skipped), |
|
|
|
format_passed(Passed)], |
|
|
|
?CONSOLE("~s", [Format]). |
|
|
|
|
|
|
|
format_failed(0) -> |
|
|
|
[]; |
|
|
|
format_failed(Failed) -> |
|
|
|
io_lib:format("Failed ~p tests. ", [Failed]). |
|
|
|
|
|
|
|
format_passed(Passed) -> |
|
|
|
io_lib:format("Passed ~p tests. ", [Passed]). |
|
|
|
|
|
|
|
format_skipped({0, 0}) -> |
|
|
|
[]; |
|
|
|
format_skipped({User, Auto}) -> |
|
|
|
io_lib:format("Skipped ~p (~p, ~p) tests. ", [User+Auto, User, Auto]). |
|
|
|
end. |