diff options
Diffstat (limited to 'lib/common_test/src')
-rw-r--r-- | lib/common_test/src/ct.erl | 6 | ||||
-rw-r--r-- | lib/common_test/src/ct_config.erl | 40 | ||||
-rw-r--r-- | lib/common_test/src/ct_config_plain.erl | 24 | ||||
-rw-r--r-- | lib/common_test/src/ct_config_xml.erl | 48 | ||||
-rw-r--r-- | lib/common_test/src/ct_framework.erl | 131 | ||||
-rw-r--r-- | lib/common_test/src/ct_hooks.erl | 143 | ||||
-rw-r--r-- | lib/common_test/src/ct_logs.erl | 57 | ||||
-rw-r--r-- | lib/common_test/src/ct_make.erl | 2 | ||||
-rw-r--r-- | lib/common_test/src/ct_run.erl | 505 | ||||
-rw-r--r-- | lib/common_test/src/ct_telnet.erl | 9 | ||||
-rw-r--r-- | lib/common_test/src/ct_testspec.erl | 30 | ||||
-rw-r--r-- | lib/common_test/src/ct_util.erl | 137 | ||||
-rw-r--r-- | lib/common_test/src/ct_util.hrl | 4 | ||||
-rw-r--r-- | lib/common_test/src/vts.erl | 133 |
14 files changed, 910 insertions, 359 deletions
diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl index 66da3ef742..f3c2029734 100644 --- a/lib/common_test/src/ct.erl +++ b/lib/common_test/src/ct.erl @@ -148,10 +148,10 @@ run(TestDirs) -> %%% {auto_compile,Bool} | {multiply_timetraps,M} | {scale_timetraps,Bool} | %%% {repeat,N} | {duration,DurTime} | {until,StopTime} | %%% {force_stop,Bool} | {decrypt,DecryptKeyOrFile} | -%%% {refresh_logs,LogDir} | {basic_html,Bool} | +%%% {refresh_logs,LogDir} | {logopts,LogOpts} | {basic_html,Bool} | %%% {ct_hooks, CTHs} %%% TestDirs = [string()] | string() -%%% Suites = [string()] | string() +%%% Suites = [string()] | [atom()] | string() | atom() %%% Cases = [atom()] | atom() %%% Groups = [atom()] | atom() %%% TestSpecs = [string()] | string() @@ -177,6 +177,8 @@ run(TestDirs) -> %%% DecryptKeyOrFile = {key,DecryptKey} | {file,DecryptFile} %%% DecryptKey = string() %%% DecryptFile = string() +%%% LogOpts = [LogOpt] +%%% LogOpt = no_nl | no_src %%% CTHs = [CTHModule | {CTHModule, CTHInitArgs}] %%% CTHModule = atom() %%% CTHInitArgs = term() diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl index 6b75937668..fc51aea7f3 100644 --- a/lib/common_test/src/ct_config.erl +++ b/lib/common_test/src/ct_config.erl @@ -204,9 +204,9 @@ get_config_file_list(Opts) -> DefaultConfigs = process_default_configs(Opts), CfgFiles = if - DefaultConfigs == []-> + DefaultConfigs == [] -> []; - true-> + true -> [{?ct_config_txt, DefaultConfigs}] end ++ process_user_configs(Opts, []), @@ -240,12 +240,12 @@ read_config_files(Opts) -> end, ConfigFiles = case lists:keyfind(config, 1, Opts) of - {config,ConfigLists}-> + {config,ConfigLists} -> lists:foldr(fun({Callback,Files}, Acc) -> AddCallback(Callback,Files) ++ Acc end,[],ConfigLists); - false-> + false -> [] end, read_config_files_int(ConfigFiles, fun store_config/3). @@ -255,7 +255,9 @@ read_config_files_int([{Callback, File}|Files], FunToSave) -> {ok, Config} -> FunToSave(Config, Callback, File), read_config_files_int(Files, FunToSave); - {error, ErrorName, ErrorDetail}-> + {error, {ErrorName, ErrorDetail}} -> + {user_error, {ErrorName, File, ErrorDetail}}; + {error, ErrorName, ErrorDetail} -> {user_error, {ErrorName, File, ErrorDetail}} end; read_config_files_int([], _FunToSave) -> @@ -283,7 +285,7 @@ rewrite_config(Config, Callback, File) -> config=File,_='_'}), Updater = fun({Key, Value}) -> case keyfindall(Key, #ct_conf.key, OldRows) of - []-> + [] -> ets:insert(?attr_table, #ct_conf{key=Key, value=Value, @@ -453,9 +455,9 @@ update_conf(Name, NewConfig) -> reload_conf(KeyOrName) -> case lookup_handler_for_config(KeyOrName) of - []-> + [] -> undefined; - HandlerList-> + HandlerList -> HandlerList2 = lists:usort(HandlerList), read_config_files_int(HandlerList2, fun rewrite_config/3), get_config(KeyOrName) @@ -711,13 +713,13 @@ random_bytes_1(N, Acc) -> random_bytes_1(N-1, [random:uniform(255)|Acc]). check_callback_load(Callback) -> case code:is_loaded(Callback) of - {file, _Filename}-> + {file, _Filename} -> check_exports(Callback); - false-> + false -> case code:load_file(Callback) of - {module, Callback}-> + {module, Callback} -> check_exports(Callback); - {error, Error}-> + {error, Error} -> {error, Error} end end. @@ -745,14 +747,14 @@ check_config_files(Configs) -> end, Files) end; - {error, Why}-> + {error, Why} -> {error, {callback, {Callback,Why}}} end; ({Callback, []}) -> case check_callback_load(Callback) of - {ok, Callback}-> + {ok, Callback} -> Callback:check_parameter([]); - {error, Why}-> + {error, Why} -> {error, {callback, {Callback,Why}}} end end, @@ -773,15 +775,15 @@ prepare_user_configs([], Acc, _) -> prepare_config_list(Args) -> ConfigFiles = case lists:keysearch(ct_config, 1, Args) of - {value,{ct_config,Files}}-> + {value,{ct_config,Files}} -> [{?ct_config_txt,[filename:absname(F) || F <- Files]}]; - false-> + false -> [] end, UserConfigs = case lists:keysearch(userconfig, 1, Args) of - {value,{userconfig,UserConfigFiles}}-> + {value,{userconfig,UserConfigFiles}} -> prepare_user_configs(UserConfigFiles, [], new); - false-> + false -> [] end, ConfigFiles ++ UserConfigs. diff --git a/lib/common_test/src/ct_config_plain.erl b/lib/common_test/src/ct_config_plain.erl index 3fbc8af9fb..6698332379 100644 --- a/lib/common_test/src/ct_config_plain.erl +++ b/lib/common_test/src/ct_config_plain.erl @@ -29,7 +29,7 @@ read_config(ConfigFile) -> {ok,Config} -> {ok, Config}; {error,enoent} -> - {error, config_file_error, enoent}; + {error,{config_file_error,file:format_error(enoent)}}; {error,Reason} -> Key = case application:get_env(common_test, decrypt) of @@ -45,23 +45,27 @@ read_config(ConfigFile) -> end, case Key of {error,no_crypt_file} -> - {error, config_file_error, Reason}; + {error,{config_file_error, + lists:flatten( + io_lib:format("~s",[file:format_error(Reason)]))}}; {error,CryptError} -> - {error, decrypt_file_error, CryptError}; + {error,{decrypt_file_error,CryptError}}; _ when is_list(Key) -> - case ct_config:decrypt_config_file(ConfigFile, undefined, {key,Key}) of + case ct_config:decrypt_config_file(ConfigFile, + undefined, + {key,Key}) of {ok,CfgBin} -> case read_config_terms(CfgBin) of {error,ReadFail} -> - {error, config_file_error, ReadFail}; + {error,{config_file_error,ReadFail}}; Config -> - {ok, Config} + {ok,Config} end; {error,DecryptFail} -> - {error, decrypt_config_error, DecryptFail} + {error,{decrypt_config_error,DecryptFail}} end; _ -> - {error, bad_decrypt_key, Key} + {error,{bad_decrypt_key,Key}} end end. @@ -69,9 +73,9 @@ read_config(ConfigFile) -> check_parameter(File)-> case filelib:is_file(File) of true-> - {ok, {file, File}}; + {ok,{file,File}}; false-> - {error, {nofile, File}} + {error,{nofile,File}} end. read_config_terms(Bin) when is_binary(Bin) -> diff --git a/lib/common_test/src/ct_config_xml.erl b/lib/common_test/src/ct_config_xml.erl index 8a6e75e635..794174e663 100644 --- a/lib/common_test/src/ct_config_xml.erl +++ b/lib/common_test/src/ct_config_xml.erl @@ -27,30 +27,30 @@ % read config file read_config(ConfigFile) -> case catch do_read_xml_config(ConfigFile) of - {ok, Config}-> - {ok, Config}; - {error, Error, ErroneousString}-> - {error, Error, ErroneousString} + {ok,Config} -> + {ok,Config}; + Error = {error,_} -> + Error end. % check file exists -check_parameter(File)-> +check_parameter(File) -> case filelib:is_file(File) of - true-> - {ok, {file, File}}; - false-> - {error, {nofile, File}} + true -> + {ok,{file,File}}; + false -> + {error,{nofile,File}} end. % actual reading of the config -do_read_xml_config(ConfigFile)-> +do_read_xml_config(ConfigFile) -> case catch xmerl_sax_parser:file(ConfigFile, - [{event_fun, fun event/3}, - {event_state, []}]) of - {ok, EntityList, _}-> - {ok, lists:reverse(transform_entity_list(EntityList))}; - Oops-> - {error, parsing_failed, Oops} + [{event_fun,fun event/3}, + {event_state,[]}]) of + {ok,EntityList,_} -> + {ok,lists:reverse(transform_entity_list(EntityList))}; + Oops -> + {error,{parsing_failed,Oops}} end. % event callback for xmerl_sax_parser @@ -92,18 +92,18 @@ tag(_El, State) -> State. % transform of the ugly deeply nested entity list to the key-value "tree" -transform_entity_list(EntityList)-> +transform_entity_list(EntityList) -> lists:map(fun transform_entity/1, EntityList). % transform entity from {list(), list()} to {atom(), term()} transform_entity({Tag, [Value|Rest]}) when - is_tuple(Value)-> + is_tuple(Value) -> {list_to_atom(Tag), transform_entity_list(lists:reverse([Value|Rest]))}; -transform_entity({Tag, String})-> +transform_entity({Tag, String}) -> case list_to_term(String) of - {ok, Value}-> + {ok, Value} -> {list_to_atom(Tag), Value}; - Error-> + Error -> throw(Error) end. @@ -111,8 +111,8 @@ transform_entity({Tag, String})-> list_to_term(String) -> {ok, T, _} = erl_scan:string(String++"."), case catch erl_parse:parse_term(T) of - {ok, Term} -> - {ok, Term}; + {ok,Term} -> + {ok,Term}; Error -> - {error, Error, String} + {error,{Error,String}} end. diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl index 809616d8e3..482c5242ce 100644 --- a/lib/common_test/src/ct_framework.erl +++ b/lib/common_test/src/ct_framework.erl @@ -24,10 +24,10 @@ -module(ct_framework). --export([init_tc/3, end_tc/3, end_tc/4, get_suite/2, report/2, warn/1]). --export([error_notification/4]). +-export([init_tc/3, end_tc/3, end_tc/4, get_suite/2, get_all_cases/1]). +-export([report/2, warn/1, error_notification/4]). --export([overview_html_header/1]). +-export([get_logopts/0, format_comment/1, overview_html_header/1]). -export([error_in_suite/1, ct_init_per_group/2, ct_end_per_group/2]). @@ -116,7 +116,7 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) -> Config = lists:keydelete(watchdog,1,Config1), if Func /= init_per_suite, DoInit /= true -> ok; - true -> + true -> %% delete all default values used in previous suite ct_config:delete_default_config(suite), %% release all name -> key bindings (once per suite) @@ -133,7 +133,7 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) -> ct_config:delete_default_config(testcase), case add_defaults(Mod,Func,TestCaseInfo,DoInit) of Error = {suite0_failed,_} -> - ct_logs:init_tc(), + ct_logs:init_tc(false), FuncSpec = group_or_func(Func,Config0), ct_event:notify(#event{name=tc_start, node=node(), @@ -143,7 +143,7 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) -> {SuiteInfo,MergeResult} -> case MergeResult of {error,Reason} when DoInit == false -> - ct_logs:init_tc(), + ct_logs:init_tc(false), FuncSpec = group_or_func(Func,Config0), ct_event:notify(#event{name=tc_start, node=node(), @@ -194,19 +194,24 @@ init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) -> Conns -> ct_util:silence_connections(Conns) end, - - ct_logs:init_tc(), + if Func /= init_per_suite, DoInit /= true -> + ct_logs:init_tc(false); + true -> + ct_logs:init_tc(true) + end, FuncSpec = group_or_func(Func,Config), ct_event:notify(#event{name=tc_start, node=node(), data={Mod,FuncSpec}}), - case configure(MergedInfo1,MergedInfo1,SuiteInfo,{Func,DoInit},Config) of + case catch configure(MergedInfo1,MergedInfo1,SuiteInfo,{Func,DoInit},Config) of {suite0_failed,Reason} -> ct_util:set_testdata({curr_tc,{Mod,{suite0_failed,{require,Reason}}}}), {skip,{require_failed_in_suite0,Reason}}; {error,Reason} -> {auto_skip,{require_failed,Reason}}; + {'EXIT',Reason} -> + {auto_skip,Reason}; {ok, FinalConfig} -> case MergeResult of {error,Reason} -> @@ -240,28 +245,45 @@ add_defaults(Mod,Func,FuncInfo,DoInit) -> case (catch Mod:suite()) of {'EXIT',{undef,_}} -> SuiteInfo = merge_with_suite_defaults(Mod,[]), - case add_defaults1(Mod,Func,FuncInfo,SuiteInfo,DoInit) of + SuiteInfoNoCTH = [I || I <- SuiteInfo, element(1,I) =/= ct_hooks], + case add_defaults1(Mod,Func,FuncInfo,SuiteInfoNoCTH,DoInit) of Error = {error,_} -> {SuiteInfo,Error}; MergedInfo -> {SuiteInfo,MergedInfo} end; - {'EXIT',Reason} -> + {'EXIT',Reason} -> + ErrStr = io_lib:format("~n*** ERROR *** " + "~w:suite/0 failed: ~p~n", + [Mod,Reason]), + io:format(ErrStr, []), + io:format(user, ErrStr, []), {suite0_failed,{exited,Reason}}; SuiteInfo when is_list(SuiteInfo) -> case lists:all(fun(E) when is_tuple(E) -> true; (_) -> false end, SuiteInfo) of true -> - SuiteInfoNoCTH = - lists:keydelete(ct_hooks,1,SuiteInfo), - SuiteInfo1 = merge_with_suite_defaults(Mod,SuiteInfoNoCTH), - case add_defaults1(Mod,Func,FuncInfo,SuiteInfo1,DoInit) of + SuiteInfo1 = merge_with_suite_defaults(Mod,SuiteInfo), + SuiteInfoNoCTH = [I || I <- SuiteInfo1, + element(1,I) =/= ct_hooks], + case add_defaults1(Mod,Func,FuncInfo, + SuiteInfoNoCTH,DoInit) of Error = {error,_} -> {SuiteInfo1,Error}; MergedInfo -> {SuiteInfo1,MergedInfo} end; false -> + ErrStr = io_lib:format("~n*** ERROR *** " + "Invalid return value from " + "~w:suite/0: ~p~n", [Mod,SuiteInfo]), + io:format(ErrStr, []), + io:format(user, ErrStr, []), {suite0_failed,bad_return_value} end; - _ -> + SuiteInfo -> + ErrStr = io_lib:format("~n*** ERROR *** " + "Invalid return value from " + "~w:suite/0: ~p~n", [Mod,SuiteInfo]), + io:format(ErrStr, []), + io:format(user, ErrStr, []), {suite0_failed,bad_return_value} end. @@ -435,7 +457,7 @@ try_set_default(Name,Key,Info,Where) -> %%% @doc Test server framework callback, called by the test_server %%% when a test case is finished. end_tc(Mod, Fun, Args) -> - %% Have to keep end_tc/3 for backwards compatabilty issues + %% Have to keep end_tc/3 for backwards compatibility issues end_tc(Mod, Fun, Args, '$end_tc_dummy'). end_tc(?MODULE,error_in_suite,_, _) -> % bad start! ok; @@ -449,7 +471,6 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) -> {value,{watchdog,Dog}} -> test_server:timetrap_cancel(Dog); false -> ok end, - %% save the testcase process pid so that it can be used %% to look up the attached trace window later case ct_util:get_testdata(interpret) of @@ -459,7 +480,6 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) -> _ -> ok end, - ct_util:delete_testdata(comment), ct_util:delete_suite_data(last_saved_config), FuncSpec = @@ -765,6 +785,37 @@ get_suite(Mod, Name) -> %%%----------------------------------------------------------------- +get_all_cases(Suite) -> + case get_suite(Suite, all) of + [{?MODULE,error_in_suite,[[{error,_}=Error]]}] -> + Error; + [{?MODULE,error_in_suite,[[Error]]}] -> + {error,Error}; + Tests -> + Cases = get_all_cases1(Suite, Tests), + lists:reverse( + lists:foldl(fun(TC, TCs) -> + case lists:member(TC, TCs) of + true -> TCs; + false -> [TC | TCs] + end + end, [], Cases)) + end. + +get_all_cases1(Suite, [{conf,_Props,_Init,GrTests,_End} | Tests]) -> + get_all_cases1(Suite, GrTests) ++ get_all_cases1(Suite, Tests); + +get_all_cases1(Suite, [Test | Tests]) when is_atom(Test) -> + [{Suite,Test} | get_all_cases1(Suite, Tests)]; + +get_all_cases1(Suite, [Test | Tests]) -> + [Test | get_all_cases1(Suite, Tests)]; + +get_all_cases1(_, []) -> + []. + +%%%----------------------------------------------------------------- + find_groups(Mod, Name, TCs, GroupDefs) -> Found = find(Mod, Name, TCs, GroupDefs, [], GroupDefs, false), trim(Found). @@ -976,15 +1027,20 @@ make_conf(Mod, Name, Props, TestSpec) -> _ -> ok end, - {InitConf,EndConf} = + {InitConf,EndConf,ExtraProps} = case erlang:function_exported(Mod,init_per_group,2) of true -> - {{Mod,init_per_group},{Mod,end_per_group}}; + {{Mod,init_per_group},{Mod,end_per_group},[]}; false -> + ct_logs:log("TEST INFO", "init_per_group/2 and " + "end_per_group/2 missing for group " + "~p in ~p, using default.", + [Name,Mod]), {{?MODULE,ct_init_per_group}, - {?MODULE,ct_end_per_group}} + {?MODULE,ct_end_per_group}, + [{suite,Mod}]} end, - {conf,[{name,Name}|Props],InitConf,TestSpec,EndConf}. + {conf,[{name,Name}|Props++ExtraProps],InitConf,TestSpec,EndConf}. %%%----------------------------------------------------------------- @@ -1157,13 +1213,15 @@ error_in_suite(Config) -> %% if the group config functions are missing in the suite, %% use these instead ct_init_per_group(GroupName, Config) -> - ct_logs:log("WARNING", "init_per_group/2 for ~w missing " + ct:comment(io_lib:format("start of ~p", [GroupName])), + ct_logs:log("TEST INFO", "init_per_group/2 for ~w missing " "in suite, using default.", [GroupName]), Config. ct_end_per_group(GroupName, _) -> - ct_logs:log("WARNING", "end_per_group/2 for ~w missing " + ct:comment(io_lib:format("end of ~p", [GroupName])), + ct_logs:log("TEST INFO", "end_per_group/2 for ~w missing " "in suite, using default.", [GroupName]), ok. @@ -1240,12 +1298,20 @@ report(What,Data) -> ok; {end_per_group,_} -> ok; + {ct_init_per_group,_} -> + ok; + {ct_end_per_group,_} -> + ok; {_,ok} -> add_to_stats(ok); {_,{skipped,{failed,{_,init_per_testcase,_}}}} -> add_to_stats(auto_skipped); {_,{skipped,{require_failed,_}}} -> add_to_stats(auto_skipped); + {_,{skipped,{timetrap_error,_}}} -> + add_to_stats(auto_skipped); + {_,{skipped,{invalid_time_format,_}}} -> + add_to_stats(auto_skipped); {_,{skipped,_}} -> add_to_stats(user_skipped); {_,{SkipOrFail,_Reason}} -> @@ -1330,6 +1396,21 @@ add_data_dir(File,Config) when is_list(File) -> end. %%%----------------------------------------------------------------- +%%% @spec get_logopts() -> [LogOpt] +get_logopts() -> + case ct_util:get_testdata(logopts) of + undefined -> + []; + LogOpts -> + LogOpts + end. + +%%%----------------------------------------------------------------- +%%% @spec format_comment(Comment) -> HtmlComment +format_comment(Comment) -> + "<font color=\"green\">" ++ Comment ++ "</font>". + +%%%----------------------------------------------------------------- %%% @spec overview_html_header(TestName) -> Header overview_html_header(TestName) -> TestName1 = lists:flatten(io_lib:format("~p", [TestName])), diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl index 984e04b90f..f243b87f54 100644 --- a/lib/common_test/src/ct_hooks.erl +++ b/lib/common_test/src/ct_hooks.erl @@ -31,11 +31,11 @@ -export([on_tc_skip/2]). -export([on_tc_fail/2]). --type proplist() :: [{atom(),term()}]. - %% If you change this, remember to update ct_util:look -> stop clause as well. -define(config_name, ct_hooks). +-record(ct_hook_config, {id, module, prio, scope, opts = [], state = []}). + %% ------------------------------------------------------------------------- %% API Functions %% ------------------------------------------------------------------------- @@ -44,22 +44,22 @@ -spec init(State :: term()) -> ok | {error, Reason :: term()}. init(Opts) -> - call([{Hook, call_id, undefined} || Hook <- get_new_hooks(Opts)], - ok, init, []). + call(get_new_hooks(Opts, undefined), ok, init, []). %% @doc Called after all suites are done. -spec terminate(Hooks :: term()) -> ok. terminate(Hooks) -> - call([{HookId, fun call_terminate/3} || {HookId,_,_} <- Hooks], + call([{HookId, fun call_terminate/3} + || #ct_hook_config{id = HookId} <- Hooks], ct_hooks_terminate_dummy, terminate, Hooks), ok. %% @doc Called as each test case is started. This includes all configuration %% tests. -spec init_tc(Mod :: atom(), Func :: atom(), Args :: list()) -> - NewConfig :: proplist() | + NewConfig :: proplists:proplist() | {skip, Reason :: term()} | {auto_skip, Reason :: term()} | {fail, Reason :: term()}. @@ -68,11 +68,11 @@ init_tc(ct_framework, _Func, Args) -> init_tc(Mod, init_per_suite, Config) -> Info = try proplists:get_value(ct_hooks, Mod:suite(),[]) of List when is_list(List) -> - [{ct_hooks,List}]; + [{?config_name,List}]; CTHook when is_atom(CTHook) -> - [{ct_hooks,[CTHook]}] + [{?config_name,[CTHook]}] catch error:undef -> - [{ct_hooks,[]}] + [{?config_name,[]}] end, call(fun call_generic/3, Config ++ Info, [pre_init_per_suite, Mod]); init_tc(Mod, end_per_suite, Config) -> @@ -92,7 +92,7 @@ init_tc(_Mod, TC, Config) -> Args :: list(), Result :: term(), Resturn :: term()) -> - NewConfig :: proplist() | + NewConfig :: proplists:proplist() | {skip, Reason :: term()} | {auto_skip, Reason :: term()} | {fail, Reason :: term()} | @@ -131,36 +131,48 @@ on_tc_fail(_How, {Suite, Case, Reason}) -> %% ------------------------------------------------------------------------- %% Internal Functions %% ------------------------------------------------------------------------- -call_id(Mod, Config, Meta) when is_atom(Mod) -> - call_id({Mod, []}, Config, Meta); -call_id({Mod, Opts}, Config, Scope) -> +call_id(#ct_hook_config{ module = Mod, opts = Opts} = Hook, Config, Scope) -> Id = catch_apply(Mod,id,[Opts], make_ref()), - {Config, {Id, scope(Scope), {Mod, {Id,Opts}}}}. + {Config, Hook#ct_hook_config{ id = Id, scope = scope(Scope)}}. -call_init({Mod,{Id,Opts}},Config,_Meta) -> - NewState = Mod:init(Id, Opts), - {Config, {Mod, NewState}}. - -call_terminate({Mod, State}, _, _) -> +call_init(#ct_hook_config{ module = Mod, opts = Opts, id = Id, prio = P} = Hook, + Config,_Meta) -> + case Mod:init(Id, Opts) of + {ok, NewState} when P =:= undefined -> + {Config, Hook#ct_hook_config{ state = NewState, prio = 0 } }; + {ok, NewState} -> + {Config, Hook#ct_hook_config{ state = NewState } }; + {ok, NewState, Prio} when P =:= undefined -> + %% Only set prio if not already set when installing hook + {Config, Hook#ct_hook_config{ state = NewState, prio = Prio } }; + {ok, NewState, _} -> + {Config, Hook#ct_hook_config{ state = NewState } }; + NewState -> %% Keep for backward compatability reasons + {Config, Hook#ct_hook_config{ state = NewState } } + end. + +call_terminate(#ct_hook_config{ module = Mod, state = State} = Hook, _, _) -> catch_apply(Mod,terminate,[State], ok), - {[],{Mod,State}}. + {[],Hook}. -call_cleanup({Mod, State}, Reason, [Function, _Suite | Args]) -> +call_cleanup(#ct_hook_config{ module = Mod, state = State} = Hook, + Reason, [Function, _Suite | Args]) -> NewState = catch_apply(Mod,Function, Args ++ [Reason, State], State), - {Reason, {Mod, NewState}}. + {Reason, Hook#ct_hook_config{ state = NewState } }. -call_generic({Mod, State}, Value, [Function | Args]) -> +call_generic(#ct_hook_config{ module = Mod, state = State} = Hook, + Value, [Function | Args]) -> {NewValue, NewState} = catch_apply(Mod, Function, Args ++ [Value, State], {Value,State}), - {NewValue, {Mod, NewState}}. + {NewValue, Hook#ct_hook_config{ state = NewState } }. %% Generic call function call(Fun, Config, Meta) -> maybe_lock(), Hooks = get_hooks(), - Res = call([{HookId,Fun} || {HookId,_, _} <- Hooks] ++ - get_new_hooks(Config, Fun), + Res = call(get_new_hooks(Config, Fun) ++ + [{HookId,Fun} || #ct_hook_config{id = HookId} <- Hooks], remove(?config_name,Config), Meta, Hooks), maybe_unlock(), Res. @@ -173,19 +185,20 @@ call(Fun, Config, Meta, NoChangeRet) when is_function(Fun) -> call([{Hook, call_id, NextFun} | Rest], Config, Meta, Hooks) -> try - {Config, {NewId, _, _} = NewHook} = call_id(Hook, Config, Meta), + {Config, #ct_hook_config{ id = NewId } = NewHook} = + call_id(Hook, Config, Meta), {NewHooks, NewRest} = - case lists:keyfind(NewId, 1, Hooks) of + case lists:keyfind(NewId, #ct_hook_config.id, Hooks) of false when NextFun =:= undefined -> {Hooks ++ [NewHook], - [{NewId, fun call_init/3} | Rest]}; + [{NewId, call_init} | Rest]}; ExistingHook when is_tuple(ExistingHook) -> {Hooks, Rest}; _ -> {Hooks ++ [NewHook], - [{NewId, fun call_init/3},{NewId,NextFun} | Rest]} + [{NewId, call_init}, {NewId,NextFun} | Rest]} end, - call(NewRest, Config, Meta, NewHooks) + call(resort(NewRest,NewHooks), Config, Meta, NewHooks) catch Error:Reason -> Trace = erlang:get_stacktrace(), ct_logs:log("Suite Hook","Failed to start a CTH: ~p:~p", @@ -193,13 +206,16 @@ call([{Hook, call_id, NextFun} | Rest], Config, Meta, Hooks) -> call([], {fail,"Failed to start CTH" ", see the CT Log for details"}, Meta, Hooks) end; +call([{HookId, call_init} | Rest], Config, Meta, Hooks) -> + call([{HookId, fun call_init/3} | Rest], Config, Meta, Hooks); call([{HookId, Fun} | Rest], Config, Meta, Hooks) -> try - {_,Scope,ModState} = lists:keyfind(HookId, 1, Hooks), - {NewConf, NewHookInfo} = Fun(ModState, Config, Meta), + Hook = lists:keyfind(HookId, #ct_hook_config.id, Hooks), + {NewConf, NewHook} = Fun(Hook, Config, Meta), NewCalls = get_new_hooks(NewConf, Fun), - NewHooks = lists:keyreplace(HookId, 1, Hooks, {HookId, Scope, NewHookInfo}), - call(NewCalls ++ Rest, remove(?config_name, NewConf), Meta, + NewHooks = lists:keyreplace(HookId, #ct_hook_config.id, Hooks, NewHook), + call(resort(NewCalls ++ Rest,NewHooks), %% Resort if call_init changed prio + remove(?config_name, NewConf), Meta, terminate_if_scope_ends(HookId, Meta, NewHooks)) catch throw:{error_in_cth_call,Reason} -> call(Rest, {fail, Reason}, Meta, @@ -237,19 +253,26 @@ terminate_if_scope_ends(HookId, [on_tc_skip,Suite,end_per_suite], Hooks) -> terminate_if_scope_ends(HookId, [Function,Tag|T], Hooks) when T =/= [] -> terminate_if_scope_ends(HookId,[Function,Tag],Hooks); terminate_if_scope_ends(HookId, Function, Hooks) -> - case lists:keyfind(HookId, 1, Hooks) of - {HookId, Function, _ModState} = Hook -> + case lists:keyfind(HookId, #ct_hook_config.id, Hooks) of + #ct_hook_config{ id = HookId, scope = Function} = Hook -> terminate([Hook]), - lists:keydelete(HookId, 1, Hooks); + lists:keydelete(HookId, #ct_hook_config.id, Hooks); _ -> Hooks end. %% Fetch hook functions get_new_hooks(Config, Fun) -> - lists:foldl(fun(NewHook, Acc) -> - [{NewHook, call_id, Fun} | Acc] - end, [], get_new_hooks(Config)). + lists:map(fun(NewHook) when is_atom(NewHook) -> + {#ct_hook_config{ module = NewHook }, call_id, Fun}; + ({NewHook,Opts}) -> + {#ct_hook_config{ module = NewHook, + opts = Opts}, call_id, Fun}; + ({NewHook,Opts,Prio}) -> + {#ct_hook_config{ module = NewHook, + opts = Opts, + prio = Prio }, call_id, Fun} + end, get_new_hooks(Config)). get_new_hooks(Config) when is_list(Config) -> lists:flatmap(fun({?config_name, HookConfigs}) -> @@ -264,7 +287,43 @@ save_suite_data_async(Hooks) -> ct_util:save_suite_data_async(?config_name, Hooks). get_hooks() -> - ct_util:read_suite_data(?config_name). + lists:keysort(#ct_hook_config.prio,ct_util:read_suite_data(?config_name)). + +%% Sort all calls in this order: +%% call_id < call_init < Hook Priority 1 < .. < Hook Priority N +%% If Hook Priority is equal, check when it has been installed and +%% sort on that instead. +resort(Calls, Hooks) -> + lists:sort( + fun({_,_,_},_) -> + true; + (_,{_,_,_}) -> + false; + ({_,call_init},_) -> + true; + (_,{_,call_init}) -> + false; + ({Id1,_},{Id2,_}) -> + P1 = (lists:keyfind(Id1, #ct_hook_config.id, Hooks))#ct_hook_config.prio, + P2 = (lists:keyfind(Id2, #ct_hook_config.id, Hooks))#ct_hook_config.prio, + if + P1 == P2 -> + %% If priorities are equal, we check the position in the + %% hooks list + pos(Id1,Hooks) < pos(Id2,Hooks); + true -> + P1 < P2 + end + end,Calls). + +pos(Id,Hooks) -> + pos(Id,Hooks,0). +pos(Id,[#ct_hook_config{ id = Id}|_],Num) -> + Num; +pos(Id,[_|Rest],Num) -> + pos(Id,Rest,Num+1). + + catch_apply(M,F,A, Default) -> try diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl index b839521e24..faec461775 100644 --- a/lib/common_test/src/ct_logs.erl +++ b/lib/common_test/src/ct_logs.erl @@ -28,7 +28,7 @@ -module(ct_logs). --export([init/1,close/1,init_tc/0,end_tc/1]). +-export([init/1,close/2,init_tc/1,end_tc/1]). -export([get_log_dir/0,log/3,start_log/1,cont_log/2,end_log/0]). -export([set_stylesheet/2,clear_stylesheet/1]). -export([add_external_logs/1,add_link/3]). @@ -97,11 +97,11 @@ logdir_node_prefix() -> logdir_prefix()++"."++atom_to_list(node()). %%%----------------------------------------------------------------- -%%% @spec close(Info) -> ok +%%% @spec close(Info, StartDir) -> ok %%% %%% @doc Create index pages with test results and close the CT Log %%% (tool-internal use only). -close(Info) -> +close(Info, StartDir) -> make_last_run_index(), ct_event:notify(#event{name=stop_logging,node=node(),data=[]}), @@ -124,14 +124,29 @@ close(Info) -> ok; Error -> io:format("Warning! Cleanup failed: ~p~n", [Error]) - end; + end, + make_all_suites_index(stop), + make_all_runs_index(stop); true -> - file:set_cwd("..") - end, - - make_all_suites_index(stop), - make_all_runs_index(stop), - + file:set_cwd(".."), + make_all_suites_index(stop), + make_all_runs_index(stop), + case ct_util:get_profile_data(browser, StartDir) of + undefined -> + ok; + BrowserData -> + case {proplists:get_value(prog, BrowserData), + proplists:get_value(args, BrowserData), + proplists:get_value(page, BrowserData)} of + {Prog,Args,Page} when is_list(Args), + is_list(Page) -> + URL = "\"file://" ++ ?abs(Page) ++ "\"", + ct_util:open_url(Prog, Args, URL); + _ -> + ok + end + end + end, ok. %%%----------------------------------------------------------------- @@ -182,15 +197,14 @@ cast(Msg) -> ?MODULE ! Msg end. - %%%----------------------------------------------------------------- -%%% @spec init_tc() -> ok +%%% @spec init_tc(RefreshLog) -> ok %%% %%% @doc Test case initiation (tool-internal use only). %%% %%% <p>This function is called by ct_framework:init_tc/3</p> -init_tc() -> - call({init_tc,self(),group_leader()}), +init_tc(RefreshLog) -> + call({init_tc,self(),group_leader(),RefreshLog}), ok. %%%----------------------------------------------------------------- @@ -469,8 +483,8 @@ logger_loop(State) -> [Str,Args]), %% stop the testcase, we need %% to see the fault - exit(Pid,logging_failed), - ok; + exit(Pid,{log_printout_error,Str,Args}), + []; IoStr when IoList == [] -> [IoStr]; IoStr -> @@ -490,10 +504,15 @@ logger_loop(State) -> [begin io:format(Fd,Str,Args),io:nl(Fd) end || {Str,Args} <- List], logger_loop(State#logger_state{tc_groupleaders=TCGLs}) end; - {{init_tc,TCPid,GL},From} -> + {{init_tc,TCPid,GL,RefreshLog},From} -> print_style(GL, State#logger_state.stylesheet), set_evmgr_gl(GL), TCGLs = add_tc_gl(TCPid,GL,State), + if not RefreshLog -> + ok; + true -> + make_last_run_index(State#logger_state.start_time) + end, return(From,ok), logger_loop(State#logger_state{tc_groupleaders=TCGLs}); {{end_tc,TCPid},From} -> @@ -819,6 +838,7 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, "" end end, + CtRunDir = filename:dirname(filename:dirname(Link)), {Lbl,Timestamp,Node,AllInfo} = case All of {true,OldRuns} -> @@ -828,7 +848,6 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, _ -> NodeOrDate end, N = ["<TD ALIGN=right><FONT SIZE=-1>",Node1,"</FONT></TD>\n"], - CtRunDir = filename:dirname(filename:dirname(Link)), L = ["<TD ALIGN=center><FONT SIZE=-1><B>",Label,"</FONT></B></TD>\n"], T = ["<TD><FONT SIZE=-1>",timestamp(CtRunDir),"</FONT></TD>\n"], CtLogFile = filename:join(CtRunDir,?ct_log_name), @@ -847,7 +866,7 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, if NotBuilt == 0 -> ["<TD ALIGN=right>",integer_to_list(NotBuilt),"</TD>\n"]; true -> - ["<TD ALIGN=right><A HREF=\"",?ct_log_name,"\">", + ["<TD ALIGN=right><A HREF=\"",filename:join(CtRunDir,?ct_log_name),"\">", integer_to_list(NotBuilt),"</A></TD>\n"] end, FailStr = diff --git a/lib/common_test/src/ct_make.erl b/lib/common_test/src/ct_make.erl index 233e45248e..40e9e99f37 100644 --- a/lib/common_test/src/ct_make.erl +++ b/lib/common_test/src/ct_make.erl @@ -177,7 +177,7 @@ members([],_MakefileMods,I,Rest) -> {I,Rest}. -%% Any flags that are not recognixed as make flags are passed directly +%% Any flags that are not recognised as make flags are passed directly %% to the compiler. %% So for example make:all([load,debug_info]) will make everything %% with the debug_info flag and load it. diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl index c01e97b358..26ca4f3cb4 100644 --- a/lib/common_test/src/ct_run.erl +++ b/lib/common_test/src/ct_run.erl @@ -33,7 +33,7 @@ %% Exported for VTS --export([run_make/3,do_run/3,tests/1,tests/2,tests/3]). +-export([run_make/3,do_run/4,tests/1,tests/2,tests/3]). %% Misc internal functions @@ -46,12 +46,14 @@ -define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)). -record(opts, {label, + profile, vts, shell, cover, coverspec, step, logdir, + logopts = [], config = [], event_handlers = [], ct_hooks = [], @@ -156,15 +158,19 @@ script_start(Args) -> end, stop_trace(Tracing), timer:sleep(1000), + io:nl(), Res. script_start1(Parent, Args) -> %% read general start flags Label = get_start_opt(label, fun([Lbl]) -> Lbl end, Args), + Profile = get_start_opt(profile, fun([Prof]) -> Prof end, Args), Vts = get_start_opt(vts, true, Args), Shell = get_start_opt(shell, true, Args), Cover = get_start_opt(cover, fun([CoverFile]) -> ?abs(CoverFile) 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), MultTT = get_start_opt(multiply_timetraps, fun([MT]) -> list_to_integer(MT) end, 1, Args), ScaleTT = get_start_opt(scale_timetraps, @@ -234,8 +240,10 @@ script_start1(Parent, Args) -> application:set_env(common_test, basic_html, true) end, - StartOpts = #opts{label = Label, vts = Vts, shell = Shell, cover = Cover, - logdir = LogDir, event_handlers = EvHandlers, + StartOpts = #opts{label = Label, profile = Profile, + vts = Vts, shell = Shell, cover = Cover, + logdir = LogDir, logopts = LogOpts, + event_handlers = EvHandlers, ct_hooks = CTHooks, include = IncludeDirs, silent_connections = SilentConns, @@ -297,9 +305,15 @@ script_start2(StartOpts = #opts{vts = undefined, Label = choose_val(StartOpts#opts.label, SpecStartOpts#opts.label), + Profile = choose_val(StartOpts#opts.profile, + SpecStartOpts#opts.profile), + LogDir = choose_val(StartOpts#opts.logdir, SpecStartOpts#opts.logdir), + AllLogOpts = merge_vals([StartOpts#opts.logopts, + SpecStartOpts#opts.logopts]), + Cover = choose_val(StartOpts#opts.cover, SpecStartOpts#opts.cover), MultTT = choose_val(StartOpts#opts.multiply_timetraps, @@ -317,9 +331,11 @@ script_start2(StartOpts = #opts{vts = undefined, application:set_env(common_test, include, AllInclude), {TS,StartOpts#opts{label = Label, + profile = Profile, testspecs = Specs, cover = Cover, logdir = LogDir, + logopts = AllLogOpts, config = SpecStartOpts#opts.config, event_handlers = AllEvHs, ct_hooks = AllCTHooks, @@ -392,50 +408,72 @@ check_and_install_configfiles(Configs, LogDir, EvHandlers, CTHooks) -> end. script_start3(StartOpts, Args) -> - case proplists:get_value(dir, Args) of - [] -> + StartOpts1 = get_start_opt(step, + fun(Step) -> + StartOpts#opts{step = Step, + cover = undefined} + end, StartOpts, 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 when is_list(Dirs) -> + + {Dirs,undefined,[]} when is_list(Dirs) -> script_start4(StartOpts#opts{tests = tests(Dirs)}, Args); - undefined -> - case proplists:get_value(suite, Args) of - [] -> - {error,no_suite_specified}; - Suites when is_list(Suites) -> - StartOpts1 = - get_start_opt(step, - fun(Step) -> - StartOpts#opts{step = Step, - cover = undefined} - end, StartOpts, Args), - DirMods = [suite_to_test(S) || S <- Suites], - case groups_and_cases(proplists:get_value(group, Args), - proplists:get_value(testcase, Args)) of - Error = {error,_} -> - Error; - [] when DirMods =/= [] -> - Ts = tests(DirMods), - script_start4(StartOpts1#opts{tests = Ts}, Args); - GroupsAndCases when length(DirMods) == 1 -> - Ts = tests(DirMods, GroupsAndCases), - script_start4(StartOpts1#opts{tests = Ts}, Args); - [_,_|_] when length(DirMods) > 1 -> - {error,multiple_suites_and_cases}; - _ -> - {error,incorrect_suite_option} - end; - undefined -> - if StartOpts#opts.vts ; StartOpts#opts.shell -> - script_start4(StartOpts#opts{tests = []}, Args); - true -> - script_usage(), - {error,incorrect_usage} - end + + {undefined,Suites,[]} when is_list(Suites) -> + Ts = tests([suite_to_test(S) || S <- Suites]), + script_start4(StartOpts1#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(StartOpts1#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(StartOpts1#opts{tests = Ts}, Args); + DirMods = [_] when GsAndCs /= [] -> + Ts = tests(DirMods, GsAndCs), + script_start4(StartOpts1#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 StartOpts#opts.vts ; StartOpts#opts.shell -> + script_start4(StartOpts#opts{tests = []}, Args); + true -> + script_usage(), + {error,missing_start_options} end end. script_start4(#opts{vts = true, config = Config, event_handlers = EvHandlers, - tests = Tests, logdir = LogDir}, _Args) -> + tests = Tests, logdir = LogDir, logopts = LogOpts}, _Args) -> ConfigFiles = lists:foldl(fun({ct_config_plain,CfgFiles}, AllFiles) when is_list(hd(CfgFiles)) -> @@ -446,15 +484,21 @@ script_start4(#opts{vts = true, config = Config, event_handlers = EvHandlers, (_, AllFiles) -> AllFiles end, [], Config), - vts:init_data(ConfigFiles, EvHandlers, ?abs(LogDir), Tests); + vts:init_data(ConfigFiles, EvHandlers, ?abs(LogDir), LogOpts, Tests); -script_start4(#opts{label = Label, shell = true, config = Config, +script_start4(#opts{label = Label, profile = Profile, + shell = true, config = Config, event_handlers = EvHandlers, ct_hooks = CTHooks, - logdir = LogDir, testspecs = Specs}, _Args) -> + logdir = LogDir, + logopts = LogOpts, + testspecs = 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), + InstallOpts = [{config,Config},{event_handler,EvHandlers}, {ct_hooks, CTHooks}], if Config == [] -> @@ -465,6 +509,7 @@ script_start4(#opts{label = Label, shell = true, config = Config, case install(InstallOpts) of ok -> ct_util:start(interactive, LogDir), + ct_util:set_testdata({logopts, LogOpts}), log_ts_names(Specs), io:nl(), ok; @@ -505,6 +550,7 @@ script_usage() -> "\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[-include InclDir1 InclDir2 .. InclDirN]" "\n\t[-no_auto_compile]" "\n\t[-multiply_timetraps N]" @@ -522,6 +568,7 @@ script_usage() -> "\n\t[-stylesheet CSSFile]" "\n\t[-cover CoverCfgFile]" "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]" + "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]" "\n\t[-ct_hooks CTHook1 CTHook2 .. CTHookN]" "\n\t[-include InclDir1 InclDir2 .. InclDirN]" "\n\t[-no_auto_compile]" @@ -541,6 +588,7 @@ script_usage() -> "\n\t[-stylesheet CSSFile]" "\n\t[-cover CoverCfgFile]" "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]" + "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]" "\n\t[-ct_hooks CTHook1 CTHook2 .. CTHookN]" "\n\t[-include InclDir1 InclDir2 .. InclDirN]" "\n\t[-no_auto_compile]" @@ -619,6 +667,16 @@ run_test(StartOpt) when is_tuple(StartOpt) -> run_test([StartOpt]); run_test(StartOpts) when is_list(StartOpts) -> + CTPid = spawn(fun() -> run_test1(StartOpts) end), + Ref = monitor(process, CTPid), + receive + {'DOWN',Ref,process,CTPid,{user_error,Error}} -> + Error; + {'DOWN',Ref,process,CTPid,Other} -> + Other + end. + +run_test1(StartOpts) when is_list(StartOpts) -> case proplists:get_value(refresh_logs, StartOpts) of undefined -> Tracing = start_trace(StartOpts), @@ -627,7 +685,7 @@ run_test(StartOpts) when is_list(StartOpts) -> Res = case ct_repeat:loop_test(func, StartOpts) of false -> - case catch run_test1(StartOpts) of + case catch run_test2(StartOpts) of {'EXIT',Reason} -> file:set_cwd(Cwd), {error,Reason}; @@ -638,20 +696,27 @@ run_test(StartOpts) when is_list(StartOpts) -> Result end, stop_trace(Tracing), - Res; + exit(Res); RefreshDir -> refresh_logs(?abs(RefreshDir)), - ok + exit(done) end. -run_test1(StartOpts) -> +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), + %% config & userconfig CfgFiles = ct_config:get_config_file_list(StartOpts), @@ -750,8 +815,9 @@ run_test1(StartOpts) -> %% stepped execution Step = get_start_opt(step, value, StartOpts), - Opts = #opts{label = Label, - cover = Cover, step = Step, logdir = LogDir, config = CfgFiles, + Opts = #opts{label = Label, profile = Profile, + cover = Cover, step = Step, logdir = LogDir, + logopts = LogOpts, config = CfgFiles, event_handlers = EvHandlers, ct_hooks = CTHooks, include = Include, @@ -792,8 +858,12 @@ run_spec_file(Relaxed, SpecOpts = get_data_for_node(TS, node()), Label = choose_val(Opts#opts.label, SpecOpts#opts.label), + Profile = choose_val(Opts#opts.profile, + SpecOpts#opts.profile), LogDir = choose_val(Opts#opts.logdir, SpecOpts#opts.logdir), + AllLogOpts = merge_vals([Opts#opts.logopts, + SpecOpts#opts.logopts]), AllConfig = merge_vals([CfgFiles, SpecOpts#opts.config]), Cover = choose_val(Opts#opts.cover, SpecOpts#opts.cover), @@ -817,8 +887,10 @@ run_spec_file(Relaxed, AllCTHooks) of ok -> Opts1 = Opts#opts{label = Label, + profile = Profile, cover = Cover, logdir = which(logdir, LogDir), + logopts = AllLogOpts, config = AllConfig, event_handlers = AllEvHs, include = AllInclude, @@ -899,67 +971,102 @@ run_dir(Opts = #opts{logdir = LogDir, ok -> ok; {error,IReason} -> exit(IReason) end, - case lists:keysearch(dir, 1, StartOpts) of - {value,{_,Dirs=[Dir|_]}} when not is_integer(Dir), - length(Dirs)>1 -> - %% multiple dirs (no suite) - reformat_result(catch do_run(tests(Dirs), [], Opts1, StartOpts)); - false -> % no dir - %% fun for converting suite name to {Dir,Mod} tuple - S2M = fun(S) when is_list(S) -> - {filename:dirname(S), - list_to_atom(filename:rootname(filename:basename(S)))}; - (A) -> - {".",A} - end, - case lists:keysearch(suite, 1, StartOpts) of - {value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) -> - {Dir,Mod} = S2M(Suite), - case groups_and_cases(proplists:get_value(group, StartOpts), - proplists:get_value(testcase, StartOpts)) of - Error = {error,_} -> - exit(Error); + 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(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(multiple_dirs_and_suites); + + {undefined,undefined,GsAndCs} when GsAndCs /= [] -> + exit(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(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(Dir, listify(Mod)), + reformat_result(catch do_run(tests(Dir2, Mod), [], Opts1, StartOpts)); - GsAndCs -> - reformat_result(catch do_run(tests(Dir, Mod, GsAndCs), + _ -> + reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs), [], Opts1, StartOpts)) end; - {value,{_,Suites}} -> - reformat_result(catch do_run(tests(lists:map(S2M, Suites)), - [], Opts1, StartOpts)); - _ -> - exit(no_tests_specified) - end; - {value,{_,Dir}} -> - case lists:keysearch(suite, 1, StartOpts) of - {value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) -> - Mod = if is_atom(Suite) -> Suite; - true -> list_to_atom(Suite) - end, - case groups_and_cases(proplists:get_value(group, StartOpts), - proplists:get_value(testcase, StartOpts)) of - Error = {error,_} -> - exit(Error); - [] -> - reformat_result(catch do_run(tests(Dir, listify(Mod)), + + is_list(Suite) -> % multiple suites + case [suite_to_test(Dir1, S) || S <- Suite] of + [_,_|_] when GsAndCs /= [] -> + exit(multiple_suites_and_cases); + [{Dir2,Mod}] when GsAndCs /= [] -> + reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs), [], Opts1, StartOpts)); - GsAndCs -> - reformat_result(catch do_run(tests(Dir, Mod, GsAndCs), + DirMods -> + reformat_result(catch do_run(tests(DirMods), [], Opts1, StartOpts)) - end; - {value,{_,Suites=[Suite|_]}} when is_list(Suite) -> - Mods = lists:map(fun(Str) -> list_to_atom(Str) end, Suites), - reformat_result(catch do_run(tests(delistify(Dir), Mods), - [], Opts1, StartOpts)); - {value,{_,Suites}} -> - reformat_result(catch do_run(tests(delistify(Dir), Suites), - [], Opts1, StartOpts)); - false -> % no suite, only dir - reformat_result(catch do_run(tests(listify(Dir)), - [], Opts1, StartOpts)) - end + end + end; + + {undefined,undefined,[]} -> + exit(no_test_specified); + + {Dir,Suite,GsAndCs} -> + exit({incorrect_start_options,{Dir,Suite,GsAndCs}}) end. %%%----------------------------------------------------------------- @@ -970,19 +1077,38 @@ run_dir(Opts = #opts{logdir = LogDir, %%% the same as those used in test specification files. %%% @equiv ct:run_testspec/1 %%%----------------------------------------------------------------- - run_testspec(TestSpec) -> + CTPid = spawn(fun() -> run_testspec1(TestSpec) end), + Ref = monitor(process, CTPid), + receive + {'DOWN',Ref,process,CTPid,{user_error,Error}} -> + Error; + {'DOWN',Ref,process,CTPid,Other} -> + Other + end. + +run_testspec1(TestSpec) -> {ok,Cwd} = file:get_cwd(), io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]), - case catch run_testspec1(TestSpec) of + case catch run_testspec2(TestSpec) of {'EXIT',Reason} -> file:set_cwd(Cwd), - {error,Reason}; + exit({error,Reason}); Result -> - Result + exit(Result) end. -run_testspec1(TestSpec) -> +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(CTReason); @@ -1014,7 +1140,9 @@ run_testspec1(TestSpec) -> end. get_data_for_node(#testspec{label = Labels, + profile = Profiles, logdir = LogDirs, + logopts = LogOptsList, cover = CoverFs, config = Cfgs, userconfig = UsrCfgs, @@ -1024,10 +1152,15 @@ get_data_for_node(#testspec{label = Labels, multiply_timetraps = MTs, scale_timetraps = STs}, 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, Cover = proplists:get_value(Node, CoverFs), MT = proplists:get_value(Node, MTs), ST = proplists:get_value(Node, STs), @@ -1037,7 +1170,9 @@ get_data_for_node(#testspec{label = Labels, FiltCTHooks = [Hook || {N,Hook} <- CTHooks, N==Node], Include = [I || {N,I} <- Incl, N==Node], #opts{label = Label, + profile = Profile, logdir = LogDir, + logopts = LogOpts, cover = Cover, config = ConfigFiles, event_handlers = EvHandlers, @@ -1118,8 +1253,24 @@ reformat_result({user_error,Reason}) -> reformat_result(Result) -> Result. -suite_to_test(Suite) -> - {filename:dirname(Suite),list_to_atom(filename:rootname(filename:basename(Suite)))}. +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 == [])) -> @@ -1153,9 +1304,11 @@ 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, ".", []). -do_run(Tests, Misc, LogDir) when is_list(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 -> @@ -1170,11 +1323,10 @@ do_run(Tests, Misc, LogDir) when is_list(Misc) -> CoverFile -> Opts#opts{cover = CoverFile} end, - do_run(Tests, [], Opts1#opts{logdir = LogDir}, []). - -do_run(Tests, Skip, Opts, Args) -> - #opts{label = Label, cover = Cover} = Opts, + do_run(Tests, [], Opts1#opts{logdir = LogDir}, []); +do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) -> + #opts{label = Label, profile = Profile, cover = Cover} = Opts, %% label - used by ct_logs TestLabel = if Label == undefined -> undefined; @@ -1184,6 +1336,15 @@ do_run(Tests, Skip, Opts, Args) -> 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 -> exit({error,no_path_to_test_server}); @@ -1218,6 +1379,8 @@ do_run(Tests, Skip, Opts, Args) -> _Pid -> %% save stylesheet info ct_util:set_testdata({stylesheet,Opts#opts.stylesheet}), + %% save logopts + ct_util:set_testdata({logopts,Opts#opts.logopts}), %% enable silent connections case Opts#opts.silent_connections of [] -> @@ -1985,7 +2148,14 @@ maybe_interpret1(Suite, Cases, StepOpts) when is_list(Cases) -> maybe_interpret2(Suite, Cases, StepOpts) -> set_break_on_config(Suite, StepOpts), - [i:ib(Suite, Case, 1) || Case <- Cases], + [begin try i:ib(Suite, Case, 1) of + _ -> ok + catch + _:_Error -> + io:format(user, "Invalid breakpoint: ~w:~w/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; @@ -1998,10 +2168,18 @@ maybe_interpret2(Suite, Cases, StepOpts) -> set_break_on_config(Suite, StepOpts) -> case lists:member(config, ensure_atom(StepOpts)) of true -> - i:ib(Suite, init_per_suite, 1), - i:ib(Suite, init_per_testcase, 2), - i:ib(Suite, end_per_testcase, 2), - i:ib(Suite, end_per_suite, 1); + SetBPIfExists = fun(F,A) -> + case erlang:function_exported(Suite, F, A) of + true -> i:ib(Suite, F, A); + false -> ok + end + end, + SetBPIfExists(init_per_suite, 1), + SetBPIfExists(init_per_group, 2), + SetBPIfExists(init_per_testcase, 2), + SetBPIfExists(end_per_testcase, 2), + SetBPIfExists(end_per_group, 2), + SetBPIfExists(end_per_suite, 1); false -> ok end. @@ -2055,6 +2233,15 @@ 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); @@ -2070,15 +2257,21 @@ ct_hooks_args2opts(Args) -> ct_hooks_args2opts( proplists:get_value(ct_hooks, Args, []),[]). +ct_hooks_args2opts([CTH,Arg,Prio,"and"| Rest],Acc) -> + 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]); + 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). @@ -2220,12 +2413,21 @@ opts2args(EnvStartOpts) -> end, EHs), [_LastAnd|StrsR] = lists:reverse(lists:flatten(Strs)), [{event_handler_init,lists:reverse(StrsR)}]; + ({logopts,LOs}) when is_list(LOs) -> + [{logopts,[atom_to_list(LO) || LO <- LOs]}]; ({ct_hooks,[]}) -> []; ({ct_hooks,CTHs}) when is_list(CTHs) -> io:format(user,"ct_hooks: ~p",[CTHs]), Strs = lists:flatmap( - fun({CTH,Arg}) -> + fun({CTH,Arg,Prio}) -> + [atom_to_list(CTH), + lists:flatten( + io_lib:format("~p",[Arg])), + lists:flatten( + io_lib:format("~p",[Prio])), + "and"]; + ({CTH,Arg}) -> [atom_to_list(CTH), lists:flatten( io_lib:format("~p",[Arg])), @@ -2276,32 +2478,31 @@ is_suite(ModOrFile) when is_list(ModOrFile) -> end. get_all_testcases(Suite) -> - %%! this needs to be updated to handle testcase groups later!! - case catch Suite:all() of - {'EXIT',Why} -> - {error,Why}; - {skip,_} -> - []; - Cases -> - AllCases = - lists:foldl(fun({sequence,SeqName}, All) -> - case catch Suite:sequences() of - {'EXIT',_} -> - All; - Seqs -> - case proplists:get_value(SeqName, Seqs) of - undefined -> - All; - SeqCases -> - lists:reverse(SeqCases) ++ All - end - end; - (Case,All) -> - [Case|All] - end, [], Cases), - lists:reverse(AllCases) + 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 @@ -2322,8 +2523,8 @@ start_trace(Args) -> false end; {_,Error} -> - io:format("Warning! Tracing not started. Reason: ~p~n~n", - [Error]), + io:format("Warning! Tracing not started. Reason: ~s~n~n", + [file:format_error(Error)]), false end; false -> diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl index c6f5fd7df4..71a784870c 100644 --- a/lib/common_test/src/ct_telnet.erl +++ b/lib/common_test/src/ct_telnet.erl @@ -245,7 +245,6 @@ cmdf(Connection,CmdFormat,Args) -> %%% Data = [string()] %%% @doc Send a telnet command and wait for prompt %%% (uses a format string and list of arguments to build the command). -%%%----------------------------------------------------------------- cmdf(Connection,CmdFormat,Args,Timeout) when is_list(Args) -> Cmd = lists:flatten(io_lib:format(CmdFormat,Args)), cmd(Connection,Cmd,Timeout). @@ -360,15 +359,15 @@ expect(Connection,Patterns) -> %%% will also be a <code>HaltReason</code> returned.</p> %%% %%% <p><underline>Examples:</underline><br/> -%%% <code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}], -%%% [sequence,{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match +%%% <code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],</code> +%%% <code>[sequence,{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match %%% "ABC" first and then "XYZ", but if "NNN" appears the function will %%% return <code>{error,{nnn,["NNN"]}}</code>. If both "ABC" and "XYZ" %%% are matched, the function will return %%% <code>{ok,[AbcMatch,XyzMatch]}</code>.</p> %%% -%%% <p><code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}], -%%% [{repeat,2},{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match +%%% <p><code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],</code> +%%% <code>[{repeat,2},{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match %%% "ABC" or "XYZ" twice. If "NNN" appears the function will return %%% with <code>HaltReason = {nnn,["NNN"]}</code>.</p> %%% diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl index d845358bb2..2cba1d8410 100644 --- a/lib/common_test/src/ct_testspec.erl +++ b/lib/common_test/src/ct_testspec.erl @@ -249,11 +249,15 @@ collect_tests_from_file1([Spec|Specs],TestSpec,Relaxed) -> SpecDir = filename:dirname(filename:absname(Spec)), case file:consult(Spec) of {ok,Terms} -> - TestSpec1 = collect_tests(Terms,TestSpec#testspec{spec_dir=SpecDir}, + TestSpec1 = collect_tests(Terms, + TestSpec#testspec{spec_dir=SpecDir}, Relaxed), collect_tests_from_file1(Specs,TestSpec1,Relaxed); {error,Reason} -> - throw({error,{Spec,Reason}}) + ReasonStr = + lists:flatten(io_lib:format("~s", + [file:format_error(Reason)])), + throw({error,{Spec,ReasonStr}}) end; collect_tests_from_file1([],TS=#testspec{config=Cfgs,event_handler=EvHs, include=Incl,tests=Tests},_) -> @@ -481,6 +485,26 @@ add_tests([{logdir,Node,Dir}|Ts],Spec) -> add_tests([{logdir,Dir}|Ts],Spec) -> add_tests([{logdir,all_nodes,Dir}|Ts],Spec); +%% --- logopts --- +add_tests([{logopts,all_nodes,Opts}|Ts],Spec) -> + LogOpts = Spec#testspec.logopts, + Tests = [{logopts,N,Opts} || + N <- list_nodes(Spec), + lists:keymember(ref2node(N,Spec#testspec.nodes),1, + LogOpts) == false], + add_tests(Tests++Ts,Spec); +add_tests([{logopts,Nodes,Opts}|Ts],Spec) when is_list(Nodes) -> + Ts1 = separate(Nodes,logopts,[Opts],Ts,Spec#testspec.nodes), + add_tests(Ts1,Spec); +add_tests([{logopts,Node,Opts}|Ts],Spec) -> + LogOpts = Spec#testspec.logopts, + LogOpts1 = [{ref2node(Node,Spec#testspec.nodes),Opts} | + lists:keydelete(ref2node(Node,Spec#testspec.nodes), + 1,LogOpts)], + add_tests(Ts,Spec#testspec{logopts=LogOpts1}); +add_tests([{logopts,Opts}|Ts],Spec) -> + add_tests([{logopts,all_nodes,Opts}|Ts],Spec); + %% --- label --- add_tests([{label,all_nodes,Lbl}|Ts],Spec) -> Labels = Spec#testspec.label, @@ -1097,6 +1121,8 @@ valid_terms() -> {merge_tests,1}, {logdir,2}, {logdir,3}, + {logopts,2}, + {logopts,3}, {label,2}, {label,3}, {event_handler,2}, diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl index b3e345b4e5..3b6ad6f98d 100644 --- a/lib/common_test/src/ct_util.erl +++ b/lib/common_test/src/ct_util.erl @@ -47,7 +47,7 @@ -export([get_mode/0, create_table/3, read_opts/0]). --export([set_cwd/1, reset_cwd/0]). +-export([set_cwd/1, reset_cwd/0, get_start_dir/0]). -export([parse_table/1]). @@ -61,6 +61,9 @@ -export([warn_duplicates/1]). +-export([get_profile_data/0, get_profile_data/1, + get_profile_data/2, open_url/3]). + -include("ct_event.hrl"). -include("ct_util.hrl"). @@ -121,13 +124,15 @@ do_start(Parent,Mode,LogDir) -> ok -> ok; E -> exit(E) end, + DoExit = fun(Reason) -> file:set_cwd(StartDir), exit(Reason) end, Opts = case read_opts() of {ok,Opts1} -> Opts1; Error -> Parent ! {self(),Error}, - exit(Error) + DoExit(Error) end, + %% start an event manager (if not already started by master) case ct_event:start_link() of {error,{already_started,_}} -> @@ -140,16 +145,23 @@ do_start(Parent,Mode,LogDir) -> ct_event:add_handler([{vts,VtsPid}]) end end, + %% start ct_config server - ct_config:start(Mode), + try ct_config:start(Mode) of + _ -> ok + catch + _Class:CfgError -> + DoExit(CfgError) + end, + %% add user event handlers case lists:keysearch(event_handler,1,Opts) of {value,{_,Handlers}} -> Add = fun({H,Args}) -> case catch gen_event:add_handler(?CT_EVMGR_REF,H,Args) of ok -> ok; - {'EXIT',Why} -> exit(Why); - Other -> exit({event_handler,Other}) + {'EXIT',Why} -> DoExit(Why); + Other -> DoExit({event_handler,Other}) end end, case catch lists:foreach(Add,Handlers) of @@ -168,10 +180,15 @@ do_start(Parent,Mode,LogDir) -> data={StartTime, lists:flatten(TestLogDir)}}), %% Initialize ct_hooks - case catch ct_hooks:init(Opts) of + try ct_hooks:init(Opts) of ok -> Parent ! {self(),started}; - {_,CTHReason} -> + {fail,CTHReason} -> + ct_logs:tc_print('Suite Callback',CTHReason,[]), + self() ! {{stop,{self(),{user_error,CTHReason}}}, + {Parent,make_ref()}} + catch + _:CTHReason -> ct_logs:tc_print('Suite Callback',CTHReason,[]), self() ! {{stop,{self(),{user_error,CTHReason}}}, {Parent,make_ref()}} @@ -243,6 +260,9 @@ set_cwd(Dir) -> reset_cwd() -> call(reset_cwd). +get_start_dir() -> + call(get_start_dir). + loop(Mode,TestData,StartDir) -> receive {update_last_run_index,From} -> @@ -319,6 +339,9 @@ loop(Mode,TestData,StartDir) -> {reset_cwd,From} -> return(From,file:set_cwd(StartDir)), loop(From,TestData,StartDir); + {get_start_dir,From} -> + return(From,StartDir), + loop(From,TestData,StartDir); {{stop,Info},From} -> Time = calendar:local_time(), ct_event:sync_notify(#event{name=test_done, @@ -332,7 +355,7 @@ loop(Mode,TestData,StartDir) -> ets:delete(?conn_table), ets:delete(?board_table), ets:delete(?suite_table), - ct_logs:close(Info), + ct_logs:close(Info, StartDir), ct_event:stop(), ct_config:stop(), file:set_cwd(StartDir), @@ -727,6 +750,79 @@ warn_duplicates(Suites) -> lists:foreach(Warn, Suites), ok. +%%%----------------------------------------------------------------- +%%% @spec +%%% +%%% @doc +get_profile_data() -> + get_profile_data(all). + +get_profile_data(KeyOrStartDir) -> + if is_atom(KeyOrStartDir) -> + get_profile_data(KeyOrStartDir, get_start_dir()); + is_list(KeyOrStartDir) -> + get_profile_data(all, KeyOrStartDir) + end. + +get_profile_data(Key, StartDir) -> + Profile = case application:get_env(common_test, profile) of + {ok,undefined} -> default; + {ok,Prof} -> Prof; + _ -> default + end, + get_profile_data(Profile, Key, StartDir). + +get_profile_data(Profile, Key, StartDir) -> + File = case Profile of + default -> + ?ct_profile_file; + _ when is_list(Profile) -> + ?ct_profile_file ++ "." ++ Profile; + _ when is_atom(Profile) -> + ?ct_profile_file ++ "." ++ atom_to_list(Profile) + end, + FullNameWD = filename:join(StartDir, File), + {WhichFile,Result} = + case file:consult(FullNameWD) of + {error,enoent} -> + case init:get_argument(home) of + {ok,[[HomeDir]]} -> + FullNameHome = filename:join(HomeDir, File), + {FullNameHome,file:consult(FullNameHome)}; + _ -> + {File,{error,enoent}} + end; + Consulted -> + {FullNameWD,Consulted} + end, + case Result of + {error,enoent} when Profile /= default -> + io:format(user, "~nERROR! Missing profile file ~p~n", [File]), + undefined; + {error,enoent} when Profile == default -> + undefined; + {error,Reason} -> + io:format(user,"~nERROR! Error in profile file ~p: ~p~n", + [WhichFile,Reason]), + undefined; + {ok,Data} -> + Data1 = case Data of + [List] when is_list(List) -> + List; + _ when is_list(Data) -> + Data; + _ -> + io:format(user, + "~nERROR! Invalid profile data in ~p~n", + [WhichFile]), + [] + end, + if Key == all -> + Data1; + true -> + proplists:get_value(Key, Data) + end + end. %%%----------------------------------------------------------------- %%% Internal functions @@ -799,3 +895,28 @@ abs_name2([H|T],Acc) -> abs_name2(T,[H|Acc]); abs_name2([],Acc) -> filename:join(lists:reverse(Acc)). + +open_url(iexplore, Args, URL) -> + {ok,R} = win32reg:open([read]), + ok = win32reg:change_key(R,"applications\\iexplore.exe\\shell\\open\\command"), + case win32reg:values(R) of + {ok, Paths} -> + Path = proplists:get_value(default, Paths), + [Cmd | _] = string:tokens(Path, "%"), + Cmd1 = Cmd ++ " " ++ Args ++ " " ++ URL, + io:format(user, "~nOpening ~s with command:~n ~s~n", [URL,Cmd1]), + open_port({spawn,Cmd1}, []); + _ -> + io:format("~nNo path to iexplore.exe~n",[]) + end, + win32reg:close(R), + ok; + +open_url(Prog, Args, URL) -> + ProgStr = if is_atom(Prog) -> atom_to_list(Prog); + is_list(Prog) -> Prog + end, + Cmd = ProgStr ++ " " ++ Args ++ " " ++ URL, + io:format(user, "~nOpening ~s with command:~n ~s~n", [URL,Cmd]), + open_port({spawn,Cmd},[]), + ok. diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl index 556f88c84d..73898fe371 100644 --- a/lib/common_test/src/ct_util.hrl +++ b/lib/common_test/src/ct_util.hrl @@ -31,7 +31,9 @@ nodes=[], init=[], label=[], + profile=[], logdir=["."], + logopts=[], cover=[], config=[], userconfig=[], @@ -58,3 +60,5 @@ -define(missing_suites_info, "missing_suites.info"). -define(ct_config_txt, ct_config_plain). + +-define(ct_profile_file, ".common_test"). diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl index f0bf090804..cc8a932887 100644 --- a/lib/common_test/src/vts.erl +++ b/lib/common_test/src/vts.erl @@ -20,7 +20,7 @@ -module(vts). -export([start/0, - init_data/4, + init_data/5, stop/0, report/2]). @@ -32,6 +32,7 @@ menu_frame/2, welcome_frame/2, config_frame/2, + browse_config_file/2, add_config_file/2, remove_config_file/2, run_frame/2, @@ -56,7 +57,7 @@ -record(state,{tests=[],config=[],event_handler=[],test_runner, running=0,reload_results=false,start_dir,current_log_dir, - total=0,ok=0,fail=0,skip=0,testruns=[]}). + logopts=[],total=0,ok=0,fail=0,skip=0,testruns=[]}). %%%----------------------------------------------------------------- @@ -65,8 +66,8 @@ start() -> webtool:start(), webtool:start_tools([],"app=vts"). -init_data(ConfigFiles,EvHandlers,LogDir,Tests) -> - call({init_data,ConfigFiles,EvHandlers,LogDir,Tests}). +init_data(ConfigFiles,EvHandlers,LogDir,LogOpts,Tests) -> + call({init_data,ConfigFiles,EvHandlers,LogDir,LogOpts,Tests}). stop() -> webtool:stop_tools([],"app=vts"), @@ -119,6 +120,8 @@ menu_frame(_Env,_Input) -> call(menu_frame). config_frame(_Env,_Input) -> call(config_frame). +browse_config_file(_Env,Input) -> + call({browse_config_file,Input}). add_config_file(_Env,Input) -> call({add_config_file,Input}). remove_config_file(_Env,Input) -> @@ -160,10 +163,11 @@ init(Parent) -> loop(State) -> receive - {{init_data,Config,EvHandlers,LogDir,Tests},From} -> + {{init_data,Config,EvHandlers,LogDir,LogOpts,Tests},From} -> %% ct:pal("State#state.current_log_dir=~p", [State#state.current_log_dir]), NewState = State#state{config=Config,event_handler=EvHandlers, - current_log_dir=LogDir,tests=Tests}, + current_log_dir=LogDir, + logopts=LogOpts,tests=Tests}, ct_install(NewState), return(From,ok), loop(NewState); @@ -182,6 +186,9 @@ loop(State) -> {config_frame,From} -> return(From,config_frame1(State)), loop(State); + {{browse_config_file,_Input},From} -> + return(From,ok), + loop(State); {{add_config_file,Input},From} -> {Return,State1} = add_config_file1(Input,State), ct_install(State1), @@ -241,10 +248,12 @@ loop(State) -> return(From,ok); {'EXIT',Pid,Reason} -> case State#state.test_runner of - Pid -> io:format("ERROR: test runner crashed: ~p\n",[Reason]); - _ -> ignore - end, - loop(State); + Pid -> + io:format("Test run error: ~p\n",[Reason]), + loop(State); + _ -> + loop(State) + end; {{test_info,_Type,_Data},From} -> return(From,ok), loop(State) @@ -270,10 +279,11 @@ return({To,Ref},Result) -> To ! {Ref, Result}. -run_test1(State=#state{tests=Tests,current_log_dir=LogDir}) -> +run_test1(State=#state{tests=Tests,current_log_dir=LogDir, + logopts=LogOpts}) -> Self=self(), RunTest = fun() -> - case ct_run:do_run(Tests,[],LogDir) of + case ct_run:do_run(Tests,[],LogDir,LogOpts) of {error,_Reason} -> aborted(); _ -> @@ -282,17 +292,18 @@ run_test1(State=#state{tests=Tests,current_log_dir=LogDir}) -> unlink(Self) end, Pid = spawn_link(RunTest), - Total = + {Total,Tests1} = receive {{test_info,start_info,{_,_,Cases}},From} -> return(From,ok), - Cases; + {Cases,Tests}; EXIT = {'EXIT',_,_} -> - self() ! EXIT + self() ! EXIT, + {0,[]} after 30000 -> - 0 + {0,[]} end, - State#state{test_runner=Pid,running=length(Tests), + State#state{test_runner=Pid,running=length(Tests1), total=Total,ok=0,fail=0,skip=0,testruns=[]}. @@ -356,22 +367,32 @@ config_frame1(State) -> config_body(State) -> Entry = [input("TYPE=file NAME=browse SIZE=40"), input("TYPE=hidden NAME=file")], + BrowseForm = + form( + "NAME=read_file_form METHOD=post ACTION=\"./browse_config_file\"", + table( + "BORDER=0", + [tr(td("1. Locate config file")), + tr(td(Entry))])), AddForm = form( - "NAME=read_file_form METHOD=post ACTION=\"./add_config_file\"", + "NAME=add_file_form METHOD=post ACTION=\"./add_config_file\"", table( "BORDER=0", - [tr( - [td(Entry), + [tr(td("2. Paste full config file name here")), + tr( + [td(input("TYPE=text NAME=file SIZE=40")), td("ALIGN=center", input("TYPE=submit onClick=\"file.value=browse.value;\"" " VALUE=\"Add\""))])])), + {Text,RemoveForm} = case State#state.config of [] -> - T = "To be able to run any tests, one or more configuration " - "files must be added. Enter the name of the configuration " - "file below and click the \"Add\" button.", + T = "Before running the tests, one or more configuration " + "files may be added. Locate the config file, copy its " + "full name, paste this into the text field below, then " + "click the \"Add\" button.", R = "", {T,R}; Files -> @@ -394,20 +415,24 @@ config_body(State) -> input("TYPE=submit VALUE=\"Remove\"")))])), {T,R} end, - + [h1("ALIGN=center","Config"), table( - "WIDTH=600 ALIGN=center CELLPADDING=5", + "WIDTH=450 ALIGN=center CELLPADDING=5", [tr(td(["BGCOLOR=",?INFO_BG_COLOR],Text)), - tr(td("ALIGN=center",AddForm)), - tr(td("ALIGN=center",RemoveForm))])]. - + tr(td("")), + tr(td("")), + tr(td("ALIGN=left",BrowseForm)), + tr(td("ALIGN=left",AddForm)), + tr(td("ALIGN=left",RemoveForm))])]. add_config_file1(Input,State) -> State1 = case get_input_data(Input,"file") of - "" -> State; - File -> State#state{config=[File|State#state.config]} + "" -> + State; + File -> + State#state{config=[File|State#state.config]} end, Return = config_frame1(State1), {Return,State1}. @@ -427,10 +452,17 @@ run_body(#state{running=Running}) when Running>0 -> [h1("ALIGN=center","Run Test"), p(["Test are ongoing: ",href("./result_frameset","Results")])]; run_body(State) -> - ConfigList = ul([li(File) || File <- State#state.config]), + ConfigList = + case State#state.config of + [] -> + ul(["none"]); + CfgFiles -> + ul([li(File) || File <- CfgFiles]) + end, ConfigFiles = [h3("Config Files"), ConfigList], - + {ok,CWD} = file:get_cwd(), + CurrWD = [h3("Current Working Directory"), ul(CWD)], AddDirForm = form( "NAME=add_dir_form METHOD=post ACTION=\"./add_test_dir\"", @@ -442,7 +474,6 @@ run_body(State) -> td("ALIGN=center", input("TYPE=submit onClick=\"dir.value=browse.value;\"" " VALUE=\"Add Test Dir\""))])])), - {LoadedTestsTable,Submit} = case create_testdir_entries(State#state.tests,1) of [] -> {"",""}; @@ -454,22 +485,20 @@ run_body(State) -> {table("CELLPADDING=5",[Heading,TestDirs]), submit_button()} end, - - %% It should be ok to have no config-file! Body = - %% case State#state.config of %% [] -> %% p("ALIGN=center", - %% href("./config_frame","Please select one or - %% more config files")); %% _ -> table( - "WIDTH=100%", - [tr(td(ConfigFiles)), + "WIDTH=450 ALIGN=center", + [tr(td("")), + tr(td("")), + tr(td(ConfigFiles)), + tr(td("")), + tr(td(CurrWD)), tr(td("")), tr(td(AddDirForm)), tr(td("")), tr(td(LoadedTestsTable)), - tr(td(Submit))]), - %% end, - + tr(td(Submit)) + ]), [h1("ALIGN=center","Run Test"), Body]. create_testdir_entries([{Dir,Suite,Case}|Tests],N) -> @@ -556,18 +585,17 @@ options([Element|Elements],Selected,N,Func) -> options([],_Selected,_N,_Func) -> []. -add_test_dir1(Input,State) -> +add_test_dir1(Input, State) -> State1 = case get_input_data(Input,"dir") of "" -> State; Dir0 -> Dir = case ct_util:is_test_dir(Dir0) of - true -> - Dir0; - false -> filename:join(Dir0,"test") + true -> Dir0; + false -> ct_util:get_testdir(Dir0, all) end, case filelib:is_dir(Dir) of - true -> + true -> Test = ct_run:tests(Dir), State#state{tests=State#state.tests++Test}; false -> @@ -577,8 +605,6 @@ add_test_dir1(Input,State) -> Return = run_frame1(State1), {Return,State1}. - - remove_test_dir1(Input,State) -> N = list_to_integer(get_input_data(Input,"dir")), State1 = State#state{tests=delete_test(N,State#state.tests)}, @@ -641,6 +667,9 @@ result_frameset2(State) -> "./redirect_to_result_log_frame"; {_Dir,0} -> filename:join(["/log_dir","index.html"]); + {_Dir,_} when State#state.testruns == [] -> + %% crash before first test + "./no_result_log_frame"; {_Dir,_} -> {_,CurrentLog} = hd(State#state.testruns), CurrentLog @@ -749,6 +778,8 @@ report1(tc_done,{_Suite,_Case,{skipped,_Reason}},State) -> State#state{skip=State#state.skip+1}; report1(tc_user_skip,{_Suite,_Case,_Reason},State) -> State#state{skip=State#state.skip+1}; +report1(tc_auto_skip,{_Suite,_Case,_Reason},State) -> + State#state{skip=State#state.skip+1}; report1(loginfo,_,State) -> State. @@ -850,6 +881,8 @@ h2(Text) -> ["<H2>",Text,"</H2>\n"]. h3(Text) -> ["<H3>",Text,"</H3>\n"]. +%%h4(Text) -> +%% ["<H4>",Text,"</H4>\n"]. font(Args,Text) -> ["<FONT ",Args,">\n",Text,"\n</FONT>\n"]. p(Text) -> |