diff options
Diffstat (limited to 'lib/common_test/src')
53 files changed, 14337 insertions, 1791 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile index 987345c679..9d751996ad 100644 --- a/lib/common_test/src/Makefile +++ b/lib/common_test/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2003-2014. All Rights Reserved. +# Copyright Ericsson AB 2003-2016. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -79,7 +79,16 @@ MODULES= \ cth_conn_log \ ct_groups \ ct_property_test \ - ct_release_test + ct_release_test \ + ct_default_gl \ + erl2html2 \ + test_server_ctrl \ + test_server_gl \ + test_server_io \ + test_server_node \ + test_server \ + test_server_sup + TARGET_MODULES= $(MODULES:%=$(EBIN)/%) BEAM_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) @@ -101,8 +110,7 @@ DTD_FILES = \ # FLAGS # ---------------------------------------------------- ERL_COMPILE_FLAGS += -pa ../ebin -I../include -I $(ERL_TOP)/lib/snmp/include/ \ - -I../../test_server/include -I../../xmerl/inc/ \ - -I $(ERL_TOP)/lib/kernel/include -Werror + -I../../xmerl/inc/ -I $(ERL_TOP)/lib/kernel/include -Werror # ---------------------------------------------------- # Targets diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src index d847907d75..0aa4aacf16 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-2014. All Rights Reserved. +%% Copyright Ericsson AB 2009-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -22,10 +22,12 @@ {vsn, "%VSN%"}, {modules, [ct_cover, ct, + ct_default_gl, ct_event, ct_framework, ct_ftp, ct_gen_conn, + ct_groups, ct_hooks, ct_hooks_lock, ct_logs, @@ -36,6 +38,8 @@ ct_master_status, ct_netconfc, ct_conn_log_h, + ct_property_test, + ct_release_test, ct_repeat, ct_rpc, ct_run, @@ -45,6 +49,8 @@ ct_telnet, ct_testspec, ct_util, + ct_webtool, + ct_webtool_sup, unix_telnet, vts, ct_config, @@ -53,7 +59,14 @@ ct_slave, cth_log_redirect, cth_conn_log, - cth_surefire + cth_surefire, + erl2html2, + test_server_ctrl, + test_server, + test_server_gl, + test_server_io, + test_server_node, + test_server_sup ]}, {registered, [ct_logs, ct_util_server, @@ -61,13 +74,27 @@ ct_make_ref, vts, ct_master, - ct_master_logs]}, + ct_master_logs, + test_server_ctrl, + test_server, + test_server_break_process]}, {applications, [kernel,stdlib]}, {env, []}, - {runtime_dependencies,["xmerl-1.3.8","tools-2.8", - "test_server-3.9","stdlib-2.5","ssh-4.0", - "snmp-5.1.2","sasl-2.4.2","runtime_tools-1.8.16", - "kernel-4.0","inets-6.0","erts-7.0", - "debugger-4.1","crypto-3.6","compiler-6.0", - "observer-2.1"]}]}. + {runtime_dependencies, + ["compiler-6.0", + "crypto-3.6", + "debugger-4.1", + "erts-7.0", + "inets-6.0", + "kernel-4.0", + "observer-2.1", + "runtime_tools-1.8.16", + "sasl-2.4.2", + "snmp-5.1.2", + "ssh-4.0", + "stdlib-3.4", + "syntax_tools-1.7", + "tools-2.8", + "xmerl-1.3.8" + ]}]}. diff --git a/lib/common_test/src/common_test.appup.src b/lib/common_test/src/common_test.appup.src index a657e4a3a6..e98f947553 100644 --- a/lib/common_test/src/common_test.appup.src +++ b/lib/common_test/src/common_test.appup.src @@ -1,7 +1,7 @@ %% -*- erlang -*- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2014. All Rights Reserved. +%% Copyright Ericsson AB 2014-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl index 7958a349b4..4c4dc8bede 100644 --- a/lib/common_test/src/ct.erl +++ b/lib/common_test/src/ct.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2013. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -64,9 +64,11 @@ -export([require/1, require/2, get_config/1, get_config/2, get_config/3, reload_config/1, - log/1, log/2, log/3, log/4, - print/1, print/2, print/3, print/4, - pal/1, pal/2, pal/3, pal/4, + escape_chars/1, escape_chars/2, + log/1, log/2, log/3, log/4, log/5, + print/1, print/2, print/3, print/4, print/5, + pal/1, pal/2, pal/3, pal/4, pal/5, + set_verbosity/2, get_verbosity/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, @@ -87,6 +89,38 @@ -export([get_target_name/1]). -export([parse_table/1, listenv/1]). +-export([remaining_test_procs/0]). + +%%---------------------------------------------------------------------- +%% Exported types +%%---------------------------------------------------------------------- +%% For ct_gen_conn +-export_type([config_key/0, + target_name/0, + key_or_name/0]). + +%% For cth_conn_log +-export_type([conn_log_options/0, + conn_log_type/0, + conn_log_mod/0]). + +%%------------------------------------------------------------------ +%% Type declarations +%% ------------------------------------------------------------------ +-type config_key() :: atom(). % Config key which exists in a config file +-type target_name() :: atom().% Name associated to a config_key() though 'require' +-type key_or_name() :: config_key() | target_name(). + +%% Types used when logging connections with the 'cth_conn_log' hook +-type conn_log_options() :: [conn_log_option()]. +-type conn_log_option() :: {log_type,conn_log_type()} | + {hosts,[key_or_name()]}. +-type conn_log_type() :: raw | pretty | html | silent. +-type conn_log_mod() :: ct_netconfc | ct_telnet. +%%---------------------------------------------------------------------- + + + %%%----------------------------------------------------------------- %%% @spec install(Opts) -> ok | {error,Reason} %%% Opts = [Opt] @@ -160,9 +194,9 @@ run(TestDirs) -> %%% {repeat,N} | {duration,DurTime} | {until,StopTime} | %%% {force_stop,ForceStop} | {decrypt,DecryptKeyOrFile} | %%% {refresh_logs,LogDir} | {logopts,LogOpts} | -%%% {verbosity,VLevels} | {basic_html,Bool} | -%%% {ct_hooks, CTHs} | {enable_builtin_hooks,Bool} | -%%% {release_shell,Bool} +%%% {verbosity,VLevels} | {basic_html,Bool} | +%%% {esc_chars,Bool} | {ct_hooks, CTHs} | +%%% {enable_builtin_hooks,Bool} | {release_shell,Bool} %%% TestDirs = [string()] | string() %%% Suites = [string()] | [atom()] | string() | atom() %%% Cases = [atom()] | atom() @@ -281,7 +315,7 @@ step(TestDir,Suite,Case,Opts) -> %%% <c>> ct_telnet:cmd(unix_telnet, "ls .").</c><br/> %%% <c>{ok,["ls","file1 ...",...]}</c></p> start_interactive() -> - ct_util:start(interactive), + _ = ct_util:start(interactive), ok. %%%----------------------------------------------------------------- @@ -509,44 +543,88 @@ get_testspec_terms(Tags) -> %%%----------------------------------------------------------------- +%%% @spec escape_chars(IoList1) -> IoList2 | {error,Reason} +%%% IoList1 = iolist() +%%% IoList2 = iolist() +%%% +%%% @doc Escape special characters to be printed in html log +%%% +escape_chars(IoList) -> + ct_logs:escape_chars(IoList). + +%%%----------------------------------------------------------------- +%%% @spec escape_chars(Format, Args) -> IoList | {error,Reason} +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Escape special characters to be printed in html log +%%% +escape_chars(Format, Args) -> + try io_lib:format(Format, Args) of + IoList -> + ct_logs:escape_chars(IoList) + catch + _:Reason -> + {error,Reason} + end. + +%%%----------------------------------------------------------------- %%% @spec log(Format) -> ok -%%% @equiv log(default,50,Format,[]) +%%% @equiv log(default,50,Format,[],[]) log(Format) -> - log(default,?STD_IMPORTANCE,Format,[]). + log(default,?STD_IMPORTANCE,Format,[],[]). %%%----------------------------------------------------------------- %%% @spec log(X1,X2) -> ok %%% X1 = Category | Importance | Format %%% X2 = Format | Args -%%% @equiv log(Category,Importance,Format,Args) +%%% @equiv log(Category,Importance,Format,Args,[]) log(X1,X2) -> {Category,Importance,Format,Args} = if is_atom(X1) -> {X1,?STD_IMPORTANCE,X2,[]}; is_integer(X1) -> {default,X1,X2,[]}; is_list(X1) -> {default,?STD_IMPORTANCE,X1,X2} end, - log(Category,Importance,Format,Args). + log(Category,Importance,Format,Args,[]). %%%----------------------------------------------------------------- %%% @spec log(X1,X2,X3) -> ok +%%% X1 = Category | Importance | Format +%%% X2 = Importance | Format | Args +%%% X3 = Format | Args | Opts +%%% @equiv log(Category,Importance,Format,Args,Opts) +log(X1,X2,X3) -> + {Category,Importance,Format,Args,Opts} = + if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[],[]}; + is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,[]}; + is_integer(X1) -> {default,X1,X2,X3,[]}; + is_list(X1), is_list(X2) -> {default,?STD_IMPORTANCE,X1,X2,X3} + end, + log(Category,Importance,Format,Args,Opts). + +%%%----------------------------------------------------------------- +%%% @spec log(X1,X2,X3,X4) -> ok %%% X1 = Category | Importance %%% X2 = Importance | Format %%% X3 = Format | Args -%%% @equiv log(Category,Importance,Format,Args) -log(X1,X2,X3) -> - {Category,Importance,Format,Args} = - if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[]}; - is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3}; - is_integer(X1) -> {default,X1,X2,X3} +%%% X4 = Args | Opts +%%% @equiv log(Category,Importance,Format,Args,Opts) +log(X1,X2,X3,X4) -> + {Category,Importance,Format,Args,Opts} = + if is_atom(X1), is_integer(X2) -> {X1,X2,X3,X4,[]}; + is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,X4}; + is_integer(X1) -> {default,X1,X2,X3,X4} end, - log(Category,Importance,Format,Args). + log(Category,Importance,Format,Args,Opts). %%%----------------------------------------------------------------- -%%% @spec log(Category,Importance,Format,Args) -> ok +%%% @spec log(Category,Importance,Format,Args,Opts) -> ok %%% Category = atom() %%% Importance = integer() %%% Format = string() %%% Args = list() +%%% Opts = [Opt] +%%% Opt = {heading,string()} | esc_chars | no_css %%% %%% @doc Printout from a test case to the log file. %%% @@ -558,49 +636,67 @@ log(X1,X2,X3) -> %%% and default value for <c>Args</c> is <c>[]</c>.</p> %%% <p>Please see the User's Guide for details on <c>Category</c> %%% and <c>Importance</c>.</p> -log(Category,Importance,Format,Args) -> - ct_logs:tc_log(Category,Importance,Format,Args). +log(Category,Importance,Format,Args,Opts) -> + ct_logs:tc_log(Category,Importance,Format,Args,Opts). %%%----------------------------------------------------------------- %%% @spec print(Format) -> ok -%%% @equiv print(default,50,Format,[]) +%%% @equiv print(default,50,Format,[],[]) print(Format) -> - print(default,?STD_IMPORTANCE,Format,[]). + print(default,?STD_IMPORTANCE,Format,[],[]). %%%----------------------------------------------------------------- %%% @spec print(X1,X2) -> ok %%% X1 = Category | Importance | Format %%% X2 = Format | Args -%%% @equiv print(Category,Importance,Format,Args) +%%% @equiv print(Category,Importance,Format,Args,[]) print(X1,X2) -> {Category,Importance,Format,Args} = if is_atom(X1) -> {X1,?STD_IMPORTANCE,X2,[]}; is_integer(X1) -> {default,X1,X2,[]}; is_list(X1) -> {default,?STD_IMPORTANCE,X1,X2} end, - print(Category,Importance,Format,Args). + print(Category,Importance,Format,Args,[]). %%%----------------------------------------------------------------- %%% @spec print(X1,X2,X3) -> ok +%%% X1 = Category | Importance | Format +%%% X2 = Importance | Format | Args +%%% X3 = Format | Args | Opts +%%% @equiv print(Category,Importance,Format,Args,Opts) +print(X1,X2,X3) -> + {Category,Importance,Format,Args,Opts} = + if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[],[]}; + is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,[]}; + is_integer(X1) -> {default,X1,X2,X3,[]}; + is_list(X1), is_list(X2) -> {default,?STD_IMPORTANCE,X1,X2,X3} + end, + print(Category,Importance,Format,Args,Opts). + +%%%----------------------------------------------------------------- +%%% @spec print(X1,X2,X3,X4) -> ok %%% X1 = Category | Importance %%% X2 = Importance | Format %%% X3 = Format | Args -%%% @equiv print(Category,Importance,Format,Args) -print(X1,X2,X3) -> - {Category,Importance,Format,Args} = - if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[]}; - is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3}; - is_integer(X1) -> {default,X1,X2,X3} +%%% X4 = Args | Opts +%%% @equiv print(Category,Importance,Format,Args,Opts) +print(X1,X2,X3,X4) -> + {Category,Importance,Format,Args,Opts} = + if is_atom(X1), is_integer(X2) -> {X1,X2,X3,X4,[]}; + is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,X4}; + is_integer(X1) -> {default,X1,X2,X3,X4} end, - print(Category,Importance,Format,Args). + print(Category,Importance,Format,Args,Opts). %%%----------------------------------------------------------------- -%%% @spec print(Category,Importance,Format,Args) -> ok +%%% @spec print(Category,Importance,Format,Args,Opts) -> ok %%% Category = atom() %%% Importance = integer() %%% Format = string() %%% Args = list() +%%% Opts = [Opt] +%%% Opt = {heading,string()} %%% %%% @doc Printout from a test case to the console. %%% @@ -612,13 +708,13 @@ print(X1,X2,X3) -> %%% and default value for <c>Args</c> is <c>[]</c>.</p> %%% <p>Please see the User's Guide for details on <c>Category</c> %%% and <c>Importance</c>.</p> -print(Category,Importance,Format,Args) -> - ct_logs:tc_print(Category,Importance,Format,Args). +print(Category,Importance,Format,Args,Opts) -> + ct_logs:tc_print(Category,Importance,Format,Args,Opts). %%%----------------------------------------------------------------- %%% @spec pal(Format) -> ok -%%% @equiv pal(default,50,Format,[]) +%%% @equiv pal(default,50,Format,[],[]) pal(Format) -> pal(default,?STD_IMPORTANCE,Format,[]). @@ -626,35 +722,53 @@ pal(Format) -> %%% @spec pal(X1,X2) -> ok %%% X1 = Category | Importance | Format %%% X2 = Format | Args -%%% @equiv pal(Category,Importance,Format,Args) +%%% @equiv pal(Category,Importance,Format,Args,[]) pal(X1,X2) -> {Category,Importance,Format,Args} = if is_atom(X1) -> {X1,?STD_IMPORTANCE,X2,[]}; is_integer(X1) -> {default,X1,X2,[]}; is_list(X1) -> {default,?STD_IMPORTANCE,X1,X2} end, - pal(Category,Importance,Format,Args). + pal(Category,Importance,Format,Args,[]). %%%----------------------------------------------------------------- %%% @spec pal(X1,X2,X3) -> ok +%%% X1 = Category | Importance | Format +%%% X2 = Importance | Format | Args +%%% X3 = Format | Args | Opts +%%% @equiv pal(Category,Importance,Format,Args,Opts) +pal(X1,X2,X3) -> + {Category,Importance,Format,Args,Opts} = + if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[],[]}; + is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,[]}; + is_integer(X1) -> {default,X1,X2,X3,[]}; + is_list(X1), is_list(X2) -> {default,?STD_IMPORTANCE,X1,X2,X3} + end, + pal(Category,Importance,Format,Args,Opts). + +%%%----------------------------------------------------------------- +%%% @spec pal(X1,X2,X3,X4) -> ok %%% X1 = Category | Importance %%% X2 = Importance | Format %%% X3 = Format | Args -%%% @equiv pal(Category,Importance,Format,Args) -pal(X1,X2,X3) -> - {Category,Importance,Format,Args} = - if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[]}; - is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3}; - is_integer(X1) -> {default,X1,X2,X3} +%%% X4 = Args | Opts +%%% @equiv pal(Category,Importance,Format,Args,Opts) +pal(X1,X2,X3,X4) -> + {Category,Importance,Format,Args,Opts} = + if is_atom(X1), is_integer(X2) -> {X1,X2,X3,X4,[]}; + is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,X4}; + is_integer(X1) -> {default,X1,X2,X3,X4} end, - pal(Category,Importance,Format,Args). + pal(Category,Importance,Format,Args,Opts). %%%----------------------------------------------------------------- -%%% @spec pal(Category,Importance,Format,Args) -> ok +%%% @spec pal(Category,Importance,Format,Args,Opts) -> ok %%% Category = atom() %%% Importance = integer() %%% Format = string() %%% Args = list() +%%% Opts = [Opt] +%%% Opt = {heading,string()} | no_css %%% %%% @doc Print and log from a test case. %%% @@ -666,8 +780,26 @@ pal(X1,X2,X3) -> %%% and default value for <c>Args</c> is <c>[]</c>.</p> %%% <p>Please see the User's Guide for details on <c>Category</c> %%% and <c>Importance</c>.</p> -pal(Category,Importance,Format,Args) -> - ct_logs:tc_pal(Category,Importance,Format,Args). +pal(Category,Importance,Format,Args,Opts) -> + ct_logs:tc_pal(Category,Importance,Format,Args,Opts). + +%%%----------------------------------------------------------------- +%%% @spec set_verbosity(Category, Level) -> ok +%%% Category = default | atom() +%%% Level = integer() +%%% +%%% @doc Set the verbosity level for a category +set_verbosity(Category, Level) -> + ct_util:set_verbosity({Category,Level}). + +%%%----------------------------------------------------------------- +%%% @spec get_verbosity(Category) -> Level | undefined +%%% Category = default | atom() +%%% Level = integer() +%%% +%%% @doc Read the verbosity level for a category +get_verbosity(Category) -> + ct_util:get_verbosity(Category). %%%----------------------------------------------------------------- %%% @spec capture_start() -> ok @@ -718,7 +850,8 @@ 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) ++ ")\">.*"), + {ok,MP} = re:compile("<div class=\"(" ++ lists:flatten(CatsStr) ++ ")\">.*", + [unicode]), lists:flatmap(fun(Str) -> case re:run(Str, MP) of {match,_} -> []; @@ -787,13 +920,13 @@ comment(Comment) when is_list(Comment) -> Formatted = case (catch io_lib:format("~ts",[Comment])) of {'EXIT',_} -> % it's a list not a string - io_lib:format("~p",[Comment]); + io_lib:format("~tp",[Comment]); String -> String end, send_html_comment(lists:flatten(Formatted)); comment(Comment) -> - Formatted = io_lib:format("~p",[Comment]), + Formatted = io_lib:format("~tp",[Comment]), send_html_comment(lists:flatten(Formatted)). %%%----------------------------------------------------------------- @@ -1343,3 +1476,36 @@ continue() -> %%% in order to let the test case proceed.</p> continue(TestCase) -> test_server:continue(TestCase). + + +%%%----------------------------------------------------------------- +%%% @spec remaining_test_procs() -> {TestProcs,SharedGL,OtherGLs} +%%% TestProcs = [{pid(),GL}] +%%% GL = SharedGL = pid() +%%% OtherGLs = [pid()] +%%% +%%% @doc <p>This function will return the identity of test- and group +%%% leader processes that are still running at the time of this call. +%%% TestProcs are processes in the system that have a Common Test IO +%%% process as group leader. SharedGL is the central Common Test +%%% IO process, responsible for printing to log files for configuration +%%% functions and sequentially executing test cases. OtherGLs are +%%% Common Test IO processes that print to log files for test cases +%%% in parallel test case groups.</p> +%%% <p>The process information returned by this function may be +%%% used to locate and terminate remaining processes after tests have +%%% finished executing. The function would typically by called from +%%% Common Test Hook functions.</p> +%%% <p>Note that processes that execute configuration functions or +%%% test cases are never included in TestProcs. It is therefore safe +%%% to use post configuration hook functions (such as post_end_per_suite, +%%% post_end_per_group, post_end_per_testcase) to terminate all processes +%%% in TestProcs that have the current group leader process as its group +%%% leader.</p> +%%% <p>Note also that the shared group leader (SharedGL) must never be +%%% terminated by the user, only by Common Test. Group leader processes +%%% for parallel test case groups (OtherGLs) may however be terminated +%%% in post_end_per_group hook functions.</p> +%%% +remaining_test_procs() -> + ct_util:remaining_test_procs(). diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl index 251204aa75..9cb9b0ba16 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2010-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -81,6 +81,7 @@ start(Mode) -> do_start(Parent) -> process_flag(trap_exit,true), + ct_util:mark_process(), register(ct_config_server,self()), ct_util:create_table(?attr_table,bag,#ct_conf.key), {ok,StartDir} = file:get_cwd(), @@ -119,7 +120,8 @@ call(Msg) -> end. return({To,Ref},Result) -> - To ! {Ref, Result}. + To ! {Ref, Result}, + ok. loop(StartDir) -> receive @@ -128,11 +130,11 @@ loop(StartDir) -> return(From,Result), loop(StartDir); {{set_default_config,{Config,Scope}},From} -> - set_config(Config,{true,Scope}), + _ = set_config(Config,{true,Scope}), return(From,ok), loop(StartDir); {{set_default_config,{Name,Config,Scope}},From} -> - set_config(Name,Config,{true,Scope}), + _ = set_config(Name,Config,{true,Scope}), return(From,ok), loop(StartDir); {{delete_default_config,Scope},From} -> @@ -149,7 +151,7 @@ loop(StartDir) -> loop(StartDir); {{stop},From} -> ets:delete(?attr_table), - file:set_cwd(StartDir), + ok = file:set_cwd(StartDir), return(From,ok) end. @@ -170,8 +172,8 @@ reload_config(KeyOrName) -> process_default_configs(Opts) -> lists:flatmap(fun({config,[_|_] = FileOrFiles}) -> - case {io_lib:printable_list(FileOrFiles), - io_lib:printable_list(hd(FileOrFiles))} of + case {io_lib:printable_unicode_list(FileOrFiles), + io_lib:printable_unicode_list(hd(FileOrFiles))} of {false,true} -> FileOrFiles; {true,false} -> @@ -257,7 +259,7 @@ read_config_files(Opts) -> read_config_files_int([{Callback, File}|Files], FunToSave) -> case Callback:read_config(File) of {ok, Config} -> - FunToSave(Config, Callback, File), + _ = FunToSave(Config, Callback, File), read_config_files_int(Files, FunToSave); {error, {ErrorName, ErrorDetail}} -> {user_error, {ErrorName, File, ErrorDetail}}; @@ -267,6 +269,15 @@ read_config_files_int([{Callback, File}|Files], FunToSave) -> read_config_files_int([], _FunToSave) -> ok. + +read_config_files(ConfigFiles, FunToSave) -> + case read_config_files_int(ConfigFiles, FunToSave) of + {user_error, Error} -> + {error, Error}; + ok -> + ok + end. + store_config(Config, Callback, File) when is_tuple(Config) -> store_config([Config], Callback, File); @@ -455,8 +466,12 @@ reload_conf(KeyOrName) -> undefined; HandlerList -> HandlerList2 = lists:usort(HandlerList), - read_config_files_int(HandlerList2, fun rewrite_config/3), - get_config(KeyOrName) + case read_config_files(HandlerList2, fun rewrite_config/3) of + ok -> + get_config(KeyOrName); + Error -> + Error + end end. release_allocated() -> @@ -490,16 +505,16 @@ associate(Name,_Key,Configs) -> associate_int(Name,Configs,os:getenv("COMMON_TEST_ALIAS_TOP")). associate_int(Name,Configs,"true") -> - lists:map(fun({K,_Config}) -> + lists:foreach(fun({K,_Config}) -> Cs = ets:match_object( ?attr_table, #ct_conf{key=element(1,K), name='_UNDEF',_='_'}), [ets:insert(?attr_table,C#ct_conf{name=Name}) || C <- Cs] - end,Configs); + end,Configs); associate_int(Name,Configs,_) -> - lists:map(fun({K,Config}) -> + lists:foreach(fun({K,Config}) -> Key = if is_tuple(K) -> element(1,K); is_atom(K) -> K end, @@ -511,7 +526,7 @@ associate_int(Name,Configs,_) -> [ets:insert(?attr_table,C#ct_conf{name=Name, value=Config}) || C <- Cs] - end,Configs). + end,Configs). @@ -576,7 +591,7 @@ encrypt_config_file(SrcFileName, EncryptFileName, {file,KeyFile}) -> end; encrypt_config_file(SrcFileName, EncryptFileName, {key,Key}) -> - crypto:start(), + _ = crypto:start(), {Key,IVec} = make_crypto_key(Key), case file:read_file(SrcFileName) of {ok,Bin0} -> @@ -615,7 +630,7 @@ decrypt_config_file(EncryptFileName, TargetFileName, {file,KeyFile}) -> end; decrypt_config_file(EncryptFileName, TargetFileName, {key,Key}) -> - crypto:start(), + _ = crypto:start(), {Key,IVec} = make_crypto_key(Key), case file:read_file(EncryptFileName) of {ok,Bin} -> @@ -694,11 +709,10 @@ make_crypto_key(String) -> {[K1,K2,K3],IVec}. random_bytes(N) -> - random:seed(os:timestamp()), random_bytes_1(N, []). random_bytes_1(0, Acc) -> Acc; -random_bytes_1(N, Acc) -> random_bytes_1(N-1, [random:uniform(255)|Acc]). +random_bytes_1(N, Acc) -> random_bytes_1(N-1, [rand:uniform(255)|Acc]). check_callback_load(Callback) -> case code:is_loaded(Callback) of @@ -779,14 +793,13 @@ prepare_config_list(Args) -> % TODO: add logging of the loaded configuration file to the CT FW log!!! add_config(Callback, []) -> - read_config_files_int([{Callback, []}], fun store_config/3); + read_config_files([{Callback, []}], fun store_config/3); add_config(Callback, [File|_Files]=Config) when is_list(File) -> lists:foreach(fun(CfgStr) -> - read_config_files_int([{Callback, CfgStr}], fun store_config/3) end, + read_config_files([{Callback, CfgStr}], fun store_config/3) end, Config); add_config(Callback, [C|_]=Config) when is_integer(C) -> - read_config_files_int([{Callback, Config}], fun store_config/3), - ok. + read_config_files([{Callback, Config}], fun store_config/3). remove_config(Callback, Config) -> ets:match_delete(?attr_table, diff --git a/lib/common_test/src/ct_config_plain.erl b/lib/common_test/src/ct_config_plain.erl index 810dec7c76..e72b55971b 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2010-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/common_test/src/ct_config_xml.erl b/lib/common_test/src/ct_config_xml.erl index 593ae3de52..4343761707 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-2011. All Rights Reserved. +%% Copyright Ericsson AB 2010-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/common_test/src/ct_conn_log_h.erl b/lib/common_test/src/ct_conn_log_h.erl index 2dd4fdac05..cf0a228e1b 100644 --- a/lib/common_test/src/ct_conn_log_h.erl +++ b/lib/common_test/src/ct_conn_log_h.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012-2013. All Rights Reserved. +%% Copyright Ericsson AB 2012-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -104,40 +104,63 @@ terminate(_,#state{logs=Logs}) -> %%%----------------------------------------------------------------- %%% Writing reports write_report(_Time,#conn_log{header=false,module=ConnMod}=Info,Data,GL,State) -> - {LogType,Fd} = get_log(Info,GL,State), - io:format(Fd,"~n~ts",[format_data(ConnMod,LogType,Data)]); + case get_log(Info,GL,State) of + {silent,_,_} -> + ok; + {LogType,Dest,Fd} -> + Str = if LogType == html, Dest == gl -> ["$tc_html","~n~ts"]; + true -> "~n~ts" + end, + io:format(Fd,Str,[format_data(ConnMod,LogType,Data)]) + end; write_report(Time,#conn_log{module=ConnMod}=Info,Data,GL,State) -> - {LogType,Fd} = get_log(Info,GL,State), - io:format(Fd,"~n~ts~ts~ts",[format_head(ConnMod,LogType,Time), - format_title(LogType,Info), - format_data(ConnMod,LogType,Data)]). + case get_log(Info,GL,State) of + {silent,_,_} -> + ok; + {LogType,Dest,Fd} -> + case format_data(ConnMod,LogType,Data) of + [] when Info#conn_log.action==send; Info#conn_log.action==recv -> + ok; + FormattedData -> + Str = if LogType == html, Dest == gl -> + ["$tc_html","~n~ts~ts~ts"]; + true -> + "~n~ts~ts~ts" + end, + io:format(Fd,Str,[format_head(ConnMod,LogType,Time), + format_title(LogType,Info), + FormattedData]) + end + end. write_error(Time,#conn_log{module=ConnMod}=Info,Report,GL,State) -> case get_log(Info,GL,State) of - {html,_} -> + {LogType,_,_} when LogType==html; LogType==silent -> %% The error will anyway be written in the html log by the %% sasl error handler, so don't write it again. ok; - {LogType,Fd} -> - io:format(Fd,"~n~ts~ts~ts", - [format_head(ConnMod,LogType,Time," ERROR"), - format_title(LogType,Info), - format_error(LogType,Report)]) + {LogType,Dest,Fd} -> + Str = if LogType == html, Dest == gl -> ["$tc_html","~n~ts~ts~ts"]; + true -> "~n~ts~ts~ts" + end, + io:format(Fd,Str,[format_head(ConnMod,LogType,Time," ERROR"), + format_title(LogType,Info), + format_error(LogType,Report)]) end. get_log(Info,GL,State) -> case proplists:get_value(GL,State#state.logs) of undefined -> - {html,State#state.default_gl}; + {html,gl,State#state.default_gl}; ConnLogs -> case proplists:get_value(Info#conn_log.module,ConnLogs) of {html,_} -> - {html,GL}; + {html,gl,GL}; {LogType,Fds} -> - {LogType,get_fd(Info,Fds)}; + {LogType,file,get_fd(Info,Fds)}; undefined -> - {html,GL} + {html,gl,GL} end end. @@ -163,7 +186,7 @@ format_head(ConnMod,_,Time,Text) -> io_lib:format("~n~ts",[Head]). format_title(raw,#conn_log{client=Client}=Info) -> - io_lib:format("Client ~w ~s ~ts",[Client,actionstr(Info),serverstr(Info)]); + io_lib:format("Client ~tw ~s ~ts",[Client,actionstr(Info),serverstr(Info)]); format_title(_,Info) -> Title = pad_char_end(?WIDTH,pretty_title(Info),$=), io_lib:format("~n~ts", [Title]). @@ -174,9 +197,9 @@ format_data(ConnMod,LogType,Data) -> ConnMod:format_data(LogType,Data). format_error(raw,Report) -> - io_lib:format("~n~p~n",[Report]); + io_lib:format("~n~tp~n",[Report]); format_error(pretty,Report) -> - [io_lib:format("~n ~p: ~p",[K,V]) || {K,V} <- Report]. + [io_lib:format("~n ~tp: ~tp",[K,V]) || {K,V} <- Report]. %%%----------------------------------------------------------------- @@ -207,7 +230,7 @@ pretty_head({{{Y,Mo,D},{H,Mi,S}},MicroS},ConnMod,Text0) -> micro2milli(MicroS)]). pretty_title(#conn_log{client=Client}=Info) -> - io_lib:format("= Client ~w ~s ~ts ", + io_lib:format("= Client ~tw ~s ~ts ", [Client,actionstr(Info),serverstr(Info)]). actionstr(#conn_log{action=send}) -> "----->"; @@ -215,16 +238,18 @@ actionstr(#conn_log{action=cmd}) -> "----->"; actionstr(#conn_log{action=recv}) -> "<-----"; actionstr(#conn_log{action=open}) -> "opened session to"; actionstr(#conn_log{action=close}) -> "closed session to"; +actionstr(#conn_log{action=connect}) -> "connected to"; +actionstr(#conn_log{action=disconnect}) -> "disconnected from"; actionstr(_) -> "<---->". serverstr(#conn_log{name=undefined,address={undefined,_}}) -> io_lib:format("server",[]); serverstr(#conn_log{name=undefined,address=Address}) -> - io_lib:format("~p",[Address]); + io_lib:format("~tp",[Address]); serverstr(#conn_log{name=Alias,address={undefined,_}}) -> - io_lib:format("~w",[Alias]); + io_lib:format("~tw",[Alias]); serverstr(#conn_log{name=Alias,address=Address}) -> - io_lib:format("~w(~p)",[Alias,Address]). + io_lib:format("~tw(~tp)",[Alias,Address]). month(1) -> "Jan"; month(2) -> "Feb"; diff --git a/lib/common_test/src/ct_cover.erl b/lib/common_test/src/ct_cover.erl index 8e5ce9b245..c258516915 100644 --- a/lib/common_test/src/ct_cover.erl +++ b/lib/common_test/src/ct_cover.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2012. All Rights Reserved. +%% Copyright Ericsson AB 2006-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/common_test/src/ct_default_gl.erl b/lib/common_test/src/ct_default_gl.erl new file mode 100644 index 0000000000..9ae430c546 --- /dev/null +++ b/lib/common_test/src/ct_default_gl.erl @@ -0,0 +1,84 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012-2016. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +-module(ct_default_gl). +-export([start_link/1, stop/0]). + +-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2]). + +%% start_link() +%% Start a new group leader process. +start_link(ParentGL) -> + do_start(ParentGL, 3). + +do_start(_ParentGL, 0) -> + exit({?MODULE,startup}); +do_start(ParentGL, Retries) -> + case whereis(?MODULE) of + undefined -> + case gen_server:start_link(?MODULE, [ParentGL], []) of + {ok,Pid} -> + {ok,Pid}; + Other -> + Other + end; + Pid -> + exit(Pid, kill), + timer:sleep(1000), + do_start(ParentGL, Retries-1) + end. + +%% stop(Pid) +%% Stop a group leader process. +stop() -> + gen_server:cast(whereis(?MODULE), stop). + + +%%% Internal functions. + +init([ParentGL]) -> + register(?MODULE, self()), + ct_util:mark_process(), + {ok,#{parent_gl_pid => ParentGL, + parent_gl_monitor => erlang:monitor(process,ParentGL)}}. + +handle_cast(stop, St) -> + {stop,normal,St}. + +%% If the parent group leader dies, fall back on using the local user process +handle_info({'DOWN',Ref,process,_,_Reason}, #{parent_gl_monitor := Ref} = St) -> + User = whereis(user), + {noreply,St#{parent_gl_pid => User, + parent_gl_monitor => erlang:monitor(process,User)}}; + +handle_info({io_request,_From,_ReplyAs,_Req} = IoReq, + #{parent_gl_pid := ParentGL} = St) -> + ParentGL ! IoReq, + {noreply,St}; + +handle_info(Msg, St) -> + io:format(user, "Common Test Group Leader process got: ~tp~n", [Msg]), + {noreply,St}. + +handle_call(_Req, _From, St) -> + {reply,ok,St}. + +terminate(_, _) -> + ok. diff --git a/lib/common_test/src/ct_event.erl b/lib/common_test/src/ct_event.erl index 01beabaa73..8b5bba7600 100644 --- a/lib/common_test/src/ct_event.erl +++ b/lib/common_test/src/ct_event.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2013. All Rights Reserved. +%% Copyright Ericsson AB 2006-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -137,6 +137,7 @@ is_alive() -> %% this function is called to initialize the event handler. %%-------------------------------------------------------------------- init(RecvPids) -> + ct_util:mark_process(), %% RecvPids = [{RecvTag,Pid}] {ok,#state{receivers=RecvPids}}. @@ -151,7 +152,7 @@ init(RecvPids) -> %%-------------------------------------------------------------------- handle_event(Event,State=#state{receivers=RecvPids}) -> print("~n=== ~w ===~n", [?MODULE]), - print("~w: ~w~n", [Event#event.name,Event#event.data]), + print("~tw: ~tw~n", [Event#event.name,Event#event.data]), lists:foreach(fun(Recv) -> report_event(Recv,Event) end, RecvPids), {ok,State}. diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl index f792269c41..6066470233 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2004-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ -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, get_html_wrapper/4]). +-export([get_log_dir/0, get_logopts/0, format_comment/1, get_html_wrapper/4]). -export([error_in_suite/1, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2]). @@ -52,9 +52,27 @@ %%% %%% @doc Test server framework callback, called by the test_server %%% when a new test case is started. -init_tc(Mod,Func,Config) -> +init_tc(_,{end_per_testcase_not_run,_},[Config]) -> + %% Testcase is completed (skipped or failed), but end_per_testcase + %% is not run - don't call pre-hook. + {ok,[Config]}; +init_tc(Mod,EPTC={end_per_testcase,_},[Config]) -> %% in case Mod == ct_framework, lookup the suite name Suite = get_suite_name(Mod, Config), + case ct_hooks:init_tc(Suite,EPTC,Config) of + NewConfig when is_list(NewConfig) -> + {ok,[NewConfig]}; + Other-> + Other + end; + +init_tc(Mod,Func0,Args) -> + %% in case Mod == ct_framework, lookup the suite name + Suite = get_suite_name(Mod, Args), + {Func,HookFunc} = case Func0 of + {init_per_testcase,F} -> {F,Func0}; + _ -> {Func0,Func0} + end, %% check if previous testcase was interpreted and has left %% a "dead" trace window behind - if so, kill it @@ -70,12 +88,15 @@ init_tc(Mod,Func,Config) -> andalso Func=/=end_per_group andalso ct_util:get_testdata(skip_rest) of true -> + initialize(false,Mod,Func,Args), {auto_skip,"Repeated test stopped by force_stop option"}; _ -> case ct_util:get_testdata(curr_tc) of {Suite,{suite0_failed,{require,Reason}}} -> + initialize(false,Mod,Func,Args), {auto_skip,{require_failed_in_suite0,Reason}}; {Suite,{suite0_failed,_}=Failure} -> + initialize(false,Mod,Func,Args), {fail,Failure}; _ -> ct_util:update_testdata(curr_tc, @@ -86,7 +107,7 @@ init_tc(Mod,Func,Config) -> end, [create]), case ct_util:read_suite_data({seq,Suite,Func}) of undefined -> - init_tc1(Mod,Suite,Func,Config); + init_tc1(Mod,Suite,Func,HookFunc,Args); Seq when is_atom(Seq) -> case ct_util:read_suite_data({seq,Suite,Seq}) of [Func|TCs] -> % this is the 1st case in Seq @@ -102,27 +123,25 @@ init_tc(Mod,Func,Config) -> _ -> ok end, - init_tc1(Mod,Suite,Func,Config); + init_tc1(Mod,Suite,Func,HookFunc,Args); {failed,Seq,BadFunc} -> - {auto_skip,{sequence_failed,Seq,BadFunc}} + initialize(false,Mod,Func,Args), + {auto_skip,{sequence_failed,Seq,BadFunc}} end end - end. + end. -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}}), - ct_suite_init(?MODULE, error_in_suite, [], Config0), - case ?val(error, Config0) of +init_tc1(?MODULE,_,error_in_suite,_,[Config0]) when is_list(Config0) -> + initialize(false,?MODULE,error_in_suite), + _ = ct_suite_init(?MODULE,error_in_suite,[],Config0), + case ?val(error,Config0) of undefined -> {fail,"unknown_error_in_suite"}; Reason -> {fail,Reason} end; -init_tc1(Mod,Suite,Func,[Config0]) when is_list(Config0) -> +init_tc1(Mod,Suite,Func,HookFunc,[Config0]) when is_list(Config0) -> Config1 = case ct_util:read_suite_data(last_saved_config) of {{Suite,LastFunc},SavedConfig} -> % last testcase @@ -156,46 +175,43 @@ init_tc1(Mod,Suite,Func,[Config0]) when is_list(Config0) -> %% testcase info function (these should only survive the %% testcase, not the whole suite) FuncSpec = group_or_func(Func,Config0), - if is_tuple(FuncSpec) -> % group - ok; - true -> - ct_config:delete_default_config(testcase) - end, - Initialize = fun() -> - ct_logs:init_tc(false), - ct_event:notify(#event{name=tc_start, - node=node(), - data={Mod,FuncSpec}}) - end, + HookFunc1 = + if is_tuple(FuncSpec) -> % group + FuncSpec; + true -> + ct_config:delete_default_config(testcase), + HookFunc + end, case add_defaults(Mod,Func,AllGroups) of Error = {suite0_failed,_} -> - Initialize(), + initialize(false,Mod,FuncSpec), ct_util:set_testdata({curr_tc,{Suite,Error}}), {error,Error}; Error = {group0_failed,_} -> - Initialize(), + initialize(false,Mod,FuncSpec), {auto_skip,Error}; Error = {testcase0_failed,_} -> - Initialize(), + initialize(false,Mod,FuncSpec), {auto_skip,Error}; {SuiteInfo,MergeResult} -> case MergeResult of {error,Reason} -> - Initialize(), + initialize(false,Mod,FuncSpec), {fail,Reason}; _ -> - init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) + init_tc2(Mod,Suite,Func,HookFunc1, + SuiteInfo,MergeResult,Config) end end; -init_tc1(_Mod,_Suite,_Func,Args) -> +init_tc1(_Mod,_Suite,_Func,_HookFunc,Args) -> {ok,Args}. -init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> +init_tc2(Mod,Suite,Func,HookFunc,SuiteInfo,MergeResult,Config) -> %% timetrap must be handled before require MergedInfo = timetrap_first(MergeResult, [], []), %% tell logger to use specified style sheet - case lists:keysearch(stylesheet,1,MergeResult++Config) of + _ = case lists:keysearch(stylesheet,1,MergeResult++Config) of {value,{stylesheet,SSFile}} -> ct_logs:set_stylesheet(Func,add_data_dir(SSFile,Config)); _ -> @@ -219,11 +235,8 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> Conns -> ct_util:silence_connections(Conns) 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}}), + initialize((Func==init_per_suite),Mod,FuncSpec), case catch configure(MergedInfo,MergedInfo,SuiteInfo, FuncSpec,[],Config) of @@ -238,7 +251,7 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> {ok,PostInitHook,Config1} -> case get('$test_server_framework_test') of undefined -> - ct_suite_init(Suite, FuncSpec, PostInitHook, Config1); + ct_suite_init(Suite,HookFunc,PostInitHook,Config1); Fun -> PostInitHookResult = do_post_init_hook(PostInitHook, Config1), @@ -251,16 +264,28 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> end end. -ct_suite_init(Suite, FuncSpec, PostInitHook, Config) when is_list(Config) -> - case ct_hooks:init_tc(Suite, FuncSpec, Config) of +initialize(RefreshLogs,Mod,Func,[Config]) when is_list(Config) -> + initialize(RefreshLogs,Mod,group_or_func(Func,Config)); +initialize(RefreshLogs,Mod,Func,_) -> + initialize(RefreshLogs,Mod,Func). + +initialize(RefreshLogs,Mod,FuncSpec) -> + ct_logs:init_tc(RefreshLogs), + ct_event:notify(#event{name=tc_start, + node=node(), + data={Mod,FuncSpec}}). + + +ct_suite_init(Suite,HookFunc,PostInitHook,Config) when is_list(Config) -> + case ct_hooks:init_tc(Suite,HookFunc,Config) of NewConfig when is_list(NewConfig) -> - PostInitHookResult = do_post_init_hook(PostInitHook, NewConfig), + PostInitHookResult = do_post_init_hook(PostInitHook,NewConfig), {ok, [PostInitHookResult ++ NewConfig]}; Else -> Else end. -do_post_init_hook(PostInitHook, Config) -> +do_post_init_hook(PostInitHook,Config) -> lists:flatmap(fun({Tag,Fun}) -> case lists:keysearch(Tag,1,Config) of {value,_} -> @@ -287,10 +312,10 @@ add_defaults(Mod,Func, GroupPath) -> end; {'EXIT',Reason} -> ErrStr = io_lib:format("~n*** ERROR *** " - "~w:suite/0 failed: ~p~n", + "~w:suite/0 failed: ~tp~n", [Suite,Reason]), io:format(ErrStr, []), - io:format(user, ErrStr, []), + io:format(?def_gl, ErrStr, []), {suite0_failed,{exited,Reason}}; SuiteInfo when is_list(SuiteInfo) -> case lists:all(fun(E) when is_tuple(E) -> true; @@ -310,18 +335,18 @@ add_defaults(Mod,Func, GroupPath) -> false -> ErrStr = io_lib:format("~n*** ERROR *** " "Invalid return value from " - "~w:suite/0: ~p~n", + "~w:suite/0: ~tp~n", [Suite,SuiteInfo]), io:format(ErrStr, []), - io:format(user, ErrStr, []), + io:format(?def_gl, ErrStr, []), {suite0_failed,bad_return_value} end; SuiteInfo -> ErrStr = io_lib:format("~n*** ERROR *** " "Invalid return value from " - "~w:suite/0: ~p~n", [Suite,SuiteInfo]), + "~w:suite/0: ~tp~n", [Suite,SuiteInfo]), io:format(ErrStr, []), - io:format(user, ErrStr, []), + io:format(?def_gl, ErrStr, []), {suite0_failed,bad_return_value} end. @@ -346,10 +371,10 @@ add_defaults1(Mod,Func, GroupPath, SuiteInfo) -> {value,{error,BadGr0Val,GrName}} -> Gr0ErrStr = io_lib:format("~n*** ERROR *** " "Invalid return value from " - "~w:group(~w): ~p~n", + "~w:group(~tw): ~tp~n", [Mod,GrName,BadGr0Val]), io:format(Gr0ErrStr, []), - io:format(user, Gr0ErrStr, []), + io:format(?def_gl, Gr0ErrStr, []), {group0_failed,bad_return_value}; _ -> Args = if Func == init_per_group ; Func == end_per_group -> @@ -368,10 +393,10 @@ add_defaults1(Mod,Func, GroupPath, SuiteInfo) -> {error,BadTC0Val} -> TC0ErrStr = io_lib:format("~n*** ERROR *** " "Invalid return value from " - "~w:~w/0: ~p~n", + "~w:~tw/0: ~tp~n", [Mod,Func,BadTC0Val]), io:format(TC0ErrStr, []), - io:format(user, TC0ErrStr, []), + io:format(?def_gl, TC0ErrStr, []), {testcase0_failed,bad_return_value}; _ -> %% let test case info (also for all config funcs) override @@ -615,10 +640,10 @@ try_set_default(Name,Key,Info,Where) -> {_,[]} -> no_default; {'_UNDEF',_} -> - [ct_config:set_default_config([CfgVal],Where) || CfgVal <- CfgElems], + _ = [ct_config:set_default_config([CfgVal],Where) || CfgVal <- CfgElems], ok; _ -> - [ct_config:set_default_config(Name,[CfgVal],Where) || CfgVal <- CfgElems], + _ = [ct_config:set_default_config(Name,[CfgVal],Where) || CfgVal <- CfgElems], ok end. @@ -657,9 +682,36 @@ end_tc(Mod,Func,{TCPid,Result,[Args]}, Return) when is_pid(TCPid) -> end_tc(Mod,Func,{Result,[Args]}, Return) -> end_tc(Mod,Func,self(),Result,Args,Return). -end_tc(Mod,Func,TCPid,Result,Args,Return) -> +end_tc(Mod,IPTC={init_per_testcase,_Func},_TCPid,Result,Args,Return) -> + case end_hook_func(IPTC,Return,IPTC) of + undefined -> ok; + _ -> + %% in case Mod == ct_framework, lookup the suite name + Suite = get_suite_name(Mod, Args), + case ct_hooks:end_tc(Suite,IPTC,Args,Result,Return) of + '$ct_no_change' -> + ok; + HookResult -> + HookResult + end + end; + +end_tc(Mod,Func0,TCPid,Result,Args,Return) -> %% in case Mod == ct_framework, lookup the suite name Suite = get_suite_name(Mod, Args), + {Func,FuncSpec,HookFunc} = + case Func0 of + {end_per_testcase_not_run,F} -> + %% Testcase is completed (skipped or failed), but + %% end_per_testcase is not run - don't call post-hook. + {F,F,undefined}; + {end_per_testcase,F} -> + {F,F,Func0}; + _ -> + FS = group_or_func(Func0,Args), + HF = end_hook_func(Func0,Return,FS), + {Func0,FS,HF} + end, test_server:timetrap_cancel(), @@ -686,16 +738,18 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) -> end, ct_util:delete_suite_data(last_saved_config), - FuncSpec = group_or_func(Func,Args), - {Result1,FinalNotify} = - case ct_hooks:end_tc( - Suite, FuncSpec, Args, Result, Return) of - '$ct_no_change' -> - {ok,Result}; - HookResult -> - {HookResult,HookResult} - end, + case HookFunc of + undefined -> + {ok,Result}; + _ -> + case ct_hooks:end_tc(Suite,HookFunc,Args,Result,Return) of + '$ct_no_change' -> + {ok,Result}; + HookResult -> + {HookResult,HookResult} + end + end, FinalResult = case get('$test_server_framework_test') of undefined -> @@ -786,6 +840,34 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) -> end, FinalResult. +%% This is to make sure that no post_init_per_* is ever called if the +%% corresponding pre_init_per_* was not called. +%% The skip or fail reasons are those that can be returned from +%% init_tc above in situations where we never came to call +%% ct_hooks:init_tc/3, e.g. if suite/0 fails, then we never call +%% ct_hooks:init_tc for init_per_suite, and thus we must not call +%% ct_hooks:end_tc for init_per_suite either. +end_hook_func({init_per_testcase,_},{auto_skip,{sequence_failed,_,_}},_) -> + undefined; +end_hook_func({init_per_testcase,_},{auto_skip,"Repeated test stopped by force_stop option"},_) -> + undefined; +end_hook_func({init_per_testcase,_},{fail,{config_name_already_in_use,_}},_) -> + undefined; +end_hook_func({init_per_testcase,_},{auto_skip,{InfoFuncError,_}},_) + when InfoFuncError==testcase0_failed; + InfoFuncError==require_failed -> + undefined; +end_hook_func(init_per_group,{auto_skip,{InfoFuncError,_}},_) + when InfoFuncError==group0_failed; + InfoFuncError==require_failed -> + undefined; +end_hook_func(init_per_suite,{auto_skip,{require_failed_in_suite0,_}},_) -> + undefined; +end_hook_func(init_per_suite,{auto_skip,{failed,{error,{suite0_failed,_}}}},_) -> + undefined; +end_hook_func(_,_,Default) -> + Default. + %% {error,Reason} | {skip,Reason} | {timetrap_timeout,TVal} | %% {testcase_aborted,Reason} | testcase_aborted_or_killed | %% {'EXIT',Reason} | {fail,Reason} | {failed,Reason} | @@ -831,15 +913,15 @@ tag(_Other) -> %%% <code>Func</code> in suite <code>Mod</code> crashing. %%% <code>Error</code> specifies the reason for failing. error_notification(Mod,Func,_Args,{Error,Loc}) -> - ErrSpec = case Error of + ErrorSpec = case Error of {What={_E,_R},Trace} when is_list(Trace) -> What; What -> What end, - ErrStr = case ErrSpec of + ErrorStr = case ErrorSpec of {badmatch,Descr} -> - Descr1 = lists:flatten(io_lib:format("~P",[Descr,10])), + Descr1 = lists:flatten(io_lib:format("~tP",[Descr,10])), if length(Descr1) > 50 -> Descr2 = string:substr(Descr1,1,50), io_lib:format("{badmatch,~ts...}",[Descr2]); @@ -849,17 +931,18 @@ error_notification(Mod,Func,_Args,{Error,Loc}) -> {test_case_failed,Reason} -> case (catch io_lib:format("{test_case_failed,~ts}", [Reason])) of {'EXIT',_} -> - io_lib:format("{test_case_failed,~p}", [Reason]); + io_lib:format("{test_case_failed,~tp}", [Reason]); Result -> Result end; {'EXIT',_Reason} = EXIT -> - io_lib:format("~P", [EXIT,5]); + io_lib:format("~tP", [EXIT,5]); {Spec,_Reason} when is_atom(Spec) -> - io_lib:format("~w", [Spec]); + io_lib:format("~tw", [Spec]); Other -> - io_lib:format("~P", [Other,5]) + io_lib:format("~tP", [Other,5]) end, - ErrorHtml = "<font color=\"brown\">" ++ ErrStr ++ "</font>", + ErrorHtml = + "<font color=\"brown\">" ++ ct_logs:escape_chars(ErrorStr) ++ "</font>", case {Mod,Error} of %% some notifications come from the main test_server process %% and for these cases the existing comment may not be modified @@ -887,41 +970,43 @@ error_notification(Mod,Func,_Args,{Error,Loc}) -> end end, - PrintErr = fun(ErrFormat, ErrArgs) -> + PrintError = fun(ErrorFormat, ErrorArgs) -> Div = "~n- - - - - - - - - - - - - - - - - - - " "- - - - - - - - - - - - - - - - - - - - -~n", - io:format(user, lists:concat([Div,ErrFormat,Div,"~n"]), - ErrArgs), + ErrorStr2 = io_lib:format(ErrorFormat, ErrorArgs), + io:format(?def_gl, lists:concat([Div,ErrorStr2,Div,"~n"]), + []), Link = "\n\n<a href=\"#end\">" "Full error description and stacktrace" "</a>", + ErrorHtml2 = ct_logs:escape_chars(ErrorStr2), ct_logs:tc_log(ct_error_notify, ?MAX_IMPORTANCE, "CT Error Notification", - ErrFormat++Link, ErrArgs) + ErrorHtml2++Link, [], []) end, case Loc of [{?MODULE,error_in_suite}] -> - PrintErr("Error in suite detected: ~ts", [ErrStr]); + PrintError("Error in suite detected: ~ts", [ErrorStr]); R when R == unknown; R == undefined -> - PrintErr("Error detected: ~ts", [ErrStr]); + PrintError("Error detected: ~ts", [ErrorStr]); %% if a function specified by all/0 does not exist, we %% pick up undef here - [{LastMod,LastFunc}|_] when ErrStr == "undef" -> - PrintErr("~w:~w could not be executed~nReason: ~ts", - [LastMod,LastFunc,ErrStr]); + [{LastMod,LastFunc}|_] when ErrorStr == "undef" -> + PrintError("~w:~tw could not be executed~nReason: ~ts", + [LastMod,LastFunc,ErrorStr]); [{LastMod,LastFunc}|_] -> - PrintErr("~w:~w failed~nReason: ~ts", [LastMod,LastFunc,ErrStr]); + PrintError("~w:~tw failed~nReason: ~ts", [LastMod,LastFunc,ErrorStr]); [{LastMod,LastFunc,LastLine}|_] -> %% print error to console, we are only %% interested in the last executed expression - PrintErr("~w:~w failed on line ~w~nReason: ~ts", - [LastMod,LastFunc,LastLine,ErrStr]), + PrintError("~w:~tw failed on line ~w~nReason: ~ts", + [LastMod,LastFunc,LastLine,ErrorStr]), case ct_util:read_suite_data({seq,Mod,Func}) of undefined -> @@ -1093,9 +1178,9 @@ get_all(Mod, ConfTests) -> case ct_util:get_testdata({error_in_suite,Mod}) of undefined -> ErrStr = io_lib:format("~n*** ERROR *** " - "~w:all/0 failed: ~p~n", + "~w:all/0 failed: ~tp~n", [Mod,ExitReason]), - io:format(user, ErrStr, []), + io:format(?def_gl, ErrStr, []), %% save the error info so it doesn't get printed twice ct_util:set_testdata_async({{error_in_suite,Mod}, ExitReason}); @@ -1209,8 +1294,8 @@ save_seq(Mod,Seq,SeqTCs,All) -> check_private(Seq,TCs,All) -> Bad = lists:filter(fun(TC) -> lists:member(TC,All) end, TCs), if Bad /= [] -> - Reason = io_lib:format("regular test cases not allowed in sequence ~p: " - "~p",[Seq,Bad]), + Reason = io_lib:format("regular test cases not allowed in sequence ~tp: " + "~tp",[Seq,Bad]), throw({error,list_to_atom(lists:flatten(Reason))}); true -> ok @@ -1227,7 +1312,7 @@ check_multiple(Mod,Seq,TCs) -> end,TCs), if Bad /= [] -> Reason = io_lib:format("test cases found in multiple sequences: " - "~p",[Bad]), + "~tp",[Bad]), throw({error,list_to_atom(lists:flatten(Reason))}); true -> ok @@ -1255,15 +1340,15 @@ end_per_suite(_Config) -> %% if the group config functions are missing in the suite, %% use these instead 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 " + ct:comment(io_lib:format("start of ~tp", [GroupName])), + ct_logs:log("TEST INFO", "init_per_group/2 for ~tw missing " "in suite, using default.", [GroupName]), Config. 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 " + ct:comment(io_lib:format("end of ~tp", [GroupName])), + ct_logs:log("TEST INFO", "end_per_group/2 for ~tw missing " "in suite, using default.", [GroupName]), ok. @@ -1277,7 +1362,7 @@ report(What,Data) -> %% top level test index page needs to be refreshed TestName = filename:basename(?val(topdir, Data), ".logs"), RunDir = ?val(rundir, Data), - ct_logs:make_all_suites_index({TestName,RunDir}), + _ = ct_logs:make_all_suites_index({TestName,RunDir}), ok; tests_start -> ok; @@ -1301,25 +1386,25 @@ report(What,Data) -> ok; tc_done -> {Suite,{Func,GrName},Result} = Data, - Data1 = if GrName == undefined -> {Suite,Func,Result}; - true -> Data - end, + FuncSpec = if GrName == undefined -> Func; + true -> {Func,GrName} + end, %% Register the group leader for the process calling the report %% function, making it possible for a hook function to print %% in the test case log file ReportingPid = self(), ct_logs:register_groupleader(ReportingPid, group_leader()), case Result of - {failed, _} -> - ct_hooks:on_tc_fail(What, Data1); - {skipped,{failed,{_,init_per_testcase,_}}} -> - ct_hooks:on_tc_skip(tc_auto_skip, Data1); - {skipped,{require_failed,_}} -> - ct_hooks:on_tc_skip(tc_auto_skip, Data1); - {skipped,_} -> - ct_hooks:on_tc_skip(tc_user_skip, Data1); - {auto_skipped,_} -> - ct_hooks:on_tc_skip(tc_auto_skip, Data1); + {failed, Reason} -> + ct_hooks:on_tc_fail(What, {Suite,FuncSpec,Reason}); + {skipped,{failed,{_,init_per_testcase,_}}=Reason} -> + ct_hooks:on_tc_skip(tc_auto_skip, {Suite,FuncSpec,Reason}); + {skipped,{require_failed,_}=Reason} -> + ct_hooks:on_tc_skip(tc_auto_skip, {Suite,FuncSpec,Reason}); + {skipped,Reason} -> + ct_hooks:on_tc_skip(tc_user_skip, {Suite,FuncSpec,Reason}); + {auto_skipped,Reason} -> + ct_hooks:on_tc_skip(tc_auto_skip, {Suite,FuncSpec,Reason}); _Else -> ok end, @@ -1480,3 +1565,8 @@ get_html_wrapper(TestName, PrintLabel, Cwd, TableCols) -> get_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) -> ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding). + +%%%----------------------------------------------------------------- +%%% @spec get_log_dir() -> {ok,LogDir} +get_log_dir() -> + ct_logs:get_log_dir(true). diff --git a/lib/common_test/src/ct_ftp.erl b/lib/common_test/src/ct_ftp.erl index 616b1a8934..8effb06e7e 100644 --- a/lib/common_test/src/ct_ftp.erl +++ b/lib/common_test/src/ct_ftp.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2013. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -119,19 +119,19 @@ open(KeyOrName) -> _ -> case ct:get_config(KeyOrName) of undefined -> - log(heading(open,KeyOrName),"Failed: ~p\n", + log(heading(open,KeyOrName),"Failed: ~tp\n", [{not_available,KeyOrName}]), {error,{not_available,KeyOrName}}; _ -> case ct:get_config({KeyOrName,username}) of undefined -> - log(heading(open,KeyOrName),"Failed: ~p\n", + log(heading(open,KeyOrName),"Failed: ~tp\n", [{not_available,{KeyOrName,username}}]), {error,{not_available,{KeyOrName,username}}}; Username -> case ct:get_config({KeyOrName,password}) of undefined -> - log(heading(open,KeyOrName),"Failed: ~p\n", + log(heading(open,KeyOrName),"Failed: ~tp\n", [{not_available,{KeyOrName,password}}]), {error,{not_available,{KeyOrName,password}}}; Password -> @@ -145,7 +145,7 @@ open(KeyOrName,Username,Password) -> log(heading(open,KeyOrName),"",[]), case ct:get_config({KeyOrName,ftp}) of undefined -> - log(heading(open,KeyOrName),"Failed: ~p\n", + log(heading(open,KeyOrName),"Failed: ~tp\n", [{not_available,{KeyOrName,ftp}}]), {error,{not_available,{KeyOrName,ftp}}}; Addr -> @@ -284,7 +284,7 @@ init(KeyOrName,{IP,Port},{Username,Password}) -> case ftp_connect(IP,Port,Username,Password) of {ok,FtpPid} -> log(heading(init,KeyOrName), - "Opened ftp connection:\nIP: ~p\nUsername: ~p\nPassword: ~p\n", + "Opened ftp connection:\nIP: ~tp\nUsername: ~tp\nPassword: ~p\n", [IP,Username,lists:duplicate(length(Password),$*)]), {ok,FtpPid,#state{ftp_pid=FtpPid,target_name=KeyOrName}}; Error -> @@ -292,7 +292,7 @@ init(KeyOrName,{IP,Port},{Username,Password}) -> end. ftp_connect(IP,Port,Username,Password) -> - inets:start(), + _ = inets:start(), case inets:start(ftpc,[{host,IP},{port,Port}]) of {ok,FtpPid} -> case ftp:user(FtpPid,Username,Password) of @@ -308,28 +308,28 @@ ftp_connect(IP,Port,Username,Password) -> %% @hidden handle_msg({send,LocalFile,RemoteFile},State) -> log(heading(send,State#state.target_name), - "LocalFile: ~p\nRemoteFile: ~p\n",[LocalFile,RemoteFile]), + "LocalFile: ~tp\nRemoteFile: ~tp\n",[LocalFile,RemoteFile]), Result = ftp:send(State#state.ftp_pid,LocalFile,RemoteFile), {Result,State}; handle_msg({recv,RemoteFile,LocalFile},State) -> log(heading(recv,State#state.target_name), - "RemoteFile: ~p\nLocalFile: ~p\n",[RemoteFile,LocalFile]), + "RemoteFile: ~tp\nLocalFile: ~tp\n",[RemoteFile,LocalFile]), Result = ftp:recv(State#state.ftp_pid,RemoteFile,LocalFile), {Result,State}; handle_msg({cd,Dir},State) -> - log(heading(cd,State#state.target_name),"Dir: ~p\n",[Dir]), + log(heading(cd,State#state.target_name),"Dir: ~tp\n",[Dir]), Result = ftp:cd(State#state.ftp_pid,Dir), {Result,State}; handle_msg({ls,Dir},State) -> - log(heading(ls,State#state.target_name),"Dir: ~p\n",[Dir]), + log(heading(ls,State#state.target_name),"Dir: ~tp\n",[Dir]), Result = ftp:ls(State#state.ftp_pid,Dir), {Result,State}; handle_msg({type,Type},State) -> - log(heading(type,State#state.target_name),"Type: ~p\n",[Type]), + log(heading(type,State#state.target_name),"Type: ~tp\n",[Type]), Result = ftp:type(State#state.ftp_pid,Type), {Result,State}; handle_msg({delete,File},State) -> - log(heading(delete,State#state.target_name),"Delete file: ~p\n",[File]), + log(heading(delete,State#state.target_name),"Delete file: ~tp\n",[File]), Result = ftp:delete(State#state.ftp_pid,File), {Result,State}. @@ -368,7 +368,7 @@ call(Pid,Msg) -> heading(Function,Name) -> - io_lib:format("ct_ftp:~w ~p",[Function,Name]). + io_lib:format("ct_ftp:~tw ~tp",[Function,Name]). log(Heading,Str,Args) -> ct_gen_conn:log(Heading,Str,Args). diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl index e46fd77383..456bfd8bd1 100644 --- a/lib/common_test/src/ct_gen_conn.erl +++ b/lib/common_test/src/ct_gen_conn.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2014. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -27,14 +27,7 @@ -export([start/4, stop/1, get_conn_pid/1, check_opts/1]). -export([call/2, call/3, return/2, do_within_time/2]). --export([log/3, start_log/1, cont_log/2, end_log/0]). - -%%---------------------------------------------------------------------- -%% Exported types -%%---------------------------------------------------------------------- --export_type([server_id/0, - target_name/0, - key_or_name/0]). +-export([log/3, start_log/1, cont_log/2, cont_log_no_timestamp/2, end_log/0]). -ifdef(debug). -define(dbg,true). @@ -54,18 +47,6 @@ cb_state, ct_util_server}). -%%------------------------------------------------------------------ -%% Type declarations -%%------------------------------------------------------------------ --type server_id() :: atom(). -%% A `ServerId' which exists in a configuration file. --type target_name() :: atom(). -%% A name which is associated to a `server_id()' via a -%% `require' statement or a call to {@link ct:require/2} in the -%% test suite. --type key_or_name() :: server_id() | target_name(). - - %%%----------------------------------------------------------------- %%% @spec start(Address,InitData,CallbackMod,Opts) -> %%% {ok,Handle} | {error,Reason} @@ -175,6 +156,14 @@ cont_log(Format,Args) -> log(cont_log,[Format,Args]). %%%----------------------------------------------------------------- +%%% @spec cont_log_no_timestamp(Format,Args) -> ok +%%% +%%% @doc Log activities on the current connection (tool-internal use only). +%%% @see ct_logs:cont_log/2 +cont_log_no_timestamp(Format,Args) -> + log(cont_log_no_timestamp,[Format,Args]). + +%%%----------------------------------------------------------------- %%% @spec end_log() -> ok %%% %%% @doc Log activities on the current connection (tool-internal use only). @@ -197,9 +186,11 @@ end_log() -> do_within_time(Fun,Timeout) -> Self = self(), Silent = get(silent), - TmpPid = spawn_link(fun() -> put(silent,Silent), - R = Fun(), - Self ! {self(),R} + TmpPid = spawn_link(fun() -> + ct_util:mark_process(), + put(silent,Silent), + R = Fun(), + Self ! {self(),R} end), ConnPid = get(conn_pid), receive @@ -258,7 +249,7 @@ do_start(Opts) -> Error; {'DOWN',MRef,process,_,Reason} -> log("ct_gen_conn:start", - "Connection process died: ~p\n", + "Connection process died: ~tp\n", [Reason]), {error,{connection_process_died,Reason}} end. @@ -312,6 +303,7 @@ return({To,Ref},Result) -> init_gen(Parent,Opts) -> process_flag(trap_exit,true), + ct_util:mark_process(), put(silent,false), try (Opts#gen_opts.callback):init(Opts#gen_opts.name, Opts#gen_opts.address, @@ -338,7 +330,7 @@ loop(Opts) -> case Opts#gen_opts.reconnect of true -> log("Connection down!\nOpening new!", - "Reason: ~p\nAddress: ~p\n", + "Reason: ~tp\nAddress: ~tp\n", [Reason,Opts#gen_opts.address]), case reconnect(Opts) of {ok, NewPid, NewState} -> @@ -349,12 +341,12 @@ loop(Opts) -> Error -> ct_util:unregister_connection(self()), log("Reconnect failed. Giving up!", - "Reason: ~p\n", + "Reason: ~tp\n", [Error]) end; false -> ct_util:unregister_connection(self()), - log("Connection closed!","Reason: ~p\n",[Reason]) + log("Connection closed!","Reason: ~tp\n",[Reason]) end; {'EXIT',Pid,Reason} -> case Opts#gen_opts.ct_util_server of @@ -365,8 +357,9 @@ loop(Opts) -> end; {stop, From} -> ct_util:unregister_connection(self()), - (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid, - Opts#gen_opts.cb_state), + ConnPid = Opts#gen_opts.conn_pid, + unlink(ConnPid), + (Opts#gen_opts.callback):terminate(ConnPid,Opts#gen_opts.cb_state), return(From,ok), ok; {{retry,{Error,_Name,CPid,_Msg}}, From} when @@ -403,8 +396,9 @@ loop(Opts) -> loop(Opts#gen_opts{cb_state=NewState}); {stop,Reply,NewState} -> ct_util:unregister_connection(self()), - (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid, - NewState), + ConnPid = Opts#gen_opts.conn_pid, + unlink(ConnPid), + (Opts#gen_opts.callback):terminate(ConnPid,NewState), return(From,Reply) end; Msg when Opts#gen_opts.forward==true -> @@ -414,8 +408,9 @@ loop(Opts) -> loop(Opts#gen_opts{cb_state=NewState}); {stop,NewState} -> ct_util:unregister_connection(self()), - (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid, - NewState) + ConnPid = Opts#gen_opts.conn_pid, + unlink(ConnPid), + (Opts#gen_opts.callback):terminate(ConnPid,NewState) end end. diff --git a/lib/common_test/src/ct_groups.erl b/lib/common_test/src/ct_groups.erl index 7636f15f59..2235365a0e 100644 --- a/lib/common_test/src/ct_groups.erl +++ b/lib/common_test/src/ct_groups.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2013. All Rights Reserved. +%% Copyright Ericsson AB 2004-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ find(Mod, all, all, [{Name,Props,Tests} | Gs], Known, Defs, _) find(Mod, all, TCs, [{Name,Props,Tests} | Gs], Known, Defs, _) when is_atom(Name), is_list(Props), is_list(Tests) -> cyclic_test(Mod, Name, Known), - Tests1 = rm_unwanted_tcs(Tests, TCs, []), + Tests1 = modify_tc_list(Tests, TCs, []), trim(make_conf(Mod, Name, Props, find(Mod, all, TCs, Tests1, [Name | Known], Defs, true))) ++ @@ -91,7 +91,7 @@ find(Mod, all, TCs, [{Name,Props,Tests} | Gs], Known, Defs, _) find(Mod, [Name|GrNames]=SPath, TCs, [{Name,Props,Tests} | Gs], Known, Defs, FindAll) when is_atom(Name), is_list(Props), is_list(Tests) -> cyclic_test(Mod, Name, Known), - Tests1 = rm_unwanted_tcs(Tests, TCs, GrNames), + Tests1 = modify_tc_list(Tests, TCs, GrNames), trim(make_conf(Mod, Name, Props, find(Mod, GrNames, TCs, Tests1, [Name|Known], Defs, FindAll))) ++ @@ -133,7 +133,7 @@ find(_Mod, [_|_], _TCs, [], _Known, _Defs, _) -> find(Mod, GrNames, TCs, [{Name,Props,Tests} | Gs], Known, Defs, FindAll) when is_atom(Name), is_list(Props), is_list(Tests) -> cyclic_test(Mod, Name, Known), - Tests1 = rm_unwanted_tcs(Tests, TCs, GrNames), + Tests1 = modify_tc_list(Tests, TCs, GrNames), trim(make_conf(Mod, Name, Props, find(Mod, GrNames, TCs, Tests1, [Name|Known], Defs, FindAll))) ++ @@ -210,7 +210,7 @@ find(Mod, _GrNames, _TCs, [BadTerm | _Gs], Known, _Defs, _FindAll) -> "group "++atom_to_list(lists:last(Known))++ " in "++atom_to_list(Mod)++":groups/0" end, - Term = io_lib:format("~p", [BadTerm]), + Term = io_lib:format("~tp", [BadTerm]), E = "Bad term "++lists:flatten(Term)++" in "++Where, throw({error,list_to_atom(E)}); @@ -284,70 +284,57 @@ trim_test(Test) -> %% GrNames is [] if the terminating group has been found. From %% that point, all specified test should be included (as well as %% sub groups for deeper search). -rm_unwanted_tcs(Tests, all, []) -> - Tests; - -rm_unwanted_tcs(Tests, TCs, []) -> - sort_tests(lists:flatmap(fun(Test) when is_tuple(Test), - (size(Test) > 2) -> - [Test]; - (Test={group,_}) -> - [Test]; - (Test={_M,TC}) -> - case lists:member(TC, TCs) of - true -> [Test]; - false -> [] - end; - (Test) when is_atom(Test) -> - case lists:keysearch(Test, 2, TCs) of - {value,_} -> - [Test]; - _ -> - case lists:member(Test, TCs) of - true -> [Test]; - false -> [] - end - end; - (Test) -> [Test] - end, Tests), TCs); - -rm_unwanted_tcs(Tests, _TCs, _) -> - [Test || Test <- Tests, not is_atom(Test)]. - -%% make sure the order of tests is according to the order in TCs -sort_tests(Tests, TCs) when is_list(TCs)-> - lists:sort(fun(T1, T2) -> - case {is_tc(T1),is_tc(T2)} of - {true,true} -> - (position(T1, TCs) =< - position(T2, TCs)); - {false,true} -> - (position(T2, TCs) == (length(TCs)+1)); - _ -> true - - end - end, Tests); -sort_tests(Tests, _) -> - Tests. - -is_tc(T) when is_atom(T) -> true; -is_tc({group,_}) -> false; -is_tc({_M,T}) when is_atom(T) -> true; -is_tc(_) -> false. - -position(T, TCs) -> - position(T, TCs, 1). - -position(T, [T|_TCs], Pos) -> - Pos; -position(T, [{_,T}|_TCs], Pos) -> - Pos; -position({M,T}, [T|_TCs], Pos) when M /= group -> - Pos; -position(T, [_|TCs], Pos) -> - position(T, TCs, Pos+1); -position(_, [], Pos) -> - Pos. +modify_tc_list(GrSpecTs, all, []) -> + GrSpecTs; + +modify_tc_list(GrSpecTs, TSCs, []) -> + modify_tc_list1(GrSpecTs, TSCs); + +modify_tc_list(GrSpecTs, _TSCs, _) -> + [Test || Test <- GrSpecTs, not is_atom(Test)]. + +modify_tc_list1(GrSpecTs, TSCs) -> + %% remove all cases in group tc list that should not be executed + GrSpecTs1 = + lists:flatmap(fun(Test) when is_tuple(Test), + (size(Test) > 2) -> + [Test]; + (Test={group,_}) -> + [Test]; + (Test={_M,TC}) -> + case lists:member(TC, TSCs) of + true -> [Test]; + false -> [] + end; + (Test) when is_atom(Test) -> + case lists:keysearch(Test, 2, TSCs) of + {value,_} -> + [Test]; + _ -> + case lists:member(Test, TSCs) of + true -> [Test]; + false -> [] + end + end; + (Test) -> [Test] + end, GrSpecTs), + {TSCs2,GrSpecTs3} = + lists:foldr( + fun(TC, {TSCs1,GrSpecTs2}) -> + case lists:member(TC,GrSpecTs1) of + true -> + {[TC|TSCs1],lists:delete(TC,GrSpecTs2)}; + false -> + case lists:keysearch(TC, 2, GrSpecTs) of + {value,Test} -> + {[Test|TSCs1], + lists:keydelete(TC, 2, GrSpecTs2)}; + false -> + {TSCs1,GrSpecTs2} + end + end + end, {[],GrSpecTs1}, TSCs), + TSCs2 ++ GrSpecTs3. %%%----------------------------------------------------------------- @@ -415,12 +402,7 @@ expand(Mod, Name, Defs) -> end. make_all_conf(Dir, Mod, Props, TestSpec) -> - case code:is_loaded(Mod) of - false -> - code:load_abs(filename:join(Dir,atom_to_list(Mod))); - _ -> - ok - end, + _ = load_abs(Dir, Mod), make_all_conf(Mod, Props, TestSpec). make_all_conf(Mod, Props, TestSpec) -> @@ -441,33 +423,40 @@ make_all_conf(Mod, Props, TestSpec) -> end. make_conf(Dir, Mod, Name, Props, TestSpec) -> + _ = load_abs(Dir, Mod), + make_conf(Mod, Name, Props, TestSpec). + +load_abs(Dir, Mod) -> case code:is_loaded(Mod) of false -> code:load_abs(filename:join(Dir,atom_to_list(Mod))); _ -> ok - end, - make_conf(Mod, Name, Props, TestSpec). + end. make_conf(Mod, Name, Props, TestSpec) -> - case code:is_loaded(Mod) of + _ = case code:is_loaded(Mod) of false -> code:load_file(Mod); _ -> ok end, {InitConf,EndConf,ExtraProps} = - case erlang:function_exported(Mod,init_per_group,2) of - true -> - {{Mod,init_per_group},{Mod,end_per_group},[]}; - false -> + case {erlang:function_exported(Mod,init_per_group,2), + erlang:function_exported(Mod,end_per_group,2)} of + {false,false} -> ct_logs:log("TEST INFO", "init_per_group/2 and " "end_per_group/2 missing for group " - "~w in ~w, using default.", + "~tw in ~w, using default.", [Name,Mod]), {{ct_framework,init_per_group}, {ct_framework,end_per_group}, - [{suite,Mod}]} + [{suite,Mod}]}; + _ -> + %% If any of these exist, the other should too + %% (required and documented). If it isn't, it will fail + %% with reason 'undef'. + {{Mod,init_per_group},{Mod,end_per_group},[]} end, {conf,[{name,Name}|Props++ExtraProps],InitConf,TestSpec,EndConf}. diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl index 86d18696dc..f0592a40be 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2004-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -67,6 +67,8 @@ terminate(Hooks) -> %% tests. -spec init_tc(Mod :: atom(), FuncSpec :: atom() | + {ConfigFunc :: init_per_testcase | end_per_testcase, + TestCase :: atom()} | {ConfigFunc :: init_per_group | end_per_group, GroupName :: atom(), Properties :: list()}, @@ -90,16 +92,24 @@ init_tc(Mod, end_per_suite, Config) -> call(fun call_generic/3, Config, [pre_end_per_suite, Mod]); init_tc(Mod, {init_per_group, GroupName, Properties}, Config) -> maybe_start_locker(Mod, GroupName, Properties), - call(fun call_generic/3, Config, [pre_init_per_group, GroupName]); -init_tc(_Mod, {end_per_group, GroupName, _}, Config) -> - call(fun call_generic/3, Config, [pre_end_per_group, GroupName]); -init_tc(_Mod, TC, Config) -> - call(fun call_generic/3, Config, [pre_init_per_testcase, TC]). + call(fun call_generic_fallback/3, Config, + [pre_init_per_group, Mod, GroupName]); +init_tc(Mod, {end_per_group, GroupName, _}, Config) -> + call(fun call_generic_fallback/3, Config, + [pre_end_per_group, Mod, GroupName]); +init_tc(Mod, {init_per_testcase,TC}, Config) -> + call(fun call_generic_fallback/3, Config, [pre_init_per_testcase, Mod, TC]); +init_tc(Mod, {end_per_testcase,TC}, Config) -> + call(fun call_generic_fallback/3, Config, [pre_end_per_testcase, Mod, TC]); +init_tc(Mod, TC = error_in_suite, Config) -> + call(fun call_generic_fallback/3, Config, [pre_init_per_testcase, Mod, TC]). %% @doc Called as each test case is completed. This includes all configuration %% tests. -spec end_tc(Mod :: atom(), - FuncSpec :: atom() | + FuncSpec :: atom() | + {ConfigFunc :: init_per_testcase | end_per_testcase, + TestCase :: atom()} | {ConfigFunc :: init_per_group | end_per_group, GroupName :: atom(), Properties :: list()}, @@ -118,17 +128,24 @@ end_tc(Mod, init_per_suite, Config, _Result, Return) -> 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, {init_per_group, GroupName, _}, Config, _Result, Return) -> + call(fun call_generic_fallback/3, Return, + [post_init_per_group, Mod, GroupName, Config], '$ct_no_change'); end_tc(Mod, {end_per_group, GroupName, Properties}, Config, Result, _Return) -> - Res = call(fun call_generic/3, Result, - [post_end_per_group, GroupName, Config], '$ct_no_change'), + Res = call(fun call_generic_fallback/3, Result, + [post_end_per_group, Mod, GroupName, Config], '$ct_no_change'), maybe_stop_locker(Mod, GroupName, Properties), Res; -end_tc(_Mod, TC, Config, Result, _Return) -> - call(fun call_generic/3, Result, [post_end_per_testcase, TC, Config], - '$ct_no_change'). +end_tc(Mod, {init_per_testcase,TC}, Config, Result, _Return) -> + call(fun call_generic_fallback/3, Result, + [post_init_per_testcase, Mod, TC, Config], '$ct_no_change'); +end_tc(Mod, {end_per_testcase,TC}, Config, Result, _Return) -> + call(fun call_generic_fallback/3, Result, + [post_end_per_testcase, Mod, TC, Config], '$ct_no_change'); +end_tc(Mod, TC = error_in_suite, Config, Result, _Return) -> + call(fun call_generic_fallback/3, Result, + [post_end_per_testcase, Mod, TC, Config], '$ct_no_change'). + %% Case = TestCase | {TestCase,GroupName} on_tc_skip(How, {Suite, Case, Reason}) -> @@ -166,15 +183,21 @@ call_terminate(#ct_hook_config{ module = Mod, state = State} = Hook, _, _) -> {[],Hook}. call_cleanup(#ct_hook_config{ module = Mod, state = State} = Hook, - Reason, [Function, _Suite | Args]) -> + Reason, [Function | Args]) -> NewState = catch_apply(Mod,Function, Args ++ [Reason, State], - State), + State, true), {Reason, Hook#ct_hook_config{ state = NewState } }. -call_generic(#ct_hook_config{ module = Mod, state = State} = Hook, - Value, [Function | Args]) -> +call_generic(Hook, Value, Meta) -> + do_call_generic(Hook, Value, Meta, false). + +call_generic_fallback(Hook, Value, Meta) -> + do_call_generic(Hook, Value, Meta, true). + +do_call_generic(#ct_hook_config{ module = Mod, state = State} = Hook, + Value, [Function | Args], Fallback) -> {NewValue, NewState} = catch_apply(Mod, Function, Args ++ [Value, State], - {Value,State}), + {Value,State}, Fallback), {NewValue, Hook#ct_hook_config{ state = NewState } }. %% Generic call function @@ -212,7 +235,7 @@ call([{Hook, call_id, NextFun} | Rest], Config, Meta, Hooks) -> 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", + ct_logs:log("Suite Hook","Failed to start a CTH: ~tp:~tp", [Error,{Reason,Trace}]), call([], {fail,"Failed to start CTH" ", see the CT Log for details"}, Meta, Hooks) @@ -242,13 +265,15 @@ remove(Key,List) when is_list(List) -> remove(_, Else) -> Else. -%% Translate scopes, i.e. init_per_group,group1 -> end_per_group,group1 etc -scope([pre_init_per_testcase, TC|_]) -> - [post_end_per_testcase, TC]; -scope([pre_init_per_group, GroupName|_]) -> - [post_end_per_group, GroupName]; -scope([post_init_per_group, GroupName|_]) -> - [post_end_per_group, GroupName]; +%% Translate scopes, i.e. is_tuplenit_per_group,group1 -> end_per_group,group1 etc +scope([pre_init_per_testcase, SuiteName, TC|_]) -> + [post_init_per_testcase, SuiteName, TC]; +scope([pre_end_per_testcase, SuiteName, TC|_]) -> + [post_end_per_testcase, SuiteName, TC]; +scope([pre_init_per_group, SuiteName, GroupName|_]) -> + [post_end_per_group, SuiteName, GroupName]; +scope([post_init_per_group, SuiteName, GroupName|_]) -> + [post_end_per_group, SuiteName, GroupName]; scope([pre_init_per_suite, SuiteName|_]) -> [post_end_per_suite, SuiteName]; scope([post_init_per_suite, SuiteName|_]) -> @@ -256,14 +281,29 @@ scope([post_init_per_suite, SuiteName|_]) -> scope(init) -> none. -terminate_if_scope_ends(HookId, [on_tc_skip,_Suite,{end_per_group,Name}], +strip_config([post_init_per_testcase, SuiteName, TC|_]) -> + [post_init_per_testcase, SuiteName, TC]; +strip_config([post_end_per_testcase, SuiteName, TC|_]) -> + [post_end_per_testcase, SuiteName, TC]; +strip_config([post_init_per_group, SuiteName, GroupName|_]) -> + [post_init_per_group, SuiteName, GroupName]; +strip_config([post_end_per_group, SuiteName, GroupName|_]) -> + [post_end_per_group, SuiteName, GroupName]; +strip_config([post_init_per_suite, SuiteName|_]) -> + [post_init_per_suite, SuiteName]; +strip_config([post_end_per_suite, SuiteName|_]) -> + [post_end_per_suite, SuiteName]; +strip_config(Other) -> + Other. + + +terminate_if_scope_ends(HookId, [on_tc_skip,Suite,{end_per_group,Name}], Hooks) -> - terminate_if_scope_ends(HookId, [post_end_per_group, Name], Hooks); + terminate_if_scope_ends(HookId, [post_end_per_group, Suite, Name], Hooks); terminate_if_scope_ends(HookId, [on_tc_skip,Suite,end_per_suite], Hooks) -> terminate_if_scope_ends(HookId, [post_end_per_suite, Suite], Hooks); -terminate_if_scope_ends(HookId, [Function,Tag|T], Hooks) when T =/= [] -> - terminate_if_scope_ends(HookId,[Function,Tag],Hooks); -terminate_if_scope_ends(HookId, Function, Hooks) -> +terminate_if_scope_ends(HookId, Function0, Hooks) -> + Function = strip_config(Function0), case lists:keyfind(HookId, #ct_hook_config.id, Hooks) of #ct_hook_config{ id = HookId, scope = Function} = Hook -> terminate([Hook]), @@ -317,7 +357,8 @@ get_hooks() -> %% 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; +resort(Calls,Hooks,[F|_R]) when F == pre_end_per_testcase; + F == post_end_per_testcase; F == pre_end_per_group; F == post_end_per_group; F == pre_end_per_suite; @@ -366,21 +407,29 @@ pos(Id,[_|Rest],Num) -> catch_apply(M,F,A, Default) -> + catch_apply(M,F,A,Default,false). +catch_apply(M,F,A, Default, Fallback) -> + not erlang:module_loaded(M) andalso (catch M:module_info()), + case erlang:function_exported(M,F,length(A)) of + false when Fallback -> + catch_apply(M,F,tl(A),Default,false); + false -> + Default; + true -> + catch_apply(M,F,A) + end. + +catch_apply(M,F,A) -> try - apply(M,F,A) + erlang:apply(M,F,A) catch _: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 -> - Default; - Trace -> - ct_logs:log("Suite Hook","Call to CTH failed: ~w:~p", - [error,{Reason,Trace}]), - throw({error_in_cth_call, - lists:flatten( - io_lib:format("~w:~w/~w CTH call failed", - [M,F,length(A)]))}) - end + Trace = erlang:get_stacktrace(), + ct_logs:log("Suite Hook","Call to CTH failed: ~w:~tp", + [error,{Reason,Trace}]), + throw({error_in_cth_call, + lists:flatten( + io_lib:format("~w:~tw/~w CTH call failed", + [M,F,length(A)]))}) end. @@ -390,7 +439,8 @@ catch_apply(M,F,A, Default) -> maybe_start_locker(Mod,GroupName,Opts) -> case lists:member(parallel,Opts) of true -> - {ok, _Pid} = ct_hooks_lock:start({Mod,GroupName}); + {ok, _Pid} = ct_hooks_lock:start({Mod,GroupName}), + ok; false -> ok end. diff --git a/lib/common_test/src/ct_hooks_lock.erl b/lib/common_test/src/ct_hooks_lock.erl index 1a058aa8ca..a82be288e1 100644 --- a/lib/common_test/src/ct_hooks_lock.erl +++ b/lib/common_test/src/ct_hooks_lock.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2011. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -78,11 +78,12 @@ release() -> %% @doc Initiates the server init(Id) -> + ct_util:mark_process(), {ok, #state{ id = Id }}. %% @doc Handling call messages handle_call({stop,Id}, _From, #state{ id = Id, requests = Reqs } = State) -> - [gen_server:reply(Req, locker_stopped) || {Req,_ReqId} <- Reqs], + _ = [gen_server:reply(Req, locker_stopped) || {Req,_ReqId} <- Reqs], {stop, normal, stopped, State}; handle_call({stop,_Id}, _From, State) -> {reply, stopped, State}; diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl index 78081380e7..fb6a095b57 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-2014. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -32,20 +32,23 @@ -export([init/2, close/2, init_tc/1, end_tc/1]). -export([register_groupleader/2, unregister_groupleader/1]). -export([get_log_dir/0, get_log_dir/1]). --export([log/3, start_log/1, cont_log/2, end_log/0]). +-export([log/3, start_log/1, cont_log/2, cont_log_no_timestamp/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/5]). +-export([get_ts_html_wrapper/5, escape_chars/1]). -export([xhtml/2, locate_priv_file/1, make_relative/1]). -export([insert_javascript/1]). -export([uri/1]). +-export([parse_keep_logs/1]). %% Logging stuff directly from testcase --export([tc_log/3, tc_log/4, tc_log/5, tc_log_async/3, tc_log_async/5, - tc_print/3, tc_print/4, - tc_pal/3, tc_pal/4, ct_log/3, basic_html/0]). +-export([tc_log/3, tc_log/4, tc_log/5, tc_log/6, + tc_log_async/3, tc_log_async/5, + tc_print/3, tc_print/4, tc_print/5, + tc_pal/3, tc_pal/4, tc_pal/5, ct_log/3, + basic_html/0]). %% Simulate logger process for use without ct environment running -export([simulate/0]). @@ -135,7 +138,8 @@ close(Info, StartDir) -> %% so we need to use a local copy of the log cache data LogCacheBin = case make_last_run_index() of - {error,_} -> % log server not responding + {error, Reason} -> % log server not responding + io:format("Warning! ct_logs not responding: ~tp~n", [Reason]), undefined; LCB -> LCB @@ -147,7 +151,7 @@ close(Info, StartDir) -> ok; CacheBin -> %% save final version of the log cache to file - file:write_file(?log_cache_name,CacheBin), + _ = file:write_file(?log_cache_name,CacheBin), put(ct_log_cache,undefined) end end, @@ -171,14 +175,14 @@ close(Info, StartDir) -> ok -> ok; Error -> - io:format("Warning! Cleanup failed: ~p~n", [Error]) + io:format("Warning! Cleanup failed: ~tp~n", [Error]) end, - make_all_suites_index(stop), + _ = make_all_suites_index(stop), make_all_runs_index(stop), Cache2File(); true -> - file:set_cwd(".."), - make_all_suites_index(stop), + ok = file:set_cwd(".."), + _ = make_all_suites_index(stop), make_all_runs_index(stop), Cache2File(), case ct_util:get_profile_data(browser, StartDir) of @@ -238,7 +242,7 @@ call(Msg) -> Pid -> MRef = erlang:monitor(process,Pid), Ref = make_ref(), - ?MODULE ! {Msg,{self(),Ref}}, + Pid ! {Msg,{self(),Ref}}, receive {Ref, Result} -> erlang:demonitor(MRef, [flush]), @@ -249,16 +253,30 @@ call(Msg) -> end. return({To,Ref},Result) -> - To ! {Ref, Result}. + To ! {Ref, Result}, + ok. cast(Msg) -> case whereis(?MODULE) of undefined -> - {error,does_not_exist}; + io:format("Warning: ct_logs not started~n"), + {_,_,_,_,_,_,Content,_} = Msg, + FormatArgs = get_format_args(Content), + _ = [io:format(Format, Args) || {Format, Args} <- FormatArgs], + ok; _Pid -> - ?MODULE ! Msg + ?MODULE ! Msg, + ok end. +get_format_args(Content) -> + lists:map(fun(C) -> + case C of + {_, FA, _} -> FA; + {_, _} -> C + end + end, Content). + %%%----------------------------------------------------------------- %%% @spec init_tc(RefreshLog) -> ok %%% @@ -267,7 +285,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 />")), + tc_io_format(group_leader(), xhtml("", "<br />"), []), ok. %%%----------------------------------------------------------------- @@ -314,9 +332,10 @@ unregister_groupleader(Pid) -> %%% data to log (as in <code>io:format(Format,Args)</code>).</p> log(Heading,Format,Args) -> cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE, - [{int_header(),[log_timestamp(?now),Heading]}, + [{hd,int_header(),[log_timestamp(?now),Heading]}, {Format,Args}, - {int_footer(),[]}]}), + {ft,int_footer(),[]}], + true}), ok. %%%----------------------------------------------------------------- @@ -336,7 +355,7 @@ log(Heading,Format,Args) -> %%% @see end_log/0 start_log(Heading) -> cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE, - [{int_header(),[log_timestamp(?now),Heading]}]}), + [{hd,int_header(),[log_timestamp(?now),Heading]}],false}), ok. %%%----------------------------------------------------------------- @@ -351,7 +370,21 @@ cont_log([],[]) -> cont_log(Format,Args) -> maybe_log_timestamp(), cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE, - [{Format,Args}]}), + [{Format,Args}],true}), + ok. + +%%%----------------------------------------------------------------- +%%% @spec cont_log_no_timestamp(Format,Args) -> ok +%%% +%%% @doc Adds information about an activity (tool-internal use only). +%%% +%%% @see start_log/1 +%%% @see end_log/0 +cont_log_no_timestamp([],[]) -> + ok; +cont_log_no_timestamp(Format,Args) -> + cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE, + [{Format,Args}],true}), ok. %%%----------------------------------------------------------------- @@ -363,7 +396,7 @@ cont_log(Format,Args) -> %%% @see cont_log/2 end_log() -> cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE, - [{int_footer(), []}]}), + [{ft,int_footer(), []}],false}), ok. @@ -392,7 +425,7 @@ add_external_logs(Logs) -> %%% @doc Print a link to a given file stored in the priv_dir of the %%% calling test suite. add_link(Heading,File,Type) -> - log(Heading,"<a href=\"~ts\" type=~p>~ts</a>\n", + log(Heading,"<a href=\"~ts\" type=~tp>~ts</a>\n", [uri(filename:join("log_private",File)),Type,File]). @@ -400,32 +433,51 @@ add_link(Heading,File,Type) -> %%% @spec tc_log(Category,Format,Args) -> ok %%% @equiv tc_log(Category,?STD_IMPORTANCE,Format,Args) tc_log(Category,Format,Args) -> - tc_log(Category,?STD_IMPORTANCE,Format,Args). + tc_log(Category,?STD_IMPORTANCE,"User",Format,Args,[]). %%%----------------------------------------------------------------- %%% @spec tc_log(Category,Importance,Format,Args) -> ok %%% @equiv tc_log(Category,Importance,"User",Format,Args) tc_log(Category,Importance,Format,Args) -> - tc_log(Category,Importance,"User",Format,Args). + tc_log(Category,Importance,"User",Format,Args,[]). %%%----------------------------------------------------------------- -%%% @spec tc_log(Category,Importance,Printer,Format,Args) -> ok +%%% @spec tc_log(Category,Importance,Format,Args) -> ok +%%% @equiv tc_log(Category,Importance,"User",Format,Args) +tc_log(Category,Importance,Format,Args,Opts) -> + tc_log(Category,Importance,"User",Format,Args,Opts). + +%%%----------------------------------------------------------------- +%%% @spec tc_log(Category,Importance,Heading,Format,Args,Opts) -> ok %%% Category = atom() %%% Importance = integer() -%%% Printer = string() +%%% Heading = string() %%% Format = string() %%% Args = list() +%%% Opts = list() %%% %%% @doc Printout from a testcase. %%% %%% <p>This function is called by <code>ct</code> when logging %%% stuff directly from a testcase (i.e. not from within the CT %%% framework).</p> -tc_log(Category,Importance,Printer,Format,Args) -> - cast({log,sync,self(),group_leader(),Category,Importance, - [{div_header(Category,Printer),[]}, - {Format,Args}, - {div_footer(),[]}]}), +tc_log(Category,Importance,Heading,Format,Args,Opts) -> + Data = + case lists:member(no_css, Opts) of + true -> + [{Format,Args}]; + false -> + Heading1 = + case proplists:get_value(heading, Opts) of + undefined -> Heading; + Str -> Str + end, + [{hd,div_header(Category,Heading1),[]}, + {Format,Args}, + {ft,div_footer(),[]}] + end, + cast({log,sync,self(),group_leader(),Category,Importance,Data, + lists:member(esc_chars, Opts)}), ok. %%%----------------------------------------------------------------- @@ -438,7 +490,7 @@ tc_log_async(Category,Format,Args) -> %%% @spec tc_log_async(Category,Importance,Format,Args) -> ok %%% Category = atom() %%% Importance = integer() -%%% Printer = string() +%%% Heading = string() %%% Format = string() %%% Args = list() %%% @@ -449,30 +501,38 @@ tc_log_async(Category,Format,Args) -> %%% to avoid deadlocks when e.g. the hook that handles SASL printouts %%% prints to the test case log file at the same time test server %%% asks ct_logs for an html wrapper.</p> -tc_log_async(Category,Importance,Printer,Format,Args) -> +tc_log_async(Category,Importance,Heading,Format,Args) -> cast({log,async,self(),group_leader(),Category,Importance, - [{div_header(Category,Printer),[]}, + [{hd,div_header(Category,Heading),[]}, {Format,Args}, - {div_footer(),[]}]}), + {ft,div_footer(),[]}], + true}), ok. %%%----------------------------------------------------------------- %%% @spec tc_print(Category,Format,Args) -%%% @equiv tc_print(Category,?STD_IMPORTANCE,Format,Args) +%%% @equiv tc_print(Category,?STD_IMPORTANCE,Format,Args,[]) tc_print(Category,Format,Args) -> - tc_print(Category,?STD_IMPORTANCE,Format,Args). + tc_print(Category,?STD_IMPORTANCE,Format,Args,[]). + +%%%----------------------------------------------------------------- +%%% @spec tc_print(Category,Importance,Format,Args) +%%% @equiv tc_print(Category,Importance,Format,Args,[]) +tc_print(Category,Importance,Format,Args) -> + tc_print(Category,Importance,Format,Args,[]). %%%----------------------------------------------------------------- -%%% @spec tc_print(Category,Importance,Format,Args) -> ok +%%% @spec tc_print(Category,Importance,Format,Args,Opts) -> ok %%% Category = atom() %%% Importance = integer() %%% Format = string() %%% Args = list() +%%% Opts = list() %%% %%% @doc Console printout from a testcase. %%% %%% <p>This function is called by <code>ct</code> when printing %%% stuff from a testcase on the user console.</p> -tc_print(Category,Importance,Format,Args) -> +tc_print(Category,Importance,Format,Args,Opts) -> VLvl = case ct_util:get_verbosity(Category) of undefined -> ct_util:get_verbosity('$unspecified'); @@ -484,82 +544,94 @@ tc_print(Category,Importance,Format,Args) -> Val end, if Importance >= (100-VLvl) -> - Head = get_heading(Category), - io:format(user, lists:concat([Head,Format,"\n\n"]), Args), + Heading = + case proplists:get_value(heading, Opts) of + undefined -> atom_to_list(Category); + Hd -> Hd + end, + Str = lists:concat([get_header(Heading),Format,"\n\n"]), + try + io:format(?def_gl, Str, Args) + catch + %% default group leader probably not started, or has stopped + _:_ -> io:format(user, Str, Args) + end, ok; true -> ok end. -get_heading(default) -> +get_header("default") -> io_lib:format("\n-----------------------------" "-----------------------\n~s\n", [log_timestamp(?now)]); -get_heading(Category) -> +get_header(Heading) -> io_lib:format("\n-----------------------------" - "-----------------------\n~s ~w\n", - [log_timestamp(?now),Category]). + "-----------------------\n~ts ~s\n", + [Heading,log_timestamp(?now)]). %%%----------------------------------------------------------------- %%% @spec tc_pal(Category,Format,Args) -> ok -%%% @equiv tc_pal(Category,?STD_IMPORTANCE,Format,Args) -> ok +%%% @equiv tc_pal(Category,?STD_IMPORTANCE,Format,Args,[]) -> ok tc_pal(Category,Format,Args) -> - tc_pal(Category,?STD_IMPORTANCE,Format,Args). + tc_pal(Category,?STD_IMPORTANCE,Format,Args,[]). %%%----------------------------------------------------------------- %%% @spec tc_pal(Category,Importance,Format,Args) -> ok +%%% @equiv tc_pal(Category,Importance,Format,Args,[]) -> ok +tc_pal(Category,Importance,Format,Args) -> + tc_pal(Category,Importance,Format,Args,[]). + +%%%----------------------------------------------------------------- +%%% @spec tc_pal(Category,Importance,Format,Args,Opts) -> ok %%% Category = atom() %%% Importance = integer() %%% Format = string() %%% Args = list() +%%% Opts = list() %%% %%% @doc Print and log from a testcase. %%% %%% <p>This function is called by <code>ct</code> when logging %%% stuff directly from a testcase. The info is written both in the %%% log and on the console.</p> -tc_pal(Category,Importance,Format,Args) -> - tc_print(Category,Importance,Format,Args), - cast({log,sync,self(),group_leader(),Category,Importance, - [{div_header(Category),[]}, - {Format,Args}, - {div_footer(),[]}]}), - ok. - +tc_pal(Category,Importance,Format,Args,Opts) -> + tc_print(Category,Importance,Format,Args,Opts), + tc_log(Category,Importance,"User",Format,Args,[esc_chars|Opts]). %%%----------------------------------------------------------------- -%%% @spec ct_pal(Category,Format,Args) -> ok +%%% @spec ct_log(Category,Format,Args) -> ok %%% Category = atom() %%% Format = string() %%% Args = list() %%% -%%% @doc Print and log to the ct framework log +%%% @doc Print 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),[]}, + cast({ct_log,[{hd,div_header(Category),[]}, {Format,Args}, - {div_footer(),[]}]}), + {ft,div_footer(),[]}], + true}), ok. %%%================================================================= %%% Internal functions int_header() -> - "<div class=\"ct_internal\"><b>*** CT ~s *** ~ts</b>". + "</pre>\n<div class=\"ct_internal\"><pre><b>*** CT ~s *** ~ts</b>". int_footer() -> - "</div>". + "</pre></div>\n<pre>". div_header(Class) -> div_header(Class,"User"). -div_header(Class,Printer) -> - "\n<div class=\"" ++ atom_to_list(Class) ++ "\"><b>*** " ++ Printer ++ - " " ++ log_timestamp(?now) ++ " ***</b>". +div_header(Class,Heading) -> + "\n</pre>\n<div class=\"" ++ atom_to_list(Class) ++ "\"><pre><b>*** " + ++ Heading ++ " " ++ log_timestamp(?now) ++ " ***</b>". div_footer() -> - "</div>". - + "</pre></div>\n<pre>". maybe_log_timestamp() -> {MS,S,US} = ?now, @@ -568,7 +640,7 @@ maybe_log_timestamp() -> ok; _ -> cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE, - [{"<i>~s</i>",[log_timestamp({MS,S,US})]}]}) + [{hd,"<i>~s</i>",[log_timestamp({MS,S,US})]}],false}) end. log_timestamp({MS,S,US}) -> @@ -589,10 +661,12 @@ log_timestamp({MS,S,US}) -> ct_log_fd, tc_groupleaders, stylesheet, - async_print_jobs}). + async_print_jobs, + tc_esc_chars}). logger(Parent, Mode, Verbosity) -> register(?MODULE,self()), + ct_util:mark_process(), %%! Below is a temporary workaround for the limitation of %%! max one test run per second. %%! ---> @@ -610,7 +684,7 @@ logger(Parent, Mode, Verbosity) -> end, %%! <--- - file:make_dir(Dir), + _ = file:make_dir(Dir), AbsDir = ?abs(Dir), put(ct_run_dir, AbsDir), @@ -630,18 +704,18 @@ logger(Parent, Mode, Verbosity) -> PrivFilesDestRun = [filename:join(AbsDir, F) || F <- PrivFiles], case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of {error,Src1,Dest1,Reason1} -> - io:format(user, "ERROR! "++ - "Priv file ~p could not be copied to ~p. "++ - "Reason: ~p~n", + io:format(?def_gl, "ERROR! "++ + "Priv file ~tp could not be copied to ~tp. "++ + "Reason: ~tp~n", [Src1,Dest1,Reason1]), exit({priv_file_error,Dest1}); ok -> case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of {error,Src2,Dest2,Reason2} -> - io:format(user, + io:format(?def_gl, "ERROR! "++ - "Priv file ~p could not be copied to ~p. " - ++"Reason: ~p~n", + "Priv file ~tp could not be copied to ~tp. " + ++"Reason: ~tp~n", [Src2,Dest2,Reason2]), exit({priv_file_error,Dest2}); ok -> @@ -650,7 +724,7 @@ logger(Parent, Mode, Verbosity) -> end end, - test_server_io:start_link(), + _ = test_server_io:start_link(), MiscIoName = filename:join(Dir, ?misc_io_log), {ok,MiscIoFd} = file:open(MiscIoName, [write,{encoding,utf8}]), @@ -680,13 +754,13 @@ logger(Parent, Mode, Verbosity) -> ct_event:notify(#event{name=start_logging,node=node(), data=AbsDir}), make_all_runs_index(start), - make_all_suites_index(start), + _ = make_all_suites_index(start), case Mode of interactive -> interactive_link(); _ -> ok end, - file:set_cwd(Dir), - make_last_run_index(Time), + ok = file:set_cwd(Dir), + _ = make_last_run_index(Time), CtLogFd = open_ctlog(?misc_io_log), io:format(CtLogFd,int_header()++int_footer(), [log_timestamp(?now),"Common Test Logger started"]), @@ -700,22 +774,26 @@ logger(Parent, Mode, Verbosity) -> GenLvl -> io:format(CtLogFd, "~-25s~3w~n", ["general level",GenLvl]) end, - [begin put({verbosity,Cat},VLvl), - if Cat == '$unspecified' -> + _ = [begin put({verbosity,Cat},VLvl), + if Cat == '$unspecified' -> ok; - true -> + true -> io:format(CtLogFd, "~-25w~3w~n", [Cat,VLvl]) - end - end || {Cat,VLvl} <- Verbosity], + end + end || {Cat,VLvl} <- Verbosity], io:nl(CtLogFd), - + TcEscChars = case application:get_env(common_test, esc_chars) of + {ok,ECBool} -> ECBool; + _ -> true + end, logger_loop(#logger_state{parent=Parent, log_dir=AbsDir, start_time=Time, orig_GL=group_leader(), ct_log_fd=CtLogFd, tc_groupleaders=[], - async_print_jobs=[]}). + async_print_jobs=[], + tc_esc_chars=TcEscChars}). copy_priv_files([SrcF | SrcFs], [DestF | DestFs]) -> case file:copy(SrcF, DestF) of @@ -729,7 +807,7 @@ copy_priv_files([], []) -> logger_loop(State) -> receive - {log,SyncOrAsync,Pid,GL,Category,Importance,List} -> + {log,SyncOrAsync,Pid,GL,Category,Importance,Content,EscChars} -> VLvl = case Category of ct_internal -> ?MAX_VERBOSITY; @@ -741,20 +819,21 @@ logger_loop(State) -> end, if Importance >= (100-VLvl) -> CtLogFd = State#logger_state.ct_log_fd, + DoEscChars = State#logger_state.tc_esc_chars and EscChars, case get_groupleader(Pid, GL, State) of {tc_log,TCGL,TCGLs} -> case erlang:is_process_alive(TCGL) of true -> State1 = print_to_log(SyncOrAsync, Pid, - Category, - TCGL, List, State), + Category, TCGL, Content, + DoEscChars, State), logger_loop(State1#logger_state{ tc_groupleaders = TCGLs}); false -> %% Group leader is dead, so write to the %% CtLog or unexpected_io log instead - unexpected_io(Pid,Category,Importance, - List,CtLogFd), + unexpected_io(Pid, Category, Importance, + Content, CtLogFd, DoEscChars), logger_loop(State) end; @@ -762,7 +841,8 @@ logger_loop(State) -> %% If category is ct_internal then write %% to ct_log, else write to unexpected_io %% log - unexpected_io(Pid,Category,Importance,List,CtLogFd), + unexpected_io(Pid, Category, Importance, Content, + CtLogFd, DoEscChars), logger_loop(State#logger_state{ tc_groupleaders = TCGLs}) end; @@ -773,10 +853,11 @@ logger_loop(State) -> %% make sure no IO for this test case from the %% CT logger gets rejected test_server:permit_io(GL, self()), - print_style(GL, State#logger_state.stylesheet), + IoFormat = fun tc_io_format/3, + print_style(GL, IoFormat, State#logger_state.stylesheet), set_evmgr_gl(GL), TCGLs = add_tc_gl(TCPid,GL,State), - if not RefreshLog -> + _ = if not RefreshLog -> ok; true -> make_last_run_index(State#logger_state.start_time) @@ -803,7 +884,7 @@ logger_loop(State) -> 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), + _ = make_last_run_index(State#logger_state.start_time), return(From,get(ct_log_cache)), logger_loop(State); {set_stylesheet,_,SSFile} when State#logger_state.stylesheet == @@ -811,17 +892,24 @@ logger_loop(State) -> logger_loop(State); {set_stylesheet,TC,SSFile} -> Fd = State#logger_state.ct_log_fd, - io:format(Fd, "~p loading external style sheet: ~ts~n", + io:format(Fd, "~tp loading external style sheet: ~ts~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}); - {ct_log, List} -> + {ct_log,Content,EscChars} -> + Str = lists:map(fun({_HdOrFt,Str,Args}) -> + [io_lib:format(Str,Args),io_lib:nl()]; + ({Str,Args}) when EscChars -> + Io = io_lib:format(Str,Args), + [escape_chars(Io),io_lib:nl()]; + ({Str,Args}) -> + [io_lib:format(Str,Args),io_lib:nl()] + end, Content), Fd = State#logger_state.ct_log_fd, - [begin io:format(Fd,Str,Args),io:nl(Fd) end || - {Str,Args} <- List], + io:format(Fd, "~ts", [Str]), logger_loop(State); {'DOWN',Ref,_,_Pid,_} -> %% there might be print jobs executing in parallel with ct_logs @@ -843,46 +931,81 @@ logger_loop(State) -> ok end. -create_io_fun(FromPid, CtLogFd) -> +create_io_fun(FromPid, CtLogFd, EscChars) -> %% 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({Str,Args}, IoList) -> - case catch io_lib:format(Str,Args) of - {'EXIT',_Reason} -> - io:format(CtLogFd, "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}), - []; + fun(FormatData, IoList) -> + {Escapable,Str,Args} = + case FormatData of + {_HdOrFt,S,A} -> {false,S,A}; + {S,A} -> {true,S,A} + end, + try io_lib:format(Str, Args) of + IoStr when Escapable, EscChars, IoList == [] -> + escape_chars(IoStr); + IoStr when Escapable, EscChars -> + [IoList,"\n",escape_chars(IoStr)]; IoStr when IoList == [] -> - [IoStr]; + IoStr; IoStr -> [IoList,"\n",IoStr] + catch + _:_Reason -> + io:format(CtLogFd, "Logging fails! Str: ~tp, Args: ~tp~n", + [Str,Args]), + %% stop the testcase, we need to see the fault + exit(FromPid, {log_printout_error,Str,Args}), + [] end end. -print_to_log(sync, FromPid, Category, TCGL, List, State) -> +escape_chars([Bin | Io]) when is_binary(Bin) -> + [Bin | escape_chars(Io)]; +escape_chars([List | Io]) when is_list(List) -> + [escape_chars(List) | escape_chars(Io)]; +escape_chars([$< | Io]) -> + ["<" | escape_chars(Io)]; +escape_chars([$> | Io]) -> + [">" | escape_chars(Io)]; +escape_chars([$& | Io]) -> + ["&" | escape_chars(Io)]; +escape_chars([Char | Io]) when is_integer(Char) -> + [Char | escape_chars(Io)]; +escape_chars([]) -> + []; +escape_chars(Bin) -> + Bin. + +print_to_log(sync, FromPid, Category, TCGL, Content, EscChars, State) -> %% in some situations (exceptions), the printout is made from the %% test server IO process and there's no valid group leader to send to CtLogFd = State#logger_state.ct_log_fd, if FromPid /= TCGL -> - IoFun = create_io_fun(FromPid, CtLogFd), - io:format(TCGL,"~ts", [lists:foldl(IoFun, [], List)]); + IoFun = create_io_fun(FromPid, CtLogFd, EscChars), + IoList = lists:foldl(IoFun, [], Content), + try tc_io_format(TCGL, "~ts", [IoList]) of + ok -> ok + catch + _:_ -> + io:format(TCGL,"~ts", [IoList]) + end; true -> - unexpected_io(FromPid,Category,?MAX_IMPORTANCE,List,CtLogFd) + unexpected_io(FromPid, Category, ?MAX_IMPORTANCE, Content, + CtLogFd, EscChars) end, State; -print_to_log(async, FromPid, Category, TCGL, List, State) -> +print_to_log(async, FromPid, Category, TCGL, Content, EscChars, State) -> %% in some situations (exceptions), the printout is made from the %% test server IO process and there's no valid group leader to send to CtLogFd = State#logger_state.ct_log_fd, Printer = if FromPid /= TCGL -> - IoFun = create_io_fun(FromPid, CtLogFd), + IoFun = create_io_fun(FromPid, CtLogFd, EscChars), fun() -> + ct_util:mark_process(), test_server:permit_io(TCGL, self()), %% Since asynchronous io gets can get buffered if @@ -894,25 +1017,29 @@ print_to_log(async, FromPid, Category, TCGL, List, State) -> case erlang:is_process_alive(TCGL) of true -> - try io:format(TCGL, "~ts", - [lists:foldl(IoFun,[],List)]) of + try tc_io_format(TCGL, "~ts", + [lists:foldl(IoFun,[],Content)]) of _ -> ok catch _:terminated -> unexpected_io(FromPid, Category, ?MAX_IMPORTANCE, - List, CtLogFd) + Content, CtLogFd, EscChars); + _:_ -> + io:format(TCGL, "~ts", + [lists:foldl(IoFun,[],Content)]) end; false -> unexpected_io(FromPid, Category, ?MAX_IMPORTANCE, - List, CtLogFd) + Content, CtLogFd, EscChars) end end; true -> fun() -> + ct_util:mark_process(), unexpected_io(FromPid, Category, ?MAX_IMPORTANCE, - List, CtLogFd) + Content, CtLogFd, EscChars) end end, case State#logger_state.async_print_jobs of @@ -1027,7 +1154,7 @@ open_ctlog(MiscIoName) -> Dir = filename:dirname(Cwd), Variables = ct_run:variables_file_name(Dir), io:format(Fd, - "Can not read the file \'~ts\' Reason: ~w\n" + "Can not read the file \'~ts\' Reason: ~tw\n" "No configuration found for test!!\n", [Variables,Reason]) end, @@ -1042,26 +1169,25 @@ open_ctlog(MiscIoName) -> "View I/O logged after the test run</a></li>\n</ul>\n", [MiscIoName,MiscIoName]), - print_style(Fd,undefined), + print_style(Fd, fun io:format/3, undefined), io:format(Fd, xhtml("<br><h2>Progress Log</h2>\n<pre>\n", "<br />\n<h4>PROGRESS LOG</h4>\n<pre>\n"), []), Fd. -print_style(Fd,undefined) -> +print_style(Fd, IoFormat, undefined) -> 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", - []); + Style = "<style>\n + div.ct_internal { background:lightgrey; color:black; }\n + div.default { background:lightgreen; color:black; }\n + </style>\n", + IoFormat(Fd, Style, []); _ -> ok end; -print_style(Fd,StyleSheet) -> +print_style(Fd, IoFormat, StyleSheet) -> case file:read_file(StyleSheet) of {ok,Bin} -> Str = b2s(Bin,encoding(StyleSheet)), @@ -1074,34 +1200,54 @@ print_style(Fd,StyleSheet) -> N1 -> N1 end, if (Pos0 == 0) and (Pos1 /= 0) -> - print_style_error(Fd,StyleSheet,missing_style_start_tag); + print_style_error(Fd, IoFormat, + StyleSheet, missing_style_start_tag); (Pos0 /= 0) and (Pos1 == 0) -> - print_style_error(Fd,StyleSheet,missing_style_end_tag); + print_style_error(Fd, IoFormat, + StyleSheet,missing_style_end_tag); Pos0 /= 0 -> Style = string:sub_string(Str,Pos0,Pos1+7), - io:format(Fd,"~ts\n",[Style]); + IoFormat(Fd,"~ts\n",[Style]); Pos0 == 0 -> - io:format(Fd,"<style>~ts</style>\n",[Str]) + IoFormat(Fd,"<style>\n~ts</style>\n",[Str]) end; {error,Reason} -> - print_style_error(Fd,StyleSheet,Reason) + print_style_error(Fd,IoFormat,StyleSheet,Reason) end. -%% Simple link version, doesn't work with all browsers unfortunately. :-( -%% print_style(Fd, StyleSheet) -> -%% io:format(Fd, -%% "<link href=~p rel=\"stylesheet\" type=\"text/css\">", -%% [StyleSheet]). - -print_style_error(Fd,StyleSheet,Reason) -> - io:format(Fd,"\n<!-- Failed to load stylesheet ~ts: ~p -->\n", - [StyleSheet,Reason]), - print_style(Fd,undefined). +print_style_error(Fd, IoFormat, StyleSheet, Reason) -> + IO = io_lib:format("\n<!-- Failed to load stylesheet ~ts: ~tp -->\n", + [StyleSheet,Reason]), + IoFormat(Fd, IO, []), + print_style(Fd, IoFormat, undefined). close_ctlog(Fd) -> io:format(Fd, "\n</pre>\n", []), io:format(Fd, [xhtml("<br><br>\n", "<br /><br />\n") | footer()], []), - file:close(Fd). + ok = file:close(Fd). + +%%%----------------------------------------------------------------- +%%% tc_io_format/3 +%%% Tell common_test's IO server (group leader) not to escape +%%% HTML characters. + +-spec tc_io_format(io:device(), io:format(), [term()]) -> 'ok'. + +tc_io_format(Fd, Format0, Args) -> + %% We know that the specially wrapped format string is handled + %% by our IO server, but Dialyzer does not and would tell us + %% that the call to io:format/3 would fail. Therefore, we must + %% fool dialyzer. + + Format = case cloaked_true() of + true -> ["$tc_html",Format0]; + false -> Format0 %Never happens. + end, + io:format(Fd, Format, Args). + +%% Return 'true', but let dialyzer think that a boolean is returned. +cloaked_true() -> + is_process_alive(self()). %%%----------------------------------------------------------------- @@ -1113,11 +1259,11 @@ make_last_run_index(StartTime) -> case catch make_last_run_index1(StartTime,IndexName) of {'EXIT', Reason} -> io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), - io:format("~p~n", [Reason]), + io:format("~tp~n", [Reason]), {error, Reason}; {error, Reason} -> io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), - io:format("~p~n", [Reason]), + io:format("~tp~n", [Reason]), {error, Reason}; ok -> ok; @@ -1364,11 +1510,11 @@ total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) -> [xhtml("<tr valign=top>\n", ["</tbody>\n<tfoot>\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><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", + "<td align=right><b>",integer_to_list(NotBuilt),"</b></td>\n", AllInfo, "</tr>\n", xhtml("","</tfoot>\n")]. @@ -1418,7 +1564,7 @@ get_missing_suites(_,_) -> []. term_to_text(Term) -> - lists:flatten(io_lib:format("~p.\n", [Term])). + lists:flatten(io_lib:format("~tp.\n", [Term])). %%% Headers and footers. @@ -1560,10 +1706,12 @@ header1(Title, SubTitle, TableCols) -> "<!-- autogenerated by '"++atom_to_list(?MODULE)++"' -->\n", "<head>\n", "<title>" ++ Title ++ " " ++ SubTitle ++ "</title>\n", - "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", - "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n", + "<meta http-equiv=\"content-type\" content=\"text/html; " + "charset=utf-8\"></meta>\n", xhtml("", - ["<link rel=\"stylesheet\" href=\"",uri(CSSFile),"\" type=\"text/css\">\n"]), + ["<link rel=\"stylesheet\" href=\"",uri(CSSFile), + "\" type=\"text/css\"></link>\n"]), xhtml("", ["<script type=\"text/javascript\" src=\"",JQueryFile, "\"></script>\n"]), @@ -1610,7 +1758,7 @@ footer() -> "Copyright © ", year(), " <a href=\"http://www.erlang.org\">Open Telecom Platform</a>", xhtml("<br>\n", "<br />\n"), - "Updated: <!date>", current_time(), "<!/date>", + "Updated: <!--date-->", current_time(), "<!--/date-->", xhtml("<br>\n", "<br />\n"), xhtml("</font></p>\n", "</div>\n"), "</center>\n" @@ -1680,11 +1828,11 @@ count_cases(Dir) -> %% file yet. {0,0,0,0}; Summary -> - write_summary(SumFile, Summary), + _ = write_summary(SumFile, Summary), Summary end; {error, Reason} -> - io:format("\nFailed to read ~p: ~p (skipped)\n", + io:format("\nFailed to read ~tp: ~tp (skipped)\n", [LogFile,Reason]), error end @@ -1766,10 +1914,10 @@ config_table_header() -> config_table1([{Key,Value}|Vars]) -> [xhtml(["<tr><td>", atom_to_list(Key), "</td>\n", - "<td><pre>",io_lib:format("~p",[Value]),"</pre></td></tr>\n"], + "<td><pre>",io_lib:format("~tp",[Value]),"</pre></td></tr>\n"], ["<tr class=\"", odd_or_even(), "\">\n", "<td>", atom_to_list(Key), "</td>\n", - "<td>", io_lib:format("~p",[Value]), "</td>\n</tr>\n"]) | + "<td>", io_lib:format("~tp",[Value]), "</td>\n</tr>\n"]) | config_table1(Vars)]; config_table1([]) -> [xhtml("","</tbody>\n"),"</table>\n"]. @@ -1780,7 +1928,7 @@ make_all_runs_index(When) -> AbsName = ?abs(?all_runs_name), notify_and_lock_file(AbsName), if When == start -> ok; - true -> io:put_chars("Updating " ++ AbsName ++ "... ") + true -> io:put_chars("Updating " ++ AbsName ++ " ... ") end, %% check if log cache should be used, and if it exists @@ -1802,7 +1950,11 @@ make_all_runs_index(When) -> end, Dirs = filelib:wildcard(logdir_prefix()++"*.*"), - DirsSorted = (catch sort_all_runs(Dirs)), + DirsSorted0 = (catch sort_all_runs(Dirs)), + DirsSorted = + if When == start -> DirsSorted0; + true -> maybe_delete_old_dirs(DirsSorted0) + end, LogCacheInfo = get_cache_data(UseCache), @@ -1920,6 +2072,36 @@ sort_ct_runs(Dirs) -> {DateHH1,MM1,SS1} =< {DateHH2,MM2,SS2} end, Dirs). +parse_keep_logs([Str="all"]) -> + parse_keep_logs(list_to_atom(Str)); +parse_keep_logs([NStr]) -> + parse_keep_logs(list_to_integer(NStr)); +parse_keep_logs(all) -> + all; +parse_keep_logs(N) when is_integer(N), N>0 -> + N. + +maybe_delete_old_dirs(Sorted) -> + {Keep,Delete} = + case application:get_env(common_test, keep_logs) of + {ok,MaxN} when is_integer(MaxN), length(Sorted)>MaxN -> + lists:split(MaxN,Sorted); + _ -> + {Sorted,[]} + end, + delete_old_dirs(Delete), + Keep. + +delete_old_dirs([]) -> + ok; +delete_old_dirs(Dirs) -> + io:put_chars("\n Removing old test directories:\n"), + [begin + io:put_chars(" " ++ Dir ++ "\n"), + rm_dir(Dir) + end|| Dir <- Dirs], + ok. + dir_diff_all_runs(Dirs, LogCache) -> case LogCache#log_cache.all_runs of [] -> @@ -1985,9 +2167,9 @@ interactive_link() -> "<!-- autogenerated by '"++atom_to_list(?MODULE)++"' -->\n", "<head>\n", "<title>Last interactive run</title>\n", - "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n", "<meta http-equiv=\"content-type\" content=\"text/html; " - "charset=utf-8\">\n", + "charset=utf-8\"></meta>\n", "</head>\n", "<body>\n", "Log from last interactive run: <a href=\"",uri(CtLog),"\">", @@ -1995,7 +2177,7 @@ interactive_link() -> "</body>\n", "</html>\n" ], - file:write_file("last_interactive.html",unicode:characters_to_binary(Body)), + _ = file:write_file("last_interactive.html",unicode:characters_to_binary(Body)), io:format("~n~nUpdated ~ts\n" "Any CT activities will be logged here\n", [?abs("last_interactive.html")]). @@ -2126,9 +2308,9 @@ runentry(Dir, _, _) -> write_totals_file(Name,Label,Logs,Totals) -> AbsName = ?abs(Name), notify_and_lock_file(AbsName), - force_write_file(AbsName, - term_to_binary({atom_to_list(node()), - Label,Logs,Totals})), + _ = force_write_file(AbsName, + term_to_binary({atom_to_list(node()), + Label,Logs,Totals})), notify_and_unlock_file(AbsName). %% this function needs to convert from old formats to new so that old @@ -2173,7 +2355,7 @@ read_totals_file(Name) -> Result. force_write_file(Name,Contents) -> - force_delete(Name), + _ = force_delete(Name), file:write_file(Name,Contents). force_delete(Name) -> @@ -2295,17 +2477,17 @@ make_all_suites_index(NewTestData = {_TestName,DirName}) -> LogDirData) of {'EXIT',Reason} -> io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), - io:format("~p~n", [Reason]), + io:format("~tp~n", [Reason]), {error,Reason}; {error,Reason} -> io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), - io:format("~p~n", [Reason]), + io:format("~tp~n", [Reason]), {error,Reason}; ok -> ok; Err -> io:format("Unknown internal error while updating ~ts. " - "Please report.\n(Err: ~p, ID: 1)", + "Please report.\n(Err: ~tp, ID: 1)", [AbsIndexName,Err]), {error, Err} end, @@ -2519,16 +2701,16 @@ update_tests_in_cache(TempData,LogCache=#log_cache{tests=Tests}) -> make_all_suites_index1(When, AbsIndexName, AllTestLogDirs) -> IndexName = ?index_name, if When == start -> ok; - true -> io:put_chars("Updating " ++ AbsIndexName ++ "... ") + true -> io:put_chars("Updating " ++ AbsIndexName ++ " ... ") end, case catch make_all_suites_index2(IndexName, AllTestLogDirs) of {'EXIT', Reason} -> io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), - io:format("~p~n", [Reason]), + io:format("~tp~n", [Reason]), {error, Reason}; {error, Reason} -> io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), - io:format("~p~n", [Reason]), + io:format("~tp~n", [Reason]), {error, Reason}; {ok,TempData} -> case When of @@ -2542,7 +2724,7 @@ make_all_suites_index1(When, AbsIndexName, AllTestLogDirs) -> end; Err -> io:format("Unknown internal error while updating ~ts. " - "Please report.\n(Err: ~p, ID: 1)", + "Please report.\n(Err: ~tp, ID: 1)", [AbsIndexName,Err]), {error, Err} end. @@ -2724,18 +2906,18 @@ get_cache_data({ok,CacheBin}) -> true -> {ok,CacheRec}; false -> - file:delete(?log_cache_name), + _ = file:delete(?log_cache_name), {error,old_cache_file} end; _ -> - file:delete(?log_cache_name), + _ = file:delete(?log_cache_name), {error,invalid_cache_file} end; get_cache_data(NoCache) -> NoCache. cache_vsn() -> - application:load(common_test), + _ = application:load(common_test), case application:get_key(common_test,vsn) of {ok,VSN} -> VSN; @@ -2838,6 +3020,7 @@ simulate() -> S = self(), Pid = spawn(fun() -> register(?MODULE,self()), + ct_util:mark_process(), S ! {self(),started}, simulate_logger_loop() end), @@ -2846,8 +3029,12 @@ simulate() -> simulate_logger_loop() -> receive - {log,_,_,_,_,_,List} -> - S = [[io_lib:format(Str,Args),io_lib:nl()] || {Str,Args} <- List], + {log,_,_,_,_,_,Content,_} -> + S = lists:map(fun({_,Str,Args}) -> + [io_lib:format(Str,Args),io_lib:nl()]; + ({Str,Args}) -> + [io_lib:format(Str,Args),io_lib:nl()] + end, Content), io:format("~ts",[S]), simulate_logger_loop(); stop -> @@ -2961,8 +3148,8 @@ locate_priv_file(FileName) -> filename:join(get(ct_run_dir), FileName); _ -> %% executed on other process than ct_logs - {ok,RunDir} = get_log_dir(true), - filename:join(RunDir, FileName) + {ok,LogDir} = get_log_dir(true), + filename:join(LogDir, FileName) end, case filelib:is_file(PrivResultFile) of true -> @@ -3017,7 +3204,7 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) -> TestName1 = if is_list(TestName) -> lists:flatten(TestName); true -> - lists:flatten(io_lib:format("~p", [TestName])) + lists:flatten(io_lib:format("~tp", [TestName])) end, Basic = basic_html(), LabelStr = @@ -3044,6 +3231,10 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) -> ?all_runs_name), Cwd), TestIndex = make_relative(filename:join(filename:dirname(CtLogdir), ?index_name), Cwd), + LatestTest = make_relative(filename:join(filename:dirname(CtLogdir), + ?suitelog_name++".latest.html"), + Cwd), + case Basic of true -> TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"), @@ -3053,15 +3244,15 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) -> "Copyright © ", year(), " <a href=\"http://www.erlang.org\">", "Open Telecom Platform</a><br>\n", - "Updated: <!date>", current_time(), "<!/date>", + "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", + "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n", "<meta http-equiv=\"content-type\" content=\"text/html; charset=", - html_encoding(Encoding),"\">\n", + html_encoding(Encoding),"\"></meta>\n", "</head>\n", "<body", Bgr, " bgcolor=\"white\" text=\"black\" ", "link=\"blue\" vlink=\"purple\" alink=\"red\">\n", @@ -3070,7 +3261,9 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) -> "<a href=\"", uri(AllRuns), "\">Test run history\n</a> | ", "<a href=\"", uri(TestIndex), - "\">Top level test index\n</a>\n</p>\n", + "\">Top level test index\n</a> | ", + "<a href=\"", uri(LatestTest), + "\">Latest test result</a>\n</p>\n", Copyright,"</center>\n</body>\n</html>\n"]}; _ -> Copyright = @@ -3078,7 +3271,7 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) -> "Copyright © ", year(), " <a href=\"http://www.erlang.org\">", "Open Telecom Platform</a><br />\n", - "Updated: <!date>", current_time(), "<!/date>", + "Updated: <!--date-->", current_time(), "<!--/date-->", "<br />\n</div>\n"], CSSFile = xhtml(fun() -> "" end, @@ -3105,9 +3298,11 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) -> "\"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", - "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n", - "<link rel=\"stylesheet\" href=\"", uri(CSSFile), "\" type=\"text/css\">\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n", + "<meta http-equiv=\"content-type\" content=\"text/html; ", + "charset=utf-8\"></meta>\n", + "<link rel=\"stylesheet\" href=\"", uri(CSSFile), + "\" type=\"text/css\"></link>\n", "<script type=\"text/javascript\" src=\"", JQueryFile, "\"></script>\n", "<script type=\"text/javascript\" src=\"", TableSorterFile, "\"></script>\n"] ++ TableSorterScript ++ ["</head>\n","<body>\n", LabelStr, "\n"], @@ -3115,7 +3310,9 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) -> "<a href=\"", uri(AllRuns), "\">Test run history\n</a> | ", "<a href=\"", uri(TestIndex), - "\">Top level test index\n</a>\n</p>\n", + "\">Top level test index\n</a> | ", + "<a href=\"", uri(LatestTest), + "\">Latest test result</a>\n</p>\n", Copyright,"</center>\n</body>\n</html>\n"]} end. @@ -3233,11 +3430,11 @@ html_encoding(latin1) -> html_encoding(utf8) -> "utf-8". -unexpected_io(Pid,ct_internal,_Importance,List,CtLogFd) -> - IoFun = create_io_fun(Pid,CtLogFd), - io:format(CtLogFd, "~ts", [lists:foldl(IoFun, [], List)]); -unexpected_io(Pid,_Category,_Importance,List,CtLogFd) -> - IoFun = create_io_fun(Pid,CtLogFd), - Data = io_lib:format("~ts", [lists:foldl(IoFun, [], List)]), +unexpected_io(Pid, ct_internal, _Importance, Content, CtLogFd, EscChars) -> + IoFun = create_io_fun(Pid, CtLogFd, EscChars), + io:format(CtLogFd, "~ts", [lists:foldl(IoFun, [], Content)]); +unexpected_io(Pid, _Category, _Importance, Content, CtLogFd, EscChars) -> + IoFun = create_io_fun(Pid, CtLogFd, EscChars), + Data = io_lib:format("~ts", [lists:foldl(IoFun, [], Content)]), test_server_io:print_unexpected(Data), ok. diff --git a/lib/common_test/src/ct_make.erl b/lib/common_test/src/ct_make.erl index f4b81a0ef6..220cb0473d 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2009-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -96,7 +96,7 @@ read_emakefile(Emakefile,Opts) -> Mods = [filename:rootname(F) || F <- filelib:wildcard("*.erl")], [{Mods, Opts}]; {error,Other} -> - io:format("make: Trouble reading 'Emakefile':~n~p~n",[Other]), + io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]), error end. @@ -151,7 +151,7 @@ get_opts_from_emakefile(Mods,Emakefile,Opts) -> {error,enoent} -> [{Mods, Opts}]; {error,Other} -> - io:format("make: Trouble reading 'Emakefile':~n~p~n",[Other]), + io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]), error end. @@ -280,15 +280,47 @@ recompile(File, NoExec, Load, Opts) -> do_recompile(_File, true, _Load, _Opts) -> out_of_date; -do_recompile(File, false, noload, Opts) -> +do_recompile(File, false, Load, Opts) -> io:format("Recompile: ~ts\n",[File]), - compile:file(File, [report_errors, report_warnings, error_summary |Opts]); -do_recompile(File, false, load, Opts) -> - io:format("Recompile: ~ts\n",[File]), - c:c(File, Opts); -do_recompile(File, false, netload, Opts) -> - io:format("Recompile: ~ts\n",[File]), - c:nc(File, Opts). + case compile:file(File, [report_errors, report_warnings |Opts]) of + Ok when is_tuple(Ok), element(1,Ok)==ok -> + maybe_load(element(2,Ok), Load, Opts); + _Error -> + error + end. + +maybe_load(_Mod, noload, _Opts) -> + ok; +maybe_load(Mod, Load, Opts) -> + %% We have compiled File with options Opts. Find out where the + %% output file went to, and load it. + case compile:output_generated(Opts) of + true -> + Dir = proplists:get_value(outdir,Opts,"."), + do_load(Dir, Mod, Load); + false -> + io:format("** Warning: No object file created - nothing loaded **~n"), + ok + end. + +do_load(Dir, Mod, load) -> + code:purge(Mod), + case code:load_abs(filename:join(Dir, Mod),Mod) of + {module,Mod} -> + {ok,Mod}; + Other -> + Other + end; +do_load(Dir, Mod, netload) -> + Obj = atom_to_list(Mod) ++ code:objfile_extension(), + Fname = filename:join(Dir, Obj), + case file:read_file(Fname) of + {ok,Bin} -> + rpc:eval_everywhere(code,load_binary,[Mod,Fname,Bin]), + {ok,Mod}; + Other -> + Other + end. exists(File) -> case file:read_file_info(File) of @@ -324,10 +356,11 @@ check_includes(File, IncludePath, ObjMTime) -> end. check_includes2(Epp, File, ObjMTime) -> + A1 = erl_anno:new(1), case epp:parse_erl_form(Epp) of - {ok, {attribute, 1, file, {File, 1}}} -> + {ok, {attribute, A1, file, {File, A1}}} -> check_includes2(Epp, File, ObjMTime); - {ok, {attribute, 1, file, {IncFile, 1}}} -> + {ok, {attribute, A1, file, {IncFile, A1}}} -> case file:read_file_info(IncFile) of {ok, #file_info{mtime=MTime}} when MTime>ObjMTime -> epp:close(Epp), @@ -341,5 +374,7 @@ check_includes2(Epp, File, ObjMTime) -> epp:close(Epp), false; {error, _Error} -> + check_includes2(Epp, File, ObjMTime); + {warning, _Warning} -> check_includes2(Epp, File, ObjMTime) end. diff --git a/lib/common_test/src/ct_master.erl b/lib/common_test/src/ct_master.erl index 228daf459b..ef2aff69b7 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2006-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ -export([run_on_node/2,run_on_node/3]). -export([run_test/1,run_test/2]). -export([get_event_mgr_ref/0]). --export([basic_html/1]). +-export([basic_html/1,esc_chars/1]). -export([abort/0,abort/1,progress/0]). @@ -317,6 +317,16 @@ basic_html(Bool) -> ok. %%%----------------------------------------------------------------- +%%% @spec esc_chars(Bool) -> ok +%%% Bool = true | false +%%% +%%% @doc If set to false, the ct_master logs will be written without +%%% special characters being escaped in the HTML logs. +esc_chars(Bool) -> + application:set_env(common_test_master, esc_chars, Bool), + ok. + +%%%----------------------------------------------------------------- %%% MASTER, runs on central controlling node. %%%----------------------------------------------------------------- start_master(NodeOptsList) -> @@ -336,6 +346,7 @@ init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs, case whereis(ct_master) of undefined -> register(ct_master,self()), + ct_util:mark_process(), ok; _Pid -> io:format("~nWarning: ct_master already running!~n"), @@ -366,7 +377,7 @@ init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs, end, %% start master event manager and add default handler - ct_master_event:start_link(), + {ok, _} = start_ct_master_event(), ct_master_event:add_handler(), %% add user handlers for master event manager Add = fun({H,Args}) -> @@ -388,6 +399,14 @@ init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs, end, init_master1(Parent,NodeOptsList,InitOptions,LogDirs). +start_ct_master_event() -> + case ct_master_event:start_link() of + {error, {already_started, Pid}} -> + {ok, Pid}; + Else -> + Else + end. + init_master1(Parent,NodeOptsList,InitOptions,LogDirs) -> {Inaccessible,NodeOptsList1,InitOptions1} = init_nodes(NodeOptsList, InitOptions), @@ -416,7 +435,7 @@ init_master1(Parent,NodeOptsList,InitOptions,LogDirs) -> init_master2(Parent,NodeOptsList,LogDirs) -> process_flag(trap_exit,true), Cookie = erlang:get_cookie(), - log(all,"Cookie","~w",[Cookie]), + log(all,"Cookie","~tw",[Cookie]), log(all,"Starting Tests", "Tests starting on: ~p",[[N || {N,_} <- NodeOptsList]]), SpawnAndMon = @@ -436,7 +455,7 @@ master_loop(#state{node_ctrl_pids=[], results=Finished}) -> Str = lists:map(fun({Node,Result}) -> - io_lib:format("~-40.40.*ts~p\n", + io_lib:format("~-40.40.*ts~tp\n", [$_,atom_to_list(Node),Result]) end,lists:reverse(Finished)), log(all,"TEST RESULTS",Str,[]), @@ -470,7 +489,7 @@ master_loop(State=#state{node_ctrl_pids=NodeCtrlPids, Bad end, log(all,"Test Info", - "Test on node ~w failed! Reason: ~p", + "Test on node ~w failed! Reason: ~tp", [Node,Error]), {Locks1,Blocked1} = update_queue(exit,Node,Locks,Blocked), @@ -483,7 +502,7 @@ master_loop(State=#state{node_ctrl_pids=NodeCtrlPids, undefined -> %% ignore (but report) exit from master_logger etc log(all,"Test Info", - "Warning! Process ~w has terminated. Reason: ~p", + "Warning! Process ~w has terminated. Reason: ~tp", [Pid,Reason]), master_loop(State) end; @@ -566,7 +585,7 @@ update_queue(take,Node,From,Lock={Op,Resource},Locks,Blocked) -> %% Blocked: [{{Operation,Resource},Node,WaitingPid},...] case lists:keysearch(Lock,1,Locks) of {value,{_Lock,Owner}} -> % other node has lock - log(html,"Lock Info","Node ~w blocked on ~w by ~w. Resource: ~p", + log(html,"Lock Info","Node ~w blocked on ~w by ~w. Resource: ~tp", [Node,Op,Owner,Resource]), Blocked1 = Blocked ++ [{Lock,Node,From}], {Locks,Blocked1}; @@ -581,7 +600,7 @@ update_queue(release,Node,_From,Lock={Op,Resource},Locks,Blocked) -> case lists:keysearch(Lock,1,Blocked) of {value,E={Lock,SomeNode,WaitingPid}} -> Blocked1 = lists:delete(E,Blocked), - log(html,"Lock Info","Node ~w proceeds with ~w. Resource: ~p", + log(html,"Lock Info","Node ~w proceeds with ~w. Resource: ~tp", [SomeNode,Op,Resource]), reply(ok,WaitingPid), % waiting process may start {Locks1,Blocked1}; @@ -648,7 +667,7 @@ refresh_logs([D|Dirs],Refreshed) -> {ok,Cwd} = file:get_cwd(), case catch ct_run:refresh_logs(D) of {'EXIT',Reason} -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), refresh_logs(Dirs,[{D,{error,Reason}}|Refreshed]); Result -> refresh_logs(Dirs,[{D,Result}|Refreshed]) @@ -660,7 +679,7 @@ refresh_logs([D|Dirs],Refreshed) -> refresh_logs([],Refreshed) -> Str = lists:map(fun({D,Result}) -> - io_lib:format("Refreshing logs in ~p... ~p", + io_lib:format("Refreshing logs in ~tp... ~tp", [D,Result]) end,Refreshed), log(all,"Info",Str,[]). @@ -672,6 +691,7 @@ refresh_logs([],Refreshed) -> init_node_ctrl(MasterPid,Cookie,Opts) -> %% make sure tests proceed even if connection to master is lost process_flag(trap_exit, true), + ct_util:mark_process(), MasterNode = node(MasterPid), group_leader(whereis(user),self()), io:format("~n********** node_ctrl process ~w started on ~w **********~n", @@ -691,10 +711,10 @@ init_node_ctrl(MasterPid,Cookie,Opts) -> end, %% start a local event manager - ct_event:start_link(), + {ok, _} = start_ct_event(), ct_event:add_handler([{master,MasterPid}]), - %% log("Running test with options: ~p~n", [Opts]), + %% log("Running test with options: ~tp~n", [Opts]), Result = case (catch ct:run_test(Opts)) of ok -> finished_ok; Other -> Other @@ -711,6 +731,14 @@ init_node_ctrl(MasterPid,Cookie,Opts) -> "Can't report result!~n~n", [MasterNode]) end. +start_ct_event() -> + case ct_event:start_link() of + {error, {already_started, Pid}} -> + {ok, Pid}; + Else -> + Else + end. + %%%----------------------------------------------------------------- %%% Event handling %%%----------------------------------------------------------------- @@ -768,7 +796,7 @@ reply(Result,To) -> ok. init_nodes(NodeOptions, InitOptions)-> - ping_nodes(NodeOptions), + _ = ping_nodes(NodeOptions), start_nodes(InitOptions), eval_on_nodes(InitOptions), {Inaccessible, NodeOptions1}=ping_nodes(NodeOptions), @@ -802,7 +830,7 @@ start_nodes(InitOptions)-> "with callback ~w~n", [NodeName,Callback]); {error, Reason, _NodeName} -> io:format("Failed to start node ~w with callback ~w! " - "Reason: ~p~n", [NodeName, Callback, Reason]) + "Reason: ~tp~n", [NodeName, Callback, Reason]) end; {true, true}-> io:format("WARNING: Node ~w is alive but has node_start " @@ -831,10 +859,10 @@ eval_on_nodes(InitOptions)-> evaluate(Node, [{M,F,A}|MFAs])-> case rpc:call(Node, M, F, A) of {badrpc,Reason}-> - io:format("WARNING: Failed to call ~w:~w/~w on node ~w " - "due to ~p~n", [M,F,length(A),Node,Reason]); + io:format("WARNING: Failed to call ~w:~tw/~w on node ~w " + "due to ~tp~n", [M,F,length(A),Node,Reason]); Result-> - io:format("Called ~w:~w/~w on node ~w, result: ~p~n", + io:format("Called ~w:~tw/~w on node ~w, result: ~tp~n", [M,F,length(A),Node,Result]) end, evaluate(Node, MFAs); diff --git a/lib/common_test/src/ct_master_event.erl b/lib/common_test/src/ct_master_event.erl index 0d7d220fd0..bd4d1efc92 100644 --- a/lib/common_test/src/ct_master_event.erl +++ b/lib/common_test/src/ct_master_event.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2013. All Rights Reserved. +%% Copyright Ericsson AB 2006-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ stop() -> {error,Reason} -> ct_master_logs:log("Error", "No response from CT Master Event.\n" - "Reason = ~p\n" + "Reason = ~tp\n" "Terminating now!\n",[Reason]), %% communication with event manager fails, kill it catch exit(whereis(?CT_MEVMGR_REF), kill); @@ -116,6 +116,7 @@ sync_notify(Event) -> %% this function is called to initialize the event handler. %%-------------------------------------------------------------------- init(_) -> + ct_util:mark_process(), ct_master_logs:log("CT Master Event Handler started","",[]), {ok,#state{}}. @@ -135,7 +136,7 @@ handle_event(#event{name=start_logging,node=Node,data=RunDir},State) -> handle_event(#event{name=Name,node=Node,data=Data},State) -> print("~n=== ~w ===~n", [?MODULE]), - print("~w on ~w: ~p~n", [Name,Node,Data]), + print("~tw on ~w: ~tp~n", [Name,Node,Data]), {ok,State}. %%-------------------------------------------------------------------- diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl index 2adced42ae..c4bb2cc69f 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2006-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -88,11 +88,12 @@ stop() -> init(Parent,LogDir,Nodes) -> register(?MODULE,self()), + ct_util:mark_process(), Time = calendar:local_time(), RunDir = make_dirname(Time), RunDirAbs = filename:join(LogDir,RunDir), - file:make_dir(RunDirAbs), - write_details_file(RunDirAbs,{node(),Nodes}), + ok = make_dir(RunDirAbs), + _ = write_details_file(RunDirAbs,{node(),Nodes}), case basic_html() of true -> @@ -110,16 +111,16 @@ init(Parent,LogDir,Nodes) -> case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of {error,Src1,Dest1,Reason1} -> io:format(user, "ERROR! "++ - "Priv file ~p could not be copied to ~p. "++ - "Reason: ~p~n", + "Priv file ~tp could not be copied to ~tp. "++ + "Reason: ~tp~n", [Src1,Dest1,Reason1]), exit({priv_file_error,Dest1}); ok -> case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of {error,Src2,Dest2,Reason2} -> io:format(user, "ERROR! "++ - "Priv file ~p could not be copied to ~p. "++ - "Reason: ~p~n", + "Priv file ~tp could not be copied to ~tp. "++ + "Reason: ~tp~n", [Src2,Dest2,Reason2]), exit({priv_file_error,Dest2}); ok -> @@ -128,7 +129,7 @@ init(Parent,LogDir,Nodes) -> end end, - make_all_runs_index(LogDir), + _ = make_all_runs_index(LogDir), CtLogFd = open_ct_master_log(RunDirAbs), NodeStr = lists:flatten(lists:map(fun(N) -> @@ -170,7 +171,7 @@ loop(State) -> case catch io:format(Fd,Str++"\n",Args) of {'EXIT',Reason} -> io:format(Fd, - "Logging fails! Str: ~p, Args: ~p~n", + "Logging fails! Str: ~tp, Args: ~tp~n", [Str,Args]), exit({logging_failed,Reason}), ok; @@ -181,7 +182,7 @@ loop(State) -> lists:foreach(Fun,List), loop(State); {make_all_runs_index,From} -> - make_all_runs_index(State#state.logdir), + _ = make_all_runs_index(State#state.logdir), return(From,State#state.logdir), loop(State); {{nodedir,Node,RunDir},From} -> @@ -189,12 +190,12 @@ loop(State) -> return(From,ok), loop(State); stop -> - make_all_runs_index(State#state.logdir), + _ = make_all_runs_index(State#state.logdir), io:format(State#state.log_fd, int_header()++int_footer(), [log_timestamp(?now),"Finished!"]), - close_ct_master_log(State#state.log_fd), - close_nodedir_index(State#state.nodedir_ix_fd), + _ = close_ct_master_log(State#state.log_fd), + _ = close_nodedir_index(State#state.nodedir_ix_fd), ok end. @@ -238,9 +239,9 @@ config_table1([]) -> ["</tbody>\n</table>\n"]. int_header() -> - "<div class=\"ct_internal\"><b>*** CT MASTER ~s *** ~ts</b>". + "</pre>\n<div class=\"ct_internal\"><pre><b>*** CT MASTER ~s *** ~ts</b>". int_footer() -> - "</div>". + "</pre></div>\n<pre>". %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% NodeDir Index functions %%% @@ -387,11 +388,12 @@ header(Title, TableCols) -> "<!-- autogenerated by '"++atom_to_list(?MODULE)++"' -->\n", "<head>\n", "<title>" ++ Title ++ "</title>\n", - "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", - "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n", + "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n", + "<meta http-equiv=\"content-type\" content=\"text/html; ", + "charset=utf-8\"></meta>\n", xhtml("", ["<link rel=\"stylesheet\" href=\"",ct_logs:uri(CSSFile), - "\" type=\"text/css\">"]), + "\" type=\"text/css\"></link>\n"]), xhtml("", ["<script type=\"text/javascript\" src=\"",JQueryFile, "\"></script>\n"]), @@ -419,7 +421,7 @@ footer() -> "Copyright © ", year(), " <a href=\"http://www.erlang.org\">Open Telecom Platform</a>", xhtml("<br>\n", "<br />\n"), - "Updated: <!date>", current_time(), "<!/date>", + "Updated: <!--date-->", current_time(), "<--!/date-->", xhtml("<br>\n", "<br />\n"), xhtml("</font></p>\n", "</div>\n"), "</center>\n" @@ -495,7 +497,7 @@ make_relative(Dir) -> ct_logs:make_relative(Dir). force_write_file(Name,Contents) -> - force_delete(Name), + _ = force_delete(Name), file:write_file(Name,Contents). force_delete(Name) -> @@ -533,13 +535,34 @@ call(Msg) -> end. return({To,Ref},Result) -> - To ! {Ref, Result}. + To ! {Ref, Result}, + ok. cast(Msg) -> case whereis(?MODULE) of undefined -> - {error,does_not_exist}; + io:format("Warning: ct_master_logs not started~n"), + {_,_,Content} = Msg, + FormatArgs = get_format_args(Content), + _ = [io:format(Format, Args) || {Format, Args} <- FormatArgs], + ok; _Pid -> - ?MODULE ! Msg + ?MODULE ! Msg, + ok end. +get_format_args(Content) -> + lists:map(fun(C) -> + case C of + {_, FA, _} -> FA; + _ -> C + end + end, Content). + +make_dir(Dir) -> + case file:make_dir(Dir) of + {error, eexist} -> + ok; + Else -> + Else + end. diff --git a/lib/common_test/src/ct_master_status.erl b/lib/common_test/src/ct_master_status.erl index 6a3572b261..b27fdd341e 100644 --- a/lib/common_test/src/ct_master_status.erl +++ b/lib/common_test/src/ct_master_status.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2013. All Rights Reserved. +%% Copyright Ericsson AB 2006-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ init(_) -> %% handle_event(#event{name=Name,node=Node,data=Data},State) -> print("~n=== ~w ===~n", [?MODULE]), - print("~w on ~w: ~p~n", [Name,Node,Data]), + print("~tw on ~w: ~tp~n", [Name,Node,Data]), {ok,State}. %%-------------------------------------------------------------------- diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl index 0de7bf03af..2c4b97df20 100644 --- a/lib/common_test/src/ct_netconfc.erl +++ b/lib/common_test/src/ct_netconfc.erl @@ -1,7 +1,7 @@ %%---------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012-2014. All Rights Reserved. +%% Copyright Ericsson AB 2012-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -23,24 +23,15 @@ %% Description: %% This file contains the Netconf client interface %% -%% @author Support +%% Netconf servers can be configured by adding the following statement +%% to a configuration file: %% -%% @doc Netconf client module. +%% {server_id(),options()}. %% -%% <p>The Netconf client is compliant with RFC4741 and RFC4742.</p> +%% The server_id() or an associated ct:target_name() shall then be +%% used in calls to open/2 connect/2. %% -%% <p> For each server to test against, the following entry can be -%% added to a configuration file:</p> -%% -%% <p>`{server_id(),options()}.'</p> -%% -%% <p> The `server_id()' or an associated `target_name()' (see -%% {@link ct}) shall then be used in calls to {@link open/2}.</p> -%% -%% <p>If no configuration exists for a server, a session can still be -%% opened by calling {@link open/2} with all necessary options given -%% in the call. The first argument to {@link open/2} can then be any -%% atom.</p> +%% If no configuration exists for a server, use open/1 and connect/1. %% %% == Logging == %% @@ -49,102 +40,15 @@ %% `ct_conn_log_h'. To use this error handler, add the `cth_conn_log' %% hook in your test suite, e.g. %% -%% ``` %% suite() -> -%% [{ct_hooks, [{cth_conn_log, [{conn_mod(),hook_options()}]}]}]. -%%''' -%% -%% The `conn_mod()' is the name of the common_test module implementing -%% the connection protocol, e.g. `ct_netconfc'. -%% -%% The hook option `log_type' specifies the type of logging: -%% -%% <dl> -%% <dt>`raw'</dt> -%% <dd>The sent and received netconf data is logged to a separate -%% text file as is without any formatting. A link to the file is -%% added to the test case HTML log.</dd> -%% -%% <dt>`pretty'</dt> -%% <dd>The sent and received netconf data is logged to a separate -%% text file with XML data nicely indented. A link to the file is -%% added to the test case HTML log.</dd> -%% -%% <dt>`html (default)'</dt> -%% <dd>The sent and received netconf traffic is pretty printed -%% directly in the test case HTML log.</dd> -%% -%% <dt>`silent'</dt> -%% <dd>Netconf traffic is not logged.</dd> -%% </dl> -%% -%% By default, all netconf traffic is logged in one single log -%% file. However, it is possible to have different connections logged -%% in separate files. To do this, use the hook option `hosts' and -%% list the names of the servers/connections that will be used in the -%% suite. Note that the connections must be named for this to work, -%% i.e. they must be opened with {@link open/2}. -%% -%% The `hosts' option has no effect if `log_type' is set to `html' or -%% `silent'. -%% -%% The hook options can also be specified in a configuration file with -%% the configuration variable `ct_conn_log': -%% -%% ``` -%% {ct_conn_log,[{conn_mod(),hook_options()}]}. -%% ''' +%% [{ct_hooks, [{cth_conn_log, [{ct:conn_log_mod(),ct:conn_log_options()}]}]}]. %% %% For example: %% -%% ``` -%% {ct_conn_log,[{ct_netconfc,[{log_type,pretty}, -%% {hosts,[key_or_name()]}]}]} -%% ''' -%% -%% <b>Note</b> that hook options specified in a configuration file -%% will overwrite the hardcoded hook options in the test suite. -%% -%% === Logging example 1 === -%% -%% The following `ct_hooks' statement will cause pretty printing of -%% netconf traffic to separate logs for the connections named -%% `nc_server1' and `nc_server2'. Any other connections will be logged -%% to default netconf log. -%% -%% ``` %% suite() -> -%% [{ct_hooks, [{cth_conn_log, [{ct_netconfc,[{log_type,pretty}}, -%% {hosts,[nc_server1,nc_server2]}]} -%% ]}]}]. -%%''' -%% -%% Connections must be opened like this: -%% -%% ``` -%% open(nc_server1,[...]), -%% open(nc_server2,[...]). -%% ''' -%% -%% === Logging example 2 === -%% -%% The following configuration file will cause raw logging of all -%% netconf traffic into one single text file. -%% -%% ``` -%% {ct_conn_log,[{ct_netconfc,[{log_type,raw}]}]}. -%% ''' -%% -%% The `ct_hooks' statement must look like this: -%% -%% ``` -%% suite() -> -%% [{ct_hooks, [{cth_conn_log, []}]}]. -%% ''' -%% -%% The same `ct_hooks' statement without the configuration file would -%% cause HTML logging of all netconf connections into the test case -%% HTML log. +%% [{ct_hooks, +%% [{cth_conn_log,[{ct_netconfc,[{log_type,pretty}, +%% {hosts,[my_configured_server]}]}]} %% %% == Notifications == %% @@ -152,11 +56,9 @@ %% Notifications, which defines a mechanism for an asynchronous %% message notification delivery service for the netconf protocol. %% -%% Specific functions to support this are {@link -%% create_subscription/6} and {@link get_event_streams/3}. (The -%% functions also exist with other arities.) +%% Specific functions to support this are create_subscription/6 +%% get_event_streams/3. (The functions also exist with other arities.) %% -%% @end %%---------------------------------------------------------------------- -module(ct_netconfc). @@ -167,7 +69,13 @@ %%---------------------------------------------------------------------- %% External exports %%---------------------------------------------------------------------- --export([open/1, +-export([connect/1, + connect/2, + disconnect/1, + session/1, + session/2, + session/3, + open/1, open/2, only_open/1, only_open/2, @@ -205,6 +113,7 @@ create_subscription/4, create_subscription/5, create_subscription/6, + get_event_streams/1, get_event_streams/2, get_event_streams/3, get_capabilities/1, @@ -215,7 +124,9 @@ %%---------------------------------------------------------------------- %% Exported types %%---------------------------------------------------------------------- --export_type([notification/0]). +-export_type([client/0, + handle/0, + notification/0]). %%---------------------------------------------------------------------- %% Internal exports @@ -234,7 +145,6 @@ %% Internal defines %%---------------------------------------------------------------------- -define(APPLICATION,?MODULE). --define(VALID_SSH_OPTS,[user, password, user_dir]). -define(DEFAULT_STREAM,"NETCONF"). -define(error(ConnName,Report), @@ -264,6 +174,7 @@ session_id, msg_id = 1, hello_status, + no_end_tag_buff = <<>>, buff = <<>>, pending = [], % [#pending] event_receiver}).% pid @@ -273,13 +184,15 @@ host, port = ?DEFAULT_PORT, timeout = ?DEFAULT_TIMEOUT, - name}). + name, + type}). %% Connection reference -record(connection, {reference, % {CM,Ch} host, port, - name}). + name, + type}). %% Pending replies from server -record(pending, {tref, % timer ref (returned from timer:xxx) @@ -291,17 +204,17 @@ %%---------------------------------------------------------------------- %% Type declarations %%---------------------------------------------------------------------- --type client() :: handle() | ct_gen_conn:server_id() | ct_gen_conn:target_name(). --type handle() :: term(). -%% An opaque reference for a connection (netconf session). See {@link -%% ct} for more information. +-type client() :: handle() | server_id() | ct:target_name(). +-opaque handle() :: pid(). -type options() :: [option()]. -%% Options used for setting up ssh connection to a netconf server. - -type option() :: {ssh,host()} | {port,inet:port_number()} | {user,string()} | {password,string()} | {user_dir,string()} | {timeout,timeout()}. + +-type session_options() :: [session_option()]. +-type session_option() :: {timeout,timeout()}. + -type host() :: inet:hostname() | inet:ip_address(). -type notification() :: {notification, xml_attributes(), notification_content()}. @@ -317,14 +230,13 @@ %% See XML Schema for Event Notifications found in RFC5277 for further %% detail about the data format for the string values. -%-type error_handler() :: module(). -type error_reason() :: term(). +-type server_id() :: atom(). + -type simple_xml() :: {xml_tag(), xml_attributes(), xml_content()} | {xml_tag(), xml_content()} | xml_tag(). -%% <p>This type is further described in the documentation for the -%% <tt>Xmerl</tt> application.</p> -type xml_tag() :: atom(). -type xml_attributes() :: [{xml_attribute_tag(),xml_attribute_value()}]. -type xml_attribute_tag() :: atom(). @@ -336,69 +248,130 @@ -type xs_datetime() :: string(). %% This date and time identifyer has the same format as the XML type %% dateTime and compliant to RFC3339. The format is -%% ```[-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]''' +%% "[-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]" %%---------------------------------------------------------------------- %% External interface functions %%---------------------------------------------------------------------- %%---------------------------------------------------------------------- --spec open(Options) -> Result when +%% Open an SSH connection to a Netconf server +%% If the server options are specified in a configuration file, use +%% open/2. +-spec connect(Options) -> Result when Options :: options(), Result :: {ok,handle()} | {error,error_reason()}. -%% @doc Open a netconf session and exchange `hello' messages. -%% -%% If the server options are specified in a configuration file, or if -%% a named client is needed for logging purposes (see {@section -%% Logging}) use {@link open/2} instead. -%% -%% The opaque `handler()' reference which is returned from this -%% function is required as client identifier when calling any other -%% function in this module. -%% -%% The `timeout' option (milli seconds) is used when setting up -%% the ssh connection and when waiting for the hello message from the -%% server. It is not used for any other purposes during the lifetime -%% of the connection. -%% -%% @end +connect(Options) -> + do_connect(Options, #options{type=connection},[]). + +-spec connect(KeyOrName,ExtraOptions) -> Result when + KeyOrName :: ct:key_or_name(), + ExtraOptions :: options(), + Result :: {ok,handle()} | {error,error_reason()}. +connect(KeyOrName, ExtraOptions) -> + SortedExtra = lists:keysort(1,ExtraOptions), + SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])), + AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra), + do_connect(AllOpts,#options{name=KeyOrName,type=connection},[{name,KeyOrName}]). + +do_connect(OptList,InitOptRec,NameOpt) -> + case check_options(OptList,InitOptRec) of + {Host,Port,Options} -> + ct_gen_conn:start({Host,Port},Options,?MODULE, + NameOpt ++ [{reconnect,false}, + {use_existing_connection,false}, + {forward_messages,false}]); + Error -> + Error + end. + %%---------------------------------------------------------------------- -open(Options) -> - open(Options,#options{},[],true). +%% Close the given SSH connection. +-spec disconnect(Conn) -> ok | {error,error_reason()} when + Conn :: handle(). +disconnect(Conn) -> + case call(Conn,get_ssh_connection) of + {ok,_} -> + ct_gen_conn:stop(Conn); + Error -> + Error + end. + +%%---------------------------------------------------------------------- +%% Open a netconf session as a channel on the given SSH connection, +%% and exchange `hello' messages. +-spec session(Conn) -> Result when + Conn :: handle(), + Result :: {ok,handle()} | {error,error_reason()}. +session(Conn) -> + do_session(Conn,[],#options{type=channel},[]). + +-spec session(Conn,Options) -> Result when + Conn :: handle(), + Options :: session_options(), + Result :: {ok,handle()} | {error,error_reason()}; + (KeyOrName,Conn) -> Result when + KeyOrName :: ct:key_or_name(), + Conn :: handle(), + Result :: {ok,handle()} | {error,error_reason()}. +session(Conn,Options) when is_list(Options) -> + do_session(Conn,Options,#options{type=channel},[]); +session(KeyOrName,Conn) -> + do_session(Conn,[],#options{name=KeyOrName,type=channel},[{name,KeyOrName}]). + +-spec session(KeyOrName,Conn,Options) -> Result when + Conn :: handle(), + Options :: session_options(), + KeyOrName :: ct:key_or_name(), + Result :: {ok,handle()} | {error,error_reason()}. +session(KeyOrName,Conn,ExtraOptions) -> + SortedExtra = lists:keysort(1,ExtraOptions), + SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])), + AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra), + do_session(Conn,AllOpts,#options{name=KeyOrName,type=channel}, + [{name,KeyOrName}]). + +do_session(Conn,OptList,InitOptRec,NameOpt) -> + case call(Conn,get_ssh_connection) of + {ok,SshConn} -> + case check_session_options(OptList,InitOptRec) of + {ok,Options} -> + case ct_gen_conn:start(SshConn,Options,?MODULE, + NameOpt ++ + [{reconnect,false}, + {use_existing_connection,false}, + {forward_messages,true}]) of + {ok,Client} -> + case hello(Client,Options#options.timeout) of + ok -> + {ok,Client}; + Error -> + Error + end; + Error -> + Error + end; + Error -> + Error + end; + Error -> + Error + end. %%---------------------------------------------------------------------- +%% Open a netconf session and exchange 'hello' messages. +%% If the server options are specified in a configuration file, use +%% open/2. +-spec open(Options) -> Result when + Options :: options(), + Result :: {ok,handle()} | {error,error_reason()}. +open(Options) -> + open(Options,#options{type=connection_and_channel},[],true). + -spec open(KeyOrName, ExtraOptions) -> Result when - KeyOrName :: ct_gen_conn:key_or_name(), + KeyOrName :: ct:key_or_name(), ExtraOptions :: options(), Result :: {ok,handle()} | {error,error_reason()}. -%% @doc Open a named netconf session and exchange `hello' messages. -%% -%% If `KeyOrName' is a configured `server_id()' or a -%% `target_name()' associated with such an ID, then the options -%% for this server will be fetched from the configuration file. -% -%% The `ExtraOptions' argument will be added to the options found in -%% the configuration file. If the same options are given, the values -%% from the configuration file will overwrite `ExtraOptions'. -%% -%% If the server is not specified in a configuration file, use {@link -%% open/1} instead. -%% -%% The opaque `handle()' reference which is returned from this -%% function can be used as client identifier when calling any other -%% function in this module. However, if `KeyOrName' is a -%% `target_name()', i.e. if the server is named via a call to -%% `ct:require/2' or a `require' statement in the test -%% suite, then this name may be used instead of the `handle()'. -%% -%% The `timeout' option (milli seconds) is used when setting up -%% the ssh connection and when waiting for the hello message from the -%% server. It is not used for any other purposes during the lifetime -%% of the connection. -%% -%% @see ct:require/2 -%% @end -%%---------------------------------------------------------------------- open(KeyOrName, ExtraOpts) -> open(KeyOrName, ExtraOpts, true). @@ -406,10 +379,11 @@ open(KeyOrName, ExtraOpts, Hello) -> SortedExtra = lists:keysort(1,ExtraOpts), SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])), AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra), - open(AllOpts,#options{name=KeyOrName},[{name,KeyOrName}],Hello). + open(AllOpts,#options{name=KeyOrName,type=connection_and_channel}, + [{name,KeyOrName}],Hello). open(OptList,InitOptRec,NameOpt,Hello) -> - case check_options(OptList,undefined,undefined,InitOptRec) of + case check_options(OptList,InitOptRec) of {Host,Port,Options} -> case ct_gen_conn:start({Host,Port},Options,?MODULE, NameOpt ++ [{reconnect,false}, @@ -431,296 +405,206 @@ open(OptList,InitOptRec,NameOpt,Hello) -> %%---------------------------------------------------------------------- +%% As open/1,2, except no 'hello' message is sent. -spec only_open(Options) -> Result when Options :: options(), Result :: {ok,handle()} | {error,error_reason()}. -%% @doc Open a netconf session, but don't send `hello'. -%% -%% As {@link open/1} but does not send a `hello' message. -%% -%% @end -%%---------------------------------------------------------------------- only_open(Options) -> - open(Options,#options{},[],false). + open(Options,#options{type=connection_and_channel},[],false). -%%---------------------------------------------------------------------- -spec only_open(KeyOrName,ExtraOptions) -> Result when - KeyOrName :: ct_gen_conn:key_or_name(), + KeyOrName :: ct:key_or_name(), ExtraOptions :: options(), Result :: {ok,handle()} | {error,error_reason()}. -%% @doc Open a name netconf session, but don't send `hello'. -%% -%% As {@link open/2} but does not send a `hello' message. -%% -%% @end -%%---------------------------------------------------------------------- only_open(KeyOrName, ExtraOpts) -> open(KeyOrName, ExtraOpts, false). %%---------------------------------------------------------------------- -%% @spec hello(Client) -> Result -%% @equiv hello(Client, [], infinity) +%% Send a 'hello' message. +-spec hello(Client) -> Result when + Client :: handle(), + Result :: ok | {error,error_reason()}. hello(Client) -> hello(Client,[],?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec hello(Client,Timeout) -> Result when Client :: handle(), Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @spec hello(Client, Timeout) -> Result -%% @equiv hello(Client, [], Timeout) hello(Client,Timeout) -> hello(Client,[],Timeout). -%%---------------------------------------------------------------------- -spec hello(Client,Options,Timeout) -> Result when Client :: handle(), Options :: [{capability, [string()]}], Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Exchange `hello' messages with the server. -%% -%% Adds optional capabilities and sends a `hello' message to the -%% server and waits for the return. -%% @end -%%---------------------------------------------------------------------- hello(Client,Options,Timeout) -> call(Client, {hello, Options, Timeout}). %%---------------------------------------------------------------------- -%% @spec get_session_id(Client) -> Result -%% @equiv get_session_id(Client, infinity) +%% Get the session id for the session specified by Client. +-spec get_session_id(Client) -> Result when + Client :: client(), + Result :: pos_integer() | {error,error_reason()}. get_session_id(Client) -> get_session_id(Client, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec get_session_id(Client, Timeout) -> Result when Client :: client(), Timeout :: timeout(), Result :: pos_integer() | {error,error_reason()}. -%% @doc Returns the session id associated with the given client. -%% -%% @end -%%---------------------------------------------------------------------- get_session_id(Client, Timeout) -> call(Client, get_session_id, Timeout). %%---------------------------------------------------------------------- -%% @spec get_capabilities(Client) -> Result -%% @equiv get_capabilities(Client, infinity) +%% Get the server side capabilities. +-spec get_capabilities(Client) -> Result when + Client :: client(), + Result :: [string()] | {error,error_reason()}. get_capabilities(Client) -> get_capabilities(Client, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec get_capabilities(Client, Timeout) -> Result when Client :: client(), Timeout :: timeout(), Result :: [string()] | {error,error_reason()}. -%% @doc Returns the server side capabilities -%% -%% The following capability identifiers, defined in RFC 4741, can be returned: -%% -%% <ul> -%% <li>`"urn:ietf:params:netconf:base:1.0"'</li> -%% <li>`"urn:ietf:params:netconf:capability:writable-running:1.0"'</li> -%% <li>`"urn:ietf:params:netconf:capability:candidate:1.0"'</li> -%% <li>`"urn:ietf:params:netconf:capability:confirmed-commit:1.0"'</li> -%% <li>`"urn:ietf:params:netconf:capability:rollback-on-error:1.0"'</li> -%% <li>`"urn:ietf:params:netconf:capability:startup:1.0"'</li> -%% <li>`"urn:ietf:params:netconf:capability:url:1.0"'</li> -%% <li>`"urn:ietf:params:netconf:capability:xpath:1.0"'</li> -%% </ul> -%% -%% Note, additional identifiers may exist, e.g. server side namespace. -%% -%% @end -%%---------------------------------------------------------------------- get_capabilities(Client, Timeout) -> call(Client, get_capabilities, Timeout). %%---------------------------------------------------------------------- -%% @spec send(Client, SimpleXml) -> Result -%% @equiv send(Client, SimpleXml, infinity) +%% Send an XML document to the server. +-spec send(Client, SimpleXml) -> Result when + Client :: client(), + SimpleXml :: simple_xml(), + Result :: simple_xml() | {error,error_reason()}. send(Client, SimpleXml) -> send(Client, SimpleXml, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec send(Client, SimpleXml, Timeout) -> Result when Client :: client(), SimpleXml :: simple_xml(), Timeout :: timeout(), Result :: simple_xml() | {error,error_reason()}. -%% @doc Send an XML document to the server. -%% -%% The given XML document is sent as is to the server. This function -%% can be used for sending XML documents that can not be expressed by -%% other interface functions in this module. send(Client, SimpleXml, Timeout) -> call(Client,{send, Timeout, SimpleXml}). %%---------------------------------------------------------------------- -%% @spec send_rpc(Client, SimpleXml) -> Result -%% @equiv send_rpc(Client, SimpleXml, infinity) +%% Wrap the given XML document in a valid netconf 'rpc' request and +%% send to the server. +-spec send_rpc(Client, SimpleXml) -> Result when + Client :: client(), + SimpleXml :: simple_xml(), + Result :: [simple_xml()] | {error,error_reason()}. send_rpc(Client, SimpleXml) -> send_rpc(Client, SimpleXml, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec send_rpc(Client, SimpleXml, Timeout) -> Result when Client :: client(), SimpleXml :: simple_xml(), Timeout :: timeout(), Result :: [simple_xml()] | {error,error_reason()}. -%% @doc Send a Netconf <code>rpc</code> request to the server. -%% -%% The given XML document is wrapped in a valid Netconf -%% <code>rpc</code> request and sent to the server. The -%% <code>message-id</code> and namespace attributes are added to the -%% <code>rpc</code> element. -%% -%% This function can be used for sending <code>rpc</code> requests -%% that can not be expressed by other interface functions in this -%% module. send_rpc(Client, SimpleXml, Timeout) -> call(Client,{send_rpc, SimpleXml, Timeout}). %%---------------------------------------------------------------------- -%% @spec lock(Client, Target) -> Result -%% @equiv lock(Client, Target, infinity) +%% Send a 'lock' request. +-spec lock(Client, Target) -> Result when + Client :: client(), + Target :: netconf_db(), + Result :: ok | {error,error_reason()}. lock(Client, Target) -> lock(Client, Target,?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec lock(Client, Target, Timeout) -> Result when Client :: client(), Target :: netconf_db(), Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Unlock configuration target. -%% -%% Which target parameters that can be used depends on if -%% `:candidate' and/or `:startup' are supported by the -%% server. If successfull, the configuration system of the device is -%% not available to other clients (Netconf, CORBA, SNMP etc). Locks -%% are intended to be short-lived. -%% -%% The operations {@link kill_session/2} or {@link kill_session/3} can -%% be used to force the release of a lock owned by another Netconf -%% session. How this is achieved by the server side is implementation -%% specific. -%% -%% @end -%%---------------------------------------------------------------------- lock(Client, Target, Timeout) -> call(Client,{send_rpc_op,lock,[Target],Timeout}). %%---------------------------------------------------------------------- -%% @spec unlock(Client, Target) -> Result -%% @equiv unlock(Client, Target, infinity) +%% Send a 'unlock' request. +-spec unlock(Client, Target) -> Result when + Client :: client(), + Target :: netconf_db(), + Result :: ok | {error,error_reason()}. unlock(Client, Target) -> unlock(Client, Target,?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec unlock(Client, Target, Timeout) -> Result when Client :: client(), Target :: netconf_db(), Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Unlock configuration target. -%% -%% If the client earlier has aquired a lock, via {@link lock/2} or -%% {@link lock/3}, this operation release the associated lock. To be -%% able to access another target than `running', the server must -%% support `:candidate' and/or `:startup'. -%% -%% @end -%%---------------------------------------------------------------------- unlock(Client, Target, Timeout) -> call(Client, {send_rpc_op, unlock, [Target], Timeout}). %%---------------------------------------------------------------------- -%% @spec get(Client, Filter) -> Result -%% @equiv get(Client, Filter, infinity) +%% Send a 'get' request. +-spec get(Client, Filter) -> Result when + Client :: client(), + Filter :: simple_xml() | xpath(), + Result :: {ok,[simple_xml()]} | {error,error_reason()}. get(Client, Filter) -> get(Client, Filter, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec get(Client, Filter, Timeout) -> Result when Client :: client(), Filter :: simple_xml() | xpath(), Timeout :: timeout(), Result :: {ok,[simple_xml()]} | {error,error_reason()}. -%% @doc Get data. -%% -%% This operation returns both configuration and state data from the -%% server. -%% -%% Filter type `xpath' can only be used if the server supports -%% `:xpath'. -%% -%% @end -%%---------------------------------------------------------------------- get(Client, Filter, Timeout) -> call(Client,{send_rpc_op, get, [Filter], Timeout}). %%---------------------------------------------------------------------- -%% @spec get_config(Client, Source, Filter) -> Result -%% @equiv get_config(Client, Source, Filter, infinity) +%% Send a 'get-config' request. +-spec get_config(Client, Source, Filter) -> Result when + Client :: client(), + Source :: netconf_db(), + Filter :: simple_xml() | xpath(), + Result :: {ok,[simple_xml()]} | {error,error_reason()}. get_config(Client, Source, Filter) -> get_config(Client, Source, Filter, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec get_config(Client, Source, Filter, Timeout) -> Result when Client :: client(), Source :: netconf_db(), Filter :: simple_xml() | xpath(), Timeout :: timeout(), Result :: {ok,[simple_xml()]} | {error,error_reason()}. -%% @doc Get configuration data. -%% -%% To be able to access another source than `running', the server -%% must advertise `:candidate' and/or `:startup'. -%% -%% Filter type `xpath' can only be used if the server supports -%% `:xpath'. -%% -%% -%% @end -%%---------------------------------------------------------------------- get_config(Client, Source, Filter, Timeout) -> call(Client, {send_rpc_op, get_config, [Source, Filter], Timeout}). %%---------------------------------------------------------------------- -%% @spec edit_config(Client, Target, Config) -> Result -%% @equiv edit_config(Client, Target, Config, [], infinity) +%% Send a 'edit-config' request. +-spec edit_config(Client, Target, Config) -> Result when + Client :: client(), + Target :: netconf_db(), + Config :: simple_xml(), + Result :: ok | {error,error_reason()}. edit_config(Client, Target, Config) -> edit_config(Client, Target, Config, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- --spec edit_config(Client, Target, Config, OptParamsOrTimeout) -> Result when +-spec edit_config(Client, Target, Config, OptParams) -> Result when + Client :: client(), + Target :: netconf_db(), + Config :: simple_xml(), + OptParams :: [simple_xml()], + Result :: ok | {error,error_reason()}; + (Client, Target, Config, Timeout) -> Result when Client :: client(), Target :: netconf_db(), Config :: simple_xml(), - OptParamsOrTimeout :: [simple_xml()] | timeout(), + Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc -%% -%% If `OptParamsOrTimeout' is a timeout value, then this is -%% equivalent to {@link edit_config/5. edit_config(Client, Target, -%% Config, [], Timeout)}. -%% -%% If `OptParamsOrTimeout' is a list of simple XML, then this is -%% equivalent to {@link edit_config/5. edit_config(Client, Target, -%% Config, OptParams, infinity)}. -%% -%% @end edit_config(Client, Target, Config, Timeout) when ?is_timeout(Timeout) -> edit_config(Client, Target, Config, [], Timeout); edit_config(Client, Target, Config, OptParams) when is_list(OptParams) -> edit_config(Client, Target, Config, OptParams, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec edit_config(Client, Target, Config, OptParams, Timeout) -> Result when Client :: client(), Target :: netconf_db(), @@ -728,99 +612,79 @@ edit_config(Client, Target, Config, OptParams) when is_list(OptParams) -> OptParams :: [simple_xml()], Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Edit configuration data. -%% -%% Per default only the running target is available, unless the server -%% include `:candidate' or `:startup' in its list of -%% capabilities. -%% -%% `OptParams' can be used for specifying optional parameters -%% (`default-operation', `test-option' or `error-option') that will be -%% added to the `edit-config' request. The value must be a list -%% containing valid simple XML, for example -%% -%% ``` -%% [{'default-operation', ["none"]}, -%% {'error-option', ["rollback-on-error"]}] -%%''' -%% -%% @end -%%---------------------------------------------------------------------- edit_config(Client, Target, Config, OptParams, Timeout) -> call(Client, {send_rpc_op, edit_config, [Target,Config,OptParams], Timeout}). %%---------------------------------------------------------------------- -%% @spec delete_config(Client, Target) -> Result -%% @equiv delete_config(Client, Target, infinity) +%% Send a 'delete-config' request. +-spec delete_config(Client, Target) -> Result when + Client :: client(), + Target :: startup | candidate, + Result :: ok | {error,error_reason()}. delete_config(Client, Target) -> delete_config(Client, Target, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec delete_config(Client, Target, Timeout) -> Result when Client :: client(), Target :: startup | candidate, Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Delete configuration data. -%% -%% The running configuration cannot be deleted and `:candidate' -%% or `:startup' must be advertised by the server. -%% -%% @end -%%---------------------------------------------------------------------- delete_config(Client, Target, Timeout) when Target == startup; Target == candidate -> call(Client,{send_rpc_op, delete_config, [Target], Timeout}). %%---------------------------------------------------------------------- -%% @spec copy_config(Client, Source, Target) -> Result -%% @equiv copy_config(Client, Source, Target, infinity) +%% Send a 'copy-config' request. +-spec copy_config(Client, Target, Source) -> Result when + Client :: client(), + Target :: netconf_db(), + Source :: netconf_db(), + Result :: ok | {error,error_reason()}. copy_config(Client, Source, Target) -> copy_config(Client, Source, Target, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec copy_config(Client, Target, Source, Timeout) -> Result when Client :: client(), Target :: netconf_db(), Source :: netconf_db(), Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Copy configuration data. -%% -%% Which source and target options that can be issued depends on the -%% capabilities supported by the server. I.e. `:candidate' and/or -%% `:startup' are required. -%% -%% @end -%%---------------------------------------------------------------------- copy_config(Client, Target, Source, Timeout) -> call(Client,{send_rpc_op, copy_config, [Target, Source], Timeout}). %%---------------------------------------------------------------------- -%% @spec action(Client, Action) -> Result -%% @equiv action(Client, Action, infinity) +%% Execute an action. +-spec action(Client, Action) -> Result when + Client :: client(), + Action :: simple_xml(), + Result :: ok | {ok,[simple_xml()]} | {error,error_reason()}. action(Client,Action) -> action(Client,Action,?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec action(Client, Action, Timeout) -> Result when Client :: client(), Action :: simple_xml(), Timeout :: timeout(), Result :: ok | {ok,[simple_xml()]} | {error,error_reason()}. -%% @doc Execute an action. If the return type is void, <c>ok</c> will -%% be returned instead of <c>{ok,[simple_xml()]}</c>. -%% -%% @end -%%---------------------------------------------------------------------- action(Client,Action,Timeout) -> call(Client,{send_rpc_op, action, [Action], Timeout}). %%---------------------------------------------------------------------- +%% Send a 'create-subscription' request +%% See RFC5277, NETCONF Event Notifications +-spec create_subscription(Client) -> Result when + Client :: client(), + Result :: ok | {error,error_reason()}. create_subscription(Client) -> create_subscription(Client,?DEFAULT_STREAM,?DEFAULT_TIMEOUT). +-spec create_subscription(Client, Stream | Filter | Timeout) -> Result when + Client :: client(), + Stream :: stream_name(), + Filter :: simple_xml() | [simple_xml()], + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. create_subscription(Client,Timeout) when ?is_timeout(Timeout) -> create_subscription(Client,?DEFAULT_STREAM,Timeout); @@ -876,6 +740,22 @@ create_subscription(Client,Stream,Filter,Timeout) [Stream,Filter,undefined,undefined], Timeout}). +-spec create_subscription(Client, Stream, StartTime, StopTime, Timeout) -> + Result when + Client :: client(), + Stream :: stream_name(), + StartTime :: xs_datetime(), + StopTime :: xs_datetime(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}; + (Client, Stream, Filter,StartTime, StopTime) -> + Result when + Client :: client(), + Stream :: stream_name(), + Filter :: simple_xml() | [simple_xml()], + StartTime :: xs_datetime(), + StopTime :: xs_datetime(), + Result :: ok | {error,error_reason()}. create_subscription(Client,Stream,StartTime,StopTime,Timeout) when ?is_string(Stream) andalso ?is_string(StartTime) andalso @@ -891,7 +771,6 @@ create_subscription(Client,Stream,Filter,StartTime,StopTime) ?is_string(StopTime) -> create_subscription(Client,Stream,Filter,StartTime,StopTime,?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec create_subscription(Client, Stream, Filter,StartTime, StopTime, Timeout) -> Result when Client :: client(), @@ -901,168 +780,75 @@ create_subscription(Client,Stream,Filter,StartTime,StopTime) StopTime :: xs_datetime(), Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Create a subscription for event notifications. -%% -%% This function sets up a subscription for netconf event -%% notifications of the given stream type, matching the given -%% filter. The calling process will receive notifications as messages -%% of type `notification()'. -%% -%% <dl> -%% <dt>Stream:</dt> -%% <dd> An optional parameter that indicates which stream of events -%% is of interest. If not present, events in the default NETCONF -%% stream will be sent.</dd> -%% -%% <dt>Filter:</dt> -%% <dd>An optional parameter that indicates which subset of all -%% possible events is of interest. The format of this parameter is -%% the same as that of the filter parameter in the NETCONF protocol -%% operations. If not present, all events not precluded by other -%% parameters will be sent.</dd> -%% -%% <dt>StartTime:</dt> -%% <dd>An optional parameter used to trigger the replay feature and -%% indicate that the replay should start at the time specified. If -%% `StartTime' is not present, this is not a replay subscription. -%% It is not valid to specify start times that are later than the -%% current time. If the `StartTime' specified is earlier than the -%% log can support, the replay will begin with the earliest -%% available notification. This parameter is of type dateTime and -%% compliant to [RFC3339]. Implementations must support time -%% zones.</dd> -%% -%% <dt>StopTime:</dt> -%% <dd>An optional parameter used with the optional replay feature -%% to indicate the newest notifications of interest. If `StopTime' -%% is not present, the notifications will continue until the -%% subscription is terminated. Must be used with and be later than -%% `StartTime'. Values of `StopTime' in the future are valid. This -%% parameter is of type dateTime and compliant to [RFC3339]. -%% Implementations must support time zones.</dd> -%% </dl> -%% -%% See RFC5277 for further details about the event notification -%% mechanism. -%% -%% @end -%%---------------------------------------------------------------------- create_subscription(Client,Stream,Filter,StartTime,StopTime,Timeout) -> call(Client,{send_rpc_op,{create_subscription, self()}, [Stream,Filter,StartTime,StopTime], Timeout}). %%---------------------------------------------------------------------- -%% @spec get_event_streams(Client, Timeout) -> Result -%% @equiv get_event_streams(Client, [], Timeout) +%% Send a request to get the given event streams +%% See RFC5277, NETCONF Event Notifications +-spec get_event_streams(Client) + -> Result when + Client :: client(), + Result :: {ok,streams()} | {error,error_reason()}. +get_event_streams(Client) -> + get_event_streams(Client,[],?DEFAULT_TIMEOUT). + +-spec get_event_streams(Client, Timeout) + -> Result when + Client :: client(), + Timeout :: timeout(), + Result :: {ok,streams()} | {error,error_reason()}; + (Client, Streams) -> Result when + Client :: client(), + Streams :: [stream_name()], + Result :: {ok,streams()} | {error,error_reason()}. get_event_streams(Client,Timeout) when is_integer(Timeout); Timeout==infinity -> get_event_streams(Client,[],Timeout); - -%%---------------------------------------------------------------------- -%% @spec get_event_streams(Client, Streams) -> Result -%% @equiv get_event_streams(Client, Streams, infinity) get_event_streams(Client,Streams) when is_list(Streams) -> get_event_streams(Client,Streams,?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec get_event_streams(Client, Streams, Timeout) -> Result when Client :: client(), Streams :: [stream_name()], Timeout :: timeout(), Result :: {ok,streams()} | {error,error_reason()}. -%% @doc Send a request to get the given event streams. -%% -%% `Streams' is a list of stream names. The following filter will -%% be sent to the netconf server in a `get' request: -%% -%% ``` -%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification"> -%% <streams> -%% <stream> -%% <name>StreamName1</name> -%% </stream> -%% <stream> -%% <name>StreamName2</name> -%% </stream> -%% ... -%% </streams> -%% </netconf> -%% ''' -%% -%% If `Streams' is an empty list, ALL streams will be requested -%% by sending the following filter: -%% -%% ``` -%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification"> -%% <streams/> -%% </netconf> -%% ''' -%% -%% If more complex filtering is needed, a use {@link get/2} or {@link -%% get/3} and specify the exact filter according to XML Schema for -%% Event Notifications found in RFC5277. -%% -%% @end -%%---------------------------------------------------------------------- get_event_streams(Client,Streams,Timeout) -> call(Client,{get_event_streams,Streams,Timeout}). %%---------------------------------------------------------------------- -%% @spec close_session(Client) -> Result -%% @equiv close_session(Client, infinity) +%% Send a 'close-session' request +-spec close_session(Client) -> Result when + Client :: client(), + Result :: ok | {error,error_reason()}. close_session(Client) -> close_session(Client, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec close_session(Client, Timeout) -> Result when Client :: client(), Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Request graceful termination of the session associated with the client. -%% -%% When a netconf server receives a `close-session' request, it -%% will gracefully close the session. The server will release any -%% locks and resources associated with the session and gracefully -%% close any associated connections. Any NETCONF requests received -%% after a `close-session' request will be ignored. -%% -%% @end -%%---------------------------------------------------------------------- close_session(Client, Timeout) -> call(Client,{send_rpc_op, close_session, [], Timeout}, true). %%---------------------------------------------------------------------- -%% @spec kill_session(Client, SessionId) -> Result -%% @equiv kill_session(Client, SessionId, infinity) +%% Send a 'kill-session' request +-spec kill_session(Client, SessionId) -> Result when + Client :: client(), + SessionId :: pos_integer(), + Result :: ok | {error,error_reason()}. kill_session(Client, SessionId) -> kill_session(Client, SessionId, ?DEFAULT_TIMEOUT). -%%---------------------------------------------------------------------- -spec kill_session(Client, SessionId, Timeout) -> Result when Client :: client(), SessionId :: pos_integer(), Timeout :: timeout(), Result :: ok | {error,error_reason()}. -%% @doc Force termination of the session associated with the supplied -%% session id. -%% -%% The server side shall abort any operations currently in process, -%% release any locks and resources associated with the session, and -%% close any associated connections. -%% -%% Only if the server is in the confirmed commit phase, the -%% configuration will be restored to its state before entering the -%% confirmed commit phase. Otherwise, no configuration roll back will -%% be performed. -%% -%% If the given `SessionId' is equal to the current session id, -%% an error will be returned. -%% -%% @end -%% ---------------------------------------------------------------------- kill_session(Client, SessionId, Timeout) -> call(Client,{send_rpc_op, kill_session, [SessionId], Timeout}). @@ -1071,24 +857,35 @@ kill_session(Client, SessionId, Timeout) -> %% Callback functions %%---------------------------------------------------------------------- -%% @private +init(_KeyOrName,{CM,{Host,Port}},Options) -> + case ssh_channel(#connection{reference=CM,host=Host,port=Port},Options) of + {ok,Connection} -> + {ok, CM, #state{connection = Connection}}; + {error,Reason}-> + {error,Reason} + end; +init(_KeyOrName,{_Host,_Port},Options) when Options#options.type==connection -> + case ssh_connect(Options) of + {ok, Connection} -> + ConnPid = Connection#connection.reference, + {ok, ConnPid, #state{connection = Connection}}; + Error -> + Error + end; init(_KeyOrName,{_Host,_Port},Options) -> case ssh_open(Options) of {ok, Connection} -> - log(Connection,open), {ConnPid,_} = Connection#connection.reference, {ok, ConnPid, #state{connection = Connection}}; {error,Reason}-> {error,Reason} end. -%% @private + terminate(_, #state{connection=Connection}) -> ssh_close(Connection), - log(Connection,close), ok. -%% @private handle_msg({hello, Options, Timeout}, From, #state{connection=Connection,hello_status=HelloStatus} = State) -> case do_send(Connection, client_hello(Options)) of @@ -1107,6 +904,14 @@ handle_msg({hello, Options, Timeout}, From, Error -> {stop, Error, State} end; +handle_msg(get_ssh_connection, _From, #state{connection=Connection}=State) -> + Reply = + case Connection#connection.reference of + {_,_} -> {error,not_an_ssh_connection}; + CM -> {ok,{CM,{Connection#connection.host, + Connection#connection.port}}} + end, + {reply, Reply, State}; handle_msg(_, _From, #state{session_id=undefined} = State) -> %% Hello is not yet excanged - this shall never happen {reply,{error,waiting_for_hello},State}; @@ -1136,7 +941,6 @@ handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) -> SimpleXml = encode_rpc_operation(get,[Filter]), do_send_rpc(Op, SimpleXml, Timeout, From, State). -%% @private handle_msg({ssh_cm, CM, {data, Ch, _Type, Data}}, State) -> ssh_connection:adjust_window(CM,Ch,size(Data)), handle_data(Data, State); @@ -1170,9 +974,8 @@ handle_msg({Ref,timeout},#state{pending=Pending} = State) -> end, %% Halfhearted try to get in correct state, this matches %% the implementation before this patch - {R,State#state{pending=Pending1, buff= <<>>}}. + {R,State#state{pending=Pending1, no_end_tag_buff= <<>>, buff= <<>>}}. -%% @private %% Called by ct_util_server to close registered connections before terminate. close(Client) -> case get_handle(Client) of @@ -1205,7 +1008,7 @@ call(Client, Msg, Timeout, WaitStop) -> {error,no_such_client}; {error,{process_down,Pid,normal}} when WaitStop -> %% This will happen when server closes connection - %% before clien received rpc-reply on + %% before client received rpc-reply on %% close-session. ok; {error,{process_down,Pid,normal}} -> @@ -1243,26 +1046,36 @@ get_handle(Client) -> Error end. +check_options(OptList,Options) -> + check_options(OptList,undefined,undefined,Options). + check_options([], undefined, _Port, _Options) -> {error, no_host_address}; check_options([], _Host, undefined, _Options) -> {error, no_port}; check_options([], Host, Port, Options) -> {Host,Port,Options}; -check_options([{ssh, Host}|T], _, Port, #options{} = Options) -> +check_options([{ssh, Host}|T], _, Port, Options) -> check_options(T, Host, Port, Options#options{host=Host}); -check_options([{port,Port}|T], Host, _, #options{} = Options) -> +check_options([{port,Port}|T], Host, _, Options) -> check_options(T, Host, Port, Options#options{port=Port}); check_options([{timeout, Timeout}|T], Host, Port, Options) when is_integer(Timeout); Timeout==infinity -> check_options(T, Host, Port, Options#options{timeout = Timeout}); -check_options([{X,_}=Opt|T], Host, Port, #options{ssh=SshOpts}=Options) -> - case lists:member(X,?VALID_SSH_OPTS) of - true -> - check_options(T, Host, Port, Options#options{ssh=[Opt|SshOpts]}); - false -> - {error, {invalid_option, Opt}} - end. +check_options([{timeout, _} = Opt|_T], _Host, _Port, _Options) -> + {error, {invalid_option, Opt}}; +check_options([Opt|T], Host, Port, #options{ssh=SshOpts}=Options) -> + %% Option verified by ssh + check_options(T, Host, Port, Options#options{ssh=[Opt|SshOpts]}). + +check_session_options([],Options) -> + {ok,Options}; +check_session_options([{timeout, Timeout}|T], Options) + when is_integer(Timeout); Timeout==infinity -> + check_session_options(T, Options#options{timeout = Timeout}); +check_session_options([Opt|_T], _Options) -> + {error, {invalid_option, Opt}}. + %%%----------------------------------------------------------------- set_request_timer(infinity) -> @@ -1272,6 +1085,14 @@ set_request_timer(T) -> {ok,TRef} = timer:send_after(T,{Ref,timeout}), {Ref,TRef}. +%%%----------------------------------------------------------------- +cancel_request_timer(undefined,undefined) -> + ok; +cancel_request_timer(Ref,TRef) -> + _ = timer:cancel(TRef), + receive {Ref,timeout} -> ok + after 0 -> ok + end. %%%----------------------------------------------------------------- client_hello(Options) when is_list(Options) -> @@ -1350,7 +1171,6 @@ do_send_rpc(Connection, MsgId, SimpleXml) -> do_send(Connection, SimpleXml) -> Xml=to_xml_doc(SimpleXml), - log(Connection,send,Xml), ssh_send(Connection, Xml). to_xml_doc(Simple) -> @@ -1364,17 +1184,37 @@ to_xml_doc(Simple) -> %%%----------------------------------------------------------------- %%% Parse and handle received XML data -handle_data(NewData,#state{connection=Connection,buff=Buff0} = State0) -> +%%% Two buffers are used: +%%% * 'no_end_tag_buff' contains data that is checked and does not +%%% contain any (part of an) end tag. +%%% * 'buff' contains all other saved data - it may or may not +%%% include (a part of) an end tag. +%%% The reason for this is to avoid running binary:split/3 multiple +%%% times on the same data when it does not contain an end tag. This +%%% can be a considerable optimation in the case when a lot of data is +%%% received (e.g. when fetching all data from a node) and the data is +%%% sent in multiple ssh packages. +handle_data(NewData,#state{connection=Connection} = State0) -> log(Connection,recv,NewData), - Data = append_wo_initial_nl(Buff0,NewData), - case binary:split(Data,[?END_TAG],[]) of + NoEndTag0 = State0#state.no_end_tag_buff, + Buff0 = State0#state.buff, + Data = <<Buff0/binary, NewData/binary>>, + case binary:split(Data,?END_TAG,[]) of [_NoEndTagFound] -> - {noreply, State0#state{buff=Data}}; - [FirstMsg,Buff1] -> + NoEndTagSize = case byte_size(Data) of + Sz when Sz<5 -> 0; + Sz -> Sz-5 + end, + <<NoEndTag1:NoEndTagSize/binary,Buff/binary>> = Data, + NoEndTag = <<NoEndTag0/binary,NoEndTag1/binary>>, + {noreply, State0#state{no_end_tag_buff=NoEndTag, buff=Buff}}; + [FirstMsg0,Buff1] -> + FirstMsg = remove_initial_nl(<<NoEndTag0/binary,FirstMsg0/binary>>), SaxArgs = [{event_fun,fun sax_event/3}, {event_state,[]}], case xmerl_sax_parser:stream(FirstMsg, SaxArgs) of {ok, Simple, _Thrash} -> - case decode(Simple, State0#state{buff=Buff1}) of + case decode(Simple, State0#state{no_end_tag_buff= <<>>, + buff=Buff1}) of {noreply, #state{buff=Buff} = State} when Buff =/= <<>> -> %% Recurse if we have more data in buffer handle_data(<<>>, State); @@ -1386,17 +1226,18 @@ handle_data(NewData,#state{connection=Connection,buff=Buff0} = State0) -> [{parse_error,Reason}, {buffer, Buff0}, {new_data,NewData}]), - handle_error(Reason, State0#state{buff= <<>>}) + handle_error(Reason, State0#state{no_end_tag_buff= <<>>, + buff= <<>>}) end end. + %% xml does not accept a leading nl and some netconf server add a nl after %% each ?END_TAG, ignore them -append_wo_initial_nl(<<>>,NewData) -> NewData; -append_wo_initial_nl(<<"\n", Data/binary>>, NewData) -> - append_wo_initial_nl(Data, NewData); -append_wo_initial_nl(Data, NewData) -> - <<Data/binary, NewData/binary>>. +remove_initial_nl(<<"\n", Data/binary>>) -> + remove_initial_nl(Data); +remove_initial_nl(Data) -> + Data. handle_error(Reason, State) -> Pending1 = case State#state.pending of @@ -1404,9 +1245,9 @@ handle_error(Reason, State) -> Pending -> %% Assuming the first request gets the %% first answer - P=#pending{tref=TRef,caller=Caller} = + P=#pending{tref=TRef,ref=Ref,caller=Caller} = lists:last(Pending), - _ = timer:cancel(TRef), + cancel_request_timer(Ref,TRef), Reason1 = {failed_to_parse_received_data,Reason}, ct_gen_conn:return(Caller,{error,Reason1}), lists:delete(P,Pending) @@ -1492,8 +1333,8 @@ decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) -> {error,Reason} -> {noreply,State#state{hello_status = {error,Reason}}} end; - #pending{tref=TRef,caller=Caller} -> - _ = timer:cancel(TRef), + #pending{tref=TRef,ref=Ref,caller=Caller} -> + cancel_request_timer(Ref,TRef), case decode_hello(E) of {ok,SessionId,Capabilities} -> ct_gen_conn:return(Caller,ok), @@ -1519,9 +1360,8 @@ decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) -> %% there is just one pending that matches (i.e. has %% undefined msg_id and op) case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of - [#pending{tref=TRef, - caller=Caller}] -> - _ = timer:cancel(TRef), + [#pending{tref=TRef,ref=Ref,caller=Caller}] -> + cancel_request_timer(Ref,TRef), ct_gen_conn:return(Caller,E), {noreply,State#state{pending=[]}}; _ -> @@ -1542,8 +1382,8 @@ get_msg_id(Attrs) -> decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) -> case lists:keytake(MsgId,#pending.msg_id,Pending) of - {value, #pending{tref=TRef,op=Op,caller=Caller}, Pending1} -> - _ = timer:cancel(TRef), + {value, #pending{tref=TRef,ref=Ref,op=Op,caller=Caller}, Pending1} -> + cancel_request_timer(Ref,TRef), Content = forward_xmlns_attr(Attrs,Content0), {CallerReply,{ServerReply,State2}} = do_decode_rpc_reply(Op,Content,State#state{pending=Pending1}), @@ -1555,10 +1395,11 @@ decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) -> %% pending that matches (i.e. has undefined msg_id and op) case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of [#pending{tref=TRef, + ref=Ref, msg_id=undefined, op=undefined, caller=Caller}] -> - _ = timer:cancel(TRef), + cancel_request_timer(Ref,TRef), ct_gen_conn:return(Caller,E), {noreply,State#state{pending=[]}}; _ -> @@ -1739,9 +1580,14 @@ decode_streams([]) -> log(Connection,Action) -> log(Connection,Action,<<>>). -log(#connection{host=Host,port=Port,name=Name},Action,Data) -> +log(#connection{reference=Ref,host=Host,port=Port,name=Name},Action,Data) -> + Address = + case Ref of + {_,Ch} -> {Host,Port,Ch}; + _ -> {Host,Port} + end, error_logger:info_report(#conn_log{client=self(), - address={Host,Port}, + address=Address, name=Name, action=Action, module=?MODULE}, @@ -1749,7 +1595,6 @@ log(#connection{host=Host,port=Port,name=Name},Action,Data) -> %% Log callback - called from the error handler process -%% @private format_data(How,Data) -> %% Assuming that the data is encoded as UTF-8. If it is not, then %% the printout might be wrong, but the format function will not @@ -1762,9 +1607,14 @@ format_data(How,Data) -> do_format_data(raw,Data) -> io_lib:format("~n~ts~n",[hide_password(Data)]); do_format_data(pretty,Data) -> - io_lib:format("~n~ts~n",[indent(Data)]); + maybe_io_lib_format(indent(Data)); do_format_data(html,Data) -> - io_lib:format("~n~ts~n",[html_format(Data)]). + maybe_io_lib_format(html_format(Data)). + +maybe_io_lib_format(<<>>) -> + []; +maybe_io_lib_format(String) -> + io_lib:format("~n~ts~n",[String]). %%%----------------------------------------------------------------- %%% Hide password elements from XML data @@ -1803,13 +1653,21 @@ indent1("<?"++Rest1,Indent1) -> Line++indent1(Rest2,Indent2); indent1("</"++Rest1,Indent1) -> %% Stop tag - {Line,Rest2,Indent2} = indent_line1(Rest1,Indent1,[$/,$<]), - "\n"++Line++indent1(Rest2,Indent2); + case indent_line1(Rest1,Indent1,[$/,$<]) of + {[],[],_} -> + []; + {Line,Rest2,Indent2} -> + "\n"++Line++indent1(Rest2,Indent2) + end; indent1("<"++Rest1,Indent1) -> %% Start- or empty tag put(tag,get_tag(Rest1)), - {Line,Rest2,Indent2} = indent_line(Rest1,Indent1,[$<]), - "\n"++Line++indent1(Rest2,Indent2); + case indent_line(Rest1,Indent1,[$<]) of + {[],[],_} -> + []; + {Line,Rest2,Indent2} -> + "\n"++Line++indent1(Rest2,Indent2) + end; indent1([H|T],Indent) -> [H|indent1(T,Indent)]; indent1([],_Indent) -> @@ -1875,42 +1733,84 @@ get_tag([]) -> %%%----------------------------------------------------------------- %%% SSH stuff - -ssh_open(#options{host=Host,timeout=Timeout,port=Port,ssh=SshOpts,name=Name}) -> +ssh_connect(#options{host=Host,timeout=Timeout,port=Port, + ssh=SshOpts,name=Name,type=Type}) -> case ssh:connect(Host, Port, [{user_interaction,false}, - {silently_accept_hosts, true}|SshOpts]) of + {silently_accept_hosts, true}|SshOpts], + Timeout) of {ok,CM} -> - case ssh_connection:session_channel(CM, Timeout) of - {ok,Ch} -> - case ssh_connection:subsystem(CM, Ch, "netconf", Timeout) of - success -> - {ok, #connection{reference = {CM,Ch}, - host = Host, - port = Port, - name = Name}}; - failure -> - ssh:close(CM), - {error,{ssh,could_not_execute_netconf_subsystem}}; - {error,timeout} -> - {error,{ssh,could_not_execute_netconf_subsystem,timeout}} - end; - {error, Reason} -> - ssh:close(CM), - {error,{ssh,could_not_open_channel,Reason}} - end; + Connection = #connection{reference = CM, + host = Host, + port = Port, + name = Name, + type = Type}, + log(Connection,connect), + {ok,Connection}; {error,Reason} -> {error,{ssh,could_not_connect_to_server,Reason}} end. -ssh_send(#connection{reference = {CM,Ch}}, Data) -> +ssh_channel(#connection{reference=CM}=Connection0, + #options{timeout=Timeout,name=Name,type=Type}) -> + case ssh_connection:session_channel(CM, Timeout) of + {ok,Ch} -> + case ssh_connection:subsystem(CM, Ch, "netconf", Timeout) of + success -> + Connection = Connection0#connection{reference = {CM,Ch}, + name = Name, + type = Type}, + log(Connection,open), + {ok, Connection}; + failure -> + ssh_connection:close(CM,Ch), + {error,{ssh,could_not_execute_netconf_subsystem}}; + {error,timeout} -> + ssh_connection:close(CM,Ch), + {error,{ssh,could_not_execute_netconf_subsystem,timeout}} + end; + {error, Reason} -> + {error,{ssh,could_not_open_channel,Reason}} + end. + + +ssh_open(Options) -> + case ssh_connect(Options) of + {ok,Connection} -> + case ssh_channel(Connection,Options) of + {ok,_} = Ok -> + Ok; + Error -> + ssh_close(Connection), + Error + end; + Error -> + Error + end. + +ssh_send(#connection{reference = {CM,Ch}}=Connection, Data) -> case ssh_connection:send(CM, Ch, Data) of - ok -> ok; - {error,Reason} -> {error,{ssh,failed_to_send_data,Reason}} + ok -> + log(Connection,send,Data), + ok; + {error,Reason} -> + {error,{ssh,failed_to_send_data,Reason}} end. -ssh_close(#connection{reference = {CM,_Ch}}) -> - ssh:close(CM). +ssh_close(Connection=#connection{reference = {CM,Ch}, type = Type}) -> + _ = ssh_connection:close(CM,Ch), + log(Connection,close), + case Type of + connection_and_channel -> + ssh_close(Connection#connection{reference = CM}); + _ -> + ok + end, + ok; +ssh_close(Connection=#connection{reference = CM}) -> + _ = ssh:close(CM), + log(Connection,disconnect), + ok. %%---------------------------------------------------------------------- diff --git a/lib/common_test/src/ct_netconfc.hrl b/lib/common_test/src/ct_netconfc.hrl index e4746fe8b7..5eeeafa318 100644 --- a/lib/common_test/src/ct_netconfc.hrl +++ b/lib/common_test/src/ct_netconfc.hrl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012. All Rights Reserved. +%% Copyright Ericsson AB 2012-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/common_test/src/ct_property_test.erl b/lib/common_test/src/ct_property_test.erl index 5ee7435695..94ccb59af9 100644 --- a/lib/common_test/src/ct_property_test.erl +++ b/lib/common_test/src/ct_property_test.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2014. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -134,7 +134,7 @@ mk_ct_return(Other, Tool) -> try lists:last(hd(Tool:counterexample())) of {set,{var,_},{call,M,F,Args}} -> - {fail, io_lib:format("~p:~p/~p returned bad result",[M,F,length(Args)])} + {fail, io_lib:format("~p:~tp/~p returned bad result",[M,F,length(Args)])} catch _:_ -> {fail, Other} @@ -161,7 +161,9 @@ property_tests_path(Dir, Config) -> add_code_pathz(Dir) -> case lists:member(Dir, code:get_path()) of true -> ok; - false -> code:add_pathz(Dir) + false -> + true = code:add_pathz(Dir), + ok end. compile_tests(Path, ToolModule) -> @@ -171,10 +173,10 @@ compile_tests(Path, ToolModule) -> {ok,FileNames} = file:list_dir("."), BeamFiles = [F || F<-FileNames, filename:extension(F) == ".beam"], - [file:delete(F) || F<-BeamFiles], - ct:pal("Compiling in ~p:~n Deleted ~p~n MacroDefs=~p",[Path,BeamFiles,MacroDefs]), + _ = [file:delete(F) || F<-BeamFiles], + ct:pal("Compiling in ~tp:~n Deleted ~p~n MacroDefs=~p",[Path,BeamFiles,MacroDefs]), Result = make:all([load|MacroDefs]), - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), Result. diff --git a/lib/common_test/src/ct_release_test.erl b/lib/common_test/src/ct_release_test.erl index 6438ea01c1..551a7e06d7 100644 --- a/lib/common_test/src/ct_release_test.erl +++ b/lib/common_test/src/ct_release_test.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2014-2015. All Rights Reserved. +%% Copyright Ericsson AB 2014-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -131,8 +131,8 @@ -include_lib("kernel/include/file.hrl"). %%----------------------------------------------------------------- --define(testnode, otp_upgrade). --define(exclude_apps, [hipe, typer, dialyzer]). % never include these apps +-define(testnode, 'ct_release_test-upgrade'). +-define(exclude_apps, [hipe, dialyzer]). % never include these apps %%----------------------------------------------------------------- -record(ct_data, {from,to}). @@ -304,7 +304,13 @@ upgrade(Apps,Level,Callback,Config) -> %% Note, we will not reach this if the test fails with a %% timetrap timeout in the test suite! Thus we can have %% hanging nodes... - Nodes = nodes(), + Nodes = lists:filter(fun(Node) -> + case atom_to_list(Node) of + "ct_release_test-" ++_ -> true; + _ -> false + end + end, + nodes()), [rpc:call(Node,erlang,halt,[]) || Node <- Nodes] end. @@ -328,8 +334,15 @@ upgrade(Apps,Level,Callback,Config) -> %% ct_release_test:cleanup(Config).''' %% cleanup(Config) -> - Nodes = [node_name(?testnode)|nodes()], - [rpc:call(Node,erlang,halt,[]) || Node <- Nodes], + AllNodes = [node_name(?testnode)|nodes()], + Nodes = lists:filter(fun(Node) -> + case atom_to_list(Node) of + "ct_release_test-" ++_ -> true; + _ -> false + end + end, + AllNodes), + _ = [rpc:call(Node,erlang,halt,[]) || Node <- Nodes], Config. %%----------------------------------------------------------------- @@ -432,7 +445,7 @@ init_upgrade_test(Level) -> end, case OldRel of false -> - ct:log("Release ~p is not available." + ct:log("Release ~tp is not available." " Upgrade on '~p' level can not be tested.", [FromVsn,Level]), undefined; @@ -455,9 +468,9 @@ get_rels(minor) -> {CurrentMajor,Current}. init_upgrade_test(FromVsn,ToVsn,OldRel) -> - OtpRel = list_to_atom("otp-"++FromVsn), + Name = list_to_atom("ct_release_test-otp-"++FromVsn), ct:log("Starting node to fetch application versions to upgrade from"), - {ok,Node} = test_server:start_node(OtpRel,peer,[{erl,[OldRel]}]), + {ok,Node} = test_server:start_node(Name,peer,[{erl,[OldRel]}]), {Apps,Path} = fetch_all_apps(Node), test_server:stop_node(Node), {FromVsn,ToVsn,Apps,Path}. @@ -509,8 +522,8 @@ upgrade(Apps,Level,Callback,CreateDir,InstallDir,Config) -> {ToVsn,ToRel,ToAppsVsns} = upgrade_system(Apps, FromRel, CreateDir, InstallDir, Data), - ct:log("Upgrade from: OTP-~ts, ~p",[FromVsn, FromAppsVsns]), - ct:log("Upgrade to: OTP-~ts, ~p",[ToVsn, ToAppsVsns]), + ct:log("Upgrade from: OTP-~ts, ~tp",[FromVsn, FromAppsVsns]), + ct:log("Upgrade to: OTP-~ts, ~tp",[ToVsn, ToAppsVsns]), do_upgrade(Callback, FromVsn, FromAppsVsns, ToRel, ToAppsVsns, InstallDir) end @@ -539,14 +552,14 @@ target_system(Apps,CreateDir,InstallDir,{FromVsn,_,AllAppsVsns,Path}) -> %% Add bin and log dirs BinDir = filename:join([InstallDir, "bin"]), - file:make_dir(BinDir), - file:make_dir(filename:join(InstallDir,"log")), + ok = make_dir(BinDir), + ok = make_dir(filename:join(InstallDir,"log")), %% Delete start scripts - they will be added later ErtsBinDir = filename:join([InstallDir, "erts-" ++ ErtsVsn, "bin"]), - file:delete(filename:join([ErtsBinDir, "erl"])), - file:delete(filename:join([ErtsBinDir, "start"])), - file:delete(filename:join([ErtsBinDir, "start_erl"])), + ok = delete_file(filename:join([ErtsBinDir, "erl"])), + ok = delete_file(filename:join([ErtsBinDir, "start"])), + ok = delete_file(filename:join([ErtsBinDir, "start_erl"])), %% Copy .boot to bin/start.boot copy_file(RelName++".boot",filename:join([BinDir, "start.boot"])), @@ -642,10 +655,6 @@ do_upgrade({Cb,InitState},FromVsn,FromAppsVsns,ToRel,ToAppsVsns,InstallDir) -> Start = filename:join([InstallDir,bin,start]), {ok,Node} = start_node(Start,FromVsn,FromAppsVsns), - %% Add path to this module, to allow calls to get_appup/2 - Dir = filename:dirname(code:which(?MODULE)), - _ = rpc:call(Node,code,add_pathz,[Dir]), - ct:log("Node started: ~p",[Node]), CtData = #ct_data{from = [{A,V,code:lib_dir(A)} || {A,V} <- FromAppsVsns], to=[{A,V,code:lib_dir(A)} || {A,V} <- ToAppsVsns]}, @@ -671,7 +680,7 @@ do_upgrade({Cb,InitState},FromVsn,FromAppsVsns,ToRel,ToAppsVsns,InstallDir) -> %% even if install_release returned {ok,...} there might be an %% emulator restart (instruction restart_emulator), so we must %% always make sure the node is running. - wait_node_up(current,ToVsn,ToAppsVsns), + {ok, _} = wait_node_up(current,ToVsn,ToAppsVsns), [{"OTP upgrade test",ToVsn,_,current}, {"OTP upgrade test",FromVsn,_,permanent}] = @@ -694,7 +703,7 @@ do_upgrade({Cb,InitState},FromVsn,FromAppsVsns,ToRel,ToAppsVsns,InstallDir) -> %% even if install_release returned {ok,...} there might be an %% emulator restart (instruction restart_emulator), so we must %% always make sure the node is running. - wait_node_up(current,FromVsn,FromAppsVsns), + {ok, _} = wait_node_up(current,FromVsn,FromAppsVsns), [{"OTP upgrade test",ToVsn,_,permanent}, {"OTP upgrade test",FromVsn,_,current}] = @@ -718,12 +727,12 @@ do_upgrade({Cb,InitState},FromVsn,FromAppsVsns,ToRel,ToAppsVsns,InstallDir) -> do_callback(Node,Mod,Func,Args) -> Dir = filename:dirname(code:which(Mod)), _ = rpc:call(Node,code,add_path,[Dir]), - ct:log("Calling ~p:~p/1",[Mod,Func]), + ct:log("Calling ~p:~tp/1",[Mod,Func]), R = rpc:call(Node,Mod,Func,Args), - ct:log("~p:~p/~w returned: ~p",[Mod,Func,length(Args),R]), + ct:log("~p:~tp/~w returned: ~tp",[Mod,Func,length(Args),R]), case R of {badrpc,Error} -> - test_server:fail({test_upgrade_callback,Mod,Func,Args,Error}); + throw({fail,{test_upgrade_callback,Mod,Func,Args,Error}}); NewState -> NewState end. @@ -753,7 +762,7 @@ create_relfile(AppsVsns,CreateDir,RelName0,RelVsn) -> %% Should test tools really be included? Some library functions %% here could be used by callback, but not everything since %% processes of these applications will not be running. - TestToolAppsVsns0 = get_vsns([test_server,common_test]), + TestToolAppsVsns0 = get_vsns([common_test]), TestToolAppsVsns = [{A,V,none} || {A,V} <- TestToolAppsVsns0, false == lists:keymember(A,1,AllAppsVsns0)], @@ -845,16 +854,13 @@ copy_file(Src, Dest, Opts) -> case lists:member(preserve, Opts) of true -> {ok, FileInfo} = file:read_file_info(Src), - file:write_file_info(Dest, FileInfo); + ok = file:write_file_info(Dest, FileInfo); false -> ok end. write_file(FName, Conts) -> - Enc = file:native_name_encoding(), - {ok, Fd} = file:open(FName, [write]), - file:write(Fd, unicode:characters_to_binary(Conts,Enc,Enc)), - file:close(Fd). + file:write_file(FName, unicode:characters_to_binary(Conts)). %% Substitute all occurrences of %Var% for Val in the given scripts subst_src_scripts(Scripts, SrcDir, DestDir, Vars, Opts) -> @@ -870,7 +876,7 @@ subst_src_script(Script, SrcDir, DestDir, Vars, Opts) -> subst_file(Src, Dest, Vars, Opts) -> {ok, Bin} = file:read_file(Src), - Conts = binary_to_list(Bin), + Conts = unicode:characters_to_list(Bin), NConts = subst(Conts, Vars), write_file(Dest, NConts), case lists:member(preserve, Opts) of @@ -882,7 +888,7 @@ subst_file(Src, Dest, Vars, Opts) -> end. subst(Str, [{Var,Val}|Vars]) -> - subst(re:replace(Str,"%"++Var++"%",Val,[{return,list}]),Vars); + subst(re:replace(Str,"%"++Var++"%",Val,[{return,list},unicode]),Vars); subst(Str, []) -> Str. @@ -935,3 +941,19 @@ rm_rf(Dir) -> _ -> ok end. + +delete_file(FileName) -> + case file:delete(FileName) of + {error, enoent} -> + ok; + Else -> + Else + end. + +make_dir(Dir) -> + case file:make_dir(Dir) of + {error, eexist} -> + ok; + Else -> + Else + end. diff --git a/lib/common_test/src/ct_repeat.erl b/lib/common_test/src/ct_repeat.erl index 1cb32f5bcd..177ef37d1f 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2007-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -43,14 +43,14 @@ loop_test(If,Args) when is_list(Args) -> no_loop -> false; E = {error,_} -> - io:format("Common Test error: ~p\n\n",[E]), - file:set_cwd(Cwd), + io:format("Common Test error: ~tp\n\n",[E]), + ok = file:set_cwd(Cwd), E; {repeat,N} -> io:format("\nCommon Test: Will repeat tests ~w times.\n\n",[N]), Args1 = [{loop_info,[{repeat,1,N}]} | Args], Result = loop(If,repeat,0,N,undefined,Args1,undefined,[]), - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), Result; {stop_time,StopTime} -> Result = @@ -70,13 +70,14 @@ loop_test(If,Args) when is_list(Args) -> CtrlPid = self(), spawn( fun() -> + ct_util:mark_process(), stop_after(CtrlPid,Secs,ForceStop) end) end, Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args], loop(If,stop_time,0,Secs,StopTime,Args1,TPid,[]) end, - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), Result end. @@ -89,18 +90,18 @@ loop(If,Type,N,Data0,Data1,Args,TPid,AccResult) -> {'EXIT',Pid,Reason} -> case Reason of {user_error,What} -> - io:format("\nTest run failed!\nReason: ~p\n\n\n", [What]), + io:format("\nTest run failed!\nReason: ~tp\n\n\n", [What]), cancel(TPid), {error,What}; _ -> io:format("Test run crashed! This could be an internal error " "- please report!\n\n" - "~p\n\n\n",[Reason]), + "~tp\n\n\n",[Reason]), cancel(TPid), {error,Reason} end; {Pid,{error,Reason}} -> - io:format("\nTest run failed!\nReason: ~p\n\n\n",[Reason]), + io:format("\nTest run failed!\nReason: ~tp\n\n\n",[Reason]), cancel(TPid), {error,Reason}; {Pid,Result} -> @@ -134,6 +135,7 @@ spawn_tester(script,Ctrl,Args) -> spawn_tester(func,Ctrl,Opts) -> Tester = fun() -> + ct_util:mark_process(), case catch ct_run:run_test2(Opts) of {'EXIT',Reason} -> exit(Reason); diff --git a/lib/common_test/src/ct_rpc.erl b/lib/common_test/src/ct_rpc.erl index 73520b3dd5..b4a0bc9d74 100644 --- a/lib/common_test/src/ct_rpc.erl +++ b/lib/common_test/src/ct_rpc.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2009. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ app_node(App, [], _, _) -> app_node(App, _Candidates = [CandidateNode | Nodes], FailOnBadRPC, Cookie) -> Cookie0 = set_the_cookie(Cookie), Result = rpc:call(CandidateNode, application, which_applications, []), - set_the_cookie(Cookie0), + _ = set_the_cookie(Cookie0), case Result of {badrpc,Reason} when FailOnBadRPC == true -> ct:fail({Reason,CandidateNode}); @@ -145,7 +145,7 @@ call({Fun, FunArgs}, Module, Function, Args, TimeOut, Cookie) -> call(Node, Module, Function, Args, TimeOut, Cookie) when is_atom(Node) -> Cookie0 = set_the_cookie(Cookie), Result = rpc:call(Node, Module, Function, Args, TimeOut), - set_the_cookie(Cookie0), + _ = set_the_cookie(Cookie0), Result. %%% @spec cast(Node, Module, Function, Args) -> ok @@ -190,7 +190,7 @@ cast({Fun, FunArgs}, Module, Function, Args, Cookie) -> cast(Node, Module, Function, Args, Cookie) when is_atom(Node) -> Cookie0 = set_the_cookie(Cookie), true = rpc:cast(Node, Module, Function, Args), - set_the_cookie(Cookie0), + _ = set_the_cookie(Cookie0), ok. diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl index ae91601f67..05b1e70098 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-2014. All Rights Reserved. +%% Copyright Ericsson AB 2004-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -65,6 +65,7 @@ logdir, logopts = [], basic_html, + esc_chars = true, verbosity = [], config = [], event_handlers = [], @@ -75,8 +76,8 @@ abort_if_missing_suites, silent_connections = [], stylesheet, - multiply_timetraps = 1, - scale_timetraps = false, + multiply_timetraps, + scale_timetraps, create_priv_dir, testspec_files = [], current_testspec, @@ -120,13 +121,13 @@ script_start() -> %% used for purpose of testing the run_test interface io:format(user, "~n-------------------- START ARGS " "--------------------~n", []), - io:format(user, "--- Init args:~n~p~n", [FlagFilter(Init)]), - io:format(user, "--- CT args:~n~p~n", [FlagFilter(CtArgs)]), + io:format(user, "--- Init args:~n~tp~n", [FlagFilter(Init)]), + io:format(user, "--- CT args:~n~tp~n", [FlagFilter(CtArgs)]), EnvArgs = opts2args(EnvStartOpts), - io:format(user, "--- Env opts -> args:~n~p~n =>~n~p~n", + io:format(user, "--- Env opts -> args:~n~tp~n =>~n~tp~n", [EnvStartOpts,EnvArgs]), Merged = merge_arguments(CtArgs ++ EnvArgs), - io:format(user, "--- Merged args:~n~p~n", [FlagFilter(Merged)]), + io:format(user, "--- Merged args:~n~tp~n", [FlagFilter(Merged)]), io:format(user, "-----------------------------------" "-----------------~n~n", []), Merged; @@ -159,18 +160,18 @@ script_start(Args) -> {'EXIT',Pid,Reason} -> case Reason of {user_error,What} -> - io:format("\nTest run failed!\nReason: ~p\n\n\n", + io:format("\nTest run failed!\nReason: ~tp\n\n\n", [What]), finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); _ -> io:format("Test run crashed! " "This could be an internal error " "- please report!\n\n" - "~p\n\n\n", [Reason]), + "~tp\n\n\n", [Reason]), finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args) end; {Pid,{error,Reason}} -> - io:format("\nTest run failed! Reason:\n~p\n\n\n",[Reason]), + io:format("\nTest run failed! Reason:\n~tp\n\n\n",[Reason]), finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); {Pid,Result} -> io:nl(), @@ -218,7 +219,7 @@ analyze_test_result([], _) -> analyze_test_result(interactive_mode, _) -> interactive_mode; analyze_test_result(Unknown, _) -> - io:format("\nTest run failed! Reason:\n~p\n\n\n",[Unknown]), + io:format("\nTest run failed! Reason:\n~tp\n\n\n",[Unknown]), ?EXIT_STATUS_TEST_RUN_FAILED. finish(Tracing, ExitStatus, Args) -> @@ -249,6 +250,8 @@ finish(Tracing, ExitStatus, Args) -> end. script_start1(Parent, Args) -> + %% tag this process + ct_util:mark_process(), %% read general start flags Label = get_start_opt(label, fun([Lbl]) -> Lbl end, Args), Profile = get_start_opt(profile, fun([Prof]) -> Prof end, Args), @@ -263,11 +266,11 @@ script_start1(Parent, Args) -> [], Args), Verbosity = verbosity_args2opts(Args), MultTT = get_start_opt(multiply_timetraps, - fun([MT]) -> list_to_integer(MT) end, 1, Args), + fun([MT]) -> list_to_integer(MT) end, Args), ScaleTT = get_start_opt(scale_timetraps, fun([CT]) -> list_to_atom(CT); ([]) -> true - end, false, Args), + end, Args), CreatePrivDir = get_start_opt(create_priv_dir, fun([PD]) -> list_to_atom(PD); ([]) -> auto_per_tc @@ -346,6 +349,15 @@ script_start1(Parent, Args) -> application:set_env(common_test, basic_html, true), true end, + %% esc_chars - used by ct_logs + EscChars = case proplists:get_value(no_esc_chars, Args) of + undefined -> + application:set_env(common_test, esc_chars, true), + undefined; + _ -> + application:set_env(common_test, esc_chars, false), + false + end, %% disable_log_cache - used by ct_logs case proplists:get_value(disable_log_cache, Args) of undefined -> @@ -353,12 +365,19 @@ script_start1(Parent, Args) -> _ -> application:set_env(common_test, disable_log_cache, true) end, + %% log_cleanup - used by ct_logs + KeepLogs = get_start_opt(keep_logs, + fun ct_logs:parse_keep_logs/1, + all, + Args), + application:set_env(common_test, keep_logs, KeepLogs), Opts = #opts{label = Label, profile = Profile, vts = Vts, shell = Shell, cover = Cover, cover_stop = CoverStop, logdir = LogDir, logopts = LogOpts, basic_html = BasicHtml, + esc_chars = EscChars, verbosity = Verbosity, event_handlers = EvHandlers, ct_hooks = CTHooks, @@ -389,21 +408,21 @@ run_or_refresh(Opts = #opts{logdir = LogDir}, Args) -> [RefreshDir] -> ?abs(RefreshDir) end, {ok,Cwd} = file:get_cwd(), - file:set_cwd(LogDir1), + ok = file:set_cwd(LogDir1), %% give the shell time to print version etc timer:sleep(500), io:nl(), case catch ct_logs:make_all_runs_index(refresh) of {'EXIT',ARReason} -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), {error,{all_runs_index,ARReason}}; _ -> case catch ct_logs:make_all_suites_index(refresh) of {'EXIT',ASReason} -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), {error,{all_suites_index,ASReason}}; _ -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), io:format("Logs in ~ts refreshed!~n~n", [LogDir1]), timer:sleep(500), % time to flush io before quitting @@ -419,13 +438,17 @@ script_start2(Opts = #opts{vts = undefined, Specs1 = get_start_opt(join_specs, [Specs], Specs, Args), %% using testspec as input for test Relaxed = get_start_opt(allow_user_terms, true, false, Args), - case catch ct_testspec:collect_tests_from_file(Specs1, Relaxed) of - {E,Reason} when E == error ; E == 'EXIT' -> + try ct_testspec:collect_tests_from_file(Specs1, Relaxed) of + TestSpecData -> + execute_all_specs(TestSpecData, Opts, Args, []) + catch + throw:{error,Reason} -> StackTrace = erlang:get_stacktrace(), {error,{invalid_testspec,{Reason,StackTrace}}}; - TestSpecData -> - execute_all_specs(TestSpecData, Opts, Args, []) - end; + _:Reason -> + StackTrace = erlang:get_stacktrace(), + {error,{invalid_testspec,{Reason,StackTrace}}} + end; [] -> {error,no_testspec_specified}; _ -> % no testspec used @@ -587,6 +610,17 @@ combine_test_opts(TS, Specs, Opts) -> BHBool end, + EscChars = + case choose_val(Opts#opts.esc_chars, + TSOpts#opts.esc_chars) of + undefined -> + true; + ECBool -> + application:set_env(common_test, esc_chars, + ECBool), + ECBool + end, + Opts#opts{label = Label, profile = Profile, testspec_files = Specs, @@ -595,6 +629,7 @@ combine_test_opts(TS, Specs, Opts) -> logdir = which(logdir, LogDir), logopts = AllLogOpts, basic_html = BasicHtml, + esc_chars = EscChars, verbosity = AllVerbosity, silent_connections = AllSilentConns, config = TSOpts#opts.config, @@ -727,14 +762,14 @@ script_start4(#opts{label = Label, profile = Profile, if Config == [] -> ok; true -> - io:format("\nInstalling: ~p\n\n", [Config]) + io:format("\nInstalling: ~tp\n\n", [Config]) end, case install([{config,Config},{event_handler,EvHandlers}, {ct_hooks, CTHooks}, {enable_builtin_hooks,EnableBuiltinHooks}]) of ok -> - ct_util:start(interactive, LogDir, - add_verbosity_defaults(Verbosity)), + _ = ct_util:start(interactive, LogDir, + add_verbosity_defaults(Verbosity)), ct_util:set_testdata({logopts, LogOpts}), log_ts_names(Specs), io:nl(), @@ -795,6 +830,7 @@ script_usage() -> "\n\t [-scale_timetraps]" "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t [-basic_html]" + "\n\t [-no_esc_chars]" "\n\t [-repeat N] |" "\n\t [-duration HHMMSS [-force_stop [skip_rest]]] |" "\n\t [-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]" @@ -822,6 +858,7 @@ script_usage() -> "\n\t [-scale_timetraps]" "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]" "\n\t [-basic_html]" + "\n\t [-no_esc_chars]" "\n\t [-repeat N] |" "\n\t [-duration HHMMSS [-force_stop [skip_rest]]] |" "\n\t [-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]\n\n"), @@ -847,7 +884,8 @@ script_usage() -> "\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"). + "\n\t [-basic_html]" + "\n\t [-no_esc_chars]\n\n"). %%%----------------------------------------------------------------- %%% @hidden @@ -873,11 +911,10 @@ install(Opts, LogDir) -> case whereis(ct_util_server) of undefined -> VarFile = variables_file_name(LogDir), - case file:open(VarFile, [write]) of + case file:open(VarFile, [write, {encoding,utf8}]) of {ok,Fd} -> - [io:format(Fd, "~p.\n", [Opt]) || Opt <- ConfOpts ], - file:close(Fd), - ok; + _ = [io:format(Fd, "~tp.\n", [Opt]) || Opt <- ConfOpts], + ok = file:close(Fd); {error,Reason} -> io:format("CT failed to install configuration data. Please " "verify that the log directory exists and that " @@ -909,7 +946,7 @@ run_test(StartOpt) when is_tuple(StartOpt) -> run_test([StartOpt]); run_test(StartOpts) when is_list(StartOpts) -> - CTPid = spawn(fun() -> run_test1(StartOpts) end), + CTPid = spawn(run_test1_fun(StartOpts)), Ref = monitor(process, CTPid), receive {'DOWN',Ref,process,CTPid,{user_error,Error}} -> @@ -918,6 +955,14 @@ run_test(StartOpts) when is_list(StartOpts) -> Other end. +-spec run_test1_fun(_) -> fun(() -> no_return()). + +run_test1_fun(StartOpts) -> + fun() -> + ct_util:mark_process(), + run_test1(StartOpts) + end. + run_test1(StartOpts) when is_list(StartOpts) -> case proplists:get_value(refresh_logs, StartOpts) of undefined -> @@ -929,7 +974,7 @@ run_test1(StartOpts) when is_list(StartOpts) -> false -> case catch run_test2(StartOpts) of {'EXIT',Reason} -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), {error,Reason}; Result -> Result @@ -940,7 +985,13 @@ run_test1(StartOpts) when is_list(StartOpts) -> stop_trace(Tracing), exit(Res); RefreshDir -> - refresh_logs(?abs(RefreshDir)), + %% log_cleanup - used by ct_logs + KeepLogs = get_start_opt(keep_logs, + fun ct_logs:parse_keep_logs/1, + all, + StartOpts), + application:set_env(common_test, keep_logs, KeepLogs), + ok = refresh_logs(?abs(RefreshDir)), exit(done) end. @@ -1025,8 +1076,8 @@ run_test2(StartOpts) -> CoverStop = get_start_opt(cover_stop, value, StartOpts), %% timetrap manipulation - MultiplyTT = get_start_opt(multiply_timetraps, value, 1, StartOpts), - ScaleTT = get_start_opt(scale_timetraps, value, false, StartOpts), + MultiplyTT = get_start_opt(multiply_timetraps, value, StartOpts), + ScaleTT = get_start_opt(scale_timetraps, value, StartOpts), %% create unique priv dir names CreatePrivDir = get_start_opt(create_priv_dir, value, StartOpts), @@ -1084,13 +1135,29 @@ run_test2(StartOpts) -> application:set_env(common_test, basic_html, BasicHtmlBool), BasicHtmlBool end, - + %% esc_chars - used by ct_logs + EscChars = + case proplists:get_value(esc_chars, StartOpts) of + undefined -> + application:set_env(common_test, esc_chars, true), + undefined; + EscCharsBool -> + application:set_env(common_test, esc_chars, EscCharsBool), + EscCharsBool + end, + %% disable_log_cache - used by ct_logs case proplists:get_value(disable_log_cache, StartOpts) of undefined -> application:set_env(common_test, disable_log_cache, false); DisableCacheBool -> application:set_env(common_test, disable_log_cache, DisableCacheBool) end, + %% log_cleanup - used by ct_logs + KeepLogs = get_start_opt(keep_logs, + fun ct_logs:parse_keep_logs/1, + all, + StartOpts), + application:set_env(common_test, keep_logs, KeepLogs), %% stepped execution Step = get_start_opt(step, value, StartOpts), @@ -1099,6 +1166,7 @@ run_test2(StartOpts) -> cover = Cover, cover_stop = CoverStop, step = Step, logdir = LogDir, logopts = LogOpts, basic_html = BasicHtml, + esc_chars = EscChars, config = CfgFiles, verbosity = Verbosity, event_handlers = EvHandlers, @@ -1139,12 +1207,16 @@ run_spec_file(Relaxed, end, AbsSpecs = lists:map(fun(SF) -> ?abs(SF) end, Specs1), AbsSpecs1 = get_start_opt(join_specs, [AbsSpecs], AbsSpecs, StartOpts), - case catch ct_testspec:collect_tests_from_file(AbsSpecs1, Relaxed) of - {Error,CTReason} when Error == error ; Error == 'EXIT' -> - StackTrace = erlang:get_stacktrace(), - exit({error,{invalid_testspec,{CTReason,StackTrace}}}); + try ct_testspec:collect_tests_from_file(AbsSpecs1, Relaxed) of TestSpecData -> run_all_specs(TestSpecData, Opts, StartOpts, []) + catch + throw:{error,CTReason} -> + StackTrace = erlang:get_stacktrace(), + exit({error,{invalid_testspec,{CTReason,StackTrace}}}); + _:CTReason -> + StackTrace = erlang:get_stacktrace(), + exit({error,{invalid_testspec,{CTReason,StackTrace}}}) end. run_all_specs([], _, _, TotResult) -> @@ -1167,7 +1239,6 @@ run_all_specs([], _, _, TotResult) -> end; run_all_specs([{Specs,TS} | TSs], Opts, StartOpts, TotResult) -> - log_ts_names(Specs), Combined = #opts{config = TSConfig} = combine_test_opts(TS, Specs, Opts), AllConfig = merge_vals([Opts#opts.config, TSConfig]), try run_one_spec(TS, @@ -1369,7 +1440,7 @@ run_dir(Opts = #opts{logdir = LogDir, %%% @equiv ct:run_testspec/1 %%%----------------------------------------------------------------- run_testspec(TestSpec) -> - CTPid = spawn(fun() -> run_testspec1(TestSpec) end), + CTPid = spawn(run_testspec1_fun(TestSpec)), Ref = monitor(process, CTPid), receive {'DOWN',Ref,process,CTPid,{user_error,Error}} -> @@ -1378,12 +1449,20 @@ run_testspec(TestSpec) -> Other end. +-spec run_testspec1_fun(_) -> fun(() -> no_return()). + +run_testspec1_fun(TestSpec) -> + fun() -> + ct_util:mark_process(), + run_testspec1(TestSpec) + end. + run_testspec1(TestSpec) -> {ok,Cwd} = file:get_cwd(), io:format("~nCommon Test starting (cwd is ~ts)~n~n", [Cwd]), case catch run_testspec2(TestSpec) of {'EXIT',Reason} -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), exit({error,Reason}); Result -> exit(Result) @@ -1435,6 +1514,7 @@ get_data_for_node(#testspec{label = Labels, logdir = LogDirs, logopts = LogOptsList, basic_html = BHs, + esc_chars = EscChs, stylesheet = SSs, verbosity = VLvls, silent_connections = SilentConnsList, @@ -1462,6 +1542,7 @@ get_data_for_node(#testspec{label = Labels, LOs -> LOs end, BasicHtml = proplists:get_value(Node, BHs), + EscChars = proplists:get_value(Node, EscChs), Stylesheet = proplists:get_value(Node, SSs), Verbosity = case proplists:get_value(Node, VLvls) of undefined -> []; @@ -1488,6 +1569,7 @@ get_data_for_node(#testspec{label = Labels, logdir = LogDir, logopts = LogOpts, basic_html = BasicHtml, + esc_chars = EscChars, stylesheet = Stylesheet, verbosity = Verbosity, silent_connections = SilentConns, @@ -1512,15 +1594,15 @@ refresh_logs(LogDir) -> _ -> case catch ct_logs:make_all_suites_index(refresh) of {'EXIT',ASReason} -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), {error,{all_suites_index,ASReason}}; _ -> case catch ct_logs:make_all_runs_index(refresh) of {'EXIT',ARReason} -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), {error,{all_runs_index,ARReason}}; _ -> - file:set_cwd(Cwd), + ok = file:set_cwd(Cwd), io:format("Logs in ~ts refreshed!~n",[LogDir]), ok end @@ -1560,22 +1642,34 @@ delistify(E) -> E. %%% @hidden %%% @equiv ct:run/3 run(TestDir, Suite, Cases) -> - install([]), - reformat_result(catch do_run(tests(TestDir, Suite, Cases), [])). + case install([]) of + ok -> + reformat_result(catch do_run(tests(TestDir, Suite, Cases), [])); + Error -> + Error + end. %%%----------------------------------------------------------------- %%% @hidden %%% @equiv ct:run/2 run(TestDir, Suite) when is_list(TestDir), is_integer(hd(TestDir)) -> - install([]), - reformat_result(catch do_run(tests(TestDir, Suite), [])). + case install([]) of + ok -> + reformat_result(catch do_run(tests(TestDir, Suite), [])); + Error -> + Error + end. %%%----------------------------------------------------------------- %%% @hidden %%% @equiv ct:run/1 run(TestDirs) -> - install([]), - reformat_result(catch do_run(tests(TestDirs), [])). + case install([]) of + ok -> + reformat_result(catch do_run(tests(TestDirs), [])); + Error -> + Error + end. reformat_result({'EXIT',{user_error,Reason}}) -> {error,Reason}; @@ -1742,10 +1836,10 @@ compile_and_run(Tests, Skip, Opts, Args) -> case lists:member(all, Conns) of true -> Conns1 = ct_util:override_silence_all_connections(), - ct_logs:log("Silent connections", "~p", [Conns1]); + ct_logs:log("Silent connections", "~tp", [Conns1]); false -> ct_util:override_silence_connections(Conns), - ct_logs:log("Silent connections", "~p", [Conns]) + ct_logs:log("Silent connections", "~tp", [Conns]) end end, log_ts_names(Opts#opts.testspec_files), @@ -1771,7 +1865,18 @@ compile_and_run(Tests, Skip, Opts, Args) -> {Tests1,Skip1} -> ReleaseSh = proplists:get_value(release_shell, Args), ct_util:set_testdata({release_shell,ReleaseSh}), - possibly_spawn(ReleaseSh == true, Tests1, Skip1, Opts) + TestResult = + possibly_spawn(ReleaseSh == true, Tests1, Skip1, Opts), + case TestResult of + {Ok,Errors,Skipped} -> + NoOfMakeErrors = + lists:foldl(fun({_,BadMods}, X) -> + X + length(BadMods) + end, 0, SuiteMakeErrors), + {Ok,Errors+NoOfMakeErrors,Skipped}; + ErrorResult -> + ErrorResult + end catch _:BadFormat -> {error,BadFormat} @@ -1809,10 +1914,12 @@ possibly_spawn(true, Tests, Skip, Opts) -> CTUtilSrv = whereis(ct_util_server), Supervisor = fun() -> + ct_util:mark_process(), process_flag(trap_exit, true), link(CTUtilSrv), TestRun = fun() -> + ct_util:mark_process(), TestResult = (catch do_run_test(Tests, Skip, Opts)), case TestResult of {EType,_} = Error when EType == user_error; @@ -1827,7 +1934,7 @@ possibly_spawn(true, Tests, Skip, Opts) -> TestRunPid = spawn_link(TestRun), receive {'EXIT',TestRunPid,{ok,TestResult}} -> - io:format(user, "~nCommon Test returned ~p~n~n", + io:format(user, "~nCommon Test returned ~tp~n~n", [TestResult]); {'EXIT',TestRunPid,Error} -> exit(Error) @@ -1846,7 +1953,7 @@ auto_compile(TestSuites) -> case application:get_env(common_test, include) of {ok,UserInclDirs} when length(UserInclDirs) > 0 -> io:format("Including the following directories:~n"), - [begin io:format("~p~n",[UserInclDir]), {i,UserInclDir} end || + [begin io:format("~tp~n",[UserInclDir]), {i,UserInclDir} end || UserInclDir <- UserInclDirs]; _ -> [] @@ -1956,7 +2063,7 @@ save_make_errors(Errors) -> "Error compiling or locating the " "following suites: ~n~p",[Suites]), %% save the info for logger - file:write_file(?missing_suites_info,term_to_binary(Errors)), + ok = file:write_file(?missing_suites_info,term_to_binary(Errors)), Errors. get_bad_suites([{{_TestDir,_Suite},Failed}|Errors], BadSuites) -> @@ -2034,6 +2141,13 @@ final_tests1([{TestDir,Suite,GrsOrCs}|Tests], Final, Skip, Bad) when ({skipped,Group,TCs}) -> [ct_groups:make_conf(TestDir, Suite, Group, [skipped], TCs)]; + ({skipped,TC}) -> + case lists:member(TC, GrsOrCs) of + true -> + []; + false -> + [TC] + end; ({GrSpec = {GroupName,_},TCs}) -> Props = [{override,GrSpec}], [ct_groups:make_conf(TestDir, Suite, @@ -2073,10 +2187,12 @@ final_skip([], Final) -> continue([], _) -> true; -continue(_MakeErrors, AbortIfMissingSuites) -> +continue(_MakeErrors, true) -> + false; +continue(_MakeErrors, _AbortIfMissingSuites) -> io:nl(), - OldGl = group_leader(), - case set_group_leader_same_as_shell() of + OldGL = group_leader(), + case set_group_leader_same_as_shell(OldGL) of true -> S = self(), io:format("Failed to compile or locate one " @@ -2092,7 +2208,7 @@ continue(_MakeErrors, AbortIfMissingSuites) -> S ! false end end), - group_leader(OldGl, self()), + group_leader(OldGL, self()), receive R when R==true; R==false -> R after 15000 -> @@ -2101,11 +2217,12 @@ continue(_MakeErrors, AbortIfMissingSuites) -> true end; false -> % no shell process to use - not AbortIfMissingSuites + true end. -set_group_leader_same_as_shell() -> - %%! Locate the shell process... UGLY!!! +set_group_leader_same_as_shell(OldGL) -> + %% find the group leader process on the node in a dirty fashion + %% (check initial function call and look in the process dictionary) GS2or3 = fun(P) -> case process_info(P,initial_call) of {initial_call,{group,server,X}} when X == 2 ; X == 3 -> @@ -2118,7 +2235,10 @@ set_group_leader_same_as_shell() -> true == lists:keymember(shell,1, element(2,process_info(P,dictionary)))] of [GL|_] -> - group_leader(GL, self()); + %% check if started from remote node (skip interaction) + if node(OldGL) /= node(GL) -> false; + true -> group_leader(GL, self()) + end; [] -> false end. @@ -2153,11 +2273,19 @@ do_run_test(Tests, Skip, Opts0) -> %% test_server needs to know the include path too InclPath = case application:get_env(common_test, include) of {ok,Incls} -> Incls; - _ -> [] + _ -> [] end, application:set_env(test_server, include, InclPath), - test_server_ctrl:start_link(local), + %% copy the escape characters setting to test_server + EscChars = + case application:get_env(common_test, esc_chars) of + {ok,ECBool} -> ECBool; + _ -> true + end, + application:set_env(test_server, esc_chars, EscChars), + + {ok, _} = test_server_ctrl:start_link(local), %% let test_server expand the test tuples and count no of cases {Suites,NoOfCases} = count_test_cases(Tests, Skip), @@ -2166,7 +2294,7 @@ do_run_test(Tests, Skip, Opts0) -> NoOfSuites = length(Suites1), ct_util:warn_duplicates(Suites1), {ok,Cwd} = file:get_cwd(), - io:format("~nCWD set to: ~p~n", [Cwd]), + io:format("~nCWD set to: ~tp~n", [Cwd]), if NoOfCases == unknown -> io:format("~nTEST INFO: ~w test(s), ~w suite(s)~n~n", [NoOfTests,NoOfSuites]), @@ -2188,8 +2316,19 @@ do_run_test(Tests, Skip, Opts0) -> _Lower -> ok end, - test_server_ctrl:multiply_timetraps(Opts0#opts.multiply_timetraps), - test_server_ctrl:scale_timetraps(Opts0#opts.scale_timetraps), + + case Opts0#opts.multiply_timetraps of + undefined -> MultTT = 1; + MultTT -> MultTT + end, + case Opts0#opts.scale_timetraps of + undefined -> ScaleTT = false; + ScaleTT -> ScaleTT + end, + ct_logs:log("TEST INFO","Timetrap time multiplier = ~w~n" + "Timetrap scaling enabled = ~w", [MultTT,ScaleTT]), + test_server_ctrl:multiply_timetraps(MultTT), + test_server_ctrl:scale_timetraps(ScaleTT), test_server_ctrl:create_priv_dir(choose_val( Opts0#opts.create_priv_dir, @@ -2218,14 +2357,14 @@ do_run_test(Tests, Skip, Opts0) -> lists:foreach(fun(Suite) -> maybe_cleanup_interpret(Suite, Opts#opts.step) end, CleanUp), - [code:del_path(Dir) || Dir <- AddedToPath], + _ = [code:del_path(Dir) || Dir <- AddedToPath], %% If a severe error has occurred in the test_server, %% we will generate an exception here. case ct_util:get_testdata(severe_error) of undefined -> ok; SevereError -> - ct_logs:log("SEVERE ERROR", "~p\n", [SevereError]), + ct_logs:log("SEVERE ERROR", "~tp\n", [SevereError]), exit(SevereError) end, @@ -2296,7 +2435,7 @@ start_cover(Opts=#opts{coverspec=CovData,cover_stop=CovStop},LogDir) -> if (CovNodes /= []) and (CovNodes /= undefined) -> ct_logs:log("COVER INFO", "Nodes included in cover " - "session: ~w", + "session: ~tw", [CovNodes]), cover:start(CovNodes); true -> @@ -2310,7 +2449,7 @@ start_cover(Opts=#opts{coverspec=CovData,cover_stop=CovStop},LogDir) -> {error,Reason} -> ct_logs:log("COVER INFO", "Importing cover data from: ~ts fails! " - "Reason: ~p", [Imp,Reason]) + "Reason: ~tp", [Imp,Reason]) end end, CovImport), {TsCoverInfo,Opts}. @@ -2345,7 +2484,7 @@ count_test_cases(Tests, Skip) -> SendResult = fun(Me, Result) -> Me ! {no_of_cases,Result} end, TSPid = test_server_ctrl:start_get_totals(SendResult), Ref = erlang:monitor(process, TSPid), - add_jobs(Tests, Skip, #opts{}, []), + _ = add_jobs(Tests, Skip, #opts{}, []), Counted = (catch count_test_cases1(length(Tests), 0, [], Ref)), erlang:demonitor(Ref, [flush]), case Counted of @@ -2605,11 +2744,9 @@ run_make(Targets, TestDir0, Mod, UserInclude) -> data=TestDir}), {ok,Cwd} = file:get_cwd(), ok = file:set_cwd(TestDir), - TestServerInclude = get_dir(test_server, "include"), CtInclude = get_dir(common_test, "include"), XmerlInclude = get_dir(xmerl, "include"), - ErlFlags = UserInclude ++ [{i,TestServerInclude}, - {i,CtInclude}, + ErlFlags = UserInclude ++ [{i,CtInclude}, {i,XmerlInclude}, debug_info], Result = @@ -2646,7 +2783,7 @@ run_make(Targets, TestDir0, Mod, UserInclude) -> {up_to_date,_} -> ok; {'EXIT',Reason} -> - io:format("{error,{make_crashed,~p}\n", [Reason]), + io:format("{error,{make_crashed,~tp}\n", [Reason]), {error,{make_crashed,TestDir,Reason}}; {error,ModInfo} -> io:format("{error,make_failed}\n", []), @@ -2655,7 +2792,7 @@ run_make(Targets, TestDir0, Mod, UserInclude) -> {error,{make_failed,Bad}} end; {error,_} -> - io:format("{error,{invalid_directory,~p}}\n", [TestDir0]), + io:format("{error,{invalid_directory,~tp}}\n", [TestDir0]), {error,{invalid_directory,TestDir0}} end. @@ -2701,14 +2838,14 @@ maybe_interpret1(Suite, Cases, StepOpts) when is_list(Cases) -> maybe_interpret2(Suite, Cases, StepOpts) -> set_break_on_config(Suite, StepOpts), - [begin try i:ib(Suite, Case, 1) of + _ = [begin try i:ib(Suite, Case, 1) of _ -> ok catch _:_Error -> - io:format(user, "Invalid breakpoint: ~w:~w/1~n", + io:format(user, "Invalid breakpoint: ~w:~tw/1~n", [Suite,Case]) end - end || Case <- Cases, is_atom(Case)], + end || Case <- Cases, is_atom(Case)], test_server_ctrl:multiply_timetraps(infinity), WinOp = case lists:member(keep_inactive, ensure_atom(StepOpts)) of true -> no_kill; @@ -2727,12 +2864,12 @@ set_break_on_config(Suite, StepOpts) -> false -> ok end end, - SetBPIfExists(init_per_suite, 1), - SetBPIfExists(init_per_group, 2), - SetBPIfExists(init_per_testcase, 2), - SetBPIfExists(end_per_testcase, 2), - SetBPIfExists(end_per_group, 2), - SetBPIfExists(end_per_suite, 1); + ok = SetBPIfExists(init_per_suite, 1), + ok = SetBPIfExists(init_per_group, 2), + ok = SetBPIfExists(init_per_testcase, 2), + ok = SetBPIfExists(end_per_testcase, 2), + ok = SetBPIfExists(end_per_group, 2), + ok = SetBPIfExists(end_per_suite, 1); false -> ok end. @@ -2836,7 +2973,7 @@ ct_hooks_args2opts([],Acc) -> parse_cth_args(String) -> try - true = io_lib:printable_list(String), + true = io_lib:printable_unicode_list(String), {ok,Toks,_} = erl_scan:string(String++"."), {ok, Args} = erl_parse:parse_term(Toks), Args @@ -2909,31 +3046,31 @@ add_verbosity_defaults(VLvls) -> %% relative dirs "post run_test erl_args" is not kept! rel_to_abs(CtArgs) -> {PA,PZ} = get_pa_pz(CtArgs, [], []), - [begin + _ = [begin Dir = rm_trailing_slash(D), Abs = make_abs(Dir), - if Dir /= Abs -> - code:del_path(Dir), - code:del_path(Abs), - io:format(user, "Converting ~p to ~p and re-inserting " + _ = if Dir /= Abs -> + _ = code:del_path(Dir), + _ = code:del_path(Abs), + io:format(user, "Converting ~tp to ~tp and re-inserting " "with add_pathz/1~n", [Dir, Abs]); true -> - code:del_path(Dir) + _ = code:del_path(Dir) end, code:add_pathz(Abs) end || D <- PZ], - [begin + _ = [begin Dir = rm_trailing_slash(D), Abs = make_abs(Dir), - if Dir /= Abs -> - code:del_path(Dir), - code:del_path(Abs), - io:format(user, "Converting ~p to ~p and re-inserting " + _ = if Dir /= Abs -> + _ = code:del_path(Dir), + _ = code:del_path(Abs), + io:format(user, "Converting ~tp to ~tp and re-inserting " "with add_patha/1~n", [Dir, Abs]); true -> - code:del_path(Dir) + _ = code:del_path(Dir) end, code:add_patha(Abs) end || D <- PA], @@ -2999,7 +3136,7 @@ opts2args(EnvStartOpts) -> ({group,G}) when is_atom(G) -> [{group,[atom_to_list(G)]}]; ({group,Gs}) when is_list(Gs) -> - LOfGStrs = [lists:flatten(io_lib:format("~w",[G])) || + LOfGStrs = [lists:flatten(io_lib:format("~tw",[G])) || G <- Gs], [{group,LOfGStrs}]; ({testcase,Case}) when is_atom(Case) -> @@ -3042,15 +3179,19 @@ opts2args(EnvStartOpts) -> [{basic_html,[]}]; ({basic_html,false}) -> []; + ({esc_chars,false}) -> + [{no_esc_chars,[]}]; + ({esc_chars,true}) -> + []; ({event_handler,EH}) when is_atom(EH) -> [{event_handler,[atom_to_list(EH)]}]; ({event_handler,EHs}) when is_list(EHs) -> [{event_handler,[atom_to_list(EH) || EH <- EHs]}]; ({event_handler,{EH,Arg}}) when is_atom(EH) -> - ArgStr = lists:flatten(io_lib:format("~p", [Arg])), + ArgStr = lists:flatten(io_lib:format("~tp", [Arg])), [{event_handler_init,[atom_to_list(EH),ArgStr]}]; ({event_handler,{EHs,Arg}}) when is_list(EHs) -> - ArgStr = lists:flatten(io_lib:format("~p", [Arg])), + ArgStr = lists:flatten(io_lib:format("~tp", [Arg])), Strs = lists:flatmap(fun(EH) -> [atom_to_list(EH), ArgStr,"and"] @@ -3081,25 +3222,25 @@ opts2args(EnvStartOpts) -> ({ct_hooks,[]}) -> []; ({ct_hooks,CTHs}) when is_list(CTHs) -> - io:format(user,"ct_hooks: ~p",[CTHs]), + io:format(user,"ct_hooks: ~tp",[CTHs]), Strs = lists:flatmap( fun({CTH,Arg,Prio}) -> [atom_to_list(CTH), lists:flatten( - io_lib:format("~p",[Arg])), + io_lib:format("~tp",[Arg])), lists:flatten( - io_lib:format("~p",[Prio])), + io_lib:format("~tp",[Prio])), "and"]; ({CTH,Arg}) -> [atom_to_list(CTH), lists:flatten( - io_lib:format("~p",[Arg])), + io_lib:format("~tp",[Arg])), "and"]; (CTH) when is_atom(CTH) -> [atom_to_list(CTH),"and"] end,CTHs), [_LastAnd|StrsR] = lists:reverse(Strs), - io:format(user,"return: ~p",[lists:reverse(StrsR)]), + io:format(user,"return: ~tp",[lists:reverse(StrsR)]), [{ct_hooks,lists:reverse(StrsR)}]; ({Opt,As=[A|_]}) when is_atom(A) -> [{Opt,[atom_to_list(Atom) || Atom <- As]}]; @@ -3181,7 +3322,7 @@ start_trace(Args) -> ok -> true; {_,Error} -> - io:format("Warning! Tracing not started. Reason: ~p~n~n", + io:format("Warning! Tracing not started. Reason: ~tp~n~n", [Error]), false end; diff --git a/lib/common_test/src/ct_slave.erl b/lib/common_test/src/ct_slave.erl index 32a1ff4dbc..b39195483b 100644 --- a/lib/common_test/src/ct_slave.erl +++ b/lib/common_test/src/ct_slave.erl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2013. All Rights Reserved. +%% Copyright Ericsson AB 2010-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -134,7 +134,7 @@ start(Host, Node) -> %%% executed after startup of the node. Note that all used modules should be %%% present in the code path on the <code>Host</code>.</p> %%% -%%% <p>The timeouts are applied as follows: +%%% <p>The timeouts are applied as follows:</p> %%% <list> %%% <item> %%% <code>BootTimeout</code> - time to start the Erlang node, in seconds. @@ -154,7 +154,7 @@ start(Host, Node) -> %%% If this timeout occurs, the result %%% <code>{error, startup_timeout, NodeName}</code> is returned. %%% </item> -%%% </list></p> +%%% </list> %%% %%% <p>Option <code>monitor_master</code> specifies, if the slave node should be %%% stopped in case of master node stop. Defaults to false.</p> @@ -170,7 +170,7 @@ start(Host, Node) -> %%% <p>Option <code>env</code> specifies a list of environment variables %%% that will extended the environment.</p> %%% -%%% <p>Special return values are: +%%% <p>Special return values are:</p> %%% <list> %%% <item><code>{error, already_started, NodeName}</code> - if the node with %%% the given name is already started on a given host;</item> @@ -179,7 +179,7 @@ start(Host, Node) -> %%% <item><code>{error, not_alive, NodeName}</code> - if node on which the %%% <code>ct_slave:start/3</code> is called, is not alive. Note that %%% <code>NodeName</code> is the name of current node in this case.</item> -%%% </list></p> +%%% </list> %%% start(Host, Node, Opts) -> ENode = enodename(Host, Node), @@ -282,6 +282,7 @@ monitor_master(MasterNode) -> % code of the masterdeath-waiter process monitor_master_int(MasterNode) -> + ct_util:mark_process(), erlang:monitor_node(MasterNode, true), receive {nodedown, MasterNode}-> @@ -309,13 +310,18 @@ is_started(ENode) -> % make a Erlang node name from name and hostname enodename(Host, Node) -> - list_to_atom(atom_to_list(Node)++"@"++atom_to_list(Host)). + case lists:member($@, atom_to_list(Node)) of + true -> + Node; + false -> + list_to_atom(atom_to_list(Node)++"@"++atom_to_list(Host)) + end. % performs actual start of the "slave" node do_start(Host, Node, Options) -> ENode = enodename(Host, Node), Functions = - lists:concat([[{ct_slave, slave_started, [ENode, self()]}], + lists:append([[{ct_slave, slave_started, [ENode, self()]}], Options#options.startup_functions, [{ct_slave, slave_ready, [ENode, self()]}]]), Functions2 = if @@ -325,7 +331,7 @@ do_start(Host, Node, Options) -> Functions end, MasterHost = gethostname(), - if + _ = if MasterHost == Host -> spawn_local_node(Node, Options); true-> @@ -359,7 +365,7 @@ do_start(Host, Node, Options) -> pang-> {error, boot_timeout, ENode} end, - case Result of + _ = case Result of {ok, ENode}-> ok; {error, Timeout, ENode} @@ -422,7 +428,7 @@ spawn_remote_node(Host, Node, Options) -> {_, _}-> [{user, Username}, {password, Password}] end ++ [{silently_accept_hosts, true}] ++ SSHOpts, - application:ensure_all_started(ssh), + {ok, _} = application:ensure_all_started(ssh), {ok, SSHConnRef} = ssh:connect(atom_to_list(Host), SSHPort, SSHOptions), {ok, SSHChannelId} = ssh_connection:session_channel(SSHConnRef, infinity), ssh_setenv(SSHConnRef, SSHChannelId, Env), diff --git a/lib/common_test/src/ct_snmp.erl b/lib/common_test/src/ct_snmp.erl index 95098bdaca..5844909d17 100644 --- a/lib/common_test/src/ct_snmp.erl +++ b/lib/common_test/src/ct_snmp.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2012. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ %%% @doc Common Test user interface module for the OTP snmp application %%% -%%% The purpose of this module is to make snmp configuration easier for +%%% <p>The purpose of this module is to make snmp configuration easier for %%% the test case writer. Many test cases can use default values for common %%% operations and then no snmp configuration files need to be supplied. When %%% it is necessary to change particular configuration parameters, a subset @@ -31,7 +31,7 @@ %%% To simplify the test suite, Common Test keeps track %%% of some of the snmp manager information. This way the test suite doesn't %%% have to handle as many input parameters as it would if it had to interface the -%%% OTP snmp manager directly. +%%% OTP snmp manager directly.</p> %%% %%% <p> The following snmp manager and agent parameters are configurable: </p> %%% @@ -221,9 +221,17 @@ start(Config, MgrAgentConfName, SnmpAppConfName) -> Config, SysName, AgentManagerIP, IP), setup_manager(StartManager, MgrAgentConfName, SnmpAppConfName, Config, AgentManagerIP), - application:start(snmp), + ok = start_application(snmp), manager_register(StartManager, MgrAgentConfName). + +start_application(App) -> + case application:start(App) of + {error, {already_started, App}} -> + ok; + Else -> + Else + end. %%% @spec stop(Config) -> ok %%% Config = [{Key, Value}] @@ -233,8 +241,8 @@ start(Config, MgrAgentConfName, SnmpAppConfName) -> %%% @doc Stops the snmp manager and/or agent removes all files created. stop(Config) -> PrivDir = ?config(priv_dir, Config), - application:stop(snmp), - application:stop(mnesia), + ok = application:stop(snmp), + ok = application:stop(mnesia), MgrDir = filename:join(PrivDir,"mgr"), ConfDir = filename:join(PrivDir, "conf"), DbDir = filename:join(PrivDir,"db"), @@ -311,7 +319,7 @@ set_info(Config) -> SetLogFile = filename:join(PrivDir, ?CT_SNMP_LOG_FILE), case file:consult(SetLogFile) of {ok, SetInfo} -> - file:delete(SetLogFile), + ok = delete_file(SetLogFile), lists:reverse(SetInfo); _ -> [] @@ -326,9 +334,9 @@ set_info(Config) -> %%% @doc Register the manager entity (=user) responsible for specific agent(s). %%% Corresponds to making an entry in users.conf. %%% -%%% This function will try to register the given users, without +%%% <p>This function will try to register the given users, without %%% checking if any of them already exist. In order to change an -%%% already registered user, the user must first be unregistered. +%%% already registered user, the user must first be unregistered.</p> register_users(MgrAgentConfName, Users) -> case setup_users(Users) of ok -> @@ -351,10 +359,10 @@ register_users(MgrAgentConfName, Users) -> %%% @doc Explicitly instruct the manager to handle this agent. %%% Corresponds to making an entry in agents.conf %%% -%%% This function will try to register the given managed agents, +%%% <p>This function will try to register the given managed agents, %%% without checking if any of them already exist. In order to change %%% an already registered managed agent, the agent must first be -%%% unregistered. +%%% unregistered.</p> register_agents(MgrAgentConfName, ManagedAgents) -> case setup_managed_agents(MgrAgentConfName,ManagedAgents) of ok -> @@ -378,9 +386,9 @@ register_agents(MgrAgentConfName, ManagedAgents) -> %%% @doc Explicitly instruct the manager to handle this USM user. %%% Corresponds to making an entry in usm.conf %%% -%%% This function will try to register the given users, without +%%% <p>This function will try to register the given users, without %%% checking if any of them already exist. In order to change an -%%% already registered user, the user must first be unregistered. +%%% already registered user, the user must first be unregistered.</p> register_usm_users(MgrAgentConfName, UsmUsers) -> EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID), case setup_usm_users(UsmUsers, EngineID) of @@ -513,7 +521,7 @@ unload_mibs(Mibs) -> prepare_snmp_env() -> %% To make sure application:set_env is not overwritten by any %% app-file settings. - application:load(snmp), + _ = application:load(snmp), %% Fix for older versions of snmp where there are some %% inappropriate default values for alway starting an @@ -533,7 +541,7 @@ setup_manager(true, MgrConfName, SnmpConfName, Config, IP) -> Users = [], Agents = [], Usms = [], - file:make_dir(MgrDir), + ok = make_dir(MgrDir), snmp_config:write_manager_snmp_files(MgrDir, IP, Port, MaxMsgSize, EngineID, Users, Agents, Usms), @@ -549,7 +557,7 @@ setup_agent(false,_, _, _, _, _, _) -> ok; setup_agent(true, AgentConfName, SnmpConfName, Config, SysName, ManagerIP, AgentIP) -> - application:start(mnesia), + ok = start_application(mnesia), PrivDir = ?config(priv_dir, Config), Vsns = ct:get_config({AgentConfName, agent_vsns}, ?CONF_FILE_VER), TrapUdp = ct:get_config({AgentConfName, agent_trap_udp}, ?TRAP_UDP), @@ -565,8 +573,8 @@ setup_agent(true, AgentConfName, SnmpConfName, ConfDir = filename:join(PrivDir, "conf"), DbDir = filename:join(PrivDir,"db"), - file:make_dir(ConfDir), - file:make_dir(DbDir), + ok = make_dir(ConfDir), + ok = make_dir(DbDir), snmp_config:write_agent_snmp_files(ConfDir, Vsns, ManagerIP, TrapUdp, AgentIP, AgentUdp, SysName, NotifType, SecType, Passwd, @@ -684,7 +692,7 @@ log(PrivDir, Agent, {_, _, Varbinds}, NewVarsAndVals) -> File = filename:join(PrivDir, ?CT_SNMP_LOG_FILE), {ok, Fd} = file:open(File, [write, append]), io:format(Fd, "~p.~n", [{Agent, OldVarsAndVals, NewVarsAndVals}]), - file:close(Fd), + ok = file:close(Fd), ok. %%%--------------------------------------------------------------------------- del_dir(Dir) -> @@ -692,7 +700,7 @@ del_dir(Dir) -> FullPathFiles = lists:map(fun(File) -> filename:join(Dir, File) end, Files), lists:foreach(fun file:delete/1, FullPathFiles), - file:del_dir(Dir), + ok = delete_dir(Dir), ok. %%%--------------------------------------------------------------------------- agent_conf(Agent, MgrAgentConfName) -> @@ -738,8 +746,8 @@ override_contexts(Config, {data_dir_file, File}) -> override_contexts(Config, Contexts) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"context.conf"), - file:delete(File), - snmp_config:write_agent_context_config(Dir, "", Contexts). + ok = delete_file(File), + ok = snmp_config:write_agent_context_config(Dir, "", Contexts). %%%--------------------------------------------------------------------------- override_sysinfo(_, undefined) -> @@ -754,8 +762,8 @@ override_sysinfo(Config, {data_dir_file, File}) -> override_sysinfo(Config, SysInfo) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"standard.conf"), - file:delete(File), - snmp_config:write_agent_standard_config(Dir, "", SysInfo). + ok = delete_file(File), + ok = snmp_config:write_agent_standard_config(Dir, "", SysInfo). %%%--------------------------------------------------------------------------- override_target_address(_, undefined) -> @@ -769,8 +777,8 @@ override_target_address(Config, {data_dir_file, File}) -> override_target_address(Config, TargetAddressConf) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"target_addr.conf"), - file:delete(File), - snmp_config:write_agent_target_addr_config(Dir, "", TargetAddressConf). + ok = delete_file(File), + ok = snmp_config:write_agent_target_addr_config(Dir, "", TargetAddressConf). %%%--------------------------------------------------------------------------- @@ -785,8 +793,8 @@ override_target_params(Config, {data_dir_file, File}) -> override_target_params(Config, TargetParamsConf) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"target_params.conf"), - file:delete(File), - snmp_config:write_agent_target_params_config(Dir, "", TargetParamsConf). + ok = delete_file(File), + ok = snmp_config:write_agent_target_params_config(Dir, "", TargetParamsConf). %%%--------------------------------------------------------------------------- override_notify(_, undefined) -> @@ -800,8 +808,8 @@ override_notify(Config, {data_dir_file, File}) -> override_notify(Config, NotifyConf) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"notify.conf"), - file:delete(File), - snmp_config:write_agent_notify_config(Dir, "", NotifyConf). + ok = delete_file(File), + ok = snmp_config:write_agent_notify_config(Dir, "", NotifyConf). %%%--------------------------------------------------------------------------- override_usm(_, undefined) -> @@ -815,8 +823,8 @@ override_usm(Config, {data_dir_file, File}) -> override_usm(Config, UsmConf) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"usm.conf"), - file:delete(File), - snmp_config:write_agent_usm_config(Dir, "", UsmConf). + ok = delete_file(File), + ok = snmp_config:write_agent_usm_config(Dir, "", UsmConf). %%%-------------------------------------------------------------------------- override_community(_, undefined) -> @@ -830,8 +838,8 @@ override_community(Config, {data_dir_file, File}) -> override_community(Config, CommunityConf) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"community.conf"), - file:delete(File), - snmp_config:write_agent_community_config(Dir, "", CommunityConf). + ok = delete_file(File), + ok = snmp_config:write_agent_community_config(Dir, "", CommunityConf). %%%--------------------------------------------------------------------------- @@ -846,8 +854,8 @@ override_vacm(Config, {data_dir_file, File}) -> override_vacm(Config, VacmConf) -> Dir = filename:join(?config(priv_dir, Config),"conf"), File = filename:join(Dir,"vacm.conf"), - file:delete(File), - snmp_config:write_agent_vacm_config(Dir, "", VacmConf). + ok = delete_file(File), + ok = snmp_config:write_agent_vacm_config(Dir, "", VacmConf). %%%--------------------------------------------------------------------------- @@ -861,3 +869,21 @@ while_ok(Fun,[H|T]) -> end; while_ok(_Fun,[]) -> ok. + +delete_file(FileName) -> + case file:delete(FileName) of + {error, enoent} -> ok; + Else -> Else + end. + +make_dir(Dir) -> + case file:make_dir(Dir) of + {error, eexist} -> ok; + Else -> Else + end. + +delete_dir(Dir) -> + case file:del_dir(Dir) of + {error, enoent} -> ok; + Else -> Else + end. diff --git a/lib/common_test/src/ct_ssh.erl b/lib/common_test/src/ct_ssh.erl index d19004fa2c..ca62357e1c 100644 --- a/lib/common_test/src/ct_ssh.erl +++ b/lib/common_test/src/ct_ssh.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2009-2013. All Rights Reserved. +%% Copyright Ericsson AB 2009-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -68,7 +68,8 @@ send_and_receive/3, send_and_receive/4, send_and_receive/5, send_and_receive/6, exec/2, exec/3, exec/4, - subsystem/3, subsystem/4]). + subsystem/3, subsystem/4, + shell/2, shell/3]). %% STFP Functions -export([sftp_connect/1, @@ -94,6 +95,7 @@ -record(state, {ssh_ref, conn_type, target}). +-type handle() :: pid(). %%%----------------------------------------------------------------- %%%------------------------ SSH COMMANDS --------------------------- @@ -159,7 +161,7 @@ connect(KeyOrName, ExtraOpts) when is_list(ExtraOpts) -> connect(KeyOrName, ConnType, ExtraOpts) -> case ct:get_config(KeyOrName) of undefined -> - log(heading(connect,KeyOrName), "Failed: ~p\n", + log(heading(connect,KeyOrName), "Failed: ~tp\n", [{not_available,KeyOrName}]), {error,{not_available,KeyOrName}}; SSHData -> @@ -212,18 +214,18 @@ connect(KeyOrName, ConnType, ExtraOpts) -> end, case {Addr,proplists:get_value(port, AllOpts1)} of {undefined,_} -> - log(heading(connect,KeyOrName), "Failed: ~p\n", + log(heading(connect,KeyOrName), "Failed: ~tp\n", [{not_available,{KeyOrName,ConnType1}}]), {error,{not_available,{KeyOrName,ConnType1}}}; {_,undefined} -> try_log(heading(connect,KeyOrName), - "Opening ~w connection to ~p:22\n", + "Opening ~w connection to ~tp:22\n", [ConnType1,Addr]), ct_gen_conn:start(KeyOrName, {ConnType1,Addr,22}, AllOpts1, ?MODULE); {_,Port} -> try_log(heading(connect,KeyOrName), - "Opening ~w connection to ~p:~w\n", + "Opening ~w connection to ~tp:~w\n", [ConnType1,Addr,Port]), ct_gen_conn:start(KeyOrName, {ConnType1,Addr,Port}, AllOpts1, ?MODULE) @@ -490,6 +492,22 @@ subsystem(SSH, ChannelId, Subsystem, Timeout) -> call(SSH, {subsystem,ChannelId,Subsystem,Timeout}). +-spec shell(SSH, ChannelId) -> Result when + SSH :: handle() | ct:target_name(), + ChannelId :: ssh:ssh_channel_id(), + Result :: ok | {error,term()}. +shell(SSH, ChannelId) -> + shell(SSH, ChannelId, ?DEFAULT_TIMEOUT). + +-spec shell(SSH, ChannelId, Timeout) -> Result when + SSH :: handle() | ct:target_name(), + ChannelId :: ssh:ssh_channel_id(), + Timeout :: timeout(), + Result :: ok | {error,term()}. +shell(SSH, ChannelId, Timeout) -> + call(SSH, {shell,ChannelId,Timeout}). + + %%%----------------------------------------------------------------- %%%------------------------ SFTP COMMANDS -------------------------- @@ -962,8 +980,8 @@ init(KeyOrName, {ConnType,Addr,Port}, AllOpts) -> end, [], AllOpts1), FinalOptions = [{silently_accept_hosts,true}, {user_interaction,false} | Options], - crypto:start(), - ssh:start(), + _ = crypto:start(), + _ = ssh:start(), Result = case ConnType of ssh -> ssh:connect(Addr, Port, FinalOptions); @@ -977,7 +995,7 @@ init(KeyOrName, {ConnType,Addr,Port}, AllOpts) -> SSHRef = element(2, Ok), try_log(heading(init,KeyOrName), "Opened ~w connection:\n" - "Host: ~p (~p)\nUser: ~p\nPassword: ~p\n", + "Host: ~tp (~p)\nUser: ~tp\nPassword: ~p\n", [ConnType,Addr,Port,User,lists:duplicate(length(Password),$*)]), {ok,SSHRef,#state{ssh_ref=SSHRef, conn_type=ConnType, target=KeyOrName}} @@ -1015,11 +1033,11 @@ handle_msg({exec,Chn,Command,TO}, State) -> end, case Chn1 of {error,_} = ChnError -> - log(heading(exec,Target), "Opening channel failed: ~p", [ChnError]), + log(heading(exec,Target), "Opening channel failed: ~tp", [ChnError]), {ChnError,State}; _ -> try_log(heading(exec,Target), - "SSH Ref: ~p, Chn: ~p, Command: ~p, Timeout: ~p", + "SSH Ref: ~p, Chn: ~p, Command: ~tp, Timeout: ~p", [SSHRef,Chn1,Command,TO]), case ssh_connection:exec(SSHRef, Chn1, Command, TO) of success -> @@ -1042,7 +1060,7 @@ handle_msg({send,Chn,Type,Data,TO}, State) -> #state{ssh_ref=SSHRef, target=Target} = State, try_log(heading(send,Target), "SSH Ref: ~p, Chn: ~p, Type: ~p, Timeout: ~p~n" - "Data: ~p", [SSHRef,Chn,Type,TO,Data]), + "Data: ~tp", [SSHRef,Chn,Type,TO,Data]), Result = ssh_connection:send(SSHRef, Chn, Type, Data, TO), {Result,State}; @@ -1050,7 +1068,7 @@ handle_msg({send_and_receive,Chn,Type,Data,End,TO}, State) -> #state{ssh_ref=SSHRef, target=Target} = State, try_log(heading(send_and_receive,Target), "SSH Ref: ~p, Chn: ~p, Type: ~p, Timeout: ~p~n" - "Data: ~p", [SSHRef,Chn,Type,TO,Data]), + "Data: ~tp", [SSHRef,Chn,Type,TO,Data]), case ssh_connection:send(SSHRef, Chn, Type, Data, TO) of ok -> Result = do_recv_response(SSHRef, Chn, [], End, TO), @@ -1062,160 +1080,168 @@ handle_msg({send_and_receive,Chn,Type,Data,End,TO}, State) -> handle_msg({subsystem,Chn,Subsystem,TO}, State) -> #state{ssh_ref=SSHRef, target=Target} = State, try_log(heading(subsystem,Target), - "SSH Ref: ~p, Chn: ~p, Subsys: ~p, Timeout: ~p", + "SSH Ref: ~p, Chn: ~p, Subsys: ~tp, Timeout: ~p", [SSHRef,Chn,Subsystem,TO]), Result = ssh_connection:subsystem(SSHRef, Chn, Subsystem, TO), {Result,State}; +handle_msg({shell,Chn,TO}, State) -> + #state{ssh_ref=SSHRef, target=Target} = State, + try_log(heading(shell,Target), + "SSH Ref: ~p, Chn: ~p, Timeout: ~p", + [SSHRef,Chn,TO]), + Result = ssh_connection:shell(SSHRef, Chn), + {Result,State}; + %% --- SFTP Commands --- handle_msg({read_file,Srv,File}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:read_file(ref(Srv,SSHRef), File),S}; handle_msg({write_file,Srv,File,Iolist}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:write_file(ref(Srv,SSHRef), File, Iolist),S}; handle_msg({list_dir,Srv,Path}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:list_dir(ref(Srv,SSHRef), Path),S}; handle_msg({open,Srv,File,Mode}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:open(ref(Srv,SSHRef), File, Mode),S}; handle_msg({opendir,Srv,Path}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:opendir(ref(Srv,SSHRef), Path),S}; handle_msg({close,Srv,Handle}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:close(ref(Srv,SSHRef), Handle),S}; handle_msg({read,Srv,Handle,Len}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:read(ref(Srv,SSHRef), Handle, Len),S}; handle_msg({pread,Srv,Handle,Position,Length}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:pread(ref(Srv,SSHRef),Handle,Position,Length),S}; handle_msg({aread,Srv,Handle,Len}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:aread(ref(Srv,SSHRef), Handle, Len),S}; handle_msg({apread,Srv,Handle,Position,Length}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:apread(ref(Srv,SSHRef), Handle, Position, Length),S}; handle_msg({write,Srv,Handle,Data}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:write(ref(Srv,SSHRef), Handle, Data),S}; handle_msg({pwrite,Srv,Handle,Position,Data}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:pwrite(ref(Srv,SSHRef), Handle, Position, Data),S}; handle_msg({awrite,Srv,Handle,Data}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:awrite(ref(Srv,SSHRef), Handle, Data),S}; handle_msg({apwrite,Srv,Handle,Position,Data}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:apwrite(ref(Srv,SSHRef), Handle, Position, Data),S}; handle_msg({position,Srv,Handle,Location}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:position(ref(Srv,SSHRef), Handle, Location),S}; handle_msg({read_file_info,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:read_file_info(ref(Srv,SSHRef), Name),S}; handle_msg({get_file_info,Srv,Handle}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:get_file_info(ref(Srv,SSHRef), Handle),S}; handle_msg({read_link_info,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:read_link_info(ref(Srv,SSHRef), Name),S}; handle_msg({write_file_info,Srv,Name,Info}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:write_file_info(ref(Srv,SSHRef), Name, Info),S}; handle_msg({read_link,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:read_link(ref(Srv,SSHRef), Name),S}; handle_msg({make_symlink,Srv,Name,Target}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:make_symlink(ref(Srv,SSHRef), Name, Target),S}; handle_msg({rename,Srv,OldName,NewName}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:rename(ref(Srv,SSHRef), OldName, NewName),S}; handle_msg({delete,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:delete(ref(Srv,SSHRef), Name),S}; handle_msg({make_dir,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:make_dir(ref(Srv,SSHRef), Name),S}; handle_msg({del_dir,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) -> try_log(heading(sftp,S#state.target), - "SSH Ref: ~p, Server: ~p~nCmd: ~p", + "SSH Ref: ~p, Server: ~p~nCmd: ~tp", [SSHRef,ref(Srv,SSHRef),mod(Cmd)]), {ssh_sftp:del_dir(ref(Srv,SSHRef), Name),S}. @@ -1259,7 +1285,7 @@ do_recv_response(SSH, Chn, Data, End, Timeout) -> {ssh_cm, SSH, {data,Chn,_,NewData}} -> ssh_connection:adjust_window(SSH, Chn, size(NewData)), - debug("RECVD~n~p", [binary_to_list(NewData)]), + debug("RECVD~n~tp", [binary_to_list(NewData)]), DataAcc = Data ++ binary_to_list(NewData), if is_function(End) -> case End(DataAcc) of @@ -1312,7 +1338,7 @@ do_recv_response(SSH, Chn, Data, End, Timeout) -> %% {ok,WCh}; Other -> - debug("UNEXPECTED MESSAGE~n~p ~p~n~p", [SSH,Chn,Other]), + debug("UNEXPECTED MESSAGE~n~p ~p~n~tp", [SSH,Chn,Other]), do_recv_response(SSH, Chn, Data, End, Timeout) after Timeout -> @@ -1365,7 +1391,7 @@ mod(Cmd) -> %%%----------------------------------------------------------------- %%% heading(Function, Ref) -> - io_lib:format("ct_ssh:~w ~p",[Function,Ref]). + io_lib:format("ct_ssh:~tw ~tp",[Function,Ref]). %%%----------------------------------------------------------------- %%% diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl index e9487e94db..b50cddd492 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-2014. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -42,7 +42,8 @@ %% {reconnection_interval,Millisec}, %% {keep_alive,Bool}, %% {poll_limit,N}, -%% {poll_interval,Millisec}]}.</pre> +%% {poll_interval,Millisec}, +%% {tcp_nodelay,Bool}]}.</pre> %% <p><code>Millisec = integer(), N = integer()</code></p> %% <p>Enter the <code>telnet_settings</code> term in a configuration %% file included in the test and ct_telnet will retrieve the information @@ -182,7 +183,8 @@ conn_to=?DEFAULT_TIMEOUT, com_to=?DEFAULT_TIMEOUT, reconns=?RECONNS, - reconn_int=?RECONN_TIMEOUT}). + reconn_int=?RECONN_TIMEOUT, + tcp_nodelay=false}). %%%----------------------------------------------------------------- %%% @spec open(Name) -> {ok,Handle} | {error,Reason} @@ -249,7 +251,7 @@ open(KeyOrName,ConnType,TargetMod) -> open(KeyOrName,ConnType,TargetMod,Extra) -> case ct:get_config({KeyOrName,ConnType}) of undefined -> - log(undefined,open,"Failed: ~p",[{not_available,KeyOrName}]), + log(undefined,open,"Failed: ~tp",[{not_available,KeyOrName}]), {error,{not_available,KeyOrName,ConnType}}; Addr -> Addr1 = @@ -271,7 +273,7 @@ open(KeyOrName,ConnType,TargetMod,Extra) -> end; Bool -> Bool end, - log(undefined,open,"Connecting to ~p(~p)", + log(undefined,open,"Connecting to ~tp(~tp)", [KeyOrName,Addr1]), Reconnect = case ct:get_config({telnet_settings,reconnection_attempts}) of @@ -327,16 +329,16 @@ cmd(Connection,Cmd) -> %%% Reason = term() %%% @doc Send a command via telnet and wait for prompt. %%% -%%% This function will by default add a newline to the end of the +%%% <p>This function will by default add a newline to the end of the %%% given command. If this is not desired, the option %%% `{newline,false}' can be used. This is necessary, for example, %%% when sending telnet command sequences (prefixed with the -%%% Interprete As Command, IAC, character). +%%% Interprete As Command, IAC, character).</p> %%% -%%% The option `timeout' specifies how long the client shall wait for +%%% <p>The option `timeout' specifies how long the client shall wait for %%% prompt. If the time expires, the function returns %%% `{error,timeout}'. See the module description for information -%%% about the default value for the command timeout. +%%% about the default value for the command timeout.</p> cmd(Connection,Cmd,Opts) when is_list(Opts) -> case check_cmd_opts(Opts) of ok -> @@ -378,7 +380,7 @@ cmdf(Connection,CmdFormat,Args) -> %%% @doc Send a telnet command and wait for prompt %%% (uses a format string and list of arguments to build the command). %%% -%%% See {@link cmd/3} further description. +%%% <p>See {@link cmd/3} further description.</p> cmdf(Connection,CmdFormat,Args,Opts) when is_list(Args) -> Cmd = lists:flatten(io_lib:format(CmdFormat,Args)), cmd(Connection,Cmd,Opts). @@ -602,8 +604,18 @@ init(Name,{Ip,Port,Type},{TargetMod,KeepAlive,Extra}) -> Settings -> set_telnet_defaults(Settings,#state{}) end, - case catch TargetMod:connect(Name,Ip,Port,S0#state.conn_to, - KeepAlive,Extra) of + %% Handle old user versions of TargetMod + _ = code:ensure_loaded(TargetMod), + try + case erlang:function_exported(TargetMod,connect,7) of + true -> + TargetMod:connect(Name,Ip,Port,S0#state.conn_to, + KeepAlive,S0#state.tcp_nodelay,Extra); + false -> + TargetMod:connect(Name,Ip,Port,S0#state.conn_to, + KeepAlive,Extra) + end + of {ok,TelnPid} -> put({ct_telnet_pid2name,TelnPid},Name), S1 = S0#state{host=Ip, @@ -625,15 +637,18 @@ init(Name,{Ip,Port,Type},{TargetMod,KeepAlive,Extra}) -> "Connection timeout: ~p\n" "Keep alive: ~w\n" "Poll limit: ~w\n" - "Poll interval: ~w", + "Poll interval: ~w\n" + "TCP nodelay: ~w", [Ip,Port,S1#state.com_to,S1#state.reconns, S1#state.reconn_int,S1#state.conn_to,KeepAlive, - S1#state.poll_limit,S1#state.poll_interval]), + S1#state.poll_limit,S1#state.poll_interval, + S1#state.tcp_nodelay]), {ok,TelnPid,S1}; - {'EXIT',Reason} -> - {error,Reason}; Error -> Error + catch + _:Reason -> + {error,Reason} end. type(telnet) -> ip; @@ -653,9 +668,11 @@ set_telnet_defaults([{poll_limit,PL}|Ss],S) -> set_telnet_defaults(Ss,S#state{poll_limit=PL}); set_telnet_defaults([{poll_interval,PI}|Ss],S) -> set_telnet_defaults(Ss,S#state{poll_interval=PI}); +set_telnet_defaults([{tcp_nodelay,NoDelay}|Ss],S) -> + set_telnet_defaults(Ss,S#state{tcp_nodelay=NoDelay}); set_telnet_defaults([Unknown|Ss],S) -> force_log(S,error, - "Bad element in telnet_settings: ~p",[Unknown]), + "Bad element in telnet_settings: ~tp",[Unknown]), set_telnet_defaults(Ss,S); set_telnet_defaults([],S) -> S. @@ -663,7 +680,7 @@ set_telnet_defaults([],S) -> %% @hidden handle_msg({cmd,Cmd,Opts},State) -> start_gen_log(heading(cmd,State#state.name)), - log(State,cmd,"Cmd: ~p",[Cmd]), + log(State,cmd,"Cmd: ~tp",[Cmd]), %% whatever is in the buffer from previous operations %% will be ignored as we go ahead with this telnet cmd @@ -671,7 +688,7 @@ handle_msg({cmd,Cmd,Opts},State) -> debug_cont_gen_log("Throwing Buffer:",[]), debug_log_lines(State#state.buffer), - case {State#state.type,State#state.prompt} of + _ = case {State#state.type,State#state.prompt} of {ts,_} -> silent_teln_expect(State#state.name, State#state.teln_pid, @@ -698,7 +715,7 @@ handle_msg({cmd,Cmd,Opts},State) -> case teln_cmd(State#state.teln_pid, Cmd, State#state.prx, Newline, TO) of {ok,Data,_PromptType,Rest} -> - log(State,recv,"Return: ~p",[{ok,Data}]), + log(State,recv,"Return: ~tp",[{ok,Data}]), {{ok,Data},Rest,true}; Error -> Retry = {retry,{Error, @@ -706,19 +723,19 @@ handle_msg({cmd,Cmd,Opts},State) -> State#state.type}, State#state.teln_pid, {cmd,Cmd,Opts}}}, - log(State,recv,"Return: ~p",[Error]), + log(State,recv,"Return: ~tp",[Error]), {Retry,[],false} end, end_gen_log(), {Return,State#state{buffer=NewBuffer,prompt=Prompt}}; handle_msg({send,Cmd,Opts},State) -> start_gen_log(heading(send,State#state.name)), - log(State,send,"Sending: ~p",[Cmd]), + log(State,send,"Sending: ~tp",[Cmd]), debug_cont_gen_log("Throwing Buffer:",[]), debug_log_lines(State#state.buffer), - case {State#state.type,State#state.prompt} of + _ = case {State#state.type,State#state.prompt} of {ts,_} -> silent_teln_expect(State#state.name, State#state.teln_pid, @@ -745,12 +762,12 @@ handle_msg(get_data,State) -> log(State,cmd,"Reading data...",[]), {ok,Data,Buffer} = teln_get_all_data(State,State#state.buffer,[],[], State#state.poll_limit), - log(State,recv,"Return: ~p",[{ok,Data}]), + log(State,recv,"Return: ~tp",[{ok,Data}]), end_gen_log(), {{ok,Data},State#state{buffer=Buffer}}; handle_msg({expect,Pattern,Opts},State) -> start_gen_log(heading(expect,State#state.name)), - log(State,expect,"Expect: ~p\nOpts = ~p\n",[Pattern,Opts]), + log(State,expect,"Expect: ~tp\nOpts = ~tp\n",[Pattern,Opts]), {Return,NewBuffer,Prompt} = case teln_expect(State#state.name, State#state.teln_pid, @@ -762,15 +779,15 @@ handle_msg({expect,Pattern,Opts},State) -> P = check_if_prompt_was_reached(Data,[]), {{ok,Data},Rest,P}; {ok,Data,HaltReason,Rest} -> - force_log(State,expect,"HaltReason: ~p",[HaltReason]), + force_log(State,expect,"HaltReason: ~tp",[HaltReason]), P = check_if_prompt_was_reached(Data,HaltReason), {{ok,Data,HaltReason},Rest,P}; {error,Reason,Rest} -> - force_log(State,expect,"Expect failed\n~p",[{error,Reason}]), + force_log(State,expect,"Expect failed\n~tp",[{error,Reason}]), P = check_if_prompt_was_reached([],Reason), {{error,Reason},Rest,P}; {error,Reason} -> - force_log(State,expect,"Expect failed\n~p",[{error,Reason}]), + force_log(State,expect,"Expect failed\n~tp",[{error,Reason}]), P = check_if_prompt_was_reached([],Reason), {{error,Reason},[],P} end, @@ -794,8 +811,17 @@ reconnect(Ip,Port,N,State=#state{name=Name, keep_alive=KeepAlive, extra=Extra, conn_to=ConnTo, - reconn_int=ReconnInt}) -> - case TargetMod:connect(Name,Ip,Port,ConnTo,KeepAlive,Extra) of + reconn_int=ReconnInt, + tcp_nodelay=NoDelay}) -> + %% Handle old user versions of TargetMod + ConnResult = + case erlang:function_exported(TargetMod,connect,7) of + true -> + TargetMod:connect(Name,Ip,Port,ConnTo,KeepAlive,NoDelay,Extra); + false -> + TargetMod:connect(Name,Ip,Port,ConnTo,KeepAlive,Extra) + end, + case ConnResult of {ok,NewPid} -> put({ct_telnet_pid2name,NewPid},Name), {ok, NewPid, State#state{teln_pid=NewPid}}; @@ -870,7 +896,7 @@ check_if_prompt_was_reached(_,_) -> heading(Action,undefined) -> io_lib:format("~w ~w",[?MODULE,Action]); heading(Action,Name) -> - io_lib:format("~w ~w for ~p",[?MODULE,Action,Name]). + io_lib:format("~w ~w for ~tp",[?MODULE,Action,Name]). force_log(State,Action,String,Args) -> log(State,Action,String,Args,true). @@ -928,7 +954,7 @@ log(#state{name=Name,teln_pid=TelnPid,host=Host,port=Port}, true -> ok; false -> - ct_gen_conn:cont_log(String,Args) + ct_gen_conn:cont_log_no_timestamp(String,Args) end; ForcePrint == true -> @@ -939,7 +965,7 @@ log(#state{name=Name,teln_pid=TelnPid,host=Host,port=Port}, %% called ct_gen_conn:log(heading(Action,Name1),String,Args); false -> - ct_gen_conn:cont_log(String,Args) + ct_gen_conn:cont_log_no_timestamp(String,Args) end end end. @@ -1198,7 +1224,6 @@ teln_expect1(Name,Pid,Data,Pattern,Acc,EO=#eo{idle_timeout=IdleTO, EOMod = if TotalTO /= infinity -> EO#eo{total_timeout=trunc(TotalTO)}; true -> EO end, - ExpectFun = case EOMod#eo.seq of true -> fun() -> seq_expect(Name,Pid,Data,Pattern,Acc,EOMod) @@ -1221,38 +1246,34 @@ teln_expect1(Name,Pid,Data,Pattern,Acc,EO=#eo{idle_timeout=IdleTO, true -> IdleTO end, + {PatOrPats1,Acc1,Rest1} = case NotFinished of + {nomatch,Rest0} -> + %% one expect + {Pattern,[],Rest0}; + {continue,Pats0,Acc0,Rest0} -> + %% sequence + {Pats0,Acc0,Rest0} + end, case timer:tc(ct_gen_conn, do_within_time, [Fun,BreakAfter]) of - {_,{error,Reason}} -> + {_,{error,Reason}} -> %% A timeout will occur when the telnet connection %% is idle for EO#eo.idle_timeout milliseconds. + if Rest1 /= [] -> + log(name_or_pid(Name,Pid)," ~ts",[Rest1]); + true -> + ok + end, {error,Reason}; {_,{ok,Data1}} when TotalTO == infinity -> - case NotFinished of - {nomatch,Rest} -> - %% One expect - teln_expect1(Name,Pid,Rest++Data1, - Pattern,[],EOMod); - {continue,Patterns1,Acc1,Rest} -> - %% Sequence - teln_expect1(Name,Pid,Rest++Data1, - Patterns1,Acc1,EOMod) - end; + teln_expect1(Name,Pid,Rest1++Data1,PatOrPats1,Acc1,EOMod); {Elapsed,{ok,Data1}} -> TVal = TotalTO - (Elapsed/1000), if TVal =< 0 -> {error,timeout}; true -> EO1 = EO#eo{total_timeout = TVal}, - case NotFinished of - {nomatch,Rest} -> - %% One expect - teln_expect1(Name,Pid,Rest++Data1, - Pattern,[],EO1); - {continue,Patterns1,Acc1,Rest} -> - %% Sequence - teln_expect1(Name,Pid,Rest++Data1, - Patterns1,Acc1,EO1) - end + teln_expect1(Name,Pid,Rest1++Data1, + PatOrPats1,Acc1,EO1) end end end. @@ -1273,7 +1294,7 @@ get_data1(Pid) -> %% one_expect: split data chunk at prompts one_expect(Name,Pid,Data,Pattern,EO) when EO#eo.prompt_check==false -> -% io:format("Raw Data ~p Pattern ~p EO ~p ",[Data,Pattern,EO]), +% io:format("Raw Data ~tp Pattern ~tp EO ~tp ",[Data,Pattern,EO]), one_expect1(Name,Pid,Data,Pattern,[],EO#eo{found_prompt=false}); one_expect(Name,Pid,Data,Pattern,EO) -> case match_prompt(Data,EO#eo.prx) of @@ -1390,14 +1411,14 @@ match_lines(Name,Pid,Data,Patterns,EO) -> case one_line(Data,[]) of {noline,Rest} when FoundPrompt=/=false -> %% This is the line including the prompt - case match_line(Name,Pid,Rest,Patterns,FoundPrompt,EO) of + case match_line(Name,Pid,Rest,Patterns,FoundPrompt,false,EO) of nomatch -> {nomatch,prompt}; {Tag,Match} -> {Tag,Match,[]} end; {noline,Rest} when EO#eo.prompt_check==false -> - case match_line(Name,Pid,Rest,Patterns,false,EO) of + case match_line(Name,Pid,Rest,Patterns,false,false,EO) of nomatch -> {nomatch,Rest}; {Tag,Match} -> @@ -1406,7 +1427,7 @@ match_lines(Name,Pid,Data,Patterns,EO) -> {noline,Rest} -> {nomatch,Rest}; {Line,Rest} -> - case match_line(Name,Pid,Line,Patterns,false,EO) of + case match_line(Name,Pid,Line,Patterns,false,true,EO) of nomatch -> match_lines(Name,Pid,Rest,Patterns,EO); {Tag,Match} -> @@ -1414,45 +1435,50 @@ match_lines(Name,Pid,Data,Patterns,EO) -> end end. - %% For one line, match each pattern -match_line(Name,Pid,Line,Patterns,FoundPrompt,EO) -> - match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,match). +match_line(Name,Pid,Line,Patterns,FoundPrompt,Terminated,EO) -> + match_line(Name,Pid,Line,Patterns,FoundPrompt,Terminated,EO,match). -match_line(Name,Pid,Line,[prompt|Patterns],false,EO,RetTag) -> - match_line(Name,Pid,Line,Patterns,false,EO,RetTag); -match_line(Name,Pid,Line,[prompt|_Patterns],FoundPrompt,_EO,RetTag) -> +match_line(Name,Pid,Line,[prompt|Patterns],false,Term,EO,RetTag) -> + match_line(Name,Pid,Line,Patterns,false,Term,EO,RetTag); +match_line(Name,Pid,Line,[prompt|_Patterns],FoundPrompt,_Term,_EO,RetTag) -> log(name_or_pid(Name,Pid)," ~ts",[Line]), log(name_or_pid(Name,Pid),"PROMPT: ~ts",[FoundPrompt]), {RetTag,{prompt,FoundPrompt}}; -match_line(Name,Pid,Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_EO,RetTag) - when PromptType==FoundPrompt -> +match_line(Name,Pid,Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_Term, + _EO,RetTag) when PromptType==FoundPrompt -> log(name_or_pid(Name,Pid)," ~ts",[Line]), log(name_or_pid(Name,Pid),"PROMPT: ~ts",[FoundPrompt]), {RetTag,{prompt,FoundPrompt}}; -match_line(Name,Pid,Line,[{prompt,PromptType}|Patterns],FoundPrompt,EO,RetTag) +match_line(Name,Pid,Line,[{prompt,PromptType}|Patterns],FoundPrompt,Term, + EO,RetTag) when PromptType=/=FoundPrompt -> - match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag); -match_line(Name,Pid,Line,[{Tag,Pattern}|Patterns],FoundPrompt,EO,RetTag) -> - case re:run(Line,Pattern,[{capture,all,list}]) of + match_line(Name,Pid,Line,Patterns,FoundPrompt,Term,EO,RetTag); +match_line(Name,Pid,Line,[{Tag,Pattern}|Patterns],FoundPrompt,Term,EO,RetTag) -> + case re:run(Line,Pattern,[{capture,all,list},unicode]) of nomatch -> - match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag); + match_line(Name,Pid,Line,Patterns,FoundPrompt,Term,EO,RetTag); {match,Match} -> log(name_or_pid(Name,Pid),"MATCH: ~ts",[Line]), {RetTag,{Tag,Match}} end; -match_line(Name,Pid,Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) -> - case re:run(Line,Pattern,[{capture,all,list}]) of +match_line(Name,Pid,Line,[Pattern|Patterns],FoundPrompt,Term,EO,RetTag) -> + case re:run(Line,Pattern,[{capture,all,list},unicode]) of nomatch -> - match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag); + match_line(Name,Pid,Line,Patterns,FoundPrompt,Term,EO,RetTag); {match,Match} -> log(name_or_pid(Name,Pid),"MATCH: ~ts",[Line]), {RetTag,Match} end; -match_line(Name,Pid,Line,[],FoundPrompt,EO,match) -> - match_line(Name,Pid,Line,EO#eo.haltpatterns,FoundPrompt,EO,halt); -match_line(Name,Pid,Line,[],_FoundPrompt,_EO,halt) -> +match_line(Name,Pid,Line,[],FoundPrompt,Term,EO,match) -> + match_line(Name,Pid,Line,EO#eo.haltpatterns,FoundPrompt,Term,EO,halt); +%% print any terminated line that can not be matched +match_line(Name,Pid,Line,[],_FoundPrompt,true,_EO,halt) -> log(name_or_pid(Name,Pid)," ~ts",[Line]), + nomatch; +%% if there's no line termination, Line is saved as Rest (above) and will +%% be printed later +match_line(_Name,_Pid,_Line,[],_FoundPrompt,false,_EO,halt) -> nomatch. one_line([$\n|Rest],Line) -> @@ -1549,7 +1575,7 @@ split_lines([],Line,Lines) -> match_prompt(Str,Prx) -> match_prompt(Str,Prx,[]). match_prompt(Str,Prx,Acc) -> - case re:run(Str,Prx) of + case re:run(Str,Prx,[unicode]) of nomatch -> noprompt; {match,[{Start,Len}]} -> diff --git a/lib/common_test/src/ct_telnet_client.erl b/lib/common_test/src/ct_telnet_client.erl index 99d683244c..76e4b9ea70 100644 --- a/lib/common_test/src/ct_telnet_client.erl +++ b/lib/common_test/src/ct_telnet_client.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2014. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ %%-define(debug, true). --export([open/2, open/3, open/4, open/5, close/1]). +-export([open/2, open/3, open/4, open/5, open/6, close/1]). -export([send_data/2, send_data/3, get_data/1]). -define(TELNET_PORT, 23). @@ -70,19 +70,22 @@ -record(state,{conn_name, get_data, keep_alive=true, log_pos=1}). open(Server, ConnName) -> - open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT, true, ConnName). + open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT, true, false, ConnName). open(Server, Port, ConnName) -> - open(Server, Port, ?OPEN_TIMEOUT, true, ConnName). + open(Server, Port, ?OPEN_TIMEOUT, true, false, ConnName). open(Server, Port, Timeout, ConnName) -> - open(Server, Port, Timeout, true, ConnName). + open(Server, Port, Timeout, true, false, ConnName). open(Server, Port, Timeout, KeepAlive, ConnName) -> + open(Server, Port, Timeout, KeepAlive, false, ConnName). + +open(Server, Port, Timeout, KeepAlive, NoDelay, ConnName) -> Self = self(), Pid = spawn(fun() -> init(Self, Server, Port, Timeout, - KeepAlive, ConnName) + KeepAlive, NoDelay, ConnName) end), receive {open,Pid} -> @@ -114,10 +117,11 @@ get_data(Pid) -> %%%----------------------------------------------------------------- %%% Internal functions -init(Parent, Server, Port, Timeout, KeepAlive, ConnName) -> - case gen_tcp:connect(Server, Port, [list,{packet,0},{nodelay,true}], Timeout) of +init(Parent, Server, Port, Timeout, KeepAlive, NoDelay, ConnName) -> + ct_util:mark_process(), + case gen_tcp:connect(Server, Port, [list,{packet,0},{nodelay,NoDelay}], Timeout) of {ok,Sock} -> - dbg("~p connected to: ~p (port: ~w, keep_alive: ~w)\n", + dbg("~tp connected to: ~tp (port: ~w, keep_alive: ~w)\n", [ConnName,Server,Port,KeepAlive]), send([?IAC,?DO,?SUPPRESS_GO_AHEAD], Sock, ConnName), Parent ! {open,self()}, @@ -269,7 +273,7 @@ send(Data, Sock, ConnName) -> _:_ -> ok end end, - gen_tcp:send(Sock, Data), + ok = gen_tcp:send(Sock, Data), ok. %% [IAC,IAC] = buffer data value 255 @@ -281,7 +285,7 @@ check_msg(Sock, [?IAC | Cs], Acc) -> case get_cmd(Cs) of {Cmd,Cs1} -> cmd_dbg("Got",Cmd), - respond_cmd(Cmd, Sock), + ok = respond_cmd(Cmd, Sock), check_msg(Sock, Cs1, Acc); error -> Acc diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl index 5d5448f352..bd3755722f 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2006-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -26,7 +26,8 @@ -export([prepare_tests/1, prepare_tests/2, collect_tests_from_list/2, collect_tests_from_list/3, - collect_tests_from_file/2, collect_tests_from_file/3]). + collect_tests_from_file/2, collect_tests_from_file/3, + get_tests/1]). -export([testspec_rec2list/1, testspec_rec2list/2]). @@ -70,13 +71,17 @@ prepare_tests(TestSpec) when is_record(TestSpec,testspec) -> Tests = TestSpec#testspec.tests, %% Sort Tests into "flat" Run and Skip lists (not sorted per node). {Run,Skip} = get_run_and_skip(Tests,[],[]), + %% Create initial list of {Node,{Run,Skip}} tuples NodeList = lists:map(fun(N) -> {N,{[],[]}} end, list_nodes(TestSpec)), + %% Get all Run tests sorted per node basis. NodeList1 = run_per_node(Run,NodeList, - TestSpec#testspec.merge_tests), + TestSpec#testspec.merge_tests), + %% Get all Skip entries sorted per node basis. NodeList2 = skip_per_node(Skip,NodeList1), + %% Change representation. Result= lists:map(fun({Node,{Run1,Skip1}}) -> @@ -103,7 +108,7 @@ run_per_node([{{Node,Dir},Test}|Ts],Result,MergeTests) -> true -> merge_tests(Dir,Test,Run) end, - run_per_node(Ts,insert_in_order({Node,{Run1,Skip}},Result), + run_per_node(Ts,insert_in_order({Node,{Run1,Skip}},Result,replace), MergeTests); run_per_node([],Result,_) -> Result. @@ -140,7 +145,7 @@ merge_suites(Dir,Test,[]) -> skip_per_node([{{Node,Dir},Test}|Ts],Result) -> {value,{Node,{Run,Skip}}} = lists:keysearch(Node,1,Result), Skip1 = [{Dir,Test}|Skip], - skip_per_node(Ts,insert_in_order({Node,{Run,Skip1}},Result)); + skip_per_node(Ts,insert_in_order({Node,{Run,Skip1}},Result,replace)); skip_per_node([],Result) -> Result. @@ -156,7 +161,7 @@ skip_per_node([],Result) -> %% %% Skip entry: {Suites,Comment} or {Suite,Cases,Comment} %% -get_run_and_skip([{{Node,Dir},Suites}|Tests],Run,Skip) -> +get_run_and_skip([{{Node,Dir},Suites}|Tests],Run,Skip) -> TestDir = ct_util:get_testdir(Dir,catch element(1,hd(Suites))), case lists:keysearch(all,1,Suites) of {value,_} -> % all Suites in Dir @@ -183,18 +188,33 @@ prepare_suites(Node,Dir,[{Suite,Cases}|Suites],Run,Skip) -> [[{{Node,Dir},{Suite,all}}]|Run], [Skipped|Skip]); false -> - {RL,SL} = prepare_cases(Node,Dir,Suite,Cases), - prepare_suites(Node,Dir,Suites,[RL|Run],[SL|Skip]) + {Run1,Skip1} = prepare_cases(Node,Dir,Suite,Cases,Run,Skip), + prepare_suites(Node,Dir,Suites,Run1,Skip1) end; prepare_suites(_Node,_Dir,[],Run,Skip) -> {lists:flatten(lists:reverse(Run)), lists:flatten(lists:reverse(Skip))}. -prepare_cases(Node,Dir,Suite,Cases) -> +prepare_cases(Node,Dir,Suite,Cases,Run,Skip) -> case get_skipped_cases(Node,Dir,Suite,Cases) of - SkipAll=[{{Node,Dir},{Suite,_Cmt}}] -> % all cases to be skipped - %% note: this adds an 'all' test even if only skip is specified - {[{{Node,Dir},{Suite,all}}],SkipAll}; + [SkipAll={{Node,Dir},{Suite,_Cmt}}] -> % all cases to be skipped + case lists:any(fun({{N,D},{S,all}}) when N == Node, + D == Dir, + S == Suite -> + true; + ({{N,D},{S,Cs}}) when N == Node, + D == Dir, + S == Suite -> + lists:member(all,Cs); + (_) -> false + end, lists:flatten(Run)) of + true -> + {Run,[SkipAll|Skip]}; + false -> + %% note: this adds an 'all' test even if + %% only skip is specified + {[{{Node,Dir},{Suite,all}}|Run],[SkipAll|Skip]} + end; Skipped -> %% note: this adds a test even if only skip is specified PrepC = lists:foldr(fun({{G,Cs},{skip,_Cmt}}, Acc) when @@ -210,11 +230,11 @@ prepare_cases(Node,Dir,Suite,Cases) -> true -> Acc; false -> - [C|Acc] + [{skipped,C}|Acc] end; (C,Acc) -> [C|Acc] end, [], Cases), - {{{Node,Dir},{Suite,PrepC}},Skipped} + {[{{Node,Dir},{Suite,PrepC}}|Run],[Skipped|Skip]} end. get_skipped_suites(Node,Dir,Suites) -> @@ -324,7 +344,7 @@ create_spec_tree([Spec|Specs],TS,JoinWithNext,Known) -> create_spec_tree(Specs,TS,JoinWithNext,Known)}; {error,Reason} -> ReasonStr = - lists:flatten(io_lib:format("~s", + lists:flatten(io_lib:format("~ts", [file:format_error(Reason)])), throw({error,{SpecAbsName,ReasonStr}}) end @@ -431,6 +451,7 @@ collect_tests({Replace,Terms},TestSpec=#testspec{alias=As,nodes=Ns},Relaxed) -> merge_tests = MergeTestsDef}), TestSpec2 = get_all_nodes(Terms2,TestSpec1), {Terms3, TestSpec3} = filter_init_terms(Terms2, [], TestSpec2), + add_tests(Terms3,TestSpec3). %% replace names (atoms) in the testspec matching those in 'define' terms by @@ -516,7 +537,7 @@ replace_names_in_elems([],Modified,_Defs) -> replace_names_in_string(Term,Defs=[{Name,Replacement=[Ch|_]}|Ds]) when is_integer(Ch) -> try re:replace(Term,[$'|atom_to_list(Name)]++"'", - Replacement,[{return,list}]) of + Replacement,[{return,list},unicode]) of Term -> % no match, proceed replace_names_in_string(Term,Ds); Term1 -> @@ -548,7 +569,7 @@ replace_names_in_node1(NodeStr,Defs=[{Name,Replacement}|Ds]) -> replace_names_in_node1(NodeStr,Ds); true -> case re:replace(NodeStr,atom_to_list(Name), - ReplStr,[{return,list}]) of + ReplStr,[{return,list},unicode]) of NodeStr -> % no match, proceed replace_names_in_node1(NodeStr,Ds); NodeStr1 -> @@ -783,6 +804,31 @@ list_nodes(#testspec{nodes=NodeRefs}) -> lists:map(fun({_Ref,Node}) -> Node end, NodeRefs). +%%%----------------------------------------------------------------- +%%% Parse the given test specs and return the complete set of specs +%%% and tests to run/skip. +%%% [Spec1,Spec2,...] means create separate tests per spec +%%% [[Spec1,Spec2,...]] means merge all specs into one +-spec get_tests(Specs) -> {ok,[{Specs,Tests}]} | {error,Reason} when + Specs :: [string()] | [[string()]], + Tests :: {Node,Run,Skip}, + Node :: atom(), + Run :: {Dir,Suites,Cases}, + Skip :: {Dir,Suites,Comment} | {Dir,Suites,Cases,Comment}, + Dir :: string(), + Suites :: atom | [atom()] | all, + Cases :: atom | [atom()] | all, + Comment :: string(), + Reason :: term(). + +get_tests(Specs) -> + case collect_tests_from_file(Specs,true) of + Tests when is_list(Tests) -> + {ok,[{S,prepare_tests(R)} || {S,R} <- Tests]}; + Error -> + Error + end. + %% ----------------------------------------------------- %% / \ %% | When adding test/config terms, remember to update | @@ -1055,7 +1101,7 @@ check_term(Term) when is_tuple(Term) -> true -> io:format("~nSuspicious term, " "please check:~n" - "~p~n", [Term]), + "~tp~n", [Term]), invalid; false -> invalid @@ -1112,6 +1158,11 @@ handle_data(verbosity,Node,VLvls,_Spec) when is_list(VLvls) -> VLvls1 = lists:map(fun(VLvl = {_Cat,_Lvl}) -> VLvl; (Lvl) -> {'$unspecified',Lvl} end, VLvls), [{Node,VLvls1}]; +handle_data(multiply_timetraps,Node,Mult,_Spec) when is_integer(Mult) -> + [{Node,Mult}]; +handle_data(scale_timetraps,Node,Scale,_Spec) when Scale == true; + Scale == false -> + [{Node,Scale}]; handle_data(silent_connections,Node,all,_Spec) -> [{Node,[all]}]; handle_data(silent_connections,Node,Conn,_Spec) when is_atom(Conn) -> @@ -1126,9 +1177,12 @@ should_be_added(Tag,Node,_Data,Spec) -> if %% list terms *without* possible duplicates here Tag == logdir; Tag == logopts; - Tag == basic_html; Tag == label; - Tag == auto_compile; Tag == abort_if_missing_suites; + Tag == basic_html; Tag == esc_chars; + Tag == label; Tag == auto_compile; + Tag == abort_if_missing_suites; Tag == stylesheet; Tag == verbosity; + Tag == multiply_timetraps; + Tag == scale_timetraps; Tag == silent_connections -> lists:keymember(ref2node(Node,Spec#testspec.nodes),1, read_field(Spec,Tag)) == false; @@ -1257,7 +1311,7 @@ insert_groups1(Suite,Groups,Suites0) -> Suites0; {value,{Suite,GrAndCases0}} -> GrAndCases = insert_groups2(Groups,GrAndCases0), - insert_in_order({Suite,GrAndCases},Suites0); + insert_in_order({Suite,GrAndCases},Suites0,replace); false -> insert_in_order({Suite,Groups},Suites0) end. @@ -1282,7 +1336,7 @@ insert_cases(Node,Dir,Suite,Cases,Tests,false) when is_list(Cases) -> insert_cases(Node,Dir,Suite,Cases,Tests,true) when is_list(Cases) -> {Tests1,Done} = lists:foldr(fun(All={{N,D},[{all,_}]},{Merged,_}) when N == Node, - D == Dir -> + D == Dir -> {[All|Merged],true}; ({{N,D},Suites0},{Merged,_}) when N == Node, D == Dir -> @@ -1312,7 +1366,7 @@ insert_cases1(Suite,Cases,Suites0) -> Suites0; {value,{Suite,Cases0}} -> Cases1 = insert_in_order(Cases,Cases0), - insert_in_order({Suite,Cases1},Suites0); + insert_in_order({Suite,Cases1},Suites0,replace); false -> insert_in_order({Suite,Cases},Suites0) end. @@ -1369,9 +1423,14 @@ skip_groups1(Suite,Groups,Cmt,Suites0) -> case lists:keysearch(Suite,1,Suites0) of {value,{Suite,GrAndCases0}} -> GrAndCases1 = GrAndCases0 ++ SkipGroups, - insert_in_order({Suite,GrAndCases1},Suites0); + insert_in_order({Suite,GrAndCases1},Suites0,replace); false -> - insert_in_order({Suite,SkipGroups},Suites0) + case Suites0 of + [{all,_}=All|Skips]-> + [All|Skips++[{Suite,SkipGroups}]]; + _ -> + insert_in_order({Suite,SkipGroups},Suites0,replace) + end end. skip_cases(Node,Dir,Suite,Cases,Cmt,Tests,false) when is_list(Cases) -> @@ -1380,7 +1439,7 @@ skip_cases(Node,Dir,Suite,Cases,Cmt,Tests,false) when is_list(Cases) -> skip_cases(Node,Dir,Suite,Cases,Cmt,Tests,true) when is_list(Cases) -> {Tests1,Done} = lists:foldr(fun({{N,D},Suites0},{Merged,_}) when N == Node, - D == Dir -> + D == Dir -> Suites1 = skip_cases1(Suite,Cases,Cmt,Suites0), {[{{N,D},Suites1}|Merged],true}; (T,{Merged,Match}) -> @@ -1401,32 +1460,55 @@ skip_cases1(Suite,Cases,Cmt,Suites0) -> case lists:keysearch(Suite,1,Suites0) of {value,{Suite,Cases0}} -> Cases1 = Cases0 ++ SkipCases, - insert_in_order({Suite,Cases1},Suites0); + insert_in_order({Suite,Cases1},Suites0,replace); false -> - insert_in_order({Suite,SkipCases},Suites0) + case Suites0 of + [{all,_}=All|Skips]-> + [All|Skips++[{Suite,SkipCases}]]; + _ -> + insert_in_order({Suite,SkipCases},Suites0,replace) + end end. append(Elem, List) -> List ++ [Elem]. -insert_in_order([E|Es],List) -> - List1 = insert_elem(E,List,[]), - insert_in_order(Es,List1); -insert_in_order([],List) -> +insert_in_order(Elems,Dest) -> + insert_in_order1(Elems,Dest,false). + +insert_in_order(Elems,Dest,replace) -> + insert_in_order1(Elems,Dest,true). + +insert_in_order1([_E|Es],all,Replace) -> + insert_in_order1(Es,all,Replace); + +insert_in_order1([E|Es],List,Replace) -> + List1 = insert_elem(E,List,[],Replace), + insert_in_order1(Es,List1,Replace); +insert_in_order1([],List,_Replace) -> List; -insert_in_order(E,List) -> - insert_elem(E,List,[]). +insert_in_order1(E,List,Replace) -> + insert_elem(E,List,[],Replace). + -%% replace an existing entry (same key) or add last in list -insert_elem({Key,_}=E,[{Key,_}|Rest],SoFar) -> +insert_elem({Key,_}=E,[{Key,_}|Rest],SoFar,true) -> lists:reverse([E|SoFar]) ++ Rest; -insert_elem({E,_},[E|Rest],SoFar) -> +insert_elem({E,_},[E|Rest],SoFar,true) -> lists:reverse([E|SoFar]) ++ Rest; -insert_elem(E,[E|Rest],SoFar) -> +insert_elem(E,[E|Rest],SoFar,true) -> + lists:reverse([E|SoFar]) ++ Rest; + +insert_elem({all,_}=E,_,SoFar,_Replace) -> + lists:reverse([E|SoFar]); +insert_elem(_E,[all|_],SoFar,_Replace) -> + lists:reverse(SoFar); +insert_elem(_E,[{all,_}],SoFar,_Replace) -> + lists:reverse(SoFar); +insert_elem({Key,_}=E,[{Key,[]}|Rest],SoFar,_Replace) -> lists:reverse([E|SoFar]) ++ Rest; -insert_elem(E,[E1|Rest],SoFar) -> - insert_elem(E,Rest,[E1|SoFar]); -insert_elem(E,[],SoFar) -> +insert_elem(E,[E1|Rest],SoFar,Replace) -> + insert_elem(E,Rest,[E1|SoFar],Replace); +insert_elem(E,[],SoFar,_Replace) -> lists:reverse([E|SoFar]). ref2node(all_nodes,_Refs) -> @@ -1501,6 +1583,8 @@ valid_terms() -> {logopts,3}, {basic_html,2}, {basic_html,3}, + {esc_chars,2}, + {esc_chars,3}, {verbosity,2}, {verbosity,3}, {silent_connections,2}, diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl index 445fce1db8..468edc4bee 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -65,6 +65,9 @@ -export([warn_duplicates/1]). +-export([mark_process/0, mark_process/1, is_marked/1, is_marked/2, + remaining_test_procs/0]). + -export([get_profile_data/0, get_profile_data/1, get_profile_data/2, open_url/3]). @@ -126,19 +129,20 @@ start(Mode, LogDir, Verbosity) -> do_start(Parent, Mode, LogDir, Verbosity) -> process_flag(trap_exit,true), register(ct_util_server,self()), + mark_process(), create_table(?conn_table,#conn.handle), create_table(?board_table,2), create_table(?suite_table,#suite_data.key), create_table(?verbosity_table,1), - [ets:insert(?verbosity_table,{Cat,Lvl}) || {Cat,Lvl} <- Verbosity], + _ = [ets:insert(?verbosity_table,{Cat,Lvl}) || {Cat,Lvl} <- Verbosity], {ok,StartDir} = file:get_cwd(), case file:set_cwd(LogDir) of ok -> ok; E -> exit(E) end, - DoExit = fun(Reason) -> file:set_cwd(StartDir), exit(Reason) end, + DoExit = fun(Reason) -> ok = file:set_cwd(StartDir), exit(Reason) end, Opts = case read_opts() of {ok,Opts1} -> Opts1; @@ -169,7 +173,7 @@ do_start(Parent, Mode, LogDir, Verbosity) -> end, %% add user event handlers - case lists:keysearch(event_handler,1,Opts) of + _ = case lists:keysearch(event_handler,1,Opts) of {value,{_,Handlers}} -> Add = fun({H,Args}) -> case catch gen_event:add_handler(?CT_EVMGR_REF,H,Args) of @@ -188,6 +192,8 @@ do_start(Parent, Mode, LogDir, Verbosity) -> ok end, + ct_default_gl:start_link(group_leader()), + {StartTime,TestLogDir} = ct_logs:init(Mode, Verbosity), ct_event:notify(#event{name=test_start, @@ -195,24 +201,17 @@ do_start(Parent, Mode, LogDir, Verbosity) -> data={StartTime, lists:flatten(TestLogDir)}}), %% Initialize ct_hooks - try ct_hooks:init(Opts) of + _ = try ct_hooks:init(Opts) of ok -> Parent ! {self(),started}; {fail,CTHReason} -> - ErrorInfo = if is_atom(CTHReason) -> - io_lib:format("{~p,~p}", - [CTHReason, - erlang:get_stacktrace()]); - true -> - CTHReason - end, - ct_logs:tc_print('Suite Callback',ErrorInfo,[]), + ct_logs:tc_print('Suite Callback',CTHReason,[]), self() ! {{stop,{self(),{user_error,CTHReason}}}, {Parent,make_ref()}} catch _:CTHReason -> ErrorInfo = if is_atom(CTHReason) -> - io_lib:format("{~p,~p}", + io_lib:format("{~tp,~tp}", [CTHReason, erlang:get_stacktrace()]); true -> @@ -228,7 +227,8 @@ create_table(TableName,KeyPos) -> create_table(TableName,set,KeyPos). create_table(TableName,Type,KeyPos) -> catch ets:delete(TableName), - ets:new(TableName,[Type,named_table,public,{keypos,KeyPos}]). + _ = ets:new(TableName,[Type,named_table,public,{keypos,KeyPos}]), + ok. read_opts() -> case file:consult(ct_run:variables_file_name("./")) of @@ -459,6 +459,7 @@ loop(Mode,TestData,StartDir) -> error:badarg -> [] end, ct_hooks:terminate(Callbacks), + close_connections(ets:tab2list(?conn_table)), ets:delete(?conn_table), ets:delete(?board_table), @@ -472,7 +473,8 @@ loop(Mode,TestData,StartDir) -> ct_logs:close(Info, StartDir), ct_event:stop(), ct_config:stop(), - file:set_cwd(StartDir), + ct_default_gl:stop(), + ok = file:set_cwd(StartDir), return(From, Info); {Ref, _Msg} when is_reference(Ref) -> %% This clause is used when doing cast operations. @@ -485,15 +487,17 @@ loop(Mode,TestData,StartDir) -> {'EXIT',Pid,Reason} -> case ets:lookup(?conn_table,Pid) of [#conn{address=A,callback=CB}] -> + ErrorStr = io_lib:format("~tp", [Reason]), + ErrorHtml = ct_logs:escape_chars(ErrorStr), %% A connection crashed - remove the connection but don't die ct_logs:tc_log_async(ct_error_notify, ?MAX_IMPORTANCE, "CT Error Notification", "Connection process died: " - "Pid: ~w, Address: ~p, " + "Pid: ~w, Address: ~tp, " "Callback: ~w\n" - "Reason: ~p\n\n", - [Pid,A,CB,Reason]), + "Reason: ~ts\n\n", + [Pid,A,CB,ErrorHtml]), catch CB:close(Pid), %% in case CB:close failed to do this: unregister_connection(Pid), @@ -501,8 +505,8 @@ loop(Mode,TestData,StartDir) -> _ -> %% Let process crash in case of error, this shouldn't happen! io:format("\n\nct_util_server got EXIT " - "from ~w: ~p\n\n", [Pid,Reason]), - file:set_cwd(StartDir), + "from ~w: ~tp\n\n", [Pid,Reason]), + ok = file:set_cwd(StartDir), exit(Reason) end end. @@ -922,7 +926,8 @@ warn_duplicates(Suites) -> [] -> ok; _ -> - io:format(user,"~nWARNING! Deprecated function: ~w:sequences/0.~n" + io:format(?def_gl, + "~nWARNING! Deprecated function: ~w:sequences/0.~n" " Use group with sequence property instead.~n",[Mod]) end end, @@ -933,6 +938,70 @@ warn_duplicates(Suites) -> %%% @spec %%% %%% @doc +mark_process() -> + mark_process(system). + +mark_process(Type) -> + put(ct_process_type, Type). + +is_marked(Pid) -> + is_marked(Pid, system). + +is_marked(Pid, Type) -> + case process_info(Pid, dictionary) of + {dictionary,List} -> + Type == proplists:get_value(ct_process_type, List); + undefined -> + false + end. + +remaining_test_procs() -> + Procs = processes(), + {SharedGL,OtherGLs,Procs2} = + lists:foldl( + fun(Pid, ProcTypes = {Shared,Other,Procs1}) -> + case is_marked(Pid, group_leader) of + true -> + if not is_pid(Shared) -> + case test_server_io:get_gl(true) of + Pid -> + {Pid,Other, + lists:delete(Pid,Procs1)}; + _ -> + {Shared,[Pid|Other],Procs1} + end; + true -> % SharedGL already found + {Shared,[Pid|Other],Procs1} + end; + false -> + case is_marked(Pid) of + true -> + {Shared,Other,lists:delete(Pid,Procs1)}; + false -> + ProcTypes + end + end + end, {undefined,[],Procs}, Procs), + + AllGLs = [SharedGL | OtherGLs], + TestProcs = + lists:flatmap(fun(Pid) -> + case process_info(Pid, group_leader) of + {group_leader,GL} -> + case lists:member(GL, AllGLs) of + true -> [{Pid,GL}]; + false -> [] + end; + undefined -> + [] + end + end, Procs2), + {TestProcs, SharedGL, OtherGLs}. + +%%%----------------------------------------------------------------- +%%% @spec +%%% +%%% @doc get_profile_data() -> get_profile_data(all). @@ -976,12 +1045,12 @@ get_profile_data(Profile, Key, StartDir) -> end, case Result of {error,enoent} when Profile /= default -> - io:format(user, "~nERROR! Missing profile file ~p~n", [File]), + io:format(?def_gl, "~nERROR! Missing profile file ~tp~n", [File]), undefined; {error,enoent} when Profile == default -> undefined; {error,Reason} -> - io:format(user,"~nERROR! Error in profile file ~p: ~p~n", + io:format(?def_gl,"~nERROR! Error in profile file ~tp: ~tp~n", [WhichFile,Reason]), undefined; {ok,Data} -> @@ -991,8 +1060,8 @@ get_profile_data(Profile, Key, StartDir) -> _ when is_list(Data) -> Data; _ -> - io:format(user, - "~nERROR! Invalid profile data in ~p~n", + io:format(?def_gl, + "~nERROR! Invalid profile data in ~tp~n", [WhichFile]), [] end, @@ -1032,10 +1101,12 @@ call(Msg, Timeout) -> end. return({To,Ref},Result) -> - To ! {Ref, Result}. + To ! {Ref, Result}, + ok. cast(Msg) -> - ct_util_server ! {Msg, {ct_util_server, make_ref()}}. + ct_util_server ! {Msg, {ct_util_server, make_ref()}}, + ok. seconds(T) -> test_server:seconds(T). @@ -1071,15 +1142,15 @@ abs_name2([],Acc) -> open_url(iexplore, Args, URL) -> {ok,R} = win32reg:open([read]), ok = win32reg:change_key(R,"applications\\iexplore.exe\\shell\\open\\command"), - case win32reg:values(R) of + _ = case win32reg:values(R) of {ok, Paths} -> Path = proplists:get_value(default, Paths), [Cmd | _] = string:tokens(Path, "%"), Cmd1 = Cmd ++ " " ++ Args ++ " " ++ URL, - io:format(user, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd1]), + io:format(?def_gl, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd1]), open_port({spawn,Cmd1}, []); _ -> - io:format("~nNo path to iexplore.exe~n",[]) + io:format(?def_gl, "~nNo path to iexplore.exe~n",[]) end, win32reg:close(R), ok; @@ -1089,6 +1160,6 @@ open_url(Prog, Args, URL) -> is_list(Prog) -> Prog end, Cmd = ProgStr ++ " " ++ Args ++ " " ++ URL, - io:format(user, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd]), + io:format(?def_gl, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd]), open_port({spawn,Cmd},[]), ok. diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl index 2c1954c2b3..039c8168ec 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-2013. All Rights Reserved. +%% Copyright Ericsson AB 2003-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ -define(board_table,ct_boards). -define(suite_table,ct_suite_data). -define(verbosity_table,ct_verbosity_table). +-define(def_gl, ct_default_gl). -record(conn, {handle, targetref, @@ -37,6 +38,7 @@ logdir=["."], logopts=[], basic_html=[], + esc_chars=[], verbosity=[], silent_connections=[], cover=[], diff --git a/lib/common_test/src/ct_webtool.erl b/lib/common_test/src/ct_webtool.erl index 014487eb10..82aa78fc4b 100644 --- a/lib/common_test/src/ct_webtool.erl +++ b/lib/common_test/src/ct_webtool.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2010. All Rights Reserved. +%% Copyright Ericsson AB 2001-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -84,27 +84,27 @@ %% Function = {FunctionName,Arity} | FunctionName | %% {Module, FunctionName, Arity} | {Module,FunctionName} debug(F) -> - ttb:tracer(all,[{file,"webtool.trc"}]), % tracing all nodes - ttb:p(all,[call,timestamp]), + {ok, _} = ttb:tracer(all,[{file,"webtool.trc"}]), % tracing all nodes + {ok, _} = ttb:p(all,[call,timestamp]), MS = [{'_',[],[{return_trace},{message,{caller}}]}], - tp(F,MS), - ttb:ctp(?MODULE,stop_debug), % don't want tracing of the stop_debug func + _ = tp(F,MS), + {ok, _} = ttb:ctp(?MODULE,stop_debug), % don't want tracing of the stop_debug func ok. tp(local,MS) -> % all functions ttb:tpl(?MODULE,MS); tp(global,MS) -> % all exported functions ttb:tp(?MODULE,MS); tp([{M,F,A}|T],MS) -> % Other module - ttb:tpl(M,F,A,MS), + {ok, _} = ttb:tpl(M,F,A,MS), tp(T,MS); tp([{M,F}|T],MS) when is_atom(F) -> % Other module - ttb:tpl(M,F,MS), + {ok, _} = ttb:tpl(M,F,MS), tp(T,MS); tp([{F,A}|T],MS) -> % function/arity - ttb:tpl(?MODULE,F,A,MS), + {ok, _} = ttb:tpl(?MODULE,F,A,MS), tp(T,MS); tp([F|T],MS) -> % function - ttb:tpl(?MODULE,F,MS), + {ok, _} = ttb:tpl(?MODULE,F,MS), tp(T,MS); tp([],_MS) -> ok. @@ -112,18 +112,18 @@ stop_debug() -> ttb:stop([format]). debug_app(Mod) -> - ttb:tracer(all,[{file,"webtool_app.trc"},{handler,{fun out/4,true}}]), - ttb:p(all,[call,timestamp]), + {ok, _} = ttb:tracer(all,[{file,"webtool_app.trc"},{handler,{fun out/4,true}}]), + {ok, _} = ttb:p(all,[call,timestamp]), MS = [{'_',[],[{return_trace},{message,{caller}}]}], - ttb:tp(Mod,MS), + {ok, _} = ttb:tp(Mod,MS), ok. out(_,{trace_ts,Pid,call,MFA={M,F,A},{W,_,_},TS},_,S) when W==webtool;W==mod_esi-> - io:format("~w: (~p)~ncall ~s~n", [TS,Pid,ffunc(MFA)]), + io:format("~w: (~p)~ncall ~ts~n", [TS,Pid,ffunc(MFA)]), [{M,F,length(A)}|S]; out(_,{trace_ts,Pid,return_from,MFA,R,TS},_,[MFA|S]) -> - io:format("~w: (~p)~nreturned from ~s -> ~p~n", [TS,Pid,ffunc(MFA),R]), + io:format("~w: (~p)~nreturned from ~ts -> ~tp~n", [TS,Pid,ffunc(MFA),R]), S; out(_,_,_,_) -> ok. @@ -145,7 +145,7 @@ script_start([App]) -> script_start([App,DefaultBrowser]); script_start([App,Browser]) -> io:format("Starting webtool...\n"), - start(), + {ok, _} = start(), AvailableApps = get_applications(), {OSType,_} = os:type(), case lists:keysearch(App,1,AvailableApps) of @@ -159,19 +159,19 @@ script_start([App,Browser]) -> _ -> "http://localhost:" ++ PortStr ++ "/" ++ StartPage end, - case Browser of + _ = case Browser of none -> ok; iexplore when OSType == win32-> io:format("Starting internet explorer...\n"), {ok,R} = win32reg:open(""), Key="\\local_machine\\SOFTWARE\\Microsoft\\IE Setup\\Setup", - win32reg:change_key(R,Key), + ok = win32reg:change_key(R,Key), {ok,Val} = win32reg:value(R,"Path"), IExplore=filename:join(win32reg:expand(Val),"iexplore.exe"), os:cmd("\"" ++ IExplore ++ "\" " ++ Url); _ when OSType == win32 -> - io:format("Starting ~w...\n",[Browser]), + io:format("Starting ~tw...\n",[Browser]), os:cmd("\"" ++ atom_to_list(Browser) ++ "\" " ++ Url); B when B==firefox; B==mozilla -> io:format("Sending URL to ~w...",[Browser]), @@ -186,7 +186,7 @@ script_start([App,Browser]) -> {Port,{exit_status,_Error}} -> io:format(" not running, starting ~w...\n", [Browser]), - os:cmd(BStr ++ " " ++ Url), + _ = os:cmd(BStr ++ " " ++ Url), ok after ?SEND_URL_TIMEOUT -> io:format(" failed, starting ~w...\n",[Browser]), @@ -194,7 +194,7 @@ script_start([App,Browser]) -> os:cmd(BStr ++ " " ++ Url) end; _ -> - io:format("Starting ~w...\n",[Browser]), + io:format("Starting ~tw...\n",[Browser]), os:cmd(atom_to_list(Browser) ++ " " ++ Url) end, ok; @@ -206,7 +206,7 @@ script_start([App,Browser]) -> usage() -> io:format("Starting webtool...\n"), - start(), + {ok, _} = start(), Apps = lists:map(fun({A,_}) -> A end,get_applications()), io:format( "\nUsage: start_webtool application [ browser ]\n" @@ -254,7 +254,12 @@ start(Path,Port) when is_integer(Port)-> start(Path,Data0)-> Data = Data0 ++ rest_of_standard_data(), - gen_server:start({local,ct_web_tool},ct_webtool,{Path,Data},[]). + case gen_server:start({local,ct_web_tool},ct_webtool,{Path,Data},[]) of + {error, {already_started, Pid}} -> + {ok, Pid}; + Else -> + Else + end. stop()-> gen_server:call(ct_web_tool,stoppit). @@ -338,6 +343,7 @@ code_change(_,State,_)-> % Start the gen_server %---------------------------------------------------------------------- init({Path,Config})-> + ct_util:mark_process(), case filelib:is_dir(Path) of true -> {ok, Table} = get_tool_files_data(), @@ -374,7 +380,7 @@ print_url(ConfigData)-> Server=proplists:get_value(server_name,ConfigData,"undefined"), Port=proplists:get_value(port,ConfigData,"undefined"), {A,B,C,D}=proplists:get_value(bind_address,ConfigData,"undefined"), - io:format("WebTool is available at http://~s:~w/~n",[Server,Port]), + io:format("WebTool is available at http://~ts:~w/~n",[Server,Port]), io:format("Or http://~w.~w.~w.~w:~w/~n",[A,B,C,D,Port]). @@ -854,8 +860,8 @@ handle_app({Name,{start,{func,Start,Stop}}},Data,_Pid,Cmd)-> %%! Here the tool disappears from the webtool interface!! io:format("\n=======ERROR (webtool, line ~w) =======\n" "Could not start application \'~p\'\n\n" - "~w:~w(~s) ->\n" - "~p\n\n", + "~w:~tw(~ts) ->\n" + "~tp\n\n", [?LINE,Name,M,F,format_args(A),Exit]), ets:delete(Data,Name); _OK-> @@ -878,16 +884,16 @@ handle_app({Name,{start,{child,ChildSpec}}},Data,Pid,Cmd)-> %%! Here the tool disappears from the webtool interface!! io:format("\n=======ERROR (webtool, line ~w) =======\n" "Could not start application \'~p\'\n\n" - "supervisor:start_child(~p,~p) ->\n" - "~p\n\n", + "supervisor:start_child(~p,~tp) ->\n" + "~tp\n\n", [?LINE,Name,Pid,ChildSpec,{error,Reason}]), ets:delete(Data,Name); Error -> %%! Here the tool disappears from the webtool interface!! io:format("\n=======ERROR (webtool, line ~w) =======\n" "Could not start application \'~p\'\n\n" - "supervisor:start_child(~p,~p) ->\n" - "~p\n\n", + "supervisor:start_child(~p,~tp) ->\n" + "~tp\n\n", [?LINE,Name,Pid,ChildSpec,Error]), ets:delete(Data,Name) end; @@ -919,7 +925,7 @@ handle_app({Name,{start,{app,Real_name}}},Data,_Pid,Cmd)-> io:format("\n=======ERROR (webtool, line ~w) =======\n" "Could not start application \'~p\'\n\n" "application:start(~p,~p) ->\n" - "~p\n\n", + "~tp\n\n", [?LINE,Name,Real_name,temporary,Error]), ets:delete(Data,Name) end; @@ -935,7 +941,7 @@ handle_app({Name,Incorrect},Data,_Pid,Cmd)-> %%! Here the tool disappears from the webtool interface!! io:format("\n=======ERROR (webtool, line ~w) =======\n" "Could not ~w application \'~p\'\n\n" - "Incorrect data: ~p\n\n", + "Incorrect data: ~tp\n\n", [?LINE,Cmd,Name,Incorrect]), ets:delete(Data,Name). @@ -1197,12 +1203,12 @@ filter_tool_files(Dir,[File|Rest]) -> %%%----------------------------------------------------------------- %%% format functions ffunc({M,F,A}) when is_list(A) -> - io_lib:format("~w:~w(~s)\n",[M,F,format_args(A)]); + io_lib:format("~w:~tw(~ts)\n",[M,F,format_args(A)]); ffunc({M,F,A}) when is_integer(A) -> - io_lib:format("~w:~w/~w\n",[M,F,A]). + io_lib:format("~w:~tw/~w\n",[M,F,A]). format_args([]) -> ""; format_args(Args) -> - Str = lists:append(["~p"|lists:duplicate(length(Args)-1,",~p")]), + Str = lists:append(["~tp"|lists:duplicate(length(Args)-1,",~tp")]), io_lib:format(Str,Args). diff --git a/lib/common_test/src/ct_webtool_sup.erl b/lib/common_test/src/ct_webtool_sup.erl index 485161c784..6c6dbde0a6 100644 --- a/lib/common_test/src/ct_webtool_sup.erl +++ b/lib/common_test/src/ct_webtool_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2009. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ stop(Pid)-> %% {error, Reason} %%---------------------------------------------------------------------- init(_StartArgs) -> + ct_util:mark_process(), %%Child1 = %%Child2 ={webcover_backend,{webcover_backend,start_link,[]},permanent,2000,worker,[webcover_backend]}, %%{ok,{{simple_one_for_one,5,10},[Child1]}}. diff --git a/lib/common_test/src/cth_conn_log.erl b/lib/common_test/src/cth_conn_log.erl index 9b3dc0b5f1..7b6f03311a 100644 --- a/lib/common_test/src/cth_conn_log.erl +++ b/lib/common_test/src/cth_conn_log.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012-2013. All Rights Reserved. +%% Copyright Ericsson AB 2012-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -24,11 +24,11 @@ %% %% suite() -> %% [{ct_hooks, [{cth_conn_log, -%% [{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}]}]. +%% [{conn_mod(),hook_options()}]}]}]. %% %% or specified in a configuration file: %% -%% {ct_conn_log,[{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}. +%% {ct_conn_log,[{conn_mod(),hook_options()}]}. %% %% The conn_mod() is the common test module implementing the protocol, %% e.g. ct_netconfc, ct_telnet, etc. This module must log by calling @@ -54,32 +54,21 @@ -include_lib("common_test/include/ct.hrl"). -export([init/2, - pre_init_per_testcase/3, - post_end_per_testcase/4]). - -%%---------------------------------------------------------------------- -%% Exported types -%%---------------------------------------------------------------------- --export_type([hook_options/0, - log_type/0, - conn_mod/0]). + pre_init_per_testcase/4, + post_end_per_testcase/5]). %%---------------------------------------------------------------------- %% Type declarations %%---------------------------------------------------------------------- --type hook_options() :: [hook_option()]. -%% Options that can be given to `cth_conn_log' in the `ct_hook' statement. --type hook_option() :: {log_type,log_type()} | - {hosts,[ct_gen_conn:key_or_name()]}. --type log_type() :: raw | pretty | html | silent. --type conn_mod() :: ct_netconfc | ct_telnet. +-type hook_options() :: ct:conn_log_options(). +-type log_type() :: ct:conn_log_type(). +-type conn_mod() :: ct:conn_log_mod(). %%---------------------------------------------------------------------- -spec init(Id, HookOpts) -> Result when Id :: term(), HookOpts :: hook_options(), - Result :: {ok,[{conn_mod(), - {log_type(),[ct_gen_conn:key_or_name()]}}]}. + Result :: {ok,[{conn_mod(),{log_type(),[ct:key_or_name()]}}]}. init(_Id, HookOpts) -> ConfOpts = ct:get_config(ct_conn_log,[]), {ok,merge_log_info(ConfOpts,HookOpts)}. @@ -104,7 +93,7 @@ get_log_opts(Mod,Opts) -> Hosts = proplists:get_value(hosts,Opts,[]), {LogType,Hosts}. -pre_init_per_testcase(TestCase,Config,CthState) -> +pre_init_per_testcase(_Suite,TestCase,Config,CthState) -> Logs = lists:map( fun({ConnMod,{LogType,Hosts}}) -> @@ -127,12 +116,12 @@ pre_init_per_testcase(TestCase,Config,CthState) -> "<table borders=1>" "<b>" ++ ConnModStr ++ " logs:</b>\n" ++ [io_lib:format( - "<tr><td>~p</td><td><a href=\"~ts\">~ts</a>" + "<tr><td>~tp</td><td><a href=\"~ts\">~ts</a>" "</td></tr>", [S,ct_logs:uri(L),filename:basename(L)]) || {S,L} <- Ls] ++ "</table>", - io:format(Str,[]), + ct:log(Str,[],[no_css]), {ConnMod,{LogType,Ls}}; _ -> {ConnMod,{LogType,[]}} @@ -158,7 +147,7 @@ pre_init_per_testcase(TestCase,Config,CthState) -> ct_util:update_testdata(?MODULE, Update, [create]), {Config,CthState}. -post_end_per_testcase(TestCase,_Config,Return,CthState) -> +post_end_per_testcase(_Suite,TestCase,_Config,Return,CthState) -> Update = fun(PrevUsers) -> case lists:delete(TestCase, PrevUsers) of @@ -170,7 +159,7 @@ post_end_per_testcase(TestCase,_Config,Return,CthState) -> end, case ct_util:update_testdata(?MODULE, Update) of deleted -> - [ct_util:delete_testdata({?MODULE,ConnMod}) || + _ = [ct_util:delete_testdata({?MODULE,ConnMod}) || {ConnMod,_} <- CthState], error_logger:delete_report_handler(ct_conn_log_h); {error,no_response} -> diff --git a/lib/common_test/src/cth_log_redirect.erl b/lib/common_test/src/cth_log_redirect.erl index e6970a2bad..77f90c0df6 100644 --- a/lib/common_test/src/cth_log_redirect.erl +++ b/lib/common_test/src/cth_log_redirect.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2011-2013. All Rights Reserved. +%% Copyright Ericsson AB 2011-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -28,9 +28,10 @@ %% CTH Callbacks -export([id/1, init/2, pre_init_per_suite/3, pre_end_per_suite/3, post_end_per_suite/4, - pre_init_per_group/3, post_init_per_group/4, - pre_end_per_group/3, post_end_per_group/4, - pre_init_per_testcase/3, post_end_per_testcase/4]). + pre_init_per_group/4, post_init_per_group/5, + pre_end_per_group/4, post_end_per_group/5, + pre_init_per_testcase/4, post_init_per_testcase/5, + pre_end_per_testcase/4, post_end_per_testcase/5]). %% Event handler Callbacks -export([init/1, @@ -55,6 +56,7 @@ id(_Opts) -> ?MODULE. init(?MODULE, _Opts) -> + ct_util:mark_process(), error_logger:add_report_handler(?MODULE), tc_log_async. @@ -70,11 +72,11 @@ post_end_per_suite(_Suite, Config, Return, State) -> set_curr_func(undefined, Config), {Return, State}. -pre_init_per_group(Group, Config, State) -> +pre_init_per_group(_Suite, Group, Config, State) -> set_curr_func({group,Group,init_per_group}, Config), {Config, State}. -post_init_per_group(Group, Config, Result, tc_log_async) when is_list(Config) -> +post_init_per_group(_Suite, Group, Config, Result, tc_log_async) when is_list(Config) -> case lists:member(parallel,proplists:get_value( tc_group_properties,Config,[])) of true -> @@ -82,27 +84,33 @@ post_init_per_group(Group, Config, Result, tc_log_async) when is_list(Config) -> false -> {Result, tc_log_async} end; -post_init_per_group(_Group, _Config, Result, State) -> +post_init_per_group(_Suite, _Group, _Config, Result, State) -> {Result, State}. -pre_init_per_testcase(TC, Config, State) -> +pre_init_per_testcase(_Suite, TC, Config, State) -> set_curr_func(TC, Config), {Config, State}. -post_end_per_testcase(_TC, _Config, Result, State) -> +post_init_per_testcase(_Suite, _TC, _Config, Return, State) -> + {Return, State}. + +pre_end_per_testcase(_Suite, _TC, Config, State) -> + {Config, State}. + +post_end_per_testcase(_Suite, _TC, _Config, Result, State) -> %% Make sure that the event queue is flushed %% before ending this test case. gen_event:call(error_logger, ?MODULE, flush, 300000), {Result, State}. -pre_end_per_group(Group, Config, {tc_log, Group}) -> +pre_end_per_group(_Suite, Group, Config, {tc_log, Group}) -> set_curr_func({group,Group,end_per_group}, Config), {Config, set_log_func(tc_log_async)}; -pre_end_per_group(Group, Config, State) -> +pre_end_per_group(_Suite, Group, Config, State) -> set_curr_func({group,Group,end_per_group}, Config), {Config, State}. -post_end_per_group(_Group, Config, Return, State) -> +post_end_per_group(_Suite, _Group, Config, Return, State) -> set_curr_func({group,undefined}, Config), {Return, State}. @@ -120,19 +128,34 @@ handle_event(Event, #eh_state{log_func = LogFunc} = State) -> _Else -> {ok, ErrLogType} = application:get_env(sasl, errlog_type), SReport = sasl_report:format_report(group_leader(), ErrLogType, - tag_event(Event)), + tag_event(Event, local)), if is_list(SReport) -> SaslHeader = format_header(State), - ct_logs:LogFunc(sasl, ?STD_IMPORTANCE, SaslHeader, SReport, []); + case LogFunc of + tc_log -> + ct_logs:tc_log(sasl, ?STD_IMPORTANCE, + SaslHeader, SReport, [], []); + tc_log_async -> + ct_logs:tc_log_async(sasl, ?STD_IMPORTANCE, + SaslHeader, SReport, []) + end; true -> %% Report is an atom if no logging is to be done ignore end end, + %% note that error_logger (unlike sasl) expects UTC time EReport = error_logger_tty_h:write_event( - tag_event(Event),io_lib), + tag_event(Event, utc), io_lib), if is_list(EReport) -> ErrHeader = format_header(State), - ct_logs:LogFunc(error_logger, ?STD_IMPORTANCE, ErrHeader, EReport, []); + case LogFunc of + tc_log -> + ct_logs:tc_log(error_logger, ?STD_IMPORTANCE, + ErrHeader, EReport, [], []); + tc_log_async -> + ct_logs:tc_log_async(error_logger, ?STD_IMPORTANCE, + ErrHeader, EReport, []) + end; true -> %% Report is an atom if no logging is to be done ignore end, @@ -199,7 +222,9 @@ terminate(_) -> terminate(_Arg, _State) -> ok. -tag_event(Event) -> +tag_event(Event, utc) -> + {calendar:universal_time(), Event}; +tag_event(Event, _) -> {calendar:local_time(), Event}. set_curr_func(CurrFunc, Config) -> @@ -226,26 +251,26 @@ format_header(#eh_state{curr_suite = Suite, format_header(#eh_state{curr_suite = Suite, curr_group = undefined, curr_func = TcOrConf}) -> - io_lib:format("System report during ~w:~w/1", + io_lib:format("System report during ~w:~tw/1", [Suite,TcOrConf]); format_header(#eh_state{curr_suite = Suite, curr_group = Group, curr_func = Conf}) when Conf == init_per_group; Conf == end_per_group -> - io_lib:format("System report during ~w:~w/2 for ~w", + io_lib:format("System report during ~w:~w/2 for ~tw", [Suite,Conf,Group]); format_header(#eh_state{curr_suite = Suite, curr_group = Group, parallel_tcs = true}) -> - io_lib:format("System report during ~w in ~w", + io_lib:format("System report during ~tw in ~w", [Group,Suite]); format_header(#eh_state{curr_suite = Suite, curr_group = Group, curr_func = TC}) -> - io_lib:format("System report during ~w:~w/1 in ~w", + io_lib:format("System report during ~w:~tw/1 in ~tw", [Suite,TC,Group]). code_change(_OldVsn, State, _Extra) -> diff --git a/lib/common_test/src/cth_surefire.erl b/lib/common_test/src/cth_surefire.erl index 31a8e1c076..da68bd105e 100644 --- a/lib/common_test/src/cth_surefire.erl +++ b/lib/common_test/src/cth_surefire.erl @@ -1,7 +1,7 @@ %%-------------------------------------------------------------------- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2012-2013. All Rights Reserved. +%% Copyright Ericsson AB 2012-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -33,16 +33,16 @@ -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_group/4]). +-export([post_init_per_group/5]). +-export([pre_end_per_group/4]). +-export([post_end_per_group/5]). --export([pre_init_per_testcase/3]). --export([post_end_per_testcase/4]). +-export([pre_init_per_testcase/4]). +-export([post_end_per_testcase/5]). --export([on_tc_fail/3]). --export([on_tc_skip/3]). +-export([on_tc_fail/4]). +-export([on_tc_skip/4]). -export([terminate/1]). @@ -82,7 +82,8 @@ init(Path, Opts) -> url_base = proplists:get_value(url_base,Opts), timer = ?now }. -pre_init_per_suite(Suite,SkipOrFail,State) when is_tuple(SkipOrFail) -> +pre_init_per_suite(Suite,SkipOrFail,#state{ test_cases = [] } = State) + when is_tuple(SkipOrFail) -> {SkipOrFail, init_tc(State#state{curr_suite = Suite, curr_suite_ts = ?now}, SkipOrFail) }; @@ -115,40 +116,39 @@ pre_end_per_suite(_Suite,Config,State) -> post_end_per_suite(_Suite,Config,Result,State) -> {Result, end_tc(end_per_suite,Config,Result,State)}. -pre_init_per_group(Group,Config,State) -> +pre_init_per_group(_Suite,Group,Config,State) -> {Config, init_tc(State#state{ curr_group = [Group|State#state.curr_group]}, Config)}. -post_init_per_group(_Group,Config,Result,State) -> +post_init_per_group(_Suite,_Group,Config,Result,State) -> {Result, end_tc(init_per_group,Config,Result,State)}. -pre_end_per_group(_Group,Config,State) -> +pre_end_per_group(_Suite,_Group,Config,State) -> {Config, init_tc(State, Config)}. -post_end_per_group(_Group,Config,Result,State) -> +post_end_per_group(_Suite,_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) -> +pre_init_per_testcase(_Suite,_TC,Config,State) -> {Config, init_tc(State, Config)}. -post_end_per_testcase(TC,Config,Result,State) -> +post_end_per_testcase(_Suite,TC,Config,Result,State) -> {Result, end_tc(TC,Config, Result,State)}. -on_tc_fail(_TC, _Res, State = #state{test_cases = []}) -> +on_tc_fail(_Suite,_TC, _Res, State = #state{test_cases = []}) -> State; -on_tc_fail(_TC, Res, State) -> +on_tc_fail(_Suite,_TC, Res, State) -> TCs = State#state.test_cases, TC = hd(TCs), NewTC = TC#testcase{ result = - {fail,lists:flatten(io_lib:format("~p",[Res]))} }, + {fail,lists:flatten(io_lib:format("~tp",[Res]))} }, State#state{ test_cases = [NewTC | tl(TCs)]}. -on_tc_skip({ConfigFunc,_GrName},{Type,_Reason} = Res, State0) - when Type == tc_auto_skip; Type == tc_user_skip -> - on_tc_skip(ConfigFunc, Res, State0); -on_tc_skip(Tc,{Type,_Reason} = Res, State0) when Type == tc_auto_skip -> +on_tc_skip(Suite,{ConfigFunc,_GrName}, Res, State) -> + on_tc_skip(Suite,ConfigFunc, Res, State); +on_tc_skip(Suite,Tc, Res, State0) -> TcStr = atom_to_list(Tc), State = case State0#state.test_cases of @@ -157,18 +157,14 @@ on_tc_skip(Tc,{Type,_Reason} = Res, State0) when Type == tc_auto_skip -> _ -> State0 end, - do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(State,[]))); -on_tc_skip(_Tc, _Res, State = #state{test_cases = []}) -> - State; -on_tc_skip(_Tc, Res, State) -> - do_tc_skip(Res, State). + do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(set_suite(Suite,State),[]))). do_tc_skip(Res, State) -> TCs = State#state.test_cases, TC = hd(TCs), NewTC = TC#testcase{ result = - {skipped,lists:flatten(io_lib:format("~p",[Res]))} }, + {skipped,lists:flatten(io_lib:format("~tp",[Res]))} }, State#state{ test_cases = [NewTC | tl(TCs)]}. init_tc(State, Config) when is_list(Config) == false -> @@ -208,6 +204,12 @@ end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite, result = passed }| State#state.test_cases], tc_log = ""}. % so old tc_log is not set if next is on_tc_skip + +set_suite(Suite,#state{curr_suite=undefined}=State) -> + State#state{curr_suite=Suite, curr_suite_ts=?now}; +set_suite(_,State) -> + State. + close_suite(#state{ test_cases = [] } = State) -> State; close_suite(#state{ test_cases = TCs, url_base = UrlBase } = State) -> @@ -227,7 +229,8 @@ close_suite(#state{ test_cases = TCs, url_base = UrlBase } = State) -> testcases = lists:reverse(TCs), log = SuiteLog, url = SuiteUrl}, - State#state{ test_cases = [], + State#state{ curr_suite = undefined, + test_cases = [], test_suites = [Suite | State#state.test_suites]}. terminate(State = #state{ test_cases = [] }) -> diff --git a/lib/common_test/src/erl2html2.erl b/lib/common_test/src/erl2html2.erl new file mode 100644 index 0000000000..7f3eb699de --- /dev/null +++ b/lib/common_test/src/erl2html2.erl @@ -0,0 +1,313 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%%%------------------------------------------------------------------ +%%% Purpose:Convert Erlang files to html. +%%%------------------------------------------------------------------ + +-module(erl2html2). +-export([convert/3, convert/4]). + +convert([], _Dest, _InclPath) -> % Fake clause. + ok; +convert(File, Dest, InclPath) -> + %% The generated code uses the BGCOLOR attribute in the + %% BODY tag, which wasn't valid until HTML 3.2. Also, + %% good HTML should either override all colour attributes + %% or none of them -- *never* just a few. + %% + %% FIXME: The colours should *really* be set with + %% stylesheets... + %% + %% The html file is written with the same encoding as the input file. + Encoding = encoding(File), + Header = ["<!DOCTYPE HTML PUBLIC " + "\"-//W3C//DTD HTML 3.2 Final//EN\">\n" + "<!-- autogenerated by '",atom_to_list(?MODULE),"'. -->\n" + "<html>\n" + "<head>\n" + "<meta http-equiv=\"Content-Type\" content=\"text/html;" + "charset=",html_encoding(Encoding),"\"/></meta>\n" + "<title>", to_raw_list(File,Encoding), "</title>\n" + "</head>\n\n" + "<body bgcolor=\"white\" text=\"black\"" + " link=\"blue\" vlink=\"purple\" alink=\"red\">\n"], + convert(File, Dest, InclPath, Header). + + +convert(File, Dest, InclPath, Header) -> + %% statistics(runtime), + case parse_file(File, InclPath) of + {ok,Functions} -> + %% {_, Time1} = statistics(runtime), + %% io:format("Parsed file in ~.2f Seconds.~n",[Time1/1000]), + case file:open(File,[raw,{read_ahead,10000}]) of + {ok,SFd} -> + case file:open(Dest,[write,raw]) of + {ok,DFd} -> + ok = file:write(DFd,[Header,"<pre>\n"]), + _Lines = build_html(SFd,DFd,encoding(File),Functions), + ok = file:write(DFd,["</pre>\n",footer(), + "</body>\n</html>\n"]), + %% {_, Time2} = statistics(runtime), + %% io:format("Converted ~p lines in ~.2f Seconds.~n", + %% [_Lines, Time2/1000]), + ok = file:close(SFd), + ok = file:close(DFd), + ok; + Error -> + Error + end; + Error -> + Error + end; + Error -> + Error + end. + +%%%----------------------------------------------------------------- +%%% Parse the input file to get the line numbers for all function +%%% definitions. This will be used when creating link targets for each +%%% function in build_html/5. +%%% +%%% All function clauses are also marked in order to allow +%%% possibly_enhance/2 to write these in bold. +%%% +%%% Use expanded preprocessor directives if possible (epp). Only if +%%% this fails, fall back on using non-expanded code (epp_dodger). + +parse_file(File, InclPath) -> + case epp:open(File, InclPath, []) of + {ok,Epp} -> + try parse_preprocessed_file(Epp,File,false) of + Forms -> + epp:close(Epp), + {ok,Forms} + catch + _:{error,_Reason,true} -> + parse_non_preprocessed_file(File); + _:{error,_Reason,false} -> + {ok,[]} + end; + Error = {error,_} -> + Error + end. + +parse_preprocessed_file(Epp, File, InCorrectFile) -> + case epp:parse_erl_form(Epp) of + {ok,Form} -> + case Form of + {attribute,_,file,{File,_}} -> + parse_preprocessed_file(Epp, File, true); + {attribute,_,file,{_OtherFile,_}} -> + parse_preprocessed_file(Epp, File, false); + {function,L,F,A,Cs} when InCorrectFile -> + {CLs,LastCL} = find_clause_lines(Cs, []), + %% tl(CLs) cause we know the start line already + [{atom_to_list(F),A,get_line(L),LastCL} | tl(CLs)] ++ + parse_preprocessed_file(Epp, File, true); + _ -> + parse_preprocessed_file(Epp, File, InCorrectFile) + end; + {error,Reason={_L,epp,{undefined,_Macro,none}}} -> + throw({error,Reason,InCorrectFile}); + {error,_Reason} -> + parse_preprocessed_file(Epp, File, InCorrectFile); + {warning,_} -> + parse_preprocessed_file(Epp, File, InCorrectFile); + {eof,_Location} -> + [] + end. + +parse_non_preprocessed_file(File) -> + case file:open(File, []) of + {ok,Epp} -> + Forms = parse_non_preprocessed_file(Epp, File, 1), + ok = file:close(Epp), + {ok,Forms}; + Error = {error,_E} -> + Error + end. + +parse_non_preprocessed_file(Epp, File, Location) -> + case epp_dodger:parse_form(Epp, Location) of + {ok,Tree,Location1} -> + try erl_syntax:revert(Tree) of + {function,L,F,A,Cs} -> + {CLs,LastCL} = find_clause_lines(Cs, []), + %% tl(CLs) cause we know the start line already + [{atom_to_list(F),A,get_line(L),LastCL} | tl(CLs)] ++ + parse_non_preprocessed_file(Epp, File, Location1); + _ -> + parse_non_preprocessed_file(Epp, File, Location1) + catch + _:_ -> parse_non_preprocessed_file(Epp, File, Location1) + end; + {error,_E,Location1} -> + parse_non_preprocessed_file(Epp, File, Location1); + {eof,_Location} -> + [] + end. + +get_line(Anno) -> + erl_anno:line(Anno). + +%%%----------------------------------------------------------------- +%%% Find the line number of the last expression in the function +find_clause_lines([{clause,CL,_Params,_Op,Exprs}], CLs) -> % last clause + case classify_exprs(Exprs) of + {anno, Anno} -> + {lists:reverse([{clause,get_line(CL)}|CLs]), get_line(Anno)}; + {tree, Exprs1} -> + find_clause_lines([{clause,CL,undefined,undefined,Exprs1}], CLs); + unknown -> + {lists:reverse([{clause,get_line(CL)}|CLs]), get_line(CL)} + end; +find_clause_lines([{clause,CL,_Params,_Op,_Exprs} | Cs], CLs) -> + find_clause_lines(Cs, [{clause,get_line(CL)}|CLs]). + +classify_exprs(Exprs) -> + case tuple_to_list(lists:last(Exprs)) of + [macro,{_var,Anno,_MACRO} | _] -> + {anno, Anno}; + [T,ExprAnno | Exprs1] -> + case erl_anno:is_anno(ExprAnno) of + true -> + {anno, ExprAnno}; + false when T =:= tree -> + {tree, Exprs1}; + false -> + unknown + end + end. + +%%%----------------------------------------------------------------- +%%% Add a link target for each line and one for each function definition. +build_html(SFd,DFd,Encoding,FuncsAndCs) -> + build_html(SFd,DFd,Encoding,file:read_line(SFd),1,FuncsAndCs, + false,undefined). + +%% line of last expression in function found +build_html(SFd,DFd,Enc,{ok,Str},LastL,FuncsAndCs,_IsFuncDef,{F,LastL}) -> + LastLineLink = test_server_ctrl:uri_encode(F++"-last_expr",utf8), + ok = file:write(DFd,["<a name=\"", + to_raw_list(LastLineLink,Enc),"\"/>"]), + build_html(SFd,DFd,Enc,{ok,Str},LastL,FuncsAndCs,true,undefined); +%% function start line found +build_html(SFd,DFd,Enc,{ok,Str},L0,[{F,A,L0,LastL}|FuncsAndCs], + _IsFuncDef,_FAndLastL) -> + FALink = test_server_ctrl:uri_encode(F++"-"++integer_to_list(A),utf8), + ok = file:write(DFd,["<a name=\"",to_raw_list(FALink,Enc),"\"/>"]), + build_html(SFd,DFd,Enc,{ok,Str},L0,FuncsAndCs,true,{F,LastL}); +build_html(SFd,DFd,Enc,{ok,Str},L,[{clause,L}|FuncsAndCs], + _IsFuncDef,FAndLastL) -> + build_html(SFd,DFd,Enc,{ok,Str},L,FuncsAndCs,true,FAndLastL); +build_html(SFd,DFd,Enc,{ok,Str},L,FuncsAndCs,IsFuncDef,FAndLastL) -> + LStr = line_number(L), + Str1 = line(Str,IsFuncDef), + ok = file:write(DFd,[LStr,Str1]), + build_html(SFd,DFd,Enc,file:read_line(SFd),L+1,FuncsAndCs,false,FAndLastL); +build_html(_SFd,_DFd,_Enc,eof,L,_FuncsAndCs,_IsFuncDef,_FAndLastL) -> + L. + +line_number(L) -> + LStr = integer_to_list(L), + Pred = + case length(LStr) of + Length when Length < 5 -> + lists:duplicate(5-Length,$\s); + _ -> + [] + end, + ["<a name=\"",LStr,"\"/>",Pred,LStr,": "]. + +line(Str,IsFuncDef) -> + Str1 = htmlize(Str), + possibly_enhance(Str1,IsFuncDef). + +%%%----------------------------------------------------------------- +%%% Substitute special characters that should not appear in HTML +htmlize([$<|Str]) -> + [$&,$l,$t,$;|htmlize(Str)]; +htmlize([$>|Str]) -> + [$&,$g,$t,$;|htmlize(Str)]; +htmlize([$&|Str]) -> + [$&,$a,$m,$p,$;|htmlize(Str)]; +htmlize([$"|Str]) -> + [$&,$q,$u,$o,$t,$;|htmlize(Str)]; +htmlize([Ch|Str]) -> + [Ch|htmlize(Str)]; +htmlize([]) -> + []. + +%%%----------------------------------------------------------------- +%%% Write comments in italic and function definitions in bold. +possibly_enhance(Str,true) -> + case lists:splitwith(fun($() -> false; (_) -> true end, Str) of + {_,[]} -> Str; + {F,A} -> ["<b>",F,"</b>",A] + end; +possibly_enhance([$%|_]=Str,_) -> + ["<i>",Str--"\n","</i>","\n"]; +possibly_enhance([$-|_]=Str,_) -> + possibly_enhance(Str,true); +possibly_enhance(Str,false) -> + Str. + +%%%----------------------------------------------------------------- +%%% End of the file +footer() -> + "". + +%%%----------------------------------------------------------------- +%%% Read encoding from source file +encoding(File) -> + case epp:read_encoding(File) of + none -> + epp:default_encoding(); + E -> + E + end. + +%%%----------------------------------------------------------------- +%%% Covert encoding atom to string for use in HTML header +html_encoding(latin1) -> + "iso-8859-1"; +html_encoding(utf8) -> + "utf-8". + +%%%----------------------------------------------------------------- +%%% Convert a string to a list of raw printable characters in the +%%% given encoding. This is necessary since the files (source and +%%% destination) are both opened in raw mode (default encoding). Byte +%%% by byte is read from source and written to the destination. This +%%% conversion is needed when printing data that is not first read +%%% from the source. +%%% +%%% Example: if the encoding of the file is utf8, and we have a string +%%% containing "Ã¥" = [229], then we need to convert this to [195,165] +%%% before writing. Note that this conversion is only necessary +%%% because the destination file is not (necessarily) opened with utf8 +%%% encoding - it is opened with default encoding in order to allow +%%% raw file mode and byte by byte copying from source. +to_raw_list(X,latin1) when is_list(X) -> + X; +to_raw_list(X,utf8) when is_list(X) -> + binary_to_list(unicode:characters_to_binary(X)). diff --git a/lib/common_test/src/test_server.erl b/lib/common_test/src/test_server.erl new file mode 100644 index 0000000000..e56106408f --- /dev/null +++ b/lib/common_test/src/test_server.erl @@ -0,0 +1,2809 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2017. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +-module(test_server). + +-define(DEFAULT_TIMETRAP_SECS, 60). + +%%% TEST_SERVER_CTRL INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([run_test_case_apply/1,init_target_info/0,init_valgrind/0]). +-export([cover_compile/1,cover_analyse/2]). + +%%% TEST_SERVER_SUP INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([get_loc/1,set_tc_state/1]). + +%%% TEST SUITE INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([lookup_config/2]). +-export([fail/0,fail/1,format/1,format/2,format/3]). +-export([capture_start/0,capture_stop/0,capture_get/0]). +-export([messages_get/0]). +-export([permit_io/2]). +-export([hours/1,minutes/1,seconds/1,sleep/1,adjusted_sleep/1,timecall/3]). +-export([timetrap_scale_factor/0,timetrap/1,get_timetrap_info/0, + timetrap_cancel/1,timetrap_cancel/0]). +-export([m_out_of_n/3,do_times/4,do_times/2]). +-export([call_crash/3,call_crash/4,call_crash/5]). +-export([temp_name/1]). +-export([start_node/3, stop_node/1, wait_for_node/1, is_release_available/1]). +-export([app_test/1, app_test/2, appup_test/1]). +-export([is_native/1]). +-export([comment/1, make_priv_dir/0]). +-export([os_type/0]). +-export([run_on_shielded_node/2]). +-export([is_cover/0,is_debug/0,is_commercial/0]). + +-export([break/1,break/2,break/3,continue/0,continue/1]). + +%%% DEBUGGER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([valgrind_new_leaks/0, valgrind_format/2, + is_valgrind/0]). + +%%% PRIVATE EXPORTED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-include("test_server_internal.hrl"). +-include_lib("kernel/include/file.hrl"). + +init_target_info() -> + [$.|Emu] = code:objfile_extension(), + {_, OTPRel} = init:script_id(), + #target_info{os_family=test_server_sup:get_os_family(), + os_type=os:type(), + version=erlang:system_info(version), + system_version=erlang:system_info(system_version), + root_dir=code:root_dir(), + emulator=Emu, + otp_release=OTPRel, + username=test_server_sup:get_username(), + cookie=atom_to_list(erlang:get_cookie())}. + +init_valgrind() -> + valgrind_new_leaks(). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% cover_compile(#cover{app=App,incl=Include,excl=Exclude,cross=Cross}) -> +%% {ok,#cover{mods=AnalyseModules}} | {error,Reason} +%% +%% App = atom() , name of application to be compiled +%% Exclude = [atom()], list of modules to exclude +%% Include = [atom()], list of modules outside of App that should be included +%% in the cover compilation +%% Cross = [atoms()], list of modules outside of App shat should be included +%% in the cover compilation, but that shall not be part of +%% the cover analysis for this application. +%% AnalyseModules = [atom()], list of successfully compiled modules +%% +%% Cover compile the given application. Return {ok,CoverInfo} if +%% compilation succeeds, else (if application is not found and there +%% are no modules to compile) {error,application_not_found}. + +cover_compile(CoverInfo=#cover{app=none,incl=Include,cross=Cross}) -> + CrossMods = lists:flatmap(fun({_,M}) -> M end,Cross), + CompileMods = Include++CrossMods, + case length(CompileMods) of + 0 -> + io:fwrite("WARNING: No modules to cover compile!\n\n",[]), + {ok, _} = start_cover(), % start cover server anyway + {ok,CoverInfo#cover{mods=[]}}; + N -> + io:fwrite("Cover compiling ~w modules - " + "this may take some time... ",[N]), + do_cover_compile(CompileMods), + io:fwrite("done\n\n",[]), + {ok,CoverInfo#cover{mods=Include}} + end; +cover_compile(CoverInfo=#cover{app=App,excl=all,incl=Include,cross=Cross}) -> + CrossMods = lists:flatmap(fun({_,M}) -> M end,Cross), + CompileMods = Include++CrossMods, + case length(CompileMods) of + 0 -> + io:fwrite("WARNING: No modules to cover compile!\n\n",[]), + {ok, _} = start_cover(), % start cover server anyway + {ok,CoverInfo#cover{mods=[]}}; + N -> + io:fwrite("Cover compiling '~w' (~w files) - " + "this may take some time... ",[App,N]), + io:format("\nWARNING: All modules in \'~w\' are excluded\n" + "Only cover compiling modules in include list " + "and the modules\nin the cross cover file:\n" + "~tp\n", [App,CompileMods]), + do_cover_compile(CompileMods), + io:fwrite("done\n\n",[]), + {ok,CoverInfo#cover{mods=Include}} + end; +cover_compile(CoverInfo=#cover{app=App,excl=Exclude, + incl=Include,cross=Cross}) -> + CrossMods = lists:flatmap(fun({_,M}) -> M end,Cross), + case code:lib_dir(App) of + {error,bad_name} -> + case Include++CrossMods of + [] -> + io:format("\nWARNING: Can't find lib_dir for \'~w\'\n" + "Not cover compiling!\n\n",[App]), + {error,application_not_found}; + CompileMods -> + io:fwrite("Cover compiling '~w' (~w files) - " + "this may take some time... ", + [App,length(CompileMods)]), + io:format("\nWARNING: Can't find lib_dir for \'~w\'\n" + "Only cover compiling modules in include list: " + "~tp\n", [App,Include]), + do_cover_compile(CompileMods), + io:fwrite("done\n\n",[]), + {ok,CoverInfo#cover{mods=Include}} + end; + LibDir -> + EbinDir = filename:join([LibDir,"ebin"]), + WC = filename:join(EbinDir,"*.beam"), + AllMods = module_names(filelib:wildcard(WC)), + AnalyseMods = (AllMods ++ Include) -- Exclude, + CompileMods = AnalyseMods ++ CrossMods, + case length(CompileMods) of + 0 -> + io:fwrite("WARNING: No modules to cover compile!\n\n",[]), + {ok, _} = start_cover(), % start cover server anyway + {ok,CoverInfo#cover{mods=[]}}; + N -> + io:fwrite("Cover compiling '~w' (~w files) - " + "this may take some time... ",[App,N]), + do_cover_compile(CompileMods), + io:fwrite("done\n\n",[]), + {ok,CoverInfo#cover{mods=AnalyseMods}} + end + end. + + +module_names(Beams) -> + [list_to_atom(filename:basename(filename:rootname(Beam))) || Beam <- Beams]. + + +do_cover_compile(Modules) -> + {ok, _} = start_cover(), + Sticky = prepare_cover_compile(Modules,[]), + R = cover:compile_beam(Modules), + _ = [warn_compile(Error) || Error <- R,element(1,Error)=/=ok], + _ = [code:stick_mod(M) || M <- Sticky], + ok. + +warn_compile({error,{Reason,Module}}) -> + io:fwrite("\nWARNING: Could not cover compile ~ts: ~tp\n", + [Module,{error,Reason}]). + +%% Make sure all modules are loaded and unstick if sticky +prepare_cover_compile([M|Ms],Sticky) -> + case {code:is_sticky(M),code:is_loaded(M)} of + {true,_} -> + code:unstick_mod(M), + prepare_cover_compile(Ms,[M|Sticky]); + {false,false} -> + case code:load_file(M) of + {module,_} -> + prepare_cover_compile([M|Ms],Sticky); + Error -> + io:fwrite("\nWARNING: Could not load ~w: ~tp\n",[M,Error]), + prepare_cover_compile(Ms,Sticky) + end; + {false,_} -> + prepare_cover_compile(Ms,Sticky) + end; +prepare_cover_compile([],Sticky) -> + Sticky. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% cover_analyse(Dir,#cover{level=Analyse,mods=Modules,stop=Stop) -> +%% [{M,{Cov,NotCov,Details}}] +%% +%% Dir = string() +%% Analyse = details | overview +%% Modules = [atom()], the modules to analyse +%% +%% Cover analysis. If Analyse==details analyse_to_file is used. +%% +%% If Analyse==overview analyse_to_file is not used, only an overview +%% containing the number of covered/not covered lines in each module. +%% +%% Also, cover data will be exported to a file called all.coverdata in +%% the given directory. +%% +%% Finally, if Stop==true, then cover will be stopped after the +%% analysis is completed. Stopping cover causes the original (non +%% cover compiled) modules to be loaded back in. If a process at this +%% point is still running old code of any of the cover compiled +%% modules, meaning that is has not done any fully qualified function +%% call after the cover compilation, the process will now be +%% killed. To avoid this scenario, it is possible to set Stop=false, +%% which means that the modules will stay cover compiled. Note that +%% this is only recommended if the erlang node is being terminated +%% after the test is completed. +cover_analyse(Dir,#cover{level=Analyse,mods=Modules,stop=Stop}) -> + io:fwrite(user, "Cover analysing... ", []), + {ATFOk,ATFFail} = + case Analyse of + details -> + case cover:export(filename:join(Dir,"all.coverdata")) of + ok -> + {result,Ok1,Fail1} = + cover:analyse_to_file(Modules,[{outdir,Dir},html]), + {lists:map(fun(OutFile) -> + M = list_to_atom( + filename:basename( + filename:rootname(OutFile, + ".COVER.html") + ) + ), + {M,{file,OutFile}} + end, Ok1), + lists:map(fun({Reason,M}) -> + {M,{error,Reason}} + end, Fail1)}; + Error -> + {[],lists:map(fun(M) -> {M,Error} end, Modules)} + end; + overview -> + case cover:export(filename:join(Dir,"all.coverdata")) of + ok -> + {[],lists:map(fun(M) -> {M,undefined} end, Modules)}; + Error -> + {[],lists:map(fun(M) -> {M,Error} end, Modules)} + end + end, + {result,AOk,AFail} = cover:analyse(Modules,module), + R0 = merge_analysis_results(AOk,ATFOk++ATFFail,[]) ++ + [{M,{error,Reason}} || {Reason,M} <- AFail], + R = lists:sort(R0), + io:fwrite(user, "done\n\n", []), + + case Stop of + true -> + Sticky = unstick_all_sticky(node()), + cover:stop(), + stick_all_sticky(node(),Sticky); + false -> + ok + end, + R. + +merge_analysis_results([{M,{Cov,NotCov}}|T],ATF,Acc) -> + case lists:keytake(M,1,ATF) of + {value,{_,R},ATF1} -> + merge_analysis_results(T,ATF1,[{M,{Cov,NotCov,R}}|Acc]); + false -> + merge_analysis_results(T,ATF,Acc) + end; +merge_analysis_results([],_,Acc) -> + Acc. + +do_cover_for_node(Node,CoverFunc) -> + do_cover_for_node(Node,CoverFunc,true). +do_cover_for_node(Node,CoverFunc,StickUnstick) -> + %% In case a slave node is starting another slave node! I.e. this + %% function is executed on a slave node - then the cover function + %% must be executed on the master node. This is for instance the + %% case in test_server's own tests. + MainCoverNode = cover:get_main_node(), + Sticky = + if StickUnstick -> unstick_all_sticky(MainCoverNode,Node); + true -> ok + end, + rpc:call(MainCoverNode,cover,CoverFunc,[Node]), + if StickUnstick -> stick_all_sticky(Node,Sticky); + true -> ok + end. + +unstick_all_sticky(Node) -> + unstick_all_sticky(node(),Node). +unstick_all_sticky(MainCoverNode,Node) -> + lists:filter( + fun(M) -> + case code:is_sticky(M) of + true -> + rpc:call(Node,code,unstick_mod,[M]), + true; + false -> + false + end + end, + rpc:call(MainCoverNode,cover,modules,[])). + +stick_all_sticky(Node,Sticky) -> + lists:foreach( + fun(M) -> + rpc:call(Node,code,stick_mod,[M]) + end, + Sticky). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% run_test_case_apply(Mod,Func,Args,Name,RunInit,TimetrapData) -> +%% {Time,Value,Loc,Opts,Comment} | {died,Reason,unknown,Comment} +%% +%% Time = float() (seconds) +%% Value = term() +%% Loc = term() +%% Comment = string() +%% Reason = term() +%% +%% Spawns off a process (case process) that actually runs the test suite. +%% The case process will have the job process as group leader, which makes +%% it possible to capture all it's output from io:format/2, etc. +%% +%% The job process then sits down and waits for news from the case process. +%% +%% Returns a tuple with the time spent (in seconds) in the test case, +%% the return value from the test case or an {'EXIT',Reason} if the case +%% failed, Loc points out where the test case crashed (if it did). Loc +%% is either the name of the function, or {<Module>,<Line>} of the last +%% line executed that had a ?line macro. If the test case did execute +%% erase/0 or similar, it may be empty. Comment is the last comment added +%% by test_server:comment/1, the reason if test_server:fail has been +%% called or the comment given by the return value {comment,Comment} from +%% a test case. +%% +%% {died,Reason,unknown,Comment} is returned if the test case was killed +%% by some other process. Reason is the kill reason provided. +%% +%% TimetrapData = {MultiplyTimetrap,ScaleTimetrap}, which indicates a +%% possible extension of all timetraps. Timetraps will be multiplied by +%% MultiplyTimetrap. If it is infinity, no timetraps will be started at all. +%% ScaleTimetrap indicates if test_server should attemp to automatically +%% compensate timetraps for runtime delays introduced by e.g. tools like +%% cover. + +run_test_case_apply({CaseNum,Mod,Func,Args,Name,RunInit,TimetrapData}) -> + case is_valgrind() of + false -> + ok; + true -> + valgrind_format("Test case #~w ~w:~w/1", [CaseNum, Mod, Func]), + os:putenv("VALGRIND_LOGFILE_INFIX",atom_to_list(Mod)++"."++ + atom_to_list(Func)++"-") + end, + ProcBef = erlang:system_info(process_count), + Result = run_test_case_apply(Mod, Func, Args, Name, RunInit, + TimetrapData), + ProcAft = erlang:system_info(process_count), + valgrind_new_leaks(), + DetFail = get(test_server_detected_fail), + {Result,DetFail,ProcBef,ProcAft}. + +-type tc_status() :: 'starting' | 'running' | 'init_per_testcase' | + 'end_per_testcase' | {'framework',atom(),atom()} | + 'tc'. +-record(st, + { + ref :: reference(), + pid :: pid(), + mf :: {atom(),atom()}, + last_known_loc :: term(), + status :: tc_status() | 'undefined', + ret_val :: term(), + comment :: list(char()), + timeout :: non_neg_integer() | 'infinity', + config :: list() | 'undefined', + end_conf_pid :: pid() | 'undefined' + }). + +run_test_case_apply(Mod, Func, Args, Name, RunInit, TimetrapData) -> + print_timestamp(minor,"Started at "), + print(minor, "", [], internal_raw), + TCCallback = get(test_server_testcase_callback), + LogOpts = get(test_server_logopts), + Ref = make_ref(), + Pid = + spawn_link( + run_test_case_eval_fun(Mod, Func, Args, Name, Ref, + RunInit, TimetrapData, + LogOpts, TCCallback)), + put(test_server_detected_fail, []), + St = #st{ref=Ref,pid=Pid,mf={Mod,Func},last_known_loc=unknown, + status=starting,ret_val=[],comment="",timeout=infinity, + config=hd(Args)}, + ct_util:mark_process(), + run_test_case_msgloop(St). + +%% Ugly bug (pre R5A): +%% If this process (group leader of the test case) terminates before +%% all messages have been replied back to the io server, the io server +%% hangs. Fixed by the 20 milli timeout check here, and by using monitor in +%% io.erl. +%% +%% A test case is known to have failed if it returns {'EXIT', _} tuple, +%% or sends a message {failed, File, Line} to it's group_leader +%% +run_test_case_msgloop(#st{ref=Ref,pid=Pid,end_conf_pid=EndConfPid0}=St0) -> + receive + {set_tc_state=Tag,From,{Status,Config0}} -> + Config = case Config0 of + unknown -> St0#st.config; + _ -> Config0 + end, + St = St0#st{status=Status,config=Config}, + From ! {self(),Tag,ok}, + run_test_case_msgloop(St); + {abort_current_testcase,_,_}=Abort when St0#st.status =:= starting -> + %% we're in init phase, must must postpone this operation + %% until test case execution is in progress (or FW:init_tc + %% gets killed) + self() ! Abort, + erlang:yield(), + run_test_case_msgloop(St0); + {abort_current_testcase,Reason,From} -> + Line = case is_process_alive(Pid) of + true -> get_loc(Pid); + false -> unknown + end, + Mon = erlang:monitor(process, Pid), + exit(Pid,{testcase_aborted,Reason,Line}), + erlang:yield(), + From ! {self(),abort_current_testcase,ok}, + St = receive + {'DOWN', Mon, process, Pid, _} -> + St0 + after 10000 -> + %% Pid is probably trapping exits, hit it harder... + exit(Pid, kill), + %% here's the only place we know Reason, so we save + %% it as a comment, potentially replacing user data + Error = lists:flatten(io_lib:format("Aborted: ~tp", + [Reason])), + Error1 = lists:flatten([string:strip(S,left) || + S <- string:tokens(Error, + [$\n])]), + Comment = if length(Error1) > 63 -> + string:substr(Error1,1,60) ++ "..."; + true -> + Error1 + end, + St0#st{comment=Comment} + end, + run_test_case_msgloop(St); + {sync_apply,From,MFA} -> + do_sync_apply(false,From,MFA), + run_test_case_msgloop(St0); + {sync_apply_proxy,Proxy,From,MFA} -> + do_sync_apply(Proxy,From,MFA), + run_test_case_msgloop(St0); + {comment,NewComment0} -> + NewComment1 = test_server_ctrl:to_string(NewComment0), + NewComment = test_server_sup:framework_call(format_comment, + [NewComment1], + NewComment1), + run_test_case_msgloop(St0#st{comment=NewComment}); + {read_comment,From} -> + From ! {self(),read_comment,St0#st.comment}, + run_test_case_msgloop(St0); + {make_priv_dir,From} -> + Config = case St0#st.config of + undefined -> []; + Config0 -> Config0 + end, + Result = + case proplists:get_value(priv_dir, Config) of + undefined -> + {error,no_priv_dir_in_config}; + PrivDir -> + case file:make_dir(PrivDir) of + ok -> + ok; + {error, eexist} -> + ok; + MkDirError -> + {error,{MkDirError,PrivDir}} + end + end, + From ! {self(),make_priv_dir,Result}, + run_test_case_msgloop(St0); + {'EXIT',Pid,{Ref,Time,Value,Loc,Opts}} -> + RetVal = {Time/1000000,Value,Loc,Opts}, + St = setup_termination(RetVal, St0#st{config=undefined}), + run_test_case_msgloop(St); + {'EXIT',Pid,Reason} -> + %% This exit typically happens when an unknown external process + %% has caused a test case process to terminate (e.g. if a linked + %% process has crashed). + St = + case Reason of + {What,[Loc0={_M,_F,A,[{file,_}|_]}|_]} when + is_integer(A) -> + Loc = rewrite_loc_item(Loc0), + handle_tc_exit(What, St0#st{last_known_loc=[Loc]}); + {What,[Details,Loc0={_M,_F,A,[{file,_}|_]}|_]} when + is_integer(A) -> + Loc = rewrite_loc_item(Loc0), + handle_tc_exit({What,Details}, St0#st{last_known_loc=[Loc]}); + _ -> + handle_tc_exit(Reason, St0) + end, + run_test_case_msgloop(St); + {EndConfPid0,{call_end_conf,Data,_Result}} -> + #st{mf={Mod,Func},config=CurrConf} = St0, + case CurrConf of + _ when is_list(CurrConf) -> + {_Mod,_Func,TCPid,TCExitReason,Loc} = Data, + spawn_fw_call(Mod,Func,CurrConf,TCPid, + TCExitReason,Loc,self()), + St = St0#st{config=undefined,end_conf_pid=undefined}, + run_test_case_msgloop(St); + _ -> + run_test_case_msgloop(St0) + end; + {_FwCallPid,fw_notify_done,{T,Value,Loc,Opts,AddToComment}} -> + %% the framework has been notified, we're finished + RetVal = {T,Value,Loc,Opts}, + Comment0 = St0#st.comment, + Comment = case AddToComment of + undefined -> + Comment0; + _ -> + if Comment0 =:= "" -> + AddToComment; + true -> + Comment0 ++ + test_server_ctrl:xhtml("<br>", + "<br />") ++ + AddToComment + end + end, + St = setup_termination(RetVal, St0#st{comment=Comment, + config=undefined}), + run_test_case_msgloop(St); + {'EXIT',_FwCallPid,{fw_notify_done,Func,Error}} -> + %% a framework function failed + CB = os:getenv("TEST_SERVER_FRAMEWORK"), + Loc = case CB of + FW when FW =:= false; FW =:= "undefined" -> + [{test_server,Func}]; + _ -> + [{list_to_atom(CB),Func}] + end, + RetVal = {died,{framework_error,Loc,Error},Loc}, + St = setup_termination(RetVal, St0#st{comment="Framework error", + config=undefined}), + run_test_case_msgloop(St); + {failed,File,Line} -> + put(test_server_detected_fail, + [{File, Line}| get(test_server_detected_fail)]), + run_test_case_msgloop(St0); + + {user_timetrap,Pid,_TrapTime,StartTime,E={user_timetrap_error,_},_} -> + case update_user_timetraps(Pid, StartTime) of + proceed -> + self() ! {abort_current_testcase,E,Pid}, + ok; + ignore -> + ok + end, + run_test_case_msgloop(St0); + {user_timetrap,Pid,TrapTime,StartTime,ElapsedTime,Scale} -> + %% a user timetrap is triggered, ignore it if new + %% timetrap has been started since + case update_user_timetraps(Pid, StartTime) of + proceed -> + TotalTime = if is_integer(TrapTime) -> + TrapTime + ElapsedTime; + true -> + TrapTime + end, + _ = timetrap(TrapTime, TotalTime, Pid, Scale), + ok; + ignore -> + ok + end, + run_test_case_msgloop(St0); + {timetrap_cancel_one,Handle,_From} -> + timetrap_cancel_one(Handle, false), + run_test_case_msgloop(St0); + {timetrap_cancel_all,TCPid,_From} -> + timetrap_cancel_all(TCPid, false), + run_test_case_msgloop(St0); + {get_timetrap_info,From,TCPid} -> + Info = get_timetrap_info(TCPid, false), + From ! {self(),get_timetrap_info,Info}, + run_test_case_msgloop(St0); + _Other when not is_tuple(_Other) -> + %% ignore anything not generated by test server + run_test_case_msgloop(St0); + _Other when element(1, _Other) /= 'EXIT', + element(1, _Other) /= started, + element(1, _Other) /= finished, + element(1, _Other) /= print -> + %% ignore anything not generated by test server + run_test_case_msgloop(St0) + after St0#st.timeout -> + #st{ret_val=RetVal,comment=Comment} = St0, + erlang:append_element(RetVal, Comment) + end. + +setup_termination(RetVal, #st{pid=Pid}=St) -> + timetrap_cancel_all(Pid, false), + St#st{ret_val=RetVal,timeout=20}. + +set_tc_state(State) -> + set_tc_state(State,unknown). +set_tc_state(State, Config) -> + tc_supervisor_req(set_tc_state, {State,Config}). + +handle_tc_exit(killed, St) -> + %% probably the result of an exit(TestCase,kill) call, which is the + %% only way to abort a testcase process that traps exits + %% (see abort_current_testcase). + #st{config=Config,mf={Mod,Func},pid=Pid} = St, + Msg = testcase_aborted_or_killed, + spawn_fw_call(Mod, Func, Config, Pid, Msg, unknown, self()), + St; +handle_tc_exit({testcase_aborted,{user_timetrap_error,_}=Msg,_}, St) -> + #st{config=Config,mf={Mod,Func},pid=Pid} = St, + spawn_fw_call(Mod, Func, Config, Pid, Msg, unknown, self()), + St; +handle_tc_exit(Reason, #st{status={framework,FwMod,FwFunc}, + config=Config,pid=Pid}=St) -> + R = case Reason of + {timetrap_timeout,TVal,_} -> + {timetrap,TVal}; + {testcase_aborted=E,AbortReason,_} -> + {E,AbortReason}; + {fw_error,{FwMod,FwFunc,FwError}} -> + FwError; + Other -> + Other + end, + Error = {framework_error,R}, + spawn_fw_call(FwMod, FwFunc, Config, Pid, Error, unknown, self()), + St; +handle_tc_exit(Reason, #st{status=tc,config=Config0,mf={Mod,Func},pid=Pid}=St) + when is_list(Config0) -> + {R,Loc1,F} = case Reason of + {timetrap_timeout=E,TVal,Loc0} -> + {{E,TVal},Loc0,E}; + {testcase_aborted=E,AbortReason,Loc0} -> + Msg = {E,AbortReason}, + {Msg,Loc0,Msg}; + Other -> + {{'EXIT',Other},unknown,Other} + end, + Timeout = end_conf_timeout(Reason, St), + Config = [{tc_status,{failed,F}}|Config0], + EndConfPid = call_end_conf(Mod, Func, Pid, R, Loc1, Config, Timeout), + St#st{end_conf_pid=EndConfPid}; +handle_tc_exit(Reason, #st{config=Config,mf={Mod,Func0},pid=Pid, + status=Status}=St) -> + {R,Loc1} = case Reason of + {timetrap_timeout=E,TVal,Loc0} -> + {{E,TVal},Loc0}; + {testcase_aborted=E,AbortReason,Loc0} -> + {{E,AbortReason},Loc0}; + Other -> + {{'EXIT',Other},St#st.last_known_loc} + end, + Func = case Status of + init_per_testcase=F -> {F,Func0}; + end_per_testcase=F -> {F,Func0}; + _ -> Func0 + end, + spawn_fw_call(Mod, Func, Config, Pid, R, Loc1, self()), + St. + +end_conf_timeout({timetrap_timeout,Timeout,_}, _) -> + Timeout; +end_conf_timeout(_, #st{config=Config}) when is_list(Config) -> + proplists:get_value(default_timeout, Config, ?DEFAULT_TIMETRAP_SECS*1000); +end_conf_timeout(_, _) -> + ?DEFAULT_TIMETRAP_SECS*1000. + +call_end_conf(Mod,Func,TCPid,TCExitReason,Loc,Conf,TVal) -> + Starter = self(), + Data = {Mod,Func,TCPid,TCExitReason,Loc}, + case erlang:function_exported(Mod,end_per_testcase,2) of + false -> + spawn_link(fun() -> + Starter ! {self(),{call_end_conf,Data,ok}} + end); + true -> + do_call_end_conf(Starter,Mod,Func,Data,TCExitReason,Conf,TVal) + end. + +do_call_end_conf(Starter,Mod,Func,Data,TCExitReason,Conf,TVal) -> + EndConfProc = + fun() -> + process_flag(trap_exit,true), % to catch timetraps + Supervisor = self(), + EndConfApply = + fun() -> + _ = timetrap(TVal), + %% We can't handle fails or skips here + %% (neither input nor output). The error can + %% be read from Conf though (tc_status). + EndConf = + case do_init_tc_call(Mod,{end_per_testcase,Func}, + [Conf], + {TCExitReason,[Conf]}) of + {_,[EPTCInit]} when is_list(EPTCInit) -> + EPTCInit; + _ -> + Conf + end, + try apply(Mod,end_per_testcase,[Func,EndConf]) of + _ -> ok + catch + _:Error -> + timer:sleep(1), + print_end_conf_result(Mod,Func,Conf, + "crashed",Error) + end, + Supervisor ! {self(),end_conf} + end, + Pid = spawn_link(EndConfApply), + receive + {Pid,end_conf} -> + Starter ! {self(),{call_end_conf,Data,ok}}; + {'EXIT',Pid,Reason} -> + print_end_conf_result(Mod,Func,Conf,"failed",Reason), + Starter ! {self(),{call_end_conf,Data,{error,Reason}}}; + {'EXIT',_OtherPid,Reason} -> + %% Probably the parent - not much to do about that + exit(Reason) + end + end, + spawn_link(EndConfProc). + +print_end_conf_result(Mod,Func,Conf,Cause,Error) -> + Str2Print = + fun(NoHTML) when NoHTML == stdout; NoHTML == major -> + io_lib:format("WARNING! " + "~w:end_per_testcase(~tw, ~tp)" + " ~s!\n\tReason: ~tp\n", + [Mod,Func,Conf,Cause,Error]); + (minor) -> + ErrorStr = test_server_ctrl:escape_chars(Error), + io_lib:format("WARNING! " + "~w:end_per_testcase(~tw, ~tp)" + " ~s!\n\tReason: ~ts\n", + [Mod,Func,Conf,Cause,ErrorStr]) + end, + group_leader() ! {printout,12,Str2Print}, + ok. + + +spawn_fw_call(Mod,IPTC={init_per_testcase,Func},CurrConf,Pid, + Why,Loc,SendTo) -> + FwCall = + fun() -> + ct_util:mark_process(), + Skip = {skip,{failed,{Mod,init_per_testcase,Why}}}, + %% if init_per_testcase fails, the test case + %% should be skipped + try begin do_end_tc_call(Mod,IPTC, {Pid,Skip,[CurrConf]}, Why), + do_init_tc_call(Mod,{end_per_testcase_not_run,Func}, + [CurrConf],{ok,[CurrConf]}), + do_end_tc_call(Mod,{end_per_testcase_not_run,Func}, + {Pid,Skip,[CurrConf]}, Why) end of + _ -> ok + catch + _:FwEndTCErr -> + exit({fw_notify_done,end_tc,FwEndTCErr}) + end, + Time = case Why of + {timetrap_timeout,TVal} -> TVal/1000; + _ -> died + end, + group_leader() ! {printout,12, + "ERROR! ~w:init_per_testcase(~tw, ~tp)" + " failed!\n\tReason: ~tp\n", + [Mod,Func,CurrConf,Why]}, + %% finished, report back + SendTo ! {self(),fw_notify_done,{Time,Skip,Loc,[],undefined}} + end, + spawn_link(FwCall); + +spawn_fw_call(Mod,EPTC={end_per_testcase,Func},EndConf,Pid, + Why,_Loc,SendTo) -> + FwCall = + fun() -> + ct_util:mark_process(), + {RetVal,Report} = + case proplists:get_value(tc_status, EndConf) of + undefined -> + E = {failed,{Mod,end_per_testcase,Why}}, + {E,E}; + E = {failed,Reason} -> + {E,{error,Reason}}; + Result -> + E = {failed,{Mod,end_per_testcase,Why}}, + {Result,E} + end, + {Time,Warn} = + case Why of + {timetrap_timeout,TVal} -> + group_leader() ! + {printout,12, + "WARNING! ~w:end_per_testcase(~tw, ~tp)" + " failed!\n\tReason: timetrap timeout" + " after ~w ms!\n", [Mod,Func,EndConf,TVal]}, + W = "<font color=\"red\">" + "WARNING: end_per_testcase timed out!</font>", + {TVal/1000,W}; + _ -> + group_leader() ! + {printout,12, + "WARNING! ~w:end_per_testcase(~tw, ~tp)" + " failed!\n\tReason: ~tp\n", + [Mod,Func,EndConf,Why]}, + W = "<font color=\"red\">" + "WARNING: end_per_testcase failed!</font>", + {died,W} + end, + try do_end_tc_call(Mod,EPTC,{Pid,Report,[EndConf]}, Why) of + _ -> ok + catch + _:FwEndTCErr -> + exit({fw_notify_done,end_tc,FwEndTCErr}) + end, + FailLoc = proplists:get_value(tc_fail_loc, EndConf), + %% finished, report back (if end_per_testcase fails, a warning + %% should be printed as part of the comment) + SendTo ! {self(),fw_notify_done, + {Time,RetVal,FailLoc,[],Warn}} + end, + spawn_link(FwCall); + +spawn_fw_call(FwMod,FwFunc,_,_Pid,{framework_error,FwError},_,SendTo) -> + FwCall = + fun() -> + ct_util:mark_process(), + test_server_sup:framework_call(report, [framework_error, + {{FwMod,FwFunc}, + FwError}]), + Comment = + lists:flatten( + io_lib:format("<font color=\"red\">" + "WARNING! ~w:~tw failed!</font>", + [FwMod,FwFunc])), + %% finished, report back + SendTo ! {self(),fw_notify_done, + {died,{error,{FwMod,FwFunc,FwError}}, + {FwMod,FwFunc},[],Comment}} + end, + spawn_link(FwCall); + +spawn_fw_call(Mod,Func,CurrConf,Pid,Error,Loc,SendTo) -> + ct_util:mark_process(), + {Func1,EndTCFunc} = case Func of + CF when CF == init_per_suite; CF == end_per_suite; + CF == init_per_group; CF == end_per_group -> + {CF,CF}; + TC -> {TC,{end_per_testcase,TC}} + end, + FwCall = + fun() -> + try fw_error_notify(Mod,Func1,[], + Error,Loc) of + _ -> ok + catch + _:FwErrorNotifyErr -> + exit({fw_notify_done,error_notification, + FwErrorNotifyErr}) + end, + Conf = [{tc_status,{failed,Error}}|CurrConf], + try do_end_tc_call(Mod,EndTCFunc,{Pid,Error,[Conf]},Error) of + _ -> ok + catch + _:FwEndTCErr -> + exit({fw_notify_done,end_tc,FwEndTCErr}) + end, + %% finished, report back + SendTo ! {self(),fw_notify_done,{died,Error,Loc,[],undefined}} + end, + spawn_link(FwCall). + +%% The job proxy process forwards messages between the test case +%% process on a shielded node (and its descendants) and the job process. +%% +%% The job proxy process have to be started by the test-case process +%% on the shielded node! +start_job_proxy() -> + group_leader(spawn(fun () -> job_proxy_msgloop() end), self()), ok. + +%% The io_reply_proxy is not the most satisfying solution but it works... +io_reply_proxy(ReplyTo) -> + ct_util:mark_process(), + receive + IoReply when is_tuple(IoReply), + element(1, IoReply) == io_reply -> + ReplyTo ! IoReply; + _ -> + io_reply_proxy(ReplyTo) + end. + +job_proxy_msgloop() -> + ct_util:mark_process(), + receive + + %% + %% Messages that need intervention by proxy... + %% + + %% io stuff ... + IoReq when tuple_size(IoReq) >= 2, + element(1, IoReq) == io_request -> + + ReplyProxy = spawn(fun () -> io_reply_proxy(element(2, IoReq)) end), + group_leader() ! setelement(2, IoReq, ReplyProxy); + + %% test_server stuff... + {sync_apply, From, MFA} -> + group_leader() ! {sync_apply_proxy, self(), From, MFA}; + {sync_result_proxy, To, Result} -> + To ! {sync_result, Result}; + + %% + %% Messages that need no intervention by proxy... + %% + Msg -> + group_leader() ! Msg + end, + job_proxy_msgloop(). + +-spec run_test_case_eval_fun(_, _, _, _, _, _, _, _, _) -> + fun(() -> no_return()). +run_test_case_eval_fun(Mod, Func, Args, Name, Ref, RunInit, + TimetrapData, LogOpts, TCCallback) -> + fun () -> + run_test_case_eval(Mod, Func, Args, Name, Ref, + RunInit, TimetrapData, + LogOpts, TCCallback) + end. + +%% A test case is known to have failed if it returns {'EXIT', _} tuple, +%% or sends a message {failed, File, Line} to it's group_leader + +run_test_case_eval(Mod, Func, Args0, Name, Ref, RunInit, + TimetrapData, LogOpts, TCCallback) -> + put(test_server_multiply_timetraps, TimetrapData), + put(test_server_logopts, LogOpts), + Where = [{Mod,Func}], + put(test_server_loc, Where), + + FWInitFunc = case RunInit of + run_init -> {init_per_testcase,Func}; + _ -> Func + end, + + FWInitResult0 = do_init_tc_call(Mod,FWInitFunc,Args0,{ok,Args0}), + + set_tc_state(running), + {{Time,Value},Loc,Opts} = + case FWInitResult0 of + {ok,Args} -> + run_test_case_eval1(Mod, Func, Args, Name, RunInit, TCCallback); + Error = {error,_Reason} -> + NewResult = do_end_tc_call(Mod,FWInitFunc, {Error,Args0}, + {auto_skip,{failed,Error}}), + {{0,NewResult},Where,[]}; + {fail,Reason} -> + Conf = [{tc_status,{failed,Reason}} | hd(Args0)], + fw_error_notify(Mod, Func, Conf, Reason), + NewResult = do_end_tc_call(Mod,FWInitFunc, + {{error,Reason},[Conf]}, + {fail,Reason}), + {{0,NewResult},Where,[]}; + Skip = {SkipType,_Reason} when SkipType == skip; + SkipType == skipped -> + NewResult = do_end_tc_call(Mod,FWInitFunc, + {Skip,Args0}, Skip), + {{0,NewResult},Where,[]}; + AutoSkip = {auto_skip,_Reason} -> + %% special case where a conf case "pretends" to be skipped + NewResult = + do_end_tc_call(Mod,FWInitFunc, {AutoSkip,Args0}, AutoSkip), + {{0,NewResult},Where,[]} + end, + exit({Ref,Time,Value,Loc,Opts}). + +run_test_case_eval1(Mod, Func, Args, Name, RunInit, TCCallback) -> + case RunInit of + run_init -> + set_tc_state(init_per_testcase, hd(Args)), + ensure_timetrap(Args), + case init_per_testcase(Mod, Func, Args) of + Skip = {SkipType,Reason} when SkipType == skip; + SkipType == skipped -> + Line = get_loc(), + Conf = [{tc_status,{skipped,Reason}}|hd(Args)], + NewRes = do_end_tc_call(Mod,{init_per_testcase,Func}, + {Skip,[Conf]}, Skip), + {{0,NewRes},Line,[]}; + {skip_and_save,Reason,SaveCfg} -> + Line = get_loc(), + Conf = [{tc_status,{skipped,Reason}}, + {save_config,SaveCfg}|hd(Args)], + NewRes = do_end_tc_call(Mod,{init_per_testcase,Func}, + {{skip,Reason},[Conf]}, + {skip,Reason}), + {{0,NewRes},Line,[]}; + FailTC = {fail,Reason} -> % user fails the testcase + EndConf = [{tc_status,{failed,Reason}} | hd(Args)], + fw_error_notify(Mod, Func, EndConf, Reason), + NewRes = do_end_tc_call(Mod,{init_per_testcase,Func}, + {{error,Reason},[EndConf]}, + FailTC), + {{0,NewRes},[{Mod,Func}],[]}; + {ok,NewConf} -> + IPTCEndRes = do_end_tc_call(Mod,{init_per_testcase,Func}, + {ok,[NewConf]}, NewConf), + {{T,Return},Loc,NewConf1} = + if not is_list(IPTCEndRes) -> + %% received skip or fail, not config + {{0,IPTCEndRes},undefined,NewConf}; + true -> + %% call user callback function if defined + NewConfUC = + user_callback(TCCallback, Mod, Func, + init, IPTCEndRes), + %% save current state in controller loop + set_tc_state(tc, NewConfUC), + %% execute the test case + {ts_tc(Mod,Func,[NewConfUC]),get_loc(),NewConfUC} + end, + {EndConf,TSReturn,FWReturn} = + case Return of + {E,TCError} when E=='EXIT' ; E==failed -> + fw_error_notify(Mod, Func, NewConf1, + TCError, Loc), + {[{tc_status,{failed,TCError}}, + {tc_fail_loc,Loc}|NewConf1], + Return,{error,TCError}}; + SaveCfg={save_config,_} -> + {[{tc_status,ok},SaveCfg|NewConf1],Return,ok}; + {skip_and_save,Why,SaveCfg} -> + Skip = {skip,Why}, + {[{tc_status,{skipped,Why}}, + {save_config,SaveCfg}|NewConf1], + Skip,Skip}; + {SkipType,Why} when SkipType == skip; + SkipType == skipped -> + {[{tc_status,{skipped,Why}}|NewConf1],Return, + Return}; + _ -> + {[{tc_status,ok}|NewConf1],Return,ok} + end, + %% call user callback function if defined + EndConf1 = + user_callback(TCCallback, Mod, Func, 'end', EndConf), + + %% We can't handle fails or skips here + EndConf2 = + case do_init_tc_call(Mod,{end_per_testcase,Func}, + [EndConf1],{ok,[EndConf1]}) of + {ok,[EPTCInitRes]} when is_list(EPTCInitRes) -> + EPTCInitRes; + _ -> + EndConf1 + end, + + %% update current state in controller loop + {FWReturn1,TSReturn1,EndConf3} = + case end_per_testcase(Mod, Func, EndConf2) of + SaveCfg1={save_config,_} -> + {FWReturn,TSReturn, + [SaveCfg1|lists:keydelete(save_config,1, + EndConf2)]}; + {fail,ReasonToFail} -> + %% user has failed the testcase + fw_error_notify(Mod, Func, EndConf2, + ReasonToFail), + {{error,ReasonToFail}, + {failed,ReasonToFail}, + EndConf2}; + {failed,{_,end_per_testcase,_}} = Failure when + FWReturn == ok -> + %% unexpected termination in end_per_testcase + %% report this as the result to the framework + {Failure,TSReturn,EndConf2}; + _ -> + %% test case result should be reported to + %% framework no matter the status of + %% end_per_testcase + {FWReturn,TSReturn,EndConf2} + end, + %% clear current state in controller loop + case do_end_tc_call(Mod,{end_per_testcase,Func}, + {FWReturn1,[EndConf3]}, TSReturn1) of + {failed,Reason} = NewReturn -> + fw_error_notify(Mod,Func,EndConf3, Reason), + {{T,NewReturn},[{Mod,Func}],[]}; + NewReturn -> + {{T,NewReturn},Loc,[]} + end + end; + skip_init -> + set_tc_state(running, hd(Args)), + %% call user callback function if defined + Args1 = user_callback(TCCallback, Mod, Func, init, Args), + ensure_timetrap(Args1), + %% ts_tc does a catch + %% if this is a named conf group, the test case (init or end conf) + %% should be called with the name as the first argument + Args2 = if Name == undefined -> Args1; + true -> [Name | Args1] + end, + %% execute the conf test case + {{T,Return},Loc} = {ts_tc(Mod, Func, Args2),get_loc()}, + %% call user callback function if defined + Return1 = user_callback(TCCallback, Mod, Func, 'end', Return), + {Return2,Opts} = process_return_val([Return1], Mod, Func, + Args1, [{Mod,Func}], Return1), + {{T,Return2},Loc,Opts} + end. + +do_init_tc_call(Mod, Func, Res, Return) -> + test_server_sup:framework_call(init_tc,[Mod,Func,Res],Return). + +do_end_tc_call(Mod, IPTC={init_per_testcase,Func}, Res, Return) -> + case Return of + {NOk,_} when NOk == auto_skip; NOk == fail; + NOk == skip ; NOk == skipped -> + {_,Args} = Res, + IPTCEndRes = + case do_end_tc_call1(Mod, IPTC, Res, Return) of + IPTCEndConfig when is_list(IPTCEndConfig) -> + IPTCEndConfig; + _ -> + Args + end, + EPTCInitRes = + case do_init_tc_call(Mod,{end_per_testcase_not_run,Func}, + IPTCEndRes,Return) of + {ok,EPTCInitConfig} when is_list(EPTCInitConfig) -> + {Return,EPTCInitConfig}; + _ -> + {Return,IPTCEndRes} + end, + do_end_tc_call1(Mod, {end_per_testcase_not_run,Func}, + EPTCInitRes, Return); + _Ok -> + do_end_tc_call1(Mod, IPTC, Res, Return) + end; +do_end_tc_call(Mod, Func, Res, Return) -> + do_end_tc_call1(Mod, Func, Res, Return). + +do_end_tc_call1(Mod, Func, Res, Return) -> + FwMod = os:getenv("TEST_SERVER_FRAMEWORK"), + Ref = make_ref(), + if FwMod == "ct_framework" ; FwMod == "undefined"; FwMod == false -> + case test_server_sup:framework_call( + end_tc, [Mod,Func,Res, Return], ok) of + {fail,FWReason} -> + {failed,FWReason}; + ok -> + case Return of + {fail,Reason} -> + {failed,Reason}; + Return -> + Return + end; + NewReturn -> + NewReturn + end; + true -> + case test_server_sup:framework_call(FwMod, end_tc, + [Mod,Func,Res], Ref) of + {fail,FWReason} -> + {failed,FWReason}; + _Else -> + Return + end + end. + +%% the return value is a list and we have to check if it contains +%% the result of an end conf case or if it's a Config list +process_return_val([Return], M,F,A, Loc, Final) when is_list(Return) -> + ReturnTags = [skip,skip_and_save,save_config,comment,return_group_result], + %% check if all elements in the list are valid end conf return value tuples + case lists:all(fun(Val) when is_tuple(Val) -> + lists:any(fun(T) -> T == element(1, Val) end, + ReturnTags); + (ok) -> + true; + (_) -> + false + end, Return) of + true -> % must be return value from end conf case + process_return_val1(Return, M,F,A, Loc, Final, []); + false -> % must be Config value from init conf case + case do_end_tc_call(M, F, {ok,A}, Return) of + {failed, FWReason} = Failed -> + fw_error_notify(M,F,A, FWReason), + {Failed, []}; + NewReturn -> + {NewReturn, []} + end + end; +%% the return value is not a list, so it's the return value from an +%% end conf case or it's a dummy value that can be ignored +process_return_val(Return, M,F,A, Loc, Final) -> + process_return_val1(Return, M,F,A, Loc, Final, []). + +process_return_val1([Failed={E,TCError}|_], M,F,A=[Args], Loc, _, SaveOpts) + when E=='EXIT'; + E==failed -> + fw_error_notify(M,F,A, TCError, Loc), + case do_end_tc_call(M,F, {{error,TCError}, + [[{tc_status,{failed,TCError}}|Args]]}, + Failed) of + {failed,FWReason} -> + {{failed,FWReason},SaveOpts}; + NewReturn -> + {NewReturn,SaveOpts} + end; +process_return_val1([SaveCfg={save_config,_}|Opts], M,F,[Args], + Loc, Final, SaveOpts) -> + process_return_val1(Opts, M,F,[[SaveCfg|Args]], Loc, Final, SaveOpts); +process_return_val1([{skip_and_save,Why,SaveCfg}|Opts], M,F,[Args], + Loc, _, SaveOpts) -> + process_return_val1(Opts, M,F,[[{save_config,SaveCfg}|Args]], + Loc, {skip,Why}, SaveOpts); +process_return_val1([GR={return_group_result,_}|Opts], M,F,A, + Loc, Final, SaveOpts) -> + process_return_val1(Opts, M,F,A, Loc, Final, [GR|SaveOpts]); +process_return_val1([RetVal={Tag,_}|Opts], M,F,A, + Loc, _, SaveOpts) when Tag==skip; + Tag==comment -> + process_return_val1(Opts, M,F,A, Loc, RetVal, SaveOpts); +process_return_val1([_|Opts], M,F,A, Loc, Final, SaveOpts) -> + process_return_val1(Opts, M,F,A, Loc, Final, SaveOpts); +process_return_val1([], M,F,A, _Loc, Final, SaveOpts) -> + case do_end_tc_call(M,F, {Final,A}, Final) of + {failed,FWReason} -> + {{failed,FWReason},SaveOpts}; + NewReturn -> + {NewReturn,lists:reverse(SaveOpts)} + end. + +user_callback(undefined, _, _, _, Args) -> + Args; +user_callback({CBMod,CBFunc}, Mod, Func, InitOrEnd, + [Args]) when is_list(Args) -> + case catch apply(CBMod, CBFunc, [InitOrEnd,Mod,Func,Args]) of + Args1 when is_list(Args1) -> + [Args1]; + _ -> + [Args] + end; +user_callback({CBMod,CBFunc}, Mod, Func, InitOrEnd, Args) -> + case catch apply(CBMod, CBFunc, [InitOrEnd,Mod,Func,Args]) of + Args1 when is_list(Args1) -> + Args1; + _ -> + Args + end. + +init_per_testcase(Mod, Func, Args) -> + case code:is_loaded(Mod) of + false -> + _ = code:load_file(Mod), + ok; + _ -> ok + end, + case erlang:function_exported(Mod, init_per_testcase, 2) of + true -> + do_init_per_testcase(Mod, [Func|Args]); + false -> + %% Optional init_per_testcase is not defined -- keep quiet. + [Config] = Args, + {ok, Config} + end. + +do_init_per_testcase(Mod, Args) -> + try apply(Mod, init_per_testcase, Args) of + {Skip,Reason} when Skip =:= skip; Skip =:= skipped -> + {skip,Reason}; + {skip_and_save,_,_}=Res -> + Res; + NewConf when is_list(NewConf) -> + case lists:filter(fun(T) when is_tuple(T) -> false; + (_) -> true end, NewConf) of + [] -> + {ok,NewConf}; + Bad -> + group_leader() ! {printout,12, + "ERROR! init_per_testcase has returned " + "bad elements in Config: ~tp\n",[Bad]}, + {skip,{failed,{Mod,init_per_testcase,bad_return}}} + end; + {fail,_Reason}=Res -> + Res; + _Other -> + group_leader() ! {printout,12, + "ERROR! init_per_testcase did not return " + "a Config list.\n",[]}, + {skip,{failed,{Mod,init_per_testcase,bad_return}}} + catch + throw:{Skip,Reason} when Skip =:= skip; Skip =:= skipped -> + {skip,Reason}; + exit:{Skip,Reason} when Skip =:= skip; Skip =:= skipped -> + {skip,Reason}; + throw:Other -> + set_loc(erlang:get_stacktrace()), + Line = get_loc(), + print_init_conf_result(Line,"thrown",Other), + {skip,{failed,{Mod,init_per_testcase,Other}}}; + _:Reason0 -> + Stk = erlang:get_stacktrace(), + Reason = {Reason0,Stk}, + set_loc(Stk), + Line = get_loc(), + print_init_conf_result(Line,"crashed",Reason), + {skip,{failed,{Mod,init_per_testcase,Reason}}} + end. + +print_init_conf_result(Line,Cause,Reason) -> + FormattedLoc = test_server_sup:format_loc(Line), + Str2Print = + fun(NoHTML) when NoHTML == stdout; NoHTML == major -> + io_lib:format("ERROR! init_per_testcase ~s!\n" + "\tLocation: ~tp\n\tReason: ~tp\n", + [Cause,Line,Reason]); + (minor) -> + ReasonStr = test_server_ctrl:escape_chars(Reason), + io_lib:format("ERROR! init_per_testcase ~s!\n" + "\tLocation: ~ts\n\tReason: ~ts\n", + [Cause,FormattedLoc,ReasonStr]) + end, + group_leader() ! {printout,12,Str2Print}, + ok. + + +end_per_testcase(Mod, Func, Conf) -> + case erlang:function_exported(Mod,end_per_testcase,2) of + true -> + do_end_per_testcase(Mod,end_per_testcase,Func,Conf); + false -> + %% Backwards compatibility! + case erlang:function_exported(Mod,fin_per_testcase,2) of + true -> + do_end_per_testcase(Mod,fin_per_testcase,Func,Conf); + false -> + ok + end + end. + +do_end_per_testcase(Mod,EndFunc,Func,Conf) -> + set_tc_state(end_per_testcase, Conf), + try Mod:EndFunc(Func, Conf) of + {save_config,_}=SaveCfg -> + SaveCfg; + {fail,_}=Fail -> + Fail; + _ -> + ok + catch + throw:Other -> + Comment0 = case read_comment() of + "" -> ""; + Cmt -> Cmt ++ test_server_ctrl:xhtml("<br>", + "<br />") + end, + set_loc(erlang:get_stacktrace()), + comment(io_lib:format("~ts<font color=\"red\">" + "WARNING: ~w thrown!" + "</font>\n",[Comment0,EndFunc])), + print_end_tc_warning(EndFunc,Other,"thrown",get_loc()), + {failed,{Mod,end_per_testcase,Other}}; + Class:Reason -> + Stk = erlang:get_stacktrace(), + set_loc(Stk), + Why = case Class of + exit -> {'EXIT',Reason}; + error -> {'EXIT',{Reason,Stk}} + end, + Comment0 = case read_comment() of + "" -> ""; + Cmt -> Cmt ++ test_server_ctrl:xhtml("<br>", + "<br />") + end, + comment(io_lib:format("~ts<font color=\"red\">" + "WARNING: ~w crashed!" + "</font>\n",[Comment0,EndFunc])), + print_end_tc_warning(EndFunc,Reason,"crashed",get_loc()), + {failed,{Mod,end_per_testcase,Why}} + end. + +print_end_tc_warning(EndFunc,Reason,Cause,Loc) -> + FormattedLoc = test_server_sup:format_loc(Loc), + Str2Print = + fun(NoHTML) when NoHTML == stdout; NoHTML == major -> + io_lib:format("WARNING: ~w ~s!\n" + "Reason: ~tp\nLine: ~tp\n", + [EndFunc,Cause,Reason,Loc]); + (minor) -> + ReasonStr = test_server_ctrl:escape_chars(Reason), + io_lib:format("WARNING: ~w ~s!\n" + "Reason: ~ts\nLine: ~ts\n", + [EndFunc,Cause,ReasonStr,FormattedLoc]) + end, + group_leader() ! {printout,12,Str2Print}, + ok. + +get_loc() -> + get(test_server_loc). + +get_loc(Pid) -> + [{current_stacktrace,Stk0},{dictionary,Dict}] = + process_info(Pid, [current_stacktrace,dictionary]), + lists:foreach(fun({Key,Val}) -> put(Key, Val) end, Dict), + Stk = [rewrite_loc_item(Loc) || Loc <- Stk0], + case get(test_server_loc) of + [{Suite,Case}] -> + %% Location info unknown, check if {Suite,Case,Line} + %% is available in stacktrace and if so, use stacktrace + %% instead of current test_server_loc. + %% If location is the last expression in a test case + %% function, the info is not available due to tail call + %% elimination. We need to check if the test case has been + %% called by ts_tc/3 and, if so, insert the test case info + %% at that position. + case [match || {S,C,_L} <- Stk, S == Suite, C == Case] of + [match|_] -> + put(test_server_loc, Stk); + _ -> + {PreTC,PostTC} = + lists:splitwith(fun({test_server,ts_tc,_}) -> + false; + (_) -> + true + end, Stk), + if PostTC == [] -> + ok; + true -> + put(test_server_loc, + PreTC++[{Suite,Case,last_expr} | PostTC]) + end + end; + _ -> + put(test_server_loc, Stk) + end, + get_loc(). + +fw_error_notify(Mod, Func, Args, Error) -> + test_server_sup:framework_call(error_notification, + [Mod,Func,[Args], + {Error,unknown}]). +fw_error_notify(Mod, Func, Args, Error, Loc) -> + test_server_sup:framework_call(error_notification, + [Mod,Func,[Args], + {Error,Loc}]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% print(Detail,Format,Args,Printer) -> ok +%% Detail = integer() +%% Format = string() +%% Args = [term()] +%% +%% Just like io:format, except that depending on the Detail value, the output +%% is directed to console, major and/or minor log files. + +%% print(Detail,Format,Args) -> +%% test_server_ctrl:print(Detail, Format, Args). + +print(Detail,Format,Args,Printer) -> + test_server_ctrl:print(Detail, Format, Args, Printer). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% print_timsteamp(Detail,Leader) -> ok +%% +%% Prints Leader followed by a time stamp (date and time). Depending on +%% the Detail value, the output is directed to console, major and/or minor +%% log files. + +print_timestamp(Detail,Leader) -> + test_server_ctrl:print_timestamp(Detail, Leader). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% lookup_config(Key,Config) -> {value,{Key,Value}} | undefined +%% Key = term() +%% Value = term() +%% Config = [{Key,Value},...] +%% +%% Looks up a specific key in the config list, and returns the value +%% of the associated key, or undefined if the key doesn't exist. + +lookup_config(Key,Config) -> + case lists:keysearch(Key,1,Config) of + {value,{Key,Val}} -> + Val; + _ -> + io:format("Could not find element ~tp in Config.~n",[Key]), + undefined + end. + +%% +%% IMPORTANT: get_loc/1 uses the name of this function when analysing +%% stack traces. If the name changes, get_loc/1 must be updated! +%% +ts_tc(M, F, A) -> + Before = erlang:monotonic_time(), + Result = try + apply(M, F, A) + catch + throw:{skip, Reason} -> {skip, Reason}; + throw:{skipped, Reason} -> {skip, Reason}; + exit:{skip, Reason} -> {skip, Reason}; + exit:{skipped, Reason} -> {skip, Reason}; + Type:Reason -> + Stk = erlang:get_stacktrace(), + set_loc(Stk), + case Type of + throw -> + {failed,{thrown,Reason}}; + error -> + {'EXIT',{Reason,Stk}}; + exit -> + {'EXIT',Reason} + end + end, + After = erlang:monotonic_time(), + Elapsed = erlang:convert_time_unit(After-Before, native, micro_seconds), + {Elapsed, Result}. + +set_loc(Stk) -> + Loc = case [rewrite_loc_item(I) || {_,_,_,_}=I <- Stk] of + [{M,F,0}|Stack] -> + [{M,F}|Stack]; + Other -> + Other + end, + put(test_server_loc, Loc). + +rewrite_loc_item({M,F,_,Loc}) -> + {M,F,proplists:get_value(line, Loc, 0)}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% TEST SUITE SUPPORT FUNCTIONS %% +%% %% +%% Note: Some of these functions have been moved to test_server_sup %% +%% in an attempt to keep this modules small (yeah, right!) %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% format(Format) -> IoLibReturn +%% format(Detail,Format) -> IoLibReturn +%% format(Format,Args) -> IoLibReturn +%% format(Detail,Format,Args) -> IoLibReturn +%% Detail = integer() +%% Format = string() +%% Args = [term(),...] +%% IoLibReturn = term() +%% +%% Logs the Format string and Args, similar to io:format/1/2 etc. If +%% Detail is not specified, the default detail level (which is 50) is used. +%% Which log files the string will be logged in depends on the thresholds +%% set with set_levels/3. Typically with default detail level, only the +%% minor log file is used. +format(Format) -> + format(minor, Format, []). + +format(major, Format) -> + format(major, Format, []); +format(minor, Format) -> + format(minor, Format, []); +format(Detail, Format) when is_integer(Detail) -> + format(Detail, Format, []); +format(Format, Args) -> + format(minor, Format, Args). + +format(Detail, Format, Args) -> + Str = + case catch io_lib:format(Format,Args) of + {'EXIT',_} -> + io_lib:format("illegal format; ~tp with args ~tp.\n", + [Format,Args]); + Valid -> Valid + end, + log({Detail, Str}). + +log(Msg) -> + group_leader() ! {structured_io, self(), Msg}, + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% capture_start() -> ok +%% capture_stop() -> ok +%% +%% Starts/stops capturing all output from io:format, and similar. Capturing +%% output doesn't stop output from happening. It just makes it possible +%% to retrieve the output using capture_get/0. +%% Starting and stopping capture doesn't affect already captured output. +%% All output is stored as messages in the message queue until retrieved + +capture_start() -> + group_leader() ! {capture,self()}, + ok. + +capture_stop() -> + group_leader() ! {capture,false}, + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% capture_get() -> Output +%% Output = [string(),...] +%% +%% Retrieves all the captured output since last call to capture_get/0. +%% Note that since output arrive as messages to the process, it takes +%% a short while from the call to io:format until all output is available +%% by capture_get/0. It is not necessary to call capture_stop/0 before +%% retreiving the output. +capture_get() -> + test_server_sup:capture_get([]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% messages_get() -> Messages +%% Messages = [term(),...] +%% +%% Returns all messages in the message queue. +messages_get() -> + test_server_sup:messages_get([]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% permit_io(GroupLeader, FromPid) -> ok +%% +%% Make sure proceeding IO from FromPid won't get rejected +permit_io(GroupLeader, FromPid) -> + GroupLeader ! {permit_io,FromPid}, + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% sleep(Time) -> ok +%% Time = integer() | float() | infinity +%% +%% Sleeps the specified number of milliseconds. This sleep also accepts +%% floating point numbers (which are truncated) and the atom 'infinity'. +sleep(infinity) -> + receive + after infinity -> + ok + end; +sleep(MSecs) -> + receive + after trunc(MSecs) -> + ok + end, + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% adjusted_sleep(Time) -> ok +%% Time = integer() | float() | infinity +%% +%% Sleeps the specified number of milliseconds, multiplied by the +%% 'multiply_timetraps' value (if set) and possibly also automatically scaled +%% up if 'scale_timetraps' is set to true (which is default). +%% This function also accepts floating point numbers (which are truncated) and +%% the atom 'infinity'. +adjusted_sleep(infinity) -> + receive + after infinity -> + ok + end; +adjusted_sleep(MSecs) -> + {Multiplier,ScaleFactor} = + case test_server_ctrl:get_timetrap_parameters() of + {undefined,undefined} -> + {1,1}; + {undefined,false} -> + {1,1}; + {undefined,true} -> + {1,timetrap_scale_factor()}; + {infinity,_} -> + {infinity,1}; + {Mult,undefined} -> + {Mult,1}; + {Mult,false} -> + {Mult,1}; + {Mult,true} -> + {Mult,timetrap_scale_factor()} + end, + receive + after trunc(MSecs*Multiplier*ScaleFactor) -> + ok + end, + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% fail(Reason) -> exit({suite_failed,Reason}) +%% +%% Immediately calls exit. Included because test suites are easier +%% to read when using this function, rather than exit directly. +fail(Reason) -> + comment(cast_to_list(Reason)), + try + exit({suite_failed,Reason}) + catch + Class:R -> + case erlang:get_stacktrace() of + [{?MODULE,fail,1,_}|Stk] -> ok; + Stk -> ok + end, + erlang:raise(Class, R, Stk) + end. + +cast_to_list(X) when is_list(X) -> X; +cast_to_list(X) when is_atom(X) -> atom_to_list(X); +cast_to_list(X) -> lists:flatten(io_lib:format("~tp", [X])). + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% fail() -> exit(suite_failed) +%% +%% Immediately calls exit. Included because test suites are easier +%% to read when using this function, rather than exit directly. +fail() -> + try + exit(suite_failed) + catch + Class:R -> + case erlang:get_stacktrace() of + [{?MODULE,fail,0,_}|Stk] -> ok; + Stk -> ok + end, + erlang:raise(Class, R, Stk) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% break(Comment) -> ok +%% +%% Break a test case so part of the test can be done manually. +%% Use continue/0 to continue. +break(Comment) -> + break(?MODULE, Comment). + +break(CBM, Comment) -> + break(CBM, '', Comment). + +break(CBM, TestCase, Comment) -> + timetrap_cancel(), + {TCName,CntArg,PName} = + if TestCase == '' -> + {"", "", test_server_break_process}; + true -> + Str = atom_to_list(TestCase), + {[32 | Str], Str, + list_to_atom("test_server_break_process_" ++ Str)} + end, + io:format(user, + "\n\n\n--- SEMIAUTOMATIC TESTING ---" + "\nThe test case~ts executes on process ~w" + "\n\n\n~ts" + "\n\n\n-----------------------------\n\n" + "Continue with --> ~w:continue(~ts).\n", + [TCName,self(),Comment,CBM,CntArg]), + case whereis(PName) of + undefined -> + spawn_break_process(self(), PName); + OldBreakProcess -> + OldBreakProcess ! cancel, + spawn_break_process(self(), PName) + end, + receive continue -> ok end. + +spawn_break_process(Pid, PName) -> + spawn(fun() -> + register(PName, self()), + ct_util:mark_process(), + receive + continue -> continue(Pid); + cancel -> ok + end + end). + +continue() -> + case whereis(test_server_break_process) of + undefined -> ok; + BreakProcess -> BreakProcess ! continue + end. + +continue(TestCase) when is_atom(TestCase) -> + PName = list_to_atom("test_server_break_process_" ++ + atom_to_list(TestCase)), + case whereis(PName) of + undefined -> ok; + BreakProcess -> BreakProcess ! continue + end; + +continue(Pid) when is_pid(Pid) -> + Pid ! continue. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timetrap_scale_factor() -> Factor +%% +%% Returns the amount to scale timetraps with. + +%% {X, fun() -> check() end} <- multiply scale with X if Fun() is true +timetrap_scale_factor() -> + timetrap_scale_factor([ + { 2, fun() -> has_lock_checking() end}, + { 3, fun() -> has_superfluous_schedulers() end}, + { 6, fun() -> is_debug() end}, + {10, fun() -> is_cover() end}, + {10, fun() -> is_valgrind() end} + ]). + +timetrap_scale_factor(Scales) -> + %% The fun in {S, Fun} a filter input to the list comprehension + lists:foldl(fun(S,O) -> O*S end, 1, [ S || {S,F} <- Scales, F()]). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timetrap(Timeout) -> Handle +%% Handle = term() +%% +%% Creates a time trap, that will kill the calling process if the +%% trap is not cancelled with timetrap_cancel/1, within Timeout milliseconds. +timetrap(Timeout) -> + MultAndScale = + case get(test_server_multiply_timetraps) of + undefined -> {fun(T) -> T end, true}; + {undefined,false} -> {fun(T) -> T end, false}; + {undefined,_} -> {fun(T) -> T end, true}; + {infinity,_} -> {fun(_) -> infinity end, false}; + {Int,Scale} -> {fun(infinity) -> infinity; + (T) -> T*Int end, Scale} + end, + timetrap(Timeout, Timeout, self(), MultAndScale). + +%% when the function is called from different process than +%% the test case, the test_server_multiply_timetraps data +%% is unknown and must be passed as argument +timetrap(Timeout, TCPid, MultAndScale) -> + timetrap(Timeout, Timeout, TCPid, MultAndScale). + +timetrap(Timeout0, TimeToReport0, TCPid, MultAndScale = {Multiplier,Scale}) -> + %% the time_ms call will either convert Timeout to ms or spawn a + %% user timetrap which sends the result to the IO server process + Timeout = time_ms(Timeout0, TCPid, MultAndScale), + Timeout1 = Multiplier(Timeout), + TimeToReport = if Timeout0 == TimeToReport0 -> + Timeout1; + true -> + %% only convert to ms, don't start a + %% user timetrap + time_ms_check(TimeToReport0) + end, + cancel_default_timetrap(self() == TCPid), + Handle = case Timeout1 of + infinity -> + infinity; + _ -> + spawn_link(test_server_sup,timetrap,[Timeout1,TimeToReport, + Scale,TCPid]) + end, + + %% ERROR! This sets dict on IO process instead of testcase process + %% if Timeout is return value from previous user timetrap!! + + case get(test_server_timetraps) of + undefined -> + put(test_server_timetraps,[{Handle,TCPid,{TimeToReport,Scale}}]); + List -> + List1 = lists:delete({infinity,TCPid,{infinity,false}}, List), + put(test_server_timetraps,[{Handle,TCPid, + {TimeToReport,Scale}}|List1]) + end, + Handle. + +ensure_timetrap(Config) -> + case get(test_server_timetraps) of + [_|_] -> + ok; + _ -> + case get(test_server_default_timetrap) of + undefined -> ok; + Garbage -> + erase(test_server_default_timetrap), + format("=== WARNING: garbage in " + "test_server_default_timetrap: ~tp~n", + [Garbage]) + end, + DTmo = case lists:keysearch(default_timeout,1,Config) of + {value,{default_timeout,Tmo}} -> Tmo; + _ -> ?DEFAULT_TIMETRAP_SECS + end, + format("=== test_server setting default " + "timetrap of ~p seconds~n", + [DTmo]), + put(test_server_default_timetrap, timetrap(seconds(DTmo))) + end. + +%% executing on IO process, no default timetrap ever set here +cancel_default_timetrap(false) -> + ok; +cancel_default_timetrap(true) -> + case get(test_server_default_timetrap) of + undefined -> + ok; + TimeTrap when is_pid(TimeTrap) -> + timetrap_cancel(TimeTrap), + erase(test_server_default_timetrap), + format("=== test_server canceled default timetrap " + "since another timetrap was set~n"), + ok; + Garbage -> + erase(test_server_default_timetrap), + format("=== WARNING: garbage in " + "test_server_default_timetrap: ~tp~n", + [Garbage]), + error + end. + +time_ms({hours,N}, _, _) -> hours(N); +time_ms({minutes,N}, _, _) -> minutes(N); +time_ms({seconds,N}, _, _) -> seconds(N); +time_ms({Other,_N}, _, _) -> + format("=== ERROR: Invalid time specification: ~tp. " + "Should be seconds, minutes, or hours.~n", [Other]), + exit({invalid_time_format,Other}); +time_ms(Ms, _, _) when is_integer(Ms) -> Ms; +time_ms(infinity, _, _) -> infinity; +time_ms(Fun, TCPid, MultAndScale) when is_function(Fun) -> + time_ms_apply(Fun, TCPid, MultAndScale); +time_ms({M,F,A}=MFA, TCPid, MultAndScale) when is_atom(M), + is_atom(F), + is_list(A) -> + time_ms_apply(MFA, TCPid, MultAndScale); +time_ms(Other, _, _) -> exit({invalid_time_format,Other}). + +time_ms_check(MFA = {M,F,A}) when is_atom(M), is_atom(F), is_list(A) -> + MFA; +time_ms_check(Fun) when is_function(Fun) -> + Fun; +time_ms_check(Other) -> + time_ms(Other, undefined, undefined). + +time_ms_apply(Func, TCPid, MultAndScale) -> + {_,GL} = process_info(TCPid, group_leader), + WhoAmI = self(), % either TC or IO server + T0 = erlang:monotonic_time(), + UserTTSup = + spawn(fun() -> + user_timetrap_supervisor(Func, WhoAmI, TCPid, + GL, T0, MultAndScale) + end), + receive + {UserTTSup,infinity} -> + %% remember the user timetrap so that it can be cancelled + save_user_timetrap(TCPid, UserTTSup, T0), + %% we need to make sure the user timetrap function + %% gets time to execute and return + timetrap(infinity, TCPid, MultAndScale) + after 5000 -> + exit(UserTTSup, kill), + if WhoAmI /= GL -> + exit({user_timetrap_error,time_ms_apply}); + true -> + format("=== ERROR: User timetrap execution failed!", []), + ignore + end + end. + +user_timetrap_supervisor(Func, Spawner, TCPid, GL, T0, MultAndScale) -> + process_flag(trap_exit, true), + ct_util:mark_process(), + Spawner ! {self(),infinity}, + MonRef = monitor(process, TCPid), + UserTTSup = self(), + group_leader(GL, UserTTSup), + UserTT = spawn_link(fun() -> call_user_timetrap(Func, UserTTSup) end), + receive + {UserTT,Result} -> + demonitor(MonRef, [flush]), + T1 = erlang:monotonic_time(), + Elapsed = erlang:convert_time_unit(T1-T0, native, milli_seconds), + try time_ms_check(Result) of + TimeVal -> + %% this is the new timetrap value to set (return value + %% from a fun or an MFA) + GL ! {user_timetrap,TCPid,TimeVal,T0,Elapsed,MultAndScale} + catch _:_ -> + %% when other than a legal timetrap value is returned + %% which will be the normal case for user timetraps + GL ! {user_timetrap,TCPid,0,T0,Elapsed,MultAndScale} + end; + {'EXIT',UserTT,Error} when Error /= normal -> + demonitor(MonRef, [flush]), + GL ! {user_timetrap,TCPid,0,T0,{user_timetrap_error,Error}, + MultAndScale}; + {'DOWN',MonRef,_,_,_} -> + demonitor(MonRef, [flush]), + exit(UserTT, kill) + end. + +call_user_timetrap(Func, Sup) when is_function(Func) -> + try Func() of + Result -> + Sup ! {self(),Result} + catch _:Error -> + exit({Error,erlang:get_stacktrace()}) + end; +call_user_timetrap({M,F,A}, Sup) -> + try apply(M,F,A) of + Result -> + Sup ! {self(),Result} + catch _:Error -> + exit({Error,erlang:get_stacktrace()}) + end. + +save_user_timetrap(TCPid, UserTTSup, StartTime) -> + %% save pid of user timetrap supervisor process so that + %% it may be stopped even before the timetrap func has returned + NewUserTT = {TCPid,{UserTTSup,StartTime}}, + case get(test_server_user_timetrap) of + undefined -> + put(test_server_user_timetrap, [NewUserTT]); + UserTTSups -> + case proplists:get_value(TCPid, UserTTSups) of + undefined -> + put(test_server_user_timetrap, + [NewUserTT | UserTTSups]); + PrevTTSup -> + %% remove prev user timetrap + remove_user_timetrap(PrevTTSup), + put(test_server_user_timetrap, + [NewUserTT | proplists:delete(TCPid, + UserTTSups)]) + end + end. + +update_user_timetraps(TCPid, StartTime) -> + %% called when a user timetrap is triggered + case get(test_server_user_timetrap) of + undefined -> + proceed; + UserTTs -> + case proplists:get_value(TCPid, UserTTs) of + {_UserTTSup,StartTime} -> % same timetrap + put(test_server_user_timetrap, + proplists:delete(TCPid, UserTTs)), + proceed; + {OtherUserTTSup,OtherStartTime} -> + case OtherStartTime - StartTime of + Diff when Diff >= 0 -> + ignore; + _ -> + exit(OtherUserTTSup, kill), + put(test_server_user_timetrap, + proplists:delete(TCPid, UserTTs)), + proceed + end; + undefined -> + proceed + end + end. + +remove_user_timetrap(TTSup) -> + exit(TTSup, kill). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timetrap_cancel(Handle) -> ok +%% Handle = term() +%% +%% Cancels a time trap. +timetrap_cancel(Handle) -> + timetrap_cancel_one(Handle, true). + +timetrap_cancel_one(infinity, _SendToServer) -> + ok; +timetrap_cancel_one(Handle, SendToServer) -> + case get(test_server_timetraps) of + undefined -> + ok; + [{Handle,_,_}] -> + erase(test_server_timetraps); + Timers -> + case lists:keysearch(Handle, 1, Timers) of + {value,_} -> + put(test_server_timetraps, + lists:keydelete(Handle, 1, Timers)); + false when SendToServer == true -> + group_leader() ! {timetrap_cancel_one,Handle,self()}; + false -> + ok + end + end, + test_server_sup:timetrap_cancel(Handle). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timetrap_cancel() -> ok +%% +%% Cancels timetrap for current test case. +timetrap_cancel() -> + timetrap_cancel_all(self(), true). + +timetrap_cancel_all(TCPid, SendToServer) -> + case get(test_server_timetraps) of + undefined -> + ok; + Timers -> + [timetrap_cancel_one(Handle, false) || + {Handle,Pid,_} <- Timers, Pid == TCPid], + ok + end, + case get(test_server_user_timetrap) of + undefined -> + ok; + UserTTs -> + case proplists:get_value(TCPid, UserTTs) of + {UserTTSup,_StartTime} -> + remove_user_timetrap(UserTTSup), + put(test_server_user_timetrap, + proplists:delete(TCPid, UserTTs)), + ok; + undefined -> + ok + end + end, + if SendToServer == true -> + group_leader() ! {timetrap_cancel_all,TCPid,self()}, + ok; + true -> + ok + end, + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% get_timetrap_info() -> {Timeout,Scale} | undefined +%% +%% Read timetrap info for current test case +get_timetrap_info() -> + get_timetrap_info(self(), true). + +get_timetrap_info(TCPid, SendToServer) -> + case get(test_server_timetraps) of + undefined -> + undefined; + Timers -> + case [Info || {Handle,Pid,Info} <- Timers, + Pid == TCPid, Handle /= infinity] of + [{TVal,true}|_] -> + {TVal,{true,test_server:timetrap_scale_factor()}}; + [{TVal,false}|_] -> + {TVal,{false,1}}; + [] when SendToServer == true -> + case tc_supervisor_req({get_timetrap_info,TCPid}) of + {TVal,true} -> + {TVal,{true,test_server:timetrap_scale_factor()}}; + {TVal,false} -> + {TVal,{false,1}}; + Error -> + Error + end; + [] -> + undefined + end + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% hours(N) -> Milliseconds +%% minutes(N) -> Milliseconds +%% seconds(N) -> Milliseconds +%% N = integer() | float() +%% Milliseconds = integer() +%% +%% Transforms the named units to milliseconds. Fractions in the input +%% are accepted. The output is an integer. +hours(N) -> trunc(N * 1000 * 60 * 60). +minutes(N) -> trunc(N * 1000 * 60). +seconds(N) -> trunc(N * 1000). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% tc_supervisor_req(Tag) -> Result +%% tc_supervisor_req(Tag, Msg) -> Result +%% + +tc_supervisor_req(Tag) -> + Pid = test_server_gl:get_tc_supervisor(group_leader()), + Pid ! {Tag,self()}, + receive + {Pid,Tag,Result} -> + Result + after 5000 -> + error(no_answer_from_tc_supervisor) + end. + +tc_supervisor_req(Tag, Msg) -> + Pid = test_server_gl:get_tc_supervisor(group_leader()), + Pid ! {Tag,self(),Msg}, + receive + {Pid,Tag,Result} -> + Result + after 5000 -> + error(no_answer_from_tc_supervisor) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timecall(M,F,A) -> {Time,Val} +%% Time = float() +%% +%% Measures the time spent evaluating MFA. The measurement is done with +%% erlang:now/0, and should have pretty good accuracy on most platforms. +%% The function is not evaluated in a catch context. +timecall(M, F, A) -> + test_server_sup:timecall(M,F,A). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% do_times(N,M,F,A) -> ok +%% do_times(N,Fun) -> +%% N = integer() +%% Fun = fun() -> void() +%% +%% Evaluates MFA or Fun N times, and returns ok. +do_times(N,M,F,A) when N>0 -> + apply(M,F,A), + do_times(N-1,M,F,A); +do_times(0,_,_,_) -> + ok. + +do_times(N,Fun) when N>0 -> + Fun(), + do_times(N-1,Fun); +do_times(0,_) -> + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% m_out_of_n(M,N,Fun) -> ok | exit({m_out_of_n_failed,{R,left_to_do}}) +%% M = integer() +%% N = integer() +%% Fun = fun() -> void() +%% R = integer() +%% +%% Repeats evaluating the given function until it succeeded (didn't crash) +%% M times. If, after N times, M successful attempts have not been +%% accomplished, the process crashes with reason {m_out_of_n_failed +%% {R,left_to_do}}, where R indicates how many cases that remained to be +%% successfully completed. +%% +%% For example: +%% m_out_of_n(1,4,fun() -> tricky_test_case() end) +%% Tries to run tricky_test_case() up to 4 times, +%% and is happy if it succeeds once. +%% +%% m_out_of_n(7,8,fun() -> clock_sanity_check() end) +%% Tries running clock_sanity_check() up to 8 +%% times and allows the function to fail once. +%% This might be useful if clock_sanity_check/0 +%% is known to fail if the clock crosses an hour +%% boundary during the test (and the up to 8 +%% test runs could never cross 2 boundaries) +m_out_of_n(0,_,_) -> + ok; +m_out_of_n(M,0,_) -> + exit({m_out_of_n_failed,{M,left_to_do}}); +m_out_of_n(M,N,Fun) -> + case catch Fun() of + {'EXIT',_} -> + m_out_of_n(M,N-1,Fun); + _Other -> + m_out_of_n(M-1,N-1,Fun) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%call_crash(M,F,A) +%%call_crash(Time,M,F,A) +%%call_crash(Time,Crash,M,F,A) +%% M - atom() +%% F - atom() +%% A - [term()] +%% Time - integer() in milliseconds. +%% Crash - term() +%% +%% Spaws a new process that calls MFA. The call is considered +%% successful if the call crashes with the given reason (Crash), +%% or any other reason if Crash is not specified. +%% ** The call must terminate withing the given Time (defaults +%% to infinity), or it is considered a failure (exit with reason +%% 'call_crash_timeout' is generated). + +call_crash(M,F,A) -> + call_crash(infinity,M,F,A). +call_crash(Time,M,F,A) -> + call_crash(Time,any,M,F,A). +call_crash(Time,Crash,M,F,A) -> + test_server_sup:call_crash(Time,Crash,M,F,A). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% start_node(SlaveName, Type, Options) -> +%% {ok, Slave} | {error, Reason} +%% +%% SlaveName = string(), atom(). +%% Type = slave | peer +%% Options = [{tuple(), term()}] +%% +%% OptionList is a tuplelist wich may contain one +%% or more of these members: +%% +%% Slave and Peer: +%% {remote, true} - Start the node on a remote host. If not specified, +%% the node will be started on the local host (with +%% some exceptions, for instance VxWorks, +%% where all nodes are started on a remote host). +%% {args, Arguments} - Arguments passed directly to the node. +%% {cleanup, false} - Nodes started with this option will not be killed +%% by the test server after completion of the test case +%% Therefore it is IMPORTANT that the USER terminates +%% the node!! +%% {erl, ReleaseList} - Use an Erlang emulator determined by ReleaseList +%% when starting nodes, instead of the same emulator +%% as the test server is running. ReleaseList is a list +%% of specifiers, where a specifier is either +%% {release, Rel}, {prog, Prog}, or 'this'. Rel is +%% either the name of a release, e.g., "r7a" or +%% 'latest'. 'this' means using the same emulator as +%% the test server. Prog is the name of an emulator +%% executable. If the list has more than one element, +%% one of them is picked randomly. (Only +%% works on Solaris and Linux, and the test +%% server gives warnings when it notices that +%% nodes are not of the same version as +%% itself.) +%% +%% Peer only: +%% {wait, false} - Don't wait for the node to be started. +%% {fail_on_error, false} - Returns {error, Reason} rather than failing +%% the test case. This option can only be used with +%% peer nodes. +%% Note that slave nodes always act as if they had +%% fail_on_error==false. +%% + +start_node(Name, Type, Options) -> + lists:foreach( + fun(N) -> + case firstname(N) of + Name -> + format("=== WARNING: Trying to start node \'~w\' when node" + " with same first name exists: ~w", [Name, N]); + _other -> ok + end + end, + nodes()), + + group_leader() ! {sync_apply, + self(), + {test_server_ctrl,start_node,[Name,Type,Options]}}, + Result = receive {sync_result,R} -> R end, + + case Result of + {ok,Node} -> + + %% Cannot run cover on shielded node or on a node started + %% by a shielded node. + Cover = case is_cover(Node) of + true -> + proplists:get_value(start_cover,Options,true); + false -> + false + end, + + net_adm:ping(Node), + case Cover of + true -> + do_cover_for_node(Node,start); + _ -> + ok + end, + {ok,Node}; + {fail,Reason} -> fail(Reason); + Error -> Error + end. + +firstname(N) -> + list_to_atom(upto($@,atom_to_list(N))). + +%% This should!!! crash if H is not member in list. +upto(H, [H | _T]) -> []; +upto(H, [X | T]) -> [X | upto(H,T)]. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% wait_for_node(Name) -> ok | {error,timeout} +%% +%% If a node is started with the options {wait,false}, this function +%% can be used to wait for the node to come up from the +%% test server point of view (i.e. wait until it has contacted +%% the test server controller after startup) +wait_for_node(Slave) -> + group_leader() ! {sync_apply, + self(), + {test_server_ctrl,wait_for_node,[Slave]}}, + Result = receive {sync_result,R} -> R end, + case Result of + ok -> + net_adm:ping(Slave), + case is_cover(Slave) of + true -> + do_cover_for_node(Slave,start); + _ -> + ok + end; + _ -> + ok + end, + Result. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% stop_node(Name) -> true|false +%% +%% Kills a (remote) node. +%% Also inform test_server_ctrl so it can clean up! +stop_node(Slave) -> + Cover = is_cover(Slave), + if Cover -> do_cover_for_node(Slave,flush,false); + true -> ok + end, + group_leader() ! {sync_apply,self(),{test_server_ctrl,stop_node,[Slave]}}, + Result = receive {sync_result,R} -> R end, + case Result of + ok -> + erlang:monitor_node(Slave, true), + slave:stop(Slave), + receive + {nodedown, Slave} -> + format(minor, "Stopped slave node: ~w", [Slave]), + format(major, "=node_stop ~w", [Slave]), + if Cover -> do_cover_for_node(Slave,stop,false); + true -> ok + end, + true + after 30000 -> + format("=== WARNING: Node ~w does not seem to terminate.", + [Slave]), + erlang:monitor_node(Slave, false), + receive {nodedown, Slave} -> ok after 0 -> ok end, + false + end; + {error, _Reason} -> + %% Either, the node is already dead or it was started + %% with the {cleanup,false} option, or it was started + %% in some other way than test_server:start_node/3 + format("=== WARNING: Attempt to stop a nonexisting slavenode (~w)~n" + "=== Trying to kill it anyway!!!", + [Slave]), + case net_adm:ping(Slave)of + pong -> + erlang:monitor_node(Slave, true), + slave:stop(Slave), + receive + {nodedown, Slave} -> + format(minor, "Stopped slave node: ~w", [Slave]), + format(major, "=node_stop ~w", [Slave]), + if Cover -> do_cover_for_node(Slave,stop,false); + true -> ok + end, + true + after 30000 -> + format("=== WARNING: Node ~w does not seem to terminate.", + [Slave]), + erlang:monitor_node(Slave, false), + receive {nodedown, Slave} -> ok after 0 -> ok end, + false + end; + pang -> + if Cover -> do_cover_for_node(Slave,stop,false); + true -> ok + end, + false + end + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_release_available(Release) -> true | false +%% Release -> string() +%% +%% Test if a release (such as "r10b") is available to be +%% started using start_node/3. + +is_release_available(Release) -> + group_leader() ! {sync_apply, + self(), + {test_server_ctrl,is_release_available,[Release]}}, + receive {sync_result,R} -> R end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% run_on_shielded_node(Fun, CArgs) -> term() +%% Fun -> function() +%% CArg -> list() +%% +%% +%% Fun is executed in a process on a temporarily created +%% hidden node. Communication with the job process goes +%% via a job proxy process on the hidden node, i.e. the +%% group leader of the test case process is the job proxy +%% process. This makes it possible to start nodes from the +%% hidden node that are unaware of the test server node. +%% Without the job proxy process all processes would have +%% a process residing on the test_server node as group_leader. +%% +%% Fun - Function to execute +%% CArg - Extra command line arguments to use when starting +%% the shielded node. +%% +%% If Fun is successfully executed, the result is returned. +%% + +run_on_shielded_node(Fun, CArgs) when is_function(Fun), is_list(CArgs) -> + Nr = erlang:unique_integer([positive]), + Name = "shielded_node-" ++ integer_to_list(Nr), + Node = case start_node(Name, slave, [{args, "-hidden " ++ CArgs}]) of + {ok, N} -> N; + Err -> fail({failed_to_start_shielded_node, Err}) + end, + Master = self(), + Ref = make_ref(), + Slave = spawn(Node, start_job_proxy_fun(Master, Fun)), + MRef = erlang:monitor(process, Slave), + Slave ! Ref, + receive + {'DOWN', MRef, _, _, Info} -> + stop_node(Node), + fail(Info); + {Ref, Res} -> + stop_node(Node), + receive + {'DOWN', MRef, _, _, _} -> + Res + end + end. + +-spec start_job_proxy_fun(_, _) -> fun(() -> no_return()). +start_job_proxy_fun(Master, Fun) -> + fun () -> + ct_util:mark_process(), + _ = start_job_proxy(), + receive + Ref -> + Master ! {Ref, Fun()}, + ok + end, + receive after infinity -> infinity end + end. + +%% Return true if Name or node() is a shielded node +is_shielded(Name) -> + case {cast_to_list(Name),atom_to_list(node())} of + {"shielded_node"++_,_} -> true; + {_,"shielded_node"++_} -> true; + _ -> false + end. + +same_version(Name) -> + ThisVersion = erlang:system_info(version), + OtherVersion = rpc:call(Name, erlang, system_info, [version]), + ThisVersion =:= OtherVersion. + +is_cover(Name) -> + case is_cover() of + true -> + not is_shielded(Name) andalso same_version(Name); + false -> + false + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% temp_name(Stem) -> string() +%% Stem = string() +%% +%% Create a unique file name, based on (starting with) Stem. +%% A filename of the form <Stem><Number> is generated, and the +%% function checks that that file doesn't already exist. +temp_name(Stem) -> + Num = erlang:unique_integer([positive]), + RandomName = Stem ++ integer_to_list(Num), + {ok,Files} = file:list_dir(filename:dirname(Stem)), + case lists:member(RandomName,Files) of + true -> + %% oh, already exists - bad luck. Try again. + temp_name(Stem); %% recursively try again + false -> + RandomName + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% app_test/1 +%% +app_test(App) -> + app_test(App, pedantic). +app_test(App, Mode) -> + test_server_sup:app_test(App, Mode). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% appup_test/1 +%% +appup_test(App) -> + test_server_sup:appup_test(App). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_native(Mod) -> true | false +%% +%% Checks wether the module is natively compiled or not. + +is_native(Mod) -> + (catch Mod:module_info(native)) =:= true. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% comment(String) -> ok +%% +%% The given String will occur in the comment field +%% of the table on the test suite result page. If +%% called several times, only the last comment is +%% printed. +%% comment/1 is also overwritten by the return value +%% {comment,Comment} or fail/1 (which prints Reason +%% as a comment). +comment(String) -> + group_leader() ! {comment,String}, + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% read_comment() -> string() +%% +%% Read the current comment string stored in +%% state during test case execution. +read_comment() -> + tc_supervisor_req(read_comment). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% make_priv_dir() -> ok +%% +%% Order test server to create the private directory +%% for the current test case. +make_priv_dir() -> + tc_supervisor_req(make_priv_dir). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% os_type() -> OsType +%% +%% Returns the OsType of the target node. OsType is +%% the same as returned from os:type() +os_type() -> + os:type(). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_cover() -> boolean() +%% +%% Returns true if cover is running, else false +is_cover() -> + case whereis(cover_server) of + undefined -> false; + _ -> true + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_debug() -> boolean() +%% +%% Returns true if the emulator is debug-compiled, false otherwise. +is_debug() -> + case catch erlang:system_info(debug_compiled) of + {'EXIT', _} -> + case string:str(erlang:system_info(system_version), "debug") of + Int when is_integer(Int), Int > 0 -> true; + _ -> false + end; + Res -> + Res + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% has_lock_checking() -> boolean() +%% +%% Returns true if the emulator has lock checking enabled, false otherwise. +has_lock_checking() -> + case catch erlang:system_info(lock_checking) of + {'EXIT', _} -> false; + Res -> Res + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% has_superfluous_schedulers() -> boolean() +%% +%% Returns true if the emulator has more scheduler threads than logical +%% processors, false otherwise. +has_superfluous_schedulers() -> + case catch {erlang:system_info(schedulers), + erlang:system_info(logical_processors)} of + {S, P} when is_integer(S), is_integer(P), S > P -> true; + _ -> false + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_commercial_build() -> boolean() +%% +%% Returns true if the current emulator is commercially supported. +%% (The emulator will not have "[source]" in its start-up message.) +%% We might want to do more tests on a commercial platform, for instance +%% ensuring that all applications have documentation). +is_commercial() -> + case string:str(erlang:system_info(system_version), "source") of + Int when is_integer(Int), Int > 0 -> false; + _ -> true + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_valgrind() -> boolean() +%% +%% Returns true if valgrind is running, else false +is_valgrind() -> + case catch erlang:system_info({valgrind, running}) of + {'EXIT', _} -> false; + Res -> Res + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% DEBUGGER INTERFACE %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% valgrind_new_leaks() -> ok +%% +%% Checks for new memory leaks if Valgrind is active. +valgrind_new_leaks() -> + catch erlang:system_info({valgrind, memory}), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% valgrind_format(Format, Args) -> ok +%% Format = string() +%% Args = lists() +%% +%% Outputs the formatted string to Valgrind's logfile,if Valgrind is active. +valgrind_format(Format, Args) -> + (catch erlang:system_info({valgrind, io_lib:format(Format, Args)})), + ok. + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Apply given function and reply to caller or proxy. +%% +do_sync_apply(Proxy, From, {M,F,A}) -> + Result = apply(M, F, A), + if is_pid(Proxy) -> + Proxy ! {sync_result_proxy,From,Result}, + ok; + true -> + From ! {sync_result,Result}, + ok + end. + +start_cover() -> + case cover:start() of + {error, {already_started, Pid}} -> + {ok, Pid}; + Else -> + Else + end. + diff --git a/lib/common_test/src/test_server_ctrl.erl b/lib/common_test/src/test_server_ctrl.erl new file mode 100644 index 0000000000..8ef28b3343 --- /dev/null +++ b/lib/common_test/src/test_server_ctrl.erl @@ -0,0 +1,5760 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2002-2017. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +-module(test_server_ctrl). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% %% +%% The Erlang Test Server %% +%% %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% MODULE DEPENDENCIES: +%% HARD TO REMOVE: erlang, lists, io_lib, gen_server, file, io, string, +%% code, ets, rpc, gen_tcp, inet, erl_tar, sets, +%% test_server, test_server_sup, test_server_node +%% EASIER TO REMOVE: filename, filelib, lib, re +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%% SUPERVISOR INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([start/0, start/1, start_link/1, stop/0]). + +%%% OPERATOR INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([add_spec/1, add_dir/2, add_dir/3]). +-export([add_module/1, add_module/2, + add_conf/3, + add_case/2, add_case/3, add_cases/2, add_cases/3]). +-export([add_dir_with_skip/3, add_dir_with_skip/4, add_tests_with_skip/3]). +-export([add_module_with_skip/2, add_module_with_skip/3, + add_conf_with_skip/4, + add_case_with_skip/3, add_case_with_skip/4, + add_cases_with_skip/3, add_cases_with_skip/4]). +-export([jobs/0, run_test/1, wait_finish/0, idle_notify/1, + abort_current_testcase/1, abort/0]). +-export([start_get_totals/1, stop_get_totals/0]). +-export([reject_io_reqs/1, get_levels/0, set_levels/3]). +-export([multiply_timetraps/1, scale_timetraps/1, get_timetrap_parameters/0]). +-export([create_priv_dir/1]). +-export([cover/1, cover/2, cover/3, + cover_compile/7, cover_analyse/2, cross_cover_analyse/2, + trc/1, stop_trace/0]). +-export([testcase_callback/1]). +-export([set_random_seed/1]). +-export([kill_slavenodes/0]). + +%%% TEST_SERVER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([print/2, print/3, print/4, print_timestamp/2]). +-export([start_node/3, stop_node/1, wait_for_node/1, is_release_available/1]). +-export([format/1, format/2, format/3, to_string/1]). +-export([get_target_info/0]). +-export([get_hosts/0]). +-export([node_started/1]). +-export([uri_encode/1,uri_encode/2]). + +%%% DEBUGGER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([i/0, p/1, p/3, pi/2, pi/4, t/0, t/1]). + +%%% PRIVATE EXPORTED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-export([init/1, terminate/2]). +-export([handle_call/3, handle_cast/2, handle_info/2]). +-export([do_test_cases/4]). +-export([do_spec/2, do_spec_list/2]). +-export([xhtml/2, escape_chars/1]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-include("test_server_internal.hrl"). +-include_lib("kernel/include/file.hrl"). +-define(suite_ext, "_SUITE"). +-define(log_ext, ".log.html"). +-define(src_listing_ext, ".src.html"). +-define(logdir_ext, ".logs"). +-define(data_dir_suffix, "_data/"). +-define(suitelog_name, "suite.log"). +-define(suitelog_latest_name, "suite.log.latest"). +-define(coverlog_name, "cover.html"). +-define(raw_coverlog_name, "cover.log"). +-define(cross_coverlog_name, "cross_cover.html"). +-define(raw_cross_coverlog_name, "cross_cover.log"). +-define(cross_cover_info, "cross_cover.info"). +-define(cover_total, "total_cover.log"). +-define(unexpected_io_log, "unexpected_io.log.html"). +-define(last_file, "last_name"). +-define(last_link, "last_link"). +-define(last_test, "last_test"). +-define(html_ext, ".html"). +-define(now, os:timestamp()). + +-define(void_fun, fun() -> ok end). +-define(mod_result(X), if X == skip -> skipped; + X == auto_skip -> skipped; + true -> X end). + +-define(auto_skip_color, "#FFA64D"). +-define(user_skip_color, "#FF8000"). +-define(sortable_table_name, "SortableTable"). + +-record(state,{jobs=[], levels={1,19,10}, reject_io_reqs=false, + multiply_timetraps=1, scale_timetraps=true, + create_priv_dir=auto_per_run, finish=false, + target_info, trc=false, cover=false, wait_for_node=[], + testcase_callback=undefined, idle_notify=[], + get_totals=false, random_seed=undefined}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% OPERATOR INTERFACE + +add_dir(Name, Job=[Dir|_Dirs]) when is_list(Dir) -> + add_job(cast_to_list(Name), + lists:map(fun(D)-> {dir,cast_to_list(D)} end, Job)); +add_dir(Name, Dir) -> + add_job(cast_to_list(Name), {dir,cast_to_list(Dir)}). + +add_dir(Name, Job=[Dir|_Dirs], Pattern) when is_list(Dir) -> + add_job(cast_to_list(Name), + lists:map(fun(D)-> {dir,cast_to_list(D), + cast_to_list(Pattern)} end, Job)); +add_dir(Name, Dir, Pattern) -> + add_job(cast_to_list(Name), {dir,cast_to_list(Dir),cast_to_list(Pattern)}). + +add_module(Mod) when is_atom(Mod) -> + add_job(atom_to_list(Mod), {Mod,all}). + +add_module(Name, Mods) when is_list(Mods) -> + add_job(cast_to_list(Name), lists:map(fun(Mod) -> {Mod,all} end, Mods)). + +add_conf(Name, Mod, Conf) when is_tuple(Conf) -> + add_job(cast_to_list(Name), {Mod,[Conf]}); + +add_conf(Name, Mod, Confs) when is_list(Confs) -> + add_job(cast_to_list(Name), {Mod,Confs}). + +add_case(Mod, Case) when is_atom(Mod), is_atom(Case) -> + add_job(atom_to_list(Mod), {Mod,Case}). + +add_case(Name, Mod, Case) when is_atom(Mod), is_atom(Case) -> + add_job(Name, {Mod,Case}). + +add_cases(Mod, Cases) when is_atom(Mod), is_list(Cases) -> + add_job(atom_to_list(Mod), {Mod,Cases}). + +add_cases(Name, Mod, Cases) when is_atom(Mod), is_list(Cases) -> + add_job(Name, {Mod,Cases}). + +add_spec(Spec) -> + Name = filename:rootname(Spec, ".spec"), + case filelib:is_file(Spec) of + true -> add_job(Name, {spec,Spec}); + false -> {error,nofile} + end. + +%% This version of the interface is to be used if there are +%% suites or cases that should be skipped. + +add_dir_with_skip(Name, Job=[Dir|_Dirs], Skip) when is_list(Dir) -> + add_job(cast_to_list(Name), + lists:map(fun(D)-> {dir,cast_to_list(D)} end, Job), + Skip); +add_dir_with_skip(Name, Dir, Skip) -> + add_job(cast_to_list(Name), {dir,cast_to_list(Dir)}, Skip). + +add_dir_with_skip(Name, Job=[Dir|_Dirs], Pattern, Skip) when is_list(Dir) -> + add_job(cast_to_list(Name), + lists:map(fun(D)-> {dir,cast_to_list(D), + cast_to_list(Pattern)} end, Job), + Skip); +add_dir_with_skip(Name, Dir, Pattern, Skip) -> + add_job(cast_to_list(Name), + {dir,cast_to_list(Dir),cast_to_list(Pattern)}, Skip). + +add_module_with_skip(Mod, Skip) when is_atom(Mod) -> + add_job(atom_to_list(Mod), {Mod,all}, Skip). + +add_module_with_skip(Name, Mods, Skip) when is_list(Mods) -> + add_job(cast_to_list(Name), lists:map(fun(Mod) -> {Mod,all} end, Mods), Skip). + +add_conf_with_skip(Name, Mod, Conf, Skip) when is_tuple(Conf) -> + add_job(cast_to_list(Name), {Mod,[Conf]}, Skip); + +add_conf_with_skip(Name, Mod, Confs, Skip) when is_list(Confs) -> + add_job(cast_to_list(Name), {Mod,Confs}, Skip). + +add_case_with_skip(Mod, Case, Skip) when is_atom(Mod), is_atom(Case) -> + add_job(atom_to_list(Mod), {Mod,Case}, Skip). + +add_case_with_skip(Name, Mod, Case, Skip) when is_atom(Mod), is_atom(Case) -> + add_job(Name, {Mod,Case}, Skip). + +add_cases_with_skip(Mod, Cases, Skip) when is_atom(Mod), is_list(Cases) -> + add_job(atom_to_list(Mod), {Mod,Cases}, Skip). + +add_cases_with_skip(Name, Mod, Cases, Skip) when is_atom(Mod), is_list(Cases) -> + add_job(Name, {Mod,Cases}, Skip). + +add_tests_with_skip(LogDir, Tests, Skip) -> + add_job(LogDir, + lists:map(fun({Dir,all,all}) -> + {Dir,{dir,Dir}}; + ({Dir,Mods,all}) -> + {Dir,lists:map(fun(M) -> {M,all} end, Mods)}; + ({Dir,Mod,Cases}) -> + {Dir,{Mod,Cases}} + end, Tests), + Skip). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% COMMAND LINE INTERFACE + +parse_cmd_line(Cmds) -> + parse_cmd_line(Cmds, [], [], local, false, false, undefined). + +parse_cmd_line(['SPEC',Spec|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> + case file:consult(Spec) of + {ok, TermList} -> + Name = filename:rootname(Spec), + parse_cmd_line(Cmds, TermList++SpecList, [Name|Names], Param, + Trc, Cov, TCCB); + {error,Reason} -> + io:format("Can't open ~tw: ~tp\n",[Spec, file:format_error(Reason)]), + parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, TCCB) + end; +parse_cmd_line(['NAME',Name|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> + parse_cmd_line(Cmds, SpecList, [{name,atom_to_list(Name)}|Names], + Param, Trc, Cov, TCCB); +parse_cmd_line(['SKIPMOD',Mod|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> + parse_cmd_line(Cmds, [{skip,{Mod,"by command line"}}|SpecList], Names, + Param, Trc, Cov, TCCB); +parse_cmd_line(['SKIPCASE',Mod,Case|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> + parse_cmd_line(Cmds, [{skip,{Mod,Case,"by command line"}}|SpecList], Names, + Param, Trc, Cov, TCCB); +parse_cmd_line(['DIR',Dir|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> + Name = filename:basename(Dir), + parse_cmd_line(Cmds, [{topcase,{dir,Name}}|SpecList], [Name|Names], + Param, Trc, Cov, TCCB); +parse_cmd_line(['MODULE',Mod|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> + parse_cmd_line(Cmds,[{topcase,{Mod,all}}|SpecList],[atom_to_list(Mod)|Names], + Param, Trc, Cov, TCCB); +parse_cmd_line(['CASE',Mod,Case|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> + parse_cmd_line(Cmds,[{topcase,{Mod,Case}}|SpecList],[atom_to_list(Mod)|Names], + Param, Trc, Cov, TCCB); +parse_cmd_line(['TRACE',Trc|Cmds], SpecList, Names, Param, _Trc, Cov, TCCB) -> + parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, TCCB); +parse_cmd_line(['COVER',App,CF,Analyse|Cmds], SpecList, Names, Param, Trc, _Cov, TCCB) -> + parse_cmd_line(Cmds, SpecList, Names, Param, Trc, {{App,CF}, Analyse}, TCCB); +parse_cmd_line(['TESTCASE_CALLBACK',Mod,Func|Cmds], SpecList, Names, Param, Trc, Cov, _) -> + parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, {Mod,Func}); +parse_cmd_line([Obj|_Cmds], _SpecList, _Names, _Param, _Trc, _Cov, _TCCB) -> + io:format("~w: Bad argument: ~tw\n", [?MODULE,Obj]), + io:format(" Use the `ts' module to start tests.\n", []), + io:format(" (If you ARE using `ts', there is a bug in `ts'.)\n", []), + halt(1); +parse_cmd_line([], SpecList, Names, Param, Trc, Cov, TCCB) -> + NameList = lists:reverse(Names, ["suite"]), + Name = case lists:keysearch(name, 1, NameList) of + {value,{name,N}} -> N; + false -> hd(NameList) + end, + {lists:reverse(SpecList), Name, Param, Trc, Cov, TCCB}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% cast_to_list(X) -> string() +%% X = list() | atom() | void() +%% Returns a string representation of whatever was input + +cast_to_list(X) when is_list(X) -> X; +cast_to_list(X) when is_atom(X) -> atom_to_list(X); +cast_to_list(X) -> lists:flatten(io_lib:format("~tw", [X])). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% START INTERFACE + +%% Kept for backwards compatibility +start(_) -> + start(). +start_link(_) -> + start_link(). + + +start() -> + case gen_server:start({local,?MODULE}, ?MODULE, [], []) of + {error, {already_started, Pid}} -> + {ok, Pid}; + Other -> + Other + end. + +start_link() -> + case gen_server:start_link({local,?MODULE}, ?MODULE, [], []) of + {error, {already_started, Pid}} -> + {ok, Pid}; + Other -> + Other + end. + +run_test(CommandLine) -> + process_flag(trap_exit,true), + {SpecList,Name,Param,Trc,Cov,TCCB} = parse_cmd_line(CommandLine), + {ok,_TSPid} = start_link(Param), + case Trc of + false -> ok; + File -> trc(File) + end, + case Cov of + false -> ok; + {{App,CoverFile},Analyse} -> cover(App, maybe_file(CoverFile), Analyse) + end, + testcase_callback(TCCB), + add_job(Name, {command_line,SpecList}), + + wait_finish(). + +%% Converted CoverFile to a string unless it is 'none' +maybe_file(none) -> + none; +maybe_file(CoverFile) -> + atom_to_list(CoverFile). + +idle_notify(Fun) -> + {ok, Pid} = controller_call({idle_notify,Fun}), + Pid. + +start_get_totals(Fun) -> + {ok, Pid} = controller_call({start_get_totals,Fun}), + Pid. + +stop_get_totals() -> + ok = controller_call(stop_get_totals), + ok. + +wait_finish() -> + OldTrap = process_flag(trap_exit, true), + {ok, Pid} = finish(true), + link(Pid), + receive + {'EXIT',Pid,_} -> + ok + end, + process_flag(trap_exit, OldTrap), + ok. + +abort_current_testcase(Reason) -> + controller_call({abort_current_testcase,Reason}). + +abort() -> + OldTrap = process_flag(trap_exit, true), + {ok, Pid} = finish(abort), + link(Pid), + receive + {'EXIT',Pid,_} -> + ok + end, + process_flag(trap_exit, OldTrap), + ok. + +finish(Abort) -> + controller_call({finish,Abort}). + +stop() -> + controller_call(stop). + +jobs() -> + controller_call(jobs). + +get_levels() -> + controller_call(get_levels). + +set_levels(Show, Major, Minor) -> + controller_call({set_levels,Show,Major,Minor}). + +reject_io_reqs(Bool) -> + controller_call({reject_io_reqs,Bool}). + +multiply_timetraps(N) -> + controller_call({multiply_timetraps,N}). + +scale_timetraps(Bool) -> + controller_call({scale_timetraps,Bool}). + +get_timetrap_parameters() -> + controller_call(get_timetrap_parameters). + +create_priv_dir(Value) -> + controller_call({create_priv_dir,Value}). + +trc(TraceFile) -> + controller_call({trace,TraceFile}, 2*?ACCEPT_TIMEOUT). + +stop_trace() -> + controller_call(stop_trace). + +node_started(Node) -> + gen_server:cast(?MODULE, {node_started,Node}). + +cover(App, Analyse) when is_atom(App) -> + cover(App, none, Analyse); +cover(CoverFile, Analyse) -> + cover(none, CoverFile, Analyse). +cover(App, CoverFile, Analyse) -> + {Excl,Incl,Cross} = read_cover_file(CoverFile), + CoverInfo = #cover{app=App, + file=CoverFile, + excl=Excl, + incl=Incl, + cross=Cross, + level=Analyse}, + controller_call({cover,CoverInfo}). + +cover(CoverInfo) -> + controller_call({cover,CoverInfo}). + +cover_compile(App,File,Excl,Incl,Cross,Analyse,Stop) -> + cover_compile(#cover{app=App, + file=File, + excl=Excl, + incl=Incl, + cross=Cross, + level=Analyse, + stop=Stop}). + +testcase_callback(ModFunc) -> + controller_call({testcase_callback,ModFunc}). + +set_random_seed(Seed) -> + controller_call({set_random_seed,Seed}). + +kill_slavenodes() -> + controller_call(kill_slavenodes). + +get_hosts() -> + get(test_server_hosts). + +%%-------------------------------------------------------------------- + +add_job(Name, TopCase) -> + add_job(Name, TopCase, []). + +add_job(Name, TopCase, Skip) -> + SuiteName = + case Name of + "." -> "current_dir"; + ".." -> "parent_dir"; + Other -> Other + end, + Dir = filename:absname(SuiteName), + controller_call({add_job,Dir,SuiteName,TopCase,Skip}). + +controller_call(Arg) -> + case catch gen_server:call(?MODULE, Arg, infinity) of + {'EXIT',{{badarg,_},{gen_server,call,_}}} -> + exit(test_server_ctrl_not_running); + {'EXIT',Reason} -> + exit(Reason); + Other -> + Other + end. +controller_call(Arg, Timeout) -> + case catch gen_server:call(?MODULE, Arg, Timeout) of + {'EXIT',{{badarg,_},{gen_server,call,_}}} -> + exit(test_server_ctrl_not_running); + {'EXIT',Reason} -> + exit(Reason); + Other -> + Other + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% init([]) +%% +%% init() is the init function of the test_server's gen_server. +%% +init([]) -> + case os:getenv("TEST_SERVER_CALL_TRACE") of + false -> + ok; + "" -> + ok; + TraceSpec -> + test_server_sup:call_trace(TraceSpec) + end, + process_flag(trap_exit, true), + %% copy format_exception setting from init arg to application environment + case init:get_argument(test_server_format_exception) of + {ok,[[TSFE]]} -> + application:set_env(test_server, format_exception, list_to_atom(TSFE)); + _ -> + ok + end, + test_server_sup:cleanup_crash_dumps(), + test_server_sup:util_start(), + State = #state{jobs=[],finish=false}, + TI0 = test_server:init_target_info(), + TargetHost = test_server_sup:hoststr(), + TI = TI0#target_info{host=TargetHost, + naming=naming(), + master=TargetHost}, + _ = ets:new(slave_tab, [named_table,set,public,{keypos,2}]), + set_hosts([TI#target_info.host]), + {ok,State#state{target_info=TI}}. + +naming() -> + case lists:member($., test_server_sup:hoststr()) of + true -> "-name"; + false -> "-sname" + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(kill_slavenodes, From, State) -> ok +%% +%% Kill all slave nodes that remain after a test case +%% is completed. +%% +handle_call(kill_slavenodes, _From, State) -> + Nodes = test_server_node:kill_nodes(), + {reply, Nodes, State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({set_hosts, HostList}, From, State) -> ok +%% +%% Set the global hostlist. +%% +handle_call({set_hosts, Hosts}, _From, State) -> + set_hosts(Hosts), + {reply, ok, State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(get_hosts, From, State) -> [Hosts] +%% +%% Returns the lists of hosts that the test server +%% can use for slave nodes. This is primarily used +%% for nodename generation. +%% +handle_call(get_hosts, _From, State) -> + Hosts = get_hosts(), + {reply, Hosts, State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({add_job,Dir,Name,TopCase,Skip}, _, State) -> +%% ok | {error,Reason} +%% +%% Dir = string() +%% Name = string() +%% TopCase = term() +%% Skip = [SkipItem] +%% SkipItem = {Mod,Comment} | {Mod,Case,Comment} | {Mod,Cases,Comment} +%% Mod = Case = atom() +%% Comment = string() +%% Cases = [Case] +%% +%% Adds a job to the job queue. The name of the job is Name. A log directory +%% will be created in Dir/Name.logs. TopCase may be anything that +%% collect_cases/3 accepts, plus the following: +%% +%% {spec,SpecName} executes the named test suite specification file. Commands +%% in the file should be in the format accepted by do_spec_list/1. +%% +%% {command_line,SpecList} executes the list of specification instructions +%% supplied, which should be in the format accepted by do_spec_list/1. + +handle_call({add_job,Dir,Name,TopCase,Skip}, _From, State) -> + LogDir = Dir ++ ?logdir_ext, + ExtraTools = + case State#state.cover of + false -> []; + CoverInfo -> [{cover,CoverInfo}] + end, + ExtraTools1 = + case State#state.random_seed of + undefined -> ExtraTools; + Seed -> [{random_seed,Seed}|ExtraTools] + end, + case lists:keysearch(Name, 1, State#state.jobs) of + false -> + case TopCase of + {spec,SpecName} -> + Pid = spawn_tester( + ?MODULE, do_spec, + [SpecName,{State#state.multiply_timetraps, + State#state.scale_timetraps}], + LogDir, Name, State#state.levels, + State#state.reject_io_reqs, + State#state.create_priv_dir, + State#state.testcase_callback, ExtraTools1), + NewJobs = [{Name,Pid}|State#state.jobs], + {reply, ok, State#state{jobs=NewJobs}}; + {command_line,SpecList} -> + Pid = spawn_tester( + ?MODULE, do_spec_list, + [SpecList,{State#state.multiply_timetraps, + State#state.scale_timetraps}], + LogDir, Name, State#state.levels, + State#state.reject_io_reqs, + State#state.create_priv_dir, + State#state.testcase_callback, ExtraTools1), + NewJobs = [{Name,Pid}|State#state.jobs], + {reply, ok, State#state{jobs=NewJobs}}; + TopCase -> + case State#state.get_totals of + {CliPid,Fun} -> + Result = count_test_cases(TopCase, Skip), + Fun(CliPid, Result), + {reply, ok, State}; + _ -> + Cfg = make_config([]), + Pid = spawn_tester( + ?MODULE, do_test_cases, + [TopCase,Skip,Cfg, + {State#state.multiply_timetraps, + State#state.scale_timetraps}], + LogDir, Name, State#state.levels, + State#state.reject_io_reqs, + State#state.create_priv_dir, + State#state.testcase_callback, ExtraTools1), + NewJobs = [{Name,Pid}|State#state.jobs], + {reply, ok, State#state{jobs=NewJobs}} + end + end; + _ -> + {reply,{error,name_already_in_use},State} + end; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(jobs, _, State) -> JobList +%% JobList = [{Name,Pid}, ...] +%% Name = string() +%% Pid = pid() +%% +%% Return the list of current jobs. + +handle_call(jobs, _From, State) -> + {reply,State#state.jobs,State}; + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({abort_current_testcase,Reason}, _, State) -> Result +%% Reason = term() +%% Result = ok | {error,no_testcase_running} +%% +%% Attempts to abort the test case that's currently running. + +handle_call({abort_current_testcase,Reason}, _From, State) -> + case State#state.jobs of + [{_,Pid}|_] -> + Pid ! {abort_current_testcase,Reason,self()}, + receive + {Pid,abort_current_testcase,Result} -> + {reply, Result, State} + after 10000 -> + {reply, {error,no_testcase_running}, State} + end; + _ -> + {reply, {error,no_testcase_running}, State} + end; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({finish,Fini}, _, State) -> {ok,Pid} +%% Fini = true | abort +%% +%% Tells the test_server to stop as soon as there are no test suites +%% running. Immediately if none are running. Abort is handled as soon +%% as current test finishes. + +handle_call({finish,Fini}, _From, State) -> + case State#state.jobs of + [] -> + lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Fini) end, + State#state.idle_notify), + State2 = State#state{finish=false}, + {stop,shutdown,{ok,self()}, State2}; + _SomeJobs -> + State2 = State#state{finish=Fini}, + {reply, {ok,self()}, State2} + end; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({idle_notify,Fun}, From, State) -> {ok,Pid} +%% +%% Lets a test client subscribe to receive a notification when the +%% test server becomes idle (can be used to syncronize jobs). +%% test_server calls Fun(From) when idle. + +handle_call({idle_notify,Fun}, {Cli,_Ref}, State) -> + case State#state.jobs of + [] -> self() ! report_idle; + _ -> ok + end, + Subscribed = State#state.idle_notify, + {reply, {ok,self()}, State#state{idle_notify=[{Cli,Fun}|Subscribed]}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(start_get_totals, From, State) -> {ok,Pid} +%% +%% Switch on the mode where the test server will only +%% report back the number of tests it would execute +%% given some subsequent jobs. + +handle_call({start_get_totals,Fun}, {Cli,_Ref}, State) -> + {reply, {ok,self()}, State#state{get_totals={Cli,Fun}}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(stop_get_totals, From, State) -> ok +%% +%% Lets a test client subscribe to receive a notification when the +%% test server becomes idle (can be used to syncronize jobs). +%% test_server calls Fun(From) when idle. + +handle_call(stop_get_totals, {_Cli,_Ref}, State) -> + {reply, ok, State#state{get_totals=false}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(get_levels, _, State) -> {Show,Major,Minor} +%% Show = integer() +%% Major = integer() +%% Minor = integer() +%% +%% Returns a 3-tuple with the logging thresholds. +%% All output and information from a test suite is tagged with a detail +%% level. Lower values are more "important". Text that is output using +%% io:format or similar is automatically tagged with detail level 50. +%% +%% All output with detail level: +%% less or equal to Show is displayed on the screen (default 1) +%% less or equal to Major is logged in the major log file (default 19) +%% greater or equal to Minor is logged in the minor log files (default 10) + +handle_call(get_levels, _From, State) -> + {reply,State#state.levels,State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({set_levels,Show,Major,Minor}, _, State) -> ok +%% Show = integer() +%% Major = integer() +%% Minor = integer() +%% +%% Sets the logging thresholds, see handle_call(get_levels,...) above. + +handle_call({set_levels,Show,Major,Minor}, _From, State) -> + {reply,ok,State#state{levels={Show,Major,Minor}}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({reject_io_reqs,Bool}, _, State) -> ok +%% Bool = bool() +%% +%% May be used to switch off stdout printouts to the minor log file + +handle_call({reject_io_reqs,Bool}, _From, State) -> + {reply,ok,State#state{reject_io_reqs=Bool}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({multiply_timetraps,N}, _, State) -> ok +%% N = integer() | infinity +%% +%% Multiplies all timetraps set by test cases with N + +handle_call({multiply_timetraps,N}, _From, State) -> + {reply,ok,State#state{multiply_timetraps=N}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({scale_timetraps,Bool}, _, State) -> ok +%% Bool = true | false +%% +%% Specifies if test_server should scale the timetrap value +%% automatically if e.g. cover is running. + +handle_call({scale_timetraps,Bool}, _From, State) -> + {reply,ok,State#state{scale_timetraps=Bool}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(get_timetrap_parameters, _, State) -> {Multiplier,Scale} +%% Multiplier = integer() | infinity +%% Scale = true | false +%% +%% Returns the parameter values that affect timetraps. + +handle_call(get_timetrap_parameters, _From, State) -> + {reply,{State#state.multiply_timetraps,State#state.scale_timetraps},State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({trace,TraceFile}, _, State) -> ok | {error,Reason} +%% +%% Starts a separate node (trace control node) which +%% starts tracing on target and all slave nodes +%% +%% TraceFile is a text file with elements of type +%% {Trace,Mod,TracePattern}. +%% {Trace,Mod,Func,TracePattern}. +%% {Trace,Mod,Func,Arity,TracePattern}. +%% +%% Trace = tp | tpl; local or global call trace +%% Mod,Func = atom(), Arity=integer(); defines what to trace +%% TracePattern = [] | match_spec() +%% +%% The 'call' trace flag is set on all processes, and then +%% the given trace patterns are set. + +handle_call({trace,TraceFile}, _From, State=#state{trc=false}) -> + TI = State#state.target_info, + case test_server_node:start_tracer_node(TraceFile, TI) of + {ok,Tracer} -> {reply,ok,State#state{trc=Tracer}}; + Error -> {reply,Error,State} + end; +handle_call({trace,_TraceFile}, _From, State) -> + {reply,{error,already_tracing},State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(stop_trace, _, State) -> ok | {error,Reason} +%% +%% Stops tracing on target and all slave nodes and +%% terminates trace control node + +handle_call(stop_trace, _From, State=#state{trc=false}) -> + {reply,{error,not_tracing},State}; +handle_call(stop_trace, _From, State) -> + R = test_server_node:stop_tracer_node(State#state.trc), + {reply,R,State#state{trc=false}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({cover,CoverInfo}, _, State) -> ok | {error,Reason} +%% +%% Set specification of cover analysis to be used when running tests +%% (see start_extra_tools/1 and stop_extra_tools/1) + +handle_call({cover,CoverInfo}, _From, State) -> + {reply,ok,State#state{cover=CoverInfo}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({create_priv_dir,Value}, _, State) -> ok | {error,Reason} +%% +%% Set create_priv_dir to either auto_per_run (create common priv dir once +%% per test run), manual_per_tc (the priv dir name will be unique for each +%% test case, but the user has to call test_server:make_priv_dir/0 to create +%% it), or auto_per_tc (unique priv dir created automatically for each test +%% case). + +handle_call({create_priv_dir,Value}, _From, State) -> + {reply,ok,State#state{create_priv_dir=Value}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({testcase_callback,{Mod,Func}}, _, State) -> ok | {error,Reason} +%% +%% Add a callback function that will be called before and after every +%% test case (on the test case process): +%% +%% Mod:Func(Suite,TestCase,InitOrEnd,Config) +%% +%% InitOrEnd = init | 'end'. + +handle_call({testcase_callback,ModFunc}, _From, State) -> + case ModFunc of + {Mod,Func} -> + _ = case code:is_loaded(Mod) of + {file,_} -> + ok; + false -> + code:load_file(Mod) + end, + case erlang:function_exported(Mod,Func,4) of + true -> + ok; + false -> + io:format(user, + "WARNING! Callback function ~w:~tw/4 undefined.~n~n", + [Mod,Func]) + end; + _ -> + ok + end, + {reply,ok,State#state{testcase_callback=ModFunc}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({set_random_seed,Seed}, _, State) -> ok | {error,Reason} +%% +%% Let operator set a random seed value to be used e.g. for shuffling +%% test cases. + +handle_call({set_random_seed,Seed}, _From, State) -> + {reply,ok,State#state{random_seed=Seed}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(stop, _, State) -> ok +%% +%% Stops the test server immediately. +%% Some cleanup is done by terminate/2 + +handle_call(stop, _From, State) -> + {stop, shutdown, ok, State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call(get_target_info, _, State) -> TI +%% +%% TI = #target_info{} +%% +%% Returns information about target + +handle_call(get_target_info, _From, State) -> + {reply, State#state.target_info, State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({start_node,Name,Type,Options}, _, State) -> +%% ok | {error,Reason} +%% +%% Starts a new node (slave or peer) + +handle_call({start_node, Name, Type, Options}, From, State) -> + %% test_server_ctrl does gen_server:reply/2 explicitly + test_server_node:start_node(Name, Type, Options, From, + State#state.target_info), + {noreply,State}; + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({wait_for_node,Node}, _, State) -> ok +%% +%% Waits for a new node to take contact. Used if +%% node is started with option {wait,false} + +handle_call({wait_for_node, Node}, From, State) -> + NewWaitList = + case ets:lookup(slave_tab,Node) of + [] -> + [{Node,From}|State#state.wait_for_node]; + _ -> + gen_server:reply(From,ok), + State#state.wait_for_node + end, + {noreply,State#state{wait_for_node=NewWaitList}}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({stop_node,Name}, _, State) -> ok | {error,Reason} +%% +%% Stops a slave or peer node. This is actually only some cleanup +%% - the node is really stopped by test_server when this returns. + +handle_call({stop_node, Name}, _From, State) -> + R = test_server_node:stop_node(Name), + {reply, R, State}; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_call({is_release_available,Name}, _, State) -> ok | {error,Reason} +%% +%% Tests if the release is available. + +handle_call({is_release_available, Release}, _From, State) -> + R = test_server_node:is_release_available(Release), + {reply, R, State}. + +%%-------------------------------------------------------------------- +set_hosts(Hosts) -> + put(test_server_hosts, Hosts). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_cast({node_started,Name}, _, State) +%% +%% Called by test_server_node when a slave/peer node is fully started. + +handle_cast({node_started,Node}, State) -> + case State#state.trc of + false -> ok; + Trc -> test_server_node:trace_nodes(Trc, [Node]) + end, + NewWaitList = + case lists:keysearch(Node,1,State#state.wait_for_node) of + {value,{Node,From}} -> + gen_server:reply(From, ok), + lists:keydelete(Node, 1, State#state.wait_for_node); + false -> + State#state.wait_for_node + end, + {noreply, State#state{wait_for_node=NewWaitList}}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_info({'EXIT',Pid,Reason}, State) +%% Pid = pid() +%% Reason = term() +%% +%% Handles exit messages from linked processes. Only test suites are +%% expected to be linked. When a test suite terminates, it is removed +%% from the job queue. + +handle_info(report_idle, State) -> + Finish = State#state.finish, + lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end, + State#state.idle_notify), + {noreply,State#state{idle_notify=[]}}; + + +handle_info({'EXIT',Pid,Reason}, State) -> + case lists:keysearch(Pid,2,State#state.jobs) of + false -> + %% not our problem + {noreply,State}; + {value,{Name,_}} -> + NewJobs = lists:keydelete(Pid, 2, State#state.jobs), + case Reason of + normal -> + fine; + killed -> + io:format("Suite ~ts was killed\n", [Name]); + _Other -> + io:format("Suite ~ts was killed with reason ~tp\n", + [Name,Reason]) + end, + State2 = State#state{jobs=NewJobs}, + Finish = State2#state.finish, + case NewJobs of + [] -> + lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end, + State2#state.idle_notify), + case Finish of + false -> + {noreply,State2#state{idle_notify=[]}}; + _ -> % true | abort + %% test_server:finish() has been called and + %% there are no jobs in the job queue => + %% stop the test_server_ctrl + {stop,shutdown,State2#state{finish=false}} + end; + _ -> % pending jobs + case Finish of + abort -> % abort test now! + lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end, + State2#state.idle_notify), + {stop,shutdown,State2#state{finish=false}}; + _ -> % true | false + {noreply, State2} + end + end + end; + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_info({tcp_closed,Sock}, State) +%% +%% A Socket was closed. This indicates that a node died. +%% This can be +%% *Slave or peer node started by a test suite +%% *Trace controll node + +handle_info({tcp_closed,Sock}, State=#state{trc=Sock}) -> + %% Tracer node died - can't really do anything + %%! Maybe print something??? + {noreply,State#state{trc=false}}; +handle_info({tcp_closed,Sock}, State) -> + test_server_node:nodedown(Sock), + {noreply,State}; +handle_info(_, State) -> + %% dummy; accept all, do nothing. + {noreply, State}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% terminate(Reason, State) -> ok +%% Reason = term() +%% +%% Cleans up when the test_server is terminating. Kills the running +%% test suites (if any) and any possible remainting slave node + +terminate(_Reason, State) -> + test_server_sup:util_stop(), + case State#state.trc of + false -> ok; + Sock -> test_server_node:stop_tracer_node(Sock) + end, + ok = kill_all_jobs(State#state.jobs), + _ = test_server_node:kill_nodes(), + ok. + +kill_all_jobs([{_Name,JobPid}|Jobs]) -> + exit(JobPid, kill), + kill_all_jobs(Jobs); +kill_all_jobs([]) -> + ok. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%----------------------- INTERNAL FUNCTIONS -----------------------%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% spawn_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs, +%% CreatePrivDir, TestCaseCallback, ExtraTools) -> Pid +%% Mod = atom() +%% Func = atom() +%% Args = [term(),...] +%% Dir = string() +%% Name = string() +%% Levels = {integer(),integer(),integer()} +%% RejectIoReqs = bool() +%% CreatePrivDir = auto_per_run | manual_per_tc | auto_per_tc +%% TestCaseCallback = {CBMod,CBFunc} | undefined +%% ExtraTools = [ExtraTool,...] +%% ExtraTool = CoverInfo | TraceInfo | RandomSeed +%% +%% Spawns a test suite execute-process, just an ordinary spawn, except +%% that it will set a lot of dictionary information before starting the +%% named function. Also, the execution is timed and protected by a catch. +%% When the named function is done executing, a summary of the results +%% is printed to the log files. + +spawn_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs, + CreatePrivDir, TCCallback, ExtraTools) -> + spawn_link(fun() -> + init_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs, + CreatePrivDir, TCCallback, ExtraTools) + end). + +init_tester(Mod, Func, Args, Dir, Name, {_,_,MinLev}=Levels, + RejectIoReqs, CreatePrivDir, TCCallback, ExtraTools) -> + process_flag(trap_exit, true), + _ = test_server_io:start_link(), + put(app, common_test), + put(test_server_name, Name), + put(test_server_dir, Dir), + put(test_server_total_time, 0), + put(test_server_ok, 0), + put(test_server_failed, 0), + put(test_server_skipped, {0,0}), + put(test_server_minor_level, MinLev), + put(test_server_create_priv_dir, CreatePrivDir), + put(test_server_random_seed, proplists:get_value(random_seed, ExtraTools)), + put(test_server_testcase_callback, TCCallback), + case os:getenv("TEST_SERVER_FRAMEWORK") of + FW when FW =:= false; FW =:= "undefined" -> + put(test_server_framework, '$none'); + FW -> + put(test_server_framework_name, list_to_atom(FW)), + case os:getenv("TEST_SERVER_FRAMEWORK_NAME") of + FWName when FWName =:= false; FWName =:= "undefined" -> + put(test_server_framework_name, '$none'); + FWName -> + put(test_server_framework_name, list_to_atom(FWName)) + end + end, + + %% before first print, read and set logging options + FWLogDir = + case test_server_sup:framework_call(get_log_dir, [], []) of + {ok,FwDir} -> FwDir; + _ -> filename:dirname(Dir) + end, + put(test_server_framework_logdir, FWLogDir), + LogOpts = test_server_sup:framework_call(get_logopts, [], []), + put(test_server_logopts, LogOpts), + + StartedExtraTools = start_extra_tools(ExtraTools), + + test_server_io:set_job_name(Name), + test_server_io:set_gl_props([{levels,Levels}, + {auto_nl,not lists:member(no_nl, LogOpts)}, + {reject_io_reqs,RejectIoReqs}]), + group_leader(test_server_io:get_gl(true), self()), + {TimeMy,Result} = ts_tc(Mod, Func, Args), + set_io_buffering(undefined), + test_server_io:set_job_name(undefined), + catch stop_extra_tools(StartedExtraTools), + case Result of + {'EXIT',test_suites_done} -> + ok; + {'EXIT',_Pid,Reason} -> + print(1, "EXIT, reason ~tp", [Reason]); + {'EXIT',Reason} -> + report_severe_error(Reason), + print(1, "EXIT, reason ~tp", [Reason]) + end, + Time = TimeMy/1000000, + SuccessStr = + case get(test_server_failed) of + 0 -> "Ok"; + _ -> "FAILED" + end, + {SkippedN,SkipStr} = + case get(test_server_skipped) of + {0,0} -> + {0,""}; + {USkipped,ASkipped} -> + Skipped = USkipped+ASkipped, + {Skipped,io_lib:format(", ~w Skipped", [Skipped])} + end, + OkN = get(test_server_ok), + FailedN = get(test_server_failed), + print(html,"\n</tbody>\n<tfoot>\n" + "<tr><td></td><td><b>TOTAL</b></td><td></td><td></td><td></td>" + "<td>~.3fs</td><td><b>~ts</b></td><td>~w Ok, ~w Failed~ts of ~w</td></tr>\n" + "</tfoot>\n", + [Time,SuccessStr,OkN,FailedN,SkipStr,OkN+FailedN+SkippedN]), + + test_server_io:stop([major,html,unexpected_io]), + {UnexpectedIoName,UnexpectedIoFooter} = get(test_server_unexpected_footer), + {ok,UnexpectedIoFd} = open_html_file(UnexpectedIoName, [append]), + io:put_chars(UnexpectedIoFd, "\n</pre>\n"++UnexpectedIoFooter), + ok = file:close(UnexpectedIoFd). + +report_severe_error(Reason) -> + test_server_sup:framework_call(report, [severe_error,Reason]). + +ts_tc(M,F,A) -> + Before = erlang:monotonic_time(), + Result = (catch apply(M, F, A)), + After = erlang:monotonic_time(), + Elapsed = erlang:convert_time_unit(After-Before, + native, + micro_seconds), + {Elapsed, Result}. + +start_extra_tools(ExtraTools) -> + start_extra_tools(ExtraTools, []). +start_extra_tools([{cover,CoverInfo} | ExtraTools], Started) -> + case start_cover(CoverInfo) of + {ok,NewCoverInfo} -> + start_extra_tools(ExtraTools,[{cover,NewCoverInfo}|Started]); + {error,_} -> + start_extra_tools(ExtraTools, Started) + end; +start_extra_tools([_ | ExtraTools], Started) -> + start_extra_tools(ExtraTools, Started); +start_extra_tools([], Started) -> + Started. + +stop_extra_tools(ExtraTools) -> + TestDir = get(test_server_log_dir_base), + case lists:keymember(cover, 1, ExtraTools) of + false -> + write_default_coverlog(TestDir); + true -> + ok + end, + stop_extra_tools(ExtraTools, TestDir). + +stop_extra_tools([{cover,CoverInfo}|ExtraTools], TestDir) -> + stop_cover(CoverInfo,TestDir), + stop_extra_tools(ExtraTools, TestDir); +%%stop_extra_tools([_ | ExtraTools], TestDir) -> +%% stop_extra_tools(ExtraTools, TestDir); +stop_extra_tools([], _) -> + ok. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% do_spec(SpecName, TimetrapSpec) -> {error,Reason} | exit(Result) +%% SpecName = string() +%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap} +%% MultiplyTimetrap = integer() | infinity +%% ScaleTimetrap = bool() +%% +%% Reads the named test suite specification file, and executes it. +%% +%% This function is meant to be called by a process created by +%% spawn_tester/10, which sets up some necessary dictionary values. + +do_spec(SpecName, TimetrapSpec) when is_list(SpecName) -> + case file:consult(SpecName) of + {ok,TermList} -> + do_spec_list(TermList,TimetrapSpec); + {error,Reason} -> + io:format("Can't open ~ts: ~tp\n", [SpecName,Reason]), + {error,{cant_open_spec,Reason}} + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% do_spec_list(TermList, TimetrapSpec) -> exit(Result) +%% TermList = [term()|...] +%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap} +%% MultiplyTimetrap = integer() | infinity +%% ScaleTimetrap = bool() +%% +%% Executes a list of test suite specification commands. The following +%% commands are available, and may occur zero or more times (if several, +%% the contents is appended): +%% +%% {topcase,TopCase} Specifies top level test goals. TopCase has the syntax +%% specified by collect_cases/3. +%% +%% {skip,Skip} Specifies test cases to skip, and lists requirements that +%% cannot be granted during the test run. Skip has the syntax specified +%% by collect_cases/3. +%% +%% {nodes,Nodes} Lists node names avaliable to the test suites. Nodes have +%% the syntax specified by collect_cases/3. +%% +%% {require_nodenames, Num} Specifies how many nodenames the test suite will +%% need. Theese are automaticly generated and inserted into the Config by the +%% test_server. The caller may specify other hosts to run theese nodes by +%% using the {hosts, Hosts} option. If there are no hosts specified, all +%% nodenames will be generated from the local host. +%% +%% {hosts, Hosts} Specifies a list of available hosts on which to start +%% slave nodes. It is used when the {remote, true} option is given to the +%% test_server:start_node/3 function. Also, if {require_nodenames, Num} is +%% contained in the TermList, the generated nodenames will be spread over +%% all hosts given in this Hosts list. The hostnames are given as atoms or +%% strings. +%% +%% {diskless, true}</c></tag> is kept for backwards compatiblilty and +%% should not be used. Use a configuration test case instead. +%% +%% This function is meant to be called by a process created by +%% spawn_tester/10, which sets up some necessary dictionary values. + +do_spec_list(TermList0, TimetrapSpec) -> + Nodes = [], + TermList = + case lists:keysearch(hosts, 1, TermList0) of + {value, {hosts, Hosts0}} -> + Hosts = lists:map(fun(H) -> cast_to_list(H) end, Hosts0), + controller_call({set_hosts, Hosts}), + lists:keydelete(hosts, 1, TermList0); + _ -> + TermList0 + end, + DefaultConfig = make_config([{nodes,Nodes}]), + {TopCases,SkipList,Config} = do_spec_terms(TermList, [], [], DefaultConfig), + do_test_cases(TopCases, SkipList, Config, TimetrapSpec). + +do_spec_terms([], TopCases, SkipList, Config) -> + {TopCases,SkipList,Config}; +do_spec_terms([{topcase,TopCase}|Terms], TopCases, SkipList, Config) -> + do_spec_terms(Terms,[TopCase|TopCases], SkipList, Config); +do_spec_terms([{skip,Skip}|Terms], TopCases, SkipList, Config) -> + do_spec_terms(Terms, TopCases, [Skip|SkipList], Config); +do_spec_terms([{nodes,Nodes}|Terms], TopCases, SkipList, Config) -> + do_spec_terms(Terms, TopCases, SkipList, + update_config(Config, {nodes,Nodes})); +do_spec_terms([{diskless,How}|Terms], TopCases, SkipList, Config) -> + do_spec_terms(Terms, TopCases, SkipList, + update_config(Config, {diskless,How})); +do_spec_terms([{config,MoreConfig}|Terms], TopCases, SkipList, Config) -> + do_spec_terms(Terms, TopCases, SkipList, Config++MoreConfig); +do_spec_terms([{default_timeout,Tmo}|Terms], TopCases, SkipList, Config) -> + do_spec_terms(Terms, TopCases, SkipList, + update_config(Config, {default_timeout,Tmo})); + +do_spec_terms([{require_nodenames,NumNames}|Terms], TopCases, SkipList, Config) -> + NodeNames0=generate_nodenames(NumNames), + NodeNames=lists:delete([], NodeNames0), + do_spec_terms(Terms, TopCases, SkipList, + update_config(Config, {nodenames,NodeNames})); +do_spec_terms([Other|Terms], TopCases, SkipList, Config) -> + io:format("** WARNING: Spec file contains unknown directive ~tp\n", + [Other]), + do_spec_terms(Terms, TopCases, SkipList, Config). + + + +generate_nodenames(Num) -> + Hosts = case controller_call(get_hosts) of + [] -> + TI = controller_call(get_target_info), + [TI#target_info.host]; + List -> + List + end, + generate_nodenames2(Num, Hosts, []). + +generate_nodenames2(0, _Hosts, Acc) -> + Acc; +generate_nodenames2(N, Hosts, Acc) -> + Host=lists:nth((N rem (length(Hosts)))+1, Hosts), + Name=list_to_atom(temp_nodename("nod", []) ++ "@" ++ Host), + generate_nodenames2(N-1, Hosts, [Name|Acc]). + +temp_nodename([], Acc) -> + lists:flatten(Acc); +temp_nodename([Chr|Base], Acc) -> + {A,B,C} = ?now, + New = [Chr | integer_to_list(Chr bxor A bxor B+A bxor C+B)], + temp_nodename(Base, [New|Acc]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% count_test_cases(TopCases, SkipCases) -> {Suites,NoOfCases} | error +%% TopCases = term() (See collect_cases/3) +%% SkipCases = term() (See collect_cases/3) +%% Suites = list() +%% NoOfCases = integer() | unknown +%% +%% Counts the test cases that are about to run and returns that number. +%% If there's a conf group in TestSpec with a repeat property, the total number +%% of cases can not be calculated and NoOfCases = unknown. +count_test_cases(TopCases, SkipCases) when is_list(TopCases) -> + case collect_all_cases(TopCases, SkipCases) of + {error,_Why} = Error -> + Error; + TestSpec -> + {get_suites(TestSpec, []), + case remove_conf(TestSpec) of + {repeats,_} -> + unknown; + TestSpec1 -> + length(TestSpec1) + end} + end; + +count_test_cases(TopCase, SkipCases) -> + count_test_cases([TopCase], SkipCases). + + +remove_conf(Cases) -> + remove_conf(Cases, [], false). + +remove_conf([{conf, _Ref, Props, _MF}|Cases], NoConf, Repeats) -> + case get_repeat(Props) of + undefined -> + remove_conf(Cases, NoConf, Repeats); + {_RepType,1} -> + remove_conf(Cases, NoConf, Repeats); + _ -> + remove_conf(Cases, NoConf, true) + end; +remove_conf([{make,_Ref,_MF}|Cases], NoConf, Repeats) -> + remove_conf(Cases, NoConf, Repeats); +remove_conf([{skip_case,{{_M,all},_Cmt},_Mode}|Cases], NoConf, Repeats) -> + remove_conf(Cases, NoConf, Repeats); +remove_conf([{skip_case,{Type,_Ref,_MF,_Cmt}}|Cases], + NoConf, Repeats) when Type==conf; + Type==make -> + remove_conf(Cases, NoConf, Repeats); +remove_conf([{skip_case,{Type,_Ref,_MF,_Cmt},_Mode}|Cases], + NoConf, Repeats) when Type==conf; + Type==make -> + remove_conf(Cases, NoConf, Repeats); +remove_conf([C={Mod,error_in_suite,_}|Cases], NoConf, Repeats) -> + FwMod = get_fw_mod(?MODULE), + if Mod == FwMod -> + remove_conf(Cases, NoConf, Repeats); + true -> + remove_conf(Cases, [C|NoConf], Repeats) + end; +remove_conf([C|Cases], NoConf, Repeats) -> + remove_conf(Cases, [C|NoConf], Repeats); +remove_conf([], NoConf, true) -> + {repeats,lists:reverse(NoConf)}; +remove_conf([], NoConf, false) -> + lists:reverse(NoConf). + +get_suites([{skip_case,{{Mod,_F},_Cmt},_Mode}|Tests], Mods) when is_atom(Mod) -> + case add_mod(Mod, Mods) of + true -> get_suites(Tests, [Mod|Mods]); + false -> get_suites(Tests, Mods) + end; +get_suites([{Mod,_Case}|Tests], Mods) when is_atom(Mod) -> + case add_mod(Mod, Mods) of + true -> get_suites(Tests, [Mod|Mods]); + false -> get_suites(Tests, Mods) + end; +get_suites([{Mod,_Func,_Args}|Tests], Mods) when is_atom(Mod) -> + case add_mod(Mod, Mods) of + true -> get_suites(Tests, [Mod|Mods]); + false -> get_suites(Tests, Mods) + end; +get_suites([_|Tests], Mods) -> + get_suites(Tests, Mods); + +get_suites([], Mods) -> + lists:reverse(Mods). + +add_mod(Mod, Mods) -> + case string:rstr(atom_to_list(Mod), "_SUITE") of + 0 -> false; + _ -> % test suite + case lists:member(Mod, Mods) of + true -> false; + false -> true + end + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% do_test_cases(TopCases, SkipCases, Config, TimetrapSpec) -> +%% exit(Result) +%% +%% TopCases = term() (See collect_cases/3) +%% SkipCases = term() (See collect_cases/3) +%% Config = term() (See collect_cases/3) +%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap} +%% MultiplyTimetrap = integer() | infinity +%% ScaleTimetrap = bool() +%% +%% Initializes and starts the test run, for "ordinary" test suites. +%% Creates log directories and log files, inserts initial timestamps and +%% configuration information into the log files. +%% +%% This function is meant to be called by a process created by +%% spawn_tester/10, which sets up some necessary dictionary values. +do_test_cases(TopCases, SkipCases, + Config, MultiplyTimetrap) when is_integer(MultiplyTimetrap); + MultiplyTimetrap == infinity -> + do_test_cases(TopCases, SkipCases, Config, {MultiplyTimetrap,true}); + +do_test_cases(TopCases, SkipCases, + Config, TimetrapData) when is_list(TopCases), + is_tuple(TimetrapData) -> + {ok,TestDir} = start_log_file(), + FwMod = get_fw_mod(?MODULE), + case collect_all_cases(TopCases, SkipCases) of + {error,Why} -> + print(1, "Error starting: ~tp", [Why]), + exit(test_suites_done); + TestSpec0 -> + N = case remove_conf(TestSpec0) of + {repeats,_} -> unknown; + TS -> length(TS) + end, + put(test_server_cases, N), + put(test_server_case_num, 0), + + TestSpec = + add_init_and_end_per_suite(TestSpec0, undefined, undefined, FwMod), + + TI = get_target_info(), + print(1, "Starting test~ts", + [print_if_known(N, {", ~w test cases",[N]}, + {" (with repeated test cases)",[]})]), + Test = get(test_server_name), + TestName = if is_list(Test) -> + lists:flatten(io_lib:format("~ts", [Test])); + true -> + lists:flatten(io_lib:format("~tp", [Test])) + end, + TestDescr = "Test " ++ TestName ++ " results", + + test_server_sup:framework_call(report, [tests_start,{Test,N}]), + + {Header,Footer} = + case test_server_sup:framework_call(get_html_wrapper, + [TestDescr,true,TestDir, + {[],[2,3,4,7,8],[1,6]}], "") of + Empty when (Empty == "") ; (element(2,Empty) == "") -> + put(basic_html, true), + {[html_header(TestDescr), + "<h2>Results for test ", TestName, "</h2>\n"], + "\n</body>\n</html>\n"}; + {basic_html,Html0,Html1} -> + put(basic_html, true), + {Html0++["<h1>Results for <i>",TestName,"</i></h1>\n"], + Html1}; + {xhtml,Html0,Html1} -> + put(basic_html, false), + {Html0++["<h1>Results for <i>",TestName,"</i></h1>\n"], + Html1} + end, + + print(html, Header), + + print(html, xhtml("<p>", "<h4>")), + print_timestamp(html, "Test started at "), + print(html, xhtml("</p>", "</h4>")), + + print(html, xhtml("\n<p><b>Host info:</b><br>\n", + "\n<p><b>Host info:</b><br />\n")), + print_who(test_server_sup:hoststr(), test_server_sup:get_username()), + print(html, xhtml("<br>Used Erlang v~ts in <tt>~ts</tt></p>\n", + "<br />Used Erlang v~ts in \"~ts\"</p>\n"), + [erlang:system_info(version), code:root_dir()]), + + if FwMod == ?MODULE -> + print(html, xhtml("\n<p><b>Target Info:</b><br>\n", + "\n<p><b>Target Info:</b><br />\n")), + print_who(TI#target_info.host, TI#target_info.username), + print(html,xhtml("<br>Used Erlang v~ts in <tt>~ts</tt></p>\n", + "<br />Used Erlang v~ts in \"~ts\"</p>\n"), + [TI#target_info.version, TI#target_info.root_dir]); + true -> + case test_server_sup:framework_call(target_info, []) of + TargetInfo when is_list(TargetInfo), + length(TargetInfo) > 0 -> + print(html, xhtml("\n<p><b>Target info:</b><br>\n", + "\n<p><b>Target info:</b><br />\n")), + print(html, "~ts</p>\n", [TargetInfo]); + _ -> + ok + end + end, + CoverLog = + case get(test_server_cover_log_dir) of + undefined -> + ?coverlog_name; + AbsLogDir -> + AbsLog = filename:join(AbsLogDir,?coverlog_name), + make_relative(AbsLog, TestDir) + end, + print(html, + "<p><ul>\n" + "<li><a href=\"~ts\">Full textual log</a></li>\n" + "<li><a href=\"~ts\">Coverage log</a></li>\n" + "<li><a href=\"~ts\">Unexpected I/O log</a></li>\n</ul></p>\n", + [?suitelog_name,CoverLog,?unexpected_io_log]), + print(html, + "<p>~ts</p>\n" ++ + xhtml(["<table bgcolor=\"white\" border=\"3\" cellpadding=\"5\">\n", + "<thead>\n"], + ["<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n"]) ++ + "<tr><th>Num</th><th>Module</th><th>Group</th>" ++ + "<th>Case</th><th>Log</th><th>Time</th><th>Result</th>" ++ + "<th>Comment</th></tr>\n</thead>\n<tbody>\n", + [print_if_known(N, {"<i>Executing <b>~w</b> test cases...</i>" + ++ xhtml("\n<br>\n", "\n<br />\n"),[N]}, + {"",[]})]), + + print(major, "=cases ~w", [get(test_server_cases)]), + print(major, "=user ~ts", [TI#target_info.username]), + print(major, "=host ~ts", [TI#target_info.host]), + + %% If there are no hosts specified,use only the local host + case controller_call(get_hosts) of + [] -> + print(major, "=hosts ~ts", [TI#target_info.host]), + controller_call({set_hosts, [TI#target_info.host]}); + Hosts -> + Str = lists:flatten(lists:map(fun(X) -> [X," "] end, Hosts)), + print(major, "=hosts ~ts", [Str]) + end, + print(major, "=emulator_vsn ~ts", [TI#target_info.version]), + print(major, "=emulator ~ts", [TI#target_info.emulator]), + print(major, "=otp_release ~ts", [TI#target_info.otp_release]), + print(major, "=started ~s", + [lists:flatten(timestamp_get(""))]), + + test_server_io:set_footer(Footer), + + run_test_cases(TestSpec, Config, TimetrapData) + end; + +do_test_cases(TopCase, SkipCases, Config, TimetrapSpec) -> + %% when not list(TopCase) + do_test_cases([TopCase], SkipCases, Config, TimetrapSpec). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% start_log_file() -> {ok,TestDirName} | exit({Error,Reason}) +%% Stem = string() +%% +%% Creates the log directories, the major log file and the html log file. +%% The log files are initialized with some header information. +%% +%% The name of the log directory will be <Name>.logs/run.<Date>/ where +%% Name is the test suite name and Date is the current date and time. + +start_log_file() -> + Dir = get(test_server_dir), + case file:make_dir(Dir) of + ok -> + ok; + {error, eexist} -> + ok; + MkDirError -> + log_file_error(MkDirError, Dir) + end, + TestDir = timestamp_filename_get(filename:join(Dir, "run.")), + TestDir1 = + case file:make_dir(TestDir) of + ok -> + TestDir; + {error,eexist} -> + timer:sleep(1000), + %% we need min 1 second between timestamps unfortunately + TestDirX = timestamp_filename_get(filename:join(Dir, "run.")), + case file:make_dir(TestDirX) of + ok -> + TestDirX; + MkDirError2 -> + log_file_error(MkDirError2, TestDirX) + end; + MkDirError2 -> + log_file_error(MkDirError2, TestDir) + end, + FilenameMode = file:native_name_encoding(), + ok = write_file(filename:join(Dir, ?last_file), + TestDir1 ++ "\n", + FilenameMode), + ok = write_file(?last_file, TestDir1 ++ "\n", FilenameMode), + put(test_server_log_dir_base,TestDir1), + + MajorName = filename:join(TestDir1, ?suitelog_name), + HtmlName = MajorName ++ ?html_ext, + UnexpectedName = filename:join(TestDir1, ?unexpected_io_log), + + {ok,Major} = open_utf8_file(MajorName), + {ok,Html} = open_html_file(HtmlName), + + {UnexpHeader,UnexpFooter} = + case test_server_sup:framework_call(get_html_wrapper, + ["Unexpected I/O log",false, + TestDir, undefined],"") of + UEmpty when (UEmpty == "") ; (element(2,UEmpty) == "") -> + {html_header("Unexpected I/O log"),"\n</body>\n</html>\n"}; + {basic_html,UH,UF} -> + {UH,UF}; + {xhtml,UH,UF} -> + {UH,UF} + end, + + {ok,Unexpected} = open_html_file(UnexpectedName), + io:put_chars(Unexpected, [UnexpHeader, + xhtml("<br>\n<h2>Unexpected I/O</h2>", + "<br />\n<h3>Unexpected I/O</h3>"), + "\n<pre>\n"]), + put(test_server_unexpected_footer,{UnexpectedName,UnexpFooter}), + + test_server_io:set_fd(major, Major), + test_server_io:set_fd(html, Html), + test_server_io:set_fd(unexpected_io, Unexpected), + + %% we must assume the redirection file (to the latest suite index) can + %% be stored on the level above the log directory of the current test + TopDir = filename:dirname(get(test_server_framework_logdir)), + RedirectLink = filename:join(TopDir, ?suitelog_latest_name ++ ?html_ext), + make_html_link(RedirectLink, HtmlName, redirect), + + make_html_link(filename:absname(?last_test ++ ?html_ext), + HtmlName, filename:basename(Dir)), + LinkName = filename:join(Dir, ?last_link), + make_html_link(LinkName ++ ?html_ext, HtmlName, + filename:basename(Dir)), + + PrivDir = filename:join(TestDir1, ?priv_dir), + ok = file:make_dir(PrivDir), + put(test_server_priv_dir,PrivDir++"/"), + print_timestamp(major, "Suite started at "), + + LogInfo = [{topdir,Dir},{rundir,lists:flatten(TestDir1)}], + test_server_sup:framework_call(report, [loginfo,LogInfo]), + {ok,TestDir1}. + +log_file_error(Error, Dir) -> + exit({cannot_create_log_dir,{Error,lists:flatten(Dir)}}). + +make_html_link(LinkName, Target, Explanation) -> + %% if possible use a relative reference to Target. + TargetL = filename:split(Target), + PwdL = filename:split(filename:dirname(LinkName)), + Href = case lists:prefix(PwdL, TargetL) of + true -> + uri_encode(filename:join(lists:nthtail(length(PwdL),TargetL))); + false -> + "file:" ++ uri_encode(Target) + end, + H = if Explanation == redirect -> + Meta = ["<meta http-equiv=\"refresh\" " + "content=\"0; url=", Href, "\" />\n"], + [html_header("redirect", Meta), "</html>\n"]; + true -> + [html_header(Explanation), + "<h1>Last test</h1>\n" + "<a href=\"",Href,"\">",Explanation,"</a>\n" + "</body>\n</html>\n"] + end, + ok = write_html_file(LinkName, H). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% start_minor_log_file(Mod, Func, ParallelTC) -> AbsName +%% Mod = atom() +%% Func = atom() +%% ParallelTC = bool() +%% AbsName = string() +%% +%% Create a minor log file for the test case Mod,Func,Args. The log file +%% will be stored in the log directory under the name <Mod>.<Func>.html. +%% Some header info will also be inserted into the log file. If the test +%% case runs in a parallel group, then to avoid clashing file names if the +%% case is executed more than once, the name <Mod>.<Func>.<Timestamp>.html +%% is used. + +start_minor_log_file(Mod, Func, ParallelTC) -> + MFA = {Mod,Func,1}, + LogDir = get(test_server_log_dir_base), + Name = minor_log_file_name(Mod,Func), + AbsName = filename:join(LogDir, Name), + case (ParallelTC orelse (element(1,file:read_file_info(AbsName))==ok)) of + false -> %% normal case, unique name + start_minor_log_file1(Mod, Func, LogDir, AbsName, MFA); + true -> %% special case, duplicate names + Tag = test_server_sup:unique_name(), + Name1 = minor_log_file_name(Mod,Func,[$.|Tag]), + AbsName1 = filename:join(LogDir, Name1), + start_minor_log_file1(Mod, Func, LogDir, AbsName1, MFA) + end. + +start_minor_log_file1(Mod, Func, LogDir, AbsName, MFA) -> + {ok,Fd} = open_html_file(AbsName), + Lev = get(test_server_minor_level)+1000, %% far down in the minor levels + put(test_server_minor_fd, Fd), + test_server_gl:set_minor_fd(group_leader(), Fd, MFA), + + TestDescr = io_lib:format("Test ~w:~tw result", [Mod,Func]), + {Header,Footer} = + case test_server_sup:framework_call(get_html_wrapper, + [TestDescr,false, + filename:dirname(AbsName), + undefined], "") of + Empty when (Empty == "") ; (element(2,Empty) == "") -> + put(basic_html, true), + {html_header(TestDescr), "\n</body>\n</html>\n"}; + {basic_html,Html0,Html1} -> + put(basic_html, true), + {Html0,Html1}; + {xhtml,Html0,Html1} -> + put(basic_html, false), + {Html0,Html1} + end, + put(test_server_minor_footer, Footer), + io:put_chars(Fd, Header), + + io:put_chars(Fd, "<a name=\"top\"></a>"), + io:put_chars(Fd, "<pre>\n"), + + SrcListing = downcase(atom_to_list(Mod)) ++ ?src_listing_ext, + + case get_fw_mod(?MODULE) of + Mod when Func == error_in_suite -> + ok; + _ -> + {Info,Arity} = + if Func == init_per_suite; Func == end_per_suite -> + {"Config function: ", 1}; + Func == init_per_group; Func == end_per_group -> + {"Config function: ", 2}; + true -> + {"Test case: ", 1} + end, + + case {filelib:is_file(filename:join(LogDir, SrcListing)), + lists:member(no_src, get(test_server_logopts))} of + {true,false} -> + print(Lev, ["$tc_html", + Info ++ "<a href=\"~ts#~ts\">~w:~tw/~w</a> " + "(click for source code)\n"], + [uri_encode(SrcListing), + uri_encode(atom_to_list(Func)++"-1",utf8), + Mod,Func,Arity]); + _ -> + print(Lev, ["$tc_html",Info ++ "~w:~tw/~w\n"], [Mod,Func,Arity]) + end + end, + + AbsName. + +stop_minor_log_file() -> + test_server_gl:unset_minor_fd(group_leader()), + Fd = get(test_server_minor_fd), + Footer = get(test_server_minor_footer), + io:put_chars(Fd, "</pre>\n" ++ Footer), + ok = file:close(Fd), + put(test_server_minor_fd, undefined). + +minor_log_file_name(Mod,Func) -> + minor_log_file_name(Mod,Func,""). +minor_log_file_name(Mod,Func,Tag) -> + Name = + downcase( + lists:flatten( + io_lib:format("~w.~tw~s~s", [Mod,Func,Tag,?html_ext]))), + Ok = file:native_name_encoding()==utf8 + orelse io_lib:printable_latin1_list(Name), + if Ok -> Name; + true -> exit({error,unicode_name_on_latin1_file_system}) + end. + +downcase(S) -> downcase(S, []). +downcase([Uc|Rest], Result) when $A =< Uc, Uc =< $Z -> + downcase(Rest, [Uc-$A+$a|Result]); +downcase([C|Rest], Result) -> + downcase(Rest, [C|Result]); +downcase([], Result) -> + lists:reverse(Result). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% html_convert_modules(TestSpec, Config) -> ok +%% Isolate the modules affected by TestSpec and +%% make sure they are converted to html. +%% +%% Errors are silently ignored. + +html_convert_modules(TestSpec, _Config, FwMod) -> + Mods = html_isolate_modules(TestSpec, FwMod), + html_convert_modules(Mods), + copy_html_files(get(test_server_dir), get(test_server_log_dir_base)). + +%% Retrieve a list of modules out of the test spec. +html_isolate_modules(List, FwMod) -> + html_isolate_modules(List, sets:new(), FwMod). + +html_isolate_modules([], Set, _) -> sets:to_list(Set); +html_isolate_modules([{skip_case,{_Case,_Cmt},_Mode}|Cases], Set, FwMod) -> + html_isolate_modules(Cases, Set, FwMod); +html_isolate_modules([{conf,_Ref,Props,{FwMod,_Func}}|Cases], Set, FwMod) -> + Set1 = case proplists:get_value(suite, Props) of + undefined -> Set; + Mod -> sets:add_element(Mod, Set) + end, + html_isolate_modules(Cases, Set1, FwMod); +html_isolate_modules([{conf,_Ref,_Props,{Mod,_Func}}|Cases], Set, FwMod) -> + html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod); +html_isolate_modules([{skip_case,{conf,_Ref,{FwMod,_Func},_Cmt},Mode}|Cases], + Set, FwMod) -> + Set1 = case proplists:get_value(suite, get_props(Mode)) of + undefined -> Set; + Mod -> sets:add_element(Mod, Set) + end, + html_isolate_modules(Cases, Set1, FwMod); +html_isolate_modules([{skip_case,{conf,_Ref,{Mod,_Func},_Cmt},_Props}|Cases], + Set, FwMod) -> + html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod); +html_isolate_modules([{Mod,_Case}|Cases], Set, FwMod) -> + html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod); +html_isolate_modules([{Mod,_Case,_Args}|Cases], Set, FwMod) -> + html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod). + +%% Given a list of modules, convert each module's source code to HTML. +html_convert_modules([Mod|Mods]) -> + case code:which(Mod) of + Path when is_list(Path) -> + SrcFile = filename:rootname(Path) ++ ".erl", + FoundSrcFile = + case file:read_file_info(SrcFile) of + {ok,SInfo} -> + {SrcFile,SInfo}; + {error,_} -> + ModInfo = Mod:module_info(compile), + case proplists:get_value(source, ModInfo) of + undefined -> + undefined; + OtherSrcFile -> + case file:read_file_info(OtherSrcFile) of + {ok,SInfo} -> + {OtherSrcFile,SInfo}; + {error,_} -> + undefined + end + end + end, + case FoundSrcFile of + undefined -> + html_convert_modules(Mods); + {SrcFile1,SrcFileInfo} -> + DestDir = get(test_server_dir), + Name = atom_to_list(Mod), + DestFile = filename:join(DestDir, + downcase(Name)++?src_listing_ext), + _ = html_possibly_convert(SrcFile1, SrcFileInfo, DestFile), + html_convert_modules(Mods) + end; + _Other -> + html_convert_modules(Mods) + end; +html_convert_modules([]) -> ok. + +%% Convert source code to HTML if possible and needed. +html_possibly_convert(Src, SrcInfo, Dest) -> + case file:read_file_info(Dest) of + {ok,DestInfo} when DestInfo#file_info.mtime >= SrcInfo#file_info.mtime -> + ok; % dest file up to date + _ -> + InclPath = case application:get_env(test_server, include) of + {ok,Incls} -> Incls; + _ -> [] + end, + + OutDir = get(test_server_log_dir_base), + case test_server_sup:framework_call(get_html_wrapper, + ["Module "++Src,false, + OutDir,undefined, + encoding(Src)], "") of + Empty when (Empty == "") ; (element(2,Empty) == "") -> + erl2html2:convert(Src, Dest, InclPath); + {_,Header,_} -> + erl2html2:convert(Src, Dest, InclPath, Header) + end + end. + +%% Copy all HTML files in InDir to OutDir. +copy_html_files(InDir, OutDir) -> + Files = filelib:wildcard(filename:join(InDir, "*" ++ ?src_listing_ext)), + lists:foreach(fun (Src) -> copy_html_file(Src, OutDir) end, Files). + +copy_html_file(Src, DestDir) -> + Dest = filename:join(DestDir, filename:basename(Src)), + case file:read_file(Src) of + {ok,Bin} -> + ok = write_binary_file(Dest, Bin); + {error,_Reason} -> + io:format("File ~ts: read failed\n", [Src]) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% add_init_and_end_per_suite(TestSpec, Mod, Ref, FwMod) -> NewTestSpec +%% +%% Expands TestSpec with an initial init_per_suite, and a final +%% end_per_suite element, per each discovered suite in the list. + +add_init_and_end_per_suite([{make,_,_}=Case|Cases], LastMod, LastRef, FwMod) -> + [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; +add_init_and_end_per_suite([{skip_case,{{Mod,all},_},_}=Case|Cases], LastMod, + LastRef, FwMod) when Mod =/= LastMod -> + {PreCases, NextMod, NextRef} = + do_add_end_per_suite_and_skip(LastMod, LastRef, Mod, FwMod), + PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; +add_init_and_end_per_suite([{skip_case,{{Mod,_},_Cmt},_Mode}=Case|Cases], + LastMod, LastRef, FwMod) when Mod =/= LastMod -> + {PreCases, NextMod, NextRef} = + do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), + PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; +add_init_and_end_per_suite([{skip_case,{conf,_,{Mod,_},_},_}=Case|Cases], + LastMod, LastRef, FwMod) when Mod =/= LastMod -> + {PreCases, NextMod, NextRef} = + do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), + PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; +add_init_and_end_per_suite([{skip_case,{conf,_,{Mod,_},_}}=Case|Cases], LastMod, + LastRef, FwMod) when Mod =/= LastMod -> + {PreCases, NextMod, NextRef} = + do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), + PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; +add_init_and_end_per_suite([{conf,Ref,Props,{FwMod,Func}}=Case|Cases], LastMod, + LastRef, FwMod) -> + %% if Mod == FwMod, this conf test is (probably) a test case group where + %% the init- and end-functions are missing in the suite, and if so, + %% the suite name should be stored as {suite,Suite} in Props + case proplists:get_value(suite, Props) of + Suite when Suite =/= undefined, Suite =/= LastMod -> + {PreCases, NextMod, NextRef} = + do_add_init_and_end_per_suite(LastMod, LastRef, Suite, FwMod), + Case1 = {conf,Ref,[{suite,NextMod}|proplists:delete(suite,Props)], + {FwMod,Func}}, + PreCases ++ [Case1|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; + _ -> + [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)] + end; +add_init_and_end_per_suite([{conf,_,_,{Mod,_}}=Case|Cases], LastMod, + LastRef, FwMod) when Mod =/= LastMod, Mod =/= FwMod -> + {PreCases, NextMod, NextRef} = + do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), + PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; +add_init_and_end_per_suite([SkipCase|Cases], LastMod, LastRef, FwMod) + when element(1,SkipCase) == skip_case; element(1,SkipCase) == auto_skip_case-> + [SkipCase|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; +add_init_and_end_per_suite([{conf,_,_,_}=Case|Cases], LastMod, LastRef, FwMod) -> + [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; +add_init_and_end_per_suite([{Mod,_}=Case|Cases], LastMod, LastRef, FwMod) + when Mod =/= LastMod, Mod =/= FwMod -> + {PreCases, NextMod, NextRef} = + do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), + PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; +add_init_and_end_per_suite([{Mod,_,_}=Case|Cases], LastMod, LastRef, FwMod) + when Mod =/= LastMod, Mod =/= FwMod -> + {PreCases, NextMod, NextRef} = + do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), + PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, + NextRef, FwMod)]; +add_init_and_end_per_suite([Case|Cases], LastMod, LastRef, FwMod)-> + [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; +add_init_and_end_per_suite([], _LastMod, undefined, _FwMod) -> + []; +add_init_and_end_per_suite([], _LastMod, skipped_suite, _FwMod) -> + []; +add_init_and_end_per_suite([], LastMod, LastRef, FwMod) -> + %% we'll add end_per_suite here even if it's not exported + %% (and simply let the call fail if it's missing) + case {erlang:function_exported(LastMod, end_per_suite, 1), + erlang:function_exported(LastMod, init_per_suite, 1)} of + {false,false} -> + %% let's call a "fake" end_per_suite if it exists + case erlang:function_exported(FwMod, end_per_suite, 1) of + true -> + [{conf,LastRef,[{suite,LastMod}],{FwMod,end_per_suite}}]; + false -> + [{conf,LastRef,[],{LastMod,end_per_suite}}] + end; + _ -> + %% If any of these exist, the other should too + %% (required and documented). If it isn't, it will fail + %% with reason 'undef'. + [{conf,LastRef,[],{LastMod,end_per_suite}}] + end. + +do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod) -> + _ = case code:is_loaded(Mod) of + false -> code:load_file(Mod); + _ -> ok + end, + {Init,NextMod,NextRef} = + case {erlang:function_exported(Mod, init_per_suite, 1), + erlang:function_exported(Mod, end_per_suite, 1)} of + {false,false} -> + %% let's call a "fake" init_per_suite if it exists + case erlang:function_exported(FwMod, init_per_suite, 1) of + true -> + Ref = make_ref(), + {[{conf,Ref,[{suite,Mod}], + {FwMod,init_per_suite}}],Mod,Ref}; + false -> + {[],Mod,undefined} + end; + _ -> + %% If any of these exist, the other should too + %% (required and documented). If it isn't, it will fail + %% with reason 'undef'. + Ref = make_ref(), + {[{conf,Ref,[],{Mod,init_per_suite}}],Mod,Ref} + end, + Cases = + if LastRef==undefined -> + Init; + LastRef==skipped_suite -> + Init; + true -> + %% we'll add end_per_suite here even if it's not exported + %% (and simply let the call fail if it's missing) + case {erlang:function_exported(LastMod, end_per_suite, 1), + erlang:function_exported(LastMod, init_per_suite, 1)} of + {false,false} -> + %% let's call a "fake" end_per_suite if it exists + case erlang:function_exported(FwMod, end_per_suite, 1) of + true -> + [{conf,LastRef,[{suite,Mod}], + {FwMod,end_per_suite}}|Init]; + false -> + [{conf,LastRef,[],{LastMod,end_per_suite}}|Init] + end; + _ -> + %% If any of these exist, the other should too + %% (required and documented). If it isn't, it will fail + %% with reason 'undef'. + [{conf,LastRef,[],{LastMod,end_per_suite}}|Init] + end + end, + {Cases,NextMod,NextRef}. + +do_add_end_per_suite_and_skip(LastMod, LastRef, Mod, FwMod) -> + case LastRef of + No when No==undefined ; No==skipped_suite -> + {[],Mod,skipped_suite}; + _Ref -> + case {erlang:function_exported(LastMod, end_per_suite, 1), + erlang:function_exported(LastMod, init_per_suite, 1)} of + {false,false} -> + case erlang:function_exported(FwMod, end_per_suite, 1) of + true -> + %% let's call "fake" end_per_suite if it exists + {[{conf,LastRef,[],{FwMod,end_per_suite}}], + Mod,skipped_suite}; + false -> + {[{conf,LastRef,[],{LastMod,end_per_suite}}], + Mod,skipped_suite} + end; + _ -> + %% If any of these exist, the other should too + %% (required and documented). If it isn't, it will fail + %% with reason 'undef'. + {[{conf,LastRef,[],{LastMod,end_per_suite}}], + Mod,skipped_suite} + end + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% run_test_cases(TestSpec, Config, TimetrapData) -> exit(Result) +%% +%% Runs the specified tests, then displays/logs the summary. + +run_test_cases(TestSpec, Config, TimetrapData) -> + test_server:init_valgrind(), + case lists:member(no_src, get(test_server_logopts)) of + true -> + ok; + false -> + FwMod = get_fw_mod(?MODULE), + html_convert_modules(TestSpec, Config, FwMod) + end, + + run_test_cases_loop(TestSpec, [Config], TimetrapData, [], []), + + {AllSkippedN,UserSkipN,AutoSkipN,SkipStr} = + case get(test_server_skipped) of + {0,0} -> {0,0,0,""}; + {US,AS} -> {US+AS,US,AS,io_lib:format(", ~w skipped", [US+AS])} + end, + OkN = get(test_server_ok), + FailedN = get(test_server_failed), + print(1, "TEST COMPLETE, ~w ok, ~w failed~ts of ~w test cases\n", + [OkN,FailedN,SkipStr,OkN+FailedN+AllSkippedN]), + test_server_sup:framework_call(report, [tests_done, + {OkN,FailedN,{UserSkipN,AutoSkipN}}]), + print(major, "=finished ~s", [lists:flatten(timestamp_get(""))]), + print(major, "=failed ~w", [FailedN]), + print(major, "=successful ~w", [OkN]), + print(major, "=user_skipped ~w", [UserSkipN]), + print(major, "=auto_skipped ~w", [AutoSkipN]), + exit(test_suites_done). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% run_test_cases_loop(TestCases, Config, TimetrapData, Mode, Status) -> ok +%% TestCases = [Test,...] +%% Config = [[{Key,Val},...],...] +%% TimetrapData = {MultiplyTimetrap,ScaleTimetrap} +%% MultiplyTimetrap = integer() | infinity +%% ScaleTimetrap = bool() +%% Mode = [{Ref,[Prop,..],StartTime}] +%% Ref = reference() +%% Prop = {name,Name} | sequence | parallel | +%% shuffle | {shuffle,Seed} | +%% repeat | {repeat,N} | +%% repeat_until_all_ok | {repeat_until_all_ok,N} | +%% repeat_until_any_ok | {repeat_until_any_ok,N} | +%% repeat_until_any_fail | {repeat_until_any_fail,N} | +%% repeat_until_all_fail | {repeat_until_all_fail,N} +%% Status = [{Ref,{{Ok,Skipped,Failed},CopiedCases}}] +%% Ok = Skipped = Failed = [Case,...] +%% +%% Execute the TestCases under configuration Config. Config is a list +%% of lists, where hd(Config) holds the config tuples for the current +%% conf case and tl(Config) is the data for the higher level conf cases. +%% Config data is "inherited" from top to nested conf cases, but +%% never the other way around. if length(Config) == 1, Config contains +%% only the initial config data for the suite. +%% +%% Test may be one of the following: +%% +%% {conf,Ref,Props,{Mod,Func}} Mod:Func is a configuration modification +%% function, call it with the current configuration as argument. It will +%% return a new configuration. +%% +%% {make,Ref,{Mod,Func,Args}} Mod:Func is a make function, and it is called +%% with the given arguments. +%% +%% {Mod,Case} This is a normal test case. Determine the correct +%% configuration, and insert {Mod,Case,Config} as head of the list, +%% then reiterate. +%% +%% {Mod,Case,Args} A test case with predefined argument (usually a normal +%% test case which just got a fresh configuration (see above)). +%% +%% {skip_case,{conf,Ref,Case,Comment}} An init conf case gets skipped +%% by the user. This will also cause the end conf case to be skipped. +%% Note that it is not possible to skip an end conf case directly (it +%% can only be skipped indirectly by a skipped init conf case). The +%% comment (which gets printed in the log files) describes why the case +%% was skipped. +%% +%% {skip_case,{Case,Comment},Mode} A normal test case skipped by the user. +%% The comment (which gets printed in the log files) describes why the +%% case was skipped. +%% +%% {auto_skip_case,{conf,Ref,Case,Comment},Mode} This is the result of +%% an end conf case being automatically skipped due to a failing init +%% conf case. It could also be a nested conf case that gets skipped +%% because of a failed or skipped top level conf. +%% +%% {auto_skip_case,{Case,Comment},Mode} This is a normal test case which +%% gets automatically skipped because of a failing init conf case or +%% because of a failing previous test case in a sequence. +%% +%% ------------------------------------------------------------------- +%% Description of IO handling during execution of parallel test cases: +%% ------------------------------------------------------------------- +%% +%% A conf group can have an associated list of properties. If the +%% parallel property is specified for a group, it means the test cases +%% should be spawned and run in parallel rather than called sequentially +%% (which is always the default mode). Test cases that execute in parallel +%% also write to their respective minor log files in parallel. Printouts +%% to common log files, such as the summary html file and the major log +%% file on text format, still have to be processed sequentially. For this +%% reason, the Mode argument specifies if a parallel group is currently +%% being executed. +%% +%% The low-level mechanism for buffering IO for the common log files +%% is handled by the test_server_io module. Buffering is turned on by +%% test_server_io:start_transaction/0 and off by calling +%% test_server_io:end_transaction/0. The buffered data for the transaction +%% can printed by calling test_server_io:print_buffered/1. +%% +%% This module is responsible for turning on IO buffering and to later +%% test_server_io:print_buffered/1 to print the data. To help with this, +%% two variables in the process dictionary are used: +%% 'test_server_common_io_handler' and 'test_server_queued_io'. The values +%% are set to as follwing: +%% +%% Value Meaning +%% ----- ------- +%% undefined No parallel test cases running +%% {tc,Pid} Running test cases in a top-level parallel group +%% {Ref,Pid} Running sequential test case inside a parallel group +%% +%% FIXME: The Pid is no longer used. +%% +%% If a conf group nested under a parallel group in the test +%% specification should be started, the 'test_server_common_io_handler' +%% value gets set also on the main process. +%% +%% During execution of a parallel group (or of a group nested under a +%% parallel group), *any* new test case being started gets registered +%% in a list saved in the dictionary with 'test_server_queued_io' as key. +%% When the top level parallel group is finished (only then can we be +%% sure all parallel test cases have finished and "reported in"), the +%% list of test cases is traversed in order and test_server_io:print_buffered/1 +%% can be called for each test case. See handle_test_case_io_and_status/0 +%% for details. +%% +%% To be able to handle nested conf groups with different properties, +%% the Mode argument specifies a list of {Ref,Properties} tuples. +%% The head of the Mode list at any given time identifies the group +%% currently being processed. The tail of the list identifies groups +%% on higher level. +%% +%% ------------------------------------------------------------------- +%% Notes on parallel execution of test cases +%% ------------------------------------------------------------------- +%% +%% A group nested under a parallel group will start executing in +%% parallel with previous (parallel) test cases (no matter what +%% properties the nested group has). Test cases are however never +%% executed in parallel with the start or end conf case of the same +%% group! Because of this, the test_server_ctrl loop waits at +%% the end conf of a group for all parallel cases to finish +%% before the end conf case actually executes. This has the effect +%% that it's only after a nested group has finished that any +%% remaining parallel cases in the previous group get spawned (*). +%% Example (all parallel cases): +%% +%% group1_init |----> +%% group1_case1 | ---------> +%% group1_case2 | ---------------------------------> +%% group2_init | ----> +%% group2_case1 | ------> +%% group2_case2 | ----------> +%% group2_end | ---> +%% group1_case3 (*)| ----> +%% group1_case4 (*)| --> +%% group1_end | ---> +%% + +run_test_cases_loop([{SkipTag,CaseData={Type,_Ref,_Case,_Comment}}|Cases], + Config, TimetrapData, Mode, Status) when + ((SkipTag==auto_skip_case) or (SkipTag==skip_case)) and + ((Type==conf) or (Type==make)) -> + run_test_cases_loop([{SkipTag,CaseData,Mode}|Cases], + Config, TimetrapData, Mode, Status); + +run_test_cases_loop([{SkipTag,{Type,Ref,Case,Comment},SkipMode}|Cases], + Config, TimetrapData, Mode, Status) when + ((SkipTag==auto_skip_case) or (SkipTag==skip_case)) and + ((Type==conf) or (Type==make)) -> + ok = file:set_cwd(filename:dirname(get(test_server_dir))), + CurrIOHandler = get(test_server_common_io_handler), + ParentMode = tl(Mode), + + {AutoOrUser,ReportTag} = + if SkipTag == auto_skip_case -> {auto,tc_auto_skip}; + SkipTag == skip_case -> {user,tc_user_skip} + end, + + %% check and update the mode for test case execution and io msg handling + case {curr_ref(Mode),check_props(parallel, Mode)} of + {Ref,Ref} -> + case check_props(parallel, ParentMode) of + false -> + %% this is a skipped end conf for a top level parallel + %% group, buffered io can be flushed + _ = handle_test_case_io_and_status(), + set_io_buffering(undefined), + {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, + false, SkipMode), + ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, + test_server_sup:framework_call(report, + [ReportTag,ConfData]), + run_test_cases_loop(Cases, Config, TimetrapData, ParentMode, + delete_status(Ref, Status)); + _ -> + %% this is a skipped end conf for a parallel group nested + %% under a parallel group (io buffering is active) + _ = wait_for_cases(Ref), + {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, + true, SkipMode), + ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, + test_server_sup:framework_call(report, [ReportTag,ConfData]), + case CurrIOHandler of + {Ref,_} -> + %% current_io_handler was set by start conf of this + %% group, so we can unset it now (no more io from main + %% process needs to be buffered) + set_io_buffering(undefined); + _ -> + ok + end, + run_test_cases_loop(Cases, Config, + TimetrapData, ParentMode, + delete_status(Ref, Status)) + end; + {Ref,false} -> + %% this is a skipped end conf for a non-parallel group that's not + %% nested under a parallel group + {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, + false, SkipMode), + ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, + test_server_sup:framework_call(report, [ReportTag,ConfData]), + + %% Check if this group is auto skipped because of error in the + %% init conf. If so, check if the parent group is a sequence, + %% and if it is, skip all proceeding tests in that group. + GrName = get_name(Mode), + Cases1 = + case get_tc_results(Status) of + {_,_,Fails} when length(Fails) > 0 -> + case lists:member({group_result,GrName}, Fails) of + true -> + case check_prop(sequence, ParentMode) of + false -> + Cases; + ParentRef -> + Reason = {group_result,GrName,failed}, + skip_cases_upto(ParentRef, Cases, + Reason, tc, ParentMode, + SkipTag) + end; + false -> + Cases + end; + _ -> + Cases + end, + run_test_cases_loop(Cases1, Config, TimetrapData, ParentMode, + delete_status(Ref, Status)); + {Ref,_} -> + %% this is a skipped end conf for a non-parallel group nested under + %% a parallel group (io buffering is active) + {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, + true, SkipMode), + ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, + test_server_sup:framework_call(report, [ReportTag,ConfData]), + case CurrIOHandler of + {Ref,_} -> + %% current_io_handler was set by start conf of this + %% group, so we can unset it now (no more io from main + %% process needs to be buffered) + set_io_buffering(undefined); + _ -> + ok + end, + run_test_cases_loop(Cases, Config, TimetrapData, tl(Mode), + delete_status(Ref, Status)); + {_,false} -> + %% this is a skipped start conf for a group which is not nested + %% under a parallel group + {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, + false, SkipMode), + ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, + test_server_sup:framework_call(report, [ReportTag,ConfData]), + run_test_cases_loop(Cases, Config, TimetrapData, + [conf(Ref,[])|Mode], Status); + {_,Ref0} when is_reference(Ref0) -> + %% this is a skipped start conf for a group nested under a parallel + %% group and if this is the first nested group, io buffering must + %% be activated + if CurrIOHandler == undefined -> + set_io_buffering({Ref,self()}); + true -> + ok + end, + {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, + true, SkipMode), + ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, + test_server_sup:framework_call(report, [ReportTag,ConfData]), + run_test_cases_loop(Cases, Config, TimetrapData, + [conf(Ref,[])|Mode], Status) + end; + +run_test_cases_loop([{auto_skip_case,{Case,Comment},SkipMode}|Cases], + Config, TimetrapData, Mode, Status) -> + {Mod,Func} = skip_case(auto, undefined, get(test_server_case_num)+1, + Case, Comment, is_io_buffered(), SkipMode), + test_server_sup:framework_call(report, [tc_auto_skip, + {Mod,{Func,get_name(SkipMode)}, + Comment}]), + run_test_cases_loop(Cases, Config, TimetrapData, Mode, + update_status(skipped, Mod, Func, Status)); + +run_test_cases_loop([{skip_case,{{Mod,all}=Case,Comment},SkipMode}|Cases], + Config, TimetrapData, Mode, Status) -> + _ = skip_case(user, undefined, 0, Case, Comment, false, SkipMode), + test_server_sup:framework_call(report, [tc_user_skip, + {Mod,{all,get_name(SkipMode)}, + Comment}]), + run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status); + +run_test_cases_loop([{skip_case,{Case,Comment},SkipMode}|Cases], + Config, TimetrapData, Mode, Status) -> + {Mod,Func} = skip_case(user, undefined, get(test_server_case_num)+1, + Case, Comment, is_io_buffered(), SkipMode), + test_server_sup:framework_call(report, [tc_user_skip, + {Mod,{Func,get_name(SkipMode)}, + Comment}]), + run_test_cases_loop(Cases, Config, TimetrapData, Mode, + update_status(skipped, Mod, Func, Status)); + +%% a start *or* end conf case, wrapping test cases or other conf cases +run_test_cases_loop([{conf,Ref,Props,{Mod,Func}}|_Cases]=Cs0, + Config, TimetrapData, Mode0, Status) -> + CurrIOHandler = get(test_server_common_io_handler), + %% check and update the mode for test case execution and io msg handling + {StartConf,Mode,IOHandler,ConfTime,Status1} = + case {curr_ref(Mode0),check_props(parallel, Mode0)} of + {Ref,Ref} -> + case check_props(parallel, tl(Mode0)) of + false -> + %% this is an end conf for a top level parallel group, + %% collect results from the test case processes + %% and calc total time + OkSkipFail = handle_test_case_io_and_status(), + ok = file:set_cwd(filename:dirname(get(test_server_dir))), + After = ?now, + Before = get(test_server_parallel_start_time), + Elapsed = timer:now_diff(After, Before)/1000000, + put(test_server_total_time, Elapsed), + {false,tl(Mode0),undefined,Elapsed, + update_status(Ref, OkSkipFail, Status)}; + _ -> + %% this is an end conf for a parallel group nested under a + %% parallel group (io buffering is active) + OkSkipFail = wait_for_cases(Ref), + queue_test_case_io(Ref, self(), 0, Mod, Func), + Elapsed = timer:now_diff(?now, conf_start(Ref, Mode0))/1000000, + case CurrIOHandler of + {Ref,_} -> + %% current_io_handler was set by start conf of this + %% group, so we can unset it after this case (no + %% more io from main process needs to be buffered) + {false,tl(Mode0),undefined,Elapsed, + update_status(Ref, OkSkipFail, Status)}; + _ -> + {false,tl(Mode0),CurrIOHandler,Elapsed, + update_status(Ref, OkSkipFail, Status)} + end + end; + {Ref,false} -> + %% this is an end conf for a non-parallel group that's not + %% nested under a parallel group, so no need to buffer io + {false,tl(Mode0),undefined, + timer:now_diff(?now, conf_start(Ref, Mode0))/1000000, Status}; + {Ref,_} -> + %% this is an end conf for a non-parallel group nested under + %% a parallel group (io buffering is active) + queue_test_case_io(Ref, self(), 0, Mod, Func), + Elapsed = timer:now_diff(?now, conf_start(Ref, Mode0))/1000000, + case CurrIOHandler of + {Ref,_} -> + %% current_io_handler was set by start conf of this + %% group, so we can unset it after this case (no + %% more io from main process needs to be buffered) + {false,tl(Mode0),undefined,Elapsed,Status}; + _ -> + {false,tl(Mode0),CurrIOHandler,Elapsed,Status} + end; + {_,false} -> + %% this is a start conf for a group which is not nested under a + %% parallel group, check if this case starts a new parallel group + case lists:member(parallel, Props) of + true -> + %% prepare for execution of parallel group + put(test_server_parallel_start_time, ?now), + put(test_server_queued_io, []); + false -> + ok + end, + {true,[conf(Ref,Props)|Mode0],undefined,0,Status}; + {_,_Ref0} -> + %% this is a start conf for a group nested under a parallel group, the + %% parallel_start_time and parallel_test_cases values have already been set + queue_test_case_io(Ref, self(), 0, Mod, Func), + %% if this is the first nested group under a parallel group, io + %% buffering must be activated + IOHandler1 = if CurrIOHandler == undefined -> + IOH = {Ref,self()}, + set_io_buffering(IOH), + IOH; + true -> + CurrIOHandler + end, + {true,[conf(Ref,Props)|Mode0],IOHandler1,0,Status} + end, + + %% if this is a start conf we check if cases should be shuffled + {[_Conf|Cases1]=Cs1,Shuffle} = + if StartConf -> + case get_shuffle(Props) of + undefined -> + {Cs0,undefined}; + {_,repeated} -> + %% if group is repeated, a new seed should not be set every + %% turn - last one is saved in dictionary + CurrSeed = get(test_server_curr_random_seed), + {shuffle_cases(Ref, Cs0, CurrSeed),{shuffle,CurrSeed}}; + {_,Seed} -> + UseSeed= + %% Determine which seed to use by: + %% 1. check the TS_RANDOM_SEED env variable + %% 2. check random_seed in process state + %% 3. use value provided with shuffle option + %% 4. use timestamp() values for seed + case os:getenv("TS_RANDOM_SEED") of + Undef when Undef == false ; Undef == "undefined" -> + case get(test_server_random_seed) of + undefined -> Seed; + TSRS -> TSRS + end; + NumStr -> + %% Ex: "123 456 789" or "123,456,789" -> {123,456,789} + list_to_tuple([list_to_integer(NS) || + NS <- string:tokens(NumStr, [$ ,$:,$,])]) + end, + {shuffle_cases(Ref, Cs0, UseSeed),{shuffle,UseSeed}} + end; + not StartConf -> + {Cs0,undefined} + end, + + %% if this is a start conf we check if Props specifies repeat and if so + %% we copy the group and carry the copy until the end conf where we + %% decide to perform the repetition or not + {Repeating,Status2,Cases,ReportRepeatStop} = + if StartConf -> + case get_repeat(Props) of + undefined -> + %% we *must* have a status entry for every conf since we + %% will continously update status with test case results + %% without knowing the Ref (but update hd(Status)) + {false,new_status(Ref, Status1),Cases1,?void_fun}; + {_RepType,N} when N =< 1 -> + {false,new_status(Ref, Status1),Cases1,?void_fun}; + _ -> + {Copied,_} = copy_cases(Ref, make_ref(), Cs1), + {true,new_status(Ref, Copied, Status1),Cases1,?void_fun} + end; + not StartConf -> + RepVal = get_repeat(get_props(Mode0)), + ReportStop = + fun() -> + print(minor, "~n*** Stopping repeat operation ~w", [RepVal]), + print(1, "Stopping repeat operation ~w", [RepVal]) + end, + CopiedCases = get_copied_cases(Status1), + EndStatus = delete_status(Ref, Status1), + %% check in Mode0 if this is a repeat conf + case RepVal of + undefined -> + {false,EndStatus,Cases1,?void_fun}; + {_RepType,N} when N =< 1 -> + {false,EndStatus,Cases1,?void_fun}; + {repeat,_} -> + {true,EndStatus,CopiedCases++Cases1,?void_fun}; + {repeat_until_all_ok,_} -> + {RestCs,Fun} = case get_tc_results(Status1) of + {_,_,[]} -> + {Cases1,ReportStop}; + _ -> + {CopiedCases++Cases1,?void_fun} + end, + {true,EndStatus,RestCs,Fun}; + {repeat_until_any_ok,_} -> + {RestCs,Fun} = case get_tc_results(Status1) of + {Ok,_,_Fails} when length(Ok) > 0 -> + {Cases1,ReportStop}; + _ -> + {CopiedCases++Cases1,?void_fun} + end, + {true,EndStatus,RestCs,Fun}; + {repeat_until_any_fail,_} -> + {RestCs,Fun} = case get_tc_results(Status1) of + {_,_,Fails} when length(Fails) > 0 -> + {Cases1,ReportStop}; + _ -> + {CopiedCases++Cases1,?void_fun} + end, + {true,EndStatus,RestCs,Fun}; + {repeat_until_all_fail,_} -> + {RestCs,Fun} = case get_tc_results(Status1) of + {[],_,_} -> + {Cases1,ReportStop}; + _ -> + {CopiedCases++Cases1,?void_fun} + end, + {true,EndStatus,RestCs,Fun} + end + end, + + ReportAbortRepeat = fun(What) when Repeating -> + print(minor, "~n*** Aborting repeat operation " + "(configuration case ~w)", [What]), + print(1, "Aborting repeat operation " + "(configuration case ~w)", [What]); + (_) -> ok + end, + CfgProps = if StartConf -> + if Shuffle == undefined -> + [{tc_group_properties,Props}]; + true -> + [{tc_group_properties, + [Shuffle|delete_shuffle(Props)]}] + end; + not StartConf -> + {TcOk,TcSkip,TcFail} = get_tc_results(Status1), + [{tc_group_properties,get_props(Mode0)}, + {tc_group_result,[{ok,TcOk}, + {skipped,TcSkip}, + {failed,TcFail}]}] + end, + + SuiteName = proplists:get_value(suite, Props), + case get(test_server_create_priv_dir) of + auto_per_run -> % use common priv_dir + TSDirs = [{priv_dir,get(test_server_priv_dir)}, + {data_dir,get_data_dir(Mod, SuiteName)}]; + _ -> + TSDirs = [{data_dir,get_data_dir(Mod, SuiteName)}] + end, + + ActualCfg = + if not StartConf -> + update_config(hd(Config), TSDirs ++ CfgProps); + true -> + GroupPath = lists:flatmap(fun({_Ref,[],_T}) -> []; + ({_Ref,GrProps,_T}) -> [GrProps] + end, Mode0), + update_config(hd(Config), + TSDirs ++ [{tc_group_path,GroupPath} | CfgProps]) + end, + + CurrMode = curr_mode(Ref, Mode0, Mode), + ConfCaseResult = run_test_case(Ref, 0, Mod, Func, [ActualCfg], skip_init, + TimetrapData, CurrMode), + + case ConfCaseResult of + {_,NewCfg,_} when Func == init_per_suite, is_list(NewCfg) -> + %% check that init_per_suite returned data on correct format + case lists:filter(fun({_,_}) -> false; + (_) -> true end, NewCfg) of + [] -> + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(Cases, [NewCfg|Config], + TimetrapData, Mode, Status2); + Bad -> + print(minor, + "~n*** ~tw returned bad elements in Config: ~tp.~n", + [Func,Bad]), + Reason = {failed,{Mod,init_per_suite,bad_return}}, + Cases2 = skip_cases_upto(Ref, Cases, Reason, conf, CurrMode, + auto_skip_case), + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(Cases2, Config, TimetrapData, Mode, + delete_status(Ref, Status2)) + end; + {_,NewCfg,_} when StartConf, is_list(NewCfg) -> + print_conf_time(ConfTime), + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(Cases, [NewCfg|Config], TimetrapData, Mode, Status2); + {_,{framework_error,{FwMod,FwFunc},Reason},_} -> + print(minor, "~n*** ~w failed in ~tw. Reason: ~tp~n", + [FwMod,FwFunc,Reason]), + print(1, "~w failed in ~tw. Reason: ~tp~n", [FwMod,FwFunc,Reason]), + exit(framework_error); + {_,Fail,_} when element(1,Fail) == 'EXIT'; + element(1,Fail) == timetrap_timeout; + element(1,Fail) == user_timetrap_error; + element(1,Fail) == failed -> + {Cases2,Config1,Status3} = + if StartConf -> + ReportAbortRepeat(failed), + print(minor, "~n*** ~tw failed.~n" + " Skipping all cases.", [Func]), + Reason = {failed,{Mod,Func,Fail}}, + {skip_cases_upto(Ref, Cases, Reason, conf, CurrMode, + auto_skip_case), + Config, + update_status(failed, group_result, get_name(Mode), + delete_status(Ref, Status2))}; + not StartConf -> + ReportRepeatStop(), + print_conf_time(ConfTime), + {Cases,tl(Config),delete_status(Ref, Status2)} + end, + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(Cases2, Config1, TimetrapData, Mode, Status3); + + {_,{auto_skip,SkipReason},_} -> + %% this case can only happen if the framework (not the user) + %% decides to skip execution of a conf function + {Cases2,Config1,Status3} = + if StartConf -> + ReportAbortRepeat(auto_skipped), + print(minor, "~n*** ~tw auto skipped.~n" + " Skipping all cases.", [Func]), + {skip_cases_upto(Ref, Cases, SkipReason, conf, CurrMode, + auto_skip_case), + Config, + delete_status(Ref, Status2)}; + not StartConf -> + ReportRepeatStop(), + print_conf_time(ConfTime), + {Cases,tl(Config),delete_status(Ref, Status2)} + end, + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(Cases2, Config1, TimetrapData, Mode, Status3); + + {_,{Skip,Reason},_} when StartConf and ((Skip==skip) or (Skip==skipped)) -> + ReportAbortRepeat(skipped), + print(minor, "~n*** ~tw skipped.~n" + " Skipping all cases.", [Func]), + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(skip_cases_upto(Ref, Cases, Reason, conf, + CurrMode, skip_case), + [hd(Config)|Config], TimetrapData, Mode, + delete_status(Ref, Status2)); + {_,{skip_and_save,Reason,_SavedConfig},_} when StartConf -> + ReportAbortRepeat(skipped), + print(minor, "~n*** ~tw skipped.~n" + " Skipping all cases.", [Func]), + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(skip_cases_upto(Ref, Cases, Reason, conf, + CurrMode, skip_case), + [hd(Config)|Config], TimetrapData, Mode, + delete_status(Ref, Status2)); + {_,_Other,_} when Func == init_per_suite -> + print(minor, "~n*** init_per_suite failed to return a Config list.~n", []), + Reason = {failed,{Mod,init_per_suite,bad_return}}, + Cases2 = skip_cases_upto(Ref, Cases, Reason, conf, CurrMode, + auto_skip_case), + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(Cases2, Config, TimetrapData, Mode, + delete_status(Ref, Status2)); + {_,_Other,_} when StartConf -> + print_conf_time(ConfTime), + set_io_buffering(IOHandler), + ReportRepeatStop(), + stop_minor_log_file(), + run_test_cases_loop(Cases, [hd(Config)|Config], TimetrapData, + Mode, Status2); + {_,_EndConfRetVal,Opts} -> + %% Check if return_group_result is set (ok, skipped or failed) and + %% if so: + %% 1) *If* the parent group is a sequence, skip all proceeding tests + %% in that group. + %% 2) Return the value to the group "above" so that result may be + %% used for evaluating a 'repeat_until_*' property. + GrName = get_name(Mode0, Func), + {Cases2,Status3} = + case lists:keysearch(return_group_result, 1, Opts) of + {value,{_,failed}} -> + case {curr_ref(Mode),check_prop(sequence, Mode)} of + {ParentRef,ParentRef} -> + Reason = {group_result,GrName,failed}, + {skip_cases_upto(ParentRef, Cases, Reason, tc, + Mode, auto_skip_case), + update_status(failed, group_result, GrName, + delete_status(Ref, Status2))}; + _ -> + {Cases,update_status(failed, group_result, GrName, + delete_status(Ref, Status2))} + end; + {value,{_,GroupResult}} -> + {Cases,update_status(GroupResult, group_result, GrName, + delete_status(Ref, Status2))}; + false -> + {Cases,update_status(ok, group_result, GrName, + delete_status(Ref, Status2))} + end, + print_conf_time(ConfTime), + ReportRepeatStop(), + set_io_buffering(IOHandler), + stop_minor_log_file(), + run_test_cases_loop(Cases2, tl(Config), TimetrapData, + Mode, Status3) + end; + +run_test_cases_loop([{make,Ref,{Mod,Func,Args}}|Cases0], Config, TimetrapData, + Mode, Status) -> + case run_test_case(Ref, 0, Mod, Func, Args, skip_init, TimetrapData) of + {_,Why={'EXIT',_},_} -> + print(minor, "~n*** ~tw failed.~n" + " Skipping all cases.", [Func]), + Reason = {failed,{Mod,Func,Why}}, + Cases = skip_cases_upto(Ref, Cases0, Reason, conf, Mode, + auto_skip_case), + stop_minor_log_file(), + run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status); + {_,_Whatever,_} -> + stop_minor_log_file(), + run_test_cases_loop(Cases0, Config, TimetrapData, Mode, Status) + end; + +run_test_cases_loop([{conf,_Ref,_Props,_X}=Conf|_Cases0], + Config, _TimetrapData, _Mode, _Status) -> + erlang:error(badarg, [Conf,Config]); + +run_test_cases_loop([{Mod,Case}|Cases], Config, TimetrapData, Mode, Status) -> + ActualCfg = + case get(test_server_create_priv_dir) of + auto_per_run -> + update_config(hd(Config), [{priv_dir,get(test_server_priv_dir)}, + {data_dir,get_data_dir(Mod)}]); + _ -> + update_config(hd(Config), [{data_dir,get_data_dir(Mod)}]) + end, + run_test_cases_loop([{Mod,Case,[ActualCfg]}|Cases], Config, + TimetrapData, Mode, Status); + +run_test_cases_loop([{Mod,Func,Args}|Cases], Config, TimetrapData, Mode, Status) -> + {Num,RunInit} = + case FwMod = get_fw_mod(?MODULE) of + Mod when Func == error_in_suite -> + {-1,skip_init}; + _ -> + {put(test_server_case_num, get(test_server_case_num)+1), + run_init} + end, + + %% check the current execution mode and save info about the case if + %% detected that printouts to common log files is handled later + + case check_prop(parallel, Mode) =:= false andalso is_io_buffered() of + true -> + %% sequential test case nested in a parallel group; + %% io is buffered, so we must queue this test case + queue_test_case_io(undefined, self(), Num+1, Mod, Func); + false -> + ok + end, + + case run_test_case(undefined, Num+1, Mod, Func, Args, + RunInit, TimetrapData, Mode) of + %% callback to framework module failed, exit immediately + {_,{framework_error,{FwMod,FwFunc},Reason},_} -> + print(minor, "~n*** ~w failed in ~tw. Reason: ~tp~n", + [FwMod,FwFunc,Reason]), + print(1, "~w failed in ~tw. Reason: ~tp~n", [FwMod,FwFunc,Reason]), + stop_minor_log_file(), + exit(framework_error); + %% sequential execution of test case finished + {Time,RetVal,_} -> + RetTag = + if is_tuple(RetVal) -> element(1,RetVal); + true -> undefined + end, + {Failed,Status1} = + case RetTag of + Skip when Skip==skip; Skip==skipped -> + {false,update_status(skipped, Mod, Func, Status)}; + Fail when Fail=='EXIT'; Fail==failed -> + {true,update_status(failed, Mod, Func, Status)}; + _ when Time==died, RetVal=/=ok -> + {true,update_status(failed, Mod, Func, Status)}; + _ -> + {false,update_status(ok, Mod, Func, Status)} + end, + case check_prop(sequence, Mode) of + false -> + stop_minor_log_file(), + run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status1); + Ref -> + %% the case is in a sequence; we must check the result and + %% determine if the following cases should run or be skipped + if not Failed -> % proceed with next case + stop_minor_log_file(), + run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status1); + true -> % skip rest of cases in sequence + print(minor, "~n*** ~tw failed.~n" + " Skipping all other cases in sequence.", + [Func]), + Reason = {failed,{Mod,Func}}, + Cases2 = skip_cases_upto(Ref, Cases, Reason, tc, + Mode, auto_skip_case), + stop_minor_log_file(), + run_test_cases_loop(Cases2, Config, TimetrapData, Mode, Status1) + end + end; + %% the test case is being executed in parallel with the main process (and + %% other test cases) and Pid is the dedicated process executing the case + Pid -> + %% io from Pid will be buffered by the test_server_io process and + %% handled later, so we have to save info about the case + queue_test_case_io(undefined, Pid, Num+1, Mod, Func), + run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status) + end; + +%% TestSpec processing finished +run_test_cases_loop([], _Config, _TimetrapData, _, _) -> + ok. + +%%-------------------------------------------------------------------- +%% various help functions + +new_status(Ref, Status) -> + [{Ref,{{[],[],[]},[]}} | Status]. + +new_status(Ref, CopiedCases, Status) -> + [{Ref,{{[],[],[]},CopiedCases}} | Status]. + +delete_status(Ref, Status) -> + lists:keydelete(Ref, 1, Status). + +update_status(ok, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) -> + [{Ref,{{Ok++[{Mod,Func}],Skip,Fail},Cs}} | Status]; + +update_status(skipped, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) -> + [{Ref,{{Ok,Skip++[{Mod,Func}],Fail},Cs}} | Status]; + +update_status(failed, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) -> + [{Ref,{{Ok,Skip,Fail++[{Mod,Func}]},Cs}} | Status]; + +update_status(_, _, _, []) -> + []. + +update_status(Ref, {Ok,Skip,Fail}, [{Ref,{{Ok0,Skip0,Fail0},Cs}} | Status]) -> + [{Ref,{{Ok0++Ok,Skip0++Skip,Fail0++Fail},Cs}} | Status]. + +get_copied_cases([{_,{_,Cases}} | _Status]) -> + Cases. + +get_tc_results([{_,{OkSkipFail,_}} | _Status]) -> + OkSkipFail; +get_tc_results([]) -> % in case init_per_suite crashed + {[],[],[]}. + +conf(Ref, Props) -> + {Ref,Props,?now}. + +curr_ref([{Ref,_Props,_}|_]) -> + Ref; +curr_ref([]) -> + undefined. + +curr_mode(Ref, Mode0, Mode1) -> + case curr_ref(Mode1) of + Ref -> Mode1; + _ -> Mode0 + end. + +get_props([{_,Props,_} | _]) -> + Props; +get_props([]) -> + []. + +check_prop(_Attrib, []) -> + false; +check_prop(Attrib, [{Ref,Props,_}|_]) -> + case lists:member(Attrib, Props) of + true -> Ref; + false -> false + end. + +check_props(Attrib, Mode) -> + case [R || {R,Ps,_} <- Mode, lists:member(Attrib, Ps)] of + [] -> false; + [Ref|_] -> Ref + end. + +get_name(Mode, Def) -> + case get_name(Mode) of + undefined -> Def; + Name -> Name + end. + +get_name([{_Ref,Props,_}|_]) -> + proplists:get_value(name, Props); +get_name([]) -> + undefined. + +conf_start(Ref, Mode) -> + case lists:keysearch(Ref, 1, Mode) of + {value,{_,_,T}} -> T; + false -> 0 + end. + + +get_data_dir(Mod) -> + get_data_dir(Mod, undefined). + +get_data_dir(Mod, Suite) -> + UseMod = if Suite == undefined -> Mod; + true -> Suite + end, + case code:which(UseMod) of + non_existing -> + print(12, "The module ~w is not loaded", [Mod]), + []; + cover_compiled -> + MainCoverNode = cover:get_main_node(), + {file,File} = rpc:call(MainCoverNode,cover,is_compiled,[UseMod]), + do_get_data_dir(UseMod,File); + FullPath -> + do_get_data_dir(UseMod,FullPath) + end. + +do_get_data_dir(Mod,File) -> + filename:dirname(File) ++ "/" ++ atom_to_list(Mod) ++ ?data_dir_suffix. + +print_conf_time(0) -> + ok; +print_conf_time(ConfTime) -> + print(major, "=group_time ~.3fs", [ConfTime]), + print(minor, "~n=== Total execution time of group: ~.3fs~n", [ConfTime]). + +print_props([]) -> + ok; +print_props(Props) -> + print(major, "=group_props ~tp", [Props]), + print(minor, "Group properties: ~tp~n", [Props]). + +%% repeat N times: {repeat,N} +%% repeat N times or until all successful: {repeat_until_all_ok,N} +%% repeat N times or until at least one successful: {repeat_until_any_ok,N} +%% repeat N times or until at least one case fails: {repeat_until_any_fail,N} +%% repeat N times or until all fails: {repeat_until_all_fail,N} +%% N = integer() | forever +get_repeat(Props) -> + get_prop([repeat,repeat_until_all_ok,repeat_until_any_ok, + repeat_until_any_fail,repeat_until_all_fail], forever, Props). + +update_repeat(Props) -> + case get_repeat(Props) of + undefined -> + Props; + {RepType,N} -> + Props1 = + if N == forever -> + [{RepType,N}|lists:keydelete(RepType, 1, Props)]; + N < 3 -> + lists:keydelete(RepType, 1, Props); + N >= 3 -> + [{RepType,N-1}|lists:keydelete(RepType, 1, Props)] + end, + %% if shuffle is used in combination with repeat, a new + %% seed shouldn't be set every new turn + case get_shuffle(Props1) of + undefined -> + Props1; + _ -> + [{shuffle,repeated}|delete_shuffle(Props1)] + end + end. + +get_shuffle(Props) -> + get_prop([shuffle], ?now, Props). + +delete_shuffle(Props) -> + delete_prop([shuffle], Props). + +%% Return {Item,Value} if found, else if Item alone +%% is found, return {Item,Default} +get_prop([Item|Items], Default, Props) -> + case lists:keysearch(Item, 1, Props) of + {value,R} -> + R; + false -> + case lists:member(Item, Props) of + true -> + {Item,Default}; + false -> + get_prop(Items, Default, Props) + end + end; +get_prop([], _Def, _Props) -> + undefined. + +delete_prop([Item|Items], Props) -> + Props1 = lists:delete(Item, lists:keydelete(Item, 1, Props)), + delete_prop(Items, Props1); +delete_prop([], Props) -> + Props. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% shuffle_cases(Ref, Cases, Seed) -> Cases1 +%% +%% Shuffles the order of Cases. + +shuffle_cases(Ref, Cases, undefined) -> + shuffle_cases(Ref, Cases, rand:seed_s(exsplus)); + +shuffle_cases(Ref, [{conf,Ref,_,_}=Start | Cases], Seed0) -> + {N,CasesToShuffle,Rest} = cases_to_shuffle(Ref, Cases), + Seed = case Seed0 of + {X,Y,Z} when is_integer(X+Y+Z) -> + rand:seed(exsplus, Seed0); + _ -> + Seed0 + end, + ShuffledCases = random_order(N, rand:uniform_s(N, Seed), CasesToShuffle, []), + [Start|ShuffledCases] ++ Rest. + +cases_to_shuffle(Ref, Cases) -> + cases_to_shuffle(Ref, Cases, 1, []). + +cases_to_shuffle(Ref, [{conf,Ref,_,_} | _]=Cs, N, Ix) -> % end + {N-1,Ix,Cs}; +cases_to_shuffle(Ref, [{skip_case,{_,Ref,_,_},_} | _]=Cs, N, Ix) -> % end + {N-1,Ix,Cs}; + +cases_to_shuffle(Ref, [{conf,Ref1,_,_}=C | Cs], N, Ix) -> % nested group + {Cs1,Rest} = get_subcases(Ref1, Cs, []), + cases_to_shuffle(Ref, Rest, N+1, [{N,[C|Cs1]} | Ix]); +cases_to_shuffle(Ref, [{skip_case,{_,Ref1,_,_},_}=C | Cs], N, Ix) -> % nested group + {Cs1,Rest} = get_subcases(Ref1, Cs, []), + cases_to_shuffle(Ref, Rest, N+1, [{N,[C|Cs1]} | Ix]); + +cases_to_shuffle(Ref, [C | Cs], N, Ix) -> + cases_to_shuffle(Ref, Cs, N+1, [{N,[C]} | Ix]). + +get_subcases(SubRef, [{conf,SubRef,_,_}=C | Cs], SubCs) -> + {lists:reverse([C|SubCs]),Cs}; +get_subcases(SubRef, [{skip_case,{_,SubRef,_,_},_}=C | Cs], SubCs) -> + {lists:reverse([C|SubCs]),Cs}; +get_subcases(SubRef, [C|Cs], SubCs) -> + get_subcases(SubRef, Cs, [C|SubCs]). + +random_order(1, {_Pos,Seed}, [{_Ix,CaseOrGroup}], Shuffled) -> + %% save current seed to be used if test cases are repeated + put(test_server_curr_random_seed, Seed), + Shuffled++CaseOrGroup; +random_order(N, {Pos,NewSeed}, IxCases, Shuffled) -> + {First,[{_Ix,CaseOrGroup}|Rest]} = lists:split(Pos-1, IxCases), + random_order(N-1, rand:uniform_s(N-1, NewSeed), + First++Rest, Shuffled++CaseOrGroup). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% skip_case(Type, Ref, CaseNum, Case, Comment, SendSync) -> {Mod,Func} +%% +%% Prints info about a skipped case in the major and html log files. +%% SendSync determines if start and finished messages must be sent so +%% that the printouts can be buffered and handled in order with io from +%% parallel processes. +skip_case(Type, Ref, CaseNum, Case, Comment, SendSync, Mode) -> + MF = {Mod,Func} = case Case of + {M,F,_A} -> {M,F}; + {M,F} -> {M,F} + end, + if SendSync -> + queue_test_case_io(Ref, self(), CaseNum, Mod, Func), + self() ! {started,Ref,self(),CaseNum,Mod,Func}, + test_server_io:start_transaction(), + skip_case1(Type, CaseNum, Mod, Func, Comment, Mode), + test_server_io:end_transaction(), + self() ! {finished,Ref,self(),CaseNum,Mod,Func,skipped,{0,skipped,[]}}; + not SendSync -> + skip_case1(Type, CaseNum, Mod, Func, Comment, Mode) + end, + MF. + +skip_case1(Type, CaseNum, Mod, Func, Comment, Mode) -> + {{Col0,Col1},_} = get_font_style((CaseNum > 0), Mode), + ResultCol = if Type == auto -> ?auto_skip_color; + Type == user -> ?user_skip_color + end, + print(major, "~n=case ~w:~tw", [Mod,Func]), + GroupName = case get_name(Mode) of + undefined -> + ""; + GrName -> + GrName1 = cast_to_list(GrName), + print(major, "=group_props ~tp", [[{name,GrName1}]]), + GrName1 + end, + print(major, "=started ~s", [lists:flatten(timestamp_get(""))]), + Comment1 = reason_to_string(Comment), + if Type == auto -> + print(major, "=result auto_skipped: ~ts", [Comment1]); + Type == user -> + print(major, "=result skipped: ~ts", [Comment1]) + end, + if CaseNum == 0 -> + print(2,"*** Skipping ~tw ***", [{Mod,Func}]); + true -> + print(2,"*** Skipping test case #~w ~tw ***", [CaseNum,{Mod,Func}]) + end, + TR = xhtml("<tr valign=\"top\">", ["<tr class=\"",odd_or_even(),"\">"]), + GroupName = case get_name(Mode) of + undefined -> ""; + Name -> cast_to_list(Name) + end, + print(html, + TR ++ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" + "<td>" ++ Col0 ++ "~w" ++ Col1 ++ "</td>" + "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" + "<td>" ++ Col0 ++ "~tw" ++ Col1 ++ "</td>" + "<td>" ++ Col0 ++ "< >" ++ Col1 ++ "</td>" + "<td>" ++ Col0 ++ "0.000s" ++ Col1 ++ "</td>" + "<td><font color=\"~ts\">SKIPPED</font></td>" + "<td>~ts</td></tr>\n", + [num2str(CaseNum),fw_name(Mod),GroupName,Func,ResultCol,Comment1]), + + if CaseNum > 0 -> + {US,AS} = get(test_server_skipped), + case Type of + user -> put(test_server_skipped, {US+1,AS}); + auto -> put(test_server_skipped, {US,AS+1}) + end, + put(test_server_case_num, CaseNum); + true -> % conf + ok + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% skip_cases_upto(Ref, Cases, Reason, Origin, Mode, SkipType) -> Cases1 +%% +%% SkipType = skip_case | auto_skip_case +%% Mark all cases tagged with Ref as skipped. + +skip_cases_upto(Ref, Cases, Reason, Origin, Mode, SkipType) -> + {_,Modified,Rest} = + modify_cases_upto(Ref, {skip,Reason,Origin,Mode,SkipType}, Cases), + Modified++Rest. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% copy_cases(OrigRef, NewRef, Cases) -> Cases1 +%% +%% Copy the test cases marked with OrigRef and tag the copies with NewRef. +%% The start conf case copy will also get its repeat property updated. + +copy_cases(OrigRef, NewRef, Cases) -> + {Original,Altered,Rest} = modify_cases_upto(OrigRef, {copy,NewRef}, Cases), + {Altered,Original++Altered++Rest}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% modify_cases_upto(Ref, ModOp, Cases) -> {Original,Altered,Remaining} +%% +%% ModOp = {skip,Reason,Origin,Mode} | {copy,NewRef} +%% Origin = conf | tc +%% +%% Modifies Cases according to ModOp and returns the original elements, +%% the modified versions of these elements and the remaining (untouched) +%% cases. + +modify_cases_upto(Ref, ModOp, Cases) -> + {Original,Altered,Rest} = modify_cases_upto(Ref, ModOp, Cases, [], []), + {lists:reverse(Original),lists:reverse(Altered),Rest}. + +%% first case of a copy operation is the start conf +modify_cases_upto(Ref, {copy,NewRef}=Op, [{conf,Ref,Props,MF}=C|T], Orig, Alt) -> + modify_cases_upto(Ref, Op, T, [C|Orig], [{conf,NewRef,update_repeat(Props),MF}|Alt]); + +modify_cases_upto(Ref, ModOp, Cases, Orig, Alt) -> + %% we need to check if there's an end conf case with the + %% same ref in the list, if not, this *is* an end conf case + case lists:any(fun({_,R,_,_}) when R == Ref -> true; + ({_,R,_}) when R == Ref -> true; + ({skip_case,{_,R,_,_},_}) when R == Ref -> true; + ({skip_case,{_,R,_,_}}) when R == Ref -> true; + (_) -> false + end, Cases) of + true -> + modify_cases_upto1(Ref, ModOp, Cases, Orig, Alt); + false -> + {[],[],Cases} + end. + +%% next case is a conf with same ref, must be end conf = we're done +modify_cases_upto1(Ref, {skip,Reason,conf,Mode,skip_case}, + [{conf,Ref,_Props,MF}|T], Orig, Alt) -> + {Orig,[{skip_case,{conf,Ref,MF,Reason},Mode}|Alt],T}; +modify_cases_upto1(Ref, {skip,Reason,conf,Mode,auto_skip_case}, + [{conf,Ref,_Props,MF}|T], Orig, Alt) -> + {Orig,[{auto_skip_case,{conf,Ref,MF,Reason},Mode}|Alt],T}; +modify_cases_upto1(Ref, {copy,NewRef}, [{conf,Ref,Props,MF}=C|T], Orig, Alt) -> + {[C|Orig],[{conf,NewRef,update_repeat(Props),MF}|Alt],T}; + +%% we've skipped all remaining cases in a sequence +modify_cases_upto1(Ref, {skip,_,tc,_,_}, + [{conf,Ref,_Props,_MF}|_]=Cs, Orig, Alt) -> + {Orig,Alt,Cs}; + +%% next is a make case +modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}, + [{make,Ref,MF}|T], Orig, Alt) -> + {Orig,[{SkipType,{make,Ref,MF,Reason},Mode}|Alt],T}; +modify_cases_upto1(Ref, {copy,NewRef}, [{make,Ref,MF}=M|T], Orig, Alt) -> + {[M|Orig],[{make,NewRef,MF}|Alt],T}; + +%% next case is a user skipped end conf with the same ref = we're done +modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}, + [{skip_case,{Type,Ref,MF,_Cmt},_}|T], Orig, Alt) -> + {Orig,[{SkipType,{Type,Ref,MF,Reason},Mode}|Alt],T}; +modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}, + [{skip_case,{Type,Ref,MF,_Cmt}}|T], Orig, Alt) -> + {Orig,[{SkipType,{Type,Ref,MF,Reason},Mode}|Alt],T}; +modify_cases_upto1(Ref, {copy,NewRef}, + [{skip_case,{Type,Ref,MF,Cmt},Mode}=C|T], Orig, Alt) -> + {[C|Orig],[{skip_case,{Type,NewRef,MF,Cmt},Mode}|Alt],T}; +modify_cases_upto1(Ref, {copy,NewRef}, + [{skip_case,{Type,Ref,MF,Cmt}}=C|T], Orig, Alt) -> + {[C|Orig],[{skip_case,{Type,NewRef,MF,Cmt}}|Alt],T}; + +%% next is a skip_case, could be one test case or 'all' in suite, we must proceed +modify_cases_upto1(Ref, ModOp, [{skip_case,{_F,_Cmt},_Mode}=MF|T], Orig, Alt) -> + modify_cases_upto1(Ref, ModOp, T, [MF|Orig], [MF|Alt]); + +%% next is a normal case (possibly in a sequence), mark as skipped, or copy, and proceed +modify_cases_upto1(Ref, {skip,Reason,_,Mode,skip_case}=Op, + [{_M,_F}=MF|T], Orig, Alt) -> + modify_cases_upto1(Ref, Op, T, Orig, [{skip_case,{MF,Reason},Mode}|Alt]); +modify_cases_upto1(Ref, {skip,Reason,_,Mode,auto_skip_case}=Op, + [{_M,_F}=MF|T], Orig, Alt) -> + modify_cases_upto1(Ref, Op, T, Orig, [{auto_skip_case,{MF,Reason},Mode}|Alt]); +modify_cases_upto1(Ref, CopyOp, [{_M,_F}=MF|T], Orig, Alt) -> + modify_cases_upto1(Ref, CopyOp, T, [MF|Orig], [MF|Alt]); + +%% next is a conf case, modify the Mode arg to keep track of sub groups +modify_cases_upto1(Ref, {skip,Reason,FType,Mode,SkipType}, + [{conf,OtherRef,Props,_MF}|T], Orig, Alt) -> + case hd(Mode) of + {OtherRef,_,_} -> % end conf + modify_cases_upto1(Ref, {skip,Reason,FType,tl(Mode),SkipType}, + T, Orig, Alt); + _ -> % start conf + Mode1 = [conf(OtherRef,Props)|Mode], + modify_cases_upto1(Ref, {skip,Reason,FType,Mode1,SkipType}, + T, Orig, Alt) + end; + +%% next is some other case, ignore or copy +modify_cases_upto1(Ref, {skip,_,_,_,_}=Op, [_Other|T], Orig, Alt) -> + modify_cases_upto1(Ref, Op, T, Orig, Alt); +modify_cases_upto1(Ref, CopyOp, [C|T], Orig, Alt) -> + modify_cases_upto1(Ref, CopyOp, T, [C|Orig], [C|Alt]). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% set_io_buffering(IOHandler) -> PrevIOHandler +%% +%% Save info about current process (always the main process) buffering +%% io printout messages from parallel test case processes (*and* possibly +%% also the main process). + +set_io_buffering(IOHandler) -> + put(test_server_common_io_handler, IOHandler). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_io_buffered() -> true|false +%% +%% Test whether is being buffered. + +is_io_buffered() -> + get(test_server_common_io_handler) =/= undefined. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% queue_test_case_io(Pid, Num, Mod, Func) -> ok +%% +%% Save info about test case that gets its io buffered. This can +%% be a parallel test case or it can be a test case (conf or normal) +%% that belongs to a group nested under a parallel group. The queue +%% is processed after io buffering is disabled. See run_test_cases_loop/4 +%% and handle_test_case_io_and_status/0 for more info. + +queue_test_case_io(Ref, Pid, Num, Mod, Func) -> + Entry = {Ref,Pid,Num,Mod,Func}, + %% the order of the test cases is very important! + put(test_server_queued_io, + get(test_server_queued_io)++[Entry]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% wait_for_cases(Ref) -> {Ok,Skipped,Failed} +%% +%% At the end of a nested parallel group, we have to wait for the test +%% cases to terminate before we can go on (since test cases never execute +%% in parallel with the end conf case of the group). When a top level +%% parallel group is finished, buffered io messages must be handled and +%% this is taken care of by handle_test_case_io_and_status/0. + +wait_for_cases(Ref) -> + case get(test_server_queued_io) of + [] -> + {[],[],[]}; + Cases -> + [_Start|TCs] = + lists:dropwhile(fun({R,_,_,_,_}) when R == Ref -> false; + (_) -> true + end, Cases), + wait_and_resend(Ref, TCs, [],[],[]) + end. + +wait_and_resend(Ref, [{OtherRef,_,0,_,_}|Ps], + Ok,Skip,Fail) when is_reference(OtherRef), + OtherRef /= Ref -> + %% ignore cases that belong to nested group + Ps1 = rm_cases_upto(OtherRef, Ps), + wait_and_resend(Ref, Ps1, Ok,Skip,Fail); + +wait_and_resend(Ref, [{_,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) -> + receive + {finished,_Ref,CurrPid,CaseNum,Mod,Func,Result,_RetVal} = Msg -> + %% resend message to main process so that it can be used + %% to test_server_io:print_buffered/1 later + self() ! Msg, + MF = {Mod,Func}, + {Ok1,Skip1,Fail1} = + case Result of + ok -> {[MF|Ok],Skip,Fail}; + skipped -> {Ok,[MF|Skip],Fail}; + failed -> {Ok,Skip,[MF|Fail]} + end, + wait_and_resend(Ref, Ps, Ok1,Skip1,Fail1); + {'EXIT',CurrPid,Reason} when Reason /= normal -> + %% unexpected termination of test case process + {value,{_,_,CaseNum,Mod,Func}} = lists:keysearch(CurrPid, 2, Cases), + print(1, "Error! Process for test case #~w (~w:~tw) died! Reason: ~tp", + [CaseNum, Mod, Func, Reason]), + exit({unexpected_termination,{CaseNum,Mod,Func},{CurrPid,Reason}}) + end; + +wait_and_resend(_, [], Ok,Skip,Fail) -> + {lists:reverse(Ok),lists:reverse(Skip),lists:reverse(Fail)}. + +rm_cases_upto(Ref, [{Ref,_,0,_,_}|Ps]) -> + Ps; +rm_cases_upto(Ref, [_|Ps]) -> + rm_cases_upto(Ref, Ps). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% handle_test_case_io_and_status() -> [Ok,Skipped,Failed} +%% +%% Each parallel test case process prints to its own minor log file during +%% execution. The common log files (major, html etc) must however be +%% written to sequentially. This is handled by calling +%% test_server_io:start_transaction/0 to tell the test_server_io process +%% to buffer all print requests. +%% +%% An io session is always started with a +%% {started,Ref,Pid,Num,Mod,Func} message (and +%% test_server_io:start_transaction/0 will be called) and terminated +%% with {finished,Ref,Pid,Num,Mod,Func,Result,RetVal} (and +%% test_server_io:end_transaction/0 will be called). The result +%% shipped with the finished message from a parallel process is used +%% to update status data of the current test run. An 'EXIT' message +%% from each parallel test case process (after finishing and +%% terminating) is also received and handled here. +%% +%% During execution of a parallel group, any cases (conf or normal) +%% belonging to a nested group will also get its io printouts buffered. +%% This is necessary to get the major and html log files written in +%% correct sequence. This function handles also the print messages +%% generated by nested group cases that have been executed sequentially +%% by the main process (note that these cases do not generate 'EXIT' +%% messages, only 'start' and 'finished' messages). +%% +%% See the header comment for run_test_cases_loop/4 for more +%% info about IO handling. +%% +%% Note: It is important that the type of messages handled here +%% do not get consumed by test_server:run_test_case_msgloop/5 +%% during the test case execution (e.g. in the catch clause of +%% the receive)! + +handle_test_case_io_and_status() -> + case get(test_server_queued_io) of + [] -> + {[],[],[]}; + Cases -> + %% Cases = [{Ref,Pid,CaseNum,Mod,Func} | ...] + Result = handle_io_and_exit_loop([], Cases, [],[],[]), + Main = self(), + %% flush normal exit messages + lists:foreach(fun({_,Pid,_,_,_}) when Pid /= Main -> + receive + {'EXIT',Pid,normal} -> ok + after + 1000 -> ok + end; + (_) -> + ok + end, Cases), + Result + end. + +%% Handle cases (without Ref) that belong to the top parallel group (i.e. when Refs = []) +handle_io_and_exit_loop([], [{undefined,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) -> + %% retrieve the start message for the current io session (= testcase) + receive + {started,_,CurrPid,CaseNum,Mod,Func} -> + {Ok1,Skip1,Fail1} = + case handle_io_and_exits(self(), CurrPid, CaseNum, Mod, Func, Cases) of + {ok,MF} -> {[MF|Ok],Skip,Fail}; + {skipped,MF} -> {Ok,[MF|Skip],Fail}; + {failed,MF} -> {Ok,Skip,[MF|Fail]} + end, + handle_io_and_exit_loop([], Ps, Ok1,Skip1,Fail1) + after + 1000 -> + exit({testcase_failed_to_start,Mod,Func}) + end; + +%% Handle cases that belong to groups nested under top parallel group +handle_io_and_exit_loop(Refs, [{Ref,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) -> + receive + {started,_,CurrPid,CaseNum,Mod,Func} -> + _ = handle_io_and_exits(self(), CurrPid, CaseNum, Mod, Func, Cases), + Refs1 = + case Refs of + [Ref|Rs] -> % must be end conf case for subgroup + Rs; + _ when is_reference(Ref) -> % must be start of new subgroup + [Ref|Refs]; + _ -> % must be normal subgroup testcase + Refs + end, + handle_io_and_exit_loop(Refs1, Ps, Ok,Skip,Fail) + after + 1000 -> + exit({testcase_failed_to_start,Mod,Func}) + end; + +handle_io_and_exit_loop(_, [], Ok,Skip,Fail) -> + {lists:reverse(Ok),lists:reverse(Skip),lists:reverse(Fail)}. + +handle_io_and_exits(Main, CurrPid, CaseNum, Mod, Func, Cases) -> + receive + {abort_current_testcase=Tag,_Reason,From} -> + %% If a parallel group is executing, there is no unique + %% current test case, so we must generate an error. + From ! {self(),Tag,{error,parallel_group}}, + handle_io_and_exits(Main, CurrPid, CaseNum, Mod, Func, Cases); + %% end of io session from test case executed by main process + {finished,_,Main,CaseNum,Mod,Func,Result,_RetVal} -> + test_server_io:print_buffered(CurrPid), + {Result,{Mod,Func}}; + %% end of io session from test case executed by parallel process + {finished,_,CurrPid,CaseNum,Mod,Func,Result,RetVal} -> + test_server_io:print_buffered(CurrPid), + case Result of + ok -> + put(test_server_ok, get(test_server_ok)+1); + failed -> + put(test_server_failed, get(test_server_failed)+1); + skipped -> + SkipCounters = + update_skip_counters(RetVal, get(test_server_skipped)), + put(test_server_skipped, SkipCounters) + end, + {Result,{Mod,Func}}; + + %% unexpected termination of test case process + {'EXIT',TCPid,Reason} when Reason /= normal -> + test_server_io:print_buffered(CurrPid), + {value,{_,_,Num,M,F}} = lists:keysearch(TCPid, 2, Cases), + print(1, "Error! Process for test case #~w (~w:~tw) died! Reason: ~tp", + [Num, M, F, Reason]), + exit({unexpected_termination,{Num,M,F},{TCPid,Reason}}) + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% run_test_case(Ref, Num, Mod, Func, Args, RunInit, +%% TimetrapData, Mode) -> RetVal +%% +%% Creates the minor log file and inserts some test case specific headers +%% and footers into the log files. Then the test case is executed and the +%% result is printed to the log files (also info about lingering processes +%% & slave nodes in the system is presented). +%% +%% RunInit decides if the per test case init is to be run (true for all +%% but conf cases). +%% +%% Mode specifies if the test case should be executed by a dedicated, +%% parallel, process rather than sequentially by the main process. If +%% the former, the new process is spawned and the dictionary of the main +%% process is copied to the test case process. +%% +%% RetVal is the result of executing the test case. It contains info +%% about the execution time and the return value of the test case function. + +run_test_case(Ref, Num, Mod, Func, Args, RunInit, TimetrapData) -> + ok = file:set_cwd(filename:dirname(get(test_server_dir))), + run_test_case1(Ref, Num, Mod, Func, Args, RunInit, + TimetrapData, [], self()). + +run_test_case(Ref, Num, Mod, Func, Args, skip_init, TimetrapData, Mode) -> + %% a conf case is always executed by the main process + run_test_case1(Ref, Num, Mod, Func, Args, skip_init, + TimetrapData, Mode, self()); + +run_test_case(Ref, Num, Mod, Func, Args, RunInit, TimetrapData, Mode) -> + ok = file:set_cwd(filename:dirname(get(test_server_dir))), + Main = self(), + case check_prop(parallel, Mode) of + false -> + %% this is a sequential test case + run_test_case1(Ref, Num, Mod, Func, Args, RunInit, + TimetrapData, Mode, Main); + _Ref -> + %% this a parallel test case, spawn the new process + Dictionary = get(), + {dictionary,Dictionary} = process_info(self(), dictionary), + spawn_link( + fun() -> + process_flag(trap_exit, true), + ct_util:mark_process(), + _ = [put(Key, Val) || {Key,Val} <- Dictionary], + set_io_buffering({tc,Main}), + run_test_case1(Ref, Num, Mod, Func, Args, RunInit, + TimetrapData, Mode, Main) + end) + end. + +run_test_case1(Ref, Num, Mod, Func, Args, RunInit, + TimetrapData, Mode, Main) -> + group_leader(test_server_io:get_gl(Main == self()), self()), + + %% if io is being buffered, send start io session message + %% (no matter if case runs on parallel or main process) + case is_io_buffered() of + false -> ok; + true -> + test_server_io:start_transaction(), + Main ! {started,Ref,self(),Num,Mod,Func}, + ok + end, + TSDir = get(test_server_dir), + + print(major, "=case ~w:~tw", [Mod, Func]), + MinorName = start_minor_log_file(Mod, Func, self() /= Main), + MinorBase = filename:basename(MinorName), + print(major, "=logfile ~ts", [filename:basename(MinorName)]), + + UpdatedArgs = + %% maybe create unique private directory for test case or config func + case get(test_server_create_priv_dir) of + auto_per_run -> + update_config(hd(Args), [{tc_logfile,MinorName}]); + PrivDirMode -> + %% create unique private directory for test case + RunDir = filename:dirname(MinorName), + Ext = + if Num == 0 -> + Int = erlang:unique_integer([positive,monotonic]), + lists:flatten(io_lib:format(".cfg.~w", [Int])); + true -> + lists:flatten(io_lib:format(".~w", [Num])) + end, + PrivDir = filename:join(RunDir, ?priv_dir) ++ Ext, + if PrivDirMode == auto_per_tc -> + ok = file:make_dir(PrivDir); + PrivDirMode == manual_per_tc -> + ok + end, + update_config(hd(Args), [{priv_dir,PrivDir++"/"}, + {tc_logfile,MinorName}]) + end, + GrName = get_name(Mode), + test_server_sup:framework_call(report, + [tc_start,{{Mod,{Func,GrName}}, + MinorName}]), + + {ok,Cwd} = file:get_cwd(), + Args2Print = if is_list(UpdatedArgs) -> + lists:keydelete(tc_group_result, 1, UpdatedArgs); + true -> + UpdatedArgs + end, + if RunInit == skip_init -> + print_props(get_props(Mode)); + true -> + ok + end, + + print(minor, + escape_chars(io_lib:format("Config value:\n\n ~tp\n", [Args2Print])), + []), + print(minor, "Current directory is ~tp\n", [Cwd]), + + GrNameStr = case GrName of + undefined -> ""; + Name -> cast_to_list(Name) + end, + print(major, "=started ~s", [lists:flatten(timestamp_get(""))]), + {{Col0,Col1},Style} = get_font_style((RunInit==run_init), Mode), + TR = xhtml("<tr valign=\"top\">", ["<tr class=\"",odd_or_even(),"\">"]), + EncMinorBase = uri_encode(MinorBase), + print(html, TR ++ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" + "<td>" ++ Col0 ++ "~w" ++ Col1 ++ "</td>" + "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" + "<td><a href=\"~ts\">~tw</a></td>" + "<td><a href=\"~ts#top\"><</a> <a href=\"~ts#end\">></a></td>", + [num2str(Num),fw_name(Mod),GrNameStr,EncMinorBase,Func, + EncMinorBase,EncMinorBase]), + + do_unless_parallel(Main, fun erlang:yield/0), + + %% run the test case + {Result,DetectedFail,ProcsBefore,ProcsAfter} = + run_test_case_apply(Num, Mod, Func, [UpdatedArgs], GrName, + RunInit, TimetrapData), + {Time,RetVal,Loc,Opts,Comment} = + case Result of + Normal={_Time,_RetVal,_Loc,_Opts,_Comment} -> Normal; + {died,DReason,DLoc,DCmt} -> {died,DReason,DLoc,[],DCmt} + end, + + print(minor, "<a name=\"end\"></a>", [], internal_raw), + print(minor, "\n", [], internal_raw), + print_timestamp(minor, "Ended at "), + print(major, "=ended ~s", [lists:flatten(timestamp_get(""))]), + + do_unless_parallel(Main, fun() -> file:set_cwd(filename:dirname(TSDir)) end), + + %% call the appropriate progress function clause to print the results to log + Status = + case {Time,RetVal} of + {died,{timetrap_timeout,TimetrapTimeout}} -> + progress(failed, Num, Mod, Func, GrName, Loc, + timetrap_timeout, TimetrapTimeout, Comment, Style); + {died,{Skip,Reason}} when Skip==skip; Skip==skipped -> + %% died in init_per_testcase + progress(skip, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {died,Reason} when Reason=/=ok -> + %% (If Reason==ok it means that process died in + %% end_per_testcase after successfully completing the + %% test case itself - then we shall not fail, but a + %% warning will be issued in the comment field.) + progress(failed, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {_,{'EXIT',{Skip,Reason}}} when Skip==skip; Skip==skipped; + Skip==auto_skip -> + progress(skip, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {_,{'EXIT',_Pid,{Skip,Reason}}} when Skip==skip; Skip==skipped -> + progress(skip, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {_,{'EXIT',_Pid,Reason}} -> + progress(failed, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {_,{'EXIT',Reason}} -> + progress(failed, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {_,{Fail,Reason}} when Fail =:= fail; Fail =:= failed -> + progress(failed, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {_,Reason={auto_skip,_Why}} -> + progress(skip, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {_,{Skip,Reason}} when Skip==skip; Skip==skipped -> + progress(skip, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style); + {Time,RetVal} -> + case DetectedFail of + [] -> + progress(ok, Num, Mod, Func, GrName, Loc, RetVal, + Time, Comment, Style); + + Reason -> + progress(failed, Num, Mod, Func, GrName, Loc, Reason, + Time, Comment, Style) + end + end, + %% if the test case was executed sequentially, this updates the + %% status count on the main process (status of parallel test cases + %% is updated later by the handle_test_case_io_and_status/0 function) + case {RunInit,Status} of + {skip_init,_} -> % conf doesn't count + ok; + {_,ok} -> + put(test_server_ok, get(test_server_ok)+1); + {_,failed} -> + put(test_server_failed, get(test_server_failed)+1); + {_,skip} -> + {US,AS} = get(test_server_skipped), + put(test_server_skipped, {US+1,AS}); + {_,auto_skip} -> + {US,AS} = get(test_server_skipped), + put(test_server_skipped, {US,AS+1}) + end, + %% only if test case execution is sequential do we care about the + %% remaining processes and slave nodes count + case self() of + Main -> + case test_server_sup:framework_call(warn, [processes], true) of + true -> + if ProcsBefore < ProcsAfter -> + print(minor, + "WARNING: ~w more processes in system after test case", + [ProcsAfter-ProcsBefore]); + ProcsBefore > ProcsAfter -> + print(minor, + "WARNING: ~w less processes in system after test case", + [ProcsBefore-ProcsAfter]); + true -> ok + end; + false -> + ok + end, + case test_server_sup:framework_call(warn, [nodes], true) of + true -> + case catch controller_call(kill_slavenodes) of + {'EXIT',_} = Exit -> + print(minor, + "WARNING: There might be slavenodes left in the" + " system. I tried to kill them, but I failed: ~tp\n", + [Exit]); + [] -> ok; + List -> + print(minor, "WARNING: ~w slave nodes in system after test"++ + "case. Tried to killed them.~n"++ + " Names:~tp", + [length(List),List]) + end; + false -> + ok + end; + _ -> + ok + end, + %% if the test case was executed sequentially, this updates the execution + %% time count on the main process (adding execution time of parallel test + %% case groups is done in run_test_cases_loop/4) + if is_number(Time) -> + put(test_server_total_time, get(test_server_total_time)+Time); + true -> + ok + end, + test_server_sup:check_new_crash_dumps(), + + %% if io is being buffered, send finished message + %% (no matter if case runs on parallel or main process) + case is_io_buffered() of + false -> + ok; + true -> + test_server_io:end_transaction(), + Main ! {finished,Ref,self(),Num,Mod,Func, + ?mod_result(Status),{Time,RetVal,Opts}}, + ok + end, + {Time,RetVal,Opts}. + + +%%-------------------------------------------------------------------- +%% various help functions + +%% Call Action if we are running on the main process (not parallel). +do_unless_parallel(Main, Action) when is_function(Action, 0) -> + case self() of + Main -> Action(); + _ -> ok + end. + +num2str(0) -> ""; +num2str(N) -> integer_to_list(N). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% progress(Result, CaseNum, Mod, Func, Location, Reason, Time, +%% Comment, TimeFormat) -> Result +%% +%% Prints the result of the test case to log file. +%% Note: Strings that are to be written to the minor log must +%% be prefixed with "=== " here, or the indentation will be wrong. + +progress(skip, CaseNum, Mod, Func, GrName, Loc, Reason, Time, + Comment, {St0,St1}) -> + {Reason1,{Color,Ret,ReportTag}} = + if_auto_skip(Reason, + fun() -> {?auto_skip_color,auto_skip,auto_skipped} end, + fun() -> {?user_skip_color,skip,skipped} end), + print(major, "=result ~w: ~tp", [ReportTag,Reason1]), + print(1, "*** SKIPPED ~ts ***", + [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), + test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName}, + {ReportTag,Reason1}}]), + TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; + true -> "~w" + end, [Time]), + ReasonStr = escape_chars(reason_to_string(Reason1)), + ReasonStr1 = lists:flatten([string:strip(S,left) || + S <- string:tokens(ReasonStr,[$\n])]), + ReasonStr2 = + if length(ReasonStr1) > 80 -> + string:substr(ReasonStr1, 1, 77) ++ "..."; + true -> + ReasonStr1 + end, + Comment1 = case Comment of + "" -> ""; + _ -> xhtml("<br>(","<br />(") ++ to_string(Comment) ++ ")" + end, + print(html, + "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" + "<td><font color=\"~ts\">SKIPPED</font></td>" + "<td>~ts~ts</td></tr>\n", + [TimeStr,Color,ReasonStr2,Comment1]), + FormatLoc = test_server_sup:format_loc(Loc), + print(minor, "=== Location: ~ts", [FormatLoc]), + print(minor, "=== Reason: ~ts", [ReasonStr1]), + Ret; + +progress(failed, CaseNum, Mod, Func, GrName, Loc, timetrap_timeout, T, + Comment0, {St0,St1}) -> + print(major, "=result failed: timeout, ~tp", [Loc]), + print(1, "*** FAILED ~ts ***", + [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), + test_server_sup:framework_call(report, + [tc_done,{Mod,{Func,GrName}, + {failed,timetrap_timeout}}]), + FormatLastLoc = test_server_sup:format_loc(get_last_loc(Loc)), + ErrorReason = io_lib:format("{timetrap_timeout,~ts}", [FormatLastLoc]), + Comment = + case Comment0 of + "" -> "<font color=\"red\">" ++ ErrorReason ++ "</font>"; + _ -> "<font color=\"red\">" ++ ErrorReason ++ + xhtml("</font><br>","</font><br />") ++ to_string(Comment0) + end, + print(html, + "<td>" ++ St0 ++ "~.3fs" ++ St1 ++ "</td>" + "<td><font color=\"red\">FAILED</font></td>" + "<td>~ts</td></tr>\n", + [T/1000,Comment]), + FormatLoc = test_server_sup:format_loc(Loc), + print(minor, "=== Location: ~ts", [FormatLoc]), + print(minor, "=== Reason: timetrap timeout", []), + failed; + +progress(failed, CaseNum, Mod, Func, GrName, Loc, {testcase_aborted,Reason}, _T, + Comment0, {St0,St1}) -> + print(major, "=result failed: testcase_aborted, ~tp", [Loc]), + print(1, "*** FAILED ~ts ***", + [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), + test_server_sup:framework_call(report, + [tc_done,{Mod,{Func,GrName}, + {failed,testcase_aborted}}]), + FormatLastLoc = test_server_sup:format_loc(get_last_loc(Loc)), + ErrorReason = io_lib:format("{testcase_aborted,~ts}", [FormatLastLoc]), + Comment = + case Comment0 of + "" -> "<font color=\"red\">" ++ ErrorReason ++ "</font>"; + _ -> "<font color=\"red\">" ++ ErrorReason ++ + xhtml("</font><br>","</font><br />") ++ to_string(Comment0) + end, + print(html, + "<td>" ++ St0 ++ "died" ++ St1 ++ "</td>" + "<td><font color=\"red\">FAILED</font></td>" + "<td>~ts</td></tr>\n", + [Comment]), + FormatLoc = test_server_sup:format_loc(Loc), + print(minor, "=== Location: ~ts", [FormatLoc]), + print(minor, + escape_chars(io_lib:format("=== Reason: {testcase_aborted,~tp}", + [Reason])), + []), + failed; + +progress(failed, CaseNum, Mod, Func, GrName, unknown, Reason, Time, + Comment0, {St0,St1}) -> + print(major, "=result failed: ~tp, ~w", [Reason,unknown_location]), + print(1, "*** FAILED ~ts ***", + [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), + test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName}, + {failed,Reason}}]), + TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; + true -> "~w" + end, [Time]), + ErrorReason = escape_chars(lists:flatten(io_lib:format("~tp", [Reason]))), + ErrorReason1 = lists:flatten([string:strip(S,left) || + S <- string:tokens(ErrorReason,[$\n])]), + ErrorReason2 = + if length(ErrorReason1) > 63 -> + string:substr(ErrorReason1, 1, 60) ++ "..."; + true -> + ErrorReason1 + end, + Comment = + case Comment0 of + "" -> "<font color=\"red\">" ++ ErrorReason2 ++ "</font>"; + _ -> "<font color=\"red\">" ++ ErrorReason2 ++ + xhtml("</font><br>","</font><br />") ++ + to_string(Comment0) + end, + print(html, + "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" + "<td><font color=\"red\">FAILED</font></td>" + "<td>~ts</td></tr>\n", + [TimeStr,Comment]), + print(minor, "=== Location: ~w", [unknown]), + {FStr,FormattedReason} = format_exception(Reason), + print(minor, + escape_chars(io_lib:format("=== Reason: " ++ FStr, [FormattedReason])), + []), + failed; + +progress(failed, CaseNum, Mod, Func, GrName, Loc, Reason, Time, + Comment0, {St0,St1}) -> + {LocMaj,LocMin} = if Func == error_in_suite -> + case get_fw_mod(undefined) of + Mod -> {unknown_location,unknown}; + _ -> {Loc,Loc} + end; + true -> {Loc,Loc} + end, + print(major, "=result failed: ~tp, ~tp", [Reason,LocMaj]), + print(1, "*** FAILED ~ts ***", + [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), + test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName}, + {failed,Reason}}]), + TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; + true -> "~w" + end, [Time]), + Comment = + case Comment0 of + "" -> ""; + _ -> xhtml("<br>","<br />") ++ to_string(Comment0) + end, + FormatLastLoc = test_server_sup:format_loc(get_last_loc(LocMaj)), + print(html, + "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" + "<td><font color=\"red\">FAILED</font></td>" + "<td><font color=\"red\">~ts</font>~ts</td></tr>\n", + [TimeStr,FormatLastLoc,Comment]), + FormatLoc = test_server_sup:format_loc(LocMin), + print(minor, "=== Location: ~ts", [FormatLoc]), + {FStr,FormattedReason} = format_exception(Reason), + print(minor, "=== Reason: " ++ + escape_chars(io_lib:format(FStr, [FormattedReason])), []), + failed; + +progress(ok, _CaseNum, Mod, Func, GrName, _Loc, RetVal, Time, + Comment0, {St0,St1}) -> + print(minor, "successfully completed test case", []), + test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName},ok}]), + TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; + true -> "~w" + end, [Time]), + Comment = + case RetVal of + {comment,RetComment} -> + String = to_string(RetComment), + HtmlCmt = test_server_sup:framework_call(format_comment, + [String], + String), + print(major, "=result ok: ~ts", [String]), + "<td>" ++ HtmlCmt ++ "</td>"; + _ -> + print(major, "=result ok", []), + case Comment0 of + "" -> "<td></td>"; + _ -> "<td>" ++ to_string(Comment0) ++ "</td>" + end + end, + print(major, "=elapsed ~p", [Time]), + print(html, + "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" + "<td><font color=\"green\">Ok</font></td>" + "~ts</tr>\n", + [TimeStr,Comment]), + print(minor, + escape_chars(io_lib:format("=== Returned value: ~tp", [RetVal])), + []), + ok. + +%%-------------------------------------------------------------------- +%% various help functions +escape_chars(Term) when not is_list(Term), not is_binary(Term) -> + esc_chars_in_list(io_lib:format("~tp", [Term])); +escape_chars(List = [Term | _]) when not is_list(Term), not is_integer(Term) -> + esc_chars_in_list(io_lib:format("~tp", [List])); +escape_chars(List) -> + esc_chars_in_list(List). + +esc_chars_in_list([Bin | Io]) when is_binary(Bin) -> + [Bin | esc_chars_in_list(Io)]; +esc_chars_in_list([List | Io]) when is_list(List) -> + [esc_chars_in_list(List) | esc_chars_in_list(Io)]; +esc_chars_in_list([$< | Io]) -> + ["<" | esc_chars_in_list(Io)]; +esc_chars_in_list([$> | Io]) -> + [">" | esc_chars_in_list(Io)]; +esc_chars_in_list([$& | Io]) -> + ["&" | esc_chars_in_list(Io)]; +esc_chars_in_list([Char | Io]) when is_integer(Char) -> + [Char | esc_chars_in_list(Io)]; +esc_chars_in_list([]) -> + []; +esc_chars_in_list(Bin) -> + Bin. + +get_fw_mod(Mod) -> + case get(test_server_framework) of + undefined -> + case os:getenv("TEST_SERVER_FRAMEWORK") of + FW when FW =:= false; FW =:= "undefined" -> + Mod; + FW -> + list_to_atom(FW) + end; + '$none' -> Mod; + FW -> FW + end. + +fw_name(?MODULE) -> + test_server; +fw_name(Mod) -> + case get(test_server_framework_name) of + undefined -> + case get_fw_mod(undefined) of + undefined -> + Mod; + Mod -> + case os:getenv("TEST_SERVER_FRAMEWORK_NAME") of + FWName when FWName =:= false; FWName =:= "undefined" -> + Mod; + FWName -> + list_to_atom(FWName) + end; + _ -> + Mod + end; + '$none' -> + Mod; + FWName -> + case get_fw_mod(Mod) of + Mod -> FWName; + _ -> Mod + end + end. + +if_auto_skip(Reason={failed,{_,init_per_testcase,_}}, True, _False) -> + {Reason,True()}; +if_auto_skip({skip,Reason={failed,{_,init_per_testcase,_}}}, True, _False) -> + {Reason,True()}; +if_auto_skip({auto_skip,Reason}, True, _False) -> + {Reason,True()}; +if_auto_skip(Reason, _True, False) -> + {Reason,False()}. + +update_skip_counters({_T,Pat,_Opts}, {US,AS}) -> + {_,Result} = if_auto_skip(Pat, fun() -> {US,AS+1} end, fun() -> {US+1,AS} end), + Result; +update_skip_counters(Pat, {US,AS}) -> + {_,Result} = if_auto_skip(Pat, fun() -> {US,AS+1} end, fun() -> {US+1,AS} end), + Result. + +get_info_str(Mod,Func, 0, _Cases) -> + io_lib:format("~tw", [{Mod,Func}]); +get_info_str(_Mod,_Func, CaseNum, unknown) -> + "test case " ++ integer_to_list(CaseNum); +get_info_str(_Mod,_Func, CaseNum, Cases) -> + "test case " ++ integer_to_list(CaseNum) ++ + " of " ++ integer_to_list(Cases). + +print_if_known(Known, {SK,AK}, {SU,AU}) -> + {S,A} = if Known == unknown -> {SU,AU}; + true -> {SK,AK} + end, + io_lib:format(S, A). + +to_string(Term) when is_list(Term) -> + case (catch io_lib:format("~ts", [Term])) of + {'EXIT',_} -> lists:flatten(io_lib:format("~tp", [Term])); + String -> lists:flatten(String) + end; +to_string(Term) -> + lists:flatten(io_lib:format("~tp", [Term])). + +get_last_loc(Loc) when is_tuple(Loc) -> + Loc; +get_last_loc([Loc|_]) when is_tuple(Loc) -> + [Loc]; +get_last_loc(Loc) -> + Loc. + +reason_to_string({failed,{_,FailFunc,bad_return}}) -> + atom_to_list(FailFunc) ++ " bad return value"; +reason_to_string({failed,{_,FailFunc,{timetrap_timeout,_}}}) -> + atom_to_list(FailFunc) ++ " timed out"; +reason_to_string(FWInitFail = {failed,{_CB,init_tc,_Reason}}) -> + to_string(FWInitFail); +reason_to_string({failed,{_,FailFunc,_}}) -> + atom_to_list(FailFunc) ++ " failed"; +reason_to_string(Other) -> + to_string(Other). + +%get_font_style(Prop) -> +% {Col,St0,St1} = get_font_style1(Prop), +% {{"<font color="++Col++">","</font>"}, +% {"<font color="++Col++">"++St0,St1++"</font>"}}. + +get_font_style(NormalCase, Mode) -> + Prop = if not NormalCase -> + default; + true -> + case check_prop(parallel, Mode) of + false -> + case check_prop(sequence, Mode) of + false -> + default; + _ -> + sequence + end; + _ -> + parallel + end + end, + {Col,St0,St1} = get_font_style1(Prop), + {{"<font color="++Col++">","</font>"}, + {"<font color="++Col++">"++St0,St1++"</font>"}}. + +get_font_style1(parallel) -> + {"\"darkslategray\"","<i>","</i>"}; +get_font_style1(sequence) -> +% {"\"darkolivegreen\"","",""}; + {"\"saddlebrown\"","",""}; +get_font_style1(default) -> + {"\"black\"","",""}. +%%get_font_style1(skipped) -> +%% {"\"lightgray\"","",""}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% format_exception({Error,Stack}) -> {CtrlSeq,Term} +%% +%% The default behaviour is that error information gets formatted +%% (like in the erlang shell) before printed to the minor log file. +%% The framework application can switch this feature off by setting +%% *its* application environment variable 'format_exception' to false. +%% It is also possible to switch formatting off by starting the +%% test_server node with init argument 'test_server_format_exception' +%% set to false. + +format_exception(Reason={_Error,Stack}) when is_list(Stack) -> + case get_fw_mod(undefined) of + undefined -> + case application:get_env(test_server, format_exception) of + {ok,false} -> + {"~tp",Reason}; + _ -> + do_format_exception(Reason) + end; + FW -> + case application:get_env(FW, format_exception) of + {ok,false} -> + {"~tp",Reason}; + _ -> + do_format_exception(Reason) + end + end; +format_exception(Error) -> + format_exception({Error,[]}). + +do_format_exception(Reason={Error,Stack}) -> + StackFun = fun(_, _, _) -> false end, + PF = fun(Term, I) -> + io_lib:format("~." ++ integer_to_list(I) ++ "tp", [Term]) + end, + case catch lib:format_exception(1, error, Error, Stack, StackFun, PF, utf8) of + {'EXIT',_R} -> + {"~tp",Reason}; + Formatted -> + Formatted1 = re:replace(Formatted, "exception error: ", "", [{return,list},unicode]), + {"~ts",lists:flatten(Formatted1)} + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% run_test_case_apply(CaseNum, Mod, Func, Args, Name, RunInit, +%% TimetrapData) -> +%% {{Time,RetVal,Loc,Opts,Comment},DetectedFail,ProcessesBefore,ProcessesAfter} | +%% {{died,Reason,unknown,Comment},DetectedFail,ProcessesBefore,ProcessesAfter} +%% Name = atom() +%% Time = float() (seconds) +%% RetVal = term() +%% Loc = term() +%% Comment = string() +%% Reason = term() +%% DetectedFail = [{File,Line}] +%% ProcessesBefore = ProcessesAfter = integer() +%% + +run_test_case_apply(CaseNum, Mod, Func, Args, Name, RunInit, + TimetrapData) -> + test_server:run_test_case_apply({CaseNum,Mod,Func,Args,Name,RunInit, + TimetrapData}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% print(Detail, Format, Args) -> ok +%% Detail = integer() +%% Format = string() +%% Args = [term()] +%% +%% Just like io:format, except that depending on the Detail value, the output +%% is directed to console, major and/or minor log files. + +print(Detail, Format) -> + print(Detail, Format, []). + +print(Detail, Format, Args) -> + print(Detail, Format, Args, internal). + +print(Detail, ["$tc_html",Format], Args, Printer) -> + Msg = io_lib:format(Format, Args), + print_or_buffer(Detail, ["$tc_html",Msg], Printer); + +print(Detail, Format, Args, Printer) -> + Msg = io_lib:format(Format, Args), + print_or_buffer(Detail, Msg, Printer). + +print_or_buffer(Detail, Msg, Printer) -> + test_server_gl:print(group_leader(), Detail, Msg, Printer). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% print_timestamp(Detail, Leader) -> ok +%% +%% Prints Leader followed by a time stamp (date and time). Depending on +%% the Detail value, the output is directed to console, major and/or minor +%% log files. + +print_timestamp(Detail, Leader) -> + print(Detail, timestamp_get(Leader), []). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% print_who(Host, User) -> ok +%% +%% Logs who runs the suite. + +print_who(Host, User) -> + UserStr = case User of + "" -> ""; + _ -> " by " ++ User + end, + print(html, "Run~ts on ~ts", [UserStr,Host]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% format(Format) -> IoLibReturn +%% format(Detail, Format) -> IoLibReturn +%% format(Format, Args) -> IoLibReturn +%% format(Detail, Format, Args) -> IoLibReturn +%% +%% Detail = integer() +%% Format = string() +%% Args = [term(),...] +%% IoLibReturn = term() +%% +%% Logs the Format string and Args, similar to io:format/1/2 etc. If +%% Detail is not specified, the default detail level (which is 50) is used. +%% Which log files the string will be logged in depends on the thresholds +%% set with set_levels/3. Typically with default detail level, only the +%% minor log file is used. + +format(Format) -> + format(minor, Format, []). + +format(major, Format) -> + format(major, Format, []); +format(minor, Format) -> + format(minor, Format, []); +format(Detail, Format) when is_integer(Detail) -> + format(Detail, Format, []); +format(Format, Args) -> + format(minor, Format, Args). + +format(Detail, Format, Args) -> + Str = + case catch io_lib:format(Format, Args) of + {'EXIT',_} -> + io_lib:format("illegal format; ~tp with args ~tp.\n", + [Format,Args]); + Valid -> Valid + end, + print_or_buffer(Detail, Str, self()). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% xhtml(BasicHtml, XHtml) -> BasicHtml | XHtml +%% +xhtml(HTML, XHTML) -> + case get(basic_html) of + true -> HTML; + _ -> XHTML + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% odd_or_even() -> "odd" | "even" +%% +odd_or_even() -> + case get(odd_or_even) of + even -> + put(odd_or_even, odd), + "even"; + _ -> + put(odd_or_even, even), + "odd" + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timestamp_filename_get(Leader) -> string() +%% Leader = string() +%% +%% Returns a string consisting of Leader concatenated with the current +%% date and time. The resulting string is suitable as a filename. +timestamp_filename_get(Leader) -> + timestamp_get_internal(Leader, + "~ts~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w"). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timestamp_get(Leader) -> string() +%% Leader = string() +%% +%% Returns a string consisting of Leader concatenated with the current +%% date and time. The resulting string is suitable for display. +timestamp_get(Leader) -> + timestamp_get_internal(Leader, + "~ts~w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w"). + +timestamp_get_internal(Leader, Format) -> + {YY,MM,DD,H,M,S} = time_get(), + io_lib:format(Format, [Leader,YY,MM,DD,H,M,S]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% time_get() -> {YY,MM,DD,H,M,S} +%% YY = integer() +%% MM = integer() +%% DD = integer() +%% H = integer() +%% M = integer() +%% S = integer() +%% +%% Returns the current Year,Month,Day,Hours,Minutes,Seconds. +%% The function checks that the date doesn't wrap while calling +%% getting the time. +time_get() -> + {YY,MM,DD} = date(), + {H,M,S} = time(), + case date() of + {YY,MM,DD} -> + {YY,MM,DD,H,M,S}; + _NewDay -> + %% date changed between call to date() and time(), try again + time_get() + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% make_config(Config) -> NewConfig +%% Config = [{Key,Value},...] +%% NewConfig = [{Key,Value},...] +%% +%% Creates a configuration list (currently returns it's input) + +make_config(Initial) -> + Initial. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% update_config(Config, Update) -> NewConfig +%% Config = [{Key,Value},...] +%% Update = [{Key,Value},...] | {Key,Value} +%% NewConfig = [{Key,Value},...] +%% +%% Adds or replaces the key-value pairs in config with those in update. +%% Returns the updated list. + +update_config(Config, {Key,Val}) -> + case lists:keymember(Key, 1, Config) of + true -> + lists:keyreplace(Key, 1, Config, {Key,Val}); + false -> + [{Key,Val}|Config] + end; +update_config(Config, [Assoc|Assocs]) -> + NewConfig = update_config(Config, Assoc), + update_config(NewConfig, Assocs); +update_config(Config, []) -> + Config. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% collect_cases(CurMod, TopCase, SkipList) -> +%% BasicCaseList | {error,Reason} +%% +%% CurMod = atom() +%% TopCase = term() +%% SkipList = [term(),...] +%% BasicCaseList = [term(),...] +%% +%% Parses the given test goal(s) in TopCase, and transforms them to a +%% simple list of test cases to call, when executing the test suite. +%% +%% CurMod is the "current" module, that is, the module the last instruction +%% was read from. May be be set to 'none' initially. +%% +%% SkipList is the list of test cases to skip and requirements to deny. +%% +%% The BasicCaseList is built out of TopCase, which may be any of the +%% following terms: +%% +%% [] Nothing is added +%% List list() The list is decomposed, and each element is +%% treated according to this table +%% Case atom() CurMod:Case(suite) is called +%% {module,Case} CurMod:Case(suite) is called +%% {Module,Case} Module:Case(suite) is called +%% {module,Module,Case} Module:Case(suite) is called +%% {module,Module,Case,Args} Module:Case is called with Args as arguments +%% {dir,Dir} All modules *_SUITE in the named directory +%% are listed, and each Module:all(suite) is called +%% {dir,Dir,Pattern} All modules <Pattern>_SUITE in the named dir +%% are listed, and each Module:all(suite) is called +%% {conf,InitMF,Cases,FinMF} +%% {conf,Props,InitMF,Cases,FinMF} +%% InitMF is placed in the BasicCaseList, then +%% Cases is treated according to this table, then +%% FinMF is placed in the BasicCaseList. InitMF +%% and FinMF are configuration manipulation +%% functions. See below. +%% {make,InitMFA,Cases,FinMFA} +%% InitMFA is placed in the BasicCaseList, then +%% Cases is treated according to this table, then +%% FinMFA is placed in the BasicCaseList. InitMFA +%% and FinMFA are make/unmake functions. If InitMFA +%% fails, Cases are not run. +%% +%% When a function is called, above, it means that the function is invoked +%% and the return is expected to be: +%% +%% [] Leaf case +%% {req,ReqList} Kept for backwards compatibility - same as [] +%% {req,ReqList,Cases} Kept for backwards compatibility - +%% Cases parsed recursively with collect_cases/3 +%% Cases (list) Recursively parsed with collect_cases/3 +%% +%% Leaf cases are added to the BasicCaseList as Module:Case(Config). Each +%% case is checked against the SkipList. If present, a skip instruction +%% is inserted instead, which only prints the case name and the reason +%% why the case was skipped in the log files. +%% +%% Configuration manipulation functions are called with the current +%% configuration list as only argument, and are expected to return a new +%% configuration list. Such a pair of function may, for example, start a +%% server and stop it after a serie of test cases. +%% +%% SkipCases is expected to be in the format: +%% +%% Other Recursively parsed with collect_cases/3 +%% {Mod,Comment} Skip Mod, with Comment +%% {Mod,Funcs,Comment} Skip listed functions in Mod with Comment +%% {Mod,Func,Comment} Skip named function in Mod with Comment +%% +-record(cc, {mod, % current module + skip}). % skip list + +collect_all_cases(Top, Skip) when is_list(Skip) -> + Result = + case collect_cases(Top, #cc{mod=[],skip=Skip}, []) of + {ok,Cases,_St} -> Cases; + Other -> Other + end, + Result. + + +collect_cases([], St, _) -> {ok,[],St}; +collect_cases([Case|Cs0], St0, Mode) -> + case collect_cases(Case, St0, Mode) of + {ok,FlatCases1,St1} -> + case collect_cases(Cs0, St1, Mode) of + {ok,FlatCases2,St} -> + {ok,FlatCases1 ++ FlatCases2,St}; + {error,_Reason} = Error -> Error + end; + {error,_Reason} = Error -> Error + end; + + +collect_cases({module,Case}, St, Mode) when is_atom(Case), is_atom(St#cc.mod) -> + collect_case({St#cc.mod,Case}, St, Mode); +collect_cases({module,Mod,Case}, St, Mode) -> + collect_case({Mod,Case}, St, Mode); +collect_cases({module,Mod,Case,Args}, St, Mode) -> + collect_case({Mod,Case,Args}, St, Mode); + +collect_cases({dir,SubDir}, St, Mode) -> + collect_files(SubDir, "*_SUITE", St, Mode); +collect_cases({dir,SubDir,Pattern}, St, Mode) -> + collect_files(SubDir, Pattern++"*", St, Mode); + +collect_cases({conf,InitF,CaseList,FinMF}, St, Mode) when is_atom(InitF) -> + collect_cases({conf,[],{St#cc.mod,InitF},CaseList,FinMF}, St, Mode); +collect_cases({conf,InitMF,CaseList,FinF}, St, Mode) when is_atom(FinF) -> + collect_cases({conf,[],InitMF,CaseList,{St#cc.mod,FinF}}, St, Mode); +collect_cases({conf,InitMF,CaseList,FinMF}, St0, Mode) -> + collect_cases({conf,[],InitMF,CaseList,FinMF}, St0, Mode); +collect_cases({conf,Props,InitF,CaseList,FinMF}, St, Mode) when is_atom(InitF) -> + case init_props(Props) of + {error,_} -> + {ok,[],St}; + Props1 -> + collect_cases({conf,Props1,{St#cc.mod,InitF},CaseList,FinMF}, + St, Mode) + end; +collect_cases({conf,Props,InitMF,CaseList,FinF}, St, Mode) when is_atom(FinF) -> + case init_props(Props) of + {error,_} -> + {ok,[],St}; + Props1 -> + collect_cases({conf,Props1,InitMF,CaseList,{St#cc.mod,FinF}}, + St, Mode) + end; +collect_cases({conf,Props,InitMF,CaseList,FinMF} = Conf, St, Mode) -> + case init_props(Props) of + {error,_} -> + {ok,[],St}; + Props1 -> + Ref = make_ref(), + Skips = St#cc.skip, + Props2 = [{suite,St#cc.mod} | lists:delete(suite,Props1)], + Mode1 = [{Ref,Props2,undefined} | Mode], + case in_skip_list({St#cc.mod,Conf}, Skips) of + {true,Comment} -> % conf init skipped + {ok,[{skip_case,{conf,Ref,InitMF,Comment},Mode1} | + [] ++ [{conf,Ref,[],FinMF}]],St}; + {true,Name,Comment} when is_atom(Name) -> % all cases skipped + case collect_cases(CaseList, St, Mode1) of + {ok,[],_St} = Empty -> + Empty; + {ok,FlatCases,St1} -> + Cases2Skip = FlatCases ++ [{conf,Ref, + keep_name(Props1), + FinMF}], + Skipped = skip_cases_upto(Ref, Cases2Skip, Comment, + conf, Mode1, skip_case), + {ok,[{skip_case,{conf,Ref,InitMF,Comment},Mode1} | + Skipped],St1}; + {error,_Reason} = Error -> + Error + end; + {true,ToSkip,_} when is_list(ToSkip) -> % some cases skipped + case collect_cases(CaseList, + St#cc{skip=ToSkip++Skips}, Mode1) of + {ok,[],_St} = Empty -> + Empty; + {ok,FlatCases,St1} -> + {ok,[{conf,Ref,Props1,InitMF} | + FlatCases ++ [{conf,Ref, + keep_name(Props1), + FinMF}]],St1#cc{skip=Skips}}; + {error,_Reason} = Error -> + Error + end; + false -> + case collect_cases(CaseList, St, Mode1) of + {ok,[],_St} = Empty -> + Empty; + {ok,FlatCases,St1} -> + {ok,[{conf,Ref,Props1,InitMF} | + FlatCases ++ [{conf,Ref, + keep_name(Props1), + FinMF}]],St1}; + {error,_Reason} = Error -> + Error + end + end + end; + +collect_cases({make,InitMFA,CaseList,FinMFA}, St0, Mode) -> + case collect_cases(CaseList, St0, Mode) of + {ok,[],_St} = Empty -> Empty; + {ok,FlatCases,St} -> + Ref = make_ref(), + {ok,[{make,Ref,InitMFA}|FlatCases ++ + [{make,Ref,FinMFA}]],St}; + {error,_Reason} = Error -> Error + end; + +collect_cases({Module, Cases}, St, Mode) when is_list(Cases) -> + case (catch collect_case(Cases, St#cc{mod=Module}, [], Mode)) of + Result = {ok,_,_} -> + Result; + Other -> + {error,Other} + end; + +collect_cases({_Mod,_Case}=Spec, St, Mode) -> + collect_case(Spec, St, Mode); + +collect_cases({_Mod,_Case,_Args}=Spec, St, Mode) -> + collect_case(Spec, St, Mode); +collect_cases(Case, St, Mode) when is_atom(Case), is_atom(St#cc.mod) -> + collect_case({St#cc.mod,Case}, St, Mode); +collect_cases(Other, St, _Mode) -> + {error,{bad_subtest_spec,St#cc.mod,Other}}. + +collect_case({Mod,{conf,_,_,_,_}=Conf}, St, Mode) -> + collect_case_invoke(Mod, Conf, [], St, Mode); + +collect_case(MFA, St, Mode) -> + case in_skip_list(MFA, St#cc.skip) of + {true,Comment} when Comment /= make_failed -> + {ok,[{skip_case,{MFA,Comment},Mode}],St}; + _ -> + case MFA of + {Mod,Case} -> collect_case_invoke(Mod, Case, MFA, St, Mode); + {_Mod,_Case,_Args} -> {ok,[MFA],St} + end + end. + +collect_case([], St, Acc, _Mode) -> + {ok, Acc, St}; + +collect_case([Case | Cases], St, Acc, Mode) -> + {ok, FlatCases, NewSt} = collect_case({St#cc.mod, Case}, St, Mode), + collect_case(Cases, NewSt, Acc ++ FlatCases, Mode). + +collect_case_invoke(Mod, Case, MFA, St, Mode) -> + case get_fw_mod(undefined) of + undefined -> + case catch apply(Mod, Case, [suite]) of + {'EXIT',_} -> + {ok,[MFA],St}; + Suite -> + collect_subcases(Mod, Case, MFA, St, Suite, Mode) + end; + _ -> + Suite = test_server_sup:framework_call(get_suite, + [Mod,Case], + []), + collect_subcases(Mod, Case, MFA, St, Suite, Mode) + end. + +collect_subcases(Mod, Case, MFA, St, Suite, Mode) -> + case Suite of + [] when Case == all -> {ok,[],St}; + [] when element(1, Case) == conf -> {ok,[],St}; + [] -> {ok,[MFA],St}; +%%%! --- START Kept for backwards compatibility --- +%%%! Requirements are not used + {req,ReqList} -> + collect_case_deny(Mod, Case, MFA, ReqList, [], St, Mode); + {req,ReqList,SubCases} -> + collect_case_deny(Mod, Case, MFA, ReqList, SubCases, St, Mode); +%%%! --- END Kept for backwards compatibility --- + {Skip,Reason} when Skip==skip; Skip==skipped -> + {ok,[{skip_case,{MFA,Reason},Mode}],St}; + {error,Reason} -> + throw(Reason); + SubCases -> + collect_case_subcases(Mod, Case, SubCases, St, Mode) + end. + +collect_case_subcases(Mod, Case, SubCases, St0, Mode) -> + OldMod = St0#cc.mod, + case collect_cases(SubCases, St0#cc{mod=Mod}, Mode) of + {ok,FlatCases,St} -> + {ok,FlatCases,St#cc{mod=OldMod}}; + {error,Reason} -> + {error,{{Mod,Case},Reason}} + end. + +collect_files(Dir, Pattern, St, Mode) -> + {ok,Cwd} = file:get_cwd(), + Dir1 = filename:join(Cwd, Dir), + Wc = filename:join([Dir1,Pattern++"{.erl,"++code:objfile_extension()++"}"]), + case catch filelib:wildcard(Wc) of + {'EXIT', Reason} -> + io:format("Could not collect files: ~tp~n", [Reason]), + {error,{collect_fail,Dir,Pattern}}; + Files -> + %% convert to module names and remove duplicates + Mods = lists:foldl(fun(File, Acc) -> + Mod = fullname_to_mod(File), + case lists:member(Mod, Acc) of + true -> Acc; + false -> [Mod | Acc] + end + end, [], Files), + Tests = [{Mod,all} || Mod <- lists:sort(Mods)], + collect_cases(Tests, St, Mode) + end. + +fullname_to_mod(Path) when is_list(Path) -> + %% If this is called with a binary, then we are probably in +fnu + %% mode and have found a beam file with name encoded as latin1. We + %% will let this crash since it can not work to load such a module + %% anyway. It should be removed or renamed! + list_to_atom(filename:rootname(filename:basename(Path))). + +collect_case_deny(Mod, Case, MFA, ReqList, SubCases, St, Mode) -> + case {check_deny(ReqList, St#cc.skip),SubCases} of + {{denied,Comment},_SubCases} -> + {ok,[{skip_case,{MFA,Comment},Mode}],St}; + {granted,[]} -> + {ok,[MFA],St}; + {granted,SubCases} -> + collect_case_subcases(Mod, Case, SubCases, St, Mode) + end. + +check_deny([Req|Reqs], DenyList) -> + case check_deny_req(Req, DenyList) of + {denied,_Comment}=Denied -> Denied; + granted -> check_deny(Reqs, DenyList) + end; +check_deny([], _DenyList) -> granted; +check_deny(Req, DenyList) -> check_deny([Req], DenyList). + +check_deny_req({Req,Val}, DenyList) -> + %%io:format("ValCheck ~p=~p in ~p\n", [Req,Val,DenyList]), + case lists:keysearch(Req, 1, DenyList) of + {value,{_Req,DenyVal}} when Val >= DenyVal -> + {denied,io_lib:format("Requirement ~tp=~tp", [Req,Val])}; + _ -> + check_deny_req(Req, DenyList) + end; +check_deny_req(Req, DenyList) -> + case lists:member(Req, DenyList) of + true -> {denied,io_lib:format("Requirement ~tp", [Req])}; + false -> granted + end. + +in_skip_list({Mod,{conf,Props,InitMF,_CaseList,_FinMF}}, SkipList) -> + case in_skip_list(InitMF, SkipList) of + {true,_} = Yes -> + Yes; + _ -> + case proplists:get_value(name, Props) of + undefined -> + false; + Name -> + ToSkip = + lists:flatmap( + fun({M,{conf,SProps,_,SCaseList,_},Cmt}) when + M == Mod -> + case proplists:get_value(name, SProps) of + all -> + [{M,all,Cmt}]; + Name -> + case SCaseList of + all -> + [{M,all,Cmt}]; + _ -> + [{M,F,Cmt} || F <- SCaseList] + end; + _ -> + [] + end; + (_) -> + [] + end, SkipList), + case ToSkip of + [] -> + false; + _ -> + case lists:keysearch(all, 2, ToSkip) of + {value,{_,_,Cmt}} -> {true,Name,Cmt}; + _ -> {true,ToSkip,""} + end + end + end + end; + +in_skip_list({Mod,Func,_Args}, SkipList) -> + in_skip_list({Mod,Func}, SkipList); +in_skip_list({Mod,Func}, [{Mod,Funcs,Comment}|SkipList]) when is_list(Funcs) -> + case lists:member(Func, Funcs) of + true -> + {true,Comment}; + _ -> + in_skip_list({Mod,Func}, SkipList) + end; +in_skip_list({Mod,Func}, [{Mod,Func,Comment}|_SkipList]) -> + {true,Comment}; +in_skip_list({Mod,_Func}, [{Mod,Comment}|_SkipList]) -> + {true,Comment}; +in_skip_list({Mod,Func}, [_|SkipList]) -> + in_skip_list({Mod,Func}, SkipList); +in_skip_list(_, []) -> + false. + +%% remove unnecessary properties +init_props(Props) -> + case get_repeat(Props) of + Repeat = {_RepType,N} when N < 2 -> + if N == 0 -> + {error,{invalid_property,Repeat}}; + true -> + lists:delete(Repeat, Props) + end; + _ -> + Props + end. + +keep_name(Props) -> + lists:filter(fun({name,_}) -> true; + ({suite,_}) -> true; + (_) -> false end, Props). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Node handling functions %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% get_target_info() -> #target_info +%% +%% Returns a record containing system information for target + +get_target_info() -> + controller_call(get_target_info). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% start_node(SlaveName, Type, Options) -> +%% {ok, Slave} | {error, Reason} +%% +%% Called by test_server. See test_server:start_node/3 for details + +start_node(Name, Type, Options) -> + T = 10 * ?ACCEPT_TIMEOUT * test_server:timetrap_scale_factor(), + format(minor, "Attempt to start ~w node ~tp with options ~tp", + [Type, Name, Options]), + case controller_call({start_node,Name,Type,Options}, T) of + {{ok,Nodename}, Host, Cmd, Info, Warning} -> + format(minor, + "Successfully started node ~w on ~tp with command: ~ts", + [Nodename, Host, Cmd]), + format(major, "=node_start ~w", [Nodename]), + case Info of + [] -> ok; + _ -> format(minor, Info) + end, + case Warning of + [] -> ok; + _ -> + format(1, Warning), + format(minor, Warning) + end, + {ok, Nodename}; + {fail,{Ret, Host, Cmd}} -> + format(minor, + "Failed to start node ~tp on ~tp with command: ~ts~n" + "Reason: ~tp", + [Name, Host, Cmd, Ret]), + {fail,Ret}; + {Ret, undefined, undefined} -> + format(minor, "Failed to start node ~tp: ~tp", [Name,Ret]), + Ret; + {Ret, Host, Cmd} -> + format(minor, + "Failed to start node ~tp on ~tp with command: ~ts~n" + "Reason: ~tp", + [Name, Host, Cmd, Ret]), + Ret + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% wait_for_node(Node) -> ok | {error,timeout} +%% +%% Wait for a slave/peer node which has been started with +%% the option {wait,false}. This function returns when +%% when the new node has contacted test_server_ctrl again + +wait_for_node(Slave) -> + T = 10000 * test_server:timetrap_scale_factor(), + case catch controller_call({wait_for_node,Slave},T) of + {'EXIT',{timeout,_}} -> {error,timeout}; + ok -> ok + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% is_release_available(Release) -> true | false +%% Release -> string() +%% +%% Test if a release (such as "r10b") is available to be +%% started using start_node/3. + +is_release_available(Release) -> + controller_call({is_release_available,Release}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% stop_node(Name) -> ok | {error,Reason} +%% +%% Clean up - test_server will stop this node + +stop_node(Slave) -> + controller_call({stop_node,Slave}). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% DEBUGGER INTERFACE %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +i() -> + hformat("Pid", "Initial Call", "Current Function", "Reducts", "Msgs"), + Line=lists:duplicate(27, "-"), + hformat(Line, Line, Line, Line, Line), + display_info(processes(), 0, 0). + +p(A,B,C) -> + pinfo(ts_pid(A,B,C)). +p(X) when is_atom(X) -> + pinfo(whereis(X)); +p({A,B,C}) -> + pinfo(ts_pid(A,B,C)); +p(X) -> + pinfo(X). + +t() -> + t(wall_clock). +t(X) -> + element(1, statistics(X)). + +pi(Item,X) -> + lists:keysearch(Item,1,p(X)). +pi(Item,A,B,C) -> + lists:keysearch(Item,1,p(A,B,C)). + +%% c:pid/3 +ts_pid(X,Y,Z) when is_integer(X), is_integer(Y), is_integer(Z) -> + list_to_pid("<" ++ integer_to_list(X) ++ "." ++ + integer_to_list(Y) ++ "." ++ + integer_to_list(Z) ++ ">"). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% display_info(Pids, Reductions, Messages) -> void +%% Pids = [pid(),...] +%% Reductions = integer() +%% Messaged = integer() +%% +%% Displays info, similar to c:i() about the processes in the list Pids. +%% Also counts the total number of reductions and msgs for the listed +%% processes, if called with Reductions = Messages = 0. + +display_info([Pid|T], R, M) -> + case pinfo(Pid) of + undefined -> + display_info(T, R, M); + Info -> + Call = fetch(initial_call, Info), + Curr = case fetch(current_function, Info) of + {Mod,F,Args} when is_list(Args) -> + {Mod,F,length(Args)}; + Other -> + Other + end, + Reds = fetch(reductions, Info), + LM = length(fetch(messages, Info)), + pformat(io_lib:format("~w", [Pid]), + io_lib:format("~tw", [Call]), + io_lib:format("~tw", [Curr]), Reds, LM), + display_info(T, R+Reds, M + LM) + end; +display_info([], R, M) -> + Line=lists:duplicate(27, "-"), + hformat(Line, Line, Line, Line, Line), + pformat("Total", "", "", R, M). + +hformat(A1, A2, A3, A4, A5) -> + io:format("~-10s ~-27s ~-27s ~8s ~4s~n", [A1,A2,A3,A4,A5]). + +pformat(A1, A2, A3, A4, A5) -> + io:format("~-10s ~-27s ~-27s ~8w ~4w~n", [A1,A2,A3,A4,A5]). + +fetch(Key, Info) -> + case lists:keysearch(Key, 1, Info) of + {value, {_, Val}} -> + Val; + _ -> + 0 + end. + +pinfo(P) -> + Node = node(), + case node(P) of + Node -> + process_info(P); + _ -> + rpc:call(node(P),erlang,process_info,[P]) + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Support functions for COVER %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% A module is included in the cover analysis if +%% - it belongs to the tested application and is not listed in the +%% {exclude,List} part of the App.cover file +%% - it does not belong to the application, but is listed in the +%% {include,List} part of the App.cover file +%% - it does not belong to the application, but is listed in the +%% {cross,[{Tag,List}]} part of the App.cover file +%% +%% The modules listed in the 'cross' part of the cover file are +%% modules that are heavily used by other tests than the one where +%% they are explicitly tested. They should then be listed as 'cross' +%% in the cover file for the test where they are used but do not +%% belong. +%% +%% After all tests are completed, the these modules can be analysed +%% with coverage data from all tests where they are compiled - see +%% cross_cover_analyse/2. The result is stored in a file called +%% cross_cover.html in the run.<timestamp> directory of the +%% test the modules belong to. +%% +%% Example: +%% If the module m1 belongs to system s1 but is heavily used also in +%% the tests for another system s2, then the cover files for the two +%% systems could be like this: +%% +%% s1.cover: +%% {include,[m1]}. +%% +%% s2.cover: +%% {include,[....]}. % modules belonging to system s2 +%% {cross,[{s1,[m1]}]}. +%% +%% When the tests for both s1 and s2 are completed, run +%% cross_cover_analyse(Level,[{s1,S1LogDir},{s2,S2LogDir}]), and +%% the accumulated cover data for m1 will be written to +%% S1LogDir/[run.<timestamp>/]cross_cover.html +%% +%% S1LogDir and S2LogDir are either the run.<timestamp> directories +%% for the two tests, or the parent directory of these, in which case +%% the latest run.<timestamp> directory will be chosen. +%% +%% Note that the m1 module will also be presented in the normal +%% coverage log for s1 (due to the include statement in s1.cover), but +%% that only includes the coverage achieved by the s1 test itself. +%% +%% The Tag in the 'cross' statement in the cover file has no other +%% purpose than mapping the list of modules ([m1] in the example +%% above) to the correct log directory where it should be included in +%% the cross_cover.html file (S1LogDir in the example above). +%% I.e. the value of the Tag has no meaning, it could be foo as well +%% as s1 above, as long as the same Tag is used in the cover file and +%% in the call to cross_cover_analyse/2. + + +%% Cover compilation +%% The compilation is executed on the target node +start_cover(#cover{}=CoverInfo) -> + cover_compile(CoverInfo); +start_cover({log,CoverLogDir}=CoverInfo) -> + %% Cover is controlled by the framework - here's the log + put(test_server_cover_log_dir,CoverLogDir), + {ok,CoverInfo}. + +cover_compile(CoverInfo) -> + test_server:cover_compile(CoverInfo). + +%% Read the coverfile for an application and return a list of modules +%% that are members of the application but shall not be compiled +%% (Exclude), and a list of modules that are not members of the +%% application but shall be compiled (Include). +read_cover_file(none) -> + {[],[],[]}; +read_cover_file(CoverFile) -> + case file:consult(CoverFile) of + {ok,List} -> + case check_cover_file(List, [], [], []) of + {ok,Exclude,Include,Cross} -> {Exclude,Include,Cross}; + error -> + io:fwrite("Faulty format of CoverFile ~tp\n", [CoverFile]), + {[],[],[]} + end; + {error,Reason} -> + io:fwrite("Can't read CoverFile ~ts\nReason: ~tp\n", + [CoverFile,Reason]), + {[],[],[]} + end. + +check_cover_file([{exclude,all}|Rest], _, Include, Cross) -> + check_cover_file(Rest, all, Include, Cross); +check_cover_file([{exclude,Exclude}|Rest], _, Include, Cross) -> + case lists:all(fun(M) -> is_atom(M) end, Exclude) of + true -> + check_cover_file(Rest, Exclude, Include, Cross); + false -> + error + end; +check_cover_file([{include,Include}|Rest], Exclude, _, Cross) -> + case lists:all(fun(M) -> is_atom(M) end, Include) of + true -> + check_cover_file(Rest, Exclude, Include, Cross); + false -> + error + end; +check_cover_file([{cross,Cross}|Rest], Exclude, Include, _) -> + case check_cross(Cross) of + true -> + check_cover_file(Rest, Exclude, Include, Cross); + false -> + error + end; +check_cover_file([], Exclude, Include, Cross) -> + {ok,Exclude,Include,Cross}. + +check_cross([{Tag,Modules}|Rest]) -> + case lists:all(fun(M) -> is_atom(M) end, [Tag|Modules]) of + true -> + check_cross(Rest); + false -> + false + end; +check_cross([]) -> + true. + + +%% Cover analysis, per application +%% This analysis is executed on the target node once the test is +%% completed for an application. This is not the same as the cross +%% cover analysis, which can be executed on any node after the tests +%% are finshed. +%% +%% This per application analysis writes the file cover.html in the +%% application's run.<timestamp> directory. +stop_cover(#cover{}=CoverInfo, TestDir) -> + cover_analyse(CoverInfo, TestDir), + ok; +stop_cover(_CoverInfo, _TestDir) -> + %% Cover is probably controlled by the framework + ok. + +make_relative(AbsDir, VsDir) -> + DirTokens = filename:split(AbsDir), + VsTokens = filename:split(VsDir), + filename:join(make_relative1(DirTokens, VsTokens)). + +make_relative1([T | DirTs], [T | VsTs]) -> + make_relative1(DirTs, VsTs); +make_relative1(Last = [_File], []) -> + Last; +make_relative1(Last = [_File], VsTs) -> + Ups = ["../" || _ <- VsTs], + Ups ++ Last; +make_relative1(DirTs, []) -> + DirTs; +make_relative1(DirTs, VsTs) -> + Ups = ["../" || _ <- VsTs], + Ups ++ DirTs. + + +cover_analyse(CoverInfo, TestDir) -> + write_default_cross_coverlog(TestDir), + + {ok,CoverLog} = open_html_file(filename:join(TestDir, ?coverlog_name)), + write_coverlog_header(CoverLog), + #cover{app=App, + file=CoverFile, + excl=Excluded, + cross=Cross} = CoverInfo, + io:fwrite(CoverLog, "<h1>Coverage for application '~w'</h1>\n", [App]), + io:fwrite(CoverLog, + "<p><a href=\"~ts\">Coverdata collected over all tests</a></p>", + [?cross_coverlog_name]), + + io:fwrite(CoverLog, "<p>CoverFile: <code>~tp</code>\n", [CoverFile]), + ok = write_cross_cover_info(TestDir,Cross), + + case length(cover:imported_modules()) of + Imps when Imps > 0 -> + io:fwrite(CoverLog, + "<p>Analysis includes data from ~w imported module(s).\n", + [Imps]); + _ -> + ok + end, + + io:fwrite(CoverLog, "<p>Excluded module(s): <code>~tp</code>\n", [Excluded]), + + Coverage = test_server:cover_analyse(TestDir, CoverInfo), + ok = write_binary_file(filename:join(TestDir,?raw_coverlog_name), + term_to_binary(Coverage)), + + case lists:filter(fun({_M,{_,_,_}}) -> false; + (_) -> true + end, Coverage) of + [] -> + ok; + Bad -> + io:fwrite(CoverLog, "<p>Analysis failed for ~w module(s): " + "<code>~w</code>\n", + [length(Bad),[BadM || {BadM,{_,_Why}} <- Bad]]) + end, + + TotPercent = write_cover_result_table(CoverLog, Coverage), + ok = write_binary_file(filename:join(TestDir, ?cover_total), + term_to_binary(TotPercent)). + +%% Cover analysis - accumulated over multiple tests +%% This can be executed on any node after all tests are finished. +%% Analyse = overview | details +%% TagDirs = [{Tag,Dir}] +%% Tag = atom(), identifier +%% Dir = string(), the log directory for Tag, it can be a +%% run.<timestamp> directory or the parent directory of +%% such (in which case the latest run.<timestamp> directory +%% is used) +cross_cover_analyse(Analyse, TagDirs0) -> + TagDirs = get_latest_run_dirs(TagDirs0), + TagMods = get_all_cross_info(TagDirs,[]), + TagDirMods = add_cross_modules(TagMods,TagDirs), + CoverdataFiles = get_coverdata_files(TagDirMods), + lists:foreach(fun(CDF) -> cover:import(CDF) end, CoverdataFiles), + io:fwrite("Cover analysing...\n", []), + DetailsFun = + case Analyse of + details -> + fun(Dir,M) -> + OutFile = filename:join(Dir, + atom_to_list(M) ++ + ".CROSS_COVER.html"), + case cover:analyse_to_file(M, OutFile, [html]) of + {ok,_} -> + {file,OutFile}; + Error -> + Error + end + end; + _ -> + fun(_,_) -> undefined end + end, + Coverage = analyse_tests(TagDirMods, DetailsFun, []), + cover:stop(), + write_cross_cover_logs(Coverage,TagDirMods). + +write_cross_cover_info(_Dir,[]) -> + ok; +write_cross_cover_info(Dir,Cross) -> + write_binary_file(filename:join(Dir,?cross_cover_info), + term_to_binary(Cross)). + +%% For each test from which there are cross cover analysed +%% modules, write a cross cover log (cross_cover.html). +write_cross_cover_logs([{Tag,Coverage}|T],TagDirMods) -> + case lists:keyfind(Tag,1,TagDirMods) of + {_,Dir,Mods} when Mods=/=[] -> + ok = write_binary_file(filename:join(Dir,?raw_cross_coverlog_name), + term_to_binary(Coverage)), + CoverLogName = filename:join(Dir,?cross_coverlog_name), + {ok,CoverLog} = open_html_file(CoverLogName), + write_coverlog_header(CoverLog), + io:fwrite(CoverLog, + "<h1>Coverage results for \'~w\' from all tests</h1>\n", + [Tag]), + write_cover_result_table(CoverLog, Coverage), + io:fwrite("Written file ~tp\n", [CoverLogName]); + _ -> + ok + end, + write_cross_cover_logs(T,TagDirMods); +write_cross_cover_logs([],_) -> + io:fwrite("done\n", []). + +%% Get the latest run.<timestamp> directories +get_latest_run_dirs([{Tag,Dir}|Rest]) -> + [{Tag,get_latest_run_dir(Dir)} | get_latest_run_dirs(Rest)]; +get_latest_run_dirs([]) -> + []. + +get_latest_run_dir(Dir) -> + case filelib:wildcard(filename:join(Dir,"run.[1-2]*")) of + [] -> + Dir; + [H|T] -> + get_latest_dir(T,H) + end. + +get_latest_dir([H|T],Latest) when H>Latest -> + get_latest_dir(T,H); +get_latest_dir([_|T],Latest) -> + get_latest_dir(T,Latest); +get_latest_dir([],Latest) -> + Latest. + +get_all_cross_info([{_Tag,Dir}|Rest],Acc) -> + case file:read_file(filename:join(Dir,?cross_cover_info)) of + {ok,Bin} -> + TagMods = binary_to_term(Bin), + get_all_cross_info(Rest,TagMods++Acc); + _ -> + get_all_cross_info(Rest,Acc) + end; +get_all_cross_info([],Acc) -> + Acc. + +%% Associate the cross cover modules with their log directories +add_cross_modules(TagMods,TagDirs)-> + do_add_cross_modules(TagMods,[{Tag,Dir,[]} || {Tag,Dir} <- TagDirs]). +do_add_cross_modules([{Tag,Mods1}|TagMods],TagDirMods)-> + NewTagDirMods = + case lists:keytake(Tag,1,TagDirMods) of + {value,{Tag,Dir,Mods},Rest} -> + [{Tag,Dir,lists:umerge(lists:sort(Mods1),Mods)}|Rest]; + false -> + TagDirMods + end, + do_add_cross_modules(TagMods,NewTagDirMods); +do_add_cross_modules([],TagDirMods) -> + %% Just to get the modules in the same order as in the normal cover log + [{Tag,Dir,lists:reverse(Mods)} || {Tag,Dir,Mods} <- TagDirMods]. + +%% Find all exported coverdata files. +get_coverdata_files(TagDirMods) -> + lists:flatmap( + fun({_,LatestDir,_}) -> + filelib:wildcard(filename:join(LatestDir,"all.coverdata")) + end, + TagDirMods). + + +%% For each test, analyse all modules +%% Used for cross cover analysis. +analyse_tests([{Tag,LastTest,Modules}|T], DetailsFun, Acc) -> + Cov = analyse_modules(LastTest, Modules, DetailsFun, []), + analyse_tests(T, DetailsFun, [{Tag,Cov}|Acc]); +analyse_tests([], _DetailsFun, Acc) -> + Acc. + +%% Analyse each module +%% Used for cross cover analysis. +analyse_modules(Dir, [M|Modules], DetailsFun, Acc) -> + {ok,{M,{Cov,NotCov}}} = cover:analyse(M, module), + Acc1 = [{M,{Cov,NotCov,DetailsFun(Dir,M)}}|Acc], + analyse_modules(Dir, Modules, DetailsFun, Acc1); +analyse_modules(_Dir, [], _DetailsFun, Acc) -> + Acc. + + +%% Support functions for writing the cover logs (both cross and normal) +write_coverlog_header(CoverLog) -> + case catch io:put_chars(CoverLog,html_header("Coverage results")) of + {'EXIT',Reason} -> + io:format("\n\nERROR: Could not write normal heading in coverlog.\n" + "CoverLog: ~tw\n" + "Reason: ~tp\n", + [CoverLog,Reason]), + io:format(CoverLog,"<html><body>\n", []); + _ -> + ok + end. + + +format_analyse(M,Cov,NotCov,undefined) -> + io_lib:fwrite("<tr><td>~w</td>" + "<td align=right>~w %</td>" + "<td align=right>~w</td>" + "<td align=right>~w</td></tr>\n", + [M,pc(Cov,NotCov),Cov,NotCov]); +format_analyse(M,Cov,NotCov,{file,File}) -> + io_lib:fwrite("<tr><td><a href=\"~ts\">~w</a></td>" + "<td align=right>~w %</td>" + "<td align=right>~w</td>" + "<td align=right>~w</td></tr>\n", + [uri_encode(filename:basename(File)), + M,pc(Cov,NotCov),Cov,NotCov]); +format_analyse(M,Cov,NotCov,{lines,Lines}) -> + CoverOutName = atom_to_list(M)++".COVER.html", + {ok,CoverOut} = open_html_file(CoverOutName), + write_not_covered(CoverOut,M,Lines), + ok = file:close(CoverOut), + io_lib:fwrite("<tr><td><a href=\"~ts\">~w</a></td>" + "<td align=right>~w %</td>" + "<td align=right>~w</td>" + "<td align=right>~w</td></tr>\n", + [uri_encode(CoverOutName),M,pc(Cov,NotCov),Cov,NotCov]); +format_analyse(M,Cov,NotCov,{error,_}) -> + io_lib:fwrite("<tr><td>~w</td>" + "<td align=right>~w %</td>" + "<td align=right>~w</td>" + "<td align=right>~w</td></tr>\n", + [M,pc(Cov,NotCov),Cov,NotCov]). + + +pc(0,0) -> + 0; +pc(Cov,NotCov) -> + round(Cov/(Cov+NotCov)*100). + + +write_not_covered(CoverOut,M,Lines) -> + io:put_chars(CoverOut,html_header("Coverage results for "++atom_to_list(M))), + io:fwrite(CoverOut, + "The following lines in module ~w are not covered:\n" + "<table border=3 cellpadding=5>\n" + "<th>Line Number</th>\n", + [M]), + lists:foreach(fun({{_M,Line},{0,1}}) -> + io:fwrite(CoverOut,"<tr><td>~w</td></tr>\n", [Line]); + (_) -> + ok + end, + Lines), + io:put_chars(CoverOut,"</table>\n</body>\n</html>\n"). + + +write_default_coverlog(TestDir) -> + {ok,CoverLog} = open_html_file(filename:join(TestDir,?coverlog_name)), + write_coverlog_header(CoverLog), + io:put_chars(CoverLog,"Cover tool is not used\n</body></html>\n"), + ok = file:close(CoverLog). + +write_default_cross_coverlog(TestDir) -> + {ok,CrossCoverLog} = + open_html_file(filename:join(TestDir,?cross_coverlog_name)), + write_coverlog_header(CrossCoverLog), + io:put_chars(CrossCoverLog, + ["No cross cover modules exist for this application,", + xhtml("<br>","<br />"), + "or cross cover analysis is not completed.\n" + "</body></html>\n"]), + ok = file:close(CrossCoverLog). + +write_cover_result_table(CoverLog,Coverage) -> + io:fwrite(CoverLog, + "<p><table border=3 cellpadding=5>\n" + "<tr><th>Module</th><th>Covered (%)</th><th>Covered (Lines)</th>" + "<th>Not covered (Lines)</th>\n", + []), + {TotCov,TotNotCov} = + lists:foldl(fun({M,{Cov,NotCov,Details}},{AccCov,AccNotCov}) -> + Str = format_analyse(M,Cov,NotCov,Details), + io:fwrite(CoverLog,"~ts", [Str]), + {AccCov+Cov,AccNotCov+NotCov}; + ({_M,{error,_Reason}},{AccCov,AccNotCov}) -> + {AccCov,AccNotCov} + end, + {0,0}, + Coverage), + TotPercent = pc(TotCov,TotNotCov), + io:fwrite(CoverLog, + "<tr><th align=left>Total</th><th align=right>~w %</th>" + "<th align=right>~w</th><th align=right>~w</th></tr>\n" + "</table>\n" + "</body>\n" + "</html>\n", + [TotPercent,TotCov,TotNotCov]), + ok = file:close(CoverLog), + TotPercent. + + +%%%----------------------------------------------------------------- +%%% Support functions for writing files + +%% HTML files are always written with utf8 encoding +html_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\"></meta>\n" + "<meta http-equiv=\"content-type\" content=\"text/html; " + "charset=utf-8\"></meta>\n" + "</head>\n" + "<body bgcolor=\"white\" text=\"black\" " + "link=\"blue\" vlink=\"purple\" alink=\"red\">\n"]. + +html_header(Title, Meta) -> + ["<!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 ++ ["</head>\n"]. + +open_html_file(File) -> + open_utf8_file(File). + +open_html_file(File,Opts) -> + open_utf8_file(File,Opts). + +write_html_file(File,Content) -> + write_file(File,Content,utf8). + +%% The 'major' log file, which is a pure text file is also written +%% with utf8 encoding +open_utf8_file(File) -> + case file:open(File,AllOpts=[write,{encoding,utf8}]) of + {error,Reason} -> {error,{Reason,{File,AllOpts}}}; + Result -> Result + end. + +open_utf8_file(File,Opts) -> + case file:open(File,AllOpts=[{encoding,utf8}|Opts]) of + {error,Reason} -> {error,{Reason,{File,AllOpts}}}; + Result -> Result + end. + +%% Write a file with specified encoding +write_file(File,Content,latin1) -> + file:write_file(File,Content); +write_file(File,Content,utf8) -> + write_binary_file(File,unicode:characters_to_binary(Content)). + +%% Write a file with only binary data +write_binary_file(File,Content) -> + file:write_file(File,Content). + +%% Encoding of hyperlinks in HTML files +uri_encode(File) -> + Encoding = file:native_name_encoding(), + uri_encode(File,Encoding). + +uri_encode(File,Encoding) -> + Components = filename:split(File), + filename:join([uri_encode_comp(C,Encoding) || C <- Components]). + +%% Encode the reference to a "filename of the given encoding" so it +%% can be inserted in a utf8 encoded HTML file. +%% This does almost the same as http_uri:encode/1, except +%% 1. it does not convert @, : and / (in order to preserve nodename and c:/) +%% 2. if the file name is in latin1, it also encodes all +%% characters >127 - i.e. latin1 but not ASCII. +uri_encode_comp([Char|Chars],Encoding) -> + Reserved = sets:is_element(Char, reserved()), + case (Char>127 andalso Encoding==latin1) orelse Reserved of + true -> + [ $% | http_util:integer_to_hexlist(Char)] ++ + uri_encode_comp(Chars,Encoding); + false -> + [Char | uri_encode_comp(Chars,Encoding)] + end; +uri_encode_comp([],_) -> + []. + +%% Copied from http_uri.erl, but slightly modified +%% (not converting @, : and /) +reserved() -> + sets:from_list([$;, $&, $=, $+, $,, $?, + $#, $[, $], $<, $>, $\", ${, $}, $|, + $\\, $', $^, $%, $ ]). + +encoding(File) -> + case epp:read_encoding(File) of + none -> + epp:default_encoding(); + E -> + E + end. diff --git a/lib/common_test/src/test_server_gl.erl b/lib/common_test/src/test_server_gl.erl new file mode 100644 index 0000000000..24dd5cd54c --- /dev/null +++ b/lib/common_test/src/test_server_gl.erl @@ -0,0 +1,362 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012-2017. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +%% This module implements group leader processes for test cases. +%% Each group leader process handles output to the minor log file for +%% a test case, and calls test_server_io to handle output to the common +%% log files. The group leader processes are created and destroyed +%% through the test_server_io module/process. + +-module(test_server_gl). +-export([start_link/1,stop/1,set_minor_fd/3,unset_minor_fd/1, + get_tc_supervisor/1,print/4,set_props/2]). + +-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2]). + +-record(st, {tc_supervisor :: 'none'|pid(), %Test case supervisor + tc :: mfa() | 'undefined', %Current test case MFA + minor :: 'none'|pid(), %Minor fd + minor_monitor, %Monitor ref for minor fd + tsio_monitor, %Monitor red for controlling proc + capture :: 'none'|pid(), %Capture output + reject_io :: boolean(), %Reject I/O requests... + permit_io, %... and exceptions + auto_nl=true :: boolean(), %Automatically add NL + levels, %{Stdout,Major,Minor} + escape_chars=true %Switch escaping HTML on/off + }). + +%% start_link() +%% Start a new group leader process. Only to be called by +%% the test_server_io process. + +start_link(TSIO) -> + case gen_server:start_link(?MODULE, [TSIO], []) of + {ok,Pid} -> + {ok,Pid}; + Other -> + Other + end. + + +%% stop(Pid) +%% Stop a group leader process. Only to be called by +%% the test_server_io process. + +stop(GL) -> + gen_server:cast(GL, stop). + + +%% set_minor_fd(GL, Fd, MFA) +%% GL = Pid for the group leader process +%% Fd = file descriptor for the minor log file +%% MFA = {M,F,A} for the test case owning the minor log file +%% +%% Register the file descriptor for the minor log file. Subsequent +%% IO directed to the minor log file will be written to this file. +%% Also register the currently executing process at the testcase +%% supervisor corresponding to this group leader process. + +set_minor_fd(GL, Fd, MFA) -> + req(GL, {set_minor_fd,Fd,MFA,self()}). + + +%% unset_minor_fd(GL, Fd, MFA) +%% GL = Pid for the group leader process +%% +%% Unregister the file descriptor for minor log file (typically +%% because the test case has ended the minor log file is about +%% to be closed). Subsequent IO (for example, by a process spawned +%% by the testcase process) will go to the unexpected_io log file. + +unset_minor_fd(GL) -> + req(GL, unset_minor_fd). + + +%% get_tc_supervisor(GL) +%% GL = Pid for the group leader process +%% +%% Return the Pid for the process that supervises the test case +%% that has this group leader. + +get_tc_supervisor(GL) -> + req(GL, get_tc_supervisor). + + +%% print(GL, Detail, Format, Args) -> ok +%% GL = Pid for the group leader process +%% Detail = integer() | minor | major | html | stdout +%% Msg = iodata() +%% Printer = internal | pid() +%% +%% Print a message to one of the log files. If Detail is an integer, +%% it will be compared to the levels (set by set_props/2) to +%% determine which log file(s) that are to receive the output. If +%% Detail is an atom, the value of the atom will directly determine +%% which log file to use. IO to the minor log file will be handled +%% directly by this group leader process (printing to the file set by +%% set_minor_fd/3), and all other IO will be handled by calling +%% test_server_io:print/3. + +print(GL, Detail, Msg, Printer) -> + req(GL, {print,Detail,Msg,Printer}). + + +%% set_props(GL, [PropertyTuple]) +%% GL = Pid for the group leader process +%% PropertyTuple = {levels,{Show,Major,Minor}} | +%% {auto_nl,boolean()} | +%% {reject_io_reqs,boolean()} +%% +%% Set properties for this group leader process. + +set_props(GL, PropList) -> + req(GL, {set_props,PropList}). + +%%% Internal functions. + +init([TSIO]) -> + ct_util:mark_process(group_leader), + EscChars = case application:get_env(test_server, esc_chars) of + {ok,ECBool} -> ECBool; + _ -> true + end, + Ref = erlang:monitor(process, TSIO), + {ok,#st{tc_supervisor=none, + minor=none, + minor_monitor=none, + tsio_monitor=Ref, + capture=none, + reject_io=false, + permit_io=gb_sets:empty(), + auto_nl=true, + levels={1,19,10}, + escape_chars=EscChars + }}. + +req(GL, Req) -> + gen_server:call(GL, Req, infinity). + +handle_call(get_tc_supervisor, _From, #st{tc_supervisor=Pid}=St) -> + {reply,Pid,St}; +handle_call({set_minor_fd,Fd,MFA,Supervisor}, _From, St) -> + Ref = erlang:monitor(process, Fd), + {reply,ok,St#st{tc=MFA,minor=Fd,minor_monitor=Ref, + tc_supervisor=Supervisor}}; +handle_call(unset_minor_fd, _From, St) -> + {reply,ok,St#st{minor=none,tc_supervisor=none}}; +handle_call({set_props,PropList}, _From, St) -> + {reply,ok,do_set_props(PropList, St)}; +handle_call({print,Detail,Msg,Printer}, {From,_}, St) -> + output(Detail, Msg, Printer, From, St), + {reply,ok,St}. + +handle_cast(stop, St) -> + {stop,normal,St}. + +handle_info({'DOWN',Ref,process,_,Reason}=D, #st{minor_monitor=Ref}=St) -> + case Reason of + normal -> ok; + _ -> + Data = io_lib:format("=== WARNING === TC: ~tw\n" + "Got down from minor Fd ~w: ~tw\n\n", + [St#st.tc,St#st.minor,D]), + test_server_io:print_unexpected(Data) + end, + {noreply,St#st{minor=none,minor_monitor=none}}; +handle_info({'DOWN',Ref,process,_,_}, #st{tsio_monitor=Ref}=St) -> + %% controlling process (test_server_io) terminated, we're done + {stop,normal,St}; +handle_info({permit_io,Pid}, #st{permit_io=P}=St) -> + {noreply,St#st{permit_io=gb_sets:add(Pid, P)}}; +handle_info({capture,Cap0}, St) -> + Cap = case Cap0 of + false -> none; + Pid when is_pid(Cap0) -> Pid + end, + {noreply,St#st{capture=Cap}}; +handle_info({io_request,From,ReplyAs,Req}=IoReq, St) -> + _ = try io_req(Req, From, St) of + passthrough -> + group_leader() ! IoReq; + {EscapeHtml,Data} -> + case is_io_permitted(From, St) of + false -> + ok; + true -> + case St of + #st{capture=none} -> + ok; + #st{capture=CapturePid} -> + CapturePid ! {captured,Data}, + ok + end, + case EscapeHtml andalso St#st.escape_chars of + true -> + output(minor, test_server_ctrl:escape_chars(Data), + From, From, St); + false -> + output(minor, Data, From, From, St) + end + end, + From ! {io_reply,ReplyAs,ok} + catch + _:_ -> + From ! {io_reply,ReplyAs,{error,arguments}} + end, + {noreply,St}; +handle_info({structured_io,ClientPid,{Detail,Str}}, St) -> + output(Detail, Str, ClientPid, ClientPid, St), + {noreply,St}; +handle_info({printout,Detail,["$tc_html",Format],Args}, St) -> + Str = io_lib:format(Format, Args), + output(Detail, ["$tc_html",Str], internal, none, St), + {noreply,St}; +handle_info({printout,Detail,Fun}, St) when is_function(Fun)-> + output(Detail, Fun, internal, none, St), + {noreply,St}; +handle_info({printout,Detail,Format,Args}, St) -> + Str = io_lib:format(Format, Args), + if not St#st.escape_chars -> + output(Detail, ["$tc_html",Str], internal, none, St); + true -> + output(Detail, Str, internal, none, St) + end, + {noreply,St}; +handle_info(Msg, #st{tc_supervisor=Pid}=St) when is_pid(Pid) -> + %% The process overseeing the testcase process also used to be + %% the group leader; thus, it is widely expected that it can be + %% reached by sending a message to the group leader. Therefore + %% we'll need to forward any non-recognized messaged to the test + %% case supervisor. + Pid ! Msg, + {noreply,St}; +handle_info(_Msg, #st{}=St) -> + %% There is no known supervisor process. Ignore this message. + {noreply,St}. + +terminate(_, _) -> + ok. + +do_set_props([{levels,Levels}|Ps], St) -> + do_set_props(Ps, St#st{levels=Levels}); +do_set_props([{auto_nl,AutoNL}|Ps], St) -> + do_set_props(Ps, St#st{auto_nl=AutoNL}); +do_set_props([{reject_io_reqs,Bool}|Ps], St) -> + do_set_props(Ps, St#st{reject_io=Bool}); +do_set_props([], St) -> St. + +io_req({put_chars,Enc,Str}, _, _) when Enc =:= latin1; Enc =:= unicode -> + case Str of + ["$tc_html",Str0] -> + {false,unicode:characters_to_list(Str0, Enc)}; + _ -> + {true,unicode:characters_to_list(Str, Enc)} + end; +io_req({put_chars,Encoding,Mod,Func,[Format,Args]}, _, _) -> + case Format of + ["$tc_html",Format0] -> + Str = Mod:Func(Format0, Args), + {false,unicode:characters_to_list(Str, Encoding)}; + _ -> + Str = Mod:Func(Format, Args), + {true,unicode:characters_to_list(Str, Encoding)} + end; +io_req(_, _, _) -> passthrough. + +output(Level, StrOrFun, Sender, From, St) when is_integer(Level) -> + case selected_by_level(Level, stdout, St) of + true when hd(StrOrFun) == "$tc_html" -> + output(stdout, tl(StrOrFun), Sender, From, St); + true when is_function(StrOrFun) -> + output(stdout, StrOrFun(stdout), Sender, From, St); + true -> + output(stdout, StrOrFun, Sender, From, St); + false -> + ok + end, + case selected_by_level(Level, major, St) of + true when hd(StrOrFun) == "$tc_html" -> + output(major, tl(StrOrFun), Sender, From, St); + true when is_function(StrOrFun) -> + output(major, StrOrFun(major), Sender, From, St); + true -> + output(major, StrOrFun, Sender, From, St); + false -> + ok + end, + case selected_by_level(Level, minor, St) of + true when hd(StrOrFun) == "$tc_html" -> + output(minor, tl(StrOrFun), Sender, From, St); + true when is_function(StrOrFun) -> + output(minor, StrOrFun(minor), Sender, From, St); + true -> + output(minor, test_server_ctrl:escape_chars(StrOrFun), + Sender, From, St); + false -> + ok + end; +output(stdout, Str, _Sender, From, St) -> + output_to_file(stdout, Str, From, St); +output(html, Str, _Sender, From, St) -> + output_to_file(html, Str, From, St); +output(Level, Str, Sender, From, St) when is_atom(Level) -> + output_to_file(Level, dress_output(Str, Sender, St), From, St). + +output_to_file(minor, Data0, From, #st{tc={M,F,A},minor=none}) -> + Data = [io_lib:format("=== ~w:~tw/~w\n", [M,F,A]),Data0], + test_server_io:print(From, unexpected_io, Data), + ok; +output_to_file(minor, Data, From, #st{tc=TC,minor=Fd}) -> + try + io:put_chars(Fd, Data) + catch + Type:Reason -> + Data1 = + [io_lib:format("=== ERROR === TC: ~tw\n" + "Failed to write to minor Fd: ~w\n" + "Type: ~w\n" + "Reason: ~tw\n", + [TC,Fd,Type,Reason]), + Data,"\n"], + test_server_io:print(From, unexpected_io, Data1) + end; +output_to_file(Detail, Data, From, _) -> + test_server_io:print(From, Detail, Data). + +is_io_permitted(From, #st{reject_io=true,permit_io=P}) -> + gb_sets:is_member(From, P); +is_io_permitted(_, #st{reject_io=false}) -> true. + +selected_by_level(Level, stdout, #st{levels={Stdout,_,_}}) -> + Level =< Stdout; +selected_by_level(Level, major, #st{levels={_,Major,_}}) -> + Level =< Major; +selected_by_level(Level, minor, #st{levels={_,_,Minor}}) -> + Level >= Minor. + +dress_output([$=|_]=Str, internal, _) -> + [Str,$\n]; +dress_output(Str, internal, _) -> + ["=== ",Str,$\n]; +dress_output(Str, _, #st{auto_nl=AutoNL}) -> + case AutoNL of + true -> [Str,$\n]; + false -> Str + end. diff --git a/lib/common_test/src/test_server_internal.hrl b/lib/common_test/src/test_server_internal.hrl new file mode 100644 index 0000000000..f0c1876dd6 --- /dev/null +++ b/lib/common_test/src/test_server_internal.hrl @@ -0,0 +1,60 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2002-2016. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +-define(priv_dir,"log_private"). +-define(MAIN_PORT,3289). +-define(ACCEPT_TIMEOUT,20000). + +%% Target information generated by test_server:init_target_info/0 +%% Once initiated, this information will never change!! +-record(target_info, {os_family, % atom(); win32 | unix + os_type, % result of os:type() + host, % string(); the name of the target machine + version, % string() + system_version, % string() + root_dir, % string() + emulator, % string() + otp_release, % string() + username, % string() + cookie, % string(); Cookie for target node + naming, % string(); "-name" | "-sname" + master}). % string(); Was used for OSE's master + % node for main target and slave nodes. + % For other platforms the target node + % itself is master for slave nodes + +%% Temporary information generated by test_server_ctrl:read_parameters/X +%% This information is used when starting the main target, and for +%% initiating the #target_info record. +-record(par, {type, + target, + naming, + master, + cookie}). + + +-record(cover, {app, % application; Name | none + file, % cover spec file + incl, % explicitly include modules + excl, % explicitly exclude modules + level, % analyse level; details | overview + mods, % actually cover compiled modules + stop=true, % stop cover after analyse; boolean() + cross}).% cross cover analyse info diff --git a/lib/common_test/src/test_server_io.erl b/lib/common_test/src/test_server_io.erl new file mode 100644 index 0000000000..ef31521950 --- /dev/null +++ b/lib/common_test/src/test_server_io.erl @@ -0,0 +1,458 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012-2017. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +%% This module implements a process with the registered name 'test_server_io', +%% which has two main responsibilities: +%% +%% * Manage group leader processes (see the test_server_gl module) +%% for test cases. A group_leader process is obtained by calling +%% get_gl/1. Group leader processes will be kept alive as along as +%% the 'test_server_io' process is alive. +%% +%% * Handle output to the common log files (stdout, major, html, +%% unexpected_io). +%% + +-module(test_server_io). +-export([start_link/0,stop/1,get_gl/1,set_fd/2, + start_transaction/0,end_transaction/0, + print_buffered/1,print/3,print_unexpected/1, + set_footer/1,set_job_name/1,set_gl_props/1, + reset_state/0,finish/0]). + +-export([init/1,handle_call/3,handle_info/2,terminate/2]). + +-record(st, {fds, % Singleton fds (gb_tree) + tags=[], % Known tag types + shared_gl :: pid(), % Shared group leader + gls, % Group leaders (gb_set) + io_buffering=false, % I/O buffering + buffered, % Buffered I/O requests + html_footer, % HTML footer + job_name, % Name of current job. + gl_props, % Properties for GL + phase, % Indicates current mode + offline_buffer, % Buffer I/O during startup + stopping, % Reply to when process stopped + pending_ops % Perform when process idle + }). + +start_link() -> + case whereis(?MODULE) of + undefined -> + case gen_server:start_link({local,?MODULE}, ?MODULE, [], []) of + {ok,Pid} -> + {ok,Pid}; + Other -> + Other + end; + Pid -> + %% already running, reset the state + reset_state(), + {ok,Pid} + end. + +stop(FilesToClose) -> + OldGL = group_leader(), + group_leader(self(), self()), + req({stop,FilesToClose}), + group_leader(OldGL, self()), + ok. + +finish() -> + req(finish). + +%% get_gl(Shared) -> Pid +%% Shared = boolean() +%% Pid = pid() +%% +%% Return a group leader (a process using the test_server_gl module). +%% If Shared is true, the shared group leader is returned (suitable for +%% running sequential test cases), otherwise a new group leader process +%% is spawned. Group leader processes will live until the +%% 'test_server_io' process is stopped. + +get_gl(Shared) when is_boolean(Shared) -> + req({get_gl,Shared}). + +%% set_fd(Tag, Fd) -> ok. +%% Tag = major | html | unexpected_io +%% Fd = a file descriptor (as returned by file:open/2) +%% +%% Associate a file descriptor with the given Tag. This +%% Tag can later be used in when calling to print/3. + +set_fd(Tag, Fd) -> + req({set_fd,Tag,Fd}). + +%% start_transaction() +%% +%% Subsequent calls to print/3 from the process executing start_transaction/0 +%% will cause the messages to be buffered instead of printed directly. + +start_transaction() -> + req({start_transaction,self()}). + +%% end_transaction() +%% +%% End the transaction started by start_transaction/0. Subsequent calls to +%% print/3 will cause the message to be printed directly. + +end_transaction() -> + req({end_transaction,self()}). + +%% print(From, Tag, Msg) +%% From = pid() +%% Tag = stdout, or any tag that has been registered using set_fd/2 +%% Msg = string or iolist +%% +%% Either print Msg to the file identified by Tag, or buffer the message +%% start_transaction/0 has been called from the process From. +%% +%% NOTE: The tags have various special meanings. For example, 'html' +%% is assumed to be a HTML file. + +print(From, Tag, Msg) -> + req({print,From,Tag,Msg}). + +%% print_buffered(Pid) +%% Pid = pid() +%% +%% Print all messages buffered in the *first* transaction buffered for Pid. +%% (If start_transaction/0 and end_transaction/0 has been called N times, +%% print_buffered/1 must be called N times to print all transactions.) + +print_buffered(Pid) -> + req({print_buffered,Pid}). + +%% print_unexpected(Msg) +%% Msg = string or iolist +%% +%% Print the given string in the unexpected_io log. + +print_unexpected(Msg) -> + print(xxxFrom,unexpected_io,Msg). + +%% set_footer(IoData) +%% +%% Set a footer for the file associated with the 'html' tag. +%% It will be used by print/3 to print a footer for the HTML file. + +set_footer(Footer) -> + req({set_footer,Footer}). + +%% set_job_name(Name) +%% +%% Set a name for the currently running job. The name will be used +%% when printing to 'stdout'. +%% + +set_job_name(Name) -> + req({set_job_name,Name}). + +%% set_gl_props(PropList) +%% +%% Set properties for group leader processes. When a group_leader process +%% is created, test_server_gl:set_props(PropList) will be called. + +set_gl_props(PropList) -> + req({set_gl_props,PropList}). + +%% reset_state +%% +%% Reset the initial state +reset_state() -> + req(reset_state). + +%%% Internal functions. + +init([]) -> + process_flag(trap_exit, true), + ct_util:mark_process(), + Empty = gb_trees:empty(), + {ok,Shared} = test_server_gl:start_link(self()), + {ok,#st{fds=Empty,shared_gl=Shared,gls=gb_sets:empty(), + io_buffering=gb_sets:empty(), + buffered=Empty, + html_footer="</body>\n</html>\n", + job_name="<name not set>", + gl_props=[], + phase=starting, + offline_buffer=[], + pending_ops=[]}}. + +req(Req) -> + gen_server:call(?MODULE, Req, infinity). + +handle_call({get_gl,false}, _From, #st{gls=Gls,gl_props=Props}=St) -> + {ok,Pid} = test_server_gl:start_link(self()), + test_server_gl:set_props(Pid, Props), + {reply,Pid,St#st{gls=gb_sets:insert(Pid, Gls)}}; +handle_call({get_gl,true}, _From, #st{shared_gl=Shared}=St) -> + {reply,Shared,St}; +handle_call({set_fd,Tag,Fd}, _From, #st{fds=Fds0,tags=Tags0, + offline_buffer=OfflineBuff}=St) -> + Fds = gb_trees:enter(Tag, Fd, Fds0), + St1 = St#st{fds=Fds,tags=[Tag|lists:delete(Tag, Tags0)]}, + OfflineBuff1 = + if OfflineBuff == [] -> + []; + true -> + %% Fd ready, print anything buffered for associated Tag + lists:filtermap(fun({T,From,Str}) when T == Tag -> + _ = output(From, Tag, Str, St1), + false; + (_) -> + true + end, lists:reverse(OfflineBuff)) + end, + {reply,ok,St1#st{phase=started, + offline_buffer=lists:reverse(OfflineBuff1)}}; +handle_call({start_transaction,Pid}, _From, #st{io_buffering=Buffer0, + buffered=Buf0}=St) -> + Buf = case gb_trees:is_defined(Pid, Buf0) of + false -> gb_trees:insert(Pid, queue:new(), Buf0); + true -> Buf0 + end, + Buffer = gb_sets:add(Pid, Buffer0), + {reply,ok,St#st{io_buffering=Buffer,buffered=Buf}}; +handle_call({print,From,Tag,Str}, _From, St0) -> + St = output(From, Tag, Str, St0), + {reply,ok,St}; +handle_call({end_transaction,Pid}, _From, #st{io_buffering=Buffer0, + buffered=Buffered0}=St0) -> + Q0 = gb_trees:get(Pid, Buffered0), + Q = queue:in(eot, Q0), + Buffered = gb_trees:update(Pid, Q, Buffered0), + Buffer = gb_sets:delete_any(Pid, Buffer0), + St = St0#st{io_buffering=Buffer,buffered=Buffered}, + {reply,ok,St}; +handle_call({print_buffered,Pid}, _From, #st{buffered=Buffered0}=St0) -> + Q0 = gb_trees:get(Pid, Buffered0), + Q = do_print_buffered(Q0, St0), + Buffered = gb_trees:update(Pid, Q, Buffered0), + St = St0#st{buffered=Buffered}, + {reply,ok,St}; +handle_call({set_footer,Footer}, _From, St) -> + {reply,ok,St#st{html_footer=Footer}}; +handle_call({set_job_name,Name}, _From, St) -> + {reply,ok,St#st{job_name=Name}}; +handle_call({set_gl_props,Props}, _From, #st{shared_gl=Shared}=St) -> + test_server_gl:set_props(Shared, Props), + {reply,ok,St#st{gl_props=Props}}; +handle_call(reset_state, From, #st{phase=stopping,pending_ops=Ops}=St) -> + %% can't reset during stopping phase, save op for later + Op = fun(NewSt) -> + {_,Result,NewSt1} = handle_call(reset_state, From, NewSt), + {Result,NewSt1} + end, + {noreply,St#st{pending_ops=[{From,Op}|Ops]}}; +handle_call(reset_state, _From, #st{fds=Fds,tags=Tags,shared_gl=Shared0,gls=Gls, + offline_buffer=OfflineBuff}) -> + %% close open log files + lists:foreach(fun(Tag) -> + case gb_trees:lookup(Tag, Fds) of + none -> + ok; + {value,Fd} -> + file:close(Fd) + end + end, Tags), + test_server_gl:stop(Shared0), + GlList = gb_sets:to_list(Gls), + _ = [test_server_gl:stop(GL) || GL <- GlList], + timer:sleep(100), + case lists:filter(fun(GlPid) -> is_process_alive(GlPid) end, GlList) of + [] -> + ok; + _ -> + timer:sleep(2000), + [exit(GL, kill) || GL <- GlList], + ok + end, + Empty = gb_trees:empty(), + {ok,Shared} = test_server_gl:start_link(self()), + {reply,ok,#st{fds=Empty,shared_gl=Shared,gls=gb_sets:empty(), + io_buffering=gb_sets:empty(), + buffered=Empty, + html_footer="</body>\n</html>\n", + job_name="<name not set>", + gl_props=[], + phase=starting, + offline_buffer=OfflineBuff, + pending_ops=[]}}; +handle_call({stop,FdTags}, From, #st{fds=Fds0,tags=Tags0, + shared_gl=SGL,gls=Gls0}=St0) -> + St = St0#st{gls=gb_sets:insert(SGL, Gls0),phase=stopping,stopping=From}, + gc(St), + %% close open log files + {Fds1,Tags1} = lists:foldl(fun(Tag, {Fds,Tags}) -> + case gb_trees:lookup(Tag, Fds) of + none -> + {Fds,Tags}; + {value,Fd} -> + _ = file:close(Fd), + {gb_trees:delete(Tag, Fds), + lists:delete(Tag, Tags)} + end + end, {Fds0,Tags0}, FdTags), + %% Give the users of the surviving group leaders some + %% time to finish. + erlang:send_after(1000, self(), stop_group_leaders), + {noreply,St#st{fds=Fds1,tags=Tags1}}; +handle_call(finish, From, St) -> + gen_server:reply(From, ok), + {stop,normal,St}. + +handle_info({'EXIT',Pid,normal}, #st{gls=Gls0,stopping=From}=St) -> + Gls = gb_sets:delete_any(Pid, Gls0), + case gb_sets:is_empty(Gls) andalso From =/= undefined of + true -> + %% No more group leaders left. + gen_server:reply(From, ok), + {noreply,St#st{gls=Gls,phase=stopping,stopping=undefined}}; + false -> + %% Wait for more group leaders to finish. + {noreply,St#st{gls=Gls,phase=stopping}} + end; +handle_info({'EXIT',Pid,killed}, #st{gls=Gls0}=St) -> + %% forced termination of group leader + {noreply,St#st{gls=gb_sets:delete_any(Pid, Gls0)}}; +handle_info({'EXIT',_Pid,Reason}, _St) -> + exit(Reason); +handle_info(stop_group_leaders, #st{gls=Gls}=St) -> + %% Stop the remaining group leaders. + GlPids = gb_sets:to_list(Gls), + _ = [test_server_gl:stop(GL) || GL <- GlPids], + timer:sleep(100), + Wait = + case lists:filter(fun(GlPid) -> is_process_alive(GlPid) end, GlPids) of + [] -> 0; + _ -> 2000 + end, + erlang:send_after(Wait, self(), kill_group_leaders), + {noreply,St}; +handle_info(kill_group_leaders, #st{gls=Gls,stopping=From, + pending_ops=Ops}=St) -> + _ = [exit(GL, kill) || GL <- gb_sets:to_list(Gls)], + if From /= undefined -> + gen_server:reply(From, ok); + true -> % reply has been sent already + ok + end, + %% we're idle, check if any ops are pending + St1 = lists:foldr(fun({ReplyTo,Op},NewSt) -> + {Result,NewSt1} = Op(NewSt), + gen_server:reply(ReplyTo, Result), + NewSt1 + end, St#st{phase=idle,pending_ops=[]}, Ops), + {noreply,St1}; +handle_info(Other, St) -> + io:format("Ignoring: ~tp\n", [Other]), + {noreply,St}. + +terminate(_, _) -> + ok. + +output(From, Tag, Str, #st{io_buffering=Buffered,buffered=Buf0, + phase=Phase,offline_buffer=OfflineBuff}=St) -> + case gb_sets:is_member(From, Buffered) of + false -> + case do_output(Tag, Str, Phase, St) of + buffer when length(OfflineBuff)>500 -> + %% something's wrong, clear buffer + St#st{offline_buffer=[]}; + buffer -> + St#st{offline_buffer=[{Tag,From,Str}|OfflineBuff]}; + _ -> + St + end; + true -> + Q0 = gb_trees:get(From, Buf0), + Q = queue:in({Tag,Str}, Q0), + Buf = gb_trees:update(From, Q, Buf0), + St#st{buffered=Buf} + end. + +do_output(stdout, Str, _, #st{job_name=undefined}) -> + io:put_chars(Str); +do_output(stdout, Str0, _, #st{job_name=Name}) -> + Str = io_lib:format("Testing ~ts: ~ts\n", [Name,Str0]), + io:put_chars(Str); +do_output(Tag, Str, Phase, #st{fds=Fds}=St) -> + case gb_trees:lookup(Tag, Fds) of + none when Phase /= started -> + buffer; + none -> + S = io_lib:format("\n*** ERROR: ~w, line ~w: No known '~tp' log file\n", + [?MODULE,?LINE,Tag]), + do_output(stdout, [S,Str], Phase, St); + {value,Fd} -> + try + io:put_chars(Fd, Str), + case Tag of + html -> finalise_table(Fd, St); + _ -> ok + end + catch _:Error -> + S = io_lib:format("\n*** ERROR: ~w, line ~w: Error writing to " + "log file '~tp': ~tp\n", + [?MODULE,?LINE,Tag,Error]), + do_output(stdout, [S,Str], Phase, St) + end + end. + +finalise_table(Fd, #st{html_footer=Footer}) -> + case file:position(Fd, {cur,0}) of + {ok,Pos} -> + %% We are writing to a seekable file. Finalise so + %% we get complete valid (and viewable) HTML code. + %% Then rewind to overwrite the finalising code. + io:put_chars(Fd, ["\n</table>\n",Footer]), + file:position(Fd, Pos); + {error,epipe} -> + %% The file is not seekable. We cannot erase what + %% we've already written --- so the reader will + %% have to wait until we're done. + ok + end. + +do_print_buffered(Q0, St) -> + Item = queue:get(Q0), + Q = queue:drop(Q0), + case Item of + eot -> + Q; + {Tag,Str} -> + _ = do_output(Tag, Str, undefined, St), + do_print_buffered(Q, St) + end. + +gc(#st{gls=Gls0}) -> + InUse0 = [begin + case process_info(P, group_leader) of + {group_leader,GL} -> GL; + undefined -> undefined + end + end || P <- processes()], + InUse = ordsets:from_list(InUse0), + Gls = gb_sets:to_list(Gls0), + NotUsed = ordsets:subtract(Gls, InUse), + _ = [test_server_gl:stop(Pid) || Pid <- NotUsed], + ok. diff --git a/lib/common_test/src/test_server_node.erl b/lib/common_test/src/test_server_node.erl new file mode 100644 index 0000000000..b3b6ae3d92 --- /dev/null +++ b/lib/common_test/src/test_server_node.erl @@ -0,0 +1,767 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2002-2017. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +-module(test_server_node). +-compile(r16). + +%%% +%%% The same compiled code for this module must be possible to load +%%% in R16B and later. +%%% + +%% Test Controller interface +-export([is_release_available/1]). +-export([start_tracer_node/2,trace_nodes/2,stop_tracer_node/1]). +-export([start_node/5, stop_node/1]). +-export([kill_nodes/0, nodedown/1]). +%% Internal export +-export([node_started/1,trc/1,handle_debug/4]). + +-include("test_server_internal.hrl"). +-record(slave_info, {name,socket,client}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% %%% +%%% All code in this module executes on the test_server_ctrl process %%% +%%% except for node_started/1 and trc/1 which execute on a new node. %%% +%%% %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +is_release_available(Rel) when is_atom(Rel) -> + is_release_available(atom_to_list(Rel)); +is_release_available(Rel) -> + case os:type() of + {unix,_} -> + Erl = find_release(Rel), + case Erl of + none -> false; + _ -> filelib:is_regular(Erl) + end; + _ -> + false + end. + +nodedown(Sock) -> + Match = #slave_info{name='$1',socket=Sock,client='$2',_='_'}, + case ets:match(slave_tab,Match) of + [[Node,_Client]] -> % Slave node died + gen_tcp:close(Sock), + ets:delete(slave_tab,Node), + slave_died; + [] -> + ok + end. + + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Start trace node +%%% +start_tracer_node(TraceFile,TI) -> + Match = #slave_info{name='$1',_='_'}, + SlaveNodes = lists:map(fun([N]) -> [" ",N] end, + ets:match(slave_tab,Match)), + TargetNode = node(), + Cookie = TI#target_info.cookie, + {ok,LSock} = gen_tcp:listen(0,[binary,{reuseaddr,true},{packet,2}]), + {ok,TracePort} = inet:port(LSock), + Prog = quote_progname(pick_erl_program(default)), + Cmd = lists:concat([Prog, " -sname tracer -hidden -setcookie ", Cookie, + " -s ", ?MODULE, " trc ", TraceFile, " ", + TracePort, " ", TI#target_info.os_family]), + spawn(fun() -> print_data(open_port({spawn,Cmd},[stream])) end), +%! open_port({spawn,Cmd},[stream]), + case gen_tcp:accept(LSock,?ACCEPT_TIMEOUT) of + {ok,Sock} -> + gen_tcp:close(LSock), + receive + {tcp,Sock,Result} when is_binary(Result) -> + case unpack(Result) of + error -> + gen_tcp:close(Sock), + {error,timeout}; + {ok,started} -> + trace_nodes(Sock,[TargetNode | SlaveNodes]), + {ok,Sock}; + {ok,Error} -> Error + end; + {tcp_closed,Sock} -> + gen_tcp:close(Sock), + {error,could_not_start_tracernode} + after ?ACCEPT_TIMEOUT -> + gen_tcp:close(Sock), + {error,timeout} + end; + Error -> + gen_tcp:close(LSock), + {error,{could_not_start_tracernode,Error}} + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Start a tracer on each of these nodes and set flags and patterns +%%% +trace_nodes(Sock,Nodes) -> + Bin = term_to_binary({add_nodes,Nodes}), + ok = gen_tcp:send(Sock, tag_trace_message(Bin)), + receive_ack(Sock). + + +receive_ack(Sock) -> + receive + {tcp,Sock,Bin} when is_binary(Bin) -> + case unpack(Bin) of + error -> receive_ack(Sock); + {ok,_} -> ok + end; + _ -> + receive_ack(Sock) + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Stop trace node +%%% +stop_tracer_node(Sock) -> + Bin = term_to_binary(id(stop)), + ok = gen_tcp:send(Sock, tag_trace_message(Bin)), + receive {tcp_closed,Sock} -> gen_tcp:close(Sock) end, + ok. + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% trc([TraceFile,Nodes]) -> ok +%% +%% Start tracing on the given nodes +%% +%% This function executes on the new node +%% +trc([TraceFile, PortAtom, Type]) -> + {Result,Patterns} = + case file:consult(TraceFile) of + {ok,TI} -> + Pat = parse_trace_info(lists:flatten(TI)), + {started,Pat}; + Error -> + {Error,[]} + end, + Port = list_to_integer(atom_to_list(PortAtom)), + case catch gen_tcp:connect("localhost", Port, [binary, + {reuseaddr,true}, + {packet,2}]) of + {ok,Sock} -> + BinResult = term_to_binary(Result), + ok = gen_tcp:send(Sock,tag_trace_message(BinResult)), + trc_loop(Sock,Patterns,Type); + _else -> + ok + end, + erlang:halt(). +trc_loop(Sock,Patterns,Type) -> + receive + {tcp,Sock,Bin} -> + case unpack(Bin) of + error -> + ttb:stop(), + gen_tcp:close(Sock); + {ok,{add_nodes,Nodes}} -> + add_nodes(Nodes,Patterns,Type), + Bin = term_to_binary(id(ok)), + ok = gen_tcp:send(Sock, tag_trace_message(Bin)), + trc_loop(Sock,Patterns,Type); + {ok,stop} -> + ttb:stop(), + gen_tcp:close(Sock) + end; + {tcp_closed,Sock} -> + ttb:stop(), + gen_tcp:close(Sock) + end. +add_nodes(Nodes,Patterns,_Type) -> + {ok, _} = ttb:tracer(Nodes,[{file,{local, test_server}}, + {handler, {{?MODULE,handle_debug},initial}}]), + {ok, _} = ttb:p(all,[call,timestamp]), + lists:foreach(fun({TP,M,F,A,Pat}) -> ttb:TP(M,F,A,Pat); + ({CTP,M,F,A}) -> ttb:CTP(M,F,A) + end, + Patterns). + +parse_trace_info([{TP,M,Pat}|Pats]) when TP=:=tp; TP=:=tpl -> + [{TP,M,'_','_',Pat}|parse_trace_info(Pats)]; +parse_trace_info([{TP,M,F,Pat}|Pats]) when TP=:=tp; TP=:=tpl -> + [{TP,M,F,'_',Pat}|parse_trace_info(Pats)]; +parse_trace_info([{TP,M,F,A,Pat}|Pats]) when TP=:=tp; TP=:=tpl -> + [{TP,M,F,A,Pat}|parse_trace_info(Pats)]; +parse_trace_info([CTP|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg -> + [{CTP,'_','_','_'}|parse_trace_info(Pats)]; +parse_trace_info([{CTP,M}|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg -> + [{CTP,M,'_','_'}|parse_trace_info(Pats)]; +parse_trace_info([{CTP,M,F}|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg -> + [{CTP,M,F,'_'}|parse_trace_info(Pats)]; +parse_trace_info([{CTP,M,F,A}|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg -> + [{CTP,M,F,A}|parse_trace_info(Pats)]; +parse_trace_info([]) -> + []; +parse_trace_info([_other|Pats]) -> % ignore + parse_trace_info(Pats). + +handle_debug(Out,Trace,TI,initial) -> + handle_debug(Out,Trace,TI,0); +handle_debug(_Out,end_of_trace,_TI,N) -> + N; +handle_debug(Out,Trace,_TI,N) -> + print_trc(Out,Trace,N), + N+1. + +print_trc(Out,{trace_ts,P,call,{M,F,A},C,Ts},N) -> + io:format(Out, + "~w: ~s~n" + "Process : ~w~n" + "Call : ~w:~tw/~w~n" + "Arguments : ~tp~n" + "Caller : ~tw~n~n", + [N,ts(Ts),P,M,F,length(A),A,C]); +print_trc(Out,{trace_ts,P,call,{M,F,A},Ts},N) -> + io:format(Out, + "~w: ~s~n" + "Process : ~w~n" + "Call : ~w:~tw/~w~n" + "Arguments : ~tp~n~n", + [N,ts(Ts),P,M,F,length(A),A]); +print_trc(Out,{trace_ts,P,return_from,{M,F,A},R,Ts},N) -> + io:format(Out, + "~w: ~s~n" + "Process : ~w~n" + "Return from : ~w:~tw/~w~n" + "Return value : ~tp~n~n", + [N,ts(Ts),P,M,F,A,R]); +print_trc(Out,{drop,X},N) -> + io:format(Out, + "~w: Tracer dropped ~w messages - too busy~n~n", + [N,X]); +print_trc(Out,Trace,N) -> + Ts = element(size(Trace),Trace), + io:format(Out, + "~w: ~s~n" + "Trace : ~tp~n~n", + [N,ts(Ts),Trace]). +ts({_, _, Micro} = Now) -> + {{Y,M,D},{H,Min,S}} = calendar:now_to_local_time(Now), + io_lib:format("~4.4.0w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w,~6.6.0w", + [Y,M,D,H,Min,S,Micro]). + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Start slave/peer nodes (initiated by test_server:start_node/5) +%%% +start_node(SlaveName, slave, Options, From, TI) when is_list(SlaveName) -> + start_node_slave(list_to_atom(SlaveName), Options, From, TI); +start_node(SlaveName, slave, Options, From, TI) -> + start_node_slave(SlaveName, Options, From, TI); +start_node(SlaveName, peer, Options, From, TI) when is_atom(SlaveName) -> + start_node_peer(atom_to_list(SlaveName), Options, From, TI); +start_node(SlaveName, peer, Options, From, TI) -> + start_node_peer(SlaveName, Options, From, TI); +start_node(_SlaveName, _Type, _Options, _From, _TI) -> + not_implemented_yet. + +%% +%% Peer nodes are always started on the same host as test_server_ctrl +%% +%% (Socket communication is used since in early days the test target +%% and the test server controller node could be on different hosts and +%% the target could not know the controller node via erlang +%% distribution) +%% +start_node_peer(SlaveName, OptList, From, TI) -> + SuppliedArgs = start_node_get_option_value(args, OptList, []), + Cleanup = start_node_get_option_value(cleanup, OptList, true), + HostStr = test_server_sup:hoststr(), + {ok,LSock} = gen_tcp:listen(0,[binary, + {reuseaddr,true}, + {packet,2}]), + {ok,WaitPort} = inet:port(LSock), + NodeStarted = lists:concat([" -s ", ?MODULE, " node_started ", + HostStr, " ", WaitPort]), + + % Support for erl_crash_dump files.. + CrashDir = test_server_sup:crash_dump_dir(), + CrashFile = filename:join([CrashDir, + "erl_crash_dump."++cast_to_list(SlaveName)]), + CrashArgs = lists:concat([" -env ERL_CRASH_DUMP \"",CrashFile,"\" "]), + FailOnError = start_node_get_option_value(fail_on_error, OptList, true), + Prog0 = start_node_get_option_value(erl, OptList, default), + Prog = quote_progname(pick_erl_program(Prog0)), + Args = + case string:str(SuppliedArgs,"-setcookie") of + 0 -> "-setcookie " ++ TI#target_info.cookie ++ " " ++ SuppliedArgs; + _ -> SuppliedArgs + end, + Cmd = lists:concat([Prog, + " -detached ", + TI#target_info.naming, " ", SlaveName, + NodeStarted, + CrashArgs, + " ", Args]), + Opts = case start_node_get_option_value(env, OptList, []) of + [] -> []; + Env -> [{env, Env}] + end, + %% peer is always started on localhost + %% + %% Bad environment can cause open port to fail. If this happens, + %% we ignore it and let the testcase handle the situation... + catch open_port({spawn, Cmd}, [stream|Opts]), + + Tmo = 60000 * test_server:timetrap_scale_factor(), + + case start_node_get_option_value(wait, OptList, true) of + true -> + Ret = wait_for_node_started(LSock,Tmo,undefined,Cleanup,TI,self()), + case {Ret,FailOnError} of + {{{ok, Node}, Warning},_} -> + gen_server:reply(From,{{ok,Node},HostStr,Cmd,[],Warning}); + {_,false} -> + gen_server:reply(From,{Ret, HostStr, Cmd}); + {_,true} -> + gen_server:reply(From,{fail,{Ret, HostStr, Cmd}}) + end; + false -> + Nodename = list_to_atom(SlaveName ++ "@" ++ HostStr), + I = "=== Not waiting for node", + gen_server:reply(From,{{ok, Nodename}, HostStr, Cmd, I, []}), + Self = self(), + spawn_link(wait_for_node_started_fun(LSock,Tmo,Cleanup,TI,Self)), + ok + end. + +-spec wait_for_node_started_fun(_, _, _, _, _) -> fun(() -> no_return()). +wait_for_node_started_fun(LSock, Tmo, Cleanup, TI, Self) -> + fun() -> + {{ok, _}, _} = wait_for_node_started(LSock,Tmo,undefined, + Cleanup,TI,Self), + receive after infinity -> ok end + end. + +%% +%% Slave nodes are started on a remote host if +%% - the option remote is given when calling test_server:start_node/3 +%% +start_node_slave(SlaveName, OptList, From, _TI) -> + SuppliedArgs = start_node_get_option_value(args, OptList, []), + Cleanup = start_node_get_option_value(cleanup, OptList, true), + + CrashDir = test_server_sup:crash_dump_dir(), + CrashFile = filename:join([CrashDir, + "erl_crash_dump."++cast_to_list(SlaveName)]), + CrashArgs = lists:concat([" -env ERL_CRASH_DUMP \"",CrashFile,"\" "]), + Args = lists:concat([" ", SuppliedArgs, CrashArgs]), + + Prog0 = start_node_get_option_value(erl, OptList, default), + Prog = pick_erl_program(Prog0), + Ret = + case start_which_node(OptList) of + {error,Reason} -> {{error,Reason},undefined,undefined}; + Host0 -> do_start_node_slave(Host0,SlaveName,Args,Prog,Cleanup) + end, + gen_server:reply(From,Ret). + + +do_start_node_slave(Host0, SlaveName, Args, Prog, Cleanup) -> + Host = + case Host0 of + local -> test_server_sup:hoststr(); + _ -> cast_to_list(Host0) + end, + Cmd = Prog ++ " " ++ Args, + case slave:start(Host, SlaveName, Args, no_link, Prog) of + {ok,Nodename} -> + case Cleanup of + true -> ets:insert(slave_tab,#slave_info{name=Nodename}); + false -> ok + end, + {{ok,Nodename}, Host, Cmd, [], []}; + Ret -> + {Ret, Host, Cmd} + end. + + +wait_for_node_started(LSock,Timeout,Client,Cleanup,TI,CtrlPid) -> + case gen_tcp:accept(LSock,Timeout) of + {ok,Sock} -> + gen_tcp:close(LSock), + receive + {tcp,Sock,Started0} when is_binary(Started0) -> + case unpack(Started0) of + error -> + gen_tcp:close(Sock), + {error, connection_closed}; + {ok,Started} -> + Version = TI#target_info.otp_release, + VsnStr = TI#target_info.system_version, + {ok,Nodename, W} = + handle_start_node_return(Version, + VsnStr, + Started), + case Cleanup of + true -> + ets:insert(slave_tab,#slave_info{name=Nodename, + socket=Sock, + client=Client}); + false -> ok + end, + ok = gen_tcp:controlling_process(Sock,CtrlPid), + test_server_ctrl:node_started(Nodename), + {{ok,Nodename},W} + end; + {tcp_closed,Sock} -> + gen_tcp:close(Sock), + {error, connection_closed} + after Timeout -> + gen_tcp:close(Sock), + {error, timeout} + end; + {error,Reason} -> + gen_tcp:close(LSock), + {error, {no_connection,Reason}} + end. + + + +handle_start_node_return(Version,VsnStr,{started, Node, Version, VsnStr}) -> + {ok, Node, []}; +handle_start_node_return(Version,VsnStr,{started, Node, OVersion, OVsnStr}) -> + Str = io_lib:format("WARNING: Started node " + "reports different system " + "version than current node! " + "Current node version: ~p, ~p " + "Started node version: ~p, ~p", + [Version, VsnStr, + OVersion, OVsnStr]), + Str1 = lists:flatten(Str), + {ok, Node, Str1}. + + +%% +%% This function executes on the new node +%% +node_started([Host,PortAtom]) -> + %% Must spawn a new process because the boot process should not + %% hang forever!! + spawn(node_started_fun(Host,PortAtom)). + +-spec node_started_fun(_, _) -> fun(() -> no_return()). +node_started_fun(Host,PortAtom) -> + fun() -> node_started(Host,PortAtom) end. + +%% This process hangs forever, just waiting for the socket to be +%% closed and terminating the node +node_started(Host,PortAtom) -> + {_, Version} = init:script_id(), + VsnStr = erlang:system_info(system_version), + Port = list_to_integer(atom_to_list(PortAtom)), + case catch gen_tcp:connect(Host,Port, [binary, + {reuseaddr,true}, + {packet,2}]) of + + {ok,Sock} -> + Started = term_to_binary({started, node(), Version, VsnStr}), + ok = gen_tcp:send(Sock, tag_trace_message(Started)), + receive _Anyting -> + gen_tcp:close(Sock), + erlang:halt() + end; + _else -> + erlang:halt() + end. + + +-compile({inline, [tag_trace_message/1]}). +-dialyzer({no_improper_lists, tag_trace_message/1}). +tag_trace_message(M) -> + [1|M]. + +% start_which_node(Optlist) -> hostname +start_which_node(Optlist) -> + case start_node_get_option_value(remote, Optlist) of + undefined -> + local; + true -> + case find_remote_host() of + {error, Other} -> + {error, Other}; + RHost -> + RHost + end + end. + +find_remote_host() -> + HostList=test_server_ctrl:get_hosts(), + case lists:delete(test_server_sup:hoststr(), HostList) of + [] -> + {error, no_remote_hosts}; + [RHost|_Rest] -> + RHost + end. + +start_node_get_option_value(Key, List) -> + start_node_get_option_value(Key, List, undefined). + +start_node_get_option_value(Key, List, Default) -> + case lists:keysearch(Key, 1, List) of + {value, {Key, Value}} -> + Value; + false -> + Default + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% stop_node(Name) -> ok | {error,Reason} +%% +%% Clean up - test_server will stop this node +stop_node(Name) -> + case ets:lookup(slave_tab,Name) of + [#slave_info{}] -> + ets:delete(slave_tab,Name), + ok; + [] -> + {error, not_a_slavenode} + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% kill_nodes() -> ok +%% +%% Brutally kill all slavenodes that were not stopped by test_server +kill_nodes() -> + case ets:match_object(slave_tab,'_') of + [] -> []; + List -> + lists:map(fun(SI) -> kill_node(SI) end, List) + end. + +kill_node(SI) -> + Name = SI#slave_info.name, + ets:delete(slave_tab,Name), + case SI#slave_info.socket of + undefined -> + catch rpc:call(Name,erlang,halt,[]); + Sock -> + gen_tcp:close(Sock) + end, + Name. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% cast_to_list(X) -> string() +%%% X = list() | atom() | void() +%%% Returns a string representation of whatever was input + +cast_to_list(X) when is_list(X) -> X; +cast_to_list(X) when is_atom(X) -> atom_to_list(X); +cast_to_list(X) -> lists:flatten(io_lib:format("~tw", [X])). + + +%%% L contains elements of the forms +%%% {prog, String} +%%% {release, Rel} where Rel = String | latest | previous +%%% this +%%% +pick_erl_program(default) -> + cast_to_list(lib:progname()); +pick_erl_program(L) -> + P = random_element(L), + case P of + {prog, S} -> + S; + {release, S} -> + find_release(S); + this -> + cast_to_list(lib:progname()) + end. + +%% This is an attempt to distinguish between spaces in the program +%% path and spaces that separate arguments. The program is quoted to +%% allow spaces in the path. +%% +%% Arguments could exist either if the executable is excplicitly given +%% ({prog,String}) or if the -program switch to beam is used and +%% includes arguments (typically done by cerl in OTP test environment +%% in order to ensure that slave/peer nodes are started with the same +%% emulator and flags as the test node. The return from lib:progname() +%% could then typically be '/<full_path_to>/cerl -gcov'). +quote_progname(Progname) -> + do_quote_progname(string:tokens(Progname," ")). + +do_quote_progname([Prog]) -> + "\""++Prog++"\""; +do_quote_progname([Prog,Arg|Args]) -> + case os:find_executable(Prog) of + false -> + do_quote_progname([Prog++" "++Arg | Args]); + _ -> + %% this one has an executable - we assume the rest are arguments + "\""++Prog++"\""++ + lists:flatten(lists:map(fun(X) -> [" ",X] end, [Arg|Args])) + end. + +random_element(L) -> + lists:nth(rand:uniform(length(L)), L). + +find_release(latest) -> + "/usr/local/otp/releases/latest/bin/erl"; +find_release(previous) -> + "kaka"; +find_release(Rel) -> + find_release(os:type(), Rel). + +find_release({unix,sunos}, Rel) -> + case os:cmd("uname -p") of + "sparc" ++ _ -> + "/usr/local/otp/releases/otp_beam_solaris8_" ++ Rel ++ "/bin/erl"; + _ -> + none + end; +find_release({unix,linux}, Rel) -> + Candidates = find_rel_linux(Rel), + case lists:dropwhile(fun(N) -> + not filelib:is_regular(N) + end, Candidates) of + [] -> none; + [Erl|_] -> Erl + end; +find_release(_, _) -> none. + +find_rel_linux(Rel) -> + case suse_release() of + none -> []; + SuseRel -> find_rel_suse(Rel, SuseRel) + end. + +find_rel_suse(Rel, SuseRel) -> + Root = "/usr/local/otp/releases/sles", + case SuseRel of + "11" -> + %% Try both SuSE 11, SuSE 10 and SuSe 9 in that order. + find_rel_suse_1(Rel, Root++"11") ++ + find_rel_suse_1(Rel, Root++"10") ++ + find_rel_suse_1(Rel, Root++"9"); + "10" -> + %% Try both SuSE 10 and SuSe 9 in that order. + find_rel_suse_1(Rel, Root++"10") ++ + find_rel_suse_1(Rel, Root++"9"); + "9" -> + find_rel_suse_1(Rel, Root++"9"); + _ -> + [] + end. + +find_rel_suse_1(Rel, RootWc) -> + case erlang:system_info(wordsize) of + 4 -> + find_rel_suse_2(Rel, RootWc++"_32"); + 8 -> + find_rel_suse_2(Rel, RootWc++"_64") ++ + find_rel_suse_2(Rel, RootWc++"_32") + end. + +find_rel_suse_2(Rel, RootWc) -> + RelDir = filename:dirname(RootWc), + Pat = filename:basename(RootWc ++ "_" ++ Rel) ++ ".*", + case file:list_dir(RelDir) of + {ok,Dirs} -> + case lists:filter(fun(Dir) -> + case re:run(Dir, Pat, [unicode]) of + nomatch -> false; + _ -> true + end + end, Dirs) of + [] -> + []; + [R|_] -> + [filename:join([RelDir,R,"bin","erl"])] + end; + _ -> + [] + end. + +%% suse_release() -> VersionString | none. +%% Return the major SuSE version number for this platform or +%% 'none' if this is not a SuSE platform. +suse_release() -> + case file:open("/etc/SuSE-release", [read]) of + {ok,Fd} -> + try + suse_release(Fd) + after + file:close(Fd) + end; + {error,_} -> none + end. + +suse_release(Fd) -> + case io:get_line(Fd, '') of + eof -> none; + Line when is_list(Line) -> + case re:run(Line, "^VERSION\\s*=\\s*(\\d+)\s*", + [{capture,all_but_first,list}]) of + nomatch -> + suse_release(Fd); + {match,[Version]} -> + Version + end + end. + +unpack(Bin) -> + {One,Term} = split_binary(Bin, 1), + case binary_to_list(One) of + [1] -> + case catch {ok,binary_to_term(Term)} of + {'EXIT',_} -> error; + {ok,_}=Res -> Res + end; + _ -> error + end. + +id(I) -> I. + +print_data(Port) -> + ct_util:mark_process(), + receive + {Port, {data, Bytes}} -> + io:put_chars(Bytes), + print_data(Port); + {Port, eof} -> + Port ! {self(), close}, + receive + {Port, closed} -> + true + end, + receive + {'EXIT', Port, _} -> + ok + after 1 -> % force context switch + ok + end + end. diff --git a/lib/common_test/src/test_server_sup.erl b/lib/common_test/src/test_server_sup.erl new file mode 100644 index 0000000000..6ddbf1ad27 --- /dev/null +++ b/lib/common_test/src/test_server_sup.erl @@ -0,0 +1,947 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1998-2017. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%%%------------------------------------------------------------------- +%%% Purpose: Test server support functions. +%%%------------------------------------------------------------------- +-module(test_server_sup). +-export([timetrap/2, timetrap/3, timetrap/4, + timetrap_cancel/1, capture_get/1, messages_get/1, + timecall/3, call_crash/5, app_test/2, check_new_crash_dumps/0, + cleanup_crash_dumps/0, crash_dump_dir/0, tar_crash_dumps/0, + get_username/0, get_os_family/0, + hostatom/0, hostatom/1, hoststr/0, hoststr/1, + framework_call/2,framework_call/3,framework_call/4, + format_loc/1, + util_start/0, util_stop/0, unique_name/0, + call_trace/1, + appup_test/1]). +-include("test_server_internal.hrl"). +-define(crash_dump_tar,"crash_dumps.tar.gz"). +-define(src_listing_ext, ".src.html"). +-record(util_state, {starter, latest_name}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timetrap(Timeout,Scale,Pid) -> Handle +%% Handle = term() +%% +%% Creates a time trap, that will kill the given process if the +%% trap is not cancelled with timetrap_cancel/1, within Timeout +%% milliseconds. +%% Scale says if the time should be scaled up to compensate for +%% delays during the test (e.g. if cover is running). + +timetrap(Timeout0, Pid) -> + timetrap(Timeout0, Timeout0, true, Pid). + +timetrap(Timeout0, Scale, Pid) -> + timetrap(Timeout0, Timeout0, Scale, Pid). + +timetrap(Timeout0, ReportTVal, Scale, Pid) -> + process_flag(priority, max), + ct_util:mark_process(), + Timeout = if not Scale -> Timeout0; + true -> test_server:timetrap_scale_factor() * Timeout0 + end, + TruncTO = trunc(Timeout), + receive + after TruncTO -> + kill_the_process(Pid, Timeout0, TruncTO, ReportTVal) + end. + +kill_the_process(Pid, Timeout0, TruncTO, ReportTVal) -> + case is_process_alive(Pid) of + true -> + TimeToReport = if Timeout0 == ReportTVal -> TruncTO; + true -> ReportTVal end, + MFLs = test_server:get_loc(Pid), + Mon = erlang:monitor(process, Pid), + Trap = {timetrap_timeout,TimeToReport,MFLs}, + exit(Pid, Trap), + receive + {'DOWN', Mon, process, Pid, _} -> + ok + after 10000 -> + %% Pid is probably trapping exits, hit it harder... + catch error_logger:warning_msg( + "Testcase process ~w not " + "responding to timetrap " + "timeout:~n" + " ~tp.~n" + "Killing testcase...~n", + [Pid, Trap]), + exit(Pid, kill) + end; + false -> + ok + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% timetrap_cancel(Handle) -> ok +%% Handle = term() +%% +%% Cancels a time trap. +timetrap_cancel(Handle) -> + unlink(Handle), + MonRef = erlang:monitor(process, Handle), + exit(Handle, kill), + receive {'DOWN',MonRef,_,_,_} -> ok + after + 2000 -> + erlang:demonitor(MonRef, [flush]), + ok + end. + +capture_get(Msgs) -> + receive + {captured,Msg} -> + capture_get([Msg|Msgs]) + after 0 -> + lists:reverse(Msgs) + end. + +messages_get(Msgs) -> + receive + Msg -> + messages_get([Msg|Msgs]) + after 0 -> + lists:reverse(Msgs) + end. + +timecall(M, F, A) -> + {Elapsed, Val} = timer:tc(M, F, A), + {Elapsed / 1000000, Val}. + + +call_crash(Time,Crash,M,F,A) -> + OldTrapExit = process_flag(trap_exit,true), + Pid = spawn_link(M,F,A), + Answer = + receive + {'EXIT',Crash} -> + ok; + {'EXIT',Pid,Crash} -> + ok; + {'EXIT',_Reason} when Crash==any -> + ok; + {'EXIT',Pid,_Reason} when Crash==any -> + ok; + {'EXIT',Reason} -> + test_server:format(12, "Wrong crash reason. Wanted ~tp, got ~tp.", + [Crash, Reason]), + exit({wrong_crash_reason,Reason}); + {'EXIT',Pid,Reason} -> + test_server:format(12, "Wrong crash reason. Wanted ~tp, got ~tp.", + [Crash, Reason]), + exit({wrong_crash_reason,Reason}); + {'EXIT',OtherPid,Reason} when OldTrapExit == false -> + exit({'EXIT',OtherPid,Reason}) + after do_trunc(Time) -> + exit(call_crash_timeout) + end, + process_flag(trap_exit,OldTrapExit), + Answer. + +do_trunc(infinity) -> infinity; +do_trunc(T) -> trunc(T). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% app_test/2 +%% +%% Checks one applications .app file for obvious errors. +%% Checks.. +%% * .. required fields +%% * .. that all modules specified actually exists +%% * .. that all requires applications exists +%% * .. that no module included in the application has export_all +%% * .. that all modules in the ebin/ dir is included +%% (This only produce a warning, as all modules does not +%% have to be included (If the `pedantic' option isn't used)) +app_test(Application, Mode) -> + case is_app(Application) of + {ok, AppFile} -> + do_app_tests(AppFile, Application, Mode); + Error -> + test_server:fail(Error) + end. + +is_app(Application) -> + case file:consult(filename:join([code:lib_dir(Application),"ebin", + atom_to_list(Application)++".app"])) of + {ok, [{application, Application, AppFile}] } -> + {ok, AppFile}; + _ -> + test_server:format(minor, + "Application (.app) file not found, " + "or it has very bad syntax.~n"), + {error, not_an_application} + end. + + +do_app_tests(AppFile, AppName, Mode) -> + DictList= + [ + {missing_fields, []}, + {missing_mods, []}, + {superfluous_mods_in_ebin, []}, + {export_all_mods, []}, + {missing_apps, []} + ], + fill_dictionary(DictList), + + %% An appfile must (?) have some fields.. + check_fields([description, modules, registered, applications], AppFile), + + %% Check for missing and extra modules. + {value, {modules, Mods}}=lists:keysearch(modules, 1, AppFile), + EBinList=lists:sort(get_ebin_modnames(AppName)), + {Missing, Extra} = common(lists:sort(Mods), EBinList), + put(superfluous_mods_in_ebin, Extra), + put(missing_mods, Missing), + + %% Check that no modules in the application has export_all. + app_check_export_all(Mods), + + %% Check that all specified applications exists. + {value, {applications, Apps}}= + lists:keysearch(applications, 1, AppFile), + check_apps(Apps), + + A=check_dict(missing_fields, "Inconsistent app file, " + "missing fields"), + B=check_dict(missing_mods, "Inconsistent app file, " + "missing modules"), + C=check_dict_tolerant(superfluous_mods_in_ebin, "Inconsistent app file, " + "Modules not included in app file.", Mode), + D=check_dict(export_all_mods, "Inconsistent app file, " + "Modules have `export_all'."), + E=check_dict(missing_apps, "Inconsistent app file, " + "missing applications."), + + erase_dictionary(DictList), + case A+B+C+D+E of + 5 -> + ok; + _ -> + test_server:fail() + end. + +app_check_export_all([]) -> + ok; +app_check_export_all([Mod|Mods]) -> + case catch apply(Mod, module_info, [compile]) of + {'EXIT', {undef,_}} -> + app_check_export_all(Mods); + COpts -> + case lists:keysearch(options, 1, COpts) of + false -> + app_check_export_all(Mods); + {value, {options, List}} -> + case lists:member(export_all, List) of + true -> + put(export_all_mods, [Mod|get(export_all_mods)]), + app_check_export_all(Mods); + false -> + app_check_export_all(Mods) + end + end + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% appup_test/1 +%% +%% Checks one applications .appup file for obvious errors. +%% Checks.. +%% * .. syntax +%% * .. that version in app file matches appup file version +%% * .. validity of appup instructions +%% +%% For library application this function checks that the proper +%% 'restart_application' upgrade and downgrade clauses exist. +appup_test(Application) -> + case is_app(Application) of + {ok, AppFile} -> + case is_appup(Application, proplists:get_value(vsn, AppFile)) of + {ok, Up, Down} -> + StartMod = proplists:get_value(mod, AppFile), + Modules = proplists:get_value(modules, AppFile), + do_appup_tests(StartMod, Application, Up, Down, Modules); + Error -> + test_server:fail(Error) + end; + Error -> + test_server:fail(Error) + end. + +is_appup(Application, Version) -> + AppupFile = atom_to_list(Application) ++ ".appup", + AppupPath = filename:join([code:lib_dir(Application), "ebin", AppupFile]), + case file:consult(AppupPath) of + {ok, [{Version, Up, Down}]} when is_list(Up), is_list(Down) -> + {ok, Up, Down}; + _ -> + test_server:format( + minor, + "Application upgrade (.appup) file not found, " + "or it has very bad syntax.~n"), + {error, appup_not_readable} + end. + +do_appup_tests(undefined, Application, Up, Down, _Modules) -> + %% library application + case Up of + [{<<".*">>, [{restart_application, Application}]}] -> + case Down of + [{<<".*">>, [{restart_application, Application}]}] -> + ok; + _ -> + test_server:format( + minor, + "Library application needs restart_application " + "downgrade instruction.~n"), + {error, library_downgrade_instruction_malformed} + end; + _ -> + test_server:format( + minor, + "Library application needs restart_application " + "upgrade instruction.~n"), + {error, library_upgrade_instruction_malformed} + end; +do_appup_tests(_, _Application, Up, Down, Modules) -> + %% normal application + case check_appup_clauses_plausible(Up, up, Modules) of + ok -> + case check_appup_clauses_plausible(Down, down, Modules) of + ok -> + test_server:format(minor, "OK~n"); + Error -> + test_server:format(minor, "ERROR ~tp~n", [Error]), + test_server:fail(Error) + end; + Error -> + test_server:format(minor, "ERROR ~tp~n", [Error]), + test_server:fail(Error) + end. + +check_appup_clauses_plausible([], _Direction, _Modules) -> + ok; +check_appup_clauses_plausible([{Re, Instrs} | Rest], Direction, Modules) + when is_binary(Re) -> + case re:compile(Re,[unicode]) of + {ok, _} -> + case check_appup_instructions(Instrs, Direction, Modules) of + ok -> + check_appup_clauses_plausible(Rest, Direction, Modules); + Error -> + Error + end; + {error, Error} -> + {error, {version_regex_malformed, Re, Error}} + end; +check_appup_clauses_plausible([{V, Instrs} | Rest], Direction, Modules) + when is_list(V) -> + case check_appup_instructions(Instrs, Direction, Modules) of + ok -> + check_appup_clauses_plausible(Rest, Direction, Modules); + Error -> + Error + end; +check_appup_clauses_plausible(Clause, _Direction, _Modules) -> + {error, {clause_malformed, Clause}}. + +check_appup_instructions(Instrs, Direction, Modules) -> + case check_instructions(Direction, Instrs, Instrs, [], [], Modules) of + {_Good, []} -> + ok; + {_, Bad} -> + {error, {bad_instructions, Bad}} + end. + +check_instructions(_, [], _, Good, Bad, _) -> + {lists:reverse(Good), lists:reverse(Bad)}; +check_instructions(UpDown, [Instr | Rest], All, Good, Bad, Modules) -> + case catch check_instruction(UpDown, Instr, All, Modules) of + ok -> + check_instructions(UpDown, Rest, All, [Instr | Good], Bad, Modules); + {error, Reason} -> + NewBad = [{Instr, Reason} | Bad], + check_instructions(UpDown, Rest, All, Good, NewBad, Modules) + end. + +check_instruction(up, {add_module, Module}, _, Modules) -> + %% A new module is added + check_module(Module, Modules); +check_instruction(down, {add_module, Module}, _, Modules) -> + %% An old module is re-added + case (catch check_module(Module, Modules)) of + {error, {unknown_module, Module, Modules}} -> ok; + ok -> throw({error, {existing_readded_module, Module}}) + end; +check_instruction(_, {load_module, Module}, _, Modules) -> + check_module(Module, Modules); +check_instruction(_, {load_module, Module, DepMods}, _, Modules) -> + check_module(Module, Modules), + check_depend(DepMods); +check_instruction(_, {load_module, Module, Pre, Post, DepMods}, _, Modules) -> + check_module(Module, Modules), + check_depend(DepMods), + check_purge(Pre), + check_purge(Post); +check_instruction(up, {delete_module, Module}, _, Modules) -> + case (catch check_module(Module, Modules)) of + {error, {unknown_module, Module, Modules}} -> + ok; + ok -> + throw({error,{existing_module_deleted, Module}}) + end; +check_instruction(down, {delete_module, Module}, _, Modules) -> + check_module(Module, Modules); +check_instruction(_, {update, Module}, _, Modules) -> + check_module(Module, Modules); +check_instruction(_, {update, Module, supervisor}, _, Modules) -> + check_module(Module, Modules); +check_instruction(_, {update, Module, DepMods}, _, Modules) + when is_list(DepMods) -> + check_module(Module, Modules); +check_instruction(_, {update, Module, Change}, _, Modules) -> + check_module(Module, Modules), + check_change(Change); +check_instruction(_, {update, Module, Change, DepMods}, _, Modules) -> + check_module(Module, Modules), + check_change(Change), + check_depend(DepMods); +check_instruction(_, {update, Module, Change, Pre, Post, DepMods}, _, Modules) -> + check_module(Module, Modules), + check_change(Change), + check_purge(Pre), + check_purge(Post), + check_depend(DepMods); +check_instruction(_, + {update, Module, Timeout, Change, Pre, Post, DepMods}, + _, + Modules) -> + check_module(Module, Modules), + check_timeout(Timeout), + check_change(Change), + check_purge(Pre), + check_purge(Post), + check_depend(DepMods); +check_instruction(_, + {update, Module, ModType, Timeout, Change, Pre, Post, DepMods}, + _, + Modules) -> + check_module(Module, Modules), + check_mod_type(ModType), + check_timeout(Timeout), + check_change(Change), + check_purge(Pre), + check_purge(Post), + check_depend(DepMods); +check_instruction(_, {restart_application, Application}, _, _) -> + check_application(Application); +check_instruction(_, {remove_application, Application}, _, _) -> + check_application(Application); +check_instruction(_, {add_application, Application}, _, _) -> + check_application(Application); +check_instruction(_, {add_application, Application, Type}, _, _) -> + check_application(Application), + check_restart_type(Type); +check_instruction(_, Instr, _, _) -> + throw({error, {low_level_or_invalid_instruction, Instr}}). + +check_module(Module, Modules) -> + case {is_atom(Module), lists:member(Module, Modules)} of + {true, true} -> ok; + {true, false} -> throw({error, {unknown_module, Module}}); + {false, _} -> throw({error, {bad_module, Module}}) + end. + +check_application(App) -> + case is_atom(App) of + true -> ok; + false -> throw({error, {bad_application, App}}) + end. + +check_depend(Dep) when is_list(Dep) -> ok; +check_depend(Dep) -> throw({error, {bad_depend, Dep}}). + +check_restart_type(permanent) -> ok; +check_restart_type(transient) -> ok; +check_restart_type(temporary) -> ok; +check_restart_type(load) -> ok; +check_restart_type(none) -> ok; +check_restart_type(Type) -> throw({error, {bad_restart_type, Type}}). + +check_timeout(T) when is_integer(T), T > 0 -> ok; +check_timeout(default) -> ok; +check_timeout(infinity) -> ok; +check_timeout(T) -> throw({error, {bad_timeout, T}}). + +check_mod_type(static) -> ok; +check_mod_type(dynamic) -> ok; +check_mod_type(Type) -> throw({error, {bad_mod_type, Type}}). + +check_purge(soft_purge) -> ok; +check_purge(brutal_purge) -> ok; +check_purge(Purge) -> throw({error, {bad_purge, Purge}}). + +check_change(soft) -> ok; +check_change({advanced, _}) -> ok; +check_change(Change) -> throw({error, {bad_change, Change}}). + +%% Given two sorted lists, L1 and L2, returns {NotInL2, NotInL1}, +%% NotInL2 is the elements of L1 which don't occurr in L2, +%% NotInL1 is the elements of L2 which don't ocurr in L1. + +common(L1, L2) -> + common(L1, L2, [], []). + +common([X|Rest1], [X|Rest2], A1, A2) -> + common(Rest1, Rest2, A1, A2); +common([X|Rest1], [Y|Rest2], A1, A2) when X < Y -> + common(Rest1, [Y|Rest2], [X|A1], A2); +common([X|Rest1], [Y|Rest2], A1, A2) -> + common([X|Rest1], Rest2, A1, [Y|A2]); +common([], L, A1, A2) -> + {A1, L++A2}; +common(L, [], A1, A2) -> + {L++A1, A2}. + +check_apps([]) -> + ok; +check_apps([App|Apps]) -> + case is_app(App) of + {ok, _AppFile} -> + ok; + {error, _} -> + put(missing_apps, [App|get(missing_apps)]) + end, + check_apps(Apps). + +check_fields([], _AppFile) -> + ok; +check_fields([L|Ls], AppFile) -> + check_field(L, AppFile), + check_fields(Ls, AppFile). + +check_field(FieldName, AppFile) -> + case lists:keymember(FieldName, 1, AppFile) of + true -> + ok; + false -> + put(missing_fields, [FieldName|get(missing_fields)]), + ok + end. + +check_dict(Dict, Reason) -> + case get(Dict) of + [] -> + 1; % All ok. + List -> + io:format("** ~ts (~ts) ->~n~tp~n",[Reason, Dict, List]), + 0 + end. + +check_dict_tolerant(Dict, Reason, Mode) -> + case get(Dict) of + [] -> + 1; % All ok. + List -> + io:format("** ~ts (~ts) ->~n~tp~n",[Reason, Dict, List]), + case Mode of + pedantic -> + 0; + _ -> + 1 + end + end. + +get_ebin_modnames(AppName) -> + Wc=filename:join([code:lib_dir(AppName),"ebin", + "*"++code:objfile_extension()]), + TheFun=fun(X, Acc) -> + [list_to_atom(filename:rootname( + filename:basename(X)))|Acc] end, + _Files=lists:foldl(TheFun, [], filelib:wildcard(Wc)). + +%% +%% This function removes any erl_crash_dump* files found in the +%% test server directory. Done only once when the test server +%% is started. +%% +cleanup_crash_dumps() -> + Dir = crash_dump_dir(), + Dumps = filelib:wildcard(filename:join(Dir, "erl_crash_dump*")), + delete_files(Dumps). + +crash_dump_dir() -> + %% If no framework is known, then we use current working directory + %% - in most cases that will be the same as the default log + %% directory. + {ok,Dir} = test_server_sup:framework_call(get_log_dir,[],file:get_cwd()), + Dir. + +tar_crash_dumps() -> + Dir = crash_dump_dir(), + case filelib:wildcard(filename:join(Dir, "erl_crash_dump*")) of + [] -> {error,no_crash_dumps}; + Dumps -> + TarFileName = filename:join(Dir,?crash_dump_tar), + {ok,Tar} = erl_tar:open(TarFileName,[write,compressed]), + lists:foreach( + fun(File) -> + ok = erl_tar:add(Tar,File,filename:basename(File),[]) + end, + Dumps), + ok = erl_tar:close(Tar), + delete_files(Dumps), + {ok,TarFileName} + end. + + +check_new_crash_dumps() -> + Dir = crash_dump_dir(), + Dumps = filelib:wildcard(filename:join(Dir, "erl_crash_dump*")), + case length(Dumps) of + 0 -> + ok; + Num -> + test_server_ctrl:format(minor, + "Found ~w crash dumps:~n", [Num]), + append_files_to_logfile(Dumps), + delete_files(Dumps) + end. + +append_files_to_logfile([]) -> ok; +append_files_to_logfile([File|Files]) -> + NodeName=from($., File), + test_server_ctrl:format(minor, "Crash dump from node ~tp:~n",[NodeName]), + Fd=get(test_server_minor_fd), + case file:read_file(File) of + {ok, Bin} -> + case file:write(Fd, Bin) of + ok -> + ok; + {error,Error} -> + %% Write failed. The following io:format/3 will probably also + %% fail, but in that case it will throw an exception so that + %% we will be aware of the problem. + io:format(Fd, "Unable to write the crash dump " + "to this file: ~tp~n", [file:format_error(Error)]) + end; + _Error -> + io:format(Fd, "Failed to read: ~ts\n", [File]) + end, + append_files_to_logfile(Files). + +delete_files([]) -> ok; +delete_files([File|Files]) -> + io:format("Deleting file: ~ts~n", [File]), + case file:delete(File) of + {error, _} -> + case file:rename(File, File++".old") of + {error, Error} -> + io:format("Could neither delete nor rename file " + "~ts: ~ts.~n", [File, Error]); + _ -> + ok + end; + _ -> + ok + end, + delete_files(Files). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% erase_dictionary(Vars) -> ok +%% Vars = [atom(),...] +%% +%% Takes a list of dictionary keys, KeyVals, erases +%% each key and returns ok. +erase_dictionary([{Var, _Val}|Vars]) -> + erase(Var), + erase_dictionary(Vars); +erase_dictionary([]) -> + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% fill_dictionary(KeyVals) -> void() +%% KeyVals = [{atom(),term()},...] +%% +%% Takes each Key-Value pair, and inserts it in the process dictionary. +fill_dictionary([{Var,Val}|Vars]) -> + put(Var,Val), + fill_dictionary(Vars); +fill_dictionary([]) -> + []. + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% get_username() -> UserName +%% +%% Returns the current user +get_username() -> + getenv_any(["USER","USERNAME"]). + +getenv_any([Key|Rest]) -> + case catch os:getenv(Key) of + String when is_list(String) -> String; + false -> getenv_any(Rest) + end; +getenv_any([]) -> "". + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% get_os_family() -> OsFamily +%% +%% Returns the OS family +get_os_family() -> + {OsFamily,_OsName} = os:type(), + OsFamily. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% hostatom()/hostatom(Node) -> Host; atom() +%% hoststr() | hoststr(Node) -> Host; string() +%% +%% Returns the OS family +hostatom() -> + hostatom(node()). +hostatom(Node) -> + list_to_atom(hoststr(Node)). +hoststr() -> + hoststr(node()). +hoststr(Node) when is_atom(Node) -> + hoststr(atom_to_list(Node)); +hoststr(Node) when is_list(Node) -> + from($@, Node). + +from(H, [H | T]) -> T; +from(H, [_ | T]) -> from(H, T); +from(_H, []) -> []. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% framework_call(Callback,Func,Args,DefaultReturn) -> Return | DefaultReturn +%% +%% Calls the given Func in Callback +framework_call(Func,Args) -> + framework_call(Func,Args,ok). +framework_call(Func,Args,DefaultReturn) -> + CB = os:getenv("TEST_SERVER_FRAMEWORK"), + framework_call(CB,Func,Args,DefaultReturn). +framework_call(FW,_Func,_Args,DefaultReturn) + when FW =:= false; FW =:= "undefined" -> + DefaultReturn; +framework_call(Callback,Func,Args,DefaultReturn) -> + Mod = list_to_atom(Callback), + _ = case code:is_loaded(Mod) of + false -> code:load_file(Mod); + _ -> ok + end, + case erlang:function_exported(Mod,Func,length(Args)) of + true -> + EH = fun(Reason) -> exit({fw_error,{Mod,Func,Reason}}) end, + SetTcState = case Func of + end_tc -> true; + init_tc -> true; + _ -> false + end, + case SetTcState of + true -> + test_server:set_tc_state({framework,Mod,Func}); + false -> + ok + end, + ct_util:mark_process(), + try apply(Mod,Func,Args) of + Result -> + Result + catch + exit:Why -> + EH(Why); + error:Why -> + EH({Why,erlang:get_stacktrace()}); + throw:Why -> + EH(Why) + end; + false -> + DefaultReturn + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% format_loc(Loc) -> string() +%% +%% Formats the printout of the line of code read from +%% process dictionary (test_server_loc). Adds link to +%% correct line in source code. +format_loc([{Mod,Func,Line}]) -> + [format_loc1({Mod,Func,Line})]; +format_loc([{Mod,Func,Line}|Rest]) -> + ["[",format_loc1({Mod,Func,Line}),",\n"|format_loc1(Rest)]; +format_loc([{Mod,LineOrFunc}]) -> + format_loc({Mod,LineOrFunc}); +format_loc({Mod,Func}) when is_atom(Func) -> + io_lib:format("{~w,~tw}",[Mod,Func]); +format_loc(Loc) -> + io_lib:format("~tp",[Loc]). + +format_loc1([{Mod,Func,Line}]) -> + [" ",format_loc1({Mod,Func,Line}),"]"]; +format_loc1([{Mod,Func,Line}|Rest]) -> + [" ",format_loc1({Mod,Func,Line}),",\n"|format_loc1(Rest)]; +format_loc1({Mod,Func,Line}) -> + ModStr = atom_to_list(Mod), + case {lists:member(no_src, get(test_server_logopts)), + lists:reverse(ModStr)} of + {false,[$E,$T,$I,$U,$S,$_|_]} -> + Link = if is_integer(Line) -> + integer_to_list(Line); + Line == last_expr -> + list_to_atom(atom_to_list(Func)++"-last_expr"); + is_atom(Line) -> + atom_to_list(Line); + true -> + Line + end, + io_lib:format("{~w,~tw,<a href=\"~ts~ts#~ts\">~tw</a>}", + [Mod,Func, + test_server_ctrl:uri_encode(downcase(ModStr)), + ?src_listing_ext,Link,Line]); + _ -> + io_lib:format("{~w,~tw,~tw}",[Mod,Func,Line]) + end. + +downcase(S) -> downcase(S, []). +downcase([Uc|Rest], Result) when $A =< Uc, Uc =< $Z -> + downcase(Rest, [Uc-$A+$a|Result]); +downcase([C|Rest], Result) -> + downcase(Rest, [C|Result]); +downcase([], Result) -> + lists:reverse(Result). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% util_start() -> ok +%% +%% Start local utility process +util_start() -> + Starter = self(), + case whereis(?MODULE) of + undefined -> + spawn_link(fun() -> + register(?MODULE, self()), + put(app, common_test), + util_loop(#util_state{starter=Starter}) + end), + ok; + _Pid -> + ok + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% util_stop() -> ok +%% +%% Stop local utility process +util_stop() -> + try (?MODULE ! {self(),stop}) of + _ -> + receive {?MODULE,stopped} -> ok + after 5000 -> exit(whereis(?MODULE), kill) + end + catch + _:_ -> + ok + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% unique_name() -> string() +%% +unique_name() -> + ?MODULE ! {self(),unique_name}, + receive {?MODULE,Name} -> Name + after 5000 -> exit({?MODULE,no_util_process}) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% util_loop(State) -> ok +%% +util_loop(State) -> + receive + {From,unique_name} -> + Nr = erlang:unique_integer([positive]), + Name = integer_to_list(Nr), + if Name == State#util_state.latest_name -> + timer:sleep(1), + self() ! {From,unique_name}, + util_loop(State); + true -> + From ! {?MODULE,Name}, + util_loop(State#util_state{latest_name = Name}) + end; + {From,stop} -> + catch unlink(State#util_state.starter), + From ! {?MODULE,stopped}, + ok + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% call_trace(TraceSpecFile) -> ok +%% +%% Read terms on format {m,Mod} | {f,Mod,Func} +%% from TraceSpecFile and enable call trace for +%% specified functions. +call_trace(TraceSpec) -> + case catch try_call_trace(TraceSpec) of + {'EXIT',Reason} -> + erlang:display(Reason), + exit(Reason); + Ok -> + Ok + end. + +try_call_trace(TraceSpec) -> + case file:consult(TraceSpec) of + {ok,Terms} -> + dbg:tracer(), + %% dbg:p(self(), [p, m, sos, call]), + dbg:p(self(), [sos, call]), + lists:foreach(fun({m,M}) -> + case dbg:tpl(M,[{'_',[],[{return_trace}]}]) of + {error,What} -> exit({error,{tracing_failed,What}}); + _ -> ok + end; + ({f,M,F}) -> + case dbg:tpl(M,F,[{'_',[],[{return_trace}]}]) of + {error,What} -> exit({error,{tracing_failed,What}}); + _ -> ok + end; + (Huh) -> + exit({error,{unrecognized_trace_term,Huh}}) + end, Terms), + ok; + {_,Error} -> + exit({error,{tracing_failed,TraceSpec,Error}}) + end. + diff --git a/lib/common_test/src/unix_telnet.erl b/lib/common_test/src/unix_telnet.erl index e5b3058999..8ac467014c 100644 --- a/lib/common_test/src/unix_telnet.erl +++ b/lib/common_test/src/unix_telnet.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2014. All Rights Reserved. +%% Copyright Ericsson AB 2004-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -27,7 +27,8 @@ %%% {port,PortNum}, % optional %%% {username,UserName}, %%% {password,Password}, -%%% {keep_alive,Bool}]}. % optional</pre> +%%% {keep_alive,Bool}, % optional +%%% {tcp_nodely,Bool}]} % optional</pre> %%% %%% <p>To communicate via telnet to the host specified by %%% <code>HostNameOrIpAddress</code>, use the interface functions in @@ -52,10 +53,8 @@ %%% @see ct_telnet -module(unix_telnet). --compile(export_all). - %% Callbacks for ct_telnet.erl --export([connect/6,get_prompt_regexp/0]). +-export([connect/7,get_prompt_regexp/0]). -import(ct_telnet,[start_gen_log/1,log/4,end_gen_log/0]). -define(username,"login: "). @@ -82,6 +81,7 @@ get_prompt_regexp() -> %%% Port = integer() %%% Timeout = integer() %%% KeepAlive = bool() +%%% TCPNoDelay = bool() %%% Extra = ct:target_name() | {Username,Password} %%% Username = string() %%% Password = string() @@ -91,25 +91,25 @@ get_prompt_regexp() -> %%% @doc Callback for ct_telnet.erl. %%% %%% <p>Setup telnet connection to a unix host.</p> -connect(ConnName,Ip,Port,Timeout,KeepAlive,Extra) -> +connect(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay,Extra) -> case Extra of {Username,Password} -> - connect1(ConnName,Ip,Port,Timeout,KeepAlive, + connect1(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay, Username,Password); KeyOrName -> case get_username_and_password(KeyOrName) of {ok,{Username,Password}} -> - connect1(ConnName,Ip,Port,Timeout,KeepAlive, + connect1(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay, Username,Password); Error -> Error end end. -connect1(Name,Ip,Port,Timeout,KeepAlive,Username,Password) -> +connect1(Name,Ip,Port,Timeout,KeepAlive,TCPNoDelay,Username,Password) -> start_gen_log("unix_telnet connect"), Result = - case ct_telnet_client:open(Ip,Port,Timeout,KeepAlive,Name) of + case ct_telnet_client:open(Ip,Port,Timeout,KeepAlive,TCPNoDelay,Name) of {ok,Pid} -> case ct_telnet:silent_teln_expect(Name,Pid,[], [prompt],?prx,[]) of @@ -132,25 +132,25 @@ connect1(Name,Ip,Port,Timeout,KeepAlive,Username,Password) -> Prompt=/=?password -> {ok,Pid}; Error -> - log(Name,recv,"Password failed\n~p\n", + log(Name,recv,"Password failed\n~tp\n", [Error]), {error,Error} end; Error -> - log(Name,recv,"Login to ~p:~p failed\n~p\n",[Ip,Port,Error]), + log(Name,recv,"Login to ~p:~p failed\n~tp\n",[Ip,Port,Error]), {error,Error} end; {ok,[{prompt,_OtherPrompt1},{prompt,_OtherPrompt2}],_} -> {ok,Pid}; Error -> log(Name,conn_error, - "Did not get expected prompt from ~p:~p\n~p\n", + "Did not get expected prompt from ~p:~p\n~tp\n", [Ip,Port,Error]), {error,Error} end; Error -> log(Name,conn_error, - "Could not open telnet connection to ~p:~p\n~p\n", + "Could not open telnet connection to ~p:~p\n~tp\n", [Ip,Port,Error]), Error end, diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl index df434d62c8..83fcde2f48 100644 --- a/lib/common_test/src/vts.erl +++ b/lib/common_test/src/vts.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2012. All Rights Reserved. +%% Copyright Ericsson AB 2003-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -64,7 +64,7 @@ %%%----------------------------------------------------------------- %%% User API start() -> - ct_webtool:start(), + {ok, _} = ct_webtool:start(), ct_webtool:start_tools([],"app=vts"). init_data(ConfigFiles,EvHandlers,LogDir,LogOpts,Tests) -> @@ -157,6 +157,7 @@ test_info(_VtsPid,Type,Data) -> init(Parent) -> register(?MODULE,self()), process_flag(trap_exit,true), + ct_util:mark_process(), Parent ! {self(),started}, {ok,Cwd} = file:get_cwd(), InitState = #state{start_dir=Cwd}, @@ -169,7 +170,7 @@ loop(State) -> NewState = State#state{config=Config,event_handler=EvHandlers, current_log_dir=LogDir, logopts=LogOpts,tests=Tests}, - ct_install(NewState), + _ = ct_install(NewState), return(From,ok), loop(NewState); {start_page,From} -> @@ -192,12 +193,12 @@ loop(State) -> loop(State); {{add_config_file,Input},From} -> {Return,State1} = add_config_file1(Input,State), - ct_install(State1), + _ = ct_install(State1), return(From,Return), loop(State1); {{remove_config_file,Input},From} -> {Return,State1} = remove_config_file1(Input,State), - ct_install(State1), + _ = ct_install(State1), return(From,Return), loop(State1); {run_frame,From} -> @@ -233,7 +234,7 @@ loop(State) -> return(From,result_summary_frame1(State)), loop(State); stop_reload_results -> - file:set_cwd(State#state.start_dir), + ok = file:set_cwd(State#state.start_dir), loop(State#state{reload_results=false}); {no_result_log_frame,From} -> return(From,no_result_log_frame1()), @@ -250,7 +251,7 @@ loop(State) -> {'EXIT',Pid,Reason} -> case State#state.test_runner of Pid -> - io:format("Test run error: ~p\n",[Reason]), + io:format("Test run error: ~tp\n",[Reason]), loop(State); _ -> loop(State) @@ -277,13 +278,14 @@ call(Msg) -> end. return({To,Ref},Result) -> - To ! {Ref, Result}. - + To ! {Ref, Result}, + ok. run_test1(State=#state{tests=Tests,current_log_dir=LogDir, logopts=LogOpts}) -> Self=self(), RunTest = fun() -> + ct_util:mark_process(), case ct_run:do_run(Tests,[],LogDir,LogOpts) of {error,_Reason} -> aborted(); @@ -311,7 +313,6 @@ run_test1(State=#state{tests=Tests,current_log_dir=LogDir, ct_install(#state{config=Config,event_handler=EvHandlers, current_log_dir=LogDir}) -> ct_run:install([{config,Config},{event_handler,EvHandlers}],LogDir). - %%%----------------------------------------------------------------- %%% HTML start_page1() -> @@ -549,10 +550,10 @@ case_select(Dir,Suite,Case,N) -> end, case MakeResult of ok -> - code:add_pathz(Dir), + true = code:add_pathz(Dir), case catch apply(Suite,all,[]) of {'EXIT',Reason} -> - io:format("\n~p\n",[Reason]), + io:format("\n~tp\n",[Reason]), red(["COULD NOT READ TESTCASES!!",br(), "See erlang shell for info"]); {skip,_Reason} -> @@ -755,7 +756,7 @@ report1(tests_start,{TestName,_N},State) -> end, State#state{testruns=TestRuns}; report1(tests_done,{_Ok,_Fail,_Skip},State) -> - timer:send_after(5000, self(),stop_reload_results), + {ok, _} = timer:send_after(5000, self(),stop_reload_results), State#state{running=State#state.running-1,reload_results=true}; report1(tc_start,{_Suite,_Case},State) -> State; |