aboutsummaryrefslogtreecommitdiffstats
path: root/lib/common_test/src
diff options
context:
space:
mode:
authorSverker Eriksson <[email protected]>2017-08-30 20:55:08 +0200
committerSverker Eriksson <[email protected]>2017-08-30 20:55:08 +0200
commit7c67bbddb53c364086f66260701bc54a61c9659c (patch)
tree92ab0d4b91d5e2f6e7a3f9d61ea25089e8a71fe0 /lib/common_test/src
parent97dc5e7f396129222419811c173edc7fa767b0f8 (diff)
parent3b7a6ffddc819bf305353a593904cea9e932e7dc (diff)
downloadotp-7c67bbddb53c364086f66260701bc54a61c9659c.tar.gz
otp-7c67bbddb53c364086f66260701bc54a61c9659c.tar.bz2
otp-7c67bbddb53c364086f66260701bc54a61c9659c.zip
Merge tag 'OTP-19.0' into sverker/19/binary_to_atom-utf8-crash/ERL-474/OTP-14590
Diffstat (limited to 'lib/common_test/src')
-rw-r--r--lib/common_test/src/Makefile38
-rw-r--r--lib/common_test/src/common_test.app.src59
-rw-r--r--lib/common_test/src/common_test.appup.src23
-rw-r--r--lib/common_test/src/ct.erl176
-rw-r--r--lib/common_test/src/ct_config.erl72
-rw-r--r--lib/common_test/src/ct_config_plain.erl21
-rw-r--r--lib/common_test/src/ct_config_xml.erl21
-rw-r--r--lib/common_test/src/ct_conn_log_h.erl164
-rw-r--r--lib/common_test/src/ct_cover.erl198
-rw-r--r--lib/common_test/src/ct_event.erl27
-rw-r--r--lib/common_test/src/ct_framework.erl583
-rw-r--r--lib/common_test/src/ct_ftp.erl25
-rw-r--r--lib/common_test/src/ct_gen_conn.erl50
-rw-r--r--lib/common_test/src/ct_groups.erl158
-rw-r--r--lib/common_test/src/ct_hooks.erl97
-rw-r--r--lib/common_test/src/ct_hooks_lock.erl23
-rw-r--r--lib/common_test/src/ct_logs.erl638
-rw-r--r--lib/common_test/src/ct_make.erl28
-rw-r--r--lib/common_test/src/ct_master.erl70
-rw-r--r--lib/common_test/src/ct_master_event.erl25
-rw-r--r--lib/common_test/src/ct_master_logs.erl85
-rw-r--r--lib/common_test/src/ct_master_status.erl27
-rw-r--r--lib/common_test/src/ct_netconfc.erl459
-rw-r--r--lib/common_test/src/ct_netconfc.hrl25
-rw-r--r--lib/common_test/src/ct_property_test.erl189
-rw-r--r--lib/common_test/src/ct_release_test.erl962
-rw-r--r--lib/common_test/src/ct_repeat.erl27
-rw-r--r--lib/common_test/src/ct_rpc.erl29
-rw-r--r--lib/common_test/src/ct_run.erl730
-rw-r--r--lib/common_test/src/ct_slave.erl71
-rw-r--r--lib/common_test/src/ct_snmp.erl117
-rw-r--r--lib/common_test/src/ct_ssh.erl25
-rw-r--r--lib/common_test/src/ct_telnet.erl1080
-rw-r--r--lib/common_test/src/ct_telnet_client.erl225
-rw-r--r--lib/common_test/src/ct_testspec.erl176
-rw-r--r--lib/common_test/src/ct_util.erl165
-rw-r--r--lib/common_test/src/ct_util.hrl32
-rw-r--r--lib/common_test/src/ct_webtool.erl1213
-rw-r--r--lib/common_test/src/ct_webtool_sup.erl75
-rw-r--r--lib/common_test/src/cth_conn_log.erl103
-rw-r--r--lib/common_test/src/cth_log_redirect.erl224
-rw-r--r--lib/common_test/src/cth_surefire.erl45
-rw-r--r--lib/common_test/src/erl2html2.erl311
-rw-r--r--lib/common_test/src/test_server.erl2753
-rw-r--r--lib/common_test/src/test_server_ctrl.erl5693
-rw-r--r--lib/common_test/src/test_server_gl.erl355
-rw-r--r--lib/common_test/src/test_server_internal.hrl60
-rw-r--r--lib/common_test/src/test_server_io.erl453
-rw-r--r--lib/common_test/src/test_server_node.erl766
-rw-r--r--lib/common_test/src/test_server_sup.erl944
-rw-r--r--lib/common_test/src/unix_telnet.erl109
-rw-r--r--lib/common_test/src/vts.erl48
52 files changed, 17900 insertions, 2172 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile
index 4600c0ad78..0f9e044f9e 100644
--- a/lib/common_test/src/Makefile
+++ b/lib/common_test/src/Makefile
@@ -1,18 +1,19 @@
#
# %CopyrightBegin%
#
-# Copyright Ericsson AB 2003-2013. All Rights Reserved.
+# Copyright Ericsson AB 2003-2016. All Rights Reserved.
#
-# The contents of this file are subject to the Erlang Public License,
-# Version 1.1, (the "License"); you may not use this file except in
-# compliance with the License. You should have received a copy of the
-# Erlang Public License along with this software. If not, it can be
-# retrieved online at http://www.erlang.org/.
+# 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
#
-# Software distributed under the License is distributed on an "AS IS"
-# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-# the License for the specific language governing rights and limitations
-# under the License.
+# 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%
#
@@ -62,6 +63,8 @@ MODULES= \
ct_telnet_client \
ct_make \
vts \
+ ct_webtool \
+ ct_webtool_sup \
unix_telnet \
ct_config \
ct_config_plain \
@@ -74,7 +77,17 @@ MODULES= \
ct_netconfc \
ct_conn_log_h \
cth_conn_log \
- ct_groups
+ ct_groups \
+ ct_property_test \
+ ct_release_test \
+ 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))
@@ -96,8 +109,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 18c1dec784..77588af59b 100644
--- a/lib/common_test/src/common_test.app.src
+++ b/lib/common_test/src/common_test.app.src
@@ -1,18 +1,19 @@
% This is an -*- erlang -*- file.
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2009-2012. All Rights Reserved.
+%% Copyright Ericsson AB 2009-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
@@ -25,6 +26,7 @@
ct_framework,
ct_ftp,
ct_gen_conn,
+ ct_groups,
ct_hooks,
ct_hooks_lock,
ct_logs,
@@ -35,6 +37,8 @@
ct_master_status,
ct_netconfc,
ct_conn_log_h,
+ ct_property_test,
+ ct_release_test,
ct_repeat,
ct_rpc,
ct_run,
@@ -44,6 +48,8 @@
ct_telnet,
ct_testspec,
ct_util,
+ ct_webtool,
+ ct_webtool_sup,
unix_telnet,
vts,
ct_config,
@@ -52,7 +58,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,
@@ -60,7 +73,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, []}]}.
+ {env, []},
+ {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-2.5",
+ "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 0fbe5f23f7..e98f947553 100644
--- a/lib/common_test/src/common_test.appup.src
+++ b/lib/common_test/src/common_test.appup.src
@@ -1 +1,22 @@
-{"%VSN%",[],[]}. \ No newline at end of file
+%% -*- erlang -*-
+%% %CopyrightBegin%
+%%
+%% 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.
+%% 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%
+{"%VSN%",
+ [{<<".*">>,[{restart_application, common_test}]}],
+ [{<<".*">>,[{restart_application, common_test}]}]
+}.
diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl
index e6732f7fc7..d7ae81a5ce 100644
--- a/lib/common_test/src/ct.erl
+++ b/lib/common_test/src/ct.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -52,6 +53,7 @@
-module(ct).
-include("ct.hrl").
+-include("ct_util.hrl").
%% Command line user interface for running tests
-export([install/1, run/1, run/2, run/3,
@@ -62,7 +64,8 @@
-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,
+ escape_chars/1, escape_chars/2,
+ log/1, log/2, log/3, log/4, log/5,
print/1, print/2, print/3, print/4,
pal/1, pal/2, pal/3, pal/4,
capture_start/0, capture_stop/0, capture_get/0, capture_get/1,
@@ -77,6 +80,8 @@
%% Other interface functions
-export([get_status/0, abort_current_testcase/1,
+ get_event_mgr_ref/0,
+ get_testspec_terms/0, get_testspec_terms/1,
encrypt_config_file/2, encrypt_config_file/3,
decrypt_config_file/2, decrypt_config_file/3]).
@@ -150,14 +155,15 @@ run(TestDirs) ->
%%% {silent_connections,Conns} | {stylesheet,CSSFile} |
%%% {cover,CoverSpecFile} | {cover_stop,Bool} | {step,StepOpts} |
%%% {event_handler,EventHandlers} | {include,InclDirs} |
-%%% {auto_compile,Bool} | {create_priv_dir,CreatePrivDir} |
+%%% {auto_compile,Bool} | {abort_if_missing_suites,Bool} |
+%%% {create_priv_dir,CreatePrivDir} |
%%% {multiply_timetraps,M} | {scale_timetraps,Bool} |
%%% {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()
@@ -276,7 +282,7 @@ step(TestDir,Suite,Case,Opts) ->
%%% <c>&gt; 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.
%%%-----------------------------------------------------------------
@@ -460,44 +466,132 @@ reload_config(Required)->
ct_config:reload_config(Required).
%%%-----------------------------------------------------------------
+%%% @spec get_testspec_terms() -> TestSpecTerms | undefined
+%%% TestSpecTerms = [{Tag,Value}]
+%%% Value = [term()]
+%%%
+%%% @doc Get a list of all test specification terms used to
+%%% configure and run this test.
+%%%
+get_testspec_terms() ->
+ case ct_util:get_testdata(testspec) of
+ undefined ->
+ undefined;
+ CurrSpecRec ->
+ ct_testspec:testspec_rec2list(CurrSpecRec)
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec get_testspec_terms(Tags) -> TestSpecTerms | undefined
+%%% Tags = [Tag] | Tag
+%%% Tag = atom()
+%%% TestSpecTerms = [{Tag,Value}] | {Tag,Value}
+%%% Value = [{Node,term()}] | [term()]
+%%% Node = atom()
+%%%
+%%% @doc Read one or more terms from the test specification used
+%%% to configure and run this test. Tag is any valid test specification
+%%% tag, such as e.g. <c>label</c>, <c>config</c>, <c>logdir</c>.
+%%% User specific terms are also available to read if the
+%%% <c>allow_user_terms</c> option has been set. Note that all value tuples
+%%% returned, except user terms, will have the node name as first element.
+%%% Note also that in order to read test terms, use <c>Tag = tests</c>
+%%% (rather than <c>suites</c>, <c>groups</c> or <c>cases</c>). Value is
+%%% then the list of *all* tests on the form:
+%%% <c>[{Node,Dir,[{TestSpec,GroupsAndCases1},...]},...], where
+%%% GroupsAndCases = [{Group,[Case]}] | [Case]</c>.
+get_testspec_terms(Tags) ->
+ case ct_util:get_testdata(testspec) of
+ undefined ->
+ undefined;
+ CurrSpecRec ->
+ ct_testspec:testspec_rec2list(Tags, CurrSpecRec)
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @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 = esc_chars | no_css
%%%
%%% @doc Printout from a test case to the log file.
%%%
@@ -509,8 +603,8 @@ 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).
%%%-----------------------------------------------------------------
@@ -681,7 +775,7 @@ capture_get([]) ->
test_server:capture_get().
%%%-----------------------------------------------------------------
-%%% @spec fail(Reason) -> void()
+%%% @spec fail(Reason) -> ok
%%% Reason = term()
%%%
%%% @doc Terminate a test case with the given error
@@ -699,7 +793,7 @@ fail(Reason) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec fail(Format, Args) -> void()
+%%% @spec fail(Format, Args) -> ok
%%% Format = string()
%%% Args = list()
%%%
@@ -725,7 +819,7 @@ fail(Format, Args) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec comment(Comment) -> void()
+%%% @spec comment(Comment) -> ok
%%% Comment = term()
%%%
%%% @doc Print the given <c>Comment</c> in the comment field in
@@ -748,7 +842,7 @@ comment(Comment) ->
send_html_comment(lists:flatten(Formatted)).
%%%-----------------------------------------------------------------
-%%% @spec comment(Format, Args) -> void()
+%%% @spec comment(Format, Args) -> ok
%%% Format = string()
%%% Args = list()
%%%
@@ -772,7 +866,7 @@ comment(Format, Args) when is_list(Format), is_list(Args) ->
send_html_comment(Comment) ->
Html = "<font color=\"green\">" ++ Comment ++ "</font>",
- ct_util:set_testdata({comment,Html}),
+ ct_util:set_testdata({{comment,group_leader()},Html}),
test_server:comment(Html).
%%%-----------------------------------------------------------------
@@ -1004,6 +1098,18 @@ abort_current_testcase(Reason) ->
test_server_ctrl:abort_current_testcase(Reason).
%%%-----------------------------------------------------------------
+%%% @spec get_event_mgr_ref() -> EvMgrRef
+%%% EvMgrRef = atom()
+%%%
+%%% @doc <p>Call this function in order to get a reference to the
+%%% CT event manager. The reference can be used to e.g. add
+%%% a user specific event handler while tests are running.
+%%% Example:
+%%% <c>gen_event:add_handler(ct:get_event_mgr_ref(), my_ev_h, [])</c></p>
+get_event_mgr_ref() ->
+ ?CT_EVMGR_REF.
+
+%%%-----------------------------------------------------------------
%%% @spec encrypt_config_file(SrcFileName, EncryptFileName) ->
%%% ok | {error,Reason}
%%% SrcFileName = string()
diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl
index 5c80a299f8..99de311570 100644
--- a/lib/common_test/src/ct_config.erl
+++ b/lib/common_test/src/ct_config.erl
@@ -1,18 +1,19 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2010-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2010-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%----------------------------------------------------------------------
@@ -118,7 +119,8 @@ call(Msg) ->
end.
return({To,Ref},Result) ->
- To ! {Ref, Result}.
+ To ! {Ref, Result},
+ ok.
loop(StartDir) ->
receive
@@ -127,11 +129,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} ->
@@ -148,7 +150,7 @@ loop(StartDir) ->
loop(StartDir);
{{stop},From} ->
ets:delete(?attr_table),
- file:set_cwd(StartDir),
+ ok = file:set_cwd(StartDir),
return(From,ok)
end.
@@ -256,7 +258,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}};
@@ -266,6 +268,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);
@@ -454,8 +465,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() ->
@@ -489,16 +504,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,
@@ -510,7 +525,7 @@ associate_int(Name,Configs,_) ->
[ets:insert(?attr_table,C#ct_conf{name=Name,
value=Config})
|| C <- Cs]
- end,Configs).
+ end,Configs).
@@ -575,7 +590,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} ->
@@ -614,7 +629,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} ->
@@ -693,12 +708,10 @@ make_crypto_key(String) ->
{[K1,K2,K3],IVec}.
random_bytes(N) ->
- {A,B,C} = now(),
- random:seed(A, B, C),
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 +792,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 c6547f0a40..e72b55971b 100644
--- a/lib/common_test/src/ct_config_plain.erl
+++ b/lib/common_test/src/ct_config_plain.erl
@@ -1,18 +1,19 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2010-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2010-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%----------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_config_xml.erl b/lib/common_test/src/ct_config_xml.erl
index 6e0a016161..4343761707 100644
--- a/lib/common_test/src/ct_config_xml.erl
+++ b/lib/common_test/src/ct_config_xml.erl
@@ -1,18 +1,19 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2010-2011. All Rights Reserved.
+%% Copyright Ericsson AB 2010-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%----------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_conn_log_h.erl b/lib/common_test/src/ct_conn_log_h.erl
index ac08a3e0ad..93e64c65fe 100644
--- a/lib/common_test/src/ct_conn_log_h.erl
+++ b/lib/common_test/src/ct_conn_log_h.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2012-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2012-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -30,99 +31,137 @@
handle_event/2, handle_call/2, handle_info/2,
terminate/2]).
--record(state, {group_leader,logs=[]}).
+-record(state, {logs=[], default_gl}).
-define(WIDTH,80).
+-define(now, os:timestamp()).
+
%%%-----------------------------------------------------------------
%%% Callbacks
-init({GL,Logs}) ->
- open_files(Logs,#state{group_leader=GL}).
+init({GL,ConnLogs}) ->
+ open_files(GL,ConnLogs,#state{default_gl=GL}).
-open_files([{ConnMod,{LogType,Logs}}|T],State) ->
- case do_open_files(Logs,[]) of
+open_files(GL,[{ConnMod,{LogType,LogFiles}}|T],State=#state{logs=Logs}) ->
+ case do_open_files(LogFiles,[]) of
{ok,Fds} ->
- open_files(T,State#state{logs=[{ConnMod,{LogType,Fds}} |
- State#state.logs]});
+ ConnInfo = proplists:get_value(GL,Logs,[]),
+ Logs1 = [{GL,[{ConnMod,{LogType,Fds}}|ConnInfo]} |
+ proplists:delete(GL,Logs)],
+ open_files(GL,T,State#state{logs=Logs1});
Error ->
Error
end;
-open_files([],State) ->
+open_files(_GL,[],State) ->
{ok,State}.
-
-do_open_files([{Tag,File}|Logs],Acc) ->
- case file:open(File, [write,{encoding,utf8}]) of
+do_open_files([{Tag,File}|LogFiles],Acc) ->
+ case file:open(File, [write,append,{encoding,utf8}]) of
{ok,Fd} ->
- do_open_files(Logs,[{Tag,Fd}|Acc]);
+ do_open_files(LogFiles,[{Tag,Fd}|Acc]);
{error,Reason} ->
{error,{could_not_open_log,File,Reason}}
end;
do_open_files([],Acc) ->
{ok,lists:reverse(Acc)}.
+handle_event({info_report,_,{From,update,{GL,ConnLogs}}},
+ State) when node(GL) == node() ->
+ Result = open_files(GL,ConnLogs,State),
+ From ! {updated,GL},
+ Result;
handle_event({_Type, GL, _Msg}, State) when node(GL) /= node() ->
{ok, State};
-handle_event({_Type,_GL,{Pid,{ct_connection,Action,ConnName},Report}},State) ->
- %% NOTE: if the format of this event is changed
- %% ({ct_connection,Action,ConnName}) then remember to change
- %% test_server_h:report_receiver as well!!!
- Info = conn_info(Pid,#conn_log{name=ConnName,action=Action}),
- write_report(now(),Info,Report,State),
+handle_event({_Type,GL,{Pid,{ct_connection,Mod,Action,ConnName},Report}},
+ State) ->
+ Info = conn_info(Pid,#conn_log{name=ConnName,action=Action,module=Mod}),
+ write_report(?now,Info,Report,GL,State),
{ok, State};
-handle_event({_Type,_GL,{Pid,Info=#conn_log{},Report}},State) ->
- %% NOTE: if the format of this event is changed
- %% (Info=#conn_log{}) then remember to change
- %% test_server_h:report_receiver as well!!!
- write_report(now(),conn_info(Pid,Info),Report,State),
+handle_event({_Type,GL,{Pid,Info=#conn_log{},Report}}, State) ->
+ write_report(?now,conn_info(Pid,Info),Report,GL,State),
{ok, State};
-handle_event({error_report,_,{Pid,_,[{ct_connection,ConnName}|R]}},State) ->
+handle_event({error_report,GL,{Pid,_,[{ct_connection,ConnName}|R]}}, State) ->
%% Error reports from connection
- write_error(now(),conn_info(Pid,#conn_log{name=ConnName}),R,State),
+ write_error(?now,conn_info(Pid,#conn_log{name=ConnName}),R,GL,State),
{ok, State};
-handle_event(_, State) ->
+handle_event(_What, State) ->
{ok, State}.
-handle_info(_, State) ->
+handle_info(_What, State) ->
{ok, State}.
handle_call(_Query, State) ->
{ok, {error, bad_query}, State}.
terminate(_,#state{logs=Logs}) ->
- [file:close(Fd) || {_,{_,Fds}} <- Logs, {_,Fd} <- Fds],
+ lists:foreach(
+ fun({_GL,ConnLogs}) ->
+ [file:close(Fd) || {_,{_,Fds}} <- ConnLogs, {_,Fd} <- Fds]
+ end, Logs),
ok.
%%%-----------------------------------------------------------------
%%% Writing reports
-write_report(Time,#conn_log{module=ConnMod}=Info,Data,State) ->
- {LogType,Fd} = get_log(Info,State),
- io:format(Fd,"~n~ts~ts~ts",[format_head(ConnMod,LogType,Time),
- format_title(LogType,Info),
- format_data(ConnMod,LogType,Data)]).
-
-write_error(Time,#conn_log{module=ConnMod}=Info,Report,State) ->
- case get_log(Info,State) of
- {html,_} ->
+write_report(_Time,#conn_log{header=false,module=ConnMod}=Info,Data,GL,State) ->
+ 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) ->
+ 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
+ {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,State) ->
- case proplists:get_value(Info#conn_log.module,State#state.logs) of
- {html,_} ->
- {html,State#state.group_leader};
- {LogType,Fds} ->
- {LogType,get_fd(Info,Fds)};
+get_log(Info,GL,State) ->
+ case proplists:get_value(GL,State#state.logs) of
undefined ->
- {html,State#state.group_leader}
+ {html,gl,State#state.default_gl};
+ ConnLogs ->
+ case proplists:get_value(Info#conn_log.module,ConnLogs) of
+ {html,_} ->
+ {html,gl,GL};
+ {LogType,Fds} ->
+ {LogType,file,get_fd(Info,Fds)};
+ undefined ->
+ {html,gl,GL}
+ end
end.
get_fd(#conn_log{name=undefined},Fds) ->
@@ -191,17 +230,22 @@ 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 Server ~ts ",
+ io_lib:format("= Client ~w ~s ~ts ",
[Client,actionstr(Info),serverstr(Info)]).
actionstr(#conn_log{action=send}) -> "----->";
+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(_) -> "<---->".
+serverstr(#conn_log{name=undefined,address={undefined,_}}) ->
+ io_lib:format("server",[]);
serverstr(#conn_log{name=undefined,address=Address}) ->
io_lib:format("~p",[Address]);
+serverstr(#conn_log{name=Alias,address={undefined,_}}) ->
+ io_lib:format("~w",[Alias]);
serverstr(#conn_log{name=Alias,address=Address}) ->
io_lib:format("~w(~p)",[Alias,Address]).
diff --git a/lib/common_test/src/ct_cover.erl b/lib/common_test/src/ct_cover.erl
index ae671c750a..c258516915 100644
--- a/lib/common_test/src/ct_cover.erl
+++ b/lib/common_test/src/ct_cover.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2012. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -47,18 +48,21 @@ add_nodes(Nodes) ->
undefined ->
{error,cover_not_running};
_ ->
- {File,Nodes0,Import,Export,AppInfo} = ct_util:get_testdata(cover),
+ Nodes0 = cover:which_nodes(),
Nodes1 = [Node || Node <- Nodes,
lists:member(Node,Nodes0) == false],
ct_logs:log("COVER INFO",
"Adding nodes to cover test: ~w", [Nodes1]),
case cover:start(Nodes1) of
- Result = {ok,_} ->
- ct_util:set_testdata({cover,{File,Nodes1++Nodes0,
- Import,Export,AppInfo}}),
-
+ Result = {ok,StartedNodes} ->
+ ct_logs:log("COVER INFO",
+ "Successfully added nodes to cover test: ~w",
+ [StartedNodes]),
Result;
Error ->
+ ct_logs:log("COVER INFO",
+ "Failed to add nodes to cover test: ~tp",
+ [Error]),
Error
end
end.
@@ -81,19 +85,20 @@ remove_nodes(Nodes) ->
undefined ->
{error,cover_not_running};
_ ->
- {File,Nodes0,Import,Export,AppInfo} = ct_util:get_testdata(cover),
+ Nodes0 = cover:which_nodes(),
ToRemove = [Node || Node <- Nodes, lists:member(Node,Nodes0)],
ct_logs:log("COVER INFO",
- "Removing nodes from cover test: ~w", [ToRemove]),
+ "Removing nodes from cover test: ~w", [ToRemove]),
case cover:stop(ToRemove) of
ok ->
- Nodes1 = lists:foldl(fun(N,Deleted) ->
- lists:delete(N,Deleted)
- end, Nodes0, ToRemove),
- ct_util:set_testdata({cover,{File,Nodes1,
- Import,Export,AppInfo}}),
+ ct_logs:log("COVER INFO",
+ "Successfully removed nodes from cover test.",
+ []),
ok;
Error ->
+ ct_logs:log("COVER INFO",
+ "Failed to remove nodes from cover test: ~tp",
+ [Error]),
Error
end
end.
@@ -124,20 +129,20 @@ get_spec(File) ->
catch get_spec_test(File).
get_spec_test(File) ->
- FullName = filename:absname(File),
- case filelib:is_file(FullName) of
+ Dir = filename:dirname(File), % always abs path in here, set in ct_run
+ case filelib:is_file(File) of
true ->
- case file:consult(FullName) of
+ case file:consult(File) of
{ok,Terms} ->
Import =
case lists:keysearch(import, 1, Terms) of
{value,{_,Imps=[S|_]}} when is_list(S) ->
ImpsFN = lists:map(fun(F) ->
- filename:absname(F)
+ filename:absname(F,Dir)
end, Imps),
test_files(ImpsFN, ImpsFN);
{value,{_,Imp=[IC|_]}} when is_integer(IC) ->
- ImpFN = filename:absname(Imp),
+ ImpFN = filename:absname(Imp,Dir),
test_files([ImpFN], [ImpFN]);
_ ->
[]
@@ -145,11 +150,11 @@ get_spec_test(File) ->
Export =
case lists:keysearch(export, 1, Terms) of
{value,{_,Exp=[EC|_]}} when is_integer(EC) ->
- filename:absname(Exp);
+ filename:absname(Exp,Dir);
{value,{_,[Exp]}} ->
- filename:absname(Exp);
+ filename:absname(Exp,Dir);
_ ->
- []
+ undefined
end,
Nodes =
case lists:keysearch(nodes, 1, Terms) of
@@ -170,12 +175,12 @@ get_spec_test(File) ->
[] -> [#cover{app=none, level=details}];
_ -> Res
end,
- case get_cover_opts(Apps, Terms, []) of
+ case get_cover_opts(Apps, Terms, Dir, []) of
E = {error,_} ->
E;
[CoverSpec] ->
CoverSpec1 = remove_excludes_and_dups(CoverSpec),
- {FullName,Nodes,Import,Export,CoverSpec1};
+ {File,Nodes,Import,Export,CoverSpec1};
_ ->
{error,multiple_apps_in_cover_spec}
end;
@@ -186,7 +191,7 @@ get_spec_test(File) ->
{error,{invalid_cover_spec,Error}}
end;
false ->
- {error,{cant_read_cover_spec_file,FullName}}
+ {error,{cant_read_cover_spec_file,File}}
end.
collect_apps([{level,Level}|Ts], Apps) ->
@@ -201,124 +206,125 @@ collect_apps([], Apps) ->
%% get_cover_opts(Terms) -> AppCoverInfo
%% AppCoverInfo: [#cover{app=App,...}]
-get_cover_opts([App | Apps], Terms, CoverInfo) ->
- case get_app_info(App, Terms) of
+get_cover_opts([App | Apps], Terms, Dir, CoverInfo) ->
+ case get_app_info(App, Terms, Dir) of
E = {error,_} -> E;
AppInfo ->
AppInfo1 = files2mods(AppInfo),
- get_cover_opts(Apps, Terms, [AppInfo1|CoverInfo])
+ get_cover_opts(Apps, Terms, Dir, [AppInfo1|CoverInfo])
end;
-get_cover_opts([], _, CoverInfo) ->
+get_cover_opts([], _, _, CoverInfo) ->
lists:reverse(CoverInfo).
-%% get_app_info(App, Terms) -> App1
+%% get_app_info(App, Terms, Dir) -> App1
-get_app_info(App=#cover{app=none}, [{incl_dirs,Dirs}|Terms]) ->
- get_app_info(App, [{incl_dirs,none,Dirs}|Terms]);
-get_app_info(App=#cover{app=Name}, [{incl_dirs,Name,Dirs}|Terms]) ->
- case get_files(Dirs, ".beam", false, []) of
+get_app_info(App=#cover{app=none}, [{incl_dirs,Dirs}|Terms], Dir) ->
+ get_app_info(App, [{incl_dirs,none,Dirs}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{incl_dirs,Name,Dirs}|Terms], Dir) ->
+ case get_files(Dirs, Dir, ".beam", false, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.incl_mods,
- get_app_info(App#cover{incl_mods=Mods++Mods1},Terms)
+ get_app_info(App#cover{incl_mods=Mods++Mods1},Terms,Dir)
end;
-get_app_info(App=#cover{app=none}, [{incl_dirs_r,Dirs}|Terms]) ->
- get_app_info(App, [{incl_dirs_r,none,Dirs}|Terms]);
-get_app_info(App=#cover{app=Name}, [{incl_dirs_r,Name,Dirs}|Terms]) ->
- case get_files(Dirs, ".beam", true, []) of
+get_app_info(App=#cover{app=none}, [{incl_dirs_r,Dirs}|Terms], Dir) ->
+ get_app_info(App, [{incl_dirs_r,none,Dirs}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{incl_dirs_r,Name,Dirs}|Terms], Dir) ->
+ case get_files(Dirs, Dir, ".beam", true, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.incl_mods,
- get_app_info(App#cover{incl_mods=Mods++Mods1},Terms)
+ get_app_info(App#cover{incl_mods=Mods++Mods1},Terms,Dir)
end;
-get_app_info(App=#cover{app=none}, [{incl_mods,Mods1}|Terms]) ->
- get_app_info(App, [{incl_mods,none,Mods1}|Terms]);
-get_app_info(App=#cover{app=Name}, [{incl_mods,Name,Mods1}|Terms]) ->
+get_app_info(App=#cover{app=none}, [{incl_mods,Mods1}|Terms], Dir) ->
+ get_app_info(App, [{incl_mods,none,Mods1}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{incl_mods,Name,Mods1}|Terms], Dir) ->
Mods = App#cover.incl_mods,
- get_app_info(App#cover{incl_mods=Mods++Mods1},Terms);
+ get_app_info(App#cover{incl_mods=Mods++Mods1},Terms,Dir);
-get_app_info(App=#cover{app=none}, [{excl_dirs,Dirs}|Terms]) ->
- get_app_info(App, [{excl_dirs,none,Dirs}|Terms]);
-get_app_info(App=#cover{app=Name}, [{excl_dirs,Name,Dirs}|Terms]) ->
- case get_files(Dirs, ".beam", false, []) of
+get_app_info(App=#cover{app=none}, [{excl_dirs,Dirs}|Terms], Dir) ->
+ get_app_info(App, [{excl_dirs,none,Dirs}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{excl_dirs,Name,Dirs}|Terms], Dir) ->
+ case get_files(Dirs, Dir, ".beam", false, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.excl_mods,
- get_app_info(App#cover{excl_mods=Mods++Mods1},Terms)
+ get_app_info(App#cover{excl_mods=Mods++Mods1},Terms,Dir)
end;
-get_app_info(App=#cover{app=none}, [{excl_dirs_r,Dirs}|Terms]) ->
- get_app_info(App, [{excl_dirs_r,none,Dirs}|Terms]);
-get_app_info(App=#cover{app=Name}, [{excl_dirs_r,Name,Dirs}|Terms]) ->
- case get_files(Dirs, ".beam", true, []) of
+get_app_info(App=#cover{app=none}, [{excl_dirs_r,Dirs}|Terms],Dir) ->
+ get_app_info(App, [{excl_dirs_r,none,Dirs}|Terms],Dir);
+get_app_info(App=#cover{app=Name}, [{excl_dirs_r,Name,Dirs}|Terms],Dir) ->
+ case get_files(Dirs, Dir, ".beam", true, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.excl_mods,
- get_app_info(App#cover{excl_mods=Mods++Mods1},Terms)
+ get_app_info(App#cover{excl_mods=Mods++Mods1},Terms,Dir)
end;
-get_app_info(App=#cover{app=none}, [{excl_mods,Mods1}|Terms]) ->
- get_app_info(App, [{excl_mods,none,Mods1}|Terms]);
-get_app_info(App=#cover{app=Name}, [{excl_mods,Name,Mods1}|Terms]) ->
+get_app_info(App=#cover{app=none}, [{excl_mods,Mods1}|Terms], Dir) ->
+ get_app_info(App, [{excl_mods,none,Mods1}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{excl_mods,Name,Mods1}|Terms], Dir) ->
Mods = App#cover.excl_mods,
- get_app_info(App#cover{excl_mods=Mods++Mods1},Terms);
+ get_app_info(App#cover{excl_mods=Mods++Mods1},Terms,Dir);
-get_app_info(App=#cover{app=none}, [{cross,Cross}|Terms]) ->
- get_app_info(App, [{cross,none,Cross}|Terms]);
-get_app_info(App=#cover{app=Name}, [{cross,Name,Cross1}|Terms]) ->
+get_app_info(App=#cover{app=none}, [{cross,Cross}|Terms], Dir) ->
+ get_app_info(App, [{cross,none,Cross}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{cross,Name,Cross1}|Terms], Dir) ->
Cross = App#cover.cross,
- get_app_info(App#cover{cross=Cross++Cross1},Terms);
+ get_app_info(App#cover{cross=Cross++Cross1},Terms,Dir);
-get_app_info(App=#cover{app=none}, [{src_dirs,Dirs}|Terms]) ->
- get_app_info(App, [{src_dirs,none,Dirs}|Terms]);
-get_app_info(App=#cover{app=Name}, [{src_dirs,Name,Dirs}|Terms]) ->
- case get_files(Dirs, ".erl", false, []) of
+get_app_info(App=#cover{app=none}, [{src_dirs,Dirs}|Terms], Dir) ->
+ get_app_info(App, [{src_dirs,none,Dirs}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{src_dirs,Name,Dirs}|Terms], Dir) ->
+ case get_files(Dirs, Dir, ".erl", false, []) of
E = {error,_} -> E;
Src1 ->
Src = App#cover.src,
- get_app_info(App#cover{src=Src++Src1},Terms)
+ get_app_info(App#cover{src=Src++Src1},Terms,Dir)
end;
-get_app_info(App=#cover{app=none}, [{src_dirs_r,Dirs}|Terms]) ->
- get_app_info(App, [{src_dirs_r,none,Dirs}|Terms]);
-get_app_info(App=#cover{app=Name}, [{src_dirs_r,Name,Dirs}|Terms]) ->
- case get_files(Dirs, ".erl", true, []) of
+get_app_info(App=#cover{app=none}, [{src_dirs_r,Dirs}|Terms], Dir) ->
+ get_app_info(App, [{src_dirs_r,none,Dirs}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{src_dirs_r,Name,Dirs}|Terms], Dir) ->
+ case get_files(Dirs, Dir, ".erl", true, []) of
E = {error,_} -> E;
Src1 ->
Src = App#cover.src,
- get_app_info(App#cover{src=Src++Src1},Terms)
+ get_app_info(App#cover{src=Src++Src1},Terms,Dir)
end;
-get_app_info(App=#cover{app=none}, [{src_files,Src1}|Terms]) ->
- get_app_info(App, [{src_files,none,Src1}|Terms]);
-get_app_info(App=#cover{app=Name}, [{src_files,Name,Src1}|Terms]) ->
+get_app_info(App=#cover{app=none}, [{src_files,Src1}|Terms], Dir) ->
+ get_app_info(App, [{src_files,none,Src1}|Terms], Dir);
+get_app_info(App=#cover{app=Name}, [{src_files,Name,Src1}|Terms], Dir) ->
Src = App#cover.src,
- get_app_info(App#cover{src=Src++Src1},Terms);
+ get_app_info(App#cover{src=Src++Src1},Terms,Dir);
-get_app_info(App, [_|Terms]) ->
- get_app_info(App, Terms);
+get_app_info(App, [_|Terms], Dir) ->
+ get_app_info(App, Terms, Dir);
-get_app_info(App, []) ->
+get_app_info(App, [], _) ->
App.
%% get_files(...)
-get_files([Dir|Dirs], Ext, Recurse, Files) ->
- case file:list_dir(Dir) of
+get_files([Dir|Dirs], RootDir, Ext, Recurse, Files) ->
+ DirAbs = filename:absname(Dir, RootDir),
+ case file:list_dir(DirAbs) of
{ok,Entries} ->
- {SubDirs,Matches} = analyse_files(Entries, Dir, Ext, [], []),
+ {SubDirs,Matches} = analyse_files(Entries, DirAbs, Ext, [], []),
if Recurse == false ->
- get_files(Dirs, Ext, Recurse, Files++Matches);
+ get_files(Dirs, RootDir, Ext, Recurse, Files++Matches);
true ->
- Files1 = get_files(SubDirs, Ext, Recurse, Files++Matches),
- get_files(Dirs, Ext, Recurse, Files1)
+ Files1 = get_files(SubDirs, RootDir, Ext, Recurse, Files++Matches),
+ get_files(Dirs, RootDir, Ext, Recurse, Files1)
end;
{error,Reason} ->
- {error,{Reason,Dir}}
+ {error,{Reason,DirAbs}}
end;
-get_files([], _Ext, _R, Files) ->
+get_files([], _RootDir, _Ext, _R, Files) ->
Files.
%% analyse_files(...)
diff --git a/lib/common_test/src/ct_event.erl b/lib/common_test/src/ct_event.erl
index c1c1d943b9..5fa9f410bf 100644
--- a/lib/common_test/src/ct_event.erl
+++ b/lib/common_test/src/ct_event.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -259,8 +260,8 @@ handle_info(_Info, State) ->
{ok, State}.
%%--------------------------------------------------------------------
-%% Function: terminate(Reason, State) -> void()
-%% Description:Whenever an event handler is deleted from an event manager,
+%% Function: terminate(Reason, State) -> ok
+%% Description: Whenever an event handler is deleted from an event manager,
%% this function is called. It should be the opposite of Module:init/1 and
%% do any necessary cleaning up.
%%--------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl
index 276f902b05..104515e57e 100644
--- a/lib/common_test/src/ct_framework.erl
+++ b/lib/common_test/src/ct_framework.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -27,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]).
@@ -51,9 +52,23 @@
%%%
%%% @doc Test server framework callback, called by the test_server
%%% when a new test case is started.
-init_tc(Mod,Func,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
@@ -69,13 +84,13 @@ init_tc(Mod,Func,Config) ->
andalso Func=/=end_per_group
andalso ct_util:get_testdata(skip_rest) of
true ->
- {skip,"Repeated test stopped by force_stop option"};
+ {auto_skip,"Repeated test stopped by force_stop option"};
_ ->
case ct_util:get_testdata(curr_tc) of
{Suite,{suite0_failed,{require,Reason}}} ->
- {skip,{require_failed_in_suite0,Reason}};
+ {auto_skip,{require_failed_in_suite0,Reason}};
{Suite,{suite0_failed,_}=Failure} ->
- {skip,Failure};
+ {fail,Failure};
_ ->
ct_util:update_testdata(curr_tc,
fun(undefined) ->
@@ -85,7 +100,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
@@ -101,26 +116,27 @@ init_tc(Mod,Func,Config) ->
_ ->
ok
end,
- init_tc1(Mod,Suite,Func,Config);
+ init_tc1(Mod,Suite,Func,HookFunc,Args);
{failed,Seq,BadFunc} ->
- {skip,{sequence_failed,Seq,BadFunc}}
+ {auto_skip,{sequence_failed,Seq,BadFunc}}
end
end
- end.
+ end.
-init_tc1(?MODULE,_,error_in_suite,[Config0]) when is_list(Config0) ->
+init_tc1(?MODULE,_,error_in_suite,_,[Config0]) when is_list(Config0) ->
ct_logs:init_tc(false),
ct_event:notify(#event{name=tc_start,
node=node(),
data={?MODULE,error_in_suite}}),
- case ?val(error, Config0) of
+ _ = ct_suite_init(?MODULE,error_in_suite,[],Config0),
+ case ?val(error,Config0) of
undefined ->
- {skip,"unknown_error_in_suite"};
+ {fail,"unknown_error_in_suite"};
Reason ->
- {skip,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
@@ -154,40 +170,49 @@ 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,
+ HookFunc1 =
+ if is_tuple(FuncSpec) -> % group
+ FuncSpec;
+ true ->
+ ct_config:delete_default_config(testcase),
+ HookFunc
+ end,
+ Initialize = fun() ->
+ ct_logs:init_tc(false),
+ ct_event:notify(#event{name=tc_start,
+ node=node(),
+ data={Mod,FuncSpec}})
+ end,
case add_defaults(Mod,Func,AllGroups) of
Error = {suite0_failed,_} ->
- ct_logs:init_tc(false),
- ct_event:notify(#event{name=tc_start,
- node=node(),
- data={Mod,FuncSpec}}),
+ Initialize(),
ct_util:set_testdata({curr_tc,{Suite,Error}}),
{error,Error};
+ Error = {group0_failed,_} ->
+ Initialize(),
+ {auto_skip,Error};
+ Error = {testcase0_failed,_} ->
+ Initialize(),
+ {auto_skip,Error};
{SuiteInfo,MergeResult} ->
case MergeResult of
{error,Reason} ->
- ct_logs:init_tc(false),
- ct_event:notify(#event{name=tc_start,
- node=node(),
- data={Mod,FuncSpec}}),
- {skip,Reason};
+ Initialize(),
+ {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));
_ ->
@@ -222,15 +247,15 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
{suite0_failed,Reason} ->
ct_util:set_testdata({curr_tc,{Mod,{suite0_failed,
{require,Reason}}}}),
- {skip,{require_failed_in_suite0,Reason}};
+ {auto_skip,{require_failed_in_suite0,Reason}};
{error,Reason} ->
{auto_skip,{require_failed,Reason}};
{'EXIT',Reason} ->
- {auto_skip,Reason};
+ {fail,Reason};
{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),
@@ -243,16 +268,16 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
end
end.
-ct_suite_init(Suite, Func, PostInitHook, Config) when is_list(Config) ->
- case ct_hooks:init_tc(Suite, Func, Config) of
+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,_} ->
@@ -272,6 +297,8 @@ add_defaults(Mod,Func, GroupPath) ->
SuiteInfo = merge_with_suite_defaults(Suite,[]),
SuiteInfoNoCTH = [I || I <- SuiteInfo, element(1,I) =/= ct_hooks],
case add_defaults1(Mod,Func, GroupPath, SuiteInfoNoCTH) of
+ Error = {group0_failed,_} -> Error;
+ Error = {testcase0_failed,_} -> Error;
Error = {error,_} -> {SuiteInfo,Error};
MergedInfo -> {SuiteInfo,MergedInfo}
end;
@@ -292,13 +319,16 @@ add_defaults(Mod,Func, GroupPath) ->
element(1,I) =/= ct_hooks],
case add_defaults1(Mod,Func, GroupPath,
SuiteInfoNoCTH) of
+ Error = {group0_failed,_} -> Error;
+ Error = {testcase0_failed,_} -> Error;
Error = {error,_} -> {SuiteInfo1,Error};
MergedInfo -> {SuiteInfo1,MergedInfo}
end;
false ->
ErrStr = io_lib:format("~n*** ERROR *** "
"Invalid return value from "
- "~w:suite/0: ~p~n", [Suite,SuiteInfo]),
+ "~w:suite/0: ~p~n",
+ [Suite,SuiteInfo]),
io:format(ErrStr, []),
io:format(user, ErrStr, []),
{suite0_failed,bad_return_value}
@@ -318,36 +348,69 @@ add_defaults1(Mod,Func, GroupPath, SuiteInfo) ->
%% [LevelXGroupInfo, LevelX-1GroupInfo, ..., TopLevelGroupInfo]
GroupPathInfo =
lists:map(fun(GroupProps) ->
- Name = ?val(name, GroupProps),
- case catch Suite:group(Name) of
- GrInfo when is_list(GrInfo) -> GrInfo;
- _ -> []
+ case ?val(name, GroupProps) of
+ undefined ->
+ [];
+ Name ->
+ case catch Suite:group(Name) of
+ GrInfo when is_list(GrInfo) -> GrInfo;
+ {'EXIT',{undef,_}} -> [];
+ BadGr0 -> {error,BadGr0,Name}
+ end
end
end, GroupPath),
- Args = if Func == init_per_group ; Func == end_per_group ->
- [?val(name, hd(GroupPath))];
- true ->
- []
- end,
- TestCaseInfo =
- case catch apply(Mod,Func,Args) of
- TCInfo when is_list(TCInfo) -> TCInfo;
- _ -> []
- end,
- %% let test case info (also for all config funcs) override group info,
- %% and lower level group info override higher level info
- TCAndGroupInfo = [TestCaseInfo | remove_info_in_prev(TestCaseInfo,
- GroupPathInfo)],
- %% find and save require terms found in suite info
- SuiteReqs =
- [SDDef || SDDef <- SuiteInfo,
- ((require == element(1,SDDef)) or
- (default_config == element(1,SDDef)))],
- case check_for_clashes(TestCaseInfo, GroupPathInfo, SuiteReqs) of
- [] ->
- add_defaults2(Mod,Func, TCAndGroupInfo,SuiteInfo,SuiteReqs);
- Clashes ->
- {error,{config_name_already_in_use,Clashes}}
+ case lists:keysearch(error, 1, GroupPathInfo) of
+ {value,{error,BadGr0Val,GrName}} ->
+ Gr0ErrStr = io_lib:format("~n*** ERROR *** "
+ "Invalid return value from "
+ "~w:group(~w): ~p~n",
+ [Mod,GrName,BadGr0Val]),
+ io:format(Gr0ErrStr, []),
+ io:format(user, Gr0ErrStr, []),
+ {group0_failed,bad_return_value};
+ _ ->
+ Args = if Func == init_per_group ; Func == end_per_group ->
+ [?val(name, hd(GroupPath))];
+ true ->
+ []
+ end,
+ TestCaseInfo =
+ case catch apply(Mod,Func,Args) of
+ TCInfo when is_list(TCInfo) -> TCInfo;
+ {'EXIT',{undef,_}} -> [];
+ BadTC0 -> {error,BadTC0}
+ end,
+
+ case TestCaseInfo of
+ {error,BadTC0Val} ->
+ TC0ErrStr = io_lib:format("~n*** ERROR *** "
+ "Invalid return value from "
+ "~w:~w/0: ~p~n",
+ [Mod,Func,BadTC0Val]),
+ io:format(TC0ErrStr, []),
+ io:format(user, TC0ErrStr, []),
+ {testcase0_failed,bad_return_value};
+ _ ->
+ %% let test case info (also for all config funcs) override
+ %% group info, and lower level group info override higher
+ %% level info
+ TCAndGroupInfo =
+ [TestCaseInfo | remove_info_in_prev(TestCaseInfo,
+ GroupPathInfo)],
+ %% find and save require terms found in suite info
+ SuiteReqs =
+ [SDDef || SDDef <- SuiteInfo,
+ ((require == element(1,SDDef))
+ or (default_config == element(1,SDDef)))],
+ case check_for_clashes(TestCaseInfo, GroupPathInfo,
+ SuiteReqs) of
+ [] ->
+ add_defaults2(Mod,Func, TCAndGroupInfo,
+ SuiteInfo,SuiteReqs);
+ Clashes ->
+ {error,{config_name_already_in_use,Clashes}}
+ end
+ end
end.
get_suite_name(?MODULE, [Cfg|_]) when is_list(Cfg), Cfg /= [] ->
@@ -569,10 +632,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.
@@ -591,16 +654,43 @@ try_set_default(Name,Key,Info,Where) ->
end_tc(Mod, Fun, Args) ->
%% Have to keep end_tc/3 for backwards compatibility issues
end_tc(Mod, Fun, Args, '$end_tc_dummy').
-end_tc(?MODULE,error_in_suite,_, _) -> % bad start!
+end_tc(?MODULE,error_in_suite,{Result,[Args]},Return) ->
+ %% this clause gets called if CT has encountered a suite that
+ %% can't be executed
+ FinalNotify =
+ case ct_hooks:end_tc(?MODULE, error_in_suite, Args, Result, Return) of
+ '$ct_no_change' ->
+ Result;
+ HookResult ->
+ HookResult
+ end,
+ Event = #event{name=tc_done,
+ node=node(),
+ data={?MODULE,error_in_suite,tag(FinalNotify)}},
+ ct_event:sync_notify(Event),
ok;
end_tc(Mod,Func,{TCPid,Result,[Args]}, Return) when is_pid(TCPid) ->
end_tc(Mod,Func,TCPid,Result,Args,Return);
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) ->
+ %% 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_tc(Mod,Func0,TCPid,Result,Args,Return) ->
%% in case Mod == ct_framework, lookup the suite name
Suite = get_suite_name(Mod, Args),
+ {EPTC,Func} = case Func0 of
+ {end_per_testcase,F} -> {true,F};
+ _ -> {false,Func0}
+ end,
test_server:timetrap_cancel(),
@@ -613,39 +703,57 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
_ ->
ok
end,
- ct_util:delete_testdata(comment),
+ if Func == end_per_group; Func == end_per_suite ->
+ %% clean up any saved comments
+ ct_util:match_delete_testdata({comment,'_'});
+ true ->
+ %% attemp to delete any saved comment for this TC
+ case process_info(TCPid, group_leader) of
+ {group_leader,TCGL} ->
+ ct_util:delete_testdata({comment,TCGL});
+ _ ->
+ ok
+ end
+ end,
ct_util:delete_suite_data(last_saved_config),
- FuncSpec = case group_or_func(Func,Args) of
- {_,_GroupName,_} = Group -> Group;
- _ -> Func
- end,
+ {FuncSpec,HookFunc} =
+ if not EPTC ->
+ FS = group_or_func(Func,Args),
+ {FS,FS};
+ true ->
+ {Func,Func0}
+ end,
+ {Result1,FinalNotify} =
+ case ct_hooks:end_tc(Suite,HookFunc,Args,Result,Return) of
+ '$ct_no_change' ->
+ {ok,Result};
+ HookResult ->
+ {HookResult,HookResult}
+ end,
+ FinalResult =
+ case get('$test_server_framework_test') of
+ undefined ->
+ %% send sync notification so that event handlers may print
+ %% in the log file before it gets closed
+ Event = #event{name=tc_done,
+ node=node(),
+ data={Mod,FuncSpec,
+ tag(FinalNotify)}},
+ ct_event:sync_notify(Event),
+ Result1;
+ Fun ->
+ %% send sync notification so that event handlers may print
+ %% in the log file before it gets closed
+ Event = #event{name=tc_done,
+ node=node(),
+ data={Mod,FuncSpec,
+ tag({'$test_server_framework_test',
+ FinalNotify})}},
+ ct_event:sync_notify(Event),
+ Fun(end_tc, Return)
+ end,
- case get('$test_server_framework_test') of
- undefined ->
- {FinalResult,FinalNotify} =
- case ct_hooks:end_tc(
- Suite, FuncSpec, Args, Result, Return) of
- '$ct_no_change' ->
- {ok,Result};
- FinalResult1 ->
- {FinalResult1,FinalResult1}
- end,
- %% send sync notification so that event handlers may print
- %% in the log file before it gets closed
- ct_event:sync_notify(#event{name=tc_done,
- node=node(),
- data={Mod,FuncSpec,
- tag_cth(FinalNotify)}});
- Fun ->
- %% send sync notification so that event handlers may print
- %% in the log file before it gets closed
- ct_event:sync_notify(#event{name=tc_done,
- node=node(),
- data={Mod,FuncSpec,tag(Result)}}),
- FinalResult = Fun(end_tc, Return)
- end,
-
case FuncSpec of
{_,GroupName,_Props} ->
if Func == end_per_group ->
@@ -683,12 +791,17 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
(undefined) ->
undefined;
(Unexpected) ->
- exit({error,{reset_curr_tc,{Mod,Func},Unexpected}})
+ {error,{reset_curr_tc,{Mod,Func},Unexpected}}
end,
- ct_util:update_testdata(curr_tc,ClearCurrTC),
+ case ct_util:update_testdata(curr_tc, ClearCurrTC) of
+ {error,_} = ClearError ->
+ exit(ClearError);
+ _ ->
+ ok
+ end,
case FinalResult of
- {skip,{sequence_failed,_,_}} ->
+ {auto_skip,{sequence_failed,_,_}} ->
%% ct_logs:init_tc is never called for a skipped test case
%% in a failing sequence, so neither should end_tc
ok;
@@ -710,32 +823,37 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
%% {error,Reason} | {skip,Reason} | {timetrap_timeout,TVal} |
%% {testcase_aborted,Reason} | testcase_aborted_or_killed |
-%% {'EXIT',Reason} | Other (ignored return value, e.g. 'ok')
+%% {'EXIT',Reason} | {fail,Reason} | {failed,Reason} |
+%% {user_timetrap_error,Reason} |
+%% Other (ignored return value, e.g. 'ok')
+tag({'$test_server_framework_test',Result}) ->
+ case tag(Result) of
+ ok -> Result;
+ Failure -> Failure
+ end;
+tag({skipped,Reason={failed,{_,init_per_testcase,_}}}) ->
+ {auto_skipped,Reason};
tag({STag,Reason}) when STag == skip; STag == skipped ->
- {skipped,Reason};
+ case Reason of
+ {failed,{_,init_per_testcase,_}} -> {auto_skipped,Reason};
+ _ -> {skipped,Reason}
+ end;
+tag({auto_skip,Reason}) ->
+ {auto_skipped,Reason};
+tag({fail,Reason}) ->
+ {failed,{error,Reason}};
+tag(Failed = {failed,_Reason}) ->
+ Failed;
tag(E = {ETag,_}) when ETag == error; ETag == 'EXIT';
- ETag == timetrap_timeout;
- ETag == testcase_aborted ->
+ ETag == timetrap_timeout;
+ ETag == testcase_aborted ->
{failed,E};
tag(E = testcase_aborted_or_killed) ->
{failed,E};
-tag(Other) ->
- Other.
-
-tag_cth({STag,Reason}) when STag == skip; STag == skipped ->
- {skipped,Reason};
-tag_cth({fail, Reason}) ->
- {failed, {error,Reason}};
-tag_cth(E = {ETag,_}) when ETag == error; ETag == 'EXIT';
- ETag == timetrap_timeout;
- ETag == testcase_aborted ->
- {failed,E};
-tag_cth(E = testcase_aborted_or_killed) ->
- {failed,E};
-tag_cth(List) when is_list(List) ->
- ok;
-tag_cth(Other) ->
- Other.
+tag(UserTimetrap = {user_timetrap_error,_Reason}) ->
+ UserTimetrap;
+tag(_Other) ->
+ ok.
%%%-----------------------------------------------------------------
%%% @spec error_notification(Mod,Func,Args,Error) -> ok
@@ -748,13 +866,13 @@ tag_cth(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])),
if length(Descr1) > 50 ->
@@ -769,12 +887,15 @@ error_notification(Mod,Func,_Args,{Error,Loc}) ->
io_lib:format("{test_case_failed,~p}", [Reason]);
Result -> Result
end;
+ {'EXIT',_Reason} = EXIT ->
+ io_lib:format("~P", [EXIT,5]);
{Spec,_Reason} when is_atom(Spec) ->
io_lib:format("~w", [Spec]);
Other ->
io_lib:format("~P", [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
@@ -789,7 +910,7 @@ error_notification(Mod,Func,_Args,{Error,Loc}) ->
_ ->
%% this notification comes from the test case process, so
%% we can add error info to comment with test_server:comment/1
- case ct_util:get_testdata(comment) of
+ case ct_util:get_testdata({comment,group_leader()}) of
undefined ->
test_server:comment(ErrorHtml);
Comment ->
@@ -802,41 +923,43 @@ error_notification(Mod,Func,_Args,{Error,Loc}) ->
end
end,
- PrintErr = fun(ErrFormat, ErrArgs) ->
- Div = "~n- - - - - - - - - - - - - - - - "
- "- - - - - - - - - -~n",
- io:format(user, lists:concat([Div,ErrFormat,Div,"~n"]),
- ErrArgs),
+ PrintError = fun(ErrorFormat, ErrorArgs) ->
+ Div = "~n- - - - - - - - - - - - - - - - - - - "
+ "- - - - - - - - - - - - - - - - - - - - -~n",
+ ErrorStr2 = io_lib:format(ErrorFormat, ErrorArgs),
+ io:format(user, 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:~w could not be executed~nReason: ~ts",
+ [LastMod,LastFunc,ErrorStr]);
[{LastMod,LastFunc}|_] ->
- PrintErr("~w:~w failed~nReason: ~ts", [LastMod,LastFunc,ErrStr]);
+ PrintError("~w:~w 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:~w failed on line ~w~nReason: ~ts",
+ [LastMod,LastFunc,LastLine,ErrorStr]),
case ct_util:read_suite_data({seq,Mod,Func}) of
undefined ->
@@ -992,9 +1115,32 @@ get_all_cases1(_, []) ->
get_all(Mod, ConfTests) ->
case catch apply(Mod, all, []) of
- {'EXIT',_} ->
- Reason =
- list_to_atom(atom_to_list(Mod)++":all/0 is missing"),
+ {'EXIT',{undef,[{Mod,all,[],_} | _]}} ->
+ Reason =
+ case code:which(Mod) of
+ non_existing ->
+ list_to_atom(atom_to_list(Mod)++
+ " can not be compiled or loaded");
+ _ ->
+ list_to_atom(atom_to_list(Mod)++":all/0 is missing")
+ end,
+ %% this makes test_server call error_in_suite as first
+ %% (and only) test case so we can report Reason properly
+ [{?MODULE,error_in_suite,[[{error,Reason}]]}];
+ {'EXIT',ExitReason} ->
+ case ct_util:get_testdata({error_in_suite,Mod}) of
+ undefined ->
+ ErrStr = io_lib:format("~n*** ERROR *** "
+ "~w:all/0 failed: ~p~n",
+ [Mod,ExitReason]),
+ io:format(user, ErrStr, []),
+ %% save the error info so it doesn't get printed twice
+ ct_util:set_testdata_async({{error_in_suite,Mod},
+ ExitReason});
+ _ExitReason ->
+ ct_util:delete_testdata({error_in_suite,Mod})
+ end,
+ Reason = list_to_atom(atom_to_list(Mod)++":all/0 failed"),
%% this makes test_server call error_in_suite as first
%% (and only) test case so we can report Reason properly
[{?MODULE,error_in_suite,[[{error,Reason}]]}];
@@ -1169,41 +1315,10 @@ 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 ->
- case ct_util:get_testdata(cover) of
- undefined ->
- ok;
- {_CovFile,_CovNodes,CovImport,CovExport,_CovAppData} ->
- %% Always import cover data from files specified by CovImport
- %% if no CovExport defined. If CovExport is defined, only
- %% import from CovImport files initially, then use CovExport
- %% to pass coverdata between proceeding tests (in the same run).
- Imps =
- case CovExport of
- [] -> % don't export data between tests
- CovImport;
- _ ->
- case filelib:is_file(CovExport) of
- true ->
- [CovExport];
- false ->
- CovImport
- end
- end,
- lists:foreach(
- fun(Imp) ->
- case cover:import(Imp) of
- ok ->
- ok;
- {error,Reason} ->
- ct_logs:log("COVER INFO",
- "Importing cover data from: ~ts fails! "
- "Reason: ~p", [Imp,Reason])
- end
- end, Imps)
- end;
+ ok;
tests_done ->
ok;
severe_error ->
@@ -1213,26 +1328,43 @@ report(What,Data) ->
ct_util:set_testdata({What,Data}),
ok;
tc_start ->
- %% Data = {{Suite,Func},LogFileName}
+ %% Data = {{Suite,{Func,GroupName}},LogFileName}
+ Data1 = case Data of
+ {{Suite,{Func,undefined}},LFN} -> {{Suite,Func},LFN};
+ _ -> Data
+ end,
ct_event:sync_notify(#event{name=tc_logfile,
node=node(),
- data=Data}),
+ data=Data1}),
ok;
tc_done ->
- {_Suite,Case,Result} = Data,
+ {Suite,{Func,GrName},Result} = Data,
+ Data1 = if GrName == undefined -> {Suite,Func,Result};
+ true -> Data
+ 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, Data);
+ ct_hooks:on_tc_fail(What, Data1);
{skipped,{failed,{_,init_per_testcase,_}}} ->
- ct_hooks:on_tc_skip(tc_auto_skip, Data);
+ ct_hooks:on_tc_skip(tc_auto_skip, Data1);
{skipped,{require_failed,_}} ->
- ct_hooks:on_tc_skip(tc_auto_skip, Data);
+ ct_hooks:on_tc_skip(tc_auto_skip, Data1);
{skipped,_} ->
- ct_hooks:on_tc_skip(tc_user_skip, Data);
+ ct_hooks:on_tc_skip(tc_user_skip, Data1);
+ {auto_skipped,_} ->
+ ct_hooks:on_tc_skip(tc_auto_skip, Data1);
_Else ->
ok
end,
- case {Case,Result} of
+ ct_logs:unregister_groupleader(ReportingPid),
+ case {Func,Result} of
+ {error_in_suite,_} when Suite == ?MODULE ->
+ ok;
{init_per_suite,_} ->
ok;
{end_per_suite,_} ->
@@ -1253,31 +1385,49 @@ report(What,Data) ->
add_to_stats(auto_skipped);
{_,{skipped,_}} ->
add_to_stats(user_skipped);
+ {_,{auto_skipped,_}} ->
+ add_to_stats(auto_skipped);
{_,{SkipOrFail,_Reason}} ->
add_to_stats(SkipOrFail)
end;
- tc_user_skip ->
- %% test case specified as skipped in testspec
- %% Data = {Suite,Case,Comment}
+ tc_user_skip ->
+ %% test case or config function specified as skipped in testspec,
+ %% or init config func for suite/group has returned {skip,Reason}
+ %% Data = {Suite,{Func,GroupName},Comment}
+ {Func,Data1} = case Data of
+ {Suite,{F,undefined},Comment} ->
+ {F,{Suite,F,Comment}};
+ D = {_,{F,_},_} ->
+ {F,D}
+ end,
ct_event:sync_notify(#event{name=tc_user_skip,
node=node(),
- data=Data}),
- ct_hooks:on_tc_skip(What, Data),
- add_to_stats(user_skipped);
+ data=Data1}),
+ ct_hooks:on_tc_skip(What, Data1),
+ if Func /= init_per_suite, Func /= init_per_group,
+ Func /= end_per_suite, Func /= end_per_group ->
+ add_to_stats(user_skipped);
+ true ->
+ ok
+ end;
tc_auto_skip ->
- %% test case skipped because of error in init_per_suite
- %% Data = {Suite,Case,Comment}
-
- {_Suite,Case,_Result} = Data,
-
+ %% test case skipped because of error in config function, or
+ %% config function skipped because of error in info function
+ %% Data = {Suite,{Func,GroupName},Comment}
+ {Func,Data1} = case Data of
+ {Suite,{F,undefined},Comment} ->
+ {F,{Suite,F,Comment}};
+ D = {_,{F,_},_} ->
+ {F,D}
+ end,
%% this test case does not have a log, so printouts
%% from event handlers should end up in the main log
ct_event:sync_notify(#event{name=tc_auto_skip,
node=node(),
- data=Data}),
- ct_hooks:on_tc_skip(What, Data),
- if Case /= end_per_suite,
- Case /= end_per_group ->
+ data=Data1}),
+ ct_hooks:on_tc_skip(What, Data1),
+ if Func /= end_per_suite,
+ Func /= end_per_group ->
add_to_stats(auto_skipped);
true ->
ok
@@ -1328,7 +1478,7 @@ warn(_What) ->
true.
%%%-----------------------------------------------------------------
-%%% @spec add_data_dir(File0) -> File1
+%%% @spec add_data_dir(File0, Config) -> File1
add_data_dir(File,Config) when is_atom(File) ->
add_data_dir(atom_to_list(File),Config);
@@ -1368,3 +1518,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 71fd8754ff..84e664b387 100644
--- a/lib/common_test/src/ct_ftp.erl
+++ b/lib/common_test/src/ct_ftp.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -291,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
diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl
index a5b736136f..23ba1ab981 100644
--- a/lib/common_test/src/ct_gen_conn.erl
+++ b/lib/common_test/src/ct_gen_conn.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -24,10 +25,16 @@
-module(ct_gen_conn).
--compile(export_all).
-
--export([start/4, stop/1, get_conn_pid/1]).
+-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]).
-ifdef(debug).
-define(dbg,true).
@@ -47,6 +54,18 @@
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}
@@ -288,7 +307,8 @@ call(Pid, Msg, Timeout) ->
end.
return({To,Ref},Result) ->
- To ! {Ref, Result}.
+ To ! {Ref, Result},
+ ok.
init_gen(Parent,Opts) ->
process_flag(trap_exit,true),
@@ -325,7 +345,7 @@ loop(Opts) ->
link(NewPid),
put(conn_pid,NewPid),
loop(Opts#gen_opts{conn_pid=NewPid,
- cb_state=NewState});
+ cb_state=NewState});
Error ->
ct_util:unregister_connection(self()),
log("Reconnect failed. Giving up!",
diff --git a/lib/common_test/src/ct_groups.erl b/lib/common_test/src/ct_groups.erl
index 14a8aab881..1375e7dcc7 100644
--- a/lib/common_test/src/ct_groups.erl
+++ b/lib/common_test/src/ct_groups.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -80,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))) ++
@@ -90,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))) ++
@@ -132,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))) ++
@@ -283,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.
%%%-----------------------------------------------------------------
@@ -414,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) ->
@@ -440,16 +423,19 @@ 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);
_ ->
diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl
index 3d87a82e24..c9a4abb5ee 100644
--- a/lib/common_test/src/ct_hooks.erl
+++ b/lib/common_test/src/ct_hooks.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -50,9 +51,8 @@
-spec init(State :: term()) -> ok |
{fail, Reason :: term()}.
init(Opts) ->
- call(get_new_hooks(Opts, undefined) ++ get_builtin_hooks(Opts),
+ call(get_builtin_hooks(Opts) ++ get_new_hooks(Opts, undefined),
ok, init, []).
-
%% @doc Called after all suites are done.
-spec terminate(Hooks :: term()) ->
@@ -65,11 +65,18 @@ terminate(Hooks) ->
%% @doc Called as each test case is started. This includes all configuration
%% tests.
--spec init_tc(Mod :: atom(), Func :: atom(), Args :: list()) ->
+-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()},
+ Args :: list()) ->
NewConfig :: proplists:proplist() |
- {skip, Reason :: term()} |
- {auto_skip, Reason :: term()} |
- {fail, Reason :: term()}.
+ {skip, Reason :: term()} |
+ {auto_skip, Reason :: term()} |
+ {fail, Reason :: term()}.
init_tc(Mod, init_per_suite, Config) ->
Info = try proplists:get_value(ct_hooks, Mod:suite(),[]) of
@@ -83,26 +90,35 @@ init_tc(Mod, init_per_suite, Config) ->
call(fun call_generic/3, Config ++ Info, [pre_init_per_suite, Mod]);
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, Opts}, Config) ->
- maybe_start_locker(Mod, GroupName, Opts),
+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) ->
+init_tc(_Mod, {init_per_testcase,TC}, Config) ->
+ call(fun call_generic/3, Config, [pre_init_per_testcase, TC]);
+init_tc(_Mod, {end_per_testcase,TC}, Config) ->
+ call(fun call_generic/3, Config, [pre_end_per_testcase, TC]);
+init_tc(_Mod, TC = error_in_suite, Config) ->
call(fun call_generic/3, Config, [pre_init_per_testcase, TC]).
%% @doc Called as each test case is completed. This includes all configuration
%% tests.
-spec end_tc(Mod :: atom(),
- Func :: atom(),
+ FuncSpec :: atom() |
+ {ConfigFunc :: init_per_testcase | end_per_testcase,
+ TestCase :: atom()} |
+ {ConfigFunc :: init_per_group | end_per_group,
+ GroupName :: atom(),
+ Properties :: list()},
Args :: list(),
Result :: term(),
- Resturn :: term()) ->
+ Return :: term()) ->
NewConfig :: proplists:proplist() |
- {skip, Reason :: term()} |
- {auto_skip, Reason :: term()} |
- {fail, Reason :: term()} |
- ok | '$ct_no_change'.
+ {skip, Reason :: term()} |
+ {auto_skip, Reason :: term()} |
+ {fail, Reason :: term()} |
+ ok | '$ct_no_change'.
end_tc(Mod, init_per_suite, Config, _Result, Return) ->
call(fun call_generic/3, Return, [post_init_per_suite, Mod, Config],
@@ -113,18 +129,27 @@ end_tc(Mod, end_per_suite, Config, Result, _Return) ->
end_tc(_Mod, {init_per_group, GroupName, _}, Config, _Result, Return) ->
call(fun call_generic/3, Return, [post_init_per_group, GroupName, Config],
'$ct_no_change');
-end_tc(Mod, {end_per_group, GroupName, Opts}, Config, Result, _Return) ->
+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'),
- maybe_stop_locker(Mod, GroupName,Opts),
+ maybe_stop_locker(Mod, GroupName, Properties),
Res;
-end_tc(_Mod, TC, Config, Result, _Return) ->
+end_tc(_Mod, {init_per_testcase,TC}, Config, Result, _Return) ->
+ call(fun call_generic/3, Result, [post_init_per_testcase, TC, Config],
+ '$ct_no_change');
+end_tc(_Mod, {end_per_testcase,TC}, Config, Result, _Return) ->
+ call(fun call_generic/3, Result, [post_end_per_testcase, TC, Config],
+ '$ct_no_change');
+end_tc(_Mod, TC = error_in_suite, Config, Result, _Return) ->
call(fun call_generic/3, Result, [post_end_per_testcase, TC, Config],
'$ct_no_change').
+
+%% Case = TestCase | {TestCase,GroupName}
on_tc_skip(How, {Suite, Case, Reason}) ->
call(fun call_cleanup/3, {How, Reason}, [on_tc_skip, Suite, Case]).
+%% Case = TestCase | {TestCase,GroupName}
on_tc_fail(_How, {Suite, Case, Reason}) ->
call(fun call_cleanup/3, Reason, [on_tc_fail, Suite, Case]).
@@ -234,6 +259,8 @@ remove(_, Else) ->
%% Translate scopes, i.e. init_per_group,group1 -> end_per_group,group1 etc
scope([pre_init_per_testcase, TC|_]) ->
+ [post_init_per_testcase, TC];
+scope([pre_end_per_testcase, TC|_]) ->
[post_end_per_testcase, TC];
scope([pre_init_per_group, GroupName|_]) ->
[post_end_per_group, GroupName];
@@ -276,8 +303,10 @@ get_new_hooks(Config, Fun) ->
end, get_new_hooks(Config)).
get_new_hooks(Config) when is_list(Config) ->
- lists:flatmap(fun({?config_name, HookConfigs}) ->
+ lists:flatmap(fun({?config_name, HookConfigs}) when is_list(HookConfigs) ->
HookConfigs;
+ ({?config_name, HookConfig}) when is_atom(HookConfig) ->
+ [HookConfig];
(_) ->
[]
end, Config);
@@ -305,7 +334,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;
@@ -355,10 +385,10 @@ pos(Id,[_|Rest],Num) ->
catch_apply(M,F,A, Default) ->
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.
+ %% Return the default if it was the CTH module which did not have the function.
[{M,F,A,_}|_] when Reason == undef ->
Default;
Trace ->
@@ -378,7 +408,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 e33fa278dc..fea298e535 100644
--- a/lib/common_test/src/ct_hooks_lock.erl
+++ b/lib/common_test/src/ct_hooks_lock.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2011. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -81,7 +82,7 @@ init(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 f5355bfefe..53245c596a 100644
--- a/lib/common_test/src/ct_logs.erl
+++ b/lib/common_test/src/ct_logs.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -29,21 +30,24 @@
-module(ct_logs).
-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([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]).
%% Logging stuff directly from testcase
--export([tc_log/3, tc_log/4, tc_log/5, tc_log_async/3, tc_log_async/5,
+-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_pal/3, tc_pal/4, ct_log/3, basic_html/0]).
+ tc_pal/3, tc_pal/4, ct_log/3,
+ basic_html/0]).
%% Simulate logger process for use without ct environment running
-export([simulate/0]).
@@ -61,6 +65,8 @@
-define(index_name, "index.html").
-define(totals_name, "totals.info").
-define(log_cache_name, "ct_log_cache").
+-define(misc_io_log, "misc_io.log.html").
+-define(coverlog_name, "cover.html"). % must be same as in test_server_ctrl
-define(table_color1,"#ADD8E6").
-define(table_color2,"#E4F0FE").
@@ -70,12 +76,14 @@
-define(abs(Name), filename:absname(Name)).
+-define(now, os:timestamp()).
+
-record(log_cache, {version,
all_runs = [],
tests = []}).
%%%-----------------------------------------------------------------
-%%% @spec init(Mode) -> Result
+%%% @spec init(Mode, Verbosity) -> Result
%%% Mode = normal | interactive
%%% Result = {StartTime,LogDir}
%%% StartTime = term()
@@ -127,7 +135,14 @@ datestr_from_dirname([]) ->
close(Info, StartDir) ->
%% close executes on the ct_util process, not on the logger process
%% so we need to use a local copy of the log cache data
- LogCacheBin = make_last_run_index(),
+ LogCacheBin =
+ case make_last_run_index() of
+ {error, Reason} -> % log server not responding
+ io:format("Warning! ct_logs not responding: ~p~n", [Reason]),
+ undefined;
+ LCB ->
+ LCB
+ end,
put(ct_log_cache,LogCacheBin),
Cache2File = fun() ->
case get(ct_log_cache) of
@@ -135,7 +150,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,
@@ -161,12 +176,12 @@ close(Info, StartDir) ->
Error ->
io:format("Warning! Cleanup failed: ~p~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
@@ -226,7 +241,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]),
@@ -237,16 +252,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
%%%
@@ -255,11 +284,11 @@ 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.
%%%-----------------------------------------------------------------
-%%% @spec end_tc(TCPid) -> ok | {error,Reason}
+%%% @spec end_tc(TCPid) -> ok
%%%
%%% @doc Test case clean up (tool-internal use only).
%%%
@@ -270,6 +299,26 @@ end_tc(TCPid) ->
call({end_tc,TCPid}).
%%%-----------------------------------------------------------------
+%%% @spec register_groupleader(Pid,GroupLeader) -> ok
+%%%
+%%% @doc To enable logging to a group leader (tool-internal use only).
+%%%
+%%% <p>This function is called by ct_framework:report/2</p>
+register_groupleader(Pid,GroupLeader) ->
+ call({register_groupleader,Pid,GroupLeader}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec unregister_groupleader(Pid) -> ok
+%%%
+%%% @doc To disable logging to a group leader (tool-internal use only).
+%%%
+%%% <p>This function is called by ct_framework:report/2</p>
+unregister_groupleader(Pid) ->
+ call({unregister_groupleader,Pid}),
+ ok.
+
+%%%-----------------------------------------------------------------
%%% @spec log(Heading,Format,Args) -> ok
%%%
%%% @doc Log internal activity (tool-internal use only).
@@ -282,9 +331,10 @@ end_tc(TCPid) ->
%%% 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.
%%%-----------------------------------------------------------------
@@ -304,7 +354,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.
%%%-----------------------------------------------------------------
@@ -319,7 +369,7 @@ 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.
%%%-----------------------------------------------------------------
@@ -331,7 +381,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.
@@ -368,32 +418,46 @@ 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,Printer,Format,Args,Opts) -> ok
%%% Category = atom()
%%% Importance = integer()
%%% Printer = 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,Printer,Format,Args,Opts) ->
+ Data =
+ case lists:member(no_css, Opts) of
+ true ->
+ [{Format,Args}];
+ false ->
+ [{hd,div_header(Category,Printer),[]},
+ {Format,Args},
+ {ft,div_footer(),[]}]
+ end,
+ cast({log,sync,self(),group_leader(),Category,Importance,Data,
+ lists:member(esc_chars, Opts)}),
ok.
%%%-----------------------------------------------------------------
@@ -419,9 +483,10 @@ tc_log_async(Category,Format,Args) ->
%%% asks ct_logs for an html wrapper.</p>
tc_log_async(Category,Importance,Printer,Format,Args) ->
cast({log,async,self(),group_leader(),Category,Importance,
- [{div_header(Category,Printer),[]},
+ [{hd,div_header(Category,Printer),[]},
{Format,Args},
- {div_footer(),[]}]}),
+ {ft,div_footer(),[]}],
+ true}),
ok.
%%%-----------------------------------------------------------------
%%% @spec tc_print(Category,Format,Args)
@@ -446,6 +511,8 @@ tc_print(Category,Importance,Format,Args) ->
ct_util:get_verbosity('$unspecified');
{error,bad_invocation} ->
?MAX_VERBOSITY;
+ {error,_Failure} ->
+ ?MAX_VERBOSITY;
Val ->
Val
end,
@@ -460,11 +527,11 @@ tc_print(Category,Importance,Format,Args) ->
get_heading(default) ->
io_lib:format("\n-----------------------------"
"-----------------------\n~s\n",
- [log_timestamp(now())]);
+ [log_timestamp(?now)]);
get_heading(Category) ->
io_lib:format("\n-----------------------------"
"-----------------------\n~s ~w\n",
- [log_timestamp(now()),Category]).
+ [log_timestamp(?now),Category]).
%%%-----------------------------------------------------------------
@@ -488,53 +555,55 @@ tc_pal(Category,Format,Args) ->
tc_pal(Category,Importance,Format,Args) ->
tc_print(Category,Importance,Format,Args),
cast({log,sync,self(),group_leader(),Category,Importance,
- [{div_header(Category),[]},
+ [{hd,div_header(Category),[]},
{Format,Args},
- {div_footer(),[]}]}),
+ {ft,div_footer(),[]}],
+ true}),
ok.
%%%-----------------------------------------------------------------
-%%% @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) ->
- "<div class=\"" ++ atom_to_list(Class) ++ "\"><b>*** " ++ Printer ++
- " " ++ log_timestamp(now()) ++ " ***</b>".
+ "\n</pre>\n<div class=\"" ++ atom_to_list(Class) ++ "\"><pre><b>*** "
+ ++ Printer ++ " " ++ log_timestamp(?now) ++ " ***</b>".
div_footer() ->
- "</div>".
+ "</pre></div>\n<pre>".
maybe_log_timestamp() ->
- {MS,S,US} = now(),
+ {MS,S,US} = ?now,
case get(log_timestamp) of
{MS,S,_} ->
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}) ->
@@ -555,7 +624,8 @@ 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()),
@@ -576,7 +646,7 @@ logger(Parent, Mode, Verbosity) ->
end,
%%! <---
- file:make_dir(Dir),
+ _ = file:make_dir(Dir),
AbsDir = ?abs(Dir),
put(ct_run_dir, AbsDir),
@@ -615,19 +685,47 @@ logger(Parent, Mode, Verbosity) ->
end
end
end,
+
+ _ = test_server_io:start_link(),
+ MiscIoName = filename:join(Dir, ?misc_io_log),
+ {ok,MiscIoFd} = file:open(MiscIoName,
+ [write,{encoding,utf8}]),
+ test_server_io:set_fd(unexpected_io, MiscIoFd),
+
+ {MiscIoHeader,MiscIoFooter} =
+ case get_ts_html_wrapper("Pre/post-test I/O log", Dir, false,
+ Dir, undefined, utf8) of
+ {basic_html,UH,UF} ->
+ {UH,UF};
+ {xhtml,UH,UF} ->
+ {UH,UF}
+ end,
+ io:put_chars(MiscIoFd,
+ [MiscIoHeader,
+ "<a name=\"pretest\"></a>\n",
+ xhtml("<br>\n<h2>Pre-test Log</h2>",
+ "<br />\n<h3>PRE-TEST LOG</h3>"),
+ "\n<pre>\n"]),
+ MiscIoDivider =
+ "\n<a name=\"posttest\"></a>\n"++
+ xhtml("</pre>\n<br><h2>Post-test Log</h2>\n<pre>\n",
+ "</pre>\n<br />\n<h3>POST-TEST LOG</h3>\n<pre>\n"),
+ ct_util:set_testdata_async({misc_io_log,{filename:absname(MiscIoName),
+ MiscIoDivider,MiscIoFooter}}),
+
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),
- CtLogFd = open_ctlog(),
+ 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"]),
+ [log_timestamp(?now),"Common Test Logger started"]),
Parent ! {started,self(),{Time,filename:absname("")}},
set_evmgr_gl(CtLogFd),
@@ -638,22 +736,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
@@ -667,7 +769,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;
@@ -678,26 +780,31 @@ logger_loop(State) ->
end
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,List,State),
+ unexpected_io(Pid, Category, Importance,
+ Content, CtLogFd, DoEscChars),
+
logger_loop(State)
end;
{ct_log,_Fd,TCGLs} ->
%% If category is ct_internal then write
%% to ct_log, else write to unexpected_io
%% log
- unexpected_io(Pid,Category,List,State),
+ unexpected_io(Pid, Category, Importance, Content,
+ CtLogFd, DoEscChars),
logger_loop(State#logger_state{
tc_groupleaders = TCGLs})
end;
@@ -708,10 +815,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)
@@ -723,6 +831,14 @@ logger_loop(State) ->
return(From,ok),
logger_loop(State#logger_state{tc_groupleaders =
rm_tc_gl(TCPid,State)});
+ {{register_groupleader,Pid,GL},From} ->
+ GLs = add_tc_gl(Pid,GL,State),
+ return(From,ok),
+ logger_loop(State#logger_state{tc_groupleaders = GLs});
+ {{unregister_groupleader,Pid},From} ->
+ return(From,ok),
+ logger_loop(State#logger_state{tc_groupleaders =
+ rm_tc_gl(Pid,State)});
{{get_log_dir,true},From} ->
return(From,{ok,State#logger_state.log_dir}),
logger_loop(State);
@@ -730,7 +846,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 ==
@@ -745,10 +861,17 @@ logger_loop(State) ->
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
@@ -765,56 +888,118 @@ logger_loop(State) ->
stop ->
io:format(State#logger_state.ct_log_fd,
int_header()++int_footer(),
- [log_timestamp(now()),"Common Test Logger finished"]),
+ [log_timestamp(?now),"Common Test Logger finished"]),
close_ctlog(State#logger_state.ct_log_fd),
ok
end.
-create_io_fun(FromPid, State) ->
+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
- Fd = State#logger_state.ct_log_fd,
- fun({Str,Args}, IoList) ->
- case catch io_lib:format(Str,Args) of
- {'EXIT',_Reason} ->
- io:format(Fd, "Logging fails! Str: ~p, Args: ~p~n",
- [Str,Args]),
- %% stop the testcase, we need to see the fault
- exit(FromPid, {log_printout_error,Str,Args}),
- [];
+ 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: ~p, Args: ~p~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]) ->
+ ["&lt;" | escape_chars(Io)];
+escape_chars([$> | Io]) ->
+ ["&gt;" | escape_chars(Io)];
+escape_chars([$& | Io]) ->
+ ["&amp;" | 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, State),
- 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,List,State)
+ 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, State),
+ IoFun = create_io_fun(FromPid, CtLogFd, EscChars),
fun() ->
test_server:permit_io(TCGL, self()),
- io:format(TCGL, "~ts", [lists:foldl(IoFun, [], List)])
+
+ %% Since asynchronous io gets can get buffered if
+ %% the file system is slow, there is also a risk that
+ %% the group leader has terminated before we get to
+ %% the io:format(GL, ...) call. We check this and
+ %% print "expired" messages to the unexpected io
+ %% log instead (best we can do).
+
+ case erlang:is_process_alive(TCGL) of
+ true ->
+ try tc_io_format(TCGL, "~ts",
+ [lists:foldl(IoFun,[],Content)]) of
+ _ -> ok
+ catch
+ _:terminated ->
+ unexpected_io(FromPid, Category,
+ ?MAX_IMPORTANCE,
+ Content, CtLogFd, EscChars);
+ _:_ ->
+ io:format(TCGL, "~ts",
+ [lists:foldl(IoFun,[],Content)])
+ end;
+ false ->
+ unexpected_io(FromPid, Category,
+ ?MAX_IMPORTANCE,
+ Content, CtLogFd, EscChars)
+ end
end;
true ->
fun() ->
- unexpected_io(FromPid,Category,List,State)
+ unexpected_io(FromPid, Category, ?MAX_IMPORTANCE,
+ Content, CtLogFd, EscChars)
end
end,
case State#logger_state.async_print_jobs of
@@ -918,7 +1103,7 @@ set_evmgr_gl(GL) ->
EvMgrPid -> group_leader(GL,EvMgrPid)
end.
-open_ctlog() ->
+open_ctlog(MiscIoName) ->
{ok,Fd} = file:open(?ct_log_name,[write,{encoding,utf8}]),
io:format(Fd, header("Common Test Framework Log", {[],[1,2],[]}), []),
case file:consult(ct_run:variables_file_name("../")) of
@@ -933,26 +1118,36 @@ open_ctlog() ->
"No configuration found for test!!\n",
[Variables,Reason])
end,
- print_style(Fd,undefined),
io:format(Fd,
- xhtml("<br><br><h2>Progress Log</h2>\n<pre>\n",
- "<br /><br /><h4>PROGRESS LOG</h4>\n<pre>\n"), []),
+ xhtml("<br><br><h2>Pre/post-test I/O Log</h2>\n",
+ "<br /><br />\n<h4>PRE/POST TEST I/O LOG</h4>\n"), []),
+ io:format(Fd,
+ "\n<ul>\n"
+ "<li><a href=\"~ts#pretest\">"
+ "View I/O logged before the test run</a></li>\n"
+ "<li><a href=\"~ts#posttest\">"
+ "View I/O logged after the test run</a></li>\n</ul>\n",
+ [MiscIoName,MiscIoName]),
+
+ 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)),
@@ -965,34 +1160,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: ~p -->\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()).
%%%-----------------------------------------------------------------
@@ -1205,7 +1420,8 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip,
integer_to_list(NotBuilt),"</a></td>\n"]
end,
FailStr =
- if Fail > 0 ->
+ if (Fail > 0) or (NotBuilt > 0) or
+ ((Success+Fail+UserSkip+AutoSkip) == 0) ->
["<font color=\"red\">",
integer_to_list(Fail),"</font>"];
true ->
@@ -1254,12 +1470,13 @@ 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",
- AllInfo, "</tr>\n</tfoot>\n"].
+ "<td align=right><b>",integer_to_list(NotBuilt),"</b></td>\n",
+ AllInfo, "</tr>\n",
+ xhtml("","</tfoot>\n")].
not_built(_BaseName,_LogDir,_All,[]) ->
0;
@@ -1323,6 +1540,19 @@ index_header(Label, StartTime) ->
format_time(StartTime),
{[],[1],[2,3,4,5]})
end,
+ Cover =
+ case filelib:is_regular(?abs(?coverlog_name)) of
+ true ->
+ xhtml(["<p><a href=\"",?coverlog_name,
+ "\">Cover Log</a></p><br>\n"],
+ ["<br />"
+ "<div id=\"button_holder\" class=\"btn\">\n"
+ "<a href=\"",?coverlog_name,
+ "\">COVER LOG</a>\n</div><br /><br />"]);
+ false ->
+ xhtml("<br>\n", "<br /><br /><br />\n")
+ end,
+
[Head |
["<center>\n",
xhtml(["<p><a href=\"",?ct_log_name,
@@ -1330,8 +1560,8 @@ index_header(Label, StartTime) ->
["<br />"
"<div id=\"button_holder\" class=\"btn\">\n"
"<a href=\"",?ct_log_name,
- "\">COMMON TEST FRAMEWORK LOG</a>\n</div>"]),
- xhtml("<br>\n", "<br /><br /><br />\n"),
+ "\">COMMON TEST FRAMEWORK LOG</a>\n</div><br>\n"]),
+ Cover,
xhtml(["<table border=\"3\" cellpadding=\"5\" "
"bgcolor=\"",?table_color3,"\">\n"],
["<table id=\"",?sortable_table_name,"\">\n",
@@ -1436,10 +1666,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"]),
@@ -1474,7 +1706,8 @@ all_suites_index_footer() ->
xhtml("<br><br>\n", "<br /><br />\n") | footer()].
all_runs_index_footer() ->
- ["</tbody>\n</table>\n",
+ [xhtml("", "</tbody>\n"),
+ "</table>\n",
"</center>\n",
xhtml("<br><br>\n", "<br /><br />\n") | footer()].
@@ -1485,7 +1718,7 @@ footer() ->
"Copyright &copy; ", 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"
@@ -1555,7 +1788,7 @@ count_cases(Dir) ->
%% file yet.
{0,0,0,0};
Summary ->
- write_summary(SumFile, Summary),
+ _ = write_summary(SumFile, Summary),
Summary
end;
{error, Reason} ->
@@ -1631,7 +1864,7 @@ config_table(Vars) ->
config_table_header() ->
[
xhtml(["<h2>Configuration</h2>\n"
- "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color1,"\"\n"],
+ "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color1,"\">\n"],
["<h4>CONFIGURATION</h4>\n",
"<table id=\"",?sortable_table_name,"\">\n",
"<thead>\n"]),
@@ -1647,7 +1880,7 @@ config_table1([{Key,Value}|Vars]) ->
"<td>", io_lib:format("~p",[Value]), "</td>\n</tr>\n"]) |
config_table1(Vars)];
config_table1([]) ->
- ["</tbody>\n</table>\n"].
+ [xhtml("","</tbody>\n"),"</table>\n"].
make_all_runs_index(When) ->
@@ -1655,7 +1888,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
@@ -1783,6 +2016,18 @@ sort_all_runs(Dirs) ->
{Date1,HH1,MM1,SS1} > {Date2,HH2,MM2,SS2}
end, Dirs).
+sort_ct_runs(Dirs) ->
+ %% Directory naming: <Prefix>.NodeName.Date_Time[/...]
+ %% Sort on Date_Time string: "YYYY-MM-DD_HH.MM.SS"
+ lists:sort(
+ fun(Dir1,Dir2) ->
+ [SS1,MM1,DateHH1 | _] =
+ lists:reverse(string:tokens(filename:dirname(Dir1),[$.])),
+ [SS2,MM2,DateHH2 | _] =
+ lists:reverse(string:tokens(filename:dirname(Dir2),[$.])),
+ {DateHH1,MM1,SS1} =< {DateHH2,MM2,SS2}
+ end, Dirs).
+
dir_diff_all_runs(Dirs, LogCache) ->
case LogCache#log_cache.all_runs of
[] ->
@@ -1797,14 +2042,27 @@ dir_diff_all_runs(LogDirs=[Dir|Dirs], Cached=[CElem|CElems],
LatestInCache, AllRunsDirs) ->
DirDate = datestr_from_dirname(Dir),
if DirDate > LatestInCache ->
- %% Dir is a new run entry
+ %% Dir is a new run entry (not cached)
dir_diff_all_runs(Dirs, Cached, LatestInCache,
[Dir|AllRunsDirs]);
DirDate == LatestInCache, CElems /= [] ->
- %% Dir is an existing run entry
+ %% Dir is an existing (cached) run entry
+
+ %% Only add the cached element instead of Dir if the totals
+ %% are "non-empty" (a test might be executing on a different
+ %% node and results haven't been saved yet)
+ ElemToAdd =
+ case CElem of
+ {_CDir,{_NodeStr,_Label,_Logs,{0,0,0,0,0}},_IxLink} ->
+ %% "empty" element in cache - this could be an
+ %% incomplete test and should be checked again
+ Dir;
+ _ ->
+ CElem
+ end,
dir_diff_all_runs(Dirs, CElems,
datestr_from_dirname(element(1,hd(CElems))),
- [CElem|AllRunsDirs]);
+ [ElemToAdd|AllRunsDirs]);
DirDate == LatestInCache, CElems == [] ->
%% we're done, Dirs must all be new
lists:reverse(Dirs)++[CElem|AllRunsDirs];
@@ -1835,9 +2093,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),"\">",
@@ -1845,7 +2103,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")]).
@@ -1860,7 +2118,8 @@ runentry(Dir, undefined, _) ->
runentry(Dir, Totals={Node,Label,Logs,
{TotSucc,TotFail,UserSkip,AutoSkip,NotBuilt}}, Index) ->
TotFailStr =
- if TotFail > 0 ->
+ if (TotFail > 0) or (NotBuilt > 0) or
+ ((TotSucc+TotFail+UserSkip+AutoSkip) == 0) ->
["<font color=\"red\">",
integer_to_list(TotFail),"</font>"];
true ->
@@ -1904,6 +2163,13 @@ runentry(Dir, Totals={Node,Label,Logs,
?testname_width-3)),
lists:flatten(io_lib:format("~ts...",[Trunc]))
end,
+ TotMissingStr =
+ if NotBuilt > 0 ->
+ ["<font color=\"red\">",
+ integer_to_list(NotBuilt),"</font>"];
+ true ->
+ integer_to_list(NotBuilt)
+ end,
Total = TotSucc+TotFail+AllSkip,
A = xhtml(["<td align=center><font size=\"-1\">",Node,
"</font></td>\n",
@@ -1923,7 +2189,7 @@ runentry(Dir, Totals={Node,Label,Logs,
"<td align=right>",TotFailStr,"</td>\n",
"<td align=right>",integer_to_list(AllSkip),
" (",UserSkipStr,"/",AutoSkipStr,")</td>\n",
- "<td align=right>",integer_to_list(NotBuilt),"</td>\n"],
+ "<td align=right>",TotMissingStr,"</td>\n"],
TotalsStr = A++B++C,
XHTML = [xhtml("<tr>\n", ["<tr class=\"",odd_or_even(),"\">\n"]),
@@ -1968,9 +2234,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
@@ -2015,7 +2281,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) ->
@@ -2081,7 +2347,8 @@ make_all_suites_index(When) when is_atom(When) ->
end
end,
- LogDirs = filelib:wildcard(logdir_prefix()++".*/*"++?logdir_ext),
+ Wildcard = logdir_prefix()++".*/*"++?logdir_ext,
+ LogDirs = sort_ct_runs(filelib:wildcard(Wildcard)),
LogCacheInfo = get_cache_data(UseCache),
@@ -2360,7 +2627,7 @@ 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} ->
@@ -2565,18 +2832,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;
@@ -2687,8 +2954,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 ->
@@ -2852,6 +3123,9 @@ make_relative1(DirTs, CwdTs) ->
%%% @doc
%%%
get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) ->
+ get_ts_html_wrapper(TestName, undefined, PrintLabel, Cwd, TableCols, Encoding).
+
+get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) ->
TestName1 = if is_list(TestName) ->
lists:flatten(TestName);
true ->
@@ -2872,7 +3146,12 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) ->
end
end,
CTPath = code:lib_dir(common_test),
- {ok,CtLogdir} = get_log_dir(true),
+
+ {ok,CtLogdir} =
+ if Logdir == undefined -> get_log_dir(true);
+ true -> {ok,Logdir}
+ end,
+
AllRuns = make_relative(filename:join(filename:dirname(CtLogdir),
?all_runs_name), Cwd),
TestIndex = make_relative(filename:join(filename:dirname(CtLogdir),
@@ -2886,15 +3165,15 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) ->
"Copyright &copy; ", 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",
@@ -2911,7 +3190,7 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) ->
"Copyright &copy; ", 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,
@@ -2938,9 +3217,11 @@ get_ts_html_wrapper(TestName, 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"],
@@ -3066,10 +3347,11 @@ html_encoding(latin1) ->
html_encoding(utf8) ->
"utf-8".
-unexpected_io(Pid,ct_internal,List,#logger_state{ct_log_fd=Fd}=State) ->
- IoFun = create_io_fun(Pid,State),
- io:format(Fd, "~ts", [lists:foldl(IoFun, [], List)]);
-unexpected_io(Pid,_Category,List,State) ->
- IoFun = create_io_fun(Pid,State),
- Data = io_lib:format("~ts", [lists:foldl(IoFun, [], List)]),
- test_server_io:print_unexpected(Data).
+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 d4bd81e78d..e7a9cfa843 100644
--- a/lib/common_test/src/ct_make.erl
+++ b/lib/common_test/src/ct_make.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2009-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2009-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -323,10 +324,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),
diff --git a/lib/common_test/src/ct_master.erl b/lib/common_test/src/ct_master.erl
index b42ff73846..4eef27d2a5 100644
--- a/lib/common_test/src/ct_master.erl
+++ b/lib/common_test/src/ct_master.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -25,7 +26,8 @@
-export([run/1,run/3,run/4]).
-export([run_on_node/2,run_on_node/3]).
-export([run_test/1,run_test/2]).
--export([basic_html/1]).
+-export([get_event_mgr_ref/0]).
+-export([basic_html/1,esc_chars/1]).
-export([abort/0,abort/1,progress/0]).
@@ -292,6 +294,18 @@ progress() ->
call(progress).
%%%-----------------------------------------------------------------
+%%% @spec get_event_mgr_ref() -> MasterEvMgrRef
+%%% MasterEvMgrRef = atom()
+%%%
+%%% @doc <p>Call this function in order to get a reference to the
+%%% CT master event manager. The reference can be used to e.g.
+%%% add a user specific event handler while tests are running.
+%%% Example:
+%%% <c>gen_event:add_handler(ct_master:get_event_mgr_ref(), my_ev_h, [])</c></p>
+get_event_mgr_ref() ->
+ ?CT_MEVMGR_REF.
+
+%%%-----------------------------------------------------------------
%%% @spec basic_html(Bool) -> ok
%%% Bool = true | false
%%%
@@ -303,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) ->
@@ -352,7 +376,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}) ->
@@ -374,6 +398,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),
@@ -634,7 +666,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])
@@ -677,7 +709,7 @@ 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]),
@@ -697,6 +729,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
%%%-----------------------------------------------------------------
@@ -754,7 +794,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),
diff --git a/lib/common_test/src/ct_master_event.erl b/lib/common_test/src/ct_master_event.erl
index fd97ab16f7..d28ef42c20 100644
--- a/lib/common_test/src/ct_master_event.erl
+++ b/lib/common_test/src/ct_master_event.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -168,7 +169,7 @@ handle_info(_Info,State) ->
{ok,State}.
%%--------------------------------------------------------------------
-%% Function: terminate(Reason, State) -> void()
+%% Function: terminate(Reason, State) -> ok
%% Description:Whenever an event handler is deleted from an event manager,
%% this function is called. It should be the opposite of Module:init/1 and
%% do any necessary cleaning up.
diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl
index 5393097f57..52003f752d 100644
--- a/lib/common_test/src/ct_master_logs.erl
+++ b/lib/common_test/src/ct_master_logs.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -37,6 +38,8 @@
-define(details_file_name,"details.info").
-define(table_color,"lightblue").
+-define(now, os:timestamp()).
+
%%%--------------------------------------------------------------------
%%% API
%%%--------------------------------------------------------------------
@@ -54,7 +57,7 @@ start(LogDir,Nodes) ->
end.
log(Heading,Format,Args) ->
- cast({log,self(),[{int_header(),[log_timestamp(now()),Heading]},
+ cast({log,self(),[{int_header(),[log_timestamp(?now),Heading]},
{Format,Args},
{int_footer(),[]}]}),
ok.
@@ -88,8 +91,8 @@ init(Parent,LogDir,Nodes) ->
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 ->
@@ -125,14 +128,14 @@ 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) ->
atom_to_list(N) ++ " "
end,Nodes)),
- io:format(CtLogFd,int_header(),[log_timestamp(now()),"Test Nodes\n"]),
+ io:format(CtLogFd,int_header(),[log_timestamp(?now),"Test Nodes\n"]),
io:format(CtLogFd,"~ts\n",[NodeStr]),
io:put_chars(CtLogFd,[int_footer(),"\n"]),
@@ -178,7 +181,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} ->
@@ -186,12 +189,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),
+ [log_timestamp(?now),"Finished!"]),
+ _ = close_ct_master_log(State#state.log_fd),
+ _ = close_nodedir_index(State#state.nodedir_ix_fd),
ok
end.
@@ -235,9 +238,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 %%%
@@ -384,11 +387,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"]),
@@ -416,7 +420,7 @@ footer() ->
"Copyright &copy; ", 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"
@@ -492,7 +496,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) ->
@@ -530,13 +534,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 f9f511ecca..7d3e54e645 100644
--- a/lib/common_test/src/ct_master_status.erl
+++ b/lib/common_test/src/ct_master_status.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -100,8 +101,8 @@ handle_info(_Info, State) ->
{ok, State}.
%%--------------------------------------------------------------------
-%% Function: terminate(Reason, State) -> void()
-%% Description:Whenever an event handler is deleted from an event manager,
+%% Function: terminate(Reason, State) -> ok
+%% Description: Whenever an event handler is deleted from an event manager,
%% this function is called. It should be the opposite of Module:init/1 and
%% do any necessary cleaning up.
%%--------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl
index e094ee877a..ff45407fe0 100644
--- a/lib/common_test/src/ct_netconfc.erl
+++ b/lib/common_test/src/ct_netconfc.erl
@@ -1,18 +1,19 @@
%%----------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2012-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2012-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -172,6 +173,7 @@
only_open/2,
hello/1,
hello/2,
+ hello/3,
close_session/1,
close_session/2,
kill_session/2,
@@ -190,6 +192,7 @@
get_config/4,
edit_config/3,
edit_config/4,
+ edit_config/5,
delete_config/2,
delete_config/3,
copy_config/3,
@@ -212,11 +215,7 @@
%%----------------------------------------------------------------------
%% Exported types
%%----------------------------------------------------------------------
--export_type([hook_options/0,
- conn_mod/0,
- log_type/0,
- key_or_name/0,
- notification/0]).
+-export_type([notification/0]).
%%----------------------------------------------------------------------
%% Internal exports
@@ -235,7 +234,6 @@
%% Internal defines
%%----------------------------------------------------------------------
-define(APPLICATION,?MODULE).
--define(VALID_SSH_OPTS,[user, password, user_dir]).
-define(DEFAULT_STREAM,"NETCONF").
-define(error(ConnName,Report),
@@ -247,7 +245,11 @@
-define(is_timeout(T), (is_integer(T) orelse T==infinity)).
-define(is_filter(F),
- (is_atom(F) orelse (is_tuple(F) andalso is_atom(element(1,F))))).
+ (?is_simple_xml(F)
+ orelse (F==[])
+ orelse (is_list(F) andalso ?is_simple_xml(hd(F))))).
+-define(is_simple_xml(Xml),
+ (is_atom(Xml) orelse (is_tuple(Xml) andalso is_atom(element(1,Xml))))).
-define(is_string(S), (is_list(S) andalso is_integer(hd(S)))).
%%----------------------------------------------------------------------
@@ -261,6 +263,7 @@
session_id,
msg_id = 1,
hello_status,
+ no_end_tag_buff = <<>>,
buff = <<>>,
pending = [], % [#pending]
event_receiver}).% pid
@@ -288,19 +291,11 @@
%%----------------------------------------------------------------------
%% Type declarations
%%----------------------------------------------------------------------
--type client() :: handle() | server_id() | target_name().
+-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 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().
-
-type options() :: [option()].
%% Options used for setting up ssh connection to a netconf server.
@@ -322,14 +317,7 @@
%% See XML Schema for Event Notifications found in RFC5277 for further
%% detail about the data format for the string values.
--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,[key_or_name()]}.
--type log_type() :: raw | pretty | html | silent.
%-type error_handler() :: module().
--type conn_mod() :: ct_netconfc.
-
-type error_reason() :: term().
-type simple_xml() :: {xml_tag(), xml_attributes(), xml_content()} |
@@ -380,7 +368,7 @@ open(Options) ->
%%----------------------------------------------------------------------
-spec open(KeyOrName, ExtraOptions) -> Result when
- KeyOrName :: key_or_name(),
+ KeyOrName :: ct_gen_conn:key_or_name(),
ExtraOptions :: options(),
Result :: {ok,handle()} | {error,error_reason()}.
%% @doc Open a named netconf session and exchange `hello' messages.
@@ -457,7 +445,7 @@ only_open(Options) ->
%%----------------------------------------------------------------------
-spec only_open(KeyOrName,ExtraOptions) -> Result when
- KeyOrName :: key_or_name(),
+ KeyOrName :: ct_gen_conn:key_or_name(),
ExtraOptions :: options(),
Result :: {ok,handle()} | {error,error_reason()}.
%% @doc Open a name netconf session, but don't send `hello'.
@@ -471,23 +459,35 @@ only_open(KeyOrName, ExtraOpts) ->
%%----------------------------------------------------------------------
%% @spec hello(Client) -> Result
-%% @equiv hello(Client, infinity)
+%% @equiv hello(Client, [], infinity)
hello(Client) ->
- hello(Client,?DEFAULT_TIMEOUT).
+ 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.
%%
-%% Sends a `hello' message to the server and waits for the return.
-%%
+%% Adds optional capabilities and sends a `hello' message to the
+%% server and waits for the return.
%% @end
%%----------------------------------------------------------------------
-hello(Client,Timeout) ->
- call(Client, {hello, Timeout}).
+hello(Client,Options,Timeout) ->
+ call(Client, {hello, Options, Timeout}).
+
%%----------------------------------------------------------------------
%% @spec get_session_id(Client) -> Result
@@ -540,22 +540,51 @@ get_capabilities(Client) ->
get_capabilities(Client, Timeout) ->
call(Client, get_capabilities, Timeout).
-%% @private
+%%----------------------------------------------------------------------
+%% @spec send(Client, SimpleXml) -> Result
+%% @equiv send(Client, SimpleXml, infinity)
send(Client, SimpleXml) ->
send(Client, SimpleXml, ?DEFAULT_TIMEOUT).
-%% @private
+
+%%----------------------------------------------------------------------
+-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}).
-%% @private
+%%----------------------------------------------------------------------
+%% @spec send_rpc(Client, SimpleXml) -> Result
+%% @equiv send_rpc(Client, SimpleXml, infinity)
send_rpc(Client, SimpleXml) ->
send_rpc(Client, SimpleXml, ?DEFAULT_TIMEOUT).
-%% @private
+
+%%----------------------------------------------------------------------
+-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)
@@ -621,7 +650,7 @@ get(Client, Filter) ->
Client :: client(),
Filter :: simple_xml() | xpath(),
Timeout :: timeout(),
- Result :: {ok,simple_xml()} | {error,error_reason()}.
+ Result :: {ok,[simple_xml()]} | {error,error_reason()}.
%% @doc Get data.
%%
%% This operation returns both configuration and state data from the
@@ -647,7 +676,7 @@ get_config(Client, Source, Filter) ->
Source :: netconf_db(),
Filter :: simple_xml() | xpath(),
Timeout :: timeout(),
- Result :: {ok,simple_xml()} | {error,error_reason()}.
+ Result :: {ok,[simple_xml()]} | {error,error_reason()}.
%% @doc Get configuration data.
%%
%% To be able to access another source than `running', the server
@@ -664,15 +693,39 @@ get_config(Client, Source, Filter, Timeout) ->
%%----------------------------------------------------------------------
%% @spec edit_config(Client, Target, Config) -> Result
-%% @equiv edit_config(Client, Target, Config, infinity)
+%% @equiv edit_config(Client, Target, Config, [], infinity)
edit_config(Client, Target, Config) ->
edit_config(Client, Target, Config, ?DEFAULT_TIMEOUT).
%%----------------------------------------------------------------------
--spec edit_config(Client, Target, Config, Timeout) -> Result when
+-spec edit_config(Client, Target, Config, OptParamsOrTimeout) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Config :: simple_xml(),
+ OptParamsOrTimeout :: [simple_xml()] | 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(),
Config :: simple_xml(),
+ OptParams :: [simple_xml()],
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
%% @doc Edit configuration data.
@@ -681,10 +734,20 @@ edit_config(Client, Target, Config) ->
%% 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, Timeout) ->
- call(Client, {send_rpc_op, edit_config, [Target,Config], Timeout}).
+edit_config(Client, Target, Config, OptParams, Timeout) ->
+ call(Client, {send_rpc_op, edit_config, [Target,Config,OptParams], Timeout}).
%%----------------------------------------------------------------------
@@ -745,8 +808,9 @@ action(Client,Action) ->
Client :: client(),
Action :: simple_xml(),
Timeout :: timeout(),
- Result :: {ok,simple_xml()} | {error,error_reason()}.
-%% @doc Execute an action.
+ 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
%%----------------------------------------------------------------------
@@ -761,7 +825,7 @@ create_subscription(Client,Timeout)
when ?is_timeout(Timeout) ->
create_subscription(Client,?DEFAULT_STREAM,Timeout);
create_subscription(Client,Stream)
- when is_list(Stream) ->
+ when ?is_string(Stream) ->
create_subscription(Client,Stream,?DEFAULT_TIMEOUT);
create_subscription(Client,Filter)
when ?is_filter(Filter) ->
@@ -769,14 +833,14 @@ create_subscription(Client,Filter)
?DEFAULT_TIMEOUT).
create_subscription(Client,Stream,Timeout)
- when is_list(Stream) andalso
+ when ?is_string(Stream) andalso
?is_timeout(Timeout) ->
call(Client,{send_rpc_op,{create_subscription,self()},
[Stream,undefined,undefined,undefined],
Timeout});
create_subscription(Client,StartTime,StopTime)
- when is_list(StartTime) andalso
- is_list(StopTime) ->
+ when ?is_string(StartTime) andalso
+ ?is_string(StopTime) ->
create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime,
?DEFAULT_TIMEOUT);
create_subscription(Client,Filter,Timeout)
@@ -784,28 +848,28 @@ create_subscription(Client,Filter,Timeout)
?is_timeout(Timeout) ->
create_subscription(Client,?DEFAULT_STREAM,Filter,Timeout);
create_subscription(Client,Stream,Filter)
- when is_list(Stream) andalso
+ when ?is_string(Stream) andalso
?is_filter(Filter) ->
create_subscription(Client,Stream,Filter,?DEFAULT_TIMEOUT).
create_subscription(Client,StartTime,StopTime,Timeout)
- when is_list(StartTime) andalso
- is_list(StopTime) andalso
+ when ?is_string(StartTime) andalso
+ ?is_string(StopTime) andalso
?is_timeout(Timeout) ->
create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime,Timeout);
create_subscription(Client,Stream,StartTime,StopTime)
- when is_list(Stream) andalso
- is_list(StartTime) andalso
- is_list(StopTime) ->
+ when ?is_string(Stream) andalso
+ ?is_string(StartTime) andalso
+ ?is_string(StopTime) ->
create_subscription(Client,Stream,StartTime,StopTime,?DEFAULT_TIMEOUT);
create_subscription(Client,Filter,StartTime,StopTime)
when ?is_filter(Filter) andalso
- is_list(StartTime) andalso
- is_list(StopTime) ->
+ ?is_string(StartTime) andalso
+ ?is_string(StopTime) ->
create_subscription(Client,?DEFAULT_STREAM,Filter,
StartTime,StopTime,?DEFAULT_TIMEOUT);
create_subscription(Client,Stream,Filter,Timeout)
- when is_list(Stream) andalso
+ when ?is_string(Stream) andalso
?is_filter(Filter) andalso
?is_timeout(Timeout) ->
call(Client,{send_rpc_op,{create_subscription,self()},
@@ -813,18 +877,18 @@ create_subscription(Client,Stream,Filter,Timeout)
Timeout}).
create_subscription(Client,Stream,StartTime,StopTime,Timeout)
- when is_list(Stream) andalso
- is_list(StartTime) andalso
- is_list(StopTime) andalso
+ when ?is_string(Stream) andalso
+ ?is_string(StartTime) andalso
+ ?is_string(StopTime) andalso
?is_timeout(Timeout) ->
call(Client,{send_rpc_op,{create_subscription,self()},
[Stream,undefined,StartTime,StopTime],
Timeout});
create_subscription(Client,Stream,Filter,StartTime,StopTime)
- when is_list(Stream) andalso
+ when ?is_string(Stream) andalso
?is_filter(Filter) andalso
- is_list(StartTime) andalso
- is_list(StopTime) ->
+ ?is_string(StartTime) andalso
+ ?is_string(StopTime) ->
create_subscription(Client,Stream,Filter,StartTime,StopTime,?DEFAULT_TIMEOUT).
%%----------------------------------------------------------------------
@@ -832,7 +896,7 @@ create_subscription(Client,Stream,Filter,StartTime,StopTime)
Result when
Client :: client(),
Stream :: stream_name(),
- Filter :: simple_xml(),
+ Filter :: simple_xml() | [simple_xml()],
StartTime :: xs_datetime(),
StopTime :: xs_datetime(),
Timeout :: timeout(),
@@ -855,8 +919,7 @@ create_subscription(Client,Stream,Filter,StartTime,StopTime)
%% 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. See section 3.6 for more information on
-%% filters.</dd>
+%% parameters will be sent.</dd>
%%
%% <dt>StartTime:</dt>
%% <dd>An optional parameter used to trigger the replay feature and
@@ -1026,9 +1089,9 @@ terminate(_, #state{connection=Connection}) ->
ok.
%% @private
-handle_msg({hello,Timeout}, From,
+handle_msg({hello, Options, Timeout}, From,
#state{connection=Connection,hello_status=HelloStatus} = State) ->
- case do_send(Connection, client_hello()) of
+ case do_send(Connection, client_hello(Options)) of
ok ->
case HelloStatus of
undefined ->
@@ -1073,6 +1136,7 @@ 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);
@@ -1097,10 +1161,16 @@ handle_msg({Ref,timeout},
ct_gen_conn:return(Caller,{error,{hello_session_failed,timeout}}),
{stop,State#state{hello_status={error,timeout}}};
handle_msg({Ref,timeout},#state{pending=Pending} = State) ->
- {value,#pending{caller=Caller},Pending1} =
+ {value,#pending{op=Op,caller=Caller},Pending1} =
lists:keytake(Ref,#pending.ref,Pending),
ct_gen_conn:return(Caller,{error,timeout}),
- {noreply,State#state{pending=Pending1}}.
+ R = case Op of
+ close_session -> stop;
+ _ -> noreply
+ end,
+ %% Halfhearted try to get in correct state, this matches
+ %% the implementation before this patch
+ {R,State#state{pending=Pending1, no_end_tag_buff= <<>>, buff= <<>>}}.
%% @private
%% Called by ct_util_server to close registered connections before terminate.
@@ -1135,7 +1205,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}} ->
@@ -1186,13 +1256,11 @@ check_options([{port,Port}|T], Host, _, #options{} = Options) ->
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]}).
%%%-----------------------------------------------------------------
set_request_timer(infinity) ->
@@ -1202,12 +1270,24 @@ 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() ->
+client_hello(Options) when is_list(Options) ->
+ UserCaps = [{capability, UserCap} ||
+ {capability, UserCap} <- Options,
+ is_list(hd(UserCap))],
{hello, ?NETCONF_NAMESPACE_ATTR,
[{capabilities,
- [{capability,[?NETCONF_BASE_CAP++?NETCONF_BASE_CAP_VSN]}]}]}.
+ [{capability,[?NETCONF_BASE_CAP++?NETCONF_BASE_CAP_VSN]}|
+ UserCaps]}]}.
%%%-----------------------------------------------------------------
@@ -1217,8 +1297,8 @@ encode_rpc_operation(get,[Filter]) ->
{get,filter(Filter)};
encode_rpc_operation(get_config,[Source,Filter]) ->
{'get-config',[{source,[Source]}] ++ filter(Filter)};
-encode_rpc_operation(edit_config,[Target,Config]) ->
- {'edit-config',[{target,[Target]},{config,[Config]}]};
+encode_rpc_operation(edit_config,[Target,Config,OptParams]) ->
+ {'edit-config',[{target,[Target]}] ++ OptParams ++ [{config,[Config]}]};
encode_rpc_operation(delete_config,[Target]) ->
{'delete-config',[{target,[Target]}]};
encode_rpc_operation(copy_config,[Target,Source]) ->
@@ -1241,8 +1321,10 @@ filter(undefined) ->
[];
filter({xpath,Filter}) when ?is_string(Filter) ->
[{filter,[{type,"xpath"},{select, Filter}],[]}];
+filter(Filter) when is_list(Filter) ->
+ [{filter,[{type,"subtree"}],Filter}];
filter(Filter) ->
- [{filter,[{type,"subtree"}],[Filter]}].
+ filter([Filter]).
maybe_element(_,undefined) ->
[];
@@ -1288,72 +1370,75 @@ to_xml_doc(Simple) ->
%%%-----------------------------------------------------------------
%%% Parse and handle received XML data
-handle_data(NewData,#state{connection=Connection,buff=Buff} = State) ->
+%%% 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 = <<Buff/binary,NewData/binary>>,
- case xmerl_sax_parser:stream(<<>>,
- [{continuation_fun,fun sax_cont/1},
- {continuation_state,{Data,Connection,false}},
- {event_fun,fun sax_event/3},
- {event_state,[]}]) of
- {ok, Simple, Rest} ->
- decode(Simple,State#state{buff=Rest});
- {fatal_error,_Loc,Reason,_EndTags,_EventState} ->
- ?error(Connection#connection.name,[{parse_error,Reason},
- {buffer,Buff},
- {new_data,NewData}]),
- case Reason of
- {could_not_fetch_data,Msg} ->
- handle_msg(Msg,State#state{buff = <<>>});
- _Other ->
- Pending1 =
- case State#state.pending of
- [] ->
- [];
- Pending ->
- %% Assuming the first request gets the
- %% first answer
- P=#pending{tref=TRef,caller=Caller} =
- lists:last(Pending),
- timer:cancel(TRef),
- Reason1 = {failed_to_parse_received_data,Reason},
- ct_gen_conn:return(Caller,{error,Reason1}),
- lists:delete(P,Pending)
- end,
- {noreply,State#state{pending=Pending1,buff = <<>>}}
+ NoEndTag0 = State0#state.no_end_tag_buff,
+ Buff0 = State0#state.buff,
+ Data = <<Buff0/binary, NewData/binary>>,
+ case binary:split(Data,?END_TAG,[]) of
+ [_NoEndTagFound] ->
+ 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{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);
+ Other ->
+ Other
+ end;
+ {fatal_error,_Loc,Reason,_EndTags,_EventState} ->
+ ?error(Connection#connection.name,
+ [{parse_error,Reason},
+ {buffer, Buff0},
+ {new_data,NewData}]),
+ handle_error(Reason, State0#state{no_end_tag_buff= <<>>,
+ buff= <<>>})
end
end.
-%%%-----------------------------------------------------------------
-%%% Parsing of XML data
-%% Contiuation function for the sax parser
-sax_cont(done) ->
- {<<>>,done};
-sax_cont({Data,Connection,false}) ->
- case binary:split(Data,[?END_TAG],[]) of
- [All] ->
- %% No end tag found. Remove what could be a part
- %% of an end tag from the data and save for next
- %% iteration
- SafeSize = size(All)-5,
- <<New:SafeSize/binary,Save:5/binary>> = All,
- {New,{Save,Connection,true}};
- [_Msg,_Rest]=Msgs ->
- %% We have at least one full message. Any excess data will
- %% be returned from xmerl_sax_parser:stream/2 in the Rest
- %% parameter.
- {list_to_binary(Msgs),done}
- end;
-sax_cont({Data,Connection,true}) ->
- case ssh_receive_data() of
- {ok,Bin} ->
- log(Connection,recv,Bin),
- sax_cont({<<Data/binary,Bin/binary>>,Connection,false});
- {error,Reason} ->
- throw({could_not_fetch_data,Reason})
- end.
-
+%% xml does not accept a leading nl and some netconf server add a nl after
+%% each ?END_TAG, ignore them
+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
+ [] -> [];
+ Pending ->
+ %% Assuming the first request gets the
+ %% first answer
+ P=#pending{tref=TRef,ref=Ref,caller=Caller} =
+ lists:last(Pending),
+ cancel_request_timer(Ref,TRef),
+ Reason1 = {failed_to_parse_received_data,Reason},
+ ct_gen_conn:return(Caller,{error,Reason1}),
+ lists:delete(P,Pending)
+ end,
+ {noreply, State#state{pending=Pending1}}.
%% Event function for the sax parser. It builds a simple XML structure.
%% Care is taken to keep namespace attributes and prefixes as in the original XML.
@@ -1434,8 +1519,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),
@@ -1461,9 +1546,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=[]}};
_ ->
@@ -1484,8 +1568,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}),
@@ -1497,10 +1581,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=[]}};
_ ->
@@ -1551,6 +1636,9 @@ decode_ok(Other) ->
decode_data([{Tag,Attrs,Content}]) ->
case get_local_name_atom(Tag) of
+ ok ->
+ %% when action has return type void
+ ok;
data ->
%% Since content of data has nothing from the netconf
%% namespace, we remove the parent's xmlns attribute here
@@ -1688,6 +1776,7 @@ 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
@@ -1700,9 +1789,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
@@ -1741,13 +1835,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) ->
@@ -1813,16 +1915,6 @@ get_tag([]) ->
%%%-----------------------------------------------------------------
%%% SSH stuff
-ssh_receive_data() ->
- receive
- {ssh_cm, CM, {data, Ch, _Type, Data}} ->
- ssh_connection:adjust_window(CM,Ch,size(Data)),
- {ok, Data};
- {ssh_cm, _CM, {Closed, _Ch}} = X when Closed == closed; Closed == eof ->
- {error,X};
- {_Ref,timeout} = X ->
- {error,X}
- end.
ssh_open(#options{host=Host,timeout=Timeout,port=Port,ssh=SshOpts,name=Name}) ->
case ssh:connect(Host, Port,
@@ -1839,14 +1931,13 @@ ssh_open(#options{host=Host,timeout=Timeout,port=Port,ssh=SshOpts,name=Name}) ->
name = Name}};
failure ->
ssh:close(CM),
- {error,{ssh,could_not_execute_netconf_subsystem}}
+ {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}};
- Other ->
- %% Bug in ssh?? got {closed,0} here once...
- {error,{ssh,unexpected_from_session_channel,Other}}
+ {error,{ssh,could_not_open_channel,Reason}}
end;
{error,Reason} ->
{error,{ssh,could_not_connect_to_server,Reason}}
diff --git a/lib/common_test/src/ct_netconfc.hrl b/lib/common_test/src/ct_netconfc.hrl
index 295a61a98b..5eeeafa318 100644
--- a/lib/common_test/src/ct_netconfc.hrl
+++ b/lib/common_test/src/ct_netconfc.hrl
@@ -1,18 +1,19 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2012. All Rights Reserved.
-%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
diff --git a/lib/common_test/src/ct_property_test.erl b/lib/common_test/src/ct_property_test.erl
new file mode 100644
index 0000000000..12c3d726d3
--- /dev/null
+++ b/lib/common_test/src/ct_property_test.erl
@@ -0,0 +1,189 @@
+%%
+%% %CopyrightBegin%
+%%
+%% 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.
+%% 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%
+%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% WARNING %%%
+%%% %%%
+%%% This is experimental code which may be changed or removed %%%
+%%% anytime without any warning. %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+%%% @doc EXPERIMENTAL support in common-test for calling property based tests.
+%%%
+%%% <p>This module is a first step towards running Property Based testing in the
+%%% Common Test framework. A property testing tool like QuickCheck or PropEr is
+%%% assumed to be installed.</p>
+%%%
+%%% <p>The idea is to have a common_test testsuite calling a property testing
+%%% tool with special property test suites as defined by that tool. In this manual
+%%% we assume the usual Erlang Application directory structure. The tests are
+%%% collected in the application's <c>test</c> directory. The test directory
+%%% has a sub-directory called <c>property_test</c> where everything needed for
+%%% the property tests are collected.</p>
+%%%
+%%% <p>A typical ct test suite using <c>ct_property_test</c> is organized as follows:
+%%% </p>
+%%% ```
+%%% -include_lib("common_test/include/ct.hrl").
+%%%
+%%% all() -> [prop_ftp_case].
+%%%
+%%% init_per_suite(Config) ->
+%%% ct_property_test:init_per_suite(Config).
+%%%
+%%% %%%---- test case
+%%% prop_ftp_case(Config) ->
+%%% ct_property_test:quickcheck(
+%%% ftp_simple_client_server:prop_ftp(Config),
+%%% Config
+%%% ).
+%%% '''
+%%%
+%%% <warning>
+%%% <p>
+%%% This is experimental code which may be changed or removed
+%%% anytime without any warning.
+%%% </p>
+%%% </warning>
+%%%
+%%% @end
+
+-module(ct_property_test).
+
+%% API
+-export([init_per_suite/1,
+ quickcheck/2]).
+
+-include_lib("common_test/include/ct.hrl").
+
+%%%-----------------------------------------------------------------
+%%% @spec init_per_suite(Config) -> Config | {skip,Reason}
+%%%
+%%% @doc Initializes Config for property testing.
+%%%
+%%% <p>The function investigates if support is available for either Quickcheck, PropEr,
+%%% or Triq.
+%%% The options <c>{property_dir,AbsPath}</c> and
+%%% <c>{property_test_tool,Tool}</c> is set in the Config returned.</p>
+%%% <p>The function is intended to be called in the init_per_suite in the test suite.</p>
+%%% <p>The property tests are assumed to be in the subdirectory <c>property_test</c>.</p>
+%%% @end
+
+init_per_suite(Config) ->
+ case which_module_exists([eqc,proper,triq]) of
+ {ok,ToolModule} ->
+ ct:pal("Found property tester ~p",[ToolModule]),
+ Path = property_tests_path("property_test", Config),
+ case compile_tests(Path,ToolModule) of
+ error ->
+ {fail, "Property test compilation failed in "++Path};
+ up_to_date ->
+ add_code_pathz(Path),
+ [{property_dir,Path},
+ {property_test_tool,ToolModule} | Config]
+ end;
+
+ not_found ->
+ ct:pal("No property tester found",[]),
+ {skip, "No property testing tool found"}
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec quickcheck(Property, Config) -> true | {fail,Reason}
+%%%
+%%% @doc Call quickcheck and return the result in a form suitable for common_test.
+%%%
+%%% <p>The function is intended to be called in the test cases in the test suite.</p>
+%%% @end
+
+quickcheck(Property, Config) ->
+ Tool = proplists:get_value(property_test_tool,Config),
+ F = function_name(quickcheck, Tool),
+ mk_ct_return( Tool:F(Property), Tool ).
+
+
+%%%================================================================
+%%%
+%%% Local functions
+%%%
+
+%%% Make return values back to the calling Common Test suite
+mk_ct_return(true, _Tool) ->
+ true;
+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)])}
+ catch
+ _:_ ->
+ {fail, Other}
+ end.
+
+%%% Check if a property testing tool is found
+which_module_exists([Module|Modules]) ->
+ case module_exists(Module) of
+ true -> {ok,Module};
+ false -> which_module_exists(Modules)
+ end;
+which_module_exists(_) ->
+ not_found.
+
+module_exists(Module) ->
+ is_list(catch Module:module_info()).
+
+%%% The path to the property tests
+property_tests_path(Dir, Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ filename:join(lists:droplast(filename:split(DataDir))++[Dir]).
+
+%%% Extend the code path with Dir if it not already present
+add_code_pathz(Dir) ->
+ case lists:member(Dir, code:get_path()) of
+ true -> ok;
+ false ->
+ true = code:add_pathz(Dir),
+ ok
+ end.
+
+compile_tests(Path, ToolModule) ->
+ MacroDefs = macro_def(ToolModule),
+ {ok,Cwd} = file:get_cwd(),
+ ok = file:set_cwd(Path),
+ {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]),
+ Result = make:all([load|MacroDefs]),
+ ok = file:set_cwd(Cwd),
+ Result.
+
+
+macro_def(eqc) -> [{d, 'EQC'}];
+macro_def(proper) -> [{d, 'PROPER'}];
+macro_def(triq) -> [{d, 'TRIQ'}].
+
+function_name(quickcheck, triq) -> check;
+function_name(F, _) -> F.
+
diff --git a/lib/common_test/src/ct_release_test.erl b/lib/common_test/src/ct_release_test.erl
new file mode 100644
index 0000000000..d783f8d04e
--- /dev/null
+++ b/lib/common_test/src/ct_release_test.erl
@@ -0,0 +1,962 @@
+%%
+%% %CopyrightBegin%
+%%
+%% 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.
+%% 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%
+%%
+%%-----------------------------------------------------------------
+%% @doc EXPERIMENTAL support for testing of upgrade.
+%%
+%% This is a library module containing support for test of release
+%% related activities in one or more applications. Currenty it
+%% supports upgrade only.
+%%
+%% == Configuration ==
+%%
+%% In order to find version numbers of applications to upgrade from,
+%% `{@module}' needs to access and start old OTP
+%% releases. A `common_test' configuration file can be used for
+%% specifying the location of such releases, for example:
+%%
+%% ```
+%% %% old-rels.cfg
+%% {otp_releases,[{r16b,"/path/to/R16B03-1/bin/erl"},
+%% {'17',"/path/to/17.3/bin/erl"}]}.'''
+%%
+%% The configuration file should preferably point out the latest patch
+%% level on each major release.
+%%
+%% If no such configuration file is given, {@link init/1} will return
+%% `{skip,Reason}' and any attempt at running {@link upgrade/4}
+%% will fail.
+%%
+%% == Callback functions ==
+%%
+%% The following functions should be exported from a {@module}
+%% callback module.
+%%
+%% All callback functions are called on the node where the upgrade is
+%% executed.
+%%
+%% <dl>
+%% <dt>Module:upgrade_init(CtData,State) -> NewState</dt>
+%% <dd>Types:
+%%
+%% <b><code>CtData = {@link ct_data()}</code></b><br/>
+%% <b><code>State = NewState = cb_state()</code></b>
+%%
+%% Initialyze system before upgrade test starts.
+%%
+%% This function is called before the upgrade is started. All
+%% applications given in {@link upgrade/4} are already started by
+%% the boot script, so this callback is intended for additional
+%% initialization, if necessary.
+%%
+%% <code>CtData</code> is an opaque data structure which shall be used
+%% in any call to <code>ct_release_test</code> inside the callback.
+%%
+%% Example:
+%%
+%% ```
+%% upgrade_init(CtData,State) ->
+%% {ok,{FromVsn,ToVsn}} = ct_release_test:get_app_vsns(CtData,myapp),
+%% open_connection(State).'''
+%% </dd>
+%%
+%% <dt>Module:upgrade_upgraded(CtData,State) -> NewState</dt>
+%% <dd>Types:
+%%
+%% <b><code>CtData = {@link ct_data()}</code></b><br/>
+%% <b><code>State = NewState = cb_state()</code></b>
+%%
+%% Check that upgrade was successful.
+%%
+%% This function is called after the release_handler has
+%% successfully unpacked and installed the new release, and it has
+%% been made permanent. It allows application specific checks to
+%% ensure that the upgrade was successful.
+%%
+%% <code>CtData</code> is an opaque data structure which shall be used
+%% in any call to <code>ct_release_test</code> inside the callback.
+%%
+%% Example:
+%%
+%% ```
+%% upgrade_upgraded(CtData,State) ->
+%% check_connection_still_open(State).'''
+%% </dd>
+%%
+%% <dt>Module:upgrade_downgraded(CtData,State) -> NewState</dt>
+%% <dd>Types:
+%%
+%% <b><code>CtData = {@link ct_data()}</code></b><br/>
+%% <b><code>State = NewState = cb_state()</code></b>
+%%
+%% Check that downgrade was successful.
+%%
+%% This function is called after the release_handler has
+%% successfully re-installed the original release, and it has been
+%% made permanent. It allows application specific checks to ensure
+%% that the downgrade was successful.
+%%
+%% <code>CtData</code> is an opaque data structure which shall be used
+%% in any call to <code>ct_release_test</code> inside the callback.
+%%
+%% Example:
+%%
+%% ```
+%% upgrade_downgraded(CtData,State) ->
+%% check_connection_closed(State).'''
+%% </dd>
+%% </dl>
+%% @end
+%%-----------------------------------------------------------------
+-module(ct_release_test).
+
+-export([init/1, upgrade/4, cleanup/1, get_app_vsns/2, get_appup/2]).
+
+-include_lib("kernel/include/file.hrl").
+
+%%-----------------------------------------------------------------
+-define(testnode, 'ct_release_test-upgrade').
+-define(exclude_apps, [hipe, typer, dialyzer]). % never include these apps
+
+%%-----------------------------------------------------------------
+-record(ct_data, {from,to}).
+
+%%-----------------------------------------------------------------
+-type config() :: [{atom(),term()}].
+-type cb_state() :: term().
+-opaque ct_data() :: #ct_data{}.
+-export_type([ct_data/0]).
+
+-callback upgrade_init(ct_data(),cb_state()) -> cb_state().
+-callback upgrade_upgraded(ct_data(),cb_state()) -> cb_state().
+-callback upgrade_downgraded(ct_data(),cb_state()) -> cb_state().
+
+%%-----------------------------------------------------------------
+-spec init(Config) -> Result when
+ Config :: config(),
+ Result :: config() | SkipOrFail,
+ SkipOrFail :: {skip,Reason} | {fail,Reason}.
+%% @doc Initialize `{@module}'.
+%%
+%% This function can be called from any of the
+%% `init_per_*' functions in the test suite. It updates
+%% the given `Config' with data that will be
+%% used by future calls to other functions in this module. The
+%% returned configuration must therefore also be returned from
+%% the calling `init_per_*'.
+%%
+%% If the initialization fails, e.g. if a required release can
+%% not be found, the function returns `{skip,Reason}'. In
+%% this case the other test support functions in this mudule
+%% can not be used.
+%%
+%% Example:
+%%
+%% ```
+%% init_per_suite(Config) ->
+%% ct_release_test:init(Config).'''
+%%
+init(Config) ->
+ try init_upgrade_test() of
+ {Major,Minor} ->
+ [{release_test,[{major,Major},{minor,Minor}]} | Config]
+ catch throw:Thrown ->
+ Thrown
+ end.
+
+%%-----------------------------------------------------------------
+-spec upgrade(App,Level,Callback,Config) -> any() when
+ App :: atom(),
+ Level :: minor | major,
+ Callback :: {module(),InitState},
+ InitState :: cb_state(),
+ Config :: config();
+ (Apps,Level,Callback,Config) -> any() when
+ Apps :: [App],
+ App :: atom(),
+ Level :: minor | major,
+ Callback :: {module(),InitState},
+ InitState :: cb_state(),
+ Config :: config().
+%% @doc Test upgrade of the given application(s).
+%%
+%% This function can be called from a test case. It requires that
+%% `Config' has been initialized by calling {@link
+%% init/1} prior to this, for example from `init_per_suite/1'.
+%%
+%% Upgrade tests are performed as follows:
+%%
+%% <ol>
+%% <li>Figure out which OTP release to test upgrade
+%% from. Start a node running that release and find the
+%% application versions on that node. Terminate the
+%% node.</li>
+%% <li>Figure out all dependencies for the applications under
+%% test.</li>
+%% <li>Create a release containing the core
+%% applications `kernel', `stdlib' and `sasl'
+%% in addition to the application(s) under test and all
+%% dependencies of these. The versions of the applications
+%% under test will be the ones found on the OTP release to
+%% upgrade from. The versions of all other applications will
+%% be those found on the current node, i.e. the common_test
+%% node. This is the "From"-release.</li>
+%% <li>Create another release containing the same
+%% applications as in the previous step, but with all
+%% application versions taken from the current node. This is
+%% the "To"-release.</li>
+%% <li>Install the "From"-release and start a new node
+%% running this release.</li>
+%% <li>Perform the upgrade test and allow customized
+%% control by using callbacks:
+%% <ol>
+%% <li>Callback: `upgrade_init/2'</li>
+%% <li>Unpack the new release</li>
+%% <li>Install the new release</li>
+%% <li>Callback: `upgrade_upgraded/2'</li>
+%% <li>Install the original release</li>
+%% <li>Callback: `upgrade_downgraded/2'</li>
+%% </ol>
+%% </li>
+%% </ol>
+%%
+%% `App' or `Apps'
+%% specifies the applications under test, i.e. the applications
+%% which shall be upgraded. All other applications that are
+%% included have the same releases in the "From"- and
+%% "To"-releases and will therefore not be upgraded.
+%%
+%% `Level' specifies which OTP release to
+%% pick the "From" versions from.
+%% <dl>
+%% <dt>major</dt>
+%% <dd>From verions are picked from the previous major
+%% release. For example, if the test is run on an OTP-17
+%% node, `{@module}' will pick the application
+%% "From" versions from an OTP installation running OTP
+%% R16B.</dd>
+%%
+%% <dt>minor</dt>
+%% <dd>From verions are picked from the current major
+%% release. For example, if the test is run on an OTP-17
+%% node, `{@module}' will pick the application
+%% "From" versions from an OTP installation running an
+%% earlier patch level of OTP-17.</dd>
+%% </dl>
+%%
+%% The application "To" versions are allways picked from the
+%% current node, i.e. the common_test node.
+%%
+%% `Callback' specifies the module (normally the
+%% test suite) which implements the {@section Callback functions}, and
+%% the initial value of the `State' variable used in these
+%% functions.
+%%
+%% `Config' is the input argument received
+%% in the test case function.
+%%
+%% Example:
+%%
+%% ```
+%% minor_upgrade(Config) ->
+%% ct_release_test:upgrade(ssl,minor,{?MODULE,[]},Config).
+%% '''
+%%
+upgrade(App,Level,Callback,Config) when is_atom(App) ->
+ upgrade([App],Level,Callback,Config);
+upgrade(Apps,Level,Callback,Config) ->
+ Dir = proplists:get_value(priv_dir,Config),
+ CreateDir = filename:join([Dir,Level,create]),
+ InstallDir = filename:join([Dir,Level,install]),
+ ok = filelib:ensure_dir(filename:join(CreateDir,"*")),
+ ok = filelib:ensure_dir(filename:join(InstallDir,"*")),
+ try upgrade(Apps,Level,Callback,CreateDir,InstallDir,Config) of
+ ok ->
+ %%rm_rf(CreateDir),
+ Tars = filelib:wildcard(filename:join(CreateDir,"*.tar.gz")),
+ _ = [file:delete(Tar) || Tar <- Tars],
+ rm_rf(InstallDir),
+ ok
+ catch throw:{fail,Reason} ->
+ ct:fail(Reason);
+ throw:{skip,Reason} ->
+ rm_rf(CreateDir),
+ rm_rf(InstallDir),
+ {skip,Reason}
+ after
+ %% Brutally kill all nodes that erroneously survived the test.
+ %% 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 = 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.
+
+%%-----------------------------------------------------------------
+-spec cleanup(Config) -> Result when
+ Config :: config(),
+ Result :: config().
+%% @doc Clean up after tests.
+%%
+%% This function shall be called from the `end_per_*' function
+%% complementing the `init_per_*' function where {@link init/1}
+%% is called.
+%%
+%% It cleans up after the test, for example kills hanging
+%% nodes.
+%%
+%% Example:
+%%
+%% ```
+%% end_per_suite(Config) ->
+%% ct_release_test:cleanup(Config).'''
+%%
+cleanup(Config) ->
+ 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.
+
+%%-----------------------------------------------------------------
+-spec get_app_vsns(CtData,App) -> {ok,{From,To}} | {error,Reason} when
+ CtData :: ct_data(),
+ App :: atom(),
+ From :: string(),
+ To :: string(),
+ Reason :: {app_not_found,App}.
+%% @doc Get versions involved in this upgrade for the given application.
+%%
+%% This function can be called from inside any of the callback
+%% functions. It returns the old (From) and new (To) versions involved
+%% in the upgrade/downgrade test for the given application.
+%%
+%% <code>CtData</code> must be the first argument received in the
+%% calling callback function - an opaque data structure set by
+%% <code>ct_release_tests</code>.
+get_app_vsns(#ct_data{from=FromApps,to=ToApps},App) ->
+ case {lists:keyfind(App,1,FromApps),lists:keyfind(App,1,ToApps)} of
+ {{App,FromVsn,_},{App,ToVsn,_}} ->
+ {ok,{FromVsn,ToVsn}};
+ _ ->
+ {error,{app_not_found,App}}
+ end.
+
+%%-----------------------------------------------------------------
+-spec get_appup(CtData,App) -> {ok,Appup} | {error,Reason} when
+ CtData :: ct_data(),
+ App :: atom(),
+ Appup :: {From,To,Up,Down},
+ From :: string(),
+ To :: string(),
+ Up :: [Instr],
+ Down :: [Instr],
+ Instr :: term(),
+ Reason :: {app_not_found,App} | {vsn_not_found,{App,From}}.
+%% @doc Get appup instructions for the given application.
+%%
+%% This function can be called from inside any of the callback
+%% functions. It reads the appup file for the given application and
+%% returns the instructions for upgrade and downgrade for the versions
+%% in the test.
+%%
+%% <code>CtData</code> must be the first argument received in the
+%% calling callback function - an opaque data structure set by
+%% <code>ct_release_tests</code>.
+%%
+%% See reference manual for appup files for types definitions for the
+%% instructions.
+get_appup(#ct_data{from=FromApps,to=ToApps},App) ->
+ case lists:keyfind(App,1,ToApps) of
+ {App,ToVsn,ToDir} ->
+ Appup = filename:join([ToDir, "ebin", atom_to_list(App)++".appup"]),
+ {ok, [{ToVsn, Ups, Downs}]} = file:consult(Appup),
+ {App,FromVsn,_} = lists:keyfind(App,1,FromApps),
+ case {systools_relup:appup_search_for_version(FromVsn,Ups),
+ systools_relup:appup_search_for_version(FromVsn,Downs)} of
+ {{ok,Up},{ok,Down}} ->
+ {ok,{FromVsn,ToVsn,Up,Down}};
+ _ ->
+ {error,{vsn_not_found,{App,FromVsn}}}
+ end;
+ false ->
+ {error,{app_not_found,App}}
+ end.
+
+%%-----------------------------------------------------------------
+init_upgrade_test() ->
+ %% Check that a real release is running, not e.g. cerl
+ ok = application:ensure_started(sasl),
+ case release_handler:which_releases() of
+ [{_,_,[],_}] ->
+ %% Fake release, no applications
+ throw({skip, "Need a real release running to create other releases"});
+ _ ->
+ Major = init_upgrade_test(major),
+ Minor = init_upgrade_test(minor),
+ {Major,Minor}
+ end.
+
+init_upgrade_test(Level) ->
+ {FromVsn,ToVsn} = get_rels(Level),
+ OldRel =
+ case test_server:is_release_available(FromVsn) of
+ true ->
+ {release,FromVsn};
+ false ->
+ case ct:get_config({otp_releases,list_to_atom(FromVsn)}) of
+ undefined ->
+ false;
+ Prog0 ->
+ case os:find_executable(Prog0) of
+ false ->
+ false;
+ Prog ->
+ {prog,Prog}
+ end
+ end
+ end,
+ case OldRel of
+ false ->
+ ct:log("Release ~p is not available."
+ " Upgrade on '~p' level can not be tested.",
+ [FromVsn,Level]),
+ undefined;
+ _ ->
+ init_upgrade_test(FromVsn,ToVsn,OldRel)
+ end.
+
+get_rels(major) ->
+ %% Given that the current major release is X, then this is an
+ %% upgrade from major release X-1 to the current release.
+ Current = erlang:system_info(otp_release),
+ PreviousMajor = previous_major(Current),
+ {PreviousMajor,Current};
+get_rels(minor) ->
+ %% Given that this is a (possibly) patched version of major
+ %% release X, then this is an upgrade from major release X to the
+ %% current release.
+ CurrentMajor = erlang:system_info(otp_release),
+ Current = CurrentMajor++"_patched",
+ {CurrentMajor,Current}.
+
+init_upgrade_test(FromVsn,ToVsn,OldRel) ->
+ 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(Name,peer,[{erl,[OldRel]}]),
+ {Apps,Path} = fetch_all_apps(Node),
+ test_server:stop_node(Node),
+ {FromVsn,ToVsn,Apps,Path}.
+
+fetch_all_apps(Node) ->
+ Paths = rpc:call(Node,code,get_path,[]),
+ %% Find all possible applications in the path
+ AppFiles =
+ lists:flatmap(
+ fun(P) ->
+ filelib:wildcard(filename:join(P,"*.app"))
+ end,
+ Paths),
+ %% Figure out which version of each application is running on this
+ %% node. Using application:load and application:get_key instead of
+ %% reading the .app files since there might be multiple versions
+ %% of a .app file and we only want the one that is actually
+ %% running.
+ AppVsns =
+ lists:flatmap(
+ fun(F) ->
+ A = list_to_atom(filename:basename(filename:rootname(F))),
+ _ = rpc:call(Node,application,load,[A]),
+ case rpc:call(Node,application,get_key,[A,vsn]) of
+ {ok,V} -> [{A,V}];
+ _ -> []
+ end
+ end,
+ AppFiles),
+ ErtsVsn = rpc:call(Node, erlang, system_info, [version]),
+ {[{erts,ErtsVsn}|AppVsns], Paths}.
+
+
+%%-----------------------------------------------------------------
+upgrade(Apps,Level,Callback,CreateDir,InstallDir,Config) ->
+ ct:log("Test upgrade of the following applications: ~p",[Apps]),
+ ct:log(".rel files and start scripts are created in:~n~ts",[CreateDir]),
+ ct:log("The release is installed in:~n~ts",[InstallDir]),
+ case proplists:get_value(release_test,Config) of
+ undefined ->
+ throw({fail,"ct_release_test:init/1 not run"});
+ RTConfig ->
+ case proplists:get_value(Level,RTConfig) of
+ undefined ->
+ throw({skip,"Old release not available"});
+ Data ->
+ {FromVsn,FromRel,FromAppsVsns} =
+ target_system(Apps, CreateDir, InstallDir, Data),
+ {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]),
+ do_upgrade(Callback, FromVsn, FromAppsVsns, ToRel,
+ ToAppsVsns, InstallDir)
+ end
+ end.
+
+%%% This is similar to sasl/examples/src/target_system.erl, but with
+%%% the following adjustments:
+%%% - add a log directory
+%%% - use an own 'start' script
+%%% - chmod 'start' and 'start_erl'
+target_system(Apps,CreateDir,InstallDir,{FromVsn,_,AllAppsVsns,Path}) ->
+ RelName0 = "otp-"++FromVsn,
+
+ AppsVsns = [{A,V} || {A,V} <- AllAppsVsns, lists:member(A,Apps)],
+ {RelName,ErtsVsn} = create_relfile(AppsVsns,CreateDir,RelName0,FromVsn),
+
+ %% Create .script and .boot
+ ok = systools(make_script,[RelName,[{path,Path}]]),
+
+ %% Create base tar file - i.e. erts and all apps
+ ok = systools(make_tar,[RelName,[{erts,code:root_dir()},
+ {path,Path}]]),
+
+ %% Unpack the tar to complete the installation
+ erl_tar:extract(RelName ++ ".tar.gz", [{cwd, InstallDir}, compressed]),
+
+ %% Add bin and log dirs
+ BinDir = filename:join([InstallDir, "bin"]),
+ 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"]),
+ 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"])),
+
+ %% Copy scripts from erts-xxx/bin to bin
+ copy_file(filename:join([ErtsBinDir, "epmd"]),
+ filename:join([BinDir, "epmd"]), [preserve]),
+ copy_file(filename:join([ErtsBinDir, "run_erl"]),
+ filename:join([BinDir, "run_erl"]), [preserve]),
+ copy_file(filename:join([ErtsBinDir, "to_erl"]),
+ filename:join([BinDir, "to_erl"]), [preserve]),
+
+ %% create start_erl.data, sys.config and start.src
+ StartErlData = filename:join([InstallDir, "releases", "start_erl.data"]),
+ write_file(StartErlData, io_lib:fwrite("~s ~s~n", [ErtsVsn, FromVsn])),
+ SysConfig = filename:join([InstallDir, "releases", FromVsn, "sys.config"]),
+ write_file(SysConfig, "[]."),
+ StartSrc = filename:join(ErtsBinDir,"start.src"),
+ write_file(StartSrc,start_script()),
+ ok = file:change_mode(StartSrc,8#0755),
+
+ %% Make start_erl executable
+ %% (this has been fixed in OTP 17 - it is now installed with
+ %% $INSTALL_SCRIPT instead of $INSTALL_DATA and should therefore
+ %% be executable from the start)
+ ok = file:change_mode(filename:join(ErtsBinDir,"start_erl.src"),8#0755),
+
+ %% Substitute variables in erl.src, start.src and start_erl.src
+ %% (.src found in erts-xxx/bin - result stored in bin)
+ subst_src_scripts(["erl", "start", "start_erl"], ErtsBinDir, BinDir,
+ [{"FINAL_ROOTDIR", InstallDir}, {"EMU", "beam"}],
+ [preserve]),
+
+ %% Create RELEASES
+ RelFile = filename:join([InstallDir, "releases",
+ filename:basename(RelName) ++ ".rel"]),
+ release_handler:create_RELEASES(InstallDir, RelFile),
+
+ {FromVsn, RelName,AppsVsns}.
+
+systools(Func,Args) ->
+ case apply(systools,Func,Args) of
+ ok ->
+ ok;
+ error ->
+ throw({fail,{systools,Func,Args}})
+ end.
+
+%%% This is a copy of $ROOT/erts-xxx/bin/start.src, modified to add
+%%% sname and heart
+start_script() ->
+ ["#!/bin/sh\n"
+ "ROOTDIR=%FINAL_ROOTDIR%\n"
+ "\n"
+ "if [ -z \"$RELDIR\" ]\n"
+ "then\n"
+ " RELDIR=$ROOTDIR/releases\n"
+ "fi\n"
+ "\n"
+ "START_ERL_DATA=${1:-$RELDIR/start_erl.data}\n"
+ "\n"
+ "$ROOTDIR/bin/run_erl -daemon /tmp/ $ROOTDIR/log \"exec $ROOTDIR/bin/start_erl $ROOTDIR $RELDIR $START_ERL_DATA -sname ",atom_to_list(?testnode)," -heart\"\n"].
+
+%%% Create a release containing the current (the test node) OTP
+%%% release, including relup to allow upgrade from an earlier OTP
+%%% release.
+upgrade_system(Apps, FromRel, CreateDir, InstallDir, {_,ToVsn,_,_}) ->
+ ct:log("Generating release to upgrade to."),
+
+ RelName0 = "otp-"++ToVsn,
+
+ AppsVsns = get_vsns(Apps),
+ {RelName,_} = create_relfile(AppsVsns,CreateDir,RelName0,ToVsn),
+ FromPath = filename:join([InstallDir,lib,"*",ebin]),
+
+ ok = systools(make_script,[RelName]),
+ ok = systools(make_relup,[RelName,[FromRel],[FromRel],
+ [{path,[FromPath]},
+ {outdir,CreateDir}]]),
+ SysConfig = filename:join([CreateDir, "sys.config"]),
+ write_file(SysConfig, "[]."),
+
+ ok = systools(make_tar,[RelName,[{erts,code:root_dir()}]]),
+
+ {ToVsn, RelName,AppsVsns}.
+
+%%% Start a new node running the release from target_system/6
+%%% above. Then upgrade to the system from upgrade_system/6.
+do_upgrade({Cb,InitState},FromVsn,FromAppsVsns,ToRel,ToAppsVsns,InstallDir) ->
+ ct:log("Upgrade test attempting to start node.~n"
+ "If test fails, logs can be found in:~n~ts",
+ [filename:join(InstallDir,log)]),
+ Start = filename:join([InstallDir,bin,start]),
+ {ok,Node} = start_node(Start,FromVsn,FromAppsVsns),
+
+ 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]},
+ State1 = do_callback(Node,Cb,upgrade_init,[CtData,InitState]),
+
+ [{"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ToRelName = filename:basename(ToRel),
+ copy_file(ToRel++".tar.gz",
+ filename:join([InstallDir,releases,ToRelName++".tar.gz"])),
+ ct:log("Unpacking new release"),
+ {ok,ToVsn} = rpc:call(Node,release_handler,unpack_release,[ToRelName]),
+ [{"OTP upgrade test",ToVsn,_,unpacked},
+ {"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ct:log("Installing new release"),
+ case rpc:call(Node,release_handler,install_release,[ToVsn]) of
+ {ok,FromVsn,_} ->
+ ok;
+ {continue_after_restart,FromVsn,_} ->
+ ct:log("Waiting for node restart")
+ end,
+ %% 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.
+ {ok, _} = wait_node_up(current,ToVsn,ToAppsVsns),
+
+ [{"OTP upgrade test",ToVsn,_,current},
+ {"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ct:log("Permanenting new release"),
+ ok = rpc:call(Node,release_handler,make_permanent,[ToVsn]),
+ [{"OTP upgrade test",ToVsn,_,permanent},
+ {"OTP upgrade test",FromVsn,_,old}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+
+ State2 = do_callback(Node,Cb,upgrade_upgraded,[CtData,State1]),
+
+ ct:log("Re-installing old release"),
+ case rpc:call(Node,release_handler,install_release,[FromVsn]) of
+ {ok,FromVsn,_} ->
+ ok;
+ {continue_after_restart,FromVsn,_} ->
+ ct:log("Waiting for node restart")
+ end,
+ %% 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.
+ {ok, _} = wait_node_up(current,FromVsn,FromAppsVsns),
+
+ [{"OTP upgrade test",ToVsn,_,permanent},
+ {"OTP upgrade test",FromVsn,_,current}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ct:log("Permanenting old release"),
+ ok = rpc:call(Node,release_handler,make_permanent,[FromVsn]),
+ [{"OTP upgrade test",ToVsn,_,old},
+ {"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+
+ _State3 = do_callback(Node,Cb,upgrade_downgraded,[CtData,State2]),
+
+ ct:log("Terminating node ~p",[Node]),
+ erlang:monitor_node(Node,true),
+ _ = rpc:call(Node,init,stop,[]),
+ receive {nodedown,Node} -> ok end,
+ ct:log("Node terminated"),
+
+ ok.
+
+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]),
+ R = rpc:call(Node,Mod,Func,Args),
+ ct:log("~p:~p/~w returned: ~p",[Mod,Func,length(Args),R]),
+ case R of
+ {badrpc,Error} ->
+ throw({fail,{test_upgrade_callback,Mod,Func,Args,Error}});
+ NewState ->
+ NewState
+ end.
+
+%%% Library functions
+previous_major("17") ->
+ "r16b";
+previous_major(Rel) ->
+ integer_to_list(list_to_integer(Rel)-1).
+
+create_relfile(AppsVsns,CreateDir,RelName0,RelVsn) ->
+ UpgradeAppsVsns = [{A,V,restart_type(A)} || {A,V} <- AppsVsns],
+
+ CoreAppVsns0 = get_vsns([kernel,stdlib,sasl]),
+ CoreAppVsns =
+ [{A,V,restart_type(A)} || {A,V} <- CoreAppVsns0,
+ false == lists:keymember(A,1,AppsVsns)],
+
+ Apps = [App || {App,_} <- AppsVsns],
+ StartDepsVsns = get_start_deps(Apps,CoreAppVsns),
+ StartApps = [StartApp || {StartApp,_,_} <- StartDepsVsns] ++ Apps,
+
+ {RuntimeDepsVsns,_} = get_runtime_deps(StartApps,StartApps,[],[]),
+
+ AllAppsVsns0 = StartDepsVsns ++ UpgradeAppsVsns ++ RuntimeDepsVsns,
+
+ %% 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([common_test]),
+ TestToolAppsVsns =
+ [{A,V,none} || {A,V} <- TestToolAppsVsns0,
+ false == lists:keymember(A,1,AllAppsVsns0)],
+
+ AllAppsVsns1 = AllAppsVsns0 ++ TestToolAppsVsns,
+ AllAppsVsns = [AV || AV={A,_,_} <- AllAppsVsns1,
+ false == lists:member(A,?exclude_apps)],
+
+ ErtsVsn = erlang:system_info(version),
+
+ %% Create the .rel file
+ RelContent = {release,{"OTP upgrade test",RelVsn},{erts,ErtsVsn},AllAppsVsns},
+ RelName = filename:join(CreateDir,RelName0),
+ RelFile = RelName++".rel",
+ {ok,Fd} = file:open(RelFile,[write,{encoding,utf8}]),
+ io:format(Fd,"~tp.~n",[RelContent]),
+ ok = file:close(Fd),
+ {RelName,ErtsVsn}.
+
+get_vsns(Apps) ->
+ [begin
+ _ = application:load(A),
+ {ok,V} = application:get_key(A,vsn),
+ {A,V}
+ end || A <- Apps].
+
+get_start_deps([App|Apps],Acc) ->
+ _ = application:load(App),
+ {ok,StartDeps} = application:get_key(App,applications),
+ StartDepsVsns =
+ [begin
+ _ = application:load(StartApp),
+ {ok,StartVsn} = application:get_key(StartApp,vsn),
+ {StartApp,StartVsn,restart_type(StartApp)}
+ end || StartApp <- StartDeps,
+ false == lists:keymember(StartApp,1,Acc)],
+ DepsStartDeps = get_start_deps(StartDeps,Acc ++ StartDepsVsns),
+ get_start_deps(Apps,DepsStartDeps);
+get_start_deps([],Acc) ->
+ Acc.
+
+get_runtime_deps([App|Apps],StartApps,Acc,Visited) ->
+ case lists:member(App,Visited) of
+ true ->
+ get_runtime_deps(Apps,StartApps,Acc,Visited);
+ false ->
+ %% runtime_dependencies should be possible to read with
+ %% application:get_key/2, but still isn't so we need to
+ %% read the .app file...
+ AppFile = code:where_is_file(atom_to_list(App) ++ ".app"),
+ {ok,[{application,App,Attrs}]} = file:consult(AppFile),
+ RuntimeDeps =
+ lists:flatmap(
+ fun(Str) ->
+ [RuntimeAppStr,_] = string:tokens(Str,"-"),
+ RuntimeApp = list_to_atom(RuntimeAppStr),
+ case {lists:keymember(RuntimeApp,1,Acc),
+ lists:member(RuntimeApp,StartApps)} of
+ {false,false} when RuntimeApp=/=erts ->
+ [RuntimeApp];
+ _ ->
+ []
+ end
+ end,
+ proplists:get_value(runtime_dependencies,Attrs,[])),
+ RuntimeDepsVsns =
+ [begin
+ _ = application:load(RuntimeApp),
+ {ok,RuntimeVsn} = application:get_key(RuntimeApp,vsn),
+ {RuntimeApp,RuntimeVsn,none}
+ end || RuntimeApp <- RuntimeDeps],
+ {DepsRuntimeDeps,NewVisited} =
+ get_runtime_deps(RuntimeDeps,StartApps,Acc++RuntimeDepsVsns,[App|Visited]),
+ get_runtime_deps(Apps,StartApps,DepsRuntimeDeps,NewVisited)
+ end;
+get_runtime_deps([],_,Acc,Visited) ->
+ {Acc,Visited}.
+
+restart_type(App) when App==kernel; App==stdlib; App==sasl ->
+ permanent;
+restart_type(_) ->
+ temporary.
+
+copy_file(Src, Dest) ->
+ copy_file(Src, Dest, []).
+
+copy_file(Src, Dest, Opts) ->
+ {ok,_} = file:copy(Src, Dest),
+ case lists:member(preserve, Opts) of
+ true ->
+ {ok, FileInfo} = file:read_file_info(Src),
+ 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]),
+ ok = file:write(Fd, unicode:characters_to_binary(Conts,Enc,Enc)),
+ ok = file:close(Fd).
+
+%% Substitute all occurrences of %Var% for Val in the given scripts
+subst_src_scripts(Scripts, SrcDir, DestDir, Vars, Opts) ->
+ lists:foreach(fun(Script) ->
+ subst_src_script(Script, SrcDir, DestDir,
+ Vars, Opts)
+ end, Scripts).
+
+subst_src_script(Script, SrcDir, DestDir, Vars, Opts) ->
+ subst_file(filename:join([SrcDir, Script ++ ".src"]),
+ filename:join([DestDir, Script]),
+ Vars, Opts).
+
+subst_file(Src, Dest, Vars, Opts) ->
+ {ok, Bin} = file:read_file(Src),
+ Conts = binary_to_list(Bin),
+ NConts = subst(Conts, Vars),
+ write_file(Dest, NConts),
+ case lists:member(preserve, Opts) of
+ true ->
+ {ok, FileInfo} = file:read_file_info(Src),
+ file:write_file_info(Dest, FileInfo);
+ false ->
+ ok
+ end.
+
+subst(Str, [{Var,Val}|Vars]) ->
+ subst(re:replace(Str,"%"++Var++"%",Val,[{return,list}]),Vars);
+subst(Str, []) ->
+ Str.
+
+%%% Start a node by executing the given start command. This node will
+%%% be used for upgrade.
+start_node(Start,ExpVsn,ExpAppsVsns) ->
+ Port = open_port({spawn_executable, Start}, []),
+ unlink(Port),
+ erlang:port_close(Port),
+ wait_node_up(permanent,ExpVsn,ExpAppsVsns).
+
+wait_node_up(ExpStatus,ExpVsn,ExpAppsVsns) ->
+ Node = node_name(?testnode),
+ wait_node_up(Node,ExpStatus,ExpVsn,lists:keysort(1,ExpAppsVsns),60).
+
+wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,0) ->
+ test_server:fail({node_not_started,app_check_failed,ExpVsn,ExpAppsVsns,
+ rpc:call(Node,release_handler,which_releases,[ExpStatus]),
+ rpc:call(Node,application,which_applications,[])});
+wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N) ->
+ case {rpc:call(Node,release_handler,which_releases,[ExpStatus]),
+ rpc:call(Node, application, which_applications, [])} of
+ {[{_,ExpVsn,_,_}],Apps} when is_list(Apps) ->
+ case [{A,V} || {A,_,V} <- lists:keysort(1,Apps),
+ lists:keymember(A,1,ExpAppsVsns)] of
+ ExpAppsVsns ->
+ {ok,Node};
+ _ ->
+ timer:sleep(2000),
+ wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N-1)
+ end;
+ _ ->
+ timer:sleep(2000),
+ wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N-1)
+ end.
+
+node_name(Sname) ->
+ {ok,Host} = inet:gethostname(),
+ list_to_atom(atom_to_list(Sname) ++ "@" ++ Host).
+
+rm_rf(Dir) ->
+ case file:read_file_info(Dir) of
+ {ok, #file_info{type = directory}} ->
+ {ok, Content} = file:list_dir_all(Dir),
+ [rm_rf(filename:join(Dir,C)) || C <- Content],
+ ok=file:del_dir(Dir),
+ ok;
+ {ok, #file_info{}} ->
+ ok=file:delete(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 f4d9949776..dac596a135 100644
--- a/lib/common_test/src/ct_repeat.erl
+++ b/lib/common_test/src/ct_repeat.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2007-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2007-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -43,13 +44,13 @@ loop_test(If,Args) when is_list(Args) ->
false;
E = {error,_} ->
io:format("Common Test error: ~p\n\n",[E]),
- file:set_cwd(Cwd),
+ 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 =
@@ -75,7 +76,7 @@ loop_test(If,Args) when is_list(Args) ->
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.
diff --git a/lib/common_test/src/ct_rpc.erl b/lib/common_test/src/ct_rpc.erl
index 03d95d1408..b4a0bc9d74 100644
--- a/lib/common_test/src/ct_rpc.erl
+++ b/lib/common_test/src/ct_rpc.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
-%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -80,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});
@@ -144,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
@@ -189,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 266ca73417..fbb9c7ab60 100644
--- a/lib/common_test/src/ct_run.erl
+++ b/lib/common_test/src/ct_run.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -64,6 +65,7 @@
logdir,
logopts = [],
basic_html,
+ esc_chars = true,
verbosity = [],
config = [],
event_handlers = [],
@@ -71,17 +73,19 @@
enable_builtin_hooks,
include = [],
auto_compile,
+ abort_if_missing_suites,
silent_connections = [],
stylesheet,
multiply_timetraps = 1,
scale_timetraps = false,
create_priv_dir,
- testspecs = [],
+ testspec_files = [],
+ current_testspec,
tests,
starter}).
%%%-----------------------------------------------------------------
-%%% @spec script_start() -> void()
+%%% @spec script_start() -> term()
%%%
%%% @doc Start tests via the ct_run program or script.
%%%
@@ -224,18 +228,24 @@ finish(Tracing, ExitStatus, Args) ->
if ExitStatus == interactive_mode ->
interactive_mode;
true ->
- %% it's possible to tell CT to finish execution with a call
- %% to a different function than the normal halt/1 BIF
- %% (meant to be used mainly for reading the CT exit status)
- case get_start_opt(halt_with,
- fun([HaltMod,HaltFunc]) ->
- {list_to_atom(HaltMod),
- list_to_atom(HaltFunc)} end,
- Args) of
- undefined ->
- halt(ExitStatus);
- {M,F} ->
- apply(M, F, [ExitStatus])
+ case get_start_opt(vts, true, Args) of
+ true ->
+ %% VTS mode, don't halt the node
+ ok;
+ _ ->
+ %% it's possible to tell CT to finish execution with a call
+ %% to a different function than the normal halt/1 BIF
+ %% (meant to be used mainly for reading the CT exit status)
+ case get_start_opt(halt_with,
+ fun([HaltMod,HaltFunc]) ->
+ {list_to_atom(HaltMod),
+ list_to_atom(HaltFunc)} end,
+ Args) of
+ undefined ->
+ halt(ExitStatus);
+ {M,F} ->
+ apply(M, F, [ExitStatus])
+ end
end
end.
@@ -243,12 +253,14 @@ script_start1(Parent, Args) ->
%% read general start flags
Label = get_start_opt(label, fun([Lbl]) -> Lbl end, Args),
Profile = get_start_opt(profile, fun([Prof]) -> Prof end, Args),
- Vts = get_start_opt(vts, true, Args),
+ Vts = get_start_opt(vts, true, undefined, Args),
Shell = get_start_opt(shell, true, Args),
Cover = get_start_opt(cover, fun([CoverFile]) -> ?abs(CoverFile) end, Args),
- CoverStop = get_start_opt(cover_stop, fun([CS]) -> list_to_atom(CS) end, Args),
+ CoverStop = get_start_opt(cover_stop,
+ fun([CS]) -> list_to_atom(CS) end, Args),
LogDir = get_start_opt(logdir, fun([LogD]) -> LogD end, Args),
- LogOpts = get_start_opt(logopts, fun(Os) -> [list_to_atom(O) || O <- Os] end,
+ LogOpts = get_start_opt(logopts,
+ fun(Os) -> [list_to_atom(O) || O <- Os] end,
[], Args),
Verbosity = verbosity_args2opts(Args),
MultTT = get_start_opt(multiply_timetraps,
@@ -290,10 +302,10 @@ script_start1(Parent, Args) ->
application:set_env(common_test, auto_compile, true),
InclDirs =
case proplists:get_value(include, Args) of
- Incl when is_list(hd(Incl)) ->
- Incl;
+ Incls when is_list(hd(Incls)) ->
+ [filename:absname(IDir) || IDir <- Incls];
Incl when is_list(Incl) ->
- [Incl];
+ [filename:absname(Incl)];
undefined ->
[]
end,
@@ -311,6 +323,12 @@ script_start1(Parent, Args) ->
application:set_env(common_test, auto_compile, false),
{false,[]}
end,
+
+ %% abort test run if some suites can't be compiled
+ AbortIfMissing = get_start_opt(abort_if_missing_suites,
+ fun([]) -> true;
+ ([Bool]) -> list_to_atom(Bool)
+ end, false, Args),
%% silent connections
SilentConns =
get_start_opt(silent_connections,
@@ -321,14 +339,23 @@ script_start1(Parent, Args) ->
Stylesheet = get_start_opt(stylesheet,
fun([SS]) -> ?abs(SS) end, Args),
%% basic_html - used by ct_logs
- BasicHtml = case proplists:get_value(basic_html, Args) of
- undefined ->
+ BasicHtml = case {Vts,proplists:get_value(basic_html, Args)} of
+ {undefined,undefined} ->
application:set_env(common_test, basic_html, false),
undefined;
_ ->
application:set_env(common_test, basic_html, true),
true
end,
+ %% esc_chars - used by ct_logs
+ EscChars = case proplists:get_value(no_esc_chars, Args) of
+ undefined ->
+ application:set_env(common_test, esc_chars, true),
+ undefined;
+ _ ->
+ application:set_env(common_test, esc_chars, false),
+ false
+ end,
%% disable_log_cache - used by ct_logs
case proplists:get_value(disable_log_cache, Args) of
undefined ->
@@ -342,11 +369,13 @@ script_start1(Parent, Args) ->
cover = Cover, cover_stop = CoverStop,
logdir = LogDir, logopts = LogOpts,
basic_html = BasicHtml,
+ esc_chars = EscChars,
verbosity = Verbosity,
event_handlers = EvHandlers,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
auto_compile = AutoCompile,
+ abort_if_missing_suites = AbortIfMissing,
include = IncludeDirs,
silent_connections = SilentConns,
stylesheet = Stylesheet,
@@ -354,9 +383,10 @@ script_start1(Parent, Args) ->
scale_timetraps = ScaleTT,
create_priv_dir = CreatePrivDir,
starter = script},
-
+
%% check if log files should be refreshed or go on to run tests...
Result = run_or_refresh(Opts, Args),
+
%% send final results to starting process waiting in script_start/0
Parent ! {self(), Result}.
@@ -370,21 +400,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
@@ -475,8 +505,11 @@ execute_one_spec(TS, Opts, Args) ->
case check_and_install_configfiles(AllConfig, TheLogDir, Opts) of
ok -> % read tests from spec
{Run,Skip} = ct_testspec:prepare_tests(TS, node()),
- do_run(Run, Skip, Opts#opts{config=AllConfig,
- logdir=TheLogDir}, Args);
+ Result = do_run(Run, Skip, Opts#opts{config=AllConfig,
+ logdir=TheLogDir,
+ current_testspec=TS}, Args),
+ ct_util:delete_testdata(testspec),
+ Result;
Error ->
Error
end.
@@ -551,6 +584,9 @@ combine_test_opts(TS, Specs, Opts) ->
ACBool
end,
+ AbortIfMissing = choose_val(Opts#opts.abort_if_missing_suites,
+ TSOpts#opts.abort_if_missing_suites),
+
BasicHtml =
case choose_val(Opts#opts.basic_html,
TSOpts#opts.basic_html) of
@@ -562,14 +598,26 @@ 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,
- testspecs = Specs,
+ testspec_files = Specs,
cover = Cover,
cover_stop = CoverStop,
logdir = which(logdir, LogDir),
logopts = AllLogOpts,
basic_html = BasicHtml,
+ esc_chars = EscChars,
verbosity = AllVerbosity,
silent_connections = AllSilentConns,
config = TSOpts#opts.config,
@@ -578,6 +626,7 @@ combine_test_opts(TS, Specs, Opts) ->
enable_builtin_hooks = EnableBuiltinHooks,
stylesheet = Stylesheet,
auto_compile = AutoCompile,
+ abort_if_missing_suites = AbortIfMissing,
include = AllInclude,
multiply_timetraps = MultTT,
scale_timetraps = ScaleTT,
@@ -662,8 +711,10 @@ script_start3(Opts, Args) ->
if Opts#opts.vts ; Opts#opts.shell ->
script_start4(Opts#opts{tests = []}, Args);
true ->
- script_usage(),
- {error,missing_start_options}
+ %% no start options, use default "-dir ./"
+ {ok,Dir} = file:get_cwd(),
+ io:format("ct_run -dir ~ts~n~n", [Dir]),
+ script_start4(Opts#opts{tests = tests([Dir])}, Args)
end
end.
@@ -688,7 +739,7 @@ script_start4(#opts{label = Label, profile = Profile,
logopts = LogOpts,
verbosity = Verbosity,
enable_builtin_hooks = EnableBuiltinHooks,
- logdir = LogDir, testspecs = Specs}, _Args) ->
+ logdir = LogDir, testspec_files = Specs}, _Args) ->
%% label - used by ct_logs
application:set_env(common_test, test_label, Label),
@@ -705,8 +756,8 @@ script_start4(#opts{label = Label, profile = Profile,
{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(),
@@ -742,78 +793,87 @@ script_start4(Opts = #opts{tests = Tests}, Args) ->
%%% @spec script_usage() -> ok
%%% @doc Print usage information for <code>ct_run</code>.
script_usage() ->
- io:format("\n\nUsage:\n\n"),
- io:format("Run tests in web based GUI:\n\n"
- "\tct_run -vts [-browser Browser]"
- "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
- "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
- "\n\t[-dir TestDir1 TestDir2 .. TestDirN] |"
- "\n\t[-suite Suite [-case Case]]"
- "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]"
- "\n\t[-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]"
- "\n\t[-include InclDir1 InclDir2 .. InclDirN]"
- "\n\t[-no_auto_compile]"
- "\n\t[-multiply_timetraps N]"
- "\n\t[-scale_timetraps]"
- "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]"
- "\n\t[-basic_html]\n\n"),
+ io:format("\nUsage:\n\n"),
io:format("Run tests from command line:\n\n"
- "\tct_run [-dir TestDir1 TestDir2 .. TestDirN] |"
- "\n\t[-suite Suite1 Suite2 .. SuiteN [-case Case1 Case2 .. CaseN]]"
- "\n\t[-step [config | keep_inactive]]"
- "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
- "\n\t[-userconfig CallbackModule ConfigFile1 .. ConfigFileN]"
- "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
- "\n\t[-logdir LogDir]"
- "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]"
- "\n\t[-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]"
- "\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
- "\n\t[-stylesheet CSSFile]"
- "\n\t[-cover CoverCfgFile]"
- "\n\t[-cover_stop Bool]"
- "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
- "\n\t[-ct_hooks CTHook1 CTHook2 .. CTHookN]"
- "\n\t[-include InclDir1 InclDir2 .. InclDirN]"
- "\n\t[-no_auto_compile]"
- "\n\t[-multiply_timetraps N]"
- "\n\t[-scale_timetraps]"
- "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]"
- "\n\t[-basic_html]"
- "\n\t[-repeat N] |"
- "\n\t[-duration HHMMSS [-force_stop [skip_rest]]] |"
- "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]\n\n"),
+ "\tct_run -dir TestDir1 TestDir2 .. TestDirN |"
+ "\n\t [-dir TestDir] -suite Suite1 Suite2 .. SuiteN"
+ "\n\t [-group Group1 Group2 .. GroupN] [-case Case1 Case2 .. CaseN]"
+ "\n\t [-step [config | keep_inactive]]"
+ "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
+ "\n\t [-userconfig CallbackModule ConfigFile1 .. ConfigFileN]"
+ "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]"
+ "\n\t [-logdir LogDir]"
+ "\n\t [-logopts LogOpt1 LogOpt2 .. LogOptN]"
+ "\n\t [-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]"
+ "\n\t [-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
+ "\n\t [-stylesheet CSSFile]"
+ "\n\t [-cover CoverCfgFile]"
+ "\n\t [-cover_stop Bool]"
+ "\n\t [-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
+ "\n\t [-ct_hooks CTHook1 CTHook2 .. CTHookN]"
+ "\n\t [-include InclDir1 InclDir2 .. InclDirN]"
+ "\n\t [-no_auto_compile]"
+ "\n\t [-abort_if_missing_suites]"
+ "\n\t [-multiply_timetraps N]"
+ "\n\t [-scale_timetraps]"
+ "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]"
+ "\n\t [-basic_html]"
+ "\n\t [-no_esc_chars]"
+ "\n\t [-repeat N] |"
+ "\n\t [-duration HHMMSS [-force_stop [skip_rest]]] |"
+ "\n\t [-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]"
+ "\n\t [-exit_status ignore_config]"
+ "\n\t [-help]\n\n"),
io:format("Run tests using test specification:\n\n"
"\tct_run -spec TestSpec1 TestSpec2 .. TestSpecN"
- "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
- "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
- "\n\t[-logdir LogDir]"
- "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]"
- "\n\t[-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]"
- "\n\t[-allow_user_terms]"
- "\n\t[-join_specs]"
- "\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
- "\n\t[-stylesheet CSSFile]"
- "\n\t[-cover CoverCfgFile]"
- "\n\t[-cover_stop Bool]"
- "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
- "\n\t[-ct_hooks CTHook1 CTHook2 .. CTHookN]"
- "\n\t[-include InclDir1 InclDir2 .. InclDirN]"
- "\n\t[-no_auto_compile]"
- "\n\t[-multiply_timetraps N]"
- "\n\t[-scale_timetraps]"
- "\n\t[-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]"
- "\n\t[-basic_html]"
- "\n\t[-repeat N] |"
- "\n\t[-duration HHMMSS [-force_stop [skip_rest]]] |"
- "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]\n\n"),
+ "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
+ "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]"
+ "\n\t [-logdir LogDir]"
+ "\n\t [-logopts LogOpt1 LogOpt2 .. LogOptN]"
+ "\n\t [-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]"
+ "\n\t [-allow_user_terms]"
+ "\n\t [-join_specs]"
+ "\n\t [-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
+ "\n\t [-stylesheet CSSFile]"
+ "\n\t [-cover CoverCfgFile]"
+ "\n\t [-cover_stop Bool]"
+ "\n\t [-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
+ "\n\t [-ct_hooks CTHook1 CTHook2 .. CTHookN]"
+ "\n\t [-include InclDir1 InclDir2 .. InclDirN]"
+ "\n\t [-no_auto_compile]"
+ "\n\t [-abort_if_missing_suites]"
+ "\n\t [-multiply_timetraps N]"
+ "\n\t [-scale_timetraps]"
+ "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]"
+ "\n\t [-basic_html]"
+ "\n\t [-no_esc_chars]"
+ "\n\t [-repeat N] |"
+ "\n\t [-duration HHMMSS [-force_stop [skip_rest]]] |"
+ "\n\t [-until [YYMoMoDD]HHMMSS [-force_stop [skip_rest]]]\n\n"),
io:format("Refresh the HTML index files:\n\n"
"\tct_run -refresh_logs [LogDir]"
- "[-logdir LogDir] "
- "[-basic_html]\n\n"),
+ " [-logdir LogDir] "
+ " [-basic_html]\n\n"),
io:format("Run CT in interactive mode:\n\n"
"\tct_run -shell"
- "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
- "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]\n\n").
+ "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
+ "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]\n\n"),
+ io:format("Run tests in web based GUI:\n\n"
+ "\tct_run -vts [-browser Browser]"
+ "\n\t [-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
+ "\n\t [-decrypt_key Key] | [-decrypt_file KeyFile]"
+ "\n\t [-dir TestDir1 TestDir2 .. TestDirN] |"
+ "\n\t [-suite Suite [-case Case]]"
+ "\n\t [-logopts LogOpt1 LogOpt2 .. LogOptN]"
+ "\n\t [-verbosity GenVLvl | [CategoryVLvl1 .. CategoryVLvlN]]"
+ "\n\t [-include InclDir1 InclDir2 .. InclDirN]"
+ "\n\t [-no_auto_compile]"
+ "\n\t [-abort_if_missing_suites]"
+ "\n\t [-multiply_timetraps N]"
+ "\n\t [-scale_timetraps]"
+ "\n\t [-create_priv_dir auto_per_run | auto_per_tc | manual_per_tc]"
+ "\n\t [-basic_html]"
+ "\n\t [-no_esc_chars]\n\n").
%%%-----------------------------------------------------------------
%%% @hidden
@@ -841,9 +901,8 @@ install(Opts, LogDir) ->
VarFile = variables_file_name(LogDir),
case file:open(VarFile, [write]) of
{ok,Fd} ->
- [io:format(Fd, "~p.\n", [Opt]) || Opt <- ConfOpts ],
- file:close(Fd),
- ok;
+ _ = [io:format(Fd, "~p.\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 "
@@ -875,7 +934,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}} ->
@@ -884,6 +943,11 @@ run_test(StartOpts) when is_list(StartOpts) ->
Other
end.
+-spec run_test1_fun(_) -> fun(() -> no_return()).
+
+run_test1_fun(StartOpts) ->
+ fun() -> run_test1(StartOpts) end.
+
run_test1(StartOpts) when is_list(StartOpts) ->
case proplists:get_value(refresh_logs, StartOpts) of
undefined ->
@@ -895,7 +959,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
@@ -906,7 +970,7 @@ run_test1(StartOpts) when is_list(StartOpts) ->
stop_trace(Tracing),
exit(Res);
RefreshDir ->
- refresh_logs(?abs(RefreshDir)),
+ ok = refresh_logs(?abs(RefreshDir)),
exit(done)
end.
@@ -1006,10 +1070,10 @@ run_test2(StartOpts) ->
case proplists:get_value(include, StartOpts) of
undefined ->
[];
- Incl when is_list(hd(Incl)) ->
- Incl;
+ Incls when is_list(hd(Incls)) ->
+ [filename:absname(IDir) || IDir <- Incls];
Incl when is_list(Incl) ->
- [Incl]
+ [filename:absname(Incl)]
end,
case os:getenv("CT_INCLUDE_PATH") of
false ->
@@ -1026,6 +1090,10 @@ run_test2(StartOpts) ->
{ACBool,[]}
end,
+ %% abort test run if some suites can't be compiled
+ AbortIfMissing = get_start_opt(abort_if_missing_suites, value, false,
+ StartOpts),
+
%% decrypt config file
case proplists:get_value(decrypt, StartOpts) of
undefined ->
@@ -1046,7 +1114,17 @@ 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);
@@ -1061,12 +1139,14 @@ 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,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
auto_compile = AutoCompile,
+ abort_if_missing_suites = AbortIfMissing,
include = Include,
silent_connections = SilentConns,
stylesheet = Stylesheet,
@@ -1080,7 +1160,7 @@ run_test2(StartOpts) ->
undefined ->
case lists:keysearch(prepared_tests, 1, StartOpts) of
{value,{_,{Run,Skip},Specs}} -> % use prepared tests
- run_prepared(Run, Skip, Opts#opts{testspecs = Specs},
+ run_prepared(Run, Skip, Opts#opts{testspec_files = Specs},
StartOpts);
false ->
run_dir(Opts, StartOpts)
@@ -1088,11 +1168,11 @@ run_test2(StartOpts) ->
Specs ->
Relaxed = get_start_opt(allow_user_terms, value, false, StartOpts),
%% using testspec(s) as input for test
- run_spec_file(Relaxed, Opts#opts{testspecs = Specs}, StartOpts)
+ run_spec_file(Relaxed, Opts#opts{testspec_files = Specs}, StartOpts)
end.
run_spec_file(Relaxed,
- Opts = #opts{testspecs = Specs},
+ Opts = #opts{testspec_files = Specs},
StartOpts) ->
Specs1 = case Specs of
[X|_] when is_integer(X) -> [Specs];
@@ -1128,10 +1208,12 @@ 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, Combined#opts{config = AllConfig}, StartOpts) of
+ try run_one_spec(TS,
+ Combined#opts{config = AllConfig,
+ current_testspec=TS},
+ StartOpts) of
Result ->
run_all_specs(TSs, Opts, StartOpts, [Result | TotResult])
catch
@@ -1310,7 +1392,9 @@ run_dir(Opts = #opts{logdir = LogDir,
end;
{undefined,undefined,[]} ->
- exit({error,no_test_specified});
+ {ok,Dir} = file:get_cwd(),
+ %% No start options, use default {dir,CWD}
+ reformat_result(catch do_run(tests(Dir), [], Opts1, StartOpts));
{Dir,Suite,GsAndCs} ->
exit({error,{incorrect_start_options,{Dir,Suite,GsAndCs}}})
@@ -1325,7 +1409,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}} ->
@@ -1334,12 +1418,17 @@ run_testspec(TestSpec) ->
Other
end.
+-spec run_testspec1_fun(_) -> fun(() -> no_return()).
+
+run_testspec1_fun(TestSpec) ->
+ fun() -> 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)
@@ -1371,11 +1460,12 @@ run_testspec2(TestSpec) ->
EnvInclude++Opts#opts.include
end,
application:set_env(common_test, include, AllInclude),
+
LogDir1 = which(logdir,Opts#opts.logdir),
case check_and_install_configfiles(
Opts#opts.config, LogDir1, Opts) of
ok ->
- Opts1 = Opts#opts{testspecs = [],
+ Opts1 = Opts#opts{testspec_files = [],
logdir = LogDir1,
include = AllInclude},
{Run,Skip} = ct_testspec:prepare_tests(TS, node()),
@@ -1390,6 +1480,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,
@@ -1401,6 +1492,7 @@ get_data_for_node(#testspec{label = Labels,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
auto_compile = ACs,
+ abort_if_missing_suites = AiMSs,
include = Incl,
multiply_timetraps = MTs,
scale_timetraps = STs,
@@ -1416,6 +1508,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 -> [];
@@ -1435,12 +1528,14 @@ get_data_for_node(#testspec{label = Labels,
EvHandlers = [{H,A} || {N,H,A} <- EvHs, N==Node],
FiltCTHooks = [Hook || {N,Hook} <- CTHooks, N==Node],
AutoCompile = proplists:get_value(Node, ACs),
+ AbortIfMissing = proplists:get_value(Node, AiMSs),
Include = [I || {N,I} <- Incl, N==Node],
#opts{label = Label,
profile = Profile,
logdir = LogDir,
logopts = LogOpts,
basic_html = BasicHtml,
+ esc_chars = EscChars,
stylesheet = Stylesheet,
verbosity = Verbosity,
silent_connections = SilentConns,
@@ -1451,6 +1546,7 @@ get_data_for_node(#testspec{label = Labels,
ct_hooks = FiltCTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
auto_compile = AutoCompile,
+ abort_if_missing_suites = AbortIfMissing,
include = Include,
multiply_timetraps = MT,
scale_timetraps = ST,
@@ -1464,15 +1560,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
@@ -1512,22 +1608,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};
@@ -1593,11 +1701,15 @@ groups_and_cases(Gs, Cs) ->
tests(TestDir, Suites, []) when is_list(TestDir), is_integer(hd(TestDir)) ->
[{?testdir(TestDir,Suites),ensure_atom(Suites),all}];
tests(TestDir, Suite, Cases) when is_list(TestDir), is_integer(hd(TestDir)) ->
+ [{?testdir(TestDir,Suite),ensure_atom(Suite),Cases}];
+tests([TestDir], Suite, Cases) when is_list(TestDir), is_integer(hd(TestDir)) ->
[{?testdir(TestDir,Suite),ensure_atom(Suite),Cases}].
tests([{Dir,Suite}],Cases) ->
[{?testdir(Dir,Suite),ensure_atom(Suite),Cases}];
tests(TestDir, Suite) when is_list(TestDir), is_integer(hd(TestDir)) ->
- tests(TestDir, ensure_atom(Suite), all).
+ tests(TestDir, ensure_atom(Suite), all);
+tests([TestDir], Suite) when is_list(TestDir), is_integer(hd(TestDir)) ->
+ tests(TestDir, ensure_atom(Suite), all).
tests(DirSuites) when is_list(DirSuites), is_tuple(hd(DirSuites)) ->
[{?testdir(Dir,Suite),ensure_atom(Suite),all} || {Dir,Suite} <- DirSuites];
tests(TestDir) when is_list(TestDir), is_integer(hd(TestDir)) ->
@@ -1621,7 +1733,7 @@ do_run(Tests, Misc, LogDir, LogOpts) when is_list(Misc),
do_run(Tests, [], Opts#opts{logdir = LogDir}, []);
do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) ->
- #opts{label = Label, profile = Profile, cover = Cover,
+ #opts{label = Label, profile = Profile,
verbosity = VLvls} = Opts,
%% label - used by ct_logs
TestLabel =
@@ -1645,22 +1757,6 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) ->
non_existing ->
{error,no_path_to_test_server};
_ ->
- Opts1 = if Cover == undefined ->
- Opts;
- true ->
- case ct_cover:get_spec(Cover) of
- {error,Reason} ->
- exit({error,Reason});
- CoverSpec ->
- CoverStop =
- case Opts#opts.cover_stop of
- undefined -> true;
- Stop -> Stop
- end,
- Opts#opts{coverspec = CoverSpec,
- cover_stop = CoverStop}
- end
- end,
%% This env variable is used by test_server to determine
%% which framework it runs under.
case os:getenv("TEST_SERVER_FRAMEWORK") of
@@ -1686,7 +1782,7 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) ->
_Pid ->
ct_util:set_testdata({starter,Opts#opts.starter}),
compile_and_run(Tests, Skip,
- Opts1#opts{verbosity=Verbosity}, Args)
+ Opts#opts{verbosity=Verbosity}, Args)
end
end.
@@ -1695,6 +1791,9 @@ compile_and_run(Tests, Skip, Opts, Args) ->
ct_util:set_testdata({stylesheet,Opts#opts.stylesheet}),
%% save logopts
ct_util:set_testdata({logopts,Opts#opts.logopts}),
+ %% save info about current testspec (testspec record or undefined)
+ ct_util:set_testdata({testspec,Opts#opts.current_testspec}),
+
%% enable silent connections
case Opts#opts.silent_connections of
[] ->
@@ -1709,7 +1808,7 @@ compile_and_run(Tests, Skip, Opts, Args) ->
ct_logs:log("Silent connections", "~p", [Conns])
end
end,
- log_ts_names(Opts#opts.testspecs),
+ log_ts_names(Opts#opts.testspec_files),
TestSuites = suite_tuples(Tests),
{_TestSuites1,SuiteMakeErrors,AllMakeErrors} =
@@ -1722,8 +1821,8 @@ compile_and_run(Tests, Skip, Opts, Args) ->
{SuiteErrs,HelpErrs} = auto_compile(TestSuites),
{TestSuites,SuiteErrs,SuiteErrs++HelpErrs}
end,
-
- case continue(AllMakeErrors) of
+
+ case continue(AllMakeErrors, Opts#opts.abort_if_missing_suites) of
true ->
SavedErrors = save_make_errors(SuiteMakeErrors),
ct_repeat:log_loop_info(Args),
@@ -1732,7 +1831,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}
@@ -1883,7 +1993,7 @@ verify_suites(TestSuites) ->
atom_to_list(
Suite)),
io:format(user,
- "Suite ~w not found"
+ "Suite ~w not found "
"in directory ~ts~n",
[Suite,TestDir]),
{Found,[{DS,[Name]}|NotFound]}
@@ -1917,7 +2027,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) ->
@@ -1958,22 +2068,7 @@ final_tests(Tests, Skip, Bad) ->
final_tests1([{TestDir,Suites,_}|Tests], Final, Skip, Bad) when
is_list(Suites), is_atom(hd(Suites)) ->
-% Separate =
-% fun(S,{DoSuite,Dont}) ->
-% case lists:keymember({TestDir,S},1,Bad) of
-% false ->
-% {[S|DoSuite],Dont};
-% true ->
-% SkipIt = {TestDir,S,"Make failed"},
-% {DoSuite,Dont++[SkipIt]}
-% end
-% end,
-
-% {DoSuites,Skip1} =
-% lists:foldl(Separate,{[],Skip},Suites),
-% Do = {TestDir,lists:reverse(DoSuites),all},
-
- Skip1 = [{TD,S,"Make failed"} || {{TD,S},_} <- Bad, S1 <- Suites,
+ Skip1 = [{TD,S,make_failed} || {{TD,S},_} <- Bad, S1 <- Suites,
S == S1, TD == TestDir],
Final1 = [{TestDir,S,all} || S <- Suites],
final_tests1(Tests, lists:reverse(Final1)++Final, Skip++Skip1, Bad);
@@ -1986,7 +2081,7 @@ final_tests1([{TestDir,all,all}|Tests], Final, Skip, Bad) ->
false ->
[]
end,
- Missing = [{TestDir,S,"Make failed"} || S <- MissingSuites],
+ Missing = [{TestDir,S,make_failed} || S <- MissingSuites],
Final1 = [{TestDir,all,all}|Final],
final_tests1(Tests, Final1, Skip++Missing, Bad);
@@ -1998,7 +2093,7 @@ final_tests1([{TestDir,Suite,GrsOrCs}|Tests], Final, Skip, Bad) when
is_list(GrsOrCs) ->
case lists:keymember({TestDir,Suite}, 1, Bad) of
true ->
- Skip1 = Skip ++ [{TestDir,Suite,all,"Make failed"}],
+ Skip1 = Skip ++ [{TestDir,Suite,all,make_failed}],
final_tests1(Tests, [{TestDir,Suite,all}|Final], Skip1, Bad);
false ->
GrsOrCs1 =
@@ -2010,6 +2105,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,
@@ -2047,9 +2149,11 @@ final_skip([Skip|Skips], Final) ->
final_skip([], Final) ->
lists:reverse(Final).
-continue([]) ->
+continue([], _) ->
true;
-continue(_MakeErrors) ->
+continue(_MakeErrors, true) ->
+ false;
+continue(_MakeErrors, _AbortIfMissingSuites) ->
io:nl(),
OldGl = group_leader(),
case set_group_leader_same_as_shell() of
@@ -2081,22 +2185,21 @@ continue(_MakeErrors) ->
end.
set_group_leader_same_as_shell() ->
- %%! Locate the shell process... UGLY!!!
GS2or3 = fun(P) ->
- case process_info(P,initial_call) of
- {initial_call,{group,server,X}} when X == 2 ; X == 3 ->
- true;
- _ ->
- false
- end
- end,
+ case process_info(P,initial_call) of
+ {initial_call,{group,server,X}} when X == 2 ; X == 3 ->
+ true;
+ _ ->
+ false
+ end
+ end,
case [P || P <- processes(), GS2or3(P),
- true == lists:keymember(shell,1,
- element(2,process_info(P,dictionary)))] of
- [GL|_] ->
- group_leader(GL, self());
- [] ->
- false
+ true == lists:keymember(shell,1,
+ element(2,process_info(P,dictionary)))] of
+ [GL|_] ->
+ group_leader(GL, self());
+ [] ->
+ false
end.
check_and_add([{TestDir0,M,_} | Tests], Added, PA) ->
@@ -2121,67 +2224,27 @@ check_and_add([{TestDir0,M,_} | Tests], Added, PA) ->
check_and_add([], _, PA) ->
{ok,PA}.
-do_run_test(Tests, Skip, Opts) ->
+do_run_test(Tests, Skip, Opts0) ->
case check_and_add(Tests, [], []) of
{ok,AddedToPath} ->
ct_util:set_testdata({stats,{0,0,{0,0}}}),
- ct_util:set_testdata({cover,undefined}),
- test_server_ctrl:start_link(local),
- case Opts#opts.coverspec of
- CovData={CovFile,
- CovNodes,
- _CovImport,
- CovExport,
- #cover{app = CovApp,
- level = CovLevel,
- excl_mods = CovExcl,
- incl_mods = CovIncl,
- cross = CovCross,
- src = _CovSrc}} ->
- ct_logs:log("COVER INFO",
- "Using cover specification file: ~ts~n"
- "App: ~w~n"
- "Cross cover: ~w~n"
- "Including ~w modules~n"
- "Excluding ~w modules",
- [CovFile,CovApp,CovCross,
- length(CovIncl),length(CovExcl)]),
-
- %% cover export file will be used for export and import
- %% between tests so make sure it doesn't exist initially
- case filelib:is_file(CovExport) of
- true ->
- DelResult = file:delete(CovExport),
- ct_logs:log("COVER INFO",
- "Warning! "
- "Export file ~ts already exists. "
- "Deleting with result: ~p",
- [CovExport,DelResult]);
- false ->
- ok
- end,
- %% tell test_server which modules should be cover compiled
- %% note that actual compilation is done when tests start
- test_server_ctrl:cover(CovApp, CovFile, CovExcl, CovIncl,
- CovCross, CovExport, CovLevel,
- Opts#opts.cover_stop),
- %% save cover data (used e.g. to add nodes dynamically)
- ct_util:set_testdata({cover,CovData}),
- %% start cover on specified nodes
- if (CovNodes /= []) and (CovNodes /= undefined) ->
- ct_logs:log("COVER INFO",
- "Nodes included in cover "
- "session: ~w",
- [CovNodes]),
- cover:start(CovNodes);
- true ->
- ok
- end,
- true;
- _ ->
- false
- end,
+ %% test_server needs to know the include path too
+ InclPath = case application:get_env(common_test, include) of
+ {ok,Incls} -> Incls;
+ _ -> []
+ end,
+ application:set_env(test_server, include, InclPath),
+
+ %% copy the escape characters setting to test_server
+ EscChars =
+ case application:get_env(common_test, esc_chars) of
+ {ok,ECBool} -> ECBool;
+ _ -> true
+ end,
+ application:set_env(test_server, esc_chars, EscChars),
+
+ {ok, _} = test_server_ctrl:start_link(local),
%% let test_server expand the test tuples and count no of cases
{Suites,NoOfCases} = count_test_cases(Tests, Skip),
@@ -2206,24 +2269,31 @@ do_run_test(Tests, Skip, Opts) ->
end,
%% if the verbosity level is set lower than ?STD_IMPORTANCE, tell
%% test_server to ignore stdout printouts to the test case log file
- case proplists:get_value(default, Opts#opts.verbosity) of
+ case proplists:get_value(default, Opts0#opts.verbosity) of
VLvl when is_integer(VLvl), (?STD_IMPORTANCE < (100-VLvl)) ->
test_server_ctrl:reject_io_reqs(true);
_Lower ->
ok
end,
- test_server_ctrl:multiply_timetraps(Opts#opts.multiply_timetraps),
- test_server_ctrl:scale_timetraps(Opts#opts.scale_timetraps),
+ test_server_ctrl:multiply_timetraps(Opts0#opts.multiply_timetraps),
+ test_server_ctrl:scale_timetraps(Opts0#opts.scale_timetraps),
test_server_ctrl:create_priv_dir(choose_val(
- Opts#opts.create_priv_dir,
+ Opts0#opts.create_priv_dir,
auto_per_run)),
+
+ {ok,LogDir} = ct_logs:get_log_dir(true),
+ {TsCoverInfo,Opts} = maybe_start_cover(Opts0, LogDir),
+
ct_event:notify(#event{name=start_info,
node=node(),
data={NoOfTests,NoOfSuites,NoOfCases}}),
CleanUp = add_jobs(Tests, Skip, Opts, []),
unlink(whereis(test_server_ctrl)),
catch test_server_ctrl:wait_finish(),
+
+ maybe_stop_cover(Opts, TsCoverInfo, LogDir),
+
%% check if last testcase has left a "dead" trace window
%% behind, and if so, kill it
case ct_util:get_testdata(interpret) of
@@ -2235,7 +2305,7 @@ do_run_test(Tests, Skip, Opts) ->
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.
@@ -2256,6 +2326,102 @@ do_run_test(Tests, Skip, Opts) ->
exit(Error)
end.
+maybe_start_cover(Opts=#opts{cover=Cover,cover_stop=CoverStop0},LogDir) ->
+ if Cover == undefined ->
+ {undefined,Opts};
+ true ->
+ case ct_cover:get_spec(Cover) of
+ {error,Reason} ->
+ exit({error,Reason});
+ CoverSpec ->
+ CoverStop =
+ case CoverStop0 of
+ undefined -> true;
+ Stop -> Stop
+ end,
+ start_cover(Opts#opts{coverspec=CoverSpec,
+ cover_stop=CoverStop},
+ LogDir)
+ end
+ end.
+
+start_cover(Opts=#opts{coverspec=CovData,cover_stop=CovStop},LogDir) ->
+ {CovFile,
+ CovNodes,
+ CovImport,
+ _CovExport,
+ #cover{app = CovApp,
+ level = CovLevel,
+ excl_mods = CovExcl,
+ incl_mods = CovIncl,
+ cross = CovCross,
+ src = _CovSrc}} = CovData,
+ ct_logs:log("COVER INFO",
+ "Using cover specification file: ~ts~n"
+ "App: ~w~n"
+ "Cross cover: ~w~n"
+ "Including ~w modules~n"
+ "Excluding ~w modules",
+ [CovFile,CovApp,CovCross,
+ length(CovIncl),length(CovExcl)]),
+
+ %% Tell test_server to print a link in its coverlog
+ %% pointing to the real coverlog which will be written in
+ %% maybe_stop_cover/2
+ test_server_ctrl:cover({log,LogDir}),
+
+ %% Cover compile all modules
+ {ok,TsCoverInfo} = test_server_ctrl:cover_compile(CovApp,CovFile,
+ CovExcl,CovIncl,
+ CovCross,CovLevel,
+ CovStop),
+ ct_logs:log("COVER INFO",
+ "Compilation completed - test_server cover info: ~tp",
+ [TsCoverInfo]),
+
+ %% start cover on specified nodes
+ if (CovNodes /= []) and (CovNodes /= undefined) ->
+ ct_logs:log("COVER INFO",
+ "Nodes included in cover "
+ "session: ~w",
+ [CovNodes]),
+ cover:start(CovNodes);
+ true ->
+ ok
+ end,
+ lists:foreach(
+ fun(Imp) ->
+ case cover:import(Imp) of
+ ok ->
+ ok;
+ {error,Reason} ->
+ ct_logs:log("COVER INFO",
+ "Importing cover data from: ~ts fails! "
+ "Reason: ~p", [Imp,Reason])
+ end
+ end, CovImport),
+ {TsCoverInfo,Opts}.
+
+maybe_stop_cover(_,undefined,_) ->
+ ok;
+maybe_stop_cover(#opts{coverspec=CovData},TsCoverInfo,LogDir) ->
+ {_CovFile,
+ _CovNodes,
+ _CovImport,
+ CovExport,
+ _AppData} = CovData,
+ case CovExport of
+ undefined -> ok;
+ _ ->
+ ct_logs:log("COVER INFO","Exporting cover data to ~tp",[CovExport]),
+ cover:export(CovExport)
+ end,
+ ct_logs:log("COVER INFO","Analysing cover data to ~tp",[LogDir]),
+ test_server_ctrl:cover_analyse(TsCoverInfo,LogDir),
+ ct_logs:log("COVER INFO","Analysis completed.",[]),
+ ok.
+
+
delete_dups([S | Suites]) ->
Suites1 = lists:delete(S, Suites),
[S | delete_dups(Suites1)];
@@ -2266,7 +2432,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
@@ -2526,11 +2692,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 =
@@ -2622,14 +2786,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",
[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;
@@ -2648,12 +2812,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.
@@ -2830,31 +2994,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),
+ _ = if Dir /= Abs ->
+ _ = code:del_path(Dir),
+ _ = code:del_path(Abs),
io:format(user, "Converting ~p to ~p and re-inserting "
"with add_pathz/1~n",
[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),
+ _ = if Dir /= Abs ->
+ _ = code:del_path(Dir),
+ _ = code:del_path(Abs),
io:format(user, "Converting ~p to ~p and re-inserting "
"with add_patha/1~n",
[Dir, Abs]);
true ->
- code:del_path(Dir)
+ _ = code:del_path(Dir)
end,
code:add_patha(Abs)
end || D <- PA],
@@ -2963,6 +3127,10 @@ 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) ->
diff --git a/lib/common_test/src/ct_slave.erl b/lib/common_test/src/ct_slave.erl
index 872c39de04..571958ca03 100644
--- a/lib/common_test/src/ct_slave.erl
+++ b/lib/common_test/src/ct_slave.erl
@@ -1,18 +1,19 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2010-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2010-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
@@ -37,7 +38,7 @@
-record(options, {username, password, boot_timeout, init_timeout,
startup_timeout, startup_functions, monitor_master,
- kill_if_fail, erl_flags, env}).
+ kill_if_fail, erl_flags, env, ssh_port, ssh_opts}).
%%%-----------------------------------------------------------------
%%% @spec start(Node) -> Result
@@ -133,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.
@@ -153,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>
@@ -169,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>
@@ -178,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),
@@ -254,11 +255,13 @@ fetch_options(Options) ->
KillIfFail = get_option_value(kill_if_fail, Options, true),
ErlFlags = get_option_value(erl_flags, Options, []),
EnvVars = get_option_value(env, Options, []),
+ SSHPort = get_option_value(ssh_port, Options, []),
+ SSHOpts = get_option_value(ssh_opts, Options, []),
#options{username=UserName, password=Password,
boot_timeout=BootTimeout, init_timeout=InitTimeout,
startup_timeout=StartupTimeout, startup_functions=StartupFunctions,
monitor_master=Monitor, kill_if_fail=KillIfFail,
- erl_flags=ErlFlags, env=EnvVars}.
+ erl_flags=ErlFlags, env=EnvVars, ssh_port=SSHPort, ssh_opts=SSHOpts}.
% send a message when slave node is started
% @hidden
@@ -312,7 +315,7 @@ enodename(Host, 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
@@ -322,7 +325,7 @@ do_start(Host, Node, Options) ->
Functions
end,
MasterHost = gethostname(),
- if
+ _ = if
MasterHost == Host ->
spawn_local_node(Node, Options);
true->
@@ -356,7 +359,7 @@ do_start(Host, Node, Options) ->
pang->
{error, boot_timeout, ENode}
end,
- case Result of
+ _ = case Result of
{ok, ENode}->
ok;
{error, Timeout, ENode}
@@ -399,27 +402,18 @@ spawn_local_node(Node, Options) ->
Cmd = get_cmd(Node, ErlFlags),
open_port({spawn, Cmd}, [stream,{env,Env}]).
-% start crypto and ssh if not yet started
-check_for_ssh_running() ->
- case application:get_application(crypto) of
- undefined->
- application:start(crypto),
- case application:get_application(ssh) of
- undefined->
- application:start(ssh);
- {ok, ssh}->
- ok
- end;
- {ok, crypto}->
- ok
- end.
-
% spawn node remotely
spawn_remote_node(Host, Node, Options) ->
#options{username=Username,
password=Password,
erl_flags=ErlFlags,
- env=Env} = Options,
+ env=Env,
+ ssh_port=MaybeSSHPort,
+ ssh_opts=SSHOpts} = Options,
+ SSHPort = case MaybeSSHPort of
+ [] -> 22; % Use default SSH port
+ A -> A
+ end,
SSHOptions = case {Username, Password} of
{[], []}->
[];
@@ -427,14 +421,13 @@ spawn_remote_node(Host, Node, Options) ->
[{user, Username}];
{_, _}->
[{user, Username}, {password, Password}]
- end ++ [{silently_accept_hosts, true}],
- check_for_ssh_running(),
- {ok, SSHConnRef} = ssh:connect(atom_to_list(Host), 22, SSHOptions),
+ end ++ [{silently_accept_hosts, true}] ++ SSHOpts,
+ {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),
ssh_connection:exec(SSHConnRef, SSHChannelId, get_cmd(Node, ErlFlags), infinity).
-
ssh_setenv(SSHConnRef, SSHChannelId, [{Var, Value} | Vars])
when is_list(Var), is_list(Value) ->
success = ssh_connection:setenv(SSHConnRef, SSHChannelId,
diff --git a/lib/common_test/src/ct_snmp.erl b/lib/common_test/src/ct_snmp.erl
index 71038bd4f4..2c59b19196 100644
--- a/lib/common_test/src/ct_snmp.erl
+++ b/lib/common_test/src/ct_snmp.erl
@@ -1,25 +1,26 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2012. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2015. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
%%% @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
@@ -30,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>
%%%
@@ -220,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}]
@@ -232,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"),
@@ -310,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);
_ ->
[]
@@ -325,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 ->
@@ -350,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 ->
@@ -377,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
@@ -512,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
@@ -532,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),
@@ -548,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),
@@ -564,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,
@@ -683,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) ->
@@ -691,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) ->
@@ -737,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) ->
@@ -753,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) ->
@@ -768,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).
%%%---------------------------------------------------------------------------
@@ -784,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) ->
@@ -799,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) ->
@@ -814,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) ->
@@ -829,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).
%%%---------------------------------------------------------------------------
@@ -845,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).
%%%---------------------------------------------------------------------------
@@ -860,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 974791bd70..6ab3bf036c 100644
--- a/lib/common_test/src/ct_ssh.erl
+++ b/lib/common_test/src/ct_ssh.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2009-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2009-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -961,8 +962,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);
diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl
index 4092d33bc0..8fb411ec4f 100644
--- a/lib/common_test/src/ct_telnet.erl
+++ b/lib/common_test/src/ct_telnet.erl
@@ -1,101 +1,190 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2015. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
-%%% @doc Common Test specific layer on top of telnet client ct_telnet_client.erl
-%%%
-%%% <p>Use this module to set up telnet connections, send commands and
-%%% perform string matching on the result.
-%%% See the <c>unix_telnet</c> manual page for information about how to use
-%%% ct_telnet, and configure connections, specifically for unix hosts.</p>
-%%% <p>The following default values are defined in ct_telnet:</p>
-%%% <pre>
-%%% Connection timeout = 10 sec (time to wait for connection)
-%%% Command timeout = 10 sec (time to wait for a command to return)
-%%% Max no of reconnection attempts = 3
-%%% Reconnection interval = 5 sek (time to wait in between reconnection attempts)
-%%% Keep alive = true (will send NOP to the server every 10 sec if connection is idle)</pre>
-%%% <p>These parameters can be altered by the user with the following
-%%% configuration term:</p>
-%%% <pre>
-%%% {telnet_settings, [{connect_timeout,Millisec},
-%%% {command_timeout,Millisec},
-%%% {reconnection_attempts,N},
-%%% {reconnection_interval,Millisec},
-%%% {keep_alive,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
-%%% automatically. Note that <c>keep_alive</c> may be specified per connection if
-%%% required. See <c>unix_telnet</c> for details.</p></doc>
-
-%%% @type connection_type() = telnet | ts1 | ts2
-
-%%% @type connection() = handle() |
-%%% {ct:target_name(),connection_type()} | ct:target_name()
-
-%%% @type handle() = ct_gen_conn:handle(). Handle for a
-%%% specific telnet connection.
-
-%%% @type prompt_regexp() = string(). A regular expression which
-%%% matches all possible prompts for a specific type of target. The
-%%% regexp must not have any groups i.e. when matching, re:run/3 shall
-%%% return a list with one single element.
-%%%
-%%% @see unix_telnet
+%% @doc Common Test specific layer on top of telnet client `ct_telnet_client.erl'
+%%
+%% <p>Use this module to set up telnet connections, send commands and
+%% perform string matching on the result.
+%% See the `unix_telnet' manual page for information about how to use
+%% `ct_telnet', and configure connections, specifically for unix hosts.</p>
+%% <p>The following default values are defined in `ct_telnet':</p>
+%% <pre>
+%% Connection timeout = 10 sec (time to wait for connection)
+%% Command timeout = 10 sec (time to wait for a command to return)
+%% Max no of reconnection attempts = 3
+%% Reconnection interval = 5 sek (time to wait in between reconnection attempts)
+%% Keep alive = true (will send NOP to the server every 8 sec if connection is idle)
+%% Polling limit = 0 (max number of times to poll to get a remaining string terminated)
+%% Polling interval = 1 sec (sleep time between polls)</pre>
+%% <p>These parameters can be altered by the user with the following
+%% configuration term:</p>
+%% <pre>
+%% {telnet_settings, [{connect_timeout,Millisec},
+%% {command_timeout,Millisec},
+%% {reconnection_attempts,N},
+%% {reconnection_interval,Millisec},
+%% {keep_alive,Bool},
+%% {poll_limit,N},
+%% {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
+%% automatically. Note that `keep_alive' may be specified per connection if
+%% required. See `unix_telnet' for details.</p>
+%%
+%% == Logging ==
+%%
+%% The default logging behaviour of `ct_telnet' is to print information
+%% to the test case HTML log about performed operations and commands
+%% and their corresponding results. What won't be printed to the HTML log
+%% are text strings sent from the telnet server that are not explicitly
+%% received by means of a `ct_telnet' function such as `expect/3'.
+%% `ct_telnet' may however be configured to use a special purpose event handler,
+%% implemented in `ct_conn_log_h', for logging <b>all</b> telnet traffic.
+%% To use this handler, you need to install a Common Test hook named
+%% `cth_conn_log'. Example (using the test suite info function):
+%%
+%% ```
+%% suite() ->
+%% [{ct_hooks, [{cth_conn_log, [{conn_mod(),hook_options()}]}]}].
+%% '''
+%%
+%% `conn_mod()' is the name of the common_test module implementing
+%% the connection protocol, i.e. `ct_telnet'.
+%%
+%% The `cth_conn_log' hook performs unformatted logging of telnet data to
+%% a separate text file. All telnet communication is captured and printed,
+%% including arbitrary data sent from the server. The link to this text file
+%% can be found on the top of the test case HTML log.
+%%
+%% By default, data for all telnet connections is logged in one common
+%% file (named `default'), which might get messy e.g. if multiple telnet
+%% sessions are running in parallel. It is therefore possible to create a
+%% separate log file for each connection. To configure 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
+%% (see the `open' function below).
+%%
+%% The hook option named `log_type' may be used to change the `cth_conn_log'
+%% behaviour. The default value of this option is `raw', which results in the
+%% behaviour described above. If the value is set to `html', all telnet
+%% communication is printed to the test case HTML log instead.
+%%
+%% All `cth_conn_log' hook options described above can also be specified in
+%% a configuration file with the configuration variable `ct_conn_log'. Example:
+%%
+%% ```
+%% {ct_conn_log, [{ct_telnet,[{log_type,raw},
+%% {hosts,[key_or_name()]}]}]}
+%% '''
+%%
+%% <b>Note</b> that hook options specified in a configuration file
+%% will overwrite any hardcoded hook options in the test suite!
+%%
+%% === Logging example ===
+%%
+%% The following `ct_hooks' statement will cause printing of telnet traffic
+%% to separate logs for the connections named `server1' and `server2'.
+%% Traffic for any other connections will be logged in the default telnet log.
+%%
+%% ```
+%% suite() ->
+%% [{ct_hooks,
+%% [{cth_conn_log, [{ct_telnet,[{hosts,[server1,server2]}]}]}]}].
+%%'''
+%%
+%% As previously explained, the above specification could also be provided
+%% by means of an entry like this in a configuration file:
+%%
+%% ```
+%% {ct_conn_log, [{ct_telnet,[{hosts,[server1,server2]}]}]}.
+%% '''
+%%
+%% in which case the `ct_hooks' statement in the test suite may simply look
+%% like this:
+%%
+%% ```
+%% suite() ->
+%% [{ct_hooks, [{cth_conn_log, []}]}].
+%% '''
+%%
+%% @end
--module(ct_telnet).
+%% @type connection_type() = telnet | ts1 | ts2
--compile(export_all).
+%% @type connection() = handle() |
+%% {ct:target_name(),connection_type()} | ct:target_name()
+
+%% @type handle() = ct_gen_conn:handle(). Handle for a
+%% specific telnet connection.
+
+%% @type prompt_regexp() = string(). A regular expression which
+%% matches all possible prompts for a specific type of target. The
+%% regexp must not have any groups i.e. when matching, re:run/3 shall
+%% return a list with one single element.
+%%
+%% @see unix_telnet
+
+-module(ct_telnet).
-export([open/1, open/2, open/3, open/4, close/1]).
-export([cmd/2, cmd/3, cmdf/3, cmdf/4, get_data/1,
- send/2, sendf/3, expect/2, expect/3]).
+ send/2, send/3, sendf/3, sendf/4,
+ expect/2, expect/3]).
%% Callbacks
-export([init/3,handle_msg/2,reconnect/2,terminate/2]).
%% Tool internals
--export([silent_teln_expect/5, teln_receive_until_prompt/3,
- start_log/1, log/3, cont_log/2, end_log/0,
- try_start_log/1, try_log/3, try_cont_log/2, try_end_log/0]).
-
+-export([silent_teln_expect/6, teln_receive_until_prompt/3,
+ format_data/2]).
+-export([start_gen_log/1, end_gen_log/0, log/3, log/4]).
-define(RECONNS,3).
-define(RECONN_TIMEOUT,5000).
-define(DEFAULT_TIMEOUT,10000).
-define(DEFAULT_PORT,23).
+-define(POLL_LIMIT,0).
+-define(POLL_INTERVAL,1000).
-include("ct_util.hrl").
--record(state,{teln_pid,
+-record(state,{host,
+ port,
+ teln_pid,
prx,
- type,
buffer=[],
prompt=false,
name,
+ type,
target_mod,
keep_alive,
+ poll_limit=?POLL_LIMIT,
+ poll_interval=?POLL_INTERVAL,
extra,
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}
@@ -108,6 +197,7 @@ open(Name) ->
%%% Name = target_name()
%%% ConnType = ct_telnet:connection_type()
%%% Handle = ct_telnet:handle()
+%%% Reason = term()
%%%
%%% @doc Open a telnet connection to the specified target host.
open(Name,ConnType) ->
@@ -137,6 +227,7 @@ open(KeyOrName,ConnType,TargetMod) ->
%%% TargetMod = atom()
%%% Extra = term()
%%% Handle = handle()
+%%% Reason = term()
%%%
%%% @doc Open a telnet connection to the specified target host.
%%%
@@ -160,8 +251,7 @@ open(KeyOrName,ConnType,TargetMod) ->
open(KeyOrName,ConnType,TargetMod,Extra) ->
case ct:get_config({KeyOrName,ConnType}) of
undefined ->
- log(heading(open,{KeyOrName,ConnType}),"Failed: ~p",
- [{not_available,KeyOrName}]),
+ log(undefined,open,"Failed: ~p",[{not_available,KeyOrName}]),
{error,{not_available,KeyOrName,ConnType}};
Addr ->
Addr1 =
@@ -183,15 +273,24 @@ open(KeyOrName,ConnType,TargetMod,Extra) ->
end;
Bool -> Bool
end,
- log(heading(open,{KeyOrName,ConnType}),
- "Opening connection to: ~p",[Addr1]),
- ct_gen_conn:start(KeyOrName,full_addr(Addr1,ConnType),
- {TargetMod,KeepAlive,Extra},?MODULE)
+ log(undefined,open,"Connecting to ~p(~p)",
+ [KeyOrName,Addr1]),
+ Reconnect =
+ case ct:get_config({telnet_settings,reconnection_attempts}) of
+ 0 -> false;
+ _ -> true
+ end,
+ ct_gen_conn:start(full_addr(Addr1,ConnType),
+ {TargetMod,KeepAlive,Extra},
+ ?MODULE, [{name,KeyOrName},
+ {reconnect,Reconnect},
+ {old,true}])
end.
%%%-----------------------------------------------------------------
%%% @spec close(Connection) -> ok | {error,Reason}
-%%% Connection = ct_telnet:connection()
+%%% Connection = ct_telnet:connection()
+%%% Reason = term()
%%%
%%% @doc Close the telnet connection and stop the process managing it.
%%%
@@ -202,9 +301,9 @@ open(KeyOrName,ConnType,TargetMod,Extra) ->
close(Connection) ->
case get_handle(Connection) of
{ok,Pid} ->
- log("ct_telnet:close","Handle: ~w",[Pid]),
+ log(undefined,close,"Connection closed, handle: ~w",[Pid]),
case ct_gen_conn:stop(Pid) of
- {error,{process_down,Pid,noproc}} ->
+ {error,{process_down,Pid,_}} ->
{error,already_closed};
Result ->
Result
@@ -217,47 +316,89 @@ close(Connection) ->
%%% Test suite interface
%%%-----------------------------------------------------------------
%%% @spec cmd(Connection,Cmd) -> {ok,Data} | {error,Reason}
-%%% @equiv cmd(Connection,Cmd,DefaultTimeout)
+%%% @equiv cmd(Connection,Cmd,[])
cmd(Connection,Cmd) ->
- cmd(Connection,Cmd,default).
+ cmd(Connection,Cmd,[]).
%%%-----------------------------------------------------------------
-%%% @spec cmd(Connection,Cmd,Timeout) -> {ok,Data} | {error,Reason}
+%%% @spec cmd(Connection,Cmd,Opts) -> {ok,Data} | {error,Reason}
%%% Connection = ct_telnet:connection()
%%% Cmd = string()
-%%% Timeout = integer()
+%%% Opts = [Opt]
+%%% Opt = {timeout,timeout()} | {newline,boolean()}
%%% Data = [string()]
+%%% Reason = term()
%%% @doc Send a command via telnet and wait for prompt.
-cmd(Connection,Cmd,Timeout) ->
- case get_handle(Connection) of
- {ok,Pid} ->
- call(Pid,{cmd,Cmd,Timeout});
+%%%
+%%% <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).</p>
+%%%
+%%% <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.</p>
+cmd(Connection,Cmd,Opts) when is_list(Opts) ->
+ case check_cmd_opts(Opts) of
+ ok ->
+ case get_handle(Connection) of
+ {ok,Pid} ->
+ call(Pid,{cmd,Cmd,Opts});
+ Error ->
+ Error
+ end;
Error ->
Error
- end.
+ end;
+cmd(Connection,Cmd,Timeout) when is_integer(Timeout); Timeout==default ->
+ %% This clause is kept for backwards compatibility only
+ cmd(Connection,Cmd,[{timeout,Timeout}]).
+
+check_cmd_opts([{timeout,Timeout}|Opts]) when is_integer(Timeout);
+ Timeout==default ->
+ check_cmd_opts(Opts);
+check_cmd_opts([]) ->
+ ok;
+check_cmd_opts(Opts) ->
+ check_send_opts(Opts).
+
%%%-----------------------------------------------------------------
%%% @spec cmdf(Connection,CmdFormat,Args) -> {ok,Data} | {error,Reason}
-%%% @equiv cmdf(Connection,CmdFormat,Args,DefaultTimeout)
+%%% @equiv cmdf(Connection,CmdFormat,Args,[])
cmdf(Connection,CmdFormat,Args) ->
- cmdf(Connection,CmdFormat,Args,default).
+ cmdf(Connection,CmdFormat,Args,[]).
%%%-----------------------------------------------------------------
-%%% @spec cmdf(Connection,CmdFormat,Args,Timeout) -> {ok,Data} | {error,Reason}
+%%% @spec cmdf(Connection,CmdFormat,Args,Opts) -> {ok,Data} | {error,Reason}
%%% Connection = ct_telnet:connection()
%%% CmdFormat = string()
%%% Args = list()
-%%% Timeout = integer()
+%%% Opts = [Opt]
+%%% Opt = {timeout,timeout()} | {newline,boolean()}
%%% Data = [string()]
+%%% Reason = term()
%%% @doc Send a telnet command and wait for prompt
%%% (uses a format string and list of arguments to build the command).
-cmdf(Connection,CmdFormat,Args,Timeout) when is_list(Args) ->
+%%%
+%%% <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,Timeout).
+ cmd(Connection,Cmd,Opts).
%%%-----------------------------------------------------------------
%%% @spec get_data(Connection) -> {ok,Data} | {error,Reason}
%%% Connection = ct_telnet:connection()
%%% Data = [string()]
-%%% @doc Get all data which has been received by the telnet client
-%%% since last command was sent.
+%%% Reason = term()
+%%% @doc Get all data that has been received by the telnet client
+%%% since the last command was sent. Note that only newline terminated
+%%% strings are returned. If the last string received has not yet
+%%% been terminated, the connection may be polled automatically until
+%%% the string is complete. The polling feature is controlled
+%%% by the `poll_limit' and `poll_interval' config values and is
+%%% by default disabled (meaning the function will immediately
+%%% return all complete strings received and save a remaining
+%%% non-terminated string for a later `get_data' call).
get_data(Connection) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -268,30 +409,67 @@ get_data(Connection) ->
%%%-----------------------------------------------------------------
%%% @spec send(Connection,Cmd) -> ok | {error,Reason}
+%%% @equiv send(Connection,Cmd,[])
+send(Connection,Cmd) ->
+ send(Connection,Cmd,[]).
+
+%%%-----------------------------------------------------------------
+%%% @spec send(Connection,Cmd,Opts) -> ok | {error,Reason}
%%% Connection = ct_telnet:connection()
%%% Cmd = string()
+%%% Opts = [Opt]
+%%% Opt = {newline,boolean()}
+%%% Reason = term()
%%% @doc Send a telnet command and return immediately.
%%%
+%%% 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).
+%%%
%%% <p>The resulting output from the command can be read with
%%% <code>get_data/1</code> or <code>expect/2/3</code>.</p>
-send(Connection,Cmd) ->
- case get_handle(Connection) of
- {ok,Pid} ->
- call(Pid,{send,Cmd});
+send(Connection,Cmd,Opts) ->
+ case check_send_opts(Opts) of
+ ok ->
+ case get_handle(Connection) of
+ {ok,Pid} ->
+ call(Pid,{send,Cmd,Opts});
+ Error ->
+ Error
+ end;
Error ->
Error
end.
+check_send_opts([{newline,Bool}|Opts]) when is_boolean(Bool) ->
+ check_send_opts(Opts);
+check_send_opts([Invalid|_]) ->
+ {error,{invalid_option,Invalid}};
+check_send_opts([]) ->
+ ok.
+
+
%%%-----------------------------------------------------------------
%%% @spec sendf(Connection,CmdFormat,Args) -> ok | {error,Reason}
+%%% @equiv sendf(Connection,CmdFormat,Args,[])
+sendf(Connection,CmdFormat,Args) when is_list(Args) ->
+ sendf(Connection,CmdFormat,Args,[]).
+
+%%%-----------------------------------------------------------------
+%%% @spec sendf(Connection,CmdFormat,Args,Opts) -> ok | {error,Reason}
%%% Connection = ct_telnet:connection()
%%% CmdFormat = string()
%%% Args = list()
+%%% Opts = [Opt]
+%%% Opt = {newline,boolean()}
+%%% Reason = term()
%%% @doc Send a telnet command and return immediately (uses a format
%%% string and a list of arguments to build the command).
-sendf(Connection,CmdFormat,Args) when is_list(Args) ->
+sendf(Connection,CmdFormat,Args,Opts) when is_list(Args) ->
Cmd = lists:flatten(io_lib:format(CmdFormat,Args)),
- send(Connection,Cmd).
+ send(Connection,Cmd,Opts).
%%%-----------------------------------------------------------------
%%% @spec expect(Connection,Patterns) -> term()
@@ -309,9 +487,12 @@ expect(Connection,Patterns) ->
%%% Prompt = string()
%%% Tag = term()
%%% Opts = [Opt]
-%%% Opt = {timeout,Timeout} | repeat | {repeat,N} | sequence |
-%%% {halt,HaltPatterns} | ignore_prompt | no_prompt_check
-%%% Timeout = integer()
+%%% Opt = {idle_timeout,IdleTimeout} | {total_timeout,TotalTimeout} |
+%%% repeat | {repeat,N} | sequence | {halt,HaltPatterns} |
+%%% ignore_prompt | no_prompt_check | wait_for_prompt |
+%%% {wait_for_prompt,Prompt}
+%%% IdleTimeout = infinity | integer()
+%%% TotalTimeout = infinity | integer()
%%% N = integer()
%%% HaltPatterns = Patterns
%%% MatchList = [Match]
@@ -322,9 +503,9 @@ expect(Connection,Patterns) ->
%%%
%%% @doc Get data from telnet and wait for the expected pattern.
%%%
-%%% <p><code>Pattern</code> can be a POSIX regular expression. If more
-%%% than one pattern is given, the function returns when the first
-%%% match is found.</p>
+%%% <p><code>Pattern</code> can be a POSIX regular expression. The function
+%%% returns as soon as a pattern has been successfully matched (at least one,
+%%% in the case of multiple patterns).</p>
%%%
%%% <p><code>RxMatch</code> is a list of matched strings. It looks
%%% like this: <code>[FullMatch, SubMatch1, SubMatch2, ...]</code>
@@ -337,15 +518,23 @@ expect(Connection,Patterns) ->
%%% will also include the matched <code>Tag</code>. Else, only
%%% <code>RxMatch</code> is returned.</p>
%%%
-%%% <p>The <code>timeout</code> option indicates that the function
+%%% <p>The <code>idle_timeout</code> option indicates that the function
%%% shall return if the telnet client is idle (i.e. if no data is
-%%% received) for more than <code>Timeout</code> milliseconds. Default
+%%% received) for more than <code>IdleTimeout</code> milliseconds. Default
%%% timeout is 10 seconds.</p>
%%%
-%%% <p>The function will always return when a prompt is found, unless
-%%% any of the <code>ignore_prompt</code> or
-%%% <code>no_prompt_check</code> options are used, in which case it
-%%% will return when a match is found or after a timeout.</p>
+%%% <p>The <code>total_timeout</code> option sets a time limit for
+%%% the complete expect operation. After <code>TotalTimeout</code>
+%%% milliseconds, <code>{error,timeout}</code> is returned. The default
+%%% value is <code>infinity</code> (i.e. no time limit).</p>
+%%%
+%%% <p>The function will return when a prompt is received, even if no
+%%% pattern has yet been matched. In this event,
+%%% <code>{error,{prompt,Prompt}}</code> is returned.
+%%% However, this behaviour may be modified with the
+%%% <code>ignore_prompt</code> or <code>no_prompt_check</code> option, which
+%%% tells <code>expect</code> to return only when a match is found or after a
+%%% timeout.</p>
%%%
%%% <p>If the <code>ignore_prompt</code> option is used,
%%% <code>ct_telnet</code> will ignore any prompt found. This option
@@ -359,6 +548,13 @@ expect(Connection,Patterns) ->
%%% is useful if, for instance, the <code>Pattern</code> itself
%%% matches the prompt.</p>
%%%
+%%% <p>The <code>wait_for_prompt</code> option forces <code>ct_telnet</code>
+%%% to wait until the prompt string has been received before returning
+%%% (even if a pattern has already been matched). This is equal to calling:
+%%% <code>expect(Conn, Patterns++[{prompt,Prompt}], [sequence|Opts])</code>.
+%%% Note that <code>idle_timeout</code> and <code>total_timeout</code>
+%%% may abort the operation of waiting for prompt.</p>
+%%%
%%% <p>The <code>repeat</code> option indicates that the pattern(s)
%%% shall be matched multiple times. If <code>N</code> is given, the
%%% pattern(s) will be matched <code>N</code> times, and the function
@@ -408,9 +604,30 @@ init(Name,{Ip,Port,Type},{TargetMod,KeepAlive,Extra}) ->
Settings ->
set_telnet_defaults(Settings,#state{})
end,
- case catch TargetMod:connect(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} ->
- log(heading(init,{Name,Type}),
+ put({ct_telnet_pid2name,TelnPid},Name),
+ S1 = S0#state{host=Ip,
+ port=Port,
+ teln_pid=TelnPid,
+ name=Name,
+ type=type(Type),
+ target_mod=TargetMod,
+ keep_alive=KeepAlive,
+ extra=Extra,
+ prx=TargetMod:get_prompt_regexp()},
+ log(S1,open,
"Opened telnet connection\n"
"IP: ~p\n"
"Port: ~p\n"
@@ -418,20 +635,20 @@ init(Name,{Ip,Port,Type},{TargetMod,KeepAlive,Extra}) ->
"Reconnection attempts: ~p\n"
"Reconnection interval: ~p\n"
"Connection timeout: ~p\n"
- "Keep alive: ~w",
- [Ip,Port,S0#state.com_to,S0#state.reconns,
- S0#state.reconn_int,S0#state.conn_to,KeepAlive]),
- {ok,TelnPid,S0#state{teln_pid=TelnPid,
- type=type(Type),
- name={Name,Type},
- target_mod=TargetMod,
- keep_alive=KeepAlive,
- extra=Extra,
- prx=TargetMod:get_prompt_regexp()}};
- {'EXIT',Reason} ->
- {error,Reason};
+ "Keep alive: ~w\n"
+ "Poll limit: ~w\n"
+ "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.tcp_nodelay]),
+ {ok,TelnPid,S1};
Error ->
Error
+ catch
+ _:Reason ->
+ {error,Reason}
end.
type(telnet) -> ip;
@@ -447,87 +664,113 @@ set_telnet_defaults([{reconnection_interval,RInt}|Ss],S) ->
set_telnet_defaults(Ss,S#state{reconn_int=RInt});
set_telnet_defaults([{keep_alive,_}|Ss],S) ->
set_telnet_defaults(Ss,S);
+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) ->
- log(heading(set_telnet_defaults,{telnet_settings,Unknown}),
- "Bad element in telnet_settings: ~p",[Unknown]),
+ force_log(S,error,
+ "Bad element in telnet_settings: ~p",[Unknown]),
set_telnet_defaults(Ss,S);
set_telnet_defaults([],S) ->
S.
%% @hidden
-handle_msg({cmd,Cmd,Timeout},State) ->
- try_start_log(heading(cmd,State#state.name)),
- try_cont_log("Cmd: ~p", [Cmd]),
- debug_cont_log("Throwing Buffer:",[]),
+handle_msg({cmd,Cmd,Opts},State) ->
+ start_gen_log(heading(cmd,State#state.name)),
+ log(State,cmd,"Cmd: ~p",[Cmd]),
+
+ %% whatever is in the buffer from previous operations
+ %% will be ignored as we go ahead with this telnet cmd
+
+ debug_cont_gen_log("Throwing Buffer:",[]),
debug_log_lines(State#state.buffer),
- case {State#state.type,State#state.prompt} of
- {ts,_} ->
- silent_teln_expect(State#state.teln_pid,
+
+ _ = case {State#state.type,State#state.prompt} of
+ {ts,_} ->
+ silent_teln_expect(State#state.name,
+ State#state.teln_pid,
State#state.buffer,
prompt,
State#state.prx,
- [{timeout,2000}]);
- {ip,false} ->
- silent_teln_expect(State#state.teln_pid,
+ [{idle_timeout,2000}]);
+ {ip,false} ->
+ silent_teln_expect(State#state.name,
+ State#state.teln_pid,
State#state.buffer,
prompt,
State#state.prx,
- [{timeout,200}]);
+ [{idle_timeout,200}]);
{ip,true} ->
ok
end,
- TO = if Timeout == default -> State#state.com_to;
- true -> Timeout
+ TO = case proplists:get_value(timeout,Opts,default) of
+ default -> State#state.com_to;
+ Timeout -> Timeout
end,
+ Newline = proplists:get_value(newline,Opts,true),
{Return,NewBuffer,Prompt} =
- case teln_cmd(State#state.teln_pid, Cmd, State#state.prx, TO) of
+ case teln_cmd(State#state.teln_pid, Cmd, State#state.prx,
+ Newline, TO) of
{ok,Data,_PromptType,Rest} ->
- try_cont_log("Return: ~p", [{ok,Data}]),
+ log(State,recv,"Return: ~p",[{ok,Data}]),
{{ok,Data},Rest,true};
Error ->
- Retry = {retry,{Error,State#state.name,State#state.teln_pid,
- {cmd,Cmd,TO}}},
- try_cont_log("Return: ~p", [Error]),
+ Retry = {retry,{Error,
+ {State#state.name,
+ State#state.type},
+ State#state.teln_pid,
+ {cmd,Cmd,Opts}}},
+ log(State,recv,"Return: ~p",[Error]),
{Retry,[],false}
end,
- try_end_log(),
+ end_gen_log(),
{Return,State#state{buffer=NewBuffer,prompt=Prompt}};
-handle_msg({send,Cmd},State) ->
- try_log(heading(send,State#state.name),"Cmd: ~p",[Cmd]),
- debug_cont_log("Throwing Buffer:",[]),
+handle_msg({send,Cmd,Opts},State) ->
+ start_gen_log(heading(send,State#state.name)),
+ log(State,send,"Sending: ~p",[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.teln_pid,
+ silent_teln_expect(State#state.name,
+ State#state.teln_pid,
State#state.buffer,
prompt,
State#state.prx,
- [{timeout,2000}]);
+ [{idle_timeout,2000}]);
{ip,false} ->
- silent_teln_expect(State#state.teln_pid,
+ silent_teln_expect(State#state.name,
+ State#state.teln_pid,
State#state.buffer,
prompt,
State#state.prx,
- [{timeout,200}]);
+ [{idle_timeout,200}]);
{ip,true} ->
ok
end,
- ct_telnet_client:send_data(State#state.teln_pid,Cmd),
+ Newline = proplists:get_value(newline,Opts,true),
+ ct_telnet_client:send_data(State#state.teln_pid,Cmd,Newline),
+ end_gen_log(),
{ok,State#state{buffer=[],prompt=false}};
handle_msg(get_data,State) ->
- try_start_log(heading(get_data,State#state.name)),
- {ok,Data,Buffer} = teln_get_all_data(State#state.teln_pid,
- State#state.prx,
- State#state.buffer,
- [],[]),
- try_cont_log("Return: ~p",[{ok,Data}]),
- try_end_log(),
+ start_gen_log(heading(get_data,State#state.name)),
+ 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}]),
+ end_gen_log(),
{{ok,Data},State#state{buffer=Buffer}};
handle_msg({expect,Pattern,Opts},State) ->
- try_start_log(heading(expect,State#state.name)),
- try_cont_log("Expect: ~p\nOpts=~p\n",[Pattern,Opts]),
+ start_gen_log(heading(expect,State#state.name)),
+ log(State,expect,"Expect: ~p\nOpts = ~p\n",[Pattern,Opts]),
{Return,NewBuffer,Prompt} =
- case teln_expect(State#state.teln_pid,
+ case teln_expect(State#state.name,
+ State#state.teln_pid,
State#state.buffer,
Pattern,
State#state.prx,
@@ -536,22 +779,23 @@ handle_msg({expect,Pattern,Opts},State) ->
P = check_if_prompt_was_reached(Data,[]),
{{ok,Data},Rest,P};
{ok,Data,HaltReason,Rest} ->
- force_cont_log("HaltReason: ~p",
- [HaltReason]),
+ force_log(State,expect,"HaltReason: ~p",[HaltReason]),
P = check_if_prompt_was_reached(Data,HaltReason),
{{ok,Data,HaltReason},Rest,P};
{error,Reason,Rest} ->
- force_cont_log("Expect failed\n~p",[{error,Reason}]),
+ force_log(State,expect,"Expect failed\n~p",[{error,Reason}]),
P = check_if_prompt_was_reached([],Reason),
{{error,Reason},Rest,P};
{error,Reason} ->
- force_cont_log("Expect failed\n~p",[{error,Reason}]),
+ force_log(State,expect,"Expect failed\n~p",[{error,Reason}]),
P = check_if_prompt_was_reached([],Reason),
{{error,Reason},[],P}
end,
- try_end_log(),
+ end_gen_log(),
Return1 = case Return of
- {error,_} -> {retry,{Return,State#state.name,
+ {error,_} -> {retry,{Return,
+ {State#state.name,
+ State#state.type},
State#state.teln_pid,
{expect,Pattern,Opts}}};
_ -> Return
@@ -562,18 +806,29 @@ handle_msg({expect,Pattern,Opts},State) ->
%% @hidden
reconnect({Ip,Port,_Type},State) ->
reconnect(Ip,Port,State#state.reconns,State).
-reconnect(Ip,Port,N,State=#state{target_mod=TargetMod,
+reconnect(Ip,Port,N,State=#state{name=Name,
+ target_mod=TargetMod,
keep_alive=KeepAlive,
extra=Extra,
conn_to=ConnTo,
- reconn_int=ReconnInt}) ->
- case TargetMod:connect(Ip,Port,ConnTo,KeepAlive,Extra) of
- {ok, NewPid} ->
+ 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}};
Error when N==0 ->
Error;
_Error ->
- log("Reconnect failed!","Retries left: ~w",[N]),
+ log(State,reconnect,"Reconnect failed!","Retries left: ~w",[N]),
timer:sleep(ReconnInt),
reconnect(Ip,Port,N-1,State)
end.
@@ -581,11 +836,9 @@ reconnect(Ip,Port,N,State=#state{target_mod=TargetMod,
%% @hidden
terminate(TelnPid,State) ->
- log(heading(terminate,State#state.name),
- "Closing telnet connection.\nId: ~w",
- [TelnPid]),
- ct_telnet_client:close(TelnPid).
-
+ Result = ct_telnet_client:close(TelnPid),
+ log(State,close,"Telnet connection for ~w closed.",[TelnPid]),
+ Result.
%%%=================================================================
%%% Internal function
@@ -637,105 +890,148 @@ check_if_prompt_was_reached(Data,_) when is_list(Data) ->
check_if_prompt_was_reached(_,_) ->
false.
-%tc(Fun) ->
-% Before = erlang:now(),
-% Val = Fun(),
-% After = erlang:now(),
-% {now_diff(After, Before), Val}.
-%now_diff({A2, B2, C2}, {A1, B1, C1}) ->
-% ((A2-A1)*1000000 + B2-B1)*1000000 + C2-C1.
+%%%-----------------------------------------------------------------
+%%% Functions for logging ct_telnet reports and telnet data
-heading(Function,Name) ->
- io_lib:format("~w:~w ~p",[?MODULE,Function,Name]).
+heading(Action,undefined) ->
+ io_lib:format("~w ~w",[?MODULE,Action]);
+heading(Action,Name) ->
+ io_lib:format("~w ~w for ~p",[?MODULE,Action,Name]).
-%%% @hidden
-%% Functions for regular (unconditional) logging, to be
-%% used during connect, reconnect, disconnect etc.
-log(Heading,Str,Args) ->
- ct_gen_conn:log(Heading,Str,Args).
-%%% @hidden
-start_log(Heading) ->
- ct_gen_conn:start_log(Heading).
-cont_log(Str,Args) ->
- ct_gen_conn:cont_log(Str,Args).
-end_log() ->
- ct_gen_conn:end_log().
+force_log(State,Action,String,Args) ->
+ log(State,Action,String,Args,true).
+%%%-----------------------------------------------------------------
%%% @hidden
-%% Functions for conditional logging, to be used by
-%% cmd, send, receive, expect etc (this output may be
-%% silenced by user).
-try_start_log(Heading) ->
- do_try_log(start_log,[Heading]).
-%%% @hidden
-try_end_log() ->
- do_try_log(end_log,[]).
+log(State,Action,String,Args) when is_record(State, state) ->
+ log(State,Action,String,Args,false);
+log(Name,Action,String,Args) when is_atom(Name) ->
+ log(#state{name=Name},Action,String,Args,false);
+log(TelnPid,Action,String,Args) when is_pid(TelnPid) ->
+ log(#state{teln_pid=TelnPid},Action,String,Args,false).
+%%%-----------------------------------------------------------------
%%% @hidden
-try_log(Heading,Str,Args) ->
- do_try_log(log,[Heading,Str,Args]).
+log(undefined,String,Args) ->
+ log(#state{},undefined,String,Args,false);
+log(Name,String,Args) when is_atom(Name) ->
+ log(#state{name=Name},undefined,String,Args,false);
+log(TelnPid,String,Args) when is_pid(TelnPid) ->
+ log(#state{teln_pid=TelnPid},undefined,String,Args).
+%%%-----------------------------------------------------------------
%%% @hidden
-try_cont_log(Str,Args) ->
- do_try_log(cont_log,[Str,Args]).
+log(#state{name=Name,teln_pid=TelnPid,host=Host,port=Port},
+ Action,String,Args,ForcePrint) ->
+ Name1 = if Name == undefined -> get({ct_telnet_pid2name,TelnPid});
+ true -> Name
+ end,
+ Silent = get(silent),
+
+ if Action == general_io ->
+ case ct_util:get_testdata({cth_conn_log,?MODULE}) of
+ HookMode when HookMode /= undefined, HookMode /= silent,
+ Silent /= true ->
+ error_logger:info_report(#conn_log{header=false,
+ client=self(),
+ conn_pid=TelnPid,
+ address={Host,Port},
+ name=Name1,
+ action=Action,
+ module=?MODULE},
+ {String,Args});
+ _ -> %% hook inactive or silence requested
+ ok
+ end;
+ true ->
+ if Action == open; Action == close; Action == reconnect;
+ Action == info; Action == error ->
+ ct_gen_conn:log(heading(Action,Name1),String,Args);
+
+ ForcePrint == false ->
+ case ct_util:is_silenced(telnet) of
+ true ->
+ ok;
+ false ->
+ ct_gen_conn:cont_log(String,Args)
+ end;
+
+ ForcePrint == true ->
+ case ct_util:is_silenced(telnet) of
+ true ->
+ %% call log/3 now instead of cont_log/2 since
+ %% start_gen_log/1 will not have been previously
+ %% called
+ ct_gen_conn:log(heading(Action,Name1),String,Args);
+ false ->
+ ct_gen_conn:cont_log(String,Args)
+ end
+ end
+ end.
+
+%%%-----------------------------------------------------------------
%%% @hidden
-do_try_log(Func,Args) ->
+start_gen_log(Heading) ->
%% check if output is suppressed
case ct_util:is_silenced(telnet) of
- true ->
- ok;
- false ->
- apply(ct_gen_conn,Func,Args)
+ true -> ok;
+ false -> ct_gen_conn:start_log(Heading)
end.
+%%%-----------------------------------------------------------------
%%% @hidden
-%% Functions that will force printout even if ct_telnet
-%% output has been silenced, to be used for error printouts.
-force_cont_log(Str,Args) ->
+end_gen_log() ->
+ %% check if output is suppressed
case ct_util:is_silenced(telnet) of
- true ->
- %% call log/3 now instead of cont_log/2 since
- %% start_log/1 will not have been previously called
- log("ct_telnet info",Str,Args);
- false ->
- cont_log(Str,Args)
+ true -> ok;
+ false -> ct_gen_conn:end_log()
end.
%%% @hidden
%% Debug printouts.
-debug_cont_log(Str,Args) ->
+debug_cont_gen_log(Str,Args) ->
Old = put(silent,true),
- cont_log(Str,Args),
+ ct_gen_conn:cont_log(Str,Args),
put(silent,Old).
-
+%% Log callback - called from the error handler process
+format_data(_How,{String,Args}) ->
+ io_lib:format(String,Args).
%%%=================================================================
%%% Abstraction layer on top of ct_telnet_client.erl
-teln_cmd(Pid,Cmd,Prx,Timeout) ->
- ct_telnet_client:send_data(Pid,Cmd),
+teln_cmd(Pid,Cmd,Prx,Newline,Timeout) ->
+ ct_telnet_client:send_data(Pid,Cmd,Newline),
teln_receive_until_prompt(Pid,Prx,Timeout).
-
-teln_get_all_data(Pid,Prx,Data,Acc,LastLine) ->
- case check_for_prompt(Prx,lists:reverse(LastLine) ++ Data) of
+teln_get_all_data(State=#state{teln_pid=Pid,prx=Prx},Data,Acc,LastLine,Polls) ->
+ case check_for_prompt(Prx,LastLine++Data) of
{prompt,Lines,_PromptType,Rest} ->
- teln_get_all_data(Pid,Prx,Rest,[Lines|Acc],[]);
+ teln_get_all_data(State,Rest,[Lines|Acc],[],State#state.poll_limit);
{noprompt,Lines,LastLine1} ->
case ct_telnet_client:get_data(Pid) of
+ {ok,[]} when LastLine1 /= [], Polls > 0 ->
+ %% No more data from server but the last string is not
+ %% a complete line (maybe because of a slow connection),
+ timer:sleep(State#state.poll_interval),
+ NewPolls = if Polls == infinity -> infinity;
+ true -> Polls-1
+ end,
+ teln_get_all_data(State,[],[Lines|Acc],LastLine1,NewPolls);
{ok,[]} ->
- {ok,lists:reverse(lists:append([Lines|Acc])),
- lists:reverse(LastLine1)};
+ {ok,lists:reverse(lists:append([Lines|Acc])),LastLine1};
{ok,Data1} ->
- teln_get_all_data(Pid,Prx,Data1,[Lines|Acc],LastLine1)
+ teln_get_all_data(State,Data1,[Lines|Acc],LastLine1,
+ State#state.poll_limit)
end
end.
%% Expect options record
-record(eo,{teln_pid,
prx,
- timeout,
+ idle_timeout,
+ total_timeout,
haltpatterns=[],
seq=false,
repeat=false,
@@ -746,15 +1042,13 @@ teln_get_all_data(Pid,Prx,Data,Acc,LastLine) ->
%% @doc Externally the silent_teln_expect function shall only be used
%% by the TargetModule, i.e. the target specific module which
%% implements connect/2 and get_prompt_regexp/0.
-silent_teln_expect(Pid,Data,Pattern,Prx,Opts) ->
+silent_teln_expect(Name,Pid,Data,Pattern,Prx,Opts) ->
Old = put(silent,true),
- try_cont_log("silent_teln_expect/5, Pattern = ~p",[Pattern]),
- Result = teln_expect(Pid,Data,Pattern,Prx,Opts),
- try_cont_log("silent_teln_expect -> ~p\n",[Result]),
+ Result = teln_expect(Name,Pid,Data,Pattern,Prx,Opts),
put(silent,Old),
Result.
-%% teln_expect/5
+%% teln_expect/6
%%
%% This function implements the expect functionality over telnet. In
%% general there are three possible ways to go:
@@ -766,7 +1060,7 @@ silent_teln_expect(Pid,Data,Pattern,Prx,Opts) ->
%% condition is fullfilled.
%% 3b) Repeat (sequence): 2) is repeated either N times or until a
%% halt condition is fullfilled.
-teln_expect(Pid,Data,Pattern0,Prx,Opts) ->
+teln_expect(Name,Pid,Data,Pattern0,Prx,Opts) ->
HaltPatterns =
case get_ignore_prompt(Opts) of
true ->
@@ -776,21 +1070,31 @@ teln_expect(Pid,Data,Pattern0,Prx,Opts) ->
end,
PromptCheck = get_prompt_check(Opts),
- Seq = get_seq(Opts),
- Pattern = convert_pattern(Pattern0,Seq),
- Timeout = get_timeout(Opts),
+ {WaitForPrompt,Pattern1,Opts1} = wait_for_prompt(Pattern0,Opts),
+
+ Seq = get_seq(Opts1),
+ Pattern2 = convert_pattern(Pattern1,Seq),
+ {IdleTimeout,TotalTimeout} = get_timeouts(Opts1),
EO = #eo{teln_pid=Pid,
prx=Prx,
- timeout=Timeout,
+ idle_timeout=IdleTimeout,
+ total_timeout=TotalTimeout,
seq=Seq,
haltpatterns=HaltPatterns,
prompt_check=PromptCheck},
- case get_repeat(Opts) of
+ case get_repeat(Opts1) of
false ->
- case teln_expect1(Data,Pattern,[],EO) of
+ case teln_expect1(Name,Pid,Data,Pattern2,[],EO) of
+ {ok,Matched,Rest} when WaitForPrompt ->
+ case lists:reverse(Matched) of
+ [{prompt,_},Matched1] ->
+ {ok,Matched1,Rest};
+ [{prompt,_}|Matched1] ->
+ {ok,lists:reverse(Matched1),Rest}
+ end;
{ok,Matched,Rest} ->
{ok,Matched,Rest};
{halt,Why,Rest} ->
@@ -800,7 +1104,7 @@ teln_expect(Pid,Data,Pattern0,Prx,Opts) ->
end;
N ->
EO1 = EO#eo{repeat=N},
- repeat_expect(Data,Pattern,[],EO1)
+ repeat_expect(Name,Pid,Data,Pattern2,[],EO1)
end.
convert_pattern(Pattern,Seq)
@@ -822,11 +1126,22 @@ rm_dupl([P|Ps],Acc) ->
rm_dupl([],Acc) ->
lists:reverse(Acc).
-get_timeout(Opts) ->
- case lists:keysearch(timeout,1,Opts) of
- {value,{timeout,T}} -> T;
- false -> ?DEFAULT_TIMEOUT
- end.
+get_timeouts(Opts) ->
+ {case lists:keysearch(idle_timeout,1,Opts) of
+ {value,{_,T}} ->
+ T;
+ false ->
+ %% this check is for backwards compatibility (pre CT v1.8)
+ case lists:keysearch(timeout,1,Opts) of
+ {value,{_,T}} -> T;
+ false -> ?DEFAULT_TIMEOUT
+ end
+ end,
+ case lists:keysearch(total_timeout,1,Opts) of
+ {value,{_,T}} -> T;
+ false -> infinity
+ end}.
+
get_repeat(Opts) ->
case lists:keysearch(repeat,1,Opts) of
{value,{repeat,N}} when is_integer(N) ->
@@ -853,25 +1168,70 @@ get_ignore_prompt(Opts) ->
get_prompt_check(Opts) ->
not lists:member(no_prompt_check,Opts).
+wait_for_prompt(Pattern, Opts) ->
+ case lists:member(wait_for_prompt, Opts) of
+ true ->
+ wait_for_prompt1(prompt, Pattern,
+ lists:delete(wait_for_prompt,Opts));
+ false ->
+ case proplists:get_value(wait_for_prompt, Opts) of
+ undefined ->
+ {false,Pattern,Opts};
+ PromptStr ->
+ wait_for_prompt1({prompt,PromptStr}, Pattern,
+ proplists:delete(wait_for_prompt,Opts))
+ end
+ end.
+
+wait_for_prompt1(Prompt, [Ch|_] = Pattern, Opts) when is_integer(Ch) ->
+ wait_for_prompt2(Prompt, [Pattern], Opts);
+wait_for_prompt1(Prompt, Pattern, Opts) when is_list(Pattern) ->
+ wait_for_prompt2(Prompt, Pattern, Opts);
+wait_for_prompt1(Prompt, Pattern, Opts) ->
+ wait_for_prompt2(Prompt, [Pattern], Opts).
+
+wait_for_prompt2(Prompt, Pattern, Opts) ->
+ Pattern1 = case lists:reverse(Pattern) of
+ [prompt|_] -> Pattern;
+ [{prompt,_}|_] -> Pattern;
+ _ -> Pattern ++ [Prompt]
+ end,
+ Opts1 = case lists:member(sequence, Opts) of
+ true -> Opts;
+ false -> [sequence|Opts]
+ end,
+ {true,Pattern1,Opts1}.
+
%% Repeat either single or sequence. All match results are accumulated
%% and returned when a halt condition is fulllfilled.
-repeat_expect(Rest,_Pattern,Acc,#eo{repeat=0}) ->
+repeat_expect(_Name,_Pid,Rest,_Pattern,Acc,#eo{repeat=0}) ->
{ok,lists:reverse(Acc),done,Rest};
-repeat_expect(Data,Pattern,Acc,EO) ->
- case teln_expect1(Data,Pattern,[],EO) of
+repeat_expect(Name,Pid,Data,Pattern,Acc,EO) ->
+ case teln_expect1(Name,Pid,Data,Pattern,[],EO) of
{ok,Matched,Rest} ->
EO1 = EO#eo{repeat=EO#eo.repeat-1},
- repeat_expect(Rest,Pattern,[Matched|Acc],EO1);
+ repeat_expect(Name,Pid,Rest,Pattern,[Matched|Acc],EO1);
{halt,Why,Rest} ->
{ok,lists:reverse(Acc),Why,Rest};
{error,Reason} ->
{error,Reason}
end.
-teln_expect1(Data,Pattern,Acc,EO) ->
- ExpectFun = case EO#eo.seq of
- true -> fun() -> seq_expect(Data,Pattern,Acc,EO) end;
- false -> fun() -> one_expect(Data,Pattern,EO) end
+teln_expect1(Name,Pid,Data,Pattern,Acc,EO=#eo{idle_timeout=IdleTO,
+ total_timeout=TotalTO}) ->
+ %% TotalTO is a float value in this loop (unless it's 'infinity'),
+ %% but an integer value will be passed to the other functions
+ 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)
+ end;
+ false -> fun() ->
+ one_expect(Name,Pid,Data,Pattern,EOMod)
+ end
end,
case ExpectFun() of
{match,Match,Rest} ->
@@ -880,20 +1240,45 @@ teln_expect1(Data,Pattern,Acc,EO) ->
{halt,Why,Rest};
NotFinished ->
%% Get more data
- Fun = fun() -> get_data1(EO#eo.teln_pid) end,
- case ct_gen_conn:do_within_time(Fun, EO#eo.timeout) of
- {error,Reason} ->
+ Fun = fun() -> get_data1(EOMod#eo.teln_pid) end,
+ BreakAfter = if TotalTO < IdleTO ->
+ %% use the integer value
+ EOMod#eo.total_timeout;
+ true ->
+ IdleTO
+ end,
+ case timer:tc(ct_gen_conn, do_within_time, [Fun,BreakAfter]) of
+ {_,{error,Reason}} ->
%% A timeout will occur when the telnet connection
- %% is idle for EO#eo.timeout milliseconds.
+ %% is idle for EO#eo.idle_timeout milliseconds.
{error,Reason};
- {ok,Data1} ->
+ {_,{ok,Data1}} when TotalTO == infinity ->
case NotFinished of
{nomatch,Rest} ->
%% One expect
- teln_expect1(Rest++Data1,Pattern,[],EO);
+ teln_expect1(Name,Pid,Rest++Data1,
+ Pattern,[],EOMod);
{continue,Patterns1,Acc1,Rest} ->
%% Sequence
- teln_expect1(Rest++Data1,Patterns1,Acc1,EO)
+ teln_expect1(Name,Pid,Rest++Data1,
+ Patterns1,Acc1,EOMod)
+ end;
+ {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
end
end
end.
@@ -909,51 +1294,49 @@ get_data1(Pid) ->
%% 1) Single expect.
%% First the whole data chunk is searched for a prompt (to avoid doing
%% a regexp match for the prompt at each line).
-%% If we are searching for anyting else, the datachunk is split into
+%% If we are searching for anything else, the datachunk is split into
%% lines and each line is matched against each pattern.
%% one_expect: split data chunk at prompts
-one_expect(Data,Pattern,EO) when EO#eo.prompt_check==false ->
+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]),
- one_expect1(Data,Pattern,[],EO#eo{found_prompt=false});
-one_expect(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
{prompt,UptoPrompt,PromptType,Rest} ->
case Pattern of
[Prompt] when Prompt==prompt; Prompt=={prompt,PromptType} ->
%% Only searching for prompt
- log_lines(UptoPrompt),
- try_cont_log("<b>PROMPT:</b> ~ts", [PromptType]),
+ log_lines(Name,Pid,UptoPrompt),
+ log(name_or_pid(Name,Pid),"PROMPT: ~ts",[PromptType]),
{match,{prompt,PromptType},Rest};
[{prompt,_OtherPromptType}] ->
- %% Only searching for one specific prompt, not thisone
- log_lines(UptoPrompt),
+ %% Only searching for one specific prompt, not this one
+ log_lines(Name,Pid,UptoPrompt),
{nomatch,Rest};
_ ->
- one_expect1(UptoPrompt,Pattern,Rest,
+ one_expect1(Name,Pid,UptoPrompt,Pattern,Rest,
EO#eo{found_prompt=PromptType})
end;
noprompt ->
case Pattern of
[Prompt] when Prompt==prompt; element(1,Prompt)==prompt ->
%% Only searching for prompt
- LastLine = log_lines_not_last(Data),
+ LastLine = log_lines_not_last(Name,Pid,Data),
{nomatch,LastLine};
_ ->
- one_expect1(Data,Pattern,[],EO#eo{found_prompt=false})
+ one_expect1(Name,Pid,Data,Pattern,[],
+ EO#eo{found_prompt=false})
end
end.
-remove_zero(List) ->
- [Ch || Ch <- List, Ch=/=0, Ch=/=13].
-
%% one_expect1: split data chunk at lines
-one_expect1(Data,Pattern,Rest,EO) ->
- case match_lines(Data,Pattern,EO) of
+one_expect1(Name,Pid,Data,Pattern,Rest,EO) ->
+ case match_lines(Name,Pid,Data,Pattern,EO) of
{match,Match,MatchRest} ->
{match,Match,MatchRest++Rest};
{nomatch,prompt} ->
- one_expect(Rest,Pattern,EO);
+ one_expect(Name,Pid,Rest,Pattern,EO);
{nomatch,NoMatchRest} ->
{nomatch,NoMatchRest++Rest};
{halt,Why,HaltRest} ->
@@ -970,77 +1353,77 @@ one_expect1(Data,Pattern,Rest,EO) ->
%% searching for the next pattern in the list.
%% seq_expect: Split data chunk at prompts
-seq_expect(Data,[],Acc,_EO) ->
+seq_expect(_Name,_Pid,Data,[],Acc,_EO) ->
{match,lists:reverse(Acc),Data};
-seq_expect([],Patterns,Acc,_EO) ->
+seq_expect(_Name,_Pid,[],Patterns,Acc,_EO) ->
{continue,Patterns,lists:reverse(Acc),[]};
-seq_expect(Data,Patterns,Acc,EO) when EO#eo.prompt_check==false ->
- seq_expect1(Data,Patterns,Acc,[],EO#eo{found_prompt=false});
-seq_expect(Data,Patterns,Acc,EO) ->
+seq_expect(Name,Pid,Data,Patterns,Acc,EO) when EO#eo.prompt_check==false ->
+ seq_expect1(Name,Pid,Data,Patterns,Acc,[],EO#eo{found_prompt=false});
+seq_expect(Name,Pid,Data,Patterns,Acc,EO) ->
case match_prompt(Data,EO#eo.prx) of
{prompt,UptoPrompt,PromptType,Rest} ->
- seq_expect1(UptoPrompt,Patterns,Acc,Rest,
+ seq_expect1(Name,Pid,UptoPrompt,Patterns,Acc,Rest,
EO#eo{found_prompt=PromptType});
noprompt ->
- seq_expect1(Data,Patterns,Acc,[],EO#eo{found_prompt=false})
+ seq_expect1(Name,Pid,Data,Patterns,Acc,[],EO#eo{found_prompt=false})
end.
%% seq_expect1: For one prompt-chunk, match each pattern - line by
%% line if it is other than the prompt we are seaching for.
-seq_expect1(Data,[prompt|Patterns],Acc,Rest,EO) ->
+seq_expect1(Name,Pid,Data,[prompt|Patterns],Acc,Rest,EO) ->
case EO#eo.found_prompt of
false ->
- LastLine = log_lines_not_last(Data),
+ LastLine = log_lines_not_last(Name,Pid,Data),
%% Rest==[] because no prompt is found
{continue,[prompt|Patterns],Acc,LastLine};
PromptType ->
- log_lines(Data),
- try_cont_log("<b>PROMPT:</b> ~ts", [PromptType]),
- seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO)
+ log_lines(Name,Pid,Data),
+ log(name_or_pid(Name,Pid),"PROMPT: ~ts",[PromptType]),
+ seq_expect(Name,Pid,Rest,Patterns,[{prompt,PromptType}|Acc],EO)
end;
-seq_expect1(Data,[{prompt,PromptType}|Patterns],Acc,Rest,EO) ->
+seq_expect1(Name,Pid,Data,[{prompt,PromptType}|Patterns],Acc,Rest,EO) ->
case EO#eo.found_prompt of
false ->
- LastLine = log_lines_not_last(Data),
+ LastLine = log_lines_not_last(Name,Pid,Data),
%% Rest==[] because no prompt is found
{continue,[{prompt,PromptType}|Patterns],Acc,LastLine};
PromptType ->
- log_lines(Data),
- try_cont_log("<b>PROMPT:</b> ~ts", [PromptType]),
- seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO);
+ log_lines(Name,Pid,Data),
+ log(name_or_pid(Name,Pid),"PROMPT: ~ts", [PromptType]),
+ seq_expect(Name,Pid,Rest,Patterns,[{prompt,PromptType}|Acc],EO);
_OtherPromptType ->
- log_lines(Data),
- seq_expect(Rest,[{prompt,PromptType}|Patterns],Acc,EO)
+ log_lines(Name,Pid,Data),
+ seq_expect(Name,Pid,Rest,[{prompt,PromptType}|Patterns],Acc,EO)
end;
-seq_expect1(Data,[Pattern|Patterns],Acc,Rest,EO) ->
- case match_lines(Data,[Pattern],EO) of
+seq_expect1(Name,Pid,Data,[Pattern|Patterns],Acc,Rest,EO) ->
+ case match_lines(Name,Pid,Data,[Pattern],EO) of
{match,Match,MatchRest} ->
- seq_expect1(MatchRest,Patterns,[Match|Acc],Rest,EO);
+ seq_expect1(Name,Pid,MatchRest,Patterns,[Match|Acc],Rest,EO);
{nomatch,prompt} ->
- seq_expect(Rest,[Pattern|Patterns],Acc,EO);
+ seq_expect(Name,Pid,Rest,[Pattern|Patterns],Acc,EO);
{nomatch,NoMatchRest} when Rest==[] ->
%% The data did not end with a prompt
{continue,[Pattern|Patterns],Acc,NoMatchRest};
{halt,Why,HaltRest} ->
{halt,Why,HaltRest++Rest}
end;
-seq_expect1(Data,[],Acc,Rest,_EO) ->
+seq_expect1(_Name,_Pid,Data,[],Acc,Rest,_EO) ->
{match,lists:reverse(Acc),Data++Rest}.
%% Split prompt-chunk at lines
-match_lines(Data,Patterns,EO) ->
+match_lines(Name,Pid,Data,Patterns,EO) ->
FoundPrompt = EO#eo.found_prompt,
case one_line(Data,[]) of
{noline,Rest} when FoundPrompt=/=false ->
%% This is the line including the prompt
- case match_line(Rest,Patterns,FoundPrompt,EO) of
+ case match_line(Name,Pid,Rest,Patterns,FoundPrompt,EO) of
nomatch ->
{nomatch,prompt};
{Tag,Match} ->
{Tag,Match,[]}
end;
{noline,Rest} when EO#eo.prompt_check==false ->
- case match_line(Rest,Patterns,false,EO) of
+ case match_line(Name,Pid,Rest,Patterns,false,EO) of
nomatch ->
{nomatch,Rest};
{Tag,Match} ->
@@ -1049,9 +1432,9 @@ match_lines(Data,Patterns,EO) ->
{noline,Rest} ->
{nomatch,Rest};
{Line,Rest} ->
- case match_line(Line,Patterns,false,EO) of
+ case match_line(Name,Pid,Line,Patterns,false,EO) of
nomatch ->
- match_lines(Rest,Patterns,EO);
+ match_lines(Name,Pid,Rest,Patterns,EO);
{Tag,Match} ->
{Tag,Match,Rest}
end
@@ -1059,43 +1442,43 @@ match_lines(Data,Patterns,EO) ->
%% For one line, match each pattern
-match_line(Line,Patterns,FoundPrompt,EO) ->
- match_line(Line,Patterns,FoundPrompt,EO,match).
-
-match_line(Line,[prompt|Patterns],false,EO,RetTag) ->
- match_line(Line,Patterns,false,EO,RetTag);
-match_line(Line,[prompt|_Patterns],FoundPrompt,_EO,RetTag) ->
- try_cont_log(" ~ts", [Line]),
- try_cont_log("<b>PROMPT:</b> ~ts", [FoundPrompt]),
+match_line(Name,Pid,Line,Patterns,FoundPrompt,EO) ->
+ match_line(Name,Pid,Line,Patterns,FoundPrompt,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) ->
+ log(name_or_pid(Name,Pid)," ~ts",[Line]),
+ log(name_or_pid(Name,Pid),"PROMPT: ~ts",[FoundPrompt]),
{RetTag,{prompt,FoundPrompt}};
-match_line(Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_EO,RetTag)
+match_line(Name,Pid,Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_EO,RetTag)
when PromptType==FoundPrompt ->
- try_cont_log(" ~ts", [Line]),
- try_cont_log("<b>PROMPT:</b> ~ts", [FoundPrompt]),
+ log(name_or_pid(Name,Pid)," ~ts",[Line]),
+ log(name_or_pid(Name,Pid),"PROMPT: ~ts",[FoundPrompt]),
{RetTag,{prompt,FoundPrompt}};
-match_line(Line,[{prompt,PromptType}|Patterns],FoundPrompt,EO,RetTag)
+match_line(Name,Pid,Line,[{prompt,PromptType}|Patterns],FoundPrompt,EO,RetTag)
when PromptType=/=FoundPrompt ->
- match_line(Line,Patterns,FoundPrompt,EO,RetTag);
-match_line(Line,[{Tag,Pattern}|Patterns],FoundPrompt,EO,RetTag) ->
+ 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
nomatch ->
- match_line(Line,Patterns,FoundPrompt,EO,RetTag);
+ match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag);
{match,Match} ->
- try_cont_log("<b>MATCH:</b> ~ts", [Line]),
+ log(name_or_pid(Name,Pid),"MATCH: ~ts",[Line]),
{RetTag,{Tag,Match}}
end;
-match_line(Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) ->
+match_line(Name,Pid,Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) ->
case re:run(Line,Pattern,[{capture,all,list}]) of
nomatch ->
- match_line(Line,Patterns,FoundPrompt,EO,RetTag);
+ match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag);
{match,Match} ->
- try_cont_log("<b>MATCH:</b> ~ts", [Line]),
+ log(name_or_pid(Name,Pid),"MATCH: ~ts",[Line]),
{RetTag,Match}
end;
-match_line(Line,[],FoundPrompt,EO,match) ->
- match_line(Line,EO#eo.haltpatterns,FoundPrompt,EO,halt);
-match_line(Line,[],_FoundPrompt,_EO,halt) ->
- try_cont_log(" ~ts", [Line]),
+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) ->
+ log(name_or_pid(Name,Pid)," ~ts",[Line]),
nomatch.
one_line([$\n|Rest],Line) ->
@@ -1111,26 +1494,29 @@ one_line([],Line) ->
debug_log_lines(String) ->
Old = put(silent,true),
- log_lines(String),
+ log_lines(undefined,undefined,String),
put(silent,Old).
-log_lines(String) ->
- case log_lines_not_last(String) of
+log_lines(Name,Pid,String) ->
+ case log_lines_not_last(Name,Pid,String) of
[] ->
ok;
LastLine ->
- try_cont_log(" ~ts", [LastLine])
+ log(name_or_pid(Name,Pid)," ~ts",[LastLine])
end.
-log_lines_not_last(String) ->
+log_lines_not_last(Name,Pid,String) ->
case add_tabs(String,[],[]) of
{[],LastLine} ->
LastLine;
{String1,LastLine} ->
- try_cont_log("~ts",[String1]),
+ log(name_or_pid(Name,Pid),"~ts",[String1]),
LastLine
end.
+name_or_pid(undefined,Pid) -> Pid;
+name_or_pid(Name,_) -> Name.
+
add_tabs([0|Rest],Acc,LastLine) ->
add_tabs(Rest,Acc,LastLine);
add_tabs([$\r|Rest],Acc,LastLine) ->
@@ -1145,8 +1531,6 @@ add_tabs([],[],LastLine) ->
{[],lists:reverse(LastLine)}.
-
-
%%% @hidden
teln_receive_until_prompt(Pid,Prx,Timeout) ->
Fun = fun() -> teln_receive_until_prompt(Pid,Prx,[],[]) end,
@@ -1154,7 +1538,7 @@ teln_receive_until_prompt(Pid,Prx,Timeout) ->
teln_receive_until_prompt(Pid,Prx,Acc,LastLine) ->
{ok,Data} = ct_telnet_client:get_data(Pid),
- case check_for_prompt(Prx,LastLine ++ Data) of
+ case check_for_prompt(Prx,LastLine++Data) of
{prompt,Lines,PromptType,Rest} ->
Return = lists:reverse(lists:append([Lines|Acc])),
{ok,Return,PromptType,Rest};
@@ -1174,8 +1558,10 @@ check_for_prompt(Prx,Data) ->
split_lines(String) ->
split_lines(String,[],[]).
-split_lines([$\n|Rest],Line,Lines) ->
+split_lines([$\n|Rest],Line,Lines) when Line /= [] ->
split_lines(Rest,[],[lists:reverse(Line)|Lines]);
+split_lines([$\n|Rest],[],Lines) ->
+ split_lines(Rest,[],Lines);
split_lines([$\r|Rest],Line,Lines) ->
split_lines(Rest,Line,Lines);
split_lines([0|Rest],Line,Lines) ->
diff --git a/lib/common_test/src/ct_telnet_client.erl b/lib/common_test/src/ct_telnet_client.erl
index 2cbcba9c77..5df7e279ac 100644
--- a/lib/common_test/src/ct_telnet_client.erl
+++ b/lib/common_test/src/ct_telnet_client.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -32,12 +33,14 @@
-module(ct_telnet_client).
--export([open/1, open/2, open/3, open/4, close/1]).
--export([send_data/2, get_data/1]).
+%%-define(debug, true).
+
+-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).
-define(OPEN_TIMEOUT,10000).
--define(IDLE_TIMEOUT,10000).
+-define(IDLE_TIMEOUT,8000).
%% telnet control characters
-define(SE, 240).
@@ -64,20 +67,26 @@
-define(TERMINAL_TYPE, 24).
-define(WINDOW_SIZE, 31).
--record(state,{get_data, keep_alive=true}).
+-record(state,{conn_name, get_data, keep_alive=true, log_pos=1}).
+
+open(Server, ConnName) ->
+ open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT, true, false, ConnName).
-open(Server) ->
- open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT, true).
+open(Server, Port, ConnName) ->
+ open(Server, Port, ?OPEN_TIMEOUT, true, false, ConnName).
-open(Server, Port) ->
- open(Server, Port, ?OPEN_TIMEOUT, true).
+open(Server, Port, Timeout, ConnName) ->
+ open(Server, Port, Timeout, true, false, ConnName).
-open(Server, Port, Timeout) ->
- open(Server, Port, Timeout, true).
+open(Server, Port, Timeout, KeepAlive, ConnName) ->
+ open(Server, Port, Timeout, KeepAlive, false, ConnName).
-open(Server, Port, Timeout, KeepAlive) ->
+open(Server, Port, Timeout, KeepAlive, NoDelay, ConnName) ->
Self = self(),
- Pid = spawn(fun() -> init(Self, Server, Port, Timeout, KeepAlive) end),
+ Pid = spawn(fun() ->
+ init(Self, Server, Port, Timeout,
+ KeepAlive, NoDelay, ConnName)
+ end),
receive
{open,Pid} ->
{ok,Pid};
@@ -86,29 +95,37 @@ open(Server, Port, Timeout, KeepAlive) ->
end.
close(Pid) ->
- Pid ! close.
+ Pid ! {close,self()},
+ receive closed -> ok
+ after 5000 -> ok
+ end.
send_data(Pid, Data) ->
- Pid ! {send_data, Data++"\n"},
+ send_data(Pid, Data, true).
+send_data(Pid, Data, true) ->
+ send_data(Pid, Data++"\n", false);
+send_data(Pid, Data, false) ->
+ Pid ! {send_data, Data},
ok.
get_data(Pid) ->
- Pid ! {get_data, self()},
+ Pid ! {get_data,self()},
receive
{data,Data} ->
- {ok, Data}
+ {ok,Data}
end.
-
%%%-----------------------------------------------------------------
%%% Internal functions
-init(Parent, Server, Port, Timeout, KeepAlive) ->
- case gen_tcp:connect(Server, Port, [list,{packet,0}], Timeout) of
+init(Parent, Server, Port, Timeout, KeepAlive, NoDelay, ConnName) ->
+ case gen_tcp:connect(Server, Port, [list,{packet,0},{nodelay,NoDelay}], Timeout) of
{ok,Sock} ->
- dbg("Connected to: ~p (port: ~w, keep_alive: ~w)\n", [Server,Port,KeepAlive]),
- send([?IAC,?DO,?SUPPRESS_GO_AHEAD], Sock),
+ dbg("~p connected to: ~p (port: ~w, keep_alive: ~w)\n",
+ [ConnName,Server,Port,KeepAlive]),
+ send([?IAC,?DO,?SUPPRESS_GO_AHEAD], Sock, ConnName),
Parent ! {open,self()},
- loop(#state{get_data=10, keep_alive=KeepAlive}, Sock, []),
+ loop(#state{conn_name=ConnName, get_data=10, keep_alive=KeepAlive},
+ Sock, []),
gen_tcp:close(Sock);
Error ->
Parent ! {Error,self()}
@@ -118,6 +135,13 @@ loop(State, Sock, Acc) ->
receive
{tcp_closed,_} ->
dbg("Connection closed\n", []),
+ Data = lists:reverse(lists:append(Acc)),
+ dbg("Printing queued messages: ~tp",[Data]),
+ ct_telnet:log(State#state.conn_name,
+ general_io, "~ts",
+ [lists:sublist(Data,
+ State#state.log_pos,
+ length(Data))]),
receive
{get_data,Pid} ->
Pid ! closed
@@ -125,11 +149,11 @@ loop(State, Sock, Acc) ->
ok
end;
{tcp,_,Msg0} ->
- dbg("tcp msg: ~p~n",[Msg0]),
+ dbg("rcv tcp msg: ~tp~n",[Msg0]),
Msg = check_msg(Sock,Msg0,[]),
loop(State, Sock, [Msg | Acc]);
{send_data,Data} ->
- send(Data, Sock),
+ send(Data, Sock, State#state.conn_name),
loop(State, Sock, Acc);
{get_data,Pid} ->
NewState =
@@ -144,56 +168,111 @@ loop(State, Sock, Acc) ->
end;
_ ->
Data = lists:reverse(lists:append(Acc)),
- dbg("get_data ~p\n",[Data]),
+ Len = length(Data),
+ dbg("get_data ~tp\n",[Data]),
+ ct_telnet:log(State#state.conn_name,
+ general_io, "~ts",
+ [lists:sublist(Data,
+ State#state.log_pos,
+ Len)]),
Pid ! {data,Data},
- State
+ State#state{log_pos = 1}
end,
loop(NewState, Sock, []);
{get_data_delayed,Pid} ->
NewState =
case State of
#state{keep_alive = true, get_data = 0} ->
- if Acc == [] -> send([?IAC,?NOP], Sock);
+ dbg("sending NOP\n",[]),
+ if Acc == [] -> send([?IAC,?NOP], Sock,
+ State#state.conn_name);
true -> ok
end,
State#state{get_data=10};
_ ->
State
end,
- NewAcc =
+ {NewAcc,Pos} =
case erlang:is_process_alive(Pid) of
- true ->
+ true when Acc /= [] ->
Data = lists:reverse(lists:append(Acc)),
- dbg("get_data_delayed ~p\n",[Data]),
+ Len = length(Data),
+ dbg("get_data_delayed ~tp\n",[Data]),
+ ct_telnet:log(State#state.conn_name,
+ general_io, "~ts",
+ [lists:sublist(Data,
+ State#state.log_pos,
+ Len)]),
Pid ! {data,Data},
- [];
+ {[],1};
+ true when Acc == [] ->
+ dbg("get_data_delayed nodata\n",[]),
+ Pid ! {data,[]},
+ {[],1};
false ->
- Acc
+ {Acc,NewState#state.log_pos}
end,
- loop(NewState, Sock, NewAcc);
- close ->
+ loop(NewState#state{log_pos=Pos}, Sock, NewAcc);
+ {close,Pid} ->
dbg("Closing connection\n", []),
+ if Acc == [] ->
+ ok;
+ true ->
+ Data = lists:reverse(lists:append(Acc)),
+ dbg("Printing queued messages: ~tp",[Data]),
+ ct_telnet:log(State#state.conn_name,
+ general_io, "~ts",
+ [lists:sublist(Data,
+ State#state.log_pos,
+ length(Data))])
+ end,
gen_tcp:close(Sock),
- ok
+ Pid ! closed
after wait(State#state.keep_alive,?IDLE_TIMEOUT) ->
- if
- Acc == [] -> send([?IAC,?NOP], Sock);
- true -> ok
- end,
- loop(State, Sock, Acc)
+ dbg("idle timeout\n",[]),
+ Data = lists:reverse(lists:append(Acc)),
+ case Data of
+ [] ->
+ dbg("sending NOP\n",[]),
+ send([?IAC,?NOP], Sock, State#state.conn_name),
+ loop(State, Sock, Acc);
+ _ when State#state.log_pos == length(Data)+1 ->
+ loop(State, Sock, Acc);
+ _ ->
+ dbg("idle timeout, printing ~tp\n",[Data]),
+ Len = length(Data),
+ ct_telnet:log(State#state.conn_name,
+ general_io, "~ts",
+ [lists:sublist(Data,
+ State#state.log_pos,
+ Len)]),
+ loop(State#state{log_pos = Len+1}, Sock, Acc)
+ end
end.
wait(true, Time) -> Time;
wait(false, _) -> infinity.
-send(Data, Sock) ->
+send(Data, Sock, ConnName) ->
case Data of
[?IAC|_] = Cmd ->
- cmd_dbg(Cmd);
+ cmd_dbg("Sending",Cmd),
+ try io_lib:format("[~w] ~w", [?MODULE,Data]) of
+ Str ->
+ ct_telnet:log(ConnName, general_io, Str, [])
+ catch
+ _:_ -> ok
+ end;
_ ->
- dbg("Sending: ~p\n", [Data])
+ dbg("Sending: ~tp\n", [Data]),
+ try io_lib:format("[~w] ~ts", [?MODULE,Data]) of
+ Str ->
+ ct_telnet:log(ConnName, general_io, Str, [])
+ catch
+ _:_ -> ok
+ end
end,
- gen_tcp:send(Sock, Data),
+ ok = gen_tcp:send(Sock, Data),
ok.
%% [IAC,IAC] = buffer data value 255
@@ -204,9 +283,8 @@ check_msg(Sock, [?IAC,?IAC | T], Acc) ->
check_msg(Sock, [?IAC | Cs], Acc) ->
case get_cmd(Cs) of
{Cmd,Cs1} ->
- dbg("Got ", []),
- cmd_dbg(Cmd),
- respond_cmd(Cmd, Sock),
+ cmd_dbg("Got",Cmd),
+ ok = respond_cmd(Cmd, Sock),
check_msg(Sock, Cs1, Acc);
error ->
Acc
@@ -224,12 +302,12 @@ check_msg(_Sock, [], Acc) ->
respond_cmd([?WILL,?ECHO], Sock) ->
R = [?IAC,?DO,?ECHO],
- cmd_dbg(R),
+ cmd_dbg("Responding",R),
gen_tcp:send(Sock, R);
respond_cmd([?DO,?ECHO], Sock) ->
R = [?IAC,?WILL,?ECHO],
- cmd_dbg(R),
+ cmd_dbg("Responding",R),
gen_tcp:send(Sock, R);
%% Answers from server
@@ -249,12 +327,12 @@ respond_cmd([?WONT | _Opt], _Sock) -> % server ack?
respond_cmd([?WILL,Opt], Sock) ->
R = [?IAC,?DONT,Opt],
- cmd_dbg(R),
+ cmd_dbg("Responding",R),
gen_tcp:send(Sock, R);
respond_cmd([?DO | Opt], Sock) ->
R = [?IAC,?WONT | Opt],
- cmd_dbg(R),
+ cmd_dbg("Responding",R),
gen_tcp:send(Sock, R);
%% Commands without options (which we ignore)
@@ -290,13 +368,14 @@ get_subcmd([Opt | Rest], Acc) ->
get_subcmd(Rest, [Opt | Acc]).
-ifdef(debug).
-dbg(_Str,_Args) ->
- io:format(_Str,_Args).
+dbg(Str,Args) ->
+ TS = timestamp(),
+ io:format("[~p ct_telnet_client, ~s]\n" ++ Str,[self(),TS|Args]).
-cmd_dbg(_Cmd) ->
- case _Cmd of
+cmd_dbg(Prefix,Cmd) ->
+ case Cmd of
[?IAC|Cmd1] ->
- cmd_dbg(Cmd1);
+ cmd_dbg(Prefix,Cmd1);
[Ctrl|Opts] ->
CtrlStr =
case Ctrl of
@@ -312,15 +391,23 @@ cmd_dbg(_Cmd) ->
[Opt] -> Opt;
_ -> Opts
end,
- io:format("~ts(~w): ~w\n", [CtrlStr,Ctrl,Opts1]);
+ dbg("~ts: ~ts(~w): ~w\n", [Prefix,CtrlStr,Ctrl,Opts1]);
Any ->
- io:format("Unexpected in cmd_dbg:~n~w~n",[Any])
+ dbg("Unexpected in cmd_dbg:~n~w~n",[Any])
end.
+timestamp() ->
+ {MS,S,US} = os:timestamp(),
+ {{Year,Month,Day}, {Hour,Min,Sec}} =
+ calendar:now_to_local_time({MS,S,US}),
+ MilliSec = trunc(US/1000),
+ lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B "
+ "~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0B",
+ [Year,Month,Day,Hour,Min,Sec,MilliSec])).
-else.
dbg(_Str,_Args) ->
ok.
-cmd_dbg(_Cmd) ->
+cmd_dbg(_Prefix,_Cmd) ->
ok.
-endif.
diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl
index c07ea323e6..991abb0666 100644
--- a/lib/common_test/src/ct_testspec.erl
+++ b/lib/common_test/src/ct_testspec.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -27,6 +28,8 @@
collect_tests_from_list/2, collect_tests_from_list/3,
collect_tests_from_file/2, collect_tests_from_file/3]).
+-export([testspec_rec2list/1, testspec_rec2list/2]).
+
-include("ct_util.hrl").
-define(testspec_fields, record_info(fields, testspec)).
@@ -67,13 +70,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}}) ->
@@ -100,7 +107,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.
@@ -137,7 +144,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.
@@ -153,7 +160,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
@@ -180,18 +187,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
@@ -207,11 +229,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) ->
@@ -428,6 +450,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
@@ -973,7 +996,8 @@ add_tests([Term={Tag,all_nodes,Data}|Ts],Spec) ->
should_be_added(Tag,Node,Data,Spec)],
add_tests(Tests++Ts,Spec);
invalid -> % ignore term
- add_tests(Ts,Spec)
+ Unknown = Spec#testspec.unknown,
+ add_tests(Ts,Spec#testspec{unknown=Unknown++[Term]})
end;
%% create one test entry per node in Nodes and reinsert
add_tests([{Tag,[],Data}|Ts],Spec) ->
@@ -1001,7 +1025,8 @@ add_tests([Term={Tag,NodeOrOther,Data}|Ts],Spec) ->
handle_data(Tag,Node,Data,Spec),
add_tests(Ts,mod_field(Spec,Tag,NodeIxData));
invalid -> % ignore term
- add_tests(Ts,Spec)
+ Unknown = Spec#testspec.unknown,
+ add_tests(Ts,Spec#testspec{unknown=Unknown++[Term]})
end;
false ->
add_tests([{Tag,all_nodes,{NodeOrOther,Data}}|Ts],Spec)
@@ -1012,13 +1037,15 @@ add_tests([Term={Tag,Data}|Ts],Spec) ->
valid ->
add_tests([{Tag,all_nodes,Data}|Ts],Spec);
invalid ->
- add_tests(Ts,Spec)
+ Unknown = Spec#testspec.unknown,
+ add_tests(Ts,Spec#testspec{unknown=Unknown++[Term]})
end;
%% some other data than a tuple
add_tests([Other|Ts],Spec) ->
case get(relaxed) of
- true ->
- add_tests(Ts,Spec);
+ true ->
+ Unknown = Spec#testspec.unknown,
+ add_tests(Ts,Spec#testspec{unknown=Unknown++[Other]});
false ->
throw({error,{undefined_term_in_spec,Other}})
end;
@@ -1119,9 +1146,11 @@ 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 == stylesheet;
- Tag == verbosity; Tag == silent_connections ->
+ Tag == basic_html; Tag == esc_chars;
+ Tag == label; Tag == auto_compile;
+ Tag == abort_if_missing_suites;
+ Tag == stylesheet; Tag == verbosity;
+ Tag == silent_connections ->
lists:keymember(ref2node(Node,Spec#testspec.nodes),1,
read_field(Spec,Tag)) == false;
%% for terms *with* possible duplicates
@@ -1148,6 +1177,24 @@ per_node([N|Ns],Tag,Data,Refs) ->
per_node([],_,_,_) ->
[].
+%% Change the testspec record "back" to a list of tuples
+testspec_rec2list(Rec) ->
+ {Terms,_} = lists:mapfoldl(fun(unknown, Pos) ->
+ {element(Pos, Rec),Pos+1};
+ (F, Pos) ->
+ {{F,element(Pos, Rec)},Pos+1}
+ end,2,?testspec_fields),
+ lists:flatten(Terms).
+
+%% Extract one or more values from a testspec record and
+%% return the result as a list of tuples
+testspec_rec2list(Field, Rec) when is_atom(Field) ->
+ [Term] = testspec_rec2list([Field], Rec),
+ Term;
+testspec_rec2list(Fields, Rec) ->
+ Terms = testspec_rec2list(Rec),
+ [{Field,proplists:get_value(Field, Terms)} || Field <- Fields].
+
%% read the value for FieldName in record Rec#testspec
read_field(Rec, FieldName) ->
catch lists:foldl(fun(F, Pos) when F == FieldName ->
@@ -1231,7 +1278,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.
@@ -1256,7 +1303,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 ->
@@ -1286,7 +1333,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.
@@ -1343,9 +1390,9 @@ 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)
+ insert_in_order({Suite,SkipGroups},Suites0,replace)
end.
skip_cases(Node,Dir,Suite,Cases,Cmt,Tests,false) when is_list(Cases) ->
@@ -1354,7 +1401,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}) ->
@@ -1375,32 +1422,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) ->
@@ -1475,6 +1545,8 @@ valid_terms() ->
{logopts,3},
{basic_html,2},
{basic_html,3},
+ {esc_chars,2},
+ {esc_chars,3},
{verbosity,2},
{verbosity,3},
{silent_connections,2},
@@ -1496,6 +1568,8 @@ valid_terms() ->
{include,3},
{auto_compile,2},
{auto_compile,3},
+ {abort_if_missing_suites,2},
+ {abort_if_missing_suites,3},
{stylesheet,2},
{stylesheet,3},
{suites,3},
diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl
index 68e76c2396..82a8743cf0 100644
--- a/lib/common_test/src/ct_util.erl
+++ b/lib/common_test/src/ct_util.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -37,7 +38,7 @@
save_suite_data_async/3, save_suite_data_async/2,
read_suite_data/1,
delete_suite_data/0, delete_suite_data/1, match_delete_suite_data/1,
- delete_testdata/0, delete_testdata/1,
+ delete_testdata/0, delete_testdata/1, match_delete_testdata/1,
set_testdata/1, get_testdata/1, get_testdata/2,
set_testdata_async/1, update_testdata/2, update_testdata/3,
set_verbosity/1, get_verbosity/1]).
@@ -77,6 +78,8 @@
-record(suite_data, {key,name,value}).
%%%-----------------------------------------------------------------
+start() ->
+ start(normal, ".", ?default_verbosity).
%%% @spec start(Mode) -> Pid | exit(Error)
%%% Mode = normal | interactive
%%% Pid = pid()
@@ -91,9 +94,6 @@
%%% <code>ct_util_server</code>.</p>
%%%
%%% @see ct
-start() ->
- start(normal, ".", ?default_verbosity).
-
start(LogDir) when is_list(LogDir) ->
start(normal, LogDir, ?default_verbosity);
start(Mode) ->
@@ -131,14 +131,14 @@ do_start(Parent, Mode, LogDir, Verbosity) ->
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 +169,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
@@ -187,6 +187,7 @@ do_start(Parent, Mode, LogDir, Verbosity) ->
false ->
ok
end,
+
{StartTime,TestLogDir} = ct_logs:init(Mode, Verbosity),
ct_event:notify(#event{name=test_start,
@@ -194,16 +195,30 @@ 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} ->
- ct_logs:tc_print('Suite Callback',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,[]),
self() ! {{stop,{self(),{user_error,CTHReason}}},
{Parent,make_ref()}}
catch
_:CTHReason ->
- ct_logs:tc_print('Suite Callback',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,[]),
self() ! {{stop,{self(),{user_error,CTHReason}}},
{Parent,make_ref()}}
end,
@@ -213,7 +228,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
@@ -256,6 +272,9 @@ delete_testdata() ->
delete_testdata(Key) ->
call({delete_testdata, Key}).
+match_delete_testdata(KeyPat) ->
+ call({match_delete_testdata, KeyPat}).
+
update_testdata(Key, Fun) ->
update_testdata(Key, Fun, []).
@@ -286,14 +305,23 @@ get_start_dir() ->
%% handle verbosity outside ct_util_server (let the client read
%% the verbosity table) to avoid possible deadlock situations
set_verbosity(Elem = {_Category,_Level}) ->
- ets:insert(?verbosity_table, Elem),
- ok.
+ try ets:insert(?verbosity_table, Elem) of
+ _ ->
+ ok
+ catch
+ _:Reason ->
+ {error,Reason}
+ end.
+
get_verbosity(Category) ->
- case ets:lookup(?verbosity_table, Category) of
+ try ets:lookup(?verbosity_table, Category) of
[{Category,Level}] ->
Level;
_ ->
undefined
+ catch
+ _:Reason ->
+ {error,Reason}
end.
loop(Mode,TestData,StartDir) ->
@@ -338,7 +366,25 @@ loop(Mode,TestData,StartDir) ->
{{delete_testdata,Key},From} ->
TestData1 = lists:keydelete(Key,1,TestData),
return(From,ok),
- loop(From,TestData1,StartDir);
+ loop(From,TestData1,StartDir);
+ {{match_delete_testdata,{Key1,Key2}},From} ->
+ %% handles keys with 2 elements
+ TestData1 =
+ lists:filter(fun({Key,_}) when not is_tuple(Key) ->
+ true;
+ ({Key,_}) when tuple_size(Key) =/= 2 ->
+ true;
+ ({{_,KeyB},_}) when Key1 == '_' ->
+ KeyB =/= Key2;
+ ({{KeyA,_},_}) when Key2 == '_' ->
+ KeyA =/= Key1;
+ (_) when Key1 == '_' ; Key2 == '_' ->
+ false;
+ (_) ->
+ true
+ end, TestData),
+ return(From,ok),
+ loop(From,TestData1,StartDir);
{{set_testdata,New = {Key,_Val}},From} ->
TestData1 = lists:keydelete(Key,1,TestData),
return(From,ok),
@@ -358,9 +404,18 @@ loop(Mode,TestData,StartDir) ->
TestData1 =
case lists:keysearch(Key,1,TestData) of
{value,{Key,Val}} ->
- NewVal = Fun(Val),
- return(From,NewVal),
- [{Key,NewVal}|lists:keydelete(Key,1,TestData)];
+ try Fun(Val) of
+ '$delete' ->
+ return(From,deleted),
+ lists:keydelete(Key,1,TestData);
+ NewVal ->
+ return(From,NewVal),
+ [{Key,NewVal}|lists:keydelete(Key,1,TestData)]
+ catch
+ _:Error ->
+ return(From,{error,Error}),
+ TestData
+ end;
_ ->
case lists:member(create,Opts) of
true ->
@@ -383,23 +438,43 @@ loop(Mode,TestData,StartDir) ->
return(From,StartDir),
loop(From,TestData,StartDir);
{{stop,Info},From} ->
+ test_server_io:reset_state(),
+ {MiscIoName,MiscIoDivider,MiscIoFooter} =
+ proplists:get_value(misc_io_log,TestData),
+ {ok,MiscIoFd} = file:open(MiscIoName,
+ [append,{encoding,utf8}]),
+ io:put_chars(MiscIoFd, MiscIoDivider),
+ test_server_io:set_fd(unexpected_io, MiscIoFd),
+
Time = calendar:local_time(),
ct_event:sync_notify(#event{name=test_done,
node=node(),
data=Time}),
- Callbacks = ets:lookup_element(?suite_table,
- ct_hooks,
- #suite_data.value),
+ Callbacks =
+ try ets:lookup_element(?suite_table,
+ ct_hooks,
+ #suite_data.value) of
+ CTHMods -> CTHMods
+ catch
+ %% this is because ct_util failed in init
+ error:badarg -> []
+ end,
ct_hooks:terminate(Callbacks),
+
close_connections(ets:tab2list(?conn_table)),
ets:delete(?conn_table),
ets:delete(?board_table),
ets:delete(?suite_table),
ets:delete(?verbosity_table),
+
+ io:put_chars(MiscIoFd, "\n</pre>\n"++MiscIoFooter),
+ test_server_io:stop([unexpected_io]),
+ test_server_io:finish(),
+
ct_logs:close(Info, StartDir),
ct_event:stop(),
ct_config:stop(),
- file:set_cwd(StartDir),
+ ok = file:set_cwd(StartDir),
return(From, Info);
{Ref, _Msg} when is_reference(Ref) ->
%% This clause is used when doing cast operations.
@@ -412,6 +487,8 @@ 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,
@@ -419,8 +496,8 @@ loop(Mode,TestData,StartDir) ->
"Connection process died: "
"Pid: ~w, Address: ~p, "
"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),
@@ -429,7 +506,7 @@ 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),
+ ok = file:set_cwd(StartDir),
exit(Reason)
end
end.
@@ -670,8 +747,14 @@ reset_silent_connections() ->
%%% @see ct
stop(Info) ->
case whereis(ct_util_server) of
- undefined -> ok;
- _ -> call({stop,Info})
+ undefined ->
+ ok;
+ CtUtilPid ->
+ Ref = monitor(process, CtUtilPid),
+ call({stop,Info}),
+ receive
+ {'DOWN',Ref,_,_,_} -> ok
+ end
end.
%%%-----------------------------------------------------------------
@@ -953,10 +1036,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).
@@ -992,7 +1077,7 @@ 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, "%"),
diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl
index 7c01e17c36..d7efa26863 100644
--- a/lib/common_test/src/ct_util.hrl
+++ b/lib/common_test/src/ct_util.hrl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -36,6 +37,7 @@
logdir=["."],
logopts=[],
basic_html=[],
+ esc_chars=[],
verbosity=[],
silent_connections=[],
cover=[],
@@ -48,12 +50,14 @@
release_shell=false,
include=[],
auto_compile=[],
+ abort_if_missing_suites=[],
stylesheet=[],
multiply_timetraps=[],
scale_timetraps=[],
create_priv_dir=[],
alias=[],
tests=[],
+ unknown=[],
merge_tests=true}).
-record(cover, {app=none,
@@ -79,4 +83,10 @@
-define(tablesorter_script, "jquery.tablesorter.min.js").
%% Logging information for error handler
--record(conn_log, {client, name, address, action, module}).
+-record(conn_log, {header=true,
+ client,
+ name,
+ address,
+ conn_pid,
+ action,
+ module}).
diff --git a/lib/common_test/src/ct_webtool.erl b/lib/common_test/src/ct_webtool.erl
new file mode 100644
index 0000000000..87af442fd3
--- /dev/null
+++ b/lib/common_test/src/ct_webtool.erl
@@ -0,0 +1,1213 @@
+%%
+%% %CopyrightBegin%
+%%
+%% 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.
+%% 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_webtool).
+-behaviour(gen_server).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% The general idea is: %%
+%% %%
+%% %%
+%% 1. Scan through the path for *.tool files and find all the web %%
+%% based tools. Query each tool for configuration data. %%
+%% 2. Add Alias for Erlscript and html for each tool to %%
+%% the webserver configuration data. %%
+%% 3. Start the webserver. %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%% API functions
+-export([start/0, start/2, stop/0]).
+
+%% Starting Webtool from a shell script
+-export([script_start/0, script_start/1]).
+
+%% Web api
+-export([started_tools/2, toolbar/2, start_tools/2, stop_tools/2]).
+
+%% API against other tools
+-export([is_localhost/0]).
+
+%% Debug export s
+-export([get_tools1/1]).
+-export([debug/1, stop_debug/0, debug_app/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-include_lib("kernel/include/file.hrl").
+-include_lib("stdlib/include/ms_transform.hrl").
+
+-record(state,{priv_dir,app_data,supvis,web_data,started=[]}).
+
+-define(MAX_NUMBER_OF_WEBTOOLS,256).
+-define(DEFAULT_PORT,8888).% must be >1024 or the user must be root on unix
+-define(DEFAULT_ADDR,{127,0,0,1}).
+
+-define(WEBTOOL_ALIAS,{ct_webtool,[{alias,{erl_alias,"/ct_webtool",[ct_webtool]}}]}).
+-define(HEADER,"Pragma:no-cache\r\n Content-type: text/html\r\n\r\n").
+-define(HTML_HEADER,"<HTML>\r\n<HEAD>\r\n<TITLE>WebTool</TITLE>\r\n</HEAD>\r\n<BODY BGCOLOR=\"#FFFFFF\">\r\n").
+-define(HTML_HEADER_RELOAD,"<HTML>\r\n<HEAD>\r\n<TITLE>WebTool
+ </TITLE>\r\n</HEAD>\r\n
+ <BODY BGCOLOR=\"#FFFFFF\" onLoad=reloadCompiledList()>\r\n").
+
+-define(HTML_END,"</BODY></HTML>").
+
+-define(SEND_URL_TIMEOUT,5000).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% For debugging only. %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Start tracing with
+%% debug(Functions).
+%% Functions = local | global | FunctionList
+%% FunctionList = [Function]
+%% Function = {FunctionName,Arity} | FunctionName |
+%% {Module, FunctionName, Arity} | {Module,FunctionName}
+debug(F) ->
+ {ok, _} = ttb:tracer(all,[{file,"webtool.trc"}]), % tracing all nodes
+ {ok, _} = ttb:p(all,[call,timestamp]),
+ MS = [{'_',[],[{return_trace},{message,{caller}}]}],
+ _ = 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
+ {ok, _} = ttb:tpl(M,F,A,MS),
+ tp(T,MS);
+tp([{M,F}|T],MS) when is_atom(F) -> % Other module
+ {ok, _} = ttb:tpl(M,F,MS),
+ tp(T,MS);
+tp([{F,A}|T],MS) -> % function/arity
+ {ok, _} = ttb:tpl(?MODULE,F,A,MS),
+ tp(T,MS);
+tp([F|T],MS) -> % function
+ {ok, _} = ttb:tpl(?MODULE,F,MS),
+ tp(T,MS);
+tp([],_MS) ->
+ ok.
+stop_debug() ->
+ ttb:stop([format]).
+
+debug_app(Mod) ->
+ {ok, _} = ttb:tracer(all,[{file,"webtool_app.trc"},{handler,{fun out/4,true}}]),
+ {ok, _} = ttb:p(all,[call,timestamp]),
+ MS = [{'_',[],[{return_trace},{message,{caller}}]}],
+ {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)]),
+ [{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]),
+ S;
+out(_,_,_,_) ->
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% Functions called via script. %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+script_start() ->
+ usage(),
+ halt().
+script_start([App]) ->
+ DefaultBrowser =
+ case os:type() of
+ {win32,_} -> iexplore;
+ _ -> firefox
+ end,
+ script_start([App,DefaultBrowser]);
+script_start([App,Browser]) ->
+ io:format("Starting webtool...\n"),
+ {ok, _} = start(),
+ AvailableApps = get_applications(),
+ {OSType,_} = os:type(),
+ case lists:keysearch(App,1,AvailableApps) of
+ {value,{App,StartPage}} ->
+ io:format("Starting ~w...\n",[App]),
+ start_tools([],"app=" ++ atom_to_list(App)),
+ PortStr = integer_to_list(get_port()),
+ Url = case StartPage of
+ "/" ++ Page ->
+ "http://localhost:" ++ PortStr ++ "/" ++ Page;
+ _ ->
+ "http://localhost:" ++ PortStr ++ "/" ++ StartPage
+ end,
+ _ = 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",
+ 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]),
+ os:cmd("\"" ++ atom_to_list(Browser) ++ "\" " ++ Url);
+ B when B==firefox; B==mozilla ->
+ io:format("Sending URL to ~w...",[Browser]),
+ BStr = atom_to_list(Browser),
+ SendCmd = BStr ++ " -raise -remote \'openUrl(" ++
+ Url ++ ")\'",
+ Port = open_port({spawn,SendCmd},[exit_status]),
+ receive
+ {Port,{exit_status,0}} ->
+ io:format("done\n"),
+ ok;
+ {Port,{exit_status,_Error}} ->
+ io:format(" not running, starting ~w...\n",
+ [Browser]),
+ _ = os:cmd(BStr ++ " " ++ Url),
+ ok
+ after ?SEND_URL_TIMEOUT ->
+ io:format(" failed, starting ~w...\n",[Browser]),
+ erlang:port_close(Port),
+ os:cmd(BStr ++ " " ++ Url)
+ end;
+ _ ->
+ io:format("Starting ~w...\n",[Browser]),
+ os:cmd(atom_to_list(Browser) ++ " " ++ Url)
+ end,
+ ok;
+ false ->
+ stop(),
+ io:format("\n{error,{unknown_app,~p}}\n",[App]),
+ halt()
+ end.
+
+usage() ->
+ io:format("Starting webtool...\n"),
+ {ok, _} = start(),
+ Apps = lists:map(fun({A,_}) -> A end,get_applications()),
+ io:format(
+ "\nUsage: start_webtool application [ browser ]\n"
+ "\nAvailable applications are: ~p\n"
+ "Default browser is \'iexplore\' (Internet Explorer) on Windows "
+ "or else \'firefox\'\n",
+ [Apps]),
+ stop().
+
+
+get_applications() ->
+ gen_server:call(ct_web_tool,get_applications).
+
+get_port() ->
+ gen_server:call(ct_web_tool,get_port).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% Api functions to the genserver. %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%----------------------------------------------------------------------
+%
+%----------------------------------------------------------------------
+
+start()->
+ start(standard_path,standard_data).
+
+start(Path,standard_data)->
+ case get_standard_data() of
+ {error,Reason} ->
+ {error,Reason};
+ Data ->
+ start(Path,Data)
+ end;
+
+start(standard_path,Data)->
+ Path=get_path(),
+ start(Path,Data);
+
+start(Path,Port) when is_integer(Port)->
+ Data = get_standard_data(Port),
+ start(Path,Data);
+
+start(Path,Data0)->
+ Data = Data0 ++ rest_of_standard_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).
+
+%----------------------------------------------------------------------
+%Web Api functions called by the web
+%----------------------------------------------------------------------
+started_tools(Env,Input)->
+ gen_server:call(ct_web_tool,{started_tools,Env,Input}).
+
+toolbar(Env,Input)->
+ gen_server:call(ct_web_tool,{toolbar,Env,Input}).
+
+start_tools(Env,Input)->
+ gen_server:call(ct_web_tool,{start_tools,Env,Input}).
+
+stop_tools(Env,Input)->
+ gen_server:call(ct_web_tool,{stop_tools,Env,Input}).
+%----------------------------------------------------------------------
+%Support API for other tools
+%----------------------------------------------------------------------
+
+is_localhost()->
+ gen_server:call(ct_web_tool,is_localhost).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%%The gen_server callback functions that builds the webbpages %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+handle_call(get_applications,_,State)->
+ MS = ets:fun2ms(fun({Tool,{web_data,{_,Start}}}) -> {Tool,Start} end),
+ Tools = ets:select(State#state.app_data,MS),
+ {reply,Tools,State};
+
+handle_call(get_port,_,State)->
+ {value,{port,Port}}=lists:keysearch(port,1,State#state.web_data),
+ {reply,Port,State};
+
+handle_call({started_tools,_Env,_Input},_,State)->
+ {reply,started_tools_page(State),State};
+
+handle_call({toolbar,_Env,_Input},_,State)->
+ {reply,toolbar(),State};
+
+handle_call({start_tools,Env,Input},_,State)->
+ {NewState,Page}=start_tools_page(Env,Input,State),
+ {reply,Page,NewState};
+
+handle_call({stop_tools,Env,Input},_,State)->
+ {NewState,Page}=stop_tools_page(Env,Input,State),
+ {reply,Page,NewState};
+
+handle_call(stoppit,_From,Data)->
+ {stop,normal,ok,Data};
+
+handle_call(is_localhost,_From,Data)->
+ Result=case proplists:get_value(bind_address, Data#state.web_data) of
+ ?DEFAULT_ADDR ->
+ true;
+ _IpNumber ->
+ false
+ end,
+ {reply,Result,Data}.
+
+
+handle_info(_Message,State)->
+ {noreply,State}.
+
+handle_cast(_Request,State)->
+ {noreply,State}.
+
+code_change(_,State,_)->
+ {ok,State}.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% The other functions needed by the gen_server behaviour
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%----------------------------------------------------------------------
+% Start the gen_server
+%----------------------------------------------------------------------
+init({Path,Config})->
+ case filelib:is_dir(Path) of
+ true ->
+ {ok, Table} = get_tool_files_data(),
+ insert_app(?WEBTOOL_ALIAS, Table),
+ case ct_webtool_sup:start_link() of
+ {ok, Pid} ->
+ case start_webserver(Table, Path, Config) of
+ {ok, _} ->
+ print_url(Config),
+ {ok,#state{priv_dir=Path,
+ app_data=Table,
+ supvis=Pid,
+ web_data=Config}};
+ {error, Error} ->
+ {stop, {error, Error}}
+ end;
+ Error ->
+ {stop,Error}
+ end;
+ false ->
+ {stop, {error, error_dir}}
+ end.
+
+terminate(_Reason,Data)->
+ %%shut down the webbserver
+ shutdown_server(Data),
+ %%Shutdown the different tools that are started with application:start
+ shutdown_apps(Data),
+ %%Shutdown the supervisor and its children will die
+ shutdown_supervisor(Data),
+ ok.
+
+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("Or http://~w.~w.~w.~w:~w/~n",[A,B,C,D,Port]).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% begin build the pages
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%----------------------------------------------------------------------
+%The page that shows the started tools
+%----------------------------------------------------------------------
+started_tools_page(State)->
+ [?HEADER,?HTML_HEADER,started_tools(State),?HTML_END].
+
+toolbar()->
+ [?HEADER,?HTML_HEADER,toolbar_page(),?HTML_END].
+
+
+start_tools_page(_Env,Input,State)->
+ %%io:format("~n======= ~n ~p ~n============~n",[Input]),
+ case get_tools(Input) of
+ {tools,Tools}->
+ %%io:format("~n======= ~n ~p ~n============~n",[Tools]),
+ {ok,NewState}=handle_apps(Tools,State,start),
+ {NewState,[?HEADER,?HTML_HEADER_RELOAD,reload_started_apps(),
+ show_unstarted_apps(NewState),?HTML_END]};
+ _ ->
+ {State,[?HEADER,?HTML_HEADER,show_unstarted_apps(State),?HTML_END]}
+ end.
+
+stop_tools_page(_Env,Input,State)->
+ case get_tools(Input) of
+ {tools,Tools}->
+ {ok,NewState}=handle_apps(Tools,State,stop),
+ {NewState,[?HEADER,?HTML_HEADER_RELOAD,reload_started_apps(),
+ show_started_apps(NewState),?HTML_END]};
+ _ ->
+ {State,[?HEADER,?HTML_HEADER,show_started_apps(State),?HTML_END]}
+ end.
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% Functions that start and config the webserver
+%% 1. Collect the config data
+%% 2. Start webserver
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%----------------------------------------------------------------------
+% Start the webserver
+%----------------------------------------------------------------------
+start_webserver(Data,Path,Config)->
+ case get_conf_data(Data,Path,Config) of
+ {ok,Conf_data}->
+ %%io:format("Conf_data: ~p~n",[Conf_data]),
+ start_server(Conf_data);
+ {error,Error} ->
+ {error,{error_server_conf_file,Error}}
+ end.
+
+start_server(Conf_data)->
+ case inets:start(httpd, Conf_data, stand_alone) of
+ {ok,Pid}->
+ {ok,Pid};
+ Error->
+ {error,{server_error,Error}}
+ end.
+
+%----------------------------------------------------------------------
+% Create config data for the webserver
+%----------------------------------------------------------------------
+get_conf_data(Data,Path,Config)->
+ Aliases=get_aliases(Data),
+ ServerRoot = filename:join([Path,"root"]),
+ MimeTypesFile = filename:join([ServerRoot,"conf","mime.types"]),
+ case httpd_conf:load_mime_types(MimeTypesFile) of
+ {ok,MimeTypes} ->
+ Config1 = Config ++ Aliases,
+ Config2 = [{server_root,ServerRoot},
+ {document_root,filename:join([Path,"root/doc"])},
+ {mime_types,MimeTypes} |
+ Config1],
+ {ok,Config2};
+ Error ->
+ Error
+ end.
+
+%----------------------------------------------------------------------
+% Control the path for *.tools files
+%----------------------------------------------------------------------
+get_tool_files_data()->
+ Tools=get_tools1(code:get_path()),
+ %%io:format("Data : ~p ~n",[Tools]),
+ get_file_content(Tools).
+
+%----------------------------------------------------------------------
+%Control that the data in the file really is erlang terms
+%----------------------------------------------------------------------
+get_file_content(Tools)->
+ Get_data=fun({tool,ToolData}) ->
+ %%io:format("Data : ~p ~n",[ToolData]),
+ case proplists:get_value(config_func,ToolData) of
+ {M,F,A}->
+ case catch apply(M,F,A) of
+ {'EXIT',_} ->
+ bad_data;
+ Data when is_tuple(Data) ->
+ Data;
+ _->
+ bad_data
+ end;
+ _ ->
+ bad_data
+ end
+ end,
+ insert_file_content([X ||X<-lists:map(Get_data,Tools),X/=bad_data]).
+
+%----------------------------------------------------------------------
+%Insert the data from the file in to the ets:table
+%----------------------------------------------------------------------
+insert_file_content(Content)->
+ Table=ets:new(app_data,[bag]),
+ lists:foreach(fun(X)->
+ insert_app(X,Table)
+ end,Content),
+ {ok,Table}.
+
+%----------------------------------------------------------------------
+%Control that we got a a tuple of a atom and a list if so add the
+%elements in the list to the ets:table
+%----------------------------------------------------------------------
+insert_app({Name,Key_val_list},Table) when is_list(Key_val_list),is_atom(Name)->
+ %%io:format("ToolData: ~p: ~p~n",[Name,Key_val_list]),
+ lists:foreach(
+ fun({alias,{erl_alias,Alias,Mods}}) ->
+ Key_val = {erl_script_alias,{Alias,Mods}},
+ %%io:format("Insert: ~p~n",[Key_val]),
+ ets:insert(Table,{Name,Key_val});
+ (Key_val_pair)->
+ %%io:format("Insert: ~p~n",[Key_val_pair]),
+ ets:insert(Table,{Name,Key_val_pair})
+ end,
+ Key_val_list);
+
+insert_app(_,_)->
+ ok.
+
+%----------------------------------------------------------------------
+% Select all the alias in the database
+%----------------------------------------------------------------------
+get_aliases(Data)->
+ MS = ets:fun2ms(fun({_,{erl_script_alias,Alias}}) ->
+ {erl_script_alias,Alias};
+ ({_,{alias,Alias}}) ->
+ {alias,Alias}
+ end),
+ ets:select(Data,MS).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% Helper functions %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+get_standard_data(Port)->
+ [
+ {port,Port},
+ {bind_address,?DEFAULT_ADDR},
+ {server_name,"localhost"}
+ ].
+
+get_standard_data()->
+ case get_free_port(?DEFAULT_PORT,?MAX_NUMBER_OF_WEBTOOLS) of
+ {error,Reason} -> {error,Reason};
+ Port ->
+ [
+ {port,Port},
+ {bind_address,?DEFAULT_ADDR},
+ {server_name,"localhost"}
+ ]
+ end.
+
+get_free_port(_Port,0) ->
+ {error,no_free_port_found};
+get_free_port(Port,N) ->
+ case gen_tcp:connect("localhost",Port,[]) of
+ {error, _Reason} ->
+ Port;
+ {ok,Sock} ->
+ gen_tcp:close(Sock),
+ get_free_port(Port+1,N-1)
+ end.
+
+rest_of_standard_data() ->
+ [
+ %% Do not allow the server to be crashed by malformed http-request
+ {max_header_siz,1024},
+ {max_header_action,reply414},
+ %% Go on a straight ip-socket
+ {com_type,ip_comm},
+ %% Do not change the order of these module names!!
+ {modules,[mod_alias,
+ mod_auth,
+ mod_esi,
+ mod_actions,
+ mod_cgi,
+ mod_include,
+ mod_dir,
+ mod_get,
+ mod_head,
+ mod_log,
+ mod_disk_log]},
+ {directory_index,["index.html"]},
+ {default_type,"text/plain"}
+ ].
+
+
+get_path()->
+ code:priv_dir(webtool).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% These functions is used to shutdown the webserver
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%----------------------------------------------------------------------
+% Shut down the webbserver
+%----------------------------------------------------------------------
+shutdown_server(State)->
+ {Addr,Port} = get_addr_and_port(State#state.web_data),
+ inets:stop(httpd,{Addr,Port}).
+
+get_addr_and_port(Config) ->
+ Addr = proplists:get_value(bind_address,Config,?DEFAULT_ADDR),
+ Port = proplists:get_value(port,Config,?DEFAULT_PORT),
+ {Addr,Port}.
+
+%----------------------------------------------------------------------
+% Select all apps in the table and close them
+%----------------------------------------------------------------------
+shutdown_apps(State)->
+ Data=State#state.app_data,
+ MS = ets:fun2ms(fun({_,{start,HowToStart}}) -> HowToStart end),
+ lists:foreach(fun(Start_app)->
+ stop_app(Start_app)
+ end,
+ ets:select(Data,MS)).
+
+%----------------------------------------------------------------------
+%Shuts down the supervisor that supervises tools that is not
+%Designed as applications
+%----------------------------------------------------------------------
+shutdown_supervisor(State)->
+ %io:format("~n==================~n"),
+ ct_webtool_sup:stop(State#state.supvis).
+ %io:format("~n==================~n").
+
+%----------------------------------------------------------------------
+%close the individual apps.
+%----------------------------------------------------------------------
+stop_app({child,_Real_name})->
+ ok;
+
+stop_app({app,Real_name})->
+ application:stop(Real_name);
+
+stop_app({func,_Start,Stop})->
+ case Stop of
+ {M,F,A} ->
+ catch apply(M,F,A);
+ _NoStop ->
+ ok
+ end.
+
+
+
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% These functions creates the webpage where the user can select if
+%% to start apps or to stop apps
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+toolbar_page()->
+ "<TABLE>
+ <TR>
+ <TD>
+ <B>Select Action</B>
+ </TD>
+ </TR>
+ <TR>
+ <TD>
+ <A HREF=\"./start_tools\" TARGET=right> Start Tools</A>
+ </TD>
+ </TR>
+ <TR>
+ <TD>
+ <A HREF=\"./stop_tools\" TARGET=right> Stop Tools</A>
+ </TD>
+ </TR>
+ </TABLE>".
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% These functions creates the webbpage that shows the started apps
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%----------------------------------------------------------------------
+% started_tools(State)->String (html table)
+% State is a record of type state
+%----------------------------------------------------------------------
+started_tools(State)->
+ Names=get_started_apps(State#state.app_data,State#state.started),
+ "<TABLE BORDER=1 WIDTH=100%>
+ "++ make_rows(Names,[],0) ++"
+ </TABLE>".
+%----------------------------------------------------------------------
+%get_started_apps(Data,Started)-> [{web_name,link}]
+%selects the started apps from the ets table of apps.
+%----------------------------------------------------------------------
+
+get_started_apps(Data,Started)->
+ SelectData=fun({Name,Link}) ->
+ {Name,Link}
+ end,
+ MS = lists:map(fun(A) -> {{A,{web_data,'$1'}},[],['$1']} end,Started),
+
+ [{"WebTool","/tool_management.html"} |
+ [SelectData(X) || X <- ets:select(Data,MS)]].
+
+%----------------------------------------------------------------------
+% make_rows(List,Result,Fields)-> String (The rows of a htmltable
+% List a list of tupler discibed above
+% Result an accumulator for the result
+% Field, counter that counts the number of cols in each row.
+%----------------------------------------------------------------------
+make_rows([],Result,Fields)->
+ Result ++ fill_out(Fields);
+make_rows([Data|Paths],Result,Field)when Field==0->
+ make_rows(Paths,Result ++ "<TR>" ++ make_field(Data),Field+1);
+
+make_rows([Path|Paths],Result,Field)when Field==4->
+ make_rows(Paths,Result ++ make_field(Path) ++ "</TR>",0);
+
+make_rows([Path|Paths],Result,Field)->
+ make_rows(Paths,Result ++ make_field(Path),Field+1).
+
+%----------------------------------------------------------------------
+% make_fields(Path)-> String that is a field i a html table
+% Path is a name url tuple {Name,url}
+%----------------------------------------------------------------------
+make_field(Path)->
+ "<TD WIDTH=20%>" ++ get_name(Path) ++ "</TD>".
+
+
+%----------------------------------------------------------------------
+%get_name({Nae,Url})->String that represents a <A> tag in html.
+%----------------------------------------------------------------------
+get_name({Name,Url})->
+ "<A HREF=\"" ++ Url ++ "\" TARGET=app_frame>" ++ Name ++ "</A>".
+
+
+%----------------------------------------------------------------------
+% fill_out(Nr)-> String, that represent Nr fields in a html-table.
+%----------------------------------------------------------------------
+fill_out(Nr)when Nr==0->
+ [];
+fill_out(Nr)when Nr==4->
+ "<TD WIDTH=\"20%\" >&nbsp</TD></TR>";
+
+fill_out(Nr)->
+ "<TD WIDTH=\"20%\">&nbsp</TD>" ++ fill_out(Nr+1).
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%%These functions starts applicatons and builds the page showing tools
+%%to start
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%----------------------------------------------------------------------
+%Controls whether the user selected a tool to start
+%----------------------------------------------------------------------
+get_tools(Input)->
+ case httpd:parse_query(Input) of
+ []->
+ no_tools;
+ Tools->
+ FormatData=fun({_Name,Data}) -> list_to_atom(Data) end,
+ SelectData=
+ fun({Name,_Data}) -> string:equal(Name,"app") end,
+ {tools,[FormatData(X)||X<-Tools,SelectData(X)]}
+ end.
+
+%----------------------------------------------------------------------
+% Selects the data to start the applications the user has ordered
+% starting of
+%----------------------------------------------------------------------
+handle_apps([],State,_Cmd)->
+ {ok,State};
+
+handle_apps([Tool|Tools],State,Cmd)->
+ case ets:match_object(State#state.app_data,{Tool,{start,'_'}}) of
+ []->
+ Started = case Cmd of
+ start ->
+ [Tool|State#state.started];
+ stop ->
+ lists:delete(Tool,State#state.started)
+ end,
+ {ok,#state{priv_dir=State#state.priv_dir,
+ app_data=State#state.app_data,
+ supvis=State#state.supvis,
+ web_data=State#state.web_data,
+ started=Started}};
+ ToStart ->
+ case handle_apps2(ToStart,State,Cmd) of
+ {ok,NewState}->
+ handle_apps(Tools,NewState,Cmd);
+ _->
+ handle_apps(Tools,State,Cmd)
+ end
+ end.
+
+%----------------------------------------------------------------------
+%execute every start or stop data about a tool.
+%----------------------------------------------------------------------
+handle_apps2([{Name,Start_data}],State,Cmd)->
+ case handle_app({Name,Start_data},State#state.app_data,State#state.supvis,Cmd) of
+ ok->
+ Started = case Cmd of
+ start ->
+ [Name|State#state.started];
+ stop ->
+
+ lists:delete(Name,State#state.started)
+ end,
+ {ok,#state{priv_dir=State#state.priv_dir,
+ app_data=State#state.app_data,
+ supvis=State#state.supvis,
+ web_data=State#state.web_data,
+ started=Started}};
+ _->
+ error
+ end;
+
+handle_apps2([{Name,Start_data}|Rest],State,Cmd)->
+ case handle_app({Name,Start_data},State#state.app_data,State#state.supvis,Cmd)of
+ ok->
+ handle_apps2(Rest,State,Cmd);
+ _->
+ error
+ end.
+
+
+%----------------------------------------------------------------------
+% Handle start and stop of applications
+%----------------------------------------------------------------------
+
+handle_app({Name,{start,{func,Start,Stop}}},Data,_Pid,Cmd)->
+ Action = case Cmd of
+ start ->
+ Start;
+ _ ->
+ Stop
+ end,
+ case Action of
+ {M,F,A} ->
+ case catch apply(M,F,A) of
+ {'EXIT',_} = Exit->
+ %%! 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",
+ [?LINE,Name,M,F,format_args(A),Exit]),
+ ets:delete(Data,Name);
+ _OK->
+ ok
+ end;
+ _NoStart ->
+ ok
+ end;
+
+
+handle_app({Name,{start,{child,ChildSpec}}},Data,Pid,Cmd)->
+ case Cmd of
+ start ->
+ case catch supervisor:start_child(Pid,ChildSpec) of
+ {ok,_}->
+ ok;
+ {ok,_,_}->
+ ok;
+ {error,Reason}->
+ %%! 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",
+ [?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",
+ [?LINE,Name,Pid,ChildSpec,Error]),
+ ets:delete(Data,Name)
+ end;
+ stop ->
+ case catch supervisor:terminate_child(websup,element(1,ChildSpec)) of
+ ok ->
+ supervisor:delete_child(websup,element(1,ChildSpec));
+ _ ->
+ error
+ end
+ end;
+
+
+
+handle_app({Name,{start,{app,Real_name}}},Data,_Pid,Cmd)->
+ case Cmd of
+ start ->
+ case application:start(Real_name,temporary) of
+ ok->
+ io:write(Name),
+ ok;
+ {error,{already_started,_}}->
+ %% Remove it from the database so we dont start
+ %% anything already started
+ ets:match_delete(Data,{Name,{start,{app,Real_name}}}),
+ ok;
+ {error,_Reason}=Error->
+ %%! Here the tool disappears from the webtool interface!!
+ io:format("\n=======ERROR (webtool, line ~w) =======\n"
+ "Could not start application \'~p\'\n\n"
+ "application:start(~p,~p) ->\n"
+ "~p\n\n",
+ [?LINE,Name,Real_name,temporary,Error]),
+ ets:delete(Data,Name)
+ end;
+
+ stop ->
+ application:stop(Real_name)
+ end;
+
+%----------------------------------------------------------------------
+% If the data is incorrect delete the app
+%----------------------------------------------------------------------
+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",
+ [?LINE,Cmd,Name,Incorrect]),
+ ets:delete(Data,Name).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% this functions creates the page that shows the unstarted tools %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+reload_started_apps()->
+ "<script>
+ function reloadCompiledList()
+ {
+ parent.parent.top1.document.location.href=\"/webtool/webtool/started_tools\";
+ }
+ </script>".
+
+show_unstarted_apps(State)->
+ "<TABLE HEIGHT=100% WIDTH=100% BORDER=0>
+ <TR HEIGHT=80%><TD ALIGN=\"center\" VALIGN=\"middle\">
+ <FORM NAME=\"stop_apps\" ACTION=\"/webtool/webtool/start_tools\" >
+ <TABLE BORDER=1 WIDTH=60%>
+ <TR BGCOLOR=\"#8899AA\">
+ <TD ALIGN=CENTER COLSPAN=2><FONT SIZE=4>Available Tools<FONT></TD>
+ </TR>
+ <TR>
+ <TD WIDTH=50%>
+ <TABLE BORDER=0>
+ "++ list_available_apps(State)++"
+ <TR><TD COLSPAN=2>&nbsp;</TD></TR>
+ <TR>
+ <TD COLSPAN=2 ALIGN=\"center\">
+ <INPUT TYPE=submit VALUE=\"Start\">
+ </TD>
+ </TR>
+ </TABLE>
+ </TD>
+ <TD>
+ To Start a Tool:
+ <UL>
+ <LI>Select the
+ checkbox for each tool to
+ start.</LI>
+ <LI>Click on the
+ button marked <EM>Start</EM>.</LI></UL>
+ </TD>
+ </TR>
+ </TABLE>
+ </FORM>
+ </TD></TR>
+ <TR><TD>&nbsp;</TD></TR>
+ </TABLE>".
+
+
+
+list_available_apps(State)->
+ MS = ets:fun2ms(fun({Tool,{web_data,{Name,_}}}) -> {Tool,Name} end),
+ Unstarted_apps=
+ lists:filter(
+ fun({Tool,_})->
+ false==lists:member(Tool,State#state.started)
+ end,
+ ets:select(State#state.app_data,MS)),
+ case Unstarted_apps of
+ []->
+ "<TR><TD>All tools are started</TD></TR>";
+ _->
+ list_apps(Unstarted_apps)
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% these functions creates the page that shows the started apps %%
+%% the user can select to shutdown %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+show_started_apps(State)->
+ "<TABLE HEIGHT=100% WIDTH=100% BORDER=0>
+ <TR HEIGHT=80%><TD ALIGN=\"center\" VALIGN=\"middle\">
+ <FORM NAME=\"stop_apps\" ACTION=\"/webtool/webtool/stop_tools\" >
+ <TABLE BORDER=1 WIDTH=60%>
+ <TR BGCOLOR=\"#8899AA\">
+ <TD ALIGN=CENTER COLSPAN=2><FONT SIZE=4>Started Tools<FONT></TD>
+ </TR>
+ <TR>
+ <TD WIDTH=50%>
+ <TABLE BORDER=0>
+ "++ list_started_apps(State)++"
+ <TR><TD COLSPAN=2>&nbsp;</TD></TR>
+ <TR>
+ <TD COLSPAN=2 ALIGN=\"center\">
+ <INPUT TYPE=submit VALUE=\"Stop\">
+ </TD>
+ </TR>
+ </TABLE>
+ </TD>
+ <TD>
+ Stop a Tool:
+ <UL>
+ <LI>Select the
+ checkbox for each tool to
+ stop.</LI>
+ <LI>Click on the
+ button marked <EM>Stop</EM>.</LI></UL>
+ </TD>
+ </TR>
+ </TABLE>
+ </FORM>
+ </TD></TR>
+ <TR><TD>&nbsp;</TD></TR>
+ </TABLE>".
+
+list_started_apps(State)->
+ MS = lists:map(fun(A) -> {{A,{web_data,{'$1','_'}}},[],[{{A,'$1'}}]} end,
+ State#state.started),
+ Started_apps= ets:select(State#state.app_data,MS),
+ case Started_apps of
+ []->
+ "<TR><TD>No tool is started yet.</TD></TR>";
+ _->
+ list_apps(Started_apps)
+ end.
+
+
+list_apps(Apps) ->
+ lists:map(fun({Tool,Name})->
+ "<TR><TD>
+ <INPUT TYPE=\"checkbox\" NAME=\"app\" VALUE=\""
+ ++ atom_to_list(Tool) ++ "\">
+ " ++ Name ++ "
+ </TD></TR>"
+ end,
+ Apps).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% Collecting the data from the *.tool files %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%----------------------------------------
+% get_tools(Dirs) => [{M,F,A},{M,F,A}...{M,F,A}]
+% Dirs - [string()] Directory names
+% Calls get_tools2/2 recursively for a number of directories
+% to retireve the configuration data for the web based tools.
+%----------------------------------------
+get_tools1(Dirs)->
+ get_tools1(Dirs,[]).
+
+get_tools1([Dir|Rest],Data) when is_list(Dir) ->
+ Tools=case filename:basename(Dir) of
+ %% Dir is an 'ebin' directory, check in '../priv' as well
+ "ebin" ->
+ [get_tools2(filename:join(filename:dirname(Dir),"priv")) |
+ get_tools2(Dir)];
+ _ ->
+ get_tools2(Dir)
+ end,
+ get_tools1(Rest,[Tools|Data]);
+
+get_tools1([],Data) ->
+ lists:flatten(Data).
+
+%----------------------------------------
+% get_tools2(Directory) => DataList
+% DataList : [WebTuple]|[]
+% WebTuple: {tool,[{web,M,F,A}]}
+%
+%----------------------------------------
+get_tools2(Dir)->
+ get_tools2(tool_files(Dir),[]).
+
+get_tools2([ToolFile|Rest],Data) ->
+ case get_tools3(ToolFile) of
+ {tool,WebData} ->
+ get_tools2(Rest,[{tool,WebData}|Data]);
+ {error,_Reason} ->
+ get_tools2(Rest,Data);
+ nodata ->
+ get_tools2(Rest,Data)
+ end;
+
+get_tools2([],Data) ->
+ Data.
+
+%----------------------------------------
+% get_tools3(ToolFile) => {ok,Tool}|{error,Reason}|nodata
+% Tool: {tool,[KeyValTuple]}
+% ToolFile - string() A .tool file
+% Now we have the file get the data and sort it out
+%----------------------------------------
+get_tools3(ToolFile) ->
+ case file:consult(ToolFile) of
+ {error,open} ->
+ {error,nofile};
+ {error,read} ->
+ {error,format};
+ {ok,[{version,"1.2"},ToolInfo]} when is_list(ToolInfo)->
+ webdata(ToolInfo);
+ {ok,[{version,_Vsn},_Info]} ->
+ {error,old_version};
+ {ok,_Other} ->
+ {error,format}
+ end.
+
+
+%----------------------------------------------------------------------
+% webdata(TupleList)-> ToolTuple| nodata
+% ToolTuple: {tool,[{config_func,{M,F,A}}]}
+%
+% There are a little unneccesary work in this format but it is extendable
+%----------------------------------------------------------------------
+webdata(TupleList)->
+ case proplists:get_value(config_func,TupleList,nodata) of
+ {M,F,A} ->
+ {tool,[{config_func,{M,F,A}}]};
+ _ ->
+ nodata
+ end.
+
+
+%=============================================================================
+% Functions for getting *.tool configuration files
+%=============================================================================
+
+%----------------------------------------
+% tool_files(Dir) => ToolFiles
+% Dir - string() Directory name
+% ToolFiles - [string()]
+% Return the list of all files in Dir ending with .tool (appended to Dir)
+%----------------------------------------
+tool_files(Dir) ->
+ case file:list_dir(Dir) of
+ {ok,Files} ->
+ filter_tool_files(Dir,Files);
+ {error,_Reason} ->
+ []
+ end.
+
+%----------------------------------------
+% filter_tool_files(Dir,Files) => ToolFiles
+% Dir - string() Directory name
+% Files, ToolFiles - [string()] File names
+% Filters out the files in Files ending with .tool and append them to Dir
+%----------------------------------------
+filter_tool_files(_Dir,[]) ->
+ [];
+filter_tool_files(Dir,[File|Rest]) ->
+ case filename:extension(File) of
+ ".tool" ->
+ [filename:join(Dir,File)|filter_tool_files(Dir,Rest)];
+ _ ->
+ filter_tool_files(Dir,Rest)
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% format functions
+ffunc({M,F,A}) when is_list(A) ->
+ io_lib:format("~w:~w(~s)\n",[M,F,format_args(A)]);
+ffunc({M,F,A}) when is_integer(A) ->
+ io_lib:format("~w:~w/~w\n",[M,F,A]).
+
+format_args([]) ->
+ "";
+format_args(Args) ->
+ Str = lists:append(["~p"|lists:duplicate(length(Args)-1,",~p")]),
+ 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
new file mode 100644
index 0000000000..c02ec69d04
--- /dev/null
+++ b/lib/common_test/src/ct_webtool_sup.erl
@@ -0,0 +1,75 @@
+%%
+%% %CopyrightBegin%
+%%
+%% 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.
+%% 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_webtool_sup).
+
+-behaviour(supervisor).
+
+%% External exports
+-export([start_link/0,stop/1]).
+
+%% supervisor callbacks
+-export([init/1]).
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+start_link() ->
+ supervisor:start_link({local,ct_websup},ct_webtool_sup, []).
+
+stop(Pid)->
+ exit(Pid,normal).
+%%%----------------------------------------------------------------------
+%%% Callback functions from supervisor
+%%%----------------------------------------------------------------------
+
+%%----------------------------------------------------------------------
+%% Func: init/1
+%% Returns: {ok, {SupFlags, [ChildSpec]}} |
+%% ignore |
+%% {error, Reason}
+%%----------------------------------------------------------------------
+init(_StartArgs) ->
+ %%Child1 =
+ %%Child2 ={webcover_backend,{webcover_backend,start_link,[]},permanent,2000,worker,[webcover_backend]},
+ %%{ok,{{simple_one_for_one,5,10},[Child1]}}.
+ {ok,{{one_for_one,100,10},[]}}.
+
+%%%----------------------------------------------------------------------
+%%% Internal functions
+%%%----------------------------------------------------------------------
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/common_test/src/cth_conn_log.erl b/lib/common_test/src/cth_conn_log.erl
index 644594e34d..883da0da0a 100644
--- a/lib/common_test/src/cth_conn_log.erl
+++ b/lib/common_test/src/cth_conn_log.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2012-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2012-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -56,11 +57,29 @@
pre_init_per_testcase/3,
post_end_per_testcase/4]).
+%%----------------------------------------------------------------------
+%% Exported types
+%%----------------------------------------------------------------------
+-export_type([hook_options/0,
+ log_type/0,
+ conn_mod/0]).
+
+%%----------------------------------------------------------------------
+%% 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.
+%%----------------------------------------------------------------------
+
-spec init(Id, HookOpts) -> Result when
Id :: term(),
- HookOpts :: ct_netconfc:hook_options(),
- Result :: {ok,[{ct_netconfc:conn_mod(),
- {ct_netconfc:log_type(),[ct_netconfc:key_or_name()]}}]}.
+ HookOpts :: hook_options(),
+ Result :: {ok,[{conn_mod(),
+ {log_type(),[ct_gen_conn:key_or_name()]}}]}.
init(_Id, HookOpts) ->
ConfOpts = ct:get_config(ct_conn_log,[]),
{ok,merge_log_info(ConfOpts,HookOpts)}.
@@ -73,20 +92,23 @@ merge_log_info([{Mod,ConfOpts}|ConfList],HookList) ->
{value,{_,HookOpts},HL1} ->
{ConfOpts ++ HookOpts, HL1} % ConfOpts overwrites HookOpts!
end,
- [{Mod,get_log_opts(Opts)} | merge_log_info(ConfList,HookList1)];
+ [{Mod,get_log_opts(Mod,Opts)} | merge_log_info(ConfList,HookList1)];
merge_log_info([],HookList) ->
- [{Mod,get_log_opts(Opts)} || {Mod,Opts} <- HookList].
+ [{Mod,get_log_opts(Mod,Opts)} || {Mod,Opts} <- HookList].
-get_log_opts(Opts) ->
- LogType = proplists:get_value(log_type,Opts,html),
+get_log_opts(Mod,Opts) ->
+ DefaultLogType = if Mod == ct_telnet -> raw;
+ true -> html
+ end,
+ LogType = proplists:get_value(log_type,Opts,DefaultLogType),
Hosts = proplists:get_value(hosts,Opts,[]),
{LogType,Hosts}.
-
pre_init_per_testcase(TestCase,Config,CthState) ->
Logs =
lists:map(
- fun({ConnMod,{LogType,Hosts}}) ->
+ fun({ConnMod,{LogType,Hosts}}) ->
+ ct_util:set_testdata({{?MODULE,ConnMod},LogType}),
case LogType of
LogType when LogType==raw; LogType==pretty ->
Dir = ?config(priv_dir,Config),
@@ -110,16 +132,51 @@ pre_init_per_testcase(TestCase,Config,CthState) ->
[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,[]}}
end
end,
CthState),
- error_logger:add_report_handler(ct_conn_log_h,{group_leader(),Logs}),
+
+ GL = group_leader(),
+ Update =
+ fun(Init) when Init == undefined; Init == [] ->
+ error_logger:add_report_handler(ct_conn_log_h,{GL,Logs}),
+ [TestCase];
+ (PrevUsers) ->
+ error_logger:info_report(update,{GL,Logs}),
+ receive
+ {updated,GL} ->
+ [TestCase|PrevUsers]
+ after
+ 5000 ->
+ {error,no_response}
+ end
+ end,
+ ct_util:update_testdata(?MODULE, Update, [create]),
{Config,CthState}.
-post_end_per_testcase(_TestCase,_Config,Return,CthState) ->
- error_logger:delete_report_handler(ct_conn_log_h),
+post_end_per_testcase(TestCase,_Config,Return,CthState) ->
+ Update =
+ fun(PrevUsers) ->
+ case lists:delete(TestCase, PrevUsers) of
+ [] ->
+ '$delete';
+ PrevUsers1 ->
+ PrevUsers1
+ end
+ end,
+ case ct_util:update_testdata(?MODULE, Update) of
+ deleted ->
+ _ = [ct_util:delete_testdata({?MODULE,ConnMod}) ||
+ {ConnMod,_} <- CthState],
+ error_logger:delete_report_handler(ct_conn_log_h);
+ {error,no_response} ->
+ exit({?MODULE,no_response_from_logger});
+ _PrevUsers ->
+ ok
+ end,
{Return,CthState}.
+
diff --git a/lib/common_test/src/cth_log_redirect.erl b/lib/common_test/src/cth_log_redirect.erl
index 958b7a94c7..6d77d7ee9e 100644
--- a/lib/common_test/src/cth_log_redirect.erl
+++ b/lib/common_test/src/cth_log_redirect.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2011-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2011-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -25,16 +26,32 @@
%% CTH Callbacks
--export([id/1, init/2, post_init_per_group/4, pre_end_per_group/3,
- post_end_per_testcase/4]).
+-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_init_per_testcase/4,
+ pre_end_per_testcase/3, post_end_per_testcase/4]).
%% Event handler Callbacks
-export([init/1,
handle_event/2, handle_call/2, handle_info/2,
- terminate/1]).
+ terminate/1, terminate/2, code_change/3]).
+
+%% Other
+-export([handle_remote_events/1]).
-include("ct.hrl").
+-behaviour(gen_event).
+
+-record(eh_state, {log_func,
+ curr_suite,
+ curr_group,
+ curr_func,
+ parallel_tcs = false,
+ handle_remote_events = false}).
+
id(_Opts) ->
?MODULE.
@@ -42,73 +59,218 @@ init(?MODULE, _Opts) ->
error_logger:add_report_handler(?MODULE),
tc_log_async.
-post_init_per_group(Group, Config, Result, tc_log_async) ->
+pre_init_per_suite(Suite, Config, State) ->
+ set_curr_func({Suite,init_per_suite}, Config),
+ {Config, State}.
+
+pre_end_per_suite(Suite, Config, State) ->
+ set_curr_func({Suite,end_per_suite}, Config),
+ {Config, State}.
+
+post_end_per_suite(_Suite, Config, Return, State) ->
+ set_curr_func(undefined, Config),
+ {Return, State}.
+
+pre_init_per_group(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) ->
case lists:member(parallel,proplists:get_value(
tc_group_properties,Config,[])) of
true ->
- {Result, {set_log_func(ct_log),Group}};
+ {Result, {set_log_func(tc_log),Group}};
false ->
{Result, tc_log_async}
end;
post_init_per_group(_Group, _Config, Result, State) ->
{Result, State}.
+pre_init_per_testcase(TC, Config, State) ->
+ set_curr_func(TC, Config),
+ {Config, State}.
+
+post_init_per_testcase(_TC, _Config, Return, State) ->
+ {Return, State}.
+
+pre_end_per_testcase(_TC, Config, State) ->
+ {Config, State}.
+
post_end_per_testcase(_TC, _Config, Result, State) ->
%% Make sure that the event queue is flushed
%% before ending this test case.
gen_event:call(error_logger, ?MODULE, flush, 300000),
{Result, State}.
-pre_end_per_group(Group, Config, {ct_log, Group}) ->
+pre_end_per_group(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(Group, Config, State) ->
+ set_curr_func({group,Group,end_per_group}, Config),
{Config, State}.
+post_end_per_group(_Group, Config, Return, State) ->
+ set_curr_func({group,undefined}, Config),
+ {Return, State}.
%% Copied and modified from sasl_report_tty_h.erl
init(_Type) ->
- {ok, tc_log_async}.
+ {ok, #eh_state{log_func = tc_log_async}}.
-handle_event({_Type, GL, _Msg}, State) when node(GL) /= node() ->
+handle_event({_Type,GL,_Msg}, #eh_state{handle_remote_events = false} = State)
+ when node(GL) /= node() ->
{ok, State};
-handle_event(Event, LogFunc) ->
+handle_event(Event, #eh_state{log_func = LogFunc} = State) ->
case lists:keyfind(sasl, 1, application:which_applications()) of
false ->
sasl_not_started;
_Else ->
{ok, ErrLogType} = application:get_env(sasl, errlog_type),
SReport = sasl_report:format_report(group_leader(), ErrLogType,
- tag_event(Event)),
+ tag_event(Event, local)),
if is_list(SReport) ->
- ct_logs:LogFunc(sasl, ?STD_IMPORTANCE, "System", SReport, []);
+ SaslHeader = format_header(State),
+ 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) ->
- ct_logs:LogFunc(error_logger, ?STD_IMPORTANCE, "System", EReport, []);
+ ErrHeader = format_header(State),
+ 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,
- {ok, LogFunc}.
+ {ok, State}.
+handle_info({'EXIT',User,killed}, State) ->
+ case whereis(user) of
+ %% init:stop/1/2 has been called, let's finish!
+ undefined ->
+ remove_handler;
+ User ->
+ remove_handler;
+ _ ->
+ {ok,State}
+ end;
-handle_info(_,State) -> {ok, State}.
+handle_info(_, State) ->
+ {ok,State}.
handle_call(flush,State) ->
{ok, ok, State};
-handle_call({set_logfunc,NewLogFunc},_) ->
- {ok, NewLogFunc, NewLogFunc};
-handle_call(_Query, _State) -> {error, bad_query}.
-terminate(_State) ->
+handle_call({set_curr_func,{group,Group,Conf},Config},
+ State) when is_list(Config) ->
+ Parallel = case proplists:get_value(tc_group_properties, Config) of
+ undefined -> false;
+ Props -> lists:member(parallel, Props)
+ end,
+ {ok, ok, State#eh_state{curr_group = Group,
+ curr_func = Conf,
+ parallel_tcs = Parallel}};
+handle_call({set_curr_func,{group,Group,Conf},_SkipOrFail}, State) ->
+ {ok, ok, State#eh_state{curr_group = Group,
+ curr_func = Conf,
+ parallel_tcs = false}};
+handle_call({set_curr_func,{group,undefined},_Config}, State) ->
+ {ok, ok, State#eh_state{curr_group = undefined,
+ curr_func = undefined,
+ parallel_tcs = false}};
+handle_call({set_curr_func,{Suite,Conf},_Config}, State) ->
+ {ok, ok, State#eh_state{curr_suite = Suite,
+ curr_func = Conf,
+ parallel_tcs = false}};
+handle_call({set_curr_func,undefined,_Config}, State) ->
+ {ok, ok, State#eh_state{curr_suite = undefined,
+ curr_func = undefined,
+ parallel_tcs = false}};
+handle_call({set_curr_func,TC,_Config}, State) ->
+ {ok, ok, State#eh_state{curr_func = TC}};
+
+handle_call({set_logfunc,NewLogFunc}, State) ->
+ {ok, NewLogFunc, State#eh_state{log_func = NewLogFunc}};
+
+handle_call({handle_remote_events,Bool}, State) ->
+ {ok, ok, State#eh_state{handle_remote_events = Bool}};
+
+handle_call(_Query, _State) ->
+ {error, bad_query}.
+
+terminate(_) ->
error_logger:delete_report_handler(?MODULE),
[].
-tag_event(Event) ->
+terminate(_Arg, _State) ->
+ ok.
+
+tag_event(Event, utc) ->
+ {calendar:universal_time(), Event};
+tag_event(Event, _) ->
{calendar:local_time(), Event}.
+set_curr_func(CurrFunc, Config) ->
+ gen_event:call(error_logger, ?MODULE, {set_curr_func, CurrFunc, Config}).
+
set_log_func(Func) ->
gen_event:call(error_logger, ?MODULE, {set_logfunc, Func}).
+
+handle_remote_events(Bool) ->
+ gen_event:call(error_logger, ?MODULE, {handle_remote_events, Bool}).
+
+%%%-----------------------------------------------------------------
+
+format_header(#eh_state{curr_suite = undefined,
+ curr_group = undefined,
+ curr_func = undefined}) ->
+ io_lib:format("System report", []);
+
+format_header(#eh_state{curr_suite = Suite,
+ curr_group = undefined,
+ curr_func = undefined}) ->
+ io_lib:format("System report during ~w", [Suite]);
+
+format_header(#eh_state{curr_suite = Suite,
+ curr_group = undefined,
+ curr_func = TcOrConf}) ->
+ io_lib:format("System report during ~w:~w/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",
+ [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",
+ [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",
+ [Suite,TC,Group]).
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
diff --git a/lib/common_test/src/cth_surefire.erl b/lib/common_test/src/cth_surefire.erl
index 1a38b6584b..59b916851e 100644
--- a/lib/common_test/src/cth_surefire.erl
+++ b/lib/common_test/src/cth_surefire.erl
@@ -1,18 +1,19 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2012-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2012-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%--------------------------------------------------------------------
@@ -59,6 +60,8 @@
-define(default_report,"junit_report.xml").
-define(suite_log,"suite.log.html").
+-define(now, os:timestamp()).
+
%% Number of dirs from log root to testcase log file.
%% ct_run.<node>.<timestamp>/<test_name>/run.<timestamp>/<tc_log>.html
-define(log_depth,3).
@@ -77,8 +80,13 @@ init(Path, Opts) ->
axis = proplists:get_value(axis,Opts,[]),
properties = proplists:get_value(properties,Opts,[]),
url_base = proplists:get_value(url_base,Opts),
- timer = now() }.
+ timer = ?now }.
+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) };
pre_init_per_suite(Suite,Config,#state{ test_cases = [] } = State) ->
TcLog = proplists:get_value(tc_logfile,Config),
CurrLogDir = filename:dirname(TcLog),
@@ -92,7 +100,7 @@ pre_init_per_suite(Suite,Config,#state{ test_cases = [] } = State) ->
end,
{Config, init_tc(State#state{ filepath = Path,
curr_suite = Suite,
- curr_suite_ts = now(),
+ curr_suite_ts = ?now,
curr_log_dir = CurrLogDir},
Config) };
pre_init_per_suite(Suite,Config,State) ->
@@ -138,6 +146,9 @@ on_tc_fail(_TC, Res, State) ->
{fail,lists:flatten(io_lib:format("~p",[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 ->
TcStr = atom_to_list(Tc),
State =
@@ -162,9 +173,9 @@ do_tc_skip(Res, State) ->
State#state{ test_cases = [NewTC | tl(TCs)]}.
init_tc(State, Config) when is_list(Config) == false ->
- State#state{ timer = now(), tc_log = "" };
+ State#state{ timer = ?now, tc_log = "" };
init_tc(State, Config) ->
- State#state{ timer = now(),
+ State#state{ timer = ?now,
tc_log = proplists:get_value(tc_logfile, Config, [])}.
end_tc(Func, Config, Res, State) when is_atom(Func) ->
@@ -187,7 +198,7 @@ end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite,
ClassName = atom_to_list(Suite),
PGroup = string:join([ atom_to_list(Group)||
Group <- lists:reverse(Groups)],"."),
- TimeTakes = io_lib:format("~f",[timer:now_diff(now(),TS) / 1000000]),
+ TimeTakes = io_lib:format("~f",[timer:now_diff(?now,TS) / 1000000]),
State#state{ test_cases = [#testcase{ log = Log,
url = Url,
timestamp = now_to_string(TS),
@@ -202,7 +213,7 @@ close_suite(#state{ test_cases = [] } = State) ->
State;
close_suite(#state{ test_cases = TCs, url_base = UrlBase } = State) ->
{Total,Fail,Skip} = count_tcs(TCs,0,0,0),
- TimeTaken = timer:now_diff(now(),State#state.curr_suite_ts) / 1000000,
+ TimeTaken = timer:now_diff(?now,State#state.curr_suite_ts) / 1000000,
SuiteLog = filename:join(State#state.curr_log_dir,?suite_log),
SuiteUrl = make_url(UrlBase,SuiteLog),
Suite = #testsuite{ name = atom_to_list(State#state.curr_suite),
@@ -330,5 +341,7 @@ count_tcs([#testcase{result={fail,_}}|TCs],Ok,F,S) ->
count_tcs(TCs,Ok,F+1,S);
count_tcs([#testcase{result={skipped,_}}|TCs],Ok,F,S) ->
count_tcs(TCs,Ok,F,S+1);
+count_tcs([#testcase{result={auto_skipped,_}}|TCs],Ok,F,S) ->
+ count_tcs(TCs,Ok,F,S+1);
count_tcs([],Ok,F,S) ->
{Ok+F+S,F,S}.
diff --git a/lib/common_test/src/erl2html2.erl b/lib/common_test/src/erl2html2.erl
new file mode 100644
index 0000000000..e819f345de
--- /dev/null
+++ b/lib/common_test/src/erl2html2.erl
@@ -0,0 +1,311 @@
+%%
+%% %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);
+ {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..924086f2bd
--- /dev/null
+++ b/lib/common_test/src/test_server.erl
@@ -0,0 +1,2753 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1996-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(test_server).
+
+-define(DEFAULT_TIMETRAP_SECS, 60).
+
+%%% TEST_SERVER_CTRL INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([run_test_case_apply/1,init_target_info/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]).
+
+%%% 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())}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% 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: ~p\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: ~p\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({Mod,Func,Args,Name,RunInit,TimetrapData}) ->
+ case os:getenv("TS_RUN_VALGRIND") of
+ false ->
+ ok;
+ _ ->
+ 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),
+ 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)},
+ 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: ~p",
+ [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(~w, ~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(~w, ~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() ->
+ 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,Func},
+ [CurrConf],{ok,[CurrConf]}),
+ do_end_tc_call(Mod,{end_per_testcase,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(~w, ~p)"
+ " 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() ->
+ {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(~w, ~p)"
+ " 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(~w, ~p)"
+ " 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() ->
+ test_server_sup:framework_call(report, [framework_error,
+ {{FwMod,FwFunc},
+ FwError}]),
+ Comment =
+ lists:flatten(
+ io_lib:format("<font color=\"red\">"
+ "WARNING! ~w:~w 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) ->
+ {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) ->
+ receive
+ IoReply when is_tuple(IoReply),
+ element(1, IoReply) == io_reply ->
+ ReplyTo ! IoReply;
+ _ ->
+ io_reply_proxy(ReplyTo)
+ end.
+
+job_proxy_msgloop() ->
+ 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,Func},
+ IPTCEndRes,Return) of
+ {ok,EPTCInitConfig} when is_list(EPTCInitConfig) ->
+ {Return,EPTCInitConfig};
+ _ ->
+ Return
+ end,
+ do_end_tc_call1(Mod, {end_per_testcase,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: ~p\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: ~p\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 ~p 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; ~p with args ~p.\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("~p", [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()),
+ 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}
+ ]).
+
+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: ~p~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: ~p~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: ~p. "
+ "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),
+ 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 () ->
+ _ = 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.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% 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..b52e4bef9b
--- /dev/null
+++ b/lib/common_test/src/test_server_ctrl.erl
@@ -0,0 +1,5693 @@
+%%
+%% %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%
+%%
+-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(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 ~w: ~p\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: ~w\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("~w", [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:~w/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 ~p\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(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
+ 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 ~p", [Reason]);
+ {'EXIT',Reason} ->
+ report_severe_error(Reason),
+ print(1, "EXIT, reason ~p", [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: ~p\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 ~p\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: ~p", [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),
+
+ 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 = [html_header(Explanation),
+ "<h1>Last test</h1>\n"
+ "<a href=\"",Href,"\">",Explanation,"</a>\n"
+ "</body>\n</html>\n"],
+ 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),
+ Name0 = lists:flatten(io_lib:format("~w.~w~ts", [Mod,Func,?html_ext])),
+ Name = downcase(Name0),
+ 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_0 =
+ lists:flatten(io_lib:format("~w.~w.~ts~ts", [Mod,Func,Tag,
+ ?html_ext])),
+ Name1 = downcase(Name1_0),
+ 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:~w 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:~w/~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:~w/~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).
+
+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) of
+ true ->
+ [{conf,LastRef,[],{LastMod,end_per_suite}}];
+ 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
+ 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) of
+ true ->
+ Ref = make_ref(),
+ {[{conf,Ref,[],{Mod,init_per_suite}}],Mod,Ref};
+ 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
+
+ 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) of
+ true ->
+ [{conf,LastRef,[],{LastMod,end_per_suite}}|Init];
+ 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
+ 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) of
+ true ->
+ {[{conf,LastRef,[],{LastMod,end_per_suite}}],
+ Mod,skipped_suite};
+ 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
+ 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) ->
+ 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*** ~w returned bad elements in Config: ~p.~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 ~w. Reason: ~p~n",
+ [FwMod,FwFunc,Reason]),
+ print(1, "~w failed in ~w. Reason: ~p~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*** ~w 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*** ~w 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*** ~w 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*** ~w 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*** ~w 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 ~w. Reason: ~p~n",
+ [FwMod,FwFunc,Reason]),
+ print(1, "~w failed in ~w. Reason: ~p~n", [FwMod,FwFunc,Reason]),
+ stop_minor_log_file(),
+ exit(framework_error);
+ %% sequential execution of test case finished
+ {Time,RetVal,_} ->
+ {Failed,Status1} =
+ case Time of
+ died ->
+ {true,update_status(failed, Mod, Func, Status)};
+ _ when is_tuple(RetVal) ->
+ case element(1, RetVal) of
+ R when R=='EXIT'; R==failed ->
+ {true,update_status(failed, Mod, Func, Status)};
+ R when R==skip; R==skipped ->
+ {false,update_status(skipped, Mod, Func, Status)};
+ _ ->
+ {false,update_status(ok, Mod, Func, Status)}
+ end;
+ _ ->
+ {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*** ~w 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 ~p", [Props]),
+ print(minor, "Group properties: ~p~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:~w", [Mod,Func]),
+ GroupName = case get_name(Mode) of
+ undefined ->
+ "";
+ GrName ->
+ GrName1 = cast_to_list(GrName),
+ print(major, "=group_props ~p", [[{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 ~w ***", [{Mod,Func}]);
+ true ->
+ print(2,"*** Skipping test case #~w ~w ***", [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 ++ "~w" ++ 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:~w) died! Reason: ~p",
+ [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:~w) died! Reason: ~p",
+ [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),
+ _ = [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:~w", [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\">~w</a></td>"
+ "<td><a href=\"~ts#top\">&lt;</a> <a href=\"~ts#end\">&gt;</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(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,Reason} ->
+ 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: ~p\n",
+ [Exit]);
+ [] -> ok;
+ List ->
+ print(minor, "WARNING: ~w slave nodes in system after test"++
+ "case. Tried to killed them.~n"++
+ " Names:~p",
+ [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: ~p", [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}}]),
+ 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 ++ "~.3fs" ++ St1 ++ "</td>"
+ "<td><font color=\"~ts\">SKIPPED</font></td>"
+ "<td>~ts~ts</td></tr>\n",
+ [Time,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, ~p", [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, ~p", [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,~p}",
+ [Reason])),
+ []),
+ failed;
+
+progress(failed, CaseNum, Mod, Func, GrName, unknown, Reason, Time,
+ Comment0, {St0,St1}) ->
+ print(major, "=result failed: ~p, ~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("~p", [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: ~p, ~p", [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}]),
+ 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 ++ "~.3fs" ++ St1 ++ "</td>"
+ "<td><font color=\"green\">Ok</font></td>"
+ "~ts</tr>\n",
+ [Time,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]) ->
+ ["&lt;" | esc_chars_in_list(Io)];
+esc_chars_in_list([$> | Io]) ->
+ ["&gt;" | esc_chars_in_list(Io)];
+esc_chars_in_list([$& | Io]) ->
+ ["&amp;" | 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("~w", [{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("~p", [Term]));
+ String -> lists:flatten(String)
+ end;
+to_string(Term) ->
+ lists:flatten(io_lib:format("~p", [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} ->
+ {"~p",Reason};
+ _ ->
+ do_format_exception(Reason)
+ end;
+ FW ->
+ case application:get_env(FW, format_exception) of
+ {ok,false} ->
+ {"~p",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) ++ "p", [Term])
+ end,
+ case catch lib:format_exception(1, error, Error, Stack, StackFun, PF) of
+ {'EXIT',_} ->
+ {"~p",Reason};
+ Formatted ->
+ Formatted1 = re:replace(Formatted, "exception error: ", "", [{return,list}]),
+ {"~ts",lists:flatten(Formatted1)}
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% run_test_case_apply(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(Mod, Func, Args, Name, RunInit,
+ TimetrapData) ->
+ test_server:run_test_case_apply({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; ~p with args ~p.\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: ~p~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 ~p=~p", [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 ~p", [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 ~p with options ~p",
+ [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: ~p",
+ [Name, Host, Cmd, Ret]),
+ {fail,Ret};
+ {Ret, undefined, undefined} ->
+ format(minor, "Failed to start node ~tp: ~p", [Name,Ret]),
+ Ret;
+ {Ret, Host, Cmd} ->
+ format(minor,
+ "Failed to start node ~tp on ~tp with command: ~ts~n"
+ "Reason: ~p",
+ [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("~w", [Call]),
+ io_lib:format("~w", [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 ~p\n", [CoverFile]),
+ {[],[],[]}
+ end;
+ {error,Reason} ->
+ io:fwrite("Can't read CoverFile ~ts\nReason: ~p\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: ~w\n"
+ "Reason: ~p\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"].
+
+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..7d6fe64b92
--- /dev/null
+++ b/lib/common_test/src/test_server_gl.erl
@@ -0,0 +1,355 @@
+%%
+%% %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%
+%%
+%% 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/0,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
+ 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() ->
+ case gen_server:start_link(?MODULE, [], []) 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([]) ->
+ EscChars = case application:get_env(test_server, esc_chars) of
+ {ok,ECBool} -> ECBool;
+ _ -> true
+ end,
+ {ok,#st{tc_supervisor=none,
+ minor=none,
+ minor_monitor=none,
+ 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: ~w\n"
+ "Got down from minor Fd ~w: ~w\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({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:~w/~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: ~w\n"
+ "Failed to write to minor Fd: ~w\n"
+ "Type: ~w\n"
+ "Reason: ~w\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..3d5238052b
--- /dev/null
+++ b/lib/common_test/src/test_server_io.erl
@@ -0,0 +1,453 @@
+%%
+%% %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%
+%%
+%% 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),
+ Empty = gb_trees:empty(),
+ {ok,Shared} = test_server_gl:start_link(),
+ {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(),
+ 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,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),
+ 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(),
+ {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 stopping =/= 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,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: ~p\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 '~p' 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 '~p': ~p\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..0b406c54cc
--- /dev/null
+++ b/lib/common_test/src/test_server_node.erl
@@ -0,0 +1,766 @@
+%%
+%% %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%
+%%
+-module(test_server_node).
+-compile(r12).
+
+%%%
+%%% The same compiled code for this module must be possible to load
+%%% in R12B 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:~w/~w~n"
+ "Arguments : ~p~n"
+ "Caller : ~w~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:~w/~w~n"
+ "Arguments : ~p~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:~w/~w~n"
+ "Return value : ~p~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 : ~p~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("~w", [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) 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) ->
+ 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..6922e01fcc
--- /dev/null
+++ b/lib/common_test/src/test_server_sup.erl
@@ -0,0 +1,944 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1998-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: 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),
+ 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"
+ " ~p.~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 ~p, got ~p.",
+ [Crash, Reason]),
+ exit({wrong_crash_reason,Reason});
+ {'EXIT',Pid,Reason} ->
+ test_server:format(12, "Wrong crash reason. Wanted ~p, got ~p.",
+ [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 ~p~n", [Error]),
+ test_server:fail(Error)
+ end;
+ Error ->
+ test_server:format(minor, "ERROR ~p~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) 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~p~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~p~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: ~p~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,
+ 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,~w}",[Mod,Func]);
+format_loc(Loc) ->
+ io_lib:format("~p",[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,~w,<a href=\"~ts~ts#~s\">~w</a>}",
+ [Mod,Func,
+ test_server_ctrl:uri_encode(downcase(ModStr)),
+ ?src_listing_ext,Link,Line]);
+ _ ->
+ io_lib:format("{~w,~w,~w}",[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()),
+ 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 88199b07d0..4897ddb2f8 100644
--- a/lib/common_test/src/unix_telnet.erl
+++ b/lib/common_test/src/unix_telnet.erl
@@ -1,24 +1,25 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
-%%% @doc Callback module for ct_telnet for talking telnet
-%%% to a unix host.
+%%% @doc Callback module for ct_telnet, for connecting to a telnet
+%%% server on a unix host.
%%%
%%% <p>It requires the following entry in the config file:</p>
%%% <pre>
@@ -26,17 +27,18 @@
%%% {port,PortNum}, % optional
%%% {username,UserName},
%%% {password,Password},
-%%% {keep_alive,Bool}]}. % optional</pre>
+%%% {keep_alive,Bool}, % optional
+%%% {tcp_nodely,Bool}]} % optional</pre>
%%%
-%%% <p>To talk telnet to the host specified by
+%%% <p>To communicate via telnet to the host specified by
%%% <code>HostNameOrIpAddress</code>, use the interface functions in
-%%% <code>ct</code>, e.g. <code>open(Name), cmd(Name,Cmd), ...</code>.</p>
+%%% <code>ct_telnet</code>, e.g. <code>open(Name), cmd(Name,Cmd), ...</code>.</p>
%%%
%%% <p><code>Name</code> is the name you allocated to the unix host in
%%% your <code>require</code> statement. E.g.</p>
-%%% <pre> suite() -> [{require,Name,{unix,[telnet,username,password]}}].</pre>
+%%% <pre> suite() -> [{require,Name,{unix,[telnet]}}].</pre>
%%% <p>or</p>
-%%% <pre> ct:require(Name,{unix,[telnet,username,password]}).</pre>
+%%% <pre> ct:require(Name,{unix,[telnet]}).</pre>
%%%
%%% <p>The "keep alive" activity (i.e. that Common Test sends NOP to the server
%%% every 10 seconds if the connection is idle) may be enabled or disabled for one
@@ -54,94 +56,107 @@
-compile(export_all).
%% Callbacks for ct_telnet.erl
--export([connect/5,get_prompt_regexp/0]).
--import(ct_telnet,[start_log/1,cont_log/2,end_log/0]).
+-export([connect/7,get_prompt_regexp/0]).
+-import(ct_telnet,[start_gen_log/1,log/4,end_gen_log/0]).
-define(username,"login: ").
-define(password,"Password: ").
-define(prx,"login: |Password: |\\\$ |> ").
%%%-----------------------------------------------------------------
-%%% @hidden
%%% @spec get_prompt_regexp() -> PromptRegexp
%%% PromptRegexp = ct_telnet:prompt_regexp()
%%%
%%% @doc Callback for ct_telnet.erl.
%%%
-%%% <p>Return the prompt regexp for telnet connections to the
-%%% interwatch instrument.</p>
+%%% <p>Return a suitable regexp string that will match common
+%%% prompts for users on unix hosts.</p>
get_prompt_regexp() ->
?prx.
%%%-----------------------------------------------------------------
-%%% @hidden
-%%% @spec connect(Ip,Port,Timeout,KeepAlive,Extra) -> {ok,Handle} | {error,Reason}
+%%% @spec connect(ConnName,Ip,Port,Timeout,KeepAlive,Extra) ->
+%%% {ok,Handle} | {error,Reason}
+%%% ConnName = ct:target_name()
%%% Ip = string() | {integer(),integer(),integer(),integer()}
%%% Port = integer()
%%% Timeout = integer()
%%% KeepAlive = bool()
-%%% Extra = {Username,Password}
+%%% TCPNoDelay = bool()
+%%% Extra = ct:target_name() | {Username,Password}
%%% Username = string()
%%% Password = string()
%%% Handle = ct_telnet:handle()
+%%% Reason = term()
%%%
%%% @doc Callback for ct_telnet.erl.
%%%
-%%% <p>Setup telnet connection to a UNIX host.</p>
-connect(Ip,Port,Timeout,KeepAlive,Extra) ->
+%%% <p>Setup telnet connection to a unix host.</p>
+connect(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay,Extra) ->
case Extra of
{Username,Password} ->
- connect1(Ip,Port,Timeout,KeepAlive,Username,Password);
- Name ->
- case get_username_and_password(Name) of
+ connect1(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay,
+ Username,Password);
+ KeyOrName ->
+ case get_username_and_password(KeyOrName) of
{ok,{Username,Password}} ->
- connect1(Ip,Port,Timeout,KeepAlive,Username,Password);
+ connect1(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay,
+ Username,Password);
Error ->
Error
end
end.
-connect1(Ip,Port,Timeout,KeepAlive,Username,Password) ->
- start_log("unix_telnet:connect"),
+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) of
+ case ct_telnet_client:open(Ip,Port,Timeout,KeepAlive,TCPNoDelay,Name) of
{ok,Pid} ->
- case ct_telnet:silent_teln_expect(Pid,[],[prompt],?prx,[]) of
+ case ct_telnet:silent_teln_expect(Name,Pid,[],
+ [prompt],?prx,[]) of
{ok,{prompt,?username},_} ->
+ log(Name,send,"Logging in to ~p:~p", [Ip,Port]),
ok = ct_telnet_client:send_data(Pid,Username),
- cont_log("Username: ~ts",[Username]),
- case ct_telnet:silent_teln_expect(Pid,[],prompt,?prx,[]) of
+ log(Name,send,"Username: ~ts",[Username]),
+ case ct_telnet:silent_teln_expect(Name,Pid,[],
+ prompt,?prx,[]) of
{ok,{prompt,?password},_} ->
ok = ct_telnet_client:send_data(Pid,Password),
Stars = lists:duplicate(length(Password),$*),
- cont_log("Password: ~s",[Stars]),
- ok = ct_telnet_client:send_data(Pid,""),
- case ct_telnet:silent_teln_expect(Pid,[],prompt,
+ log(Name,send,"Password: ~s",[Stars]),
+% ok = ct_telnet_client:send_data(Pid,""),
+ case ct_telnet:silent_teln_expect(Name,Pid,[],
+ prompt,
?prx,[]) of
{ok,{prompt,Prompt},_}
- when Prompt=/=?username, Prompt=/=?password ->
+ when Prompt=/=?username,
+ Prompt=/=?password ->
{ok,Pid};
Error ->
- cont_log("Password failed\n~p\n",
- [Error]),
+ log(Name,recv,"Password failed\n~p\n",
+ [Error]),
{error,Error}
end;
Error ->
- cont_log("Login failed\n~p\n",[Error]),
+ log(Name,recv,"Login to ~p:~p failed\n~p\n",[Ip,Port,Error]),
{error,Error}
end;
{ok,[{prompt,_OtherPrompt1},{prompt,_OtherPrompt2}],_} ->
{ok,Pid};
Error ->
- cont_log("Did not get expected prompt\n~p\n",[Error]),
+ log(Name,conn_error,
+ "Did not get expected prompt from ~p:~p\n~p\n",
+ [Ip,Port,Error]),
{error,Error}
end;
Error ->
- cont_log("Could not open telnet connection\n~p\n",[Error]),
+ log(Name,conn_error,
+ "Could not open telnet connection to ~p:~p\n~p\n",
+ [Ip,Port,Error]),
Error
end,
- end_log(),
+ end_gen_log(),
Result.
get_username_and_password(Name) ->
diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl
index b340c6fdd1..f1c5051164 100644
--- a/lib/common_test/src/vts.erl
+++ b/lib/common_test/src/vts.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2012. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2016. All Rights Reserved.
%%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% 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
%%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%% 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%
%%
@@ -63,21 +64,21 @@
%%%-----------------------------------------------------------------
%%% User API
start() ->
- webtool:start(),
- webtool:start_tools([],"app=vts").
+ {ok, _} = ct_webtool:start(),
+ ct_webtool:start_tools([],"app=vts").
init_data(ConfigFiles,EvHandlers,LogDir,LogOpts,Tests) ->
call({init_data,ConfigFiles,EvHandlers,LogDir,LogOpts,Tests}).
stop() ->
- webtool:stop_tools([],"app=vts"),
- webtool:stop().
+ ct_webtool:stop_tools([],"app=vts"),
+ ct_webtool:stop().
report(What,Data) ->
call({report,What,Data}).
%%%-----------------------------------------------------------------
-%%% Return config data used by webtool
+%%% Return config data used by ct_webtool
config_data() ->
{ok,LogDir} =
case lists:keysearch(logdir,1,init:get_arguments()) of
@@ -168,7 +169,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} ->
@@ -191,12 +192,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} ->
@@ -232,7 +233,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()),
@@ -276,8 +277,8 @@ 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}) ->
@@ -310,7 +311,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() ->
@@ -548,7 +548,7 @@ 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]),
@@ -754,7 +754,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;