%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2004-2023. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% -module(ct_run). %% Script interface -export([script_start/0,script_usage/0]). %% User interface -export([install/1,install/2,run/1,run/2,run/3,run_test/1, run_testspec/1,step/3,step/4,refresh_logs/1]). %% Misc internal API functions -export([variables_file_name/1,script_start1/2,run_test2/1, run_make/3]). -include("ct.hrl"). -include("ct_event.hrl"). -include("ct_util.hrl"). -define(abs(Name), filename:absname(Name)). -define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)). -define(EXIT_STATUS_TEST_SUCCESSFUL, 0). -define(EXIT_STATUS_TEST_CASE_FAILED, 1). -define(EXIT_STATUS_TEST_RUN_FAILED, 2). -define(default_verbosity, [{default,?MAX_VERBOSITY}, {'$unspecified',?MAX_VERBOSITY}]). -record(opts, {label, profile, shell, cover, cover_stop, coverspec, step, logdir, logopts = [], basic_html, esc_chars = true, verbosity = [], config = [], event_handlers = [], ct_hooks = [], enable_builtin_hooks, include = [], auto_compile, abort_if_missing_suites, silent_connections = [], stylesheet, multiply_timetraps, scale_timetraps, create_priv_dir, testspec_files = [], current_testspec, tests, starter}). script_start() -> process_flag(trap_exit, true), Init = init:get_arguments(), CtArgs = lists:takewhile(fun({ct_erl_args,_}) -> false; (_) -> true end, Init), %% convert relative dirs added with pa or pz (pre erl_args on %% the ct_run command line) to absolute so that app modules %% can be found even after CT changes CWD to logdir rel_to_abs(CtArgs), Args = case application:get_env(common_test, run_test_start_opts) of {ok,EnvStartOpts} -> FlagFilter = fun(Flags) -> lists:filter(fun({root,_}) -> false; ({progname,_}) -> false; ({home,_}) -> false; ({noshell,_}) -> false; ({noinput,_}) -> false; (_) -> true end, Flags) end, %% used for purpose of testing the run_test interface io:format(user, "~n-------------------- START ARGS " "--------------------~n", []), io:format(user, "--- Init args:~n~tp~n", [FlagFilter(Init)]), io:format(user, "--- CT args:~n~tp~n", [FlagFilter(CtArgs)]), EnvArgs = opts2args(EnvStartOpts), io:format(user, "--- Env opts -> args:~n~tp~n =>~n~tp~n", [EnvStartOpts,EnvArgs]), Merged = merge_arguments(CtArgs ++ EnvArgs), io:format(user, "--- Merged args:~n~tp~n", [FlagFilter(Merged)]), io:format(user, "-----------------------------------" "-----------------~n~n", []), Merged; _ -> merge_arguments(CtArgs) end, case proplists:get_value(help, Args) of undefined -> script_start(Args); _ -> script_usage() end. script_start(Args) -> Tracing = start_trace(Args), case ct_repeat:loop_test(script, Args) of false -> {ok,Cwd} = file:get_cwd(), CTVsn = case filename:basename(code:lib_dir(common_test)) of CTBase when is_list(CTBase) -> case string:lexemes(CTBase, "-") of ["common_test",Vsn] -> " v"++Vsn; _ -> "" end end, io:format("~nCommon Test~s starting (cwd is ~ts)~n~n", [CTVsn,Cwd]), Self = self(), Pid = spawn_link(fun() -> script_start1(Self, Args) end), receive {'EXIT',Pid,Reason} -> case Reason of {user_error,What} -> io:format("\nTest run failed!\nReason: ~tp\n\n\n", [What]), finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); _ -> io:format("Test run crashed! " "This could be an internal error " "- please report!\n\n" "~tp\n\n\n", [Reason]), finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args) end; {Pid,{error,Reason}} -> io:format("\nTest run failed! Reason:\n~tp\n\n\n",[Reason]), finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); {Pid,Result} -> io:nl(), finish(Tracing, analyze_test_result(Result, Args), Args) end; {error,_LoopReason} -> finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); Result -> io:nl(), finish(Tracing, analyze_test_result(Result, Args), Args) end. %% analyze the result of one test run, or many (in case of looped test) analyze_test_result(ok, _) -> ?EXIT_STATUS_TEST_SUCCESSFUL; analyze_test_result({error,_Reason}, _) -> ?EXIT_STATUS_TEST_RUN_FAILED; analyze_test_result({_Ok,Failed,{_UserSkipped,AutoSkipped}}, Args) -> if Failed > 0 -> ?EXIT_STATUS_TEST_CASE_FAILED; true -> case AutoSkipped of 0 -> ?EXIT_STATUS_TEST_SUCCESSFUL; _ -> case get_start_opt(exit_status, fun([ExitOpt]) -> ExitOpt end, Args) of undefined -> ?EXIT_STATUS_TEST_CASE_FAILED; "ignore_config" -> ?EXIT_STATUS_TEST_SUCCESSFUL end end end; analyze_test_result([Result|Rs], Args) -> case analyze_test_result(Result, Args) of ?EXIT_STATUS_TEST_SUCCESSFUL -> analyze_test_result(Rs, Args); Other -> Other end; analyze_test_result([], _) -> ?EXIT_STATUS_TEST_SUCCESSFUL; analyze_test_result(interactive_mode, _) -> interactive_mode; analyze_test_result(Unknown, _) -> io:format("\nTest run failed! Reason:\n~tp\n\n\n",[Unknown]), ?EXIT_STATUS_TEST_RUN_FAILED. finish(Tracing, ExitStatus, Args) -> stop_trace(Tracing), timer:sleep(1000), if ExitStatus == interactive_mode -> interactive_mode; true -> %% it's possible to tell CT to finish execution with a call %% to a different function than the normal halt/1 BIF %% (meant to be used mainly for reading the CT exit status) case get_start_opt(halt_with, fun([HaltMod,HaltFunc]) -> {list_to_atom(HaltMod), list_to_atom(HaltFunc)} end, Args) of undefined -> halt(ExitStatus, [{flush, false}]); {M,F} -> apply(M, F, [ExitStatus]) end end. script_start1(Parent, Args) -> %% tag this process ct_util:mark_process(), %% read general start flags Label = get_start_opt(label, fun([Lbl]) -> Lbl end, Args), Profile = get_start_opt(profile, fun([Prof]) -> Prof end, Args), Shell = get_start_opt(shell, true, Args), Cover = get_start_opt(cover, fun([CoverFile]) -> ?abs(CoverFile) end, Args), CoverStop = get_start_opt(cover_stop, fun([CS]) -> list_to_atom(CS) end, Args), LogDir = get_start_opt(logdir, fun([LogD]) -> LogD end, Args), LogOpts = get_start_opt(logopts, fun(Os) -> [list_to_atom(O) || O <- Os] end, [], Args), Verbosity = verbosity_args2opts(Args), MultTT = get_start_opt(multiply_timetraps, fun([MT]) -> list_to_number(MT) end, Args), ScaleTT = get_start_opt(scale_timetraps, fun([CT]) -> list_to_atom(CT); ([]) -> true end, Args), CreatePrivDir = get_start_opt(create_priv_dir, fun([PD]) -> list_to_atom(PD); ([]) -> auto_per_tc end, Args), EvHandlers = event_handler_args2opts(Args), CTHooks = ct_hooks_args2opts(Args), EnableBuiltinHooks = get_start_opt(enable_builtin_hooks, fun([CT]) -> list_to_atom(CT); ([]) -> undefined end, undefined, Args), %% check flags and set corresponding application env variables %% ct_decrypt_key | ct_decrypt_file case proplists:get_value(ct_decrypt_key, Args) of [DecryptKey] -> application:set_env(common_test, decrypt, {key,DecryptKey}); undefined -> case proplists:get_value(ct_decrypt_file, Args) of [DecryptFile] -> application:set_env(common_test, decrypt, {file,?abs(DecryptFile)}); undefined -> application:unset_env(common_test, decrypt) end end, %% no_auto_compile + include {AutoCompile,IncludeDirs} = case proplists:get_value(no_auto_compile, Args) of undefined -> application:set_env(common_test, auto_compile, true), InclDirs = case proplists:get_value(include, Args) of Incls when is_list(hd(Incls)) -> [filename:absname(IDir) || IDir <- Incls]; Incl when is_list(Incl) -> [filename:absname(Incl)]; undefined -> [] end, case os:getenv("CT_INCLUDE_PATH") of false -> application:set_env(common_test, include, InclDirs), {undefined,InclDirs}; CtInclPath -> AllInclDirs = string:lexemes(CtInclPath,[$:,$ ,$,]) ++ InclDirs, application:set_env(common_test, include, AllInclDirs), {undefined,AllInclDirs} end; _ -> application:set_env(common_test, auto_compile, false), {false,[]} end, %% abort test run if some suites can't be compiled AbortIfMissing = get_start_opt(abort_if_missing_suites, fun([]) -> true; ([Bool]) -> list_to_atom(Bool) end, false, Args), %% silent connections SilentConns = get_start_opt(silent_connections, fun(["all"]) -> [all]; (Conns) -> [list_to_atom(Conn) || Conn <- Conns] end, [], Args), %% stylesheet Stylesheet = get_start_opt(stylesheet, fun([SS]) -> ?abs(SS) end, Args), %% basic_html - used by ct_logs BasicHtml = case proplists:get_value(basic_html, Args) of undefined -> application:set_env(common_test, basic_html, false), undefined; _ -> application:set_env(common_test, basic_html, true), true end, %% esc_chars - used by ct_logs EscChars = case proplists:get_value(no_esc_chars, Args) of undefined -> application:set_env(common_test, esc_chars, true), undefined; _ -> application:set_env(common_test, esc_chars, false), false end, %% disable_log_cache - used by ct_logs case proplists:get_value(disable_log_cache, Args) of undefined -> application:set_env(common_test, disable_log_cache, false); _ -> application:set_env(common_test, disable_log_cache, true) end, %% log_cleanup - used by ct_logs KeepLogs = get_start_opt(keep_logs, fun ct_logs:parse_keep_logs/1, all, Args), application:set_env(common_test, keep_logs, KeepLogs), Opts = #opts{label = Label, profile = Profile, shell = Shell, cover = Cover, cover_stop = CoverStop, logdir = LogDir, logopts = LogOpts, basic_html = BasicHtml, esc_chars = EscChars, verbosity = Verbosity, event_handlers = EvHandlers, ct_hooks = CTHooks, enable_builtin_hooks = EnableBuiltinHooks, auto_compile = AutoCompile, abort_if_missing_suites = AbortIfMissing, include = IncludeDirs, silent_connections = SilentConns, stylesheet = Stylesheet, multiply_timetraps = MultTT, scale_timetraps = ScaleTT, create_priv_dir = CreatePrivDir, starter = script}, %% check if log files should be refreshed or go on to run tests... Result = run_or_refresh(Opts, Args), %% send final results to starting process waiting in script_start/0 Parent ! {self(), Result}. run_or_refresh(Opts = #opts{logdir = LogDir}, Args) -> case proplists:get_value(refresh_logs, Args) of undefined -> script_start2(Opts, Args); Refresh -> LogDir1 = case Refresh of [] -> which(logdir,LogDir); [RefreshDir] -> ?abs(RefreshDir) end, {ok,Cwd} = file:get_cwd(), ok = file:set_cwd(LogDir1), %% give the shell time to print version etc timer:sleep(500), io:nl(), case catch ct_logs:make_all_runs_index(refresh) of {'EXIT',ARReason} -> ok = file:set_cwd(Cwd), {error,{all_runs_index,ARReason}}; _ -> case catch ct_logs:make_all_suites_index(refresh) of {'EXIT',ASReason} -> ok = file:set_cwd(Cwd), {error,{all_suites_index,ASReason}}; _ -> ok = file:set_cwd(Cwd), io:format("Logs in ~ts refreshed!~n~n", [LogDir1]), timer:sleep(500), % time to flush io before quitting ok end end end. script_start2(Opts = #opts{shell = undefined}, Args) -> case proplists:get_value(spec, Args) of Specs when Specs =/= [], Specs =/= undefined -> Specs1 = get_start_opt(join_specs, [Specs], Specs, Args), %% using testspec as input for test Relaxed = get_start_opt(allow_user_terms, true, false, Args), try ct_testspec:collect_tests_from_file(Specs1, Relaxed) of TestSpecData -> execute_all_specs(TestSpecData, Opts, Args, []) catch throw:{error,Reason}:StackTrace -> {error,{invalid_testspec,{Reason,StackTrace}}}; _:Reason:StackTrace -> {error,{invalid_testspec,{Reason,StackTrace}}} end; [] -> {error,no_testspec_specified}; _ -> % no testspec used %% read config/userconfig from start flags InitConfig = ct_config:prepare_config_list(Args), TheLogDir = which(logdir, Opts#opts.logdir), case check_and_install_configfiles(InitConfig, TheLogDir, Opts) of ok -> % go on read tests from start flags script_start3(Opts#opts{config=InitConfig, logdir=TheLogDir}, Args); Error -> Error end end; script_start2(Opts, Args) -> %% read config/userconfig from start flags InitConfig = ct_config:prepare_config_list(Args), LogDir = which(logdir, Opts#opts.logdir), case check_and_install_configfiles(InitConfig, LogDir, Opts) of ok -> % go on read tests from start flags script_start3(Opts#opts{config=InitConfig, logdir=LogDir}, Args); Error -> Error end. execute_all_specs([], _, _, Result) -> Result1 = lists:reverse(Result), case lists:keysearch('EXIT', 1, Result1) of {value,{_,_,ExitReason}} -> exit(ExitReason); false -> case lists:keysearch(error, 1, Result1) of {value,Error} -> Error; false -> lists:foldl(fun({Ok,Fail,{UserSkip,AutoSkip}}, {Ok1,Fail1,{UserSkip1,AutoSkip1}}) -> {Ok1+Ok,Fail1+Fail, {UserSkip1+UserSkip, AutoSkip1+AutoSkip}} end, {0,0,{0,0}}, Result1) end end; execute_all_specs([{Specs,TS} | TSs], Opts, Args, Result) -> CombinedOpts = combine_test_opts(TS, Specs, Opts), try execute_one_spec(TS, CombinedOpts, Args) of ExecResult -> execute_all_specs(TSs, Opts, Args, [ExecResult|Result]) catch _ : ExitReason -> execute_all_specs(TSs, Opts, Args, [{'EXIT',self(),ExitReason}|Result]) end. execute_one_spec(TS, Opts, Args) -> %% read config/userconfig from start flags InitConfig = ct_config:prepare_config_list(Args), TheLogDir = which(logdir, Opts#opts.logdir), %% merge config from start flags with config from testspec AllConfig = merge_vals([InitConfig, Opts#opts.config]), case check_and_install_configfiles(AllConfig, TheLogDir, Opts) of ok -> % read tests from spec {Run,Skip} = ct_testspec:prepare_tests(TS, node()), Result = do_run(Run, Skip, Opts#opts{config=AllConfig, logdir=TheLogDir, current_testspec=TS}, Args), ct_util:delete_testdata(testspec), Result; Error -> Error end. combine_test_opts(TS, Specs, Opts) -> TSOpts = get_data_for_node(TS, node()), Label = choose_val(Opts#opts.label, TSOpts#opts.label), Profile = choose_val(Opts#opts.profile, TSOpts#opts.profile), LogDir = choose_val(Opts#opts.logdir, TSOpts#opts.logdir), AllLogOpts = merge_vals([Opts#opts.logopts, TSOpts#opts.logopts]), AllVerbosity = merge_keyvals([Opts#opts.verbosity, TSOpts#opts.verbosity]), AllSilentConns = merge_vals([Opts#opts.silent_connections, TSOpts#opts.silent_connections]), Cover = choose_val(Opts#opts.cover, TSOpts#opts.cover), CoverStop = choose_val(Opts#opts.cover_stop, TSOpts#opts.cover_stop), MultTT = choose_val(Opts#opts.multiply_timetraps, TSOpts#opts.multiply_timetraps), ScaleTT = choose_val(Opts#opts.scale_timetraps, TSOpts#opts.scale_timetraps), CreatePrivDir = choose_val(Opts#opts.create_priv_dir, TSOpts#opts.create_priv_dir), AllEvHs = merge_vals([Opts#opts.event_handlers, TSOpts#opts.event_handlers]), AllCTHooks = merge_vals( [Opts#opts.ct_hooks, TSOpts#opts.ct_hooks]), EnableBuiltinHooks = choose_val( Opts#opts.enable_builtin_hooks, TSOpts#opts.enable_builtin_hooks), Stylesheet = choose_val(Opts#opts.stylesheet, TSOpts#opts.stylesheet), AllInclude = merge_vals([Opts#opts.include, TSOpts#opts.include]), application:set_env(common_test, include, AllInclude), AutoCompile = case choose_val(Opts#opts.auto_compile, TSOpts#opts.auto_compile) of undefined -> true; ACBool -> application:set_env(common_test, auto_compile, ACBool), ACBool end, AbortIfMissing = choose_val(Opts#opts.abort_if_missing_suites, TSOpts#opts.abort_if_missing_suites), BasicHtml = case choose_val(Opts#opts.basic_html, TSOpts#opts.basic_html) of undefined -> false; BHBool -> application:set_env(common_test, basic_html, BHBool), BHBool end, EscChars = case choose_val(Opts#opts.esc_chars, TSOpts#opts.esc_chars) of undefined -> true; ECBool -> application:set_env(common_test, esc_chars, ECBool), ECBool end, Opts#opts{label = Label, profile = Profile, testspec_files = Specs, cover = Cover, cover_stop = CoverStop, logdir = which(logdir, LogDir), logopts = AllLogOpts, basic_html = BasicHtml, esc_chars = EscChars, verbosity = AllVerbosity, silent_connections = AllSilentConns, config = TSOpts#opts.config, event_handlers = AllEvHs, ct_hooks = AllCTHooks, enable_builtin_hooks = EnableBuiltinHooks, stylesheet = Stylesheet, auto_compile = AutoCompile, abort_if_missing_suites = AbortIfMissing, include = AllInclude, multiply_timetraps = MultTT, scale_timetraps = ScaleTT, create_priv_dir = CreatePrivDir}. check_and_install_configfiles( Configs, LogDir, #opts{ event_handlers = EvHandlers, ct_hooks = CTHooks, enable_builtin_hooks = EnableBuiltinHooks} ) -> case ct_config:check_config_files(Configs) of false -> install([{config,Configs}, {event_handler,EvHandlers}, {ct_hooks,CTHooks}, {enable_builtin_hooks,EnableBuiltinHooks}], LogDir); {value,{error,{nofile,File}}} -> {error,{cant_read_config_file,File}}; {value,{error,{wrong_config,Message}}}-> {error,{wrong_config,Message}}; {value,{error,{callback,Info}}} -> {error,{cant_load_callback_module,Info}} end. script_start3(Opts, Args) -> Opts1 = get_start_opt(step, fun(Step) -> Opts#opts{step = Step, cover = undefined} end, Opts, Args), case {proplists:get_value(dir, Args), proplists:get_value(suite, Args), groups_and_cases(proplists:get_value(group, Args), proplists:get_value(testcase, Args))} of %% flag specified without data {_,_,Error={error,_}} -> Error; {_,[],_} -> {error,no_suite_specified}; {[],_,_} -> {error,no_dir_specified}; {Dirs,undefined,[]} when is_list(Dirs) -> script_start4(Opts#opts{tests = tests(Dirs)}, Args); {undefined,Suites,[]} when is_list(Suites) -> Ts = tests([suite_to_test(S) || S <- Suites]), script_start4(Opts1#opts{tests = Ts}, Args); {undefined,Suite,GsAndCs} when is_list(Suite) -> case [suite_to_test(S) || S <- Suite] of DirMods = [_] -> Ts = tests(DirMods, GsAndCs), script_start4(Opts1#opts{tests = Ts}, Args); [_,_|_] -> {error,multiple_suites_and_cases}; _ -> {error,incorrect_start_options} end; {[_,_|_],Suite,[]} when is_list(Suite) -> {error,multiple_dirs_and_suites}; {[Dir],Suite,GsAndCs} when is_list(Dir), is_list(Suite) -> case [suite_to_test(Dir,S) || S <- Suite] of DirMods when GsAndCs == [] -> Ts = tests(DirMods), script_start4(Opts1#opts{tests = Ts}, Args); DirMods = [_] when GsAndCs /= [] -> Ts = tests(DirMods, GsAndCs), script_start4(Opts1#opts{tests = Ts}, Args); [_,_|_] when GsAndCs /= [] -> {error,multiple_suites_and_cases}; _ -> {error,incorrect_start_options} end; {undefined,undefined,GsAndCs} when GsAndCs /= [] -> {error,incorrect_start_options}; {undefined,undefined,_} -> if Opts#opts.shell -> script_start4(Opts#opts{tests = []}, Args); true -> %% no start options, use default "-dir ./" {ok,Dir} = file:get_cwd(), io:format("ct_run -dir ~ts~n~n", [Dir]), script_start4(Opts#opts{tests = tests([Dir])}, Args) end end. script_start4(#opts{label = Label, profile = Profile, shell = true, config = Config, event_handlers = EvHandlers, ct_hooks = CTHooks, logopts = LogOpts, verbosity = Verbosity, enable_builtin_hooks = EnableBuiltinHooks, logdir = LogDir, testspec_files = Specs}, _Args) -> %% label - used by ct_logs application:set_env(common_test, test_label, Label), %% profile - used in ct_util application:set_env(common_test, profile, Profile), if Config == [] -> ok; true -> io:format("\nInstalling: ~tp\n\n", [Config]) end, case install([{config,Config},{event_handler,EvHandlers}, {ct_hooks, CTHooks}, {enable_builtin_hooks,EnableBuiltinHooks}]) of ok -> _ = ct_util:start(interactive, LogDir, add_verbosity_defaults(Verbosity)), ct_util:set_testdata({logopts, LogOpts}), log_ts_names(Specs), io:nl(), interactive_mode; Error -> Error end; script_start4(Opts = #opts{tests = Tests}, Args) -> do_run(Tests, [], Opts, Args). script_usage() -> io:format("\nUsage:\n\n"), io:format("Run tests from command line:\n\n" "\tct_run -dir TestDir1 TestDir2 .. TestDirN |" "\n\t [-dir TestDir] -suite Suite1 Suite2 .. SuiteN" "\n\t [-group Group1 Group2 .. GroupN] [-case Case1 Case2 .. CaseN]" "\n\t [-step [config | keep_inactive]]" "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]" "\n\t [-userconfig CallbackModule ConfigFile1 .. ConfigFileN]" "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]" "\n\t [-logdir LogDir]" "\n\t [-logopts LogOpt1 LogOpt2 .. LogOptN]" "\n\t [-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]" "\n\t [-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]" "\n\t [-stylesheet CSSFile]" "\n\t [-cover CoverCfgFile]" "\n\t [-cover_stop Bool]" "\n\t [-event_handler EvHandler1 EvHandler2 .. EvHandlerN]" "\n\t [-ct_hooks CTHook1 CTHook2 .. CTHookN]" "\n\t [-include InclDir1 InclDir2 .. InclDirN]" "\n\t [-no_auto_compile]" "\n\t [-abort_if_missing_suites]" "\n\t [-multiply_timetraps N]" "\n\t [-scale_timetraps]" "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t [-basic_html]" "\n\t [-no_esc_chars]" "\n\t [-repeat N] |" "\n\t [-duration HHMMSS [-force_stop [skip_rest]]] |" "\n\t [-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]" "\n\t [-exit_status ignore_config]" "\n\t [-help]\n\n"), io:format("Run tests using test specification:\n\n" "\tct_run -spec TestSpec1 TestSpec2 .. TestSpecN" "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]" "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]" "\n\t [-logdir LogDir]" "\n\t [-logopts LogOpt1 LogOpt2 .. LogOptN]" "\n\t [-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]" "\n\t [-allow_user_terms]" "\n\t [-join_specs]" "\n\t [-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]" "\n\t [-stylesheet CSSFile]" "\n\t [-cover CoverCfgFile]" "\n\t [-cover_stop Bool]" "\n\t [-event_handler EvHandler1 EvHandler2 .. EvHandlerN]" "\n\t [-ct_hooks CTHook1 CTHook2 .. CTHookN]" "\n\t [-include InclDir1 InclDir2 .. InclDirN]" "\n\t [-no_auto_compile]" "\n\t [-abort_if_missing_suites]" "\n\t [-multiply_timetraps N]" "\n\t [-scale_timetraps]" "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t [-basic_html]" "\n\t [-no_esc_chars]" "\n\t [-repeat N] |" "\n\t [-duration HHMMSS [-force_stop [skip_rest]]] |" "\n\t [-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]\n\n"), io:format("Refresh the HTML index files:\n\n" "\tct_run -refresh_logs [LogDir]" " [-logdir LogDir] " " [-basic_html]\n\n"), io:format("Run CT in interactive mode:\n\n" "\tct_run -shell" "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]" "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]\n\n"), io:format("Run tests in web based GUI:\n\n" "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]" "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]" "\n\t [-dir TestDir1 TestDir2 .. TestDirN] |" "\n\t [-suite Suite [-case Case]]" "\n\t [-logopts LogOpt1 LogOpt2 .. LogOptN]" "\n\t [-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]" "\n\t [-include InclDir1 InclDir2 .. InclDirN]" "\n\t [-no_auto_compile]" "\n\t [-abort_if_missing_suites]" "\n\t [-multiply_timetraps N]" "\n\t [-scale_timetraps]" "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t [-basic_html]" "\n\t [-no_esc_chars]\n\n"). install(Opts) -> install(Opts, "."). install(Opts, LogDir) -> ConfOpts = ct_config:add_default_callback(Opts), case application:get_env(common_test, decrypt) of {ok,_} -> ok; _ -> case lists:keysearch(decrypt, 1, Opts) of {value,{_,KeyOrFile}} -> application:set_env(common_test, decrypt, KeyOrFile); false -> application:unset_env(common_test, decrypt) end end, case whereis(ct_util_server) of undefined -> VarFile = variables_file_name(LogDir), case file:open(VarFile, [write, {encoding,utf8}]) of {ok,Fd} -> _ = [io:format(Fd, "~tp.\n", [Opt]) || Opt <- ConfOpts], ok = file:close(Fd); {error,Reason} -> io:format("CT failed to install configuration data. Please " "verify that the log directory exists and that " "write permission is set.\n\n", []), {error,{VarFile,Reason}} end; _ -> io:format("It is not possible to install CT while running " "in interactive mode.\n" "To exit this mode, run ct:stop_interactive().\n" "To enter the interactive mode again, " "run ct:start_interactive()\n\n", []), {error,interactive_mode} end. variables_file_name(Dir) -> filename:join(Dir, "variables-"++atom_to_list(node())). run_test(StartOpt) when is_tuple(StartOpt) -> run_test([StartOpt]); run_test(StartOpts) when is_list(StartOpts) -> CTPid = spawn(run_test1_fun(StartOpts)), Ref = monitor(process, CTPid), receive {'DOWN',Ref,process,CTPid,{user_error,Error}} -> {error,Error}; {'DOWN',Ref,process,CTPid,Other} -> Other end. -spec run_test1_fun(_) -> fun(() -> no_return()). run_test1_fun(StartOpts) -> fun() -> ct_util:mark_process(), run_test1(StartOpts) end. run_test1(StartOpts) when is_list(StartOpts) -> case proplists:get_value(refresh_logs, StartOpts) of undefined -> Tracing = start_trace(StartOpts), {ok,Cwd} = file:get_cwd(), io:format("~nCommon Test starting (cwd is ~ts)~n~n", [Cwd]), Res = case ct_repeat:loop_test(func, StartOpts) of false -> case catch run_test2(StartOpts) of {'EXIT',Reason} -> ok = file:set_cwd(Cwd), {error,Reason}; Result -> Result end; Result -> Result end, stop_trace(Tracing), exit(Res); RefreshDir -> %% log_cleanup - used by ct_logs KeepLogs = get_start_opt(keep_logs, fun ct_logs:parse_keep_logs/1, all, StartOpts), application:set_env(common_test, keep_logs, KeepLogs), ok = refresh_logs(?abs(RefreshDir)), exit(done) end. run_test2(StartOpts) -> %% label Label = get_start_opt(label, fun(Lbl) when is_list(Lbl) -> Lbl; (Lbl) when is_atom(Lbl) -> atom_to_list(Lbl) end, StartOpts), %% profile Profile = get_start_opt(profile, fun(Prof) when is_list(Prof) -> Prof; (Prof) when is_atom(Prof) -> atom_to_list(Prof) end, StartOpts), %% logdir LogDir = get_start_opt(logdir, fun(LD) when is_list(LD) -> LD end, StartOpts), %% logopts LogOpts = get_start_opt(logopts, value, [], StartOpts), %% verbosity Verbosity = get_start_opt(verbosity, fun(VLvls) when is_list(VLvls) -> lists:map(fun(VLvl = {_Cat,_Lvl}) -> VLvl; (Lvl) -> {'$unspecified',Lvl} end, VLvls); (VLvl) when is_integer(VLvl) -> [{'$unspecified',VLvl}] end, [], StartOpts), %% config & userconfig CfgFiles = ct_config:get_config_file_list(StartOpts), %% event handlers EvHandlers = case proplists:get_value(event_handler, StartOpts) of undefined -> []; H when is_atom(H) -> [{H,[]}]; H -> Hs = if is_tuple(H) -> [H]; is_list(H) -> H; true -> [] end, lists:flatten( lists:map(fun(EH) when is_atom(EH) -> {EH,[]}; ({HL,Args}) when is_list(HL) -> [{EH,Args} || EH <- HL]; ({EH,Args}) when is_atom(EH) -> {EH,Args}; (_) -> [] end, Hs)) end, %% CT Hooks CTHooks = get_start_opt(ct_hooks, value, [], StartOpts), EnableBuiltinHooks = get_start_opt(enable_builtin_hooks, fun(EBH) when EBH == true; EBH == false -> EBH end, undefined, StartOpts), %% silent connections SilentConns = get_start_opt(silent_connections, fun(all) -> [all]; (Conns) -> Conns end, [], StartOpts), %% stylesheet Stylesheet = get_start_opt(stylesheet, fun(SS) -> ?abs(SS) end, StartOpts), %% code coverage Cover = get_start_opt(cover, fun(CoverFile) -> ?abs(CoverFile) end, StartOpts), CoverStop = get_start_opt(cover_stop, value, StartOpts), %% timetrap manipulation MultiplyTT = get_start_opt(multiply_timetraps, value, StartOpts), ScaleTT = get_start_opt(scale_timetraps, value, StartOpts), %% create unique priv dir names CreatePrivDir = get_start_opt(create_priv_dir, value, StartOpts), %% auto compile & include files {AutoCompile,Include} = case proplists:get_value(auto_compile, StartOpts) of undefined -> application:set_env(common_test, auto_compile, true), InclDirs = case proplists:get_value(include, StartOpts) of undefined -> []; Incls when is_list(hd(Incls)) -> [filename:absname(IDir) || IDir <- Incls]; Incl when is_list(Incl) -> [filename:absname(Incl)] end, case os:getenv("CT_INCLUDE_PATH") of false -> application:set_env(common_test, include, InclDirs), {undefined,InclDirs}; CtInclPath -> InclDirs1 = string:lexemes(CtInclPath, [$:,$ ,$,]), AllInclDirs = InclDirs1++InclDirs, application:set_env(common_test, include, AllInclDirs), {undefined,AllInclDirs} end; ACBool -> application:set_env(common_test, auto_compile, ACBool), {ACBool,[]} end, %% abort test run if some suites can't be compiled AbortIfMissing = get_start_opt(abort_if_missing_suites, value, false, StartOpts), %% decrypt config file case proplists:get_value(decrypt, StartOpts) of undefined -> application:unset_env(common_test, decrypt); Key={key,_} -> application:set_env(common_test, decrypt, Key); {file,KeyFile} -> application:set_env(common_test, decrypt, {file,?abs(KeyFile)}) end, %% basic html - used by ct_logs BasicHtml = case proplists:get_value(basic_html, StartOpts) of undefined -> application:set_env(common_test, basic_html, false), undefined; BasicHtmlBool -> application:set_env(common_test, basic_html, BasicHtmlBool), BasicHtmlBool end, %% esc_chars - used by ct_logs EscChars = case proplists:get_value(esc_chars, StartOpts) of undefined -> application:set_env(common_test, esc_chars, true), undefined; EscCharsBool -> application:set_env(common_test, esc_chars, EscCharsBool), EscCharsBool end, %% disable_log_cache - used by ct_logs case proplists:get_value(disable_log_cache, StartOpts) of undefined -> application:set_env(common_test, disable_log_cache, false); DisableCacheBool -> application:set_env(common_test, disable_log_cache, DisableCacheBool) end, %% log_cleanup - used by ct_logs KeepLogs = get_start_opt(keep_logs, fun ct_logs:parse_keep_logs/1, all, StartOpts), application:set_env(common_test, keep_logs, KeepLogs), %% stepped execution Step = get_start_opt(step, value, StartOpts), Opts = #opts{label = Label, profile = Profile, cover = Cover, cover_stop = CoverStop, step = Step, logdir = LogDir, logopts = LogOpts, basic_html = BasicHtml, esc_chars = EscChars, config = CfgFiles, verbosity = Verbosity, event_handlers = EvHandlers, ct_hooks = CTHooks, enable_builtin_hooks = EnableBuiltinHooks, auto_compile = AutoCompile, abort_if_missing_suites = AbortIfMissing, include = Include, silent_connections = SilentConns, stylesheet = Stylesheet, multiply_timetraps = MultiplyTT, scale_timetraps = ScaleTT, create_priv_dir = CreatePrivDir, starter = ct}, %% test specification case proplists:get_value(spec, StartOpts) of undefined -> case lists:keysearch(prepared_tests, 1, StartOpts) of {value,{_,{Run,Skip},Specs}} -> % use prepared tests run_prepared(Run, Skip, Opts#opts{testspec_files = Specs}, StartOpts); false -> run_dir(Opts, StartOpts) end; Specs -> Relaxed = get_start_opt(allow_user_terms, value, false, StartOpts), %% using testspec(s) as input for test run_spec_file(Relaxed, Opts#opts{testspec_files = Specs}, StartOpts) end. run_spec_file(Relaxed, Opts = #opts{testspec_files = Specs}, StartOpts) -> Specs1 = case Specs of [X|_] when is_integer(X) -> [Specs]; _ -> Specs end, AbsSpecs = lists:map(fun(SF) -> ?abs(SF) end, Specs1), AbsSpecs1 = get_start_opt(join_specs, [AbsSpecs], AbsSpecs, StartOpts), try ct_testspec:collect_tests_from_file(AbsSpecs1, Relaxed) of TestSpecData -> run_all_specs(TestSpecData, Opts, StartOpts, []) catch throw:{error,CTReason}:StackTrace -> exit({error,{invalid_testspec,{CTReason,StackTrace}}}); _:CTReason:StackTrace -> exit({error,{invalid_testspec,{CTReason,StackTrace}}}) end. run_all_specs([], _, _, TotResult) -> TotResult1 = lists:reverse(TotResult), case lists:keysearch('EXIT', 1, TotResult1) of {value,{_,_,ExitReason}} -> exit(ExitReason); false -> case lists:keysearch(error, 1, TotResult1) of {value,Error} -> Error; false -> lists:foldl(fun({Ok,Fail,{UserSkip,AutoSkip}}, {Ok1,Fail1,{UserSkip1,AutoSkip1}}) -> {Ok1+Ok,Fail1+Fail, {UserSkip1+UserSkip, AutoSkip1+AutoSkip}}; (Pid, Acc) when is_pid(Pid) -> Acc end, {0,0,{0,0}}, TotResult1) end end; run_all_specs([{Specs,TS} | TSs], Opts, StartOpts, TotResult) -> Combined = #opts{config = TSConfig} = combine_test_opts(TS, Specs, Opts), AllConfig = merge_vals([Opts#opts.config, TSConfig]), try run_one_spec(TS, Combined#opts{config = AllConfig, current_testspec=TS}, StartOpts) of Result -> run_all_specs(TSs, Opts, StartOpts, [Result | TotResult]) catch _ : Reason -> run_all_specs(TSs, Opts, StartOpts, [{error,Reason} | TotResult]) end. run_one_spec(TS, CombinedOpts, StartOpts) -> #opts{logdir = Logdir, config = Config} = CombinedOpts, case check_and_install_configfiles(Config, Logdir, CombinedOpts) of ok -> {Run,Skip} = ct_testspec:prepare_tests(TS, node()), reformat_result(catch do_run(Run, Skip, CombinedOpts, StartOpts)); Error -> Error end. run_prepared(Run, Skip, Opts = #opts{logdir = LogDir, config = CfgFiles}, StartOpts) -> LogDir1 = which(logdir, LogDir), case check_and_install_configfiles(CfgFiles, LogDir1, Opts) of ok -> reformat_result(catch do_run(Run, Skip, Opts#opts{logdir = LogDir1}, StartOpts)); {error,_Reason} = Error -> exit(Error) end. check_config_file(Callback, File)-> case code:is_loaded(Callback) of false -> case code:load_file(Callback) of {module,_} -> ok; {error,Why} -> exit({error,{cant_load_callback_module,Why}}) end; _ -> ok end, case Callback:check_parameter(File) of {ok,{file,File}}-> ?abs(File); {ok,{config,_}}-> File; {error,{wrong_config,Message}}-> exit({error,{wrong_config,{Callback,Message}}}); {error,{nofile,File}}-> exit({error,{no_such_file,?abs(File)}}) end. run_dir(Opts = #opts{logdir = LogDir, config = CfgFiles, event_handlers = EvHandlers, ct_hooks = CTHook, enable_builtin_hooks = EnableBuiltinHooks}, StartOpts) -> LogDir1 = which(logdir, LogDir), Opts1 = Opts#opts{logdir = LogDir1}, AbsCfgFiles = lists:map(fun({Callback,FileList})-> case code:is_loaded(Callback) of {file,_Path}-> ok; false -> case code:load_file(Callback) of {module,Callback}-> ok; {error,_}-> exit({error,{no_such_module, Callback}}) end end, {Callback, lists:map(fun(File)-> check_config_file(Callback, File) end, FileList)} end, CfgFiles), case install([{config,AbsCfgFiles}, {event_handler,EvHandlers}, {ct_hooks, CTHook}, {enable_builtin_hooks,EnableBuiltinHooks}], LogDir1) of ok -> ok; {error,_IReason} = IError -> exit(IError) end, case {proplists:get_value(dir, StartOpts), proplists:get_value(suite, StartOpts), groups_and_cases(proplists:get_value(group, StartOpts), proplists:get_value(testcase, StartOpts))} of %% flag specified without data {_,_,Error={error,_}} -> Error; {_,[],_} -> {error,no_suite_specified}; {[],_,_} -> {error,no_dir_specified}; {Dirs=[Hd|_],undefined,[]} when is_list(Dirs), not is_integer(Hd) -> Dirs1 = [if is_atom(D) -> atom_to_list(D); true -> D end || D <- Dirs], reformat_result(catch do_run(tests(Dirs1), [], Opts1, StartOpts)); {Dir=[Hd|_],undefined,[]} when is_list(Dir) and is_integer(Hd) -> reformat_result(catch do_run(tests(Dir), [], Opts1, StartOpts)); {Dir,undefined,[]} when is_atom(Dir) and (Dir /= undefined) -> reformat_result(catch do_run(tests(atom_to_list(Dir)), [], Opts1, StartOpts)); {undefined,Suites=[Hd|_],[]} when not is_integer(Hd) -> Suites1 = [suite_to_test(S) || S <- Suites], reformat_result(catch do_run(tests(Suites1), [], Opts1, StartOpts)); {undefined,Suite,[]} when is_atom(Suite) and (Suite /= undefined) -> {Dir,Mod} = suite_to_test(Suite), reformat_result(catch do_run(tests(Dir, Mod), [], Opts1, StartOpts)); {undefined,Suite,GsAndCs} when is_atom(Suite) and (Suite /= undefined) -> {Dir,Mod} = suite_to_test(Suite), reformat_result(catch do_run(tests(Dir, Mod, GsAndCs), [], Opts1, StartOpts)); {undefined,[Hd,_|_],_GsAndCs} when not is_integer(Hd) -> exit({error,multiple_suites_and_cases}); {undefined,Suite=[Hd|Tl],GsAndCs} when is_integer(Hd) ; (is_list(Hd) and (Tl == [])) ; (is_atom(Hd) and (Tl == [])) -> {Dir,Mod} = suite_to_test(Suite), reformat_result(catch do_run(tests(Dir, Mod, GsAndCs), [], Opts1, StartOpts)); {[Hd,_|_],_Suites,[]} when is_list(Hd) ; not is_integer(Hd) -> exit({error,multiple_dirs_and_suites}); {undefined,undefined,GsAndCs} when GsAndCs /= [] -> exit({error,incorrect_start_options}); {Dir,Suite,GsAndCs} when is_integer(hd(Dir)) ; (is_atom(Dir) and (Dir /= undefined)) ; ((length(Dir) == 1) and is_atom(hd(Dir))) ; ((length(Dir) == 1) and is_list(hd(Dir))) -> Dir1 = if is_atom(Dir) -> atom_to_list(Dir); true -> Dir end, if Suite == undefined -> exit({error,incorrect_start_options}); is_integer(hd(Suite)) ; (is_atom(Suite) and (Suite /= undefined)) ; ((length(Suite) == 1) and is_atom(hd(Suite))) ; ((length(Suite) == 1) and is_list(hd(Suite))) -> {Dir2,Mod} = suite_to_test(Dir1, Suite), case GsAndCs of [] -> reformat_result(catch do_run(tests(Dir2, Mod), [], Opts1, StartOpts)); _ -> reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs), [], Opts1, StartOpts)) end; is_list(Suite) -> % multiple suites case [suite_to_test(Dir1, S) || S <- Suite] of [_,_|_] when GsAndCs /= [] -> exit({error,multiple_suites_and_cases}); [{Dir2,Mod}] when GsAndCs /= [] -> reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs), [], Opts1, StartOpts)); DirMods -> reformat_result(catch do_run(tests(DirMods), [], Opts1, StartOpts)) end end; {undefined,undefined,[]} -> {ok,Dir} = file:get_cwd(), %% No start options, use default {dir,CWD} reformat_result(catch do_run(tests(Dir), [], Opts1, StartOpts)); {Dir,Suite,GsAndCs} -> exit({error,{incorrect_start_options,{Dir,Suite,GsAndCs}}}) end. run_testspec(TestSpec) -> CTPid = spawn(run_testspec1_fun(TestSpec)), Ref = monitor(process, CTPid), receive {'DOWN',Ref,process,CTPid,{user_error,Error}} -> Error; {'DOWN',Ref,process,CTPid,Other} -> Other end. -spec run_testspec1_fun(_) -> fun(() -> no_return()). run_testspec1_fun(TestSpec) -> fun() -> ct_util:mark_process(), run_testspec1(TestSpec) end. run_testspec1(TestSpec) -> {ok,Cwd} = file:get_cwd(), io:format("~nCommon Test starting (cwd is ~ts)~n~n", [Cwd]), case catch run_testspec2(TestSpec) of {'EXIT',Reason} -> ok = file:set_cwd(Cwd), exit({error,Reason}); Result -> exit(Result) end. run_testspec2(File) when is_list(File), is_integer(hd(File)) -> case file:read_file_info(File) of {ok,_} -> exit("Bad argument, " "use ct:run_test([{spec," ++ File ++ "}])"); _ -> exit("Bad argument, list of tuples expected, " "use ct:run_test/1 for test specification files") end; run_testspec2(TestSpec) -> case catch ct_testspec:collect_tests_from_list(TestSpec, false) of {E,CTReason} when E == error ; E == 'EXIT' -> exit({error,CTReason}); TS -> Opts = get_data_for_node(TS, node()), AllInclude = case os:getenv("CT_INCLUDE_PATH") of false -> Opts#opts.include; CtInclPath -> EnvInclude = string:lexemes(CtInclPath, [$:,$ ,$,]), EnvInclude++Opts#opts.include end, application:set_env(common_test, include, AllInclude), LogDir1 = which(logdir,Opts#opts.logdir), case check_and_install_configfiles( Opts#opts.config, LogDir1, Opts) of ok -> Opts1 = Opts#opts{testspec_files = [], logdir = LogDir1, include = AllInclude}, {Run,Skip} = ct_testspec:prepare_tests(TS, node()), reformat_result(catch do_run(Run, Skip, Opts1, [])); {error,_GCFReason} = GCFError -> exit(GCFError) end end. get_data_for_node(#testspec{label = Labels, profile = Profiles, logdir = LogDirs, logopts = LogOptsList, basic_html = BHs, esc_chars = EscChs, stylesheet = SSs, verbosity = VLvls, silent_connections = SilentConnsList, cover = CoverFs, cover_stop = CoverStops, config = Cfgs, userconfig = UsrCfgs, event_handler = EvHs, ct_hooks = CTHooks, enable_builtin_hooks = EnableBuiltinHooks, auto_compile = ACs, abort_if_missing_suites = AiMSs, include = Incl, multiply_timetraps = MTs, scale_timetraps = STs, create_priv_dir = PDs}, Node) -> Label = proplists:get_value(Node, Labels), Profile = proplists:get_value(Node, Profiles), LogDir = case proplists:get_value(Node, LogDirs) of undefined -> "."; Dir -> Dir end, LogOpts = case proplists:get_value(Node, LogOptsList) of undefined -> []; LOs -> LOs end, BasicHtml = proplists:get_value(Node, BHs), EscChars = proplists:get_value(Node, EscChs), Stylesheet = proplists:get_value(Node, SSs), Verbosity = case proplists:get_value(Node, VLvls) of undefined -> []; Lvls -> Lvls end, SilentConns = case proplists:get_value(Node, SilentConnsList) of undefined -> []; SCs -> SCs end, Cover = proplists:get_value(Node, CoverFs), CoverStop = proplists:get_value(Node, CoverStops), MT = proplists:get_value(Node, MTs), ST = proplists:get_value(Node, STs), CreatePrivDir = proplists:get_value(Node, PDs), ConfigFiles = [{?ct_config_txt,F} || {N,F} <- Cfgs, N==Node] ++ [CBF || {N,CBF} <- UsrCfgs, N==Node], EvHandlers = [{H,A} || {N,H,A} <- EvHs, N==Node], FiltCTHooks = [Hook || {N,Hook} <- CTHooks, N==Node], AutoCompile = proplists:get_value(Node, ACs), AbortIfMissing = proplists:get_value(Node, AiMSs), Include = [I || {N,I} <- Incl, N==Node], #opts{label = Label, profile = Profile, logdir = LogDir, logopts = LogOpts, basic_html = BasicHtml, esc_chars = EscChars, stylesheet = Stylesheet, verbosity = Verbosity, silent_connections = SilentConns, cover = Cover, cover_stop = CoverStop, config = ConfigFiles, event_handlers = EvHandlers, ct_hooks = FiltCTHooks, enable_builtin_hooks = EnableBuiltinHooks, auto_compile = AutoCompile, abort_if_missing_suites = AbortIfMissing, include = Include, multiply_timetraps = MT, scale_timetraps = ST, create_priv_dir = CreatePrivDir}. refresh_logs(LogDir) -> {ok,Cwd} = file:get_cwd(), case file:set_cwd(LogDir) of E = {error,_Reason} -> E; _ -> case catch ct_logs:make_all_suites_index(refresh) of {'EXIT',ASReason} -> ok = file:set_cwd(Cwd), {error,{all_suites_index,ASReason}}; _ -> case catch ct_logs:make_all_runs_index(refresh) of {'EXIT',ARReason} -> ok = file:set_cwd(Cwd), {error,{all_runs_index,ARReason}}; _ -> ok = file:set_cwd(Cwd), io:format("Logs in ~ts refreshed!~n",[LogDir]), ok end end end. which(logdir, undefined) -> "."; which(logdir, Dir) -> Dir. choose_val(undefined, V1) -> V1; choose_val(V0, _V1) -> V0. merge_vals(Vs) -> lists:append(Vs). merge_keyvals(Vs) -> make_unique(lists:append(Vs)). make_unique([Elem={Key,_} | Elems]) -> [Elem | make_unique(proplists:delete(Key, Elems))]; make_unique([]) -> []. listify([C|_]=Str) when is_integer(C) -> [Str]; listify(L) when is_list(L) -> L; listify(E) -> [E]. delistify([E]) -> E; delistify(E) -> E. run(TestDir, Suite, Cases) -> case install([]) of ok -> reformat_result(catch do_run(tests(TestDir, Suite, Cases), [])); Error -> Error end. run(TestDir, Suite) when is_list(TestDir), is_integer(hd(TestDir)) -> case install([]) of ok -> reformat_result(catch do_run(tests(TestDir, Suite), [])); Error -> Error end. run(TestDirs) -> case install([]) of ok -> reformat_result(catch do_run(tests(TestDirs), [])); Error -> Error end. reformat_result({'EXIT',{user_error,Reason}}) -> {error,Reason}; reformat_result({user_error,Reason}) -> {error,Reason}; reformat_result(Result) -> Result. suite_to_test(Suite) when is_atom(Suite) -> suite_to_test(atom_to_list(Suite)); suite_to_test(Suite) when is_list(Suite) -> {filename:dirname(Suite), list_to_atom(filename:rootname(filename:basename(Suite)))}. suite_to_test(Dir, Suite) when is_atom(Suite) -> suite_to_test(Dir, atom_to_list(Suite)); suite_to_test(Dir, Suite) when is_list(Suite) -> case filename:dirname(Suite) of "." -> {Dir,list_to_atom(filename:rootname(Suite))}; DirName -> % ignore Dir File = filename:basename(Suite), {DirName,list_to_atom(filename:rootname(File))} end. groups_and_cases(Gs, Cs) when ((Gs == undefined) or (Gs == [])) and ((Cs == undefined) or (Cs == [])) -> []; groups_and_cases(Gs, Cs) when Gs == undefined ; Gs == [] -> if (Cs == all) or (Cs == [all]) or (Cs == ["all"]) -> all; true -> [ensure_atom(C) || C <- listify(Cs)] end; groups_and_cases(GOrGs, Cs) when (is_atom(GOrGs) orelse (is_list(GOrGs) andalso (is_atom(hd(GOrGs)) orelse (is_list(hd(GOrGs)) andalso is_atom(hd(hd(GOrGs))))))) -> if (Cs == undefined) or (Cs == []) or (Cs == all) or (Cs == [all]) or (Cs == ["all"]) -> [{GOrGs,all}]; true -> [{GOrGs,[ensure_atom(C) || C <- listify(Cs)]}] end; groups_and_cases(Gs, Cs) when is_integer(hd(hd(Gs))) -> %% if list of strings, this comes from 'ct_run -group G1 G2 ...' and %% we need to parse the strings Gs1 = if (Gs == [all]) or (Gs == ["all"]) -> all; true -> lists:map(fun(G) -> {ok,Ts,_} = erl_scan:string(G++"."), {ok,Term} = erl_parse:parse_term(Ts), Term end, Gs) end, groups_and_cases(Gs1, Cs); groups_and_cases(Gs, Cs) -> {error,{incorrect_group_or_case_option,Gs,Cs}}. tests(TestDir, Suites, []) when is_list(TestDir), is_integer(hd(TestDir)) -> [{?testdir(TestDir,Suites),ensure_atom(Suites),all}]; tests(TestDir, Suite, Cases) when is_list(TestDir), is_integer(hd(TestDir)) -> [{?testdir(TestDir,Suite),ensure_atom(Suite),Cases}]; tests([TestDir], Suite, Cases) when is_list(TestDir), is_integer(hd(TestDir)) -> [{?testdir(TestDir,Suite),ensure_atom(Suite),Cases}]. tests([{Dir,Suite}],Cases) -> [{?testdir(Dir,Suite),ensure_atom(Suite),Cases}]; tests(TestDir, Suite) when is_list(TestDir), is_integer(hd(TestDir)) -> tests(TestDir, ensure_atom(Suite), all); tests([TestDir], Suite) when is_list(TestDir), is_integer(hd(TestDir)) -> tests(TestDir, ensure_atom(Suite), all). tests(DirSuites) when is_list(DirSuites), is_tuple(hd(DirSuites)) -> [{?testdir(Dir,Suite),ensure_atom(Suite),all} || {Dir,Suite} <- DirSuites]; tests(TestDir) when is_list(TestDir), is_integer(hd(TestDir)) -> tests([TestDir]); tests(TestDirs) when is_list(TestDirs), is_list(hd(TestDirs)) -> [{?testdir(TestDir,all),all,all} || TestDir <- TestDirs]. do_run(Tests, Misc) when is_list(Misc) -> do_run(Tests, Misc, ".", []). do_run(Tests, Misc, LogDir, LogOpts) when is_list(Misc), is_list(LogDir), is_list(LogOpts) -> Opts = case proplists:get_value(step, Misc) of undefined -> #opts{}; StepOpts -> #opts{step = StepOpts} end, do_run(Tests, [], Opts#opts{logdir = LogDir}, []); do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) -> #opts{label = Label, profile = Profile, verbosity = VLvls} = Opts, %% label - used by ct_logs TestLabel = if Label == undefined -> undefined; is_atom(Label) -> atom_to_list(Label); is_list(Label) -> Label; true -> undefined end, application:set_env(common_test, test_label, TestLabel), %% profile - used in ct_util TestProfile = if Profile == undefined -> undefined; is_atom(Profile) -> atom_to_list(Profile); is_list(Profile) -> Profile; true -> undefined end, application:set_env(common_test, profile, TestProfile), case code:which(test_server) of non_existing -> {error,no_path_to_test_server}; _ -> %% This env variable is used by test_server to determine %% which framework it runs under. case os:getenv("TEST_SERVER_FRAMEWORK") of false -> os:putenv("TEST_SERVER_FRAMEWORK", "ct_framework"), os:putenv("TEST_SERVER_FRAMEWORK_NAME", "common_test"); "ct_framework" -> ok; Other -> erlang:display( list_to_atom( "Note: TEST_SERVER_FRAMEWORK = " ++ Other)) end, Verbosity = add_verbosity_defaults(VLvls), case ct_util:start(Opts#opts.logdir, Verbosity) of {error,interactive_mode} -> io:format("CT is started in interactive mode. " "To exit this mode, " "run ct:stop_interactive().\n" "To enter the interactive mode again, " "run ct:start_interactive()\n\n",[]), {error,interactive_mode}; _Pid -> ct_util:set_testdata({starter,Opts#opts.starter}), compile_and_run(Tests, Skip, Opts#opts{verbosity=Verbosity}, Args) end end. compile_and_run(Tests, Skip, Opts, Args) -> %% save stylesheet info ct_util:set_testdata({stylesheet,Opts#opts.stylesheet}), %% save logopts ct_util:set_testdata({logopts,Opts#opts.logopts}), %% save info about current testspec (testspec record or undefined) ct_util:set_testdata({testspec,Opts#opts.current_testspec}), %% enable silent connections case Opts#opts.silent_connections of [] -> ok; Conns -> case lists:member(all, Conns) of true -> Conns1 = ct_util:override_silence_all_connections(), ct_logs:log("Silent connections", "~tp", [Conns1]); false -> ct_util:override_silence_connections(Conns), ct_logs:log("Silent connections", "~tp", [Conns]) end end, log_ts_names(Opts#opts.testspec_files), TestSuites = suite_tuples(Tests), {_TestSuites1,SuiteMakeErrors,AllMakeErrors} = case application:get_env(common_test, auto_compile) of {ok,false} -> {TestSuites1,SuitesNotFound} = verify_suites(TestSuites), {TestSuites1,SuitesNotFound,SuitesNotFound}; _ -> {SuiteErrs,HelpErrs} = auto_compile(TestSuites), {TestSuites,SuiteErrs,SuiteErrs++HelpErrs} end, case continue(AllMakeErrors, Opts#opts.abort_if_missing_suites) of true -> SavedErrors = save_make_errors(SuiteMakeErrors), ct_repeat:log_loop_info(Args), try final_tests(Tests,Skip,SavedErrors) of {Tests1,Skip1} -> ReleaseSh = proplists:get_value(release_shell, Args), ct_util:set_testdata({release_shell,ReleaseSh}), TestResult = possibly_spawn(ReleaseSh == true, Tests1, Skip1, Opts), case TestResult of {Ok,Errors,Skipped} -> NoOfMakeErrors = lists:foldl(fun({_,BadMods}, X) -> X + length(BadMods) end, 0, SuiteMakeErrors), {Ok,Errors+NoOfMakeErrors,Skipped}; ErrorResult -> ErrorResult end catch _:BadFormat -> {error,BadFormat} end; false -> io:nl(), ct_util:stop(clean), BadMods = lists:foldr( fun({{_,_},Ms}, Acc) -> Ms ++ lists:foldl( fun(M, Acc1) -> lists:delete(M, Acc1) end, Acc, Ms) end, [], AllMakeErrors), {error,{make_failed,BadMods}} end. %% keep the shell as the top controlling process possibly_spawn(false, Tests, Skip, Opts) -> TestResult = (catch do_run_test(Tests, Skip, Opts)), case TestResult of {EType,_} = Error when EType == user_error; EType == error -> ct_util:stop(clean), exit(Error); _ -> ct_util:stop(normal), TestResult end; %% we must return control to the shell now, so we spawn %% a test supervisor process to keep an eye on the test run possibly_spawn(true, Tests, Skip, Opts) -> CTUtilSrv = whereis(ct_util_server), Supervisor = fun() -> ct_util:mark_process(), process_flag(trap_exit, true), link(CTUtilSrv), TestRun = fun() -> ct_util:mark_process(), TestResult = (catch do_run_test(Tests, Skip, Opts)), case TestResult of {EType,_} = Error when EType == user_error; EType == error -> ct_util:stop(clean), exit(Error); _ -> ct_util:stop(normal), exit({ok,TestResult}) end end, TestRunPid = spawn_link(TestRun), receive {'EXIT',TestRunPid,{ok,TestResult}} -> io:format(user, "~nCommon Test returned ~tp~n~n", [TestResult]); {'EXIT',TestRunPid,Error} -> exit(Error) end end, unlink(CTUtilSrv), SupPid = spawn(Supervisor), io:format(user, "~nTest control handed over to process ~w~n~n", [SupPid]), SupPid. %% attempt to compile the modules specified in TestSuites auto_compile(TestSuites) -> io:format("~nCommon Test: Running make in test directories...~n"), UserInclude = case application:get_env(common_test, include) of {ok,UserInclDirs} when length(UserInclDirs) > 0 -> io:format("Including the following directories:~n"), [begin io:format("~tp~n",[UserInclDir]), {i,UserInclDir} end || UserInclDir <- UserInclDirs]; _ -> [] end, SuiteMakeErrors = lists:flatmap(fun({TestDir,Suite} = TS) -> case run_make(suites, TestDir, Suite, UserInclude, [nowarn_export_all]) of {error,{make_failed,Bad}} -> [{TS,Bad}]; {error,_} -> [{TS,[filename:join(TestDir, "*_SUITE")]}]; _ -> [] end end, TestSuites), %% try to compile other modules than SUITEs in the test directories {_,HelpMakeErrors} = lists:foldl( fun({Dir,Suite}, {Done,Failed}) -> case lists:member(Dir, Done) of false -> Failed1 = case run_make(helpmods, Dir, Suite, UserInclude, []) of {error,{make_failed,BadMods}} -> [{{Dir,all},BadMods}|Failed]; {error,_} -> [{{Dir,all},[Dir]}|Failed]; _ -> Failed end, {[Dir|Done],Failed1}; true -> % already visited {Done,Failed} end end, {[],[]}, TestSuites), {SuiteMakeErrors,lists:reverse(HelpMakeErrors)}. %% verify that specified test suites exist (if auto compile is disabled) verify_suites(TestSuites) -> io:nl(), Verify = fun({Dir,Suite}=DS,{Found,NotFound}) -> case locate_test_dir(Dir, Suite) of {ok,TestDir} -> if Suite == all -> {[DS|Found],NotFound}; true -> Beam = filename:join(TestDir, atom_to_list(Suite)++ ".beam"), case filelib:is_regular(Beam) of true -> {[DS|Found],NotFound}; false -> case code:is_loaded(Suite) of {file,SuiteFile} -> %% test suite is already %% loaded and since %% auto_compile == false, %% let's assume the user has %% loaded the beam file %% explicitly ActualDir = filename:dirname(SuiteFile), {[{ActualDir,Suite}|Found], NotFound}; false -> Name = filename:join(TestDir, atom_to_list( Suite)), io:format(user, "Suite ~w not found " "in directory ~ts~n", [Suite,TestDir]), {Found,[{DS,[Name]}|NotFound]} end end end; {error,_Reason} -> case code:is_loaded(Suite) of {file,SuiteFile} -> %% test suite is already loaded and since %% auto_compile == false, let's assume the %% user has loaded the beam file explicitly ActualDir = filename:dirname(SuiteFile), {[{ActualDir,Suite}|Found],NotFound}; false -> io:format(user, "Directory ~ts is " "invalid~n", [Dir]), Name = filename:join(Dir, atom_to_list(Suite)), {Found,[{DS,[Name]}|NotFound]} end end end, {ActualFound,Missing} = lists:foldl(Verify, {[],[]}, TestSuites), {lists:reverse(ActualFound),lists:reverse(Missing)}. save_make_errors([]) -> []; save_make_errors(Errors) -> Suites = get_bad_suites(Errors,[]), ct_logs:log("MAKE RESULTS", "Error compiling or locating the " "following suites: ~n~p",[Suites]), %% save the info for logger ok = file:write_file(?missing_suites_info,term_to_binary(Errors)), Errors. get_bad_suites([{{_TestDir,_Suite},Failed}|Errors], BadSuites) -> get_bad_suites(Errors,BadSuites++Failed); get_bad_suites([], BadSuites) -> BadSuites. step(TestDir, Suite, Case) -> step(TestDir, Suite, Case, []). step(TestDir, Suite, Case, Opts) when is_list(TestDir), is_atom(Suite), is_atom(Case), Suite =/= all, Case =/= all -> do_run([{TestDir,Suite,Case}], [{step,Opts}]). %%%----------------------------------------------------------------- %%% Internal suite_tuples([{TestDir,Suites,_} | Tests]) when is_list(Suites) -> lists:map(fun(S) -> {TestDir,S} end, Suites) ++ suite_tuples(Tests); suite_tuples([{TestDir,Suite,_} | Tests]) when is_atom(Suite) -> [{TestDir,Suite} | suite_tuples(Tests)]; suite_tuples([]) -> []. final_tests(Tests, Skip, Bad) -> {Tests1,Skip1} = final_tests1(Tests, [], Skip, Bad), Skip2 = final_skip(Skip1, []), {Tests1,Skip2}. final_tests1([{TestDir,Suites,_}|Tests], Final, Skip, Bad) when is_list(Suites), is_atom(hd(Suites)) -> Skip1 = [{TD,S,make_failed} || {{TD,S},_} <- Bad, S1 <- Suites, S == S1, TD == TestDir], Final1 = [{TestDir,S,all} || S <- Suites], final_tests1(Tests, lists:reverse(Final1)++Final, Skip++Skip1, Bad); final_tests1([{TestDir,all,all}|Tests], Final, Skip, Bad) -> MissingSuites = case lists:keysearch({TestDir,all}, 1, Bad) of {value,{_,Failed}} -> [list_to_atom(filename:basename(F)) || F <- Failed]; false -> [] end, Missing = [{TestDir,S,make_failed} || S <- MissingSuites], Final1 = [{TestDir,all,all}|Final], final_tests1(Tests, Final1, Skip++Missing, Bad); final_tests1([{TestDir,Suite,Cases}|Tests], Final, Skip, Bad) when Cases==[]; Cases==all -> final_tests1([{TestDir,[Suite],all}|Tests], Final, Skip, Bad); final_tests1([{TestDir,Suite,GrsOrCs}|Tests], Final, Skip, Bad) when is_list(GrsOrCs) -> case lists:keymember({TestDir,Suite}, 1, Bad) of true -> Skip1 = Skip ++ [{TestDir,Suite,all,make_failed}], final_tests1(Tests, [{TestDir,Suite,all}|Final], Skip1, Bad); false -> GrsOrCs1 = lists:flatmap( %% for now, only flat group defs are allowed as %% start options and test spec terms fun({all,all}) -> [ct_groups:make_conf(TestDir, Suite, all, [], all)]; ({skipped,Group,TCs}) -> [ct_groups:make_conf(TestDir, Suite, Group, [skipped], TCs)]; ({skipped,TC}) -> case lists:member(TC, GrsOrCs) of true -> []; false -> [TC] end; ({GrSpec = {GroupName,_},TCs}) -> Props = [{override,GrSpec}], [ct_groups:make_conf(TestDir, Suite, GroupName, Props, TCs)]; ({GrSpec = {GroupName,_,_},TCs}) -> Props = [{override,GrSpec}], [ct_groups:make_conf(TestDir, Suite, GroupName, Props, TCs)]; ({GroupOrGroups,TCs}) -> case GroupOrGroups of [GroupList] when is_list(GroupList) -> {GrpNames, Props} = lists:foldl( fun({GrpName,_} = GrSpec, {GrpNames, Props}) -> {lists:append(GrpNames, [GrpName]), [GrSpec | Props]}; ({GrpName,_,_} = GrSpec, {GrpNames, Props}) -> {lists:append(GrpNames, [GrpName]), [GrSpec | Props]}; (GrpName, {GrpNames, Props}) -> {lists:append(GrpNames, [GrpName]), Props} end, {[], []}, GroupList), [ct_groups:make_conf(TestDir, Suite, [GrpNames], [{override, Props}], TCs)]; _ -> [ct_groups:make_conf(TestDir, Suite, GroupOrGroups, [], TCs)] end; (TC) -> [TC] end, GrsOrCs), Do = {TestDir,Suite,GrsOrCs1}, final_tests1(Tests, [Do|Final], Skip, Bad) end; final_tests1([], Final, Skip, _Bad) -> {lists:reverse(Final),Skip}. final_skip([{TestDir,Suite,{all,all},Reason}|Skips], Final) -> SkipConf = ct_groups:make_conf(TestDir, Suite, all, [], all), Skip = {TestDir,Suite,SkipConf,Reason}, final_skip(Skips, [Skip|Final]); final_skip([{TestDir,Suite,{Group,TCs},Reason}|Skips], Final) -> Conf = ct_groups:make_conf(TestDir, Suite, Group, [], TCs), Skip = {TestDir,Suite,Conf,Reason}, final_skip(Skips, [Skip|Final]); final_skip([Skip|Skips], Final) -> final_skip(Skips, [Skip|Final]); final_skip([], Final) -> lists:reverse(Final). continue([], _) -> true; continue(_MakeErrors, true) -> false; continue(_MakeErrors, _AbortIfMissingSuites) -> io:nl(), OldGL = group_leader(), case set_group_leader_same_as_shell(OldGL) of true -> S = self(), io:format("Failed to compile or locate one " "or more test suites\n" "Press \'c\' to continue or \'a\' to abort.\n" "Will continue in 15 seconds if no " "answer is given!\n"), Pid = spawn(fun() -> case io:get_line('(c/a) ') of "c\n" -> S ! true; _ -> S ! false end end), group_leader(OldGL, self()), receive R when R==true; R==false -> R after 15000 -> exit(Pid, kill), io:format("... timeout - continuing!!\n"), true end; false -> % no shell process to use true end. set_group_leader_same_as_shell(OldGL) -> %% find the group leader process on the node in a dirty fashion %% (check initial function call and look in the process dictionary) GS2or3 = fun(P) -> case process_info(P,initial_call) of {initial_call,{group,server,X}} when X == 2 ; X == 3 -> true; _ -> false end end, case [P || P <- processes(), GS2or3(P), true == lists:keymember(shell,1, element(2,process_info(P,dictionary)))] of [GL|_] -> %% check if started from remote node (skip interaction) if node(OldGL) /= node(GL) -> false; true -> group_leader(GL, self()) end; [] -> false end. check_and_add([{TestDir0,M,_} | Tests], Added, PA) -> case locate_test_dir(TestDir0, M) of {ok,TestDir} -> case lists:member(TestDir, Added) of true -> check_and_add(Tests, Added, PA); false -> case lists:member(rm_trailing_slash(TestDir), code:get_path()) of false -> true = code:add_patha(TestDir), check_and_add(Tests, [TestDir|Added], [TestDir|PA]); true -> check_and_add(Tests, [TestDir|Added], PA) end end; {error,_} -> {error,{invalid_directory,TestDir0}} end; check_and_add([], _, PA) -> {ok,PA}. do_run_test(Tests, Skip, Opts0) -> case check_and_add(Tests, [], []) of {ok,AddedToPath} -> ct_util:set_testdata({stats,{0,0,{0,0}}}), %% test_server needs to know the include path too InclPath = case application:get_env(common_test, include) of {ok,Incls} -> Incls; _ -> [] end, application:set_env(test_server, include, InclPath), %% copy the escape characters setting to test_server EscChars = case application:get_env(common_test, esc_chars) of {ok,ECBool} -> ECBool; _ -> true end, application:set_env(test_server, esc_chars, EscChars), {ok, _} = test_server_ctrl:start_link(local), %% let test_server expand the test tuples and count no of cases {Suites,NoOfCases} = count_test_cases(Tests, Skip), Suites1 = delete_dups(Suites), NoOfTests = length(Tests), NoOfSuites = length(Suites1), ct_util:warn_duplicates(Suites1), {ok,Cwd} = file:get_cwd(), io:format("~nCWD set to: ~tp~n", [Cwd]), if NoOfCases == unknown -> io:format("~nTEST INFO: ~w test(s), ~w suite(s)~n~n", [NoOfTests,NoOfSuites]), ct_logs:log("TEST INFO","~w test(s), ~w suite(s)", [NoOfTests,NoOfSuites]); true -> io:format("~nTEST INFO: ~w test(s), ~w case(s) " "in ~w suite(s)~n~n", [NoOfTests,NoOfCases,NoOfSuites]), ct_logs:log("TEST INFO","~w test(s), ~w case(s) " "in ~w suite(s)", [NoOfTests,NoOfCases,NoOfSuites]) end, %% if the verbosity level is set lower than ?STD_IMPORTANCE, tell %% test_server to ignore stdout printouts to the test case log file case proplists:get_value(default, Opts0#opts.verbosity) of VLvl when is_integer(VLvl), (?STD_IMPORTANCE < (100-VLvl)) -> test_server_ctrl:reject_io_reqs(true); _Lower -> ok end, case Opts0#opts.multiply_timetraps of undefined -> MultTT = 1; MultTT -> MultTT end, case Opts0#opts.scale_timetraps of undefined -> ScaleTT = false; ScaleTT -> ScaleTT end, ct_logs:log("TEST INFO","Timetrap time multiplier = ~w~n" "Timetrap scaling enabled = ~w", [MultTT,ScaleTT]), test_server_ctrl:multiply_timetraps(MultTT), test_server_ctrl:scale_timetraps(ScaleTT), test_server_ctrl:create_priv_dir(choose_val( Opts0#opts.create_priv_dir, auto_per_run)), {ok,LogDir} = ct_logs:get_log_dir(true), {TsCoverInfo,Opts} = maybe_start_cover(Opts0, LogDir), ct_event:notify(#event{name=start_info, node=node(), data={NoOfTests,NoOfSuites,NoOfCases}}), CleanUp = add_jobs(Tests, Skip, Opts, []), unlink(whereis(test_server_ctrl)), catch test_server_ctrl:wait_finish(), maybe_stop_cover(Opts, TsCoverInfo, LogDir), %% check if last testcase has left a "dead" trace window %% behind, and if so, kill it case ct_util:get_testdata(interpret) of {_What,kill,{TCPid,AttPid}} -> ct_util:kill_attached(TCPid, AttPid); _ -> ok end, lists:foreach(fun(Suite) -> maybe_cleanup_interpret(Suite, Opts#opts.step) end, CleanUp), _ = [code:del_path(Dir) || Dir <- AddedToPath], %% If a severe error has occurred in the test_server, %% we will generate an exception here. case ct_util:get_testdata(severe_error) of undefined -> ok; SevereError -> ct_logs:log("SEVERE ERROR", "~tp\n", [SevereError]), exit(SevereError) end, case ct_util:get_testdata(stats) of Stats = {_Ok,_Failed,{_UserSkipped,_AutoSkipped}} -> Stats; _ -> {error,test_result_unknown} end; Error -> exit(Error) end. maybe_start_cover(Opts=#opts{cover=Cover,cover_stop=CoverStop0},LogDir) -> if Cover == undefined -> {undefined,Opts}; true -> case ct_cover:get_spec(Cover) of {error,Reason} -> exit({error,Reason}); CoverSpec -> CoverStop = case CoverStop0 of undefined -> true; Stop -> Stop end, start_cover(Opts#opts{coverspec=CoverSpec, cover_stop=CoverStop}, LogDir) end end. start_cover(Opts=#opts{coverspec=CovData,cover_stop=CovStop},LogDir) -> {CovFile, CovNodes, CovImport, _CovExport, #cover{app = CovApp, local_only = LocalOnly, level = CovLevel, excl_mods = CovExcl, incl_mods = CovIncl, cross = CovCross, src = _CovSrc}} = CovData, case LocalOnly of true -> cover:local_only(); false -> ok end, ct_logs:log("COVER INFO", "Using cover specification file: ~ts~n" "App: ~w~n" "Local only: ~w~n" "Cross cover: ~w~n" "Including ~w modules~n" "Excluding ~w modules", [CovFile,CovApp,LocalOnly,CovCross, length(CovIncl),length(CovExcl)]), %% Tell test_server to print a link in its coverlog %% pointing to the real coverlog which will be written in %% maybe_stop_cover/2 test_server_ctrl:cover({log,LogDir}), %% Cover compile all modules {ok,TsCoverInfo} = test_server_ctrl:cover_compile(CovApp,CovFile, CovExcl,CovIncl, CovCross,CovLevel, CovStop), ct_logs:log("COVER INFO", "Compilation completed - test_server cover info: ~tp", [TsCoverInfo]), %% start cover on specified nodes if (CovNodes /= []) and (CovNodes /= undefined) -> ct_logs:log("COVER INFO", "Nodes included in cover " "session: ~tw", [CovNodes]), cover:start(CovNodes); true -> ok end, lists:foreach( fun(Imp) -> case cover:import(Imp) of ok -> ok; {error,Reason} -> ct_logs:log("COVER INFO", "Importing cover data from: ~ts fails! " "Reason: ~tp", [Imp,Reason]) end end, CovImport), {TsCoverInfo,Opts}. maybe_stop_cover(_,undefined,_) -> ok; maybe_stop_cover(#opts{coverspec=CovData},TsCoverInfo,LogDir) -> {_CovFile, _CovNodes, _CovImport, CovExport, _AppData} = CovData, case CovExport of undefined -> ok; _ -> ct_logs:log("COVER INFO","Exporting cover data to ~tp",[CovExport]), cover:export(CovExport) end, ct_logs:log("COVER INFO","Analysing cover data to ~tp",[LogDir]), test_server_ctrl:cover_analyse(TsCoverInfo,LogDir), ct_logs:log("COVER INFO","Analysis completed.",[]), ok. delete_dups([S | Suites]) -> Suites1 = lists:delete(S, Suites), [S | delete_dups(Suites1)]; delete_dups([]) -> []. count_test_cases(Tests, Skip) -> SendResult = fun(Me, Result) -> Me ! {no_of_cases,Result} end, TSPid = test_server_ctrl:start_get_totals(SendResult), Ref = erlang:monitor(process, TSPid), _ = add_jobs(Tests, Skip, #opts{}, []), Counted = (catch count_test_cases1(length(Tests), 0, [], Ref)), erlang:demonitor(Ref, [flush]), case Counted of {error,{test_server_died}} = Error -> throw(Error); {error,Reason} -> unlink(whereis(test_server_ctrl)), test_server_ctrl:stop(), throw({user_error,Reason}); Result -> test_server_ctrl:stop_get_totals(), Result end. count_test_cases1(0, N, Suites, _) -> {lists:flatten(Suites), N}; count_test_cases1(Jobs, N, Suites, Ref) -> receive {_,{error,_Reason} = Error} -> throw(Error); {no_of_cases,{Ss,N1}} -> count_test_cases1(Jobs-1, add_known(N,N1), [Ss|Suites], Ref); {'DOWN', Ref, _, _, Info} -> throw({error,{test_server_died,Info}}) end. add_known(unknown, _) -> unknown; add_known(_, unknown) -> unknown; add_known(N, N1) -> N+N1. add_jobs([{TestDir,all,_}|Tests], Skip, Opts, CleanUp) -> Name = get_name(TestDir), case catch test_server_ctrl:add_dir_with_skip(Name, TestDir, skiplist(TestDir,Skip)) of {'EXIT',_} -> CleanUp; _ -> case wait_for_idle() of ok -> add_jobs(Tests, Skip, Opts, CleanUp); _ -> CleanUp end end; add_jobs([{TestDir,[Suite],all}|Tests], Skip, Opts, CleanUp) when is_atom(Suite) -> add_jobs([{TestDir,Suite,all}|Tests], Skip, Opts, CleanUp); add_jobs([{TestDir,Suites,all}|Tests], Skip, Opts, CleanUp) when is_list(Suites) -> Name = get_name(TestDir) ++ ".suites", case catch test_server_ctrl:add_module_with_skip(Name, Suites, skiplist(TestDir,Skip)) of {'EXIT',_} -> CleanUp; _ -> case wait_for_idle() of ok -> add_jobs(Tests, Skip, Opts, CleanUp); _ -> CleanUp end end; add_jobs([{TestDir,Suite,all}|Tests], Skip, Opts, CleanUp) -> case maybe_interpret(Suite, all, Opts) of ok -> Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite), case catch test_server_ctrl:add_module_with_skip(Name, [Suite], skiplist(TestDir, Skip)) of {'EXIT',_} -> CleanUp; _ -> case wait_for_idle() of ok -> add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); _ -> CleanUp end end; Error -> Error end; %% group (= conf case in test_server) add_jobs([{TestDir,Suite,Confs}|Tests], Skip, Opts, CleanUp) when element(1, hd(Confs)) == conf -> Group = fun(Conf) -> proplists:get_value(name, element(2, Conf)) end, TestCases = fun(Conf) -> element(4, Conf) end, TCTestName = fun(all) -> ""; ([C]) when is_atom(C) -> "." ++ atom_to_list(C); (Cs) when is_list(Cs) -> ".cases" end, GrTestName = case Confs of [Conf] -> case Group(Conf) of GrName when is_atom(GrName) -> "." ++ atom_to_list(GrName) ++ TCTestName(TestCases(Conf)); _ -> ".groups" ++ TCTestName(TestCases(Conf)) end; _ -> ".groups" end, TestName = get_name(TestDir) ++ "." ++ atom_to_list(Suite) ++ GrTestName, case maybe_interpret(Suite, init_per_group, Opts) of ok -> case catch test_server_ctrl:add_conf_with_skip(TestName, Suite, Confs, skiplist(TestDir, Skip)) of {'EXIT',_} -> CleanUp; _ -> case wait_for_idle() of ok -> add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); _ -> CleanUp end end; Error -> Error end; %% test case add_jobs([{TestDir,Suite,[Case]}|Tests], Skip, Opts, CleanUp) when is_atom(Case) -> add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opts, CleanUp); add_jobs([{TestDir,Suite,Cases}|Tests], Skip, Opts, CleanUp) when is_list(Cases) -> Cases1 = lists:map(fun({GroupName,_}) when is_atom(GroupName) -> GroupName; (Case) -> Case end, Cases), case maybe_interpret(Suite, Cases1, Opts) of ok -> Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite) ++ ".cases", case catch test_server_ctrl:add_cases_with_skip(Name, Suite, Cases1, skiplist(TestDir, Skip)) of {'EXIT',_} -> CleanUp; _ -> case wait_for_idle() of ok -> add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); _ -> CleanUp end end; Error -> Error end; add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opts, CleanUp) when is_atom(Case) -> case maybe_interpret(Suite, Case, Opts) of ok -> Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite) ++ "." ++ atom_to_list(Case), case catch test_server_ctrl:add_case_with_skip(Name, Suite, Case, skiplist(TestDir, Skip)) of {'EXIT',_} -> CleanUp; _ -> case wait_for_idle() of ok -> add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); _ -> CleanUp end end; Error -> Error end; add_jobs([], _, _, CleanUp) -> CleanUp. wait_for_idle() -> ct_util:update_last_run_index(), Notify = fun(Me,IdleState) -> Me ! {idle,IdleState}, receive {Me,proceed} -> ok after 30000 -> ok end end, case catch test_server_ctrl:idle_notify(Notify) of {'EXIT',_} -> error; TSPid -> %% so we don't hang forever if test_server dies Ref = erlang:monitor(process, TSPid), Result = receive {idle,abort} -> aborted; {idle,_} -> ok; {'DOWN', Ref, _, _, _} -> error end, erlang:demonitor(Ref, [flush]), ct_util:update_last_run_index(), %% let test_server_ctrl proceed (and possibly shut down) now TSPid ! {self(),proceed}, Result end. skiplist(Dir, [{Dir,all,Cmt}|Skip]) -> %% we need to turn 'all' into list of modules since %% test_server doesn't do skips on Dir level Ss = filelib:wildcard(filename:join(Dir, "*_SUITE.beam")), [{list_to_atom(filename:basename(S,".beam")),Cmt} || S <- Ss] ++ skiplist(Dir,Skip); skiplist(Dir, [{Dir,S,Cmt}|Skip]) -> [{S,Cmt} | skiplist(Dir, Skip)]; skiplist(Dir, [{Dir,S,C,Cmt}|Skip]) -> [{S,C,Cmt} | skiplist(Dir, Skip)]; skiplist(Dir, [_|Skip]) -> skiplist(Dir, Skip); skiplist(_Dir, []) -> []. get_name(Dir) -> TestDir = case filename:basename(Dir) of "test" -> filename:dirname(Dir); _ -> Dir end, Base = filename:basename(TestDir), case filename:basename(filename:dirname(TestDir)) of "" -> Base; TopDir -> TopDir ++ "." ++ Base end. run_make(TestDir, Mod, UserInclude) -> run_make(suites, TestDir, Mod, UserInclude, [nowarn_export_all]). run_make(Targets, TestDir0, Mod, UserInclude, COpts) when is_list(Mod) -> run_make(Targets, TestDir0, list_to_atom(Mod), UserInclude, COpts); run_make(Targets, TestDir0, Mod, UserInclude, COpts) -> case locate_test_dir(TestDir0, Mod) of {ok,TestDir} -> %% send a start_make notification which may suspend %% the process if some other node is compiling files %% in the same directory ct_event:sync_notify(#event{name=start_make, node=node(), data=TestDir}), {ok,Cwd} = file:get_cwd(), ok = file:set_cwd(TestDir), CtInclude = get_dir(common_test, "include"), XmerlInclude = get_dir(xmerl, "include"), ErlFlags = UserInclude ++ [{i,CtInclude}, {i,XmerlInclude}, debug_info] ++ COpts, Result = if Mod == all ; Targets == helpmods -> case (catch ct_make:all([noexec|ErlFlags])) of {'EXIT',_} = Failure -> Failure; MakeInfo -> FileTest = fun(F, suites) -> is_suite(F); (F, helpmods) -> not is_suite(F) end, Files = lists:flatmap(fun({F,out_of_date}) -> case FileTest(F, Targets) of true -> [F]; false -> [] end; (_) -> [] end, MakeInfo), (catch ct_make:files(Files, [load|ErlFlags])) end; true -> (catch ct_make:files([Mod], [load|ErlFlags])) end, ok = file:set_cwd(Cwd), %% send finished_make notification ct_event:notify(#event{name=finished_make, node=node(), data=TestDir}), case Result of {up_to_date,_} -> ok; {'EXIT',Reason} -> io:format("{error,{make_crashed,~tp}\n", [Reason]), {error,{make_crashed,TestDir,Reason}}; {error,ModInfo} -> io:format("{error,make_failed}\n", []), Bad = [filename:join(TestDir, M) || {M,R} <- ModInfo, R == error], {error,{make_failed,Bad}} end; {error,_} -> io:format("{error,{invalid_directory,~tp}}\n", [TestDir0]), {error,{invalid_directory,TestDir0}} end. get_dir(App, Dir) -> filename:join(code:lib_dir(App), Dir). maybe_interpret(Suite, Cases, #opts{step = StepOpts}) when StepOpts =/= undefined -> %% if other suite has run before this one, check if last testcase %% has left a "dead" trace window behind, and if so, kill it case ct_util:get_testdata(interpret) of {_What,kill,{TCPid,AttPid}} -> ct_util:kill_attached(TCPid, AttPid); _ -> ok end, maybe_interpret1(Suite, Cases, StepOpts); maybe_interpret(_, _, _) -> ok. maybe_interpret1(Suite, all, StepOpts) -> case i:ii(Suite) of {module,_} -> i:iaa([break]), case get_all_testcases(Suite) of {error,_} -> {error,no_testcases_found}; Cases -> maybe_interpret2(Suite, Cases, StepOpts) end; error -> {error,could_not_interpret_module} end; maybe_interpret1(Suite, Case, StepOpts) when is_atom(Case) -> maybe_interpret1(Suite, [Case], StepOpts); maybe_interpret1(Suite, Cases, StepOpts) when is_list(Cases) -> case i:ii(Suite) of {module,_} -> i:iaa([break]), maybe_interpret2(Suite, Cases, StepOpts); error -> {error,could_not_interpret_module} end. maybe_interpret2(Suite, Cases, StepOpts) -> set_break_on_config(Suite, StepOpts), _ = [begin try i:ib(Suite, Case, 1) of _ -> ok catch _:_Error -> io:format(user, "Invalid breakpoint: ~w:~tw/1~n", [Suite,Case]) end end || Case <- Cases, is_atom(Case)], test_server_ctrl:multiply_timetraps(infinity), WinOp = case lists:member(keep_inactive, ensure_atom(StepOpts)) of true -> no_kill; false -> kill end, ct_util:set_testdata({interpret,{{Suite,Cases},WinOp, {undefined,undefined}}}), ok. set_break_on_config(Suite, StepOpts) -> case lists:member(config, ensure_atom(StepOpts)) of true -> SetBPIfExists = fun(F,A) -> case erlang:function_exported(Suite, F, A) of true -> i:ib(Suite, F, A); false -> ok end end, ok = SetBPIfExists(init_per_suite, 1), ok = SetBPIfExists(init_per_group, 2), ok = SetBPIfExists(init_per_testcase, 2), ok = SetBPIfExists(end_per_testcase, 2), ok = SetBPIfExists(end_per_group, 2), ok = SetBPIfExists(end_per_suite, 1); false -> ok end. maybe_cleanup_interpret(_, undefined) -> ok; maybe_cleanup_interpret(Suite, _) -> i:iq(Suite). log_ts_names([]) -> ok; log_ts_names(Specs) -> List = lists:map(fun(Name) -> Name ++ " " end, Specs), ct_logs:log("Test Specification file(s)", "~ts", [lists:flatten(List)]). merge_arguments(Args) -> merge_arguments(Args, []). merge_arguments([LogDir={logdir,_}|Args], Merged) -> merge_arguments(Args, handle_arg(replace, LogDir, Merged)); merge_arguments([CoverFile={cover,_}|Args], Merged) -> merge_arguments(Args, handle_arg(replace, CoverFile, Merged)); merge_arguments([CoverStop={cover_stop,_}|Args], Merged) -> merge_arguments(Args, handle_arg(replace, CoverStop, Merged)); merge_arguments([{'case',TC}|Args], Merged) -> merge_arguments(Args, handle_arg(merge, {testcase,TC}, Merged)); merge_arguments([Arg|Args], Merged) -> merge_arguments(Args, handle_arg(merge, Arg, Merged)); merge_arguments([], Merged) -> Merged. handle_arg(replace, {Key,Elems}, [{Key,_}|Merged]) -> [{Key,Elems}|Merged]; handle_arg(merge, {event_handler_init,Elems}, [{event_handler_init,PrevElems}|Merged]) -> [{event_handler_init,PrevElems++["add"|Elems]}|Merged]; handle_arg(merge, {userconfig,Elems}, [{userconfig,PrevElems}|Merged]) -> [{userconfig,PrevElems++["add"|Elems]}|Merged]; handle_arg(merge, {Key,Elems}, [{Key,PrevElems}|Merged]) -> [{Key,PrevElems++Elems}|Merged]; handle_arg(Op, Arg, [Other|Merged]) -> [Other|handle_arg(Op, Arg, Merged)]; handle_arg(_,Arg,[]) -> [Arg]. get_start_opt(Key, IfExists, Args) -> get_start_opt(Key, IfExists, undefined, Args). get_start_opt(Key, IfExists, IfNotExists, Args) -> try try_get_start_opt(Key, IfExists, IfNotExists, Args) of Result -> Result catch error:_ -> exit({user_error,{bad_argument,Key}}) end. try_get_start_opt(Key, IfExists, IfNotExists, Args) -> case lists:keysearch(Key, 1, Args) of {value,{Key,Val}} when is_function(IfExists) -> IfExists(Val); {value,{Key,Val}} when IfExists == value -> Val; {value,{Key,_Val}} -> IfExists; _ -> IfNotExists end. ct_hooks_args2opts(Args) -> lists:foldl(fun({ct_hooks,Hooks}, Acc) -> ct_hooks_args2opts(Hooks,Acc); (_,Acc) -> Acc end,[],Args). ct_hooks_args2opts([CTH,Arg,Prio,"and"| Rest],Acc) when Arg /= "and" -> ct_hooks_args2opts(Rest,[{list_to_atom(CTH), parse_cth_args(Arg), parse_cth_args(Prio)}|Acc]); ct_hooks_args2opts([CTH,Arg,"and"| Rest],Acc) -> ct_hooks_args2opts(Rest,[{list_to_atom(CTH), parse_cth_args(Arg)}|Acc]); ct_hooks_args2opts([CTH], Acc) -> ct_hooks_args2opts([CTH,"and"],Acc); ct_hooks_args2opts([CTH, "and" | Rest], Acc) -> ct_hooks_args2opts(Rest,[list_to_atom(CTH)|Acc]); ct_hooks_args2opts([CTH, Args], Acc) -> ct_hooks_args2opts([CTH, Args, "and"],Acc); ct_hooks_args2opts([CTH, Args, Prio], Acc) -> ct_hooks_args2opts([CTH, Args, Prio, "and"],Acc); ct_hooks_args2opts([],Acc) -> lists:reverse(Acc). parse_cth_args(String) -> try true = io_lib:printable_unicode_list(String), {ok,Toks,_} = erl_scan:string(String++"."), {ok, Args} = erl_parse:parse_term(Toks), Args catch _:_ -> String end. event_handler_args2opts(Args) -> case proplists:get_value(event_handler, Args) of undefined -> event_handler_args2opts([], Args); EHs -> event_handler_args2opts([{list_to_atom(EH),[]} || EH <- EHs], Args) end. event_handler_args2opts(Default, Args) -> case proplists:get_value(event_handler_init, Args) of undefined -> Default; EHs -> event_handler_init_args2opts(EHs) end. event_handler_init_args2opts([EH, Arg, "and" | EHs]) -> [{list_to_atom(EH),lists:flatten(io_lib:format("~ts",[Arg]))} | event_handler_init_args2opts(EHs)]; event_handler_init_args2opts([EH, Arg]) -> [{list_to_atom(EH),lists:flatten(io_lib:format("~ts",[Arg]))}]; event_handler_init_args2opts([]) -> []. verbosity_args2opts(Args) -> case proplists:get_value(verbosity, Args) of undefined -> []; VArgs -> GetVLvls = fun("and", {new,SoFar}) when is_list(SoFar) -> {new,SoFar}; ("and", {Lvl,SoFar}) when is_list(SoFar) -> {new,[{'$unspecified',list_to_integer(Lvl)} | SoFar]}; (CatOrLvl, {new,SoFar}) when is_list(SoFar) -> {CatOrLvl,SoFar}; (Lvl, {Cat,SoFar}) -> {new,[{list_to_atom(Cat),list_to_integer(Lvl)} | SoFar]} end, case lists:foldl(GetVLvls, {new,[]}, VArgs) of {new,Parsed} -> Parsed; {Lvl,Parsed} -> [{'$unspecified',list_to_integer(Lvl)} | Parsed] end end. add_verbosity_defaults(VLvls) -> case {proplists:get_value('$unspecified', VLvls), proplists:get_value(default, VLvls)} of {undefined,undefined} -> ?default_verbosity ++ VLvls; {Lvl,undefined} -> [{default,Lvl} | VLvls]; {undefined,_Lvl} -> [{'$unspecified',?MAX_VERBOSITY} | VLvls]; _ -> VLvls end. %% This function reads pa and pz arguments, converts dirs from relative %% to absolute, and re-inserts them in the code path. The order of the %% dirs in the code path remain the same. Note however that since this %% function is only used for arguments "pre run_test erl_args", the order %% relative dirs "post run_test erl_args" is not kept! rel_to_abs(CtArgs) -> {PA,PZ} = get_pa_pz(CtArgs, [], []), _ = [begin Dir = rm_trailing_slash(D), Abs = make_abs(Dir), _ = if Dir /= Abs -> _ = code:del_path(Dir), _ = code:del_path(Abs), io:format(user, "Converting ~tp to ~tp and re-inserting " "with add_pathz/1~n", [Dir, Abs]); true -> _ = code:del_path(Dir) end, code:add_pathz(Abs) end || D <- PZ], _ = [begin Dir = rm_trailing_slash(D), Abs = make_abs(Dir), _ = if Dir /= Abs -> _ = code:del_path(Dir), _ = code:del_path(Abs), io:format(user, "Converting ~tp to ~tp and re-inserting " "with add_patha/1~n", [Dir, Abs]); true -> _ = code:del_path(Dir) end, code:add_patha(Abs) end || D <- PA], io:format(user, "~n", []). rm_trailing_slash(Dir) -> filename:join(filename:split(Dir)). get_pa_pz([{pa,Dirs} | Args], PA, PZ) -> get_pa_pz(Args, PA ++ Dirs, PZ); get_pa_pz([{pz,Dirs} | Args], PA, PZ) -> get_pa_pz(Args, PA, PZ ++ Dirs); get_pa_pz([_ | Args], PA, PZ) -> get_pa_pz(Args, PA, PZ); get_pa_pz([], PA, PZ) -> {PA,PZ}. make_abs(RelDir) -> Tokens = filename:split(filename:absname(RelDir)), filename:join(lists:reverse(make_abs1(Tokens, []))). make_abs1([".."|Dirs], [_Dir|Path]) -> make_abs1(Dirs, Path); make_abs1(["."|Dirs], Path) -> make_abs1(Dirs, Path); make_abs1([Dir|Dirs], Path) -> make_abs1(Dirs, [Dir|Path]); make_abs1([], Path) -> Path. %% This function translates ct:run_test/1 start options %% to ct_run start arguments (on the init arguments format) - %% this is useful mainly for testing the ct_run start functions. opts2args(EnvStartOpts) -> lists:flatmap(fun({exit_status,ExitStatusOpt}) when is_atom(ExitStatusOpt) -> [{exit_status,[atom_to_list(ExitStatusOpt)]}]; ({halt_with,{HaltM,HaltF}}) -> [{halt_with,[atom_to_list(HaltM), atom_to_list(HaltF)]}]; ({interactive_mode,true}) -> [{shell,[]}]; ({config,CfgFile}) when is_integer(hd(CfgFile)) -> [{ct_config,[CfgFile]}]; ({config,CfgFiles}) when is_list(hd(CfgFiles)) -> [{ct_config,CfgFiles}]; ({userconfig,{CBM,CfgStr=[X|_]}}) when is_integer(X) -> [{userconfig,[atom_to_list(CBM),CfgStr]}]; ({userconfig,{CBM,CfgStrs}}) when is_list(CfgStrs) -> [{userconfig,[atom_to_list(CBM) | CfgStrs]}]; ({userconfig,UserCfg}) when is_list(UserCfg) -> Strs = lists:map(fun({CBM,CfgStr=[X|_]}) when is_integer(X) -> [atom_to_list(CBM), CfgStr,"and"]; ({CBM,CfgStrs}) when is_list(CfgStrs) -> [atom_to_list(CBM) | CfgStrs] ++ ["and"] end, UserCfg), [_LastAnd|StrsR] = lists:reverse(lists:flatten(Strs)), [{userconfig,lists:reverse(StrsR)}]; ({group,G}) when is_atom(G) -> [{group,[atom_to_list(G)]}]; ({group,Gs}) when is_list(Gs) -> LOfGStrs = [lists:flatten(io_lib:format("~tw",[G])) || G <- Gs], [{group,LOfGStrs}]; ({testcase,Case}) when is_atom(Case) -> [{'case',[atom_to_list(Case)]}]; ({testcase,Cases}) -> [{'case',[atom_to_list(C) || C <- Cases]}]; ({'case',Cases}) -> [{'case',[atom_to_list(C) || C <- Cases]}]; ({allow_user_terms,true}) -> [{allow_user_terms,[]}]; ({allow_user_terms,false}) -> []; ({join_specs,true}) -> [{join_specs,[]}]; ({join_specs,false}) -> []; ({auto_compile,false}) -> [{no_auto_compile,[]}]; ({auto_compile,true}) -> []; ({scale_timetraps,true}) -> [{scale_timetraps,[]}]; ({scale_timetraps,false}) -> []; ({create_priv_dir,auto_per_run}) -> []; ({create_priv_dir,PD}) when is_atom(PD) -> [{create_priv_dir,[atom_to_list(PD)]}]; ({force_stop,skip_rest}) -> [{force_stop,["skip_rest"]}]; ({force_stop,true}) -> [{force_stop,[]}]; ({force_stop,false}) -> []; ({decrypt,{key,Key}}) -> [{ct_decrypt_key,[Key]}]; ({decrypt,{file,File}}) -> [{ct_decrypt_file,[File]}]; ({basic_html,true}) -> [{basic_html,[]}]; ({basic_html,false}) -> []; ({esc_chars,false}) -> [{no_esc_chars,[]}]; ({esc_chars,true}) -> []; ({event_handler,EH}) when is_atom(EH) -> [{event_handler,[atom_to_list(EH)]}]; ({event_handler,EHs}) when is_list(EHs) -> [{event_handler,[atom_to_list(EH) || EH <- EHs]}]; ({event_handler,{EH,Arg}}) when is_atom(EH) -> ArgStr = lists:flatten(io_lib:format("~tp", [Arg])), [{event_handler_init,[atom_to_list(EH),ArgStr]}]; ({event_handler,{EHs,Arg}}) when is_list(EHs) -> ArgStr = lists:flatten(io_lib:format("~tp", [Arg])), Strs = lists:flatmap(fun(EH) -> [atom_to_list(EH), ArgStr,"and"] end, EHs), [_LastAnd | StrsR] = lists:reverse(Strs), [{event_handler_init,lists:reverse(StrsR)}]; ({logopts,LOs}) when is_list(LOs) -> [{logopts,[atom_to_list(LO) || LO <- LOs]}]; ({verbosity,?default_verbosity}) -> []; ({verbosity,VLvl}) when is_integer(VLvl) -> [{verbosity,[integer_to_list(VLvl)]}]; ({verbosity,VLvls}) when is_list(VLvls) -> VLvlArgs = lists:flatmap(fun({'$unspecified',Lvl}) -> [integer_to_list(Lvl), "and"]; ({Cat,Lvl}) -> [atom_to_list(Cat), integer_to_list(Lvl), "and"]; (Lvl) -> [integer_to_list(Lvl), "and"] end, VLvls), [_LastAnd|VLvlArgsR] = lists:reverse(VLvlArgs), [{verbosity,lists:reverse(VLvlArgsR)}]; ({ct_hooks,[]}) -> []; ({ct_hooks,CTHs}) when is_list(CTHs) -> io:format(user,"ct_hooks: ~tp",[CTHs]), Strs = lists:flatmap( fun({CTH,Arg,Prio}) -> [atom_to_list(CTH), lists:flatten( io_lib:format("~tp",[Arg])), lists:flatten( io_lib:format("~tp",[Prio])), "and"]; ({CTH,Arg}) -> [atom_to_list(CTH), lists:flatten( io_lib:format("~tp",[Arg])), "and"]; (CTH) when is_atom(CTH) -> [atom_to_list(CTH),"and"] end,CTHs), [_LastAnd|StrsR] = lists:reverse(Strs), io:format(user,"return: ~tp",[lists:reverse(StrsR)]), [{ct_hooks,lists:reverse(StrsR)}]; ({Opt,As=[A|_]}) when is_atom(A) -> [{Opt,[atom_to_list(Atom) || Atom <- As]}]; ({Opt,Strs=[S|_]}) when is_list(S) -> [{Opt,Strs}]; ({Opt,A}) when is_atom(A) -> [{Opt,[atom_to_list(A)]}]; ({Opt,I}) when is_integer(I) -> [{Opt,[integer_to_list(I)]}]; ({Opt,I}) when is_float(I) -> [{Opt,[float_to_list(I)]}]; ({Opt,S}) when is_list(S) -> [{Opt,[S]}]; (Opt) -> Opt end, EnvStartOpts). locate_test_dir(Dir, Suite) -> TestDir = case ct_util:is_test_dir(Dir) of true -> Dir; false -> ct_util:get_testdir(Dir, Suite) end, case filelib:is_dir(TestDir) of true -> {ok,TestDir}; false -> {error,invalid} end. is_suite(Mod) when is_atom(Mod) -> is_suite(atom_to_list(Mod)); is_suite(ModOrFile) when is_list(ModOrFile) -> case lists:reverse(filename:basename(ModOrFile, ".erl")) of [$E,$T,$I,$U,$S,$_|_] -> true; _ -> case lists:reverse(filename:basename(ModOrFile, ".beam")) of [$E,$T,$I,$U,$S,$_|_] -> true; _ -> false end end. get_all_testcases(Suite) -> try ct_framework:get_all_cases(Suite) of {error,_Reason} = Error -> Error; SuiteCases -> Cases = [C || {_S,C} <- SuiteCases], try Suite:sequences() of [] -> Cases; Seqs -> TCs1 = lists:flatten([TCs || {_,TCs} <- Seqs]), lists:reverse( lists:foldl(fun(TC, Acc) -> case lists:member(TC, Acc) of true -> Acc; false -> [TC | Acc] end end, [], Cases ++ TCs1)) catch _:_ -> Cases end catch _:Error -> {error,Error} end. %% Internal tracing support. If {ct_trace,TraceSpec} is present, the %% TraceSpec file will be consulted and dbg used to trace function %% calls during test run. Expected terms in TraceSpec: %% {m,Mod} or {f,Mod,Func}. start_trace(Args) -> case lists:keysearch(ct_trace,1,Args) of {value,{ct_trace,File}} -> TraceSpec = delistify(File), case file:consult(TraceSpec) of {ok,Terms} -> case catch do_trace(Terms) of ok -> true; {_,Error} -> io:format("Warning! Tracing not started. Reason: ~tp~n~n", [Error]), false end; {_,Error} -> io:format("Warning! Tracing not started. Reason: ~ts~n~n", [file:format_error(Error)]), false end; false -> false end. do_trace(Terms) -> dbg:tracer(), dbg:p(self(), [sos,call]), lists:foreach(fun({m,M}) -> case dbg:tpl(M,x) of {error,What} -> exit({error,{tracing_failed,What}}); _ -> ok end; ({me,M}) -> case dbg:tp(M,[{'_',[],[{exception_trace}, {message,{caller}}]}]) of {error,What} -> exit({error,{tracing_failed,What}}); _ -> ok end; ({f,M,F}) -> case dbg:tpl(M,F,[{'_',[],[{exception_trace}, {message,{caller}}]}]) of {error,What} -> exit({error,{tracing_failed,What}}); _ -> ok end; (Huh) -> exit({error,{unrecognized_trace_term,Huh}}) end, Terms), ok. stop_trace(true) -> dbg:stop(); stop_trace(false) -> ok. list_to_number(S) -> try list_to_integer(S) catch error:badarg -> list_to_float(S) end. ensure_atom(Atom) when is_atom(Atom) -> Atom; ensure_atom(String) when is_list(String), is_integer(hd(String)) -> list_to_atom(String); ensure_atom(List) when is_list(List) -> [ensure_atom(Item) || Item <- List]; ensure_atom(Other) -> Other.