diff options
Diffstat (limited to 'lib/common_test/src')
33 files changed, 2547 insertions, 1319 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile index dd2923ece9..4600c0ad78 100644 --- a/lib/common_test/src/Makefile +++ b/lib/common_test/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2003-2012. All Rights Reserved. +# Copyright Ericsson AB 2003-2013. All Rights Reserved. # # The contents of this file are subject to the Erlang Public License, # Version 1.1, (the "License"); you may not use this file except in @@ -97,7 +97,7 @@ DTD_FILES = \ # ---------------------------------------------------- ERL_COMPILE_FLAGS += -pa ../ebin -I../include -I $(ERL_TOP)/lib/snmp/include/ \ -I../../test_server/include -I../../xmerl/inc/ \ - -I $(ERL_TOP)/lib/kernel/include + -I $(ERL_TOP)/lib/kernel/include -Werror # ---------------------------------------------------- # Targets @@ -127,10 +127,10 @@ clean: # Special Build Targets # ---------------------------------------------------- $(APP_TARGET): $(APP_SRC) ../vsn.mk - sed -e 's;%VSN%;$(VSN);' $< > $@ + $(vsn_verbose)sed -e 's;%VSN%;$(VSN);' $< > $@ $(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk - sed -e 's;%VSN%;$(VSN);' $< > $@ + $(vsn_verbose)sed -e 's;%VSN%;$(VSN);' $< > $@ # ---------------------------------------------------- # Release Target diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl index ad9bf4e2d6..e6732f7fc7 100644 --- a/lib/common_test/src/ct.erl +++ b/lib/common_test/src/ct.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -144,16 +144,16 @@ run(TestDirs) -> %%% @spec run_test(Opts) -> Result %%% Opts = [OptTuples] %%% OptTuples = {dir,TestDirs} | {suite,Suites} | {group,Groups} | -%%% {testcase,Cases} | {spec,TestSpecs} | {label,Label} | -%%% {config,CfgFiles} | {userconfig, UserConfig} | +%%% {testcase,Cases} | {spec,TestSpecs} | {join_specs,Bool} | +%%% {label,Label} | {config,CfgFiles} | {userconfig, UserConfig} | %%% {allow_user_terms,Bool} | {logdir,LogDir} | %%% {silent_connections,Conns} | {stylesheet,CSSFile} | -%%% {cover,CoverSpecFile} | {step,StepOpts} | +%%% {cover,CoverSpecFile} | {cover_stop,Bool} | {step,StepOpts} | %%% {event_handler,EventHandlers} | {include,InclDirs} | %%% {auto_compile,Bool} | {create_priv_dir,CreatePrivDir} | %%% {multiply_timetraps,M} | {scale_timetraps,Bool} | %%% {repeat,N} | {duration,DurTime} | {until,StopTime} | -%%% {force_stop,Bool} | {decrypt,DecryptKeyOrFile} | +%%% {force_stop,ForceStop} | {decrypt,DecryptKeyOrFile} | %%% {refresh_logs,LogDir} | {logopts,LogOpts} | %%% {verbosity,VLevels} | {basic_html,Bool} | %%% {ct_hooks, CTHs} | {enable_builtin_hooks,Bool} | @@ -184,6 +184,7 @@ run(TestDirs) -> %%% N = integer() %%% DurTime = string(HHMMSS) %%% StopTime = string(YYMoMoDDHHMMSS) | string(HHMMSS) +%%% ForceStop = skip_rest | Bool %%% DecryptKeyOrFile = {key,DecryptKey} | {file,DecryptFile} %%% DecryptKey = string() %%% DecryptFile = string() @@ -735,7 +736,7 @@ fail(Format, Args) -> %%% overwrites the string set by this function.</p> comment(Comment) when is_list(Comment) -> Formatted = - case (catch io_lib:format("~s",[Comment])) of + case (catch io_lib:format("~ts",[Comment])) of {'EXIT',_} -> % it's a list not a string io_lib:format("~p",[Comment]); String -> @@ -988,8 +989,9 @@ get_testdata(Key) -> end. %%%----------------------------------------------------------------- -%%% @spec abort_current_testcase(Reason) -> ok | {error,no_testcase_running} +%%% @spec abort_current_testcase(Reason) -> ok | {error,ErrorReason} %%% Reason = term() +%%% ErrorReason = no_testcase_running | parallel_group %%% %%% @doc <p>When calling this function, the currently executing test case will be aborted. %%% It is the user's responsibility to know for sure which test case is currently diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl index b1d709bc75..5c80a299f8 100644 --- a/lib/common_test/src/ct_config.erl +++ b/lib/common_test/src/ct_config.erl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2012. All Rights Reserved. +%% Copyright Ericsson AB 2010-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -46,7 +46,7 @@ decrypt_config_file/2, decrypt_config_file/3, get_crypt_key_from_file/0, get_crypt_key_from_file/1]). --export([get_ref_from_name/1, get_name_from_ref/1, get_key_from_name/1]). +-export([get_key_from_name/1]). -export([check_config_files/1, add_default_callback/1, prepare_config_list/1]). @@ -56,7 +56,7 @@ -define(cryptfile, ".ct_config.crypt"). --record(ct_conf,{key,value,handler,config,ref,name='_UNDEF',default=false}). +-record(ct_conf,{key,value,handler,config,name='_UNDEF',default=false}). start(Mode) -> case whereis(ct_config_server) of @@ -266,13 +266,15 @@ read_config_files_int([{Callback, File}|Files], FunToSave) -> read_config_files_int([], _FunToSave) -> ok. -store_config(Config, Callback, File) -> +store_config(Config, Callback, File) when is_tuple(Config) -> + store_config([Config], Callback, File); + +store_config(Config, Callback, File) when is_list(Config) -> [ets:insert(?attr_table, #ct_conf{key=Key, value=Val, handler=Callback, config=File, - ref=ct_util:ct_make_ref(), default=false}) || {Key,Val} <- Config]. @@ -293,13 +295,11 @@ rewrite_config(Config, Callback, File) -> #ct_conf{key=Key, value=Value, handler=Callback, - config=File, - ref=ct_util:ct_make_ref()}); + config=File}); RowsToUpdate -> Inserter = fun(Row) -> ets:insert(?attr_table, - Row#ct_conf{value=Value, - ref=ct_util:ct_make_ref()}) + Row#ct_conf{value=Value}) end, lists:foreach(Inserter, RowsToUpdate) end @@ -311,7 +311,7 @@ set_config(Config,Default) -> set_config(Name,Config,Default) -> [ets:insert(?attr_table, - #ct_conf{key=Key,value=Val,ref=ct_util:ct_make_ref(), + #ct_conf{key=Key,value=Val, name=Name,default=Default}) || {Key,Val} <- Config]. @@ -556,26 +556,6 @@ encrypt_config_file(SrcFileName, EncryptFileName) -> encrypt_config_file(SrcFileName, EncryptFileName, {key,Key}) end. -get_ref_from_name(Name) -> - case ets:select(?attr_table,[{#ct_conf{name=Name,ref='$1',_='_'}, - [], - ['$1']}]) of - [Ref] -> - {ok,Ref}; - _ -> - {error,{no_such_name,Name}} - end. - -get_name_from_ref(Ref) -> - case ets:select(?attr_table,[{#ct_conf{name='$1',ref=Ref,_='_'}, - [], - ['$1']}]) of - [Name] -> - {ok,Name}; - _ -> - {error,{no_such_ref,Ref}} - end. - get_key_from_name(Name) -> case ets:select(?attr_table,[{#ct_conf{name=Name,key='$1',_='_'}, [], @@ -596,7 +576,7 @@ encrypt_config_file(SrcFileName, EncryptFileName, {file,KeyFile}) -> encrypt_config_file(SrcFileName, EncryptFileName, {key,Key}) -> crypto:start(), - {K1,K2,K3,IVec} = make_crypto_key(Key), + {Key,IVec} = make_crypto_key(Key), case file:read_file(SrcFileName) of {ok,Bin0} -> Bin1 = term_to_binary({SrcFileName,Bin0}), @@ -604,10 +584,10 @@ encrypt_config_file(SrcFileName, EncryptFileName, {key,Key}) -> 0 -> Bin1; N -> list_to_binary([Bin1,random_bytes(8-N)]) end, - EncBin = crypto:des3_cbc_encrypt(K1, K2, K3, IVec, Bin2), + EncBin = crypto:block_encrypt(des3_cbc, Key, IVec, Bin2), case file:write_file(EncryptFileName, EncBin) of ok -> - io:format("~s --(encrypt)--> ~s~n", + io:format("~ts --(encrypt)--> ~ts~n", [SrcFileName,EncryptFileName]), ok; {error,Reason} -> @@ -635,10 +615,10 @@ decrypt_config_file(EncryptFileName, TargetFileName, {file,KeyFile}) -> decrypt_config_file(EncryptFileName, TargetFileName, {key,Key}) -> crypto:start(), - {K1,K2,K3,IVec} = make_crypto_key(Key), + {Key,IVec} = make_crypto_key(Key), case file:read_file(EncryptFileName) of {ok,Bin} -> - DecBin = crypto:des3_cbc_decrypt(K1, K2, K3, IVec, Bin), + DecBin = crypto:block_decrypt(des3_cbc, Key, IVec, Bin), case catch binary_to_term(DecBin) of {'EXIT',_} -> {error,bad_file}; @@ -649,7 +629,7 @@ decrypt_config_file(EncryptFileName, TargetFileName, {key,Key}) -> _ -> case file:write_file(TargetFileName, SrcBin) of ok -> - io:format("~s --(decrypt)--> ~s~n", + io:format("~ts --(decrypt)--> ~ts~n", [EncryptFileName,TargetFileName]), ok; {error,Reason} -> @@ -700,7 +680,7 @@ get_crypt_key_from_file() -> _ -> case catch string:tokens(binary_to_list(Result), [$\n,$\r]) of [Key] -> - io:format("~nCrypt key file: ~s~n", [FullName]), + io:format("~nCrypt key file: ~ts~n", [FullName]), Key; _ -> {error,{bad_crypt_file,FullName}} @@ -710,7 +690,7 @@ get_crypt_key_from_file() -> make_crypto_key(String) -> <<K1:8/binary,K2:8/binary>> = First = erlang:md5(String), <<K3:8/binary,IVec:8/binary>> = erlang:md5([First|lists:reverse(String)]), - {K1,K2,K3,IVec}. + {[K1,K2,K3],IVec}. random_bytes(N) -> {A,B,C} = now(), diff --git a/lib/common_test/src/ct_config_plain.erl b/lib/common_test/src/ct_config_plain.erl index 237df5c8f3..c6547f0a40 100644 --- a/lib/common_test/src/ct_config_plain.erl +++ b/lib/common_test/src/ct_config_plain.erl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2011. All Rights Reserved. +%% Copyright Ericsson AB 2010-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -47,7 +47,7 @@ read_config(ConfigFile) -> {error,no_crypt_file} -> {error,{config_file_error, lists:flatten( - io_lib:format("~s",[file:format_error(Reason)]))}}; + io_lib:format("~ts",[file:format_error(Reason)]))}}; {error,CryptError} -> {error,{decrypt_file_error,CryptError}}; _ when is_list(Key) -> diff --git a/lib/common_test/src/ct_conn_log_h.erl b/lib/common_test/src/ct_conn_log_h.erl index d7bd18606b..ac08a3e0ad 100644 --- a/lib/common_test/src/ct_conn_log_h.erl +++ b/lib/common_test/src/ct_conn_log_h.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012. All Rights Reserved. +%% Copyright Ericsson AB 2012-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -52,7 +52,7 @@ open_files([],State) -> do_open_files([{Tag,File}|Logs],Acc) -> - case file:open(File, [write]) of + case file:open(File, [write,{encoding,utf8}]) of {ok,Fd} -> do_open_files(Logs,[{Tag,Fd}|Acc]); {error,Reason} -> @@ -98,9 +98,9 @@ terminate(_,#state{logs=Logs}) -> %%% Writing reports write_report(Time,#conn_log{module=ConnMod}=Info,Data,State) -> {LogType,Fd} = get_log(Info,State), - io:format(Fd,"~n~s~s~s",[format_head(ConnMod,LogType,Time), - format_title(LogType,Info), - format_data(ConnMod,LogType,Data)]). + io:format(Fd,"~n~ts~ts~ts",[format_head(ConnMod,LogType,Time), + format_title(LogType,Info), + format_data(ConnMod,LogType,Data)]). write_error(Time,#conn_log{module=ConnMod}=Info,Report,State) -> case get_log(Info,State) of @@ -109,9 +109,10 @@ write_error(Time,#conn_log{module=ConnMod}=Info,Report,State) -> %% sasl error handler, so don't write it again. ok; {LogType,Fd} -> - io:format(Fd,"~n~s~s~s",[format_head(ConnMod,LogType,Time," ERROR"), - format_title(LogType,Info), - format_error(LogType,Report)]) + io:format(Fd,"~n~ts~ts~ts", + [format_head(ConnMod,LogType,Time," ERROR"), + format_title(LogType,Info), + format_error(LogType,Report)]) end. get_log(Info,State) -> @@ -140,16 +141,16 @@ format_head(ConnMod,LogType,Time) -> format_head(ConnMod,LogType,Time,""). format_head(ConnMod,raw,Time,Text) -> - io_lib:format("~n~p, ~p~s, ",[now_to_time(Time),ConnMod,Text]); + io_lib:format("~n~w, ~w~ts, ",[now_to_time(Time),ConnMod,Text]); format_head(ConnMod,_,Time,Text) -> Head = pad_char_end(?WIDTH,pretty_head(now_to_time(Time),ConnMod,Text),$=), - io_lib:format("~n~s",[Head]). + io_lib:format("~n~ts",[Head]). format_title(raw,#conn_log{client=Client}=Info) -> - io_lib:format("Client ~p ~s ~s",[Client,actionstr(Info),serverstr(Info)]); + io_lib:format("Client ~w ~s ~ts",[Client,actionstr(Info),serverstr(Info)]); format_title(_,Info) -> Title = pad_char_end(?WIDTH,pretty_title(Info),$=), - io_lib:format("~n~s", [Title]). + io_lib:format("~n~ts", [Title]). format_data(_,_,NoData) when NoData == ""; NoData == <<>> -> ""; @@ -162,8 +163,6 @@ format_error(pretty,Report) -> [io_lib:format("~n ~p: ~p",[K,V]) || {K,V} <- Report]. - - %%%----------------------------------------------------------------- %%% Helpers conn_info(LoggingProc, #conn_log{client=undefined} = ConnInfo) -> @@ -187,12 +186,12 @@ now_to_time({_,_,MicroS}=Now) -> pretty_head({{{Y,Mo,D},{H,Mi,S}},MicroS},ConnMod,Text0) -> Text = string:to_upper(atom_to_list(ConnMod) ++ Text0), - io_lib:format("= ~s ==== ~s-~s-~p::~s:~s:~s,~s ", + io_lib:format("= ~s ==== ~s-~s-~w::~s:~s:~s,~s ", [Text,t(D),month(Mo),Y,t(H),t(Mi),t(S), micro2milli(MicroS)]). pretty_title(#conn_log{client=Client}=Info) -> - io_lib:format("= Client ~p ~s Server ~s ", + io_lib:format("= Client ~w ~s Server ~ts ", [Client,actionstr(Info),serverstr(Info)]). actionstr(#conn_log{action=send}) -> "----->"; @@ -204,7 +203,7 @@ actionstr(_) -> "<---->". serverstr(#conn_log{name=undefined,address=Address}) -> io_lib:format("~p",[Address]); serverstr(#conn_log{name=Alias,address=Address}) -> - io_lib:format("~p(~p)",[Alias,Address]). + io_lib:format("~w(~p)",[Alias,Address]). month(1) -> "Jan"; month(2) -> "Feb"; diff --git a/lib/common_test/src/ct_cover.erl b/lib/common_test/src/ct_cover.erl index d39f50ba00..ae671c750a 100644 --- a/lib/common_test/src/ct_cover.erl +++ b/lib/common_test/src/ct_cover.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2009. All Rights Reserved. +%% Copyright Ericsson AB 2006-2012. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -24,7 +24,7 @@ -module(ct_cover). --export([get_spec/1, add_nodes/1, remove_nodes/1]). +-export([get_spec/1, add_nodes/1, remove_nodes/1, cross_cover_analyse/2]). -include("ct_util.hrl"). @@ -100,6 +100,22 @@ remove_nodes(Nodes) -> %%%----------------------------------------------------------------- +%%% @spec cross_cover_analyse(Level,Tests) -> ok +%%% Level = overview | details +%%% Tests = [{Tag,Dir}] +%%% Tag = atom() +%%% Dir = string() +%%% +%%% @doc Accumulate cover results over multiple tests. +%%% See the chapter about <seealso +%%% marker="cover_chapter#cross_cover">cross cover +%%% analysis</seealso> in the users's guide. +%%% +cross_cover_analyse(Level,Tests) -> + test_server_ctrl:cross_cover_analyse(Level,Tests). + + +%%%----------------------------------------------------------------- %%% @hidden %% Read cover specification file and return the parsed info. @@ -249,9 +265,11 @@ get_app_info(App=#cover{app=Name}, [{excl_mods,Name,Mods1}|Terms]) -> Mods = App#cover.excl_mods, get_app_info(App#cover{excl_mods=Mods++Mods1},Terms); -get_app_info(App=#cover{app=Name}, [{cross_apps,Name,AppMods1}|Terms]) -> - AppMods = App#cover.cross, - get_app_info(App#cover{cross=AppMods++AppMods1},Terms); +get_app_info(App=#cover{app=none}, [{cross,Cross}|Terms]) -> + get_app_info(App, [{cross,none,Cross}|Terms]); +get_app_info(App=#cover{app=Name}, [{cross,Name,Cross1}|Terms]) -> + Cross = App#cover.cross, + get_app_info(App#cover{cross=Cross++Cross1},Terms); get_app_info(App=#cover{app=none}, [{src_dirs,Dirs}|Terms]) -> get_app_info(App, [{src_dirs,none,Dirs}|Terms]); @@ -354,10 +372,10 @@ remove_excludes_and_dups(CoverData=#cover{excl_mods=Excl,incl_mods=Incl}) -> files2mods(Info=#cover{excl_mods=ExclFs, incl_mods=InclFs, - cross=CrossFs}) -> + cross=Cross}) -> Info#cover{excl_mods=files2mods1(ExclFs), incl_mods=files2mods1(InclFs), - cross=files2mods1(CrossFs)}. + cross=[{Tag,files2mods1(Fs)} || {Tag,Fs} <- Cross]}. files2mods1([M|Fs]) when is_atom(M) -> [M|files2mods1(Fs)]; diff --git a/lib/common_test/src/ct_event.erl b/lib/common_test/src/ct_event.erl index 49e0635d79..c1c1d943b9 100644 --- a/lib/common_test/src/ct_event.erl +++ b/lib/common_test/src/ct_event.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2012. All Rights Reserved. +%% Copyright Ericsson AB 2006-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -150,7 +150,7 @@ init(RecvPids) -> %%-------------------------------------------------------------------- handle_event(Event,State=#state{receivers=RecvPids}) -> print("~n=== ~w ===~n", [?MODULE]), - print("~p: ~p~n", [Event#event.name,Event#event.data]), + print("~w: ~w~n", [Event#event.name,Event#event.data]), lists:foreach(fun(Recv) -> report_event(Recv,Event) end, RecvPids), {ok,State}. diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl index 403eab66cb..276f902b05 100644 --- a/lib/common_test/src/ct_framework.erl +++ b/lib/common_test/src/ct_framework.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2012. All Rights Reserved. +%% Copyright Ericsson AB 2004-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -32,6 +32,7 @@ -export([error_in_suite/1, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2]). +-include("ct.hrl"). -include("ct_event.hrl"). -include("ct_util.hrl"). @@ -64,38 +65,46 @@ init_tc(Mod,Func,Config) -> ok end, - case ct_util:get_testdata(curr_tc) of - {Suite,{suite0_failed,{require,Reason}}} -> - {skip,{require_failed_in_suite0,Reason}}; - {Suite,{suite0_failed,_}=Failure} -> - {skip,Failure}; + case Func=/=end_per_suite + andalso Func=/=end_per_group + andalso ct_util:get_testdata(skip_rest) of + true -> + {skip,"Repeated test stopped by force_stop option"}; _ -> - ct_util:update_testdata(curr_tc, - fun(undefined) -> - [{Suite,Func}]; - (Running) -> - [{Suite,Func}|Running] - end, [create]), - case ct_util:read_suite_data({seq,Suite,Func}) of - undefined -> - init_tc1(Mod,Suite,Func,Config); - Seq when is_atom(Seq) -> - case ct_util:read_suite_data({seq,Suite,Seq}) of - [Func|TCs] -> % this is the 1st case in Seq - %% make sure no cases in this seq are - %% marked as failed from an earlier execution - %% in the same suite - lists:foreach( - fun(TC) -> - ct_util:save_suite_data({seq,Suite,TC}, - Seq) - end, TCs); - _ -> - ok - end, - init_tc1(Mod,Suite,Func,Config); - {failed,Seq,BadFunc} -> - {skip,{sequence_failed,Seq,BadFunc}} + case ct_util:get_testdata(curr_tc) of + {Suite,{suite0_failed,{require,Reason}}} -> + {skip,{require_failed_in_suite0,Reason}}; + {Suite,{suite0_failed,_}=Failure} -> + {skip,Failure}; + _ -> + ct_util:update_testdata(curr_tc, + fun(undefined) -> + [{Suite,Func}]; + (Running) -> + [{Suite,Func}|Running] + end, [create]), + case ct_util:read_suite_data({seq,Suite,Func}) of + undefined -> + init_tc1(Mod,Suite,Func,Config); + Seq when is_atom(Seq) -> + case ct_util:read_suite_data({seq,Suite,Seq}) of + [Func|TCs] -> % this is the 1st case in Seq + %% make sure no cases in this seq are + %% marked as failed from an earlier execution + %% in the same suite + lists:foreach( + fun(TC) -> + ct_util:save_suite_data( + {seq,Suite,TC}, + Seq) + end, TCs); + _ -> + ok + end, + init_tc1(Mod,Suite,Func,Config); + {failed,Seq,BadFunc} -> + {skip,{sequence_failed,Seq,BadFunc}} + end end end. @@ -750,12 +759,12 @@ error_notification(Mod,Func,_Args,{Error,Loc}) -> Descr1 = lists:flatten(io_lib:format("~P",[Descr,10])), if length(Descr1) > 50 -> Descr2 = string:substr(Descr1,1,50), - io_lib:format("{badmatch,~s...}",[Descr2]); + io_lib:format("{badmatch,~ts...}",[Descr2]); true -> - io_lib:format("{badmatch,~s}",[Descr1]) + io_lib:format("{badmatch,~ts}",[Descr1]) end; {test_case_failed,Reason} -> - case (catch io_lib:format("{test_case_failed,~s}", [Reason])) of + case (catch io_lib:format("{test_case_failed,~ts}", [Reason])) of {'EXIT',_} -> io_lib:format("{test_case_failed,~p}", [Reason]); Result -> Result @@ -788,7 +797,7 @@ error_notification(Mod,Func,_Args,{Error,Loc}) -> "<font color=\"green\">" ++ "(" ++ "</font>" ++ Comment ++ "<font color=\"green\">" ++ ")" ++ "</font>", - Str = io_lib:format("~s ~s", [ErrorHtml,CommentHtml]), + Str = io_lib:format("~ts ~ts", [ErrorHtml,CommentHtml]), test_server:comment(Str) end end, @@ -798,29 +807,35 @@ error_notification(Mod,Func,_Args,{Error,Loc}) -> "- - - - - - - - - -~n", io:format(user, lists:concat([Div,ErrFormat,Div,"~n"]), ErrArgs), - ct_logs:tc_log(ct_error_notify, "CT Error Notification", - ErrFormat, ErrArgs) + Link = + "\n\n<a href=\"#end\">" + "Full error description and stacktrace" + "</a>", + ct_logs:tc_log(ct_error_notify, + ?MAX_IMPORTANCE, + "CT Error Notification", + ErrFormat++Link, ErrArgs) end, case Loc of [{?MODULE,error_in_suite}] -> - PrintErr("Error in suite detected: ~s", [ErrStr]); + PrintErr("Error in suite detected: ~ts", [ErrStr]); R when R == unknown; R == undefined -> - PrintErr("Error detected: ~s", [ErrStr]); + PrintErr("Error detected: ~ts", [ErrStr]); %% if a function specified by all/0 does not exist, we %% pick up undef here [{LastMod,LastFunc}|_] when ErrStr == "undef" -> - PrintErr("~w:~w could not be executed~nReason: ~s", + PrintErr("~w:~w could not be executed~nReason: ~ts", [LastMod,LastFunc,ErrStr]); [{LastMod,LastFunc}|_] -> - PrintErr("~w:~w failed~nReason: ~s", [LastMod,LastFunc,ErrStr]); + PrintErr("~w:~w failed~nReason: ~ts", [LastMod,LastFunc,ErrStr]); [{LastMod,LastFunc,LastLine}|_] -> %% print error to console, we are only %% interested in the last executed expression - PrintErr("~w:~w failed on line ~w~nReason: ~s", + PrintErr("~w:~w failed on line ~w~nReason: ~ts", [LastMod,LastFunc,LastLine,ErrStr]), case ct_util:read_suite_data({seq,Mod,Func}) of @@ -1184,13 +1199,19 @@ report(What,Data) -> ok; {error,Reason} -> ct_logs:log("COVER INFO", - "Importing cover data from: ~s fails! " + "Importing cover data from: ~ts fails! " "Reason: ~p", [Imp,Reason]) end end, Imps) end; tests_done -> ok; + severe_error -> + ct_event:sync_notify(#event{name=What, + node=node(), + data=Data}), + ct_util:set_testdata({What,Data}), + ok; tc_start -> %% Data = {{Suite,Func},LogFileName} ct_event:sync_notify(#event{name=tc_logfile, @@ -1343,4 +1364,7 @@ format_comment(Comment) -> %%%----------------------------------------------------------------- %%% @spec get_html_wrapper(TestName, PrintLabel, Cwd) -> Header get_html_wrapper(TestName, PrintLabel, Cwd, TableCols) -> - ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols). + get_html_wrapper(TestName, PrintLabel, Cwd, TableCols, utf8). + +get_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) -> + ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding). diff --git a/lib/common_test/src/ct_ftp.erl b/lib/common_test/src/ct_ftp.erl index 8790393b36..71fd8754ff 100644 --- a/lib/common_test/src/ct_ftp.erl +++ b/lib/common_test/src/ct_ftp.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -348,10 +348,10 @@ terminate(FtpPid,State) -> get_handle(Pid) when is_pid(Pid) -> {ok,Pid}; get_handle(Name) -> - case ct_util:get_connections(Name,?MODULE) of - {ok,[{Pid,_}|_]} -> + case ct_util:get_connection(Name,?MODULE) of + {ok,{Pid,_}} -> {ok,Pid}; - {ok,[]} -> + {error,no_registered_connection} -> open(Name); Error -> Error diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl index 1f01d84601..a5b736136f 100644 --- a/lib/common_test/src/ct_gen_conn.erl +++ b/lib/common_test/src/ct_gen_conn.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -26,7 +26,7 @@ -compile(export_all). --export([start/4, stop/1]). +-export([start/4, stop/1, get_conn_pid/1]). -export([call/2, call/3, return/2, do_within_time/2]). -ifdef(debug). @@ -120,8 +120,16 @@ start(Name,Address,InitData,CallbackMod) -> %%% Handle = handle() %%% %%% @doc Close the connection and stop the process managing it. -stop(Pid) -> - call(Pid,stop,5000). +stop(Handle) -> + call(Handle,stop,5000). + +%%%----------------------------------------------------------------- +%%% @spec get_conn_pid(Handle) -> ok +%%% Handle = handle() +%%% +%%% @doc Return the connection pid associated with Handle +get_conn_pid(Handle) -> + call(Handle,get_conn_pid). %%%----------------------------------------------------------------- %%% @spec log(Heading,Format,Args) -> ok @@ -222,7 +230,8 @@ do_start(Opts) -> receive {connected,Pid} -> erlang:demonitor(MRef, [flush]), - ct_util:register_connection(Opts#gen_opts.name, Opts#gen_opts.address, + ct_util:register_connection(Opts#gen_opts.name, + Opts#gen_opts.address, Opts#gen_opts.callback, Pid), {ok,Pid}; {Error,Pid} -> @@ -272,7 +281,7 @@ call(Pid, Msg, Timeout) -> after Timeout -> erlang:demonitor(MRef, [flush]), log("ct_gen_conn", - "Connection process ~p not responding. Killing now!", + "Connection process ~w not responding. Killing now!", [Pid]), exit(Pid, kill), {error,{process_down,Pid,forced_termination}} @@ -315,10 +324,12 @@ loop(Opts) -> {ok, NewPid, NewState} -> link(NewPid), put(conn_pid,NewPid), - loop(Opts#gen_opts{conn_pid=NewPid,cb_state=NewState}); + loop(Opts#gen_opts{conn_pid=NewPid, + cb_state=NewState}); Error -> ct_util:unregister_connection(self()), - log("Reconnect failed. Giving up!","Reason: ~p\n", + log("Reconnect failed. Giving up!", + "Reason: ~p\n", [Error]) end; false -> @@ -338,7 +349,8 @@ loop(Opts) -> Opts#gen_opts.cb_state), return(From,ok), ok; - {{retry,{Error,_Name,CPid,_Msg}}, From} when CPid == Opts#gen_opts.conn_pid -> + {{retry,{Error,_Name,CPid,_Msg}}, From} when + CPid == Opts#gen_opts.conn_pid -> %% only retry if failure is because of a reconnection Return = case Error of {error,_} -> Error; @@ -347,12 +359,16 @@ loop(Opts) -> return(From, Return), loop(Opts); {{retry,{_Error,_Name,_CPid,Msg}}, From} -> - log("Rerunning command","Connection reestablished. Rerunning command...",[]), + log("Rerunning command","Connection reestablished. " + "Rerunning command...",[]), {Return,NewState} = (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state), return(From, Return), loop(Opts#gen_opts{cb_state=NewState}); - {Msg,From={Pid,_Ref}} when is_pid(Pid), Opts#gen_opts.old==true -> + {get_conn_pid, From} -> + return(From, Opts#gen_opts.conn_pid), + loop(Opts); + {Msg, From={Pid,_Ref}} when is_pid(Pid), Opts#gen_opts.old==true -> {Return,NewState} = (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state), return(From, Return), @@ -372,7 +388,8 @@ loop(Opts) -> return(From,Reply) end; Msg when Opts#gen_opts.forward==true -> - case (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state) of + case (Opts#gen_opts.callback):handle_msg(Msg, + Opts#gen_opts.cb_state) of {noreply,NewState} -> loop(Opts#gen_opts{cb_state=NewState}); {stop,NewState} -> diff --git a/lib/common_test/src/ct_groups.erl b/lib/common_test/src/ct_groups.erl index 74ab5e5439..14a8aab881 100644 --- a/lib/common_test/src/ct_groups.erl +++ b/lib/common_test/src/ct_groups.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2012. All Rights Reserved. +%% Copyright Ericsson AB 2004-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -462,7 +462,7 @@ make_conf(Mod, Name, Props, TestSpec) -> false -> ct_logs:log("TEST INFO", "init_per_group/2 and " "end_per_group/2 missing for group " - "~p in ~p, using default.", + "~w in ~w, using default.", [Name,Mod]), {{ct_framework,init_per_group}, {ct_framework,end_per_group}, diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl index 1bcc63738e..3d87a82e24 100644 --- a/lib/common_test/src/ct_hooks.erl +++ b/lib/common_test/src/ct_hooks.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2012. All Rights Reserved. +%% Copyright Ericsson AB 2004-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -362,11 +362,11 @@ catch_apply(M,F,A, Default) -> [{M,F,A,_}|_] when Reason == undef -> Default; Trace -> - ct_logs:log("Suite Hook","Call to CTH failed: ~p:~p", + ct_logs:log("Suite Hook","Call to CTH failed: ~w:~p", [error,{Reason,Trace}]), throw({error_in_cth_call, lists:flatten( - io_lib:format("~p:~p/~p CTH call failed", + io_lib:format("~w:~w/~w CTH call failed", [M,F,length(A)]))}) end end. diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl index 0b7a8bb075..f5355bfefe 100644 --- a/lib/common_test/src/ct_logs.erl +++ b/lib/common_test/src/ct_logs.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -35,12 +35,14 @@ -export([add_external_logs/1, add_link/3]). -export([make_last_run_index/0]). -export([make_all_suites_index/1,make_all_runs_index/1]). --export([get_ts_html_wrapper/4]). +-export([get_ts_html_wrapper/5]). -export([xhtml/2, locate_priv_file/1, make_relative/1]). -export([insert_javascript/1]). +-export([uri/1]). %% Logging stuff directly from testcase --export([tc_log/3, tc_log/4, tc_log_async/3, tc_print/3, tc_print/4, +-export([tc_log/3, tc_log/4, tc_log/5, tc_log_async/3, tc_log_async/5, + tc_print/3, tc_print/4, tc_pal/3, tc_pal/4, ct_log/3, basic_html/0]). %% Simulate logger process for use without ct environment running @@ -58,6 +60,7 @@ -define(all_runs_name, "all_runs.html"). -define(index_name, "index.html"). -define(totals_name, "totals.info"). +-define(log_cache_name, "ct_log_cache"). -define(table_color1,"#ADD8E6"). -define(table_color2,"#E4F0FE"). @@ -67,6 +70,10 @@ -define(abs(Name), filename:absname(Name)). +-record(log_cache, {version, + all_runs = [], + tests = []}). + %%%----------------------------------------------------------------- %%% @spec init(Mode) -> Result %%% Mode = normal | interactive @@ -92,14 +99,25 @@ init(Mode, Verbosity) -> exit({could_not_start_process,?MODULE,Reason}) end. -make_dirname({{YY,MM,DD},{H,M,S}}) -> - io_lib:format(logdir_node_prefix()++".~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w", - [YY,MM,DD,H,M,S]). - +date2str({{YY,MM,DD},{H,M,S}}) -> + lists:flatten(io_lib:format("~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w", + [YY,MM,DD,H,M,S])). logdir_prefix() -> "ct_run". logdir_node_prefix() -> - logdir_prefix()++"."++atom_to_list(node()). + logdir_prefix() ++ "." ++ atom_to_list(node()). + +make_dirname(DateTime) -> + logdir_node_prefix() ++ "." ++ date2str(DateTime). + +datestr_from_dirname([Y1,Y2,Y3,Y4,$-,Mo1,Mo2,$-,D1,D2,$_, + H1,H2,$.,M1,M2,$.,S1,S2 | _]) -> + [Y1,Y2,Y3,Y4,$-,Mo1,Mo2,$-,D1,D2,$_, + H1,H2,$.,M1,M2,$.,S1,S2]; +datestr_from_dirname([_Ch | Rest]) -> + datestr_from_dirname(Rest); +datestr_from_dirname([]) -> + "". %%%----------------------------------------------------------------- %%% @spec close(Info, StartDir) -> ok @@ -107,8 +125,21 @@ logdir_node_prefix() -> %%% @doc Create index pages with test results and close the CT Log %%% (tool-internal use only). close(Info, StartDir) -> - make_last_run_index(), - + %% close executes on the ct_util process, not on the logger process + %% so we need to use a local copy of the log cache data + LogCacheBin = make_last_run_index(), + put(ct_log_cache,LogCacheBin), + Cache2File = fun() -> + case get(ct_log_cache) of + undefined -> + ok; + CacheBin -> + %% save final version of the log cache to file + file:write_file(?log_cache_name,CacheBin), + put(ct_log_cache,undefined) + end + end, + ct_event:notify(#event{name=stop_logging,node=node(),data=[]}), case whereis(?MODULE) of @@ -131,11 +162,13 @@ close(Info, StartDir) -> io:format("Warning! Cleanup failed: ~p~n", [Error]) end, make_all_suites_index(stop), - make_all_runs_index(stop); + make_all_runs_index(stop), + Cache2File(); true -> file:set_cwd(".."), make_all_suites_index(stop), make_all_runs_index(stop), + Cache2File(), case ct_util:get_profile_data(browser, StartDir) of undefined -> ok; @@ -167,12 +200,19 @@ clear_stylesheet(TC) -> %%%----------------------------------------------------------------- %%% @spec get_log_dir() -> {ok,Dir} | {error,Reason} get_log_dir() -> - call({get_log_dir,false}). + get_log_dir(false). %%%----------------------------------------------------------------- %%% @spec get_log_dir(ReturnAbsName) -> {ok,Dir} | {error,Reason} get_log_dir(ReturnAbsName) -> - call({get_log_dir,ReturnAbsName}). + case call({get_log_dir,ReturnAbsName}) of + {error,does_not_exist} when ReturnAbsName == true -> + {ok,filename:absname(".")}; + {error,does_not_exist} -> + {ok,"."}; + Result -> + Result + end. %%%----------------------------------------------------------------- %%% make_last_run_index() -> ok @@ -307,8 +347,8 @@ end_log() -> %%% calling test suite.</p> add_external_logs(Logs) -> start_log("External Logs"), - [cont_log("<a href=~p>~s</a>\n", - [filename:join("log_private",Log),Log]) || Log <- Logs], + [cont_log("<a href=\"~ts\">~ts</a>\n", + [uri(filename:join("log_private",Log)),Log]) || Log <- Logs], end_log(). %%%----------------------------------------------------------------- @@ -320,8 +360,8 @@ add_external_logs(Logs) -> %%% @doc Print a link to a given file stored in the priv_dir of the %%% calling test suite. add_link(Heading,File,Type) -> - log(Heading,"<a href=~p type=~p>~s</a>\n", - [filename:join("log_private",File),Type,File]). + log(Heading,"<a href=\"~ts\" type=~p>~ts</a>\n", + [uri(filename:join("log_private",File)),Type,File]). %%%----------------------------------------------------------------- @@ -332,8 +372,15 @@ tc_log(Category,Format,Args) -> %%%----------------------------------------------------------------- %%% @spec tc_log(Category,Importance,Format,Args) -> ok +%%% @equiv tc_log(Category,Importance,"User",Format,Args) +tc_log(Category,Importance,Format,Args) -> + tc_log(Category,Importance,"User",Format,Args). + +%%%----------------------------------------------------------------- +%%% @spec tc_log(Category,Importance,Printer,Format,Args) -> ok %%% Category = atom() %%% Importance = integer() +%%% Printer = string() %%% Format = string() %%% Args = list() %%% @@ -342,9 +389,6 @@ tc_log(Category,Format,Args) -> %%% <p>This function is called by <code>ct</code> when logging %%% stuff directly from a testcase (i.e. not from within the CT %%% framework).</p> -tc_log(Category,Importance,Format,Args) -> - tc_log(Category,Importance,"User",Format,Args). - tc_log(Category,Importance,Printer,Format,Args) -> cast({log,sync,self(),group_leader(),Category,Importance, [{div_header(Category,Printer),[]}, @@ -354,14 +398,15 @@ tc_log(Category,Importance,Printer,Format,Args) -> %%%----------------------------------------------------------------- %%% @spec tc_log_async(Category,Format,Args) -> ok -%%% @equiv tc_log_async(Category,?STD_IMPORTANCE,Format,Args) +%%% @equiv tc_log_async(Category,?STD_IMPORTANCE,"User",Format,Args) tc_log_async(Category,Format,Args) -> - tc_log_async(Category,?STD_IMPORTANCE,Format,Args). + tc_log_async(Category,?STD_IMPORTANCE,"User",Format,Args). %%%----------------------------------------------------------------- %%% @spec tc_log_async(Category,Importance,Format,Args) -> ok %%% Category = atom() %%% Importance = integer() +%%% Printer = string() %%% Format = string() %%% Args = list() %%% @@ -372,9 +417,9 @@ tc_log_async(Category,Format,Args) -> %%% to avoid deadlocks when e.g. the hook that handles SASL printouts %%% prints to the test case log file at the same time test server %%% asks ct_logs for an html wrapper.</p> -tc_log_async(Category,Importance,Format,Args) -> +tc_log_async(Category,Importance,Printer,Format,Args) -> cast({log,async,self(),group_leader(),Category,Importance, - [{div_header(Category),[]}, + [{div_header(Category,Printer),[]}, {Format,Args}, {div_footer(),[]}]}), ok. @@ -396,9 +441,9 @@ tc_print(Category,Format,Args) -> %%% <p>This function is called by <code>ct</code> when printing %%% stuff from a testcase on the user console.</p> tc_print(Category,Importance,Format,Args) -> - VLvl = case ct_util:get_testdata({verbosity,Category}) of + VLvl = case ct_util:get_verbosity(Category) of undefined -> - ct_util:get_testdata({verbosity,'$unspecified'}); + ct_util:get_verbosity('$unspecified'); {error,bad_invocation} -> ?MAX_VERBOSITY; Val -> @@ -469,7 +514,7 @@ ct_log(Category,Format,Args) -> %%%================================================================= %%% Internal functions int_header() -> - "<div class=\"ct_internal\"><b>*** CT ~s *** ~s</b>". + "<div class=\"ct_internal\"><b>*** CT ~s *** ~ts</b>". int_footer() -> "</div>". @@ -514,7 +559,6 @@ log_timestamp({MS,S,US}) -> logger(Parent, Mode, Verbosity) -> register(?MODULE,self()), - %%! Below is a temporary workaround for the limitation of %%! max one test run per second. %%! ---> @@ -560,9 +604,10 @@ logger(Parent, Mode, Verbosity) -> ok -> case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of {error,Src2,Dest2,Reason2} -> - io:format(user, "ERROR! "++ - "Priv file ~p could not be copied to ~p. "++ - "Reason: ~p~n", + io:format(user, + "ERROR! "++ + "Priv file ~p could not be copied to ~p. " + ++"Reason: ~p~n", [Src2,Dest2,Reason2]), exit({priv_file_error,Dest2}); ok -> @@ -638,20 +683,21 @@ logger_loop(State) -> case erlang:is_process_alive(TCGL) of true -> State1 = print_to_log(SyncOrAsync, Pid, + Category, TCGL, List, State), logger_loop(State1#logger_state{ tc_groupleaders = TCGLs}); false -> %% Group leader is dead, so write to the - %% CtLog instead - Fd = State#logger_state.ct_log_fd, - [begin io:format(Fd,Str,Args), - io:nl(Fd) end || {Str,Args} <- List], + %% CtLog or unexpected_io log instead + unexpected_io(Pid,Category,List,State), logger_loop(State) end; - {ct_log,Fd,TCGLs} -> - [begin io:format(Fd,Str,Args),io:nl(Fd) end || - {Str,Args} <- List], + {ct_log,_Fd,TCGLs} -> + %% If category is ct_internal then write + %% to ct_log, else write to unexpected_io + %% log + unexpected_io(Pid,Category,List,State), logger_loop(State#logger_state{ tc_groupleaders = TCGLs}) end; @@ -685,14 +731,14 @@ logger_loop(State) -> logger_loop(State); {make_last_run_index,From} -> make_last_run_index(State#logger_state.start_time), - return(From,filename:basename(State#logger_state.log_dir)), + return(From,get(ct_log_cache)), logger_loop(State); {set_stylesheet,_,SSFile} when State#logger_state.stylesheet == SSFile -> logger_loop(State); {set_stylesheet,TC,SSFile} -> Fd = State#logger_state.ct_log_fd, - io:format(Fd, "~p loading external style sheet: ~s~n", + io:format(Fd, "~p loading external style sheet: ~ts~n", [TC,SSFile]), logger_loop(State#logger_state{stylesheet = SSFile}); {clear_stylesheet,_} when State#logger_state.stylesheet == undefined -> @@ -745,27 +791,32 @@ create_io_fun(FromPid, State) -> end end. -print_to_log(sync, FromPid, TCGL, List, State) -> - IoFun = create_io_fun(FromPid, State), +print_to_log(sync, FromPid, Category, TCGL, List, State) -> %% in some situations (exceptions), the printout is made from the %% test server IO process and there's no valid group leader to send to - IoProc = if FromPid /= TCGL -> TCGL; - true -> State#logger_state.ct_log_fd - end, - io:format(IoProc, "~s", [lists:foldl(IoFun, [], List)]), + if FromPid /= TCGL -> + IoFun = create_io_fun(FromPid, State), + io:format(TCGL,"~ts", [lists:foldl(IoFun, [], List)]); + true -> + unexpected_io(FromPid,Category,List,State) + end, State; -print_to_log(async, FromPid, TCGL, List, State) -> - IoFun = create_io_fun(FromPid, State), +print_to_log(async, FromPid, Category, TCGL, List, State) -> %% in some situations (exceptions), the printout is made from the %% test server IO process and there's no valid group leader to send to - IoProc = if FromPid /= TCGL -> TCGL; - true -> State#logger_state.ct_log_fd - end, - Printer = fun() -> - test_server:permit_io(IoProc, self()), - io:format(IoProc, "~s", [lists:foldl(IoFun, [], List)]) - end, + Printer = + if FromPid /= TCGL -> + IoFun = create_io_fun(FromPid, State), + fun() -> + test_server:permit_io(TCGL, self()), + io:format(TCGL, "~ts", [lists:foldl(IoFun, [], List)]) + end; + true -> + fun() -> + unexpected_io(FromPid,Category,List,State) + end + end, case State#logger_state.async_print_jobs of [] -> {_Pid,Ref} = spawn_monitor(Printer), @@ -868,7 +919,7 @@ set_evmgr_gl(GL) -> end. open_ctlog() -> - {ok,Fd} = file:open(?ct_log_name,[write]), + {ok,Fd} = file:open(?ct_log_name,[write,{encoding,utf8}]), io:format(Fd, header("Common Test Framework Log", {[],[1,2],[]}), []), case file:consult(ct_run:variables_file_name("../")) of {ok,Vars} -> @@ -878,7 +929,7 @@ open_ctlog() -> Dir = filename:dirname(Cwd), Variables = ct_run:variables_file_name(Dir), io:format(Fd, - "Can not read the file \'~s\' Reason: ~w\n" + "Can not read the file \'~ts\' Reason: ~w\n" "No configuration found for test!!\n", [Variables,Reason]) end, @@ -904,7 +955,7 @@ print_style(Fd,undefined) -> print_style(Fd,StyleSheet) -> case file:read_file(StyleSheet) of {ok,Bin} -> - Str = binary_to_list(Bin), + Str = b2s(Bin,encoding(StyleSheet)), Pos0 = case string:str(Str,"<style>") of 0 -> string:str(Str,"<STYLE>"); N0 -> N0 @@ -919,9 +970,9 @@ print_style(Fd,StyleSheet) -> print_style_error(Fd,StyleSheet,missing_style_end_tag); Pos0 /= 0 -> Style = string:sub_string(Str,Pos0,Pos1+7), - io:format(Fd,"~s\n",[Style]); + io:format(Fd,"~ts\n",[Style]); Pos0 == 0 -> - io:format(Fd,"<style>~s</style>\n",[Str]) + io:format(Fd,"<style>~ts</style>\n",[Str]) end; {error,Reason} -> print_style_error(Fd,StyleSheet,Reason) @@ -934,45 +985,42 @@ print_style(Fd,StyleSheet) -> %% [StyleSheet]). print_style_error(Fd,StyleSheet,Reason) -> - io:format(Fd,"\n<!-- Failed to load stylesheet ~s: ~p -->\n", + io:format(Fd,"\n<!-- Failed to load stylesheet ~ts: ~p -->\n", [StyleSheet,Reason]), print_style(Fd,undefined). close_ctlog(Fd) -> - io:format(Fd,"\n</pre>\n",[]), - io:format(Fd,footer(),[]), + io:format(Fd, "\n</pre>\n", []), + io:format(Fd, [xhtml("<br><br>\n", "<br /><br />\n") | footer()], []), file:close(Fd). - %%%----------------------------------------------------------------- %%% Make an index page for the last run make_last_run_index(StartTime) -> IndexName = ?index_name, AbsIndexName = ?abs(IndexName), - case catch make_last_run_index1(StartTime,IndexName) of - {'EXIT', Reason} -> - io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), - io:format("~p~n", [Reason]), - {error, Reason}; - {error, Reason} -> - io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), - io:format("~p~n", [Reason]), - {error, Reason}; - ok -> -% io:put_chars("done\n"), - ok; - Err -> - io:format("Unknown internal error while updating ~s. " - "Please report.\n(Err: ~p, ID: 1)", - [AbsIndexName,Err]), - {error, Err} - end. + Result = + case catch make_last_run_index1(StartTime,IndexName) of + {'EXIT', Reason} -> + io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), + io:format("~p~n", [Reason]), + {error, Reason}; + {error, Reason} -> + io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), + io:format("~p~n", [Reason]), + {error, Reason}; + ok -> + ok; + Err -> + io:format("Unknown internal error while updating ~ts. " + "Please report.\n(Err: ~p, ID: 1)", + [AbsIndexName,Err]), + {error, Err} + end, + Result. make_last_run_index1(StartTime,IndexName) -> - %% this manoeuvre is to ensure the tests get logged - %% in correct order of time (the 1 sec resolution - %% of the dirnames may be too big) Logs1 = case filelib:wildcard([$*|?logdir_ext]) of [Log] -> % first test @@ -1000,8 +1048,9 @@ make_last_run_index1(StartTime,IndexName) -> 0, 0, 0, 0, 0, Missing), %% write current Totals to file, later to be used in all_runs log write_totals_file(?totals_name,Label,Logs1,Totals), - Index = [Index0|index_footer()], - case force_write_file(IndexName, Index) of + Index = [Index0|last_run_index_footer()], + + case force_write_file(IndexName, unicode:characters_to_binary(Index)) of ok -> ok; {error, Reason} -> @@ -1039,22 +1088,26 @@ make_last_run_index([Name|Rest], Result, TotSucc, TotFail, TotNotBuilt1, Missing) end; -make_last_run_index([], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, _) -> - {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, false)], +make_last_run_index([], Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt, _) -> + {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt, false)], {TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}}. make_last_run_index1(SuiteName, [LogDir | LogDirs], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, Missing) -> - case make_one_index_entry(SuiteName, LogDir, "-", false, Missing) of - {Result1,Succ,Fail,USkip,ASkip,NotBuilt} -> + case make_one_index_entry(SuiteName, LogDir, "-", false, + Missing, undefined) of + {Result1,Succ,Fail,USkip,ASkip,NotBuilt,_URIs1} -> %% for backwards compatibility AutoSkip1 = case catch AutoSkip+ASkip of {'EXIT',_} -> undefined; Res -> Res end, - make_last_run_index1(SuiteName, LogDirs, [Result|Result1], TotSucc+Succ, - TotFail+Fail, UserSkip+USkip, AutoSkip1, - TotNotBuilt+NotBuilt, Missing); + make_last_run_index1(SuiteName, LogDirs, [Result|Result1], + TotSucc+Succ, + TotFail+Fail, UserSkip+USkip, AutoSkip1, + TotNotBuilt+NotBuilt, Missing); error -> make_last_run_index1(SuiteName, LogDirs, Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, Missing) @@ -1063,35 +1116,49 @@ make_last_run_index1(_, [], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, _) -> {Result,TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}. -make_one_index_entry(SuiteName, LogDir, Label, All, Missing) -> +make_one_index_entry(SuiteName, LogDir, Label, All, Missing, URIs) -> case count_cases(LogDir) of {Succ,Fail,UserSkip,AutoSkip} -> NotBuilt = not_built(SuiteName, LogDir, All, Missing), - NewResult = make_one_index_entry1(SuiteName, LogDir, Label, Succ, Fail, - UserSkip, AutoSkip, NotBuilt, All, - normal), - {NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt}; + {NewResult,URIs1} = make_one_index_entry1(SuiteName, LogDir, Label, + Succ, Fail, + UserSkip, AutoSkip, + NotBuilt, All, + normal, URIs), + {NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt,URIs1}; error -> error end. make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, - NotBuilt, All, Mode) -> + NotBuilt, All, Mode, URIs) -> LogFile = filename:join(Link, ?suitelog_name ++ ".html"), + CtRunDir = filename:dirname(filename:dirname(Link)), + CrashDumpName = SuiteName ++ "_erl_crash.dump", + + URIs1 = {CtRunLogURI,LogFileURI,CrashDumpURI} = + case URIs of + undefined -> + {uri(filename:join(CtRunDir,?ct_log_name)), + uri(LogFile), + uri(CrashDumpName)}; + _ -> + URIs + end, + CrashDumpLink = case Mode of - cached -> + temp -> ""; normal -> - CrashDumpName = SuiteName ++ "_erl_crash.dump", case filelib:is_file(CrashDumpName) of true -> - [" <a href=\"", CrashDumpName, + [" <a href=\"", CrashDumpURI, "\">(CrashDump)</a>"]; false -> "" end end, - CtRunDir = filename:dirname(filename:dirname(Link)), + {Lbl,Timestamp,Node,AllInfo} = case All of {true,OldRuns} -> @@ -1100,7 +1167,9 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, 0 -> "-"; _ -> NodeOrDate end, + TS = timestamp(CtRunDir), + N = xhtml(["<td align=right><font size=\"-1\">",Node1, "</font></td>\n"], ["<td align=right>",Node1,"</td>\n"]), @@ -1109,27 +1178,31 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, ["<td align=center><b>",Label,"</b></td>\n"]), T = xhtml(["<td><font size=\"-1\">",TS,"</font></td>\n"], ["<td>",TS,"</td>\n"]), - CtLogFile = filename:join(CtRunDir,?ct_log_name), + OldRunsLink = case OldRuns of [] -> "none"; _ -> "<a href=\""++?all_runs_name++"\">Old Runs</a>" end, - A = xhtml(["<td><font size=\"-1\"><a href=\"",CtLogFile, + + A = xhtml(["<td><font size=\"-1\"><a href=\"",CtRunLogURI, "\">CT Log</a></font></td>\n", - "<td><font size=\"-1\">",OldRunsLink,"</font></td>\n"], - ["<td><a href=\"",CtLogFile,"\">CT Log</a></td>\n", + "<td><font size=\"-1\">",OldRunsLink, + "</font></td>\n"], + ["<td><a href=\"",CtRunLogURI, + "\">CT Log</a></td>\n", "<td>",OldRunsLink,"</td>\n"]), {L,T,N,A}; false -> {"","","",""} end, + NotBuiltStr = if NotBuilt == 0 -> ["<td align=right>",integer_to_list(NotBuilt),"</td>\n"]; true -> - ["<td align=right><a href=\"",filename:join(CtRunDir,?ct_log_name),"\">", - integer_to_list(NotBuilt),"</a></td>\n"] + ["<td align=right><a href=\"",CtRunLogURI,"\">", + integer_to_list(NotBuilt),"</a></td>\n"] end, FailStr = if Fail > 0 -> @@ -1148,17 +1221,17 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, end, {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr} end, - [xhtml("<tr valign=top>\n", - ["<tr class=\"",odd_or_even(),"\">\n"]), - xhtml("<td><font size=\"-1\"><a href=\"", "<td><a href=\""), - LogFile,"\">",SuiteName,"</a>", CrashDumpLink, - xhtml("</font></td>\n", "</td>\n"), - Lbl, Timestamp, - "<td align=right>",integer_to_list(Success),"</td>\n", - "<td align=right>",FailStr,"</td>\n", - "<td align=right>",integer_to_list(AllSkip), - " (",UserSkipStr,"/",AutoSkipStr,")</td>\n", - NotBuiltStr, Node, AllInfo, "</tr>\n"]. + {[xhtml("<tr valign=top>\n", + ["<tr class=\"",odd_or_even(),"\">\n"]), + xhtml("<td><font size=\"-1\"><a href=\"", "<td><a href=\""), + LogFileURI,"\">",SuiteName,"</a>", CrashDumpLink, + xhtml("</font></td>\n", "</td>\n"), + Lbl, Timestamp, + "<td align=right>",integer_to_list(Success),"</td>\n", + "<td align=right>",FailStr,"</td>\n", + "<td align=right>",integer_to_list(AllSkip), + " (",UserSkipStr,"/",AutoSkipStr,")</td>\n", + NotBuiltStr, Node, AllInfo, "</tr>\n"], URIs1}. total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) -> {Label,TimestampCell,AllInfo} = @@ -1364,8 +1437,9 @@ header1(Title, SubTitle, TableCols) -> "<head>\n", "<title>" ++ Title ++ " " ++ SubTitle ++ "</title>\n", "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n", xhtml("", - ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">\n"]), + ["<link rel=\"stylesheet\" href=\"",uri(CSSFile),"\" type=\"text/css\">\n"]), xhtml("", ["<script type=\"text/javascript\" src=\"",JQueryFile, "\"></script>\n"]), @@ -1383,17 +1457,30 @@ header1(Title, SubTitle, TableCols) -> "</center>\n", SubTitleHTML,"\n"]. -index_footer() -> - ["</table>\n" +last_run_index_footer() -> + AllRuns = filename:join("../",?all_runs_name), + TestIndex = filename:join("../",?index_name), + ["</table>\n", + xhtml("<br><hr><p>\n", "<br /><hr /><p>\n"), + "<a href=\"", uri(AllRuns), + "\">Test run history\n</a> | ", + "<a href=\"", uri(TestIndex), + "\">Top level test index\n</a>\n</p>\n", "</center>\n" | footer()]. +all_suites_index_footer() -> + ["</table>\n", + "</center>\n", + xhtml("<br><br>\n", "<br /><br />\n") | footer()]. + all_runs_index_footer() -> - ["</tbody>\n</table>\n" - "</center>\n" | footer()]. + ["</tbody>\n</table>\n", + "</center>\n", + xhtml("<br><br>\n", "<br /><br />\n") | footer()]. footer() -> ["<center>\n", - xhtml("<br><br>\n<hr>\n", "<br /><br />\n"), + xhtml("<hr>\n", ""), xhtml("<p><font size=\"-1\">\n", "<div class=\"copyright\">"), "Copyright © ", year(), " <a href=\"http://www.erlang.org\">Open Telecom Platform</a>", @@ -1405,7 +1492,6 @@ footer() -> "</body>\n" "</html>\n"]. - body_tag() -> CTPath = code:lib_dir(common_test), TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"), @@ -1462,7 +1548,7 @@ count_cases(Dir) -> LogFile = filename:join(Dir, ?suitelog_name), case file:read_file(LogFile) of {ok, Bin} -> - case count_cases1(binary_to_list(Bin), + case count_cases1(b2s(Bin), {undefined,undefined,undefined,undefined}) of {error,not_complete} -> %% The test is not complete - dont write summary @@ -1472,8 +1558,9 @@ count_cases(Dir) -> write_summary(SumFile, Summary), Summary end; - {error, _Reason} -> - io:format("\nFailed to read ~p (skipped)\n", [LogFile]), + {error, Reason} -> + io:format("\nFailed to read ~p: ~p (skipped)\n", + [LogFile,Reason]), error end end. @@ -1557,7 +1644,7 @@ config_table1([{Key,Value}|Vars]) -> "<td><pre>",io_lib:format("~p",[Value]),"</pre></td></tr>\n"], ["<tr class=\"", odd_or_even(), "\">\n", "<td>", atom_to_list(Key), "</td>\n", - "<td>", io_lib:format("~p",[Value]), "</td>\n</tr>\n"]) | + "<td>", io_lib:format("~p",[Value]), "</td>\n</tr>\n"]) | config_table1(Vars)]; config_table1([]) -> ["</tbody>\n</table>\n"]. @@ -1570,137 +1657,313 @@ make_all_runs_index(When) -> if When == start -> ok; true -> io:put_chars("Updating " ++ AbsName ++ "... ") end, + + %% check if log cache should be used, and if it exists + UseCache = + if When == refresh -> + save_only; + true -> + case application:get_env(common_test, disable_log_cache) of + {ok,true} -> + disabled; + _ -> + case get(ct_log_cache) of + undefined -> + file:read_file(?log_cache_name); + LogCacheBin -> + {ok,LogCacheBin} + end + end + end, + Dirs = filelib:wildcard(logdir_prefix()++"*.*"), DirsSorted = (catch sort_all_runs(Dirs)), - Header = all_runs_header(), - Index = [runentry(Dir) || Dir <- DirsSorted], - Result = file:write_file(AbsName,Header++Index++ - all_runs_index_footer()), + + LogCacheInfo = get_cache_data(UseCache), + + Result = + case LogCacheInfo of + {ok,LogCache} -> + %% use the log cache file to generate the index + make_all_runs_from_cache(AbsName,DirsSorted,LogCache); + + _WhyNot -> + %% no cache file exists (or feature has been disabled) + Header = all_runs_header(), + GetLogResult = + fun(Dir,{RunData,LogTxt}) -> + {Tot,XHTML,IxLink} = runentry(Dir, + undefined, + undefined), + {[{Dir,Tot,IxLink}|RunData],[XHTML|LogTxt]} + end, + {AllRunsData,Index} = + lists:foldr(GetLogResult,{[],[]},DirsSorted), + + %% update cache with result unless the cache is disabled + if UseCache == disabled -> ok; + true -> update_all_runs_in_cache(AllRunsData) + end, + %% write all_runs log file + ok = file:write_file(AbsName, + unicode:characters_to_binary( + Header++Index++ + all_runs_index_footer())) + end, + notify_and_unlock_file(AbsName), if When == start -> ok; true -> io:put_chars("done\n") end, - notify_and_unlock_file(AbsName), Result. +make_all_runs_from_cache(AbsName, Dirs, LogCache) -> + Header = all_runs_header(), + + %% Note that both Dirs and the cache is sorted! + AllRunsDirs = dir_diff_all_runs(Dirs, LogCache), + + GetLogResult = + fun({Dir,no_test_data,IxLink},{RunData,LogTxt}) -> + {Tot,XHTML,_} = runentry(Dir,undefined,IxLink), + {[{Dir,Tot,IxLink}|RunData],[XHTML|LogTxt]}; + ({Dir,CachedTotals,IxLink},{RunData,LogTxt}) -> + %% create log entry using cached data + {Tot,XHTML,_} = runentry(Dir,CachedTotals,IxLink), + {[{Dir,Tot,IxLink}|RunData],[XHTML|LogTxt]}; + (Dir,{RunData,LogTxt}) -> + %% create log entry from scratch + {Tot,XHTML,IxLink} = runentry(Dir,undefined,undefined), + {[{Dir,Tot,IxLink}|RunData],[XHTML|LogTxt]} + end, + {AllRunsData,Index} = lists:foldr(GetLogResult,{[],[]},AllRunsDirs), + %% update cache with result + update_all_runs_in_cache(AllRunsData,LogCache), + %% write all_runs log file + ok = file:write_file(AbsName, + unicode:characters_to_binary( + Header++Index++ + all_runs_index_footer())). + +update_all_runs_in_cache(AllRunsData) -> + case get(ct_log_cache) of + undefined -> + LogCache = #log_cache{version = cache_vsn(), + all_runs = AllRunsData}, + case {self(),whereis(?MODULE)} of + {_Pid,_Pid} -> + %% save the cache in RAM so it doesn't have to be + %% read from file as long as this logger process is alive + put(ct_log_cache,term_to_binary(LogCache)); + _ -> + file:write_file(?log_cache_name,term_to_binary(LogCache)) + end; + SavedLogCache -> + update_all_runs_in_cache(AllRunsData,binary_to_term(SavedLogCache)) + end. + +update_all_runs_in_cache(AllRunsData, LogCache) -> + LogCache1 = LogCache#log_cache{all_runs = AllRunsData}, + case {self(),whereis(?MODULE)} of + {_Pid,_Pid} -> + %% save the cache in RAM so it doesn't have to be + %% read from file as long as this logger process is alive + put(ct_log_cache,term_to_binary(LogCache1)); + _ -> + file:write_file(?log_cache_name,term_to_binary(LogCache1)) + end. + sort_all_runs(Dirs) -> %% sort on time string, always last and on the format: %% "YYYY-MM-DD_HH.MM.SS" - KeyList = - lists:map(fun(Dir) -> - case lists:reverse(string:tokens(Dir,[$.,$_])) of - [SS,MM,HH,Date|_] -> - {{Date,HH,MM,SS},Dir}; - _Other -> - throw(Dirs) - end - end,Dirs), - lists:reverse(lists:map(fun({_,Dir}) -> - Dir - end,lists:keysort(1,KeyList))). + lists:sort(fun(Dir1,Dir2) -> + [SS1,MM1,HH1,Date1|_] = + lists:reverse(string:tokens(Dir1,[$.,$_])), + [SS2,MM2,HH2,Date2|_] = + lists:reverse(string:tokens(Dir2,[$.,$_])), + {Date1,HH1,MM1,SS1} > {Date2,HH2,MM2,SS2} + end, Dirs). + +dir_diff_all_runs(Dirs, LogCache) -> + case LogCache#log_cache.all_runs of + [] -> + Dirs; + Cached = [{CDir,_,_}|_] -> + AllRunsDirs = + dir_diff_all_runs(Dirs, Cached, datestr_from_dirname(CDir), []), + lists:reverse(AllRunsDirs) + end. +dir_diff_all_runs(LogDirs=[Dir|Dirs], Cached=[CElem|CElems], + LatestInCache, AllRunsDirs) -> + DirDate = datestr_from_dirname(Dir), + if DirDate > LatestInCache -> + %% Dir is a new run entry + dir_diff_all_runs(Dirs, Cached, LatestInCache, + [Dir|AllRunsDirs]); + DirDate == LatestInCache, CElems /= [] -> + %% Dir is an existing run entry + dir_diff_all_runs(Dirs, CElems, + datestr_from_dirname(element(1,hd(CElems))), + [CElem|AllRunsDirs]); + DirDate == LatestInCache, CElems == [] -> + %% we're done, Dirs must all be new + lists:reverse(Dirs)++[CElem|AllRunsDirs]; + CElems /= [] -> % DirDate < LatestInCache + %% current CDir not in Dirs, update timestamp and check next + dir_diff_all_runs(LogDirs, CElems, + datestr_from_dirname(element(1,hd(CElems))), + AllRunsDirs); + CElems == [] -> + %% we're done, LogDirs must all be new + lists:reverse(LogDirs)++AllRunsDirs + end; + +dir_diff_all_runs([], _Cached, _, AllRunsDirs) -> + AllRunsDirs. interactive_link() -> [Dir|_] = lists:reverse(filelib:wildcard(logdir_prefix()++"*.*")), CtLog = filename:join(Dir,"ctlog.html"), - Body = ["Log from last interactive run: <a href=\"",CtLog,"\">", - timestamp(Dir),"</a>"], - file:write_file("last_interactive.html",Body), - io:format("~n~nUpdated ~s\n" + Body = + [xhtml( + ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n", + "<html>\n"], + ["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n", + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n", + "<html xmlns=\"http://www.w3.org/1999/xhtml\" ", + "xml:lang=\"en\" lang=\"en\">\n"]), + "<!-- autogenerated by '"++atom_to_list(?MODULE)++"' -->\n", + "<head>\n", + "<title>Last interactive run</title>\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + "<meta http-equiv=\"content-type\" content=\"text/html; " + "charset=utf-8\">\n", + "</head>\n", + "<body>\n", + "Log from last interactive run: <a href=\"",uri(CtLog),"\">", + timestamp(Dir),"</a>", + "</body>\n", + "</html>\n" + ], + file:write_file("last_interactive.html",unicode:characters_to_binary(Body)), + io:format("~n~nUpdated ~ts\n" "Any CT activities will be logged here\n", [?abs("last_interactive.html")]). -runentry(Dir) -> +%% use if cache disabled or non-existing +runentry(Dir, undefined, _) -> TotalsFile = filename:join(Dir,?totals_name), - TotalsStr = - case read_totals_file(TotalsFile) of - {Node,Label,Logs,{TotSucc,TotFail,UserSkip,AutoSkip,NotBuilt}} -> - TotFailStr = - if TotFail > 0 -> - ["<font color=\"red\">", - integer_to_list(TotFail),"</font>"]; - true -> - integer_to_list(TotFail) - end, - {AllSkip,UserSkipStr,AutoSkipStr} = - if AutoSkip == undefined -> {UserSkip,"?","?"}; - true -> - ASStr = if AutoSkip > 0 -> - ["<font color=\"brown\">", - integer_to_list(AutoSkip),"</font>"]; - true -> integer_to_list(AutoSkip) - end, - {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr} - end, - NoOfTests = case length(Logs) of - 0 -> "-"; - N -> integer_to_list(N) - end, - StripExt = - fun(File) -> - string:sub_string(File,1, - length(File)- - length(?logdir_ext)) ++ ", " - end, - Polish = fun(S) -> case lists:reverse(S) of - [32,$,|Rev] -> lists:reverse(Rev); - [$,|Rev] -> lists:reverse(Rev); - _ -> S - end - end, - TestNames = Polish(lists:flatten(lists:map(StripExt,Logs))), - TestNamesTrunc = - if TestNames=="" -> - ""; - length(TestNames) < ?testname_width -> - TestNames; - true -> - Trunc = Polish(string:substr(TestNames,1,?testname_width-3)), - lists:flatten(io_lib:format("~s...",[Trunc])) - end, - Total = TotSucc+TotFail+AllSkip, - A = xhtml(["<td align=center><font size=\"-1\">",Node, - "</font></td>\n", - "<td align=center><font size=\"-1\"><b>",Label, - "</b></font></td>\n", - "<td align=right>",NoOfTests,"</td>\n"], - ["<td align=center>",Node,"</td>\n", - "<td align=center><b>",Label,"</b></td>\n", - "<td align=right>",NoOfTests,"</td>\n"]), - B = xhtml(["<td align=center title='",TestNames,"'><font size=\"-1\"> ", - TestNamesTrunc,"</font></td>\n"], - ["<td align=center title='",TestNames,"'> ", - TestNamesTrunc,"</td>\n"]), - C = ["<td align=right>",integer_to_list(Total),"</td>\n", - "<td align=right>",integer_to_list(TotSucc),"</td>\n", - "<td align=right>",TotFailStr,"</td>\n", - "<td align=right>",integer_to_list(AllSkip), - " (",UserSkipStr,"/",AutoSkipStr,")</td>\n", - "<td align=right>",integer_to_list(NotBuilt),"</td>\n"], - A++B++C; - _ -> - A = xhtml(["<td align=center><font size=\"-1\" color=\"red\">" - "Test data missing or corrupt</font></td>\n", - "<td align=center><font size=\"-1\">?</font></td>\n", - "<td align=right>?</td>\n"], - ["<td align=center><font color=\"red\">" - "Test data missing or corrupt</font></td>\n", - "<td align=center>?</td>\n", - "<td align=right>?</td>\n"]), - B = xhtml(["<td align=center><font size=\"-1\">?</font></td>\n"], - ["<td align=center>?</td>\n"]), - C = ["<td align=right>?</td>\n", - "<td align=right>?</td>\n", - "<td align=right>?</td>\n", - "<td align=right>?</td>\n", - "<td align=right>?</td>\n"], - A++B++C - end, - Index = filename:join(Dir,?index_name), - [xhtml("<tr>\n", ["<tr class=\"",odd_or_even(),"\">\n"]), - xhtml(["<td><font size=\"-1\"><a href=\"",Index,"\">",timestamp(Dir),"</a>", - TotalsStr,"</font></td>\n"], - ["<td><a href=\"",Index,"\">",timestamp(Dir),"</a>",TotalsStr,"</td>\n"]), - "</tr>\n"]. + Index = uri(filename:join(Dir,?index_name)), + runentry(Dir, read_totals_file(TotalsFile), Index); + +%% use cached data +runentry(Dir, Totals={Node,Label,Logs, + {TotSucc,TotFail,UserSkip,AutoSkip,NotBuilt}}, Index) -> + TotFailStr = + if TotFail > 0 -> + ["<font color=\"red\">", + integer_to_list(TotFail),"</font>"]; + true -> + integer_to_list(TotFail) + end, + {AllSkip,UserSkipStr,AutoSkipStr} = + if AutoSkip == undefined -> {UserSkip,"?","?"}; + true -> + ASStr = if AutoSkip > 0 -> + ["<font color=\"brown\">", + integer_to_list(AutoSkip), + "</font>"]; + true -> integer_to_list(AutoSkip) + end, + {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr} + end, + NoOfTests = case length(Logs) of + 0 -> "-"; + N -> integer_to_list(N) + end, + StripExt = + fun(File) -> + string:sub_string(File,1, + length(File)- + length(?logdir_ext)) ++ ", " + end, + Polish = fun(S) -> case lists:reverse(S) of + [32,$,|Rev] -> lists:reverse(Rev); + [$,|Rev] -> lists:reverse(Rev); + _ -> S + end + end, + TestNames = Polish(lists:flatten(lists:map(StripExt,Logs))), + TestNamesTrunc = + if TestNames=="" -> + ""; + length(TestNames) < ?testname_width -> + TestNames; + true -> + Trunc = Polish(string:substr(TestNames,1, + ?testname_width-3)), + lists:flatten(io_lib:format("~ts...",[Trunc])) + end, + Total = TotSucc+TotFail+AllSkip, + A = xhtml(["<td align=center><font size=\"-1\">",Node, + "</font></td>\n", + "<td align=center><font size=\"-1\"><b>",Label, + "</b></font></td>\n", + "<td align=right>",NoOfTests,"</td>\n"], + ["<td align=center>",Node,"</td>\n", + "<td align=center><b>",Label,"</b></td>\n", + "<td align=right>",NoOfTests,"</td>\n"]), + B = xhtml(["<td align=center title='",TestNames, + "'><font size=\"-1\"> ", + TestNamesTrunc,"</font></td>\n"], + ["<td align=center title='",TestNames,"'> ", + TestNamesTrunc,"</td>\n"]), + C = ["<td align=right>",integer_to_list(Total),"</td>\n", + "<td align=right>",integer_to_list(TotSucc),"</td>\n", + "<td align=right>",TotFailStr,"</td>\n", + "<td align=right>",integer_to_list(AllSkip), + " (",UserSkipStr,"/",AutoSkipStr,")</td>\n", + "<td align=right>",integer_to_list(NotBuilt),"</td>\n"], + TotalsStr = A++B++C, + + XHTML = [xhtml("<tr>\n", ["<tr class=\"",odd_or_even(),"\">\n"]), + xhtml(["<td><font size=\"-1\"><a href=\"",Index,"\">", + timestamp(Dir),"</a>", + TotalsStr,"</font></td>\n"], + ["<td><a href=\"",Index,"\">",timestamp(Dir),"</a>",TotalsStr, + "</td>\n"]), + "</tr>\n"], + {Totals,XHTML,Index}; + +%% handle missing or corrupt data (missing e.g. if the test is in progress) +runentry(Dir, _, _) -> + A = xhtml(["<td align=center><font size=\"-1\" color=\"red\">" + "Test data missing or corrupt</font></td>\n", + "<td align=center><font size=\"-1\">?</font></td>\n", + "<td align=right>?</td>\n"], + ["<td align=center><font color=\"red\">" + "Test data missing or corrupt</font></td>\n", + "<td align=center>?</td>\n", + "<td align=right>?</td>\n"]), + B = xhtml(["<td align=center><font size=\"-1\">?</font></td>\n"], + ["<td align=center>?</td>\n"]), + C = ["<td align=right>?</td>\n", + "<td align=right>?</td>\n", + "<td align=right>?</td>\n", + "<td align=right>?</td>\n", + "<td align=right>?</td>\n"], + TotalsStr = A++B++C, + + Index = uri(filename:join(Dir,?index_name)), + + XHTML = [xhtml("<tr>\n", ["<tr class=\"",odd_or_even(),"\">\n"]), + xhtml(["<td><font size=\"-1\"><a href=\"",Index,"\">", + timestamp(Dir),"</a>", + TotalsStr,"</font></td>\n"], + ["<td><a href=\"",Index,"\">",timestamp(Dir),"</a>",TotalsStr, + "</td>\n"]), + "</tr>\n"], + {no_test_data,XHTML,Index}. write_totals_file(Name,Label,Logs,Totals) -> AbsName = ?abs(Name), @@ -1727,17 +1990,19 @@ read_totals_file(Name) -> _ -> Label end, case Tot of - {_Ok,_Fail,_USkip,_ASkip,_NoBuild} -> % latest format + {_Ok,_Fail,_USkip,_ASkip,_NoBuild} -> % latest format {Node,Label1,Ls,Tot}; {TotSucc,TotFail,AllSkip,NotBuilt} -> - {Node,Label1,Ls,{TotSucc,TotFail,AllSkip,undefined,NotBuilt}} + {Node,Label1,Ls, + {TotSucc,TotFail,AllSkip,undefined,NotBuilt}} end; {Node,Ls,Tot} -> % no label found case Tot of - {_Ok,_Fail,_USkip,_ASkip,_NoBuild} -> % latest format + {_Ok,_Fail,_USkip,_ASkip,_NoBuild} -> % latest format {Node,"-",Ls,Tot}; {TotSucc,TotFail,AllSkip,NotBuilt} -> - {Node,"-",Ls,{TotSucc,TotFail,AllSkip,undefined,NotBuilt}} + {Node,"-",Ls, + {TotSucc,TotFail,AllSkip,undefined,NotBuilt}} end; %% for backwards compatibility {Ls,Tot} -> {"-",Ls,Tot}; @@ -1791,29 +2056,73 @@ timestamp(Dir) -> %% run will not show until after the final refresh. %% ------------------------------------------------------------------------- -%% Creates the top level index file. When == start | refresh. -%% A copy of the dir tree under logdir is cached as a result. +%% Creates the top level index file. When == start | stop | refresh. +%% A copy of the dir tree under logdir is saved temporarily as a result. make_all_suites_index(When) when is_atom(When) -> put(basic_html, basic_html()), AbsIndexName = ?abs(?index_name), notify_and_lock_file(AbsIndexName), + + %% check if log cache should be used, and if it exists + UseCache = + if When == refresh -> + save_only; + true -> + case application:get_env(common_test, disable_log_cache) of + {ok,true} -> + disabled; + _ -> + case get(ct_log_cache) of + undefined -> + file:read_file(?log_cache_name); + LogCacheBin -> + {ok,LogCacheBin} + end + end + end, + LogDirs = filelib:wildcard(logdir_prefix()++".*/*"++?logdir_ext), - Sorted = sort_logdirs(LogDirs, []), - Result = make_all_suites_index1(When, AbsIndexName, Sorted), - notify_and_unlock_file(AbsIndexName), - Result; -%% This updates the top level index file using cached data from -%% the initial index file creation. -make_all_suites_index(NewTestData = {_TestName,DirName}) -> + LogCacheInfo = get_cache_data(UseCache), + + Result = + case LogCacheInfo of + {ok,LogCache} -> + %% use the log cache file to generate the index + make_all_suites_index_from_cache(When,AbsIndexName, + LogDirs,LogCache); + _WhyNot -> + %% no cache file exists (or feature has been disabled) + Sorted = sort_and_filter_logdirs(LogDirs), + TempData = make_all_suites_index1(When,AbsIndexName,Sorted), + notify_and_unlock_file(AbsIndexName), + + %% save new cache file unless the feature is disabled + if UseCache == disabled -> ok; + true -> update_tests_in_cache(TempData) + end, + TempData + end, + + case Result of + Error = {error,_} -> Error; + _ -> ok + end; + +%% This updates the top level index file using data from the initial +%% index file creation, saved temporarily in a table. +make_all_suites_index(NewTestData = {_TestName,DirName}) -> put(basic_html, basic_html()), - %% AllLogDirs = [{TestName,Label,Missing,{LastLogDir,Summary},OldDirs}|...] + + %% AllLogDirs = [{TestName,Label,Missing, + %% {LastLogDir,Summary,URIs},OldDirs}|...] + {AbsIndexName,LogDirData} = ct_util:get_testdata(test_index), CtRunDirPos = length(filename:split(AbsIndexName)), CtRunDir = filename:join(lists:sublist(filename:split(DirName), CtRunDirPos)), - + Label = case read_totals_file(filename:join(CtRunDir, ?totals_name)) of {_,"-",_,_} -> "..."; {_,Lbl,_,_} -> Lbl; @@ -1821,10 +2130,10 @@ make_all_suites_index(NewTestData = {_TestName,DirName}) -> end, notify_and_lock_file(AbsIndexName), Result = - case catch make_all_suites_ix_cached(AbsIndexName, - NewTestData, - Label, - LogDirData) of + case catch make_all_suites_ix_temp(AbsIndexName, + NewTestData, + Label, + LogDirData) of {'EXIT',Reason} -> io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), io:format("~p~n", [Reason]), @@ -1836,51 +2145,224 @@ make_all_suites_index(NewTestData = {_TestName,DirName}) -> ok -> ok; Err -> - io:format("Unknown internal error while updating ~s. " + io:format("Unknown internal error while updating ~ts. " "Please report.\n(Err: ~p, ID: 1)", [AbsIndexName,Err]), {error, Err} end, - notify_and_unlock_file(AbsIndexName), + notify_and_unlock_file(AbsIndexName), Result. -sort_logdirs([Dir|Dirs],Groups) -> +make_all_suites_index_from_cache(When, AbsIndexName, LogDirs, LogCache) -> + + %% The structure of the cache: + %% + %% #log_cache{tests = {TestName,Label,Missing, + %% {LastLogDir,Summary,URIs},OldDirs} + %% } + %% Summary = {Succ,Fail,USkip,ASkip} | error + %% + + {NewAdded,OldTests} = dir_diff_tests(LogDirs,LogCache), + + LogCache1 = delete_tests_from_cache(OldTests,LogCache), + Sorted = sort_and_filter_logdirs(NewAdded, + LogCache1#log_cache.tests), + TempData = + if Sorted /= [] -> + make_all_suites_index1(When,AbsIndexName, + Sorted); + true -> + Data = LogCache1#log_cache.tests, + ct_util:set_testdata_async({test_index,{AbsIndexName, + Data}}), + Data + end, + + notify_and_unlock_file(AbsIndexName), + + update_tests_in_cache(TempData,LogCache1), + TempData. + +sort_and_filter_logdirs(NewDirs,CachedTests) when CachedTests /= [] -> + NewSorted = sort_and_filter_logdirs1(NewDirs,[]), + sort_and_filter_logdirs(NewSorted,CachedTests,[]); + +sort_and_filter_logdirs(NewDirs,_CachedTests) -> + sort_and_filter_logdirs(NewDirs). + +%% sort latest dirs found and combine them with cached entries +sort_and_filter_logdirs([{TestName,IxDirs}|Tests],CachedTests,Combined) -> + case lists:keysearch(TestName,1,CachedTests) of + {value,{TestName,_,_,{IxDir0,_,_},IxDirs0}} -> + Groups = sort_and_filter_logdirs2(TestName, + IxDirs++[IxDir0|IxDirs0], + []), + sort_and_filter_logdirs(Tests,CachedTests,Groups++Combined); + _ -> + IxDirs1 = lists:map(fun(Elem = {_,_}) -> + Elem; + (RunDir) -> + {filename:basename(RunDir),RunDir} + end, IxDirs), + sort_and_filter_logdirs(Tests,CachedTests, + [{TestName,IxDirs1}|Combined]) + end; +sort_and_filter_logdirs([],CachedTests,Combined) -> + Cached1 = lists:foldl(fun({TestName,_},Cached) -> + lists:keydelete(TestName,1,Cached) + end, CachedTests, Combined), + lists:keysort(1,sort_each_group(Combined)++Cached1). + +sort_and_filter_logdirs(Dirs) -> + sort_and_filter_logdirs1(Dirs, []). + +%% sort and filter directories (no cache) +sort_and_filter_logdirs1([Dir|Dirs],Groups) -> TestName = filename:rootname(filename:basename(Dir)), case filelib:wildcard(filename:join(Dir,"run.*")) of RunDirs = [_|_] -> - Groups1 = sort_logdirs1(TestName,RunDirs,Groups), - sort_logdirs(Dirs,Groups1); + Groups1 = sort_and_filter_logdirs2(TestName,RunDirs,Groups), + sort_and_filter_logdirs1(Dirs,Groups1); _ -> % ignore missing run directory - sort_logdirs(Dirs,Groups) + sort_and_filter_logdirs1(Dirs,Groups) end; -sort_logdirs([],Groups) -> +sort_and_filter_logdirs1([],Groups) -> lists:keysort(1,sort_each_group(Groups)). -sort_logdirs1(TestName,[RunDir|RunDirs],Groups) -> +sort_and_filter_logdirs2(TestName,[RunDir|RunDirs],Groups) -> Groups1 = insert_test(TestName,{filename:basename(RunDir),RunDir},Groups), - sort_logdirs1(TestName,RunDirs,Groups1); -sort_logdirs1(_,[],Groups) -> + sort_and_filter_logdirs2(TestName,RunDirs,Groups1); +sort_and_filter_logdirs2(_,[],Groups) -> Groups. +%% new rundir for Test found, add to (not sorted) list of prev rundirs insert_test(Test,IxDir,[{Test,IxDirs}|Groups]) -> [{Test,[IxDir|IxDirs]}|Groups]; +%% first occurance of Test insert_test(Test,IxDir,[]) -> [{Test,[IxDir]}]; insert_test(Test,IxDir,[TestDir|Groups]) -> [TestDir|insert_test(Test,IxDir,Groups)]. - + +%% sort the list of rundirs for each Test sort_each_group([{Test,IxDirs}|Groups]) -> Sorted = lists:reverse([Dir || {_,Dir} <- lists:keysort(1,IxDirs)]), - [{Test,Sorted}| sort_each_group(Groups)]; + [{Test,Sorted}|sort_each_group(Groups)]; sort_each_group([]) -> []. -make_all_suites_index1(When, AbsIndexName, AllLogDirs) -> +dir_diff_tests(LogDirs, #log_cache{tests = CachedTests}) -> + AllTestNames = + [TestName || {TestName,_,_,_,_} <- CachedTests], + dir_diff_tests(LogDirs, CachedTests, [], AllTestNames, [], []). + +dir_diff_tests([LogDir|LogDirs], CachedTests, NewAdded, DeletedTests, + ValidLast, InvalidLast) -> + TestName = filename:rootname(filename:basename(LogDir)), + Time = datestr_from_dirname(LogDir), + %% check if the test already exists in the cache + {New,DeletedTests1,ValidLast1,InvalidLast1} = + case lists:keysearch(TestName,1,CachedTests) of + {value,{_,_,_,{LastLogDir,_,_},_PrevLogDirs}} -> + LastLogTime = datestr_from_dirname(LastLogDir), + if Time > LastLogTime -> + %% this is a new test run, not in cache + {[LogDir|NewAdded], + lists:delete(TestName,DeletedTests), + ValidLast,[{TestName,LastLogDir}|InvalidLast]}; + Time == LastLogTime -> + %% this is the latest test run, already in cache + TDir = {TestName,LastLogDir}, + {NewAdded, + lists:delete(TestName,DeletedTests), + [TDir|ValidLast],InvalidLast}; + true -> + %% this is an old test run + {[], + lists:delete(TestName,DeletedTests), + ValidLast,[{TestName,LastLogDir}|InvalidLast]} + end; + _ -> + %% this is a test run for a new test, not in cache + {[LogDir|NewAdded], + DeletedTests,ValidLast,InvalidLast} + end, + dir_diff_tests(LogDirs, CachedTests, New, DeletedTests1, + ValidLast1,InvalidLast1); + +dir_diff_tests([], _CachedTests, NewAdded, DeletedTests, + ValidLast, InvalidLast) -> + %% We have to check if LastLogDir still exists or if it's been + %% deleted. InvalidLast contains all log dirs that should be deleted, + %% if not present in ValidLast. + InvalidLast1 = + lists:foldl(fun(TDir,IL) -> + case lists:member(TDir,ValidLast) of + true -> + [TD || TD <- IL, TD /= TDir]; + false -> + [TDir | [TD || TD <- IL, TD /= TDir]] + end + end, InvalidLast, InvalidLast), + + %% Collect all tests for which LastLogDir has been deleted. + DeletedTests1 = [T || {T,_} <- InvalidLast1] ++ DeletedTests, + + %% Make sure that directories for tests that are to be deleted are + %% saved in NewAdded so that tests don't disappear from the log if + %% older run dirs for them exist. + NewAdded1 = lists:map(fun({_TestName,RunDir}) -> + [TopDir,TestDir|_] = filename:split(RunDir), + filename:join(TopDir,TestDir) + end, InvalidLast1) ++ NewAdded, + + {NewAdded1,DeletedTests1}. + +delete_tests_from_cache(OldTests, LogCache=#log_cache{tests=Tests}) -> + Tests2 = lists:foldl(fun(T,Tests1) -> + lists:keydelete(T,1,Tests1) + end, Tests, OldTests), + LogCache#log_cache{tests = Tests2}. + +update_tests_in_cache(TempData) -> + case get(ct_log_cache) of + undefined -> + update_tests_in_cache(TempData,#log_cache{version = cache_vsn(), + tests=[]}); + SavedLogCache -> + update_tests_in_cache(TempData,binary_to_term(SavedLogCache)) + end. + +update_tests_in_cache(TempData,LogCache=#log_cache{tests=Tests}) -> + Cached1 = + if Tests == [] -> + []; + true -> + lists:foldl(fun({TestName,_,_,_,_},Cached) -> + lists:keydelete(TestName,1,Cached) + end, Tests, TempData) + end, + Tests1 = lists:keysort(1,TempData++Cached1), + CacheBin = term_to_binary(LogCache#log_cache{tests = Tests1}), + case {self(),whereis(?MODULE)} of + {_Pid,_Pid} -> + put(ct_log_cache,CacheBin); + _ -> + file:write_file(?log_cache_name,CacheBin) + end. + +%% +%% AllTestLogDirs = +%% [{TestName,[IxDir|IxDirs]} | ...] (non-cached), or +%% [{TestName,Label,Missing,{IxDir,Summary,URIs},IxDirs} | ...] (cached) +%% +make_all_suites_index1(When, AbsIndexName, AllTestLogDirs) -> IndexName = ?index_name, if When == start -> ok; true -> io:put_chars("Updating " ++ AbsIndexName ++ "... ") end, - case catch make_all_suites_index2(IndexName, AllLogDirs) of + case catch make_all_suites_index2(IndexName, AllTestLogDirs) of {'EXIT', Reason} -> io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), io:format("~p~n", [Reason]), @@ -1889,39 +2371,75 @@ make_all_suites_index1(When, AbsIndexName, AllLogDirs) -> io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), io:format("~p~n", [Reason]), {error, Reason}; - {ok,CacheData} -> + {ok,TempData} -> case When of start -> ct_util:set_testdata_async({test_index,{AbsIndexName, - CacheData}}), - ok; + TempData}}), + TempData; _ -> io:put_chars("done\n"), - ok + TempData end; Err -> - io:format("Unknown internal error while updating ~s. " + io:format("Unknown internal error while updating ~ts. " "Please report.\n(Err: ~p, ID: 1)", [AbsIndexName,Err]), {error, Err} end. make_all_suites_index2(IndexName, AllTestLogDirs) -> - {ok,Index0,_Totals,CacheData} = + {ok,Index0,_Totals,TempData} = make_all_suites_index3(AllTestLogDirs, all_suites_index_header(), 0, 0, 0, 0, 0, [], []), - Index = [Index0|index_footer()], - case force_write_file(IndexName, Index) of + Index = [Index0|all_suites_index_footer()], + case force_write_file(IndexName, unicode:characters_to_binary(Index)) of ok -> - {ok,CacheData}; + {ok,TempData}; {error, Reason} -> {error,{index_write_error, Reason}} end. +%% +%% AllTestLogDirs = [{TestName,Label,Missing,{LogDir,Summary,URIs},OldDirs}] +%% Summary = {Succ,Fail,UserSkip,AutoSkip} | error +%% URIs = {CtRunLogURI,LogFileURI,CrashDumpURI} | undefined +%% +%% this clause is for handling entries in the log cache +make_all_suites_index3([IxEntry = {TestName,Label,Missing, + {LastLogDir,Summary,URIs},OldDirs} | Rest], + Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, + Labels, TempData) -> + [EntryDir|_] = filename:split(LastLogDir), + Labels1 = [{EntryDir,Label}|Labels], + case Summary of + {Succ,Fail,USkip,ASkip} -> + All = {true,OldDirs}, + NotBuilt = not_built(TestName, LastLogDir, All, Missing), + + {Result1,_} = make_one_index_entry1(TestName, LastLogDir, Label, + Succ, Fail, USkip, ASkip, + NotBuilt, All, temp, URIs), + + AutoSkip1 = case catch AutoSkip+ASkip of + {'EXIT',_} -> undefined; + Res -> Res + end, + make_all_suites_index3(Rest, [Result|Result1], TotSucc+Succ, + TotFail+Fail, UserSkip+USkip, AutoSkip1, + TotNotBuilt+NotBuilt, Labels1, + [IxEntry|TempData]); + error -> + make_all_suites_index3(Rest, Result, TotSucc, TotFail, + UserSkip, AutoSkip, TotNotBuilt, Labels1, + [IxEntry|TempData]) + end; + +%% this clause is for handling non-cached directories make_all_suites_index3([{TestName,[LastLogDir|OldDirs]}|Rest], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, - Labels, CacheData) -> + Labels, TempData) -> [EntryDir|_] = filename:split(LastLogDir), Missing = case file:read_file(filename:join(EntryDir, ?missing_suites_info)) of @@ -1938,39 +2456,51 @@ make_all_suites_index3([{TestName,[LastLogDir|OldDirs]}|Rest], Lbl -> {Lbl,Labels} end, - case make_one_index_entry(TestName, LastLogDir, Label, {true,OldDirs}, Missing) of - {Result1,Succ,Fail,USkip,ASkip,NotBuilt} -> + case make_one_index_entry(TestName, LastLogDir, Label, + {true,OldDirs}, Missing, undefined) of + {Result1,Succ,Fail,USkip,ASkip,NotBuilt,URIs} -> %% for backwards compatibility AutoSkip1 = case catch AutoSkip+ASkip of {'EXIT',_} -> undefined; Res -> Res end, IxEntry = {TestName,Label,Missing, - {LastLogDir,{Succ,Fail,USkip,ASkip}},OldDirs}, + {LastLogDir,{Succ,Fail,USkip,ASkip},URIs},OldDirs}, + make_all_suites_index3(Rest, [Result|Result1], TotSucc+Succ, TotFail+Fail, UserSkip+USkip, AutoSkip1, TotNotBuilt+NotBuilt, Labels1, - [IxEntry|CacheData]); + [IxEntry|TempData]); error -> - IxEntry = {TestName,Label,Missing,{LastLogDir,error},OldDirs}, + IxEntry = {TestName,Label,Missing, + {LastLogDir,error,undefined},OldDirs}, make_all_suites_index3(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, Labels1, - [IxEntry|CacheData]) + [IxEntry|TempData]) end; + +%% something wrong with this test dir, ignore +make_all_suites_index3([_|Rest], Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt, Labels, TempData) -> + make_all_suites_index3(Rest, Result, TotSucc, TotFail, + UserSkip, AutoSkip, TotNotBuilt, Labels, + TempData); + make_all_suites_index3([], Result, TotSucc, TotFail, UserSkip, AutoSkip, - TotNotBuilt, _, CacheData) -> - {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt,true)], - {TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}, lists:reverse(CacheData)}. + TotNotBuilt, _, TempData) -> + {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt,true)], + {TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}, lists:reverse(TempData)}. -make_all_suites_ix_cached(AbsIndexName, NewTestData, Label, AllTestLogDirs) -> +make_all_suites_ix_temp(AbsIndexName, NewTestData, Label, AllTestLogDirs) -> AllTestLogDirs1 = insert_new_test_data(NewTestData, Label, AllTestLogDirs), IndexDir = filename:dirname(AbsIndexName), - Index0 = make_all_suites_ix_cached1(AllTestLogDirs1, - all_suites_index_header(IndexDir), - 0, 0, 0, 0, 0), - Index = [Index0|index_footer()], - case force_write_file(AbsIndexName, Index) of + Index0 = make_all_suites_ix_temp1(AllTestLogDirs1, + all_suites_index_header(IndexDir), + 0, 0, 0, 0, 0), + Index = [Index0|all_suites_index_footer()], + case force_write_file(AbsIndexName, unicode:characters_to_binary(Index)) of ok -> ok; {error, Reason} -> @@ -1980,51 +2510,94 @@ make_all_suites_ix_cached(AbsIndexName, NewTestData, Label, AllTestLogDirs) -> insert_new_test_data({NewTestName,NewTestDir}, NewLabel, AllTestLogDirs) -> AllTestLogDirs1 = case lists:keysearch(NewTestName, 1, AllTestLogDirs) of - {value,{_,_,_,{LastLogDir,_},OldDirs}} -> - [{NewTestName,NewLabel,[],{NewTestDir,{0,0,0,0}}, + {value,{_,_,_,{LastLogDir,_,_},OldDirs}} -> + [{NewTestName,NewLabel,[],{NewTestDir,{0,0,0,0},undefined}, [LastLogDir|OldDirs]} | lists:keydelete(NewTestName, 1, AllTestLogDirs)]; false -> - [{NewTestName,NewLabel,[],{NewTestDir,{0,0,0,0}},[]} | + [{NewTestName,NewLabel,[],{NewTestDir,{0,0,0,0},undefined},[]} | AllTestLogDirs] end, lists:keysort(1, AllTestLogDirs1). -make_all_suites_ix_cached1([{TestName,Label,Missing,LastLogDirData,OldDirs}|Rest], - Result, TotSucc, TotFail, UserSkip, AutoSkip, - TotNotBuilt) -> - - case make_one_ix_entry_cached(TestName, LastLogDirData, - Label, {true,OldDirs}, Missing) of - {Result1,Succ,Fail,USkip,ASkip,NotBuilt} -> +make_all_suites_ix_temp1([{TestName,Label,Missing,LastLogDirData,OldDirs}|Rest], + Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt) -> + case make_one_ix_entry_temp(TestName, LastLogDirData, + Label, {true,OldDirs}, Missing) of + {Result1,Succ,Fail,USkip,ASkip,NotBuilt,_URIs} -> %% for backwards compatibility AutoSkip1 = case catch AutoSkip+ASkip of {'EXIT',_} -> undefined; Res -> Res end, - make_all_suites_ix_cached1(Rest, [Result|Result1], TotSucc+Succ, - TotFail+Fail, UserSkip+USkip, AutoSkip1, - TotNotBuilt+NotBuilt); + make_all_suites_ix_temp1(Rest, [Result|Result1], TotSucc+Succ, + TotFail+Fail, UserSkip+USkip, AutoSkip1, + TotNotBuilt+NotBuilt); error -> - make_all_suites_ix_cached1(Rest, Result, TotSucc, TotFail, - UserSkip, AutoSkip, TotNotBuilt) + make_all_suites_ix_temp1(Rest, Result, TotSucc, TotFail, + UserSkip, AutoSkip, TotNotBuilt) end; -make_all_suites_ix_cached1([], Result, TotSucc, TotFail, UserSkip, AutoSkip, - TotNotBuilt) -> +make_all_suites_ix_temp1([], Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt) -> [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, true)]. -make_one_ix_entry_cached(TestName, {LogDir,Summary}, Label, All, Missing) -> +make_one_ix_entry_temp(TestName, {LogDir,Summary,URIs}, Label, All, Missing) -> case Summary of {Succ,Fail,UserSkip,AutoSkip} -> NotBuilt = not_built(TestName, LogDir, All, Missing), - NewResult = make_one_index_entry1(TestName, LogDir, Label, - Succ, Fail, UserSkip, AutoSkip, - NotBuilt, All, cached), - {NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt}; + {NewResult,URIs1} = make_one_index_entry1(TestName, LogDir, Label, + Succ, Fail, + UserSkip, AutoSkip, + NotBuilt, All, temp, URIs), + {NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt,URIs1}; error -> error end. +%%%----------------------------------------------------------------- +%%% +get_cache_data({ok,CacheBin}) -> + case binary_to_term(CacheBin) of + CacheRec when is_record(CacheRec,log_cache) -> + %% make sure we don't use a cache on old format + case is_correct_cache_vsn(CacheRec) of + true -> + {ok,CacheRec}; + false -> + file:delete(?log_cache_name), + {error,old_cache_file} + end; + _ -> + file:delete(?log_cache_name), + {error,invalid_cache_file} + end; +get_cache_data(NoCache) -> + NoCache. + +cache_vsn() -> + application:load(common_test), + case application:get_key(common_test,vsn) of + {ok,VSN} -> + VSN; + _ -> + EbinDir = filename:dirname(code:which(ct)), + VSNfile = filename:join([EbinDir,"..","vsn.mk"]), + case file:read_file(VSNfile) of + {ok,Bin} -> + [_,VSN] = string:tokens(binary_to_list(Bin),[$=,$\n,$ ]), + VSN; + _ -> + undefined + end + end. + +is_correct_cache_vsn(#log_cache{version = CVSN}) -> + case cache_vsn() of + CVSN -> true; + _ -> false + end. + %%----------------------------------------------------------------- %% Remove log files. %% Cwd should always be set to the root logdir when finished. @@ -2116,7 +2689,7 @@ simulate_logger_loop() -> receive {log,_,_,_,_,_,List} -> S = [[io_lib:format(Str,Args),io_lib:nl()] || {Str,Args} <- List], - io:format("~s",[S]), + io:format("~ts",[S]), simulate_logger_loop(); stop -> ok @@ -2273,11 +2846,12 @@ make_relative1(DirTs, CwdTs) -> Ups ++ DirTs. %%%----------------------------------------------------------------- -%%% @spec get_ts_html_wrapper(TestName, PrintLabel, Cwd) -> {Mode,Header,Footer} +%%% @spec get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) +%%% -> {Mode,Header,Footer} %%% %%% @doc %%% -get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols) -> +get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) -> TestName1 = if is_list(TestName) -> lists:flatten(TestName); true -> @@ -2319,14 +2893,16 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols) -> "<html>\n", "<head><title>", TestName1, "</title>\n", "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + "<meta http-equiv=\"content-type\" content=\"text/html; charset=", + html_encoding(Encoding),"\">\n", "</head>\n", "<body", Bgr, " bgcolor=\"white\" text=\"black\" ", "link=\"blue\" vlink=\"purple\" alink=\"red\">\n", LabelStr, "\n"], ["<center>\n<br><hr><p>\n", - "<a href=\"", AllRuns, + "<a href=\"", uri(AllRuns), "\">Test run history\n</a> | ", - "<a href=\"", TestIndex, + "<a href=\"", uri(TestIndex), "\">Top level test index\n</a>\n</p>\n", Copyright,"</center>\n</body>\n</html>\n"]}; _ -> @@ -2363,14 +2939,15 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols) -> "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n", "<head>\n<title>", TestName1, "</title>\n", "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", - "<link rel=\"stylesheet\" href=\"", CSSFile, "\" type=\"text/css\">\n", + "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n", + "<link rel=\"stylesheet\" href=\"", uri(CSSFile), "\" type=\"text/css\">\n", "<script type=\"text/javascript\" src=\"", JQueryFile, "\"></script>\n", "<script type=\"text/javascript\" src=\"", TableSorterFile, "\"></script>\n"] ++ TableSorterScript ++ ["</head>\n","<body>\n", LabelStr, "\n"], ["<center>\n<br /><hr /><p>\n", - "<a href=\"", AllRuns, + "<a href=\"", uri(AllRuns), "\">Test run history\n</a> | ", - "<a href=\"", TestIndex, + "<a href=\"", uri(TestIndex), "\">Top level test index\n</a>\n</p>\n", Copyright,"</center>\n</body>\n</html>\n"]} end. @@ -2460,3 +3037,39 @@ insert_javascript({tablesorter,TableName, " $(\"#",TableName,"\").trigger(\"update\");\n", " $(\"#",TableName,"\").trigger(\"appendCache\");\n", "});\n</script>\n"]. + +uri("") -> + ""; +uri(Href) -> + test_server_ctrl:uri_encode(Href). + +%% Read magic comment to get encoding of text file. +%% If no magic comment exists, assume default encoding +encoding(File) -> + case epp:read_encoding(File) of + none -> + epp:default_encoding(); + E -> + E + end. + +%% Convert binary to string using default encoding +b2s(Bin) -> + b2s(Bin,epp:default_encoding()). + +%% Convert binary to string using given encoding +b2s(Bin,Encoding) -> + unicode:characters_to_list(Bin,Encoding). + +html_encoding(latin1) -> + "iso-8859-1"; +html_encoding(utf8) -> + "utf-8". + +unexpected_io(Pid,ct_internal,List,#logger_state{ct_log_fd=Fd}=State) -> + IoFun = create_io_fun(Pid,State), + io:format(Fd, "~ts", [lists:foldl(IoFun, [], List)]); +unexpected_io(Pid,_Category,List,State) -> + IoFun = create_io_fun(Pid,State), + Data = io_lib:format("~ts", [lists:foldl(IoFun, [], List)]), + test_server_io:print_unexpected(Data). diff --git a/lib/common_test/src/ct_make.erl b/lib/common_test/src/ct_make.erl index 8ddb91d355..d4bd81e78d 100644 --- a/lib/common_test/src/ct_make.erl +++ b/lib/common_test/src/ct_make.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2009-2011. All Rights Reserved. +%% Copyright Ericsson AB 2009-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -280,13 +280,13 @@ recompile(File, NoExec, Load, Opts) -> do_recompile(_File, true, _Load, _Opts) -> out_of_date; do_recompile(File, false, noload, Opts) -> - io:format("Recompile: ~s\n",[File]), + io:format("Recompile: ~ts\n",[File]), compile:file(File, [report_errors, report_warnings, error_summary |Opts]); do_recompile(File, false, load, Opts) -> - io:format("Recompile: ~s\n",[File]), + io:format("Recompile: ~ts\n",[File]), c:c(File, Opts); do_recompile(File, false, netload, Opts) -> - io:format("Recompile: ~s\n",[File]), + io:format("Recompile: ~ts\n",[File]), c:nc(File, Opts). exists(File) -> diff --git a/lib/common_test/src/ct_master.erl b/lib/common_test/src/ct_master.erl index 042c5ba267..b42ff73846 100644 --- a/lib/common_test/src/ct_master.erl +++ b/lib/common_test/src/ct_master.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2012. All Rights Reserved. +%% Copyright Ericsson AB 2006-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -51,7 +51,7 @@ %%% {testcase,Cases} | {spec,TestSpecs} | {allow_user_terms,Bool} | %%% {logdir,LogDir} | {event_handler,EventHandlers} | %%% {silent_connections,Conns} | {cover,CoverSpecFile} | -%%% {userconfig, UserCfgFiles} +%%% {cover_stop,Bool} | {userconfig, UserCfgFiles} %%% CfgFiles = string() | [string()] %%% TestDirs = string() | [string()] %%% Suites = atom() | [atom()] @@ -82,39 +82,48 @@ run_test(NodeOptsList) when is_list(NodeOptsList) -> %%% ExclNodes = [atom()] %%% %%% @doc Tests are spawned on the nodes as specified in <code>TestSpecs</code>. -%%% Each specification in TestSpec will be handled separately. It is however possible -%%% to also specify a list of specifications that should be merged into one before -%%% the tests are executed. Any test without a particular node specification will -%%% also be executed on the nodes in <code>InclNodes</code>. Nodes in the -%%% <code>ExclNodes</code> list will be excluded from the test. +%%% Each specification in TestSpec will be handled separately. It is however +%%% possible to also specify a list of specifications that should be merged +%%% into one before the tests are executed. Any test without a particular node +%%% specification will also be executed on the nodes in <code>InclNodes</code>. +%%% Nodes in the <code>ExclNodes</code> list will be excluded from the test. run([TS|TestSpecs],AllowUserTerms,InclNodes,ExclNodes) when is_list(TS), is_list(InclNodes), is_list(ExclNodes) -> - TS1 = - case TS of - List=[S|_] when is_list(S) -> List; - Spec -> [Spec] - end, - Result = - case catch ct_testspec:collect_tests_from_file(TS1,InclNodes,AllowUserTerms) of - {error,Reason} -> - {error,Reason}; - TSRec=#testspec{logdir=AllLogDirs, - config=StdCfgFiles, - userconfig=UserCfgFiles, - include=AllIncludes, - init=AllInitOpts, - event_handler=AllEvHs} -> - AllCfgFiles = {StdCfgFiles, UserCfgFiles}, - RunSkipPerNode = ct_testspec:prepare_tests(TSRec), - RunSkipPerNode2 = exclude_nodes(ExclNodes,RunSkipPerNode), - run_all(RunSkipPerNode2,AllLogDirs,AllCfgFiles,AllEvHs, - AllIncludes,[],[],AllInitOpts,TS1) - end, - [{TS,Result} | run(TestSpecs,AllowUserTerms,InclNodes,ExclNodes)]; + %% Note: [Spec] means run one test with Spec + %% [Spec1,Spec2] means run two tests separately + %% [[Spec1,Spec2]] means run one test, with the two specs merged + case catch ct_testspec:collect_tests_from_file([TS],InclNodes, + AllowUserTerms) of + {error,Reason} -> + [{error,Reason} | run(TestSpecs,AllowUserTerms,InclNodes,ExclNodes)]; + Tests -> + RunResult = + lists:map( + fun({Specs,TSRec=#testspec{logdir=AllLogDirs, + config=StdCfgFiles, + userconfig=UserCfgFiles, + include=AllIncludes, + init=AllInitOpts, + event_handler=AllEvHs}}) -> + AllCfgFiles = + {StdCfgFiles,UserCfgFiles}, + RunSkipPerNode = + ct_testspec:prepare_tests(TSRec), + RunSkipPerNode2 = + exclude_nodes(ExclNodes,RunSkipPerNode), + TSList = if is_integer(hd(TS)) -> [TS]; + true -> TS end, + {Specs,run_all(RunSkipPerNode2,AllLogDirs, + AllCfgFiles,AllEvHs, + AllIncludes,[],[],AllInitOpts,TSList)} + end, Tests), + RunResult ++ run(TestSpecs,AllowUserTerms,InclNodes,ExclNodes) + end; run([],_,_,_) -> []; -run(TS,AllowUserTerms,InclNodes,ExclNodes) when is_list(InclNodes), is_list(ExclNodes) -> +run(TS,AllowUserTerms,InclNodes,ExclNodes) when is_list(InclNodes), + is_list(ExclNodes) -> run([TS],AllowUserTerms,InclNodes,ExclNodes). %%%----------------------------------------------------------------- @@ -152,29 +161,32 @@ exclude_nodes([],RunSkipPerNode) -> %%% AllowUserTerms = bool() %%% Node = atom() %%% -%%% @doc Tests are spawned on <code>Node</code> according to <code>TestSpecs</code>. +%%% @doc Tests are spawned on <code>Node</code> according to +%%% <code>TestSpecs</code>. run_on_node([TS|TestSpecs],AllowUserTerms,Node) when is_list(TS),is_atom(Node) -> - TS1 = - case TS of - [List|_] when is_list(List) -> List; - Spec -> [Spec] - end, - Result = - case catch ct_testspec:collect_tests_from_file(TS1,[Node],AllowUserTerms) of - {error,Reason} -> - {error,Reason}; - TSRec=#testspec{logdir=AllLogDirs, - config=StdCfgFiles, - init=AllInitOpts, - include=AllIncludes, - userconfig=UserCfgFiles, - event_handler=AllEvHs} -> - AllCfgFiles = {StdCfgFiles, UserCfgFiles}, - {Run,Skip} = ct_testspec:prepare_tests(TSRec,Node), - run_all([{Node,Run,Skip}],AllLogDirs,AllCfgFiles,AllEvHs, - AllIncludes, [],[],AllInitOpts,TS1) - end, - [{TS,Result} | run_on_node(TestSpecs,AllowUserTerms,Node)]; + case catch ct_testspec:collect_tests_from_file([TS],[Node], + AllowUserTerms) of + {error,Reason} -> + [{error,Reason} | run_on_node(TestSpecs,AllowUserTerms,Node)]; + Tests -> + RunResult = + lists:map( + fun({Specs,TSRec=#testspec{logdir=AllLogDirs, + config=StdCfgFiles, + init=AllInitOpts, + include=AllIncludes, + userconfig=UserCfgFiles, + event_handler=AllEvHs}}) -> + AllCfgFiles = {StdCfgFiles,UserCfgFiles}, + {Run,Skip} = ct_testspec:prepare_tests(TSRec,Node), + TSList = if is_integer(hd(TS)) -> [TS]; + true -> TS end, + {Specs,run_all([{Node,Run,Skip}],AllLogDirs, + AllCfgFiles,AllEvHs, + AllIncludes, [],[],AllInitOpts,TSList)} + end, Tests), + RunResult ++ run_on_node(TestSpecs,AllowUserTerms,Node) + end; run_on_node([],_,_) -> []; run_on_node(TS,AllowUserTerms,Node) when is_atom(Node) -> @@ -244,8 +256,9 @@ run_all([],AllLogDirs,_,AllEvHs,_AllIncludes, {value,{_,Dir}} -> Dir; false -> "." end, - log(tty,"Master Logdir","~s",[MasterLogDir]), - start_master(lists:reverse(NodeOpts),Handlers,MasterLogDir,LogDirs,InitOptions,Specs), + log(tty,"Master Logdir","~ts",[MasterLogDir]), + start_master(lists:reverse(NodeOpts),Handlers,MasterLogDir, + LogDirs,InitOptions,Specs), ok. @@ -297,13 +310,15 @@ start_master(NodeOptsList) -> start_master(NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Specs) -> Master = spawn_link(?MODULE,init_master,[self(),NodeOptsList,EvHandlers, - MasterLogDir,LogDirs,InitOptions,Specs]), + MasterLogDir,LogDirs, + InitOptions,Specs]), receive {Master,Result} -> Result end. %%% @hidden -init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Specs) -> +init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs, + InitOptions,Specs) -> case whereis(ct_master) of undefined -> register(ct_master,self()), @@ -325,13 +340,14 @@ init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Spec {MLPid,_} = ct_master_logs:start(MasterLogDir, [N || {N,_} <- NodeOptsList]), log(all,"Master Logger process started","~w",[MLPid]), + case Specs of [] -> ok; _ -> SpecsStr = lists:map(fun(Name) -> Name ++ " " end,Specs), - ct_master_logs:log("Test Specification file(s)","~s", + ct_master_logs:log("Test Specification file(s)","~ts", [lists:flatten(SpecsStr)]) end, @@ -340,7 +356,7 @@ init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Spec ct_master_event:add_handler(), %% add user handlers for master event manager Add = fun({H,Args}) -> - log(all,"Adding Event Handler","~p",[H]), + log(all,"Adding Event Handler","~w",[H]), case gen_event:add_handler(?CT_MEVMGR_REF,H,Args) of ok -> ok; {'EXIT',Why} -> exit(Why); @@ -359,7 +375,8 @@ init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Spec init_master1(Parent,NodeOptsList,InitOptions,LogDirs). init_master1(Parent,NodeOptsList,InitOptions,LogDirs) -> - {Inaccessible,NodeOptsList1,InitOptions1} = init_nodes(NodeOptsList,InitOptions), + {Inaccessible,NodeOptsList1,InitOptions1} = init_nodes(NodeOptsList, + InitOptions), case Inaccessible of [] -> init_master2(Parent,NodeOptsList,LogDirs); @@ -391,8 +408,9 @@ init_master2(Parent,NodeOptsList,LogDirs) -> SpawnAndMon = fun({Node,Opts}) -> monitor_node(Node,true), - log(all,"Test Info","Starting test(s) on ~p...",[Node]), - {spawn_link(Node,?MODULE,init_node_ctrl,[self(),Cookie,Opts]),Node} + log(all,"Test Info","Starting test(s) on ~w...",[Node]), + {spawn_link(Node,?MODULE,init_node_ctrl,[self(),Cookie,Opts]), + Node} end, NodeCtrlPids = lists:map(SpawnAndMon,NodeOptsList), Result = master_loop(#state{node_ctrl_pids=NodeCtrlPids, @@ -404,12 +422,13 @@ master_loop(#state{node_ctrl_pids=[], results=Finished}) -> Str = lists:map(fun({Node,Result}) -> - io_lib:format("~-40.40.*s~p\n",[$_,atom_to_list(Node),Result]) + io_lib:format("~-40.40.*ts~p\n", + [$_,atom_to_list(Node),Result]) end,lists:reverse(Finished)), log(all,"TEST RESULTS",Str,[]), log(all,"Info","Updating log files",[]), refresh_logs(LogDirs,[]), - + ct_master_event:stop(), ct_master_logs:stop(), ok; @@ -437,18 +456,20 @@ master_loop(State=#state{node_ctrl_pids=NodeCtrlPids, Bad end, log(all,"Test Info", - "Test on node ~w failed! Reason: ~p",[Node,Error]), + "Test on node ~w failed! Reason: ~p", + [Node,Error]), {Locks1,Blocked1} = update_queue(exit,Node,Locks,Blocked), master_loop(State#state{node_ctrl_pids=NodeCtrlPids1, - results=[{Node,Error}|Results], + results=[{Node, + Error}|Results], locks=Locks1, blocked=Blocked1}) end; undefined -> %% ignore (but report) exit from master_logger etc log(all,"Test Info", - "Warning! Process ~p has terminated. Reason: ~p", + "Warning! Process ~w has terminated. Reason: ~p", [Pid,Reason]), master_loop(State) end; @@ -531,7 +552,7 @@ update_queue(take,Node,From,Lock={Op,Resource},Locks,Blocked) -> %% Blocked: [{{Operation,Resource},Node,WaitingPid},...] case lists:keysearch(Lock,1,Locks) of {value,{_Lock,Owner}} -> % other node has lock - log(html,"Lock Info","Node ~p blocked on ~w by ~w. Resource: ~p", + log(html,"Lock Info","Node ~w blocked on ~w by ~w. Resource: ~p", [Node,Op,Owner,Resource]), Blocked1 = Blocked ++ [{Lock,Node,From}], {Locks,Blocked1}; @@ -546,7 +567,7 @@ update_queue(release,Node,_From,Lock={Op,Resource},Locks,Blocked) -> case lists:keysearch(Lock,1,Blocked) of {value,E={Lock,SomeNode,WaitingPid}} -> Blocked1 = lists:delete(E,Blocked), - log(html,"Lock Info","Node ~p proceeds with ~w. Resource: ~p", + log(html,"Lock Info","Node ~w proceeds with ~w. Resource: ~p", [SomeNode,Op,Resource]), reply(ok,WaitingPid), % waiting process may start {Locks1,Blocked1}; @@ -625,7 +646,8 @@ refresh_logs([D|Dirs],Refreshed) -> refresh_logs([],Refreshed) -> Str = lists:map(fun({D,Result}) -> - io_lib:format("Refreshing logs in ~p... ~p",[D,Result]) + io_lib:format("Refreshing logs in ~p... ~p", + [D,Result]) end,Refreshed), log(all,"Info",Str,[]). @@ -638,7 +660,7 @@ init_node_ctrl(MasterPid,Cookie,Opts) -> process_flag(trap_exit, true), MasterNode = node(MasterPid), group_leader(whereis(user),self()), - io:format("~n********** node_ctrl process ~p started on ~p **********~n", + io:format("~n********** node_ctrl process ~w started on ~w **********~n", [self(),node()]), %% initially this node must have the same cookie as the master node %% but now we set it explicitly for the connection so that test suites @@ -671,7 +693,7 @@ init_node_ctrl(MasterPid,Cookie,Opts) -> pong -> MasterPid ! {self(),{result,Result}}; pang -> - io:format("Warning! Connection to master node ~p is lost. " + io:format("Warning! Connection to master node ~w is lost. " "Can't report result!~n~n", [MasterNode]) end. @@ -696,8 +718,9 @@ status(MasterPid,Event) -> log(To,Heading,Str,Args) -> if To == all ; To == tty -> - Str1 = ["=== ",Heading," ===\n",io_lib:format(Str,Args),"\n"], - io:format(Str1,[]); + Chars = ["=== ",Heading," ===\n", + io_lib:format(Str,Args),"\n"], + io:put_chars(Chars); true -> ok end, @@ -751,21 +774,25 @@ start_nodes(InitOptions)-> IsAlive = lists:member(NodeName, nodes()), case {HasNodeStart, IsAlive} of {false, false}-> - io:format("WARNING: Node ~p is not alive but has no node_start option~n", [NodeName]); + io:format("WARNING: Node ~w is not alive but has no " + "node_start option~n", [NodeName]); {false, true}-> - io:format("Node ~p is alive~n", [NodeName]); + io:format("Node ~w is alive~n", [NodeName]); {true, false}-> {node_start, NodeStart} = lists:keyfind(node_start, 1, Options), {value, {callback_module, Callback}, NodeStart2}= lists:keytake(callback_module, 1, NodeStart), case Callback:start(Host, Node, NodeStart2) of {ok, NodeName} -> - io:format("Node ~p started successfully with callback ~p~n", [NodeName,Callback]); + io:format("Node ~w started successfully " + "with callback ~w~n", [NodeName,Callback]); {error, Reason, _NodeName} -> - io:format("Failed to start node ~p with callback ~p! Reason: ~p~n", [NodeName, Callback, Reason]) + io:format("Failed to start node ~w with callback ~w! " + "Reason: ~p~n", [NodeName, Callback, Reason]) end; {true, true}-> - io:format("WARNING: Node ~p is alive but has node_start option~n", [NodeName]) + io:format("WARNING: Node ~w is alive but has node_start " + "option~n", [NodeName]) end end, InitOptions). @@ -778,7 +805,8 @@ eval_on_nodes(InitOptions)-> {false,_}-> ok; {true,false}-> - io:format("WARNING: Node ~p is not alive but has eval option ~n", [NodeName]); + io:format("WARNING: Node ~w is not alive but has eval " + "option~n", [NodeName]); {true,true}-> {eval, MFAs} = lists:keyfind(eval, 1, Options), evaluate(NodeName, MFAs) @@ -789,9 +817,11 @@ eval_on_nodes(InitOptions)-> evaluate(Node, [{M,F,A}|MFAs])-> case rpc:call(Node, M, F, A) of {badrpc,Reason}-> - io:format("WARNING: Failed to call ~p:~p/~p on node ~p due to ~p~n", [M,F,length(A),Node,Reason]); + io:format("WARNING: Failed to call ~w:~w/~w on node ~w " + "due to ~p~n", [M,F,length(A),Node,Reason]); Result-> - io:format("Called ~p:~p/~p on node ~p, result: ~p~n", [M,F,length(A),Node,Result]) + io:format("Called ~w:~w/~w on node ~w, result: ~p~n", + [M,F,length(A),Node,Result]) end, evaluate(Node, MFAs); evaluate(_Node, [])-> diff --git a/lib/common_test/src/ct_master_event.erl b/lib/common_test/src/ct_master_event.erl index a70baefaaf..fd97ab16f7 100644 --- a/lib/common_test/src/ct_master_event.erl +++ b/lib/common_test/src/ct_master_event.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2009. All Rights Reserved. +%% Copyright Ericsson AB 2006-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -66,16 +66,30 @@ add_handler(Args) -> %% Description: Stops the event manager %%-------------------------------------------------------------------- stop() -> - flush(), - gen_event:stop(?CT_MEVMGR_REF). + case flush() of + {error,Reason} -> + ct_master_logs:log("Error", + "No response from CT Master Event.\n" + "Reason = ~p\n" + "Terminating now!\n",[Reason]), + %% communication with event manager fails, kill it + catch exit(whereis(?CT_MEVMGR_REF), kill); + _ -> + gen_event:stop(?CT_MEVMGR_REF) + end. flush() -> - case gen_event:call(?CT_MEVMGR_REF,?MODULE,flush) of + try gen_event:call(?CT_MEVMGR_REF,?MODULE,flush,1800000) of flushing -> timer:sleep(1), flush(); done -> - ok + ok; + Error = {error,_} -> + Error + catch + _:Reason -> + {error,Reason} end. %%-------------------------------------------------------------------- @@ -114,13 +128,13 @@ init(_) -> %% each installed event handler to handle the event. %%-------------------------------------------------------------------- handle_event(#event{name=start_logging,node=Node,data=RunDir},State) -> - ct_master_logs:log("CT Master Event Handler","Got ~s from ~p",[RunDir,Node]), + ct_master_logs:log("CT Master Event Handler","Got ~ts from ~w",[RunDir,Node]), ct_master_logs:nodedir(Node,RunDir), {ok,State}; handle_event(#event{name=Name,node=Node,data=Data},State) -> print("~n=== ~w ===~n", [?MODULE]), - print("~p on ~p: ~p~n", [Name,Node,Data]), + print("~w on ~w: ~p~n", [Name,Node,Data]), {ok,State}. %%-------------------------------------------------------------------- diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl index 9e61d5b16f..5393097f57 100644 --- a/lib/common_test/src/ct_master_logs.erl +++ b/lib/common_test/src/ct_master_logs.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2012. All Rights Reserved. +%% Copyright Ericsson AB 2006-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -133,8 +133,8 @@ init(Parent,LogDir,Nodes) -> end,Nodes)), io:format(CtLogFd,int_header(),[log_timestamp(now()),"Test Nodes\n"]), - io:format(CtLogFd,"~s\n",[NodeStr]), - io:format(CtLogFd,int_footer()++"\n",[]), + io:format(CtLogFd,"~ts\n",[NodeStr]), + io:put_chars(CtLogFd,[int_footer(),"\n"]), NodeDirIxFd = open_nodedir_index(RunDirAbs,Time), Parent ! {started,self(),{Time,RunDirAbs}}, @@ -201,25 +201,22 @@ loop(State) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%% open_ct_master_log(Dir) -> FullName = filename:join(Dir,?ct_master_log_name), - {ok,Fd} = file:open(FullName,[write]), - io:format(Fd,header("Common Test Master Log", {[],[1,2],[]}),[]), + {ok,Fd} = file:open(FullName,[write,{encoding,utf8}]), + io:put_chars(Fd,header("Common Test Master Log", {[],[1,2],[]})), %% maybe add config info here later - io:format(Fd, config_table([]), []), - io:format(Fd, - "<style>\n" - "div.ct_internal { background:lightgrey; color:black }\n" - "div.default { background:lightgreen; color:black }\n" - "</style>\n", - []), - io:format(Fd, - xhtml("<br><h2>Progress Log</h2>\n<pre>\n", - "<br /><h2>Progress Log</h2>\n<pre>\n"), - []), + io:put_chars(Fd,config_table([])), + io:put_chars(Fd, + "<style>\n" + "div.ct_internal { background:lightgrey; color:black }\n" + "div.default { background:lightgreen; color:black }\n" + "</style>\n"), + io:put_chars(Fd, + xhtml("<br><h2>Progress Log</h2>\n<pre>\n", + "<br /><h2>Progress Log</h2>\n<pre>\n")), Fd. close_ct_master_log(Fd) -> - io:format(Fd,"</pre>",[]), - io:format(Fd,footer(),[]), + io:put_chars(Fd,["</pre>",footer()]), file:close(Fd). config_table(Vars) -> @@ -238,7 +235,7 @@ config_table1([]) -> ["</tbody>\n</table>\n"]. int_header() -> - "<div class=\"ct_internal\"><b>*** CT MASTER ~s *** ~s</b>". + "<div class=\"ct_internal\"><b>*** CT MASTER ~s *** ~ts</b>". int_footer() -> "</div>". @@ -247,21 +244,22 @@ int_footer() -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% open_nodedir_index(Dir,StartTime) -> FullName = filename:join(Dir,?nodedir_index_name), - {ok,Fd} = file:open(FullName,[write]), - io:format(Fd,nodedir_index_header(StartTime),[]), + {ok,Fd} = file:open(FullName,[write,{encoding,utf8}]), + io:put_chars(Fd,nodedir_index_header(StartTime)), Fd. print_nodedir(Node,RunDir,Fd) -> Index = filename:join(RunDir,"index.html"), - io:format(Fd, - ["<tr>\n" - "<td align=center>",atom_to_list(Node),"</td>\n", - "<td align=left><a href=\"",Index,"\">",Index,"</a></td>\n", - "</tr>\n"],[]), + io:put_chars(Fd, + ["<tr>\n" + "<td align=center>",atom_to_list(Node),"</td>\n", + "<td align=left><a href=\"",ct_logs:uri(Index),"\">",Index, + "</a></td>\n", + "</tr>\n"]), ok. close_nodedir_index(Fd) -> - io:format(Fd,index_footer(),[]), + io:put_chars(Fd,index_footer()), file:close(Fd). nodedir_index_header(StartTime) -> @@ -286,7 +284,9 @@ make_all_runs_index(LogDir) -> DirsSorted = (catch sort_all_runs(Dirs)), Header = all_runs_header(), Index = [runentry(Dir) || Dir <- DirsSorted], - Result = file:write_file(FullName,Header++Index++index_footer()), + Result = file:write_file(FullName, + unicode:characters_to_binary( + Header++Index++index_footer())), Result. sort_all_runs(Dirs) -> @@ -326,7 +326,8 @@ runentry(Dir) -> end, Index = filename:join(Dir,?nodedir_index_name), ["<tr>\n" - "<td align=center><a href=\"",Index,"\">",timestamp(Dir),"</a></td>\n", + "<td align=center><a href=\"",ct_logs:uri(Index),"\">", + timestamp(Dir),"</a></td>\n", "<td align=center>",MasterStr,"</td>\n", "<td align=center>",NodesStr,"</td>\n", "</tr>\n"]. @@ -384,8 +385,10 @@ header(Title, TableCols) -> "<head>\n", "<title>" ++ Title ++ "</title>\n", "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n", xhtml("", - ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">"]), + ["<link rel=\"stylesheet\" href=\"",ct_logs:uri(CSSFile), + "\" type=\"text/css\">"]), xhtml("", ["<script type=\"text/javascript\" src=\"",JQueryFile, "\"></script>\n"]), diff --git a/lib/common_test/src/ct_master_status.erl b/lib/common_test/src/ct_master_status.erl index 76060fb7bb..f9f511ecca 100644 --- a/lib/common_test/src/ct_master_status.erl +++ b/lib/common_test/src/ct_master_status.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2009. All Rights Reserved. +%% Copyright Ericsson AB 2006-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -70,7 +70,7 @@ init(_) -> %% handle_event(#event{name=Name,node=Node,data=Data},State) -> print("~n=== ~w ===~n", [?MODULE]), - print("~p on ~p: ~p~n", [Name,Node,Data]), + print("~w on ~w: ~p~n", [Name,Node,Data]), {ok,State}. %%-------------------------------------------------------------------- diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl index 294b82bff6..e094ee877a 100644 --- a/lib/common_test/src/ct_netconfc.erl +++ b/lib/common_test/src/ct_netconfc.erl @@ -1,7 +1,7 @@ %%---------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012. All Rights Reserved. +%% Copyright Ericsson AB 2012-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -307,7 +307,7 @@ -type option() :: {ssh,host()} | {port,inet:port_number()} | {user,string()} | {password,string()} | {user_dir,string()} | {timeout,timeout()}. --type host() :: inet:host_name() | inet:ip_address(). +-type host() :: inet:hostname() | inet:ip_address(). -type notification() :: {notification, xml_attributes(), notification_content()}. -type notification_content() :: [event_time()|simple_xml()]. @@ -1073,7 +1073,8 @@ handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) -> SimpleXml = encode_rpc_operation(get,[Filter]), do_send_rpc(Op, SimpleXml, Timeout, From, State). -handle_msg({ssh_cm, _CM, {data, _Ch, _Type, Data}}, State) -> +handle_msg({ssh_cm, CM, {data, Ch, _Type, Data}}, State) -> + ssh_connection:adjust_window(CM,Ch,size(Data)), handle_data(Data, State); handle_msg({ssh_cm, _CM, _SshCloseMsg}, State) -> %% _SshCloseMsg can probably be one of @@ -1163,13 +1164,11 @@ call(Client, Msg, Timeout, WaitStop) -> get_handle(Client) when is_pid(Client) -> {ok,Client}; get_handle(Client) -> - case ct_util:get_connections(Client, ?MODULE) of - {ok,[{Pid,_}]} -> + case ct_util:get_connection(Client, ?MODULE) of + {ok,{Pid,_}} -> {ok,Pid}; - {ok,[]} -> + {error,no_registered_connection} -> {error,{no_connection_found,Client}}; - {ok,Conns} -> - {error,{multiple_connections_found,Client,Conns}}; Error -> Error end. @@ -1280,10 +1279,11 @@ do_send(Connection, SimpleXml) -> to_xml_doc(Simple) -> Prolog = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", - Xml = list_to_binary(xmerl:export_simple([Simple], - xmerl_xml, - [#xmlAttribute{name=prolog, - value=Prolog}])), + Xml = unicode:characters_to_binary( + xmerl:export_simple([Simple], + xmerl_xml, + [#xmlAttribute{name=prolog, + value=Prolog}])), <<Xml/binary,?END_TAG/binary>>. %%%----------------------------------------------------------------- @@ -1300,7 +1300,8 @@ handle_data(NewData,#state{connection=Connection,buff=Buff} = State) -> decode(Simple,State#state{buff=Rest}); {fatal_error,_Loc,Reason,_EndTags,_EventState} -> ?error(Connection#connection.name,[{parse_error,Reason}, - {data,Data}]), + {buffer,Buff}, + {new_data,NewData}]), case Reason of {could_not_fetch_data,Msg} -> handle_msg(Msg,State#state{buff = <<>>}); @@ -1687,18 +1688,27 @@ log(#connection{host=Host,port=Port,name=Name},Action,Data) -> %% Log callback - called from the error handler process -format_data(raw,Data) -> - io_lib:format("~n~s~n",[hide_password(Data)]); -format_data(pretty,Data) -> - io_lib:format("~n~s~n",[indent(Data)]); -format_data(html,Data) -> - io_lib:format("~n~s~n",[html_format(Data)]). +format_data(How,Data) -> + %% Assuming that the data is encoded as UTF-8. If it is not, then + %% the printout might be wrong, but the format function will not + %% crash! + %% FIXME: should probably read encoding from the data and do + %% unicode:characters_to_binary(Data,InEncoding,utf8) when calling + %% log/3 instead of assuming utf8 in as done here! + do_format_data(How,unicode:characters_to_binary(Data)). + +do_format_data(raw,Data) -> + io_lib:format("~n~ts~n",[hide_password(Data)]); +do_format_data(pretty,Data) -> + io_lib:format("~n~ts~n",[indent(Data)]); +do_format_data(html,Data) -> + io_lib:format("~n~ts~n",[html_format(Data)]). %%%----------------------------------------------------------------- %%% Hide password elements from XML data hide_password(Bin) -> re:replace(Bin,<<"(<password[^>]*>)[^<]*(</password>)">>,<<"\\1*****\\2">>, - [global,{return,binary}]). + [global,{return,binary},unicode]). %%%----------------------------------------------------------------- %%% HTML formatting @@ -1716,13 +1726,13 @@ indent(Bin) -> Part -> indent1(lists:reverse(Part)++String,erase(indent)) end, - list_to_binary(IndentedString). + unicode:characters_to_binary(IndentedString). %% Normalizes the XML document by removing all space and newline %% between two XML tags. %% Returns a list, no matter if the input was a list or a binary. -normalize(Str) -> - re:replace(Str,<<">[ \r\n\t]+<">>,<<"><">>,[global,{return,list}]). +normalize(Bin) -> + re:replace(Bin,<<">[ \r\n\t]+<">>,<<"><">>,[global,{return,list},unicode]). indent1("<?"++Rest1,Indent1) -> @@ -1805,7 +1815,8 @@ get_tag([]) -> %%% SSH stuff ssh_receive_data() -> receive - {ssh_cm, _CM, {data, _Ch, _Type, Data}} -> + {ssh_cm, CM, {data, Ch, _Type, Data}} -> + ssh_connection:adjust_window(CM,Ch,size(Data)), {ok, Data}; {ssh_cm, _CM, {Closed, _Ch}} = X when Closed == closed; Closed == eof -> {error,X}; diff --git a/lib/common_test/src/ct_repeat.erl b/lib/common_test/src/ct_repeat.erl index a47309c6ee..f4d9949776 100644 --- a/lib/common_test/src/ct_repeat.erl +++ b/lib/common_test/src/ct_repeat.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2007-2012. All Rights Reserved. +%% Copyright Ericsson AB 2007-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -23,7 +23,7 @@ %%% start flags (or equivalent ct:run_test/1 options) are supported: %%% -until <StopTime>, StopTime = YYMoMoDDHHMMSS | HHMMSS %%% -duration <DurTime>, DurTime = HHMMSS -%%% -force_stop +%%% -force_stop [skip_rest] %%% -repeat <N>, N = integer()</p> -module(ct_repeat). @@ -62,12 +62,15 @@ loop_test(If,Args) when is_list(Args) -> io:format("\nCommon Test: " "Will repeat tests for ~s.\n\n",[ts(Secs)]), TPid = - case lists:keymember(force_stop,1,Args) of - true -> + case proplists:get_value(force_stop,Args) of + False when False==false; False==undefined -> + undefined; + ForceStop -> CtrlPid = self(), - spawn(fun() -> stop_after(CtrlPid,Secs) end); - false -> - undefined + spawn( + fun() -> + stop_after(CtrlPid,Secs,ForceStop) + end) end, Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args], loop(If,stop_time,0,Secs,StopTime,Args1,TPid,[]) @@ -212,7 +215,7 @@ get_stop_time(until,[Y1,Y2,Mo1,Mo2,D1,D2,H1,H2,Mi1,Mi2,S1,S2]) -> list_to_integer([S1,S2])}, calendar:datetime_to_gregorian_seconds({Date,Time}); -get_stop_time(until,Time) -> +get_stop_time(until,Time=[_,_,_,_,_,_]) -> get_stop_time(until,"000000"++Time); get_stop_time(duration,[H1,H2,Mi1,Mi2,S1,S2]) -> @@ -227,10 +230,17 @@ cancel(Pid) -> %% After Secs, abort will make the test_server finish the current %% job, then empty the job queue and stop. -stop_after(_CtrlPid,Secs) -> +stop_after(_CtrlPid,Secs,ForceStop) -> timer:sleep(Secs*1000), + case ForceStop of + SkipRest when SkipRest==skip_rest; SkipRest==["skip_rest"] -> + ct_util:set_testdata({skip_rest,true}); + _ -> + ok + end, test_server_ctrl:abort(). + %% Callback from ct_run to print loop info to system log. log_loop_info(Args) -> case lists:keysearch(loop_info,1,Args) of @@ -259,11 +269,11 @@ log_loop_info(Args) -> io_lib:format("Test time remaining: ~w secs (~w%)\n", [Secs,trunc((Secs/Secs0)*100)]), LogStr4 = - case lists:keymember(force_stop,1,Args) of - true -> - io_lib:format("force_stop is enabled",[]); - _ -> - "" + case proplists:get_value(force_stop,Args) of + False when False==false; False==undefined -> + ""; + ForceStop -> + io_lib:format("force_stop is set to: ~w",[ForceStop]) end, ct_logs:log("Test loop info",LogStr1++LogStr2++LogStr3++LogStr4,[]) end. diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl index 96b2934382..266ca73417 100644 --- a/lib/common_test/src/ct_run.erl +++ b/lib/common_test/src/ct_run.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2012. All Rights Reserved. +%% Copyright Ericsson AB 2004-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -58,6 +58,7 @@ vts, shell, cover, + cover_stop, coverspec, step, logdir, @@ -147,7 +148,7 @@ script_start(Args) -> _ -> "" end end, - io:format("~nCommon Test~s starting (cwd is ~s)~n~n", + io:format("~nCommon Test~s starting (cwd is ~ts)~n~n", [CTVsn,Cwd]), Self = self(), Pid = spawn_link(fun() -> script_start1(Self, Args) end), @@ -245,6 +246,7 @@ script_start1(Parent, 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), + 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), @@ -327,33 +329,41 @@ script_start1(Parent, Args) -> application:set_env(common_test, basic_html, true), true 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, - StartOpts = #opts{label = Label, profile = Profile, - vts = Vts, shell = Shell, cover = Cover, - logdir = LogDir, logopts = LogOpts, - basic_html = BasicHtml, - verbosity = Verbosity, - event_handlers = EvHandlers, - ct_hooks = CTHooks, - enable_builtin_hooks = EnableBuiltinHooks, - auto_compile = AutoCompile, - include = IncludeDirs, - silent_connections = SilentConns, - stylesheet = Stylesheet, - multiply_timetraps = MultTT, - scale_timetraps = ScaleTT, - create_priv_dir = CreatePrivDir, - starter = script}, - + Opts = #opts{label = Label, profile = Profile, + vts = Vts, shell = Shell, + cover = Cover, cover_stop = CoverStop, + logdir = LogDir, logopts = LogOpts, + basic_html = BasicHtml, + verbosity = Verbosity, + event_handlers = EvHandlers, + ct_hooks = CTHooks, + enable_builtin_hooks = EnableBuiltinHooks, + auto_compile = AutoCompile, + 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(StartOpts, Args), + Result = run_or_refresh(Opts, Args), %% send final results to starting process waiting in script_start/0 Parent ! {self(), Result}. -run_or_refresh(StartOpts = #opts{logdir = LogDir}, Args) -> +run_or_refresh(Opts = #opts{logdir = LogDir}, Args) -> case proplists:get_value(refresh_logs, Args) of undefined -> - script_start2(StartOpts, Args); + script_start2(Opts, Args); Refresh -> LogDir1 = case Refresh of [] -> which(logdir,LogDir); @@ -375,167 +385,204 @@ run_or_refresh(StartOpts = #opts{logdir = LogDir}, Args) -> {error,{all_suites_index,ASReason}}; _ -> file:set_cwd(Cwd), - io:format("Logs in ~s refreshed!~n~n", [LogDir1]), + io:format("Logs in ~ts refreshed!~n~n", + [LogDir1]), timer:sleep(500), % time to flush io before quitting ok end end end. -script_start2(StartOpts = #opts{vts = undefined, - shell = undefined}, Args) -> - TestSpec = proplists:get_value(spec, Args), - {Terms,Opts} = - case TestSpec of - Specs when Specs =/= [], Specs =/= undefined -> - %% using testspec as input for test - Relaxed = get_start_opt(allow_user_terms, true, false, Args), - case catch ct_testspec:collect_tests_from_file(Specs, Relaxed) of - {E,Reason} when E == error ; E == 'EXIT' -> - {{error,Reason},StartOpts}; - TS -> - SpecStartOpts = get_data_for_node(TS, node()), - - 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]), - AllVerbosity = - merge_keyvals([StartOpts#opts.verbosity, - SpecStartOpts#opts.verbosity]), - AllSilentConns = - merge_vals([StartOpts#opts.silent_connections, - SpecStartOpts#opts.silent_connections]), - Cover = - choose_val(StartOpts#opts.cover, - SpecStartOpts#opts.cover), - MultTT = - choose_val(StartOpts#opts.multiply_timetraps, - SpecStartOpts#opts.multiply_timetraps), - ScaleTT = - choose_val(StartOpts#opts.scale_timetraps, - SpecStartOpts#opts.scale_timetraps), - - CreatePrivDir = - choose_val(StartOpts#opts.create_priv_dir, - SpecStartOpts#opts.create_priv_dir), - - AllEvHs = - merge_vals([StartOpts#opts.event_handlers, - SpecStartOpts#opts.event_handlers]), - - AllCTHooks = merge_vals( - [StartOpts#opts.ct_hooks, - SpecStartOpts#opts.ct_hooks]), - - EnableBuiltinHooks = - choose_val( - StartOpts#opts.enable_builtin_hooks, - SpecStartOpts#opts.enable_builtin_hooks), - - Stylesheet = - choose_val(StartOpts#opts.stylesheet, - SpecStartOpts#opts.stylesheet), - - AllInclude = merge_vals([StartOpts#opts.include, - SpecStartOpts#opts.include]), - application:set_env(common_test, include, AllInclude), - - AutoCompile = - case choose_val(StartOpts#opts.auto_compile, - SpecStartOpts#opts.auto_compile) of - undefined -> - true; - ACBool -> - application:set_env(common_test, - auto_compile, - ACBool), - ACBool - end, - - BasicHtml = - case choose_val(StartOpts#opts.basic_html, - SpecStartOpts#opts.basic_html) of - undefined -> - false; - BHBool -> - application:set_env(common_test, basic_html, - BHBool), - BHBool - end, - - {TS,StartOpts#opts{label = Label, - profile = Profile, - testspecs = Specs, - cover = Cover, - logdir = LogDir, - logopts = AllLogOpts, - basic_html = BasicHtml, - verbosity = AllVerbosity, - silent_connections = AllSilentConns, - config = SpecStartOpts#opts.config, - event_handlers = AllEvHs, - ct_hooks = AllCTHooks, - enable_builtin_hooks = - EnableBuiltinHooks, - stylesheet = Stylesheet, - auto_compile = AutoCompile, - include = AllInclude, - multiply_timetraps = MultTT, - scale_timetraps = ScaleTT, - create_priv_dir = CreatePrivDir}} - end; - _ -> - {undefined,StartOpts} - end, - %% read config/userconfig from start flags - InitConfig = ct_config:prepare_config_list(Args), - TheLogDir = which(logdir, Opts#opts.logdir), - case {TestSpec,Terms} of - {_,{error,_}=Error} -> - Error; - {[],_} -> +script_start2(Opts = #opts{vts = undefined, + 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), + case catch ct_testspec:collect_tests_from_file(Specs1, Relaxed) of + {E,Reason} when E == error ; E == 'EXIT' -> + StackTrace = erlang:get_stacktrace(), + {error,{invalid_testspec,{Reason,StackTrace}}}; + TestSpecData -> + execute_all_specs(TestSpecData, Opts, Args, []) + end; + [] -> {error,no_testspec_specified}; - {undefined,_} -> % no testspec used - case check_and_install_configfiles(InitConfig, TheLogDir, Opts) of + _ -> % 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; - {_,_} -> % testspec used - %% 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(Terms, node()), - do_run(Run, Skip, Opts#opts{config=AllConfig, - logdir=TheLogDir}, Args); - Error -> - Error end end; -script_start2(StartOpts, Args) -> +script_start2(Opts, Args) -> %% read config/userconfig from start flags InitConfig = ct_config:prepare_config_list(Args), - LogDir = which(logdir, StartOpts#opts.logdir), - case check_and_install_configfiles(InitConfig, LogDir, StartOpts) of + 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(StartOpts#opts{config=InitConfig, - logdir=LogDir}, Args); + 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()), + do_run(Run, Skip, Opts#opts{config=AllConfig, + logdir=TheLogDir}, Args); + 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, + + 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, + + Opts#opts{label = Label, + profile = Profile, + testspecs = Specs, + cover = Cover, + cover_stop = CoverStop, + logdir = which(logdir, LogDir), + logopts = AllLogOpts, + basic_html = BasicHtml, + verbosity = AllVerbosity, + silent_connections = AllSilentConns, + config = TSOpts#opts.config, + event_handlers = AllEvHs, + ct_hooks = AllCTHooks, + enable_builtin_hooks = EnableBuiltinHooks, + stylesheet = Stylesheet, + auto_compile = AutoCompile, + include = AllInclude, + multiply_timetraps = MultTT, + scale_timetraps = ScaleTT, + create_priv_dir = CreatePrivDir}. + check_and_install_configfiles( Configs, LogDir, #opts{ event_handlers = EvHandlers, @@ -555,12 +602,12 @@ check_and_install_configfiles( {error,{cant_load_callback_module,Info}} end. -script_start3(StartOpts, Args) -> - StartOpts1 = get_start_opt(step, - fun(Step) -> - StartOpts#opts{step = Step, - cover = undefined} - end, StartOpts, Args), +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), @@ -574,17 +621,17 @@ script_start3(StartOpts, Args) -> {error,no_dir_specified}; {Dirs,undefined,[]} when is_list(Dirs) -> - script_start4(StartOpts#opts{tests = tests(Dirs)}, Args); + script_start4(Opts#opts{tests = tests(Dirs)}, Args); {undefined,Suites,[]} when is_list(Suites) -> Ts = tests([suite_to_test(S) || S <- Suites]), - script_start4(StartOpts1#opts{tests = Ts}, Args); + 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(StartOpts1#opts{tests = Ts}, Args); + script_start4(Opts1#opts{tests = Ts}, Args); [_,_|_] -> {error,multiple_suites_and_cases}; _ -> @@ -598,10 +645,10 @@ script_start3(StartOpts, Args) -> case [suite_to_test(Dir,S) || S <- Suite] of DirMods when GsAndCs == [] -> Ts = tests(DirMods), - script_start4(StartOpts1#opts{tests = Ts}, Args); + script_start4(Opts1#opts{tests = Ts}, Args); DirMods = [_] when GsAndCs /= [] -> Ts = tests(DirMods, GsAndCs), - script_start4(StartOpts1#opts{tests = Ts}, Args); + script_start4(Opts1#opts{tests = Ts}, Args); [_,_|_] when GsAndCs /= [] -> {error,multiple_suites_and_cases}; _ -> @@ -612,8 +659,8 @@ script_start3(StartOpts, Args) -> {error,incorrect_start_options}; {undefined,undefined,_} -> - if StartOpts#opts.vts ; StartOpts#opts.shell -> - script_start4(StartOpts#opts{tests = []}, Args); + if Opts#opts.vts ; Opts#opts.shell -> + script_start4(Opts#opts{tests = []}, Args); true -> script_usage(), {error,missing_start_options} @@ -723,6 +770,7 @@ script_usage() -> "\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]" @@ -731,9 +779,9 @@ script_usage() -> "\n\t[-scale_timetraps]" "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t[-basic_html]" - "\n\t[-repeat N [-force_stop]] |" - "\n\t[-duration HHMMSS [-force_stop]] |" - "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop]]\n\n"), + "\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("Run tests using test specification:\n\n" "\tct_run -spec TestSpec1 TestSpec2 .. TestSpecN" "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]" @@ -742,9 +790,11 @@ script_usage() -> "\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]" @@ -753,9 +803,9 @@ script_usage() -> "\n\t[-scale_timetraps]" "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t[-basic_html]" - "\n\t[-repeat N [-force_stop]] |" - "\n\t[-duration HHMMSS [-force_stop]] |" - "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop]]\n\n"), + "\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] " @@ -839,7 +889,7 @@ run_test1(StartOpts) when is_list(StartOpts) -> undefined -> Tracing = start_trace(StartOpts), {ok,Cwd} = file:get_cwd(), - io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]), + io:format("~nCommon Test starting (cwd is ~ts)~n~n", [Cwd]), Res = case ct_repeat:loop_test(func, StartOpts) of false -> @@ -938,6 +988,7 @@ run_test2(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, 1, StartOpts), @@ -996,11 +1047,19 @@ run_test2(StartOpts) -> BasicHtmlBool end, + 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, + %% stepped execution Step = get_start_opt(step, value, StartOpts), Opts = #opts{label = Label, profile = Profile, - cover = Cover, step = Step, logdir = LogDir, + cover = Cover, cover_stop = CoverStop, + step = Step, logdir = LogDir, logopts = LogOpts, basic_html = BasicHtml, config = CfgFiles, verbosity = Verbosity, @@ -1033,102 +1092,61 @@ run_test2(StartOpts) -> end. run_spec_file(Relaxed, - Opts = #opts{testspecs = Specs, config = CfgFiles}, + Opts = #opts{testspecs = Specs}, StartOpts) -> Specs1 = case Specs of [X|_] when is_integer(X) -> [Specs]; _ -> Specs end, AbsSpecs = lists:map(fun(SF) -> ?abs(SF) end, Specs1), - log_ts_names(AbsSpecs), - case catch ct_testspec:collect_tests_from_file(AbsSpecs, Relaxed) of + AbsSpecs1 = get_start_opt(join_specs, [AbsSpecs], AbsSpecs, StartOpts), + case catch ct_testspec:collect_tests_from_file(AbsSpecs1, Relaxed) of {Error,CTReason} when Error == error ; Error == 'EXIT' -> - exit({error,CTReason}); - TS -> - 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]), - Stylesheet = choose_val(Opts#opts.stylesheet, - SpecOpts#opts.stylesheet), - AllVerbosity = merge_keyvals([Opts#opts.verbosity, - SpecOpts#opts.verbosity]), - AllSilentConns = merge_vals([Opts#opts.silent_connections, - SpecOpts#opts.silent_connections]), - AllConfig = merge_vals([CfgFiles, SpecOpts#opts.config]), - Cover = choose_val(Opts#opts.cover, - SpecOpts#opts.cover), - MultTT = choose_val(Opts#opts.multiply_timetraps, - SpecOpts#opts.multiply_timetraps), - ScaleTT = choose_val(Opts#opts.scale_timetraps, - SpecOpts#opts.scale_timetraps), - CreatePrivDir = choose_val(Opts#opts.create_priv_dir, - SpecOpts#opts.create_priv_dir), - AllEvHs = merge_vals([Opts#opts.event_handlers, - SpecOpts#opts.event_handlers]), - AllInclude = merge_vals([Opts#opts.include, - SpecOpts#opts.include]), - AllCTHooks = merge_vals([Opts#opts.ct_hooks, - SpecOpts#opts.ct_hooks]), - EnableBuiltinHooks = choose_val(Opts#opts.enable_builtin_hooks, - SpecOpts#opts.enable_builtin_hooks), - - application:set_env(common_test, include, AllInclude), - - AutoCompile = case choose_val(Opts#opts.auto_compile, - SpecOpts#opts.auto_compile) of - undefined -> - true; - ACBool -> - application:set_env(common_test, auto_compile, - ACBool), - ACBool - end, + StackTrace = erlang:get_stacktrace(), + exit({error,{invalid_testspec,{CTReason,StackTrace}}}); + TestSpecData -> + run_all_specs(TestSpecData, Opts, StartOpts, []) + end. - BasicHtml = case choose_val(Opts#opts.basic_html, - SpecOpts#opts.basic_html) of - undefined -> - false; - BHBool -> - application:set_env(common_test, basic_html, - BHBool), - BHBool - end, - - Opts1 = Opts#opts{label = Label, - profile = Profile, - cover = Cover, - logdir = which(logdir, LogDir), - logopts = AllLogOpts, - stylesheet = Stylesheet, - basic_html = BasicHtml, - verbosity = AllVerbosity, - silent_connections = AllSilentConns, - config = AllConfig, - event_handlers = AllEvHs, - auto_compile = AutoCompile, - include = AllInclude, - testspecs = AbsSpecs, - multiply_timetraps = MultTT, - scale_timetraps = ScaleTT, - create_priv_dir = CreatePrivDir, - ct_hooks = AllCTHooks, - enable_builtin_hooks = EnableBuiltinHooks - }, - - case check_and_install_configfiles(AllConfig,Opts1#opts.logdir, - Opts1) of - ok -> - {Run,Skip} = ct_testspec:prepare_tests(TS, node()), - reformat_result(catch do_run(Run, Skip, Opts1, StartOpts)); - {error,GCFReason} -> - exit({error,GCFReason}) +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}} + end, {0,0,{0,0}}, TotResult1) end + end; + +run_all_specs([{Specs,TS} | TSs], Opts, StartOpts, TotResult) -> + log_ts_names(Specs), + 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}, 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, @@ -1318,7 +1336,7 @@ run_testspec(TestSpec) -> run_testspec1(TestSpec) -> {ok,Cwd} = file:get_cwd(), - io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]), + io:format("~nCommon Test starting (cwd is ~ts)~n~n", [Cwd]), case catch run_testspec2(TestSpec) of {'EXIT',Reason} -> file:set_cwd(Cwd), @@ -1376,6 +1394,7 @@ get_data_for_node(#testspec{label = Labels, verbosity = VLvls, silent_connections = SilentConnsList, cover = CoverFs, + cover_stop = CoverStops, config = Cfgs, userconfig = UsrCfgs, event_handler = EvHs, @@ -1407,6 +1426,7 @@ get_data_for_node(#testspec{label = Labels, 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), @@ -1425,6 +1445,7 @@ get_data_for_node(#testspec{label = Labels, verbosity = Verbosity, silent_connections = SilentConns, cover = Cover, + cover_stop = CoverStop, config = ConfigFiles, event_handlers = EvHandlers, ct_hooks = FiltCTHooks, @@ -1452,7 +1473,7 @@ refresh_logs(LogDir) -> {error,{all_runs_index,ARReason}}; _ -> file:set_cwd(Cwd), - io:format("Logs in ~s refreshed!~n",[LogDir]), + io:format("Logs in ~ts refreshed!~n",[LogDir]), ok end end @@ -1597,14 +1618,7 @@ do_run(Tests, Misc, LogDir, LogOpts) when is_list(Misc), StepOpts -> #opts{step = StepOpts} end, - Opts1 = - case proplists:get_value(cover, Misc) of - undefined -> - Opts; - CoverFile -> - Opts#opts{cover = CoverFile} - end, - do_run(Tests, [], Opts1#opts{logdir = LogDir}, []); + do_run(Tests, [], Opts#opts{logdir = LogDir}, []); do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) -> #opts{label = Label, profile = Profile, cover = Cover, @@ -1638,7 +1652,13 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) -> {error,Reason} -> exit({error,Reason}); CoverSpec -> - Opts#opts{coverspec = CoverSpec} + CoverStop = + case Opts#opts.cover_stop of + undefined -> true; + Stop -> Stop + end, + Opts#opts{coverspec = CoverSpec, + cover_stop = CoverStop} end end, %% This env variable is used by test_server to determine @@ -1776,7 +1796,7 @@ possibly_spawn(true, Tests, Skip, Opts) -> end, unlink(CTUtilSrv), SupPid = spawn(Supervisor), - io:format(user, "~nTest control handed over to process ~p~n~n", + io:format(user, "~nTest control handed over to process ~w~n~n", [SupPid]), SupPid. @@ -1864,7 +1884,7 @@ verify_suites(TestSuites) -> Suite)), io:format(user, "Suite ~w not found" - "in directory ~s~n", + "in directory ~ts~n", [Suite,TestDir]), {Found,[{DS,[Name]}|NotFound]} end @@ -1879,7 +1899,7 @@ verify_suites(TestSuites) -> ActualDir = filename:dirname(SuiteFile), {[{ActualDir,Suite}|Found],NotFound}; false -> - io:format(user, "Directory ~s is " + io:format(user, "Directory ~ts is " "invalid~n", [Dir]), Name = filename:join(Dir, atom_to_list(Suite)), {Found,[{DS,[Name]}|NotFound]} @@ -2119,7 +2139,7 @@ do_run_test(Tests, Skip, Opts) -> cross = CovCross, src = _CovSrc}} -> ct_logs:log("COVER INFO", - "Using cover specification file: ~s~n" + "Using cover specification file: ~ts~n" "App: ~w~n" "Cross cover: ~w~n" "Including ~w modules~n" @@ -2134,7 +2154,7 @@ do_run_test(Tests, Skip, Opts) -> DelResult = file:delete(CovExport), ct_logs:log("COVER INFO", "Warning! " - "Export file ~s already exists. " + "Export file ~ts already exists. " "Deleting with result: ~p", [CovExport,DelResult]); false -> @@ -2144,7 +2164,8 @@ do_run_test(Tests, Skip, Opts) -> %% tell test_server which modules should be cover compiled %% note that actual compilation is done when tests start test_server_ctrl:cover(CovApp, CovFile, CovExcl, CovIncl, - CovCross, CovExport, CovLevel), + CovCross, CovExport, CovLevel, + Opts#opts.cover_stop), %% save cover data (used e.g. to add nodes dynamically) ct_util:set_testdata({cover,CovData}), %% start cover on specified nodes @@ -2216,6 +2237,15 @@ do_run_test(Tests, Skip, Opts) -> 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", "~p\n", [SevereError]), + exit(SevereError) + end, + case ct_util:get_testdata(stats) of Stats = {_Ok,_Failed,{_UserSkipped,_AutoSkipped}} -> Stats; @@ -2277,8 +2307,12 @@ add_jobs([{TestDir,all,_}|Tests], Skip, Opts, CleanUp) -> {'EXIT',_} -> CleanUp; _ -> - wait_for_idle(), - add_jobs(Tests, Skip, Opts, 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) -> @@ -2291,8 +2325,12 @@ add_jobs([{TestDir,Suites,all}|Tests], Skip, {'EXIT',_} -> CleanUp; _ -> - wait_for_idle(), - add_jobs(Tests, Skip, Opts, 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 @@ -2304,8 +2342,12 @@ add_jobs([{TestDir,Suite,all}|Tests], Skip, Opts, CleanUp) -> {'EXIT',_} -> CleanUp; _ -> - wait_for_idle(), - add_jobs(Tests, Skip, Opts, [Suite|CleanUp]) + case wait_for_idle() of + ok -> + add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); + _ -> + CleanUp + end end; Error -> Error @@ -2344,8 +2386,12 @@ add_jobs([{TestDir,Suite,Confs}|Tests], Skip, Opts, CleanUp) when {'EXIT',_} -> CleanUp; _ -> - wait_for_idle(), - add_jobs(Tests, Skip, Opts, [Suite|CleanUp]) + case wait_for_idle() of + ok -> + add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); + _ -> + CleanUp + end end; Error -> Error @@ -2370,8 +2416,12 @@ add_jobs([{TestDir,Suite,Cases}|Tests], {'EXIT',_} -> CleanUp; _ -> - wait_for_idle(), - add_jobs(Tests, Skip, Opts, [Suite|CleanUp]) + case wait_for_idle() of + ok -> + add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); + _ -> + CleanUp + end end; Error -> Error @@ -2387,8 +2437,12 @@ add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opts, CleanUp) when is_atom(Case) - {'EXIT',_} -> CleanUp; _ -> - wait_for_idle(), - add_jobs(Tests, Skip, Opts, [Suite|CleanUp]) + case wait_for_idle() of + ok -> + add_jobs(Tests, Skip, Opts, [Suite|CleanUp]); + _ -> + CleanUp + end end; Error -> Error @@ -2398,7 +2452,13 @@ add_jobs([], _, _, CleanUp) -> wait_for_idle() -> ct_util:update_last_run_index(), - Notify = fun(Me) -> Me ! idle end, + 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; @@ -2406,11 +2466,14 @@ wait_for_idle() -> %% so we don't hang forever if test_server dies Ref = erlang:monitor(process, TSPid), Result = receive - idle -> ok; + {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. @@ -2606,7 +2669,7 @@ log_ts_names(Specs) -> List = lists:map(fun(Name) -> Name ++ " " end, Specs), - ct_logs:log("Test Specification file(s)", "~s", + ct_logs:log("Test Specification file(s)", "~ts", [lists:flatten(List)]). merge_arguments(Args) -> @@ -2618,6 +2681,9 @@ merge_arguments([LogDir={logdir,_}|Args], 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)); @@ -2714,10 +2780,10 @@ event_handler_args2opts(Default, Args) -> event_handler_init_args2opts(EHs) end. event_handler_init_args2opts([EH, Arg, "and" | EHs]) -> - [{list_to_atom(EH),lists:flatten(io_lib:format("~s",[Arg]))} | + [{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("~s",[Arg]))}]; + [{list_to_atom(EH),lists:flatten(io_lib:format("~ts",[Arg]))}]; event_handler_init_args2opts([]) -> []. @@ -2867,6 +2933,10 @@ opts2args(EnvStartOpts) -> [{allow_user_terms,[]}]; ({allow_user_terms,false}) -> []; + ({join_specs,true}) -> + [{join_specs,[]}]; + ({join_specs,false}) -> + []; ({auto_compile,false}) -> [{no_auto_compile,[]}]; ({auto_compile,true}) -> @@ -2879,6 +2949,8 @@ opts2args(EnvStartOpts) -> []; ({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}) -> @@ -2900,11 +2972,11 @@ opts2args(EnvStartOpts) -> [{event_handler_init,[atom_to_list(EH),ArgStr]}]; ({event_handler,{EHs,Arg}}) when is_list(EHs) -> ArgStr = lists:flatten(io_lib:format("~p", [Arg])), - Strs = lists:map(fun(EH) -> - [atom_to_list(EH), - ArgStr,"and"] - end, EHs), - [_LastAnd|StrsR] = lists:reverse(lists:flatten(Strs)), + 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]}]; @@ -3035,7 +3107,7 @@ start_trace(Args) -> false end; {_,Error} -> - io:format("Warning! Tracing not started. Reason: ~s~n~n", + io:format("Warning! Tracing not started. Reason: ~ts~n~n", [file:format_error(Error)]), false end; diff --git a/lib/common_test/src/ct_slave.erl b/lib/common_test/src/ct_slave.erl index aa3413fa89..872c39de04 100644 --- a/lib/common_test/src/ct_slave.erl +++ b/lib/common_test/src/ct_slave.erl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010. All Rights Reserved. +%% Copyright Ericsson AB 2010-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -37,18 +37,19 @@ -record(options, {username, password, boot_timeout, init_timeout, startup_timeout, startup_functions, monitor_master, - kill_if_fail, erl_flags}). + kill_if_fail, erl_flags, env}). %%%----------------------------------------------------------------- %%% @spec start(Node) -> Result %%% Node = atom() %%% Result = {ok, NodeName} | -%%% {error, already_started, NodeName} | -%%% {error, started_not_connected, NodeName} | -%%% {error, boot_timeout, NodeName} | -%%% {error, init_timeout, NodeName} | -%%% {error, startup_timeout, NodeName} | -%%% {error, not_alive, NodeName} +%%% {error, Reason, NodeName} +%%% Reason = already_started | +%%% started_not_connected | +%%% boot_timeout | +%%% init_timeout | +%%% startup_timeout | +%%% not_alive %%% NodeName = atom() %%% @doc Starts an Erlang node with name <code>Node</code> on the local host. %%% @see start/3 @@ -56,20 +57,28 @@ start(Node) -> start(gethostname(), Node). %%%----------------------------------------------------------------- -%%% @spec start(Host, Node) -> Result -%%% Node = atom() -%%% Host = atom() +%%% @spec start(HostOrNode, NodeOrOpts) -> Result +%%% HostOrNode = atom() +%%% NodeOrOpts = atom() | list() %%% Result = {ok, NodeName} | -%%% {error, already_started, NodeName} | -%%% {error, started_not_connected, NodeName} | -%%% {error, boot_timeout, NodeName} | -%%% {error, init_timeout, NodeName} | -%%% {error, startup_timeout, NodeName} | -%%% {error, not_alive, NodeName} +%%% {error, Reason, NodeName} +%%% Reason = already_started | +%%% started_not_connected | +%%% boot_timeout | +%%% init_timeout | +%%% startup_timeout | +%%% not_alive %%% NodeName = atom() -%%% @doc Starts an Erlang node with name <code>Node</code> on host -%%% <code>Host</code> with the default options. +%%% @doc Starts an Erlang node with default options on a specified +%%% host, or on the local host with specified options. That is, +%%% the call is interpreted as <code>start(Host, Node)</code> when the +%%% second argument is atom-valued and <code>start(Node, Opts)</code> +%%% when it's list-valued. %%% @see start/3 +start(_HostOrNode = Node, _NodeOrOpts = Opts) %% match to satiate edoc + when is_list(Opts) -> + start(gethostname(), Node, Opts); + start(Host, Node) -> start(Host, Node, []). @@ -85,7 +94,8 @@ start(Host, Node) -> %%% {startup_functions, StartupFunctions} | %%% {monitor_master, Monitor} | %%% {kill_if_fail, KillIfFail} | -%%% {erl_flags, ErlangFlags} +%%% {erl_flags, ErlangFlags} | +%%% {env, [{EnvVar,Value}]} %%% Username = string() %%% Password = string() %%% BootTimeout = integer() @@ -99,12 +109,16 @@ start(Host, Node) -> %%% Monitor = bool() %%% KillIfFail = bool() %%% ErlangFlags = string() -%%% Result = {ok, NodeName} | {error, already_started, NodeName} | -%%% {error, started_not_connected, NodeName} | -%%% {error, boot_timeout, NodeName} | -%%% {error, init_timeout, NodeName} | -%%% {error, startup_timeout, NodeName} | -%%% {error, not_alive, NodeName} +%%% EnvVar = string() +%%% Value = string() +%%% Result = {ok, NodeName} | +%%% {error, Reason, NodeName} +%%% Reason = already_started | +%%% started_not_connected | +%%% boot_timeout | +%%% init_timeout | +%%% startup_timeout | +%%% not_alive %%% NodeName = atom() %%% @doc Starts an Erlang node with name <code>Node</code> on host %%% <code>Host</code> as specified by the combination of options in @@ -152,6 +166,9 @@ start(Host, Node) -> %%% <p>Option <code>erlang_flags</code> specifies, which flags will be added %%% to the parameters of the <code>erl</code> executable.</p> %%% +%%% <p>Option <code>env</code> specifies a list of environment variables +%%% that will extended the environment.</p> +%%% %%% <p>Special return values are: %%% <list> %%% <item><code>{error, already_started, NodeName}</code> - if the node with @@ -163,7 +180,7 @@ start(Host, Node) -> %%% <code>NodeName</code> is the name of current node in this case.</item> %%% </list></p> %%% -start(Host, Node, Options) -> +start(Host, Node, Opts) -> ENode = enodename(Host, Node), case erlang:is_alive() of false-> @@ -171,7 +188,7 @@ start(Host, Node, Options) -> true-> case is_started(ENode) of false-> - OptionsRec = fetch_options(Options), + OptionsRec = fetch_options(Opts), do_start(Host, Node, OptionsRec); {true, not_connected}-> {error, started_not_connected, ENode}; @@ -183,9 +200,11 @@ start(Host, Node, Options) -> %%% @spec stop(Node) -> Result %%% Node = atom() %%% Result = {ok, NodeName} | -%%% {error, not_started, NodeName} | -%%% {error, not_connected, NodeName} | -%%% {error, stop_timeout, NodeName} +%%% {error, Reason, NodeName} +%%% Reason = not_started | +%%% not_connected | +%%% stop_timeout + %%% NodeName = atom() %%% @doc Stops the running Erlang node with name <code>Node</code> on %%% the localhost. @@ -196,9 +215,10 @@ stop(Node) -> %%% Host = atom() %%% Node = atom() %%% Result = {ok, NodeName} | -%%% {error, not_started, NodeName} | -%%% {error, not_connected, NodeName} | -%%% {error, stop_timeout, NodeName} +%%% {error, Reason, NodeName} +%%% Reason = not_started | +%%% not_connected | +%%% stop_timeout %%% NodeName = atom() %%% @doc Stops the running Erlang node with name <code>Node</code> on %%% host <code>Host</code>. @@ -233,10 +253,12 @@ fetch_options(Options) -> Monitor = get_option_value(monitor_master, Options, false), KillIfFail = get_option_value(kill_if_fail, Options, true), ErlFlags = get_option_value(erl_flags, Options, []), + EnvVars = get_option_value(env, Options, []), #options{username=UserName, password=Password, boot_timeout=BootTimeout, init_timeout=InitTimeout, startup_timeout=StartupTimeout, startup_functions=StartupFunctions, - monitor_master=Monitor, kill_if_fail=KillIfFail, erl_flags=ErlFlags}. + monitor_master=Monitor, kill_if_fail=KillIfFail, + erl_flags=ErlFlags, env=EnvVars}. % send a message when slave node is started % @hidden @@ -306,11 +328,19 @@ do_start(Host, Node, Options) -> true-> spawn_remote_node(Host, Node, Options) end, + BootTimeout = Options#options.boot_timeout, InitTimeout = Options#options.init_timeout, StartupTimeout = Options#options.startup_timeout, Result = case wait_for_node_alive(ENode, BootTimeout) of pong-> + case test_server:is_cover() of + true -> + MainCoverNode = cover:get_main_node(), + rpc:call(MainCoverNode,cover,start,[ENode]); + false -> + ok + end, call_functions(ENode, Functions2), receive {node_started, ENode}-> @@ -365,9 +395,9 @@ get_cmd(Node, Flags) -> % spawn node locally spawn_local_node(Node, Options) -> - ErlFlags = Options#options.erl_flags, + #options{env=Env,erl_flags=ErlFlags} = Options, Cmd = get_cmd(Node, ErlFlags), - open_port({spawn, Cmd}, [stream]). + open_port({spawn, Cmd}, [stream,{env,Env}]). % start crypto and ssh if not yet started check_for_ssh_running() -> @@ -386,9 +416,10 @@ check_for_ssh_running() -> % spawn node remotely spawn_remote_node(Host, Node, Options) -> - Username = Options#options.username, - Password = Options#options.password, - ErlFlags = Options#options.erl_flags, + #options{username=Username, + password=Password, + erl_flags=ErlFlags, + env=Env} = Options, SSHOptions = case {Username, Password} of {[], []}-> []; @@ -400,8 +431,17 @@ spawn_remote_node(Host, Node, Options) -> check_for_ssh_running(), {ok, SSHConnRef} = ssh:connect(atom_to_list(Host), 22, SSHOptions), {ok, SSHChannelId} = ssh_connection:session_channel(SSHConnRef, infinity), + ssh_setenv(SSHConnRef, SSHChannelId, Env), ssh_connection:exec(SSHConnRef, SSHChannelId, get_cmd(Node, ErlFlags), infinity). + +ssh_setenv(SSHConnRef, SSHChannelId, [{Var, Value} | Vars]) + when is_list(Var), is_list(Value) -> + success = ssh_connection:setenv(SSHConnRef, SSHChannelId, + Var, Value, infinity), + ssh_setenv(SSHConnRef, SSHChannelId, Vars); +ssh_setenv(_SSHConnRef, _SSHChannelId, []) -> ok. + % call functions on a remote Erlang node call_functions(_Node, []) -> ok; @@ -423,8 +463,29 @@ wait_for_node_alive(Node, N) -> % call init:stop on a remote node do_stop(ENode) -> + {Cover,MainCoverNode} = + case test_server:is_cover() of + true -> + Main = cover:get_main_node(), + rpc:call(Main,cover,flush,[ENode]), + {true,Main}; + false -> + {false,undefined} + end, spawn(ENode, init, stop, []), - wait_for_node_dead(ENode, 5). + case wait_for_node_dead(ENode, 5) of + {ok,ENode} -> + if Cover -> + %% To avoid that cover is started again if a node + %% with the same name is started later. + rpc:call(MainCoverNode,cover,stop,[ENode]); + true -> + ok + end, + {ok,ENode}; + Error -> + Error + end. % wait N seconds until node is disconnected wait_for_node_dead(Node, 0) -> diff --git a/lib/common_test/src/ct_snmp.erl b/lib/common_test/src/ct_snmp.erl index 8fe63e8ed1..71038bd4f4 100644 --- a/lib/common_test/src/ct_snmp.erl +++ b/lib/common_test/src/ct_snmp.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2010. All Rights Reserved. +%% Copyright Ericsson AB 2004-2012. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -39,7 +39,7 @@ %%% %%% Manager config %%% [{start_manager, boolean()} % Optional - default is true %%% {users, [{user_name(), [call_back_module(), user_data()]}]}, %% Optional -%%% {usm_users, [{usm_user_name(), usm_config()}]},%% Optional - snmp v3 only +%%% {usm_users, [{usm_user_name(), [usm_config()]}]},%% Optional - snmp v3 only %%% % managed_agents is optional %%% {managed_agents,[{agent_name(), [user_name(), agent_ip(), agent_port(), [agent_config()]]}]}, %%% {max_msg_size, integer()}, % Optional - default is 484 @@ -130,7 +130,7 @@ %%% @type agent_config() = {Item, Value} %%% @type user_name() = atom() %%% @type usm_user_name() = string() -%%% @type usm_config() = string() +%%% @type usm_config() = {Item, Value} %%% @type call_back_module() = atom() %%% @type user_data() = term() %%% @type oids() = [oid()] @@ -157,8 +157,9 @@ %%% API -export([start/2, start/3, stop/1, get_values/3, get_next_values/3, set_values/4, set_info/1, register_users/2, register_agents/2, register_usm_users/2, - unregister_users/1, unregister_agents/1, update_usm_users/2, - load_mibs/1]). + unregister_users/1, unregister_users/2, unregister_agents/1, + unregister_agents/2, unregister_usm_users/1, unregister_usm_users/2, + load_mibs/1, unload_mibs/1]). %% Manager values -define(CT_SNMP_LOG_FILE, "ct_snmp_set.log"). @@ -250,10 +251,8 @@ stop(Config) -> %%% %%% @doc Issues a synchronous snmp get request. get_values(Agent, Oids, MgrAgentConfName) -> - [Uid, AgentIp, AgentUdpPort | _] = - agent_conf(Agent, MgrAgentConfName), - {ok, SnmpReply, _} = - snmpm:g(Uid, AgentIp, AgentUdpPort, Oids), + [Uid | _] = agent_conf(Agent, MgrAgentConfName), + {ok, SnmpReply, _} = snmpm:sync_get2(Uid, target_name(Agent), Oids), SnmpReply. %%% @spec get_next_values(Agent, Oids, MgrAgentConfName) -> SnmpReply @@ -265,10 +264,8 @@ get_values(Agent, Oids, MgrAgentConfName) -> %%% %%% @doc Issues a synchronous snmp get next request. get_next_values(Agent, Oids, MgrAgentConfName) -> - [Uid, AgentIp, AgentUdpPort | _] = - agent_conf(Agent, MgrAgentConfName), - {ok, SnmpReply, _} = - snmpm:gn(Uid, AgentIp, AgentUdpPort, Oids), + [Uid | _] = agent_conf(Agent, MgrAgentConfName), + {ok, SnmpReply, _} = snmpm:sync_get_next2(Uid, target_name(Agent), Oids), SnmpReply. %%% @spec set_values(Agent, VarsAndVals, MgrAgentConfName, Config) -> SnmpReply @@ -282,13 +279,11 @@ get_next_values(Agent, Oids, MgrAgentConfName) -> %%% @doc Issues a synchronous snmp set request. set_values(Agent, VarsAndVals, MgrAgentConfName, Config) -> PrivDir = ?config(priv_dir, Config), - [Uid, AgentIp, AgentUdpPort | _] = - agent_conf(Agent, MgrAgentConfName), + [Uid | _] = agent_conf(Agent, MgrAgentConfName), Oids = lists:map(fun({Oid, _, _}) -> Oid end, VarsAndVals), - {ok, SnmpGetReply, _} = - snmpm:g(Uid, AgentIp, AgentUdpPort, Oids), - {ok, SnmpSetReply, _} = - snmpm:s(Uid, AgentIp, AgentUdpPort, VarsAndVals), + TargetName = target_name(Agent), + {ok, SnmpGetReply, _} = snmpm:sync_get2(Uid, TargetName, Oids), + {ok, SnmpSetReply, _} = snmpm:sync_set2(Uid, TargetName, VarsAndVals), case SnmpSetReply of {noError, 0, _} when PrivDir /= false -> log(PrivDir, Agent, SnmpGetReply, VarsAndVals); @@ -328,12 +323,23 @@ set_info(Config) -> %%% Reason = term() %%% %%% @doc Register the manager entity (=user) responsible for specific agent(s). -%%% Corresponds to making an entry in users.conf +%%% Corresponds to making an entry in users.conf. +%%% +%%% This function will try to register the given users, without +%%% checking if any of them already exist. In order to change an +%%% already registered user, the user must first be unregistered. register_users(MgrAgentConfName, Users) -> - {snmp, SnmpVals} = ct:get_config(MgrAgentConfName), - NewSnmpVals = lists:keyreplace(users, 1, SnmpVals, {users, Users}), - ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}), - setup_users(Users). + case setup_users(Users) of + ok -> + SnmpVals = ct:get_config(MgrAgentConfName), + OldUsers = ct:get_config({MgrAgentConfName,users},[]), + NewSnmpVals = lists:keystore(users, 1, SnmpVals, + {users, Users ++ OldUsers}), + ct_config:update_config(MgrAgentConfName, NewSnmpVals), + ok; + Error -> + Error + end. %%% @spec register_agents(MgrAgentConfName, ManagedAgents) -> ok | {error, Reason} %%% @@ -343,12 +349,24 @@ register_users(MgrAgentConfName, Users) -> %%% %%% @doc Explicitly instruct the manager to handle this agent. %%% Corresponds to making an entry in agents.conf +%%% +%%% This function will try to register the given managed agents, +%%% without checking if any of them already exist. In order to change +%%% an already registered managed agent, the agent must first be +%%% unregistered. register_agents(MgrAgentConfName, ManagedAgents) -> - {snmp, SnmpVals} = ct:get_config(MgrAgentConfName), - NewSnmpVals = lists:keyreplace(managed_agents, 1, SnmpVals, - {managed_agents, ManagedAgents}), - ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}), - setup_managed_agents(ManagedAgents). + case setup_managed_agents(MgrAgentConfName,ManagedAgents) of + ok -> + SnmpVals = ct:get_config(MgrAgentConfName), + OldAgents = ct:get_config({MgrAgentConfName,managed_agents},[]), + NewSnmpVals = lists:keystore(managed_agents, 1, SnmpVals, + {managed_agents, + ManagedAgents ++ OldAgents}), + ct_config:update_config(MgrAgentConfName, NewSnmpVals), + ok; + Error -> + Error + end. %%% @spec register_usm_users(MgrAgentConfName, UsmUsers) -> ok | {error, Reason} %%% @@ -358,60 +376,115 @@ register_agents(MgrAgentConfName, ManagedAgents) -> %%% %%% @doc Explicitly instruct the manager to handle this USM user. %%% Corresponds to making an entry in usm.conf +%%% +%%% This function will try to register the given users, without +%%% checking if any of them already exist. In order to change an +%%% already registered user, the user must first be unregistered. register_usm_users(MgrAgentConfName, UsmUsers) -> - {snmp, SnmpVals} = ct:get_config(MgrAgentConfName), - NewSnmpVals = lists:keyreplace(users, 1, SnmpVals, {usm_users, UsmUsers}), - ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}), EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID), - setup_usm_users(UsmUsers, EngineID). + case setup_usm_users(UsmUsers, EngineID) of + ok -> + SnmpVals = ct:get_config(MgrAgentConfName), + OldUsmUsers = ct:get_config({MgrAgentConfName,usm_users},[]), + NewSnmpVals = lists:keystore(usm_users, 1, SnmpVals, + {usm_users, UsmUsers ++ OldUsmUsers}), + ct_config:update_config(MgrAgentConfName, NewSnmpVals), + ok; + Error -> + Error + end. -%%% @spec unregister_users(MgrAgentConfName) -> ok | {error, Reason} +%%% @spec unregister_users(MgrAgentConfName) -> ok %%% %%% MgrAgentConfName = atom() %%% Reason = term() %%% -%%% @doc Removes information added when calling register_users/2. +%%% @doc Unregister all users. unregister_users(MgrAgentConfName) -> - Users = lists:map(fun({UserName, _}) -> UserName end, - ct:get_config({MgrAgentConfName, users})), - {snmp, SnmpVals} = ct:get_config(MgrAgentConfName), - NewSnmpVals = lists:keyreplace(users, 1, SnmpVals, {users, []}), - ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}), - takedown_users(Users). + Users = [Id || {Id,_} <- ct:get_config({MgrAgentConfName, users},[])], + unregister_users(MgrAgentConfName,Users). -%%% @spec unregister_agents(MgrAgentConfName) -> ok | {error, Reason} +%%% @spec unregister_users(MgrAgentConfName,Users) -> ok %%% %%% MgrAgentConfName = atom() +%%% Users = [user_name()] %%% Reason = term() %%% -%%% @doc Removes information added when calling register_agents/2. +%%% @doc Unregister the given users. +unregister_users(MgrAgentConfName,Users) -> + takedown_users(Users), + SnmpVals = ct:get_config(MgrAgentConfName), + AllUsers = ct:get_config({MgrAgentConfName, users},[]), + RemainingUsers = lists:filter(fun({Id,_}) -> + not lists:member(Id,Users) + end, + AllUsers), + NewSnmpVals = lists:keyreplace(users, 1, SnmpVals, {users,RemainingUsers}), + ct_config:update_config(MgrAgentConfName, NewSnmpVals), + ok. + +%%% @spec unregister_agents(MgrAgentConfName) -> ok +%%% +%%% MgrAgentConfName = atom() +%%% Reason = term() +%%% +%%% @doc Unregister all managed agents. unregister_agents(MgrAgentConfName) -> - ManagedAgents = lists:map(fun({_, [Uid, AgentIP, AgentPort, _]}) -> - {Uid, AgentIP, AgentPort} - end, - ct:get_config({MgrAgentConfName, managed_agents})), - {snmp, SnmpVals} = ct:get_config(MgrAgentConfName), - NewSnmpVals = lists:keyreplace(managed_agents, 1, SnmpVals, - {managed_agents, []}), - ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}), - takedown_managed_agents(ManagedAgents). + ManagedAgents = [AgentName || + {AgentName, _} <- + ct:get_config({MgrAgentConfName,managed_agents},[])], + unregister_agents(MgrAgentConfName,ManagedAgents). +%%% @spec unregister_agents(MgrAgentConfName,ManagedAgents) -> ok +%%% +%%% MgrAgentConfName = atom() +%%% ManagedAgents = [agent_name()] +%%% Reason = term() +%%% +%%% @doc Unregister the given managed agents. +unregister_agents(MgrAgentConfName,ManagedAgents) -> + takedown_managed_agents(MgrAgentConfName, ManagedAgents), + SnmpVals = ct:get_config(MgrAgentConfName), + AllAgents = ct:get_config({MgrAgentConfName,managed_agents},[]), + RemainingAgents = lists:filter(fun({Name,_}) -> + not lists:member(Name,ManagedAgents) + end, + AllAgents), + NewSnmpVals = lists:keyreplace(managed_agents, 1, SnmpVals, + {managed_agents,RemainingAgents}), + ct_config:update_config(MgrAgentConfName, NewSnmpVals), + ok. -%%% @spec update_usm_users(MgrAgentConfName, UsmUsers) -> ok | {error, Reason} +%%% @spec unregister_usm_users(MgrAgentConfName) -> ok %%% %%% MgrAgentConfName = atom() -%%% UsmUsers = usm_users() %%% Reason = term() %%% -%%% @doc Alters information added when calling register_usm_users/2. -update_usm_users(MgrAgentConfName, UsmUsers) -> - - {snmp, SnmpVals} = ct:get_config(MgrAgentConfName), - NewSnmpVals = lists:keyreplace(usm_users, 1, SnmpVals, - {usm_users, UsmUsers}), - ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}), +%%% @doc Unregister all usm users. +unregister_usm_users(MgrAgentConfName) -> + UsmUsers = [Id || {Id,_} <- ct:get_config({MgrAgentConfName, usm_users},[])], + unregister_usm_users(MgrAgentConfName,UsmUsers). + +%%% @spec unregister_usm_users(MgrAgentConfName,UsmUsers) -> ok +%%% +%%% MgrAgentConfName = atom() +%%% UsmUsers = [usm_user_name()] +%%% Reason = term() +%%% +%%% @doc Unregister the given usm users. +unregister_usm_users(MgrAgentConfName,UsmUsers) -> EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID), - do_update_usm_users(UsmUsers, EngineID). + takedown_usm_users(UsmUsers,EngineID), + SnmpVals = ct:get_config(MgrAgentConfName), + AllUsmUsers = ct:get_config({MgrAgentConfName, usm_users},[]), + RemainingUsmUsers = lists:filter(fun({Id,_}) -> + not lists:member(Id,UsmUsers) + end, + AllUsmUsers), + NewSnmpVals = lists:keyreplace(usm_users, 1, SnmpVals, + {usm_users,RemainingUsmUsers}), + ct_config:update_config(MgrAgentConfName, NewSnmpVals), + ok. %%% @spec load_mibs(Mibs) -> ok | {error, Reason} %%% @@ -423,6 +496,15 @@ update_usm_users(MgrAgentConfName, UsmUsers) -> load_mibs(Mibs) -> snmpa:load_mibs(snmp_master_agent, Mibs). +%%% @spec unload_mibs(Mibs) -> ok | {error, Reason} +%%% +%%% Mibs = [MibName] +%%% MibName = string() +%%% Reason = term() +%%% +%%% @doc Unload the mibs from the agent 'snmp_master_agent'. +unload_mibs(Mibs) -> + snmpa:unload_mibs(snmp_master_agent, Mibs). %%%======================================================================== %%% Internal functions @@ -486,9 +568,8 @@ setup_agent(true, AgentConfName, SnmpConfName, file:make_dir(DbDir), snmp_config:write_agent_snmp_files(ConfDir, Vsns, ManagerIP, TrapUdp, AgentIP, AgentUdp, SysName, - atom_to_list(NotifType), - SecType, Passwd, AgentEngineID, - AgentMaxMsgSize), + NotifType, SecType, Passwd, + AgentEngineID, AgentMaxMsgSize), override_default_configuration(Config, AgentConfName), @@ -497,7 +578,8 @@ setup_agent(true, AgentConfName, SnmpConfName, {verbosity, trace}]}, {agent_type, master}, {agent_verbosity, trace}, - {net_if, [{verbosity, trace}]}], + {net_if, [{verbosity, trace}]}, + {versions, Vsns}], ct:get_config({SnmpConfName,agent})), application:set_env(snmp, agent, SnmpEnv). %%%--------------------------------------------------------------------------- @@ -535,65 +617,61 @@ manager_register(true, MgrAgentConfName) -> setup_usm_users(UsmUsers, EngineID), setup_users(Users), - setup_managed_agents(Agents). + setup_managed_agents(MgrAgentConfName,Agents). %%%--------------------------------------------------------------------------- setup_users(Users) -> - lists:foreach(fun({Id, [Module, Data]}) -> - snmpm:register_user(Id, Module, Data) - end, Users). + while_ok(fun({Id, [Module, Data]}) -> + snmpm:register_user(Id, Module, Data) + end, Users). %%%--------------------------------------------------------------------------- -setup_managed_agents([]) -> - ok; - -setup_managed_agents([{_, [Uid, AgentIp, AgentUdpPort, AgentConf]} | - Rest]) -> - NewAgentIp = case AgentIp of - IpTuple when is_tuple(IpTuple) -> - IpTuple; - HostName when is_list(HostName) -> - {ok,Hostent} = inet:gethostbyname(HostName), - [IpTuple|_] = Hostent#hostent.h_addr_list, - IpTuple - end, - ok = snmpm:register_agent(Uid, NewAgentIp, AgentUdpPort), - lists:foreach(fun({Item, Val}) -> - snmpm:update_agent_info(Uid, NewAgentIp, - AgentUdpPort, Item, Val) - end, AgentConf), - setup_managed_agents(Rest). +setup_managed_agents(AgentConfName,Agents) -> + Fun = + fun({AgentName, [Uid, AgentIp, AgentUdpPort, AgentConf0]}) -> + NewAgentIp = case AgentIp of + IpTuple when is_tuple(IpTuple) -> + IpTuple; + HostName when is_list(HostName) -> + {ok,Hostent} = inet:gethostbyname(HostName), + [IpTuple|_] = Hostent#hostent.h_addr_list, + IpTuple + end, + AgentConf = + case lists:keymember(engine_id,1,AgentConf0) of + true -> + AgentConf0; + false -> + DefaultEngineID = + ct:get_config({AgentConfName,agent_engine_id}, + ?AGENT_ENGINE_ID), + [{engine_id,DefaultEngineID}|AgentConf0] + end, + snmpm:register_agent(Uid, target_name(AgentName), + [{address,NewAgentIp},{port,AgentUdpPort} | + AgentConf]) + end, + while_ok(Fun,Agents). %%%--------------------------------------------------------------------------- setup_usm_users(UsmUsers, EngineID)-> - lists:foreach(fun({UsmUser, Conf}) -> - snmpm:register_usm_user(EngineID, UsmUser, Conf) - end, UsmUsers). + while_ok(fun({UsmUser, Conf}) -> + snmpm:register_usm_user(EngineID, UsmUser, Conf) + end, UsmUsers). %%%--------------------------------------------------------------------------- takedown_users(Users) -> - lists:foreach(fun({Id}) -> + lists:foreach(fun(Id) -> snmpm:unregister_user(Id) end, Users). %%%--------------------------------------------------------------------------- -takedown_managed_agents([{Uid, AgentIp, AgentUdpPort} | - Rest]) -> - NewAgentIp = case AgentIp of - IpTuple when is_tuple(IpTuple) -> - IpTuple; - HostName when is_list(HostName) -> - {ok,Hostent} = inet:gethostbyname(HostName), - [IpTuple|_] = Hostent#hostent.h_addr_list, - IpTuple - end, - ok = snmpm:unregister_agent(Uid, NewAgentIp, AgentUdpPort), - takedown_managed_agents(Rest); - -takedown_managed_agents([]) -> - ok. +takedown_managed_agents(MgrAgentConfName,ManagedAgents) -> + lists:foreach(fun(AgentName) -> + [Uid | _] = agent_conf(AgentName, MgrAgentConfName), + snmpm:unregister_agent(Uid, target_name(AgentName)) + end, ManagedAgents). %%%--------------------------------------------------------------------------- -do_update_usm_users(UsmUsers, EngineID) -> - lists:foreach(fun({UsmUser, {Item, Val}}) -> - snmpm:update_usm_user_info(EngineID, UsmUser, - Item, Val) - end, UsmUsers). +takedown_usm_users(UsmUsers, EngineID) -> + lists:foreach(fun(Id) -> + snmpm:unregister_usm_user(EngineID, Id) + end, UsmUsers). %%%--------------------------------------------------------------------------- log(PrivDir, Agent, {_, _, Varbinds}, NewVarsAndVals) -> @@ -657,7 +735,7 @@ override_contexts(Config, {data_dir_file, File}) -> override_contexts(Config, ContextInfo); override_contexts(Config, Contexts) -> - Dir = ?config(priv_dir, Config), + Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"context.conf"), file:delete(File), snmp_config:write_agent_context_config(Dir, "", Contexts). @@ -673,7 +751,7 @@ override_sysinfo(Config, {data_dir_file, File}) -> override_sysinfo(Config, SysInfo); override_sysinfo(Config, SysInfo) -> - Dir = ?config(priv_dir, Config), + Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"standard.conf"), file:delete(File), snmp_config:write_agent_standard_config(Dir, "", SysInfo). @@ -688,7 +766,7 @@ override_target_address(Config, {data_dir_file, File}) -> override_target_address(Config, TargetAddressConf); override_target_address(Config, TargetAddressConf) -> - Dir = ?config(priv_dir, Config), + Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"target_addr.conf"), file:delete(File), snmp_config:write_agent_target_addr_config(Dir, "", TargetAddressConf). @@ -704,7 +782,7 @@ override_target_params(Config, {data_dir_file, File}) -> override_target_params(Config, TargetParamsConf); override_target_params(Config, TargetParamsConf) -> - Dir = ?config(priv_dir, Config), + Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"target_params.conf"), file:delete(File), snmp_config:write_agent_target_params_config(Dir, "", TargetParamsConf). @@ -719,7 +797,7 @@ override_notify(Config, {data_dir_file, File}) -> override_notify(Config, NotifyConf); override_notify(Config, NotifyConf) -> - Dir = ?config(priv_dir, Config), + Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"notify.conf"), file:delete(File), snmp_config:write_agent_notify_config(Dir, "", NotifyConf). @@ -734,7 +812,7 @@ override_usm(Config, {data_dir_file, File}) -> override_usm(Config, UsmConf); override_usm(Config, UsmConf) -> - Dir = ?config(priv_dir, Config), + Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"usm.conf"), file:delete(File), snmp_config:write_agent_usm_config(Dir, "", UsmConf). @@ -749,7 +827,7 @@ override_community(Config, {data_dir_file, File}) -> override_community(Config, CommunityConf); override_community(Config, CommunityConf) -> - Dir = ?config(priv_dir, Config), + Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"community.conf"), file:delete(File), snmp_config:write_agent_community_config(Dir, "", CommunityConf). @@ -765,7 +843,20 @@ override_vacm(Config, {data_dir_file, File}) -> override_vacm(Config, VacmConf); override_vacm(Config, VacmConf) -> - Dir = ?config(priv_dir, Config), - File = filename:join(Dir,"vacm.conf"), + Dir = filename:join(?config(priv_dir, Config),"conf"), + File = filename:join(Dir,"vacm.conf"), file:delete(File), snmp_config:write_agent_vacm_config(Dir, "", VacmConf). + +%%%--------------------------------------------------------------------------- + +target_name(Agent) -> + atom_to_list(Agent). + +while_ok(Fun,[H|T]) -> + case Fun(H) of + ok -> while_ok(Fun,T); + Error -> Error + end; +while_ok(_Fun,[]) -> + ok. diff --git a/lib/common_test/src/ct_ssh.erl b/lib/common_test/src/ct_ssh.erl index c6ea27b10e..974791bd70 100644 --- a/lib/common_test/src/ct_ssh.erl +++ b/lib/common_test/src/ct_ssh.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2009-2012. All Rights Reserved. +%% Copyright Ericsson AB 2009-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -1328,10 +1328,10 @@ do_recv_response(SSH, Chn, Data, End, Timeout) -> get_handle(SSH) when is_pid(SSH) -> {ok,SSH}; get_handle(SSH) -> - case ct_util:get_connections(SSH, ?MODULE) of - {ok,[{Pid,_}]} -> + case ct_util:get_connection(SSH, ?MODULE) of + {ok,{Pid,_}} -> {ok,Pid}; - {ok,[]} -> + {error,no_registered_connection} -> connect(SSH); Error -> Error diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl index b13c050e32..4092d33bc0 100644 --- a/lib/common_test/src/ct_telnet.erl +++ b/lib/common_test/src/ct_telnet.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -183,7 +183,8 @@ open(KeyOrName,ConnType,TargetMod,Extra) -> end; Bool -> Bool end, - log(heading(open,{KeyOrName,ConnType}),"Opening connection to: ~p",[Addr1]), + log(heading(open,{KeyOrName,ConnType}), + "Opening connection to: ~p",[Addr1]), ct_gen_conn:start(KeyOrName,full_addr(Addr1,ConnType), {TargetMod,KeepAlive,Extra},?MODULE) end. @@ -201,7 +202,7 @@ open(KeyOrName,ConnType,TargetMod,Extra) -> close(Connection) -> case get_handle(Connection) of {ok,Pid} -> - log("ct_telnet:close","Handle: ~p",[Pid]), + log("ct_telnet:close","Handle: ~w",[Pid]), case ct_gen_conn:stop(Pid) of {error,{process_down,Pid,noproc}} -> {error,already_closed}; @@ -309,7 +310,7 @@ expect(Connection,Patterns) -> %%% Tag = term() %%% Opts = [Opt] %%% Opt = {timeout,Timeout} | repeat | {repeat,N} | sequence | -%%% {halt,HaltPatterns} | ignore_prompt +%%% {halt,HaltPatterns} | ignore_prompt | no_prompt_check %%% Timeout = integer() %%% N = integer() %%% HaltPatterns = Patterns @@ -336,14 +337,28 @@ expect(Connection,Patterns) -> %%% will also include the matched <code>Tag</code>. Else, only %%% <code>RxMatch</code> is returned.</p> %%% -%%% <p>The function will always return when a prompt is found, unless -%%% the <code>ignore_prompt</code> options is used.</p> -%%% %%% <p>The <code>timeout</code> option indicates that the function %%% shall return if the telnet client is idle (i.e. if no data is %%% received) for more than <code>Timeout</code> milliseconds. Default %%% timeout is 10 seconds.</p> %%% +%%% <p>The function will always return when a prompt is found, unless +%%% any of the <code>ignore_prompt</code> or +%%% <code>no_prompt_check</code> options are used, in which case it +%%% will return when a match is found or after a timeout.</p> +%%% +%%% <p>If the <code>ignore_prompt</code> option is used, +%%% <code>ct_telnet</code> will ignore any prompt found. This option +%%% is useful if data sent by the server could include a pattern that +%%% would match the prompt regexp (as returned by +%%% <code>TargedMod:get_prompt_regexp/0</code>), but which should not +%%% cause the function to return.</p> +%%% +%%% <p>If the <code>no_prompt_check</code> option is used, +%%% <code>ct_telnet</code> will not search for a prompt at all. This +%%% is useful if, for instance, the <code>Pattern</code> itself +%%% matches the prompt.</p> +%%% %%% <p>The <code>repeat</code> option indicates that the pattern(s) %%% shall be matched multiple times. If <code>N</code> is given, the %%% pattern(s) will be matched <code>N</code> times, and the function @@ -558,7 +573,7 @@ reconnect(Ip,Port,N,State=#state{target_mod=TargetMod, Error when N==0 -> Error; _Error -> - log("Reconnect failed!","Retries left: ~p",[N]), + log("Reconnect failed!","Retries left: ~w",[N]), timer:sleep(ReconnInt), reconnect(Ip,Port,N-1,State) end. @@ -567,7 +582,7 @@ reconnect(Ip,Port,N,State=#state{target_mod=TargetMod, %% @hidden terminate(TelnPid,State) -> log(heading(terminate,State#state.name), - "Closing telnet connection.\nId: ~p", + "Closing telnet connection.\nId: ~w", [TelnPid]), ct_telnet_client:close(TelnPid). @@ -577,9 +592,9 @@ terminate(TelnPid,State) -> get_handle(Pid) when is_pid(Pid) -> {ok,Pid}; get_handle({Name,Type}) when Type==telnet;Type==ts1;Type==ts2 -> - case ct_util:get_connections(Name,?MODULE) of - {ok,Conns} when Conns /= [] -> - case get_handle(Type,Conns) of + case ct_util:get_connection(Name,?MODULE) of + {ok,Conn} -> + case get_handle(Type,Conn) of {ok,Pid} -> {ok,Pid}; _Error -> @@ -594,19 +609,15 @@ get_handle({Name,Type}) when Type==telnet;Type==ts1;Type==ts2 -> Error end end; - {ok,[]} -> - {error,already_closed}; Error -> Error end; get_handle(Name) -> get_handle({Name,telnet}). -get_handle(Type,[{Pid,{_,_,Type}}|_]) -> +get_handle(Type,{Pid,{_,_,Type}}) -> {ok,Pid}; -get_handle(Type,[_H|T]) -> - get_handle(Type,T); -get_handle(Type,[]) -> +get_handle(Type,_) -> {error,{no_such_connection,Type}}. full_addr({Ip,Port},Type) -> @@ -728,7 +739,8 @@ teln_get_all_data(Pid,Prx,Data,Acc,LastLine) -> haltpatterns=[], seq=false, repeat=false, - found_prompt=false}). + found_prompt=false, + prompt_check=true}). %% @hidden %% @doc Externally the silent_teln_expect function shall only be used @@ -754,20 +766,27 @@ silent_teln_expect(Pid,Data,Pattern,Prx,Opts) -> %% condition is fullfilled. %% 3b) Repeat (sequence): 2) is repeated either N times or until a %% halt condition is fullfilled. -teln_expect(Pid,Data,Pattern0,Prx,Opts) -> HaltPatterns = case - get_ignore_prompt(Opts) of true -> get_haltpatterns(Opts); false - -> [prompt | get_haltpatterns(Opts)] end, +teln_expect(Pid,Data,Pattern0,Prx,Opts) -> + HaltPatterns = + case get_ignore_prompt(Opts) of + true -> + get_haltpatterns(Opts); + false -> + [prompt | get_haltpatterns(Opts)] + end, + PromptCheck = get_prompt_check(Opts), Seq = get_seq(Opts), Pattern = convert_pattern(Pattern0,Seq), Timeout = get_timeout(Opts), - + EO = #eo{teln_pid=Pid, prx=Prx, timeout=Timeout, seq=Seq, - haltpatterns=HaltPatterns}, + haltpatterns=HaltPatterns, + prompt_check=PromptCheck}, case get_repeat(Opts) of false -> @@ -831,7 +850,9 @@ get_haltpatterns(Opts) -> end. get_ignore_prompt(Opts) -> lists:member(ignore_prompt,Opts). - +get_prompt_check(Opts) -> + not lists:member(no_prompt_check,Opts). + %% Repeat either single or sequence. All match results are accumulated %% and returned when a halt condition is fulllfilled. repeat_expect(Rest,_Pattern,Acc,#eo{repeat=0}) -> @@ -892,6 +913,9 @@ get_data1(Pid) -> %% lines and each line is matched against each pattern. %% one_expect: split data chunk at prompts +one_expect(Data,Pattern,EO) when EO#eo.prompt_check==false -> +% io:format("Raw Data ~p Pattern ~p EO ~p ",[Data,Pattern,EO]), + one_expect1(Data,Pattern,[],EO#eo{found_prompt=false}); one_expect(Data,Pattern,EO) -> case match_prompt(Data,EO#eo.prx) of {prompt,UptoPrompt,PromptType,Rest} -> @@ -899,7 +923,7 @@ one_expect(Data,Pattern,EO) -> [Prompt] when Prompt==prompt; Prompt=={prompt,PromptType} -> %% Only searching for prompt log_lines(UptoPrompt), - try_cont_log("<b>PROMPT:</b> ~s", [PromptType]), + try_cont_log("<b>PROMPT:</b> ~ts", [PromptType]), {match,{prompt,PromptType},Rest}; [{prompt,_OtherPromptType}] -> %% Only searching for one specific prompt, not thisone @@ -950,6 +974,8 @@ seq_expect(Data,[],Acc,_EO) -> {match,lists:reverse(Acc),Data}; seq_expect([],Patterns,Acc,_EO) -> {continue,Patterns,lists:reverse(Acc),[]}; +seq_expect(Data,Patterns,Acc,EO) when EO#eo.prompt_check==false -> + seq_expect1(Data,Patterns,Acc,[],EO#eo{found_prompt=false}); seq_expect(Data,Patterns,Acc,EO) -> case match_prompt(Data,EO#eo.prx) of {prompt,UptoPrompt,PromptType,Rest} -> @@ -969,7 +995,7 @@ seq_expect1(Data,[prompt|Patterns],Acc,Rest,EO) -> {continue,[prompt|Patterns],Acc,LastLine}; PromptType -> log_lines(Data), - try_cont_log("<b>PROMPT:</b> ~s", [PromptType]), + try_cont_log("<b>PROMPT:</b> ~ts", [PromptType]), seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO) end; seq_expect1(Data,[{prompt,PromptType}|Patterns],Acc,Rest,EO) -> @@ -980,7 +1006,7 @@ seq_expect1(Data,[{prompt,PromptType}|Patterns],Acc,Rest,EO) -> {continue,[{prompt,PromptType}|Patterns],Acc,LastLine}; PromptType -> log_lines(Data), - try_cont_log("<b>PROMPT:</b> ~s", [PromptType]), + try_cont_log("<b>PROMPT:</b> ~ts", [PromptType]), seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO); _OtherPromptType -> log_lines(Data), @@ -1013,6 +1039,13 @@ match_lines(Data,Patterns,EO) -> {Tag,Match} -> {Tag,Match,[]} end; + {noline,Rest} when EO#eo.prompt_check==false -> + case match_line(Rest,Patterns,false,EO) of + nomatch -> + {nomatch,Rest}; + {Tag,Match} -> + {Tag,Match,[]} + end; {noline,Rest} -> {nomatch,Rest}; {Line,Rest} -> @@ -1032,13 +1065,13 @@ match_line(Line,Patterns,FoundPrompt,EO) -> match_line(Line,[prompt|Patterns],false,EO,RetTag) -> match_line(Line,Patterns,false,EO,RetTag); match_line(Line,[prompt|_Patterns],FoundPrompt,_EO,RetTag) -> - try_cont_log(" ~s", [Line]), - try_cont_log("<b>PROMPT:</b> ~s", [FoundPrompt]), + try_cont_log(" ~ts", [Line]), + try_cont_log("<b>PROMPT:</b> ~ts", [FoundPrompt]), {RetTag,{prompt,FoundPrompt}}; match_line(Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_EO,RetTag) when PromptType==FoundPrompt -> - try_cont_log(" ~s", [Line]), - try_cont_log("<b>PROMPT:</b> ~s", [FoundPrompt]), + try_cont_log(" ~ts", [Line]), + try_cont_log("<b>PROMPT:</b> ~ts", [FoundPrompt]), {RetTag,{prompt,FoundPrompt}}; match_line(Line,[{prompt,PromptType}|Patterns],FoundPrompt,EO,RetTag) when PromptType=/=FoundPrompt -> @@ -1048,7 +1081,7 @@ match_line(Line,[{Tag,Pattern}|Patterns],FoundPrompt,EO,RetTag) -> nomatch -> match_line(Line,Patterns,FoundPrompt,EO,RetTag); {match,Match} -> - try_cont_log("<b>MATCH:</b> ~s", [Line]), + try_cont_log("<b>MATCH:</b> ~ts", [Line]), {RetTag,{Tag,Match}} end; match_line(Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) -> @@ -1056,13 +1089,13 @@ match_line(Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) -> nomatch -> match_line(Line,Patterns,FoundPrompt,EO,RetTag); {match,Match} -> - try_cont_log("<b>MATCH:</b> ~s", [Line]), + try_cont_log("<b>MATCH:</b> ~ts", [Line]), {RetTag,Match} end; match_line(Line,[],FoundPrompt,EO,match) -> match_line(Line,EO#eo.haltpatterns,FoundPrompt,EO,halt); match_line(Line,[],_FoundPrompt,_EO,halt) -> - try_cont_log(" ~s", [Line]), + try_cont_log(" ~ts", [Line]), nomatch. one_line([$\n|Rest],Line) -> @@ -1086,7 +1119,7 @@ log_lines(String) -> [] -> ok; LastLine -> - try_cont_log(" ~s", [LastLine]) + try_cont_log(" ~ts", [LastLine]) end. log_lines_not_last(String) -> @@ -1094,7 +1127,7 @@ log_lines_not_last(String) -> {[],LastLine} -> LastLine; {String1,LastLine} -> - try_cont_log("~s",[String1]), + try_cont_log("~ts",[String1]), LastLine end. diff --git a/lib/common_test/src/ct_telnet_client.erl b/lib/common_test/src/ct_telnet_client.erl index d703b39ac5..2cbcba9c77 100644 --- a/lib/common_test/src/ct_telnet_client.erl +++ b/lib/common_test/src/ct_telnet_client.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2010. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -143,7 +143,9 @@ loop(State, Sock, Acc) -> State end; _ -> - Pid ! {data,lists:reverse(lists:append(Acc))}, + Data = lists:reverse(lists:append(Acc)), + dbg("get_data ~p\n",[Data]), + Pid ! {data,Data}, State end, loop(NewState, Sock, []); @@ -161,7 +163,9 @@ loop(State, Sock, Acc) -> NewAcc = case erlang:is_process_alive(Pid) of true -> - Pid ! {data,lists:reverse(lists:append(Acc))}, + Data = lists:reverse(lists:append(Acc)), + dbg("get_data_delayed ~p\n",[Data]), + Pid ! {data,Data}, []; false -> Acc @@ -308,7 +312,7 @@ cmd_dbg(_Cmd) -> [Opt] -> Opt; _ -> Opts end, - io:format("~s(~w): ~w\n", [CtrlStr,Ctrl,Opts1]); + io:format("~ts(~w): ~w\n", [CtrlStr,Ctrl,Opts1]); Any -> io:format("Unexpected in cmd_dbg:~n~w~n",[Any]) end. diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl index 5ce095e38e..c07ea323e6 100644 --- a/lib/common_test/src/ct_testspec.erl +++ b/lib/common_test/src/ct_testspec.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2012. All Rights Reserved. +%% Copyright Ericsson AB 2006-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -28,7 +28,6 @@ collect_tests_from_file/2, collect_tests_from_file/3]). -include("ct_util.hrl"). - -define(testspec_fields, record_info(fields, testspec)). %%%------------------------------------------------------------------ @@ -93,7 +92,7 @@ prepare_tests(TestSpec) when is_record(TestSpec,testspec) -> %% run_per_node/2 takes the Run list as input and returns a list %% of {Node,RunPerNode,[]} tuples where the tests have been sorted %% on a per node basis. -run_per_node([{{Node,Dir},Test}|Ts],Result, MergeTests) -> +run_per_node([{{Node,Dir},Test}|Ts],Result,MergeTests) -> {value,{Node,{Run,Skip}}} = lists:keysearch(Node,1,Result), Run1 = case MergeTests of false -> @@ -190,7 +189,7 @@ prepare_suites(_Node,_Dir,[],Run,Skip) -> prepare_cases(Node,Dir,Suite,Cases) -> case get_skipped_cases(Node,Dir,Suite,Cases) of - SkipAll=[{{Node,Dir},{Suite,_Cmt}}] -> % all cases to be skipped + SkipAll=[{{Node,Dir},{Suite,_Cmt}}] -> % all cases to be skipped %% note: this adds an 'all' test even if only skip is specified {[{{Node,Dir},{Suite,all}}],SkipAll}; Skipped -> @@ -241,34 +240,155 @@ get_skipped_cases1(_,_,_,[]) -> %%% collect_tests_from_file reads a testspec file and returns a record %%% containing the data found. -collect_tests_from_file(Specs, Relaxed) -> +collect_tests_from_file(Specs,Relaxed) -> collect_tests_from_file(Specs,[node()],Relaxed). collect_tests_from_file(Specs,Nodes,Relaxed) when is_list(Nodes) -> NodeRefs = lists:map(fun(N) -> {undefined,N} end, Nodes), - catch collect_tests_from_file1(Specs,#testspec{nodes=NodeRefs},Relaxed). + %% [Spec1,Spec2,...] means create one testpec record per Spec file + %% [[Spec1,Spec2,...]] means merge all specs into one testspec record + {Join,Specs1} = if is_list(hd(hd(Specs))) -> {true,hd(Specs)}; + true -> {false,Specs} + end, + Specs2 = [filename:absname(S) || S <- Specs1], + TS0 = #testspec{nodes=NodeRefs}, + + try create_testspecs(Specs2,TS0,Relaxed,Join) of + {{[],_},SeparateTestSpecs} -> + filter_and_convert(SeparateTestSpecs); + {{_,#testspec{tests=[]}},SeparateTestSpecs} -> + filter_and_convert(SeparateTestSpecs); + {Joined,SeparateTestSpecs} -> + [filter_and_convert(Joined) | + filter_and_convert(SeparateTestSpecs)] + catch + _:Error={error,_} -> + Error; + _:Error -> + {error,Error} + end. + +filter_and_convert(Joined) when is_tuple(Joined) -> + hd(filter_and_convert([Joined])); +filter_and_convert([{_,#testspec{tests=[]}}|TSs]) -> + filter_and_convert(TSs); +filter_and_convert([{[{SpecFile,MergeTests}|SMs],TestSpec}|TSs]) -> + #testspec{config = CfgFiles} = TestSpec, + TestSpec1 = TestSpec#testspec{config = delete_dups(CfgFiles), + merge_tests = MergeTests}, + %% set the merge_tests value for the testspec to the value + %% of the first test spec in the set + [{[SpecFile | [SF || {SF,_} <- SMs]], TestSpec1} | filter_and_convert(TSs)]; +filter_and_convert([]) -> + []. + +delete_dups(Elems) -> + delete_dups1(lists:reverse(Elems),[]). -collect_tests_from_file1([Spec|Specs],TestSpec,Relaxed) -> +delete_dups1([E|Es],Keep) -> + case lists:member(E,Es) of + true -> + delete_dups1(Es,Keep); + false -> + delete_dups1(Es,[E|Keep]) + end; +delete_dups1([],Keep) -> + Keep. + +create_testspecs(Specs,TestSpec,Relaxed,Join) -> + %% SpecsTree = {SpecAbsName, TermsInSpec, + %% IncludedJoinTree, IncludedSeparateTree, + %% JoinSpecWithRest, RestSpecsTree} + SpecsTree = create_spec_tree(Specs,TestSpec,Join,[]), + create_specs(SpecsTree,TestSpec,TestSpec,Relaxed). + +create_spec_tree([Spec|Specs],TS,JoinWithNext,Known) -> SpecDir = filename:dirname(filename:absname(Spec)), - case file:consult(Spec) of - {ok,Terms} -> - case collect_tests(Terms, - TestSpec#testspec{spec_dir=SpecDir}, - Relaxed) of - TS = #testspec{tests=Tests, logdir=LogDirs} when Specs == [] -> - LogDirs1 = lists:delete(".",LogDirs) ++ ["."], - TS#testspec{tests=lists:flatten(Tests), logdir=LogDirs1}; - TS = #testspec{alias = As, nodes = Ns} -> - TS1 = TS#testspec{alias = lists:reverse(As), - nodes = lists:reverse(Ns)}, - collect_tests_from_file1(Specs,TS1,Relaxed) - end; - {error,Reason} -> - ReasonStr = - lists:flatten(io_lib:format("~s", - [file:format_error(Reason)])), - throw({error,{Spec,ReasonStr}}) - end. + TS1 = TS#testspec{spec_dir=SpecDir}, + SpecAbsName = get_absfile(Spec,TS1), + case lists:member(SpecAbsName,Known) of + true -> + throw({error,{cyclic_reference,SpecAbsName}}); + false -> + case file:consult(SpecAbsName) of + {ok,Terms} -> + Terms1 = replace_names(Terms), + {InclJoin,InclSep} = get_included_specs(Terms1,TS1), + {SpecAbsName,Terms1, + create_spec_tree(InclJoin,TS,true,[SpecAbsName|Known]), + create_spec_tree(InclSep,TS,false,[SpecAbsName|Known]), + JoinWithNext, + create_spec_tree(Specs,TS,JoinWithNext,Known)}; + {error,Reason} -> + ReasonStr = + lists:flatten(io_lib:format("~s", + [file:format_error(Reason)])), + throw({error,{SpecAbsName,ReasonStr}}) + end + end; +create_spec_tree([],_TS,_JoinWithNext,_Known) -> + []. + +create_specs({Spec,Terms,InclJoin,InclSep,JoinWithNext,NextSpec}, + TestSpec,TestSpec0,Relaxed) -> + SpecDir = filename:dirname(filename:absname(Spec)), + TestSpec1 = create_spec(Terms,TestSpec#testspec{spec_dir=SpecDir}, + JoinWithNext,Relaxed), + + {{JoinSpecs1,JoinTS1},Separate1} = create_specs(InclJoin,TestSpec1, + TestSpec0,Relaxed), + + {{JoinSpecs2,JoinTS2},Separate2} = + case JoinWithNext of + true -> + create_specs(NextSpec,JoinTS1, + TestSpec0,Relaxed); + false -> + {{[],JoinTS1},[]} + end, + {SepJoinSpecs,Separate3} = create_specs(InclSep,TestSpec0, + TestSpec0,Relaxed), + {SepJoinSpecs1,Separate4} = + case JoinWithNext of + true -> + {{[],TestSpec},[]}; + false -> + create_specs(NextSpec,TestSpec0, + TestSpec0,Relaxed) + end, + + SpecInfo = {Spec,TestSpec1#testspec.merge_tests}, + AllSeparate = + [TSData || TSData = {Ss,_TS} <- Separate3++Separate1++ + [SepJoinSpecs]++Separate2++ + [SepJoinSpecs1]++Separate4, + Ss /= []], + case {JoinWithNext,JoinSpecs1} of + {true,_} -> + {{[SpecInfo|(JoinSpecs1++JoinSpecs2)],JoinTS2}, + AllSeparate}; + {false,[]} -> + {{[],TestSpec}, + [{[SpecInfo],TestSpec1}|AllSeparate]}; + {false,_} -> + {{[SpecInfo|(JoinSpecs1++JoinSpecs2)],JoinTS2}, + AllSeparate} + end; +create_specs([],TestSpec,_,_Relaxed) -> + {{[],TestSpec},[]}. + +create_spec(Terms,TestSpec,JoinedByPrev,Relaxed) -> + %% it's the "includer" that decides the value of merge_tests + Terms1 = if not JoinedByPrev -> + [{set_merge_tests,true}|Terms]; + true -> + [{set_merge_tests,false}|Terms] + end, + TS = #testspec{tests=Tests, logdir=LogDirs} = + collect_tests({false,Terms1},TestSpec,Relaxed), + LogDirs1 = lists:delete(".",LogDirs) ++ ["."], + TS#testspec{tests=lists:flatten(Tests), + logdir=LogDirs1}. collect_tests_from_list(Terms,Relaxed) -> collect_tests_from_list(Terms,[node()],Relaxed). @@ -276,8 +396,8 @@ collect_tests_from_list(Terms,Relaxed) -> collect_tests_from_list(Terms,Nodes,Relaxed) when is_list(Nodes) -> {ok,Cwd} = file:get_cwd(), NodeRefs = lists:map(fun(N) -> {undefined,N} end, Nodes), - case catch collect_tests(Terms,#testspec{nodes=NodeRefs, - spec_dir=Cwd}, + case catch collect_tests({true,Terms},#testspec{nodes=NodeRefs, + spec_dir=Cwd}, Relaxed) of E = {error,_} -> E; @@ -287,13 +407,28 @@ collect_tests_from_list(Terms,Nodes,Relaxed) when is_list(Nodes) -> TS#testspec{tests=lists:flatten(Tests), logdir=LogDirs1} end. -collect_tests(Terms,TestSpec,Relaxed) -> +collect_tests({Replace,Terms},TestSpec=#testspec{alias=As,nodes=Ns},Relaxed) -> put(relaxed,Relaxed), - Terms1 = replace_names(Terms), - TestSpec1 = get_global(Terms1,TestSpec), - TestSpec2 = get_all_nodes(Terms1,TestSpec1), - {Terms2, TestSpec3} = filter_init_terms(Terms1, [], TestSpec2), - add_tests(Terms2,TestSpec3). + Terms1 = if Replace -> replace_names(Terms); + true -> Terms + end, + {MergeTestsDef,Terms2} = + case proplists:get_value(set_merge_tests,Terms1,true) of + false -> + %% disable merge_tests + {TestSpec#testspec.merge_tests, + proplists:delete(merge_tests,Terms1)}; + true -> + {true,Terms1} + end, + %% reverse nodes and aliases initially to get the order of them right + %% in case this spec is being joined with a previous one + TestSpec1 = get_global(Terms2,TestSpec#testspec{alias = lists:reverse(As), + nodes = lists:reverse(Ns), + merge_tests = MergeTestsDef}), + TestSpec2 = get_all_nodes(Terms2,TestSpec1), + {Terms3, TestSpec3} = filter_init_terms(Terms2, [], TestSpec2), + add_tests(Terms3,TestSpec3). %% replace names (atoms) in the testspec matching those in 'define' terms by %% searching recursively through tuples and lists @@ -420,9 +555,30 @@ replace_names_in_node1(NodeStr,Defs=[{Name,Replacement}|Ds]) -> replace_names_in_node1(NodeStr,[]) -> NodeStr. +%% look for other specification files, either to join with the +%% current spec, or execute as separate test runs +get_included_specs(Terms,TestSpec) -> + get_included_specs(Terms,TestSpec,[],[]). + +get_included_specs([{specs,How,SpecOrSpecs}|Ts],TestSpec,Join,Sep) -> + Specs = case SpecOrSpecs of + [File|_] when is_list(File) -> + [get_absfile(Spec,TestSpec) || Spec <- SpecOrSpecs]; + [Ch|_] when is_integer(Ch) -> + [get_absfile(SpecOrSpecs,TestSpec)] + end, + if How == join -> + get_included_specs(Ts,TestSpec,Join++Specs,Sep); + true -> + get_included_specs(Ts,TestSpec,Join,Sep++Specs) + end; +get_included_specs([_|Ts],TestSpec,Join,Sep) -> + get_included_specs(Ts,TestSpec,Join,Sep); +get_included_specs([],_,Join,Sep) -> + {Join,Sep}. %% global terms that will be used for analysing all other terms in the spec -get_global([{merge_tests,Bool} | Ts], Spec) -> +get_global([{merge_tests,Bool}|Ts],Spec) -> get_global(Ts,Spec#testspec{merge_tests=Bool}); %% the 'define' term replaces the 'alias' and 'node' terms, but we need to keep @@ -588,7 +744,7 @@ add_option({Key,Value},Node,List,WarnIfExists) when is_list(Value) -> NewOption = case lists:keyfind(Key,1,OldOptions) of {Key,OldOption} when WarnIfExists,OldOption/=[]-> io:format("There is an option ~w=~w already " - "defined for node ~p, skipping new ~w~n", + "defined for node ~w, skipping new ~w~n", [Key,OldOption,Node,Value]), OldOption; {Key,OldOption}-> @@ -637,7 +793,7 @@ add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec) -> add_tests([{suites,Dir,Ss}|Ts],Spec) -> add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec); add_tests([{suites,Nodes,Dir,Ss}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,suites,[Dir,Ss],Ts,Spec#testspec.nodes), + Ts1 = per_node(Nodes,suites,[Dir,Ss],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{suites,Node,Dir,Ss}|Ts],Spec) -> Tests = Spec#testspec.tests, @@ -660,11 +816,11 @@ add_tests([{groups,Dir,Suite,Gs}|Ts],Spec) -> add_tests([{groups,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) -> add_tests([{groups,all_nodes,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec); add_tests([{groups,Nodes,Dir,Suite,Gs}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,groups,[Dir,Suite,Gs],Ts,Spec#testspec.nodes), + Ts1 = per_node(Nodes,groups,[Dir,Suite,Gs],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{groups,Nodes,Dir,Suite,Gs,{cases,TCs}}|Ts], Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,groups,[Dir,Suite,Gs,{cases,TCs}],Ts, + Ts1 = per_node(Nodes,groups,[Dir,Suite,Gs,{cases,TCs}],Ts, Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{groups,Node,Dir,Suite,Gs}|Ts],Spec) -> @@ -688,13 +844,14 @@ add_tests([{cases,all_nodes,Dir,Suite,Cs}|Ts],Spec) -> add_tests([{cases,Dir,Suite,Cs}|Ts],Spec) -> add_tests([{cases,all_nodes,Dir,Suite,Cs}|Ts],Spec); add_tests([{cases,Nodes,Dir,Suite,Cs}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,cases,[Dir,Suite,Cs],Ts,Spec#testspec.nodes), + Ts1 = per_node(Nodes,cases,[Dir,Suite,Cs],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{cases,Node,Dir,Suite,Cs}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = insert_cases(ref2node(Node,Spec#testspec.nodes), ref2dir(Dir,Spec), - Suite,Cs,Tests, Spec#testspec.merge_tests), + Suite,Cs,Tests, + Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); %% --- skip_suites --- @@ -703,7 +860,7 @@ add_tests([{skip_suites,all_nodes,Dir,Ss,Cmt}|Ts],Spec) -> add_tests([{skip_suites,Dir,Ss,Cmt}|Ts],Spec) -> add_tests([{skip_suites,all_nodes,Dir,Ss,Cmt}|Ts],Spec); add_tests([{skip_suites,Nodes,Dir,Ss,Cmt}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,skip_suites,[Dir,Ss,Cmt],Ts,Spec#testspec.nodes), + Ts1 = per_node(Nodes,skip_suites,[Dir,Ss,Cmt],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, @@ -724,11 +881,11 @@ add_tests([{skip_groups,Dir,Suite,Gs,Cmt}|Ts],Spec) -> add_tests([{skip_groups,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) -> add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec); add_tests([{skip_groups,Nodes,Dir,Suite,Gs,Cmt}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,Cmt],Ts,Spec#testspec.nodes), + Ts1 = per_node(Nodes,skip_groups,[Dir,Suite,Gs,Cmt],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{skip_groups,Nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts], Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,{cases,TCs},Cmt],Ts, + Ts1 = per_node(Nodes,skip_groups,[Dir,Suite,Gs,{cases,TCs},Cmt],Ts, Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{skip_groups,Node,Dir,Suite,Gs,Cmt}|Ts],Spec) -> @@ -752,7 +909,7 @@ add_tests([{skip_cases,all_nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) -> add_tests([{skip_cases,Dir,Suite,Cs,Cmt}|Ts],Spec) -> add_tests([{skip_cases,all_nodes,Dir,Suite,Cs,Cmt}|Ts],Spec); add_tests([{skip_cases,Nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,skip_cases,[Dir,Suite,Cs,Cmt],Ts,Spec#testspec.nodes), + Ts1 = per_node(Nodes,skip_cases,[Dir,Suite,Cs,Cmt],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{skip_cases,Node,Dir,Suite,Cs,Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, @@ -783,6 +940,9 @@ add_tests([{release_shell,Bool}|Ts],Spec) -> add_tests(Ts, Spec#testspec{release_shell = Bool}); %% --- handled/errors --- +add_tests([{set_merge_tests,_}|Ts],Spec) -> % internal + add_tests(Ts,Spec); + add_tests([{define,_,_}|Ts],Spec) -> % handled add_tests(Ts,Spec); @@ -792,7 +952,10 @@ add_tests([{alias,_,_}|Ts],Spec) -> % handled add_tests([{node,_,_}|Ts],Spec) -> % handled add_tests(Ts,Spec); -add_tests([{merge_tests, _} | Ts], Spec) -> % handled +add_tests([{merge_tests,_} | Ts], Spec) -> % handled + add_tests(Ts,Spec); + +add_tests([{specs,_,_} | Ts], Spec) -> % handled add_tests(Ts,Spec); %% -------------------------------------------------- @@ -821,7 +984,7 @@ add_tests([{Tag,NodesOrOther,Data}|Ts],Spec) when is_list(NodesOrOther) -> case lists:all(fun(Test) -> is_node(Test,Spec#testspec.nodes) end, NodesOrOther) of true -> - Ts1 = separate(NodesOrOther,Tag,[Data],Ts,Spec#testspec.nodes), + Ts1 = per_node(NodesOrOther,Tag,[Data],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); false -> add_tests([{Tag,all_nodes,{NodesOrOther,Data}}|Ts],Spec) @@ -866,7 +1029,7 @@ add_tests([],Spec) -> % done %% check if it's a CT term that has bad format or if the user seems to %% have added something of his/her own, which we'll let pass if relaxed %% mode is enabled. -check_term(Term) -> +check_term(Term) when is_tuple(Term) -> Size = size(Term), [Name|_] = tuple_to_list(Term), Valid = valid_terms(), @@ -903,6 +1066,8 @@ handle_data(logdir,Node,Dir,Spec) -> [{Node,ref2dir(Dir,Spec)}]; handle_data(cover,Node,File,Spec) -> [{Node,get_absfile(File,Spec)}]; +handle_data(cover_stop,Node,Stop,_Spec) -> + [{Node,Stop}]; handle_data(include,Node,Dirs=[D|_],Spec) when is_list(D) -> [{Node,ref2dir(Dir,Spec)} || Dir <- Dirs]; handle_data(include,Node,Dir=[Ch|_],Spec) when is_integer(Ch) -> @@ -975,12 +1140,12 @@ update_recorded(Tag,Node,Spec) -> end. %% create one test term per node -separate(Nodes,Tag,Data,Tests,Refs) -> - Separated = separate(Nodes,Tag,Data,Refs), +per_node(Nodes,Tag,Data,Tests,Refs) -> + Separated = per_node(Nodes,Tag,Data,Refs), Separated ++ Tests. -separate([N|Ns],Tag,Data,Refs) -> - [list_to_tuple([Tag,ref2node(N,Refs)|Data])|separate(Ns,Tag,Data,Refs)]; -separate([],_,_,_) -> +per_node([N|Ns],Tag,Data,Refs) -> + [list_to_tuple([Tag,ref2node(N,Refs)|Data])|per_node(Ns,Tag,Data,Refs)]; +per_node([],_,_,_) -> []. %% read the value for FieldName in record Rec#testspec @@ -1037,14 +1202,21 @@ insert_groups(Node,Dir,Suite,Groups,Cases,Tests,true) when {[Gr],Cases}; true -> {Gr,Cases} end || Gr <- Groups], - case lists:keysearch({Node,Dir},1,Tests) of - {value,{{Node,Dir},[{all,_}]}} -> - Tests; - {value,{{Node,Dir},Suites0}} -> - Suites1 = insert_groups1(Suite,Groups1,Suites0), - insert_in_order({{Node,Dir},Suites1},Tests); - false -> - insert_in_order({{Node,Dir},[{Suite,Groups1}]},Tests) + {Tests1,Done} = + lists:foldr(fun(All={{N,D},[{all,_}]},{Replaced,_}) when N == Node, + D == Dir -> + {[All|Replaced],true}; + ({{N,D},Suites0},{Replaced,_}) when N == Node, + D == Dir -> + Suites1 = insert_groups1(Suite,Groups1,Suites0), + {[{{N,D},Suites1}|Replaced],true}; + (T,{Replaced,Match}) -> + {[T|Replaced],Match} + end, {[],false}, Tests), + if not Done -> + Tests ++ [{{Node,Dir},[{Suite,Groups1}]}]; + true -> + Tests1 end; insert_groups(Node,Dir,Suite,Groups,Case,Tests, MergeTests) when is_atom(Case) -> @@ -1082,14 +1254,26 @@ insert_groups2([],GrAndCases) -> insert_cases(Node,Dir,Suite,Cases,Tests,false) when is_list(Cases) -> append({{Node,Dir},[{Suite,Cases}]},Tests); insert_cases(Node,Dir,Suite,Cases,Tests,true) when is_list(Cases) -> - case lists:keysearch({Node,Dir},1,Tests) of - {value,{{Node,Dir},[{all,_}]}} -> - Tests; - {value,{{Node,Dir},Suites0}} -> - Suites1 = insert_cases1(Suite,Cases,Suites0), - insert_in_order({{Node,Dir},Suites1},Tests); - false -> - insert_in_order({{Node,Dir},[{Suite,Cases}]},Tests) + {Tests1,Done} = + lists:foldr(fun(All={{N,D},[{all,_}]},{Merged,_}) when N == Node, + D == Dir -> + {[All|Merged],true}; + ({{N,D},Suites0},{Merged,_}) when N == Node, + D == Dir -> + Suites1 = insert_cases1(Suite,Cases,Suites0), + {[{{N,D},Suites1}|Merged],true}; + (T,{Merged,Match}) -> + {[T|Merged],Match} + end, {[],false}, Tests), + if Tests == [] -> + %% initial case with length(Cases) > 1, we need to do this + %% to merge possible duplicate cases in Cases + [{{Node,Dir},insert_cases1(Suite,Cases,[{Suite,[]}])}]; + not Done -> + %% no merging done, simply add these cases to Tests + Tests ++ [{{Node,Dir},[{Suite,Cases}]}]; + true -> + Tests1 end; insert_cases(Node,Dir,Suite,Case,Tests,MergeTests) when is_atom(Case) -> insert_cases(Node,Dir,Suite,[Case],Tests,MergeTests). @@ -1130,15 +1314,23 @@ skip_groups(Node,Dir,Suite,Groups,Cases,Cmt,Tests,false) when append({{Node,Dir},Suites1},Tests); skip_groups(Node,Dir,Suite,Groups,Cases,Cmt,Tests,true) when ((Cases == all) or is_list(Cases)) and is_list(Groups) -> - Suites = - case lists:keysearch({Node,Dir},1,Tests) of - {value,{{Node,Dir},Suites0}} -> - Suites0; - false -> - [] - end, - Suites1 = skip_groups1(Suite,[{Gr,Cases} || Gr <- Groups],Cmt,Suites), - insert_in_order({{Node,Dir},Suites1},Tests); + {Tests1,Done} = + lists:foldr(fun({{N,D},Suites0},{Merged,_}) when N == Node, + D == Dir -> + Suites1 = skip_groups1(Suite, + [{Gr,Cases} || Gr <- Groups], + Cmt,Suites0), + {[{{N,D},Suites1}|Merged],true}; + (T,{Merged,Match}) -> + {[T|Merged],Match} + end, {[],false}, Tests), + if not Done -> + Tests ++ [{{Node,Dir},skip_groups1(Suite, + [{Gr,Cases} || Gr <- Groups], + Cmt,[])}]; + true -> + Tests1 + end; skip_groups(Node,Dir,Suite,Groups,Case,Cmt,Tests,MergeTests) when is_atom(Case) -> Cases = if Case == all -> all; true -> [Case] end, @@ -1160,15 +1352,19 @@ skip_cases(Node,Dir,Suite,Cases,Cmt,Tests,false) when is_list(Cases) -> Suites1 = skip_cases1(Suite,Cases,Cmt,[]), append({{Node,Dir},Suites1},Tests); skip_cases(Node,Dir,Suite,Cases,Cmt,Tests,true) when is_list(Cases) -> - Suites = - case lists:keysearch({Node,Dir},1,Tests) of - {value,{{Node,Dir},Suites0}} -> - Suites0; - false -> - [] - end, - Suites1 = skip_cases1(Suite,Cases,Cmt,Suites), - insert_in_order({{Node,Dir},Suites1},Tests); + {Tests1,Done} = + lists:foldr(fun({{N,D},Suites0},{Merged,_}) when N == Node, + D == Dir -> + Suites1 = skip_cases1(Suite,Cases,Cmt,Suites0), + {[{{N,D},Suites1}|Merged],true}; + (T,{Merged,Match}) -> + {[T|Merged],Match} + end, {[],false}, Tests), + if not Done -> + Tests ++ [{{Node,Dir},skip_cases1(Suite,Cases,Cmt,[])}]; + true -> + Tests1 + end; skip_cases(Node,Dir,Suite,Case,Cmt,Tests,MergeTests) when is_atom(Case) -> skip_cases(Node,Dir,Suite,[Case],Cmt,Tests,MergeTests). @@ -1258,10 +1454,14 @@ is_node([],_) -> valid_terms() -> [ + {set_merge_tests,2}, {define,3}, + {specs,3}, {node,3}, {cover,2}, {cover,3}, + {cover_stop,2}, + {cover_stop,3}, {config,2}, {config,3}, {config,4}, diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl index cf891ed043..68e76c2396 100644 --- a/lib/common_test/src/ct_util.erl +++ b/lib/common_test/src/ct_util.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -25,13 +25,13 @@ %%% -module(ct_util). --export([start/0,start/1,start/2,start/3, - stop/1,update_last_run_index/0]). +-export([start/0, start/1, start/2, start/3, + stop/1, update_last_run_index/0]). --export([register_connection/4,unregister_connection/1, - does_connection_exist/3,get_key_from_name/1]). +-export([register_connection/4, unregister_connection/1, + does_connection_exist/3, get_key_from_name/1]). --export([close_connections/0]). +-export([get_connections/1, close_connections/0]). -export([save_suite_data/3, save_suite_data/2, save_suite_data_async/3, save_suite_data_async/2, @@ -39,7 +39,8 @@ delete_suite_data/0, delete_suite_data/1, match_delete_suite_data/1, delete_testdata/0, delete_testdata/1, set_testdata/1, get_testdata/1, get_testdata/2, - set_testdata_async/1, update_testdata/2, update_testdata/3]). + set_testdata_async/1, update_testdata/2, update_testdata/3, + set_verbosity/1, get_verbosity/1]). -export([override_silence_all_connections/0, override_silence_connections/1, get_overridden_silenced_connections/0, @@ -55,11 +56,11 @@ -export([listenv/1]). --export([get_target_name/1, get_connections/2]). +-export([get_target_name/1, get_connection/2]). -export([is_test_dir/1, get_testdir/2]). --export([kill_attached/2, get_attached/1, ct_make_ref/0]). +-export([kill_attached/2, get_attached/1]). -export([warn_duplicates/1]). @@ -128,6 +129,10 @@ do_start(Parent, Mode, LogDir, Verbosity) -> create_table(?conn_table,#conn.handle), create_table(?board_table,2), create_table(?suite_table,#suite_data.key), + + create_table(?verbosity_table,1), + [ets:insert(?verbosity_table,{Cat,Lvl}) || {Cat,Lvl} <- Verbosity], + {ok,StartDir} = file:get_cwd(), case file:set_cwd(LogDir) of ok -> ok; @@ -202,7 +207,7 @@ do_start(Parent, Mode, LogDir, Verbosity) -> self() ! {{stop,{self(),{user_error,CTHReason}}}, {Parent,make_ref()}} end, - loop(Mode, [{{verbosity,Cat},Lvl} || {Cat,Lvl} <- Verbosity], StartDir). + loop(Mode, [], StartDir). create_table(TableName,KeyPos) -> create_table(TableName,set,KeyPos). @@ -278,6 +283,19 @@ reset_cwd() -> get_start_dir() -> call(get_start_dir). +%% handle verbosity outside ct_util_server (let the client read +%% the verbosity table) to avoid possible deadlock situations +set_verbosity(Elem = {_Category,_Level}) -> + ets:insert(?verbosity_table, Elem), + ok. +get_verbosity(Category) -> + case ets:lookup(?verbosity_table, Category) of + [{Category,Level}] -> + Level; + _ -> + undefined + end. + loop(Mode,TestData,StartDir) -> receive {update_last_run_index,From} -> @@ -377,6 +395,7 @@ loop(Mode,TestData,StartDir) -> ets:delete(?conn_table), ets:delete(?board_table), ets:delete(?suite_table), + ets:delete(?verbosity_table), ct_logs:close(Info, StartDir), ct_event:stop(), ct_config:stop(), @@ -395,16 +414,21 @@ loop(Mode,TestData,StartDir) -> [#conn{address=A,callback=CB}] -> %% A connection crashed - remove the connection but don't die ct_logs:tc_log_async(ct_error_notify, + ?MAX_IMPORTANCE, + "CT Error Notification", "Connection process died: " - "Pid: ~p, Address: ~p, Callback: ~p\n" + "Pid: ~w, Address: ~p, " + "Callback: ~w\n" "Reason: ~p\n\n", [Pid,A,CB,Reason]), catch CB:close(Pid), + %% in case CB:close failed to do this: + unregister_connection(Pid), loop(Mode,TestData,StartDir); _ -> %% Let process crash in case of error, this shouldn't happen! - io:format("\n\nct_util_server got EXIT from ~p: ~p\n\n", - [Pid,Reason]), + io:format("\n\nct_util_server got EXIT " + "from ~w: ~p\n\n", [Pid,Reason]), file:set_cwd(StartDir), exit(Reason) end @@ -434,10 +458,13 @@ get_key_from_name(Name)-> %%% table, and ct_util will close all registered connections when the %%% test is finished by calling <code>Callback:close/1</code>.</p> register_connection(TargetName,Address,Callback,Handle) -> + %% If TargetName is a registered alias for a config + %% variable, use it as reference for the connection, + %% otherwise use the Handle value. TargetRef = - case ct_config:get_ref_from_name(TargetName) of - {ok,Ref} -> - Ref; + case ct_config:get_key_from_name(TargetName) of + {ok,_Key} -> + TargetName; _ -> %% no config name associated with connection, %% use handle for identification instead @@ -473,10 +500,10 @@ unregister_connection(Handle) -> %%% %%% @doc Check if a connection already exists. does_connection_exist(TargetName,Address,Callback) -> - case ct_config:get_ref_from_name(TargetName) of - {ok,TargetRef} -> + case ct_config:get_key_from_name(TargetName) of + {ok,_Key} -> case ets:select(?conn_table,[{#conn{handle='$1', - targetref=TargetRef, + targetref=TargetName, address=Address, callback=Callback}, [], @@ -491,41 +518,76 @@ does_connection_exist(TargetName,Address,Callback) -> end. %%%----------------------------------------------------------------- -%%% @spec get_connections(TargetName,Callback) -> -%%% {ok,Connections} | {error,Reason} +%%% @spec get_connection(TargetName,Callback) -> +%%% {ok,Connection} | {error,Reason} %%% TargetName = ct:target_name() %%% Callback = atom() -%%% Connections = [Connection] %%% Connection = {Handle,Address} %%% Handle = term() %%% Address = term() %%% -%%% @doc Return all connections for the <code>Callback</code> on the +%%% @doc Return the connection for <code>Callback</code> on the %%% given target (<code>TargetName</code>). -get_connections(TargetName,Callback) -> - case ct_config:get_ref_from_name(TargetName) of - {ok,Ref} -> - {ok,ets:select(?conn_table,[{#conn{handle='$1', - address='$2', - targetref=Ref, - callback=Callback}, - [], - [{{'$1','$2'}}]}])}; +get_connection(TargetName,Callback) -> + %% check that TargetName is a registered alias + case ct_config:get_key_from_name(TargetName) of + {ok,_Key} -> + case ets:select(?conn_table,[{#conn{handle='$1', + address='$2', + targetref=TargetName, + callback=Callback}, + [], + [{{'$1','$2'}}]}]) of + [Result] -> + {ok,Result}; + [] -> + {error,no_registered_connection} + end; Error -> Error end. %%%----------------------------------------------------------------- +%%% @spec get_connections(ConnPid) -> +%%% {ok,Connections} | {error,Reason} +%%% Connections = [Connection] +%%% Connection = {TargetName,Handle,Callback,Address} +%%% TargetName = ct:target_name() | undefined +%%% Handle = term() +%%% Callback = atom() +%%% Address = term() +%%% +%%% @doc Get data for all connections associated with a particular +%%% connection pid (see Callback:init/3). +get_connections(ConnPid) -> + Conns = ets:tab2list(?conn_table), + lists:flatmap(fun(#conn{targetref=TargetName, + handle=Handle, + callback=Callback, + address=Address}) -> + case ct_gen_conn:get_conn_pid(Handle) of + ConnPid when is_atom(TargetName) -> + [{TargetName,Handle, + Callback,Address}]; + ConnPid -> + [{undefined,Handle, + Callback,Address}]; + _ -> + [] + end + end, Conns). + +%%%----------------------------------------------------------------- %%% @hidden %%% @equiv ct:get_target_name/1 -get_target_name(ConnPid) -> - case ets:select(?conn_table,[{#conn{handle=ConnPid,targetref='$1',_='_'}, +get_target_name(Handle) -> + case ets:select(?conn_table,[{#conn{handle=Handle,targetref='$1',_='_'}, [], ['$1']}]) of - [TargetRef] -> - ct_config:get_name_from_ref(TargetRef); - [] -> - {error,{unknown_connection,ConnPid}} + [TargetName] when is_atom(TargetName) -> + {ok,TargetName}; + _ -> + {error,{unknown_connection,Handle}} end. %%%----------------------------------------------------------------- @@ -899,29 +961,8 @@ cast(Msg) -> seconds(T) -> test_server:seconds(T). -ct_make_ref() -> - Pid = case whereis(ct_make_ref) of - undefined -> - spawn_link(fun() -> ct_make_ref_init() end); - P -> - P - end, - Pid ! {self(),ref_req}, - receive - {Pid,Ref} -> Ref - end. - -ct_make_ref_init() -> - register(ct_make_ref,self()), - ct_make_ref_loop(0). - -ct_make_ref_loop(N) -> - receive - {From,ref_req} -> - From ! {self(),N}, - ct_make_ref_loop(N+1) - end. - +abs_name("/") -> + "/"; abs_name(Dir0) -> Abs = filename:absname(Dir0), Dir = case lists:reverse(Abs) of @@ -956,7 +997,7 @@ open_url(iexplore, Args, URL) -> 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]), + io:format(user, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd1]), open_port({spawn,Cmd1}, []); _ -> io:format("~nNo path to iexplore.exe~n",[]) @@ -969,6 +1010,6 @@ open_url(Prog, Args, URL) -> is_list(Prog) -> Prog end, Cmd = ProgStr ++ " " ++ Args ++ " " ++ URL, - io:format(user, "~nOpening ~s with command:~n ~s~n", [URL,Cmd]), + io:format(user, "~nOpening ~ts with command:~n ~ts~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 196b5e46d0..7c01e17c36 100644 --- a/lib/common_test/src/ct_util.hrl +++ b/lib/common_test/src/ct_util.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -21,6 +21,7 @@ -define(conn_table,ct_connections). -define(board_table,ct_boards). -define(suite_table,ct_suite_data). +-define(verbosity_table,ct_verbosity_table). -record(conn, {handle, targetref, @@ -38,6 +39,7 @@ verbosity=[], silent_connections=[], cover=[], + cover_stop=[], config=[], userconfig=[], event_handler=[], diff --git a/lib/common_test/src/cth_conn_log.erl b/lib/common_test/src/cth_conn_log.erl index 3af89db3a5..644594e34d 100644 --- a/lib/common_test/src/cth_conn_log.erl +++ b/lib/common_test/src/cth_conn_log.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012. All Rights Reserved. +%% Copyright Ericsson AB 2012-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -58,7 +58,7 @@ -spec init(Id, HookOpts) -> Result when Id :: term(), - HookOpts :: ct:hook_options(), + HookOpts :: ct_netconfc:hook_options(), Result :: {ok,[{ct_netconfc:conn_mod(), {ct_netconfc:log_type(),[ct_netconfc:key_or_name()]}}]}. init(_Id, HookOpts) -> @@ -105,8 +105,9 @@ pre_init_per_testcase(TestCase,Config,CthState) -> "<table borders=1>" "<b>" ++ ConnModStr ++ " logs:</b>\n" ++ [io_lib:format( - "<tr><td>~p</td><td><a href=~p>~s</a></td></tr>", - [S,L,filename:basename(L)]) + "<tr><td>~p</td><td><a href=\"~ts\">~ts</a>" + "</td></tr>", + [S,ct_logs:uri(L),filename:basename(L)]) || {S,L} <- Ls] ++ "</table>", io:format(Str,[]), diff --git a/lib/common_test/src/cth_log_redirect.erl b/lib/common_test/src/cth_log_redirect.erl index 77f57c6195..958b7a94c7 100644 --- a/lib/common_test/src/cth_log_redirect.erl +++ b/lib/common_test/src/cth_log_redirect.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2011-2012. All Rights Reserved. +%% Copyright Ericsson AB 2011-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -33,6 +33,8 @@ handle_event/2, handle_call/2, handle_info/2, terminate/1]). +-include("ct.hrl"). + id(_Opts) -> ?MODULE. @@ -54,7 +56,7 @@ post_init_per_group(_Group, _Config, Result, State) -> post_end_per_testcase(_TC, _Config, Result, State) -> %% Make sure that the event queue is flushed %% before ending this test case. - gen_event:call(error_logger, ?MODULE, flush), + gen_event:call(error_logger, ?MODULE, flush, 300000), {Result, State}. pre_end_per_group(Group, Config, {ct_log, Group}) -> @@ -78,7 +80,7 @@ handle_event(Event, LogFunc) -> SReport = sasl_report:format_report(group_leader(), ErrLogType, tag_event(Event)), if is_list(SReport) -> - ct_logs:LogFunc(sasl, SReport, []); + ct_logs:LogFunc(sasl, ?STD_IMPORTANCE, "System", SReport, []); true -> %% Report is an atom if no logging is to be done ignore end @@ -86,7 +88,7 @@ handle_event(Event, LogFunc) -> EReport = error_logger_tty_h:write_event( tag_event(Event),io_lib), if is_list(EReport) -> - ct_logs:LogFunc(error_logger, EReport, []); + ct_logs:LogFunc(error_logger, ?STD_IMPORTANCE, "System", EReport, []); true -> %% Report is an atom if no logging is to be done ignore end, diff --git a/lib/common_test/src/cth_surefire.erl b/lib/common_test/src/cth_surefire.erl index e6eaad8d48..1a38b6584b 100644 --- a/lib/common_test/src/cth_surefire.erl +++ b/lib/common_test/src/cth_surefire.erl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012. All Rights Reserved. +%% Copyright Ericsson AB 2012-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -297,7 +297,7 @@ sanitize([]) -> now_to_string(Now) -> {{YY,MM,DD},{HH,Mi,SS}} = calendar:now_to_local_time(Now), - io_lib:format("~p-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B",[YY,MM,DD,HH,Mi,SS]). + io_lib:format("~w-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B",[YY,MM,DD,HH,Mi,SS]). make_url(undefined,_) -> undefined; diff --git a/lib/common_test/src/unix_telnet.erl b/lib/common_test/src/unix_telnet.erl index 25b9d4d5d2..88199b07d0 100644 --- a/lib/common_test/src/unix_telnet.erl +++ b/lib/common_test/src/unix_telnet.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2010. All Rights Reserved. +%% Copyright Ericsson AB 2004-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -95,9 +95,9 @@ connect(Ip,Port,Timeout,KeepAlive,Extra) -> connect1(Ip,Port,Timeout,KeepAlive,Username,Password); Name -> case get_username_and_password(Name) of - {ok,{Username,Password}} -> + {ok,{Username,Password}} -> connect1(Ip,Port,Timeout,KeepAlive,Username,Password); - Error -> + Error -> Error end end. @@ -110,7 +110,7 @@ connect1(Ip,Port,Timeout,KeepAlive,Username,Password) -> case ct_telnet:silent_teln_expect(Pid,[],[prompt],?prx,[]) of {ok,{prompt,?username},_} -> ok = ct_telnet_client:send_data(Pid,Username), - cont_log("Username: ~s",[Username]), + cont_log("Username: ~ts",[Username]), case ct_telnet:silent_teln_expect(Pid,[],prompt,?prx,[]) of {ok,{prompt,?password},_} -> ok = ct_telnet_client:send_data(Pid,Password), |