%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ts=4 sw=4 et -module(rebar_prv_common_test). -behaviour(provider). -export([init/1, do/1, format_error/1]). -ifdef(TEST). %% exported for test purposes -export([compile/2, prepare_tests/1, translate_paths/2, maybe_write_coverdata/1]). -endif. -include("rebar.hrl"). -include_lib("providers/include/providers.hrl"). -define(PROVIDER, ct). %% we need to modify app_info state before compile -define(DEPS, [lock]). %% =================================================================== %% Public API %% =================================================================== -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> Provider = providers:create([{name, ?PROVIDER}, {module, ?MODULE}, {deps, ?DEPS}, {bare, true}, {example, "rebar3 ct"}, {short_desc, "Run Common Tests."}, {desc, "Run Common Tests."}, {opts, ct_opts(State)}, {profiles, [test]}]), {ok, rebar_state:add_provider(State, Provider)}. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> setup_name(State), Tests = prepare_tests(State), case compile(State, Tests) of %% successfully compiled apps {ok, S} -> {RawOpts, _} = rebar_state:command_parsed_args(S), case proplists:get_value(compile_only, RawOpts, false) of true -> {ok, S}; false -> do(S, Tests) end; %% this should look like a compiler error, not a ct error Error -> Error end. do(State, Tests) -> ?INFO("Running Common Test suites...", []), rebar_paths:set_paths([deps, plugins], State), %% Run ct provider prehooks Providers = rebar_state:providers(State), Cwd = rebar_dir:get_cwd(), %% Run ct provider pre hooks for all project apps and top level project hooks rebar_hooks:run_project_and_app_hooks(Cwd, pre, ?PROVIDER, Providers, State), case Tests of {ok, T} -> case run_tests(State, T) of ok -> %% Run ct provider post hooks for all project apps and top level project hooks rebar_hooks:run_project_and_app_hooks(Cwd, post, ?PROVIDER, Providers, State), rebar_paths:set_paths([plugins, deps], State), symlink_to_last_ct_logs(State, T), {ok, State}; Error -> rebar_paths:set_paths([plugins, deps], State), symlink_to_last_ct_logs(State, T), Error end; Error -> rebar_paths:set_paths([plugins, deps], State), Error end. run_tests(State, Opts) -> T = translate_paths(State, Opts), Opts1 = setup_logdir(State, T), Opts2 = turn_off_auto_compile(Opts1), ?DEBUG("Running tests with {ct_opts, ~p}.", [Opts2]), {RawOpts, _} = rebar_state:command_parsed_args(State), Result = case proplists:get_value(verbose, RawOpts, false) of true -> run_test_verbose(Opts2); false -> run_test_quiet(Opts2) end, ok = maybe_write_coverdata(State), Result. -spec format_error(any()) -> iolist(). format_error({error, Reason}) -> io_lib:format("Error running tests:~n ~p", [Reason]); format_error({error_running_tests, Reason}) -> format_error({error, Reason}); format_error({failures_running_tests, {Failed, AutoSkipped}}) -> io_lib:format("Failures occurred running tests: ~b", [Failed+AutoSkipped]); format_error({badconfig, {Msg, {Value, Key}}}) -> io_lib:format(Msg, [Value, Key]); format_error({badconfig, Msg}) -> io_lib:format(Msg, []); format_error({multiple_errors, Errors}) -> io_lib:format(lists:concat(["Error running tests:"] ++ lists:map(fun(Error) -> "~n " ++ Error end, Errors)), []); format_error({error_reading_testspec, Reason}) -> io_lib:format("Error reading testspec: ~p", [Reason]). %% =================================================================== %% Internal functions %% =================================================================== %% @doc Tries to make the symlink `_build//logs/last' to the `ct_run' directory %% of the last common test run. -spec symlink_to_last_ct_logs(rebar_state:t(), list()) -> ok. symlink_to_last_ct_logs(State, Opts) -> LogDir = case proplists:get_value(logdir, Opts) of undefined -> filename:join([rebar_dir:base_dir(State), "logs"]); Dir -> Dir end, {ok, Filenames} = file:list_dir(LogDir), CtRunDirs = lists:filter(fun(S) -> re:run(S, "ct_run", [unicode]) /= nomatch end, Filenames), case CtRunDirs of [] -> % If for some reason there are no such directories, we should not try to set up a link either. ok; _ -> NewestDir = lists:last(lists:sort(CtRunDirs)), Target = filename:join([LogDir, "last"]), Existing = filename:join([LogDir, NewestDir]), case rebar_file_utils:symlink_or_copy(Existing, Target) of ok -> ok; exists -> %% in case the symlink already exists we remove it %% and make a new updated one rebar_file_utils:rm_rf(Target), rebar_file_utils:symlink_or_copy(Existing, Target); Reason -> ?DIAGNOSTIC("Warning, couldn't make a symlink to ~ts, reason: ~p.", [Target, Reason]) end end. setup_name(State) -> {Long, Short, Opts} = rebar_dist_utils:find_options(State), rebar_dist_utils:either(Long, Short, Opts). prepare_tests(State) -> %% command line test options CmdOpts = cmdopts(State), %% rebar.config test options CfgOpts = cfgopts(State), ProjectApps = rebar_state:project_apps(State), %% prioritize tests to run first trying any command line specified %% tests falling back to tests specified in the config file finally %% running a default set if no other tests are present select_tests(State, ProjectApps, CmdOpts, CfgOpts). cmdopts(State) -> {RawOpts, _} = rebar_state:command_parsed_args(State), %% filter out opts common_test doesn't know about and convert %% to ct acceptable forms transform_retry(transform_opts(RawOpts, []), State). transform_opts([], Acc) -> lists:reverse(Acc); transform_opts([{dir, Dirs}|Rest], Acc) -> transform_opts(Rest, [{dir, split_string(Dirs)}|Acc]); transform_opts([{suite, Suites}|Rest], Acc) -> transform_opts(Rest, [{suite, split_string(Suites)}|Acc]); transform_opts([{group, Groups}|Rest], Acc) -> transform_opts(Rest, [{group, transform_group(Groups)}|Acc]); transform_opts([{testcase, Cases}|Rest], Acc) -> transform_opts(Rest, [{testcase, split_string(Cases)}|Acc]); transform_opts([{config, Configs}|Rest], Acc) -> transform_opts(Rest, [{config, split_string(Configs)}|Acc]); transform_opts([{spec, Specs}|Rest], Acc) -> transform_opts(Rest, [{spec, split_string(Specs)}|Acc]); transform_opts([{include, Includes}|Rest], Acc) -> transform_opts(Rest, [{include, split_string(Includes)}|Acc]); transform_opts([{logopts, LogOpts}|Rest], Acc) -> transform_opts(Rest, [{logopts, lists:map(fun(P) -> list_to_atom(P) end, split_string(LogOpts))}|Acc]); transform_opts([{force_stop, "true"}|Rest], Acc) -> transform_opts(Rest, [{force_stop, true}|Acc]); transform_opts([{force_stop, "false"}|Rest], Acc) -> transform_opts(Rest, [{force_stop, false}|Acc]); transform_opts([{force_stop, "skip_rest"}|Rest], Acc) -> transform_opts(Rest, [{force_stop, skip_rest}|Acc]); transform_opts([{create_priv_dir, CreatePrivDir}|Rest], Acc) -> transform_opts(Rest, [{create_priv_dir, list_to_atom(CreatePrivDir)}|Acc]); %% drop cover from opts, ct doesn't care about it transform_opts([{cover, _}|Rest], Acc) -> transform_opts(Rest, Acc); %% drop verbose from opts, ct doesn't care about it transform_opts([{verbose, _}|Rest], Acc) -> transform_opts(Rest, Acc); %% drop fail_fast from opts, ct doesn't care about it transform_opts([{fail_fast, _}|Rest], Acc) -> transform_opts(Rest, Acc); %% getopt should handle anything else transform_opts([Opt|Rest], Acc) -> transform_opts(Rest, [Opt|Acc]). %% @private only retry if specified and if no other spec %% is given. transform_retry(Opts, State) -> case proplists:get_value(retry, Opts, false) andalso not is_any_defined([spec,dir,suite], Opts) of false -> Opts; true -> Path = filename:join([rebar_dir:base_dir(State), "logs", "retry.spec"]), filelib:is_file(Path) andalso [{spec, Path}|Opts] end. split_string(String) -> rebar_string:lexemes(String, [$,]). transform_group(String) -> case rebar_string:consult([$[, String, $], $.]) of [Terms] when is_list(Terms) -> Terms; Terms when is_list(Terms) -> Terms; {error, _} -> %% try a normal string split split_string(String) end. cfgopts(State) -> case rebar_state:get(State, ct_opts, []) of Opts when is_list(Opts) -> ensure_opts(add_hooks(Opts, State), []); Wrong -> %% probably a single non list term ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, ct_opts}}}) end. ensure_opts([], Acc) -> lists:reverse(Acc); ensure_opts([{cover, _}|Rest], Acc) -> ?WARN("Cover specs not supported. See https://www.rebar3.org/docs/testing/ct/", []), ensure_opts(Rest, Acc); ensure_opts([{auto_compile, _}|Rest], Acc) -> ?WARN("Auto compile not supported", []), ensure_opts(Rest, Acc); ensure_opts([{suite, Suite}|Rest], Acc) when is_integer(hd(Suite)) -> ensure_opts(Rest, [{suite, Suite}|Acc]); ensure_opts([{suite, Suite}|Rest], Acc) when is_atom(Suite) -> ensure_opts(Rest, [{suite, atom_to_list(Suite)}|Acc]); ensure_opts([{suite, Suites}|Rest], Acc) -> NewSuites = {suite, lists:map(fun(S) when is_atom(S) -> atom_to_list(S); (S) when is_list(S) -> S end, Suites)}, ensure_opts(Rest, [NewSuites|Acc]); ensure_opts([{K, V}|Rest], Acc) -> ensure_opts(Rest, [{K, V}|Acc]); %% pass through other options, in case of things like config terms %% in `ct_opts` ensure_opts([V|Rest], Acc) -> ensure_opts(Rest, [V|Acc]). add_hooks(Opts, State) -> FailFast = case fails_fast(State) of true -> [cth_fail_fast]; false -> [] end, case {readable(State), lists:keyfind(ct_hooks, 1, Opts)} of {false, _} -> Opts; {Other, false} -> [{ct_hooks, [cth_readable_failonly, readable_shell_type(Other), cth_retry] ++ FailFast} | Opts]; {Other, {ct_hooks, Hooks}} -> %% Make sure hooks are there once only and add wanted hooks that are not defined yet ReadableHooks = [cth_readable_failonly, readable_shell_type(Other), cth_retry] ++ FailFast, NewHooks = Hooks ++ [ReadableHook || ReadableHook <- ReadableHooks, not is_defined(ReadableHook, Hooks) ], lists:keyreplace(ct_hooks, 1, Opts, {ct_hooks, NewHooks}) end. is_defined(_Key, []) -> false; is_defined(Key, [Key | _Hs]) -> true; is_defined(Key, [{Key, _Opts} | _Hs]) -> true; is_defined(Key, [{Key, _Opts, _Prio} | _Hs]) -> true; is_defined(Key, [_ | Hs]) -> is_defined(Key, Hs). readable_shell_type(true) -> cth_readable_shell; readable_shell_type(compact) -> cth_readable_compact_shell. select_tests(_, _, _, {error, _} = Error) -> Error; select_tests(State, ProjectApps, CmdOpts, CfgOpts) -> %% set application env if sys_config argument is provided SysConfigs = sys_config_list(CmdOpts, CfgOpts), Configs = lists:flatmap(fun(Filename) -> rebar_file_utils:consult_config(State, Filename) end, SysConfigs), %% NB: load the applications (from user directories too) to support OTP < 17 %% to our best ability. rebar_paths:set_paths([deps, plugins], State), [application:load(Application) || Config <- Configs, {Application, _} <- Config], rebar_utils:reread_config(Configs, [update_logger]), Opts = merge_opts(CmdOpts,CfgOpts), discover_tests(State, ProjectApps, Opts). %% Merge the option lists from command line and rebar.config: %% %% - Options set on the command line will replace the same options if %% set in rebar.config. %% %% - Special care is taken with options that select which tests to %% run - ANY such option on the command line will replace ALL such %% options in the config. %% %% Note that if 'spec' is given, common_test will ignore all 'dir', %% 'suite', 'group' and 'case', so there is no need to explicitly %% remove any options from the command line. %% %% All faulty combinations of options are also handled by %% common_test and are not taken into account here. merge_opts(CmdOpts0, CfgOpts0) -> TestSelectOpts = [spec,dir,suite,group,testcase], CmdOpts = lists:ukeysort(1, CmdOpts0), CfgOpts1 = lists:ukeysort(1, CfgOpts0), CfgOpts = case is_any_defined(TestSelectOpts,CmdOpts) of false -> CfgOpts1; true -> [Opt || Opt={K,_} <- CfgOpts1, not lists:member(K,TestSelectOpts)] end, lists:ukeymerge(1, CmdOpts, CfgOpts). is_any_defined([Key|Keys],Opts) -> proplists:is_defined(Key,Opts) orelse is_any_defined(Keys,Opts); is_any_defined([],_Opts) -> false. sys_config_list(CmdOpts, CfgOpts) -> CmdSysConfigs = split_string(proplists:get_value(sys_config, CmdOpts, "")), case proplists:get_value(sys_config, CfgOpts, []) of [H | _]=Configs when is_list(H) -> Configs ++ CmdSysConfigs; [] -> CmdSysConfigs; Configs -> [Configs | CmdSysConfigs] end. discover_tests(State, ProjectApps, Opts) -> case is_any_defined([spec,dir,suite],Opts) of %% no tests defined, try using `$APP/test` and `$ROOT/test` as dirs false -> {ok, default_tests(State, ProjectApps) ++ Opts}; true -> {ok, Opts} end. default_tests(State, ProjectApps) -> BareTest = filename:join([rebar_state:dir(State), "test"]), F = fun(App) -> rebar_app_info:dir(App) == rebar_state:dir(State) end, AppTests = application_dirs(ProjectApps, []), case filelib:is_dir(BareTest) andalso not lists:any(F, ProjectApps) of %% `test` dir at root of project is already scheduled to be %% included or `test` does not exist false -> %% The rest of the call-chain expects the list of tests to not be %% empty, thus we drop the parameter in that case entirely. case AppTests of [] -> []; _ -> [{dir, AppTests}] end; %% need to add `test` dir at root to dirs to be included true -> [{dir, AppTests ++ [BareTest]}] end. application_dirs([], []) -> []; application_dirs([], Acc) -> lists:reverse(Acc); application_dirs([App|Rest], Acc) -> TestDir = filename:join([rebar_app_info:dir(App), "test"]), case filelib:is_dir(TestDir) of true -> application_dirs(Rest, [TestDir|Acc]); false -> application_dirs(Rest, Acc) end. compile(State, {ok, _} = Tests) -> %% inject `ct_first_files`, `ct_compile_opts` and `include` (from `ct_opts` %% and command line options) into the applications to be compiled case inject_ct_state(State, Tests) of {ok, NewState} -> do_compile(NewState); Error -> Error end; %% maybe compile even in the face of errors? compile(_State, Error) -> Error. do_compile(State) -> ?DEBUG("Re-compiling the project under the test profile with CT options injected...", []), {ok, S} = rebar_prv_compile:do(State), ok = maybe_cover_compile(S), {ok, S}. inject_ct_state(State, {ok, Tests}) -> Apps = rebar_state:project_apps(State), case inject_ct_state(State, Tests, Apps, []) of {ok, {NewState, ModdedApps}} -> test_dirs(NewState, ModdedApps, Tests); {error, _} = Error -> Error end. inject_ct_state(State, Tests, [App|Rest], Acc) -> case inject(rebar_app_info:opts(App), State, Tests) of {error, _} = Error -> Error; NewOpts -> NewApp = rebar_app_info:opts(App, NewOpts), inject_ct_state(State, Tests, Rest, [NewApp|Acc]) end; inject_ct_state(State, Tests, [], Acc) -> case inject(rebar_state:opts(State), State, Tests) of {error, _} = Error -> Error; NewOpts -> {ok, {rebar_state:opts(State, NewOpts), lists:reverse(Acc)}} end. opts(Opts, Key, Default) -> case rebar_opts:get(Opts, Key, Default) of Vs when is_list(Vs) -> Vs; Wrong -> ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, Key}}}) end. inject(Opts, State, Tests) -> erl_opts(Opts, State, Tests). erl_opts(Opts, State, Tests) -> %% append `ct_compile_opts` to app defined `erl_opts` ErlOpts = opts(Opts, erl_opts, []), CTOpts = opts(Opts, ct_compile_opts, []), case add_transforms(append(CTOpts, ErlOpts), State) of {error, _} = Error -> Error; NewErlOpts -> first_files(rebar_opts:set(Opts, erl_opts, NewErlOpts), Tests) end. first_files(Opts, Tests) -> %% append `ct_first_files` to app defined `erl_first_files` FirstFiles = opts(Opts, erl_first_files, []), CTFirstFiles = opts(Opts, ct_first_files, []), case append(CTFirstFiles, FirstFiles) of {error, _} = Error -> Error; NewFirstFiles -> include_files(rebar_opts:set(Opts, erl_first_files, NewFirstFiles), Tests) end. include_files(Opts, Tests) -> %% append include dirs from command line and `ct_opts` to app defined %% `erl_opts` ErlOpts = opts(Opts, erl_opts, []), Includes = proplists:get_value(include, Tests, []), Is = lists:map(fun(I) -> {i, I} end, Includes), case append(Is, ErlOpts) of {error, _} = Error -> Error; NewIncludes -> ct_macro(rebar_opts:set(Opts, erl_opts, NewIncludes)) end. ct_macro(Opts) -> ErlOpts = opts(Opts, erl_opts, []), NewOpts = safe_define_ct_macro(ErlOpts), rebar_opts:set(Opts, erl_opts, NewOpts). safe_define_ct_macro(Opts) -> %% defining a compile macro twice results in an exception so %% make sure 'COMMON_TEST' is only defined once case test_defined(Opts) of true -> Opts; false -> [{d, 'COMMON_TEST'}|Opts] end. test_defined([{d, 'COMMON_TEST'}|_]) -> true; test_defined([{d, 'COMMON_TEST', true}|_]) -> true; test_defined([_|Rest]) -> test_defined(Rest); test_defined([]) -> false. append({error, _} = Error, _) -> Error; append(_, {error, _} = Error) -> Error; append(A, B) -> A ++ B. add_transforms(CTOpts, State) when is_list(CTOpts) -> case readable(State) of false -> CTOpts; Other when Other == true; Other == compact -> ReadableTransform = [{parse_transform, cth_readable_transform}], (CTOpts -- ReadableTransform) ++ ReadableTransform end; add_transforms({error, _} = Error, _State) -> Error. readable(State) -> {RawOpts, _} = rebar_state:command_parsed_args(State), case proplists:get_value(readable, RawOpts) of "true" -> true; "false" -> false; "compact" -> compact; undefined -> rebar_state:get(State, ct_readable, compact) end. fails_fast(State) -> {RawOpts, _} = rebar_state:command_parsed_args(State), proplists:get_value(fail_fast, RawOpts) == true. test_dirs(State, Apps, Opts) -> case proplists:get_value(spec, Opts) of undefined -> case {proplists:get_value(suite, Opts), proplists:get_value(dir, Opts)} of {undefined, undefined} -> {ok, rebar_state:project_apps(State, Apps)}; {Suites, undefined} -> set_compile_dirs(State, Apps, {suite, Suites}); {undefined, Dirs} -> set_compile_dirs(State, Apps, {dir, Dirs}); {Suites, Dir} when is_integer(hd(Dir)) -> set_compile_dirs(State, Apps, join(Suites, Dir)); {Suites, [Dir]} when is_integer(hd(Dir)) -> set_compile_dirs(State, Apps, join(Suites, Dir)); {_Suites, _Dirs} -> {error, "Only a single directory may be specified when specifying suites"} end; Spec when is_integer(hd(Spec)) -> spec_test_dirs(State, Apps, [Spec]); Specs -> spec_test_dirs(State, Apps, Specs) end. spec_test_dirs(State, Apps, Specs0) -> case get_dirs_from_specs(Specs0) of {ok,{Specs,SuiteDirs}} -> {State1,Apps1} = set_compile_dirs1(State, Apps, {dir, SuiteDirs}), {State2,Apps2} = set_compile_dirs1(State1, Apps1, {spec, Specs}), [maybe_copy_spec(State2,Apps2,S) || S <- Specs], {ok, rebar_state:project_apps(State2, Apps2)}; Error -> Error end. join(Suite, Dir) when is_integer(hd(Suite)) -> {suite, [filename:join([Dir, Suite])]}; join(Suites, Dir) -> {suite, lists:map(fun(S) -> filename:join([Dir, S]) end, Suites)}. set_compile_dirs(State, Apps, What) -> {NewState,NewApps} = set_compile_dirs1(State, Apps, What), {ok, rebar_state:project_apps(NewState, NewApps)}. set_compile_dirs1(State, Apps, {dir, Dir}) when is_integer(hd(Dir)) -> %% single directory %% insert `Dir` into an app if relative, or the base state if not %% app relative but relative to the root or not at all if outside %% project scope maybe_inject_test_dir(State, [], Apps, Dir); set_compile_dirs1(State, Apps, {dir, Dirs}) -> %% multiple directories F = fun(Dir, {S, A}) -> maybe_inject_test_dir(S, [], A, Dir) end, lists:foldl(F, {State, Apps}, Dirs); set_compile_dirs1(State, Apps, {Type, Files}) when Type==spec; Type==suite -> %% specs or suites with dir component Dirs = find_file_dirs(Files), F = fun(Dir, {S, A}) -> maybe_inject_test_dir(S, [], A, Dir) end, lists:foldl(F, {State, Apps}, Dirs). find_file_dirs(Files) -> AllDirs = lists:map(fun(F) -> filename:dirname(filename:absname(F)) end, Files), %% eliminate duplicates lists:usort(AllDirs). maybe_inject_test_dir(State, AppAcc, [App|Rest], Dir) -> case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of {ok, []} -> %% normal operation involves copying the entire directory a %% suite exists in but if the suite is in the app root directory %% the current compiler tries to compile all subdirs including priv %% instead copy only files ending in `.erl' and directories %% ending in `_SUITE_data' into the `_build/PROFILE/lib/APP' dir ok = copy_bare_suites(Dir, rebar_app_info:out_dir(App)), Opts = inject_test_dir(rebar_state:opts(State), rebar_app_info:out_dir(App)), {rebar_state:opts(State, Opts), AppAcc ++ [App]}; {ok, Path} -> Opts = inject_test_dir(rebar_app_info:opts(App), Path), {State, AppAcc ++ [rebar_app_info:opts(App, Opts)] ++ Rest}; {error, badparent} -> maybe_inject_test_dir(State, AppAcc ++ [App], Rest, Dir) end; maybe_inject_test_dir(State, AppAcc, [], Dir) -> case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of {ok, []} -> %% normal operation involves copying the entire directory a %% suite exists in but if the suite is in the root directory %% that results in a loop as we copy `_build' into itself %% instead copy only files ending in `.erl' and directories %% ending in `_SUITE_data' in the `_build/PROFILE/extras' dir ExtrasDir = filename:join([rebar_dir:base_dir(State), "extras"]), ok = copy_bare_suites(Dir, ExtrasDir), Opts = inject_test_dir(rebar_state:opts(State), ExtrasDir), {rebar_state:opts(State, Opts), AppAcc}; {ok, Path} -> Opts = inject_test_dir(rebar_state:opts(State), Path), {rebar_state:opts(State, Opts), AppAcc}; {error, badparent} -> {State, AppAcc} end. copy_bare_suites(From, To) -> filelib:ensure_dir(filename:join([To, "dummy.txt"])), SrcFiles = rebar_utils:find_files(From, ".*\\.[e|h]rl\$", false), DataDirs = lists:filter(fun filelib:is_dir/1, filelib:wildcard(filename:join([From, "*_SUITE_data"]))), ok = rebar_file_utils:cp_r(SrcFiles, To), rebar_file_utils:cp_r(DataDirs, To). maybe_copy_spec(State, [App|Apps], Spec) -> case rebar_file_utils:path_from_ancestor(filename:dirname(Spec), rebar_app_info:dir(App)) of {ok, []} -> ok = rebar_file_utils:cp_r([Spec],rebar_app_info:out_dir(App)); {ok,_} -> ok; {error,badparent} -> maybe_copy_spec(State, Apps, Spec) end; maybe_copy_spec(State, [], Spec) -> case rebar_file_utils:path_from_ancestor(filename:dirname(Spec), rebar_state:dir(State)) of {ok, []} -> ExtrasDir = filename:join([rebar_dir:base_dir(State), "extras"]), ok = rebar_file_utils:cp_r([Spec],ExtrasDir); _R -> ok end. inject_test_dir(Opts, Dir) -> %% append specified test targets to app defined `extra_src_dirs` ExtraSrcDirs = rebar_opts:get(Opts, extra_src_dirs, []), rebar_opts:set(Opts, extra_src_dirs, ExtraSrcDirs ++ [Dir]). get_dirs_from_specs(Specs) -> case get_tests_from_specs(Specs) of {ok,Tests} -> {SpecLists,NodeRunSkipLists} = lists:unzip(Tests), SpecList = lists:append(SpecLists), NodeRunSkipList = lists:append(NodeRunSkipLists), RunList = lists:append([R || {_,R,_} <- NodeRunSkipList]), DirList = [element(1,R) || R <- RunList], {ok,{SpecList,DirList}}; {error,Reason} -> {error,{?MODULE,{error_reading_testspec,Reason}}} end. get_tests_from_specs(Specs) -> _ = ct_testspec:module_info(), % make sure ct_testspec is loaded case erlang:function_exported(ct_testspec,get_tests,1) of true -> ct_testspec:get_tests(Specs); false -> case ct_testspec:collect_tests_from_file(Specs,true) of Tests when is_list(Tests) -> {ok,[{S,ct_testspec:prepare_tests(R)} || {S,R} <- Tests]}; Error -> Error end end. translate_paths(State, Opts) -> case proplists:get_value(spec, Opts) of undefined -> case {proplists:get_value(suite, Opts), proplists:get_value(dir, Opts)} of {_Suites, undefined} -> translate_paths(State, suite, Opts, []); {undefined, _Dirs} -> translate_paths(State, dir, Opts, []); %% both dirs and suites are defined, only translate dir paths _ -> translate_paths(State, dir, Opts, []) end; _Specs -> translate_paths(State, spec, Opts, []) end. translate_paths(_State, _Type, [], Acc) -> lists:reverse(Acc); translate_paths(State, Type, [{Type, Val}|Rest], Acc) when is_integer(hd(Val)) -> %% single file or dir translate_paths(State, Type, [{Type, [Val]}|Rest], Acc); translate_paths(State, Type, [{Type, Files}|Rest], Acc) -> Apps = rebar_state:project_apps(State), New = {Type, lists:map(fun(File) -> translate(State, Apps, File) end, Files)}, translate_paths(State, Type, Rest, [New|Acc]); translate_paths(State, Type, [Test|Rest], Acc) -> translate_paths(State, Type, Rest, [Test|Acc]). translate(State, [App|Rest], Path) -> case rebar_file_utils:path_from_ancestor(Path, rebar_app_info:dir(App)) of {ok, P} -> filename:join([rebar_app_info:out_dir(App), P]); {error, badparent} -> translate(State, Rest, Path) end; translate(State, [], Path) -> case rebar_file_utils:path_from_ancestor(Path, rebar_state:dir(State)) of {ok, P} -> filename:join([rebar_dir:base_dir(State), "extras", P]); %% not relative, leave as is {error, badparent} -> Path end. -spec handle_keep_logs(file:filename(), pos_integer()) -> ok. handle_keep_logs(LogDir, N) -> case file:list_dir(LogDir) of {ok, Filenames} -> Dirs = lists:filter(fun(File) -> filelib:is_dir(filename:join([LogDir, File])) end, Filenames) -- ["last"], %% we ignore the symlink as we later handle it case Dirs of %% first time running the tests, there are no logs to delete [] -> ok; %% during the next run we would crash because of keep_logs _ when length(Dirs) >= N -> SortedDirs = lists:reverse(lists:sort(Dirs)), %% sort the log dirs and keep the N - 1 newest {_Keep, Discard} = lists:split(N - 1, SortedDirs), ?DEBUG("Removing the following directories because keep_logs is in ct_opts: ~p", [Discard]), [rebar_file_utils:rm_rf(filename:join([LogDir, Dir])) || Dir <- Discard], ok; %% we still dont have enough log run directories as to crash _ -> ok end; _ -> ok 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, filelib:ensure_dir(filename:join([Logdir, "dummy.beam"])), case proplists:get_value(keep_logs, Opts) of all -> ok; undefined -> ok; N -> handle_keep_logs(Logdir, N) end, [{logdir, Logdir}|lists:keydelete(logdir, 1, Opts)]. turn_off_auto_compile(Opts) -> [{auto_compile, false}|lists:keydelete(auto_compile, 1, Opts)]. run_test_verbose(Opts) -> handle_results(ct:run_test(Opts)). run_test_quiet(Opts) -> Pid = self(), Ref = erlang:make_ref(), LogDir = proplists:get_value(logdir, Opts), {_, Monitor} = erlang:spawn_monitor(fun() -> {ok, F} = file:open(filename:join([LogDir, "ct.latest.log"]), [write]), true = group_leader(F, self()), Pid ! {Ref, ct:run_test(Opts)} end), receive {Ref, Result} -> handle_quiet_results(Opts, Result); {'DOWN', Monitor, _, _, Reason} -> handle_results(?PRV_ERROR(Reason)) 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}}; sum_results(_, {error, Reason}) -> {error, Reason}; sum_results(Unknown, _) -> {error, Unknown}. handle_quiet_results(_, {error, _} = Result) -> handle_results(Result); 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("~ts", [Format]); format_result(_Unknown) -> %% Happens when CT itself encounters a bug ok. 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]). maybe_cover_compile(State) -> {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, rebar_prv_cover:maybe_cover_compile(State1). maybe_write_coverdata(State) -> {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, Name = proplists:get_value(cover_export_name, RawOpts, ?PROVIDER), rebar_prv_cover:maybe_write_coverdata(State1, Name). ct_opts(_State) -> [{dir, undefined, "dir", string, help(dir)}, %% comma-separated list {suite, undefined, "suite", string, help(suite)}, %% comma-separated list {group, undefined, "group", string, help(group)}, %% comma-separated list {testcase, undefined, "case", string, help(testcase)}, %% comma-separated list {label, undefined, "label", string, help(label)}, %% String {config, undefined, "config", string, help(config)}, %% comma-separated list {spec, undefined, "spec", string, help(spec)}, %% comma-separated list {join_specs, undefined, "join_specs", boolean, help(join_specs)}, {allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool {logdir, undefined, "logdir", string, help(logdir)}, %% dir {logopts, undefined, "logopts", string, help(logopts)}, %% comma-separated list {verbosity, undefined, "verbosity", integer, help(verbosity)}, %% Integer {cover, $c, "cover", {boolean, false}, help(cover)}, {cover_export_name, undefined, "cover_export_name", string, help(cover_export_name)}, {repeat, undefined, "repeat", integer, help(repeat)}, %% integer {duration, undefined, "duration", string, help(duration)}, % format: HHMMSS {until, undefined, "until", string, help(until)}, %% format: YYMoMoDD[HHMMSS] {force_stop, undefined, "force_stop", string, help(force_stop)}, %% String {basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Boolean {stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% String {decrypt_key, undefined, "decrypt_key", string, help(decrypt_key)}, %% String {decrypt_file, undefined, "decrypt_file", string, help(decrypt_file)}, %% String {abort_if_missing_suites, undefined, "abort_if_missing_suites", {boolean, true}, help(abort_if_missing_suites)}, %% Boolean {multiply_timetraps, undefined, "multiply_timetraps", integer, help(multiple_timetraps)}, %% Integer {scale_timetraps, undefined, "scale_timetraps", boolean, help(scale_timetraps)}, {create_priv_dir, undefined, "create_priv_dir", string, help(create_priv_dir)}, {include, undefined, "include", string, help(include)}, {readable, undefined, "readable", string, help(readable)}, {verbose, $v, "verbose", boolean, help(verbose)}, {name, undefined, "name", atom, help(name)}, {sname, undefined, "sname", atom, help(sname)}, {setcookie, undefined, "setcookie", atom, help(setcookie)}, {sys_config, undefined, "sys_config", string, help(sys_config)}, %% comma-separated list {compile_only, undefined, "compile_only", boolean, help(compile_only)}, {retry, undefined, "retry", boolean, help(retry)}, {fail_fast, undefined, "fail_fast", {boolean, false}, help(fail_fast)} ]. help(compile_only) -> "Compile modules in the project with the test configuration but do not run the tests"; help(dir) -> "List of additional directories containing test suites"; help(suite) -> "List of test suites to run"; help(group) -> "List of test groups to run"; help(testcase) -> "List of test cases to run"; help(label) -> "Test label"; help(config) -> "List of config files"; help(spec) -> "List of test specifications"; help(join_specs) -> "Merge all test specifications and perform a single test run"; help(sys_config) -> "List of application config files"; help(allow_user_terms) -> "Allow user defined config values in config files"; help(logdir) -> "Log folder"; help(logopts) -> "Options for common test logging"; help(verbosity) -> "Verbosity"; help(cover) -> "Generate cover data"; help(cover_export_name) -> "Base name of the coverdata file to write"; help(repeat) -> "How often to repeat tests"; help(duration) -> "Max runtime (format: HHMMSS)"; help(until) -> "Run until (format: HHMMSS)"; help(force_stop) -> "Force stop on test timeout (true | false | skip_rest)"; help(basic_html) -> "Show basic HTML"; help(stylesheet) -> "CSS stylesheet to apply to html output"; help(decrypt_key) -> "Path to key for decrypting config"; help(decrypt_file) -> "Path to file containing key for decrypting config"; help(abort_if_missing_suites) -> "Abort if suites are missing"; help(multiply_timetraps) -> "Multiply timetraps"; help(scale_timetraps) -> "Scale timetraps"; help(create_priv_dir) -> "Create priv dir (auto_per_run | auto_per_tc | manual_per_tc)"; help(include) -> "Directories containing additional include files"; help(readable) -> "Shows test case names and only displays logs to shell on failures (true | compact | false)"; help(verbose) -> "Verbose output"; help(name) -> "Gives a long name to the node"; help(sname) -> "Gives a short name to the node"; help(setcookie) -> "Sets the cookie if the node is distributed"; help(retry) -> "Experimental feature. If any specification for previously failing test is found, runs them."; help(fail_fast) -> "Experimental feature. If any test fails, the run is aborted. Since common test does not " "support this natively, we abort the rebar3 run on a failure. This May break CT's disk logging and " "other rebar3 features."; help(_) -> "".