diff options
Diffstat (limited to 'lib/common_test/src')
22 files changed, 2049 insertions, 1045 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile index 84b122b5e4..6a16c6f3af 100644 --- a/lib/common_test/src/Makefile +++ b/lib/common_test/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2003-2011. All Rights Reserved. +# Copyright Ericsson AB 2003-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 @@ -40,7 +40,6 @@ RELSYSDIR = $(RELEASE_PATH)/lib/common_test-$(VSN) # ---------------------------------------------------- MODULES= \ - ct_line \ ct \ ct_logs \ ct_framework \ @@ -69,9 +68,12 @@ MODULES= \ ct_config_xml \ ct_slave \ ct_hooks\ - ct_hooks_lock + ct_hooks_lock\ + cth_log_redirect\ + cth_surefire TARGET_MODULES= $(MODULES:%=$(EBIN)/%) +BEAM_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) ERL_FILES= $(MODULES:=.erl) HRL_FILES = \ @@ -97,7 +99,7 @@ ERL_COMPILE_FLAGS += -pa ../ebin -I../include -I $(ERL_TOP)/lib/snmp/include/ \ # ---------------------------------------------------- TARGET_FILES = \ $(GEN_ERL_FILES:%.erl=$(EBIN)/%.$(EMULATOR)) \ - $(MODULES:%=$(EBIN)/%.$(EMULATOR)) \ + $(BEAM_FILES) \ $(APP_TARGET) $(APPUP_TARGET) APP_FILE= common_test.app diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src index b42173f412..ae9a51faeb 100644 --- a/lib/common_test/src/common_test.app.src +++ b/lib/common_test/src/common_test.app.src @@ -1,7 +1,7 @@ % This is an -*- erlang -*- file. %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2009-2010. All Rights Reserved. +%% Copyright Ericsson AB 2009-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 @@ -25,7 +25,8 @@ ct_framework, ct_ftp, ct_gen_conn, - ct_line, + ct_hooks, + ct_hooks_lock, ct_logs, ct_make, ct_master, @@ -46,7 +47,9 @@ ct_config, ct_config_plain, ct_config_xml, - ct_slave + ct_slave, + cth_log_redirect, + cth_surefire ]}, {registered, [ct_logs, ct_util_server, diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl index f3c2029734..571d99029f 100644 --- a/lib/common_test/src/ct.erl +++ b/lib/common_test/src/ct.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2011. All Rights Reserved. +%% Copyright Ericsson AB 2003-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 @@ -63,9 +63,10 @@ log/1, log/2, log/3, print/1, print/2, print/3, pal/1, pal/2, pal/3, - fail/1, comment/1, + capture_start/0, capture_stop/0, capture_get/0, capture_get/1, + fail/1, fail/2, comment/1, comment/2, make_priv_dir/0, testcases/2, userdata/2, userdata/3, - timetrap/1, sleep/1]). + timetrap/1, get_timetrap_info/0, sleep/1]). %% New API for manipulating with config handlers -export([add_config/2, remove_config/2]). @@ -108,7 +109,7 @@ install(Opts) -> %%% Cases = atom() | [atom()] %%% Result = [TestResult] | {error,Reason} %%% -%%% @doc Run the given testcase(s). +%%% @doc Run the given test case(s). %%% %%% <p>Requires that <code>ct:install/1</code> has been run first.</p> %%% @@ -121,7 +122,7 @@ run(TestDir,Suite,Cases) -> %%%----------------------------------------------------------------- %%% @spec run(TestDir,Suite) -> Result %%% -%%% @doc Run all testcases in the given suite. +%%% @doc Run all test cases in the given suite. %%% @see run/3. run(TestDir,Suite) -> ct_run:run(TestDir,Suite). @@ -130,7 +131,7 @@ run(TestDir,Suite) -> %%% @spec run(TestDirs) -> Result %%% TestDirs = TestDir | [TestDir] %%% -%%% @doc Run all testcases in all suites in the given directories. +%%% @doc Run all test cases in all suites in the given directories. %%% @see run/3. run(TestDirs) -> ct_run:run(TestDirs). @@ -145,11 +146,12 @@ run(TestDirs) -> %%% {silent_connections,Conns} | {stylesheet,CSSFile} | %%% {cover,CoverSpecFile} | {step,StepOpts} | %%% {event_handler,EventHandlers} | {include,InclDirs} | -%%% {auto_compile,Bool} | {multiply_timetraps,M} | {scale_timetraps,Bool} | +%%% {auto_compile,Bool} | {create_priv_dir,CreatePrivDir} | +%%% {multiply_timetraps,M} | {scale_timetraps,Bool} | %%% {repeat,N} | {duration,DurTime} | {until,StopTime} | %%% {force_stop,Bool} | {decrypt,DecryptKeyOrFile} | -%%% {refresh_logs,LogDir} | {logopts,LogOpts} | {basic_html,Bool} | -%%% {ct_hooks, CTHs} +%%% {refresh_logs,LogDir} | {logopts,LogOpts} | {basic_html,Bool} | +%%% {ct_hooks, CTHs} | {enable_builtin_hooks,Bool} %%% TestDirs = [string()] | string() %%% Suites = [string()] | [atom()] | string() | atom() %%% Cases = [atom()] | atom() @@ -170,6 +172,7 @@ run(TestDirs) -> %%% EH = atom() | {atom(),InitArgs} | {[atom()],InitArgs} %%% InitArgs = [term()] %%% InclDirs = [string()] | string() +%%% CreatePrivDir = auto_per_run | auto_per_tc | manual_per_tc %%% M = integer() %%% N = integer() %%% DurTime = string(HHMMSS) @@ -440,11 +443,10 @@ log(X1,X2) -> %%% Format = string() %%% Args = list() %%% -%%% @doc Printout from a testcase to the log. +%%% @doc Printout from a test case to the log file. %%% -%%% <p>This function is meant for printing stuff directly from a -%%% testcase (i.e. not from within the CT framework) in the test -%%% log.</p> +%%% <p>This function is meant for printing a string directly from a +%%% test case to the test case log file.</p> %%% %%% <p>Default <code>Category</code> is <code>default</code> and %%% default <code>Args</code> is <code>[]</code>.</p> @@ -473,10 +475,10 @@ print(X1,X2) -> %%% Format = string() %%% Args = list() %%% -%%% @doc Printout from a testcase to the console. +%%% @doc Printout from a test case to the console. %%% -%%% <p>This function is meant for printing stuff from a testcase on -%%% the console.</p> +%%% <p>This function is meant for printing a string from a test case +%%% to the console.</p> %%% %%% <p>Default <code>Category</code> is <code>default</code> and %%% default <code>Args</code> is <code>[]</code>.</p> @@ -508,16 +510,75 @@ pal(X1,X2) -> %%% Format = string() %%% Args = list() %%% -%%% @doc Print and log from a testcase. +%%% @doc Print and log from a test case. %%% -%%% <p>This function is meant for printing stuff from a testcase both -%%% in the log and on the console.</p> +%%% <p>This function is meant for printing a string from a test case, +%%% both to the test case log file and to the console.</p> %%% %%% <p>Default <code>Category</code> is <code>default</code> and %%% default <code>Args</code> is <code>[]</code>.</p> pal(Category,Format,Args) -> ct_logs:tc_pal(Category,Format,Args). +%%%----------------------------------------------------------------- +%%% @spec capture_start() -> ok +%%% +%%% @doc Start capturing all text strings printed to stdout during +%%% execution of the test case. +%%% +%%% @see capture_stop/0 +%%% @see capture_get/1 +capture_start() -> + test_server:capture_start(). + +%%%----------------------------------------------------------------- +%%% @spec capture_stop() -> ok +%%% +%%% @doc Stop capturing text strings (a session started with +%%% <code>capture_start/0</code>). +%%% +%%% @see capture_start/0 +%%% @see capture_get/1 +capture_stop() -> + test_server:capture_stop(). + +%%%----------------------------------------------------------------- +%%% @spec capture_get() -> ListOfStrings +%%% ListOfStrings = [string()] +%%% +%%% @equiv capture_get([default]) +capture_get() -> + %% remove default log printouts (e.g. ct:log/2 printouts) + capture_get([default]). + +%%%----------------------------------------------------------------- +%%% @spec capture_get(ExclCategories) -> ListOfStrings +%%% ExclCategories = [atom()] +%%% ListOfStrings = [string()] +%%% +%%% @doc Return and purge the list of text strings buffered +%%% during the latest session of capturing printouts to stdout. +%%% With <code>ExclCategories</code> it's possible to specify +%%% log categories that should be ignored in <code>ListOfStrings</code>. +%%% If <code>ExclCategories = []</code>, no filtering takes place. +%%% +%%% @see capture_start/0 +%%% @see capture_stop/0 +%%% @see log/3 +capture_get([ExclCat | ExclCategories]) -> + Strs = test_server:capture_get(), + CatsStr = [atom_to_list(ExclCat) | + [[$| | atom_to_list(EC)] || EC <- ExclCategories]], + {ok,MP} = re:compile("<div class=\"(" ++ lists:flatten(CatsStr) ++ ")\">.*"), + lists:flatmap(fun(Str) -> + case re:run(Str, MP) of + {match,_} -> []; + nomatch -> [Str] + end + end, Strs); + +capture_get([]) -> + test_server:capture_get(). %%%----------------------------------------------------------------- %%% @spec fail(Reason) -> void() @@ -526,20 +587,53 @@ pal(Category,Format,Args) -> %%% @doc Terminate a test case with the given error %%% <code>Reason</code>. fail(Reason) -> - exit({test_case_failed,Reason}). + try + exit({test_case_failed,Reason}) + catch + Class:R -> + case erlang:get_stacktrace() of + [{?MODULE,fail,1,_}|Stk] -> ok; + Stk -> ok + end, + erlang:raise(Class, R, Stk) + end. + +%%%----------------------------------------------------------------- +%%% @spec fail(Format, Args) -> void() +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Terminate a test case with an error message specified +%%% by a format string and a list of values (used as arguments to +%%% <code>io_lib:format/2</code>). +fail(Format, Args) -> + try io_lib:format(Format, Args) of + Str -> + try + exit({test_case_failed,lists:flatten(Str)}) + catch + Class:R -> + case erlang:get_stacktrace() of + [{?MODULE,fail,2,_}|Stk] -> ok; + Stk -> ok + end, + erlang:raise(Class, R, Stk) + end + catch + _:BadArgs -> + exit({BadArgs,{?MODULE,fail,[Format,Args]}}) + end. %%%----------------------------------------------------------------- %%% @spec comment(Comment) -> void() %%% Comment = term() %%% -%%% @doc Print the given <code>Comment</code> in the comment field of +%%% @doc Print the given <code>Comment</code> in the comment field in %%% the table on the test suite result page. %%% %%% <p>If called several times, only the last comment is printed. -%%% <code>comment/1</code> is also overwritten by the return value -%%% <code>{comment,Comment}</code> or by the function -%%% <code>fail/1</code> (which prints <code>Reason</code> as a -%%% comment).</p> +%%% The test case return value <code>{comment,Comment}</code> +%%% overwrites the string set by this function.</p> comment(Comment) when is_list(Comment) -> Formatted = case (catch io_lib:format("~s",[Comment])) of @@ -553,11 +647,43 @@ comment(Comment) -> Formatted = io_lib:format("~p",[Comment]), send_html_comment(lists:flatten(Formatted)). +%%%----------------------------------------------------------------- +%%% @spec comment(Format, Args) -> void() +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Print the formatted string in the comment field in +%%% the table on the test suite result page. +%%% +%%% <p>The <code>Format</code> and <code>Args</code> arguments are +%%% used in call to <code>io_lib:format/2</code> in order to create +%%% the comment string. The behaviour of <code>comment/2</code> is +%%% otherwise the same as the <code>comment/1</code> function (see +%%% above for details).</p> +comment(Format, Args) when is_list(Format), is_list(Args) -> + Formatted = + case (catch io_lib:format(Format, Args)) of + {'EXIT',Reason} -> % bad args + exit({Reason,{?MODULE,comment,[Format,Args]}}); + String -> + lists:flatten(String) + end, + send_html_comment(Formatted). + send_html_comment(Comment) -> Html = "<font color=\"green\">" ++ Comment ++ "</font>", ct_util:set_testdata({comment,Html}), test_server:comment(Html). +%%%----------------------------------------------------------------- +%%% @spec make_priv_dir() -> ok | {error,Reason} +%%% Reason = term() +%%% @doc If the test has been started with the create_priv_dir +%%% option set to manual_per_tc, in order for the test case to use +%%% the private directory, it must first create it by calling +%%% this function. +make_priv_dir() -> + test_server:make_priv_dir(). %%%----------------------------------------------------------------- %%% @spec get_target_name(Handle) -> {ok,TargetName} | {error,Reason} @@ -606,7 +732,7 @@ listenv(Telnet) -> %%% Testcases = list() %%% Reason = term() %%% -%%% @doc Returns all testcases in the specified suite. +%%% @doc Returns all test cases in the specified suite. testcases(TestDir, Suite) -> case make_and_load(TestDir, Suite) of E = {error,_} -> @@ -664,7 +790,8 @@ userdata(TestDir, Suite) -> get_userdata(Info, "suite/0") end. -get_userdata({'EXIT',{undef,_}}, Spec) -> +get_userdata({'EXIT',{Undef,_}}, Spec) when Undef == undef; + Undef == function_clause -> {error,list_to_atom(Spec ++ " is not defined")}; get_userdata({'EXIT',Reason}, Spec) -> {error,{list_to_atom("error in " ++ Spec),Reason}}; @@ -680,16 +807,27 @@ get_userdata(_BadTerm, Spec) -> {error,list_to_atom(Spec ++ " must return a list")}. %%%----------------------------------------------------------------- -%%% @spec userdata(TestDir, Suite, Case) -> TCUserData | {error,Reason} +%%% @spec userdata(TestDir, Suite, GroupOrCase) -> TCUserData | {error,Reason} %%% TestDir = string() %%% Suite = atom() -%%% Case = atom() +%%% GroupOrCase = {group,GroupName} | atom() +%%% GroupName = atom() %%% TCUserData = [term()] %%% Reason = term() %%% %%% @doc Returns any data specified with the tag <code>userdata</code> -%%% in the list of tuples returned from <code>Suite:Case/0</code>. -userdata(TestDir, Suite, Case) -> +%%% in the list of tuples returned from <code>Suite:group(GroupName)</code> +%%% or <code>Suite:Case()</code>. +userdata(TestDir, Suite, {group,GroupName}) -> + case make_and_load(TestDir, Suite) of + E = {error,_} -> + E; + _ -> + Info = (catch apply(Suite, group, [GroupName])), + get_userdata(Info, "group("++atom_to_list(GroupName)++")") + end; + +userdata(TestDir, Suite, Case) when is_atom(Case) -> case make_and_load(TestDir, Suite) of E = {error,_} -> E; @@ -734,6 +872,8 @@ get_status() -> get_testdata(Key) -> case catch ct_util:get_testdata(Key) of + {error,ct_util_server_not_running} -> + no_tests_running; Error = {error,_Reason} -> Error; {'EXIT',_Reason} -> @@ -855,18 +995,38 @@ remove_config(Callback, Config) -> %%%----------------------------------------------------------------- %%% @spec timetrap(Time) -> ok -%%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity +%%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity | Func %%% Hours = integer() %%% Mins = integer() %%% Secs = integer() %%% Millisecs = integer() | float() -%%% -%%% @doc <p>Use this function to set a new timetrap for the running test case.</p> +%%% Func = {M,F,A} | fun() +%%% M = atom() +%%% F = atom() +%%% A = list() +%%% +%%% @doc <p>Use this function to set a new timetrap for the running test case. +%%% If the argument is <code>Func</code>, the timetrap will be triggered +%%% when this function returns. <code>Func</code> may also return a new +%%% <code>Time</code> value, which in that case will be the value for the +%%% new timetrap.</p> timetrap(Time) -> test_server:timetrap_cancel(), test_server:timetrap(Time). %%%----------------------------------------------------------------- +%%% @spec get_timetrap_info() -> {Time,Scale} +%%% Time = integer() | infinity +%%% Scale = true | false +%%% +%%% @doc <p>Read info about the timetrap set for the current test case. +%%% <c>Scale</c> indicates if Common Test will attempt to automatically +%%% compensate timetraps for runtime delays introduced by e.g. tools like +%%% cover.</p> +get_timetrap_info() -> + test_server:get_timetrap_info(). + +%%%----------------------------------------------------------------- %%% @spec sleep(Time) -> ok %%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity %%% Hours = integer() diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl index fc51aea7f3..9277af5bc1 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. All Rights Reserved. +%% Copyright Ericsson AB 2010-2011. 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 diff --git a/lib/common_test/src/ct_config_plain.erl b/lib/common_test/src/ct_config_plain.erl index 6698332379..237df5c8f3 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. All Rights Reserved. +%% Copyright Ericsson AB 2010-2011. 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 diff --git a/lib/common_test/src/ct_config_xml.erl b/lib/common_test/src/ct_config_xml.erl index 794174e663..6e0a016161 100644 --- a/lib/common_test/src/ct_config_xml.erl +++ b/lib/common_test/src/ct_config_xml.erl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010. All Rights Reserved. +%% Copyright Ericsson AB 2010-2011. 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 diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl index 482c5242ce..11575cd0fb 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-2011. 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 @@ -27,15 +27,20 @@ -export([init_tc/3, end_tc/3, end_tc/4, get_suite/2, get_all_cases/1]). -export([report/2, warn/1, error_notification/4]). --export([get_logopts/0, format_comment/1, overview_html_header/1]). +-export([get_logopts/0, format_comment/1, get_html_wrapper/3]). --export([error_in_suite/1, ct_init_per_group/2, ct_end_per_group/2]). +-export([error_in_suite/1, init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2]). -export([make_all_conf/3, make_conf/5]). -include("ct_event.hrl"). -include("ct_util.hrl"). +-define(val(Key, List), proplists:get_value(Key, List)). +-define(val(Key, List, Def), proplists:get_value(Key, List, Def)). +-define(rev(L), lists:reverse(L)). + %%%----------------------------------------------------------------- %%% @spec init_tc(Mod,Func,Args) -> {ok,NewArgs} | {error,Reason} | %%% {skip,Reason} | {auto_skip,Reason} @@ -48,6 +53,9 @@ %%% @doc Test server framework callback, called by the test_server %%% when a new test case is started. init_tc(Mod,Func,Config) -> + %% in case Mod == ct_framework, lookup the suite name + Suite = get_suite_name(Mod, Config), + %% check if previous testcase was interpreted and has left %% a "dead" trace window behind - if so, kill it case ct_util:get_testdata(interpret) of @@ -57,55 +65,56 @@ init_tc(Mod,Func,Config) -> _ -> ok end, - - %% check if we need to add defaults explicitly because - %% there's no init_per_suite exported from Mod - {InitFailed,DoInit} = - case ct_util:get_testdata(curr_tc) of - {Mod,{suite0_failed,_}=Failure} -> - {Failure,false}; - {Mod,_} -> - {false,false}; - _ when Func == init_per_suite -> - {false,false}; - _ -> - {false,true} - end, - case InitFailed of - false -> - ct_util:set_testdata({curr_tc,{Mod,Func}}), - case ct_util:read_suite_data({seq,Mod,Func}) of + + 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:set_testdata({curr_tc,{Suite,Func}}), + case ct_util:read_suite_data({seq,Suite,Func}) of undefined -> - init_tc1(Mod,Func,Config,DoInit); + init_tc1(Mod,Suite,Func,Config); Seq when is_atom(Seq) -> - case ct_util:read_suite_data({seq,Mod,Seq}) of + 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,Mod,TC},Seq) - end, TCs); + lists:foreach( + fun(TC) -> + ct_util:save_suite_data({seq,Suite,TC},Seq) + end, TCs); _ -> ok end, - init_tc1(Mod,Func,Config,DoInit); + init_tc1(Mod,Suite,Func,Config); {failed,Seq,BadFunc} -> {skip,{sequence_failed,Seq,BadFunc}} - end; - {_,{require,Reason}} -> - {skip,{require_failed_in_suite0,Reason}}; - _ -> - {skip,InitFailed} + end end. -init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) -> +init_tc1(?MODULE,_,error_in_suite,[Config0]) when is_list(Config0) -> + ct_logs:init_tc(false), + ct_event:notify(#event{name=tc_start, + node=node(), + data={?MODULE,error_in_suite}}), + case ?val(error, Config0) of + undefined -> + {skip,"unknown_error_in_suite"}; + Reason -> + {skip,Reason} + end; + +init_tc1(Mod,Suite,Func,[Config0]) when is_list(Config0) -> Config1 = case ct_util:read_suite_data(last_saved_config) of - {{Mod,LastFunc},SavedConfig} -> % last testcase + {{Suite,LastFunc},SavedConfig} -> % last testcase [{saved_config,{LastFunc,SavedConfig}} | lists:keydelete(saved_config,1,Config0)]; - {{LastSuite,InitOrEnd},SavedConfig} when InitOrEnd == init_per_suite ; - InitOrEnd == end_per_suite -> + {{LastSuite,InitOrEnd}, + SavedConfig} when InitOrEnd == init_per_suite ; + InitOrEnd == end_per_suite -> %% last suite [{saved_config,{LastSuite,SavedConfig}} | lists:keydelete(saved_config,1,Config0)]; @@ -114,63 +123,57 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) -> end, ct_util:delete_suite_data(last_saved_config), Config = lists:keydelete(watchdog,1,Config1), - if Func /= init_per_suite, DoInit /= true -> - ok; - true -> + + if Func == init_per_suite -> %% delete all default values used in previous suite ct_config:delete_default_config(suite), %% release all name -> key bindings (once per suite) - ct_config:release_allocated() + ct_config:release_allocated(); + Func /= init_per_suite -> + ok end, - TestCaseInfo = - case catch apply(Mod,Func,[]) of - Result when is_list(Result) -> Result; - _ -> [] - end, + + GroupPath = ?val(tc_group_path, Config, []), + AllGroups = [?val(tc_group_properties, Config, []) | GroupPath], + %% clear all config data default values set by previous %% testcase info function (these should only survive the %% testcase, not the whole suite) - ct_config:delete_default_config(testcase), - case add_defaults(Mod,Func,TestCaseInfo,DoInit) of + FuncSpec = group_or_func(Func,Config0), + if is_tuple(FuncSpec) -> % group + ok; + true -> + ct_config:delete_default_config(testcase) + end, + case add_defaults(Mod,Func,AllGroups) of Error = {suite0_failed,_} -> ct_logs:init_tc(false), - FuncSpec = group_or_func(Func,Config0), ct_event:notify(#event{name=tc_start, node=node(), data={Mod,FuncSpec}}), - ct_util:set_testdata({curr_tc,{Mod,Error}}), + ct_util:set_testdata({curr_tc,{Suite,Error}}), {error,Error}; {SuiteInfo,MergeResult} -> case MergeResult of - {error,Reason} when DoInit == false -> + {error,Reason} -> ct_logs:init_tc(false), - FuncSpec = group_or_func(Func,Config0), ct_event:notify(#event{name=tc_start, node=node(), data={Mod,FuncSpec}}), {skip,Reason}; _ -> - init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) + init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) end end; -init_tc1(_Mod,_Func,Args,_DoInit) -> - {ok,Args}. -init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) -> - %% if first testcase fails when there's no init_per_suite - %% we must do suite/0 configurations before skipping test - MergedInfo = - case MergeResult of - {error,_} when DoInit == true -> - SuiteInfo; - _ -> - MergeResult - end, +init_tc1(_Mod,_Suite,_Func,Args) -> + {ok,Args}. +init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> %% timetrap must be handled before require - MergedInfo1 = timetrap_first(MergedInfo, [], []), + MergedInfo = timetrap_first(MergeResult, [], []), %% tell logger to use specified style sheet - case lists:keysearch(stylesheet,1,MergedInfo++Config) of + case lists:keysearch(stylesheet,1,MergeResult++Config) of {value,{stylesheet,SSFile}} -> ct_logs:set_stylesheet(Func,add_data_dir(SSFile,Config)); _ -> @@ -185,7 +188,7 @@ init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) -> %% list of {Type,Bool} tuples, e.g. {telnet,true}), case ct_util:get_overridden_silenced_connections() of undefined -> - case lists:keysearch(silent_connections,1,MergedInfo++Config) of + case lists:keysearch(silent_connections,1,MergeResult++Config) of {value,{silent_connections,Conns}} -> ct_util:silence_connections(Conns); _ -> @@ -194,17 +197,14 @@ init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) -> Conns -> ct_util:silence_connections(Conns) end, - if Func /= init_per_suite, DoInit /= true -> - ct_logs:init_tc(false); - true -> - ct_logs:init_tc(true) - end, + ct_logs:init_tc(Func == init_per_suite), FuncSpec = group_or_func(Func,Config), ct_event:notify(#event{name=tc_start, node=node(), data={Mod,FuncSpec}}), - - case catch configure(MergedInfo1,MergedInfo1,SuiteInfo,{Func,DoInit},Config) of + + case catch configure(MergedInfo,MergedInfo,SuiteInfo, + FuncSpec,Config) of {suite0_failed,Reason} -> ct_util:set_testdata({curr_tc,{Mod,{suite0_failed,{require,Reason}}}}), {skip,{require_failed_in_suite0,Reason}}; @@ -212,48 +212,42 @@ init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) -> {auto_skip,{require_failed,Reason}}; {'EXIT',Reason} -> {auto_skip,Reason}; - {ok, FinalConfig} -> - case MergeResult of - {error,Reason} -> - %% suite0 configure finished now, report that - %% first test case actually failed - {skip,Reason}; - _ -> - case get('$test_server_framework_test') of - undefined -> - ct_suite_init(Mod, FuncSpec, FinalConfig); - Fun -> - case Fun(init_tc, FinalConfig) of - NewConfig when is_list(NewConfig) -> - {ok,NewConfig}; - Else -> - Else - end + {ok,Config1} -> + case get('$test_server_framework_test') of + undefined -> + ct_suite_init(Suite, FuncSpec, Config1); + Fun -> + case Fun(init_tc, Config1) of + NewConfig when is_list(NewConfig) -> + {ok,NewConfig}; + Else -> + Else end end end. -ct_suite_init(Mod, Func, [Config]) when is_list(Config) -> - case ct_hooks:init_tc( Mod, Func, Config) of +ct_suite_init(Suite, Func, [Config]) when is_list(Config) -> + case ct_hooks:init_tc(Suite, Func, Config) of NewConfig when is_list(NewConfig) -> {ok, [NewConfig]}; Else -> Else end. -add_defaults(Mod,Func,FuncInfo,DoInit) -> - case (catch Mod:suite()) of +add_defaults(Mod,Func, GroupPath) -> + Suite = get_suite_name(Mod, GroupPath), + case (catch Suite:suite()) of {'EXIT',{undef,_}} -> - SuiteInfo = merge_with_suite_defaults(Mod,[]), + SuiteInfo = merge_with_suite_defaults(Suite,[]), SuiteInfoNoCTH = [I || I <- SuiteInfo, element(1,I) =/= ct_hooks], - case add_defaults1(Mod,Func,FuncInfo,SuiteInfoNoCTH,DoInit) of + case add_defaults1(Mod,Func, GroupPath, SuiteInfoNoCTH) of Error = {error,_} -> {SuiteInfo,Error}; MergedInfo -> {SuiteInfo,MergedInfo} end; {'EXIT',Reason} -> ErrStr = io_lib:format("~n*** ERROR *** " "~w:suite/0 failed: ~p~n", - [Mod,Reason]), + [Suite,Reason]), io:format(ErrStr, []), io:format(user, ErrStr, []), {suite0_failed,{exited,Reason}}; @@ -262,18 +256,18 @@ add_defaults(Mod,Func,FuncInfo,DoInit) -> (_) -> false end, SuiteInfo) of true -> - SuiteInfo1 = merge_with_suite_defaults(Mod,SuiteInfo), + SuiteInfo1 = merge_with_suite_defaults(Suite, SuiteInfo), SuiteInfoNoCTH = [I || I <- SuiteInfo1, element(1,I) =/= ct_hooks], - case add_defaults1(Mod,Func,FuncInfo, - SuiteInfoNoCTH,DoInit) of + case add_defaults1(Mod,Func, GroupPath, + SuiteInfoNoCTH) of Error = {error,_} -> {SuiteInfo1,Error}; MergedInfo -> {SuiteInfo1,MergedInfo} end; false -> ErrStr = io_lib:format("~n*** ERROR *** " "Invalid return value from " - "~w:suite/0: ~p~n", [Mod,SuiteInfo]), + "~w:suite/0: ~p~n", [Suite,SuiteInfo]), io:format(ErrStr, []), io:format(user, ErrStr, []), {suite0_failed,bad_return_value} @@ -281,57 +275,157 @@ add_defaults(Mod,Func,FuncInfo,DoInit) -> SuiteInfo -> ErrStr = io_lib:format("~n*** ERROR *** " "Invalid return value from " - "~w:suite/0: ~p~n", [Mod,SuiteInfo]), + "~w:suite/0: ~p~n", [Suite,SuiteInfo]), io:format(ErrStr, []), io:format(user, ErrStr, []), {suite0_failed,bad_return_value} end. -add_defaults1(_Mod,init_per_suite,[],SuiteInfo,_DoInit) -> - SuiteInfo; - -add_defaults1(Mod,Func,FuncInfo,SuiteInfo,DoInit) -> - %% mustn't re-require suite variables in test case info function, - %% can result in weird behaviour (suite values get overwritten) +add_defaults1(Mod,Func, GroupPath, SuiteInfo) -> + Suite = get_suite_name(Mod, GroupPath), + %% GroupPathInfo (for subgroup on level X) = + %% [LevelXGroupInfo, LevelX-1GroupInfo, ..., TopLevelGroupInfo] + GroupPathInfo = + lists:map(fun(GroupProps) -> + Name = ?val(name, GroupProps), + case catch Suite:group(Name) of + GrInfo when is_list(GrInfo) -> GrInfo; + _ -> [] + end + end, GroupPath), + Args = if Func == init_per_group ; Func == end_per_group -> + [?val(name, hd(GroupPath))]; + true -> + [] + end, + TestCaseInfo = + case catch apply(Mod,Func,Args) of + TCInfo when is_list(TCInfo) -> TCInfo; + _ -> [] + end, + %% let test case info (also for all config funcs) override group info, + %% and lower level group info override higher level info + TCAndGroupInfo = [TestCaseInfo | remove_info_in_prev(TestCaseInfo, + GroupPathInfo)], + %% find and save require terms found in suite info SuiteReqs = [SDDef || SDDef <- SuiteInfo, ((require == element(1,SDDef)) or (default_config == element(1,SDDef)))], - FuncReqs = - [FIDef || FIDef <- FuncInfo, - require == element(1,FIDef)], - case [element(2,Clash) || Clash <- SuiteReqs, - require == element(1, Clash), - true == lists:keymember(element(2,Clash),2, - FuncReqs)] of + case check_for_clashes(TestCaseInfo, GroupPathInfo, SuiteReqs) of [] -> - add_defaults2(Mod,Func,FuncInfo,SuiteInfo,SuiteReqs,DoInit); + add_defaults2(Mod,Func, TCAndGroupInfo,SuiteInfo,SuiteReqs); Clashes -> {error,{config_name_already_in_use,Clashes}} end. -add_defaults2(Mod,init_per_suite,IPSInfo,SuiteInfo,SuiteReqs,false) -> - %% not common practise to use a test case info function for - %% init_per_suite (usually handled by suite/0), but let's support - %% it just in case... - add_defaults2(Mod,init_per_suite,IPSInfo,SuiteInfo,SuiteReqs,true); - -add_defaults2(_Mod,_Func,FuncInfo,SuiteInfo,_,false) -> - %% include require elements from test case info, but not from suite/0 - %% (since we've already required those vars) - FuncInfo ++ - [SFDef || SFDef <- SuiteInfo, - require /= element(1,SFDef), - false == lists:keymember(element(1,SFDef),1,FuncInfo)]; - -add_defaults2(_Mod,_Func,FuncInfo,SuiteInfo,SuiteReqs,true) -> - %% We must include require elements from suite/0 here since - %% there's no init_per_suite call before this first test case. - %% Let other test case info elements override those from suite/0. - FuncInfo ++ SuiteReqs ++ - [SDDef || SDDef <- SuiteInfo, - require /= element(1,SDDef), - false == lists:keymember(element(1,SDDef),1,FuncInfo)]. +get_suite_name(?MODULE, [Cfg|_]) when is_list(Cfg), Cfg /= [] -> + get_suite_name(?MODULE, Cfg); + +get_suite_name(?MODULE, Cfg) when is_list(Cfg), Cfg /= [] -> + case ?val(tc_group_properties, Cfg) of + undefined -> + case ?val(suite, Cfg) of + undefined -> ?MODULE; + Suite -> Suite + end; + GrProps -> + case ?val(suite, GrProps) of + undefined -> ?MODULE; + Suite -> Suite + end + end; +get_suite_name(Mod, _) -> + Mod. + +%% Check that alias names are not already in use +check_for_clashes(TCInfo, [CurrGrInfo|Path], SuiteInfo) -> + ReqNames = fun(Info) -> [element(2,R) || R <- Info, + size(R) == 3, + require == element(1,R)] + end, + ExistingNames = lists:flatten([ReqNames(L) || L <- [SuiteInfo|Path]]), + CurrGrReqNs = ReqNames(CurrGrInfo), + GrClashes = [Name || Name <- CurrGrReqNs, + true == lists:member(Name, ExistingNames)], + AllReqNs = CurrGrReqNs ++ ExistingNames, + TCClashes = [Name || Name <- ReqNames(TCInfo), + true == lists:member(Name, AllReqNs)], + TCClashes ++ GrClashes. + +%% Delete the info terms in Terms from all following info lists +remove_info_in_prev(Terms, [[] | Rest]) -> + [[] | remove_info_in_prev(Terms, Rest)]; +remove_info_in_prev(Terms, [Info | Rest]) -> + UniqueInInfo = [U || U <- Info, + ((timetrap == element(1,U)) and + (not lists:keymember(timetrap,1,Terms))) or + ((require == element(1,U)) and + (not lists:member(U,Terms))) or + ((default_config == element(1,U)) and + (not keysmember([default_config,1, + element(2,U),2], Terms)))], + OtherTermsInInfo = [T || T <- Info, + timetrap /= element(1,T), + require /= element(1,T), + default_config /= element(1,T), + false == lists:keymember(element(1,T),1, + Terms)], + KeptInfo = UniqueInInfo ++ OtherTermsInInfo, + [KeptInfo | remove_info_in_prev(Terms ++ KeptInfo, Rest)]; +remove_info_in_prev(_, []) -> + []. + +keysmember([Key,Pos|Next], List) -> + case [Elem || Elem <- List, Key == element(Pos,Elem)] of + [] -> false; + Found -> keysmember(Next, Found) + end; +keysmember([], _) -> true. + + +add_defaults2(_Mod,init_per_suite, IPSInfo, SuiteInfo,SuiteReqs) -> + Info = lists:flatten([IPSInfo, SuiteReqs]), + lists:flatten([Info,remove_info_in_prev(Info, [SuiteInfo])]); + +add_defaults2(_Mod,init_per_group, IPGAndGroupInfo, SuiteInfo,SuiteReqs) -> + SuiteInfo1 = + remove_info_in_prev(lists:flatten([IPGAndGroupInfo, + SuiteReqs]), [SuiteInfo]), + %% don't require terms in prev groups (already processed) + case IPGAndGroupInfo of + [IPGInfo] -> + lists:flatten([IPGInfo,SuiteInfo1]); + [IPGInfo | [CurrGroupInfo | PrevGroupInfo]] -> + PrevGroupInfo1 = delete_require_terms(PrevGroupInfo), + lists:flatten([IPGInfo,CurrGroupInfo,PrevGroupInfo1, + SuiteInfo1]) + end; + +add_defaults2(_Mod,_Func, TCAndGroupInfo, SuiteInfo,SuiteReqs) -> + %% Include require elements from test case info and current group, + %% but not from previous groups or suite/0 (since we've already required + %% those vars). Let test case info elements override group and suite + %% info elements. + SuiteInfo1 = remove_info_in_prev(lists:flatten([TCAndGroupInfo, + SuiteReqs]), [SuiteInfo]), + %% don't require terms in prev groups (already processed) + case TCAndGroupInfo of + [TCInfo] -> + lists:flatten([TCInfo,SuiteInfo1]); + [TCInfo | [CurrGroupInfo | PrevGroupInfo]] -> + PrevGroupInfo1 = delete_require_terms(PrevGroupInfo), + lists:flatten([TCInfo,CurrGroupInfo,PrevGroupInfo1, + SuiteInfo1]) + end. + +delete_require_terms([Info | Prev]) -> + Info1 = [T || T <- Info, + require /= element(1,T), + default_config /= element(1,T)], + [Info1 | delete_require_terms(Prev)]; +delete_require_terms([]) -> + []. merge_with_suite_defaults(Mod,SuiteInfo) -> case lists:keysearch(suite_defaults,1,Mod:module_info(attributes)) of @@ -355,16 +449,17 @@ timetrap_first([Trap = {timetrap,_} | Rest],Info,Found) -> timetrap_first([Other | Rest],Info,Found) -> timetrap_first(Rest,[Other | Info],Found); timetrap_first([],Info,[]) -> - [{timetrap,{minutes,30}} | lists:reverse(Info)]; + [{timetrap,{minutes,30}} | ?rev(Info)]; timetrap_first([],Info,Found) -> - lists:reverse(Found) ++ lists:reverse(Info). + ?rev(Found) ++ ?rev(Info). configure([{require,Required}|Rest],Info,SuiteInfo,Scope,Config) -> case ct:require(Required) of ok -> configure(Rest,Info,SuiteInfo,Scope,Config); Error = {error,Reason} -> - case required_default('_UNDEF',Required,Info,SuiteInfo,Scope) of + case required_default('_UNDEF',Required,Info, + SuiteInfo,Scope) of ok -> configure(Rest,Info,SuiteInfo,Scope,Config); _ -> @@ -406,18 +501,15 @@ configure([],_,_,_,Config) -> {ok,[Config]}. %% the require element in Info may come from suite/0 and -%% should be scoped 'suite', or come from the testcase info -%% function and should then be scoped 'testcase' -required_default(Name,Key,Info,SuiteInfo,{Func,true}) -> - case try_set_default(Name,Key,SuiteInfo,suite) of - ok -> - ok; - _ -> - required_default(Name,Key,Info,[],{Func,false}) - end; -required_default(Name,Key,Info,_,{init_per_suite,_}) -> +%% should be scoped 'suite', or come from the group info +%% function and be scoped 'group', or come from the testcase +%% info function and then be scoped 'testcase' + +required_default(Name,Key,Info,_,init_per_suite) -> try_set_default(Name,Key,Info,suite); -required_default(Name,Key,Info,_,_) -> +required_default(Name,Key,Info,_,{init_per_group,GrName,_}) -> + try_set_default(Name,Key,Info,{group,GrName}); +required_default(Name,Key,Info,_,_FuncSpec) -> try_set_default(Name,Key,Info,testcase). try_set_default(Name,Key,Info,Where) -> @@ -467,6 +559,9 @@ end_tc(Mod,Func,{Result,[Args]}, Return) -> end_tc(Mod,Func,self(),Result,Args,Return). end_tc(Mod,Func,TCPid,Result,Args,Return) -> + %% in case Mod == ct_framework, lookup the suite name + Suite = get_suite_name(Mod, Args), + case lists:keysearch(watchdog,1,Args) of {value,{watchdog,Dog}} -> test_server:timetrap_cancel(Dog); false -> ok @@ -482,56 +577,62 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) -> end, ct_util:delete_testdata(comment), ct_util:delete_suite_data(last_saved_config), - FuncSpec = - case group_or_func(Func,Args) of - {_,GroupName,_Props} = Group -> - case lists:keysearch(save_config,1,Args) of - {value,{save_config,SaveConfig}} -> - ct_util:save_suite_data( - last_saved_config, - {Mod,{group,GroupName}}, - SaveConfig), - Group; - false -> - Group - end; - _ -> - case lists:keysearch(save_config,1,Args) of - {value,{save_config,SaveConfig}} -> - ct_util:save_suite_data(last_saved_config, - {Mod,Func},SaveConfig), - Func; - false -> - Func - end - end, - ct_util:reset_silent_connections(), + + FuncSpec = case group_or_func(Func,Args) of + {_,_GroupName,_} = Group -> Group; + _ -> Func + end, case get('$test_server_framework_test') of undefined -> {FinalResult,FinalNotify} = case ct_hooks:end_tc( - Mod, FuncSpec, Args, Result, Return) of + Suite, FuncSpec, Args, Result, Return) of '$ct_no_change' -> {ok,Result}; FinalResult1 -> {FinalResult1,FinalResult1} end, - % send sync notification so that event handlers may print - % in the log file before it gets closed + %% send sync notification so that event handlers may print + %% in the log file before it gets closed ct_event:sync_notify(#event{name=tc_done, node=node(), data={Mod,FuncSpec, tag_cth(FinalNotify)}}); Fun -> - % send sync notification so that event handlers may print - % in the log file before it gets closed + %% send sync notification so that event handlers may print + %% in the log file before it gets closed ct_event:sync_notify(#event{name=tc_done, node=node(), data={Mod,FuncSpec,tag(Result)}}), FinalResult = Fun(end_tc, Return) + end, + + case FuncSpec of + {_,GroupName,_Props} -> + if Func == end_per_group -> + ct_config:delete_default_config({group,GroupName}); + true -> ok + end, + case lists:keysearch(save_config,1,Args) of + {value,{save_config,SaveConfig}} -> + ct_util:save_suite_data(last_saved_config, + {Suite,{group,GroupName}}, + SaveConfig); + false -> + ok + end; + _ -> + case lists:keysearch(save_config,1,Args) of + {value,{save_config,SaveConfig}} -> + ct_util:save_suite_data(last_saved_config, + {Suite,Func},SaveConfig); + false -> + ok + end end, + ct_util:reset_silent_connections(), case FinalResult of {skip,{sequence_failed,_,_}} -> @@ -548,7 +649,7 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) -> end, case Func of end_per_suite -> - ct_util:match_delete_suite_data({seq,Mod,'_'}); + ct_util:match_delete_suite_data({seq,Suite,'_'}); _ -> ok end, @@ -648,31 +749,36 @@ error_notification(Mod,Func,_Args,{Error,Loc}) -> end end, - io:format(user, "~n- - - - - - - - - - - - - - - - " - "- - - - - - - - - -~n", []), + PrintErr = fun(ErrFormat, ErrArgs) -> + Div = "~n- - - - - - - - - - - - - - - - " + "- - - - - - - - - -~n", + io:format(user, lists:concat([Div,ErrFormat,Div,"~n"]), + ErrArgs), + ct_logs:tc_log(ct_error_notify, "CT Error Notification", + ErrFormat, ErrArgs) + end, case Loc of - %% we don't use the line parse transform as we compile this - %% module so location will be on form {M,F} [{?MODULE,error_in_suite}] -> - io:format(user, "Error in suite detected: ~s", [ErrStr]); + PrintErr("Error in suite detected: ~s", [ErrStr]); - R when R == unknown; R == undefined -> - io:format(user, "Error detected: ~s", [ErrStr]); + R when R == unknown; R == undefined -> + PrintErr("Error detected: ~s", [ErrStr]); %% if a function specified by all/0 does not exist, we %% pick up undef here - [{LastMod,LastFunc}] -> - io:format(user, "~w:~w could not be executed~n", - [LastMod,LastFunc]), - io:format(user, "Reason: ~s", [ErrStr]); + [{LastMod,LastFunc}|_] when ErrStr == "undef" -> + PrintErr("~w:~w could not be executed~nReason: ~s", + [LastMod,LastFunc,ErrStr]); + + [{LastMod,LastFunc}|_] -> + PrintErr("~w:~w failed~nReason: ~s", [LastMod,LastFunc,ErrStr]); [{LastMod,LastFunc,LastLine}|_] -> %% print error to console, we are only %% interested in the last executed expression - io:format(user, "~w:~w failed on line ~w~n", - [LastMod,LastFunc,LastLine]), - io:format(user, "Reason: ~s", [ErrStr]), - + PrintErr("~w:~w failed on line ~w~nReason: ~s", + [LastMod,LastFunc,LastLine,ErrStr]), + case ct_util:read_suite_data({seq,Mod,Func}) of undefined -> ok; @@ -681,8 +787,6 @@ error_notification(Mod,Func,_Args,{Error,Loc}) -> mark_as_failed(Seq,Mod,Func,SeqTCs) end end, - io:format(user, "~n- - - - - - - - - - - - - - - - " - "- - - - - - - - - -~n~n", []), ok. %% cases in seq that have already run @@ -704,11 +808,11 @@ mark_as_failed1(_,_,_,[]) -> group_or_func(Func, Config) when Func == init_per_group; Func == end_per_group -> - case proplists:get_value(tc_group_properties,Config) of + case ?val(tc_group_properties, Config) of undefined -> {Func,unknown,[]}; GrProps -> - GrName = proplists:get_value(name,GrProps), + GrName = ?val(name,GrProps), {Func,GrName,proplists:delete(name,GrProps)} end; group_or_func(Func, _Config) -> @@ -732,7 +836,7 @@ get_suite(Mod, all) -> %% (and only) test case so we can report Error properly [{?MODULE,error_in_suite,[[Error]]}]; ConfTests -> - get_all(Mod, ConfTests) + get_all(Mod, ConfTests) end; _ -> E = "Bad return value from "++atom_to_list(Mod)++":groups/0", @@ -746,7 +850,7 @@ get_suite(Mod, all) -> %% group get_suite(Mod, Group={conf,Props,_Init,TCs,_End}) -> - Name = proplists:get_value(name, Props), + Name = ?val(name, Props), case catch apply(Mod, groups, []) of {'EXIT',_} -> [Group]; @@ -764,14 +868,25 @@ get_suite(Mod, Group={conf,Props,_Init,TCs,_End}) -> %% a *subgroup* specified *only* as skipped (and not %% as an explicit test) should not be returned, or %% init/end functions for top groups will be executed - case catch proplists:get_value(name, element(2, hd(ConfTests))) of + case catch ?val(name, element(2, hd(ConfTests))) of Name -> % top group delete_subs(ConfTests, ConfTests); _ -> [] end; false -> - delete_subs(ConfTests, ConfTests) + ConfTests1 = delete_subs(ConfTests, ConfTests), + case ?val(override, Props) of + undefined -> + ConfTests1; + [] -> + ConfTests1; + ORSpec -> + ORSpec1 = if is_tuple(ORSpec) -> [ORSpec]; + true -> ORSpec end, + search_and_override(ConfTests1, + ORSpec1, Mod) + end end end; _ -> @@ -793,13 +908,12 @@ get_all_cases(Suite) -> {error,Error}; Tests -> Cases = get_all_cases1(Suite, Tests), - lists:reverse( - lists:foldl(fun(TC, TCs) -> - case lists:member(TC, TCs) of + ?rev(lists:foldl(fun(TC, TCs) -> + case lists:member(TC, TCs) of true -> TCs; - false -> [TC | TCs] - end - end, [], Cases)) + false -> [TC | TCs] + end + end, [], Cases)) end. get_all_cases1(Suite, [{conf,_Props,_Init,GrTests,_End} | Tests]) -> @@ -918,14 +1032,14 @@ delete_subs([], All) -> All. delete_conf({conf,Props,_,_,_}, Confs) -> - Name = proplists:get_value(name, Props), + Name = ?val(name, Props), [Conf || Conf = {conf,Props0,_,_,_} <- Confs, - Name =/= proplists:get_value(name, Props0)]. + Name =/= ?val(name, Props0)]. is_sub({conf,Props,_,_,_}=Conf, [{conf,_,_,Tests,_} | Confs]) -> - Name = proplists:get_value(name, Props), + Name = ?val(name, Props), case lists:any(fun({conf,Props0,_,_,_}) -> - case proplists:get_value(name, Props0) of + case ?val(name, Props0) of N when N == Name -> true; _ -> @@ -1036,8 +1150,8 @@ make_conf(Mod, Name, Props, TestSpec) -> "end_per_group/2 missing for group " "~p in ~p, using default.", [Name,Mod]), - {{?MODULE,ct_init_per_group}, - {?MODULE,ct_end_per_group}, + {{?MODULE,init_per_group}, + {?MODULE,end_per_group}, [{suite,Mod}]} end, {conf,[{name,Name}|Props++ExtraProps],InitConf,TestSpec,EndConf}. @@ -1078,29 +1192,116 @@ expand_groups([H | T], ConfTests, Mod) -> expand_groups([], _ConfTests, _Mod) -> []; expand_groups({group,Name}, ConfTests, Mod) -> - FindConf = - fun({conf,Props,_,_,_}) -> - case proplists:get_value(name, Props) of - Name -> true; - _ -> false + expand_groups({group,Name,default,[]}, ConfTests, Mod); +expand_groups({group,Name,default}, ConfTests, Mod) -> + expand_groups({group,Name,default,[]}, ConfTests, Mod); +expand_groups({group,Name,ORProps}, ConfTests, Mod) when is_list(ORProps) -> + expand_groups({group,Name,ORProps,[]}, ConfTests, Mod); +expand_groups({group,Name,ORProps,SubORSpec}, ConfTests, Mod) -> + FindConf = + fun(Conf = {conf,Props,Init,Ts,End}) -> + case ?val(name, Props) of + Name when ORProps == default -> + [Conf]; + Name -> + [{conf,[{name,Name}|ORProps],Init,Ts,End}]; + _ -> + [] end end, - case lists:filter(FindConf, ConfTests) of - [ConfTest|_] -> - expand_groups(ConfTest, ConfTests, Mod); + case lists:flatmap(FindConf, ConfTests) of [] -> - E = "Invalid reference to group "++ - atom_to_list(Name)++" in "++ - atom_to_list(Mod)++":all/0", - throw({error,list_to_atom(E)}) + throw({error,invalid_ref_msg(Name, Mod)}); + Matching when SubORSpec == [] -> + Matching; + Matching -> + override_props(Matching, SubORSpec, Name,Mod) end; expand_groups(SeqOrTC, _ConfTests, _Mod) -> SeqOrTC. +%% search deep for the matching conf test and modify it and any +%% sub tests according to the override specification +search_and_override([Conf = {conf,Props,Init,Tests,End}], ORSpec, Mod) -> + Name = ?val(name, Props), + case lists:keysearch(Name, 1, ORSpec) of + {value,{Name,default}} -> + [Conf]; + {value,{Name,ORProps}} -> + [{conf,[{name,Name}|ORProps],Init,Tests,End}]; + {value,{Name,default,[]}} -> + [Conf]; + {value,{Name,default,SubORSpec}} -> + override_props([Conf], SubORSpec, Name,Mod); + {value,{Name,ORProps,SubORSpec}} -> + override_props([{conf,[{name,Name}|ORProps], + Init,Tests,End}], SubORSpec, Name,Mod); + _ -> + [{conf,Props,Init,search_and_override(Tests,ORSpec,Mod),End}] + end. + +%% Modify the Tests element according to the override specification +override_props([{conf,Props,Init,Tests,End} | Confs], SubORSpec, Name,Mod) -> + {Subs,SubORSpec1} = override_sub_props(Tests, [], SubORSpec, Mod), + [{conf,Props,Init,Subs,End} | override_props(Confs, SubORSpec1, Name,Mod)]; +override_props([], [], _,_) -> + []; +override_props([], SubORSpec, Name,Mod) -> + Es = [invalid_ref_msg(Name, element(1,Spec), Mod) || Spec <- SubORSpec], + throw({error,Es}). + +override_sub_props([], New, ORSpec, _) -> + {?rev(New),ORSpec}; +override_sub_props([T = {conf,Props,Init,Tests,End} | Ts], + New, ORSpec, Mod) -> + Name = ?val(name, Props), + case lists:keysearch(Name, 1, ORSpec) of + {value,Spec} -> % group found in spec + Props1 = + case element(2, Spec) of + default -> Props; + ORProps -> [{name,Name} | ORProps] + end, + case catch element(3, Spec) of + Undef when Undef == [] ; 'EXIT' == element(1, Undef) -> + override_sub_props(Ts, [{conf,Props1,Init,Tests,End} | New], + lists:keydelete(Name, 1, ORSpec), Mod); + SubORSpec when is_list(SubORSpec) -> + case override_sub_props(Tests, [], SubORSpec, Mod) of + {Subs,[]} -> + override_sub_props(Ts, [{conf,Props1,Init, + Subs,End} | New], + lists:keydelete(Name, 1, ORSpec), + Mod); + {_,NonEmptySpec} -> + Es = [invalid_ref_msg(Name, element(1, GrRef), + Mod) || GrRef <- NonEmptySpec], + throw({error,Es}) + end; + BadGrSpec -> + throw({error,{invalid_form,BadGrSpec}}) + end; + _ -> % not a group in spec + override_sub_props(Ts, [T | New], ORSpec, Mod) + end; +override_sub_props([TC | Ts], New, ORSpec, Mod) -> + override_sub_props(Ts, [TC | New], ORSpec, Mod). + +invalid_ref_msg(Name, Mod) -> + E = "Invalid reference to group "++ + atom_to_list(Name)++" in "++ + atom_to_list(Mod)++":all/0", + list_to_atom(E). + +invalid_ref_msg(Name0, Name1, Mod) -> + E = "Invalid reference to group "++ + atom_to_list(Name1)++" from "++atom_to_list(Name0)++ + " in "++atom_to_list(Mod)++":all/0", + list_to_atom(E). %%!============================================================ %%! The support for sequences by means of using sequences/0 -%%! will be removed in OTP R14. The code below is only kept +%%! will be removed in OTP R15. The code below is only kept %%! for backwards compatibility. From OTP R13 groups with %%! sequence property should be used instead! %%!============================================================ @@ -1210,22 +1411,31 @@ error_in_suite(Config) -> Reason = test_server:lookup_config(error,Config), exit(Reason). +%% if init_per_suite and end_per_suite are missing in the suite, +%% these will be called instead (without any trace of them in the +%% log files), only so that it's possible to call hook functions +%% for configuration +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + %% if the group config functions are missing in the suite, %% use these instead -ct_init_per_group(GroupName, Config) -> +init_per_group(GroupName, Config) -> ct:comment(io_lib:format("start of ~p", [GroupName])), ct_logs:log("TEST INFO", "init_per_group/2 for ~w missing " "in suite, using default.", [GroupName]), Config. -ct_end_per_group(GroupName, _) -> +end_per_group(GroupName, _) -> ct:comment(io_lib:format("end of ~p", [GroupName])), ct_logs:log("TEST INFO", "end_per_group/2 for ~w missing " "in suite, using default.", [GroupName]), ok. - %%%----------------------------------------------------------------- %%% @spec report(What,Data) -> ok @@ -1234,8 +1444,8 @@ report(What,Data) -> loginfo -> %% logfiles and direcories have been created for a test and the %% top level test index page needs to be refreshed - TestName = filename:basename(proplists:get_value(topdir, Data), ".logs"), - RunDir = proplists:get_value(rundir, Data), + TestName = filename:basename(?val(topdir, Data), ".logs"), + RunDir = ?val(rundir, Data), ct_logs:make_all_suites_index({TestName,RunDir}), ok; tests_start -> @@ -1274,6 +1484,10 @@ report(What,Data) -> tests_done -> ok; tc_start -> + %% Data = {{Suite,Func},LogFileName} + ct_event:sync_notify(#event{name=tc_logfile, + node=node(), + data=Data}), ok; tc_done -> {_Suite,Case,Result} = Data, @@ -1298,10 +1512,6 @@ report(What,Data) -> ok; {end_per_group,_} -> ok; - {ct_init_per_group,_} -> - ok; - {ct_end_per_group,_} -> - ok; {_,ok} -> add_to_stats(ok); {_,{skipped,{failed,{_,init_per_testcase,_}}}} -> @@ -1337,11 +1547,23 @@ report(What,Data) -> node=node(), data=Data}), ct_hooks:on_tc_skip(What, Data), - if Case /= end_per_suite, Case /= end_per_group -> + if Case /= end_per_suite, + Case /= end_per_group -> add_to_stats(auto_skipped); true -> ok end; + framework_error -> + case Data of + {{M,F},E} -> + ct_event:sync_notify(#event{name=tc_done, + node=node(), + data={M,F,{framework_error,E}}}); + _ -> + ct_event:sync_notify(#event{name=tc_done, + node=node(), + data=Data}) + end; _ -> ok end, @@ -1411,30 +1633,6 @@ format_comment(Comment) -> "<font color=\"green\">" ++ Comment ++ "</font>". %%%----------------------------------------------------------------- -%%% @spec overview_html_header(TestName) -> Header -overview_html_header(TestName) -> - TestName1 = lists:flatten(io_lib:format("~p", [TestName])), - Label = case application:get_env(common_test, test_label) of - {ok,Lbl} when Lbl =/= undefined -> - "<H1><FONT color=\"green\">" ++ Lbl ++ "</FONT></H1>\n"; - _ -> - "" - end, - Bgr = case ct_logs:basic_html() of - true -> - ""; - false -> - CTPath = code:lib_dir(common_test), - TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"), - " background=\"" ++ TileFile ++ "\"" - end, - - ["<html>\n", - "<head><title>Test ", TestName1, " results</title>\n", - "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", - "</head>\n", - "<body", Bgr, " bgcolor=\"white\" text=\"black\" ", - "link=\"blue\" vlink=\"purple\" alink=\"red\">\n", - Label, - "<H2>Results from test ", TestName1, "</H2>\n"]. - +%%% @spec get_html_wrapper(TestName, PrintLabel, Cwd) -> Header +get_html_wrapper(TestName, PrintLabel, Cwd) -> + ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd). diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl index f243b87f54..0fe6e03079 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-2011. 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 @@ -34,6 +34,12 @@ %% If you change this, remember to update ct_util:look -> stop clause as well. -define(config_name, ct_hooks). +%% All of the hooks which are to be started by default. Remove by issuing +%% -enable_builtin_hooks false to when starting common test. +-define(BUILTIN_HOOKS,[#ct_hook_config{ module = cth_log_redirect, + opts = [], + prio = ctfirst }]). + -record(ct_hook_config, {id, module, prio, scope, opts = [], state = []}). %% ------------------------------------------------------------------------- @@ -44,7 +50,8 @@ -spec init(State :: term()) -> ok | {error, Reason :: term()}. init(Opts) -> - call(get_new_hooks(Opts, undefined), ok, init, []). + call(get_new_hooks(Opts, undefined) ++ get_builtin_hooks(Opts), + ok, init, []). %% @doc Called after all suites are done. @@ -63,8 +70,7 @@ terminate(Hooks) -> {skip, Reason :: term()} | {auto_skip, Reason :: term()} | {fail, Reason :: term()}. -init_tc(ct_framework, _Func, Args) -> - Args; + init_tc(Mod, init_per_suite, Config) -> Info = try proplists:get_value(ct_hooks, Mod:suite(),[]) of List when is_list(List) -> @@ -97,27 +103,21 @@ init_tc(_Mod, TC, Config) -> {auto_skip, Reason :: term()} | {fail, Reason :: term()} | ok | '$ct_no_change'. -end_tc(ct_framework, _Func, _Args, Result, _Return) -> - Result; end_tc(Mod, init_per_suite, Config, _Result, Return) -> call(fun call_generic/3, Return, [post_init_per_suite, Mod, Config], '$ct_no_change'); - end_tc(Mod, end_per_suite, Config, Result, _Return) -> call(fun call_generic/3, Result, [post_end_per_suite, Mod, Config], '$ct_no_change'); - end_tc(_Mod, {init_per_group, GroupName, _}, Config, _Result, Return) -> call(fun call_generic/3, Return, [post_init_per_group, GroupName, Config], '$ct_no_change'); - end_tc(Mod, {end_per_group, GroupName, Opts}, Config, Result, _Return) -> Res = call(fun call_generic/3, Result, [post_end_per_group, GroupName, Config], '$ct_no_change'), maybe_stop_locker(Mod, GroupName,Opts), Res; - end_tc(_Mod, TC, Config, Result, _Return) -> call(fun call_generic/3, Result, [post_end_per_testcase, TC, Config], '$ct_no_change'). @@ -171,8 +171,9 @@ call_generic(#ct_hook_config{ module = Mod, state = State} = Hook, call(Fun, Config, Meta) -> maybe_lock(), Hooks = get_hooks(), - Res = call(get_new_hooks(Config, Fun) ++ - [{HookId,Fun} || #ct_hook_config{id = HookId} <- Hooks], + Calls = get_new_hooks(Config, Fun) ++ + [{HookId,Fun} || #ct_hook_config{id = HookId} <- Hooks], + Res = call(resort(Calls,Hooks,Meta), remove(?config_name,Config), Meta, Hooks), maybe_unlock(), Res. @@ -198,7 +199,7 @@ call([{Hook, call_id, NextFun} | Rest], Config, Meta, Hooks) -> {Hooks ++ [NewHook], [{NewId, call_init}, {NewId,NextFun} | Rest]} end, - call(resort(NewRest,NewHooks), Config, Meta, NewHooks) + call(resort(NewRest,NewHooks,Meta), Config, Meta, NewHooks) catch Error:Reason -> Trace = erlang:get_stacktrace(), ct_logs:log("Suite Hook","Failed to start a CTH: ~p:~p", @@ -214,7 +215,7 @@ call([{HookId, Fun} | Rest], Config, Meta, Hooks) -> {NewConf, NewHook} = Fun(Hook, Config, Meta), NewCalls = get_new_hooks(NewConf, Fun), NewHooks = lists:keyreplace(HookId, #ct_hook_config.id, Hooks, NewHook), - call(resort(NewCalls ++ Rest,NewHooks), %% Resort if call_init changed prio + call(resort(NewCalls ++ Rest,NewHooks,Meta), %% Resort if call_init changed prio remove(?config_name, NewConf), Meta, terminate_if_scope_ends(HookId, Meta, NewHooks)) catch throw:{error_in_cth_call,Reason} -> @@ -283,6 +284,14 @@ get_new_hooks(Config) when is_list(Config) -> get_new_hooks(_Config) -> []. +get_builtin_hooks(Opts) -> + case proplists:get_value(enable_builtin_hooks,Opts) of + false -> + []; + _Else -> + [{HookConf, call_id, undefined} || HookConf <- ?BUILTIN_HOOKS] + end. + save_suite_data_async(Hooks) -> ct_util:save_suite_data_async(?config_name, Hooks). @@ -290,9 +299,21 @@ get_hooks() -> lists:keysort(#ct_hook_config.prio,ct_util:read_suite_data(?config_name)). %% Sort all calls in this order: -%% call_id < call_init < Hook Priority 1 < .. < Hook Priority N +%% call_id < call_init < ctfirst < Priority 1 < .. < Priority N < ctlast %% If Hook Priority is equal, check when it has been installed and %% sort on that instead. +%% If we are doing a cleanup call i.e. {post,pre}_end_per_*, all priorities +%% are reversed. Probably want to make this sorting algorithm pluginable +%% as some point... +resort(Calls,Hooks,[F|_R]) when F == post_end_per_testcase; + F == pre_end_per_group; + F == post_end_per_group; + F == pre_end_per_suite; + F == post_end_per_suite -> + lists:reverse(resort(Calls,Hooks)); +resort(Calls,Hooks,_Meta) -> + resort(Calls,Hooks). + resort(Calls, Hooks) -> lists:sort( fun({_,_,_},_) -> @@ -311,6 +332,14 @@ resort(Calls, Hooks) -> %% If priorities are equal, we check the position in the %% hooks list pos(Id1,Hooks) < pos(Id2,Hooks); + P1 == ctfirst -> + true; + P2 == ctfirst -> + false; + P1 == ctlast -> + false; + P2 == ctlast -> + true; true -> P1 < P2 end @@ -331,7 +360,7 @@ catch_apply(M,F,A, Default) -> catch error:Reason -> case erlang:get_stacktrace() of %% Return the default if it was the CTH module which did not have the function. - [{M,F,A}|_] when Reason == undef -> + [{M,F,A,_}|_] when Reason == undef -> Default; Trace -> ct_logs:log("Suite Hook","Call to CTH failed: ~p:~p", diff --git a/lib/common_test/src/ct_line.erl b/lib/common_test/src/ct_line.erl deleted file mode 100644 index 4af9da5463..0000000000 --- a/lib/common_test/src/ct_line.erl +++ /dev/null @@ -1,266 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2003-2009. 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 -%% compliance with the License. You should have received a copy of the -%% Erlang Public License along with this software. If not, it can be -%% retrieved online at http://www.erlang.org/. -%% -%% Software distributed under the License is distributed on an "AS IS" -%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%% the License for the specific language governing rights and limitations -%% under the License. -%% -%% %CopyrightEnd% -%% - -%%% @doc Parse transform for inserting line numbers - --module(ct_line). - --record(vars, {module, % atom() Module name - vsn, % atom() - - init_info=[], % [{M,F,A,C,L}] - - function, % atom() - arity, % int() - clause, % int() - lines, % [int()] - depth, % int() - is_guard=false % boolean - }). - --export([parse_transform/2, - line/1]). - -line(LOC={{Mod,Func},_Line}) -> - Lines = case get(test_server_loc) of - [{{Mod,Func},_}|Ls] -> - Ls; - Ls when is_list(Ls) -> - case length(Ls) of - 10 -> - [_|T]=lists:reverse(Ls), - lists:reverse(T); - _ -> - Ls - end; - _ -> - [] - end, - put(test_server_loc,[LOC|Lines]). - -parse_transform(Forms, _Options) -> - transform(Forms, _Options). - -%% forms(Fs) -> lists:map(fun (F) -> form(F) end, Fs). - -transform(Forms, _Options)-> - Vars0 = #vars{}, - {ok, MungedForms, _Vars} = transform(Forms, [], Vars0), - MungedForms. - - -transform([Form|Forms], MungedForms, Vars) -> - case munge(Form, Vars) of - ignore -> - transform(Forms, MungedForms, Vars); - {MungedForm, Vars2} -> - transform(Forms, [MungedForm|MungedForms], Vars2) - end; -transform([], MungedForms, Vars) -> - {ok, lists:reverse(MungedForms), Vars}. - -%% This code traverses the abstract code, stored as the abstract_code -%% chunk in the BEAM file, as described in absform(3) for Erlang/OTP R8B -%% (Vsn=abstract_v2). -%% The abstract format after preprocessing differs slightly from the abstract -%% format given eg using epp:parse_form, this has been noted in comments. -munge(Form={attribute,_,module,Module}, Vars) -> - Vars2 = Vars#vars{module=Module}, - {Form, Vars2}; - -munge({function,0,module_info,_Arity,_Clauses}, _Vars) -> - ignore; % module_info will be added again when the forms are recompiled -munge({function,Line,Function,Arity,Clauses}, Vars) -> - Vars2 = Vars#vars{function=Function, - arity=Arity, - clause=1, - lines=[], - depth=1}, - {MungedClauses, Vars3} = munge_clauses(Clauses, Vars2, []), - {{function,Line,Function,Arity,MungedClauses}, Vars3}; -munge(Form, Vars) -> % attributes - {Form, Vars}. - -munge_clauses([{clause,Line,Pattern,Guards,Body}|Clauses], Vars, MClauses) -> - {MungedGuards, _Vars} = munge_exprs(Guards, Vars#vars{is_guard=true},[]), - - case Vars#vars.depth of - 1 -> % function clause - {MungedBody, Vars2} = munge_body(Body, Vars#vars{depth=2}, []), - ClauseInfo = {Vars2#vars.module, - Vars2#vars.function, - Vars2#vars.arity, - Vars2#vars.clause, - length(Vars2#vars.lines)}, - InitInfo = [ClauseInfo | Vars2#vars.init_info], - Vars3 = Vars2#vars{init_info=InitInfo, - clause=(Vars2#vars.clause)+1, - lines=[], - depth=1}, - munge_clauses(Clauses, Vars3, - [{clause,Line,Pattern,MungedGuards,MungedBody}| - MClauses]); - - 2 -> % receive-, case- or if clause - {MungedBody, Vars2} = munge_body(Body, Vars, []), - munge_clauses(Clauses, Vars2, - [{clause,Line,Pattern,MungedGuards,MungedBody}| - MClauses]) - end; -munge_clauses([], Vars, MungedClauses) -> - {lists:reverse(MungedClauses), Vars}. - -munge_body([Expr|Body], Vars, MungedBody) -> - %% Here is the place to add a call to cover:bump/6! - Line = element(2, Expr), - Lines = Vars#vars.lines, - case lists:member(Line,Lines) of - true -> % already a bump at this line! - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - munge_body(Body, Vars2, [MungedExpr|MungedBody]); - false -> - Bump = {call, 0, {remote,0,{atom,0,?MODULE},{atom,0,line}}, - [{tuple,0,[{tuple,0,[{atom,0,Vars#vars.module}, - {atom, 0, Vars#vars.function}]}, - {integer, 0, Line}]}]}, - Lines2 = [Line|Lines], - - {MungedExpr, Vars2} = munge_expr(Expr, Vars#vars{lines=Lines2}), - munge_body(Body, Vars2, [MungedExpr,Bump|MungedBody]) - end; -munge_body([], Vars, MungedBody) -> - {lists:reverse(MungedBody), Vars}. - -munge_expr({match,Line,ExprL,ExprR}, Vars) -> - {MungedExprL, Vars2} = munge_expr(ExprL, Vars), - {MungedExprR, Vars3} = munge_expr(ExprR, Vars2), - {{match,Line,MungedExprL,MungedExprR}, Vars3}; -munge_expr({tuple,Line,Exprs}, Vars) -> - {MungedExprs, Vars2} = munge_exprs(Exprs, Vars, []), - {{tuple,Line,MungedExprs}, Vars2}; -munge_expr({record,Line,Expr,Exprs}, Vars) -> - %% Only for Vsn=raw_abstract_v1 - {MungedExprName, Vars2} = munge_expr(Expr, Vars), - {MungedExprFields, Vars3} = munge_exprs(Exprs, Vars2, []), - {{record,Line,MungedExprName,MungedExprFields}, Vars3}; -munge_expr({record_field,Line,ExprL,ExprR}, Vars) -> - %% Only for Vsn=raw_abstract_v1 - {MungedExprL, Vars2} = munge_expr(ExprL, Vars), - {MungedExprR, Vars3} = munge_expr(ExprR, Vars2), - {{record_field,Line,MungedExprL,MungedExprR}, Vars3}; -munge_expr({cons,Line,ExprH,ExprT}, Vars) -> - {MungedExprH, Vars2} = munge_expr(ExprH, Vars), - {MungedExprT, Vars3} = munge_expr(ExprT, Vars2), - {{cons,Line,MungedExprH,MungedExprT}, Vars3}; -munge_expr({op,Line,Op,ExprL,ExprR}, Vars) -> - {MungedExprL, Vars2} = munge_expr(ExprL, Vars), - {MungedExprR, Vars3} = munge_expr(ExprR, Vars2), - {{op,Line,Op,MungedExprL,MungedExprR}, Vars3}; -munge_expr({op,Line,Op,Expr}, Vars) -> - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - {{op,Line,Op,MungedExpr}, Vars2}; -munge_expr({'catch',Line,Expr}, Vars) -> - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - {{'catch',Line,MungedExpr}, Vars2}; -munge_expr({call,Line1,{remote,Line2,ExprM,ExprF},Exprs}, - Vars) when Vars#vars.is_guard==false-> - {MungedExprM, Vars2} = munge_expr(ExprM, Vars), - {MungedExprF, Vars3} = munge_expr(ExprF, Vars2), - {MungedExprs, Vars4} = munge_exprs(Exprs, Vars3, []), - {{call,Line1,{remote,Line2,MungedExprM,MungedExprF},MungedExprs}, Vars4}; -munge_expr({call,Line1,{remote,_Line2,_ExprM,ExprF},Exprs}, - Vars) when Vars#vars.is_guard==true -> - %% Difference in abstract format after preprocessing: BIF calls in guards - %% are translated to {remote,...} (which is not allowed as source form) - %% NOT NECESSARY FOR Vsn=raw_abstract_v1 - munge_expr({call,Line1,ExprF,Exprs}, Vars); -munge_expr({call,Line,Expr,Exprs}, Vars) -> - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - {MungedExprs, Vars3} = munge_exprs(Exprs, Vars2, []), - {{call,Line,MungedExpr,MungedExprs}, Vars3}; -munge_expr({lc,Line,Expr,LC}, Vars) -> - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - {MungedLC, Vars3} = munge_lc(LC, Vars2, []), - {{lc,Line,MungedExpr,MungedLC}, Vars3}; -munge_expr({block,Line,Body}, Vars) -> - {MungedBody, Vars2} = munge_body(Body, Vars, []), - {{block,Line,MungedBody}, Vars2}; -munge_expr({'if',Line,Clauses}, Vars) -> - {MungedClauses,Vars2} = munge_clauses(Clauses, Vars, []), - {{'if',Line,MungedClauses}, Vars2}; -munge_expr({'case',Line,Expr,Clauses}, Vars) -> - {MungedExpr,Vars2} = munge_expr(Expr,Vars), - {MungedClauses,Vars3} = munge_clauses(Clauses, Vars2, []), - {{'case',Line,MungedExpr,MungedClauses}, Vars3}; -munge_expr({'receive',Line,Clauses}, Vars) -> - {MungedClauses,Vars2} = munge_clauses(Clauses, Vars, []), - {{'receive',Line,MungedClauses}, Vars2}; -munge_expr({'receive',Line,Clauses,Expr,Body}, Vars) -> - {MungedClauses,Vars2} = munge_clauses(Clauses, Vars, []), - {MungedExpr, Vars3} = munge_expr(Expr, Vars2), - {MungedBody, Vars4} = munge_body(Body, Vars3, []), - {{'receive',Line,MungedClauses,MungedExpr,MungedBody}, Vars4}; -munge_expr({'try',Line,Exprs,Clauses,CatchClauses}, Vars) -> - {MungedExprs, Vars1} = munge_exprs(Exprs, Vars, []), - {MungedClauses, Vars2} = munge_clauses(Clauses, Vars1, []), - {MungedCatchClauses, Vars3} = munge_clauses(CatchClauses, Vars2, []), - {{'try',Line,MungedExprs,MungedClauses,MungedCatchClauses}, Vars3}; -%% Difference in abstract format after preprocessing: Funs get an extra -%% element Extra. -%% NOT NECESSARY FOR Vsn=raw_abstract_v1 -munge_expr({'fun',Line,{function,Name,Arity},_Extra}, Vars) -> - {{'fun',Line,{function,Name,Arity}}, Vars}; -munge_expr({'fun',Line,{clauses,Clauses},_Extra}, Vars) -> - {MungedClauses,Vars2}=munge_clauses(Clauses, Vars, []), - {{'fun',Line,{clauses,MungedClauses}}, Vars2}; -munge_expr({'fun',Line,{clauses,Clauses}}, Vars) -> - %% Only for Vsn=raw_abstract_v1 - {MungedClauses,Vars2}=munge_clauses(Clauses, Vars, []), - {{'fun',Line,{clauses,MungedClauses}}, Vars2}; -munge_expr(Form, Vars) -> % var|char|integer|float|string|atom|nil|bin|eof - {Form, Vars}. - -munge_exprs([Expr|Exprs], Vars, MungedExprs) when Vars#vars.is_guard==true, - is_list(Expr) -> - {MungedExpr, _Vars} = munge_exprs(Expr, Vars, []), - munge_exprs(Exprs, Vars, [MungedExpr|MungedExprs]); -munge_exprs([Expr|Exprs], Vars, MungedExprs) -> - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - munge_exprs(Exprs, Vars2, [MungedExpr|MungedExprs]); -munge_exprs([], Vars, MungedExprs) -> - {lists:reverse(MungedExprs), Vars}. - -munge_lc([{generate,Line,Pattern,Expr}|LC], Vars, MungedLC) -> - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - munge_lc(LC, Vars2, [{generate,Line,Pattern,MungedExpr}|MungedLC]); -munge_lc([Expr|LC], Vars, MungedLC) -> - {MungedExpr, Vars2} = munge_expr(Expr, Vars), - munge_lc(LC, Vars2, [MungedExpr|MungedLC]); -munge_lc([], Vars, MungedLC) -> - {lists:reverse(MungedLC), Vars}. - - - - - - - - - - diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl index faec461775..1ccbdc3718 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-2011. All Rights Reserved. +%% Copyright Ericsson AB 2003-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 @@ -29,14 +29,17 @@ -module(ct_logs). -export([init/1,close/2,init_tc/1,end_tc/1]). --export([get_log_dir/0,log/3,start_log/1,cont_log/2,end_log/0]). +-export([get_log_dir/0,get_log_dir/1]). +-export([log/3,start_log/1,cont_log/2,end_log/0]). -export([set_stylesheet/2,clear_stylesheet/1]). -export([add_external_logs/1,add_link/3]). -export([make_last_run_index/0]). -export([make_all_suites_index/1,make_all_runs_index/1]). +-export([get_ts_html_wrapper/3]). +-export([xhtml/2, locate_default_css_file/0, make_relative/1]). %% Logging stuff directly from testcase --export([tc_log/3,tc_print/3,tc_pal/3, +-export([tc_log/3,tc_log/4,tc_log_async/3,tc_print/3,tc_pal/3,ct_log/3, basic_html/0]). %% Simulate logger process for use without ct environment running @@ -53,6 +56,7 @@ -define(all_runs_name, "all_runs.html"). -define(index_name, "index.html"). -define(totals_name, "totals.info"). +-define(css_default, "ct_default.css"). -define(table_color1,"#ADD8E6"). -define(table_color2,"#E4F0FE"). @@ -162,7 +166,12 @@ clear_stylesheet(TC) -> %%%----------------------------------------------------------------- %%% @spec get_log_dir() -> {ok,Dir} | {error,Reason} get_log_dir() -> - call(get_log_dir). + call({get_log_dir,false}). + +%%%----------------------------------------------------------------- +%%% @spec get_log_dir(ReturnAbsName) -> {ok,Dir} | {error,Reason} +get_log_dir(ReturnAbsName) -> + call({get_log_dir,ReturnAbsName}). %%%----------------------------------------------------------------- %%% make_last_run_index() -> ok @@ -205,6 +214,7 @@ cast(Msg) -> %%% <p>This function is called by ct_framework:init_tc/3</p> init_tc(RefreshLog) -> call({init_tc,self(),group_leader(),RefreshLog}), + io:format(xhtml("", "<br />")), ok. %%%----------------------------------------------------------------- @@ -230,7 +240,7 @@ end_tc(TCPid) -> %%% activity it is. <code>Format</code> and <code>Args</code> is the %%% data to log (as in <code>io:format(Format,Args)</code>).</p> log(Heading,Format,Args) -> - cast({log,self(),group_leader(), + cast({log,sync,self(),group_leader(), [{int_header(),[log_timestamp(now()),Heading]}, {Format,Args}, {int_footer(),[]}]}), @@ -252,7 +262,7 @@ log(Heading,Format,Args) -> %%% @see cont_log/2 %%% @see end_log/0 start_log(Heading) -> - cast({log,self(),group_leader(), + cast({log,sync,self(),group_leader(), [{int_header(),[log_timestamp(now()),Heading]}]}), ok. @@ -267,7 +277,7 @@ cont_log([],[]) -> ok; cont_log(Format,Args) -> maybe_log_timestamp(), - cast({log,self(),group_leader(),[{Format,Args}]}), + cast({log,sync,self(),group_leader(),[{Format,Args}]}), ok. %%%----------------------------------------------------------------- @@ -278,7 +288,7 @@ cont_log(Format,Args) -> %%% @see start_log/1 %%% @see cont_log/2 end_log() -> - cast({log,self(),group_leader(),[{int_footer(), []}]}), + cast({log,sync,self(),group_leader(),[{int_footer(), []}]}), ok. @@ -324,9 +334,32 @@ add_link(Heading,File,Type) -> %%% stuff directly from a testcase (i.e. not from within the CT %%% framework).</p> tc_log(Category,Format,Args) -> - cast({log,self(),group_leader(),[{div_header(Category),[]}, - {Format,Args}, - {div_footer(),[]}]}), + tc_log(Category,"User",Format,Args). + +tc_log(Category,Printer,Format,Args) -> + cast({log,sync,self(),group_leader(),[{div_header(Category,Printer),[]}, + {Format,Args}, + {div_footer(),[]}]}), + ok. + + +%%%----------------------------------------------------------------- +%%% @spec tc_log_async(Category,Format,Args) -> ok +%%% Category = atom() +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Internal use only. +%%% +%%% <p>This function is used to perform asynchronous printouts +%%% towards the test server IO handler. This is necessary in order +%%% 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,Format,Args) -> + cast({log,async,self(),group_leader(),[{div_header(Category),[]}, + {Format,Args}, + {div_footer(),[]}]}), ok. %%%----------------------------------------------------------------- @@ -340,19 +373,18 @@ tc_log(Category,Format,Args) -> %%% <p>This function is called by <code>ct</code> when printing %%% stuff a testcase on the user console.</p> tc_print(Category,Format,Args) -> - print_heading(Category), - io:format(user,Format,Args), - io:format(user,"\n\n",[]), + Head = get_heading(Category), + io:format(user, lists:concat([Head,Format,"\n\n"]), Args), ok. -print_heading(default) -> - io:format(user, - "----------------------------------------------------\n~s\n", - [log_timestamp(now())]); -print_heading(Category) -> - io:format(user, - "----------------------------------------------------\n~s ~w\n", - [log_timestamp(now()),Category]). +get_heading(default) -> + io_lib:format("-----------------------------" + "-----------------------\n~s\n", + [log_timestamp(now())]); +get_heading(Category) -> + io_lib:format("-----------------------------" + "-----------------------\n~s ~w\n", + [log_timestamp(now()),Category]). %%%----------------------------------------------------------------- @@ -368,9 +400,26 @@ print_heading(Category) -> %%% log and on the console.</p> tc_pal(Category,Format,Args) -> tc_print(Category,Format,Args), - cast({log,self(),group_leader(),[{div_header(Category),[]}, - {Format,Args}, - {div_footer(),[]}]}), + cast({log,sync,self(),group_leader(),[{div_header(Category),[]}, + {Format,Args}, + {div_footer(),[]}]}), + ok. + + +%%%----------------------------------------------------------------- +%%% @spec tc_pal(Category,Format,Args) -> ok +%%% Category = atom() +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Print and log to the ct framework log +%%% +%%% <p>This function is called by internal ct functions to +%%% force logging to the ct framework log</p> +ct_log(Category,Format,Args) -> + cast({ct_log,[{div_header(Category),[]}, + {Format,Args}, + {div_footer(),[]}]}), ok. @@ -382,8 +431,10 @@ int_footer() -> "</div>". div_header(Class) -> - "<div class=\"" ++ atom_to_list(Class) ++ "\"><b>*** User " ++ - log_timestamp(now()) ++ " ***</b>". + div_header(Class,"User"). +div_header(Class,Printer) -> + "<div class=\"" ++ atom_to_list(Class) ++ "\"><b>*** " ++ Printer ++ + " " ++ log_timestamp(now()) ++ " ***</b>". div_footer() -> "</div>". @@ -394,7 +445,7 @@ maybe_log_timestamp() -> {MS,S,_} -> ok; _ -> - cast({log,self(),group_leader(), + cast({log,sync,self(),group_leader(), [{"<i>~s</i>",[log_timestamp({MS,S,US})]}]}) end. @@ -415,7 +466,8 @@ log_timestamp({MS,S,US}) -> orig_GL, ct_log_fd, tc_groupleaders, - stylesheet}). + stylesheet, + async_print_jobs}). logger(Parent,Mode) -> register(?MODULE,self()), @@ -431,7 +483,6 @@ logger(Parent,Mode) -> timer:sleep(1000), Time1 = calendar:local_time(), Dir1 = make_dirname(Time1), - {Time1,Dir1}; false -> {Time0,Dir0} @@ -439,8 +490,44 @@ logger(Parent,Mode) -> %%! <--- file:make_dir(Dir), + AbsDir = ?abs(Dir), + put(ct_run_dir, AbsDir), + + case basic_html() of + true -> + put(basic_html, true); + BasicHtml -> + put(basic_html, BasicHtml), + %% copy stylesheet to log dir (both top dir and test run + %% dir) so logs are independent of Common Test installation + {ok,Cwd} = file:get_cwd(), + CTPath = code:lib_dir(common_test), + CSSFileSrc = filename:join(filename:join(CTPath, "priv"), + ?css_default), + CSSFileDestTop = filename:join(Cwd, ?css_default), + CSSFileDestRun = filename:join(AbsDir, ?css_default), + case file:copy(CSSFileSrc, CSSFileDestTop) of + {error,Reason0} -> + io:format(user, "ERROR! "++ + "CSS file ~p could not be copied to ~p. "++ + "Reason: ~p~n", + [CSSFileSrc,CSSFileDestTop,Reason0]), + exit({css_file_error,CSSFileDestTop}); + _ -> + case file:copy(CSSFileSrc, CSSFileDestRun) of + {error,Reason1} -> + io:format(user, "ERROR! "++ + "CSS file ~p could not be copied to ~p. "++ + "Reason: ~p~n", + [CSSFileSrc,CSSFileDestRun,Reason1]), + exit({css_file_error,CSSFileDestRun}); + _ -> + ok + end + end + end, ct_event:notify(#event{name=start_logging,node=node(), - data=?abs(Dir)}), + data=AbsDir}), make_all_runs_index(start), make_all_suites_index(start), case Mode of @@ -455,54 +542,36 @@ logger(Parent,Mode) -> Parent ! {started,self(),{Time,filename:absname("")}}, set_evmgr_gl(CtLogFd), logger_loop(#logger_state{parent=Parent, - log_dir=Dir, + log_dir=AbsDir, start_time=Time, orig_GL=group_leader(), ct_log_fd=CtLogFd, - tc_groupleaders=[]}). + tc_groupleaders=[], + async_print_jobs=[]}). logger_loop(State) -> receive - {log,Pid,GL,List} -> - case get_groupleader(Pid,GL,State) of + {log,SyncOrAsync,Pid,GL,List} -> + case get_groupleader(Pid, GL, State) of {tc_log,TCGL,TCGLs} -> case erlang:is_process_alive(TCGL) of true -> - %% we have to build one io-list of all strings - %% before printing, or other io printouts (made in - %% parallel) may get printed between this header - %% and footer - Fun = - fun({Str,Args},IoList) -> - case catch io_lib:format(Str,Args) of - {'EXIT',_Reason} -> - Fd = State#logger_state.ct_log_fd, - io:format(Fd, - "Logging fails! " - "Str: ~p, Args: ~p~n", - [Str,Args]), - %% stop the testcase, we need - %% to see the fault - exit(Pid,{log_printout_error,Str,Args}), - []; - IoStr when IoList == [] -> - [IoStr]; - IoStr -> - [IoList,"\n",IoStr] - end - end, - io:format(TCGL,"~s",[lists:foldl(Fun,[],List)]), - logger_loop(State#logger_state{tc_groupleaders=TCGLs}); + State1 = print_to_log(SyncOrAsync, Pid, TCGL, + List, State), + logger_loop(State1#logger_state{tc_groupleaders = + TCGLs}); false -> - %% Group leader is dead, so write to the CtLog instead + %% 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], logger_loop(State) end; {ct_log,Fd,TCGLs} -> - [begin io:format(Fd,Str,Args),io:nl(Fd) end || {Str,Args} <- List], - logger_loop(State#logger_state{tc_groupleaders=TCGLs}) + [begin io:format(Fd,Str,Args),io:nl(Fd) end || + {Str,Args} <- List], + logger_loop(State#logger_state{tc_groupleaders = TCGLs}) end; {{init_tc,TCPid,GL,RefreshLog},From} -> print_style(GL, State#logger_state.stylesheet), @@ -514,28 +583,51 @@ logger_loop(State) -> make_last_run_index(State#logger_state.start_time) end, return(From,ok), - logger_loop(State#logger_state{tc_groupleaders=TCGLs}); + logger_loop(State#logger_state{tc_groupleaders = TCGLs}); {{end_tc,TCPid},From} -> set_evmgr_gl(State#logger_state.ct_log_fd), return(From,ok), - logger_loop(State#logger_state{tc_groupleaders=rm_tc_gl(TCPid,State)}); - {get_log_dir,From} -> + logger_loop(State#logger_state{tc_groupleaders = + rm_tc_gl(TCPid,State)}); + {{get_log_dir,true},From} -> return(From,{ok,State#logger_state.log_dir}), logger_loop(State); + {{get_log_dir,false},From} -> + return(From,{ok,filename:basename(State#logger_state.log_dir)}), + logger_loop(State); {make_last_run_index,From} -> make_last_run_index(State#logger_state.start_time), - return(From,State#logger_state.log_dir), + return(From,filename:basename(State#logger_state.log_dir)), logger_loop(State); - {set_stylesheet,_,SSFile} when State#logger_state.stylesheet == SSFile -> + {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", [TC,SSFile]), - logger_loop(State#logger_state{stylesheet=SSFile}); + io:format(Fd, "~p loading external style sheet: ~s~n", + [TC,SSFile]), + logger_loop(State#logger_state{stylesheet = SSFile}); {clear_stylesheet,_} when State#logger_state.stylesheet == undefined -> logger_loop(State); {clear_stylesheet,_} -> - logger_loop(State#logger_state{stylesheet=undefined}); + logger_loop(State#logger_state{stylesheet = undefined}); + {ct_log, List} -> + Fd = State#logger_state.ct_log_fd, + [begin io:format(Fd,Str,Args),io:nl(Fd) end || + {Str,Args} <- List], + logger_loop(State); + {'DOWN',Ref,_,_Pid,_} -> + %% there might be print jobs executing in parallel with ct_logs + %% and whenever one is finished (indicated by 'DOWN'), the + %% next job should be spawned + case lists:delete(Ref, State#logger_state.async_print_jobs) of + [] -> + logger_loop(State#logger_state{async_print_jobs = []}); + Jobs -> + [Next|JobsRev] = lists:reverse(Jobs), + Jobs1 = [print_next(Next)|lists:reverse(JobsRev)], + logger_loop(State#logger_state{async_print_jobs = Jobs1}) + end; stop -> io:format(State#logger_state.ct_log_fd, int_header()++int_footer(), @@ -544,6 +636,49 @@ logger_loop(State) -> ok end. +create_io_fun(FromPid, State) -> + %% we have to build one io-list of all strings + %% before printing, or other io printouts (made in + %% parallel) may get printed between this header + %% and footer + Fd = State#logger_state.ct_log_fd, + fun({Str,Args}, IoList) -> + case catch io_lib:format(Str,Args) of + {'EXIT',_Reason} -> + io:format(Fd, "Logging fails! Str: ~p, Args: ~p~n", + [Str,Args]), + %% stop the testcase, we need to see the fault + exit(FromPid, {log_printout_error,Str,Args}), + []; + IoStr when IoList == [] -> + [IoStr]; + IoStr -> + [IoList,"\n",IoStr] + end + end. + +print_to_log(sync, FromPid, TCGL, List, State) -> + IoFun = create_io_fun(FromPid, State), + io:format(TCGL, "~s", [lists:foldl(IoFun, [], List)]), + State; + +print_to_log(async, FromPid, TCGL, List, State) -> + IoFun = create_io_fun(FromPid, State), + Printer = fun() -> + io:format(TCGL, "~s", [lists:foldl(IoFun, [], List)]) + end, + case State#logger_state.async_print_jobs of + [] -> + {_Pid,Ref} = spawn_monitor(Printer), + State#logger_state{async_print_jobs = [Ref]}; + Queue -> + State#logger_state{async_print_jobs = [Printer|Queue]} + end. + +print_next(PrintFun) -> + {_Pid,Ref} = spawn_monitor(PrintFun), + Ref. + %% #logger_state.tc_groupleaders == [{Pid,{Type,GLPid}},...] %% Type = tc | io %% @@ -635,7 +770,7 @@ set_evmgr_gl(GL) -> open_ctlog() -> {ok,Fd} = file:open(?ct_log_name,[write]), - io:format(Fd,header("Common Test Framework"),[]), + io:format(Fd, header("Common Test Framework Log"), []), case file:consult(ct_run:variables_file_name("../")) of {ok,Vars} -> io:format(Fd, config_table(Vars), []); @@ -650,17 +785,22 @@ open_ctlog() -> end, print_style(Fd,undefined), io:format(Fd, - "<br><br><h2>Progress Log</h2>\n" - "<pre>\n",[]), + xhtml("<br><br><h2>Progress Log</h2>\n<pre>\n", + "<br /><br /><h4>PROGRESS LOG</h4>\n<pre>\n"), []), Fd. print_style(Fd,undefined) -> - io:format(Fd, - "<style>\n" - "div.ct_internal { background:lightgrey; color:black }\n" - "div.default { background:lightgreen; color:black }\n" - "</style>\n", - []); + case basic_html() of + true -> + io:format(Fd, + "<style>\n" + "div.ct_internal { background:lightgrey; color:black; }\n" + "div.default { background:lightgreen; color:black; }\n" + "</style>\n", + []); + _ -> + ok + end; print_style(Fd,StyleSheet) -> case file:read_file(StyleSheet) of @@ -670,20 +810,19 @@ print_style(Fd,StyleSheet) -> 0 -> string:str(Str,"<STYLE>"); N0 -> N0 end, - case Pos0 of - 0 -> print_style_error(Fd,StyleSheet,missing_style_tag); - _ -> - Pos1 = case string:str(Str,"</style>") of - 0 -> string:str(Str,"</STYLE>"); - N1 -> N1 - end, - case Pos1 of - 0 -> - print_style_error(Fd,StyleSheet,missing_style_end_tag); - _ -> - Style = string:sub_string(Str,Pos0,Pos1+7), - io:format(Fd,"~s\n",[Style]) - end + Pos1 = case string:str(Str,"</style>") of + 0 -> string:str(Str,"</STYLE>"); + N1 -> N1 + end, + if (Pos0 == 0) and (Pos1 /= 0) -> + print_style_error(Fd,StyleSheet,missing_style_start_tag); + (Pos0 /= 0) and (Pos1 == 0) -> + 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]); + Pos0 == 0 -> + io:format(Fd,"<style>~s</style>\n",[Str]) end; {error,Reason} -> print_style_error(Fd,StyleSheet,Reason) @@ -701,7 +840,7 @@ print_style_error(Fd,StyleSheet,Reason) -> print_style(Fd,undefined). close_ctlog(Fd) -> - io:format(Fd,"</pre>",[]), + io:format(Fd,"\n</pre>\n",[]), io:format(Fd,footer(),[]), file:close(Fd). @@ -782,33 +921,48 @@ insert_dir(D,[D1|Ds]) -> insert_dir(D,[]) -> [D]. -make_last_run_index([Name|Rest], Result, TotSucc, TotFail, UserSkip, AutoSkip, - TotNotBuilt, Missing) -> - case last_test(Name) of +make_last_run_index([Name|Rest], Result, TotSucc, TotFail, + UserSkip, AutoSkip, TotNotBuilt, Missing) -> + case get_run_dirs(Name) of false -> %% Silently skip. - make_last_run_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, - TotNotBuilt, Missing); - LastLogDir -> + make_last_run_index(Rest, Result, TotSucc, TotFail, + UserSkip, AutoSkip, TotNotBuilt, Missing); + LogDirs -> SuiteName = filename:rootname(filename:basename(Name)), - case make_one_index_entry(SuiteName, LastLogDir, "-", false, Missing) of - {Result1,Succ,Fail,USkip,ASkip,NotBuilt} -> - %% for backwards compatibility - AutoSkip1 = case catch AutoSkip+ASkip of - {'EXIT',_} -> undefined; - Res -> Res - end, - make_last_run_index(Rest, [Result|Result1], TotSucc+Succ, - TotFail+Fail, UserSkip+USkip, AutoSkip1, - TotNotBuilt+NotBuilt, Missing); - error -> - make_last_run_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, - TotNotBuilt, Missing) - end + {Result1,TotSucc1,TotFail1,UserSkip1,AutoSkip1,TotNotBuilt1} = + make_last_run_index1(SuiteName, LogDirs, Result, + TotSucc, TotFail, + UserSkip, AutoSkip, + TotNotBuilt, Missing), + make_last_run_index(Rest, Result1, TotSucc1, TotFail1, + UserSkip1, AutoSkip1, + TotNotBuilt1, Missing) end; + 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} -> + %% 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); + error -> + make_last_run_index1(SuiteName, LogDirs, Result, TotSucc, TotFail, + UserSkip, AutoSkip, TotNotBuilt, Missing) + end; +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) -> case count_cases(LogDir) of @@ -832,8 +986,8 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, CrashDumpName = SuiteName ++ "_erl_crash.dump", case filelib:is_file(CrashDumpName) of true -> - [" <A HREF=\"", CrashDumpName, - "\">(CrashDump)</A>"]; + [" <a href=\"", CrashDumpName, + "\">(CrashDump)</a>"]; false -> "" end @@ -847,32 +1001,41 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, 0 -> "-"; _ -> NodeOrDate end, - N = ["<TD ALIGN=right><FONT SIZE=-1>",Node1,"</FONT></TD>\n"], - L = ["<TD ALIGN=center><FONT SIZE=-1><B>",Label,"</FONT></B></TD>\n"], - T = ["<TD><FONT SIZE=-1>",timestamp(CtRunDir),"</FONT></TD>\n"], + TS = timestamp(CtRunDir), + N = xhtml(["<td align=right><font size=\"-1\">",Node1, + "</font></td>\n"], + ["<td align=right>",Node1,"</td>\n"]), + L = xhtml(["<td align=center><font size=\"-1\"><b>",Label, + "</font></b></td>\n"], + ["<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>" + _ -> "<a href=\""++?all_runs_name++"\">Old Runs</a>" end, - A=["<TD><FONT SIZE=-1><A HREF=\"",CtLogFile,"\">CT Log</A></FONT></TD>\n", - "<TD><FONT SIZE=-1>",OldRunsLink,"</FONT></TD>\n"], + A = xhtml(["<td><font size=\"-1\"><a href=\"",CtLogFile, + "\">CT Log</a></font></td>\n", + "<td><font size=\"-1\">",OldRunsLink,"</font></td>\n"], + ["<td><a href=\"",CtLogFile,"\">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"]; + ["<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=\"",filename:join(CtRunDir,?ct_log_name),"\">", + integer_to_list(NotBuilt),"</a></td>\n"] end, FailStr = if Fail > 0 -> - ["<FONT color=\"red\">", - integer_to_list(Fail),"</FONT>"]; + ["<font color=\"red\">", + integer_to_list(Fail),"</font>"]; true -> integer_to_list(Fail) end, @@ -880,31 +1043,33 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip, if AutoSkip == undefined -> {UserSkip,"?","?"}; true -> ASStr = if AutoSkip > 0 -> - ["<FONT color=\"brown\">", - integer_to_list(AutoSkip),"</FONT>"]; + ["<font color=\"brown\">", + integer_to_list(AutoSkip),"</font>"]; true -> integer_to_list(AutoSkip) end, {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr} end, - ["<TR valign=top>\n", - "<TD><FONT SIZE=-1><A HREF=\"",LogFile,"\">",SuiteName,"</A>",CrashDumpLink,"</FONT></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=\""), + 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"]. + total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) -> {Label,TimestampCell,AllInfo} = case All of true -> - {"<TD> </TD>\n", - "<TD> </TD>\n", - "<TD> </TD>\n<TD> </TD>\n"}; + {"<td> </td>\n", + "<td> </td>\n", + "<td> </td>\n" + "<td> </td>\n" + "<td> </td>\n"}; false -> {"","",""} end, @@ -914,17 +1079,15 @@ total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) -> true -> {UserSkip+AutoSkip, integer_to_list(UserSkip),integer_to_list(AutoSkip)} end, - ["<TR valign=top>\n", - "<TD><B>Total</B></TD>", - Label, - TimestampCell, - "<TD ALIGN=right><B>",integer_to_list(Success),"<B></TD>\n", - "<TD ALIGN=right><B>",integer_to_list(Fail),"<B></TD>\n", - "<TD ALIGN=right>",integer_to_list(AllSkip), - " (",UserSkipStr,"/",AutoSkipStr,")</TD>\n", - "<TD ALIGN=right><B>",integer_to_list(NotBuilt),"<B></TD>\n", - AllInfo, - "</TR>\n"]. + [xhtml("<tr valign=top>\n", + ["<tr class=\"",odd_or_even(),"\">\n"]), + "<td><b>Total</b></td>\n", Label, TimestampCell, + "<td align=right><b>",integer_to_list(Success),"<b></td>\n", + "<td align=right><b>",integer_to_list(Fail),"<b></td>\n", + "<td align=right>",integer_to_list(AllSkip), + " (",UserSkipStr,"/",AutoSkipStr,")</td>\n", + "<td align=right><b>",integer_to_list(NotBuilt),"<b></td>\n", + AllInfo, "</tr>\n"]. not_built(_BaseName,_LogDir,_All,[]) -> 0; @@ -983,41 +1146,52 @@ index_header(Label, StartTime) -> undefined -> header("Test Results", format_time(StartTime)); _ -> - header("Test Results for \"" ++ Label ++ "\"", + header("Test Results for '" ++ Label ++ "'", format_time(StartTime)) end, [Head | - ["<CENTER>\n", - "<P><A HREF=\"",?ct_log_name,"\">Common Test Framework Log</A></P>", - "<TABLE border=\"3\" cellpadding=\"5\" " - "BGCOLOR=\"",?table_color3,"\">\n" - "<th><B>Test Name</B></th>\n", - "<th><font color=\"",?table_color3,"\">_</font>Ok" - "<font color=\"",?table_color3,"\">_</font></th>\n" + ["<center>\n", + xhtml(["<p><a href=\"",?ct_log_name, + "\">Common Test Framework Log</a></p>"], + ["<br />" + "<div id=\"button_holder\" class=\"btn\">\n" + "<a href=\"",?ct_log_name, + "\">COMMON TEST FRAMEWORK LOG</a>\n</div>"]), + xhtml("<br>\n", "<br /><br /><br />\n"), + xhtml(["<table border=\"3\" cellpadding=\"5\" " + "bgcolor=\"",?table_color3,"\">\n"], "<table>\n"), + "<th><b>Test Name</b></th>\n", + xhtml(["<th><font color=\"",?table_color3,"\">_</font>Ok" + "<font color=\"",?table_color3,"\">_</font></th>\n"], + "<th>Ok</th>\n"), "<th>Failed</th>\n", - "<th>Skipped<br>(User/Auto)</th>\n" - "<th>Missing<br>Suites</th>\n" + "<th>Skipped", xhtml("<br>", "<br />"), "(User/Auto)</th>\n" + "<th>Missing", xhtml("<br>", "<br />"), "Suites</th>\n" "\n"]]. - all_suites_index_header() -> {ok,Cwd} = file:get_cwd(), all_suites_index_header(Cwd). all_suites_index_header(IndexDir) -> LogDir = filename:basename(IndexDir), - AllRuns = "All test runs in \"" ++ LogDir ++ "\"", + AllRuns = xhtml(["All test runs in \"" ++ LogDir ++ "\""], + "ALL RUNS"), + AllRunsLink = xhtml(["<a href=\"",?all_runs_name,"\">",AllRuns,"</a>\n"], + ["<div id=\"button_holder\" class=\"btn\">\n" + "<a href=\"",?all_runs_name,"\">",AllRuns,"</a>\n</div>"]), [header("Test Results") | - ["<CENTER>\n", - "<A HREF=\"",?all_runs_name,"\">",AllRuns,"</A>\n", - "<br><br>\n", - "<TABLE border=\"3\" cellpadding=\"5\" " - "BGCOLOR=\"",?table_color2,"\">\n" + ["<center>\n", + AllRunsLink, + xhtml("<br><br>\n", "<br /><br />\n"), + xhtml(["<table border=\"3\" cellpadding=\"5\" " + "bgcolor=\"",?table_color2,"\">\n"], "<table>\n"), "<th>Test Name</th>\n", "<th>Label</th>\n", "<th>Test Run Started</th>\n", - "<th><font color=\"",?table_color2,"\">_</font>Ok" - "<font color=\"",?table_color2,"\">_</font></th>\n" + xhtml(["<th><font color=\"",?table_color2,"\">_</font>Ok" + "<font color=\"",?table_color2,"\">_</font></th>\n"], + "<th>Ok</th>\n"), "<th>Failed</th>\n", "<th>Skipped<br>(User/Auto)</th>\n" "<th>Missing<br>Suites</th>\n" @@ -1030,17 +1204,25 @@ all_runs_header() -> {ok,Cwd} = file:get_cwd(), LogDir = filename:basename(Cwd), Title = "All test runs in \"" ++ LogDir ++ "\"", + IxLink = [xhtml(["<p><a href=\"",?index_name, + "\">Test Index Page</a></p>"], + ["<div id=\"button_holder\" class=\"btn\">\n" + "<a href=\"",?index_name, + "\">TEST INDEX PAGE</a>\n</div>"]), + xhtml("<br>\n", "<br /><br />\n")], [header(Title) | - ["<CENTER><TABLE border=\"3\" cellpadding=\"5\" " - "BGCOLOR=\"",?table_color1,"\">\n" - "<th><B>History</B></th>\n" - "<th><B>Node</B></th>\n" - "<th><B>Label</B></th>\n" + ["<center>\n", IxLink, + xhtml(["<table border=\"3\" cellpadding=\"5\" " + "bgcolor=\"",?table_color1,"\">\n"], "<table>\n"), + "<th><b>History</b></th>\n" + "<th><b>Node</b></th>\n" + "<th><b>Label</b></th>\n" "<th>Tests</th>\n" - "<th><B>Test Names</B></th>\n" - "<th>Total</th>\n" - "<th><font color=\"",?table_color1,"\">_</font>Ok" - "<font color=\"",?table_color1,"\">_</font></th>\n" + "<th><b>Test Names</b></th>\n" + "<th>Total</th>\n", + xhtml(["<th><font color=\"",?table_color1,"\">_</font>Ok" + "<font color=\"",?table_color1,"\">_</font></th>\n"], + "<th>Ok</th>\n"), "<th>Failed</th>\n" "<th>Skipped<br>(User/Auto)</th>\n" "<th>Missing<br>Suites</th>\n" @@ -1053,60 +1235,56 @@ header(Title, SubTitle) -> header1(Title, SubTitle) -> SubTitleHTML = if SubTitle =/= "" -> - ["<CENTER>\n", - "<H2>" ++ SubTitle ++ "</H2>\n", - "</CENTER>\n<BR>\n"]; - true -> "<BR>\n" + ["<center>\n", + "<h3>" ++ SubTitle ++ "</h3>\n", + xhtml("</center>\n<br>\n", "</center>\n<br />\n")]; + true -> xhtml("<br>\n", "<br />\n") end, - ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n" - "<!-- autogenerated by '"++atom_to_list(?MODULE)++"'. -->\n" - "<HTML>\n", - "<HEAD>\n", - - "<TITLE>" ++ Title ++ " " ++ SubTitle ++ "</TITLE>\n", - "<META HTTP-EQUIV=\"CACHE-CONTROL\" CONTENT=\"NO-CACHE\">\n", - - "</HEAD>\n", - + CSSFile = xhtml(fun() -> "" end, + fun() -> make_relative(locate_default_css_file()) end), + [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>" ++ Title ++ " " ++ SubTitle ++ "</title>\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + xhtml("", + ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">"]), + "</head>\n", body_tag(), - - "<!-- ---- DOCUMENT TITLE ---- -->\n", - - "<CENTER>\n", - "<H1>" ++ Title ++ "</H1>\n", - "</CENTER>\n", - SubTitleHTML, - - "<!-- ---- CONTENT ---- -->\n"]. + "<center>\n", + "<h1>" ++ Title ++ "</h1>\n", + "</center>\n", + SubTitleHTML,"\n"]. index_footer() -> - ["</TABLE>\n" - "</CENTER>\n" | footer()]. + ["</table>\n" + "</center>\n" | footer()]. footer() -> - ["<P><CENTER>\n" - "<BR><BR>\n" - "<HR>\n" - "<P><FONT SIZE=-1>\n" + ["<center>\n", + xhtml("<br><br>\n<hr>\n", "<br /><br />\n"), + xhtml("<p><font size=\"-1\">\n", "<div class=\"copyright\">"), "Copyright © ", year(), - " <A HREF=\"http://erlang.ericsson.se\">Open Telecom Platform</A><BR>\n" - "Updated: <!date>", current_time(), "<!/date><BR>\n" - "</FONT>\n" - "</CENTER>\n" + " <a href=\"http://www.erlang.org\">Open Telecom Platform</a>", + xhtml("<br>\n", "<br />\n"), + "Updated: <!date>", current_time(), "<!/date>", + xhtml("<br>\n", "<br />\n"), + xhtml("</font></p>\n", "</div>\n"), + "</center>\n" "</body>\n"]. body_tag() -> - case basic_html() of - true -> - "<body bgcolor=\"#FFFFFF\" text=\"#000000\" link=\"#0000FF\" " - "vlink=\"#800080\" alink=\"#FF0000\">\n"; - false -> - CTPath = code:lib_dir(common_test), - TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"), - "<body background=\"" ++ TileFile ++ "\" bgcolor=\"#FFFFFF\" text=\"#000000\" link=\"#0000FF\" " - "vlink=\"#800080\" alink=\"#FF0000\">\n" - end. + CTPath = code:lib_dir(common_test), + TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"), + xhtml("<body background=\"" ++ TileFile ++ + "\" bgcolor=\"#FFFFFF\" text=\"#000000\" link=\"#0000FF\" " + "vlink=\"#800080\" alink=\"#FF0000\">\n", + "<body>\n"). current_time() -> format_time(calendar:local_time()). @@ -1236,20 +1414,25 @@ config_table(Vars) -> [config_table_header()|config_table1(Vars)]. config_table_header() -> - ["<h2>Configuration</h2>\n", - "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color1, - "\"\n", + [ + xhtml(["<h2>Configuration</h2>\n" + "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color1,"\"\n"], + "<h4>CONFIGURATION</h4>\n<table>\n"), "<tr><th>Key</th><th>Value</th></tr>\n"]. config_table1([{Key,Value}|Vars]) -> - ["<tr><td>", atom_to_list(Key), "</td>\n", - "<td><pre>",io_lib:format("~p",[Value]),"</pre></td></tr>\n" | + [xhtml(["<tr><td>", atom_to_list(Key), "</td>\n", + "<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"]) | config_table1(Vars)]; config_table1([]) -> ["</table>\n"]. make_all_runs_index(When) -> + put(basic_html, basic_html()), AbsName = ?abs(?all_runs_name), notify_and_lock_file(AbsName), if When == start -> ok; @@ -1258,8 +1441,7 @@ make_all_runs_index(When) -> Dirs = filelib:wildcard(logdir_prefix()++"*.*"), DirsSorted = (catch sort_all_runs(Dirs)), Header = all_runs_header(), - BasicHtml = basic_html(), - Index = [runentry(Dir, BasicHtml) || Dir <- DirsSorted], + Index = [runentry(Dir) || Dir <- DirsSorted], Result = file:write_file(AbsName,Header++Index++index_footer()), if When == start -> ok; true -> io:put_chars("done\n") @@ -1287,22 +1469,22 @@ sort_all_runs(Dirs) -> 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>"], + 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" "Any CT activities will be logged here\n", [?abs("last_interactive.html")]). -runentry(Dir, BasicHtml) -> +runentry(Dir) -> 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>"]; + ["<font color=\"red\">", + integer_to_list(TotFail),"</font>"]; true -> integer_to_list(TotFail) end, @@ -1310,8 +1492,8 @@ runentry(Dir, BasicHtml) -> if AutoSkip == undefined -> {UserSkip,"?","?"}; true -> ASStr = if AutoSkip > 0 -> - ["<FONT color=\"brown\">", - integer_to_list(AutoSkip),"</FONT>"]; + ["<font color=\"brown\">", + integer_to_list(AutoSkip),"</font>"]; true -> integer_to_list(AutoSkip) end, {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr} @@ -1343,30 +1525,49 @@ runentry(Dir, BasicHtml) -> lists:flatten(io_lib:format("~s...",[Trunc])) end, Total = TotSucc+TotFail+AllSkip, - A = ["<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"], - B = if BasicHtml -> - ["<TD ALIGN=center><FONT SIZE=-1>",TestNamesTrunc,"</FONT></TD>\n"]; - true -> - ["<TD ALIGN=center TITLE='",TestNames,"'><FONT SIZE=-1> ", - TestNamesTrunc,"</FONT></TD>\n"] - end, - 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 = 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; _ -> - ["<TD ALIGN=center><FONT size=-1 color=\"red\">", - "Test data missing or corrupt","</FONT></TD>\n"] + 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), - ["<TR>\n" - "<TD><FONT SIZE=-1><A HREF=\"",Index,"\">",timestamp(Dir),"</A>",TotalsStr,"</FONT></TD>\n" - "</TR>\n"]. + [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"]. write_totals_file(Name,Label,Logs,Totals) -> AbsName = ?abs(Name), @@ -1460,6 +1661,7 @@ timestamp(Dir) -> %% Creates the top level index file. When == start | refresh. %% A copy of the dir tree under logdir is cached 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), LogDirs = filelib:wildcard(logdir_prefix()++".*/*"++?logdir_ext), @@ -1471,6 +1673,7 @@ make_all_suites_index(When) when is_atom(When) -> %% This updates the top level index file using cached data from %% the initial index file creation. make_all_suites_index(NewTestData = {_TestName,DirName}) -> + put(basic_html, basic_html()), %% AllLogDirs = [{TestName,Label,Missing,{LastLogDir,Summary},OldDirs}|...] {AbsIndexName,LogDirData} = ct_util:get_testdata(test_index), @@ -1511,8 +1714,8 @@ make_all_suites_index(NewTestData = {_TestName,DirName}) -> sort_logdirs([Dir|Dirs],Groups) -> TestName = filename:rootname(filename:basename(Dir)), case filelib:wildcard(filename:join(Dir,"run.*")) of - [RunDir] -> - Groups1 = insert_test(TestName,{filename:basename(RunDir),RunDir},Groups), + RunDirs = [_|_] -> + Groups1 = sort_logdirs1(TestName,RunDirs,Groups), sort_logdirs(Dirs,Groups1); _ -> % ignore missing run directory sort_logdirs(Dirs,Groups) @@ -1520,6 +1723,12 @@ sort_logdirs([Dir|Dirs],Groups) -> sort_logdirs([],Groups) -> lists:keysort(1,sort_each_group(Groups)). +sort_logdirs1(TestName,[RunDir|RunDirs],Groups) -> + Groups1 = insert_test(TestName,{filename:basename(RunDir),RunDir},Groups), + sort_logdirs1(TestName,RunDirs,Groups1); +sort_logdirs1(_,[],Groups) -> + Groups. + insert_test(Test,IxDir,[{Test,IxDirs}|Groups]) -> [{Test,[IxDir|IxDirs]}|Groups]; insert_test(Test,IxDir,[]) -> @@ -1772,7 +1981,7 @@ simulate() -> simulate_logger_loop() -> receive - {log,_,_,List} -> + {log,_,_,_,List} -> S = [[io_lib:format(Str,Args),io_lib:nl()] || {Str,Args} <- List], io:format("~s",[S]), simulate_logger_loop(); @@ -1811,21 +2020,49 @@ notify_and_unlock_file(File) -> end. %%%----------------------------------------------------------------- -%%% @spec last_test(Dir) -> string() | false +%%% @spec get_run_dirs(Dir) -> [string()] | false +%%% +%%% @doc +%%% +get_run_dirs(Dir) -> + case filelib:wildcard(filename:join(Dir, "run.[1-2]*")) of + [] -> + false; + RunDirs -> + lists:sort(RunDirs) + end. + +%%%----------------------------------------------------------------- +%%% @spec xhtml(HTML, XHTML) -> HTML | XHTML +%%% +%%% @doc +%%% +xhtml(HTML, XHTML) when is_function(HTML), + is_function(XHTML) -> + case get(basic_html) of + true -> HTML(); + _ -> XHTML() + end; +xhtml(HTML, XHTML) -> + case get(basic_html) of + true -> HTML; + _ -> XHTML + end. + +%%%----------------------------------------------------------------- +%%% @spec odd_or_even() -> "odd" | "even" %%% %%% @doc %%% -last_test(Dir) -> - last_test(filelib:wildcard(filename:join(Dir, "run.[1-2]*")), false). - -last_test([Run|Rest], false) -> - last_test(Rest, Run); -last_test([Run|Rest], Latest) when Run > Latest -> - last_test(Rest, Run); -last_test([_|Rest], Latest) -> - last_test(Rest, Latest); -last_test([], Latest) -> - Latest. +odd_or_even() -> + case get(odd_or_even) of + even -> + put(odd_or_even, odd), + "even"; + _ -> + put(odd_or_even, even), + "odd" + end. %%%----------------------------------------------------------------- %%% @spec basic_html() -> true | false @@ -1839,3 +2076,149 @@ basic_html() -> _ -> false end. + +%%%----------------------------------------------------------------- +%%% @spec locate_default_css_file() -> CSSFile +%%% +%%% @doc +%%% +locate_default_css_file() -> + {ok,CWD} = file:get_cwd(), + CSSFileInCwd = filename:join(CWD, ?css_default), + case filelib:is_file(CSSFileInCwd) of + true -> + CSSFileInCwd; + false -> + CSSResultFile = + case {whereis(?MODULE),self()} of + {Self,Self} -> + %% executed on the ct_logs process + filename:join(get(ct_run_dir), ?css_default); + _ -> + %% executed on other process than ct_logs + {ok,RunDir} = get_log_dir(true), + filename:join(RunDir, ?css_default) + end, + case filelib:is_file(CSSResultFile) of + true -> + CSSResultFile; + false -> + %% last resort, try use css file in CT installation + CTPath = code:lib_dir(common_test), + filename:join(filename:join(CTPath, "priv"), ?css_default) + end + end. + +%%%----------------------------------------------------------------- +%%% @spec make_relative(AbsDir, Cwd) -> RelDir +%%% +%%% @doc Return directory path to File (last element of AbsDir), which +%%% is the path relative to Cwd. Examples when Cwd == "/ldisk/test/logs": +%%% make_relative("/ldisk/test/logs/run/trace.log") -> "run/trace.log" +%%% make_relative("/ldisk/test/trace.log") -> "../trace.log" +%%% make_relative("/ldisk/test/logs/trace.log") -> "trace.log" +make_relative(AbsDir) -> + {ok,Cwd} = file:get_cwd(), + make_relative(AbsDir, Cwd). + +make_relative(AbsDir, Cwd) -> + DirTokens = filename:split(AbsDir), + CwdTokens = filename:split(Cwd), + filename:join(make_relative1(DirTokens, CwdTokens)). + +make_relative1([T | DirTs], [T | CwdTs]) -> + make_relative1(DirTs, CwdTs); +make_relative1(Last = [_File], []) -> + Last; +make_relative1(Last = [_File], CwdTs) -> + Ups = ["../" || _ <- CwdTs], + Ups ++ Last; +make_relative1(DirTs, []) -> + DirTs; +make_relative1(DirTs, CwdTs) -> + Ups = ["../" || _ <- CwdTs], + Ups ++ DirTs. + +%%%----------------------------------------------------------------- +%%% @spec get_ts_html_wrapper(TestName, PrintLabel, Cwd) -> {Mode,Header,Footer} +%%% +%%% @doc +%%% +get_ts_html_wrapper(TestName, PrintLabel, Cwd) -> + TestName1 = if is_list(TestName) -> + lists:flatten(TestName); + true -> + lists:flatten(io_lib:format("~p", [TestName])) + end, + Basic = basic_html(), + LabelStr = + if not PrintLabel -> + ""; + true -> + case {Basic,application:get_env(common_test, test_label)} of + {true,{ok,Lbl}} when Lbl =/= undefined -> + "<h1><font color=\"green\">" ++ Lbl ++ "</font></h1>\n"; + {_,{ok,Lbl}} when Lbl =/= undefined -> + "<div class=\"label\">'" ++ Lbl ++ "'</div>\n"; + _ -> + "" + end + end, + CTPath = code:lib_dir(common_test), + {ok,CtLogdir} = get_log_dir(true), + AllRuns = make_relative(filename:join(filename:dirname(CtLogdir), + ?all_runs_name), Cwd), + TestIndex = make_relative(filename:join(filename:dirname(CtLogdir), + ?index_name), Cwd), + case Basic of + true -> + TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"), + Bgr = " background=\"" ++ TileFile ++ "\"", + Copyright = + ["<p><font size=\"-1\">\n", + "Copyright © ", year(), + " <a href=\"http://www.erlang.org\">", + "Open Telecom Platform</a><br>\n", + "Updated: <!date>", current_time(), "<!/date>", + "<br>\n</font></p>\n"], + {basic_html, + ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n", + "<html>\n", + "<head><title>", TestName1, "</title>\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\">\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, + "\">Test run history\n</a> | ", + "<a href=\"", TestIndex, + "\">Top level test index\n</a>\n</p>\n", + Copyright,"</center>\n</body>\n</html>\n"]}; + _ -> + Copyright = + ["<div class=\"copyright\">", + "Copyright © ", year(), + " <a href=\"http://www.erlang.org\">", + "Open Telecom Platform</a><br />\n", + "Updated: <!date>", current_time(), "<!/date>", + "<br />\n</div>\n"], + CSSFile = xhtml(fun() -> "" end, + fun() -> make_relative(locate_default_css_file(), Cwd) end), + {xhtml, + ["<!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", + "<head>\n<title>", TestName1, "</title>\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + "<link rel=\"stylesheet\" href=\"", CSSFile, "\" type=\"text/css\">", + "</head>\n","<body>\n", + LabelStr, "\n"], + ["<center>\n<br /><hr /><p>\n", + "<a href=\"", AllRuns, + "\">Test run history\n</a> | ", + "<a href=\"", TestIndex, + "\">Top level test index\n</a>\n</p>\n", + Copyright,"</center>\n</body>\n</html>\n"]} + end. diff --git a/lib/common_test/src/ct_make.erl b/lib/common_test/src/ct_make.erl index 40e9e99f37..8ddb91d355 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. All Rights Reserved. +%% Copyright Ericsson AB 2009-2011. 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 diff --git a/lib/common_test/src/ct_master.erl b/lib/common_test/src/ct_master.erl index 2ea2ba106a..042c5ba267 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-2010. 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 @@ -25,6 +25,7 @@ -export([run/1,run/3,run/4]). -export([run_on_node/2,run_on_node/3]). -export([run_test/1,run_test/2]). +-export([basic_html/1]). -export([abort/0,abort/1,progress/0]). @@ -277,7 +278,17 @@ abort(Node) when is_atom(Node) -> progress() -> call(progress). - +%%%----------------------------------------------------------------- +%%% @spec basic_html(Bool) -> ok +%%% Bool = true | false +%%% +%%% @doc If set to true, the ct_master logs will be written on a +%%% primitive html format, not using the Common Test CSS style +%%% sheet. +basic_html(Bool) -> + application:set_env(common_test_master, basic_html, Bool), + ok. + %%%----------------------------------------------------------------- %%% MASTER, runs on central controlling node. %%%----------------------------------------------------------------- diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl index 244faace06..2a951bc5cf 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-2010. 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 @@ -23,7 +23,8 @@ %%% node.</p> -module(ct_master_logs). --export([start/2, make_all_runs_index/0, log/3, nodedir/2, stop/0]). +-export([start/2, make_all_runs_index/0, log/3, nodedir/2, + stop/0]). -record(state, {log_fd, start_time, logdir, rundir, nodedir_ix_fd, nodes, nodedirs=[]}). @@ -32,6 +33,7 @@ -define(all_runs_name, "master_runs.html"). -define(nodedir_index_name, "index.html"). -define(details_file_name,"details.info"). +-define(css_default, "ct_default.css"). -define(table_color,"lightblue"). %%%-------------------------------------------------------------------- @@ -87,6 +89,40 @@ init(Parent,LogDir,Nodes) -> RunDirAbs = filename:join(LogDir,RunDir), file:make_dir(RunDirAbs), write_details_file(RunDirAbs,{node(),Nodes}), + + case basic_html() of + true -> + put(basic_html, true); + BasicHtml -> + put(basic_html, BasicHtml), + %% copy stylesheet to log dir (both top dir and test run + %% dir) so logs are independent of Common Test installation + CTPath = code:lib_dir(common_test), + CSSFileSrc = filename:join(filename:join(CTPath, "priv"), + ?css_default), + CSSFileDestTop = filename:join(LogDir, ?css_default), + CSSFileDestRun = filename:join(RunDirAbs, ?css_default), + case file:copy(CSSFileSrc, CSSFileDestTop) of + {error,Reason0} -> + io:format(user, "ERROR! "++ + "CSS file ~p could not be copied to ~p. "++ + "Reason: ~p~n", + [CSSFileSrc,CSSFileDestTop,Reason0]), + exit({css_file_error,CSSFileDestTop}); + _ -> + case file:copy(CSSFileSrc, CSSFileDestRun) of + {error,Reason1} -> + io:format(user, "ERROR! "++ + "CSS file ~p could not be copied to ~p. "++ + "Reason: ~p~n", + [CSSFileSrc,CSSFileDestRun,Reason1]), + exit({css_file_error,CSSFileDestRun}); + _ -> + ok + end + end + end, + make_all_runs_index(LogDir), CtLogFd = open_ct_master_log(RunDirAbs), NodeStr = @@ -164,8 +200,9 @@ open_ct_master_log(Dir) -> "</style>\n", []), io:format(Fd, - "<br><h2>Progress Log</h2>\n" - "<pre>\n",[]), + xhtml("<br><h2>Progress Log</h2>\n<pre>\n", + "<br /><h2>Progress Log</h2>\n<pre>\n"), + []), Fd. close_ct_master_log(Fd) -> @@ -178,18 +215,10 @@ config_table(Vars) -> config_table_header() -> ["<h2>Configuration</h2>\n", - "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color, - "\"\n", + xhtml(["<table border=\"3\" cellpadding=\"5\" " + "bgcolor=\"",?table_color,"\"\n"], "<table>\n"), "<tr><th>Key</th><th>Value</th></tr>\n"]. -%% -%% keep for possible later use -%% -%%config_table1([{Key,Value}|Vars]) -> -%% ["<tr><td>", atom_to_list(Key), "</td>\n", -%% "<td><pre>",io_lib:format("~p",[Value]),"</pre></td></tr>\n" | -%% config_table1(Vars)]; - config_table1([]) -> ["</table>\n"]. @@ -210,10 +239,10 @@ open_nodedir_index(Dir,StartTime) -> 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"],[]), + ["<tr>\n" + "<td align=center>",atom_to_list(Node),"</td>\n", + "<td align=left><a href=\"",Index,"\">",Index,"</a></td>\n", + "</tr>\n"],[]), ok. close_nodedir_index(Fd) -> @@ -222,12 +251,12 @@ close_nodedir_index(Fd) -> nodedir_index_header(StartTime) -> [header("Log Files " ++ format_time(StartTime)) | - ["<CENTER>\n", - "<P><A HREF=\"",?ct_master_log_name,"\">Common Test Master Log</A></P>", - "<TABLE border=\"3\" cellpadding=\"5\" ", - "BGCOLOR=\"",?table_color,"\">\n", - "<th><B>Node</B></th>\n", - "<th><B>Log</B></th>\n", + ["<center>\n", + "<p><a href=\"",?ct_master_log_name,"\">Common Test Master Log</a></p>", + xhtml(["<table border=\"3\" cellpadding=\"5\" " + "bgcolor=\"",?table_color,"\">\n"], "<table>\n"), + "<th><b>Node</b></th>\n", + "<th><b>Log</b></th>\n", "\n"]]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -279,20 +308,20 @@ runentry(Dir) -> {"unknown",""} end, Index = filename:join(Dir,?nodedir_index_name), - ["<TR>\n" - "<TD ALIGN=center><A HREF=\"",Index,"\">",timestamp(Dir),"</A></TD>\n", - "<TD ALIGN=center>",MasterStr,"</TD>\n", - "<TD ALIGN=center>",NodesStr,"</TD>\n", - "</TR>\n"]. + ["<tr>\n" + "<td align=center><a href=\"",Index,"\">",timestamp(Dir),"</a></td>\n", + "<td align=center>",MasterStr,"</td>\n", + "<td align=center>",NodesStr,"</td>\n", + "</tr>\n"]. all_runs_header() -> [header("Master Test Runs") | - ["<CENTER>\n", - "<TABLE border=\"3\" cellpadding=\"5\" " - "BGCOLOR=\"",?table_color,"\">\n" - "<th><B>History</B></th>\n" - "<th><B>Master Host</B></th>\n" - "<th><B>Test Nodes</B></th>\n" + ["<center>\n", + xhtml(["<table border=\"3\" cellpadding=\"5\" " + "bgcolor=\"",?table_color,"\">\n"], "<table>\n"), + "<th><b>History</b></th>\n" + "<th><b>Master Host</b></th>\n" + "<th><b>Test Nodes</b></th>\n" "\n"]]. timestamp(Dir) -> @@ -318,44 +347,46 @@ read_details_file(Dir) -> %%%-------------------------------------------------------------------- header(Title) -> - ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n" - "<!-- autogenerated by '"++atom_to_list(?MODULE)++"'. -->\n" - "<HTML>\n", - "<HEAD>\n", - - "<TITLE>" ++ Title ++ "</TITLE>\n", - "<META HTTP-EQUIV=\"CACHE-CONTROL\" CONTENT=\"NO-CACHE\">\n", - - "</HEAD>\n", - + CSSFile = xhtml(fun() -> "" end, + fun() -> make_relative(locate_default_css_file()) end), + [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>" ++ Title ++ "</title>\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + xhtml("", + ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">"]), + "</head>\n", body_tag(), - - "<!-- ---- DOCUMENT TITLE ---- -->\n", - - "<CENTER>\n", - "<H1>" ++ Title ++ "</H1>\n", - "</CENTER>\n", - - "<!-- ---- CONTENT ---- -->\n"]. + "<center>\n", + "<h1>" ++ Title ++ "</h1>\n", + "</center>\n"]. index_footer() -> - ["</TABLE>\n" - "</CENTER>\n" | footer()]. + ["</table>\n" + "</center>\n" | footer()]. footer() -> - ["<P><CENTER>\n" - "<HR>\n" - "<P><FONT SIZE=-1>\n" + ["<center>\n", + xhtml("<br><hr>\n", "<br />\n"), + xhtml("<p><font size=\"-1\">\n", "<div class=\"copyright\">"), "Copyright © ", year(), - " <A HREF=\"http://erlang.ericsson.se\">Open Telecom Platform</A><BR>\n" - "Updated: <!date>", current_time(), "<!/date><BR>\n" - "</FONT>\n" - "</CENTER>\n" + " <a href=\"http://www.erlang.org\">Open Telecom Platform</a>", + xhtml("<br>\n", "<br />\n"), + "Updated: <!date>", current_time(), "<!/date>", + xhtml("<br>\n", "<br />\n"), + xhtml("</font></p>\n", "</div>\n"), + "</center>\n" "</body>\n"]. body_tag() -> - "<body bgcolor=\"#FFFFFF\" text=\"#000000\" link=\"#0000FF\"" - "vlink=\"#800080\" alink=\"#FF0000\">\n". + xhtml("<body bgcolor=\"#FFFFFF\" text=\"#000000\" link=\"#0000FF\" " + "vlink=\"#800080\" alink=\"#FF0000\">\n", + "<body>\n"). current_time() -> format_time(calendar:local_time()). @@ -404,6 +435,23 @@ log_timestamp(Now) -> lists:flatten(io_lib:format("~2.2.0w:~2.2.0w:~2.2.0w", [H,M,S])). +basic_html() -> + case application:get_env(common_test_master, basic_html) of + {ok,true} -> + true; + _ -> + false + end. + +xhtml(HTML, XHTML) -> + ct_logs:xhtml(HTML, XHTML). + +locate_default_css_file() -> + ct_logs:locate_default_css_file(). + +make_relative(Dir) -> + ct_logs:make_relative(Dir). + force_write_file(Name,Contents) -> force_delete(Name), file:write_file(Name,Contents). @@ -452,3 +500,4 @@ cast(Msg) -> _Pid -> ?MODULE ! Msg end. + diff --git a/lib/common_test/src/ct_repeat.erl b/lib/common_test/src/ct_repeat.erl index be3c485b75..8ecd82f771 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-2010. All Rights Reserved. +%% Copyright Ericsson AB 2007-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 @@ -116,7 +116,7 @@ spawn_tester(script,Ctrl,Args) -> spawn_tester(func,Ctrl,Opts) -> Tester = fun() -> - case catch ct_run:run_test1(Opts) of + case catch ct_run:run_test2(Opts) of {'EXIT',Reason} -> exit(Reason); Result -> diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl index 26ca4f3cb4..46aec04ec1 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-2011. 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 @@ -37,7 +37,7 @@ %% Misc internal functions --export([variables_file_name/1,script_start1/2,run_test1/1]). +-export([variables_file_name/1,script_start1/2,run_test2/1]). -include("ct_event.hrl"). -include("ct_util.hrl"). @@ -57,11 +57,13 @@ config = [], event_handlers = [], ct_hooks = [], + enable_builtin_hooks, include = [], silent_connections, stylesheet, multiply_timetraps = 1, scale_timetraps = false, + create_priv_dir, testspecs = [], tests}). @@ -177,8 +179,16 @@ script_start1(Parent, Args) -> fun([CT]) -> list_to_atom(CT); ([]) -> true end, false, Args), + CreatePrivDir = get_start_opt(create_priv_dir, + fun([PD]) -> list_to_atom(PD); + ([]) -> auto_per_tc + end, Args), EvHandlers = event_handler_args2opts(Args), CTHooks = ct_hooks_args2opts(Args), + EnableBuiltinHooks = get_start_opt(enable_builtin_hooks, + fun([CT]) -> list_to_atom(CT); + ([]) -> undefined + end, undefined, Args), %% check flags and set corresponding application env variables @@ -245,11 +255,13 @@ script_start1(Parent, Args) -> logdir = LogDir, logopts = LogOpts, event_handlers = EvHandlers, ct_hooks = CTHooks, + enable_builtin_hooks = EnableBuiltinHooks, include = IncludeDirs, silent_connections = SilentConns, stylesheet = Stylesheet, multiply_timetraps = MultTT, - scale_timetraps = ScaleTT}, + scale_timetraps = ScaleTT, + create_priv_dir = CreatePrivDir}, %% check if log files should be refreshed or go on to run tests... Result = run_or_refresh(StartOpts, Args), @@ -316,15 +328,29 @@ script_start2(StartOpts = #opts{vts = undefined, 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), - AllEvHs = merge_vals([StartOpts#opts.event_handlers, - SpecStartOpts#opts.event_handlers]), + 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), AllInclude = merge_vals([StartOpts#opts.include, SpecStartOpts#opts.include]), @@ -339,9 +365,12 @@ script_start2(StartOpts = #opts{vts = undefined, config = SpecStartOpts#opts.config, event_handlers = AllEvHs, ct_hooks = AllCTHooks, + enable_builtin_hooks = + EnableBuiltinHooks, include = AllInclude, multiply_timetraps = MultTT, - scale_timetraps = ScaleTT}} + scale_timetraps = ScaleTT, + create_priv_dir = CreatePrivDir}} end; _ -> {undefined,StartOpts} @@ -355,9 +384,7 @@ script_start2(StartOpts = #opts{vts = undefined, {[],_} -> {error,no_testspec_specified}; {undefined,_} -> % no testspec used - case check_and_install_configfiles(InitConfig, TheLogDir, - Opts#opts.event_handlers, - Opts#opts.ct_hooks) of + 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); @@ -367,9 +394,7 @@ script_start2(StartOpts = #opts{vts = undefined, {_,_} -> % 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#opts.event_handlers, - Opts#opts.ct_hooks) of + 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, @@ -383,9 +408,7 @@ script_start2(StartOpts, 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#opts.event_handlers, - StartOpts#opts.ct_hooks) of + case check_and_install_configfiles(InitConfig, LogDir, StartOpts) of ok -> % go on read tests from start flags script_start3(StartOpts#opts{config=InitConfig, logdir=LogDir}, Args); @@ -393,12 +416,17 @@ script_start2(StartOpts, Args) -> Error end. -check_and_install_configfiles(Configs, LogDir, EvHandlers, CTHooks) -> +check_and_install_configfiles( + Configs, LogDir, #opts{ + event_handlers = EvHandlers, + ct_hooks = CTHooks, + enable_builtin_hooks = EnableBuiltinHooks} ) -> case ct_config:check_config_files(Configs) of false -> install([{config,Configs}, {event_handler,EvHandlers}, - {ct_hooks,CTHooks}], LogDir); + {ct_hooks,CTHooks}, + {enable_builtin_hooks,EnableBuiltinHooks}], LogDir); {value,{error,{nofile,File}}} -> {error,{cant_read_config_file,File}}; {value,{error,{wrong_config,Message}}}-> @@ -490,23 +518,23 @@ script_start4(#opts{label = Label, profile = Profile, shell = true, config = Config, event_handlers = EvHandlers, ct_hooks = CTHooks, - logdir = LogDir, logopts = LogOpts, - testspecs = Specs}, _Args) -> + enable_builtin_hooks = EnableBuiltinHooks, + logdir = LogDir, testspecs = Specs}, _Args) -> %% label - used by ct_logs application:set_env(common_test, test_label, Label), %% profile - used in ct_util application:set_env(common_test, profile, Profile), - InstallOpts = [{config,Config},{event_handler,EvHandlers}, - {ct_hooks, CTHooks}], if Config == [] -> ok; true -> io:format("\nInstalling: ~p\n\n", [Config]) end, - case install(InstallOpts) of + case install([{config,Config},{event_handler,EvHandlers}, + {ct_hooks, CTHooks}, + {enable_builtin_hooks,EnableBuiltinHooks}]) of ok -> ct_util:start(interactive, LogDir), ct_util:set_testdata({logopts, LogOpts}), @@ -555,6 +583,7 @@ script_usage() -> "\n\t[-no_auto_compile]" "\n\t[-multiply_timetraps N]" "\n\t[-scale_timetraps]" + "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t[-basic_html]\n\n"), io:format("Run tests from command line:\n\n" "\tct_run [-dir TestDir1 TestDir2 .. TestDirN] |" @@ -574,6 +603,7 @@ script_usage() -> "\n\t[-no_auto_compile]" "\n\t[-multiply_timetraps N]" "\n\t[-scale_timetraps]" + "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t[-basic_html]" "\n\t[-repeat N [-force_stop]] |" "\n\t[-duration HHMMSS [-force_stop]] |" @@ -594,6 +624,7 @@ script_usage() -> "\n\t[-no_auto_compile]" "\n\t[-multiply_timetraps N]" "\n\t[-scale_timetraps]" + "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t[-basic_html]" "\n\t[-repeat N [-force_stop]] |" "\n\t[-duration HHMMSS [-force_stop]] |" @@ -747,6 +778,11 @@ run_test2(StartOpts) -> %% CT Hooks CTHooks = get_start_opt(ct_hooks, value, [], StartOpts), + EnableBuiltinHooks = get_start_opt(enable_builtin_hooks, + fun(EBH) when EBH == true; + EBH == false -> + EBH + end, undefined, StartOpts), %% silent connections SilentConns = get_start_opt(silent_connections, @@ -765,6 +801,9 @@ run_test2(StartOpts) -> MultiplyTT = get_start_opt(multiply_timetraps, value, 1, StartOpts), ScaleTT = get_start_opt(scale_timetraps, value, false, StartOpts), + %% create unique priv dir names + CreatePrivDir = get_start_opt(create_priv_dir, value, StartOpts), + %% auto compile & include files Include = case proplists:get_value(auto_compile, StartOpts) of @@ -820,11 +859,13 @@ run_test2(StartOpts) -> logopts = LogOpts, config = CfgFiles, event_handlers = EvHandlers, ct_hooks = CTHooks, + enable_builtin_hooks = EnableBuiltinHooks, include = Include, silent_connections = SilentConns, stylesheet = Stylesheet, multiply_timetraps = MultiplyTT, - scale_timetraps = ScaleTT}, + scale_timetraps = ScaleTT, + create_priv_dir = CreatePrivDir}, %% test specification case proplists:get_value(spec, StartOpts) of @@ -871,6 +912,8 @@ run_spec_file(Relaxed, 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, @@ -878,26 +921,30 @@ run_spec_file(Relaxed, 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), - case check_and_install_configfiles(AllConfig, - which(logdir,LogDir), - AllEvHs, - AllCTHooks) of + Opts1 = Opts#opts{label = Label, + profile = Profile, + cover = Cover, + logdir = which(logdir, LogDir), + logopts = AllLogOpts, + config = AllConfig, + event_handlers = AllEvHs, + 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 -> - Opts1 = Opts#opts{label = Label, - profile = Profile, - cover = Cover, - logdir = which(logdir, LogDir), - logopts = AllLogOpts, - config = AllConfig, - event_handlers = AllEvHs, - include = AllInclude, - testspecs = AbsSpecs, - multiply_timetraps = MultTT, - scale_timetraps = ScaleTT, - ct_hooks = AllCTHooks}, {Run,Skip} = ct_testspec:prepare_tests(TS, node()), reformat_result(catch do_run(Run, Skip, Opts1, StartOpts)); {error,GCFReason} -> @@ -906,13 +953,10 @@ run_spec_file(Relaxed, end. run_prepared(Run, Skip, Opts = #opts{logdir = LogDir, - config = CfgFiles, - event_handlers = EvHandlers, - ct_hooks = CTHooks}, + config = CfgFiles }, StartOpts) -> LogDir1 = which(logdir, LogDir), - case check_and_install_configfiles(CfgFiles, LogDir1, - EvHandlers, CTHooks) of + case check_and_install_configfiles(CfgFiles, LogDir1, Opts) of ok -> reformat_result(catch do_run(Run, Skip, Opts#opts{logdir = LogDir1}, StartOpts)); @@ -944,7 +988,8 @@ check_config_file(Callback, File)-> run_dir(Opts = #opts{logdir = LogDir, config = CfgFiles, event_handlers = EvHandlers, - ct_hooks = CTHook }, StartOpts) -> + ct_hooks = CTHook, + enable_builtin_hooks = EnableBuiltinHooks }, StartOpts) -> LogDir1 = which(logdir, LogDir), Opts1 = Opts#opts{logdir = LogDir1}, AbsCfgFiles = @@ -967,7 +1012,8 @@ run_dir(Opts = #opts{logdir = LogDir, end, CfgFiles), case install([{config,AbsCfgFiles}, {event_handler,EvHandlers}, - {ct_hooks, CTHook}], LogDir1) of + {ct_hooks, CTHook}, + {enable_builtin_hooks,EnableBuiltinHooks}], LogDir1) of ok -> ok; {error,IReason} -> exit(IReason) end, @@ -1125,9 +1171,8 @@ run_testspec2(TestSpec) -> end, application:set_env(common_test, include, AllInclude), LogDir1 = which(logdir,Opts#opts.logdir), - case check_and_install_configfiles(Opts#opts.config, LogDir1, - Opts#opts.event_handlers, - Opts#opts.ct_hooks) of + case check_and_install_configfiles( + Opts#opts.config, LogDir1, Opts) of ok -> Opts1 = Opts#opts{testspecs = [], logdir = LogDir1, @@ -1148,9 +1193,11 @@ get_data_for_node(#testspec{label = Labels, userconfig = UsrCfgs, event_handler = EvHs, ct_hooks = CTHooks, + enable_builtin_hooks = EnableBuiltinHooks, include = Incl, multiply_timetraps = MTs, - scale_timetraps = STs}, Node) -> + scale_timetraps = STs, + create_priv_dir = PDs}, Node) -> Label = proplists:get_value(Node, Labels), Profile = proplists:get_value(Node, Profiles), LogDir = case proplists:get_value(Node, LogDirs) of @@ -1164,6 +1211,7 @@ get_data_for_node(#testspec{label = Labels, Cover = proplists:get_value(Node, CoverFs), MT = proplists:get_value(Node, MTs), ST = proplists:get_value(Node, STs), + CreatePrivDir = proplists:get_value(Node, PDs), ConfigFiles = [{?ct_config_txt,F} || {N,F} <- Cfgs, N==Node] ++ [CBF || {N,CBF} <- UsrCfgs, N==Node], EvHandlers = [{H,A} || {N,H,A} <- EvHs, N==Node], @@ -1177,9 +1225,11 @@ get_data_for_node(#testspec{label = Labels, config = ConfigFiles, event_handlers = EvHandlers, ct_hooks = FiltCTHooks, + enable_builtin_hooks = EnableBuiltinHooks, include = Include, multiply_timetraps = MT, - scale_timetraps = ST}. + scale_timetraps = ST, + create_priv_dir = CreatePrivDir}. refresh_logs(LogDir) -> {ok,Cwd} = file:get_cwd(), @@ -1363,7 +1413,8 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) -> %% which framework it runs under. case os:getenv("TEST_SERVER_FRAMEWORK") of false -> - os:putenv("TEST_SERVER_FRAMEWORK", "ct_framework"); + os:putenv("TEST_SERVER_FRAMEWORK", "ct_framework"), + os:putenv("TEST_SERVER_FRAMEWORK_NAME", "common_test"); "ct_framework" -> ok; Other -> @@ -1639,6 +1690,14 @@ final_tests1([{TestDir,Suite,GrsOrCs}|Tests], Final, Skip, Bad) when ({skipped,Group,TCs}) -> [ct_framework:make_conf(TestDir, Suite, Group, [skipped], TCs)]; + ({GrSpec = {Group,_},TCs}) -> + Props = [{override,GrSpec}], + [ct_framework:make_conf(TestDir, Suite, + Group, Props, TCs)]; + ({GrSpec = {Group,_,_},TCs}) -> + Props = [{override,GrSpec}], + [ct_framework:make_conf(TestDir, Suite, + Group, Props, TCs)]; ({Group,TCs}) -> [ct_framework:make_conf(TestDir, Suite, Group, [], TCs)]; @@ -1717,25 +1776,31 @@ set_group_leader_same_as_shell() -> false end. -check_and_add([{TestDir0,M,_} | Tests], Added) -> +check_and_add([{TestDir0,M,_} | Tests], Added, PA) -> case locate_test_dir(TestDir0, M) of {ok,TestDir} -> case lists:member(TestDir, Added) of true -> - check_and_add(Tests, Added); + check_and_add(Tests, Added, PA); false -> - true = code:add_patha(TestDir), - check_and_add(Tests, [TestDir|Added]) + case lists:member(rm_trailing_slash(TestDir), + code:get_path()) of + false -> + true = code:add_patha(TestDir), + check_and_add(Tests, [TestDir|Added], [TestDir|PA]); + true -> + check_and_add(Tests, [TestDir|Added], PA) + end end; {error,_} -> {error,{invalid_directory,TestDir0}} end; -check_and_add([], _) -> - ok. +check_and_add([], _, PA) -> + {ok,PA}. do_run_test(Tests, Skip, Opts) -> - case check_and_add(Tests, []) of - ok -> + case check_and_add(Tests, [], []) of + {ok,AddedToPath} -> ct_util:set_testdata({stats,{0,0,{0,0}}}), ct_util:set_testdata({cover,undefined}), test_server_ctrl:start_link(local), @@ -1813,6 +1878,8 @@ do_run_test(Tests, Skip, Opts) -> test_server_ctrl:multiply_timetraps(Opts#opts.multiply_timetraps), test_server_ctrl:scale_timetraps(Opts#opts.scale_timetraps), + test_server_ctrl:create_priv_dir(choose_val(Opts#opts.create_priv_dir, + auto_per_run)), ct_event:notify(#event{name=start_info, node=node(), data={NoOfTests,NoOfSuites,NoOfCases}}), @@ -1829,7 +1896,9 @@ do_run_test(Tests, Skip, Opts) -> end, lists:foreach(fun(Suite) -> maybe_cleanup_interpret(Suite, Opts#opts.step) - end, CleanUp); + end, CleanUp), + [code:del_path(Dir) || Dir <- AddedToPath], + ok; Error -> Error end. @@ -2254,10 +2323,13 @@ try_get_start_opt(Key, IfExists, IfNotExists, Args) -> end. ct_hooks_args2opts(Args) -> - ct_hooks_args2opts( - proplists:get_value(ct_hooks, Args, []),[]). + lists:foldl(fun({ct_hooks,Hooks}, Acc) -> + ct_hooks_args2opts(Hooks,Acc); + (_,Acc) -> + Acc + end,[],Args). -ct_hooks_args2opts([CTH,Arg,Prio,"and"| Rest],Acc) -> +ct_hooks_args2opts([CTH,Arg,Prio,"and"| Rest],Acc) when Arg /= "and" -> ct_hooks_args2opts(Rest,[{list_to_atom(CTH), parse_cth_args(Arg), parse_cth_args(Prio)}|Acc]); @@ -2315,31 +2387,38 @@ event_handler_init_args2opts([]) -> %% relative dirs "post run_test erl_args" is not kept! rel_to_abs(CtArgs) -> {PA,PZ} = get_pa_pz(CtArgs, [], []), - io:format(user, "~n", []), [begin - code:del_path(filename:basename(D)), - Abs = filename:absname(D), - code:add_pathz(Abs), - if D /= Abs -> + Dir = rm_trailing_slash(D), + Abs = make_abs(Dir), + if Dir /= Abs -> + code:del_path(Dir), + code:del_path(Abs), io:format(user, "Converting ~p to ~p and re-inserting " "with add_pathz/1~n", - [D, Abs]); + [Dir, Abs]); true -> - ok - end + code:del_path(Dir) + end, + code:add_pathz(Abs) end || D <- PZ], [begin - code:del_path(filename:basename(D)), - Abs = filename:absname(D), - code:add_patha(Abs), - if D /= Abs -> + Dir = rm_trailing_slash(D), + Abs = make_abs(Dir), + if Dir /= Abs -> + code:del_path(Dir), + code:del_path(Abs), io:format(user, "Converting ~p to ~p and re-inserting " "with add_patha/1~n", - [D, Abs]); - true ->ok - end + [Dir, Abs]); + true -> + code:del_path(Dir) + end, + code:add_patha(Abs) end || D <- PA], - io:format(user, "~n", []). + io:format(user, "~n", []). + +rm_trailing_slash(Dir) -> + filename:join(filename:split(Dir)). get_pa_pz([{pa,Dirs} | Args], PA, PZ) -> get_pa_pz(Args, PA ++ Dirs, PZ); @@ -2350,6 +2429,19 @@ get_pa_pz([_ | Args], PA, PZ) -> get_pa_pz([], PA, PZ) -> {PA,PZ}. +make_abs(RelDir) -> + Tokens = filename:split(filename:absname(RelDir)), + filename:join(lists:reverse(make_abs1(Tokens, []))). + +make_abs1([".."|Dirs], [_Dir|Path]) -> + make_abs1(Dirs, Path); +make_abs1(["."|Dirs], Path) -> + make_abs1(Dirs, Path); +make_abs1([Dir|Dirs], Path) -> + make_abs1(Dirs, [Dir|Path]); +make_abs1([], Path) -> + Path. + %% This function translates ct:run_test/1 start options %% to ct_run start arguments (on the init arguments format) - %% this is useful mainly for testing the ct_run start functions. @@ -2387,6 +2479,10 @@ opts2args(EnvStartOpts) -> [{scale_timetraps,[]}]; ({scale_timetraps,false}) -> []; + ({create_priv_dir,auto_per_run}) -> + []; + ({create_priv_dir,PD}) when is_atom(PD) -> + [{create_priv_dir,[atom_to_list(PD)]}]; ({force_stop,true}) -> [{force_stop,[]}]; ({force_stop,false}) -> diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl index 71a784870c..f4a551e3ff 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-2010. All Rights Reserved. +%% Copyright Ericsson AB 2003-2011. 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 diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl index 2cba1d8410..4c05f57520 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-2011. 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 @@ -568,6 +568,21 @@ add_tests([{scale_timetraps,Node,ST}|Ts],Spec) -> add_tests([{scale_timetraps,ST}|Ts],Spec) -> add_tests([{scale_timetraps,all_nodes,ST}|Ts],Spec); +%% --- create_priv_dir --- +add_tests([{create_priv_dir,all_nodes,PD}|Ts],Spec) -> + Tests = lists:map(fun(N) -> {create_priv_dir,N,PD} end, list_nodes(Spec)), + add_tests(Tests++Ts,Spec); +add_tests([{create_priv_dir,Nodes,PD}|Ts],Spec) when is_list(Nodes) -> + Ts1 = separate(Nodes,create_priv_dir,[PD],Ts,Spec#testspec.nodes), + add_tests(Ts1,Spec); +add_tests([{create_priv_dir,Node,PD}|Ts],Spec) -> + PDs = Spec#testspec.create_priv_dir, + PDs1 = [{ref2node(Node,Spec#testspec.nodes),PD} | + lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,PDs)], + add_tests(Ts,Spec#testspec{create_priv_dir=PDs1}); +add_tests([{create_priv_dir,PD}|Ts],Spec) -> + add_tests([{create_priv_dir,all_nodes,PD}|Ts],Spec); + %% --- config --- add_tests([{config,all_nodes,Files}|Ts],Spec) -> Tests = lists:map(fun(N) -> {config,N,Files} end, list_nodes(Spec)), @@ -670,6 +685,10 @@ add_tests([{ct_hooks, _Node, []}|Ts], Spec) -> add_tests([{ct_hooks, Hooks}|Ts], Spec) -> add_tests([{ct_hooks, all_nodes, Hooks}|Ts], Spec); +%% -- enable_builtin_hooks -- +add_tests([{enable_builtin_hooks,Bool}|Ts],Spec) -> + add_tests(Ts, Spec#testspec{ enable_builtin_hooks = Bool }); + %% --- include --- add_tests([{include,all_nodes,InclDirs}|Ts],Spec) -> Tests = lists:map(fun(N) -> {include,N,InclDirs} end, list_nodes(Spec)), @@ -874,7 +893,11 @@ separate([],_,_,_) -> %% {Suite2,[GrOrCase21,GrOrCase22,...]},...]} %% {{Node,Dir},[{Suite1,{skip,Cmt}}, %% {Suite2,[{GrOrCase21,{skip,Cmt}},GrOrCase22,...]},...]} -%% GrOrCase = {GroupName,[Case1,Case2,...]} | Case +%% GrOrCase = {GroupSpec,[Case1,Case2,...]} | Case +%% GroupSpec = {GroupName,OverrideProps} | +%% {GroupName,OverrideProps,SubGroupSpec} +%% OverrideProps = Props | default +%% SubGroupSpec = GroupSpec | [] insert_suites(Node,Dir,[S|Ss],Tests, MergeTests) -> Tests1 = insert_cases(Node,Dir,S,all,Tests,MergeTests), @@ -885,7 +908,7 @@ insert_suites(Node,Dir,S,Tests,MergeTests) -> insert_suites(Node,Dir,[S],Tests,MergeTests). insert_groups(Node,Dir,Suite,Group,Cases,Tests,MergeTests) - when is_atom(Group) -> + when is_atom(Group); is_tuple(Group) -> insert_groups(Node,Dir,Suite,[Group],Cases,Tests,MergeTests); insert_groups(Node,Dir,Suite,Groups,Cases,Tests,false) when ((Cases == all) or is_list(Cases)) and is_list(Groups) -> @@ -1130,6 +1153,7 @@ valid_terms() -> {event_handler,4}, {ct_hooks,2}, {ct_hooks,3}, + {enable_builtin_hooks,1}, {multiply_timetraps,2}, {multiply_timetraps,3}, {scale_timetraps,2}, @@ -1149,7 +1173,8 @@ valid_terms() -> {skip_groups,6}, {skip_groups,7}, {skip_cases,5}, - {skip_cases,6} + {skip_cases,6}, + {create_priv_dir,2} ]. %% this function "guesses" if the user has misspelled a term name diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl index 3b6ad6f98d..9d6ee3c8b9 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-2011. All Rights Reserved. +%% Copyright Ericsson AB 2003-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 @@ -827,15 +827,20 @@ get_profile_data(Profile, Key, StartDir) -> %%%----------------------------------------------------------------- %%% Internal functions call(Msg) -> - MRef = erlang:monitor(process,whereis(ct_util_server)), - Ref = make_ref(), - ct_util_server ! {Msg,{self(),Ref}}, - receive - {Ref, Result} -> - erlang:demonitor(MRef, [flush]), - Result; - {'DOWN',MRef,process,_,Reason} -> - {error,{ct_util_server_down,Reason}} + case whereis(ct_util_server) of + undefined -> + {error,ct_util_server_not_running}; + Pid -> + MRef = erlang:monitor(process, Pid), + Ref = make_ref(), + ct_util_server ! {Msg,{self(),Ref}}, + receive + {Ref, Result} -> + erlang:demonitor(MRef, [flush]), + Result; + {'DOWN',MRef,process,_,Reason} -> + {error,{ct_util_server_down,Reason}} + end end. return({To,Ref},Result) -> diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl index 73898fe371..6b016e95df 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-2011. All Rights Reserved. +%% Copyright Ericsson AB 2003-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,12 +39,14 @@ userconfig=[], event_handler=[], ct_hooks=[], + enable_builtin_hooks=true, include=[], multiply_timetraps=[], scale_timetraps=[], + create_priv_dir=[], alias=[], tests=[], - merge_tests = true }). + merge_tests=true}). -record(cover, {app=none, level=details, diff --git a/lib/common_test/src/cth_log_redirect.erl b/lib/common_test/src/cth_log_redirect.erl new file mode 100644 index 0000000000..77f57c6195 --- /dev/null +++ b/lib/common_test/src/cth_log_redirect.erl @@ -0,0 +1,112 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2011-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 +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +-module(cth_log_redirect). + +%%% @doc Common Test Framework functions handling test specifications. +%%% +%%% <p>This module redirects sasl and error logger info to common test log.</p> +%%% @end + + +%% CTH Callbacks +-export([id/1, init/2, post_init_per_group/4, pre_end_per_group/3, + post_end_per_testcase/4]). + +%% Event handler Callbacks +-export([init/1, + handle_event/2, handle_call/2, handle_info/2, + terminate/1]). + +id(_Opts) -> + ?MODULE. + +init(?MODULE, _Opts) -> + error_logger:add_report_handler(?MODULE), + tc_log_async. + +post_init_per_group(Group, Config, Result, tc_log_async) -> + case lists:member(parallel,proplists:get_value( + tc_group_properties,Config,[])) of + true -> + {Result, {set_log_func(ct_log),Group}}; + false -> + {Result, tc_log_async} + end; +post_init_per_group(_Group, _Config, Result, State) -> + {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), + {Result, State}. + +pre_end_per_group(Group, Config, {ct_log, Group}) -> + {Config, set_log_func(tc_log_async)}; +pre_end_per_group(_Group, Config, State) -> + {Config, State}. + + +%% Copied and modified from sasl_report_tty_h.erl +init(_Type) -> + {ok, tc_log_async}. + +handle_event({_Type, GL, _Msg}, State) when node(GL) /= node() -> + {ok, State}; +handle_event(Event, LogFunc) -> + case lists:keyfind(sasl, 1, application:which_applications()) of + false -> + sasl_not_started; + _Else -> + {ok, ErrLogType} = application:get_env(sasl, errlog_type), + SReport = sasl_report:format_report(group_leader(), ErrLogType, + tag_event(Event)), + if is_list(SReport) -> + ct_logs:LogFunc(sasl, SReport, []); + true -> %% Report is an atom if no logging is to be done + ignore + end + end, + EReport = error_logger_tty_h:write_event( + tag_event(Event),io_lib), + if is_list(EReport) -> + ct_logs:LogFunc(error_logger, EReport, []); + true -> %% Report is an atom if no logging is to be done + ignore + end, + {ok, LogFunc}. + + +handle_info(_,State) -> {ok, State}. + +handle_call(flush,State) -> + {ok, ok, State}; +handle_call({set_logfunc,NewLogFunc},_) -> + {ok, NewLogFunc, NewLogFunc}; +handle_call(_Query, _State) -> {error, bad_query}. + +terminate(_State) -> + error_logger:delete_report_handler(?MODULE), + []. + +tag_event(Event) -> + {calendar:local_time(), Event}. + +set_log_func(Func) -> + gen_event:call(error_logger, ?MODULE, {set_logfunc, Func}). diff --git a/lib/common_test/src/cth_surefire.erl b/lib/common_test/src/cth_surefire.erl new file mode 100644 index 0000000000..c42f956b3a --- /dev/null +++ b/lib/common_test/src/cth_surefire.erl @@ -0,0 +1,199 @@ +%%% @doc Common Test Framework functions handling test specifications. +%%% +%%% <p>This module creates a junit report of the test run if plugged in +%%% as a suite_callback.</p> + +-module(cth_surefire). + +%% Suite Callbacks +-export([id/1, init/2]). + +-export([pre_init_per_suite/3]). +-export([post_init_per_suite/4]). +-export([pre_end_per_suite/3]). +-export([post_end_per_suite/4]). + +-export([pre_init_per_group/3]). +-export([post_init_per_group/4]). +-export([pre_end_per_group/3]). +-export([post_end_per_group/4]). + +-export([pre_init_per_testcase/3]). +-export([post_end_per_testcase/4]). + +-export([on_tc_fail/3]). +-export([on_tc_skip/3]). + +-export([terminate/1]). + +-record(state, { filepath, axis, properties, package, hostname, + curr_suite, curr_suite_ts, curr_group = [], curr_tc, + curr_log_dir, timer, tc_log, + test_cases = [], + test_suites = [] }). + +-record(testcase, { log, group, classname, name, time, failure, timestamp }). +-record(testsuite, { errors, failures, hostname, name, tests, + time, timestamp, id, package, + properties, testcases }). + +id(Opts) -> + filename:absname(proplists:get_value(path, Opts, "junit_report.xml")). + +init(Path, Opts) -> + {ok, Host} = inet:gethostname(), + #state{ filepath = Path, + hostname = proplists:get_value(hostname,Opts,Host), + package = proplists:get_value(package,Opts), + axis = proplists:get_value(axis,Opts,[]), + properties = proplists:get_value(properties,Opts,[]), + timer = now() }. + +pre_init_per_suite(Suite,Config,State) -> + {Config, init_tc(State#state{ curr_suite = Suite, curr_suite_ts = now() }, + Config) }. + +post_init_per_suite(_Suite,Config, Result, State) -> + {Result, end_tc(init_per_suite,Config,Result,State)}. + +pre_end_per_suite(_Suite,Config,State) -> {Config, init_tc(State, Config)}. + +post_end_per_suite(_Suite,Config,Result,State) -> + NewState = end_tc(end_per_suite,Config,Result,State), + TCs = NewState#state.test_cases, + Suite = get_suite(NewState, TCs), + {Result, State#state{ test_cases = [], + test_suites = [Suite | State#state.test_suites]}}. + +pre_init_per_group(Group,Config,State) -> + {Config, init_tc(State#state{ curr_group = [Group|State#state.curr_group]}, + Config)}. + +post_init_per_group(_Group,Config,Result,State) -> + {Result, end_tc(init_per_group,Config,Result,State)}. + +pre_end_per_group(_Group,Config,State) -> {Config, init_tc(State, Config)}. + +post_end_per_group(_Group,Config,Result,State) -> + NewState = end_tc(end_per_group, Config, Result, State), + {Result, NewState#state{ curr_group = tl(NewState#state.curr_group)}}. + +pre_init_per_testcase(_TC,Config,State) -> {Config, init_tc(State, Config)}. + +post_end_per_testcase(TC,Config,Result,State) -> + {Result, end_tc(TC,Config, Result,State)}. + +on_tc_fail(_TC, Res, State) -> + TCs = State#state.test_cases, + TC = hd(State#state.test_cases), + NewTC = TC#testcase{ failure = + {fail,lists:flatten(io_lib:format("~p",[Res]))} }, + State#state{ test_cases = [NewTC | tl(TCs)]}. + +on_tc_skip(_Tc, Res, State) -> + TCs = State#state.test_cases, + TC = hd(State#state.test_cases), + NewTC = TC#testcase{ + failure = + {skipped,lists:flatten(io_lib:format("~p",[Res]))} }, + State#state{ test_cases = [NewTC | tl(TCs)]}. + +init_tc(State, Config) -> + State#state{ timer = now(), + tc_log = proplists:get_value(tc_logfile, Config)}. + +end_tc(Func, Config, Res, State) when is_atom(Func) -> + end_tc(atom_to_list(Func), Config, Res, State); +end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite, + curr_group = Groups, + timer = TS, tc_log = Log } ) -> + ClassName = atom_to_list(Suite), + PGroup = string:join([ atom_to_list(Group)|| + Group <- lists:reverse(Groups)],"."), + TimeTakes = io_lib:format("~f",[timer:now_diff(now(),TS) / 1000000]), + State#state{ test_cases = [#testcase{ log = Log, + timestamp = now_to_string(TS), + classname = ClassName, + group = PGroup, + name = Name, + time = TimeTakes, + failure = passed }| State#state.test_cases]}. + +get_suite(State, TCs) -> + Total = length(TCs), + Succ = length(lists:filter(fun(#testcase{ failure = F }) -> + F == passed + end,TCs)), + Fail = Total - Succ, + TimeTaken = timer:now_diff(now(),State#state.curr_suite_ts) / 1000000, + #testsuite{ name = atom_to_list(State#state.curr_suite), + package = State#state.package, + time = io_lib:format("~f",[TimeTaken]), + timestamp = now_to_string(State#state.curr_suite_ts), + errors = Fail, tests = Total, testcases = lists:reverse(TCs) }. + +terminate(State) -> + {ok,D} = file:open(State#state.filepath,[write]), + io:format(D, "<?xml version=\"1.0\" encoding= \"UTF-8\" ?>", []), + io:format(D, to_xml(State), []), + catch file:sync(D), + catch file:close(D). + +to_xml(#testcase{ group = Group, classname = CL, log = L, name = N, time = T, timestamp = TS, failure = F}) -> + ["<testcase ", + [["group=\"",Group,"\""]||Group /= ""]," " + "name=\"",N,"\" " + "time=\"",T,"\" " + "timestamp=\"",TS,"\" " + "log=\"",L,"\">", + case F of + passed -> + []; + {skipped,Reason} -> + ["<skipped type=\"skip\" message=\"Test ",N," in ",CL, + " skipped!\">", sanitize(Reason),"</skipped>"]; + {fail,Reason} -> + ["<failure message=\"Test ",N," in ",CL," failed!\" type=\"crash\">", + sanitize(Reason),"</failure>"] + end,"</testcase>"]; +to_xml(#testsuite{ package = P, hostname = H, errors = E, time = Time, + timestamp = TS, tests = T, name = N, testcases = Cases }) -> + ["<testsuite ", + [["package=\"",P,"\" "]||P /= undefined], + [["hostname=\"",P,"\" "]||H /= undefined], + [["name=\"",N,"\" "]||N /= undefined], + [["time=\"",Time,"\" "]||Time /= undefined], + [["timestamp=\"",TS,"\" "]||TS /= undefined], + "errors=\"",integer_to_list(E),"\" " + "tests=\"",integer_to_list(T),"\">", + [to_xml(Case) || Case <- Cases], + "</testsuite>"]; +to_xml(#state{ test_suites = TestSuites, axis = Axis, properties = Props }) -> + ["<testsuites>",properties_to_xml(Axis,Props), + [to_xml(TestSuite) || TestSuite <- TestSuites],"</testsuites>"]. + +properties_to_xml(Axis,Props) -> + ["<properties>", + [["<property name=\"",Name,"\" axis=\"yes\" value=\"",Value,"\" />"] || {Name,Value} <- Axis], + [["<property name=\"",Name,"\" value=\"",Value,"\" />"] || {Name,Value} <- Props], + "</properties>" + ]. + +sanitize([$>|T]) -> + ">" ++ sanitize(T); +sanitize([$<|T]) -> + "<" ++ sanitize(T); +sanitize([$"|T]) -> + """ ++ sanitize(T); +sanitize([$'|T]) -> + "'" ++ sanitize(T); +sanitize([$&|T]) -> + "&" ++ sanitize(T); +sanitize([H|T]) -> + [H|sanitize(T)]; +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]). diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl index cc8a932887..b340c6fdd1 100644 --- a/lib/common_test/src/vts.erl +++ b/lib/common_test/src/vts.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2011. All Rights Reserved. +%% Copyright Ericsson AB 2003-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 @@ -766,10 +766,6 @@ report1(tc_done,{_Suite,init_per_group,_},State) -> State; report1(tc_done,{_Suite,end_per_group,_},State) -> State; -report1(tc_done,{_Suite,ct_init_per_group,_},State) -> - State; -report1(tc_done,{_Suite,ct_end_per_group,_},State) -> - State; report1(tc_done,{_Suite,_Case,ok},State) -> State#state{ok=State#state.ok+1}; report1(tc_done,{_Suite,_Case,{failed,_Reason}},State) -> |