aboutsummaryrefslogtreecommitdiffstats
path: root/lib/common_test/src
diff options
context:
space:
mode:
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.src64
-rw-r--r--lib/common_test/src/common_test.appup.src21
-rw-r--r--lib/common_test/src/ct.erl1045
-rw-r--r--lib/common_test/src/ct_config.erl81
-rw-r--r--lib/common_test/src/ct_config_plain.erl23
-rw-r--r--lib/common_test/src/ct_config_xml.erl21
-rw-r--r--lib/common_test/src/ct_conn_log_h.erl104
-rw-r--r--lib/common_test/src/ct_cover.erl63
-rw-r--r--lib/common_test/src/ct_default_gl.erl84
-rw-r--r--lib/common_test/src/ct_event.erl36
-rw-r--r--lib/common_test/src/ct_framework.erl417
-rw-r--r--lib/common_test/src/ct_ftp.erl188
-rw-r--r--lib/common_test/src/ct_gen_conn.erl174
-rw-r--r--lib/common_test/src/ct_groups.erl184
-rw-r--r--lib/common_test/src/ct_hooks.erl181
-rw-r--r--lib/common_test/src/ct_hooks_lock.erl36
-rw-r--r--lib/common_test/src/ct_logs.erl921
-rw-r--r--lib/common_test/src/ct_make.erl82
-rw-r--r--lib/common_test/src/ct_master.erl183
-rw-r--r--lib/common_test/src/ct_master_event.erl36
-rw-r--r--lib/common_test/src/ct_master_logs.erl120
-rw-r--r--lib/common_test/src/ct_master_status.erl35
-rw-r--r--lib/common_test/src/ct_netconfc.erl1058
-rw-r--r--lib/common_test/src/ct_netconfc.hrl25
-rw-r--r--lib/common_test/src/ct_property_test.erl96
-rw-r--r--lib/common_test/src/ct_release_test.erl939
-rw-r--r--lib/common_test/src/ct_repeat.erl43
-rw-r--r--lib/common_test/src/ct_rpc.erl139
-rw-r--r--lib/common_test/src/ct_run.erl663
-rw-r--r--lib/common_test/src/ct_slave.erl254
-rw-r--r--lib/common_test/src/ct_snmp.erl388
-rw-r--r--lib/common_test/src/ct_ssh.erl746
-rw-r--r--lib/common_test/src/ct_telnet.erl598
-rw-r--r--lib/common_test/src/ct_telnet_client.erl49
-rw-r--r--lib/common_test/src/ct_testspec.erl223
-rw-r--r--lib/common_test/src/ct_util.erl274
-rw-r--r--lib/common_test/src/ct_util.hrl24
-rw-r--r--lib/common_test/src/ct_webtool.erl95
-rw-r--r--lib/common_test/src/ct_webtool_sup.erl24
-rw-r--r--lib/common_test/src/cth_conn_log.erl58
-rw-r--r--lib/common_test/src/cth_log_redirect.erl284
-rw-r--r--lib/common_test/src/cth_surefire.erl115
-rw-r--r--lib/common_test/src/erl2html2.erl313
-rw-r--r--lib/common_test/src/test_server.erl2807
-rw-r--r--lib/common_test/src/test_server_ctrl.erl5763
-rw-r--r--lib/common_test/src/test_server_gl.erl362
-rw-r--r--lib/common_test/src/test_server_internal.hrl60
-rw-r--r--lib/common_test/src/test_server_io.erl458
-rw-r--r--lib/common_test/src/test_server_node.erl769
-rw-r--r--lib/common_test/src/test_server_sup.erl947
-rw-r--r--lib/common_test/src/unix_telnet.erl104
-rw-r--r--lib/common_test/src/vts.erl48
53 files changed, 16281 insertions, 5582 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile
index 449cba6c83..80eaed70bd 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-2018. 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%
#
@@ -77,7 +78,17 @@ MODULES= \
ct_conn_log_h \
cth_conn_log \
ct_groups \
- ct_property_test
+ ct_property_test \
+ ct_release_test \
+ ct_default_gl \
+ erl2html2 \
+ test_server_ctrl \
+ test_server_gl \
+ test_server_io \
+ test_server_node \
+ test_server \
+ test_server_sup
+
TARGET_MODULES= $(MODULES:%=$(EBIN)/%)
BEAM_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR))
@@ -99,8 +110,7 @@ DTD_FILES = \
# FLAGS
# ----------------------------------------------------
ERL_COMPILE_FLAGS += -pa ../ebin -I../include -I $(ERL_TOP)/lib/snmp/include/ \
- -I../../test_server/include -I../../xmerl/inc/ \
- -I $(ERL_TOP)/lib/kernel/include -Werror
+ -I../../xmerl/inc/ -I $(ERL_TOP)/lib/kernel/include -Werror
# ----------------------------------------------------
# Targets
@@ -155,3 +165,5 @@ release_tests_spec: opt
release_docs_spec: docs
+# Include dependencies -- list below added by Kostis Sagonas
+$(EBIN)/cth_log_redirect.beam: ../../kernel/include/logger.hrl ../../kernel/src/logger_internal.hrl
diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src
index 580d5dbd7b..efebea896c 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-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2009-2018. 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%
@@ -21,10 +22,12 @@
{vsn, "%VSN%"},
{modules, [ct_cover,
ct,
+ ct_default_gl,
ct_event,
ct_framework,
ct_ftp,
ct_gen_conn,
+ ct_groups,
ct_hooks,
ct_hooks_lock,
ct_logs,
@@ -35,6 +38,8 @@
ct_master_status,
ct_netconfc,
ct_conn_log_h,
+ ct_property_test,
+ ct_release_test,
ct_repeat,
ct_rpc,
ct_run,
@@ -44,6 +49,8 @@
ct_telnet,
ct_testspec,
ct_util,
+ ct_webtool,
+ ct_webtool_sup,
unix_telnet,
vts,
ct_config,
@@ -52,7 +59,14 @@
ct_slave,
cth_log_redirect,
cth_conn_log,
- cth_surefire
+ cth_surefire,
+ erl2html2,
+ test_server_ctrl,
+ test_server,
+ test_server_gl,
+ test_server_io,
+ test_server_node,
+ test_server_sup
]},
{registered, [ct_logs,
ct_util_server,
@@ -60,12 +74,28 @@
ct_make_ref,
vts,
ct_master,
- ct_master_logs]},
+ ct_master_logs,
+ test_server_ctrl,
+ test_server,
+ test_server_break_process]},
{applications, [kernel,stdlib]},
{env, []},
- {runtime_dependencies,["xmerl-1.3.7","webtool-0.8.10","tools-2.6.14",
- "test_server-3.7.1","stdlib-2.0","ssh-3.0.1",
- "snmp-4.25.1","sasl-2.4","runtime_tools-1.8.14",
- "kernel-3.0","inets-5.10","erts-6.0",
- "debugger-4.0","crypto-3.3","compiler-5.0"]}]}.
+ {runtime_dependencies,
+ ["compiler-6.0",
+ "crypto-3.6",
+ "debugger-4.1",
+ "erts-7.0",
+ "ftp-1.0.0",
+ "inets-6.0",
+ "kernel-4.0",
+ "observer-2.1",
+ "runtime_tools-1.8.16",
+ "sasl-2.4.2",
+ "snmp-5.1.2",
+ "ssh-4.0",
+ "stdlib-3.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 4dfd9f1b0d..e98f947553 100644
--- a/lib/common_test/src/common_test.appup.src
+++ b/lib/common_test/src/common_test.appup.src
@@ -1,18 +1,19 @@
%% -*- erlang -*-
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2014. All Rights Reserved.
+%% Copyright Ericsson AB 2014-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%
{"%VSN%",
diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl
index 9d8fce2789..778ea2e9e2 100644
--- a/lib/common_test/src/ct.erl
+++ b/lib/common_test/src/ct.erl
@@ -1,54 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2018. 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 Main user interface for the Common Test framework.
-%%%
-%%% <p> This module implements the command line interface for running
-%%% tests and some basic functions for common test case issues
-%%% such as configuration and logging. </p>
-%%%
-%%% <p><strong>Test Suite Support Macros</strong></p>
-%%%
-%%% <p>The <c>config</c> macro is defined in <c>ct.hrl</c>. This
-%%% macro should be used to retrieve information from the
-%%% <c>Config</c> variable sent to all test cases. It is used with two
-%%% arguments, where the first is the name of the configuration
-%%% variable you wish to retrieve, and the second is the <c>Config</c>
-%%% variable supplied to the test case.</p>
-%%%
-%%% <p>Possible configuration variables include:</p>
-%%% <ul>
-%%% <li><c>data_dir</c> - Data file directory.</li>
-%%% <li><c>priv_dir</c> - Scratch file directory.</li>
-%%% <li>Whatever added by <c>init_per_suite/1</c> or
-%%% <c>init_per_testcase/2</c> in the test suite.</li>
-%%% </ul>
-
-%%% @type var_name() = atom(). A variable name which is specified when
-%%% <c>ct:require/2</c> is called,
-%%% e.g. <c>ct:require(mynodename,{node,[telnet]})</c>
-%%%
-%%% @type target_name() = var_name(). The name of a target.
-%%%
-%%% @type handle() = ct_gen_conn:handle() | term(). The identity of a
-%%% specific connection.
-
-module(ct).
-include("ct.hrl").
@@ -63,9 +32,11 @@
-export([require/1, require/2,
get_config/1, get_config/2, get_config/3,
reload_config/1,
- log/1, log/2, log/3, log/4,
- print/1, print/2, print/3, print/4,
- pal/1, pal/2, pal/3, pal/4,
+ escape_chars/1, escape_chars/2,
+ log/1, log/2, log/3, log/4, log/5,
+ print/1, print/2, print/3, print/4, print/5,
+ pal/1, pal/2, pal/3, pal/4, pal/5,
+ set_verbosity/2, get_verbosity/1,
capture_start/0, capture_stop/0, capture_get/0, capture_get/1,
fail/1, fail/2, comment/1, comment/2, make_priv_dir/0,
testcases/2, userdata/2, userdata/3,
@@ -79,214 +50,73 @@
%% 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]).
-export([get_target_name/1]).
+-export([get_progname/0]).
-export([parse_table/1, listenv/1]).
-%%%-----------------------------------------------------------------
-%%% @spec install(Opts) -> ok | {error,Reason}
-%%% Opts = [Opt]
-%%% Opt = {config,ConfigFiles} | {event_handler,Modules} |
-%%% {decrypt,KeyOrFile}
-%%% ConfigFiles = [ConfigFile]
-%%% ConfigFile = string()
-%%% Modules = [atom()]
-%%% KeyOrFile = {key,Key} | {file,KeyFile}
-%%% Key = string()
-%%% KeyFile = string()
-%%% @doc Install config files and event handlers.
-%%%
-%%% <p>Run this function once before first test.</p>
-%%%
-%%% <p>Example:<br/>
-%%% <c>install([{config,["config_node.ctc","config_user.ctc"]}])</c>.</p>
-%%%
-%%% <p>Note that this function is automatically run by the
-%%% <c>ct_run</c> program.</p>
+-export([remaining_test_procs/0]).
+
+%%----------------------------------------------------------------------
+%% Exported types
+%%----------------------------------------------------------------------
+%% For ct_gen_conn
+-export_type([config_key/0,
+ target_name/0,
+ key_or_name/0]).
+
+%% For cth_conn_log
+-export_type([conn_log_options/0,
+ conn_log_type/0,
+ conn_log_mod/0]).
+
+%%------------------------------------------------------------------
+%% Type declarations
+%% ------------------------------------------------------------------
+-type config_key() :: atom(). % Config key which exists in a config file
+-type target_name() :: atom().% Name associated to a config_key() though 'require'
+-type key_or_name() :: config_key() | target_name().
+
+%% Types used when logging connections with the 'cth_conn_log' hook
+-type conn_log_options() :: [conn_log_option()].
+-type conn_log_option() :: {log_type,conn_log_type()} |
+ {hosts,[key_or_name()]}.
+-type conn_log_type() :: raw | pretty | html | silent.
+-type conn_log_mod() :: ct_netconfc | ct_telnet.
+%%----------------------------------------------------------------------
+
+
install(Opts) ->
ct_run:install(Opts).
-%%%-----------------------------------------------------------------
-%%% @spec run(TestDir,Suite,Cases) -> Result
-%%% TestDir = string()
-%%% Suite = atom()
-%%% Cases = atom() | [atom()]
-%%% Result = [TestResult] | {error,Reason}
-%%%
-%%% @doc Run the given test case(s).
-%%%
-%%% <p>Requires that <c>ct:install/1</c> has been run first.</p>
-%%%
-%%% <p>Suites (*_SUITE.erl) files must be stored in
-%%% <c>TestDir</c> or <c>TestDir/test</c>. All suites
-%%% will be compiled when test is run.</p>
run(TestDir,Suite,Cases) ->
ct_run:run(TestDir,Suite,Cases).
-%%%-----------------------------------------------------------------
-%%% @spec run(TestDir,Suite) -> Result
-%%%
-%%% @doc Run all test cases in the given suite.
-%%% @see run/3.
run(TestDir,Suite) ->
ct_run:run(TestDir,Suite).
-%%%-----------------------------------------------------------------
-%%% @spec run(TestDirs) -> Result
-%%% TestDirs = TestDir | [TestDir]
-%%%
-%%% @doc Run all test cases in all suites in the given directories.
-%%% @see run/3.
run(TestDirs) ->
ct_run:run(TestDirs).
-%%%-----------------------------------------------------------------
-%%% @spec run_test(Opts) -> Result
-%%% Opts = [OptTuples]
-%%% OptTuples = {dir,TestDirs} | {suite,Suites} | {group,Groups} |
-%%% {testcase,Cases} | {spec,TestSpecs} | {join_specs,Bool} |
-%%% {label,Label} | {config,CfgFiles} | {userconfig, UserConfig} |
-%%% {allow_user_terms,Bool} | {logdir,LogDir} |
-%%% {silent_connections,Conns} | {stylesheet,CSSFile} |
-%%% {cover,CoverSpecFile} | {cover_stop,Bool} | {step,StepOpts} |
-%%% {event_handler,EventHandlers} | {include,InclDirs} |
-%%% {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}
-%%% TestDirs = [string()] | string()
-%%% Suites = [string()] | [atom()] | string() | atom()
-%%% Cases = [atom()] | atom()
-%%% Groups = GroupNameOrPath | [GroupNameOrPath]
-%%% GroupNameOrPath = [atom()] | atom() | all
-%%% TestSpecs = [string()] | string()
-%%% Label = string() | atom()
-%%% CfgFiles = [string()] | string()
-%%% UserConfig = [{CallbackMod,CfgStrings}] | {CallbackMod,CfgStrings}
-%%% CallbackMod = atom()
-%%% CfgStrings = [string()] | string()
-%%% LogDir = string()
-%%% Conns = all | [atom()]
-%%% CSSFile = string()
-%%% CoverSpecFile = string()
-%%% StepOpts = [StepOpt] | []
-%%% StepOpt = config | keep_inactive
-%%% EventHandlers = EH | [EH]
-%%% EH = atom() | {atom(),InitArgs} | {[atom()],InitArgs}
-%%% InitArgs = [term()]
-%%% InclDirs = [string()] | string()
-%%% CreatePrivDir = auto_per_run | auto_per_tc | manual_per_tc
-%%% M = integer()
-%%% N = integer()
-%%% DurTime = string(HHMMSS)
-%%% StopTime = string(YYMoMoDDHHMMSS) | string(HHMMSS)
-%%% ForceStop = skip_rest | Bool
-%%% DecryptKeyOrFile = {key,DecryptKey} | {file,DecryptFile}
-%%% DecryptKey = string()
-%%% DecryptFile = string()
-%%% LogOpts = [LogOpt]
-%%% LogOpt = no_nl | no_src
-%%% VLevels = VLevel | [{Category,VLevel}]
-%%% VLevel = integer()
-%%% Category = atom()
-%%% CTHs = [CTHModule | {CTHModule, CTHInitArgs}]
-%%% CTHModule = atom()
-%%% CTHInitArgs = term()
-%%% Result = {Ok,Failed,{UserSkipped,AutoSkipped}} | TestRunnerPid | {error,Reason}
-%%% Ok = integer()
-%%% Failed = integer()
-%%% UserSkipped = integer()
-%%% AutoSkipped = integer()
-%%% TestRunnerPid = pid()
-%%% Reason = term()
-%%% @doc <p>Run tests as specified by the combination of options in <c>Opts</c>.
-%%% The options are the same as those used with the
-%%% <seealso marker="ct_run#ct_run"><c>ct_run</c></seealso> program.
-%%% Note that here a <c>TestDir</c> can be used to point out the path to
-%%% a <c>Suite</c>. Note also that the option <c>testcase</c>
-%%% corresponds to the <c>-case</c> option in the <c>ct_run</c>
-%%% program. Configuration files specified in <c>Opts</c> will be
-%%% installed automatically at startup.</p>
-%%% <p><c>TestRunnerPid</c> is returned if <c>release_shell == true</c>
-%%% (see <c>break/1</c> for details).</p>
-%%% <p><c>Reason</c> indicates what type of error has been encountered.</p>
run_test(Opts) ->
ct_run:run_test(Opts).
-%%%-----------------------------------------------------------------
-%%% @spec run_testspec(TestSpec) -> Result
-%%% TestSpec = [term()]
-%%% Result = {Ok,Failed,{UserSkipped,AutoSkipped}} | {error,Reason}
-%%% Ok = integer()
-%%% Failed = integer()
-%%% UserSkipped = integer()
-%%% AutoSkipped = integer()
-%%% Reason = term()
-%%% @doc Run test specified by <c>TestSpec</c>. The terms are
-%%% the same as those used in test specification files.
-%%% <p><c>Reason</c> indicates what type of error has been encountered.</p>
run_testspec(TestSpec) ->
ct_run:run_testspec(TestSpec).
-%%%-----------------------------------------------------------------
-%%% @spec step(TestDir,Suite,Case) -> Result
-%%% Case = atom()
-%%%
-%%% @doc Step through a test case with the debugger.
-%%% @see run/3
step(TestDir,Suite,Case) ->
ct_run:step(TestDir,Suite,Case).
-%%%-----------------------------------------------------------------
-%%% @spec step(TestDir,Suite,Case,Opts) -> Result
-%%% Case = atom()
-%%% Opts = [Opt] | []
-%%% Opt = config | keep_inactive
-%%%
-%%% @doc Step through a test case with the debugger. If the
-%%% <c>config</c> option has been given, breakpoints will
-%%% be set also on the configuration functions in <c>Suite</c>.
-%%% @see run/3
step(TestDir,Suite,Case,Opts) ->
ct_run:step(TestDir,Suite,Case,Opts).
-%%%-----------------------------------------------------------------
-%%% @spec start_interactive() -> ok
-%%%
-%%% @doc Start CT in interactive mode.
-%%%
-%%% <p>From this mode all test case support functions can be executed
-%%% directly from the erlang shell. The interactive mode can also be
-%%% started from the OS command line with <c>ct_run -shell
-%%% [-config File...]</c>.</p>
-%%%
-%%% <p>If any functions using "required config data" (e.g. telnet or
-%%% ftp functions) are to be called from the erlang shell, config data
-%%% must first be required with <c>ct:require/2</c>.</p>
-%%%
-%%% <p>Example:<br/>
-%%% <c>&gt; ct:require(unix_telnet, unix).</c><br/>
-%%% <c>ok</c><br/>
-%%% <c>&gt; ct_telnet:open(unix_telnet).</c><br/>
-%%% <c>{ok,&lt;0.105.0&gt;}</c><br/>
-%%% <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.
-%%%-----------------------------------------------------------------
-%%% @spec stop_interactive() -> ok
-%%%
-%%% @doc Exit the interactive mode.
-%%% @see start_interactive/0
stop_interactive() ->
ct_util:stop(normal),
ok.
@@ -295,384 +125,167 @@ stop_interactive() ->
%%% MISC INTERFACE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-%%%-----------------------------------------------------------------
-%%% @spec require(Required) -> ok | {error,Reason}
-%%% Required = Key | {Key,SubKeys} | {Key,SubKey,SubKeys}
-%%% Key = atom()
-%%% SubKeys = SubKey | [SubKey]
-%%% SubKey = atom()
-%%%
-%%% @doc Check if the required configuration is available. It is possible
-%%% to specify arbitrarily deep tuples as <c>Required</c>. Note that it is
-%%% only the last element of the tuple which can be a list of <c>SubKey</c>s.
-%%%
-%%% <p>Example 1: require the variable <c>myvar</c>:</p>
-%%% <pre>ok = ct:require(myvar).</pre>
-%%%
-%%% <p>In this case the config file must at least contain:</p>
-%%% <pre>{myvar,Value}.</pre>
-%%%
-%%% <p>Example 2: require the key <c>myvar</c> with
-%%% subkeys <c>sub1</c> and <c>sub2</c>:</p>
-%%% <pre>ok = ct:require({myvar,[sub1,sub2]}).</pre>
-%%%
-%%% <p>In this case the config file must at least contain:</p>
-%%% <pre>{myvar,[{sub1,Value},{sub2,Value}]}.</pre>
-%%%
-%%% <p>Example 3: require the key <c>myvar</c> with
-%%% subkey <c>sub1</c> with <c>subsub1</c>:</p>
-%%% <pre>ok = ct:require({myvar,sub1,sub2}).</pre>
-%%%
-%%% <p>In this case the config file must at least contain:</p>
-%%% <pre>{myvar,[{sub1,[{sub2,Value}]}]}.</pre>
-%%%
-%%% @see require/2
-%%% @see get_config/1
-%%% @see get_config/2
-%%% @see get_config/3
require(Required) ->
ct_config:require(Required).
-%%%-----------------------------------------------------------------
-%%% @spec require(Name,Required) -> ok | {error,Reason}
-%%% Name = atom()
-%%% Required = Key | {Key,SubKey} | {Key,SubKey,SubKey}
-%%% SubKey = Key
-%%% Key = atom()
-%%%
-%%% @doc Check if the required configuration is available, and give it
-%%% a name. The semantics for <c>Required</c> is the same as in
-%%% <c>required/1</c> except that it is not possible to specify a list
-%%% of <c>SubKey</c>s.
-%%%
-%%% <p>If the requested data is available, the sub entry will be
-%%% associated with <c>Name</c> so that the value of the element
-%%% can be read with <c>get_config/1,2</c> provided
-%%% <c>Name</c> instead of the whole <c>Required</c> term.</p>
-%%%
-%%% <p>Example: Require one node with a telnet connection and an
-%%% ftp connection. Name the node <c>a</c>:
-%%% <pre>ok = ct:require(a,{machine,node}).</pre>
-%%% All references to this node may then use the node name.
-%%% E.g. you can fetch a file over ftp like this:</p>
-%%% <pre>ok = ct:ftp_get(a,RemoteFile,LocalFile).</pre>
-%%%
-%%% <p>For this to work, the config file must at least contain:</p>
-%%% <pre>{machine,[{node,[{telnet,IpAddr},{ftp,IpAddr}]}]}.</pre>
-%%%
-%%% <note><p>The behaviour of this function changed radically in common_test
-%%% 1.6.2. In order too keep some backwards compatability it is still possible
-%%% to do: <br/><c>ct:require(a,{node,[telnet,ftp]}).</c><br/>
-%%% This will associate the name <c>a</c> with the top level <c>node</c> entry.
-%%% For this to work, the config file must at least contain:<br/>
-%%% <c>{node,[{telnet,IpAddr},{ftp,IpAddr}]}.</c></p></note>
-%%%
-%%% @see require/1
-%%% @see get_config/1
-%%% @see get_config/2
-%%% @see get_config/3
require(Name,Required) ->
ct_config:require(Name,Required).
-%%%-----------------------------------------------------------------
-%%% @spec get_config(Required) -> Value
-%%% @equiv get_config(Required,undefined,[])
get_config(Required) ->
ct_config:get_config(Required,undefined,[]).
-%%%-----------------------------------------------------------------
-%%% @spec get_config(Required,Default) -> Value
-%%% @equiv get_config(Required,Default,[])
get_config(Required,Default) ->
ct_config:get_config(Required,Default,[]).
-%%%-----------------------------------------------------------------
-%%% @spec get_config(Required,Default,Opts) -> ValueOrElement
-%%% Required = KeyOrName | {KeyOrName,SubKey} | {KeyOrName,SubKey,SubKey}
-%%% KeyOrName = atom()
-%%% SubKey = atom()
-%%% Default = term()
-%%% Opts = [Opt] | []
-%%% Opt = element | all
-%%% ValueOrElement = term() | Default
-%%%
-%%% @doc Read config data values.
-%%%
-%%% <p>This function returns the matching value(s) or config element(s),
-%%% given a config variable key or its associated name
-%%% (if one has been specified with <c>require/2</c> or a
-%%% require statement).</p>
-%%%
-%%% <p>Example, given the following config file:</p>
-%%% <pre>
-%%% {unix,[{telnet,IpAddr},
-%%% {user,[{username,Username},
-%%% {password,Password}]}]}.</pre>
-%%% <p><c>ct:get_config(unix,Default) ->
-%%% [{telnet,IpAddr},
-%%% {user, [{username,Username},
-%%% {password,Password}]}]</c><br/>
-%%% <c>ct:get_config({unix,telnet},Default) -> IpAddr</c><br/>
-%%% <c>ct:get_config({unix,user,username},Default) -> Username</c><br/>
-%%% <c>ct:get_config({unix,ftp},Default) -> Default</c><br/>
-%%% <c>ct:get_config(unknownkey,Default) -> Default</c></p>
-%%%
-%%% <p>If a config variable key has been associated with a name (by
-%%% means of <c>require/2</c> or a require statement), the name
-%%% may be used instead of the key to read the value:</p>
-%%%
-%%% <p><c>ct:require(myuser,{unix,user}) -> ok.</c><br/>
-%%% <c>ct:get_config(myuser,Default) ->
-%%% [{username,Username},
-%%% {password,Password}]</c></p>
-%%%
-%%% <p>If a config variable is defined in multiple files and you want to
-%%% access all possible values, use the <c>all</c> option. The
-%%% values will be returned in a list and the order of the elements
-%%% corresponds to the order that the config files were specified at
-%%% startup.</p>
-%%%
-%%% <p>If you want config elements (key-value tuples) returned as result
-%%% instead of values, use the <c>element</c> option.
-%%% The returned elements will then be on the form <c>{Required,Value}</c></p>
-%%%
-%%% @see get_config/1
-%%% @see get_config/2
-%%% @see require/1
-%%% @see require/2
get_config(Required,Default,Opts) ->
ct_config:get_config(Required,Default,Opts).
-%%%-----------------------------------------------------------------
-%%% @spec reload_config(Required) -> ValueOrElement
-%%% Required = KeyOrName | {KeyOrName,SubKey} | {KeyOrName,SubKey,SubKey}
-%%% KeyOrName = atom()
-%%% SubKey = atom()
-%%% ValueOrElement = term()
-%%%
-%%% @doc Reload config file which contains specified configuration key.
-%%%
-%%% <p>This function performs updating of the configuration data from which the
-%%% given configuration variable was read, and returns the (possibly) new
-%%% value of this variable.</p>
-%%% <p>Note that if some variables were present in the configuration but are not loaded
-%%% using this function, they will be removed from the configuration table together
-%%% with their aliases.</p>
-%%%
reload_config(Required)->
ct_config:reload_config(Required).
-%%%-----------------------------------------------------------------
-%%% @spec log(Format) -> ok
-%%% @equiv log(default,50,Format,[])
+get_testspec_terms() ->
+ case ct_util:get_testdata(testspec) of
+ undefined ->
+ undefined;
+ CurrSpecRec ->
+ ct_testspec:testspec_rec2list(CurrSpecRec)
+ end.
+
+get_testspec_terms(Tags) ->
+ case ct_util:get_testdata(testspec) of
+ undefined ->
+ undefined;
+ CurrSpecRec ->
+ ct_testspec:testspec_rec2list(Tags, CurrSpecRec)
+ end.
+
+escape_chars(IoList) ->
+ ct_logs:escape_chars(IoList).
+
+escape_chars(Format, Args) ->
+ try io_lib:format(Format, Args) of
+ IoList ->
+ ct_logs:escape_chars(IoList)
+ catch
+ _:Reason ->
+ {error,Reason}
+ end.
+
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)
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).
-
-%%%-----------------------------------------------------------------
-%%% @spec log(X1,X2,X3) -> ok
-%%% X1 = Category | Importance
-%%% X2 = Importance | Format
-%%% X3 = Format | Args
-%%% @equiv log(Category,Importance,Format,Args)
+ 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}
+ {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).
-
-%%%-----------------------------------------------------------------
-%%% @spec log(Category,Importance,Format,Args) -> ok
-%%% Category = atom()
-%%% Importance = integer()
-%%% Format = string()
-%%% Args = list()
-%%%
-%%% @doc Printout from a test case to the log file.
-%%%
-%%% <p>This function is meant for printing a string directly from a
-%%% test case to the test case log file.</p>
-%%%
-%%% <p>Default <c>Category</c> is <c>default</c>,
-%%% default <c>Importance</c> is <c>?STD_IMPORTANCE</c>,
-%%% 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).
-
-
-%%%-----------------------------------------------------------------
-%%% @spec print(Format) -> ok
-%%% @equiv print(default,50,Format,[])
+ 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,Opts).
+
+log(Category,Importance,Format,Args,Opts) ->
+ ct_logs:tc_log(Category,Importance,Format,Args,Opts).
+
print(Format) ->
- print(default,?STD_IMPORTANCE,Format,[]).
+ print(default,?STD_IMPORTANCE,Format,[],[]).
-%%%-----------------------------------------------------------------
-%%% @spec print(X1,X2) -> ok
-%%% X1 = Category | Importance | Format
-%%% X2 = Format | Args
-%%% @equiv print(Category,Importance,Format,Args)
print(X1,X2) ->
{Category,Importance,Format,Args} =
if is_atom(X1) -> {X1,?STD_IMPORTANCE,X2,[]};
is_integer(X1) -> {default,X1,X2,[]};
is_list(X1) -> {default,?STD_IMPORTANCE,X1,X2}
end,
- print(Category,Importance,Format,Args).
-
-%%%-----------------------------------------------------------------
-%%% @spec print(X1,X2,X3) -> ok
-%%% X1 = Category | Importance
-%%% X2 = Importance | Format
-%%% X3 = Format | Args
-%%% @equiv print(Category,Importance,Format,Args)
+ print(Category,Importance,Format,Args,[]).
+
print(X1,X2,X3) ->
- {Category,Importance,Format,Args} =
- if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[]};
- is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3};
- is_integer(X1) -> {default,X1,X2,X3}
+ {Category,Importance,Format,Args,Opts} =
+ if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[],[]};
+ is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,[]};
+ is_integer(X1) -> {default,X1,X2,X3,[]};
+ is_list(X1), is_list(X2) -> {default,?STD_IMPORTANCE,X1,X2,X3}
end,
- print(Category,Importance,Format,Args).
-
-%%%-----------------------------------------------------------------
-%%% @spec print(Category,Importance,Format,Args) -> ok
-%%% Category = atom()
-%%% Importance = integer()
-%%% Format = string()
-%%% Args = list()
-%%%
-%%% @doc Printout from a test case to the console.
-%%%
-%%% <p>This function is meant for printing a string from a test case
-%%% to the console.</p>
-%%%
-%%% <p>Default <c>Category</c> is <c>default</c>,
-%%% default <c>Importance</c> is <c>?STD_IMPORTANCE</c>,
-%%% and default value for <c>Args</c> is <c>[]</c>.</p>
-%%% <p>Please see the User's Guide for details on <c>Category</c>
-%%% and <c>Importance</c>.</p>
-print(Category,Importance,Format,Args) ->
- ct_logs:tc_print(Category,Importance,Format,Args).
-
-
-%%%-----------------------------------------------------------------
-%%% @spec pal(Format) -> ok
-%%% @equiv pal(default,50,Format,[])
+ print(Category,Importance,Format,Args,Opts).
+
+print(X1,X2,X3,X4) ->
+ {Category,Importance,Format,Args,Opts} =
+ if is_atom(X1), is_integer(X2) -> {X1,X2,X3,X4,[]};
+ is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,X4};
+ is_integer(X1) -> {default,X1,X2,X3,X4}
+ end,
+ print(Category,Importance,Format,Args,Opts).
+
+print(Category,Importance,Format,Args,Opts) ->
+ ct_logs:tc_print(Category,Importance,Format,Args,Opts).
+
pal(Format) ->
pal(default,?STD_IMPORTANCE,Format,[]).
-%%%-----------------------------------------------------------------
-%%% @spec pal(X1,X2) -> ok
-%%% X1 = Category | Importance | Format
-%%% X2 = Format | Args
-%%% @equiv pal(Category,Importance,Format,Args)
pal(X1,X2) ->
{Category,Importance,Format,Args} =
if is_atom(X1) -> {X1,?STD_IMPORTANCE,X2,[]};
is_integer(X1) -> {default,X1,X2,[]};
is_list(X1) -> {default,?STD_IMPORTANCE,X1,X2}
end,
- pal(Category,Importance,Format,Args).
-
-%%%-----------------------------------------------------------------
-%%% @spec pal(X1,X2,X3) -> ok
-%%% X1 = Category | Importance
-%%% X2 = Importance | Format
-%%% X3 = Format | Args
-%%% @equiv pal(Category,Importance,Format,Args)
+ pal(Category,Importance,Format,Args,[]).
+
pal(X1,X2,X3) ->
- {Category,Importance,Format,Args} =
- if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[]};
- is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3};
- is_integer(X1) -> {default,X1,X2,X3}
+ {Category,Importance,Format,Args,Opts} =
+ if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[],[]};
+ is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,[]};
+ is_integer(X1) -> {default,X1,X2,X3,[]};
+ is_list(X1), is_list(X2) -> {default,?STD_IMPORTANCE,X1,X2,X3}
end,
- pal(Category,Importance,Format,Args).
-
-%%%-----------------------------------------------------------------
-%%% @spec pal(Category,Importance,Format,Args) -> ok
-%%% Category = atom()
-%%% Importance = integer()
-%%% Format = string()
-%%% Args = list()
-%%%
-%%% @doc Print and log from a test case.
-%%%
-%%% <p>This function is meant for printing a string from a test case,
-%%% both to the test case log file and to the console.</p>
-%%%
-%%% <p>Default <c>Category</c> is <c>default</c>,
-%%% default <c>Importance</c> is <c>?STD_IMPORTANCE</c>,
-%%% and default value for <c>Args</c> is <c>[]</c>.</p>
-%%% <p>Please see the User's Guide for details on <c>Category</c>
-%%% and <c>Importance</c>.</p>
-pal(Category,Importance,Format,Args) ->
- ct_logs:tc_pal(Category,Importance,Format,Args).
-
-%%%-----------------------------------------------------------------
-%%% @spec capture_start() -> ok
-%%%
-%%% @doc Start capturing all text strings printed to stdout during
-%%% execution of the test case.
-%%%
-%%% @see capture_stop/0
-%%% @see capture_get/1
+ pal(Category,Importance,Format,Args,Opts).
+
+pal(X1,X2,X3,X4) ->
+ {Category,Importance,Format,Args,Opts} =
+ if is_atom(X1), is_integer(X2) -> {X1,X2,X3,X4,[]};
+ is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3,X4};
+ is_integer(X1) -> {default,X1,X2,X3,X4}
+ end,
+ pal(Category,Importance,Format,Args,Opts).
+
+pal(Category,Importance,Format,Args,Opts) ->
+ ct_logs:tc_pal(Category,Importance,Format,Args,Opts).
+
+set_verbosity(Category, Level) ->
+ ct_util:set_verbosity({Category,Level}).
+
+get_verbosity(Category) ->
+ ct_util:get_verbosity(Category).
+
capture_start() ->
test_server:capture_start().
-%%%-----------------------------------------------------------------
-%%% @spec capture_stop() -> ok
-%%%
-%%% @doc Stop capturing text strings (a session started with
-%%% <c>capture_start/0</c>).
-%%%
-%%% @see capture_start/0
-%%% @see capture_get/1
capture_stop() ->
test_server:capture_stop().
-%%%-----------------------------------------------------------------
-%%% @spec capture_get() -> ListOfStrings
-%%% ListOfStrings = [string()]
-%%%
-%%% @equiv capture_get([default])
capture_get() ->
%% remove default log printouts (e.g. ct:log/2 printouts)
capture_get([default]).
-%%%-----------------------------------------------------------------
-%%% @spec capture_get(ExclCategories) -> ListOfStrings
-%%% ExclCategories = [atom()]
-%%% ListOfStrings = [string()]
-%%%
-%%% @doc Return and purge the list of text strings buffered
-%%% during the latest session of capturing printouts to stdout.
-%%% With <c>ExclCategories</c> it's possible to specify
-%%% log categories that should be ignored in <c>ListOfStrings</c>.
-%%% If <c>ExclCategories = []</c>, no filtering takes place.
-%%%
-%%% @see capture_start/0
-%%% @see capture_stop/0
-%%% @see log/3
capture_get([ExclCat | ExclCategories]) ->
Strs = test_server:capture_get(),
CatsStr = [atom_to_list(ExclCat) |
[[$| | atom_to_list(EC)] || EC <- ExclCategories]],
- {ok,MP} = re:compile("<div class=\"(" ++ lists:flatten(CatsStr) ++ ")\">.*"),
+ {ok,MP} = re:compile("<div class=\"(" ++ lists:flatten(CatsStr) ++ ")\">.*",
+ [unicode]),
lists:flatmap(fun(Str) ->
case re:run(Str, MP) of
{match,_} -> [];
@@ -683,40 +296,26 @@ capture_get([ExclCat | ExclCategories]) ->
capture_get([]) ->
test_server:capture_get().
-%%%-----------------------------------------------------------------
-%%% @spec fail(Reason) -> void()
-%%% Reason = term()
-%%%
-%%% @doc Terminate a test case with the given error
-%%% <c>Reason</c>.
fail(Reason) ->
try
exit({test_case_failed,Reason})
catch
- Class:R ->
- case erlang:get_stacktrace() of
+ Class:R:S ->
+ case S of
[{?MODULE,fail,1,_}|Stk] -> ok;
Stk -> ok
end,
erlang:raise(Class, R, Stk)
end.
-%%%-----------------------------------------------------------------
-%%% @spec fail(Format, Args) -> void()
-%%% Format = string()
-%%% Args = list()
-%%%
-%%% @doc Terminate a test case with an error message specified
-%%% by a format string and a list of values (used as arguments to
-%%% <c>io_lib:format/2</c>).
fail(Format, Args) ->
try io_lib:format(Format, Args) of
Str ->
try
exit({test_case_failed,lists:flatten(Str)})
catch
- Class:R ->
- case erlang:get_stacktrace() of
+ Class:R:S ->
+ case S of
[{?MODULE,fail,2,_}|Stk] -> ok;
Stk -> ok
end,
@@ -727,42 +326,19 @@ fail(Format, Args) ->
exit({BadArgs,{?MODULE,fail,[Format,Args]}})
end.
-%%%-----------------------------------------------------------------
-%%% @spec comment(Comment) -> void()
-%%% Comment = term()
-%%%
-%%% @doc Print the given <c>Comment</c> in the comment field in
-%%% the table on the test suite result page.
-%%%
-%%% <p>If called several times, only the last comment is printed.
-%%% The test case return value <c>{comment,Comment}</c>
-%%% overwrites the string set by this function.</p>
comment(Comment) when is_list(Comment) ->
Formatted =
case (catch io_lib:format("~ts",[Comment])) of
{'EXIT',_} -> % it's a list not a string
- io_lib:format("~p",[Comment]);
+ io_lib:format("~tp",[Comment]);
String ->
String
end,
send_html_comment(lists:flatten(Formatted));
comment(Comment) ->
- Formatted = io_lib:format("~p",[Comment]),
+ Formatted = io_lib:format("~tp",[Comment]),
send_html_comment(lists:flatten(Formatted)).
-%%%-----------------------------------------------------------------
-%%% @spec comment(Format, Args) -> void()
-%%% Format = string()
-%%% Args = list()
-%%%
-%%% @doc Print the formatted string in the comment field in
-%%% the table on the test suite result page.
-%%%
-%%% <p>The <c>Format</c> and <c>Args</c> arguments are
-%%% used in call to <c>io_lib:format/2</c> in order to create
-%%% the comment string. The behaviour of <c>comment/2</c> is
-%%% otherwise the same as the <c>comment/1</c> function (see
-%%% above for details).</p>
comment(Format, Args) when is_list(Format), is_list(Args) ->
Formatted =
case (catch io_lib:format(Format, Args)) of
@@ -778,64 +354,28 @@ send_html_comment(Comment) ->
ct_util:set_testdata({{comment,group_leader()},Html}),
test_server:comment(Html).
-%%%-----------------------------------------------------------------
-%%% @spec make_priv_dir() -> ok | {error,Reason}
-%%% Reason = term()
-%%% @doc If the test has been started with the create_priv_dir
-%%% option set to manual_per_tc, in order for the test case to use
-%%% the private directory, it must first create it by calling
-%%% this function.
make_priv_dir() ->
test_server:make_priv_dir().
-%%%-----------------------------------------------------------------
-%%% @spec get_target_name(Handle) -> {ok,TargetName} | {error,Reason}
-%%% Handle = handle()
-%%% TargetName = target_name()
-%%% @doc Return the name of the target that the given connection
-%%% belongs to.
get_target_name(Handle) ->
ct_util:get_target_name(Handle).
-
-%%%-----------------------------------------------------------------
-%%% @spec parse_table(Data) -> {Heading,Table}
-%%% Data = [string()]
-%%% Heading = tuple()
-%%% Table = [tuple()]
-%%%
-%%% @doc Parse the printout from an SQL table and return a list of tuples.
-%%%
-%%% <p>The printout to parse would typically be the result of a
-%%% <c>select</c> command in SQL. The returned
-%%% <c>Table</c> is a list of tuples, where each tuple is a row
-%%% in the table.</p>
-%%%
-%%% <p><c>Heading</c> is a tuple of strings representing the
-%%% headings of each column in the table.</p>
+
+-spec get_progname() -> string().
+
+get_progname() ->
+ case init:get_argument(progname) of
+ {ok, [[Prog]]} ->
+ Prog;
+ _Other ->
+ "no_prog_name"
+ end.
+
parse_table(Data) ->
ct_util:parse_table(Data).
-%%%-----------------------------------------------------------------
-%%% @spec listenv(Telnet) -> [Env]
-%%% Telnet = term()
-%%% Env = {Key,Value}
-%%% Key = string()
-%%% Value = string()
-%%%
-%%% @doc Performs the listenv command on the given telnet connection
-%%% and returns the result as a list of Key-Value pairs.
listenv(Telnet) ->
ct_util:listenv(Telnet).
-
-%%%-----------------------------------------------------------------
-%%% @spec testcases(TestDir, Suite) -> Testcases | {error,Reason}
-%%% TestDir = string()
-%%% Suite = atom()
-%%% Testcases = list()
-%%% Reason = term()
-%%%
-%%% @doc Returns all test cases in the specified suite.
testcases(TestDir, Suite) ->
case make_and_load(TestDir, Suite) of
E = {error,_} ->
@@ -853,7 +393,7 @@ make_and_load(Dir, Suite) ->
EnvInclude =
case os:getenv("CT_INCLUDE_PATH") of
false -> [];
- CtInclPath -> string:tokens(CtInclPath, [$:,$ ,$,])
+ CtInclPath -> string:lexemes(CtInclPath, [$:,$ ,$,])
end,
StartInclude =
case init:get_argument(include) of
@@ -875,15 +415,6 @@ make_and_load(Dir, Suite) ->
end
end.
-%%%-----------------------------------------------------------------
-%%% @spec userdata(TestDir, Suite) -> SuiteUserData | {error,Reason}
-%%% TestDir = string()
-%%% Suite = atom()
-%%% SuiteUserData = [term()]
-%%% Reason = term()
-%%%
-%%% @doc Returns any data specified with the tag <c>userdata</c>
-%%% in the list of tuples returned from <c>Suite:suite/0</c>.
userdata(TestDir, Suite) ->
case make_and_load(TestDir, Suite) of
E = {error,_} ->
@@ -909,18 +440,6 @@ get_userdata(List, _) when is_list(List) ->
get_userdata(_BadTerm, Spec) ->
{error,list_to_atom(Spec ++ " must return a list")}.
-%%%-----------------------------------------------------------------
-%%% @spec userdata(TestDir, Suite, GroupOrCase) -> TCUserData | {error,Reason}
-%%% TestDir = string()
-%%% Suite = atom()
-%%% GroupOrCase = {group,GroupName} | atom()
-%%% GroupName = atom()
-%%% TCUserData = [term()]
-%%% Reason = term()
-%%%
-%%% @doc Returns any data specified with the tag <c>userdata</c>
-%%% in the list of tuples returned from <c>Suite:group(GroupName)</c>
-%%% or <c>Suite:Case()</c>.
userdata(TestDir, Suite, {group,GroupName}) ->
case make_and_load(TestDir, Suite) of
E = {error,_} ->
@@ -939,27 +458,6 @@ userdata(TestDir, Suite, Case) when is_atom(Case) ->
get_userdata(Info, atom_to_list(Case)++"/0")
end.
-
-%%%-----------------------------------------------------------------
-%%% @spec get_status() -> TestStatus | {error,Reason} | no_tests_running
-%%% TestStatus = [StatusElem]
-%%% StatusElem = {current,TestCaseInfo} | {successful,Successful} |
-%%% {failed,Failed} | {skipped,Skipped} | {total,Total}
-%%% TestCaseInfo = {Suite,TestCase} | [{Suite,TestCase}]
-%%% Suite = atom()
-%%% TestCase = atom()
-%%% Successful = integer()
-%%% Failed = integer()
-%%% Skipped = {UserSkipped,AutoSkipped}
-%%% UserSkipped = integer()
-%%% AutoSkipped = integer()
-%%% Total = integer()
-%%% Reason = term()
-%%%
-%%% @doc Returns status of ongoing test. The returned list contains info about
-%%% which test case is currently executing (a list of cases when a
-%%% parallel test case group is executing), as well as counters for
-%%% successful, failed, skipped, and total test cases so far.
get_status() ->
case get_testdata(curr_tc) of
{ok,TestCase} ->
@@ -991,176 +489,37 @@ get_testdata(Key) ->
{ok,Data}
end.
-%%%-----------------------------------------------------------------
-%%% @spec abort_current_testcase(Reason) -> ok | {error,ErrorReason}
-%%% Reason = term()
-%%% ErrorReason = no_testcase_running | parallel_group
-%%%
-%%% @doc <p>When calling this function, the currently executing test case will be aborted.
-%%% It is the user's responsibility to know for sure which test case is currently
-%%% executing. The function is therefore only safe to call from a function which
-%%% has been called (or synchronously invoked) by the test case.</p>
-%%%
-%%% <p><c>Reason</c>, the reason for aborting the test case, is printed
-%%% in the test case log.</p>
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()
-%%% EncryptFileName = string()
-%%% Reason = term()
-%%%
-%%% @doc <p>This function encrypts the source config file with DES3 and
-%%% saves the result in file <c>EncryptFileName</c>. The key,
-%%% a string, must be available in a text file named
-%%% <c>.ct_config.crypt</c> in the current directory, or the
-%%% home directory of the user (it is searched for in that order).</p>
-%%% <p>See the Common Test User's Guide for information about using
-%%% encrypted config files when running tests.</p>
-%%% <p>See the <c>crypto</c> application for details on DES3
-%%% encryption/decryption.</p>
encrypt_config_file(SrcFileName, EncryptFileName) ->
ct_config:encrypt_config_file(SrcFileName, EncryptFileName).
-%%%-----------------------------------------------------------------
-%%% @spec encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile) ->
-%%% ok | {error,Reason}
-%%% SrcFileName = string()
-%%% EncryptFileName = string()
-%%% KeyOrFile = {key,string()} | {file,string()}
-%%% Reason = term()
-%%%
-%%% @doc <p>This function encrypts the source config file with DES3 and
-%%% saves the result in the target file <c>EncryptFileName</c>.
-%%% The encryption key to use is either the value in
-%%% <c>{key,Key}</c> or the value stored in the file specified
-%%% by <c>{file,File}</c>.</p>
-%%% <p>See the Common Test User's Guide for information about using
-%%% encrypted config files when running tests.</p>
-%%% <p>See the <c>crypto</c> application for details on DES3
-%%% encryption/decryption.</p>
encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile) ->
ct_config:encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile).
-%%%-----------------------------------------------------------------
-%%% @spec decrypt_config_file(EncryptFileName, TargetFileName) ->
-%%% ok | {error,Reason}
-%%% EncryptFileName = string()
-%%% TargetFileName = string()
-%%% Reason = term()
-%%%
-%%% @doc <p>This function decrypts <c>EncryptFileName</c>, previously
-%%% generated with <c>encrypt_config_file/2/3</c>. The original
-%%% file contents is saved in the target file. The encryption key, a
-%%% string, must be available in a text file named
-%%% <c>.ct_config.crypt</c> in the current directory, or the
-%%% home directory of the user (it is searched for in that order).</p>
decrypt_config_file(EncryptFileName, TargetFileName) ->
ct_config:decrypt_config_file(EncryptFileName, TargetFileName).
-%%%-----------------------------------------------------------------
-%%% @spec decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile) ->
-%%% ok | {error,Reason}
-%%% EncryptFileName = string()
-%%% TargetFileName = string()
-%%% KeyOrFile = {key,string()} | {file,string()}
-%%% Reason = term()
-%%%
-%%% @doc <p>This function decrypts <c>EncryptFileName</c>, previously
-%%% generated with <c>encrypt_config_file/2/3</c>. The original
-%%% file contents is saved in the target file. The key must have the
-%%% the same value as that used for encryption.</p>
decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile) ->
ct_config:decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile).
-
-%%%-----------------------------------------------------------------
-%%% @spec add_config(Callback, Config) -> ok | {error, Reason}
-%%% Callback = atom()
-%%% Config = string()
-%%% Reason = term()
-%%%
-%%% @doc <p>This function loads configuration variables using the
-%%% given callback module and configuration string. Callback module
-%%% should be either loaded or present in the code part. Loaded
-%%% configuration variables can later be removed using
-%%% <c>remove_config/2</c> function.</p>
add_config(Callback, Config)->
ct_config:add_config(Callback, Config).
-%%%-----------------------------------------------------------------
-%%% @spec remove_config(Callback, Config) -> ok
-%%% Callback = atom()
-%%% Config = string()
-%%% Reason = term()
-%%%
-%%% @doc <p>This function removes configuration variables (together with
-%%% their aliases) which were loaded with specified callback module and
-%%% configuration string.</p>
remove_config(Callback, Config) ->
ct_config:remove_config(Callback, Config).
-%%%-----------------------------------------------------------------
-%%% @spec timetrap(Time) -> ok
-%%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity | Func
-%%% Hours = integer()
-%%% Mins = integer()
-%%% Secs = integer()
-%%% Millisecs = integer() | float()
-%%% Func = {M,F,A} | fun()
-%%% M = atom()
-%%% F = atom()
-%%% A = list()
-%%%
-%%% @doc <p>Use this function to set a new timetrap for the running test case.
-%%% If the argument is <c>Func</c>, the timetrap will be triggered
-%%% when this function returns. <c>Func</c> may also return a new
-%%% <c>Time</c> value, which in that case will be the value for the
-%%% new timetrap.</p>
timetrap(Time) ->
test_server:timetrap_cancel(),
test_server:timetrap(Time).
-%%%-----------------------------------------------------------------
-%%% @spec get_timetrap_info() -> {Time,Scale}
-%%% Time = integer() | infinity
-%%% Scale = true | false
-%%%
-%%% @doc <p>Read info about the timetrap set for the current test case.
-%%% <c>Scale</c> indicates if Common Test will attempt to automatically
-%%% compensate timetraps for runtime delays introduced by e.g. tools like
-%%% cover.</p>
get_timetrap_info() ->
test_server:get_timetrap_info().
-%%%-----------------------------------------------------------------
-%%% @spec sleep(Time) -> ok
-%%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity
-%%% Hours = integer()
-%%% Mins = integer()
-%%% Secs = integer()
-%%% Millisecs = integer() | float()
-%%%
-%%% @doc <p>This function, similar to <c>timer:sleep/1</c>, suspends the test
-%%% case for specified time. However, this function also multiplies
-%%% <c>Time</c> with the 'multiply_timetraps' value (if set) and under
-%%% certain circumstances also scales up the time automatically
-%%% if 'scale_timetraps' is set to true (default is false).</p>
sleep({hours,Hs}) ->
sleep(trunc(Hs * 1000 * 60 * 60));
sleep({minutes,Ms}) ->
@@ -1170,52 +529,12 @@ sleep({seconds,Ss}) ->
sleep(Time) ->
test_server:adjusted_sleep(Time).
-%%%-----------------------------------------------------------------
-%%% @spec notify(Name,Data) -> ok
-%%% Name = atom()
-%%% Data = term()
-%%%
-%%% @doc <p>Sends a asynchronous notification of type <c>Name</c> with
-%%% <c>Data</c>to the common_test event manager. This can later be
-%%% caught by any installed event manager. </p>
-%%% @see //stdlib/gen_event
notify(Name,Data) ->
ct_event:notify(Name, Data).
-%%%-----------------------------------------------------------------
-%%% @spec sync_notify(Name,Data) -> ok
-%%% Name = atom()
-%%% Data = term()
-%%%
-%%% @doc <p>Sends a synchronous notification of type <c>Name</c> with
-%%% <c>Data</c>to the common_test event manager. This can later be
-%%% caught by any installed event manager. </p>
-%%% @see //stdlib/gen_event
sync_notify(Name,Data) ->
ct_event:sync_notify(Name, Data).
-%%%-----------------------------------------------------------------
-%%% @spec break(Comment) -> ok | {error,Reason}
-%%% Comment = string()
-%%% Reason = {multiple_cases_running,TestCases} |
-%%% 'enable break with release_shell option'
-%%% TestCases = [atom()]
-%%%
-%%% @doc <p>This function will cancel any active timetrap and pause the
-%%% execution of the current test case until the user calls the
-%%% <c>continue/0</c> function. It gives the user the opportunity
-%%% to interact with the erlang node running the tests, e.g. for
-%%% debugging purposes or for manually executing a part of the
-%%% test case. If a parallel group is executing, <c>break/2</c>
-%%% should be called instead.</p>
-%%% <p>A cancelled timetrap will not be automatically
-%%% reactivated after the break, but must be started exlicitly with
-%%% <c>ct:timetrap/1</c></p>
-%%% <p>In order for the break/continue functionality to work,
-%%% Common Test must release the shell process controlling stdin.
-%%% This is done by setting the <c>release_shell</c> start option
-%%% to <c>true</c>. See the User's Guide for more information.</p>
-
break(Comment) ->
case {ct_util:get_testdata(starter),
ct_util:get_testdata(release_shell)} of
@@ -1238,19 +557,6 @@ break(Comment) ->
end
end.
-%%%-----------------------------------------------------------------
-%%% @spec break(TestCase, Comment) -> ok | {error,Reason}
-%%% TestCase = atom()
-%%% Comment = string()
-%%% Reason = 'test case not running' |
-%%% 'enable break with release_shell option'
-%%%
-%%% @doc <p>This function works the same way as <c>break/1</c>,
-%%% only the <c>TestCase</c> argument makes it possible to
-%%% pause a test case executing in a parallel group. The
-%%% <c>continue/1</c> function should be used to resume
-%%% execution of <c>TestCase</c>.</p>
-%%% <p>See <c>break/1</c> for more details.</p>
break(TestCase, Comment) ->
case {ct_util:get_testdata(starter),
ct_util:get_testdata(release_shell)} of
@@ -1277,23 +583,12 @@ break(TestCase, Comment) ->
end
end.
-%%%-----------------------------------------------------------------
-%%% @spec continue() -> ok
-%%%
-%%% @doc <p>This function must be called in order to continue after a
-%%% test case (not executing in a parallel group) has called
-%%% <c>break/1</c>.</p>
continue() ->
test_server:continue().
-%%%-----------------------------------------------------------------
-%%% @spec continue(TestCase) -> ok
-%%% TestCase = atom()
-%%%
-%%% @doc <p>This function must be called in order to continue after a
-%%% test case has called <c>break/2</c>. If the paused test case,
-%%% <c>TestCase</c>, executes in a parallel group, this
-%%% function - rather than <c>continue/0</c> - must be used
-%%% in order to let the test case proceed.</p>
continue(TestCase) ->
test_server:continue(TestCase).
+
+
+remaining_test_procs() ->
+ ct_util:remaining_test_procs().
diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl
index 5c80a299f8..a10d939919 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-2018. 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,6 +81,7 @@ start(Mode) ->
do_start(Parent) ->
process_flag(trap_exit,true),
+ ct_util:mark_process(),
register(ct_config_server,self()),
ct_util:create_table(?attr_table,bag,#ct_conf.key),
{ok,StartDir} = file:get_cwd(),
@@ -118,7 +120,8 @@ call(Msg) ->
end.
return({To,Ref},Result) ->
- To ! {Ref, Result}.
+ To ! {Ref, Result},
+ ok.
loop(StartDir) ->
receive
@@ -127,11 +130,11 @@ loop(StartDir) ->
return(From,Result),
loop(StartDir);
{{set_default_config,{Config,Scope}},From} ->
- set_config(Config,{true,Scope}),
+ _ = set_config(Config,{true,Scope}),
return(From,ok),
loop(StartDir);
{{set_default_config,{Name,Config,Scope}},From} ->
- set_config(Name,Config,{true,Scope}),
+ _ = set_config(Name,Config,{true,Scope}),
return(From,ok),
loop(StartDir);
{{delete_default_config,Scope},From} ->
@@ -148,7 +151,7 @@ loop(StartDir) ->
loop(StartDir);
{{stop},From} ->
ets:delete(?attr_table),
- file:set_cwd(StartDir),
+ ok = file:set_cwd(StartDir),
return(From,ok)
end.
@@ -169,8 +172,8 @@ reload_config(KeyOrName) ->
process_default_configs(Opts) ->
lists:flatmap(fun({config,[_|_] = FileOrFiles}) ->
- case {io_lib:printable_list(FileOrFiles),
- io_lib:printable_list(hd(FileOrFiles))} of
+ case {io_lib:printable_unicode_list(FileOrFiles),
+ io_lib:printable_unicode_list(hd(FileOrFiles))} of
{false,true} ->
FileOrFiles;
{true,false} ->
@@ -256,7 +259,7 @@ read_config_files(Opts) ->
read_config_files_int([{Callback, File}|Files], FunToSave) ->
case Callback:read_config(File) of
{ok, Config} ->
- FunToSave(Config, Callback, File),
+ _ = FunToSave(Config, Callback, File),
read_config_files_int(Files, FunToSave);
{error, {ErrorName, ErrorDetail}} ->
{user_error, {ErrorName, File, ErrorDetail}};
@@ -266,6 +269,15 @@ read_config_files_int([{Callback, File}|Files], FunToSave) ->
read_config_files_int([], _FunToSave) ->
ok.
+
+read_config_files(ConfigFiles, FunToSave) ->
+ case read_config_files_int(ConfigFiles, FunToSave) of
+ {user_error, Error} ->
+ {error, Error};
+ ok ->
+ ok
+ end.
+
store_config(Config, Callback, File) when is_tuple(Config) ->
store_config([Config], Callback, File);
@@ -454,8 +466,12 @@ reload_conf(KeyOrName) ->
undefined;
HandlerList ->
HandlerList2 = lists:usort(HandlerList),
- read_config_files_int(HandlerList2, fun rewrite_config/3),
- get_config(KeyOrName)
+ case read_config_files(HandlerList2, fun rewrite_config/3) of
+ ok ->
+ get_config(KeyOrName);
+ Error ->
+ Error
+ end
end.
release_allocated() ->
@@ -489,16 +505,16 @@ associate(Name,_Key,Configs) ->
associate_int(Name,Configs,os:getenv("COMMON_TEST_ALIAS_TOP")).
associate_int(Name,Configs,"true") ->
- lists:map(fun({K,_Config}) ->
+ lists:foreach(fun({K,_Config}) ->
Cs = ets:match_object(
?attr_table,
#ct_conf{key=element(1,K),
name='_UNDEF',_='_'}),
[ets:insert(?attr_table,C#ct_conf{name=Name})
|| C <- Cs]
- end,Configs);
+ end,Configs);
associate_int(Name,Configs,_) ->
- lists:map(fun({K,Config}) ->
+ lists:foreach(fun({K,Config}) ->
Key = if is_tuple(K) -> element(1,K);
is_atom(K) -> K
end,
@@ -510,7 +526,7 @@ associate_int(Name,Configs,_) ->
[ets:insert(?attr_table,C#ct_conf{name=Name,
value=Config})
|| C <- Cs]
- end,Configs).
+ end,Configs).
@@ -575,7 +591,7 @@ encrypt_config_file(SrcFileName, EncryptFileName, {file,KeyFile}) ->
end;
encrypt_config_file(SrcFileName, EncryptFileName, {key,Key}) ->
- crypto:start(),
+ _ = crypto:start(),
{Key,IVec} = make_crypto_key(Key),
case file:read_file(SrcFileName) of
{ok,Bin0} ->
@@ -614,7 +630,7 @@ decrypt_config_file(EncryptFileName, TargetFileName, {file,KeyFile}) ->
end;
decrypt_config_file(EncryptFileName, TargetFileName, {key,Key}) ->
- crypto:start(),
+ _ = crypto:start(),
{Key,IVec} = make_crypto_key(Key),
case file:read_file(EncryptFileName) of
{ok,Bin} ->
@@ -644,7 +660,7 @@ decrypt_config_file(EncryptFileName, TargetFileName, {key,Key}) ->
get_crypt_key_from_file(File) ->
case file:read_file(File) of
{ok,Bin} ->
- case catch string:tokens(binary_to_list(Bin), [$\n,$\r]) of
+ case catch string:lexemes(binary_to_list(Bin), [$\n, [$\r,$\n]]) of
[Key] ->
Key;
_ ->
@@ -678,7 +694,7 @@ get_crypt_key_from_file() ->
noent ->
Result;
_ ->
- case catch string:tokens(binary_to_list(Result), [$\n,$\r]) of
+ case catch string:lexemes(binary_to_list(Result), [$\n, [$\r,$\n]]) of
[Key] ->
io:format("~nCrypt key file: ~ts~n", [FullName]),
Key;
@@ -693,12 +709,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 +793,13 @@ prepare_config_list(Args) ->
% TODO: add logging of the loaded configuration file to the CT FW log!!!
add_config(Callback, []) ->
- read_config_files_int([{Callback, []}], fun store_config/3);
+ read_config_files([{Callback, []}], fun store_config/3);
add_config(Callback, [File|_Files]=Config) when is_list(File) ->
lists:foreach(fun(CfgStr) ->
- read_config_files_int([{Callback, CfgStr}], fun store_config/3) end,
+ read_config_files([{Callback, CfgStr}], fun store_config/3) end,
Config);
add_config(Callback, [C|_]=Config) when is_integer(C) ->
- read_config_files_int([{Callback, Config}], fun store_config/3),
- ok.
+ read_config_files([{Callback, Config}], fun store_config/3).
remove_config(Callback, Config) ->
ets:match_delete(?attr_table,
diff --git a/lib/common_test/src/ct_config_plain.erl b/lib/common_test/src/ct_config_plain.erl
index c6547f0a40..7b68ac6597 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-2018. 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%
%%----------------------------------------------------------------------
@@ -105,7 +106,7 @@ read_config_terms1({done,{eof,EL},_}, L, _, _) ->
read_config_terms1({done,{error,Info,EL},_}, L, _, _) ->
{error,{Info,{L,EL}}};
read_config_terms1({more,_}, L, Terms, Rest) ->
- case string:tokens(Rest, [$\n,$\r,$\t]) of
+ case string:lexemes(Rest, [$\n,[$\r,$\n],$\t]) of
[] ->
lists:reverse(Terms);
_ ->
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 cff02a46d9..3e83020f45 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-2017. 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%
%%
@@ -34,6 +35,8 @@
-define(WIDTH,80).
+-define(now, os:timestamp()).
+
%%%-----------------------------------------------------------------
%%% Callbacks
init({GL,ConnLogs}) ->
@@ -72,14 +75,14 @@ handle_event({_Type, GL, _Msg}, State) when node(GL) /= node() ->
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),
+ write_report(?now,Info,Report,GL,State),
{ok, State};
handle_event({_Type,GL,{Pid,Info=#conn_log{},Report}}, State) ->
- write_report(now(),conn_info(Pid,Info),Report,GL,State),
+ write_report(?now,conn_info(Pid,Info),Report,GL,State),
{ok, 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,GL,State),
+ write_error(?now,conn_info(Pid,#conn_log{name=ConnName}),R,GL,State),
{ok, State};
handle_event(_What, State) ->
{ok, State}.
@@ -101,40 +104,63 @@ terminate(_,#state{logs=Logs}) ->
%%%-----------------------------------------------------------------
%%% Writing reports
write_report(_Time,#conn_log{header=false,module=ConnMod}=Info,Data,GL,State) ->
- {LogType,Fd} = get_log(Info,GL,State),
- io:format(Fd,"~n~ts",[format_data(ConnMod,LogType,Data)]);
+ case get_log(Info,GL,State) of
+ {silent,_,_} ->
+ ok;
+ {LogType,Dest,Fd} ->
+ Str = if LogType == html, Dest == gl -> ["$tc_html","~n~ts"];
+ true -> "~n~ts"
+ end,
+ io:format(Fd,Str,[format_data(ConnMod,LogType,Data)])
+ end;
write_report(Time,#conn_log{module=ConnMod}=Info,Data,GL,State) ->
- {LogType,Fd} = get_log(Info,GL,State),
- io:format(Fd,"~n~ts~ts~ts",[format_head(ConnMod,LogType,Time),
- format_title(LogType,Info),
- format_data(ConnMod,LogType,Data)]).
+ case get_log(Info,GL,State) of
+ {silent,_,_} ->
+ ok;
+ {LogType,Dest,Fd} ->
+ case format_data(ConnMod,LogType,Data) of
+ [] when Info#conn_log.action==send; Info#conn_log.action==recv ->
+ ok;
+ FormattedData ->
+ Str = if LogType == html, Dest == gl ->
+ ["$tc_html","~n~ts~ts~ts"];
+ true ->
+ "~n~ts~ts~ts"
+ end,
+ io:format(Fd,Str,[format_head(ConnMod,LogType,Time),
+ format_title(LogType,Info),
+ FormattedData])
+ end
+ end.
write_error(Time,#conn_log{module=ConnMod}=Info,Report,GL,State) ->
case get_log(Info,GL,State) of
- {html,_} ->
+ {LogType,_,_} when LogType==html; LogType==silent ->
%% The error will anyway be written in the html log by the
%% sasl error handler, so don't write it again.
ok;
- {LogType,Fd} ->
- io:format(Fd,"~n~ts~ts~ts",
- [format_head(ConnMod,LogType,Time," ERROR"),
- format_title(LogType,Info),
- format_error(LogType,Report)])
+ {LogType,Dest,Fd} ->
+ Str = if LogType == html, Dest == gl -> ["$tc_html","~n~ts~ts~ts"];
+ true -> "~n~ts~ts~ts"
+ end,
+ io:format(Fd,Str,[format_head(ConnMod,LogType,Time," ERROR"),
+ format_title(LogType,Info),
+ format_error(LogType,Report)])
end.
get_log(Info,GL,State) ->
case proplists:get_value(GL,State#state.logs) of
undefined ->
- {html,State#state.default_gl};
+ {html,gl,State#state.default_gl};
ConnLogs ->
case proplists:get_value(Info#conn_log.module,ConnLogs) of
{html,_} ->
- {html,GL};
+ {html,gl,GL};
{LogType,Fds} ->
- {LogType,get_fd(Info,Fds)};
+ {LogType,file,get_fd(Info,Fds)};
undefined ->
- {html,GL}
+ {html,gl,GL}
end
end.
@@ -160,7 +186,7 @@ format_head(ConnMod,_,Time,Text) ->
io_lib:format("~n~ts",[Head]).
format_title(raw,#conn_log{client=Client}=Info) ->
- io_lib:format("Client ~w ~s ~ts",[Client,actionstr(Info),serverstr(Info)]);
+ io_lib:format("Client ~tw ~s ~ts",[Client,actionstr(Info),serverstr(Info)]);
format_title(_,Info) ->
Title = pad_char_end(?WIDTH,pretty_title(Info),$=),
io_lib:format("~n~ts", [Title]).
@@ -171,9 +197,9 @@ format_data(ConnMod,LogType,Data) ->
ConnMod:format_data(LogType,Data).
format_error(raw,Report) ->
- io_lib:format("~n~p~n",[Report]);
+ io_lib:format("~n~tp~n",[Report]);
format_error(pretty,Report) ->
- [io_lib:format("~n ~p: ~p",[K,V]) || {K,V} <- Report].
+ [io_lib:format("~n ~tp: ~tp",[K,V]) || {K,V} <- Report].
%%%-----------------------------------------------------------------
@@ -198,13 +224,13 @@ now_to_time({_,_,MicroS}=Now) ->
{calendar:now_to_local_time(Now),MicroS}.
pretty_head({{{Y,Mo,D},{H,Mi,S}},MicroS},ConnMod,Text0) ->
- Text = string:to_upper(atom_to_list(ConnMod) ++ Text0),
+ Text = string:uppercase(atom_to_list(ConnMod) ++ Text0),
io_lib:format("= ~s ==== ~s-~s-~w::~s:~s:~s,~s ",
[Text,t(D),month(Mo),Y,t(H),t(Mi),t(S),
micro2milli(MicroS)]).
pretty_title(#conn_log{client=Client}=Info) ->
- io_lib:format("= Client ~w ~s ~ts ",
+ io_lib:format("= Client ~tw ~s ~ts ",
[Client,actionstr(Info),serverstr(Info)]).
actionstr(#conn_log{action=send}) -> "----->";
@@ -212,16 +238,18 @@ actionstr(#conn_log{action=cmd}) -> "----->";
actionstr(#conn_log{action=recv}) -> "<-----";
actionstr(#conn_log{action=open}) -> "opened session to";
actionstr(#conn_log{action=close}) -> "closed session to";
+actionstr(#conn_log{action=connect}) -> "connected to";
+actionstr(#conn_log{action=disconnect}) -> "disconnected from";
actionstr(_) -> "<---->".
serverstr(#conn_log{name=undefined,address={undefined,_}}) ->
io_lib:format("server",[]);
serverstr(#conn_log{name=undefined,address=Address}) ->
- io_lib:format("~p",[Address]);
+ io_lib:format("~tp",[Address]);
serverstr(#conn_log{name=Alias,address={undefined,_}}) ->
- io_lib:format("~w",[Alias]);
+ io_lib:format("~tw",[Alias]);
serverstr(#conn_log{name=Alias,address=Address}) ->
- io_lib:format("~w(~p)",[Alias,Address]).
+ io_lib:format("~tw(~tp)",[Alias,Address]).
month(1) -> "Jan";
month(2) -> "Feb";
@@ -247,7 +275,7 @@ pad0(N,Str) ->
lists:duplicate(N-M,$0) ++ Str.
pad_char_end(N,Str,Char) ->
- case length(lists:flatten(Str)) of
+ case string:length(Str) of
M when M<N -> Str ++ lists:duplicate(N-M,Char);
_ -> Str
end.
diff --git a/lib/common_test/src/ct_cover.erl b/lib/common_test/src/ct_cover.erl
index b630a51835..d286f20a4d 100644
--- a/lib/common_test/src/ct_cover.erl
+++ b/lib/common_test/src/ct_cover.erl
@@ -1,27 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2012. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2018. 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%
%%
-%%% @doc Common Test Framework code coverage support module.
-%%%
-%%% <p>This module exports help functions for performing code
-%%% coverage analysis.</p>
-
-module(ct_cover).
-export([get_spec/1, add_nodes/1, remove_nodes/1, cross_cover_analyse/2]).
@@ -30,16 +26,6 @@
-include_lib("kernel/include/file.hrl").
-%%%-----------------------------------------------------------------
-%%% @spec add_nodes(Nodes) -> {ok,StartedNodes} | {error,Reason}
-%%% Nodes = [atom()]
-%%% StartedNodes = [atom()]
-%%% Reason = cover_not_running | not_main_node
-%%%
-%%% @doc Add nodes to current cover test (only works if cover support
-%%% is active!). To have effect, this function should be called
-%%% from init_per_suite/1 before any actual tests are performed.
-%%%
add_nodes([]) ->
{ok,[]};
add_nodes(Nodes) ->
@@ -66,17 +52,6 @@ add_nodes(Nodes) ->
end
end.
-
-%%%-----------------------------------------------------------------
-%%% @spec remove_nodes(Nodes) -> ok | {error,Reason}
-%%% Nodes = [atom()]
-%%% Reason = cover_not_running | not_main_node
-%%%
-%%% @doc Remove nodes from current cover test. Call this function
-%%% to stop cover test on nodes previously added with add_nodes/1.
-%%% Results on the remote node are transferred to the Common Test
-%%% node.
-%%%
remove_nodes([]) ->
ok;
remove_nodes(Nodes) ->
@@ -102,25 +77,11 @@ remove_nodes(Nodes) ->
end
end.
-
-%%%-----------------------------------------------------------------
-%%% @spec cross_cover_analyse(Level,Tests) -> ok
-%%% Level = overview | details
-%%% Tests = [{Tag,Dir}]
-%%% Tag = atom()
-%%% Dir = string()
-%%%
-%%% @doc Accumulate cover results over multiple tests.
-%%% See the chapter about <seealso
-%%% marker="cover_chapter#cross_cover">cross cover
-%%% analysis</seealso> in the users's guide.
-%%%
cross_cover_analyse(Level,Tests) ->
test_server_ctrl:cross_cover_analyse(Level,Tests).
%%%-----------------------------------------------------------------
-%%% @hidden
%% Read cover specification file and return the parsed info.
%% -> CoverSpec: {CoverFile,Nodes,Import,Export,AppCoverInfo}
diff --git a/lib/common_test/src/ct_default_gl.erl b/lib/common_test/src/ct_default_gl.erl
new file mode 100644
index 0000000000..cf1bcc058d
--- /dev/null
+++ b/lib/common_test/src/ct_default_gl.erl
@@ -0,0 +1,84 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2012-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+-module(ct_default_gl).
+-export([start_link/1, stop/0]).
+
+-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2]).
+
+%% start_link()
+%% Start a new group leader process.
+start_link(ParentGL) ->
+ do_start(ParentGL, 3).
+
+do_start(_ParentGL, 0) ->
+ exit({?MODULE,startup});
+do_start(ParentGL, Retries) ->
+ case whereis(?MODULE) of
+ undefined ->
+ case gen_server:start_link(?MODULE, [ParentGL], []) of
+ {ok,Pid} ->
+ {ok,Pid};
+ Other ->
+ Other
+ end;
+ Pid ->
+ exit(Pid, kill),
+ timer:sleep(1000),
+ do_start(ParentGL, Retries-1)
+ end.
+
+%% stop(Pid)
+%% Stop a group leader process.
+stop() ->
+ gen_server:cast(whereis(?MODULE), stop).
+
+
+%%% Internal functions.
+
+init([ParentGL]) ->
+ register(?MODULE, self()),
+ ct_util:mark_process(),
+ {ok,#{parent_gl_pid => ParentGL,
+ parent_gl_monitor => erlang:monitor(process,ParentGL)}}.
+
+handle_cast(stop, St) ->
+ {stop,normal,St}.
+
+%% If the parent group leader dies, fall back on using the local user process
+handle_info({'DOWN',Ref,process,_,_Reason}, #{parent_gl_monitor := Ref} = St) ->
+ User = whereis(user),
+ {noreply,St#{parent_gl_pid => User,
+ parent_gl_monitor => erlang:monitor(process,User)}};
+
+handle_info({io_request,_From,_ReplyAs,_Req} = IoReq,
+ #{parent_gl_pid := ParentGL} = St) ->
+ ParentGL ! IoReq,
+ {noreply,St};
+
+handle_info(Msg, St) ->
+ io:format(user, "Common Test Group Leader process got: ~tp~n", [Msg]),
+ {noreply,St}.
+
+handle_call(_Req, _From, St) ->
+ {reply,ok,St}.
+
+terminate(_, _) ->
+ ok.
diff --git a/lib/common_test/src/ct_event.erl b/lib/common_test/src/ct_event.erl
index c1c1d943b9..3689c6bc45 100644
--- a/lib/common_test/src/ct_event.erl
+++ b/lib/common_test/src/ct_event.erl
@@ -1,30 +1,31 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2018. 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%
%%
-%%% @doc Common Test Framework Event Handler
+%%% doc Common Test Framework Event Handler
%%%
-%%% <p>This module implements an event handler that CT uses to
+%%% This module implements an event handler that CT uses to
%%% handle status and progress notifications during test runs.
%%% The notifications are handled locally (per node) and passed
%%% on to ct_master when CT runs in distributed mode. This
%%% module may be used as a template for other event handlers
-%%% that can be plugged in to handle local logging and reporting.</p>
+%%% that can be plugged in to handle local logging and reporting.
-module(ct_event).
-behaviour(gen_event).
@@ -136,6 +137,7 @@ is_alive() ->
%% this function is called to initialize the event handler.
%%--------------------------------------------------------------------
init(RecvPids) ->
+ ct_util:mark_process(),
%% RecvPids = [{RecvTag,Pid}]
{ok,#state{receivers=RecvPids}}.
@@ -150,7 +152,7 @@ init(RecvPids) ->
%%--------------------------------------------------------------------
handle_event(Event,State=#state{receivers=RecvPids}) ->
print("~n=== ~w ===~n", [?MODULE]),
- print("~w: ~w~n", [Event#event.name,Event#event.data]),
+ print("~tw: ~tw~n", [Event#event.name,Event#event.data]),
lists:foreach(fun(Recv) -> report_event(Recv,Event) end, RecvPids),
{ok,State}.
@@ -259,8 +261,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 498950c9d1..134ae0e1cc 100644
--- a/lib/common_test/src/ct_framework.erl
+++ b/lib/common_test/src/ct_framework.erl
@@ -1,33 +1,34 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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 Framework callback module.
+%%% Common Test Framework callback module.
%%%
-%%% <p>This module exports framework callback functions which are
-%%% called from the test_server.</p>
+%%% This module exports framework callback functions which are
+%%% called from the test_server.
-module(ct_framework).
-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]).
@@ -41,7 +42,7 @@
-define(rev(L), lists:reverse(L)).
%%%-----------------------------------------------------------------
-%%% @spec init_tc(Mod,Func,Args) -> {ok,NewArgs} | {error,Reason} |
+%%% -spec init_tc(Mod,Func,Args) -> {ok,NewArgs} | {error,Reason} |
%%% {skip,Reason} | {auto_skip,Reason}
%%% Mod = atom()
%%% Func = atom()
@@ -49,11 +50,29 @@
%%% NewArgs = list()
%%% Reason = term()
%%%
-%%% @doc Test server framework callback, called by the test_server
+%%% Test server framework callback, called by the test_server
%%% when a new test case is started.
-init_tc(Mod,Func,Config) ->
+init_tc(_,{end_per_testcase_not_run,_},[Config]) ->
+ %% Testcase is completed (skipped or failed), but end_per_testcase
+ %% is not run - don't call pre-hook.
+ {ok,[Config]};
+init_tc(Mod,EPTC={end_per_testcase,_},[Config]) ->
%% in case Mod == ct_framework, lookup the suite name
Suite = get_suite_name(Mod, Config),
+ case ct_hooks:init_tc(Suite,EPTC,Config) of
+ NewConfig when is_list(NewConfig) ->
+ {ok,[NewConfig]};
+ Other->
+ Other
+ end;
+
+init_tc(Mod,Func0,Args) ->
+ %% in case Mod == ct_framework, lookup the suite name
+ Suite = get_suite_name(Mod, Args),
+ {Func,HookFunc} = case Func0 of
+ {init_per_testcase,F} -> {F,Func0};
+ _ -> {Func0,Func0}
+ end,
%% check if previous testcase was interpreted and has left
%% a "dead" trace window behind - if so, kill it
@@ -69,12 +88,15 @@ init_tc(Mod,Func,Config) ->
andalso Func=/=end_per_group
andalso ct_util:get_testdata(skip_rest) of
true ->
+ initialize(false,Mod,Func,Args),
{auto_skip,"Repeated test stopped by force_stop option"};
_ ->
case ct_util:get_testdata(curr_tc) of
{Suite,{suite0_failed,{require,Reason}}} ->
+ initialize(false,Mod,Func,Args),
{auto_skip,{require_failed_in_suite0,Reason}};
{Suite,{suite0_failed,_}=Failure} ->
+ initialize(false,Mod,Func,Args),
{fail,Failure};
_ ->
ct_util:update_testdata(curr_tc,
@@ -85,7 +107,7 @@ init_tc(Mod,Func,Config) ->
end, [create]),
case ct_util:read_suite_data({seq,Suite,Func}) of
undefined ->
- init_tc1(Mod,Suite,Func,Config);
+ init_tc1(Mod,Suite,Func,HookFunc,Args);
Seq when is_atom(Seq) ->
case ct_util:read_suite_data({seq,Suite,Seq}) of
[Func|TCs] -> % this is the 1st case in Seq
@@ -101,26 +123,25 @@ init_tc(Mod,Func,Config) ->
_ ->
ok
end,
- init_tc1(Mod,Suite,Func,Config);
+ init_tc1(Mod,Suite,Func,HookFunc,Args);
{failed,Seq,BadFunc} ->
- {auto_skip,{sequence_failed,Seq,BadFunc}}
+ initialize(false,Mod,Func,Args),
+ {auto_skip,{sequence_failed,Seq,BadFunc}}
end
end
- end.
+ end.
-init_tc1(?MODULE,_,error_in_suite,[Config0]) when is_list(Config0) ->
- ct_logs:init_tc(false),
- ct_event:notify(#event{name=tc_start,
- node=node(),
- data={?MODULE,error_in_suite}}),
- case ?val(error, Config0) of
+init_tc1(?MODULE,_,error_in_suite,_,[Config0]) when is_list(Config0) ->
+ initialize(false,?MODULE,error_in_suite),
+ _ = ct_suite_init(?MODULE,error_in_suite,[],Config0),
+ case ?val(error,Config0) of
undefined ->
{fail,"unknown_error_in_suite"};
Reason ->
{fail,Reason}
end;
-init_tc1(Mod,Suite,Func,[Config0]) when is_list(Config0) ->
+init_tc1(Mod,Suite,Func,HookFunc,[Config0]) when is_list(Config0) ->
Config1 =
case ct_util:read_suite_data(last_saved_config) of
{{Suite,LastFunc},SavedConfig} -> % last testcase
@@ -154,46 +175,43 @@ init_tc1(Mod,Suite,Func,[Config0]) when is_list(Config0) ->
%% testcase info function (these should only survive the
%% testcase, not the whole suite)
FuncSpec = group_or_func(Func,Config0),
- if is_tuple(FuncSpec) -> % group
- ok;
- true ->
- ct_config:delete_default_config(testcase)
- end,
- Initialize = fun() ->
- ct_logs:init_tc(false),
- ct_event:notify(#event{name=tc_start,
- node=node(),
- data={Mod,FuncSpec}})
- end,
+ HookFunc1 =
+ if is_tuple(FuncSpec) -> % group
+ FuncSpec;
+ true ->
+ ct_config:delete_default_config(testcase),
+ HookFunc
+ end,
case add_defaults(Mod,Func,AllGroups) of
Error = {suite0_failed,_} ->
- Initialize(),
+ initialize(false,Mod,FuncSpec),
ct_util:set_testdata({curr_tc,{Suite,Error}}),
{error,Error};
Error = {group0_failed,_} ->
- Initialize(),
+ initialize(false,Mod,FuncSpec),
{auto_skip,Error};
Error = {testcase0_failed,_} ->
- Initialize(),
+ initialize(false,Mod,FuncSpec),
{auto_skip,Error};
{SuiteInfo,MergeResult} ->
case MergeResult of
{error,Reason} ->
- Initialize(),
+ initialize(false,Mod,FuncSpec),
{fail,Reason};
_ ->
- init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config)
+ init_tc2(Mod,Suite,Func,HookFunc1,
+ SuiteInfo,MergeResult,Config)
end
end;
-init_tc1(_Mod,_Suite,_Func,Args) ->
+init_tc1(_Mod,_Suite,_Func,_HookFunc,Args) ->
{ok,Args}.
-init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
+init_tc2(Mod,Suite,Func,HookFunc,SuiteInfo,MergeResult,Config) ->
%% timetrap must be handled before require
MergedInfo = timetrap_first(MergeResult, [], []),
%% tell logger to use specified style sheet
- case lists:keysearch(stylesheet,1,MergeResult++Config) of
+ _ = case lists:keysearch(stylesheet,1,MergeResult++Config) of
{value,{stylesheet,SSFile}} ->
ct_logs:set_stylesheet(Func,add_data_dir(SSFile,Config));
_ ->
@@ -217,11 +235,8 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
Conns ->
ct_util:silence_connections(Conns)
end,
- ct_logs:init_tc(Func == init_per_suite),
FuncSpec = group_or_func(Func,Config),
- ct_event:notify(#event{name=tc_start,
- node=node(),
- data={Mod,FuncSpec}}),
+ initialize((Func==init_per_suite),Mod,FuncSpec),
case catch configure(MergedInfo,MergedInfo,SuiteInfo,
FuncSpec,[],Config) of
@@ -236,7 +251,7 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
{ok,PostInitHook,Config1} ->
case get('$test_server_framework_test') of
undefined ->
- ct_suite_init(Suite, FuncSpec, PostInitHook, Config1);
+ ct_suite_init(Suite,HookFunc,PostInitHook,Config1);
Fun ->
PostInitHookResult = do_post_init_hook(PostInitHook,
Config1),
@@ -249,16 +264,28 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
end
end.
-ct_suite_init(Suite, FuncSpec, PostInitHook, Config) when is_list(Config) ->
- case ct_hooks:init_tc(Suite, FuncSpec, Config) of
+initialize(RefreshLogs,Mod,Func,[Config]) when is_list(Config) ->
+ initialize(RefreshLogs,Mod,group_or_func(Func,Config));
+initialize(RefreshLogs,Mod,Func,_) ->
+ initialize(RefreshLogs,Mod,Func).
+
+initialize(RefreshLogs,Mod,FuncSpec) ->
+ ct_logs:init_tc(RefreshLogs),
+ ct_event:notify(#event{name=tc_start,
+ node=node(),
+ data={Mod,FuncSpec}}).
+
+
+ct_suite_init(Suite,HookFunc,PostInitHook,Config) when is_list(Config) ->
+ case ct_hooks:init_tc(Suite,HookFunc,Config) of
NewConfig when is_list(NewConfig) ->
- PostInitHookResult = do_post_init_hook(PostInitHook, NewConfig),
+ PostInitHookResult = do_post_init_hook(PostInitHook,NewConfig),
{ok, [PostInitHookResult ++ NewConfig]};
Else ->
Else
end.
-do_post_init_hook(PostInitHook, Config) ->
+do_post_init_hook(PostInitHook,Config) ->
lists:flatmap(fun({Tag,Fun}) ->
case lists:keysearch(Tag,1,Config) of
{value,_} ->
@@ -285,10 +312,10 @@ add_defaults(Mod,Func, GroupPath) ->
end;
{'EXIT',Reason} ->
ErrStr = io_lib:format("~n*** ERROR *** "
- "~w:suite/0 failed: ~p~n",
+ "~w:suite/0 failed: ~tp~n",
[Suite,Reason]),
io:format(ErrStr, []),
- io:format(user, ErrStr, []),
+ io:format(?def_gl, ErrStr, []),
{suite0_failed,{exited,Reason}};
SuiteInfo when is_list(SuiteInfo) ->
case lists:all(fun(E) when is_tuple(E) -> true;
@@ -308,18 +335,18 @@ add_defaults(Mod,Func, GroupPath) ->
false ->
ErrStr = io_lib:format("~n*** ERROR *** "
"Invalid return value from "
- "~w:suite/0: ~p~n",
+ "~w:suite/0: ~tp~n",
[Suite,SuiteInfo]),
io:format(ErrStr, []),
- io:format(user, ErrStr, []),
+ io:format(?def_gl, ErrStr, []),
{suite0_failed,bad_return_value}
end;
SuiteInfo ->
ErrStr = io_lib:format("~n*** ERROR *** "
"Invalid return value from "
- "~w:suite/0: ~p~n", [Suite,SuiteInfo]),
+ "~w:suite/0: ~tp~n", [Suite,SuiteInfo]),
io:format(ErrStr, []),
- io:format(user, ErrStr, []),
+ io:format(?def_gl, ErrStr, []),
{suite0_failed,bad_return_value}
end.
@@ -344,10 +371,10 @@ add_defaults1(Mod,Func, GroupPath, SuiteInfo) ->
{value,{error,BadGr0Val,GrName}} ->
Gr0ErrStr = io_lib:format("~n*** ERROR *** "
"Invalid return value from "
- "~w:group(~w): ~p~n",
+ "~w:group(~tw): ~tp~n",
[Mod,GrName,BadGr0Val]),
io:format(Gr0ErrStr, []),
- io:format(user, Gr0ErrStr, []),
+ io:format(?def_gl, Gr0ErrStr, []),
{group0_failed,bad_return_value};
_ ->
Args = if Func == init_per_group ; Func == end_per_group ->
@@ -366,10 +393,10 @@ add_defaults1(Mod,Func, GroupPath, SuiteInfo) ->
{error,BadTC0Val} ->
TC0ErrStr = io_lib:format("~n*** ERROR *** "
"Invalid return value from "
- "~w:~w/0: ~p~n",
+ "~w:~tw/0: ~tp~n",
[Mod,Func,BadTC0Val]),
io:format(TC0ErrStr, []),
- io:format(user, TC0ErrStr, []),
+ io:format(?def_gl, TC0ErrStr, []),
{testcase0_failed,bad_return_value};
_ ->
%% let test case info (also for all config funcs) override
@@ -613,16 +640,16 @@ 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.
%%%-----------------------------------------------------------------
-%%% @spec end_tc(Mod,Func,Args) -> {ok,NewArgs}| {error,Reason} |
+%%% -spec end_tc(Mod,Func,Args) -> {ok,NewArgs}| {error,Reason} |
%%% {skip,Reason} | {auto_skip,Reason}
%%% Mod = atom()
%%% Func = atom()
@@ -630,21 +657,61 @@ try_set_default(Name,Key,Info,Where) ->
%%% NewArgs = list()
%%% Reason = term()
%%%
-%%% @doc Test server framework callback, called by the test_server
+%%% Test server framework callback, called by the test_server
%%% when a test case is finished.
end_tc(Mod, Fun, Args) ->
%% Have to keep end_tc/3 for backwards 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) ->
+ case end_hook_func(IPTC,Return,IPTC) of
+ undefined -> ok;
+ _ ->
+ %% in case Mod == ct_framework, lookup the suite name
+ Suite = get_suite_name(Mod, Args),
+ case ct_hooks:end_tc(Suite,IPTC,Args,Result,Return) of
+ '$ct_no_change' ->
+ ok;
+ HookResult ->
+ HookResult
+ end
+ end;
+
+end_tc(Mod,Func0,TCPid,Result,Args,Return) ->
%% in case Mod == ct_framework, lookup the suite name
Suite = get_suite_name(Mod, Args),
+ {Func,FuncSpec,HookFunc} =
+ case Func0 of
+ {end_per_testcase_not_run,F} ->
+ %% Testcase is completed (skipped or failed), but
+ %% end_per_testcase is not run - don't call post-hook.
+ {F,F,undefined};
+ {end_per_testcase,F} ->
+ {F,F,Func0};
+ _ ->
+ FS = group_or_func(Func0,Args),
+ HF = end_hook_func(Func0,Return,FS),
+ {Func0,FS,HF}
+ end,
test_server:timetrap_cancel(),
@@ -671,16 +738,18 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
end,
ct_util:delete_suite_data(last_saved_config),
- FuncSpec = group_or_func(Func,Args),
-
{Result1,FinalNotify} =
- case ct_hooks:end_tc(
- Suite, FuncSpec, Args, Result, Return) of
- '$ct_no_change' ->
- {ok,Result};
- HookResult ->
- {HookResult,HookResult}
- end,
+ case HookFunc of
+ undefined ->
+ {ok,Result};
+ _ ->
+ case ct_hooks:end_tc(Suite,HookFunc,Args,Result,Return) of
+ '$ct_no_change' ->
+ {ok,Result};
+ HookResult ->
+ {HookResult,HookResult}
+ end
+ end,
FinalResult =
case get('$test_server_framework_test') of
undefined ->
@@ -771,6 +840,34 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
end,
FinalResult.
+%% This is to make sure that no post_init_per_* is ever called if the
+%% corresponding pre_init_per_* was not called.
+%% The skip or fail reasons are those that can be returned from
+%% init_tc above in situations where we never came to call
+%% ct_hooks:init_tc/3, e.g. if suite/0 fails, then we never call
+%% ct_hooks:init_tc for init_per_suite, and thus we must not call
+%% ct_hooks:end_tc for init_per_suite either.
+end_hook_func({init_per_testcase,_},{auto_skip,{sequence_failed,_,_}},_) ->
+ undefined;
+end_hook_func({init_per_testcase,_},{auto_skip,"Repeated test stopped by force_stop option"},_) ->
+ undefined;
+end_hook_func({init_per_testcase,_},{fail,{config_name_already_in_use,_}},_) ->
+ undefined;
+end_hook_func({init_per_testcase,_},{auto_skip,{InfoFuncError,_}},_)
+ when InfoFuncError==testcase0_failed;
+ InfoFuncError==require_failed ->
+ undefined;
+end_hook_func(init_per_group,{auto_skip,{InfoFuncError,_}},_)
+ when InfoFuncError==group0_failed;
+ InfoFuncError==require_failed ->
+ undefined;
+end_hook_func(init_per_suite,{auto_skip,{require_failed_in_suite0,_}},_) ->
+ undefined;
+end_hook_func(init_per_suite,{auto_skip,{failed,{error,{suite0_failed,_}}}},_) ->
+ undefined;
+end_hook_func(_,_,Default) ->
+ Default.
+
%% {error,Reason} | {skip,Reason} | {timetrap_timeout,TVal} |
%% {testcase_aborted,Reason} | testcase_aborted_or_killed |
%% {'EXIT',Reason} | {fail,Reason} | {failed,Reason} |
@@ -806,27 +903,28 @@ tag(_Other) ->
ok.
%%%-----------------------------------------------------------------
-%%% @spec error_notification(Mod,Func,Args,Error) -> ok
+%%% -spec error_notification(Mod,Func,Args,Error) -> ok
%%% Mod = atom()
%%% Func = atom()
%%% Args = list()
%%% Error = term()
%%%
-%%% @doc This function is called as the result of testcase
-%%% <code>Func</code> in suite <code>Mod</code> crashing.
-%%% <code>Error</code> specifies the reason for failing.
+%%% This function is called as the result of testcase
+%%% Func in suite Mod crashing.
+%%% Error 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 ->
- Descr2 = string:substr(Descr1,1,50),
+ Descr1 = io_lib:format("~tP",[Descr,10]),
+ DescrLength = string:length(Descr1),
+ if DescrLength > 50 ->
+ Descr2 = string:slice(Descr1,0,50),
io_lib:format("{badmatch,~ts...}",[Descr2]);
true ->
io_lib:format("{badmatch,~ts}",[Descr1])
@@ -834,17 +932,18 @@ error_notification(Mod,Func,_Args,{Error,Loc}) ->
{test_case_failed,Reason} ->
case (catch io_lib:format("{test_case_failed,~ts}", [Reason])) of
{'EXIT',_} ->
- io_lib:format("{test_case_failed,~p}", [Reason]);
+ io_lib:format("{test_case_failed,~tp}", [Reason]);
Result -> Result
end;
{'EXIT',_Reason} = EXIT ->
- io_lib:format("~P", [EXIT,5]);
+ io_lib:format("~tP", [EXIT,5]);
{Spec,_Reason} when is_atom(Spec) ->
- io_lib:format("~w", [Spec]);
+ io_lib:format("~tw", [Spec]);
Other ->
- io_lib:format("~P", [Other,5])
+ io_lib:format("~tP", [Other,5])
end,
- ErrorHtml = "<font color=\"brown\">" ++ ErrStr ++ "</font>",
+ ErrorHtml =
+ "<font color=\"brown\">" ++ ct_logs:escape_chars(ErrorStr) ++ "</font>",
case {Mod,Error} of
%% some notifications come from the main test_server process
%% and for these cases the existing comment may not be modified
@@ -872,41 +971,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(?def_gl, lists:concat([Div,ErrorStr2,Div,"~n"]),
+ []),
Link =
"\n\n<a href=\"#end\">"
"Full error description and stacktrace"
"</a>",
+ ErrorHtml2 = ct_logs:escape_chars(ErrorStr2),
ct_logs:tc_log(ct_error_notify,
?MAX_IMPORTANCE,
"CT Error Notification",
- ErrFormat++Link, ErrArgs)
+ ErrorHtml2++Link, [], [])
end,
case Loc of
[{?MODULE,error_in_suite}] ->
- PrintErr("Error in suite detected: ~ts", [ErrStr]);
+ PrintError("Error in suite detected: ~ts", [ErrorStr]);
R when R == unknown; R == undefined ->
- PrintErr("Error detected: ~ts", [ErrStr]);
+ PrintError("Error detected: ~ts", [ErrorStr]);
%% if a function specified by all/0 does not exist, we
%% pick up undef here
- [{LastMod,LastFunc}|_] when ErrStr == "undef" ->
- PrintErr("~w:~w could not be executed~nReason: ~ts",
- [LastMod,LastFunc,ErrStr]);
+ [{LastMod,LastFunc}|_] when ErrorStr == "undef" ->
+ PrintError("~w:~tw could not be executed~nReason: ~ts",
+ [LastMod,LastFunc,ErrorStr]);
[{LastMod,LastFunc}|_] ->
- PrintErr("~w:~w failed~nReason: ~ts", [LastMod,LastFunc,ErrStr]);
+ PrintError("~w:~tw failed~nReason: ~ts", [LastMod,LastFunc,ErrorStr]);
[{LastMod,LastFunc,LastLine}|_] ->
%% print error to console, we are only
%% interested in the last executed expression
- PrintErr("~w:~w failed on line ~w~nReason: ~ts",
- [LastMod,LastFunc,LastLine,ErrStr]),
+ PrintError("~w:~tw failed on line ~w~nReason: ~ts",
+ [LastMod,LastFunc,LastLine,ErrorStr]),
case ct_util:read_suite_data({seq,Mod,Func}) of
undefined ->
@@ -948,11 +1049,11 @@ group_or_func(Func, _Config) ->
Func.
%%%-----------------------------------------------------------------
-%%% @spec get_suite(Mod, Func) -> Tests
+%%% -spec get_suite(Mod, Func) -> Tests
%%%
-%%% @doc Called from test_server for every suite (<code>Func==all</code>)
-%%% and every test case. If the former, all test cases in the suite
-%%% should be returned.
+%%% Called from test_server for every suite (Func==all)
+%%% and every test case. If the former, all test cases in the suite
+%%% should be returned.
get_suite(Mod, all) ->
case catch apply(Mod, groups, []) of
@@ -1062,9 +1163,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: ~tp~n",
+ [Mod,ExitReason]),
+ io:format(?def_gl, ErrStr, []),
+ %% save the error info so it doesn't get printed twice
+ ct_util:set_testdata_async({{error_in_suite,Mod},
+ ExitReason});
+ _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}]]}];
@@ -1171,8 +1295,8 @@ save_seq(Mod,Seq,SeqTCs,All) ->
check_private(Seq,TCs,All) ->
Bad = lists:filter(fun(TC) -> lists:member(TC,All) end, TCs),
if Bad /= [] ->
- Reason = io_lib:format("regular test cases not allowed in sequence ~p: "
- "~p",[Seq,Bad]),
+ Reason = io_lib:format("regular test cases not allowed in sequence ~tp: "
+ "~tp",[Seq,Bad]),
throw({error,list_to_atom(lists:flatten(Reason))});
true ->
ok
@@ -1189,7 +1313,7 @@ check_multiple(Mod,Seq,TCs) ->
end,TCs),
if Bad /= [] ->
Reason = io_lib:format("test cases found in multiple sequences: "
- "~p",[Bad]),
+ "~tp",[Bad]),
throw({error,list_to_atom(lists:flatten(Reason))});
true ->
ok
@@ -1217,21 +1341,21 @@ end_per_suite(_Config) ->
%% if the group config functions are missing in the suite,
%% use these instead
init_per_group(GroupName, Config) ->
- ct:comment(io_lib:format("start of ~p", [GroupName])),
- ct_logs:log("TEST INFO", "init_per_group/2 for ~w missing "
+ ct:comment(io_lib:format("start of ~tp", [GroupName])),
+ ct_logs:log("TEST INFO", "init_per_group/2 for ~tw missing "
"in suite, using default.",
[GroupName]),
Config.
end_per_group(GroupName, _) ->
- ct:comment(io_lib:format("end of ~p", [GroupName])),
- ct_logs:log("TEST INFO", "end_per_group/2 for ~w missing "
+ ct:comment(io_lib:format("end of ~tp", [GroupName])),
+ ct_logs:log("TEST INFO", "end_per_group/2 for ~tw missing "
"in suite, using default.",
[GroupName]),
ok.
%%%-----------------------------------------------------------------
-%%% @spec report(What,Data) -> ok
+%%% -spec report(What,Data) -> ok
report(What,Data) ->
case What of
loginfo ->
@@ -1239,7 +1363,7 @@ report(What,Data) ->
%% top level test index page needs to be refreshed
TestName = filename:basename(?val(topdir, Data), ".logs"),
RunDir = ?val(rundir, Data),
- ct_logs:make_all_suites_index({TestName,RunDir}),
+ _ = ct_logs:make_all_suites_index({TestName,RunDir}),
ok;
tests_start ->
ok;
@@ -1263,30 +1387,32 @@ report(What,Data) ->
ok;
tc_done ->
{Suite,{Func,GrName},Result} = Data,
- Data1 = if GrName == undefined -> {Suite,Func,Result};
- true -> Data
- end,
+ FuncSpec = if GrName == undefined -> Func;
+ true -> {Func,GrName}
+ end,
%% Register the group leader for the process calling the report
%% function, making it possible for a hook function to print
%% in the test case log file
ReportingPid = self(),
ct_logs:register_groupleader(ReportingPid, group_leader()),
case Result of
- {failed, _} ->
- ct_hooks:on_tc_fail(What, Data1);
- {skipped,{failed,{_,init_per_testcase,_}}} ->
- ct_hooks:on_tc_skip(tc_auto_skip, Data1);
- {skipped,{require_failed,_}} ->
- ct_hooks:on_tc_skip(tc_auto_skip, Data1);
- {skipped,_} ->
- ct_hooks:on_tc_skip(tc_user_skip, Data1);
- {auto_skipped,_} ->
- ct_hooks:on_tc_skip(tc_auto_skip, Data1);
+ {failed, Reason} ->
+ ct_hooks:on_tc_fail(What, {Suite,FuncSpec,Reason});
+ {skipped,{failed,{_,init_per_testcase,_}}=Reason} ->
+ ct_hooks:on_tc_skip(tc_auto_skip, {Suite,FuncSpec,Reason});
+ {skipped,{require_failed,_}=Reason} ->
+ ct_hooks:on_tc_skip(tc_auto_skip, {Suite,FuncSpec,Reason});
+ {skipped,Reason} ->
+ ct_hooks:on_tc_skip(tc_user_skip, {Suite,FuncSpec,Reason});
+ {auto_skipped,Reason} ->
+ ct_hooks:on_tc_skip(tc_auto_skip, {Suite,FuncSpec,Reason});
_Else ->
ok
end,
ct_logs:unregister_groupleader(ReportingPid),
case {Func,Result} of
+ {error_in_suite,_} when Suite == ?MODULE ->
+ ok;
{init_per_suite,_} ->
ok;
{end_per_suite,_} ->
@@ -1393,14 +1519,14 @@ add_to_stats(Result) ->
ct_util:update_testdata(stats, Update).
%%%-----------------------------------------------------------------
-%%% @spec warn(What) -> true | false
+%%% -spec warn(What) -> true | false
warn(What) when What==nodes; What==processes ->
false;
warn(_What) ->
true.
%%%-----------------------------------------------------------------
-%%% @spec add_data_dir(File0, Config) -> 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);
@@ -1419,7 +1545,7 @@ add_data_dir(File,Config) when is_list(File) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec get_logopts() -> [LogOpt]
+%%% -spec get_logopts() -> [LogOpt]
get_logopts() ->
case ct_util:get_testdata(logopts) of
undefined ->
@@ -1429,14 +1555,19 @@ get_logopts() ->
end.
%%%-----------------------------------------------------------------
-%%% @spec format_comment(Comment) -> HtmlComment
+%%% -spec format_comment(Comment) -> HtmlComment
format_comment(Comment) ->
"<font color=\"green\">" ++ Comment ++ "</font>".
%%%-----------------------------------------------------------------
-%%% @spec get_html_wrapper(TestName, PrintLabel, Cwd) -> Header
+%%% -spec get_html_wrapper(TestName, PrintLabel, Cwd) -> Header
get_html_wrapper(TestName, PrintLabel, Cwd, TableCols) ->
get_html_wrapper(TestName, PrintLabel, Cwd, TableCols, utf8).
get_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) ->
ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding).
+
+%%%-----------------------------------------------------------------
+%%% -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..93d1f88041 100644
--- a/lib/common_test/src/ct_ftp.erl
+++ b/lib/common_test/src/ct_ftp.erl
@@ -1,28 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2018. 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%
%%
-%%% @doc FTP client module (based on the FTP support of the INETS application).
-%%%
-%%% @type connection() = handle() | ct:target_name()
-%%% @type handle() = ct_gen_conn:handle(). Handle for a specific
-%%% ftp connection.
-
-module(ct_ftp).
%% API
@@ -41,76 +36,14 @@
%%%=================================================================
%%% API
-%%%-----------------------------------------------------------------
-%%% @spec put(KeyOrName,LocalFile,RemoteFile) -> ok | {error,Reason}
-%%% KeyOrName = Key | Name
-%%% Key = atom()
-%%% Name = ct:target_name()
-%%% LocalFile = string()
-%%% RemoteFile = string()
-%%%
-%%% @doc Open a ftp connection and send a file to the remote host.
-%%%
-%%% <p><code>LocalFile</code> and <code>RemoteFile</code> must be
-%%% absolute paths.</p>
-%%%
-%%% <p>If the target host is a "special" node, the ftp address must be
-%%% specified in the config file like this:</p>
-%%% <pre>
-%%% {node,[{ftp,IpAddr}]}.</pre>
-%%%
-%%% <p>If the target host is something else, e.g. a unix host, the
-%%% config file must also include the username and password (both
-%%% strings):</p>
-%%% <pre>
-%%% {unix,[{ftp,IpAddr},
-%%% {username,Username},
-%%% {password,Password}]}.</pre>
-%%% @see ct:require/2
put(KeyOrName,LocalFile,RemoteFile) ->
Fun = fun(Ftp) -> send(Ftp,LocalFile,RemoteFile) end,
open_and_do(KeyOrName,Fun).
-%%%-----------------------------------------------------------------
-%%% @spec get(KeyOrName,RemoteFile,LocalFile) -> ok | {error,Reason}
-%%% KeyOrName = Key | Name
-%%% Key = atom()
-%%% Name = ct:target_name()
-%%% RemoteFile = string()
-%%% LocalFile = string()
-%%%
-%%% @doc Open a ftp connection and fetch a file from the remote host.
-%%%
-%%% <p><code>RemoteFile</code> and <code>LocalFile</code> must be
-%%% absolute paths.</p>
-%%%
-%%% <p>The config file must be as for put/3.</p>
-%%% @see put/3
-%%% @see ct:require/2
get(KeyOrName,RemoteFile,LocalFile) ->
Fun = fun(Ftp) -> recv(Ftp,RemoteFile,LocalFile) end,
open_and_do(KeyOrName,Fun).
-
-%%%-----------------------------------------------------------------
-%%% @spec open(KeyOrName) -> {ok,Handle} | {error,Reason}
-%%% KeyOrName = Key | Name
-%%% Key = atom()
-%%% Name = ct:target_name()
-%%% Handle = handle()
-%%%
-%%% @doc Open an FTP connection to the specified node.
-%%% <p>You can open one connection for a particular <code>Name</code> and
-%%% use the same name as reference for all subsequent operations. If you
-%%% want the connection to be associated with <code>Handle</code> instead
-%%% (in case you need to open multiple connections to a host for example),
-%%% simply use <code>Key</code>, the configuration variable name, to
-%%% specify the target. Note that a connection that has no associated target
-%%% name can only be closed with the handle value.</p>
-%%%
-%%% <p>See <c>ct:require/2</c> for how to create a new <c>Name</c></p>
-%%%
-%%% @see ct:require/2
open(KeyOrName) ->
case ct_util:get_key_from_name(KeyOrName) of
{ok,node} ->
@@ -118,19 +51,19 @@ open(KeyOrName) ->
_ ->
case ct:get_config(KeyOrName) of
undefined ->
- log(heading(open,KeyOrName),"Failed: ~p\n",
+ log(heading(open,KeyOrName),"Failed: ~tp\n",
[{not_available,KeyOrName}]),
{error,{not_available,KeyOrName}};
_ ->
case ct:get_config({KeyOrName,username}) of
undefined ->
- log(heading(open,KeyOrName),"Failed: ~p\n",
+ log(heading(open,KeyOrName),"Failed: ~tp\n",
[{not_available,{KeyOrName,username}}]),
{error,{not_available,{KeyOrName,username}}};
Username ->
case ct:get_config({KeyOrName,password}) of
undefined ->
- log(heading(open,KeyOrName),"Failed: ~p\n",
+ log(heading(open,KeyOrName),"Failed: ~tp\n",
[{not_available,{KeyOrName,password}}]),
{error,{not_available,{KeyOrName,password}}};
Password ->
@@ -144,32 +77,16 @@ open(KeyOrName,Username,Password) ->
log(heading(open,KeyOrName),"",[]),
case ct:get_config({KeyOrName,ftp}) of
undefined ->
- log(heading(open,KeyOrName),"Failed: ~p\n",
+ log(heading(open,KeyOrName),"Failed: ~tp\n",
[{not_available,{KeyOrName,ftp}}]),
{error,{not_available,{KeyOrName,ftp}}};
Addr ->
ct_gen_conn:start(KeyOrName,full_addr(Addr),{Username,Password},?MODULE)
end.
-
-%%%-----------------------------------------------------------------
-%%% @spec send(Connection,LocalFile) -> ok | {error,Reason}
-%%%
-%%% @doc Send a file over FTP.
-%%% <p>The file will get the same name on the remote host.</p>
-%%% @see send/3
send(Connection,LocalFile) ->
send(Connection,LocalFile,filename:basename(LocalFile)).
-%%%-----------------------------------------------------------------
-%%% @spec send(Connection,LocalFile,RemoteFile) -> ok | {error,Reason}
-%%% Connection = connection()
-%%% LocalFile = string()
-%%% RemoteFile = string()
-%%%
-%%% @doc Send a file over FTP.
-%%%
-%%% <p>The file will be named <code>RemoteFile</code> on the remote host.</p>
send(Connection,LocalFile,RemoteFile) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -178,24 +95,9 @@ send(Connection,LocalFile,RemoteFile) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec recv(Connection,RemoteFile) -> ok | {error,Reason}
-%%%
-%%% @doc Fetch a file over FTP.
-%%% <p>The file will get the same name on the local host.</p>
-%%% @see recv/3
recv(Connection,RemoteFile) ->
recv(Connection,RemoteFile,filename:basename(RemoteFile)).
-%%%-----------------------------------------------------------------
-%%% @spec recv(Connection,RemoteFile,LocalFile) -> ok | {error,Reason}
-%%% Connection = connection()
-%%% RemoteFile = string()
-%%% LocalFile = string()
-%%%
-%%% @doc Fetch a file over FTP.
-%%%
-%%% <p>The file will be named <code>LocalFile</code> on the local host.</p>
recv(Connection,RemoteFile,LocalFile) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -204,12 +106,6 @@ recv(Connection,RemoteFile,LocalFile) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec cd(Connection,Dir) -> ok | {error,Reason}
-%%% Connection = connection()
-%%% Dir = string()
-%%%
-%%% @doc Change directory on remote host.
cd(Connection,Dir) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -218,13 +114,6 @@ cd(Connection,Dir) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec ls(Connection,Dir) -> {ok,Listing} | {error,Reason}
-%%% Connection = connection()
-%%% Dir = string()
-%%% Listing = string()
-%%%
-%%% @doc List the directory Dir.
ls(Connection,Dir) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -233,12 +122,6 @@ ls(Connection,Dir) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec type(Connection,Type) -> ok | {error,Reason}
-%%% Connection = connection()
-%%% Type = ascii | binary
-%%%
-%%% @doc Change file transfer type
type(Connection,Type) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -247,12 +130,6 @@ type(Connection,Type) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec delete(Connection,File) -> ok | {error,Reason}
-%%% Connection = connection()
-%%% File = string()
-%%%
-%%% @doc Delete a file on remote host
delete(Connection,File) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -261,11 +138,6 @@ delete(Connection,File) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec close(Connection) -> ok | {error,Reason}
-%%% Connection = connection()
-%%%
-%%% @doc Close the FTP connection.
close(Connection) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -278,21 +150,20 @@ close(Connection) ->
%%%=================================================================
%%% Callback functions
-%% @hidden
init(KeyOrName,{IP,Port},{Username,Password}) ->
case ftp_connect(IP,Port,Username,Password) of
{ok,FtpPid} ->
log(heading(init,KeyOrName),
- "Opened ftp connection:\nIP: ~p\nUsername: ~p\nPassword: ~p\n",
- [IP,Username,lists:duplicate(length(Password),$*)]),
+ "Opened ftp connection:\nIP: ~tp\nUsername: ~tp\nPassword: ~p\n",
+ [IP,Username,lists:duplicate(string:length(Password),$*)]),
{ok,FtpPid,#state{ftp_pid=FtpPid,target_name=KeyOrName}};
Error ->
Error
end.
ftp_connect(IP,Port,Username,Password) ->
- inets:start(),
- case inets:start(ftpc,[{host,IP},{port,Port}]) of
+ _ = ftp:start(),
+ case ftp:start_service([{host,IP},{port,Port}]) of
{ok,FtpPid} ->
case ftp:user(FtpPid,Username,Password) of
ok ->
@@ -304,43 +175,40 @@ ftp_connect(IP,Port,Username,Password) ->
{error,{open,Reason}}
end.
-%% @hidden
handle_msg({send,LocalFile,RemoteFile},State) ->
log(heading(send,State#state.target_name),
- "LocalFile: ~p\nRemoteFile: ~p\n",[LocalFile,RemoteFile]),
+ "LocalFile: ~tp\nRemoteFile: ~tp\n",[LocalFile,RemoteFile]),
Result = ftp:send(State#state.ftp_pid,LocalFile,RemoteFile),
{Result,State};
handle_msg({recv,RemoteFile,LocalFile},State) ->
log(heading(recv,State#state.target_name),
- "RemoteFile: ~p\nLocalFile: ~p\n",[RemoteFile,LocalFile]),
+ "RemoteFile: ~tp\nLocalFile: ~tp\n",[RemoteFile,LocalFile]),
Result = ftp:recv(State#state.ftp_pid,RemoteFile,LocalFile),
{Result,State};
handle_msg({cd,Dir},State) ->
- log(heading(cd,State#state.target_name),"Dir: ~p\n",[Dir]),
+ log(heading(cd,State#state.target_name),"Dir: ~tp\n",[Dir]),
Result = ftp:cd(State#state.ftp_pid,Dir),
{Result,State};
handle_msg({ls,Dir},State) ->
- log(heading(ls,State#state.target_name),"Dir: ~p\n",[Dir]),
+ log(heading(ls,State#state.target_name),"Dir: ~tp\n",[Dir]),
Result = ftp:ls(State#state.ftp_pid,Dir),
{Result,State};
handle_msg({type,Type},State) ->
- log(heading(type,State#state.target_name),"Type: ~p\n",[Type]),
+ log(heading(type,State#state.target_name),"Type: ~tp\n",[Type]),
Result = ftp:type(State#state.ftp_pid,Type),
{Result,State};
handle_msg({delete,File},State) ->
- log(heading(delete,State#state.target_name),"Delete file: ~p\n",[File]),
+ log(heading(delete,State#state.target_name),"Delete file: ~tp\n",[File]),
Result = ftp:delete(State#state.ftp_pid,File),
{Result,State}.
-%% @hidden
reconnect(_Addr,_State) ->
{error,no_reconnection_of_ftp}.
-%% @hidden
terminate(FtpPid,State) ->
log(heading(terminate,State#state.target_name),
"Closing FTP connection.\nHandle: ~p\n",[FtpPid]),
- inets:stop(ftpc,FtpPid).
+ ftp:stop_service(FtpPid).
%%%=================================================================
@@ -367,7 +235,7 @@ call(Pid,Msg) ->
heading(Function,Name) ->
- io_lib:format("ct_ftp:~w ~p",[Function,Name]).
+ io_lib:format("ct_ftp:~tw ~tp",[Function,Name]).
log(Heading,Str,Args) ->
ct_gen_conn:log(Heading,Str,Args).
diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl
index 56082086f6..1ab9946d96 100644
--- a/lib/common_test/src/ct_gen_conn.erl
+++ b/lib/common_test/src/ct_gen_conn.erl
@@ -1,40 +1,33 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2018. 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 Generic connection owner process.
+%%% Generic connection owner process.
%%%
-%%% @type handle() = pid(). A handle for using a connection implemented
+%%% -type handle() = pid(). A handle for using a connection implemented
%%% with ct_gen_conn.erl.
-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]).
-
-%%----------------------------------------------------------------------
-%% Exported types
-%%----------------------------------------------------------------------
--export_type([server_id/0,
- target_name/0,
- key_or_name/0]).
+-export([log/3, start_log/1, cont_log/2, cont_log_no_timestamp/2, end_log/0]).
-ifdef(debug).
-define(dbg,true).
@@ -54,20 +47,8 @@
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) ->
+%%% -spec start(Address,InitData,CallbackMod,Opts) ->
%%% {ok,Handle} | {error,Reason}
%%% Name = term()
%%% CallbackMod = atom()
@@ -77,57 +58,52 @@
%%% Opt = {name,Name} | {use_existing_connection,boolean()} |
%%% {reconnect,boolean()} | {forward_messages,boolean()}
%%%
-%%% @doc Open a connection and start the generic connection owner process.
+%%% Open a connection and start the generic connection owner process.
%%%
-%%% <p>The <code>CallbackMod</code> is a specific callback module for
+%%% The CallbackMod is a specific callback module for
%%% each type of connection (e.g. telnet, ftp,...). It must export the
-%%% function <code>init/3</code> which takes the arguments
-%%% <code>Name</code>, <code>Addresse</code>) and
-%%% <code>InitData</code> and returna
-%%% <code>{ok,ConnectionPid,State}</code> or
-%%% <code>{error,Reason}</code>.</p>
+%%% function init/3 which takes the arguments
+%%% Name, Addresse) and
+%%% InitData and returna
+%%% {ok,ConnectionPid,State} or
+%%% {error,Reason}.
%%%
-%%% If no name is given, the <code>Name</code> argument in init/3 will
-%%% have the value <code>undefined</code>.
+%%% If no name is given, the Name argument in init/3 will
+%%% have the value undefined.
%%%
%%% The callback modules must also export
-%%% ```
+%%%
%%% handle_msg(Msg,From,State) -> {reply,Reply,State} |
%%% {noreply,State} |
%%% {stop,Reply,State}
%%% terminate(ConnectionPid,State) -> term()
%%% close(Handle) -> term()
-%%% '''
%%%
-%%% The <code>close/1</code> callback function is actually a callback
+%%% The close/1 callback function is actually a callback
%%% for ct_util, for closing registered connections when
-%%% ct_util_server is terminated. <code>Handle</code> is the Pid of
+%%% ct_util_server is terminated. Handle is the Pid of
%%% the ct_gen_conn process.
%%%
-%%% If option <code>reconnect</code> is <code>true</code>, then the
+%%% If option reconnect is true, then the
%%% callback must also export
-%%% ```
+%%%
%%% reconnect(Address,State) -> {ok,ConnectionPid,State}
-%%% '''
%%%
-%%% If option <code>forward_messages</code> is <ocde>true</code>, then
+%%% If option forward_messages is <ocde>true, then
%%% the callback must also export
-%%% ```
+%%%
%%% handle_msg(Msg,State) -> {noreply,State} | {stop,State}
-%%% '''
%%%
%%% An old interface still exists. This is used by ct_telnet, ct_ftp
%%% and ct_ssh. The start function then has an explicit
-%%% <code>Name</code> argument, and no <code>Opts</code> argument. The
+%%% Name argument, and no Opts argument. The
%%% callback must export:
%%%
-%%% ```
%%% init(Name,Address,InitData) -> {ok,ConnectionPid,State}
%%% handle_msg(Msg,State) -> {Reply,State}
%%% reconnect(Address,State) -> {ok,ConnectionPid,State}
%%% terminate(ConnectionPid,State) -> term()
%%% close(Handle) -> term()
-%%% '''
%%%
start(Address,InitData,CallbackMod,Opts) when is_list(Opts) ->
do_start(Address,InitData,CallbackMod,Opts);
@@ -135,71 +111,81 @@ start(Name,Address,InitData,CallbackMod) ->
do_start(Address,InitData,CallbackMod,[{name,Name},{old,true}]).
%%%-----------------------------------------------------------------
-%%% @spec stop(Handle) -> ok
+%%% -spec stop(Handle) -> ok
%%% Handle = handle()
%%%
-%%% @doc Close the connection and stop the process managing it.
+%%% Close the connection and stop the process managing it.
stop(Handle) ->
call(Handle,stop,5000).
%%%-----------------------------------------------------------------
-%%% @spec get_conn_pid(Handle) -> ok
+%%% -spec get_conn_pid(Handle) -> ok
%%% Handle = handle()
%%%
-%%% @doc Return the connection pid associated with Handle
+%%% Return the connection pid associated with Handle
get_conn_pid(Handle) ->
call(Handle,get_conn_pid).
%%%-----------------------------------------------------------------
-%%% @spec log(Heading,Format,Args) -> ok
+%%% -spec log(Heading,Format,Args) -> ok
%%%
-%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:log/3
+%%% Log activities on the current connection (tool-internal use only).
+%%% See ct_logs:log/3
log(Heading,Format,Args) ->
log(log,[Heading,Format,Args]).
%%%-----------------------------------------------------------------
-%%% @spec start_log(Heading) -> ok
+%%% -spec start_log(Heading) -> ok
%%%
-%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:start_log/1
+%%% Log activities on the current connection (tool-internal use only).
+%%% See ct_logs:start_log/1
start_log(Heading) ->
log(start_log,[Heading]).
%%%-----------------------------------------------------------------
-%%% @spec cont_log(Format,Args) -> ok
+%%% -spec cont_log(Format,Args) -> ok
%%%
-%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:cont_log/2
+%%% Log activities on the current connection (tool-internal use only).
+%%% See ct_logs:cont_log/2
cont_log(Format,Args) ->
log(cont_log,[Format,Args]).
%%%-----------------------------------------------------------------
-%%% @spec end_log() -> ok
+%%% -spec cont_log_no_timestamp(Format,Args) -> ok
+%%%
+%%% Log activities on the current connection (tool-internal use only).
+%%% See ct_logs:cont_log/2
+cont_log_no_timestamp(Format,Args) ->
+ log(cont_log_no_timestamp,[Format,Args]).
+
+%%%-----------------------------------------------------------------
+%%% -spec end_log() -> ok
%%%
-%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:end_log/0
+%%% Log activities on the current connection (tool-internal use only).
+%%% See ct_logs:end_log/0
end_log() ->
log(end_log,[]).
%%%-----------------------------------------------------------------
-%%% @spec do_within_time(Fun,Timeout) -> FunResult | {error,Reason}
+%%% -spec do_within_time(Fun,Timeout) -> FunResult | {error,Reason}
%%% Fun = function()
%%% Timeout = integer()
%%%
-%%% @doc Execute a function within a limited time (tool-internal use only).
+%%% Execute a function within a limited time (tool-internal use only).
%%%
-%%% <p>Execute the given <code>Fun</code>, but interrupt if it takes
-%%% more than <code>Timeout</code> milliseconds.</p>
+%%% Execute the given Fun, but interrupt if it takes
+%%% more than Timeout milliseconds.
%%%
-%%% <p>The execution is also interrupted if the connection is
-%%% closed.</p>
+%%% The execution is also interrupted if the connection is
+%%% closed.
do_within_time(Fun,Timeout) ->
Self = self(),
Silent = get(silent),
- TmpPid = spawn_link(fun() -> put(silent,Silent),
- R = Fun(),
- Self ! {self(),R}
+ TmpPid = spawn_link(fun() ->
+ ct_util:mark_process(),
+ put(silent,Silent),
+ R = Fun(),
+ Self ! {self(),R}
end),
ConnPid = get(conn_pid),
receive
@@ -258,7 +244,7 @@ do_start(Opts) ->
Error;
{'DOWN',MRef,process,_,Reason} ->
log("ct_gen_conn:start",
- "Connection process died: ~p\n",
+ "Connection process died: ~tp\n",
[Reason]),
{error,{connection_process_died,Reason}}
end.
@@ -312,6 +298,7 @@ return({To,Ref},Result) ->
init_gen(Parent,Opts) ->
process_flag(trap_exit,true),
+ ct_util:mark_process(),
put(silent,false),
try (Opts#gen_opts.callback):init(Opts#gen_opts.name,
Opts#gen_opts.address,
@@ -338,7 +325,7 @@ loop(Opts) ->
case Opts#gen_opts.reconnect of
true ->
log("Connection down!\nOpening new!",
- "Reason: ~p\nAddress: ~p\n",
+ "Reason: ~tp\nAddress: ~tp\n",
[Reason,Opts#gen_opts.address]),
case reconnect(Opts) of
{ok, NewPid, NewState} ->
@@ -349,12 +336,12 @@ loop(Opts) ->
Error ->
ct_util:unregister_connection(self()),
log("Reconnect failed. Giving up!",
- "Reason: ~p\n",
+ "Reason: ~tp\n",
[Error])
end;
false ->
ct_util:unregister_connection(self()),
- log("Connection closed!","Reason: ~p\n",[Reason])
+ log("Connection closed!","Reason: ~tp\n",[Reason])
end;
{'EXIT',Pid,Reason} ->
case Opts#gen_opts.ct_util_server of
@@ -365,8 +352,9 @@ loop(Opts) ->
end;
{stop, From} ->
ct_util:unregister_connection(self()),
- (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid,
- Opts#gen_opts.cb_state),
+ ConnPid = Opts#gen_opts.conn_pid,
+ unlink(ConnPid),
+ (Opts#gen_opts.callback):terminate(ConnPid,Opts#gen_opts.cb_state),
return(From,ok),
ok;
{{retry,{Error,_Name,CPid,_Msg}}, From} when
@@ -403,8 +391,9 @@ loop(Opts) ->
loop(Opts#gen_opts{cb_state=NewState});
{stop,Reply,NewState} ->
ct_util:unregister_connection(self()),
- (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid,
- NewState),
+ ConnPid = Opts#gen_opts.conn_pid,
+ unlink(ConnPid),
+ (Opts#gen_opts.callback):terminate(ConnPid,NewState),
return(From,Reply)
end;
Msg when Opts#gen_opts.forward==true ->
@@ -414,8 +403,9 @@ loop(Opts) ->
loop(Opts#gen_opts{cb_state=NewState});
{stop,NewState} ->
ct_util:unregister_connection(self()),
- (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid,
- NewState)
+ ConnPid = Opts#gen_opts.conn_pid,
+ unlink(ConnPid),
+ (Opts#gen_opts.callback):terminate(ConnPid,NewState)
end
end.
diff --git a/lib/common_test/src/ct_groups.erl b/lib/common_test/src/ct_groups.erl
index 14a8aab881..d867069dce 100644
--- a/lib/common_test/src/ct_groups.erl
+++ b/lib/common_test/src/ct_groups.erl
@@ -1,27 +1,26 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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 Framework callback module.
-%%%
-%%% <p>This module contains CT internal help functions for searching
-%%% through groups specification trees and producing resulting
-%%% tests.</p>
+%%% This module contains CT internal help functions for searching
+%%% through groups specification trees and producing resulting
+%%% tests.
-module(ct_groups).
@@ -80,7 +79,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 +89,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 +131,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))) ++
@@ -209,7 +208,7 @@ find(Mod, _GrNames, _TCs, [BadTerm | _Gs], Known, _Defs, _FindAll) ->
"group "++atom_to_list(lists:last(Known))++
" in "++atom_to_list(Mod)++":groups/0"
end,
- Term = io_lib:format("~p", [BadTerm]),
+ Term = io_lib:format("~tp", [BadTerm]),
E = "Bad term "++lists:flatten(Term)++" in "++Where,
throw({error,list_to_atom(E)});
@@ -283,70 +282,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 +400,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,33 +421,40 @@ make_all_conf(Mod, Props, TestSpec) ->
end.
make_conf(Dir, Mod, Name, Props, TestSpec) ->
+ _ = load_abs(Dir, Mod),
+ make_conf(Mod, Name, Props, TestSpec).
+
+load_abs(Dir, Mod) ->
case code:is_loaded(Mod) of
false ->
code:load_abs(filename:join(Dir,atom_to_list(Mod)));
_ ->
ok
- end,
- make_conf(Mod, Name, Props, TestSpec).
+ end.
make_conf(Mod, Name, Props, TestSpec) ->
- case code:is_loaded(Mod) of
+ _ = case code:is_loaded(Mod) of
false ->
code:load_file(Mod);
_ ->
ok
end,
{InitConf,EndConf,ExtraProps} =
- case erlang:function_exported(Mod,init_per_group,2) of
- true ->
- {{Mod,init_per_group},{Mod,end_per_group},[]};
- false ->
+ case {erlang:function_exported(Mod,init_per_group,2),
+ erlang:function_exported(Mod,end_per_group,2)} of
+ {false,false} ->
ct_logs:log("TEST INFO", "init_per_group/2 and "
"end_per_group/2 missing for group "
- "~w in ~w, using default.",
+ "~tw in ~w, using default.",
[Name,Mod]),
{{ct_framework,init_per_group},
{ct_framework,end_per_group},
- [{suite,Mod}]}
+ [{suite,Mod}]};
+ _ ->
+ %% If any of these exist, the other should too
+ %% (required and documented). If it isn't, it will fail
+ %% with reason 'undef'.
+ {{Mod,init_per_group},{Mod,end_per_group},[]}
end,
{conf,[{name,Name}|Props++ExtraProps],InitConf,TestSpec,EndConf}.
diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl
index df4c98d9d1..49587b3edd 100644
--- a/lib/common_test/src/ct_hooks.erl
+++ b/lib/common_test/src/ct_hooks.erl
@@ -1,26 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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 Framework test execution control module.
-%%%
-%%% <p>This module is a proxy for calling and handling common test hooks.</p>
-
-module(ct_hooks).
%% API Exports
@@ -46,14 +43,12 @@
%% API Functions
%% -------------------------------------------------------------------------
-%% @doc Called before any suites are started
-spec init(State :: term()) -> ok |
{fail, Reason :: term()}.
init(Opts) ->
call(get_builtin_hooks(Opts) ++ get_new_hooks(Opts, undefined),
ok, init, []).
-%% @doc Called after all suites are done.
-spec terminate(Hooks :: term()) ->
ok.
terminate(Hooks) ->
@@ -62,10 +57,10 @@ terminate(Hooks) ->
ct_hooks_terminate_dummy, terminate, Hooks),
ok.
-%% @doc Called as each test case is started. This includes all configuration
-%% tests.
-spec init_tc(Mod :: atom(),
FuncSpec :: atom() |
+ {ConfigFunc :: init_per_testcase | end_per_testcase,
+ TestCase :: atom()} |
{ConfigFunc :: init_per_group | end_per_group,
GroupName :: atom(),
Properties :: list()},
@@ -89,16 +84,22 @@ init_tc(Mod, end_per_suite, Config) ->
call(fun call_generic/3, Config, [pre_end_per_suite, Mod]);
init_tc(Mod, {init_per_group, GroupName, Properties}, Config) ->
maybe_start_locker(Mod, GroupName, Properties),
- call(fun call_generic/3, Config, [pre_init_per_group, GroupName]);
-init_tc(_Mod, {end_per_group, GroupName, _}, Config) ->
- call(fun call_generic/3, Config, [pre_end_per_group, GroupName]);
-init_tc(_Mod, TC, Config) ->
- call(fun call_generic/3, Config, [pre_init_per_testcase, TC]).
-
-%% @doc Called as each test case is completed. This includes all configuration
-%% tests.
+ call(fun call_generic_fallback/3, Config,
+ [pre_init_per_group, Mod, GroupName]);
+init_tc(Mod, {end_per_group, GroupName, _}, Config) ->
+ call(fun call_generic_fallback/3, Config,
+ [pre_end_per_group, Mod, GroupName]);
+init_tc(Mod, {init_per_testcase,TC}, Config) ->
+ call(fun call_generic_fallback/3, Config, [pre_init_per_testcase, Mod, TC]);
+init_tc(Mod, {end_per_testcase,TC}, Config) ->
+ call(fun call_generic_fallback/3, Config, [pre_end_per_testcase, Mod, TC]);
+init_tc(Mod, TC = error_in_suite, Config) ->
+ call(fun call_generic_fallback/3, Config, [pre_init_per_testcase, Mod, TC]).
+
-spec end_tc(Mod :: atom(),
- FuncSpec :: atom() |
+ FuncSpec :: atom() |
+ {ConfigFunc :: init_per_testcase | end_per_testcase,
+ TestCase :: atom()} |
{ConfigFunc :: init_per_group | end_per_group,
GroupName :: atom(),
Properties :: list()},
@@ -117,17 +118,24 @@ end_tc(Mod, init_per_suite, Config, _Result, Return) ->
end_tc(Mod, end_per_suite, Config, Result, _Return) ->
call(fun call_generic/3, Result, [post_end_per_suite, Mod, Config],
'$ct_no_change');
-end_tc(_Mod, {init_per_group, GroupName, _}, Config, _Result, Return) ->
- call(fun call_generic/3, Return, [post_init_per_group, GroupName, Config],
- '$ct_no_change');
+end_tc(Mod, {init_per_group, GroupName, _}, Config, _Result, Return) ->
+ call(fun call_generic_fallback/3, Return,
+ [post_init_per_group, Mod, GroupName, Config], '$ct_no_change');
end_tc(Mod, {end_per_group, GroupName, Properties}, Config, Result, _Return) ->
- Res = call(fun call_generic/3, Result,
- [post_end_per_group, GroupName, Config], '$ct_no_change'),
+ Res = call(fun call_generic_fallback/3, Result,
+ [post_end_per_group, Mod, GroupName, Config], '$ct_no_change'),
maybe_stop_locker(Mod, GroupName, Properties),
Res;
-end_tc(_Mod, TC, Config, Result, _Return) ->
- call(fun call_generic/3, Result, [post_end_per_testcase, TC, Config],
- '$ct_no_change').
+end_tc(Mod, {init_per_testcase,TC}, Config, Result, _Return) ->
+ call(fun call_generic_fallback/3, Result,
+ [post_init_per_testcase, Mod, TC, Config], '$ct_no_change');
+end_tc(Mod, {end_per_testcase,TC}, Config, Result, _Return) ->
+ call(fun call_generic_fallback/3, Result,
+ [post_end_per_testcase, Mod, TC, Config], '$ct_no_change');
+end_tc(Mod, TC = error_in_suite, Config, Result, _Return) ->
+ call(fun call_generic_fallback/3, Result,
+ [post_end_per_testcase, Mod, TC, Config], '$ct_no_change').
+
%% Case = TestCase | {TestCase,GroupName}
on_tc_skip(How, {Suite, Case, Reason}) ->
@@ -165,15 +173,21 @@ call_terminate(#ct_hook_config{ module = Mod, state = State} = Hook, _, _) ->
{[],Hook}.
call_cleanup(#ct_hook_config{ module = Mod, state = State} = Hook,
- Reason, [Function, _Suite | Args]) ->
+ Reason, [Function | Args]) ->
NewState = catch_apply(Mod,Function, Args ++ [Reason, State],
- State),
+ State, true),
{Reason, Hook#ct_hook_config{ state = NewState } }.
-call_generic(#ct_hook_config{ module = Mod, state = State} = Hook,
- Value, [Function | Args]) ->
+call_generic(Hook, Value, Meta) ->
+ do_call_generic(Hook, Value, Meta, false).
+
+call_generic_fallback(Hook, Value, Meta) ->
+ do_call_generic(Hook, Value, Meta, true).
+
+do_call_generic(#ct_hook_config{ module = Mod, state = State} = Hook,
+ Value, [Function | Args], Fallback) ->
{NewValue, NewState} = catch_apply(Mod, Function, Args ++ [Value, State],
- {Value,State}),
+ {Value,State}, Fallback),
{NewValue, Hook#ct_hook_config{ state = NewState } }.
%% Generic call function
@@ -209,9 +223,8 @@ call([{Hook, call_id, NextFun} | Rest], Config, Meta, Hooks) ->
Rest ++ [{NewId, call_init}, {NewId,NextFun}]}
end,
call(resort(NewRest,NewHooks,Meta), Config, Meta, NewHooks)
- catch Error:Reason ->
- Trace = erlang:get_stacktrace(),
- ct_logs:log("Suite Hook","Failed to start a CTH: ~p:~p",
+ catch Error:Reason:Trace ->
+ ct_logs:log("Suite Hook","Failed to start a CTH: ~tp:~tp",
[Error,{Reason,Trace}]),
call([], {fail,"Failed to start CTH"
", see the CT Log for details"}, Meta, Hooks)
@@ -241,13 +254,15 @@ remove(Key,List) when is_list(List) ->
remove(_, Else) ->
Else.
-%% Translate scopes, i.e. init_per_group,group1 -> end_per_group,group1 etc
-scope([pre_init_per_testcase, TC|_]) ->
- [post_end_per_testcase, TC];
-scope([pre_init_per_group, GroupName|_]) ->
- [post_end_per_group, GroupName];
-scope([post_init_per_group, GroupName|_]) ->
- [post_end_per_group, GroupName];
+%% Translate scopes, i.e. is_tuplenit_per_group,group1 -> end_per_group,group1 etc
+scope([pre_init_per_testcase, SuiteName, TC|_]) ->
+ [post_init_per_testcase, SuiteName, TC];
+scope([pre_end_per_testcase, SuiteName, TC|_]) ->
+ [post_end_per_testcase, SuiteName, TC];
+scope([pre_init_per_group, SuiteName, GroupName|_]) ->
+ [post_end_per_group, SuiteName, GroupName];
+scope([post_init_per_group, SuiteName, GroupName|_]) ->
+ [post_end_per_group, SuiteName, GroupName];
scope([pre_init_per_suite, SuiteName|_]) ->
[post_end_per_suite, SuiteName];
scope([post_init_per_suite, SuiteName|_]) ->
@@ -255,14 +270,29 @@ scope([post_init_per_suite, SuiteName|_]) ->
scope(init) ->
none.
-terminate_if_scope_ends(HookId, [on_tc_skip,_Suite,{end_per_group,Name}],
+strip_config([post_init_per_testcase, SuiteName, TC|_]) ->
+ [post_init_per_testcase, SuiteName, TC];
+strip_config([post_end_per_testcase, SuiteName, TC|_]) ->
+ [post_end_per_testcase, SuiteName, TC];
+strip_config([post_init_per_group, SuiteName, GroupName|_]) ->
+ [post_init_per_group, SuiteName, GroupName];
+strip_config([post_end_per_group, SuiteName, GroupName|_]) ->
+ [post_end_per_group, SuiteName, GroupName];
+strip_config([post_init_per_suite, SuiteName|_]) ->
+ [post_init_per_suite, SuiteName];
+strip_config([post_end_per_suite, SuiteName|_]) ->
+ [post_end_per_suite, SuiteName];
+strip_config(Other) ->
+ Other.
+
+
+terminate_if_scope_ends(HookId, [on_tc_skip,Suite,{end_per_group,Name}],
Hooks) ->
- terminate_if_scope_ends(HookId, [post_end_per_group, Name], Hooks);
+ terminate_if_scope_ends(HookId, [post_end_per_group, Suite, Name], Hooks);
terminate_if_scope_ends(HookId, [on_tc_skip,Suite,end_per_suite], Hooks) ->
terminate_if_scope_ends(HookId, [post_end_per_suite, Suite], Hooks);
-terminate_if_scope_ends(HookId, [Function,Tag|T], Hooks) when T =/= [] ->
- terminate_if_scope_ends(HookId,[Function,Tag],Hooks);
-terminate_if_scope_ends(HookId, Function, Hooks) ->
+terminate_if_scope_ends(HookId, Function0, Hooks) ->
+ Function = strip_config(Function0),
case lists:keyfind(HookId, #ct_hook_config.id, Hooks) of
#ct_hook_config{ id = HookId, scope = Function} = Hook ->
terminate([Hook]),
@@ -316,7 +346,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;
@@ -365,21 +396,28 @@ pos(Id,[_|Rest],Num) ->
catch_apply(M,F,A, Default) ->
+ catch_apply(M,F,A,Default,false).
+catch_apply(M,F,A, Default, Fallback) ->
+ not erlang:module_loaded(M) andalso (catch M:module_info()),
+ case erlang:function_exported(M,F,length(A)) of
+ false when Fallback ->
+ catch_apply(M,F,tl(A),Default,false);
+ false ->
+ Default;
+ true ->
+ catch_apply(M,F,A)
+ end.
+
+catch_apply(M,F,A) ->
try
- apply(M,F,A)
- catch _:Reason ->
- case erlang:get_stacktrace() of
- %% Return the default if it was the CTH module which did not have the function.
- [{M,F,A,_}|_] when Reason == undef ->
- Default;
- Trace ->
- ct_logs:log("Suite Hook","Call to CTH failed: ~w:~p",
- [error,{Reason,Trace}]),
- throw({error_in_cth_call,
- lists:flatten(
- io_lib:format("~w:~w/~w CTH call failed",
- [M,F,length(A)]))})
- end
+ erlang:apply(M,F,A)
+ catch _:Reason:Trace ->
+ ct_logs:log("Suite Hook","Call to CTH failed: ~w:~tp",
+ [error,{Reason,Trace}]),
+ throw({error_in_cth_call,
+ lists:flatten(
+ io_lib:format("~w:~tw/~w CTH call failed",
+ [M,F,length(A)]))})
end.
@@ -389,7 +427,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..be50a33e01 100644
--- a/lib/common_test/src/ct_hooks_lock.erl
+++ b/lib/common_test/src/ct_hooks_lock.erl
@@ -1,27 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2011. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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 Framework test execution control module.
-%%%
-%%% <p>This module is a proxy for calling and handling locks in
-%%% common test hooks.</p>
-
-module(ct_hooks_lock).
-behaviour(gen_server).
@@ -41,7 +37,6 @@
%%% API
%%%===================================================================
-%% @doc Starts the server
start(Id) ->
case gen_server:start({local, ?SERVER}, ?MODULE, Id, []) of
{error,{already_started, Pid}} ->
@@ -75,13 +70,12 @@ release() ->
%%% gen_server callbacks
%%%===================================================================
-%% @doc Initiates the server
init(Id) ->
+ ct_util:mark_process(),
{ok, #state{ id = Id }}.
-%% @doc Handling call messages
handle_call({stop,Id}, _From, #state{ id = Id, requests = Reqs } = State) ->
- [gen_server:reply(Req, locker_stopped) || {Req,_ReqId} <- Reqs],
+ _ = [gen_server:reply(Req, locker_stopped) || {Req,_ReqId} <- Reqs],
{stop, normal, stopped, State};
handle_call({stop,_Id}, _From, State) ->
{reply, stopped, State};
@@ -106,11 +100,9 @@ handle_call({release, Pid}, _From,
handle_call({release, _Pid}, _From, State) ->
{reply, not_locked, State}.
-%% @doc Handling cast messages
handle_cast(_Msg, State) ->
{noreply, State}.
-%% @doc Handling all non call/cast messages
handle_info({'DOWN',Ref,process,Pid,_},
#state{ locked = {true, Pid, Ref},
requests = [{NextFrom,NextPid}|Rest] } = State) ->
@@ -119,11 +111,9 @@ handle_info({'DOWN',Ref,process,Pid,_},
{noreply,State#state{ locked = {true, NextPid, NextRef},
requests = Rest } }.
-%% @doc This function is called by a gen_server when it is about to terminate.
terminate(_Reason, _State) ->
ok.
-%% @doc Convert process state when code is changed
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl
index fa55a9754e..07a1693d5d 100644
--- a/lib/common_test/src/ct_logs.erl
+++ b/lib/common_test/src/ct_logs.erl
@@ -1,50 +1,52 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2018. 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 Logging functionality for Common Test Framework.
+%%% Logging functionality for Common Test Framework.
+%%%
+%%% This module implements:
%%%
-%%% <p>This module implements
-%%% <ul>
-%%% <li>Internal logging of activities in Common Test Framework</li>
-%%% <li>Compilation of test results into index pages on several levels</li>
-%%% </ul>
-%%% </p>
+%%% Internal logging of activities in Common Test Framework, and
+%%% Compilation of test results into index pages on several levels
-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([log/3, start_log/1, cont_log/2, cont_log_no_timestamp/2, end_log/0]).
-export([set_stylesheet/2, clear_stylesheet/1]).
-export([add_external_logs/1, add_link/3]).
-export([make_last_run_index/0]).
-export([make_all_suites_index/1,make_all_runs_index/1]).
--export([get_ts_html_wrapper/5]).
+-export([get_ts_html_wrapper/5, escape_chars/1]).
-export([xhtml/2, locate_priv_file/1, make_relative/1]).
-export([insert_javascript/1]).
-export([uri/1]).
+-export([parse_keep_logs/1]).
%% Logging stuff directly from testcase
--export([tc_log/3, tc_log/4, tc_log/5, tc_log_async/3, tc_log_async/5,
- tc_print/3, tc_print/4,
- tc_pal/3, tc_pal/4, ct_log/3, basic_html/0]).
+-export([tc_log/3, tc_log/4, tc_log/5, tc_log/6,
+ tc_log_async/3, tc_log_async/5,
+ tc_print/3, tc_print/4, tc_print/5,
+ tc_pal/3, tc_pal/4, tc_pal/5, ct_log/3,
+ basic_html/0]).
%% Simulate logger process for use without ct environment running
-export([simulate/0]).
@@ -73,22 +75,24 @@
-define(abs(Name), filename:absname(Name)).
+-define(now, os:timestamp()).
+
-record(log_cache, {version,
all_runs = [],
tests = []}).
%%%-----------------------------------------------------------------
-%%% @spec init(Mode, Verbosity) -> Result
+%%% -spec init(Mode, Verbosity) -> Result
%%% Mode = normal | interactive
%%% Result = {StartTime,LogDir}
%%% StartTime = term()
%%% LogDir = string()
%%%
-%%% @doc Initiate the logging mechanism (tool-internal use only).
+%%% Initiate the logging mechanism (tool-internal use only).
%%%
-%%% <p>This function is called by ct_util.erl when testing is
-%%% started. A new directory named ct_run.&lt;timestamp&gt; is created
-%%% and all logs are stored under this directory.</p>
+%%% This function is called by ct_util.erl when testing is
+%%% started. A new directory named ct_run.<timestamp> is created
+%%% and all logs are stored under this directory.
%%%
init(Mode, Verbosity) ->
Self = self(),
@@ -123,16 +127,17 @@ datestr_from_dirname([]) ->
"".
%%%-----------------------------------------------------------------
-%%% @spec close(Info, StartDir) -> ok
+%%% -spec close(Info, StartDir) -> ok
%%%
-%%% @doc Create index pages with test results and close the CT Log
+%%% Create index pages with test results and close the CT Log
%%% (tool-internal use only).
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 =
case make_last_run_index() of
- {error,_} -> % log server not responding
+ {error, Reason} -> % log server not responding
+ io:format("Warning! ct_logs not responding: ~tp~n", [Reason]),
undefined;
LCB ->
LCB
@@ -144,7 +149,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,
@@ -168,14 +173,14 @@ close(Info, StartDir) ->
ok ->
ok;
Error ->
- io:format("Warning! Cleanup failed: ~p~n", [Error])
+ io:format("Warning! Cleanup failed: ~tp~n", [Error])
end,
- make_all_suites_index(stop),
+ _ = make_all_suites_index(stop),
make_all_runs_index(stop),
Cache2File();
true ->
- file:set_cwd(".."),
- make_all_suites_index(stop),
+ ok = file:set_cwd(".."),
+ _ = make_all_suites_index(stop),
make_all_runs_index(stop),
Cache2File(),
case ct_util:get_profile_data(browser, StartDir) of
@@ -197,22 +202,22 @@ close(Info, StartDir) ->
ok.
%%%-----------------------------------------------------------------
-%%% @spec set_stylesheet(TC,SSFile) -> ok
+%%% -spec set_stylesheet(TC,SSFile) -> ok
set_stylesheet(TC, SSFile) ->
cast({set_stylesheet,TC,SSFile}).
%%%-----------------------------------------------------------------
-%%% @spec clear_stylesheet(TC) -> ok
+%%% -spec clear_stylesheet(TC) -> ok
clear_stylesheet(TC) ->
cast({clear_stylesheet,TC}).
%%%-----------------------------------------------------------------
-%%% @spec get_log_dir() -> {ok,Dir} | {error,Reason}
+%%% -spec get_log_dir() -> {ok,Dir} | {error,Reason}
get_log_dir() ->
get_log_dir(false).
%%%-----------------------------------------------------------------
-%%% @spec get_log_dir(ReturnAbsName) -> {ok,Dir} | {error,Reason}
+%%% -spec get_log_dir(ReturnAbsName) -> {ok,Dir} | {error,Reason}
get_log_dir(ReturnAbsName) ->
case call({get_log_dir,ReturnAbsName}) of
{error,does_not_exist} when ReturnAbsName == true ->
@@ -235,7 +240,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]),
@@ -246,134 +251,158 @@ 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
+%%% -spec init_tc(RefreshLog) -> ok
%%%
-%%% @doc Test case initiation (tool-internal use only).
+%%% Test case initiation (tool-internal use only).
%%%
-%%% <p>This function is called by ct_framework:init_tc/3</p>
+%%% This function is called by ct_framework:init_tc/3
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
+%%% -spec end_tc(TCPid) -> ok
%%%
-%%% @doc Test case clean up (tool-internal use only).
+%%% Test case clean up (tool-internal use only).
%%%
-%%% <p>This function is called by ct_framework:end_tc/3</p>
+%%% This function is called by ct_framework:end_tc/3
end_tc(TCPid) ->
%% use call here so that the TC process will wait and receive
%% possible exit signals from ct_logs before end_tc returns ok
call({end_tc,TCPid}).
%%%-----------------------------------------------------------------
-%%% @spec register_groupleader(Pid,GroupLeader) -> ok
+%%% -spec register_groupleader(Pid,GroupLeader) -> ok
%%%
-%%% @doc To enable logging to a group leader (tool-internal use only).
+%%% To enable logging to a group leader (tool-internal use only).
%%%
-%%% <p>This function is called by ct_framework:report/2</p>
+%%% This function is called by ct_framework:report/2
register_groupleader(Pid,GroupLeader) ->
call({register_groupleader,Pid,GroupLeader}),
ok.
%%%-----------------------------------------------------------------
-%%% @spec unregister_groupleader(Pid) -> ok
+%%% -spec unregister_groupleader(Pid) -> ok
%%%
-%%% @doc To disable logging to a group leader (tool-internal use only).
+%%% To disable logging to a group leader (tool-internal use only).
%%%
-%%% <p>This function is called by ct_framework:report/2</p>
+%%% This function is called by ct_framework:report/2
unregister_groupleader(Pid) ->
call({unregister_groupleader,Pid}),
ok.
%%%-----------------------------------------------------------------
-%%% @spec log(Heading,Format,Args) -> ok
+%%% -spec log(Heading,Format,Args) -> ok
%%%
-%%% @doc Log internal activity (tool-internal use only).
+%%% Log internal activity (tool-internal use only).
%%%
-%%% <p>This function writes an entry to the currently active log,
-%%% i.e. either the CT log or a test case log.</p>
+%%% This function writes an entry to the currently active log,
+%%% i.e. either the CT log or a test case log.
%%%
-%%% <p><code>Heading</code> is a short string indicating what type of
-%%% activity it is. <code>Format</code> and <code>Args</code> is the
-%%% data to log (as in <code>io:format(Format,Args)</code>).</p>
+%%% Heading is a short string indicating what type of
+%%% activity it is. Format and Args is the
+%%% data to log (as in io:format(Format,Args)).
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.
%%%-----------------------------------------------------------------
-%%% @spec start_log(Heading) -> ok
+%%% -spec start_log(Heading) -> ok
%%%
-%%% @doc Starts the logging of an activity (tool-internal use only).
+%%% Starts the logging of an activity (tool-internal use only).
%%%
-%%% <p>This function must be used in combination with
-%%% <code>cont_log/2</code> and <code>end_log/0</code>. The intention
-%%% is to call <code>start_log</code> once, then <code>cont_log</code>
-%%% any number of times and finally <code>end_log</code> once.</p>
+%%% This function must be used in combination with
+%%% cont_log/2 and end_log/0. The intention
+%%% is to call start_log once, then cont_log
+%%% any number of times and finally end_log once.
%%%
-%%% <p>For information about the parameters, see <code>log/3</code>.</p>
+%%% For information about the parameters, see log/3.
%%%
-%%% @see log/3
-%%% @see cont_log/2
-%%% @see end_log/0
+%%% See log/3, cont_log/2, and 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.
%%%-----------------------------------------------------------------
-%%% @spec cont_log(Format,Args) -> ok
+%%% -spec cont_log(Format,Args) -> ok
%%%
-%%% @doc Adds information about an activity (tool-internal use only).
+%%% Adds information about an activity (tool-internal use only).
%%%
-%%% @see start_log/1
-%%% @see end_log/0
+%%% See start_log/1 and end_log/0.
cont_log([],[]) ->
ok;
cont_log(Format,Args) ->
maybe_log_timestamp(),
cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE,
- [{Format,Args}]}),
+ [{Format,Args}],true}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% -spec cont_log_no_timestamp(Format,Args) -> ok
+%%%
+%%% Adds information about an activity (tool-internal use only).
+%%%
+%%% See start_log/1 and end_log/0.
+cont_log_no_timestamp([],[]) ->
+ ok;
+cont_log_no_timestamp(Format,Args) ->
+ cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE,
+ [{Format,Args}],true}),
ok.
%%%-----------------------------------------------------------------
-%%% @spec end_log() -> ok
+%%% -spec end_log() -> ok
%%%
-%%% @doc Ends the logging of an activity (tool-internal use only).
+%%% Ends the logging of an activity (tool-internal use only).
%%%
-%%% @see start_log/1
-%%% @see cont_log/2
+%%% See start_log/1 and cont_log/2.
end_log() ->
cast({log,sync,self(),group_leader(),ct_internal,?MAX_IMPORTANCE,
- [{int_footer(), []}]}),
+ [{ft,int_footer(), []}],false}),
ok.
%%%-----------------------------------------------------------------
-%%% @spec add_external_logs(Logs) -> ok
+%%% -spec add_external_logs(Logs) -> ok
%%% Logs = [Log]
%%% Log = string()
%%%
-%%% @doc Print a link to each given <code>Log</code> in the test case
+%%% Print a link to each given Log in the test case
%%% log.
%%%
-%%% <p>The given <code>Logs</code> must exist in the priv dir of the
-%%% calling test suite.</p>
+%%% The given Logs must exist in the priv dir of the
+%%% calling test suite.
add_external_logs(Logs) ->
start_log("External Logs"),
[cont_log("<a href=\"~ts\">~ts</a>\n",
@@ -381,95 +410,122 @@ add_external_logs(Logs) ->
end_log().
%%%-----------------------------------------------------------------
-%%% @spec add_link(Heading,File,Type) -> ok
+%%% -spec add_link(Heading,File,Type) -> ok
%%% Heading = string()
%%% File = string()
%%% Type = string()
%%%
-%%% @doc Print a link to a given file stored in the priv_dir of the
+%%% Print a link to a given file stored in the priv_dir of the
%%% calling test suite.
add_link(Heading,File,Type) ->
- log(Heading,"<a href=\"~ts\" type=~p>~ts</a>\n",
+ log(Heading,"<a href=\"~ts\" type=~tp>~ts</a>\n",
[uri(filename:join("log_private",File)),Type,File]).
%%%-----------------------------------------------------------------
-%%% @spec tc_log(Category,Format,Args) -> ok
-%%% @equiv tc_log(Category,?STD_IMPORTANCE,Format,Args)
+%%% -spec tc_log(Category,Format,Args) -> ok
+%%% Equivalent to 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)
+%%% -spec tc_log(Category,Importance,Format,Args) -> ok
+%%% Equivalent to 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,Format,Args) -> ok
+%%% Equivalent to 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) -> ok
+%%% -spec tc_log(Category,Importance,Heading,Format,Args,Opts) -> ok
%%% Category = atom()
%%% Importance = integer()
-%%% Printer = string()
+%%% Heading = string()
%%% Format = string()
%%% Args = list()
+%%% Opts = list()
%%%
-%%% @doc Printout from a testcase.
+%%% Printout from a testcase.
%%%
-%%% <p>This function is called by <code>ct</code> when logging
+%%% This function is called by ct 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(),[]}]}),
+%%% framework).
+tc_log(Category,Importance,Heading,Format,Args,Opts) ->
+ Data =
+ case lists:member(no_css, Opts) of
+ true ->
+ [{Format,Args}];
+ false ->
+ Heading1 =
+ case proplists:get_value(heading, Opts) of
+ undefined -> Heading;
+ Str -> Str
+ end,
+ [{hd,div_header(Category,Heading1),[]},
+ {Format,Args},
+ {ft,div_footer(),[]}]
+ end,
+ cast({log,sync,self(),group_leader(),Category,Importance,Data,
+ lists:member(esc_chars, Opts)}),
ok.
%%%-----------------------------------------------------------------
-%%% @spec tc_log_async(Category,Format,Args) -> ok
-%%% @equiv tc_log_async(Category,?STD_IMPORTANCE,"User",Format,Args)
+%%% -spec tc_log_async(Category,Format,Args) -> ok
+%%% Equivalent to tc_log_async(Category,?STD_IMPORTANCE,"User",Format,Args)
tc_log_async(Category,Format,Args) ->
tc_log_async(Category,?STD_IMPORTANCE,"User",Format,Args).
%%%-----------------------------------------------------------------
-%%% @spec tc_log_async(Category,Importance,Format,Args) -> ok
+%%% -spec tc_log_async(Category,Importance,Format,Args) -> ok
%%% Category = atom()
%%% Importance = integer()
-%%% Printer = string()
+%%% Heading = string()
%%% Format = string()
%%% Args = list()
%%%
-%%% @doc Internal use only.
+%%% Internal use only.
%%%
-%%% <p>This function is used to perform asynchronous printouts
+%%% This function is used to perform asynchronous printouts
%%% towards the test server IO handler. This is necessary in order
%%% to avoid deadlocks when e.g. the hook that handles SASL printouts
%%% prints to the test case log file at the same time test server
-%%% asks ct_logs for an html wrapper.</p>
-tc_log_async(Category,Importance,Printer,Format,Args) ->
+%%% asks ct_logs for an html wrapper.
+tc_log_async(Category,Importance,Heading,Format,Args) ->
cast({log,async,self(),group_leader(),Category,Importance,
- [{div_header(Category,Printer),[]},
+ [{hd,div_header(Category,Heading),[]},
{Format,Args},
- {div_footer(),[]}]}),
+ {ft,div_footer(),[]}],
+ true}),
ok.
%%%-----------------------------------------------------------------
-%%% @spec tc_print(Category,Format,Args)
-%%% @equiv tc_print(Category,?STD_IMPORTANCE,Format,Args)
+%%% -spec tc_print(Category,Format,Args)
+%%% Equivalent to tc_print(Category,?STD_IMPORTANCE,Format,Args,[])
tc_print(Category,Format,Args) ->
- tc_print(Category,?STD_IMPORTANCE,Format,Args).
+ tc_print(Category,?STD_IMPORTANCE,Format,Args,[]).
+
+%%%-----------------------------------------------------------------
+%%% -spec tc_print(Category,Importance,Format,Args)
+%%% Equivalent to tc_print(Category,Importance,Format,Args,[])
+tc_print(Category,Importance,Format,Args) ->
+ tc_print(Category,Importance,Format,Args,[]).
%%%-----------------------------------------------------------------
-%%% @spec tc_print(Category,Importance,Format,Args) -> ok
+%%% -spec tc_print(Category,Importance,Format,Args,Opts) -> ok
%%% Category = atom()
%%% Importance = integer()
%%% Format = string()
%%% Args = list()
+%%% Opts = list()
%%%
-%%% @doc Console printout from a testcase.
+%%% Console printout from a testcase.
%%%
-%%% <p>This function is called by <code>ct</code> when printing
-%%% stuff from a testcase on the user console.</p>
-tc_print(Category,Importance,Format,Args) ->
+%%% This function is called by ct when printing
+%%% stuff from a testcase on the user console.
+tc_print(Category,Importance,Format,Args,Opts) ->
VLvl = case ct_util:get_verbosity(Category) of
undefined ->
ct_util:get_verbosity('$unspecified');
@@ -481,91 +537,103 @@ tc_print(Category,Importance,Format,Args) ->
Val
end,
if Importance >= (100-VLvl) ->
- Head = get_heading(Category),
- io:format(user, lists:concat([Head,Format,"\n\n"]), Args),
+ Heading =
+ case proplists:get_value(heading, Opts) of
+ undefined -> atom_to_list(Category);
+ Hd -> Hd
+ end,
+ Str = lists:concat([get_header(Heading),Format,"\n\n"]),
+ try
+ io:format(?def_gl, Str, Args)
+ catch
+ %% default group leader probably not started, or has stopped
+ _:_ -> io:format(user, Str, Args)
+ end,
ok;
true ->
ok
end.
-get_heading(default) ->
+get_header("default") ->
io_lib:format("\n-----------------------------"
"-----------------------\n~s\n",
- [log_timestamp(now())]);
-get_heading(Category) ->
+ [log_timestamp(?now)]);
+get_header(Heading) ->
io_lib:format("\n-----------------------------"
- "-----------------------\n~s ~w\n",
- [log_timestamp(now()),Category]).
+ "-----------------------\n~ts ~s\n",
+ [Heading,log_timestamp(?now)]).
%%%-----------------------------------------------------------------
-%%% @spec tc_pal(Category,Format,Args) -> ok
-%%% @equiv tc_pal(Category,?STD_IMPORTANCE,Format,Args) -> ok
+%%% -spec tc_pal(Category,Format,Args) -> ok
+%%% Equivalent to tc_pal(Category,?STD_IMPORTANCE,Format,Args,[]) -> ok
tc_pal(Category,Format,Args) ->
- tc_pal(Category,?STD_IMPORTANCE,Format,Args).
+ tc_pal(Category,?STD_IMPORTANCE,Format,Args,[]).
%%%-----------------------------------------------------------------
-%%% @spec tc_pal(Category,Importance,Format,Args) -> ok
+%%% -spec tc_pal(Category,Importance,Format,Args) -> ok
+%%% Equivalent to tc_pal(Category,Importance,Format,Args,[]) -> ok
+tc_pal(Category,Importance,Format,Args) ->
+ tc_pal(Category,Importance,Format,Args,[]).
+
+%%%-----------------------------------------------------------------
+%%% -spec tc_pal(Category,Importance,Format,Args,Opts) -> ok
%%% Category = atom()
%%% Importance = integer()
%%% Format = string()
%%% Args = list()
+%%% Opts = list()
%%%
-%%% @doc Print and log from a testcase.
+%%% Print and log from a testcase.
%%%
-%%% <p>This function is called by <code>ct</code> when logging
+%%% This function is called by ct when logging
%%% stuff directly from a testcase. The info is written both in the
-%%% log and on the console.</p>
-tc_pal(Category,Importance,Format,Args) ->
- tc_print(Category,Importance,Format,Args),
- cast({log,sync,self(),group_leader(),Category,Importance,
- [{div_header(Category),[]},
- {Format,Args},
- {div_footer(),[]}]}),
- ok.
-
+%%% log and on the console.
+tc_pal(Category,Importance,Format,Args,Opts) ->
+ tc_print(Category,Importance,Format,Args,Opts),
+ tc_log(Category,Importance,"User",Format,Args,[esc_chars|Opts]).
%%%-----------------------------------------------------------------
-%%% @spec ct_pal(Category,Format,Args) -> ok
+%%% -spec ct_log(Category,Format,Args) -> ok
%%% Category = atom()
%%% Format = string()
%%% Args = list()
%%%
-%%% @doc Print and log to the ct framework log
+%%% Print to the ct framework log
%%%
-%%% <p>This function is called by internal ct functions to
-%%% force logging to the ct framework log</p>
+%%% This function is called by internal ct functions to
+%%% force logging to the ct framework log
ct_log(Category,Format,Args) ->
- cast({ct_log,[{div_header(Category),[]},
+ cast({ct_log,[{hd,div_header(Category),[]},
{Format,Args},
- {div_footer(),[]}]}),
+ {ft,div_footer(),[]}],
+ true}),
ok.
%%%=================================================================
%%% Internal functions
int_header() ->
- "<div class=\"ct_internal\"><b>*** CT ~s *** ~ts</b>".
+ "</pre>\n<div class=\"ct_internal\"><pre><b>*** CT ~s *** ~ts</b>".
int_footer() ->
- "</div>".
+ "</pre></div>\n<pre>".
div_header(Class) ->
div_header(Class,"User").
-div_header(Class,Printer) ->
- "\n<div class=\"" ++ atom_to_list(Class) ++ "\"><b>*** " ++ Printer ++
- " " ++ log_timestamp(now()) ++ " ***</b>".
+div_header(Class,Heading) ->
+ "\n</pre>\n<div class=\"" ++ atom_to_list(Class) ++ "\"><pre><b>*** "
+ ++ Heading ++ " " ++ log_timestamp(?now) ++ " ***</b>".
div_footer() ->
- "</div>".
-
+ "</pre></div>\n<pre>".
maybe_log_timestamp() ->
- {MS,S,US} = now(),
+ {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}) ->
@@ -586,10 +654,12 @@ log_timestamp({MS,S,US}) ->
ct_log_fd,
tc_groupleaders,
stylesheet,
- async_print_jobs}).
+ async_print_jobs,
+ tc_esc_chars}).
logger(Parent, Mode, Verbosity) ->
register(?MODULE,self()),
+ ct_util:mark_process(),
%%! Below is a temporary workaround for the limitation of
%%! max one test run per second.
%%! --->
@@ -607,7 +677,7 @@ logger(Parent, Mode, Verbosity) ->
end,
%%! <---
- file:make_dir(Dir),
+ _ = file:make_dir(Dir),
AbsDir = ?abs(Dir),
put(ct_run_dir, AbsDir),
@@ -627,18 +697,18 @@ logger(Parent, Mode, Verbosity) ->
PrivFilesDestRun = [filename:join(AbsDir, F) || F <- PrivFiles],
case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of
{error,Src1,Dest1,Reason1} ->
- io:format(user, "ERROR! "++
- "Priv file ~p could not be copied to ~p. "++
- "Reason: ~p~n",
+ io:format(?def_gl, "ERROR! "++
+ "Priv file ~tp could not be copied to ~tp. "++
+ "Reason: ~tp~n",
[Src1,Dest1,Reason1]),
exit({priv_file_error,Dest1});
ok ->
case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of
{error,Src2,Dest2,Reason2} ->
- io:format(user,
+ io:format(?def_gl,
"ERROR! "++
- "Priv file ~p could not be copied to ~p. "
- ++"Reason: ~p~n",
+ "Priv file ~tp could not be copied to ~tp. "
+ ++"Reason: ~tp~n",
[Src2,Dest2,Reason2]),
exit({priv_file_error,Dest2});
ok ->
@@ -647,7 +717,7 @@ logger(Parent, Mode, Verbosity) ->
end
end,
- test_server_io:start_link(),
+ _ = test_server_io:start_link(),
MiscIoName = filename:join(Dir, ?misc_io_log),
{ok,MiscIoFd} = file:open(MiscIoName,
[write,{encoding,utf8}]),
@@ -677,16 +747,16 @@ logger(Parent, Mode, Verbosity) ->
ct_event:notify(#event{name=start_logging,node=node(),
data=AbsDir}),
make_all_runs_index(start),
- make_all_suites_index(start),
+ _ = make_all_suites_index(start),
case Mode of
interactive -> interactive_link();
_ -> ok
end,
- file:set_cwd(Dir),
- make_last_run_index(Time),
+ ok = file:set_cwd(Dir),
+ _ = make_last_run_index(Time),
CtLogFd = open_ctlog(?misc_io_log),
io:format(CtLogFd,int_header()++int_footer(),
- [log_timestamp(now()),"Common Test Logger started"]),
+ [log_timestamp(?now),"Common Test Logger started"]),
Parent ! {started,self(),{Time,filename:absname("")}},
set_evmgr_gl(CtLogFd),
@@ -697,22 +767,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
@@ -726,7 +800,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;
@@ -738,20 +812,21 @@ logger_loop(State) ->
end,
if Importance >= (100-VLvl) ->
CtLogFd = State#logger_state.ct_log_fd,
+ DoEscChars = State#logger_state.tc_esc_chars and EscChars,
case get_groupleader(Pid, GL, State) of
{tc_log,TCGL,TCGLs} ->
case erlang:is_process_alive(TCGL) of
true ->
State1 = print_to_log(SyncOrAsync, Pid,
- Category,
- TCGL, List, State),
+ Category, TCGL, Content,
+ DoEscChars, State),
logger_loop(State1#logger_state{
tc_groupleaders = TCGLs});
false ->
%% Group leader is dead, so write to the
%% CtLog or unexpected_io log instead
- unexpected_io(Pid,Category,Importance,
- List,CtLogFd),
+ unexpected_io(Pid, Category, Importance,
+ Content, CtLogFd, DoEscChars),
logger_loop(State)
end;
@@ -759,7 +834,8 @@ logger_loop(State) ->
%% If category is ct_internal then write
%% to ct_log, else write to unexpected_io
%% log
- unexpected_io(Pid,Category,Importance,List,CtLogFd),
+ unexpected_io(Pid, Category, Importance, Content,
+ CtLogFd, DoEscChars),
logger_loop(State#logger_state{
tc_groupleaders = TCGLs})
end;
@@ -770,10 +846,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)
@@ -800,7 +877,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 ==
@@ -808,17 +885,24 @@ logger_loop(State) ->
logger_loop(State);
{set_stylesheet,TC,SSFile} ->
Fd = State#logger_state.ct_log_fd,
- io:format(Fd, "~p loading external style sheet: ~ts~n",
+ io:format(Fd, "~tp loading external style sheet: ~ts~n",
[TC,SSFile]),
logger_loop(State#logger_state{stylesheet = SSFile});
{clear_stylesheet,_} when State#logger_state.stylesheet == undefined ->
logger_loop(State);
{clear_stylesheet,_} ->
logger_loop(State#logger_state{stylesheet = undefined});
- {ct_log, List} ->
+ {ct_log,Content,EscChars} ->
+ Str = lists:map(fun({_HdOrFt,Str,Args}) ->
+ [io_lib:format(Str,Args),io_lib:nl()];
+ ({Str,Args}) when EscChars ->
+ Io = io_lib:format(Str,Args),
+ [escape_chars(Io),io_lib:nl()];
+ ({Str,Args}) ->
+ [io_lib:format(Str,Args),io_lib:nl()]
+ end, Content),
Fd = State#logger_state.ct_log_fd,
- [begin io:format(Fd,Str,Args),io:nl(Fd) end ||
- {Str,Args} <- List],
+ io:format(Fd, "~ts", [Str]),
logger_loop(State);
{'DOWN',Ref,_,_Pid,_} ->
%% there might be print jobs executing in parallel with ct_logs
@@ -835,51 +919,86 @@ 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, CtLogFd) ->
+create_io_fun(FromPid, CtLogFd, EscChars) ->
%% we have to build one io-list of all strings
%% before printing, or other io printouts (made in
%% parallel) may get printed between this header
%% and footer
- fun({Str,Args}, IoList) ->
- case catch io_lib:format(Str,Args) of
- {'EXIT',_Reason} ->
- io:format(CtLogFd, "Logging fails! Str: ~p, Args: ~p~n",
- [Str,Args]),
- %% stop the testcase, we need to see the fault
- exit(FromPid, {log_printout_error,Str,Args}),
- [];
+ fun(FormatData, IoList) ->
+ {Escapable,Str,Args} =
+ case FormatData of
+ {_HdOrFt,S,A} -> {false,S,A};
+ {S,A} -> {true,S,A}
+ end,
+ try io_lib:format(Str, Args) of
+ IoStr when Escapable, EscChars, IoList == [] ->
+ escape_chars(IoStr);
+ IoStr when Escapable, EscChars ->
+ [IoList,"\n",escape_chars(IoStr)];
IoStr when IoList == [] ->
- [IoStr];
+ IoStr;
IoStr ->
[IoList,"\n",IoStr]
+ catch
+ _:_Reason ->
+ io:format(CtLogFd, "Logging fails! Str: ~tp, Args: ~tp~n",
+ [Str,Args]),
+ %% stop the testcase, we need to see the fault
+ exit(FromPid, {log_printout_error,Str,Args}),
+ []
end
end.
-print_to_log(sync, FromPid, Category, TCGL, List, State) ->
+escape_chars([Bin | Io]) when is_binary(Bin) ->
+ [Bin | escape_chars(Io)];
+escape_chars([List | Io]) when is_list(List) ->
+ [escape_chars(List) | escape_chars(Io)];
+escape_chars([$< | Io]) ->
+ ["&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, CtLogFd),
- io:format(TCGL,"~ts", [lists:foldl(IoFun, [], List)]);
+ IoFun = create_io_fun(FromPid, CtLogFd, EscChars),
+ IoList = lists:foldl(IoFun, [], Content),
+ try tc_io_format(TCGL, "~ts", [IoList]) of
+ ok -> ok
+ catch
+ _:_ ->
+ io:format(TCGL,"~ts", [IoList])
+ end;
true ->
- unexpected_io(FromPid,Category,?MAX_IMPORTANCE,List,CtLogFd)
+ unexpected_io(FromPid, Category, ?MAX_IMPORTANCE, Content,
+ CtLogFd, EscChars)
end,
State;
-print_to_log(async, FromPid, Category, TCGL, List, State) ->
+print_to_log(async, FromPid, Category, TCGL, Content, EscChars, State) ->
%% in some situations (exceptions), the printout is made from the
%% test server IO process and there's no valid group leader to send to
CtLogFd = State#logger_state.ct_log_fd,
Printer =
if FromPid /= TCGL ->
- IoFun = create_io_fun(FromPid, CtLogFd),
+ IoFun = create_io_fun(FromPid, CtLogFd, EscChars),
fun() ->
+ ct_util:mark_process(),
test_server:permit_io(TCGL, self()),
%% Since asynchronous io gets can get buffered if
@@ -891,25 +1010,29 @@ print_to_log(async, FromPid, Category, TCGL, List, State) ->
case erlang:is_process_alive(TCGL) of
true ->
- try io:format(TCGL, "~ts",
- [lists:foldl(IoFun,[],List)]) of
+ try tc_io_format(TCGL, "~ts",
+ [lists:foldl(IoFun,[],Content)]) of
_ -> ok
catch
_:terminated ->
unexpected_io(FromPid, Category,
?MAX_IMPORTANCE,
- List, CtLogFd)
+ Content, CtLogFd, EscChars);
+ _:_ ->
+ io:format(TCGL, "~ts",
+ [lists:foldl(IoFun,[],Content)])
end;
false ->
unexpected_io(FromPid, Category,
?MAX_IMPORTANCE,
- List, CtLogFd)
+ Content, CtLogFd, EscChars)
end
end;
true ->
fun() ->
+ ct_util:mark_process(),
unexpected_io(FromPid, Category, ?MAX_IMPORTANCE,
- List, CtLogFd)
+ Content, CtLogFd, EscChars)
end
end,
case State#logger_state.async_print_jobs of
@@ -1024,7 +1147,7 @@ open_ctlog(MiscIoName) ->
Dir = filename:dirname(Cwd),
Variables = ct_run:variables_file_name(Dir),
io:format(Fd,
- "Can not read the file \'~ts\' Reason: ~w\n"
+ "Can not read the file \'~ts\' Reason: ~tw\n"
"No configuration found for test!!\n",
[Variables,Reason])
end,
@@ -1039,66 +1162,82 @@ open_ctlog(MiscIoName) ->
"View I/O logged after the test run</a></li>\n</ul>\n",
[MiscIoName,MiscIoName]),
- print_style(Fd,undefined),
+ print_style(Fd, fun io:format/3, undefined),
io:format(Fd,
xhtml("<br><h2>Progress Log</h2>\n<pre>\n",
"<br />\n<h4>PROGRESS LOG</h4>\n<pre>\n"), []),
Fd.
-print_style(Fd,undefined) ->
+print_style(Fd, IoFormat, undefined) ->
case basic_html() of
true ->
- io:format(Fd,
- "<style>\n"
- "div.ct_internal { background:lightgrey; color:black; }\n"
- "div.default { background:lightgreen; color:black; }\n"
- "</style>\n",
- []);
+ Style = "<style>\n
+ div.ct_internal { background:lightgrey; color:black; }\n
+ div.default { background:lightgreen; color:black; }\n
+ </style>\n",
+ IoFormat(Fd, Style, []);
_ ->
ok
end;
-print_style(Fd,StyleSheet) ->
+print_style(Fd, IoFormat, StyleSheet) ->
case file:read_file(StyleSheet) of
{ok,Bin} ->
Str = b2s(Bin,encoding(StyleSheet)),
- Pos0 = case string:str(Str,"<style>") of
- 0 -> string:str(Str,"<STYLE>");
- N0 -> N0
- end,
- Pos1 = case string:str(Str,"</style>") of
- 0 -> string:str(Str,"</STYLE>");
- N1 -> N1
- end,
- if (Pos0 == 0) and (Pos1 /= 0) ->
- print_style_error(Fd,StyleSheet,missing_style_start_tag);
- (Pos0 /= 0) and (Pos1 == 0) ->
- print_style_error(Fd,StyleSheet,missing_style_end_tag);
- Pos0 /= 0 ->
- Style = string:sub_string(Str,Pos0,Pos1+7),
- io:format(Fd,"~ts\n",[Style]);
- Pos0 == 0 ->
- io:format(Fd,"<style>~ts</style>\n",[Str])
- end;
+ case re:run(Str,"<style>.*</style>",
+ [dotall,caseless,{capture,all,list}]) of
+ nomatch ->
+ case re:run(Str,"</?style>",[caseless,{capture,all,list}]) of
+ nomatch ->
+ IoFormat(Fd,"<style>\n~ts</style>\n",[Str]);
+ {match,["</"++_]} ->
+ print_style_error(Fd, IoFormat,
+ StyleSheet,
+ missing_style_start_tag);
+ {match,[_]} ->
+ print_style_error(Fd, IoFormat,
+ StyleSheet,missing_style_end_tag)
+ end;
+ {match,[Style]} ->
+ IoFormat(Fd,"~ts\n",[Style])
+ end;
{error,Reason} ->
- print_style_error(Fd,StyleSheet,Reason)
+ print_style_error(Fd,IoFormat,StyleSheet,Reason)
end.
-%% Simple link version, doesn't work with all browsers unfortunately. :-(
-%% print_style(Fd, StyleSheet) ->
-%% io:format(Fd,
-%% "<link href=~p rel=\"stylesheet\" type=\"text/css\">",
-%% [StyleSheet]).
-
-print_style_error(Fd,StyleSheet,Reason) ->
- io:format(Fd,"\n<!-- Failed to load stylesheet ~ts: ~p -->\n",
- [StyleSheet,Reason]),
- print_style(Fd,undefined).
+print_style_error(Fd, IoFormat, StyleSheet, Reason) ->
+ IO = io_lib:format("\n<!-- Failed to load stylesheet ~ts: ~tp -->\n",
+ [StyleSheet,Reason]),
+ IoFormat(Fd, IO, []),
+ print_style(Fd, IoFormat, undefined).
close_ctlog(Fd) ->
io:format(Fd, "\n</pre>\n", []),
io:format(Fd, [xhtml("<br><br>\n", "<br /><br />\n") | footer()], []),
- file:close(Fd).
+ ok = file:close(Fd).
+
+%%%-----------------------------------------------------------------
+%%% tc_io_format/3
+%%% Tell common_test's IO server (group leader) not to escape
+%%% HTML characters.
+
+-spec tc_io_format(io:device(), io:format(), [term()]) -> 'ok'.
+
+tc_io_format(Fd, Format0, Args) ->
+ %% We know that the specially wrapped format string is handled
+ %% by our IO server, but Dialyzer does not and would tell us
+ %% that the call to io:format/3 would fail. Therefore, we must
+ %% fool dialyzer.
+
+ Format = case cloaked_true() of
+ true -> ["$tc_html",Format0];
+ false -> Format0 %Never happens.
+ end,
+ io:format(Fd, Format, Args).
+
+%% Return 'true', but let dialyzer think that a boolean is returned.
+cloaked_true() ->
+ is_process_alive(self()).
%%%-----------------------------------------------------------------
@@ -1110,11 +1249,11 @@ make_last_run_index(StartTime) ->
case catch make_last_run_index1(StartTime,IndexName) of
{'EXIT', Reason} ->
io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"),
- io:format("~p~n", [Reason]),
+ io:format("~tp~n", [Reason]),
{error, Reason};
{error, Reason} ->
io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"),
- io:format("~p~n", [Reason]),
+ io:format("~tp~n", [Reason]),
{error, Reason};
ok ->
ok;
@@ -1268,9 +1407,9 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip,
{Lbl,Timestamp,Node,AllInfo} =
case All of
{true,OldRuns} ->
- [_Prefix,NodeOrDate|_] = string:tokens(Link,"."),
- Node1 = case string:chr(NodeOrDate,$@) of
- 0 -> "-";
+ [_Prefix,NodeOrDate|_] = string:lexemes(Link,"."),
+ Node1 = case string:find(NodeOrDate,[$@]) of
+ nomatch -> "-";
_ -> NodeOrDate
end,
@@ -1361,11 +1500,11 @@ total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) ->
[xhtml("<tr valign=top>\n",
["</tbody>\n<tfoot>\n<tr class=\"",odd_or_even(),"\">\n"]),
"<td><b>Total</b></td>\n", Label, TimestampCell,
- "<td align=right><b>",integer_to_list(Success),"<b></td>\n",
- "<td align=right><b>",integer_to_list(Fail),"<b></td>\n",
+ "<td align=right><b>",integer_to_list(Success),"</b></td>\n",
+ "<td align=right><b>",integer_to_list(Fail),"</b></td>\n",
"<td align=right>",integer_to_list(AllSkip),
" (",UserSkipStr,"/",AutoSkipStr,")</td>\n",
- "<td align=right><b>",integer_to_list(NotBuilt),"<b></td>\n",
+ "<td align=right><b>",integer_to_list(NotBuilt),"</b></td>\n",
AllInfo, "</tr>\n",
xhtml("","</tfoot>\n")].
@@ -1377,7 +1516,7 @@ not_built(BaseName,_LogDir,_All,Missing) ->
%% Top.ObjDir | Top.ObjDir.suites | Top.ObjDir.Suite |
%% Top.ObjDir.Suite.cases | Top.ObjDir.Suite.Case
Failed =
- case string:tokens(BaseName,".") of
+ case string:lexemes(BaseName,".") of
[T,O] when is_list(T) -> % all under Top.ObjDir
locate_info({T,O},all,Missing);
[T,O,"suites"] ->
@@ -1415,7 +1554,7 @@ get_missing_suites(_,_) ->
[].
term_to_text(Term) ->
- lists:flatten(io_lib:format("~p.\n", [Term])).
+ lists:flatten(io_lib:format("~tp.\n", [Term])).
%%% Headers and footers.
@@ -1557,10 +1696,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"]),
@@ -1607,7 +1748,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"
@@ -1677,11 +1818,11 @@ count_cases(Dir) ->
%% file yet.
{0,0,0,0};
Summary ->
- write_summary(SumFile, Summary),
+ _ = write_summary(SumFile, Summary),
Summary
end;
{error, Reason} ->
- io:format("\nFailed to read ~p: ~p (skipped)\n",
+ io:format("\nFailed to read ~tp: ~tp (skipped)\n",
[LogFile,Reason]),
error
end
@@ -1763,10 +1904,10 @@ config_table_header() ->
config_table1([{Key,Value}|Vars]) ->
[xhtml(["<tr><td>", atom_to_list(Key), "</td>\n",
- "<td><pre>",io_lib:format("~p",[Value]),"</pre></td></tr>\n"],
+ "<td><pre>",io_lib:format("~tp",[Value]),"</pre></td></tr>\n"],
["<tr class=\"", odd_or_even(), "\">\n",
"<td>", atom_to_list(Key), "</td>\n",
- "<td>", io_lib:format("~p",[Value]), "</td>\n</tr>\n"]) |
+ "<td>", io_lib:format("~tp",[Value]), "</td>\n</tr>\n"]) |
config_table1(Vars)];
config_table1([]) ->
[xhtml("","</tbody>\n"),"</table>\n"].
@@ -1777,7 +1918,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
@@ -1799,7 +1940,11 @@ make_all_runs_index(When) ->
end,
Dirs = filelib:wildcard(logdir_prefix()++"*.*"),
- DirsSorted = (catch sort_all_runs(Dirs)),
+ DirsSorted0 = (catch sort_all_runs(Dirs)),
+ DirsSorted =
+ if When == start -> DirsSorted0;
+ true -> maybe_delete_old_dirs(DirsSorted0)
+ end,
LogCacheInfo = get_cache_data(UseCache),
@@ -1899,9 +2044,9 @@ sort_all_runs(Dirs) ->
%% "YYYY-MM-DD_HH.MM.SS"
lists:sort(fun(Dir1,Dir2) ->
[SS1,MM1,HH1,Date1|_] =
- lists:reverse(string:tokens(Dir1,[$.,$_])),
+ lists:reverse(string:lexemes(Dir1,[$.,$_])),
[SS2,MM2,HH2,Date2|_] =
- lists:reverse(string:tokens(Dir2,[$.,$_])),
+ lists:reverse(string:lexemes(Dir2,[$.,$_])),
{Date1,HH1,MM1,SS1} > {Date2,HH2,MM2,SS2}
end, Dirs).
@@ -1911,12 +2056,42 @@ sort_ct_runs(Dirs) ->
lists:sort(
fun(Dir1,Dir2) ->
[SS1,MM1,DateHH1 | _] =
- lists:reverse(string:tokens(filename:dirname(Dir1),[$.])),
+ lists:reverse(string:lexemes(filename:dirname(Dir1),[$.])),
[SS2,MM2,DateHH2 | _] =
- lists:reverse(string:tokens(filename:dirname(Dir2),[$.])),
+ lists:reverse(string:lexemes(filename:dirname(Dir2),[$.])),
{DateHH1,MM1,SS1} =< {DateHH2,MM2,SS2}
end, Dirs).
+parse_keep_logs([Str="all"]) ->
+ parse_keep_logs(list_to_atom(Str));
+parse_keep_logs([NStr]) ->
+ parse_keep_logs(list_to_integer(NStr));
+parse_keep_logs(all) ->
+ all;
+parse_keep_logs(N) when is_integer(N), N>0 ->
+ N.
+
+maybe_delete_old_dirs(Sorted) ->
+ {Keep,Delete} =
+ case application:get_env(common_test, keep_logs) of
+ {ok,MaxN} when is_integer(MaxN), length(Sorted)>MaxN ->
+ lists:split(MaxN,Sorted);
+ _ ->
+ {Sorted,[]}
+ end,
+ delete_old_dirs(Delete),
+ Keep.
+
+delete_old_dirs([]) ->
+ ok;
+delete_old_dirs(Dirs) ->
+ io:put_chars("\n Removing old test directories:\n"),
+ [begin
+ io:put_chars(" " ++ Dir ++ "\n"),
+ rm_dir(Dir)
+ end|| Dir <- Dirs],
+ ok.
+
dir_diff_all_runs(Dirs, LogCache) ->
case LogCache#log_cache.all_runs of
[] ->
@@ -1982,9 +2157,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),"\">",
@@ -1992,7 +2167,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")]).
@@ -2029,29 +2204,24 @@ runentry(Dir, Totals={Node,Label,Logs,
0 -> "-";
N -> integer_to_list(N)
end,
- StripExt =
- fun(File) ->
- string:sub_string(File,1,
- length(File)-
- length(?logdir_ext)) ++ ", "
- end,
- Polish = fun(S) -> case lists:reverse(S) of
- [32,$,|Rev] -> lists:reverse(Rev);
- [$,|Rev] -> lists:reverse(Rev);
- _ -> S
- end
- end,
- TestNames = Polish(lists:flatten(lists:map(StripExt,Logs))),
+
+ RootNames = lists:map(fun(F) -> filename:rootname(F,?logdir_ext) end, Logs),
+ TestNames = lists:flatten(lists:join(", ", RootNames)),
TestNamesTrunc =
- if TestNames=="" ->
- "";
- length(TestNames) < ?testname_width ->
+ if length(TestNames) < ?testname_width ->
TestNames;
true ->
- Trunc = Polish(string:substr(TestNames,1,
- ?testname_width-3)),
+ Trunc = string:trim(string:slice(TestNames,0,?testname_width-3),
+ trailing,",\s"),
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",
@@ -2071,7 +2241,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"]),
@@ -2116,9 +2286,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
@@ -2163,7 +2333,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) ->
@@ -2185,7 +2355,7 @@ force_rename(From,To,Number) ->
timestamp(Dir) ->
- TsR = lists:reverse(string:tokens(Dir,".-_")),
+ TsR = lists:reverse(string:lexemes(Dir,".-_")),
[S,Min,H,D,M,Y] = [list_to_integer(N) || N <- lists:sublist(TsR,6)],
format_time({{Y,M,D},{H,Min,S}}).
@@ -2285,17 +2455,17 @@ make_all_suites_index(NewTestData = {_TestName,DirName}) ->
LogDirData) of
{'EXIT',Reason} ->
io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"),
- io:format("~p~n", [Reason]),
+ io:format("~tp~n", [Reason]),
{error,Reason};
{error,Reason} ->
io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"),
- io:format("~p~n", [Reason]),
+ io:format("~tp~n", [Reason]),
{error,Reason};
ok ->
ok;
Err ->
io:format("Unknown internal error while updating ~ts. "
- "Please report.\n(Err: ~p, ID: 1)",
+ "Please report.\n(Err: ~tp, ID: 1)",
[AbsIndexName,Err]),
{error, Err}
end,
@@ -2509,16 +2679,16 @@ update_tests_in_cache(TempData,LogCache=#log_cache{tests=Tests}) ->
make_all_suites_index1(When, AbsIndexName, AllTestLogDirs) ->
IndexName = ?index_name,
if When == start -> ok;
- true -> io:put_chars("Updating " ++ AbsIndexName ++ "... ")
+ true -> io:put_chars("Updating " ++ AbsIndexName ++ " ... ")
end,
case catch make_all_suites_index2(IndexName, AllTestLogDirs) of
{'EXIT', Reason} ->
io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"),
- io:format("~p~n", [Reason]),
+ io:format("~tp~n", [Reason]),
{error, Reason};
{error, Reason} ->
io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"),
- io:format("~p~n", [Reason]),
+ io:format("~tp~n", [Reason]),
{error, Reason};
{ok,TempData} ->
case When of
@@ -2532,7 +2702,7 @@ make_all_suites_index1(When, AbsIndexName, AllTestLogDirs) ->
end;
Err ->
io:format("Unknown internal error while updating ~ts. "
- "Please report.\n(Err: ~p, ID: 1)",
+ "Please report.\n(Err: ~tp, ID: 1)",
[AbsIndexName,Err]),
{error, Err}
end.
@@ -2714,18 +2884,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;
@@ -2734,7 +2904,7 @@ cache_vsn() ->
VSNfile = filename:join([EbinDir,"..","vsn.mk"]),
case file:read_file(VSNfile) of
{ok,Bin} ->
- [_,VSN] = string:tokens(binary_to_list(Bin),[$=,$\n,$ ]),
+ [_,VSN] = string:lexemes(binary_to_list(Bin),[$=,$\n,$ ]),
VSN;
_ ->
undefined
@@ -2816,18 +2986,19 @@ rm_files([]) ->
ok.
%%%-----------------------------------------------------------------
-%%% @spec simulate() -> pid()
+%%% -spec simulate() -> pid()
%%%
-%%% @doc Simulate the logger process.
+%%% Simulate the logger process.
%%%
-%%% <p>Simulate the logger process - for use when testing code using
+%%% Simulate the logger process - for use when testing code using
%%% ct_logs logging mechanism without using the ct
-%%% environment. (E.g. when testing code with ts)</p>
+%%% environment. (E.g. when testing code with ts)
simulate() ->
cast(stop),
S = self(),
Pid = spawn(fun() ->
register(?MODULE,self()),
+ ct_util:mark_process(),
S ! {self(),started},
simulate_logger_loop()
end),
@@ -2836,8 +3007,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 ->
@@ -2845,9 +3020,7 @@ simulate_logger_loop() ->
end.
%%%-----------------------------------------------------------------
-%%% @spec notify_and_lock_file(Files) -> ok
-%%%
-%%% @doc
+%%% -spec notify_and_lock_file(Files) -> ok
%%%
notify_and_lock_file(File) ->
case ct_event:is_alive() of
@@ -2860,9 +3033,7 @@ notify_and_lock_file(File) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec notify_and_unlock_file(Files) -> ok
-%%%
-%%% @doc
+%%% -spec notify_and_unlock_file(Files) -> ok
%%%
notify_and_unlock_file(File) ->
case ct_event:is_alive() of
@@ -2875,9 +3046,7 @@ notify_and_unlock_file(File) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec get_run_dirs(Dir) -> [string()] | false
-%%%
-%%% @doc
+%%% -spec get_run_dirs(Dir) -> [string()] | false
%%%
get_run_dirs(Dir) ->
case filelib:wildcard(filename:join(Dir, "run.[1-2]*")) of
@@ -2888,9 +3057,7 @@ get_run_dirs(Dir) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec xhtml(HTML, XHTML) -> HTML | XHTML
-%%%
-%%% @doc
+%%% -spec xhtml(HTML, XHTML) -> HTML | XHTML
%%%
xhtml(HTML, XHTML) when is_function(HTML),
is_function(XHTML) ->
@@ -2905,9 +3072,7 @@ xhtml(HTML, XHTML) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec odd_or_even() -> "odd" | "even"
-%%%
-%%% @doc
+%%% -spec odd_or_even() -> "odd" | "even"
%%%
odd_or_even() ->
case get(odd_or_even) of
@@ -2920,9 +3085,7 @@ odd_or_even() ->
end.
%%%-----------------------------------------------------------------
-%%% @spec basic_html() -> true | false
-%%%
-%%% @doc
+%%% -spec basic_html() -> true | false
%%%
basic_html() ->
case application:get_env(common_test, basic_html) of
@@ -2933,9 +3096,7 @@ basic_html() ->
end.
%%%-----------------------------------------------------------------
-%%% @spec locate_priv_file(FileName) -> PrivFile
-%%%
-%%% @doc
+%%% -spec locate_priv_file(FileName) -> PrivFile
%%%
locate_priv_file(FileName) ->
{ok,CWD} = file:get_cwd(),
@@ -2951,8 +3112,8 @@ locate_priv_file(FileName) ->
filename:join(get(ct_run_dir), FileName);
_ ->
%% executed on other process than ct_logs
- {ok,RunDir} = get_log_dir(true),
- filename:join(RunDir, FileName)
+ {ok,LogDir} = get_log_dir(true),
+ filename:join(LogDir, FileName)
end,
case filelib:is_file(PrivResultFile) of
true ->
@@ -2965,13 +3126,13 @@ locate_priv_file(FileName) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec make_relative(AbsDir, Cwd) -> RelDir
+%%% -spec make_relative(AbsDir, Cwd) -> RelDir
%%%
-%%% @doc Return directory path to File (last element of AbsDir), which
-%%% is the path relative to Cwd. Examples when Cwd == "/ldisk/test/logs":
-%%% make_relative("/ldisk/test/logs/run/trace.log") -> "run/trace.log"
-%%% make_relative("/ldisk/test/trace.log") -> "../trace.log"
-%%% make_relative("/ldisk/test/logs/trace.log") -> "trace.log"
+%%% Return directory path to File (last element of AbsDir), which
+%%% is the path relative to Cwd. Examples when Cwd == "/ldisk/test/logs":
+%%% make_relative("/ldisk/test/logs/run/trace.log") -> "run/trace.log"
+%%% make_relative("/ldisk/test/trace.log") -> "../trace.log"
+%%% make_relative("/ldisk/test/logs/trace.log") -> "trace.log"
make_relative(AbsDir) ->
{ok,Cwd} = file:get_cwd(),
make_relative(AbsDir, Cwd).
@@ -2995,11 +3156,9 @@ make_relative1(DirTs, CwdTs) ->
Ups ++ DirTs.
%%%-----------------------------------------------------------------
-%%% @spec get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding)
+%%% -spec get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding)
%%% -> {Mode,Header,Footer}
%%%
-%%% @doc
-%%%
get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols, Encoding) ->
get_ts_html_wrapper(TestName, undefined, PrintLabel, Cwd, TableCols, Encoding).
@@ -3007,7 +3166,7 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) ->
TestName1 = if is_list(TestName) ->
lists:flatten(TestName);
true ->
- lists:flatten(io_lib:format("~p", [TestName]))
+ lists:flatten(io_lib:format("~tp", [TestName]))
end,
Basic = basic_html(),
LabelStr =
@@ -3034,6 +3193,10 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) ->
?all_runs_name), Cwd),
TestIndex = make_relative(filename:join(filename:dirname(CtLogdir),
?index_name), Cwd),
+ LatestTest = make_relative(filename:join(filename:dirname(CtLogdir),
+ ?suitelog_name++".latest.html"),
+ Cwd),
+
case Basic of
true ->
TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"),
@@ -3043,15 +3206,15 @@ get_ts_html_wrapper(TestName, Logdir, 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",
@@ -3060,7 +3223,9 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) ->
"<a href=\"", uri(AllRuns),
"\">Test run history\n</a> | ",
"<a href=\"", uri(TestIndex),
- "\">Top level test index\n</a>\n</p>\n",
+ "\">Top level test index\n</a> | ",
+ "<a href=\"", uri(LatestTest),
+ "\">Latest test result</a>\n</p>\n",
Copyright,"</center>\n</body>\n</html>\n"]};
_ ->
Copyright =
@@ -3068,7 +3233,7 @@ get_ts_html_wrapper(TestName, Logdir, 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,
@@ -3095,9 +3260,11 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) ->
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n",
"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n",
"<head>\n<title>", TestName1, "</title>\n",
- "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n",
- "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n",
- "<link rel=\"stylesheet\" href=\"", uri(CSSFile), "\" type=\"text/css\">\n",
+ "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n",
+ "<meta http-equiv=\"content-type\" content=\"text/html; ",
+ "charset=utf-8\"></meta>\n",
+ "<link rel=\"stylesheet\" href=\"", uri(CSSFile),
+ "\" type=\"text/css\"></link>\n",
"<script type=\"text/javascript\" src=\"", JQueryFile, "\"></script>\n",
"<script type=\"text/javascript\" src=\"", TableSorterFile, "\"></script>\n"] ++
TableSorterScript ++ ["</head>\n","<body>\n", LabelStr, "\n"],
@@ -3105,7 +3272,9 @@ get_ts_html_wrapper(TestName, Logdir, PrintLabel, Cwd, TableCols, Encoding) ->
"<a href=\"", uri(AllRuns),
"\">Test run history\n</a> | ",
"<a href=\"", uri(TestIndex),
- "\">Top level test index\n</a>\n</p>\n",
+ "\">Top level test index\n</a> | ",
+ "<a href=\"", uri(LatestTest),
+ "\">Latest test result</a>\n</p>\n",
Copyright,"</center>\n</body>\n</html>\n"]}
end.
@@ -3125,7 +3294,7 @@ insert_javascript({tablesorter,TableName,
end, [{"CTDateSorter",DateCols},
{"CTTextSorter",TextCols},
{"CTValSorter",ValCols}]))),
- Headers1 = string:substr(Headers, 1, length(Headers)-2),
+ Headers1 = string:trim(Headers, trailing, ",\n"),
["<script type=\"text/javascript\">\n",
"// Parser for date format, e.g: Wed Jul 4 2012 11:24:15\n",
@@ -3223,11 +3392,11 @@ html_encoding(latin1) ->
html_encoding(utf8) ->
"utf-8".
-unexpected_io(Pid,ct_internal,_Importance,List,CtLogFd) ->
- IoFun = create_io_fun(Pid,CtLogFd),
- io:format(CtLogFd, "~ts", [lists:foldl(IoFun, [], List)]);
-unexpected_io(Pid,_Category,_Importance,List,CtLogFd) ->
- IoFun = create_io_fun(Pid,CtLogFd),
- Data = io_lib:format("~ts", [lists:foldl(IoFun, [], List)]),
+unexpected_io(Pid, ct_internal, _Importance, Content, CtLogFd, EscChars) ->
+ IoFun = create_io_fun(Pid, CtLogFd, EscChars),
+ io:format(CtLogFd, "~ts", [lists:foldl(IoFun, [], Content)]);
+unexpected_io(Pid, _Category, _Importance, Content, CtLogFd, EscChars) ->
+ IoFun = create_io_fun(Pid, CtLogFd, EscChars),
+ Data = io_lib:format("~ts", [lists:foldl(IoFun, [], Content)]),
test_server_io:print_unexpected(Data),
ok.
diff --git a/lib/common_test/src/ct_make.erl b/lib/common_test/src/ct_make.erl
index d4bd81e78d..220cb0473d 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-2017. 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%
%%
@@ -95,7 +96,7 @@ read_emakefile(Emakefile,Opts) ->
Mods = [filename:rootname(F) || F <- filelib:wildcard("*.erl")],
[{Mods, Opts}];
{error,Other} ->
- io:format("make: Trouble reading 'Emakefile':~n~p~n",[Other]),
+ io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]),
error
end.
@@ -150,7 +151,7 @@ get_opts_from_emakefile(Mods,Emakefile,Opts) ->
{error,enoent} ->
[{Mods, Opts}];
{error,Other} ->
- io:format("make: Trouble reading 'Emakefile':~n~p~n",[Other]),
+ io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]),
error
end.
@@ -279,15 +280,47 @@ recompile(File, NoExec, Load, Opts) ->
do_recompile(_File, true, _Load, _Opts) ->
out_of_date;
-do_recompile(File, false, noload, Opts) ->
+do_recompile(File, false, Load, Opts) ->
io:format("Recompile: ~ts\n",[File]),
- compile:file(File, [report_errors, report_warnings, error_summary |Opts]);
-do_recompile(File, false, load, Opts) ->
- io:format("Recompile: ~ts\n",[File]),
- c:c(File, Opts);
-do_recompile(File, false, netload, Opts) ->
- io:format("Recompile: ~ts\n",[File]),
- c:nc(File, Opts).
+ case compile:file(File, [report_errors, report_warnings |Opts]) of
+ Ok when is_tuple(Ok), element(1,Ok)==ok ->
+ maybe_load(element(2,Ok), Load, Opts);
+ _Error ->
+ error
+ end.
+
+maybe_load(_Mod, noload, _Opts) ->
+ ok;
+maybe_load(Mod, Load, Opts) ->
+ %% We have compiled File with options Opts. Find out where the
+ %% output file went to, and load it.
+ case compile:output_generated(Opts) of
+ true ->
+ Dir = proplists:get_value(outdir,Opts,"."),
+ do_load(Dir, Mod, Load);
+ false ->
+ io:format("** Warning: No object file created - nothing loaded **~n"),
+ ok
+ end.
+
+do_load(Dir, Mod, load) ->
+ code:purge(Mod),
+ case code:load_abs(filename:join(Dir, Mod),Mod) of
+ {module,Mod} ->
+ {ok,Mod};
+ Other ->
+ Other
+ end;
+do_load(Dir, Mod, netload) ->
+ Obj = atom_to_list(Mod) ++ code:objfile_extension(),
+ Fname = filename:join(Dir, Obj),
+ case file:read_file(Fname) of
+ {ok,Bin} ->
+ rpc:eval_everywhere(code,load_binary,[Mod,Fname,Bin]),
+ {ok,Mod};
+ Other ->
+ Other
+ end.
exists(File) ->
case file:read_file_info(File) of
@@ -323,10 +356,11 @@ check_includes(File, IncludePath, ObjMTime) ->
end.
check_includes2(Epp, File, ObjMTime) ->
+ A1 = erl_anno:new(1),
case epp:parse_erl_form(Epp) of
- {ok, {attribute, 1, file, {File, 1}}} ->
+ {ok, {attribute, A1, file, {File, A1}}} ->
check_includes2(Epp, File, ObjMTime);
- {ok, {attribute, 1, file, {IncFile, 1}}} ->
+ {ok, {attribute, A1, file, {IncFile, A1}}} ->
case file:read_file_info(IncFile) of
{ok, #file_info{mtime=MTime}} when MTime>ObjMTime ->
epp:close(Epp),
@@ -340,5 +374,7 @@ check_includes2(Epp, File, ObjMTime) ->
epp:close(Epp),
false;
{error, _Error} ->
+ check_includes2(Epp, File, ObjMTime);
+ {warning, _Warning} ->
check_includes2(Epp, File, ObjMTime)
end.
diff --git a/lib/common_test/src/ct_master.erl b/lib/common_test/src/ct_master.erl
index 2cdb259899..fd33ee2280 100644
--- a/lib/common_test/src/ct_master.erl
+++ b/lib/common_test/src/ct_master.erl
@@ -1,32 +1,30 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2018. 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 Distributed test execution control for Common Test.
-%%% <p>This module exports functions for running Common Test nodes
-%%% on multiple hosts in parallel.</p>
-module(ct_master).
-export([run/1,run/3,run/4]).
-export([run_on_node/2,run_on_node/3]).
-export([run_test/1,run_test/2]).
-export([get_event_mgr_ref/0]).
--export([basic_html/1]).
+-export([basic_html/1,esc_chars/1]).
-export([abort/0,abort/1,progress/0]).
@@ -44,50 +42,14 @@
blocked=[]
}).
-%%%-----------------------------------------------------------------
-%%% @spec run_test(Node,Opts) -> ok
-%%% Node = atom()
-%%% Opts = [OptTuples]
-%%% OptTuples = {config,CfgFiles} | {dir,TestDirs} | {suite,Suites} |
-%%% {testcase,Cases} | {spec,TestSpecs} | {allow_user_terms,Bool} |
-%%% {logdir,LogDir} | {event_handler,EventHandlers} |
-%%% {silent_connections,Conns} | {cover,CoverSpecFile} |
-%%% {cover_stop,Bool} | {userconfig, UserCfgFiles}
-%%% CfgFiles = string() | [string()]
-%%% TestDirs = string() | [string()]
-%%% Suites = atom() | [atom()]
-%%% Cases = atom() | [atom()]
-%%% TestSpecs = string() | [string()]
-%%% LogDir = string()
-%%% EventHandlers = EH | [EH]
-%%% EH = atom() | {atom(),InitArgs} | {[atom()],InitArgs}
-%%% InitArgs = [term()]
-%%% Conns = all | [atom()]
-%%%
-%%% @doc Tests are spawned on <code>Node</code> using <code>ct:run_test/1</code>.
run_test(Node,Opts) ->
run_test([{Node,Opts}]).
-%%% @hidden
run_test({Node,Opts}) ->
run_test([{Node,Opts}]);
run_test(NodeOptsList) when is_list(NodeOptsList) ->
start_master(NodeOptsList).
-%%%-----------------------------------------------------------------
-%%% @spec run(TestSpecs,AllowUserTerms,InclNodes,ExclNodes) -> ok
-%%% TestSpecs = string() | [SeparateOrMerged]
-%%% SeparateOrMerged = string() | [string()]
-%%% AllowUserTerms = bool()
-%%% InclNodes = [atom()]
-%%% ExclNodes = [atom()]
-%%%
-%%% @doc Tests are spawned on the nodes as specified in <code>TestSpecs</code>.
-%%% Each specification in TestSpec will be handled separately. It is however
-%%% possible to also specify a list of specifications that should be merged
-%%% into one before the tests are executed. Any test without a particular node
-%%% specification will also be executed on the nodes in <code>InclNodes</code>.
-%%% Nodes in the <code>ExclNodes</code> list will be excluded from the test.
run([TS|TestSpecs],AllowUserTerms,InclNodes,ExclNodes) when is_list(TS),
is_list(InclNodes),
is_list(ExclNodes) ->
@@ -127,22 +89,9 @@ run(TS,AllowUserTerms,InclNodes,ExclNodes) when is_list(InclNodes),
is_list(ExclNodes) ->
run([TS],AllowUserTerms,InclNodes,ExclNodes).
-%%%-----------------------------------------------------------------
-%%% @spec run(TestSpecs,InclNodes,ExclNodes) -> ok
-%%% TestSpecs = string() | [SeparateOrMerged]
-%%% SeparateOrMerged = string() | [string()]
-%%% InclNodes = [atom()]
-%%% ExclNodes = [atom()]
-%%%
-%%% @equiv run(TestSpecs,false,InclNodes,ExclNodes)
run(TestSpecs,InclNodes,ExclNodes) ->
run(TestSpecs,false,InclNodes,ExclNodes).
-%%%-----------------------------------------------------------------
-%%% @spec run(TestSpecs) -> ok
-%%% TestSpecs = string() | [SeparateOrMerged]
-%%%
-%%% @equiv run(TestSpecs,false,[],[])
run(TestSpecs=[TS|_]) when is_list(TS) ->
run(TestSpecs,false,[],[]);
run(TS) ->
@@ -155,15 +104,6 @@ exclude_nodes([],RunSkipPerNode) ->
RunSkipPerNode.
-%%%-----------------------------------------------------------------
-%%% @spec run_on_node(TestSpecs,AllowUserTerms,Node) -> ok
-%%% TestSpecs = string() | [SeparateOrMerged]
-%%% SeparateOrMerged = string() | [string()]
-%%% AllowUserTerms = bool()
-%%% Node = atom()
-%%%
-%%% @doc Tests are spawned on <code>Node</code> according to
-%%% <code>TestSpecs</code>.
run_on_node([TS|TestSpecs],AllowUserTerms,Node) when is_list(TS),is_atom(Node) ->
case catch ct_testspec:collect_tests_from_file([TS],[Node],
AllowUserTerms) of
@@ -193,13 +133,6 @@ run_on_node([],_,_) ->
run_on_node(TS,AllowUserTerms,Node) when is_atom(Node) ->
run_on_node([TS],AllowUserTerms,Node).
-%%%-----------------------------------------------------------------
-%%% @spec run_on_node(TestSpecs,Node) -> ok
-%%% TestSpecs = string() | [SeparateOrMerged]
-%%% SeparateOrMerged = string() | [string()]
-%%% Node = atom()
-%%%
-%%% @equiv run_on_node(TestSpecs,false,Node)
run_on_node(TestSpecs,Node) ->
run_on_node(TestSpecs,false,Node).
@@ -263,58 +196,29 @@ run_all([],AllLogDirs,_,AllEvHs,_AllIncludes,
ok.
-%%%-----------------------------------------------------------------
-%%% @spec abort() -> ok
-%%%
-%%% @doc Stops all running tests.
abort() ->
call(abort).
-%%%-----------------------------------------------------------------
-%%% @spec abort(Nodes) -> ok
-%%% Nodes = atom() | [atom()]
-%%%
-%%% @doc Stops tests on specified nodes.
abort(Nodes) when is_list(Nodes) ->
call({abort,Nodes});
abort(Node) when is_atom(Node) ->
abort([Node]).
-%%%-----------------------------------------------------------------
-%%% @spec progress() -> [{Node,Status}]
-%%% Node = atom()
-%%% Status = finished_ok | ongoing | aborted | {error,Reason}
-%%% Reason = term()
-%%%
-%%% @doc Returns test progress. If <code>Status</code> is <code>ongoing</code>,
-%%% tests are running on the node and have not yet finished.
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
-%%%
-%%% @doc If set to true, the ct_master logs will be written on a
-%%% primitive html format, not using the Common Test CSS style
-%%% sheet.
basic_html(Bool) ->
application:set_env(common_test_master, basic_html, Bool),
ok.
+esc_chars(Bool) ->
+ application:set_env(common_test_master, esc_chars, Bool),
+ ok.
+
%%%-----------------------------------------------------------------
%%% MASTER, runs on central controlling node.
%%%-----------------------------------------------------------------
@@ -329,12 +233,12 @@ start_master(NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Specs) ->
{Master,Result} -> Result
end.
-%%% @hidden
init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,
InitOptions,Specs) ->
case whereis(ct_master) of
undefined ->
register(ct_master,self()),
+ ct_util:mark_process(),
ok;
_Pid ->
io:format("~nWarning: ct_master already running!~n"),
@@ -365,7 +269,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}) ->
@@ -387,6 +291,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),
@@ -415,7 +327,7 @@ init_master1(Parent,NodeOptsList,InitOptions,LogDirs) ->
init_master2(Parent,NodeOptsList,LogDirs) ->
process_flag(trap_exit,true),
Cookie = erlang:get_cookie(),
- log(all,"Cookie","~w",[Cookie]),
+ log(all,"Cookie","~tw",[Cookie]),
log(all,"Starting Tests",
"Tests starting on: ~p",[[N || {N,_} <- NodeOptsList]]),
SpawnAndMon =
@@ -435,7 +347,7 @@ master_loop(#state{node_ctrl_pids=[],
results=Finished}) ->
Str =
lists:map(fun({Node,Result}) ->
- io_lib:format("~-40.40.*ts~p\n",
+ io_lib:format("~-40.40.*ts~tp\n",
[$_,atom_to_list(Node),Result])
end,lists:reverse(Finished)),
log(all,"TEST RESULTS",Str,[]),
@@ -469,7 +381,7 @@ master_loop(State=#state{node_ctrl_pids=NodeCtrlPids,
Bad
end,
log(all,"Test Info",
- "Test on node ~w failed! Reason: ~p",
+ "Test on node ~w failed! Reason: ~tp",
[Node,Error]),
{Locks1,Blocked1} =
update_queue(exit,Node,Locks,Blocked),
@@ -482,7 +394,7 @@ master_loop(State=#state{node_ctrl_pids=NodeCtrlPids,
undefined ->
%% ignore (but report) exit from master_logger etc
log(all,"Test Info",
- "Warning! Process ~w has terminated. Reason: ~p",
+ "Warning! Process ~w has terminated. Reason: ~tp",
[Pid,Reason]),
master_loop(State)
end;
@@ -565,7 +477,7 @@ update_queue(take,Node,From,Lock={Op,Resource},Locks,Blocked) ->
%% Blocked: [{{Operation,Resource},Node,WaitingPid},...]
case lists:keysearch(Lock,1,Locks) of
{value,{_Lock,Owner}} -> % other node has lock
- log(html,"Lock Info","Node ~w blocked on ~w by ~w. Resource: ~p",
+ log(html,"Lock Info","Node ~w blocked on ~w by ~w. Resource: ~tp",
[Node,Op,Owner,Resource]),
Blocked1 = Blocked ++ [{Lock,Node,From}],
{Locks,Blocked1};
@@ -580,7 +492,7 @@ update_queue(release,Node,_From,Lock={Op,Resource},Locks,Blocked) ->
case lists:keysearch(Lock,1,Blocked) of
{value,E={Lock,SomeNode,WaitingPid}} ->
Blocked1 = lists:delete(E,Blocked),
- log(html,"Lock Info","Node ~w proceeds with ~w. Resource: ~p",
+ log(html,"Lock Info","Node ~w proceeds with ~w. Resource: ~tp",
[SomeNode,Op,Resource]),
reply(ok,WaitingPid), % waiting process may start
{Locks1,Blocked1};
@@ -647,7 +559,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])
@@ -659,7 +571,7 @@ refresh_logs([D|Dirs],Refreshed) ->
refresh_logs([],Refreshed) ->
Str =
lists:map(fun({D,Result}) ->
- io_lib:format("Refreshing logs in ~p... ~p",
+ io_lib:format("Refreshing logs in ~tp... ~tp",
[D,Result])
end,Refreshed),
log(all,"Info",Str,[]).
@@ -667,10 +579,10 @@ refresh_logs([],Refreshed) ->
%%%-----------------------------------------------------------------
%%% NODE CONTROLLER, runs and controls tests on a test node.
%%%-----------------------------------------------------------------
-%%% @hidden
init_node_ctrl(MasterPid,Cookie,Opts) ->
%% make sure tests proceed even if connection to master is lost
process_flag(trap_exit, true),
+ ct_util:mark_process(),
MasterNode = node(MasterPid),
group_leader(whereis(user),self()),
io:format("~n********** node_ctrl process ~w started on ~w **********~n",
@@ -690,10 +602,10 @@ init_node_ctrl(MasterPid,Cookie,Opts) ->
end,
%% start a local event manager
- ct_event:start_link(),
+ {ok, _} = start_ct_event(),
ct_event:add_handler([{master,MasterPid}]),
- %% log("Running test with options: ~p~n", [Opts]),
+ %% log("Running test with options: ~tp~n", [Opts]),
Result = case (catch ct:run_test(Opts)) of
ok -> finished_ok;
Other -> Other
@@ -710,10 +622,17 @@ 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
%%%-----------------------------------------------------------------
-%%% @hidden
status(MasterPid,Event=#event{name=start_make}) ->
call(MasterPid,Event);
status(MasterPid,Event=#event{name=finished_make}) ->
@@ -767,7 +686,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),
@@ -780,7 +699,7 @@ filter_accessible(InitOptions, Inaccessible)->
start_nodes(InitOptions)->
lists:foreach(fun({NodeName, Options})->
- [NodeS,HostS]=string:tokens(atom_to_list(NodeName), "@"),
+ [NodeS,HostS]=string:lexemes(atom_to_list(NodeName), "@"),
Node=list_to_atom(NodeS),
Host=list_to_atom(HostS),
HasNodeStart = lists:keymember(node_start, 1, Options),
@@ -801,7 +720,7 @@ start_nodes(InitOptions)->
"with callback ~w~n", [NodeName,Callback]);
{error, Reason, _NodeName} ->
io:format("Failed to start node ~w with callback ~w! "
- "Reason: ~p~n", [NodeName, Callback, Reason])
+ "Reason: ~tp~n", [NodeName, Callback, Reason])
end;
{true, true}->
io:format("WARNING: Node ~w is alive but has node_start "
@@ -830,10 +749,10 @@ eval_on_nodes(InitOptions)->
evaluate(Node, [{M,F,A}|MFAs])->
case rpc:call(Node, M, F, A) of
{badrpc,Reason}->
- io:format("WARNING: Failed to call ~w:~w/~w on node ~w "
- "due to ~p~n", [M,F,length(A),Node,Reason]);
+ io:format("WARNING: Failed to call ~w:~tw/~w on node ~w "
+ "due to ~tp~n", [M,F,length(A),Node,Reason]);
Result->
- io:format("Called ~w:~w/~w on node ~w, result: ~p~n",
+ io:format("Called ~w:~tw/~w on node ~w, result: ~tp~n",
[M,F,length(A),Node,Result])
end,
evaluate(Node, MFAs);
diff --git a/lib/common_test/src/ct_master_event.erl b/lib/common_test/src/ct_master_event.erl
index fd97ab16f7..a6ec5a7a75 100644
--- a/lib/common_test/src/ct_master_event.erl
+++ b/lib/common_test/src/ct_master_event.erl
@@ -1,29 +1,30 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2018. 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%
%%
-%%% @doc Common Test Framework Event Handler
+%%% Common Test Framework Event Handler
%%%
-%%% <p>This module implements an event handler that the CT Master
+%%% This module implements an event handler that the CT Master
%%% uses to handle status and progress notifications sent to the
%%% master node during test runs. This module may be used as a
%%% template for other event handlers that can be plugged in to
-%%% handle logging and reporting on the master node.</p>
+%%% handle logging and reporting on the master node.
-module(ct_master_event).
-behaviour(gen_event).
@@ -70,7 +71,7 @@ stop() ->
{error,Reason} ->
ct_master_logs:log("Error",
"No response from CT Master Event.\n"
- "Reason = ~p\n"
+ "Reason = ~tp\n"
"Terminating now!\n",[Reason]),
%% communication with event manager fails, kill it
catch exit(whereis(?CT_MEVMGR_REF), kill);
@@ -115,6 +116,7 @@ sync_notify(Event) ->
%% this function is called to initialize the event handler.
%%--------------------------------------------------------------------
init(_) ->
+ ct_util:mark_process(),
ct_master_logs:log("CT Master Event Handler started","",[]),
{ok,#state{}}.
@@ -134,7 +136,7 @@ handle_event(#event{name=start_logging,node=Node,data=RunDir},State) ->
handle_event(#event{name=Name,node=Node,data=Data},State) ->
print("~n=== ~w ===~n", [?MODULE]),
- print("~w on ~w: ~p~n", [Name,Node,Data]),
+ print("~tw on ~w: ~tp~n", [Name,Node,Data]),
{ok,State}.
%%--------------------------------------------------------------------
@@ -168,7 +170,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..ea6f93b565 100644
--- a/lib/common_test/src/ct_master_logs.erl
+++ b/lib/common_test/src/ct_master_logs.erl
@@ -1,26 +1,27 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2018. 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 Logging functionality for Common Test Master.
+%%% Logging functionality for Common Test Master.
%%%
-%%% <p>This module implements a logger for the master
-%%% node.</p>
+%%% This module implements a logger for the master
+%%% node.
-module(ct_master_logs).
-export([start/2, make_all_runs_index/0, log/3, nodedir/2,
@@ -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.
@@ -85,11 +88,12 @@ stop() ->
init(Parent,LogDir,Nodes) ->
register(?MODULE,self()),
+ ct_util:mark_process(),
Time = calendar:local_time(),
RunDir = make_dirname(Time),
RunDirAbs = filename:join(LogDir,RunDir),
- file:make_dir(RunDirAbs),
- write_details_file(RunDirAbs,{node(),Nodes}),
+ ok = make_dir(RunDirAbs),
+ _ = write_details_file(RunDirAbs,{node(),Nodes}),
case basic_html() of
true ->
@@ -107,16 +111,16 @@ init(Parent,LogDir,Nodes) ->
case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of
{error,Src1,Dest1,Reason1} ->
io:format(user, "ERROR! "++
- "Priv file ~p could not be copied to ~p. "++
- "Reason: ~p~n",
+ "Priv file ~tp could not be copied to ~tp. "++
+ "Reason: ~tp~n",
[Src1,Dest1,Reason1]),
exit({priv_file_error,Dest1});
ok ->
case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of
{error,Src2,Dest2,Reason2} ->
io:format(user, "ERROR! "++
- "Priv file ~p could not be copied to ~p. "++
- "Reason: ~p~n",
+ "Priv file ~tp could not be copied to ~tp. "++
+ "Reason: ~tp~n",
[Src2,Dest2,Reason2]),
exit({priv_file_error,Dest2});
ok ->
@@ -125,14 +129,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"]),
@@ -167,7 +171,7 @@ loop(State) ->
case catch io:format(Fd,Str++"\n",Args) of
{'EXIT',Reason} ->
io:format(Fd,
- "Logging fails! Str: ~p, Args: ~p~n",
+ "Logging fails! Str: ~tp, Args: ~tp~n",
[Str,Args]),
exit({logging_failed,Reason}),
ok;
@@ -178,7 +182,7 @@ loop(State) ->
lists:foreach(Fun,List),
loop(State);
{make_all_runs_index,From} ->
- make_all_runs_index(State#state.logdir),
+ _ = make_all_runs_index(State#state.logdir),
return(From,State#state.logdir),
loop(State);
{{nodedir,Node,RunDir},From} ->
@@ -186,12 +190,12 @@ loop(State) ->
return(From,ok),
loop(State);
stop ->
- make_all_runs_index(State#state.logdir),
+ _ = make_all_runs_index(State#state.logdir),
io:format(State#state.log_fd,
int_header()++int_footer(),
- [log_timestamp(now()),"Finished!"]),
- close_ct_master_log(State#state.log_fd),
- close_nodedir_index(State#state.nodedir_ix_fd),
+ [log_timestamp(?now),"Finished!"]),
+ _ = close_ct_master_log(State#state.log_fd),
+ _ = close_nodedir_index(State#state.nodedir_ix_fd),
ok
end.
@@ -235,9 +239,9 @@ config_table1([]) ->
["</tbody>\n</table>\n"].
int_header() ->
- "<div class=\"ct_internal\"><b>*** CT MASTER ~s *** ~ts</b>".
+ "</pre>\n<div class=\"ct_internal\"><pre><b>*** CT MASTER ~s *** ~ts</b>".
int_footer() ->
- "</div>".
+ "</pre></div>\n<pre>".
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% NodeDir Index functions %%%
@@ -294,7 +298,7 @@ sort_all_runs(Dirs) ->
%% "YYYY-MM-DD_HH.MM.SS"
KeyList =
lists:map(fun(Dir) ->
- case lists:reverse(string:tokens(Dir,[$.,$_])) of
+ case lists:reverse(string:lexemes(Dir,[$.,$_])) of
[SS,MM,HH,Date|_] ->
{{Date,HH,MM,SS},Dir};
_Other ->
@@ -309,18 +313,8 @@ runentry(Dir) ->
{MasterStr,NodesStr} =
case read_details_file(Dir) of
{Master,Nodes} when is_list(Nodes) ->
- [_,Host] = string:tokens(atom_to_list(Master),"@"),
- NodesList =
- lists:reverse(lists:map(fun(N) ->
- atom_to_list(N) ++ ", "
- end,Nodes)),
- case NodesList of
- [N|NListR] ->
- N1 = string:sub_string(N,1,length(N)-2),
- {Host,lists:flatten(lists:reverse([N1|NListR]))};
- [] ->
- {Host,""}
- end;
+ [_,Host] = string:lexemes(atom_to_list(Master),"@"),
+ {Host,lists:concat(lists:join(", ",Nodes))};
_Error ->
{"unknown",""}
end,
@@ -345,7 +339,7 @@ all_runs_header() ->
xhtml("", "</tr></thead>\n<tbody>\n")]].
timestamp(Dir) ->
- [S,Min,H,D,M,Y|_] = lists:reverse(string:tokens(Dir,".-_")),
+ [S,Min,H,D,M,Y|_] = lists:reverse(string:lexemes(Dir,".-_")),
[S1,Min1,H1,D1,M1,Y1] = [list_to_integer(N) || N <- [S,Min,H,D,M,Y]],
format_time({{Y1,M1,D1},{H1,Min1,S1}}).
@@ -384,11 +378,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 +411,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 +487,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 +525,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..cb01220121 100644
--- a/lib/common_test/src/ct_master_status.erl
+++ b/lib/common_test/src/ct_master_status.erl
@@ -1,28 +1,29 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2018. 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%
%%
-%%% @doc Logging functionality for Common Test Master.
+%%% doc Logging functionality for Common Test Master.
%%%
-%%% <p>This module keeps a list of <code>{Node,Status}</code>
+%%% This module keeps a list of {Node,Status}
%%% tuples. It is possible to anytime during a test run get
%%% a snapshot of the test status. The module is an event
-%%% handler for the master event manager.</p>
+%%% handler for the master event manager.
-module(ct_master_status).
-behaviour(gen_event).
@@ -70,7 +71,7 @@ init(_) ->
%%
handle_event(#event{name=Name,node=Node,data=Data},State) ->
print("~n=== ~w ===~n", [?MODULE]),
- print("~w on ~w: ~p~n", [Name,Node,Data]),
+ print("~tw on ~w: ~tp~n", [Name,Node,Data]),
{ok,State}.
%%--------------------------------------------------------------------
@@ -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 80ffb51ab9..29188a648e 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-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2012-2017. 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%
%%
@@ -22,24 +23,15 @@
%% Description:
%% This file contains the Netconf client interface
%%
-%% @author Support
-%%
-%% @doc Netconf client module.
-%%
-%% <p>The Netconf client is compliant with RFC4741 and RFC4742.</p>
+%% Netconf servers can be configured by adding the following statement
+%% to a configuration file:
%%
-%% <p> For each server to test against, the following entry can be
-%% added to a configuration file:</p>
+%% {server_id(),options()}.
%%
-%% <p>`{server_id(),options()}.'</p>
+%% The server_id() or an associated ct:target_name() shall then be
+%% used in calls to open/2 connect/2.
%%
-%% <p> The `server_id()' or an associated `target_name()' (see
-%% {@link ct}) shall then be used in calls to {@link open/2}.</p>
-%%
-%% <p>If no configuration exists for a server, a session can still be
-%% opened by calling {@link open/2} with all necessary options given
-%% in the call. The first argument to {@link open/2} can then be any
-%% atom.</p>
+%% If no configuration exists for a server, use open/1 and connect/1.
%%
%% == Logging ==
%%
@@ -48,102 +40,15 @@
%% `ct_conn_log_h'. To use this error handler, add the `cth_conn_log'
%% hook in your test suite, e.g.
%%
-%% ```
%% suite() ->
-%% [{ct_hooks, [{cth_conn_log, [{conn_mod(),hook_options()}]}]}].
-%%'''
-%%
-%% The `conn_mod()' is the name of the common_test module implementing
-%% the connection protocol, e.g. `ct_netconfc'.
-%%
-%% The hook option `log_type' specifies the type of logging:
-%%
-%% <dl>
-%% <dt>`raw'</dt>
-%% <dd>The sent and received netconf data is logged to a separate
-%% text file as is without any formatting. A link to the file is
-%% added to the test case HTML log.</dd>
-%%
-%% <dt>`pretty'</dt>
-%% <dd>The sent and received netconf data is logged to a separate
-%% text file with XML data nicely indented. A link to the file is
-%% added to the test case HTML log.</dd>
-%%
-%% <dt>`html (default)'</dt>
-%% <dd>The sent and received netconf traffic is pretty printed
-%% directly in the test case HTML log.</dd>
-%%
-%% <dt>`silent'</dt>
-%% <dd>Netconf traffic is not logged.</dd>
-%% </dl>
-%%
-%% By default, all netconf traffic is logged in one single log
-%% file. However, it is possible to have different connections logged
-%% in separate files. To do this, use the hook option `hosts' and
-%% list the names of the servers/connections that will be used in the
-%% suite. Note that the connections must be named for this to work,
-%% i.e. they must be opened with {@link open/2}.
-%%
-%% The `hosts' option has no effect if `log_type' is set to `html' or
-%% `silent'.
-%%
-%% The hook options can also be specified in a configuration file with
-%% the configuration variable `ct_conn_log':
-%%
-%% ```
-%% {ct_conn_log,[{conn_mod(),hook_options()}]}.
-%% '''
+%% [{ct_hooks, [{cth_conn_log, [{ct:conn_log_mod(),ct:conn_log_options()}]}]}].
%%
%% For example:
%%
-%% ```
-%% {ct_conn_log,[{ct_netconfc,[{log_type,pretty},
-%% {hosts,[key_or_name()]}]}]}
-%% '''
-%%
-%% <b>Note</b> that hook options specified in a configuration file
-%% will overwrite the hardcoded hook options in the test suite.
-%%
-%% === Logging example 1 ===
-%%
-%% The following `ct_hooks' statement will cause pretty printing of
-%% netconf traffic to separate logs for the connections named
-%% `nc_server1' and `nc_server2'. Any other connections will be logged
-%% to default netconf log.
-%%
-%% ```
%% suite() ->
-%% [{ct_hooks, [{cth_conn_log, [{ct_netconfc,[{log_type,pretty}},
-%% {hosts,[nc_server1,nc_server2]}]}
-%% ]}]}].
-%%'''
-%%
-%% Connections must be opened like this:
-%%
-%% ```
-%% open(nc_server1,[...]),
-%% open(nc_server2,[...]).
-%% '''
-%%
-%% === Logging example 2 ===
-%%
-%% The following configuration file will cause raw logging of all
-%% netconf traffic into one single text file.
-%%
-%% ```
-%% {ct_conn_log,[{ct_netconfc,[{log_type,raw}]}]}.
-%% '''
-%%
-%% The `ct_hooks' statement must look like this:
-%%
-%% ```
-%% suite() ->
-%% [{ct_hooks, [{cth_conn_log, []}]}].
-%% '''
-%%
-%% The same `ct_hooks' statement without the configuration file would
-%% cause HTML logging of all netconf connections into the test case
-%% HTML log.
+%% [{ct_hooks,
+%% [{cth_conn_log,[{ct_netconfc,[{log_type,pretty},
+%% {hosts,[my_configured_server]}]}]}
%%
%% == Notifications ==
%%
@@ -151,11 +56,9 @@
%% Notifications, which defines a mechanism for an asynchronous
%% message notification delivery service for the netconf protocol.
%%
-%% Specific functions to support this are {@link
-%% create_subscription/6} and {@link get_event_streams/3}. (The
-%% functions also exist with other arities.)
+%% Specific functions to support this are create_subscription/6
+%% get_event_streams/3. (The functions also exist with other arities.)
%%
-%% @end
%%----------------------------------------------------------------------
-module(ct_netconfc).
@@ -166,7 +69,13 @@
%%----------------------------------------------------------------------
%% External exports
%%----------------------------------------------------------------------
--export([open/1,
+-export([connect/1,
+ connect/2,
+ disconnect/1,
+ session/1,
+ session/2,
+ session/3,
+ open/1,
open/2,
only_open/1,
only_open/2,
@@ -191,6 +100,7 @@
get_config/4,
edit_config/3,
edit_config/4,
+ edit_config/5,
delete_config/2,
delete_config/3,
copy_config/3,
@@ -203,6 +113,7 @@
create_subscription/4,
create_subscription/5,
create_subscription/6,
+ get_event_streams/1,
get_event_streams/2,
get_event_streams/3,
get_capabilities/1,
@@ -213,7 +124,9 @@
%%----------------------------------------------------------------------
%% Exported types
%%----------------------------------------------------------------------
--export_type([notification/0]).
+-export_type([client/0,
+ handle/0,
+ notification/0]).
%%----------------------------------------------------------------------
%% Internal exports
@@ -232,7 +145,6 @@
%% Internal defines
%%----------------------------------------------------------------------
-define(APPLICATION,?MODULE).
--define(VALID_SSH_OPTS,[user, password, user_dir]).
-define(DEFAULT_STREAM,"NETCONF").
-define(error(ConnName,Report),
@@ -262,6 +174,7 @@
session_id,
msg_id = 1,
hello_status,
+ no_end_tag_buff = <<>>,
buff = <<>>,
pending = [], % [#pending]
event_receiver}).% pid
@@ -271,13 +184,15 @@
host,
port = ?DEFAULT_PORT,
timeout = ?DEFAULT_TIMEOUT,
- name}).
+ name,
+ type}).
%% Connection reference
-record(connection, {reference, % {CM,Ch}
host,
port,
- name}).
+ name,
+ type}).
%% Pending replies from server
-record(pending, {tref, % timer ref (returned from timer:xxx)
@@ -289,17 +204,17 @@
%%----------------------------------------------------------------------
%% Type declarations
%%----------------------------------------------------------------------
--type client() :: handle() | ct_gen_conn:server_id() | ct_gen_conn:target_name().
--type handle() :: term().
-%% An opaque reference for a connection (netconf session). See {@link
-%% ct} for more information.
+-type client() :: handle() | server_id() | ct:target_name().
+-opaque handle() :: pid().
-type options() :: [option()].
-%% Options used for setting up ssh connection to a netconf server.
-
-type option() :: {ssh,host()} | {port,inet:port_number()} | {user,string()} |
{password,string()} | {user_dir,string()} |
{timeout,timeout()}.
+
+-type session_options() :: [session_option()].
+-type session_option() :: {timeout,timeout()}.
+
-type host() :: inet:hostname() | inet:ip_address().
-type notification() :: {notification, xml_attributes(), notification_content()}.
@@ -315,14 +230,13 @@
%% See XML Schema for Event Notifications found in RFC5277 for further
%% detail about the data format for the string values.
-%-type error_handler() :: module().
-type error_reason() :: term().
+-type server_id() :: atom().
+
-type simple_xml() :: {xml_tag(), xml_attributes(), xml_content()} |
{xml_tag(), xml_content()} |
xml_tag().
-%% <p>This type is further described in the documentation for the
-%% <tt>Xmerl</tt> application.</p>
-type xml_tag() :: atom().
-type xml_attributes() :: [{xml_attribute_tag(),xml_attribute_value()}].
-type xml_attribute_tag() :: atom().
@@ -334,69 +248,130 @@
-type xs_datetime() :: string().
%% This date and time identifyer has the same format as the XML type
%% dateTime and compliant to RFC3339. The format is
-%% ```[-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]'''
+%% "[-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]"
%%----------------------------------------------------------------------
%% External interface functions
%%----------------------------------------------------------------------
%%----------------------------------------------------------------------
--spec open(Options) -> Result when
+%% Open an SSH connection to a Netconf server
+%% If the server options are specified in a configuration file, use
+%% open/2.
+-spec connect(Options) -> Result when
Options :: options(),
Result :: {ok,handle()} | {error,error_reason()}.
-%% @doc Open a netconf session and exchange `hello' messages.
-%%
-%% If the server options are specified in a configuration file, or if
-%% a named client is needed for logging purposes (see {@section
-%% Logging}) use {@link open/2} instead.
-%%
-%% The opaque `handler()' reference which is returned from this
-%% function is required as client identifier when calling any other
-%% function in this module.
-%%
-%% The `timeout' option (milli seconds) is used when setting up
-%% the ssh connection and when waiting for the hello message from the
-%% server. It is not used for any other purposes during the lifetime
-%% of the connection.
-%%
-%% @end
+connect(Options) ->
+ do_connect(Options, #options{type=connection},[]).
+
+-spec connect(KeyOrName,ExtraOptions) -> Result when
+ KeyOrName :: ct:key_or_name(),
+ ExtraOptions :: options(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+connect(KeyOrName, ExtraOptions) ->
+ SortedExtra = lists:keysort(1,ExtraOptions),
+ SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])),
+ AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra),
+ do_connect(AllOpts,#options{name=KeyOrName,type=connection},[{name,KeyOrName}]).
+
+do_connect(OptList,InitOptRec,NameOpt) ->
+ case check_options(OptList,InitOptRec) of
+ {Host,Port,Options} ->
+ ct_gen_conn:start({Host,Port},Options,?MODULE,
+ NameOpt ++ [{reconnect,false},
+ {use_existing_connection,false},
+ {forward_messages,false}]);
+ Error ->
+ Error
+ end.
+
%%----------------------------------------------------------------------
-open(Options) ->
- open(Options,#options{},[],true).
+%% Close the given SSH connection.
+-spec disconnect(Conn) -> ok | {error,error_reason()} when
+ Conn :: handle().
+disconnect(Conn) ->
+ case call(Conn,get_ssh_connection) of
+ {ok,_} ->
+ ct_gen_conn:stop(Conn);
+ Error ->
+ Error
+ end.
+
+%%----------------------------------------------------------------------
+%% Open a netconf session as a channel on the given SSH connection,
+%% and exchange `hello' messages.
+-spec session(Conn) -> Result when
+ Conn :: handle(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+session(Conn) ->
+ do_session(Conn,[],#options{type=channel},[]).
+
+-spec session(Conn,Options) -> Result when
+ Conn :: handle(),
+ Options :: session_options(),
+ Result :: {ok,handle()} | {error,error_reason()};
+ (KeyOrName,Conn) -> Result when
+ KeyOrName :: ct:key_or_name(),
+ Conn :: handle(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+session(Conn,Options) when is_list(Options) ->
+ do_session(Conn,Options,#options{type=channel},[]);
+session(KeyOrName,Conn) ->
+ do_session(Conn,[],#options{name=KeyOrName,type=channel},[{name,KeyOrName}]).
+
+-spec session(KeyOrName,Conn,Options) -> Result when
+ Conn :: handle(),
+ Options :: session_options(),
+ KeyOrName :: ct:key_or_name(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+session(KeyOrName,Conn,ExtraOptions) ->
+ SortedExtra = lists:keysort(1,ExtraOptions),
+ SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])),
+ AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra),
+ do_session(Conn,AllOpts,#options{name=KeyOrName,type=channel},
+ [{name,KeyOrName}]).
+
+do_session(Conn,OptList,InitOptRec,NameOpt) ->
+ case call(Conn,get_ssh_connection) of
+ {ok,SshConn} ->
+ case check_session_options(OptList,InitOptRec) of
+ {ok,Options} ->
+ case ct_gen_conn:start(SshConn,Options,?MODULE,
+ NameOpt ++
+ [{reconnect,false},
+ {use_existing_connection,false},
+ {forward_messages,true}]) of
+ {ok,Client} ->
+ case hello(Client,Options#options.timeout) of
+ ok ->
+ {ok,Client};
+ Error ->
+ Error
+ end;
+ Error ->
+ Error
+ end;
+ Error ->
+ Error
+ end;
+ Error ->
+ Error
+ end.
%%----------------------------------------------------------------------
+%% Open a netconf session and exchange 'hello' messages.
+%% If the server options are specified in a configuration file, use
+%% open/2.
+-spec open(Options) -> Result when
+ Options :: options(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+open(Options) ->
+ open(Options,#options{type=connection_and_channel},[],true).
+
-spec open(KeyOrName, ExtraOptions) -> Result when
- KeyOrName :: ct_gen_conn:key_or_name(),
+ KeyOrName :: ct:key_or_name(),
ExtraOptions :: options(),
Result :: {ok,handle()} | {error,error_reason()}.
-%% @doc Open a named netconf session and exchange `hello' messages.
-%%
-%% If `KeyOrName' is a configured `server_id()' or a
-%% `target_name()' associated with such an ID, then the options
-%% for this server will be fetched from the configuration file.
-%
-%% The `ExtraOptions' argument will be added to the options found in
-%% the configuration file. If the same options are given, the values
-%% from the configuration file will overwrite `ExtraOptions'.
-%%
-%% If the server is not specified in a configuration file, use {@link
-%% open/1} instead.
-%%
-%% The opaque `handle()' reference which is returned from this
-%% function can be used as client identifier when calling any other
-%% function in this module. However, if `KeyOrName' is a
-%% `target_name()', i.e. if the server is named via a call to
-%% `ct:require/2' or a `require' statement in the test
-%% suite, then this name may be used instead of the `handle()'.
-%%
-%% The `timeout' option (milli seconds) is used when setting up
-%% the ssh connection and when waiting for the hello message from the
-%% server. It is not used for any other purposes during the lifetime
-%% of the connection.
-%%
-%% @see ct:require/2
-%% @end
-%%----------------------------------------------------------------------
open(KeyOrName, ExtraOpts) ->
open(KeyOrName, ExtraOpts, true).
@@ -404,10 +379,11 @@ open(KeyOrName, ExtraOpts, Hello) ->
SortedExtra = lists:keysort(1,ExtraOpts),
SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])),
AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra),
- open(AllOpts,#options{name=KeyOrName},[{name,KeyOrName}],Hello).
+ open(AllOpts,#options{name=KeyOrName,type=connection_and_channel},
+ [{name,KeyOrName}],Hello).
open(OptList,InitOptRec,NameOpt,Hello) ->
- case check_options(OptList,undefined,undefined,InitOptRec) of
+ case check_options(OptList,InitOptRec) of
{Host,Port,Options} ->
case ct_gen_conn:start({Host,Port},Options,?MODULE,
NameOpt ++ [{reconnect,false},
@@ -429,362 +405,286 @@ open(OptList,InitOptRec,NameOpt,Hello) ->
%%----------------------------------------------------------------------
+%% As open/1,2, except no 'hello' message is sent.
-spec only_open(Options) -> Result when
Options :: options(),
Result :: {ok,handle()} | {error,error_reason()}.
-%% @doc Open a netconf session, but don't send `hello'.
-%%
-%% As {@link open/1} but does not send a `hello' message.
-%%
-%% @end
-%%----------------------------------------------------------------------
only_open(Options) ->
- open(Options,#options{},[],false).
+ open(Options,#options{type=connection_and_channel},[],false).
-%%----------------------------------------------------------------------
-spec only_open(KeyOrName,ExtraOptions) -> Result when
- KeyOrName :: ct_gen_conn:key_or_name(),
+ KeyOrName :: ct:key_or_name(),
ExtraOptions :: options(),
Result :: {ok,handle()} | {error,error_reason()}.
-%% @doc Open a name netconf session, but don't send `hello'.
-%%
-%% As {@link open/2} but does not send a `hello' message.
-%%
-%% @end
-%%----------------------------------------------------------------------
only_open(KeyOrName, ExtraOpts) ->
open(KeyOrName, ExtraOpts, false).
%%----------------------------------------------------------------------
-%% @spec hello(Client) -> Result
-%% @equiv hello(Client, [], infinity)
+%% Send a 'hello' message.
+-spec hello(Client) -> Result when
+ Client :: handle(),
+ Result :: ok | {error,error_reason()}.
hello(Client) ->
hello(Client,[],?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec hello(Client,Timeout) -> Result when
Client :: handle(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @spec hello(Client, Timeout) -> Result
-%% @equiv hello(Client, [], Timeout)
hello(Client,Timeout) ->
hello(Client,[],Timeout).
-%%----------------------------------------------------------------------
-spec hello(Client,Options,Timeout) -> Result when
Client :: handle(),
Options :: [{capability, [string()]}],
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Exchange `hello' messages with the server.
-%%
-%% Adds optional capabilities and sends a `hello' message to the
-%% server and waits for the return.
-%% @end
-%%----------------------------------------------------------------------
hello(Client,Options,Timeout) ->
call(Client, {hello, Options, Timeout}).
%%----------------------------------------------------------------------
-%% @spec get_session_id(Client) -> Result
-%% @equiv get_session_id(Client, infinity)
+%% Get the session id for the session specified by Client.
+-spec get_session_id(Client) -> Result when
+ Client :: client(),
+ Result :: pos_integer() | {error,error_reason()}.
get_session_id(Client) ->
get_session_id(Client, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec get_session_id(Client, Timeout) -> Result when
Client :: client(),
Timeout :: timeout(),
Result :: pos_integer() | {error,error_reason()}.
-%% @doc Returns the session id associated with the given client.
-%%
-%% @end
-%%----------------------------------------------------------------------
get_session_id(Client, Timeout) ->
call(Client, get_session_id, Timeout).
%%----------------------------------------------------------------------
-%% @spec get_capabilities(Client) -> Result
-%% @equiv get_capabilities(Client, infinity)
+%% Get the server side capabilities.
+-spec get_capabilities(Client) -> Result when
+ Client :: client(),
+ Result :: [string()] | {error,error_reason()}.
get_capabilities(Client) ->
get_capabilities(Client, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec get_capabilities(Client, Timeout) -> Result when
Client :: client(),
Timeout :: timeout(),
Result :: [string()] | {error,error_reason()}.
-%% @doc Returns the server side capabilities
-%%
-%% The following capability identifiers, defined in RFC 4741, can be returned:
-%%
-%% <ul>
-%% <li>`"urn:ietf:params:netconf:base:1.0"'</li>
-%% <li>`"urn:ietf:params:netconf:capability:writable-running:1.0"'</li>
-%% <li>`"urn:ietf:params:netconf:capability:candidate:1.0"'</li>
-%% <li>`"urn:ietf:params:netconf:capability:confirmed-commit:1.0"'</li>
-%% <li>`"urn:ietf:params:netconf:capability:rollback-on-error:1.0"'</li>
-%% <li>`"urn:ietf:params:netconf:capability:startup:1.0"'</li>
-%% <li>`"urn:ietf:params:netconf:capability:url:1.0"'</li>
-%% <li>`"urn:ietf:params:netconf:capability:xpath:1.0"'</li>
-%% </ul>
-%%
-%% Note, additional identifiers may exist, e.g. server side namespace.
-%%
-%% @end
-%%----------------------------------------------------------------------
get_capabilities(Client, Timeout) ->
call(Client, get_capabilities, Timeout).
%%----------------------------------------------------------------------
-%% @spec send(Client, SimpleXml) -> Result
-%% @equiv send(Client, SimpleXml, infinity)
+%% Send an XML document to the server.
+-spec send(Client, SimpleXml) -> Result when
+ Client :: client(),
+ SimpleXml :: simple_xml(),
+ Result :: simple_xml() | {error,error_reason()}.
send(Client, SimpleXml) ->
send(Client, SimpleXml, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec send(Client, SimpleXml, Timeout) -> Result when
Client :: client(),
SimpleXml :: simple_xml(),
Timeout :: timeout(),
Result :: simple_xml() | {error,error_reason()}.
-%% @doc Send an XML document to the server.
-%%
-%% The given XML document is sent as is to the server. This function
-%% can be used for sending XML documents that can not be expressed by
-%% other interface functions in this module.
send(Client, SimpleXml, Timeout) ->
call(Client,{send, Timeout, SimpleXml}).
%%----------------------------------------------------------------------
-%% @spec send_rpc(Client, SimpleXml) -> Result
-%% @equiv send_rpc(Client, SimpleXml, infinity)
+%% Wrap the given XML document in a valid netconf 'rpc' request and
+%% send to the server.
+-spec send_rpc(Client, SimpleXml) -> Result when
+ Client :: client(),
+ SimpleXml :: simple_xml(),
+ Result :: [simple_xml()] | {error,error_reason()}.
send_rpc(Client, SimpleXml) ->
send_rpc(Client, SimpleXml, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec send_rpc(Client, SimpleXml, Timeout) -> Result when
Client :: client(),
SimpleXml :: simple_xml(),
Timeout :: timeout(),
Result :: [simple_xml()] | {error,error_reason()}.
-%% @doc Send a Netconf <code>rpc</code> request to the server.
-%%
-%% The given XML document is wrapped in a valid Netconf
-%% <code>rpc</code> request and sent to the server. The
-%% <code>message-id</code> and namespace attributes are added to the
-%% <code>rpc</code> element.
-%%
-%% This function can be used for sending <code>rpc</code> requests
-%% that can not be expressed by other interface functions in this
-%% module.
send_rpc(Client, SimpleXml, Timeout) ->
call(Client,{send_rpc, SimpleXml, Timeout}).
%%----------------------------------------------------------------------
-%% @spec lock(Client, Target) -> Result
-%% @equiv lock(Client, Target, infinity)
+%% Send a 'lock' request.
+-spec lock(Client, Target) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Result :: ok | {error,error_reason()}.
lock(Client, Target) ->
lock(Client, Target,?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec lock(Client, Target, Timeout) -> Result when
Client :: client(),
Target :: netconf_db(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Unlock configuration target.
-%%
-%% Which target parameters that can be used depends on if
-%% `:candidate' and/or `:startup' are supported by the
-%% server. If successfull, the configuration system of the device is
-%% not available to other clients (Netconf, CORBA, SNMP etc). Locks
-%% are intended to be short-lived.
-%%
-%% The operations {@link kill_session/2} or {@link kill_session/3} can
-%% be used to force the release of a lock owned by another Netconf
-%% session. How this is achieved by the server side is implementation
-%% specific.
-%%
-%% @end
-%%----------------------------------------------------------------------
lock(Client, Target, Timeout) ->
call(Client,{send_rpc_op,lock,[Target],Timeout}).
%%----------------------------------------------------------------------
-%% @spec unlock(Client, Target) -> Result
-%% @equiv unlock(Client, Target, infinity)
+%% Send a 'unlock' request.
+-spec unlock(Client, Target) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Result :: ok | {error,error_reason()}.
unlock(Client, Target) ->
unlock(Client, Target,?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec unlock(Client, Target, Timeout) -> Result when
Client :: client(),
Target :: netconf_db(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Unlock configuration target.
-%%
-%% If the client earlier has aquired a lock, via {@link lock/2} or
-%% {@link lock/3}, this operation release the associated lock. To be
-%% able to access another target than `running', the server must
-%% support `:candidate' and/or `:startup'.
-%%
-%% @end
-%%----------------------------------------------------------------------
unlock(Client, Target, Timeout) ->
call(Client, {send_rpc_op, unlock, [Target], Timeout}).
%%----------------------------------------------------------------------
-%% @spec get(Client, Filter) -> Result
-%% @equiv get(Client, Filter, infinity)
+%% Send a 'get' request.
+-spec get(Client, Filter) -> Result when
+ Client :: client(),
+ Filter :: simple_xml() | xpath(),
+ Result :: {ok,[simple_xml()]} | {error,error_reason()}.
get(Client, Filter) ->
get(Client, Filter, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec get(Client, Filter, Timeout) -> Result when
Client :: client(),
Filter :: simple_xml() | xpath(),
Timeout :: timeout(),
Result :: {ok,[simple_xml()]} | {error,error_reason()}.
-%% @doc Get data.
-%%
-%% This operation returns both configuration and state data from the
-%% server.
-%%
-%% Filter type `xpath' can only be used if the server supports
-%% `:xpath'.
-%%
-%% @end
-%%----------------------------------------------------------------------
get(Client, Filter, Timeout) ->
call(Client,{send_rpc_op, get, [Filter], Timeout}).
%%----------------------------------------------------------------------
-%% @spec get_config(Client, Source, Filter) -> Result
-%% @equiv get_config(Client, Source, Filter, infinity)
+%% Send a 'get-config' request.
+-spec get_config(Client, Source, Filter) -> Result when
+ Client :: client(),
+ Source :: netconf_db(),
+ Filter :: simple_xml() | xpath(),
+ Result :: {ok,[simple_xml()]} | {error,error_reason()}.
get_config(Client, Source, Filter) ->
get_config(Client, Source, Filter, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec get_config(Client, Source, Filter, Timeout) -> Result when
Client :: client(),
Source :: netconf_db(),
Filter :: simple_xml() | xpath(),
Timeout :: timeout(),
Result :: {ok,[simple_xml()]} | {error,error_reason()}.
-%% @doc Get configuration data.
-%%
-%% To be able to access another source than `running', the server
-%% must advertise `:candidate' and/or `:startup'.
-%%
-%% Filter type `xpath' can only be used if the server supports
-%% `:xpath'.
-%%
-%%
-%% @end
-%%----------------------------------------------------------------------
get_config(Client, Source, Filter, Timeout) ->
call(Client, {send_rpc_op, get_config, [Source, Filter], Timeout}).
%%----------------------------------------------------------------------
-%% @spec edit_config(Client, Target, Config) -> Result
-%% @equiv edit_config(Client, Target, Config, infinity)
+%% Send a 'edit-config' request.
+-spec edit_config(Client, Target, Config) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Config :: simple_xml(),
+ Result :: ok | {error,error_reason()}.
edit_config(Client, Target, Config) ->
edit_config(Client, Target, Config, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
--spec edit_config(Client, Target, Config, Timeout) -> Result when
+-spec edit_config(Client, Target, Config, OptParams) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Config :: simple_xml(),
+ OptParams :: [simple_xml()],
+ Result :: ok | {error,error_reason()};
+ (Client, Target, Config, Timeout) -> Result when
Client :: client(),
Target :: netconf_db(),
Config :: simple_xml(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Edit configuration data.
-%%
-%% Per default only the running target is available, unless the server
-%% include `:candidate' or `:startup' in its list of
-%% capabilities.
-%%
-%% @end
-%%----------------------------------------------------------------------
-edit_config(Client, Target, Config, Timeout) ->
- call(Client, {send_rpc_op, edit_config, [Target,Config], Timeout}).
+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()}.
+edit_config(Client, Target, Config, OptParams, Timeout) ->
+ call(Client, {send_rpc_op, edit_config, [Target,Config,OptParams], Timeout}).
%%----------------------------------------------------------------------
-%% @spec delete_config(Client, Target) -> Result
-%% @equiv delete_config(Client, Target, infinity)
+%% Send a 'delete-config' request.
+-spec delete_config(Client, Target) -> Result when
+ Client :: client(),
+ Target :: startup | candidate,
+ Result :: ok | {error,error_reason()}.
delete_config(Client, Target) ->
delete_config(Client, Target, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec delete_config(Client, Target, Timeout) -> Result when
Client :: client(),
Target :: startup | candidate,
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Delete configuration data.
-%%
-%% The running configuration cannot be deleted and `:candidate'
-%% or `:startup' must be advertised by the server.
-%%
-%% @end
-%%----------------------------------------------------------------------
delete_config(Client, Target, Timeout) when Target == startup;
Target == candidate ->
call(Client,{send_rpc_op, delete_config, [Target], Timeout}).
%%----------------------------------------------------------------------
-%% @spec copy_config(Client, Source, Target) -> Result
-%% @equiv copy_config(Client, Source, Target, infinity)
+%% Send a 'copy-config' request.
+-spec copy_config(Client, Target, Source) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Source :: netconf_db(),
+ Result :: ok | {error,error_reason()}.
copy_config(Client, Source, Target) ->
copy_config(Client, Source, Target, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec copy_config(Client, Target, Source, Timeout) -> Result when
Client :: client(),
Target :: netconf_db(),
Source :: netconf_db(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Copy configuration data.
-%%
-%% Which source and target options that can be issued depends on the
-%% capabilities supported by the server. I.e. `:candidate' and/or
-%% `:startup' are required.
-%%
-%% @end
-%%----------------------------------------------------------------------
copy_config(Client, Target, Source, Timeout) ->
call(Client,{send_rpc_op, copy_config, [Target, Source], Timeout}).
%%----------------------------------------------------------------------
-%% @spec action(Client, Action) -> Result
-%% @equiv action(Client, Action, infinity)
+%% Execute an action.
+-spec action(Client, Action) -> Result when
+ Client :: client(),
+ Action :: simple_xml(),
+ Result :: ok | {ok,[simple_xml()]} | {error,error_reason()}.
action(Client,Action) ->
action(Client,Action,?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec action(Client, Action, Timeout) -> Result when
Client :: client(),
Action :: simple_xml(),
Timeout :: timeout(),
Result :: ok | {ok,[simple_xml()]} | {error,error_reason()}.
-%% @doc Execute an action. If the return type is void, <c>ok</c> will
-%% be returned instead of <c>{ok,[simple_xml()]}</c>.
-%%
-%% @end
-%%----------------------------------------------------------------------
action(Client,Action,Timeout) ->
call(Client,{send_rpc_op, action, [Action], Timeout}).
%%----------------------------------------------------------------------
+%% Send a 'create-subscription' request
+%% See RFC5277, NETCONF Event Notifications
+-spec create_subscription(Client) -> Result when
+ Client :: client(),
+ Result :: ok | {error,error_reason()}.
create_subscription(Client) ->
create_subscription(Client,?DEFAULT_STREAM,?DEFAULT_TIMEOUT).
+-spec create_subscription(Client, Stream | Filter | Timeout) -> Result when
+ Client :: client(),
+ Stream :: stream_name(),
+ Filter :: simple_xml() | [simple_xml()],
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
create_subscription(Client,Timeout)
when ?is_timeout(Timeout) ->
create_subscription(Client,?DEFAULT_STREAM,Timeout);
@@ -840,6 +740,22 @@ create_subscription(Client,Stream,Filter,Timeout)
[Stream,Filter,undefined,undefined],
Timeout}).
+-spec create_subscription(Client, Stream, StartTime, StopTime, Timeout) ->
+ Result when
+ Client :: client(),
+ Stream :: stream_name(),
+ StartTime :: xs_datetime(),
+ StopTime :: xs_datetime(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()};
+ (Client, Stream, Filter,StartTime, StopTime) ->
+ Result when
+ Client :: client(),
+ Stream :: stream_name(),
+ Filter :: simple_xml() | [simple_xml()],
+ StartTime :: xs_datetime(),
+ StopTime :: xs_datetime(),
+ Result :: ok | {error,error_reason()}.
create_subscription(Client,Stream,StartTime,StopTime,Timeout)
when ?is_string(Stream) andalso
?is_string(StartTime) andalso
@@ -855,7 +771,6 @@ create_subscription(Client,Stream,Filter,StartTime,StopTime)
?is_string(StopTime) ->
create_subscription(Client,Stream,Filter,StartTime,StopTime,?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec create_subscription(Client, Stream, Filter,StartTime, StopTime, Timeout) ->
Result when
Client :: client(),
@@ -865,168 +780,75 @@ create_subscription(Client,Stream,Filter,StartTime,StopTime)
StopTime :: xs_datetime(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Create a subscription for event notifications.
-%%
-%% This function sets up a subscription for netconf event
-%% notifications of the given stream type, matching the given
-%% filter. The calling process will receive notifications as messages
-%% of type `notification()'.
-%%
-%% <dl>
-%% <dt>Stream:</dt>
-%% <dd> An optional parameter that indicates which stream of events
-%% is of interest. If not present, events in the default NETCONF
-%% stream will be sent.</dd>
-%%
-%% <dt>Filter:</dt>
-%% <dd>An optional parameter that indicates which subset of all
-%% possible events is of interest. The format of this parameter is
-%% the same as that of the filter parameter in the NETCONF protocol
-%% operations. If not present, all events not precluded by other
-%% parameters will be sent.</dd>
-%%
-%% <dt>StartTime:</dt>
-%% <dd>An optional parameter used to trigger the replay feature and
-%% indicate that the replay should start at the time specified. If
-%% `StartTime' is not present, this is not a replay subscription.
-%% It is not valid to specify start times that are later than the
-%% current time. If the `StartTime' specified is earlier than the
-%% log can support, the replay will begin with the earliest
-%% available notification. This parameter is of type dateTime and
-%% compliant to [RFC3339]. Implementations must support time
-%% zones.</dd>
-%%
-%% <dt>StopTime:</dt>
-%% <dd>An optional parameter used with the optional replay feature
-%% to indicate the newest notifications of interest. If `StopTime'
-%% is not present, the notifications will continue until the
-%% subscription is terminated. Must be used with and be later than
-%% `StartTime'. Values of `StopTime' in the future are valid. This
-%% parameter is of type dateTime and compliant to [RFC3339].
-%% Implementations must support time zones.</dd>
-%% </dl>
-%%
-%% See RFC5277 for further details about the event notification
-%% mechanism.
-%%
-%% @end
-%%----------------------------------------------------------------------
create_subscription(Client,Stream,Filter,StartTime,StopTime,Timeout) ->
call(Client,{send_rpc_op,{create_subscription, self()},
[Stream,Filter,StartTime,StopTime],
Timeout}).
%%----------------------------------------------------------------------
-%% @spec get_event_streams(Client, Timeout) -> Result
-%% @equiv get_event_streams(Client, [], Timeout)
+%% Send a request to get the given event streams
+%% See RFC5277, NETCONF Event Notifications
+-spec get_event_streams(Client)
+ -> Result when
+ Client :: client(),
+ Result :: {ok,streams()} | {error,error_reason()}.
+get_event_streams(Client) ->
+ get_event_streams(Client,[],?DEFAULT_TIMEOUT).
+
+-spec get_event_streams(Client, Timeout)
+ -> Result when
+ Client :: client(),
+ Timeout :: timeout(),
+ Result :: {ok,streams()} | {error,error_reason()};
+ (Client, Streams) -> Result when
+ Client :: client(),
+ Streams :: [stream_name()],
+ Result :: {ok,streams()} | {error,error_reason()}.
get_event_streams(Client,Timeout) when is_integer(Timeout); Timeout==infinity ->
get_event_streams(Client,[],Timeout);
-
-%%----------------------------------------------------------------------
-%% @spec get_event_streams(Client, Streams) -> Result
-%% @equiv get_event_streams(Client, Streams, infinity)
get_event_streams(Client,Streams) when is_list(Streams) ->
get_event_streams(Client,Streams,?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec get_event_streams(Client, Streams, Timeout)
-> Result when
Client :: client(),
Streams :: [stream_name()],
Timeout :: timeout(),
Result :: {ok,streams()} | {error,error_reason()}.
-%% @doc Send a request to get the given event streams.
-%%
-%% `Streams' is a list of stream names. The following filter will
-%% be sent to the netconf server in a `get' request:
-%%
-%% ```
-%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification">
-%% <streams>
-%% <stream>
-%% <name>StreamName1</name>
-%% </stream>
-%% <stream>
-%% <name>StreamName2</name>
-%% </stream>
-%% ...
-%% </streams>
-%% </netconf>
-%% '''
-%%
-%% If `Streams' is an empty list, ALL streams will be requested
-%% by sending the following filter:
-%%
-%% ```
-%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification">
-%% <streams/>
-%% </netconf>
-%% '''
-%%
-%% If more complex filtering is needed, a use {@link get/2} or {@link
-%% get/3} and specify the exact filter according to XML Schema for
-%% Event Notifications found in RFC5277.
-%%
-%% @end
-%%----------------------------------------------------------------------
get_event_streams(Client,Streams,Timeout) ->
call(Client,{get_event_streams,Streams,Timeout}).
%%----------------------------------------------------------------------
-%% @spec close_session(Client) -> Result
-%% @equiv close_session(Client, infinity)
+%% Send a 'close-session' request
+-spec close_session(Client) -> Result when
+ Client :: client(),
+ Result :: ok | {error,error_reason()}.
close_session(Client) ->
close_session(Client, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec close_session(Client, Timeout) -> Result when
Client :: client(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Request graceful termination of the session associated with the client.
-%%
-%% When a netconf server receives a `close-session' request, it
-%% will gracefully close the session. The server will release any
-%% locks and resources associated with the session and gracefully
-%% close any associated connections. Any NETCONF requests received
-%% after a `close-session' request will be ignored.
-%%
-%% @end
-%%----------------------------------------------------------------------
close_session(Client, Timeout) ->
call(Client,{send_rpc_op, close_session, [], Timeout}, true).
%%----------------------------------------------------------------------
-%% @spec kill_session(Client, SessionId) -> Result
-%% @equiv kill_session(Client, SessionId, infinity)
+%% Send a 'kill-session' request
+-spec kill_session(Client, SessionId) -> Result when
+ Client :: client(),
+ SessionId :: pos_integer(),
+ Result :: ok | {error,error_reason()}.
kill_session(Client, SessionId) ->
kill_session(Client, SessionId, ?DEFAULT_TIMEOUT).
-%%----------------------------------------------------------------------
-spec kill_session(Client, SessionId, Timeout) -> Result when
Client :: client(),
SessionId :: pos_integer(),
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
-%% @doc Force termination of the session associated with the supplied
-%% session id.
-%%
-%% The server side shall abort any operations currently in process,
-%% release any locks and resources associated with the session, and
-%% close any associated connections.
-%%
-%% Only if the server is in the confirmed commit phase, the
-%% configuration will be restored to its state before entering the
-%% confirmed commit phase. Otherwise, no configuration roll back will
-%% be performed.
-%%
-%% If the given `SessionId' is equal to the current session id,
-%% an error will be returned.
-%%
-%% @end
-%% ----------------------------------------------------------------------
kill_session(Client, SessionId, Timeout) ->
call(Client,{send_rpc_op, kill_session, [SessionId], Timeout}).
@@ -1035,24 +857,35 @@ kill_session(Client, SessionId, Timeout) ->
%% Callback functions
%%----------------------------------------------------------------------
-%% @private
+init(_KeyOrName,{CM,{Host,Port}},Options) ->
+ case ssh_channel(#connection{reference=CM,host=Host,port=Port},Options) of
+ {ok,Connection} ->
+ {ok, CM, #state{connection = Connection}};
+ {error,Reason}->
+ {error,Reason}
+ end;
+init(_KeyOrName,{_Host,_Port},Options) when Options#options.type==connection ->
+ case ssh_connect(Options) of
+ {ok, Connection} ->
+ ConnPid = Connection#connection.reference,
+ {ok, ConnPid, #state{connection = Connection}};
+ Error ->
+ Error
+ end;
init(_KeyOrName,{_Host,_Port},Options) ->
case ssh_open(Options) of
{ok, Connection} ->
- log(Connection,open),
{ConnPid,_} = Connection#connection.reference,
{ok, ConnPid, #state{connection = Connection}};
{error,Reason}->
{error,Reason}
end.
-%% @private
+
terminate(_, #state{connection=Connection}) ->
ssh_close(Connection),
- log(Connection,close),
ok.
-%% @private
handle_msg({hello, Options, Timeout}, From,
#state{connection=Connection,hello_status=HelloStatus} = State) ->
case do_send(Connection, client_hello(Options)) of
@@ -1071,6 +904,14 @@ handle_msg({hello, Options, Timeout}, From,
Error ->
{stop, Error, State}
end;
+handle_msg(get_ssh_connection, _From, #state{connection=Connection}=State) ->
+ Reply =
+ case Connection#connection.reference of
+ {_,_} -> {error,not_an_ssh_connection};
+ CM -> {ok,{CM,{Connection#connection.host,
+ Connection#connection.port}}}
+ end,
+ {reply, Reply, State};
handle_msg(_, _From, #state{session_id=undefined} = State) ->
%% Hello is not yet excanged - this shall never happen
{reply,{error,waiting_for_hello},State};
@@ -1133,9 +974,8 @@ handle_msg({Ref,timeout},#state{pending=Pending} = State) ->
end,
%% Halfhearted try to get in correct state, this matches
%% the implementation before this patch
- {R,State#state{pending=Pending1, buff= <<>>}}.
+ {R,State#state{pending=Pending1, no_end_tag_buff= <<>>, buff= <<>>}}.
-%% @private
%% Called by ct_util_server to close registered connections before terminate.
close(Client) ->
case get_handle(Client) of
@@ -1168,7 +1008,7 @@ call(Client, Msg, Timeout, WaitStop) ->
{error,no_such_client};
{error,{process_down,Pid,normal}} when WaitStop ->
%% This will happen when server closes connection
- %% before clien received rpc-reply on
+ %% before client received rpc-reply on
%% close-session.
ok;
{error,{process_down,Pid,normal}} ->
@@ -1206,26 +1046,36 @@ get_handle(Client) ->
Error
end.
+check_options(OptList,Options) ->
+ check_options(OptList,undefined,undefined,Options).
+
check_options([], undefined, _Port, _Options) ->
{error, no_host_address};
check_options([], _Host, undefined, _Options) ->
{error, no_port};
check_options([], Host, Port, Options) ->
{Host,Port,Options};
-check_options([{ssh, Host}|T], _, Port, #options{} = Options) ->
+check_options([{ssh, Host}|T], _, Port, Options) ->
check_options(T, Host, Port, Options#options{host=Host});
-check_options([{port,Port}|T], Host, _, #options{} = Options) ->
+check_options([{port,Port}|T], Host, _, Options) ->
check_options(T, Host, Port, Options#options{port=Port});
check_options([{timeout, Timeout}|T], Host, Port, Options)
when is_integer(Timeout); Timeout==infinity ->
check_options(T, Host, Port, Options#options{timeout = Timeout});
-check_options([{X,_}=Opt|T], Host, Port, #options{ssh=SshOpts}=Options) ->
- case lists:member(X,?VALID_SSH_OPTS) of
- true ->
- check_options(T, Host, Port, Options#options{ssh=[Opt|SshOpts]});
- false ->
- {error, {invalid_option, Opt}}
- end.
+check_options([{timeout, _} = Opt|_T], _Host, _Port, _Options) ->
+ {error, {invalid_option, Opt}};
+check_options([Opt|T], Host, Port, #options{ssh=SshOpts}=Options) ->
+ %% Option verified by ssh
+ check_options(T, Host, Port, Options#options{ssh=[Opt|SshOpts]}).
+
+check_session_options([],Options) ->
+ {ok,Options};
+check_session_options([{timeout, Timeout}|T], Options)
+ when is_integer(Timeout); Timeout==infinity ->
+ check_session_options(T, Options#options{timeout = Timeout});
+check_session_options([Opt|_T], _Options) ->
+ {error, {invalid_option, Opt}}.
+
%%%-----------------------------------------------------------------
set_request_timer(infinity) ->
@@ -1235,6 +1085,14 @@ set_request_timer(T) ->
{ok,TRef} = timer:send_after(T,{Ref,timeout}),
{Ref,TRef}.
+%%%-----------------------------------------------------------------
+cancel_request_timer(undefined,undefined) ->
+ ok;
+cancel_request_timer(Ref,TRef) ->
+ _ = timer:cancel(TRef),
+ receive {Ref,timeout} -> ok
+ after 0 -> ok
+ end.
%%%-----------------------------------------------------------------
client_hello(Options) when is_list(Options) ->
@@ -1254,8 +1112,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]) ->
@@ -1313,7 +1171,6 @@ do_send_rpc(Connection, MsgId, SimpleXml) ->
do_send(Connection, SimpleXml) ->
Xml=to_xml_doc(SimpleXml),
- log(Connection,send,Xml),
ssh_send(Connection, Xml).
to_xml_doc(Simple) ->
@@ -1327,17 +1184,37 @@ to_xml_doc(Simple) ->
%%%-----------------------------------------------------------------
%%% Parse and handle received XML data
-handle_data(NewData,#state{connection=Connection,buff=Buff0} = State0) ->
+%%% Two buffers are used:
+%%% * 'no_end_tag_buff' contains data that is checked and does not
+%%% contain any (part of an) end tag.
+%%% * 'buff' contains all other saved data - it may or may not
+%%% include (a part of) an end tag.
+%%% The reason for this is to avoid running binary:split/3 multiple
+%%% times on the same data when it does not contain an end tag. This
+%%% can be a considerable optimation in the case when a lot of data is
+%%% received (e.g. when fetching all data from a node) and the data is
+%%% sent in multiple ssh packages.
+handle_data(NewData,#state{connection=Connection} = State0) ->
log(Connection,recv,NewData),
- Data = append_wo_initial_nl(Buff0,NewData),
- case binary:split(Data,[?END_TAG],[]) of
+ NoEndTag0 = State0#state.no_end_tag_buff,
+ Buff0 = State0#state.buff,
+ Data = <<Buff0/binary, NewData/binary>>,
+ case binary:split(Data,?END_TAG,[]) of
[_NoEndTagFound] ->
- {noreply, State0#state{buff=Data}};
- [FirstMsg,Buff1] ->
+ NoEndTagSize = case byte_size(Data) of
+ Sz when Sz<5 -> 0;
+ Sz -> Sz-5
+ end,
+ <<NoEndTag1:NoEndTagSize/binary,Buff/binary>> = Data,
+ NoEndTag = <<NoEndTag0/binary,NoEndTag1/binary>>,
+ {noreply, State0#state{no_end_tag_buff=NoEndTag, buff=Buff}};
+ [FirstMsg0,Buff1] ->
+ FirstMsg = remove_initial_nl(<<NoEndTag0/binary,FirstMsg0/binary>>),
SaxArgs = [{event_fun,fun sax_event/3}, {event_state,[]}],
case xmerl_sax_parser:stream(FirstMsg, SaxArgs) of
{ok, Simple, _Thrash} ->
- case decode(Simple, State0#state{buff=Buff1}) of
+ case decode(Simple, State0#state{no_end_tag_buff= <<>>,
+ buff=Buff1}) of
{noreply, #state{buff=Buff} = State} when Buff =/= <<>> ->
%% Recurse if we have more data in buffer
handle_data(<<>>, State);
@@ -1349,17 +1226,18 @@ handle_data(NewData,#state{connection=Connection,buff=Buff0} = State0) ->
[{parse_error,Reason},
{buffer, Buff0},
{new_data,NewData}]),
- handle_error(Reason, State0#state{buff= <<>>})
+ handle_error(Reason, State0#state{no_end_tag_buff= <<>>,
+ buff= <<>>})
end
end.
+
%% xml does not accept a leading nl and some netconf server add a nl after
%% each ?END_TAG, ignore them
-append_wo_initial_nl(<<>>,NewData) -> NewData;
-append_wo_initial_nl(<<"\n", Data/binary>>, NewData) ->
- append_wo_initial_nl(Data, NewData);
-append_wo_initial_nl(Data, NewData) ->
- <<Data/binary, NewData/binary>>.
+remove_initial_nl(<<"\n", Data/binary>>) ->
+ remove_initial_nl(Data);
+remove_initial_nl(Data) ->
+ Data.
handle_error(Reason, State) ->
Pending1 = case State#state.pending of
@@ -1367,9 +1245,9 @@ handle_error(Reason, State) ->
Pending ->
%% Assuming the first request gets the
%% first answer
- P=#pending{tref=TRef,caller=Caller} =
+ P=#pending{tref=TRef,ref=Ref,caller=Caller} =
lists:last(Pending),
- _ = timer:cancel(TRef),
+ cancel_request_timer(Ref,TRef),
Reason1 = {failed_to_parse_received_data,Reason},
ct_gen_conn:return(Caller,{error,Reason1}),
lists:delete(P,Pending)
@@ -1455,8 +1333,8 @@ decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) ->
{error,Reason} ->
{noreply,State#state{hello_status = {error,Reason}}}
end;
- #pending{tref=TRef,caller=Caller} ->
- _ = timer:cancel(TRef),
+ #pending{tref=TRef,ref=Ref,caller=Caller} ->
+ cancel_request_timer(Ref,TRef),
case decode_hello(E) of
{ok,SessionId,Capabilities} ->
ct_gen_conn:return(Caller,ok),
@@ -1482,9 +1360,8 @@ decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) ->
%% there is just one pending that matches (i.e. has
%% undefined msg_id and op)
case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of
- [#pending{tref=TRef,
- caller=Caller}] ->
- _ = timer:cancel(TRef),
+ [#pending{tref=TRef,ref=Ref,caller=Caller}] ->
+ cancel_request_timer(Ref,TRef),
ct_gen_conn:return(Caller,E),
{noreply,State#state{pending=[]}};
_ ->
@@ -1505,8 +1382,8 @@ get_msg_id(Attrs) ->
decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) ->
case lists:keytake(MsgId,#pending.msg_id,Pending) of
- {value, #pending{tref=TRef,op=Op,caller=Caller}, Pending1} ->
- _ = timer:cancel(TRef),
+ {value, #pending{tref=TRef,ref=Ref,op=Op,caller=Caller}, Pending1} ->
+ cancel_request_timer(Ref,TRef),
Content = forward_xmlns_attr(Attrs,Content0),
{CallerReply,{ServerReply,State2}} =
do_decode_rpc_reply(Op,Content,State#state{pending=Pending1}),
@@ -1518,10 +1395,11 @@ decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) ->
%% pending that matches (i.e. has undefined msg_id and op)
case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of
[#pending{tref=TRef,
+ ref=Ref,
msg_id=undefined,
op=undefined,
caller=Caller}] ->
- _ = timer:cancel(TRef),
+ cancel_request_timer(Ref,TRef),
ct_gen_conn:return(Caller,E),
{noreply,State#state{pending=[]}};
_ ->
@@ -1589,7 +1467,7 @@ decode_data(Other) ->
{error,{unexpected_rpc_reply,Other}}.
get_qualified_name(Tag) ->
- case string:tokens(atom_to_list(Tag),":") of
+ case string:lexemes(atom_to_list(Tag),":") of
[TagStr] -> {[],TagStr};
[PrefixStr,TagStr] -> {PrefixStr,TagStr}
end.
@@ -1702,9 +1580,14 @@ decode_streams([]) ->
log(Connection,Action) ->
log(Connection,Action,<<>>).
-log(#connection{host=Host,port=Port,name=Name},Action,Data) ->
+log(#connection{reference=Ref,host=Host,port=Port,name=Name},Action,Data) ->
+ Address =
+ case Ref of
+ {_,Ch} -> {Host,Port,Ch};
+ _ -> {Host,Port}
+ end,
error_logger:info_report(#conn_log{client=self(),
- address={Host,Port},
+ address=Address,
name=Name,
action=Action,
module=?MODULE},
@@ -1724,9 +1607,14 @@ format_data(How,Data) ->
do_format_data(raw,Data) ->
io_lib:format("~n~ts~n",[hide_password(Data)]);
do_format_data(pretty,Data) ->
- io_lib:format("~n~ts~n",[indent(Data)]);
+ maybe_io_lib_format(indent(Data));
do_format_data(html,Data) ->
- io_lib:format("~n~ts~n",[html_format(Data)]).
+ maybe_io_lib_format(html_format(Data)).
+
+maybe_io_lib_format(<<>>) ->
+ [];
+maybe_io_lib_format(String) ->
+ io_lib:format("~n~ts~n",[String]).
%%%-----------------------------------------------------------------
%%% Hide password elements from XML data
@@ -1765,13 +1653,21 @@ indent1("<?"++Rest1,Indent1) ->
Line++indent1(Rest2,Indent2);
indent1("</"++Rest1,Indent1) ->
%% Stop tag
- {Line,Rest2,Indent2} = indent_line1(Rest1,Indent1,[$/,$<]),
- "\n"++Line++indent1(Rest2,Indent2);
+ case indent_line1(Rest1,Indent1,[$/,$<]) of
+ {[],[],_} ->
+ [];
+ {Line,Rest2,Indent2} ->
+ "\n"++Line++indent1(Rest2,Indent2)
+ end;
indent1("<"++Rest1,Indent1) ->
%% Start- or empty tag
put(tag,get_tag(Rest1)),
- {Line,Rest2,Indent2} = indent_line(Rest1,Indent1,[$<]),
- "\n"++Line++indent1(Rest2,Indent2);
+ case indent_line(Rest1,Indent1,[$<]) of
+ {[],[],_} ->
+ [];
+ {Line,Rest2,Indent2} ->
+ "\n"++Line++indent1(Rest2,Indent2)
+ end;
indent1([H|T],Indent) ->
[H|indent1(T,Indent)];
indent1([],_Indent) ->
@@ -1837,42 +1733,84 @@ get_tag([]) ->
%%%-----------------------------------------------------------------
%%% SSH stuff
-
-ssh_open(#options{host=Host,timeout=Timeout,port=Port,ssh=SshOpts,name=Name}) ->
+ssh_connect(#options{host=Host,timeout=Timeout,port=Port,
+ ssh=SshOpts,name=Name,type=Type}) ->
case ssh:connect(Host, Port,
[{user_interaction,false},
- {silently_accept_hosts, true}|SshOpts]) of
+ {silently_accept_hosts, true}|SshOpts],
+ Timeout) of
{ok,CM} ->
- case ssh_connection:session_channel(CM, Timeout) of
- {ok,Ch} ->
- case ssh_connection:subsystem(CM, Ch, "netconf", Timeout) of
- success ->
- {ok, #connection{reference = {CM,Ch},
- host = Host,
- port = Port,
- name = Name}};
- failure ->
- ssh:close(CM),
- {error,{ssh,could_not_execute_netconf_subsystem}};
- {error,timeout} ->
- {error,{ssh,could_not_execute_netconf_subsystem,timeout}}
- end;
- {error, Reason} ->
- ssh:close(CM),
- {error,{ssh,could_not_open_channel,Reason}}
- end;
+ Connection = #connection{reference = CM,
+ host = Host,
+ port = Port,
+ name = Name,
+ type = Type},
+ log(Connection,connect),
+ {ok,Connection};
{error,Reason} ->
{error,{ssh,could_not_connect_to_server,Reason}}
end.
-ssh_send(#connection{reference = {CM,Ch}}, Data) ->
+ssh_channel(#connection{reference=CM}=Connection0,
+ #options{timeout=Timeout,name=Name,type=Type}) ->
+ case ssh_connection:session_channel(CM, Timeout) of
+ {ok,Ch} ->
+ case ssh_connection:subsystem(CM, Ch, "netconf", Timeout) of
+ success ->
+ Connection = Connection0#connection{reference = {CM,Ch},
+ name = Name,
+ type = Type},
+ log(Connection,open),
+ {ok, Connection};
+ failure ->
+ ssh_connection:close(CM,Ch),
+ {error,{ssh,could_not_execute_netconf_subsystem}};
+ {error,timeout} ->
+ ssh_connection:close(CM,Ch),
+ {error,{ssh,could_not_execute_netconf_subsystem,timeout}}
+ end;
+ {error, Reason} ->
+ {error,{ssh,could_not_open_channel,Reason}}
+ end.
+
+
+ssh_open(Options) ->
+ case ssh_connect(Options) of
+ {ok,Connection} ->
+ case ssh_channel(Connection,Options) of
+ {ok,_} = Ok ->
+ Ok;
+ Error ->
+ ssh_close(Connection),
+ Error
+ end;
+ Error ->
+ Error
+ end.
+
+ssh_send(#connection{reference = {CM,Ch}}=Connection, Data) ->
case ssh_connection:send(CM, Ch, Data) of
- ok -> ok;
- {error,Reason} -> {error,{ssh,failed_to_send_data,Reason}}
+ ok ->
+ log(Connection,send,Data),
+ ok;
+ {error,Reason} ->
+ {error,{ssh,failed_to_send_data,Reason}}
end.
-ssh_close(#connection{reference = {CM,_Ch}}) ->
- ssh:close(CM).
+ssh_close(Connection=#connection{reference = {CM,Ch}, type = Type}) ->
+ _ = ssh_connection:close(CM,Ch),
+ log(Connection,close),
+ case Type of
+ connection_and_channel ->
+ ssh_close(Connection#connection{reference = CM});
+ _ ->
+ ok
+ end,
+ ok;
+ssh_close(Connection=#connection{reference = CM}) ->
+ _ = ssh:close(CM),
+ log(Connection,disconnect),
+ ok.
%%----------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_netconfc.hrl b/lib/common_test/src/ct_netconfc.hrl
index 295a61a98b..1d81c762ba 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.
+%% Copyright Ericsson AB 2012-2018. 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%
%%
@@ -23,9 +24,7 @@
%% This file defines constant values and records used by the
%% netconf client ct_netconfc.
%%
-%% @author Support
-%% @doc Netconf Client Interface.
-%% @end
+%% Netconf Client Interface.
%%----------------------------------------------------------------------
%%----------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_property_test.erl b/lib/common_test/src/ct_property_test.erl
index 52acda5388..f51f454d08 100644
--- a/lib/common_test/src/ct_property_test.erl
+++ b/lib/common_test/src/ct_property_test.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2018. 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%
%%
@@ -27,46 +28,6 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-%%% @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
@@ -75,19 +36,6 @@
-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} ->
@@ -107,14 +55,6 @@ init_per_suite(Config) ->
{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),
@@ -133,7 +73,7 @@ mk_ct_return(Other, Tool) ->
try lists:last(hd(Tool:counterexample()))
of
{set,{var,_},{call,M,F,Args}} ->
- {fail, io_lib:format("~p:~p/~p returned bad result",[M,F,length(Args)])}
+ {fail, io_lib:format("~p:~tp/~p returned bad result",[M,F,length(Args)])}
catch
_:_ ->
{fail, Other}
@@ -160,7 +100,9 @@ property_tests_path(Dir, Config) ->
add_code_pathz(Dir) ->
case lists:member(Dir, code:get_path()) of
true -> ok;
- false -> code:add_pathz(Dir)
+ false ->
+ true = code:add_pathz(Dir),
+ ok
end.
compile_tests(Path, ToolModule) ->
@@ -170,10 +112,10 @@ compile_tests(Path, ToolModule) ->
{ok,FileNames} = file:list_dir("."),
BeamFiles = [F || F<-FileNames,
filename:extension(F) == ".beam"],
- [file:delete(F) || F<-BeamFiles],
- ct:pal("Compiling in ~p:~n Deleted ~p~n MacroDefs=~p",[Path,BeamFiles,MacroDefs]),
+ _ = [file:delete(F) || F<-BeamFiles],
+ ct:pal("Compiling in ~tp:~n Deleted ~p~n MacroDefs=~p",[Path,BeamFiles,MacroDefs]),
Result = make:all([load|MacroDefs]),
- file:set_cwd(Cwd),
+ ok = file:set_cwd(Cwd),
Result.
diff --git a/lib/common_test/src/ct_release_test.erl b/lib/common_test/src/ct_release_test.erl
new file mode 100644
index 0000000000..60d17f43dc
--- /dev/null
+++ b/lib/common_test/src/ct_release_test.erl
@@ -0,0 +1,939 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2014-2018. 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%
+%%
+%%-----------------------------------------------------------------
+%% 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,
+%% ct_release_test 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, init/1 will return
+%% {skip,Reason} and any attempt at running upgrade/4
+%% will fail.
+%%
+%% == Callback functions ==
+%%
+%% The following functions should be exported from a ct_release_test
+%% callback module.
+%%
+%% All callback functions are called on the node where the upgrade is
+%% executed.
+%%
+%% Module:upgrade_init(CtData,State) -> NewState
+%% Types:
+%%
+%% CtData = ct_data()
+%% State = NewState = cb_state()
+%%
+%% Initialyze system before upgrade test starts.
+%%
+%% This function is called before the upgrade is started. All
+%% applications given in upgrade/4 are already started by
+%% the boot script, so this callback is intended for additional
+%% initialization, if necessary.
+%%
+%% CtData is an opaque data structure which shall be used
+%% in any call to ct_release_test inside the callback.
+%%
+%% Example:
+%%
+%% upgrade_init(CtData,State) ->
+%% {ok,{FromVsn,ToVsn}} = ct_release_test:get_app_vsns(CtData,myapp),
+%% open_connection(State).
+%%
+%% Module:upgrade_upgraded(CtData,State) -> NewState
+%% Types:
+%%
+%% CtData = ct_data()
+%% State = NewState = cb_state()
+%%
+%% 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.
+%%
+%% CtData is an opaque data structure which shall be used
+%% in any call to ct_release_test inside the callback.
+%%
+%% Example:
+%%
+%% upgrade_upgraded(CtData,State) ->
+%% check_connection_still_open(State).
+%%
+%% Module:upgrade_downgraded(CtData,State) -> NewState
+%% Types:
+%%
+%% CtData = ct_data()
+%% State = NewState = cb_state()
+%%
+%% 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.
+%%
+%% CtData is an opaque data structure which shall be used
+%% in any call to ct_release_test inside the callback.
+%%
+%% Example:
+%%
+%% upgrade_downgraded(CtData,State) ->
+%% check_connection_closed(State).
+%%-----------------------------------------------------------------
+-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, 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}.
+%% Initialize ct_release_test.
+%%
+%% 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().
+%% 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
+%% init/1 prior to this, for example from `init_per_suite/1'.
+%%
+%% Upgrade tests are performed as follows:
+%%
+%% 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.
+%% Figure out all dependencies for the applications under
+%% test.
+%% 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.
+%% 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.
+%% Install the "From"-release and start a new node
+%% running this release.
+%% Perform the upgrade test and allow customized
+%% control by using callbacks:
+%% Callback: `upgrade_init/2'
+%% Unpack the new release
+%% Install the new release
+%% Callback: `upgrade_upgraded/2'
+%% Install the original release
+%% Callback: `upgrade_downgraded/2'
+%%
+%% `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.
+%% major
+%% From verions are picked from the previous major
+%% release. For example, if the test is run on an OTP-17
+%% node, ct_release_test will pick the application
+%% "From" versions from an OTP installation running OTP
+%% R16B.
+%%
+%% minor
+%% From verions are picked from the current major
+%% release. For example, if the test is run on an OTP-17
+%% node, ct_release_test will pick the application
+%% "From" versions from an OTP installation running an
+%% earlier patch level of OTP-17.
+%%
+%% 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 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().
+%% Clean up after tests.
+%%
+%% This function shall be called from the `end_per_*' function
+%% complementing the `init_per_*' function where 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}.
+%% 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.
+%%
+%% CtData must be the first argument received in the
+%% calling callback function - an opaque data structure set by
+%% ct_release_tests.
+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}}.
+%% 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.
+%%
+%% CtData must be the first argument received in the
+%% calling callback function - an opaque data structure set by
+%% ct_release_tests.
+%%
+%% 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 ~tp 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, ~tp",[FromVsn, FromAppsVsns]),
+ ct:log("Upgrade to: OTP-~ts, ~tp",[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:~tp/1",[Mod,Func]),
+ R = rpc:call(Node,Mod,Func,Args),
+ ct:log("~p:~tp/~w returned: ~tp",[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:lexemes(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) ->
+ file:write_file(FName, unicode:characters_to_binary(Conts)).
+
+%% Substitute all occurrences of %Var% for Val in the given scripts
+subst_src_scripts(Scripts, SrcDir, DestDir, Vars, Opts) ->
+ 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 = unicode:characters_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},unicode]),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..8b1c7d47bb 100644
--- a/lib/common_test/src/ct_repeat.erl
+++ b/lib/common_test/src/ct_repeat.erl
@@ -1,30 +1,31 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2007-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2007-2018. 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 Framework module that handles repeated test runs
+%%% doc Common Test Framework module that handles repeated test runs
%%%
-%%% <p>This module exports functions for repeating tests. The following
+%%% This module exports functions for repeating tests. The following
%%% start flags (or equivalent ct:run_test/1 options) are supported:
%%% -until <StopTime>, StopTime = YYMoMoDDHHMMSS | HHMMSS
%%% -duration <DurTime>, DurTime = HHMMSS
%%% -force_stop [skip_rest]
-%%% -repeat <N>, N = integer()</p>
+%%% -repeat <N>, N = integer()
-module(ct_repeat).
@@ -42,14 +43,14 @@ loop_test(If,Args) when is_list(Args) ->
no_loop ->
false;
E = {error,_} ->
- io:format("Common Test error: ~p\n\n",[E]),
- file:set_cwd(Cwd),
+ io:format("Common Test error: ~tp\n\n",[E]),
+ ok = file:set_cwd(Cwd),
E;
{repeat,N} ->
io:format("\nCommon Test: Will repeat tests ~w times.\n\n",[N]),
Args1 = [{loop_info,[{repeat,1,N}]} | Args],
Result = loop(If,repeat,0,N,undefined,Args1,undefined,[]),
- file:set_cwd(Cwd),
+ ok = file:set_cwd(Cwd),
Result;
{stop_time,StopTime} ->
Result =
@@ -69,13 +70,14 @@ loop_test(If,Args) when is_list(Args) ->
CtrlPid = self(),
spawn(
fun() ->
+ ct_util:mark_process(),
stop_after(CtrlPid,Secs,ForceStop)
end)
end,
Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args],
loop(If,stop_time,0,Secs,StopTime,Args1,TPid,[])
end,
- file:set_cwd(Cwd),
+ ok = file:set_cwd(Cwd),
Result
end.
@@ -88,18 +90,18 @@ loop(If,Type,N,Data0,Data1,Args,TPid,AccResult) ->
{'EXIT',Pid,Reason} ->
case Reason of
{user_error,What} ->
- io:format("\nTest run failed!\nReason: ~p\n\n\n", [What]),
+ io:format("\nTest run failed!\nReason: ~tp\n\n\n", [What]),
cancel(TPid),
{error,What};
_ ->
io:format("Test run crashed! This could be an internal error "
"- please report!\n\n"
- "~p\n\n\n",[Reason]),
+ "~tp\n\n\n",[Reason]),
cancel(TPid),
{error,Reason}
end;
{Pid,{error,Reason}} ->
- io:format("\nTest run failed!\nReason: ~p\n\n\n",[Reason]),
+ io:format("\nTest run failed!\nReason: ~tp\n\n\n",[Reason]),
cancel(TPid),
{error,Reason};
{Pid,Result} ->
@@ -133,6 +135,7 @@ spawn_tester(script,Ctrl,Args) ->
spawn_tester(func,Ctrl,Opts) ->
Tester = fun() ->
+ ct_util:mark_process(),
case catch ct_run:run_test2(Opts) of
{'EXIT',Reason} ->
exit(Reason);
diff --git a/lib/common_test/src/ct_rpc.erl b/lib/common_test/src/ct_rpc.erl
index 03d95d1408..cef5ad14e5 100644
--- a/lib/common_test/src/ct_rpc.erl
+++ b/lib/common_test/src/ct_rpc.erl
@@ -1,24 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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%
%%
-%%% @doc Common Test specific layer on Erlang/OTP rpc.
-
-module(ct_rpc).
%%% API
@@ -28,49 +27,12 @@
%%%=========================================================================
%%% API
%%%=========================================================================
-%%% @spec app_node(App, Candidates) -> NodeName
-%%%
-%%% App = atom()
-%%% Candidates = [NodeName]
-%%% NodeName = atom()
-%%%
-%%% @doc From a set of candidate nodes determines which of them is
-%%% running the application App. If none of the candidate nodes
-%%% is running the application the function will make the test case
-%%% calling this function fail. This function is the same as calling
-%%% <code>app_node(App, Candidates, true)</code>.
-%%%
app_node(App, Candidates) ->
app_node(App, Candidates, true, []).
-%%% @spec app_node(App, Candidates, FailOnBadRPC) -> NodeName
-%%%
-%%% App = atom()
-%%% Candidates = [NodeName]
-%%% NodeName = atom()
-%%% FailOnBadRPC = true | false
-%%%
-%%% @doc Same as <code>app_node/2</code> only the <code>FailOnBadRPC</code>
-%%% argument will determine if the search for a candidate node should
-%%% stop or not if <code>badrpc</code> is received at some point.
-%%%
app_node(App, Candidates, FailOnBadRPC) ->
app_node(App, Candidates, FailOnBadRPC, []).
-%%% @spec app_node(App, Candidates, FailOnBadRPC, Cookie) -> NodeName
-%%%
-%%% App = atom()
-%%% Candidates = [NodeName]
-%%% NodeName = atom()
-%%% FailOnBadRPC = true | false
-%%% Cookie = atom()
-%%%
-%%% @doc Same as <code>app_node/2</code> only the <code>FailOnBadRPC</code>
-%%% argument will determine if the search for a candidate node should
-%%% stop or not if <code>badrpc</code> is received at some point.
-%%% The cookie on the client node will be set to <code>Cookie</code>
-%%% for this rpc operation (use to match the server node cookie).
-%%%
app_node(App, [], _, _) ->
ct:fail({application_not_running, App});
@@ -80,7 +42,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});
@@ -95,107 +57,36 @@ app_node(App, _Candidates = [CandidateNode | Nodes], FailOnBadRPC, Cookie) ->
end
end.
-%%% @spec call(Node, Module, Function, Args) -> term() | {badrpc, Reason}
-%%%
-%%% @doc Same as call(Node, Module, Function, Args, infinity)
-%%%
call(Node, Module, Function, Args) ->
call(Node, Module, Function, Args, infinity, []).
-%%% @spec call(Node, Module, Function, Args, TimeOut) ->
-%%% term() | {badrpc, Reason}
-%%% Node = NodeName | {Fun, FunArgs}
-%%% Fun = fun()
-%%% FunArgs = term()
-%%% NodeName = atom()
-%%% Module = atom()
-%%% Function = atom()
-%%% Args = [term()]
-%%% Reason = timeout | term()
-%%%
-%%% @doc Evaluates apply(Module, Function, Args) on the node Node.
-%%% Returns whatever Function returns or {badrpc, Reason} if the
-%%% remote procedure call fails. If Node is {Fun, FunArgs} applying
-%%% Fun to FunArgs should return a node name.
call(Node, Module, Function, Args, TimeOut) ->
call(Node, Module, Function, Args, TimeOut, []).
-%%% @spec call(Node, Module, Function, Args, TimeOut, Cookie) ->
-%%% term() | {badrpc, Reason}
-%%% Node = NodeName | {Fun, FunArgs}
-%%% Fun = fun()
-%%% FunArgs = term()
-%%% NodeName = atom()
-%%% Module = atom()
-%%% Function = atom()
-%%% Args = [term()]
-%%% Reason = timeout | term()
-%%% Cookie = atom()
-%%%
-%%% @doc Evaluates apply(Module, Function, Args) on the node Node.
-%%% Returns whatever Function returns or {badrpc, Reason} if the
-%%% remote procedure call fails. If Node is {Fun, FunArgs} applying
-%%% Fun to FunArgs should return a node name.
-%%% The cookie on the client node will be set to <code>Cookie</code>
-%%% for this rpc operation (use to match the server node cookie).
call({Fun, FunArgs}, Module, Function, Args, TimeOut, Cookie) ->
Node = Fun(FunArgs),
call(Node, 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
-%%% Node = NodeName | {Fun, FunArgs}
-%%% Fun = fun()
-%%% FunArgs = term()
-%%% NodeName = atom()
-%%% Module = atom()
-%%% Function = atom()
-%%% Args = [term()]
-%%% Reason = timeout | term()
-%%%
-%%% @doc Evaluates apply(Module, Function, Args) on the node Node.
-%%% No response is delivered and the process which makes the call is
-%%% not suspended until the evaluation is compleated as in the case of
-%%% call/[3,4]. If Node is {Fun, FunArgs} applying
-%%% Fun to FunArgs should return a node name.
cast(Node, Module, Function, Args) ->
cast(Node, Module, Function, Args, []).
-%%% @spec cast(Node, Module, Function, Args, Cookie) -> ok
-%%% Node = NodeName | {Fun, FunArgs}
-%%% Fun = fun()
-%%% FunArgs = term()
-%%% NodeName = atom()
-%%% Module = atom()
-%%% Function = atom()
-%%% Args = [term()]
-%%% Reason = timeout | term()
-%%% Cookie = atom()
-%%%
-%%% @doc Evaluates apply(Module, Function, Args) on the node Node.
-%%% No response is delivered and the process which makes the call is
-%%% not suspended until the evaluation is compleated as in the case of
-%%% call/[3,4]. If Node is {Fun, FunArgs} applying
-%%% Fun to FunArgs should return a node name.
-%%% The cookie on the client node will be set to <code>Cookie</code>
-%%% for this rpc operation (use to match the server node cookie).
cast({Fun, FunArgs}, Module, Function, Args, Cookie) ->
Node = Fun(FunArgs),
cast(Node, 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.
%%%---------- Internal -----------
-%%% @hidden
set_the_cookie([]) ->
[];
set_the_cookie(Cookie) ->
diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl
index be547b443b..c9d406f1fd 100644
--- a/lib/common_test/src/ct_run.erl
+++ b/lib/common_test/src/ct_run.erl
@@ -1,27 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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 Framework test execution control module.
-%%%
-%%% <p>This module exports functions for installing and running tests
-%%% withing the Common Test Framework.</p>
-
-module(ct_run).
%% Script interface
@@ -64,6 +60,7 @@
logdir,
logopts = [],
basic_html,
+ esc_chars = true,
verbosity = [],
config = [],
event_handlers = [],
@@ -74,24 +71,14 @@
abort_if_missing_suites,
silent_connections = [],
stylesheet,
- multiply_timetraps = 1,
- scale_timetraps = false,
+ multiply_timetraps,
+ scale_timetraps,
create_priv_dir,
- testspecs = [],
+ testspec_files = [],
+ current_testspec,
tests,
starter}).
-%%%-----------------------------------------------------------------
-%%% @spec script_start() -> void()
-%%%
-%%% @doc Start tests via the ct_run program or script.
-%%%
-%%% <p>Example:<br/><code>./ct_run -config config.ctc -dir
-%%% $TEST_DIR</code></p>
-%%%
-%%% <p>Example:<br/><code>./ct_run -config config.ctc -suite
-%%% $SUITE_PATH/$SUITE_NAME [-case $CASE_NAME]</code></p>
-%%%
script_start() ->
process_flag(trap_exit, true),
Init = init:get_arguments(),
@@ -118,13 +105,13 @@ script_start() ->
%% used for purpose of testing the run_test interface
io:format(user, "~n-------------------- START ARGS "
"--------------------~n", []),
- io:format(user, "--- Init args:~n~p~n", [FlagFilter(Init)]),
- io:format(user, "--- CT args:~n~p~n", [FlagFilter(CtArgs)]),
+ io:format(user, "--- Init args:~n~tp~n", [FlagFilter(Init)]),
+ io:format(user, "--- CT args:~n~tp~n", [FlagFilter(CtArgs)]),
EnvArgs = opts2args(EnvStartOpts),
- io:format(user, "--- Env opts -> args:~n~p~n =>~n~p~n",
+ io:format(user, "--- Env opts -> args:~n~tp~n =>~n~tp~n",
[EnvStartOpts,EnvArgs]),
Merged = merge_arguments(CtArgs ++ EnvArgs),
- io:format(user, "--- Merged args:~n~p~n", [FlagFilter(Merged)]),
+ io:format(user, "--- Merged args:~n~tp~n", [FlagFilter(Merged)]),
io:format(user, "-----------------------------------"
"-----------------~n~n", []),
Merged;
@@ -144,7 +131,7 @@ script_start(Args) ->
CTVsn =
case filename:basename(code:lib_dir(common_test)) of
CTBase when is_list(CTBase) ->
- case string:tokens(CTBase, "-") of
+ case string:lexemes(CTBase, "-") of
["common_test",Vsn] -> " v"++Vsn;
_ -> ""
end
@@ -157,18 +144,18 @@ script_start(Args) ->
{'EXIT',Pid,Reason} ->
case Reason of
{user_error,What} ->
- io:format("\nTest run failed!\nReason: ~p\n\n\n",
+ io:format("\nTest run failed!\nReason: ~tp\n\n\n",
[What]),
finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args);
_ ->
io:format("Test run crashed! "
"This could be an internal error "
"- please report!\n\n"
- "~p\n\n\n", [Reason]),
+ "~tp\n\n\n", [Reason]),
finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args)
end;
{Pid,{error,Reason}} ->
- io:format("\nTest run failed! Reason:\n~p\n\n\n",[Reason]),
+ io:format("\nTest run failed! Reason:\n~tp\n\n\n",[Reason]),
finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args);
{Pid,Result} ->
io:nl(),
@@ -216,7 +203,7 @@ analyze_test_result([], _) ->
analyze_test_result(interactive_mode, _) ->
interactive_mode;
analyze_test_result(Unknown, _) ->
- io:format("\nTest run failed! Reason:\n~p\n\n\n",[Unknown]),
+ io:format("\nTest run failed! Reason:\n~tp\n\n\n",[Unknown]),
?EXIT_STATUS_TEST_RUN_FAILED.
finish(Tracing, ExitStatus, Args) ->
@@ -247,6 +234,8 @@ finish(Tracing, ExitStatus, Args) ->
end.
script_start1(Parent, Args) ->
+ %% tag this process
+ ct_util:mark_process(),
%% read general start flags
Label = get_start_opt(label, fun([Lbl]) -> Lbl end, Args),
Profile = get_start_opt(profile, fun([Prof]) -> Prof end, Args),
@@ -261,11 +250,11 @@ script_start1(Parent, Args) ->
[], Args),
Verbosity = verbosity_args2opts(Args),
MultTT = get_start_opt(multiply_timetraps,
- fun([MT]) -> list_to_integer(MT) end, 1, Args),
+ fun([MT]) -> list_to_integer(MT) end, Args),
ScaleTT = get_start_opt(scale_timetraps,
fun([CT]) -> list_to_atom(CT);
([]) -> true
- end, false, Args),
+ end, Args),
CreatePrivDir = get_start_opt(create_priv_dir,
fun([PD]) -> list_to_atom(PD);
([]) -> auto_per_tc
@@ -312,7 +301,7 @@ script_start1(Parent, Args) ->
{undefined,InclDirs};
CtInclPath ->
AllInclDirs =
- string:tokens(CtInclPath,[$:,$ ,$,]) ++ InclDirs,
+ string:lexemes(CtInclPath,[$:,$ ,$,]) ++ InclDirs,
application:set_env(common_test, include, AllInclDirs),
{undefined,AllInclDirs}
end;
@@ -344,6 +333,15 @@ script_start1(Parent, Args) ->
application:set_env(common_test, basic_html, true),
true
end,
+ %% esc_chars - used by ct_logs
+ EscChars = case proplists:get_value(no_esc_chars, Args) of
+ undefined ->
+ application:set_env(common_test, esc_chars, true),
+ undefined;
+ _ ->
+ application:set_env(common_test, esc_chars, false),
+ false
+ end,
%% disable_log_cache - used by ct_logs
case proplists:get_value(disable_log_cache, Args) of
undefined ->
@@ -351,12 +349,19 @@ script_start1(Parent, Args) ->
_ ->
application:set_env(common_test, disable_log_cache, true)
end,
+ %% log_cleanup - used by ct_logs
+ KeepLogs = get_start_opt(keep_logs,
+ fun ct_logs:parse_keep_logs/1,
+ all,
+ Args),
+ application:set_env(common_test, keep_logs, KeepLogs),
Opts = #opts{label = Label, profile = Profile,
vts = Vts, shell = Shell,
cover = Cover, cover_stop = CoverStop,
logdir = LogDir, logopts = LogOpts,
basic_html = BasicHtml,
+ esc_chars = EscChars,
verbosity = Verbosity,
event_handlers = EvHandlers,
ct_hooks = CTHooks,
@@ -387,21 +392,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
@@ -417,13 +422,15 @@ script_start2(Opts = #opts{vts = undefined,
Specs1 = get_start_opt(join_specs, [Specs], Specs, Args),
%% using testspec as input for test
Relaxed = get_start_opt(allow_user_terms, true, false, Args),
- case catch ct_testspec:collect_tests_from_file(Specs1, Relaxed) of
- {E,Reason} when E == error ; E == 'EXIT' ->
- StackTrace = erlang:get_stacktrace(),
- {error,{invalid_testspec,{Reason,StackTrace}}};
- TestSpecData ->
+ try ct_testspec:collect_tests_from_file(Specs1, Relaxed) of
+ TestSpecData ->
execute_all_specs(TestSpecData, Opts, Args, [])
- end;
+ catch
+ throw:{error,Reason}:StackTrace ->
+ {error,{invalid_testspec,{Reason,StackTrace}}};
+ _:Reason:StackTrace ->
+ {error,{invalid_testspec,{Reason,StackTrace}}}
+ end;
[] ->
{error,no_testspec_specified};
_ -> % no testspec used
@@ -492,8 +499,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.
@@ -582,14 +592,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,
@@ -683,8 +705,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.
@@ -709,7 +733,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),
@@ -720,14 +744,14 @@ script_start4(#opts{label = Label, profile = Profile,
if Config == [] ->
ok;
true ->
- io:format("\nInstalling: ~p\n\n", [Config])
+ io:format("\nInstalling: ~tp\n\n", [Config])
end,
case install([{config,Config},{event_handler,EvHandlers},
{ct_hooks, CTHooks},
{enable_builtin_hooks,EnableBuiltinHooks}]) of
ok ->
- ct_util:start(interactive, LogDir,
- add_verbosity_defaults(Verbosity)),
+ _ = ct_util:start(interactive, LogDir,
+ add_verbosity_defaults(Verbosity)),
ct_util:set_testdata({logopts, LogOpts}),
log_ts_names(Specs),
io:nl(),
@@ -759,90 +783,89 @@ script_start4(#opts{shell = true, cover = Cover}, _) ->
script_start4(Opts = #opts{tests = Tests}, Args) ->
do_run(Tests, [], Opts, Args).
-%%%-----------------------------------------------------------------
-%%% @spec script_usage() -> ok
-%%% @doc Print usage information for <code>ct_run</code>.
script_usage() ->
- io:format("\n\nUsage:\n\n"),
+ io:format("\nUsage:\n\n"),
io:format("Run tests from command line:\n\n"
- "\tct_run [-dir TestDir1 TestDir2 .. TestDirN] |"
- "\n\t[[-dir TestDir] -suite Suite1 Suite2 .. SuiteN"
- "\n\t [[-group Groups1 Groups2 .. GroupsN] [-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[-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[-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[-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\n").
+ "\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
-%%% @equiv ct:install/1
install(Opts) ->
install(Opts, ".").
@@ -864,11 +887,10 @@ install(Opts, LogDir) ->
case whereis(ct_util_server) of
undefined ->
VarFile = variables_file_name(LogDir),
- case file:open(VarFile, [write]) of
+ case file:open(VarFile, [write, {encoding,utf8}]) of
{ok,Fd} ->
- [io:format(Fd, "~p.\n", [Opt]) || Opt <- ConfOpts ],
- file:close(Fd),
- ok;
+ _ = [io:format(Fd, "~tp.\n", [Opt]) || Opt <- ConfOpts],
+ ok = file:close(Fd);
{error,Reason} ->
io:format("CT failed to install configuration data. Please "
"verify that the log directory exists and that "
@@ -887,20 +909,11 @@ install(Opts, LogDir) ->
variables_file_name(Dir) ->
filename:join(Dir, "variables-"++atom_to_list(node())).
-%%%-----------------------------------------------------------------
-%%% @spec run_test(Opts) -> Result
-%%% Opts = [tuple()]
-%%% Result = [TestResult] | {error,Reason}
-%%%
-%%% @doc Start tests from the erlang shell or from an erlang program.
-%%% @equiv ct:run_test/1
-%%%-----------------------------------------------------------------
-
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}} ->
@@ -909,6 +922,14 @@ run_test(StartOpts) when is_list(StartOpts) ->
Other
end.
+-spec run_test1_fun(_) -> fun(() -> no_return()).
+
+run_test1_fun(StartOpts) ->
+ fun() ->
+ ct_util:mark_process(),
+ run_test1(StartOpts)
+ end.
+
run_test1(StartOpts) when is_list(StartOpts) ->
case proplists:get_value(refresh_logs, StartOpts) of
undefined ->
@@ -920,7 +941,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
@@ -931,7 +952,13 @@ run_test1(StartOpts) when is_list(StartOpts) ->
stop_trace(Tracing),
exit(Res);
RefreshDir ->
- refresh_logs(?abs(RefreshDir)),
+ %% log_cleanup - used by ct_logs
+ KeepLogs = get_start_opt(keep_logs,
+ fun ct_logs:parse_keep_logs/1,
+ all,
+ StartOpts),
+ application:set_env(common_test, keep_logs, KeepLogs),
+ ok = refresh_logs(?abs(RefreshDir)),
exit(done)
end.
@@ -1016,8 +1043,8 @@ run_test2(StartOpts) ->
CoverStop = get_start_opt(cover_stop, value, StartOpts),
%% timetrap manipulation
- MultiplyTT = get_start_opt(multiply_timetraps, value, 1, StartOpts),
- ScaleTT = get_start_opt(scale_timetraps, value, false, StartOpts),
+ MultiplyTT = get_start_opt(multiply_timetraps, value, StartOpts),
+ ScaleTT = get_start_opt(scale_timetraps, value, StartOpts),
%% create unique priv dir names
CreatePrivDir = get_start_opt(create_priv_dir, value, StartOpts),
@@ -1041,7 +1068,7 @@ run_test2(StartOpts) ->
application:set_env(common_test, include, InclDirs),
{undefined,InclDirs};
CtInclPath ->
- InclDirs1 = string:tokens(CtInclPath, [$:,$ ,$,]),
+ InclDirs1 = string:lexemes(CtInclPath, [$:,$ ,$,]),
AllInclDirs = InclDirs1++InclDirs,
application:set_env(common_test, include, AllInclDirs),
{undefined,AllInclDirs}
@@ -1075,13 +1102,29 @@ run_test2(StartOpts) ->
application:set_env(common_test, basic_html, BasicHtmlBool),
BasicHtmlBool
end,
-
+ %% esc_chars - used by ct_logs
+ EscChars =
+ case proplists:get_value(esc_chars, StartOpts) of
+ undefined ->
+ application:set_env(common_test, esc_chars, true),
+ undefined;
+ EscCharsBool ->
+ application:set_env(common_test, esc_chars, EscCharsBool),
+ EscCharsBool
+ end,
+ %% disable_log_cache - used by ct_logs
case proplists:get_value(disable_log_cache, StartOpts) of
undefined ->
application:set_env(common_test, disable_log_cache, false);
DisableCacheBool ->
application:set_env(common_test, disable_log_cache, DisableCacheBool)
end,
+ %% log_cleanup - used by ct_logs
+ KeepLogs = get_start_opt(keep_logs,
+ fun ct_logs:parse_keep_logs/1,
+ all,
+ StartOpts),
+ application:set_env(common_test, keep_logs, KeepLogs),
%% stepped execution
Step = get_start_opt(step, value, StartOpts),
@@ -1090,6 +1133,7 @@ run_test2(StartOpts) ->
cover = Cover, cover_stop = CoverStop,
step = Step, logdir = LogDir,
logopts = LogOpts, basic_html = BasicHtml,
+ esc_chars = EscChars,
config = CfgFiles,
verbosity = Verbosity,
event_handlers = EvHandlers,
@@ -1110,7 +1154,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)
@@ -1118,11 +1162,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];
@@ -1130,12 +1174,14 @@ run_spec_file(Relaxed,
end,
AbsSpecs = lists:map(fun(SF) -> ?abs(SF) end, Specs1),
AbsSpecs1 = get_start_opt(join_specs, [AbsSpecs], AbsSpecs, StartOpts),
- case catch ct_testspec:collect_tests_from_file(AbsSpecs1, Relaxed) of
- {Error,CTReason} when Error == error ; Error == 'EXIT' ->
- StackTrace = erlang:get_stacktrace(),
- exit({error,{invalid_testspec,{CTReason,StackTrace}}});
+ try ct_testspec:collect_tests_from_file(AbsSpecs1, Relaxed) of
TestSpecData ->
run_all_specs(TestSpecData, Opts, StartOpts, [])
+ catch
+ throw:{error,CTReason}:StackTrace ->
+ exit({error,{invalid_testspec,{CTReason,StackTrace}}});
+ _:CTReason:StackTrace ->
+ exit({error,{invalid_testspec,{CTReason,StackTrace}}})
end.
run_all_specs([], _, _, TotResult) ->
@@ -1158,10 +1204,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
@@ -1340,22 +1388,16 @@ 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}}})
end.
-%%%-----------------------------------------------------------------
-%%% @spec run_testspec(TestSpec) -> Result
-%%% TestSpec = [term()]
-%%%
-%%% @doc Run test specified by <code>TestSpec</code>. The terms are
-%%% the same as those used in test specification files.
-%%% @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}} ->
@@ -1364,12 +1406,20 @@ run_testspec(TestSpec) ->
Other
end.
+-spec run_testspec1_fun(_) -> fun(() -> no_return()).
+
+run_testspec1_fun(TestSpec) ->
+ fun() ->
+ ct_util:mark_process(),
+ run_testspec1(TestSpec)
+ end.
+
run_testspec1(TestSpec) ->
{ok,Cwd} = file:get_cwd(),
io:format("~nCommon Test starting (cwd is ~ts)~n~n", [Cwd]),
case catch run_testspec2(TestSpec) of
{'EXIT',Reason} ->
- file:set_cwd(Cwd),
+ ok = file:set_cwd(Cwd),
exit({error,Reason});
Result ->
exit(Result)
@@ -1397,7 +1447,7 @@ run_testspec2(TestSpec) ->
false ->
Opts#opts.include;
CtInclPath ->
- EnvInclude = string:tokens(CtInclPath, [$:,$ ,$,]),
+ EnvInclude = string:lexemes(CtInclPath, [$:,$ ,$,]),
EnvInclude++Opts#opts.include
end,
application:set_env(common_test, include, AllInclude),
@@ -1406,7 +1456,7 @@ run_testspec2(TestSpec) ->
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()),
@@ -1421,6 +1471,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,
@@ -1448,6 +1499,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 -> [];
@@ -1474,6 +1526,7 @@ get_data_for_node(#testspec{label = Labels,
logdir = LogDir,
logopts = LogOpts,
basic_html = BasicHtml,
+ esc_chars = EscChars,
stylesheet = Stylesheet,
verbosity = Verbosity,
silent_connections = SilentConns,
@@ -1498,15 +1551,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
@@ -1542,26 +1595,29 @@ delistify([E]) -> E;
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};
@@ -1627,11 +1683,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)) ->
@@ -1713,6 +1773,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
[] ->
@@ -1721,13 +1784,13 @@ compile_and_run(Tests, Skip, Opts, Args) ->
case lists:member(all, Conns) of
true ->
Conns1 = ct_util:override_silence_all_connections(),
- ct_logs:log("Silent connections", "~p", [Conns1]);
+ ct_logs:log("Silent connections", "~tp", [Conns1]);
false ->
ct_util:override_silence_connections(Conns),
- ct_logs:log("Silent connections", "~p", [Conns])
+ ct_logs:log("Silent connections", "~tp", [Conns])
end
end,
- log_ts_names(Opts#opts.testspecs),
+ log_ts_names(Opts#opts.testspec_files),
TestSuites = suite_tuples(Tests),
{_TestSuites1,SuiteMakeErrors,AllMakeErrors} =
@@ -1750,7 +1813,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}
@@ -1788,10 +1862,12 @@ possibly_spawn(true, Tests, Skip, Opts) ->
CTUtilSrv = whereis(ct_util_server),
Supervisor =
fun() ->
+ ct_util:mark_process(),
process_flag(trap_exit, true),
link(CTUtilSrv),
TestRun =
fun() ->
+ ct_util:mark_process(),
TestResult = (catch do_run_test(Tests, Skip, Opts)),
case TestResult of
{EType,_} = Error when EType == user_error;
@@ -1806,7 +1882,7 @@ possibly_spawn(true, Tests, Skip, Opts) ->
TestRunPid = spawn_link(TestRun),
receive
{'EXIT',TestRunPid,{ok,TestResult}} ->
- io:format(user, "~nCommon Test returned ~p~n~n",
+ io:format(user, "~nCommon Test returned ~tp~n~n",
[TestResult]);
{'EXIT',TestRunPid,Error} ->
exit(Error)
@@ -1825,7 +1901,7 @@ auto_compile(TestSuites) ->
case application:get_env(common_test, include) of
{ok,UserInclDirs} when length(UserInclDirs) > 0 ->
io:format("Including the following directories:~n"),
- [begin io:format("~p~n",[UserInclDir]), {i,UserInclDir} end ||
+ [begin io:format("~tp~n",[UserInclDir]), {i,UserInclDir} end ||
UserInclDir <- UserInclDirs];
_ ->
[]
@@ -1833,7 +1909,8 @@ auto_compile(TestSuites) ->
SuiteMakeErrors =
lists:flatmap(fun({TestDir,Suite} = TS) ->
case run_make(suites, TestDir,
- Suite, UserInclude) of
+ Suite, UserInclude,
+ [nowarn_export_all]) of
{error,{make_failed,Bad}} ->
[{TS,Bad}];
{error,_} ->
@@ -1851,7 +1928,7 @@ auto_compile(TestSuites) ->
case lists:member(Dir, Done) of
false ->
Failed1 =
- case run_make(helpmods, Dir, Suite, UserInclude) of
+ case run_make(helpmods, Dir, Suite, UserInclude, []) of
{error,{make_failed,BadMods}} ->
[{{Dir,all},BadMods}|Failed];
{error,_} ->
@@ -1935,7 +2012,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) ->
@@ -1944,16 +2021,9 @@ get_bad_suites([], BadSuites) ->
BadSuites.
-
-%%%-----------------------------------------------------------------
-%%% @hidden
-%%% @equiv ct:step/3
step(TestDir, Suite, Case) ->
step(TestDir, Suite, Case, []).
-%%%-----------------------------------------------------------------
-%%% @hidden
-%%% @equiv ct:step/4
step(TestDir, Suite, Case, Opts) when is_list(TestDir),
is_atom(Suite), is_atom(Case),
Suite =/= all, Case =/= all ->
@@ -1976,22 +2046,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);
@@ -2004,7 +2059,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);
@@ -2016,7 +2071,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 =
@@ -2028,6 +2083,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,
@@ -2067,10 +2129,12 @@ final_skip([], Final) ->
continue([], _) ->
true;
-continue(_MakeErrors, AbortIfMissingSuites) ->
+continue(_MakeErrors, true) ->
+ false;
+continue(_MakeErrors, _AbortIfMissingSuites) ->
io:nl(),
- OldGl = group_leader(),
- case set_group_leader_same_as_shell() of
+ OldGL = group_leader(),
+ case set_group_leader_same_as_shell(OldGL) of
true ->
S = self(),
io:format("Failed to compile or locate one "
@@ -2086,7 +2150,7 @@ continue(_MakeErrors, AbortIfMissingSuites) ->
S ! false
end
end),
- group_leader(OldGl, self()),
+ group_leader(OldGL, self()),
receive R when R==true; R==false ->
R
after 15000 ->
@@ -2095,11 +2159,12 @@ continue(_MakeErrors, AbortIfMissingSuites) ->
true
end;
false -> % no shell process to use
- not AbortIfMissingSuites
+ true
end.
-set_group_leader_same_as_shell() ->
- %%! Locate the shell process... UGLY!!!
+set_group_leader_same_as_shell(OldGL) ->
+ %% find the group leader process on the node in a dirty fashion
+ %% (check initial function call and look in the process dictionary)
GS2or3 = fun(P) ->
case process_info(P,initial_call) of
{initial_call,{group,server,X}} when X == 2 ; X == 3 ->
@@ -2112,7 +2177,10 @@ set_group_leader_same_as_shell() ->
true == lists:keymember(shell,1,
element(2,process_info(P,dictionary)))] of
[GL|_] ->
- group_leader(GL, self());
+ %% check if started from remote node (skip interaction)
+ if node(OldGL) /= node(GL) -> false;
+ true -> group_leader(GL, self())
+ end;
[] ->
false
end.
@@ -2147,11 +2215,19 @@ do_run_test(Tests, Skip, Opts0) ->
%% test_server needs to know the include path too
InclPath = case application:get_env(common_test, include) of
{ok,Incls} -> Incls;
- _ -> []
+ _ -> []
end,
application:set_env(test_server, include, InclPath),
- test_server_ctrl:start_link(local),
+ %% copy the escape characters setting to test_server
+ EscChars =
+ case application:get_env(common_test, esc_chars) of
+ {ok,ECBool} -> ECBool;
+ _ -> true
+ end,
+ application:set_env(test_server, esc_chars, EscChars),
+
+ {ok, _} = test_server_ctrl:start_link(local),
%% let test_server expand the test tuples and count no of cases
{Suites,NoOfCases} = count_test_cases(Tests, Skip),
@@ -2160,7 +2236,7 @@ do_run_test(Tests, Skip, Opts0) ->
NoOfSuites = length(Suites1),
ct_util:warn_duplicates(Suites1),
{ok,Cwd} = file:get_cwd(),
- io:format("~nCWD set to: ~p~n", [Cwd]),
+ io:format("~nCWD set to: ~tp~n", [Cwd]),
if NoOfCases == unknown ->
io:format("~nTEST INFO: ~w test(s), ~w suite(s)~n~n",
[NoOfTests,NoOfSuites]),
@@ -2182,8 +2258,19 @@ do_run_test(Tests, Skip, Opts0) ->
_Lower ->
ok
end,
- test_server_ctrl:multiply_timetraps(Opts0#opts.multiply_timetraps),
- test_server_ctrl:scale_timetraps(Opts0#opts.scale_timetraps),
+
+ case Opts0#opts.multiply_timetraps of
+ undefined -> MultTT = 1;
+ MultTT -> MultTT
+ end,
+ case Opts0#opts.scale_timetraps of
+ undefined -> ScaleTT = false;
+ ScaleTT -> ScaleTT
+ end,
+ ct_logs:log("TEST INFO","Timetrap time multiplier = ~w~n"
+ "Timetrap scaling enabled = ~w", [MultTT,ScaleTT]),
+ test_server_ctrl:multiply_timetraps(MultTT),
+ test_server_ctrl:scale_timetraps(ScaleTT),
test_server_ctrl:create_priv_dir(choose_val(
Opts0#opts.create_priv_dir,
@@ -2212,14 +2299,14 @@ do_run_test(Tests, Skip, Opts0) ->
lists:foreach(fun(Suite) ->
maybe_cleanup_interpret(Suite, Opts#opts.step)
end, CleanUp),
- [code:del_path(Dir) || Dir <- AddedToPath],
+ _ = [code:del_path(Dir) || Dir <- AddedToPath],
%% If a severe error has occurred in the test_server,
%% we will generate an exception here.
case ct_util:get_testdata(severe_error) of
undefined -> ok;
SevereError ->
- ct_logs:log("SEVERE ERROR", "~p\n", [SevereError]),
+ ct_logs:log("SEVERE ERROR", "~tp\n", [SevereError]),
exit(SevereError)
end,
@@ -2290,7 +2377,7 @@ start_cover(Opts=#opts{coverspec=CovData,cover_stop=CovStop},LogDir) ->
if (CovNodes /= []) and (CovNodes /= undefined) ->
ct_logs:log("COVER INFO",
"Nodes included in cover "
- "session: ~w",
+ "session: ~tw",
[CovNodes]),
cover:start(CovNodes);
true ->
@@ -2304,7 +2391,7 @@ start_cover(Opts=#opts{coverspec=CovData,cover_stop=CovStop},LogDir) ->
{error,Reason} ->
ct_logs:log("COVER INFO",
"Importing cover data from: ~ts fails! "
- "Reason: ~p", [Imp,Reason])
+ "Reason: ~tp", [Imp,Reason])
end
end, CovImport),
{TsCoverInfo,Opts}.
@@ -2339,7 +2426,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
@@ -2583,12 +2670,12 @@ get_name(Dir) ->
run_make(TestDir, Mod, UserInclude) ->
- run_make(suites, TestDir, Mod, UserInclude).
+ run_make(suites, TestDir, Mod, UserInclude, [nowarn_export_all]).
-run_make(Targets, TestDir0, Mod, UserInclude) when is_list(Mod) ->
- run_make(Targets, TestDir0, list_to_atom(Mod), UserInclude);
+run_make(Targets, TestDir0, Mod, UserInclude, COpts) when is_list(Mod) ->
+ run_make(Targets, TestDir0, list_to_atom(Mod), UserInclude, COpts);
-run_make(Targets, TestDir0, Mod, UserInclude) ->
+run_make(Targets, TestDir0, Mod, UserInclude, COpts) ->
case locate_test_dir(TestDir0, Mod) of
{ok,TestDir} ->
%% send a start_make notification which may suspend
@@ -2599,13 +2686,11 @@ 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],
+ debug_info] ++ COpts,
Result =
if Mod == all ; Targets == helpmods ->
case (catch ct_make:all([noexec|ErlFlags])) of
@@ -2640,7 +2725,7 @@ run_make(Targets, TestDir0, Mod, UserInclude) ->
{up_to_date,_} ->
ok;
{'EXIT',Reason} ->
- io:format("{error,{make_crashed,~p}\n", [Reason]),
+ io:format("{error,{make_crashed,~tp}\n", [Reason]),
{error,{make_crashed,TestDir,Reason}};
{error,ModInfo} ->
io:format("{error,make_failed}\n", []),
@@ -2649,7 +2734,7 @@ run_make(Targets, TestDir0, Mod, UserInclude) ->
{error,{make_failed,Bad}}
end;
{error,_} ->
- io:format("{error,{invalid_directory,~p}}\n", [TestDir0]),
+ io:format("{error,{invalid_directory,~tp}}\n", [TestDir0]),
{error,{invalid_directory,TestDir0}}
end.
@@ -2695,14 +2780,14 @@ maybe_interpret1(Suite, Cases, StepOpts) when is_list(Cases) ->
maybe_interpret2(Suite, Cases, StepOpts) ->
set_break_on_config(Suite, StepOpts),
- [begin try i:ib(Suite, Case, 1) of
+ _ = [begin try i:ib(Suite, Case, 1) of
_ -> ok
catch
_:_Error ->
- io:format(user, "Invalid breakpoint: ~w:~w/1~n",
+ io:format(user, "Invalid breakpoint: ~w:~tw/1~n",
[Suite,Case])
end
- end || Case <- Cases, is_atom(Case)],
+ end || Case <- Cases, is_atom(Case)],
test_server_ctrl:multiply_timetraps(infinity),
WinOp = case lists:member(keep_inactive, ensure_atom(StepOpts)) of
true -> no_kill;
@@ -2721,12 +2806,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,7 +2915,7 @@ ct_hooks_args2opts([],Acc) ->
parse_cth_args(String) ->
try
- true = io_lib:printable_list(String),
+ true = io_lib:printable_unicode_list(String),
{ok,Toks,_} = erl_scan:string(String++"."),
{ok, Args} = erl_parse:parse_term(Toks),
Args
@@ -2903,31 +2988,31 @@ add_verbosity_defaults(VLvls) ->
%% relative dirs "post run_test erl_args" is not kept!
rel_to_abs(CtArgs) ->
{PA,PZ} = get_pa_pz(CtArgs, [], []),
- [begin
+ _ = [begin
Dir = rm_trailing_slash(D),
Abs = make_abs(Dir),
- if Dir /= Abs ->
- code:del_path(Dir),
- code:del_path(Abs),
- io:format(user, "Converting ~p to ~p and re-inserting "
+ _ = if Dir /= Abs ->
+ _ = code:del_path(Dir),
+ _ = code:del_path(Abs),
+ io:format(user, "Converting ~tp to ~tp and re-inserting "
"with add_pathz/1~n",
[Dir, Abs]);
true ->
- code:del_path(Dir)
+ _ = code:del_path(Dir)
end,
code:add_pathz(Abs)
end || D <- PZ],
- [begin
+ _ = [begin
Dir = rm_trailing_slash(D),
Abs = make_abs(Dir),
- if Dir /= Abs ->
- code:del_path(Dir),
- code:del_path(Abs),
- io:format(user, "Converting ~p to ~p and re-inserting "
+ _ = if Dir /= Abs ->
+ _ = code:del_path(Dir),
+ _ = code:del_path(Abs),
+ io:format(user, "Converting ~tp to ~tp and re-inserting "
"with add_patha/1~n",
[Dir, Abs]);
true ->
- code:del_path(Dir)
+ _ = code:del_path(Dir)
end,
code:add_patha(Abs)
end || D <- PA],
@@ -2993,7 +3078,7 @@ opts2args(EnvStartOpts) ->
({group,G}) when is_atom(G) ->
[{group,[atom_to_list(G)]}];
({group,Gs}) when is_list(Gs) ->
- LOfGStrs = [lists:flatten(io_lib:format("~w",[G])) ||
+ LOfGStrs = [lists:flatten(io_lib:format("~tw",[G])) ||
G <- Gs],
[{group,LOfGStrs}];
({testcase,Case}) when is_atom(Case) ->
@@ -3036,15 +3121,19 @@ opts2args(EnvStartOpts) ->
[{basic_html,[]}];
({basic_html,false}) ->
[];
+ ({esc_chars,false}) ->
+ [{no_esc_chars,[]}];
+ ({esc_chars,true}) ->
+ [];
({event_handler,EH}) when is_atom(EH) ->
[{event_handler,[atom_to_list(EH)]}];
({event_handler,EHs}) when is_list(EHs) ->
[{event_handler,[atom_to_list(EH) || EH <- EHs]}];
({event_handler,{EH,Arg}}) when is_atom(EH) ->
- ArgStr = lists:flatten(io_lib:format("~p", [Arg])),
+ ArgStr = lists:flatten(io_lib:format("~tp", [Arg])),
[{event_handler_init,[atom_to_list(EH),ArgStr]}];
({event_handler,{EHs,Arg}}) when is_list(EHs) ->
- ArgStr = lists:flatten(io_lib:format("~p", [Arg])),
+ ArgStr = lists:flatten(io_lib:format("~tp", [Arg])),
Strs = lists:flatmap(fun(EH) ->
[atom_to_list(EH),
ArgStr,"and"]
@@ -3075,25 +3164,25 @@ opts2args(EnvStartOpts) ->
({ct_hooks,[]}) ->
[];
({ct_hooks,CTHs}) when is_list(CTHs) ->
- io:format(user,"ct_hooks: ~p",[CTHs]),
+ io:format(user,"ct_hooks: ~tp",[CTHs]),
Strs = lists:flatmap(
fun({CTH,Arg,Prio}) ->
[atom_to_list(CTH),
lists:flatten(
- io_lib:format("~p",[Arg])),
+ io_lib:format("~tp",[Arg])),
lists:flatten(
- io_lib:format("~p",[Prio])),
+ io_lib:format("~tp",[Prio])),
"and"];
({CTH,Arg}) ->
[atom_to_list(CTH),
lists:flatten(
- io_lib:format("~p",[Arg])),
+ io_lib:format("~tp",[Arg])),
"and"];
(CTH) when is_atom(CTH) ->
[atom_to_list(CTH),"and"]
end,CTHs),
[_LastAnd|StrsR] = lists:reverse(Strs),
- io:format(user,"return: ~p",[lists:reverse(StrsR)]),
+ io:format(user,"return: ~tp",[lists:reverse(StrsR)]),
[{ct_hooks,lists:reverse(StrsR)}];
({Opt,As=[A|_]}) when is_atom(A) ->
[{Opt,[atom_to_list(Atom) || Atom <- As]}];
@@ -3175,7 +3264,7 @@ start_trace(Args) ->
ok ->
true;
{_,Error} ->
- io:format("Warning! Tracing not started. Reason: ~p~n~n",
+ io:format("Warning! Tracing not started. Reason: ~tp~n~n",
[Error]),
false
end;
diff --git a/lib/common_test/src/ct_slave.erl b/lib/common_test/src/ct_slave.erl
index 872c39de04..dde33440be 100644
--- a/lib/common_test/src/ct_slave.erl
+++ b/lib/common_test/src/ct_slave.erl
@@ -1,28 +1,22 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2010-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2010-2018. 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 Framework functions for starting and stopping nodes for
-%%% Large Scale Testing.
-%%%
-%%% <p>This module exports functions which are used by the Common Test Master
-%%% to start and stop "slave" nodes. It is the default callback module for the
-%%% <code>{init, node_start}</code> term of the Test Specification.</p>
-
%%----------------------------------------------------------------------
%% File : ct_slave.erl
%% Description : CT module for starting nodes for large-scale testing.
@@ -37,44 +31,12 @@
-record(options, {username, password, boot_timeout, init_timeout,
startup_timeout, startup_functions, monitor_master,
- kill_if_fail, erl_flags, env}).
-
-%%%-----------------------------------------------------------------
-%%% @spec start(Node) -> Result
-%%% Node = atom()
-%%% Result = {ok, NodeName} |
-%%% {error, Reason, NodeName}
-%%% Reason = already_started |
-%%% started_not_connected |
-%%% boot_timeout |
-%%% init_timeout |
-%%% startup_timeout |
-%%% not_alive
-%%% NodeName = atom()
-%%% @doc Starts an Erlang node with name <code>Node</code> on the local host.
-%%% @see start/3
+ kill_if_fail, erl_flags, env, ssh_port, ssh_opts,
+ stop_timeout}).
+
start(Node) ->
start(gethostname(), Node).
-%%%-----------------------------------------------------------------
-%%% @spec start(HostOrNode, NodeOrOpts) -> Result
-%%% HostOrNode = atom()
-%%% NodeOrOpts = atom() | list()
-%%% Result = {ok, NodeName} |
-%%% {error, Reason, NodeName}
-%%% Reason = already_started |
-%%% started_not_connected |
-%%% boot_timeout |
-%%% init_timeout |
-%%% startup_timeout |
-%%% not_alive
-%%% NodeName = atom()
-%%% @doc Starts an Erlang node with default options on a specified
-%%% host, or on the local host with specified options. That is,
-%%% the call is interpreted as <code>start(Host, Node)</code> when the
-%%% second argument is atom-valued and <code>start(Node, Opts)</code>
-%%% when it's list-valued.
-%%% @see start/3
start(_HostOrNode = Node, _NodeOrOpts = Opts) %% match to satiate edoc
when is_list(Opts) ->
start(gethostname(), Node, Opts);
@@ -82,104 +44,6 @@ start(_HostOrNode = Node, _NodeOrOpts = Opts) %% match to satiate edoc
start(Host, Node) ->
start(Host, Node, []).
-%%%-----------------------------------------------------------------
-%%% @spec start(Host, Node, Opts) -> Result
-%%% Node = atom()
-%%% Host = atom()
-%%% Opts = [OptTuples]
-%%% OptTuples = {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, ErlangFlags} |
-%%% {env, [{EnvVar,Value}]}
-%%% Username = string()
-%%% Password = string()
-%%% BootTimeout = integer()
-%%% InitTimeout = integer()
-%%% StartupTimeout = integer()
-%%% StartupFunctions = [StartupFunctionSpec]
-%%% StartupFunctionSpec = {Module, Function, Arguments}
-%%% Module = atom()
-%%% Function = atom()
-%%% Arguments = [term]
-%%% Monitor = bool()
-%%% KillIfFail = bool()
-%%% ErlangFlags = string()
-%%% EnvVar = string()
-%%% Value = string()
-%%% Result = {ok, NodeName} |
-%%% {error, Reason, NodeName}
-%%% Reason = already_started |
-%%% started_not_connected |
-%%% boot_timeout |
-%%% init_timeout |
-%%% startup_timeout |
-%%% not_alive
-%%% NodeName = atom()
-%%% @doc Starts an Erlang node with name <code>Node</code> on host
-%%% <code>Host</code> as specified by the combination of options in
-%%% <code>Opts</code>.
-%%%
-%%% <p>Options <code>Username</code> and <code>Password</code> will be used
-%%% to log in onto the remote host <code>Host</code>.
-%%% Username, if omitted, defaults to the current user name,
-%%% and password is empty by default.</p>
-%%%
-%%% <p>A list of functions specified in the <code>Startup</code> option will be
-%%% 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:
-%%% <list>
-%%% <item>
-%%% <code>BootTimeout</code> - time to start the Erlang node, in seconds.
-%%% Defaults to 3 seconds. If node does not become pingable within this time,
-%%% the result <code>{error, boot_timeout, NodeName}</code> is returned;
-%%% </item>
-%%% <item>
-%%% <code>InitTimeout</code> - time to wait for the node until it calls the
-%%% internal callback function informing master about successfull startup.
-%%% Defaults to one second.
-%%% In case of timed out message the result
-%%% <code>{error, init_timeout, NodeName}</code> is returned;
-%%% </item>
-%%% <item>
-%%% <code>StartupTimeout</code> - time to wait intil the node finishes to run
-%%% the <code>StartupFunctions</code>. Defaults to one second.
-%%% If this timeout occurs, the result
-%%% <code>{error, startup_timeout, NodeName}</code> is returned.
-%%% </item>
-%%% </list></p>
-%%%
-%%% <p>Option <code>monitor_master</code> specifies, if the slave node should be
-%%% stopped in case of master node stop. Defaults to false.</p>
-%%%
-%%% <p>Option <code>kill_if_fail</code> specifies, if the slave node should be
-%%% killed in case of a timeout during initialization or startup.
-%%% Defaults to true. Note that node also may be still alive it the boot
-%%% timeout occurred, but it will not be killed in this case.</p>
-%%%
-%%% <p>Option <code>erlang_flags</code> specifies, which flags will be added
-%%% to the parameters of the <code>erl</code> executable.</p>
-%%%
-%%% <p>Option <code>env</code> specifies a list of environment variables
-%%% that will extended the environment.</p>
-%%%
-%%% <p>Special return values are:
-%%% <list>
-%%% <item><code>{error, already_started, NodeName}</code> - if the node with
-%%% the given name is already started on a given host;</item>
-%%% <item><code>{error, started_not_connected, NodeName}</code> - if node is
-%%% started, but not connected to the master node.</item>
-%%% <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>
-%%%
start(Host, Node, Opts) ->
ENode = enodename(Host, Node),
case erlang:is_alive() of
@@ -197,36 +61,22 @@ start(Host, Node, Opts) ->
end
end.
-%%% @spec stop(Node) -> Result
-%%% Node = atom()
-%%% Result = {ok, NodeName} |
-%%% {error, Reason, NodeName}
-%%% Reason = not_started |
-%%% not_connected |
-%%% stop_timeout
-
-%%% NodeName = atom()
-%%% @doc Stops the running Erlang node with name <code>Node</code> on
-%%% the localhost.
stop(Node) ->
stop(gethostname(), Node).
-%%% @spec stop(Host, Node) -> Result
-%%% Host = atom()
-%%% Node = atom()
-%%% Result = {ok, NodeName} |
-%%% {error, Reason, NodeName}
-%%% Reason = not_started |
-%%% not_connected |
-%%% stop_timeout
-%%% NodeName = atom()
-%%% @doc Stops the running Erlang node with name <code>Node</code> on
-%%% host <code>Host</code>.
+stop(_HostOrNode = Node, _NodeOrOpts = Opts) %% match to satiate edoc
+ when is_list(Opts) ->
+ stop(gethostname(), Node, Opts);
+
stop(Host, Node) ->
+ stop(Host, Node, []).
+
+stop(Host, Node, Opts) ->
ENode = enodename(Host, Node),
case is_started(ENode) of
{true, connected}->
- do_stop(ENode);
+ OptionsRec = fetch_options(Opts),
+ do_stop(ENode, OptionsRec);
{true, not_connected}->
{error, not_connected, ENode};
false->
@@ -254,31 +104,33 @@ 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, []),
+ StopTimeout = get_option_value(stop_timeout, Options, 5),
#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,
+ stop_timeout=StopTimeout}.
% send a message when slave node is started
-% @hidden
slave_started(ENode, MasterPid) ->
MasterPid ! {node_started, ENode},
ok.
% send a message when slave node has finished startup
-% @hidden
slave_ready(ENode, MasterPid) ->
MasterPid ! {node_ready, ENode},
ok.
% start monitoring of the master node
-% @hidden
monitor_master(MasterNode) ->
spawn(fun() -> monitor_master_int(MasterNode) end).
% code of the masterdeath-waiter process
monitor_master_int(MasterNode) ->
+ ct_util:mark_process(),
erlang:monitor_node(MasterNode, true),
receive
{nodedown, MasterNode}->
@@ -306,13 +158,18 @@ is_started(ENode) ->
% make a Erlang node name from name and hostname
enodename(Host, Node) ->
- list_to_atom(atom_to_list(Node)++"@"++atom_to_list(Host)).
+ case lists:member($@, atom_to_list(Node)) of
+ true ->
+ Node;
+ false ->
+ list_to_atom(atom_to_list(Node)++"@"++atom_to_list(Host))
+ end.
% performs actual start of the "slave" node
do_start(Host, Node, Options) ->
ENode = enodename(Host, Node),
Functions =
- lists:concat([[{ct_slave, slave_started, [ENode, self()]}],
+ lists:append([[{ct_slave, slave_started, [ENode, self()]}],
Options#options.startup_functions,
[{ct_slave, slave_ready, [ENode, self()]}]]),
Functions2 = if
@@ -322,7 +179,7 @@ do_start(Host, Node, Options) ->
Functions
end,
MasterHost = gethostname(),
- if
+ _ = if
MasterHost == Host ->
spawn_local_node(Node, Options);
true->
@@ -356,7 +213,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 +256,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 +275,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,
@@ -463,6 +310,8 @@ wait_for_node_alive(Node, N) ->
% call init:stop on a remote node
do_stop(ENode) ->
+ do_stop(ENode, fetch_options([])).
+do_stop(ENode, Options) ->
{Cover,MainCoverNode} =
case test_server:is_cover() of
true ->
@@ -473,7 +322,8 @@ do_stop(ENode) ->
{false,undefined}
end,
spawn(ENode, init, stop, []),
- case wait_for_node_dead(ENode, 5) of
+ StopTimeout = Options#options.stop_timeout,
+ case wait_for_node_dead(ENode, StopTimeout) of
{ok,ENode} ->
if Cover ->
%% To avoid that cover is started again if a node
diff --git a/lib/common_test/src/ct_snmp.erl b/lib/common_test/src/ct_snmp.erl
index 71038bd4f4..27b74dd04e 100644
--- a/lib/common_test/src/ct_snmp.erl
+++ b/lib/common_test/src/ct_snmp.erl
@@ -1,155 +1,25 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2012. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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
-%%% 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
-%%% of the relevant snmp configuration files may be passed to <code>ct_snmp</code>
-%%% by means of Common Test configuration files.
-%%% For more specialized configuration parameters, it is possible to place a
-%%% "simple snmp configuration file" in the test suite data directory.
-%%% 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.
-%%%
-%%% <p> The following snmp manager and agent parameters are configurable: </p>
-%%%
-%%% <pre>
-%%% {snmp,
-%%% %%% Manager config
-%%% [{start_manager, boolean()} % Optional - default is true
-%%% {users, [{user_name(), [call_back_module(), user_data()]}]}, %% Optional
-%%% {usm_users, [{usm_user_name(), [usm_config()]}]},%% Optional - snmp v3 only
-%%% % managed_agents is optional
-%%% {managed_agents,[{agent_name(), [user_name(), agent_ip(), agent_port(), [agent_config()]]}]},
-%%% {max_msg_size, integer()}, % Optional - default is 484
-%%% {mgr_port, integer()}, % Optional - default is 5000
-%%% {engine _id, string()}, % Optional - default is "mgrEngine"
-%%%
-%%% %%% Agent config
-%%% {start_agent, boolean()}, % Optional - default is false
-%%% {agent_sysname, string()}, % Optional - default is "ct_test"
-%%% {agent_manager_ip, manager_ip()}, % Optional - default is localhost
-%%% {agent_vsns, list()}, % Optional - default is [v2]
-%%% {agent_trap_udp, integer()}, % Optional - default is 5000
-%%% {agent_udp, integer()}, % Optional - default is 4000
-%%% {agent_notify_type, atom()}, % Optional - default is trap
-%%% {agent_sec_type, sec_type()}, % Optional - default is none
-%%% {agent_passwd, string()}, % Optional - default is ""
-%%% {agent_engine_id, string()}, % Optional - default is "agentEngine"
-%%% {agent_max_msg_size, string()},% Optional - default is 484
-%%%
-%%% %% The following parameters represents the snmp configuration files
-%%% %% context.conf, standard.conf, community.conf, vacm.conf,
-%%% %% usm.conf, notify.conf, target_addr.conf and target_params.conf.
-%%% %% Note all values in agent.conf can be altered by the parametes
-%%% %% above. All these configuration files have default values set
-%%% %% up by the snmp application. These values can be overridden by
-%%% %% suppling a list of valid configuration values or a file located
-%%% %% in the test suites data dir that can produce a list
-%%% %% of valid configuration values if you apply file:consult/1 to the
-%%% %% file.
-%%% {agent_contexts, [term()] | {data_dir_file, rel_path()}}, % Optional
-%%% {agent_community, [term()] | {data_dir_file, rel_path()}},% Optional
-%%% {agent_sysinfo, [term()] | {data_dir_file, rel_path()}}, % Optional
-%%% {agent_vacm, [term()] | {data_dir_file, rel_path()}}, % Optional
-%%% {agent_usm, [term()] | {data_dir_file, rel_path()}}, % Optional
-%%% {agent_notify_def, [term()] | {data_dir_file, rel_path()}},% Optional
-%%% {agent_target_address_def, [term()] | {data_dir_file, rel_path()}},% Optional
-%%% {agent_target_param_def, [term()] | {data_dir_file, rel_path()}},% Optional
-%%% ]}.
-%%% </pre>
-%%%
-%%% <p>The <code>MgrAgentConfName</code> parameter in the functions
-%%% should be a name you allocate in your test suite using a
-%%% <code>require</code> statement.
-%%% Example (where <code>MgrAgentConfName = snmp_mgr_agent</code>):</p>
-%%% <pre> suite() -> [{require, snmp_mgr_agent, snmp}].</pre>
-%%% <p>or</p>
-%%% <pre> ct:require(snmp_mgr_agent, snmp).</pre>
-%%%
-%%% <p> Note that Usm users are needed for snmp v3 configuration and are
-%%% not to be confused with users.</p>
-%%%
-%%% <p> Snmp traps, inform and report messages are handled by the
-%%% user callback module. For more information about this see
-%%% the snmp application. </p>
-%%% <p> Note: It is recommended to use the .hrl-files created by the
-%%% Erlang/OTP mib-compiler to define the oids.
-%%% Example for the getting the erlang node name from the erlNodeTable
-%%% in the OTP-MIB:</p>
-%%% <pre>Oid = ?erlNodeEntry ++ [?erlNodeName, 1] </pre>
-%%%
-%%% <p>It is also possible to set values for snmp application configuration
-%%% parameters, such as <code>config</code>, <code>server</code>,
-%%% <code>net_if</code>, etc (see the "Configuring the application" chapter in
-%%% the OTP snmp User's Guide for a list of valid parameters and types). This is
-%%% done by defining a configuration data variable on the following form:</p>
-%%% <pre>
-%%% {snmp_app, [{manager, [snmp_app_manager_params()]},
-%%% {agent, [snmp_app_agent_params()]}]}.</pre>
-%%%
-%%% <p>A name for the data needs to be allocated in the suite using
-%%% <code>require</code> (see example above), and this name passed as
-%%% the <code>SnmpAppConfName</code> argument to <code>start/3</code>.
-%%% <code>ct_snmp</code> specifies default values for some snmp application
-%%% configuration parameters (such as <code>{verbosity,trace}</code> for the
-%%% <code>config</code> parameter). This set of defaults will be
-%%% merged with the parameters specified by the user, and user values
-%%% override <code>ct_snmp</code> defaults.</p>
-
-module(ct_snmp).
-%%% Common Types
-%%% @type agent_ip() = ip()
-%%% @type manager_ip() = ip()
-%%% @type agent_name() = atom()
-%%% @type ip() = string() | {integer(), integer(),
-%%% integer(), integer()}
-%%% @type agent_port() = integer()
-%%% @type agent_config() = {Item, Value}
-%%% @type user_name() = atom()
-%%% @type usm_user_name() = string()
-%%% @type usm_config() = {Item, Value}
-%%% @type call_back_module() = atom()
-%%% @type user_data() = term()
-%%% @type oids() = [oid()]
-%%% @type oid() = [byte()]
-%%% @type snmpreply() = {error_status(), error_index(), varbinds()}
-%%% @type error_status() = noError | atom()
-%%% @type error_index() = integer()
-%%% @type varbinds() = [varbind()]
-%%% @type varbind() = term()
-%%% @type value_type() = o ('OBJECT IDENTIFIER') | i ('INTEGER') |
-%%% u ('Unsigned32') | g ('Unsigned32') | s ('OCTET STRING')
-%%% @type varsandvals() = [var_and_val()]
-%%% @type var_and_val() = {oid(), value_type(), value()}
-%%% @type sec_type() = none | minimum | semi
-%%% @type rel_path() = string()
-%%% @type snmp_app_manager_params() = term()
-%%% @type snmp_app_agent_params() = term()
-
-
-include("snmp_types.hrl").
-include("inet.hrl").
-include("ct.hrl").
@@ -180,31 +50,9 @@
%%% API
%%%=========================================================================
-%%%-----------------------------------------------------------------
-%%% @spec start(Config, MgrAgentConfName) -> ok
-%%% @equiv start(Config, MgrAgentConfName, undefined)
start(Config, MgrAgentConfName) ->
start(Config, MgrAgentConfName, undefined).
-%%% @spec start(Config, MgrAgentConfName, SnmpAppConfName) -> ok
-%%% Config = [{Key, Value}]
-%%% Key = atom()
-%%% Value = term()
-%%% MgrAgentConfName = atom()
-%%% SnmpConfName = atom()
-%%%
-%%% @doc Starts an snmp manager and/or agent. In the manager case,
-%%% registrations of users and agents as specified by the configuration
-%%% <code>MgrAgentConfName</code> will be performed. When using snmp
-%%% v3 also so called usm users will be registered. Note that users,
-%%% usm_users and managed agents may also be registered at a later time
-%%% using ct_snmp:register_users/2, ct_snmp:register_agents/2, and
-%%% ct_snmp:register_usm_users/2. The agent started will be
-%%% called <code>snmp_master_agent</code>. Use ct_snmp:load_mibs/1 to load
-%%% mibs into the agent. With <code>SnmpAppConfName</code> it's possible
-%%% to configure the snmp application with parameters such as <code>config</code>,
-%%% <code>mibs</code>, <code>net_if</code>, etc. The values will be merged
-%%% with (and possibly override) default values set by <code>ct_snmp</code>.
start(Config, MgrAgentConfName, SnmpAppConfName) ->
StartManager= ct:get_config({MgrAgentConfName, start_manager}, true),
StartAgent = ct:get_config({MgrAgentConfName, start_agent}, false),
@@ -220,20 +68,22 @@ 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}]
-%%% Key = atom()
-%%% Value = term()
-%%%
-%%% @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"),
@@ -242,41 +92,16 @@ stop(Config) ->
catch del_dir(DbDir).
-%%% @spec get_values(Agent, Oids, MgrAgentConfName) -> SnmpReply
-%%%
-%%% Agent = agent_name()
-%%% Oids = oids()
-%%% MgrAgentConfName = atom()
-%%% SnmpReply = snmpreply()
-%%%
-%%% @doc Issues a synchronous snmp get request.
get_values(Agent, Oids, MgrAgentConfName) ->
[Uid | _] = agent_conf(Agent, MgrAgentConfName),
{ok, SnmpReply, _} = snmpm:sync_get2(Uid, target_name(Agent), Oids),
SnmpReply.
-%%% @spec get_next_values(Agent, Oids, MgrAgentConfName) -> SnmpReply
-%%%
-%%% Agent = agent_name()
-%%% Oids = oids()
-%%% MgrAgentConfName = atom()
-%%% SnmpReply = snmpreply()
-%%%
-%%% @doc Issues a synchronous snmp get next request.
get_next_values(Agent, Oids, MgrAgentConfName) ->
[Uid | _] = agent_conf(Agent, MgrAgentConfName),
{ok, SnmpReply, _} = snmpm:sync_get_next2(Uid, target_name(Agent), Oids),
SnmpReply.
-%%% @spec set_values(Agent, VarsAndVals, MgrAgentConfName, Config) -> SnmpReply
-%%%
-%%% Agent = agent_name()
-%%% Oids = oids()
-%%% MgrAgentConfName = atom()
-%%% Config = [{Key, Value}]
-%%% SnmpReply = snmpreply()
-%%%
-%%% @doc Issues a synchronous snmp set request.
set_values(Agent, VarsAndVals, MgrAgentConfName, Config) ->
PrivDir = ?config(priv_dir, Config),
[Uid | _] = agent_conf(Agent, MgrAgentConfName),
@@ -292,42 +117,17 @@ set_values(Agent, VarsAndVals, MgrAgentConfName, Config) ->
end,
SnmpSetReply.
-%%% @spec set_info(Config) -> [{Agent, OldVarsAndVals, NewVarsAndVals}]
-%%%
-%%% Config = [{Key, Value}]
-%%% Agent = agent_name()
-%%% OldVarsAndVals = varsandvals()
-%%% NewVarsAndVals = varsandvals()
-%%%
-%%% @doc Returns a list of all successful set requests performed in
-%%% the test case in reverse order. The list contains the involved
-%%% user and agent, the value prior to the set and the new value. This
-%%% is intended to facilitate the clean up in the end_per_testcase
-%%% function i.e. the undoing of the set requests and its possible
-%%% side-effects.
set_info(Config) ->
PrivDir = ?config(priv_dir, 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);
_ ->
[]
end.
-%%% @spec register_users(MgrAgentConfName, Users) -> ok | {error, Reason}
-%%%
-%%% MgrAgentConfName = atom()
-%%% Users = [user()]
-%%% Reason = term()
-%%%
-%%% @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
-%%% checking if any of them already exist. In order to change an
-%%% already registered user, the user must first be unregistered.
register_users(MgrAgentConfName, Users) ->
case setup_users(Users) of
ok ->
@@ -341,19 +141,6 @@ register_users(MgrAgentConfName, Users) ->
Error
end.
-%%% @spec register_agents(MgrAgentConfName, ManagedAgents) -> ok | {error, Reason}
-%%%
-%%% MgrAgentConfName = atom()
-%%% ManagedAgents = [agent()]
-%%% Reason = term()
-%%%
-%%% @doc Explicitly instruct the manager to handle this agent.
-%%% Corresponds to making an entry in agents.conf
-%%%
-%%% This function will try to register the given managed agents,
-%%% without checking if any of them already exist. In order to change
-%%% an already registered managed agent, the agent must first be
-%%% unregistered.
register_agents(MgrAgentConfName, ManagedAgents) ->
case setup_managed_agents(MgrAgentConfName,ManagedAgents) of
ok ->
@@ -368,18 +155,6 @@ register_agents(MgrAgentConfName, ManagedAgents) ->
Error
end.
-%%% @spec register_usm_users(MgrAgentConfName, UsmUsers) -> ok | {error, Reason}
-%%%
-%%% MgrAgentConfName = atom()
-%%% UsmUsers = [usm_user()]
-%%% Reason = term()
-%%%
-%%% @doc Explicitly instruct the manager to handle this USM user.
-%%% Corresponds to making an entry in usm.conf
-%%%
-%%% This function will try to register the given users, without
-%%% checking if any of them already exist. In order to change an
-%%% already registered user, the user must first be unregistered.
register_usm_users(MgrAgentConfName, UsmUsers) ->
EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID),
case setup_usm_users(UsmUsers, EngineID) of
@@ -394,23 +169,10 @@ register_usm_users(MgrAgentConfName, UsmUsers) ->
Error
end.
-%%% @spec unregister_users(MgrAgentConfName) -> ok
-%%%
-%%% MgrAgentConfName = atom()
-%%% Reason = term()
-%%%
-%%% @doc Unregister all users.
unregister_users(MgrAgentConfName) ->
Users = [Id || {Id,_} <- ct:get_config({MgrAgentConfName, users},[])],
unregister_users(MgrAgentConfName,Users).
-%%% @spec unregister_users(MgrAgentConfName,Users) -> ok
-%%%
-%%% MgrAgentConfName = atom()
-%%% Users = [user_name()]
-%%% Reason = term()
-%%%
-%%% @doc Unregister the given users.
unregister_users(MgrAgentConfName,Users) ->
takedown_users(Users),
SnmpVals = ct:get_config(MgrAgentConfName),
@@ -423,25 +185,12 @@ unregister_users(MgrAgentConfName,Users) ->
ct_config:update_config(MgrAgentConfName, NewSnmpVals),
ok.
-%%% @spec unregister_agents(MgrAgentConfName) -> ok
-%%%
-%%% MgrAgentConfName = atom()
-%%% Reason = term()
-%%%
-%%% @doc Unregister all managed agents.
unregister_agents(MgrAgentConfName) ->
ManagedAgents = [AgentName ||
{AgentName, _} <-
ct:get_config({MgrAgentConfName,managed_agents},[])],
unregister_agents(MgrAgentConfName,ManagedAgents).
-%%% @spec unregister_agents(MgrAgentConfName,ManagedAgents) -> ok
-%%%
-%%% MgrAgentConfName = atom()
-%%% ManagedAgents = [agent_name()]
-%%% Reason = term()
-%%%
-%%% @doc Unregister the given managed agents.
unregister_agents(MgrAgentConfName,ManagedAgents) ->
takedown_managed_agents(MgrAgentConfName, ManagedAgents),
SnmpVals = ct:get_config(MgrAgentConfName),
@@ -455,23 +204,10 @@ unregister_agents(MgrAgentConfName,ManagedAgents) ->
ct_config:update_config(MgrAgentConfName, NewSnmpVals),
ok.
-%%% @spec unregister_usm_users(MgrAgentConfName) -> ok
-%%%
-%%% MgrAgentConfName = atom()
-%%% Reason = term()
-%%%
-%%% @doc Unregister all usm users.
unregister_usm_users(MgrAgentConfName) ->
UsmUsers = [Id || {Id,_} <- ct:get_config({MgrAgentConfName, usm_users},[])],
unregister_usm_users(MgrAgentConfName,UsmUsers).
-%%% @spec unregister_usm_users(MgrAgentConfName,UsmUsers) -> ok
-%%%
-%%% MgrAgentConfName = atom()
-%%% UsmUsers = [usm_user_name()]
-%%% Reason = term()
-%%%
-%%% @doc Unregister the given usm users.
unregister_usm_users(MgrAgentConfName,UsmUsers) ->
EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID),
takedown_usm_users(UsmUsers,EngineID),
@@ -486,23 +222,9 @@ unregister_usm_users(MgrAgentConfName,UsmUsers) ->
ct_config:update_config(MgrAgentConfName, NewSnmpVals),
ok.
-%%% @spec load_mibs(Mibs) -> ok | {error, Reason}
-%%%
-%%% Mibs = [MibName]
-%%% MibName = string()
-%%% Reason = term()
-%%%
-%%% @doc Load the mibs into the agent 'snmp_master_agent'.
load_mibs(Mibs) ->
snmpa:load_mibs(snmp_master_agent, Mibs).
-%%% @spec unload_mibs(Mibs) -> ok | {error, Reason}
-%%%
-%%% Mibs = [MibName]
-%%% MibName = string()
-%%% Reason = term()
-%%%
-%%% @doc Unload the mibs from the agent 'snmp_master_agent'.
unload_mibs(Mibs) ->
snmpa:unload_mibs(snmp_master_agent, Mibs).
@@ -512,7 +234,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 +254,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 +270,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 +286,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 +405,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 +413,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 +459,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 +475,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 +490,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 +506,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 +521,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 +536,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 +551,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 +567,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 +582,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..79ab122452 100644
--- a/lib/common_test/src/ct_ssh.erl
+++ b/lib/common_test/src/ct_ssh.erl
@@ -1,60 +1,23 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2009-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2009-2018. 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 SSH/SFTP client module.
-%%%
-%%% <p>ct_ssh uses the OTP ssh application and more detailed information
-%%% about e.g. functions, types and options can be found in the
-%%% documentation for this application.</p>
-%%%
-%%% <p>The <code>Server</code> argument in the SFTP functions should
-%%% only be used for SFTP sessions that have been started on existing
-%%% SSH connections (i.e. when the original connection type is
-%%% <code>ssh</code>). Whenever the connection type is
-%%% <code>sftp</code>, use the SSH connection reference only.</p>
-%%%
-%%% <p>The following options are valid for specifying an SSH/SFTP
-%%% connection (i.e. may be used as config elements):</p>
-%%%
-%%% <pre>
-%%%
-%%% [{ConnType, Addr},
-%%% {port, Port},
-%%% {user, UserName}
-%%% {password, Pwd}
-%%% {user_dir, String}
-%%% {public_key_alg, PubKeyAlg}
-%%% {connect_timeout, Timeout}
-%%% {key_cb, KeyCallbackMod}]
-%%% </pre>
-%%%
-%%% <p><code>ConnType = ssh | sftp</code>.</p>
-%%% <p>Please see ssh(3) for other types.</p>
-%%%
-%%% <p>All timeout parameters in ct_ssh functions are values in
-%%% milliseconds.</p>
-%%%
-%%% @type connection() = handle() | ct:target_name()
-%%% @type handle() = ct_gen_conn:handle(). Handle for a specific
-%%% SSH/SFTP connection.
-%%% @type ssh_sftp_return() = term(). A return value from an ssh_sftp function.
-
-module(ct_ssh).
%% SSH Functions
@@ -67,7 +30,8 @@
send_and_receive/3, send_and_receive/4, send_and_receive/5,
send_and_receive/6,
exec/2, exec/3, exec/4,
- subsystem/3, subsystem/4]).
+ subsystem/3, subsystem/4,
+ shell/2, shell/3]).
%% STFP Functions
-export([sftp_connect/1,
@@ -93,72 +57,24 @@
-record(state, {ssh_ref, conn_type, target}).
+-type handle() :: pid().
%%%-----------------------------------------------------------------
%%%------------------------ SSH COMMANDS ---------------------------
-%%%-----------------------------------------------------------------
-%%% @spec connect(KeyOrName) -> {ok,Handle} | {error,Reason}
-%%% @equiv connect(KeyOrName,host,[])
connect(KeyOrName) ->
connect(KeyOrName, host).
-%%%-----------------------------------------------------------------
-%%% @spec connect(KeyOrName,ConnType) -> {ok,Handle} | {error,Reason}
-%%% @equiv connect(KeyOrName,ConnType,[])
connect(KeyOrName, ConnType) when is_atom(ConnType) ->
connect(KeyOrName, ConnType, []);
-%%%-----------------------------------------------------------------
-%%% @spec connect(KeyOrName,ExtraOpts) -> {ok,Handle} | {error,Reason}
-%%% @equiv connect(KeyOrName,host,ExtraOpts)
connect(KeyOrName, ExtraOpts) when is_list(ExtraOpts) ->
connect(KeyOrName, host, ExtraOpts).
-%%%-----------------------------------------------------------------
-%%% @spec connect(KeyOrName,ConnType,ExtraOpts) ->
-%%% {ok,Handle} | {error,Reason}
-%%% KeyOrName = Key | Name
-%%% Key = atom()
-%%% Name = ct:target_name()
-%%% ConnType = ssh | sftp | host
-%%% ExtraOpts = ssh_connect_options()
-%%% Handle = handle()
-%%% Reason = term()
-%%%
-%%% @doc Open an SSH or SFTP connection using the information
-%%% associated with <code>KeyOrName</code>.
-%%%
-%%% <p>If <code>Name</code> (an alias name for <code>Key</code>),
-%%% is used to identify the connection, this name may
-%%% be used as connection reference for subsequent calls.
-%%% It's only possible to have one open connection at a time
-%%% associated with <code>Name</code>. If <code>Key</code> is
-%%% used, the returned handle must be used for subsequent calls
-%%% (multiple connections may be opened using the config
-%%% data specified by <code>Key</code>). See <c>ct:require/2</c>
-%%% for how to create a new <c>Name</c></p>
-%%%
-%%% <p><code>ConnType</code> will always override the type
-%%% specified in the address tuple in the configuration data (and
-%%% in <code>ExtraOpts</code>). So it is possible to for example
-%%% open an sftp connection directly using data originally
-%%% specifying an ssh connection. The value <code>host</code>
-%%% means the connection type specified by the host option
-%%% (either in the configuration data or in <code>ExtraOpts</code>)
-%%% will be used.</p>
-%%%
-%%% <p><code>ExtraOpts</code> (optional) are extra SSH options
-%%% to be added to the config data for <code>KeyOrName</code>.
-%%% The extra options will override any existing options with the
-%%% same key in the config data. For details on valid SSH
-%%% options, see the documentation for the OTP ssh application.</p>
-%%%
-%%% @see ct:require/2
connect(KeyOrName, ConnType, ExtraOpts) ->
case ct:get_config(KeyOrName) of
undefined ->
- log(heading(connect,KeyOrName), "Failed: ~p\n",
+ log(heading(connect,KeyOrName), "Failed: ~tp\n",
[{not_available,KeyOrName}]),
{error,{not_available,KeyOrName}};
SSHData ->
@@ -211,30 +127,24 @@ connect(KeyOrName, ConnType, ExtraOpts) ->
end,
case {Addr,proplists:get_value(port, AllOpts1)} of
{undefined,_} ->
- log(heading(connect,KeyOrName), "Failed: ~p\n",
+ log(heading(connect,KeyOrName), "Failed: ~tp\n",
[{not_available,{KeyOrName,ConnType1}}]),
{error,{not_available,{KeyOrName,ConnType1}}};
{_,undefined} ->
try_log(heading(connect,KeyOrName),
- "Opening ~w connection to ~p:22\n",
+ "Opening ~w connection to ~tp:22\n",
[ConnType1,Addr]),
ct_gen_conn:start(KeyOrName, {ConnType1,Addr,22},
AllOpts1, ?MODULE);
{_,Port} ->
try_log(heading(connect,KeyOrName),
- "Opening ~w connection to ~p:~w\n",
+ "Opening ~w connection to ~tp:~w\n",
[ConnType1,Addr,Port]),
ct_gen_conn:start(KeyOrName, {ConnType1,Addr,Port},
AllOpts1, ?MODULE)
end
end.
-%%%-----------------------------------------------------------------
-%%% @spec disconnect(SSH) -> ok | {error,Reason}
-%%% SSH = connection()
-%%% Reason = term()
-%%%
-%%% @doc Close an SSH/SFTP connection.
disconnect(SSH) ->
case get_handle(SSH) of
{ok,Pid} ->
@@ -249,682 +159,252 @@ disconnect(SSH) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec session_open(SSH) -> {ok,ChannelId} | {error, Reason}
-%%% @equiv session_open(SSH,DefaultTimeout)
session_open(SSH) ->
call(SSH, {session_open,?DEFAULT_TIMEOUT}).
-%%%-----------------------------------------------------------------
-%%% @spec session_open(SSH,Timeout) -> {ok,ChannelId} | {error, Reason}
-%%% SSH = connection()
-%%% Timeout = integer()
-%%% ChannelId = integer()
-%%% Reason = term()
-%%%
-%%% @doc Opens a channel for an SSH session.
session_open(SSH, Timeout) ->
call(SSH, {session_open,Timeout}).
-%%%-----------------------------------------------------------------
-%%% @spec session_close(SSH,ChannelId) -> ok | {error, Reason}
-%%% SSH = connection()
-%%% ChannelId = integer()
-%%% Reason = term()
-%%%
-%%% @doc Closes an SSH session channel.
session_close(SSH, ChannelId) ->
call(SSH, {session_close,ChannelId}).
-%%%-----------------------------------------------------------------
-%%% @spec exec(SSH,Command) -> {ok,Data} | {error,Reason}
-%%% @equiv exec(SSH,Command,DefaultTimeout)
exec(SSH, Command) ->
exec(SSH, undefined, Command, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec exec(SSH,Command,Timeout) -> {ok,Data} | {error,Reason}
-%%% SSH = connection()
-%%% Command = string()
-%%% Timeout = integer()
-%%% Data = list()
-%%% Reason = term()
-%%%
-%%% @doc Requests server to perform <code>Command</code>. A session
-%%% channel is opened automatically for the request.
-%%% <code>Data</code> is received from the server as a result
-%%% of the command.
exec(SSH, Command, Timeout) when is_list(Command) ->
exec(SSH, undefined, Command, Timeout);
-%%%-----------------------------------------------------------------
-%%% @spec exec(SSH,ChannelId,Command) -> {ok,Data} | {error,Reason}
-%%% @equiv exec(SSH,ChannelId,Command,DefaultTimeout)
exec(SSH, ChannelId, Command) when is_integer(ChannelId) ->
exec(SSH, ChannelId, Command, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec exec(SSH,ChannelId,Command,Timeout) -> {ok,Data} | {error,Reason}
-%%% SSH = connection()
-%%% ChannelId = integer()
-%%% Command = string()
-%%% Timeout = integer()
-%%% Data = list()
-%%% Reason = term()
-%%%
-%%% @doc Requests server to perform <code>Command</code>. A previously
-%%% opened session channel is used for the request.
-%%% <code>Data</code> is received from the server as a result
-%%% of the command.
exec(SSH, ChannelId, Command, Timeout) ->
call(SSH, {exec,ChannelId,Command,Timeout}).
-%%%-----------------------------------------------------------------
-%%% @spec receive_response(SSH,ChannelId) -> {ok,Data} | {error,Reason}
-%%% @equiv receive_response(SSH,ChannelId,close)
receive_response(SSH, ChannelId) ->
receive_response(SSH, ChannelId, close, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec receive_response(SSH,ChannelId,End) -> {ok,Data} | {error,Reason}
-%%% @equiv receive_response(SSH,ChannelId,End,DefaultTimeout)
receive_response(SSH, ChannelId, End) when is_function(End) ->
receive_response(SSH, ChannelId, End, ?DEFAULT_TIMEOUT);
-%%%-----------------------------------------------------------------
-%%% @spec receive_response(SSH,ChannelId,Timeout) -> {ok,Data} | {error,Reason}
-%%% @equiv receive_response(SSH,ChannelId,close,Timeout)
receive_response(SSH, ChannelId, Timeout) when is_integer(Timeout) ->
receive_response(SSH, ChannelId, close, Timeout).
-%%%-----------------------------------------------------------------
-%%% @spec receive_response(SSH,ChannelId,End,Timeout) ->
-%%% {ok,Data} | {timeout,Data} | {error,Reason}
-%%% SSH = connection()
-%%% ChannelId = integer()
-%%% End = Fun | close | timeout
-%%% Timeout = integer()
-%%% Data = list()
-%%% Reason = term()
-%%%
-%%% @doc Receives expected data from server on the specified
-%%% session channel.
-%%%
-%%% <p>If <code>End == close</code>, data is returned
-%%% to the caller when the channel is closed by the
-%%% server. If a timeout occurs before this happens,
-%%% the function returns <code>{timeout,Data}</code>
-%%% (where <code>Data</code> is the data received so far).
-%%% If <code>End == timeout</code>, a timeout is expected
-%%% and <code>{ok,Data}</code> is returned both in the case
-%%% of a timeout and when the channel is closed. If
-%%% <code>End</code> is a fun, this fun will be
-%%% called with one argument - the data value in a received
-%%% <code>ssh_cm</code> message (see ssh_connection(3)). The
-%%% fun should return <code>true</code> to end the receiving
-%%% operation (and have the so far collected data returned), or
-%%% <code>false</code> to wait for more data from the server.
-%%% (Note that even if a fun is supplied, the function returns
-%%% immediately if the server closes the channel).</p>
receive_response(SSH, ChannelId, End, Timeout) ->
call(SSH, {receive_response,ChannelId,End,Timeout}).
-%%%-----------------------------------------------------------------
-%%% @spec send(SSH,ChannelId,Data) -> ok | {error,Reason}
-%%% @equiv send(SSH,ChannelId,0,Data,DefaultTimeout)
send(SSH, ChannelId, Data) ->
send(SSH, ChannelId, 0, Data, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec send(SSH,ChannelId,Data,Timeout) -> ok | {error,Reason}
-%%% @equiv send(SSH,ChannelId,0,Data,Timeout)
send(SSH, ChannelId, Data, Timeout) when is_integer(Timeout) ->
send(SSH, ChannelId, 0, Data, Timeout);
-%%%-----------------------------------------------------------------
-%%% @spec send(SSH,ChannelId,Type,Data) -> ok | {error,Reason}
-%%% @equiv send(SSH,ChannelId,Type,Data,DefaultTimeout)
send(SSH, ChannelId, Type, Data) when is_integer(Type) ->
send(SSH, ChannelId, Type, Data, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec send(SSH,ChannelId,Type,Data,Timeout) -> ok | {error,Reason}
-%%% SSH = connection()
-%%% ChannelId = integer()
-%%% Type = integer()
-%%% Data = list()
-%%% Timeout = integer()
-%%% Reason = term()
-%%%
-%%% @doc Send data to server on specified session channel.
send(SSH, ChannelId, Type, Data, Timeout) ->
call(SSH, {send,ChannelId,Type,Data,Timeout}).
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Data) ->
-%%% {ok,Data} | {error,Reason}
-%%% @equiv send_and_receive(SSH,ChannelId,Data,close)
send_and_receive(SSH, ChannelId, Data) ->
send_and_receive(SSH, ChannelId, 0, Data, close, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Data,End) ->
-%%% {ok,Data} | {error,Reason}
-%%% @equiv send_and_receive(SSH,ChannelId,0,Data,End,DefaultTimeout)
send_and_receive(SSH, ChannelId, Data, End) when is_function(End) ->
send_and_receive(SSH, ChannelId, 0, Data, End, ?DEFAULT_TIMEOUT);
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Data,Timeout) ->
-%%% {ok,Data} | {error,Reason}
-%%% @equiv send_and_receive(SSH,ChannelId,0,Data,close,Timeout)
send_and_receive(SSH, ChannelId, Data, Timeout) when is_integer(Timeout) ->
send_and_receive(SSH, ChannelId, 0, Data, close, Timeout);
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Type,Data) ->
-%%% {ok,Data} | {error,Reason}
-%%% @equiv send_and_receive(SSH,ChannelId,Type,Data,close,DefaultTimeout)
send_and_receive(SSH, ChannelId, Type, Data) when is_integer(Type) ->
send_and_receive(SSH, ChannelId, Type, Data, close, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Data,End,Timeout) ->
-%%% {ok,Data} | {error,Reason}
-%%% @equiv send_and_receive(SSH,ChannelId,0,Data,End,Timeout)
send_and_receive(SSH, ChannelId, Data, End, Timeout) when is_integer(Timeout) ->
send_and_receive(SSH, ChannelId, 0, Data, End, Timeout);
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Type,Data,Timeout) ->
-%%% {ok,Data} | {error,Reason}
-%%% @equiv send_and_receive(SSH,ChannelId,Type,Data,close,Timeout)
send_and_receive(SSH, ChannelId, Type, Data, Timeout) when is_integer(Type) ->
send_and_receive(SSH, ChannelId, Type, Data, close, Timeout);
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Type,Data,End) ->
-%%% {ok,Data} | {error,Reason}
-%%% @equiv send_and_receive(SSH,ChannelId,Type,Data,End,DefaultTimeout)
send_and_receive(SSH, ChannelId, Type, Data, End) when is_function(End) ->
send_and_receive(SSH, ChannelId, Type, Data, End, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec send_and_receive(SSH,ChannelId,Type,Data,End,Timeout) ->
-%%% {ok,Data} | {error,Reason}
-%%% SSH = connection()
-%%% ChannelId = integer()
-%%% Type = integer()
-%%% Data = list()
-%%% End = Fun | close | timeout
-%%% Timeout = integer()
-%%% Reason = term()
-%%%
-%%% @doc Send data to server on specified session channel and wait
-%%% to receive the server response.
-%%%
-%%% <p>See <code>receive_response/4</code> for details on the
-%%% <code>End</code> argument.</p>
send_and_receive(SSH, ChannelId, Type, Data, End, Timeout) ->
call(SSH, {send_and_receive,ChannelId,Type,Data,End,Timeout}).
-%%%-----------------------------------------------------------------
-%%% @spec subsystem(SSH,ChannelId,Subsystem) -> Status | {error,Reason}
-%%% @equiv subsystem(SSH,ChannelId,Subsystem,DefaultTimeout)
subsystem(SSH, ChannelId, Subsystem) ->
subsystem(SSH, ChannelId, Subsystem, ?DEFAULT_TIMEOUT).
-%%%-----------------------------------------------------------------
-%%% @spec subsystem(SSH,ChannelId,Subsystem,Timeout) ->
-%%% Status | {error,Reason}
-%%% SSH = connection()
-%%% ChannelId = integer()
-%%% Subsystem = string()
-%%% Timeout = integer()
-%%% Status = success | failure
-%%% Reason = term()
-%%%
-%%% @doc Sends a request to execute a predefined subsystem.
subsystem(SSH, ChannelId, Subsystem, Timeout) ->
call(SSH, {subsystem,ChannelId,Subsystem,Timeout}).
+-spec shell(SSH, ChannelId) -> Result when
+ SSH :: handle() | ct:target_name(),
+ ChannelId :: ssh:ssh_channel_id(),
+ Result :: ok | {error,term()}.
+shell(SSH, ChannelId) ->
+ shell(SSH, ChannelId, ?DEFAULT_TIMEOUT).
+
+-spec shell(SSH, ChannelId, Timeout) -> Result when
+ SSH :: handle() | ct:target_name(),
+ ChannelId :: ssh:ssh_channel_id(),
+ Timeout :: timeout(),
+ Result :: ok | {error,term()}.
+shell(SSH, ChannelId, Timeout) ->
+ call(SSH, {shell,ChannelId,Timeout}).
+
+
%%%-----------------------------------------------------------------
%%%------------------------ SFTP COMMANDS --------------------------
-%%%-----------------------------------------------------------------
-%%% @spec sftp_connect(SSH) -> {ok,Server} | {error,Reason}
-%%% SSH = connection()
-%%% Server = pid()
-%%% Reason = term()
-%%% @doc Starts an SFTP session on an already existing SSH connection.
-%%% <code>Server</code> identifies the new session and must be
-%%% specified whenever SFTP requests are to be sent.
sftp_connect(SSH) ->
call(SSH, sftp_connect).
-%%%-----------------------------------------------------------------
-%%% @spec read_file(SSH, File) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
read_file(SSH, File) ->
call(SSH, {read_file,sftp,File}).
-%%%-----------------------------------------------------------------
-%%% @spec read_file(SSH, Server, File) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
read_file(SSH, Server, File) ->
call(SSH, {read_file,Server,File}).
-%%%-----------------------------------------------------------------
-%%% @spec write_file(SSH, File, Iolist) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
write_file(SSH, File, Iolist) ->
call(SSH, {write_file,sftp,File,Iolist}).
-%%%-----------------------------------------------------------------
-%%% @spec write_file(SSH, Server, File, Iolist) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
write_file(SSH, Server, File, Iolist) ->
call(SSH, {write_file,Server,File,Iolist}).
-%%%-----------------------------------------------------------------
-%%% @spec list_dir(SSH, Path) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
list_dir(SSH, Path) ->
call(SSH, {list_dir,sftp,Path}).
-%%%-----------------------------------------------------------------
-%%% @spec list_dir(SSH, Server, Path) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
list_dir(SSH, Server, Path) ->
call(SSH, {list_dir,Server,Path}).
-%%%-----------------------------------------------------------------
-%%% @spec open(SSH, File, Mode) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
open(SSH, File, Mode) ->
call(SSH, {open,sftp,File,Mode}).
-%%%-----------------------------------------------------------------
-%%% @spec open(SSH, Server, File, Mode) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
open(SSH, Server, File, Mode) ->
call(SSH, {open,Server,File,Mode}).
-%%%-----------------------------------------------------------------
-%%% @spec opendir(SSH, Path) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
opendir(SSH, Path) ->
call(SSH, {opendir,sftp,Path}).
-%%%-----------------------------------------------------------------
-%%% @spec opendir(SSH, Server, Path) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
opendir(SSH, Server, Path) ->
call(SSH, {opendir,Server,Path}).
-%%%-----------------------------------------------------------------
-%%% @spec close(SSH, Handle) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
close(SSH, Handle) ->
call(SSH, {close,sftp,Handle}).
-%%%-----------------------------------------------------------------
-%%% @spec close(SSH, Server, Handle) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
close(SSH, Server, Handle) ->
call(SSH, {close,Server,Handle}).
-%%%-----------------------------------------------------------------
-%%% @spec read(SSH, Handle, Len) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
read(SSH, Handle, Len) ->
call(SSH, {read,sftp,Handle,Len}).
-%%%-----------------------------------------------------------------
-%%% @spec read(SSH, Server, Handle, Len) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
read(SSH, Server, Handle, Len) ->
call(SSH, {read,Server,Handle,Len}).
-%%%-----------------------------------------------------------------
-%%% @spec pread(SSH, Handle, Position, Length) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
pread(SSH, Handle, Position, Length) ->
call(SSH, {pread,sftp,Handle,Position,Length}).
-%%%-----------------------------------------------------------------
-%%% @spec pread(SSH, Server, Handle, Position, Length) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
pread(SSH, Server, Handle, Position, Length) ->
call(SSH, {pread,Server,Handle,Position,Length}).
-%%%-----------------------------------------------------------------
-%%% @spec aread(SSH, Handle, Len) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
aread(SSH, Handle, Len) ->
call(SSH, {aread,sftp,Handle,Len}).
-%%%-----------------------------------------------------------------
-%%% @spec aread(SSH, Server, Handle, Len) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
aread(SSH, Server, Handle, Len) ->
call(SSH, {aread,Server,Handle,Len}).
-%%%-----------------------------------------------------------------
-%%% @spec apread(SSH, Handle, Position, Length) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
apread(SSH, Handle, Position, Length) ->
call(SSH, {apread,sftp,Handle,Position,Length}).
-%%%-----------------------------------------------------------------
-%%% @spec apread(SSH, Server, Handle, Position, Length) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
apread(SSH, Server, Handle, Position, Length) ->
call(SSH, {apread,Server,Handle,Position,Length}).
-%%%-----------------------------------------------------------------
-%%% @spec write(SSH, Handle, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
write(SSH, Handle, Data) ->
call(SSH, {write,sftp,Handle,Data}).
-%%%-----------------------------------------------------------------
-%%% @spec write(SSH, Server, Handle, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
write(SSH, Server, Handle, Data) ->
call(SSH, {write,Server,Handle,Data}).
-%%%-----------------------------------------------------------------
-%%% @spec pwrite(SSH, Handle, Position, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
pwrite(SSH, Handle, Position, Data) ->
call(SSH, {pwrite,sftp,Handle,Position,Data}).
-%%%-----------------------------------------------------------------
-%%% @spec pwrite(SSH, Server, Handle, Position, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
pwrite(SSH, Server, Handle, Position, Data) ->
call(SSH, {pwrite,Server,Handle,Position,Data}).
-%%%-----------------------------------------------------------------
-%%% @spec awrite(SSH, Handle, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
awrite(SSH, Handle, Data) ->
call(SSH, {awrite,sftp,Handle, Data}).
-%%%-----------------------------------------------------------------
-%%% @spec awrite(SSH, Server, Handle, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
awrite(SSH, Server, Handle, Data) ->
call(SSH, {awrite,Server,Handle, Data}).
-%%%-----------------------------------------------------------------
-%%% @spec apwrite(SSH, Handle, Position, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
apwrite(SSH, Handle, Position, Data) ->
call(SSH, {apwrite,sftp,Handle,Position,Data}).
-%%%-----------------------------------------------------------------
-%%% @spec apwrite(SSH, Server, Handle, Position, Data) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
apwrite(SSH, Server, Handle, Position, Data) ->
call(SSH, {apwrite,Server,Handle,Position,Data}).
-%%%-----------------------------------------------------------------
-%%% @spec position(SSH, Handle, Location) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
position(SSH, Handle, Location) ->
call(SSH, {position,sftp,Handle,Location}).
-%%%-----------------------------------------------------------------
-%%% @spec position(SSH, Server, Handle, Location) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
position(SSH, Server, Handle, Location) ->
call(SSH, {position,Server,Handle,Location}).
-%%%-----------------------------------------------------------------
-%%% @spec read_file_info(SSH, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
read_file_info(SSH, Name) ->
call(SSH, {read_file_info,sftp,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec read_file_info(SSH, Server, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
read_file_info(SSH, Server, Name) ->
call(SSH, {read_file_info,Server,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec get_file_info(SSH, Handle) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
get_file_info(SSH, Handle) ->
call(SSH, {get_file_info,sftp,Handle}).
-%%%-----------------------------------------------------------------
-%%% @spec get_file_info(SSH, Server, Handle) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
get_file_info(SSH, Server, Handle) ->
call(SSH, {get_file_info,Server,Handle}).
-%%%-----------------------------------------------------------------
-%%% @spec read_link_info(SSH, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
read_link_info(SSH, Name) ->
call(SSH, {read_link_info,sftp,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec read_link_info(SSH, Server, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
read_link_info(SSH, Server, Name) ->
call(SSH, {read_link_info,Server,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec write_file_info(SSH, Name, Info) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
write_file_info(SSH, Name, Info) ->
call(SSH, {write_file_info,sftp,Name,Info}).
-%%%-----------------------------------------------------------------
-%%% @spec write_file_info(SSH, Server, Name, Info) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
write_file_info(SSH, Server, Name, Info) ->
call(SSH, {write_file_info,Server,Name,Info}).
-%%%-----------------------------------------------------------------
-%%% @spec read_link(SSH, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
read_link(SSH, Name) ->
call(SSH, {read_link,sftp,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec read_link(SSH, Server, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
read_link(SSH, Server, Name) ->
call(SSH, {read_link,Server,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec make_symlink(SSH, Name, Target) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
make_symlink(SSH, Name, Target) ->
call(SSH, {make_symlink,sftp,Name,Target}).
-%%%-----------------------------------------------------------------
-%%% @spec make_symlink(SSH, Server, Name, Target) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
make_symlink(SSH, Server, Name, Target) ->
call(SSH, {make_symlink,Server,Name,Target}).
-%%%-----------------------------------------------------------------
-%%% @spec rename(SSH, OldName, NewName) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
rename(SSH, OldName, NewName) ->
call(SSH, {rename,sftp,OldName,NewName}).
-%%%-----------------------------------------------------------------
-%%% @spec rename(SSH, Server, OldName, NewName) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
rename(SSH, Server, OldName, NewName) ->
call(SSH, {rename,Server,OldName,NewName}).
-%%%-----------------------------------------------------------------
-%%% @spec delete(SSH, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
delete(SSH, Name) ->
call(SSH, {delete,sftp,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec delete(SSH, Server, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
delete(SSH, Server, Name) ->
call(SSH, {delete,Server,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec make_dir(SSH, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
make_dir(SSH, Name) ->
call(SSH, {make_dir,sftp,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec make_dir(SSH, Server, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
make_dir(SSH, Server, Name) ->
call(SSH, {make_dir,Server,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec del_dir(SSH, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
del_dir(SSH, Name) ->
call(SSH, {del_dir,sftp,Name}).
-%%%-----------------------------------------------------------------
-%%% @spec del_dir(SSH, Server, Name) -> Result
-%%% SSH = connection()
-%%% Result = ssh_sftp_return() | {error,Reason}
-%%% Reason = term()
-%%% @doc For info and other types, see ssh_sftp(3).
+
del_dir(SSH, Server, Name) ->
call(SSH, {del_dir,Server,Name}).
@@ -933,7 +413,6 @@ del_dir(SSH, Server, Name) ->
%%% Callback functions
%%%=================================================================
-%% @hidden
init(KeyOrName, {ConnType,Addr,Port}, AllOpts) ->
User = proplists:get_value(user, AllOpts),
Password = case proplists:get_value(password, AllOpts) of
@@ -961,8 +440,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);
@@ -976,13 +455,13 @@ init(KeyOrName, {ConnType,Addr,Port}, AllOpts) ->
SSHRef = element(2, Ok),
try_log(heading(init,KeyOrName),
"Opened ~w connection:\n"
- "Host: ~p (~p)\nUser: ~p\nPassword: ~p\n",
- [ConnType,Addr,Port,User,lists:duplicate(length(Password),$*)]),
+ "Host: ~tp (~p)\nUser: ~tp\nPassword: ~p\n",
+ [ConnType,Addr,Port,User,
+ lists:duplicate(string:length(Password),$*)]),
{ok,SSHRef,#state{ssh_ref=SSHRef, conn_type=ConnType,
target=KeyOrName}}
end.
-%% @hidden
handle_msg(sftp_connect, State) ->
#state{ssh_ref=SSHRef, target=Target} = State,
try_log(heading(sftp_connect,Target), "SSH Ref: ~p", [SSHRef]),
@@ -1014,11 +493,11 @@ handle_msg({exec,Chn,Command,TO}, State) ->
end,
case Chn1 of
{error,_} = ChnError ->
- log(heading(exec,Target), "Opening channel failed: ~p", [ChnError]),
+ log(heading(exec,Target), "Opening channel failed: ~tp", [ChnError]),
{ChnError,State};
_ ->
try_log(heading(exec,Target),
- "SSH Ref: ~p, Chn: ~p, Command: ~p, Timeout: ~p",
+ "SSH Ref: ~p, Chn: ~p, Command: ~tp, Timeout: ~p",
[SSHRef,Chn1,Command,TO]),
case ssh_connection:exec(SSHRef, Chn1, Command, TO) of
success ->
@@ -1041,7 +520,7 @@ handle_msg({send,Chn,Type,Data,TO}, State) ->
#state{ssh_ref=SSHRef, target=Target} = State,
try_log(heading(send,Target),
"SSH Ref: ~p, Chn: ~p, Type: ~p, Timeout: ~p~n"
- "Data: ~p", [SSHRef,Chn,Type,TO,Data]),
+ "Data: ~tp", [SSHRef,Chn,Type,TO,Data]),
Result = ssh_connection:send(SSHRef, Chn, Type, Data, TO),
{Result,State};
@@ -1049,7 +528,7 @@ handle_msg({send_and_receive,Chn,Type,Data,End,TO}, State) ->
#state{ssh_ref=SSHRef, target=Target} = State,
try_log(heading(send_and_receive,Target),
"SSH Ref: ~p, Chn: ~p, Type: ~p, Timeout: ~p~n"
- "Data: ~p", [SSHRef,Chn,Type,TO,Data]),
+ "Data: ~tp", [SSHRef,Chn,Type,TO,Data]),
case ssh_connection:send(SSHRef, Chn, Type, Data, TO) of
ok ->
Result = do_recv_response(SSHRef, Chn, [], End, TO),
@@ -1061,172 +540,177 @@ handle_msg({send_and_receive,Chn,Type,Data,End,TO}, State) ->
handle_msg({subsystem,Chn,Subsystem,TO}, State) ->
#state{ssh_ref=SSHRef, target=Target} = State,
try_log(heading(subsystem,Target),
- "SSH Ref: ~p, Chn: ~p, Subsys: ~p, Timeout: ~p",
+ "SSH Ref: ~p, Chn: ~p, Subsys: ~tp, Timeout: ~p",
[SSHRef,Chn,Subsystem,TO]),
Result = ssh_connection:subsystem(SSHRef, Chn, Subsystem, TO),
{Result,State};
+handle_msg({shell,Chn,TO}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ try_log(heading(shell,Target),
+ "SSH Ref: ~p, Chn: ~p, Timeout: ~p",
+ [SSHRef,Chn,TO]),
+ Result = ssh_connection:shell(SSHRef, Chn),
+ {Result,State};
+
%% --- SFTP Commands ---
handle_msg({read_file,Srv,File}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:read_file(ref(Srv,SSHRef), File),S};
handle_msg({write_file,Srv,File,Iolist}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:write_file(ref(Srv,SSHRef), File, Iolist),S};
handle_msg({list_dir,Srv,Path}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:list_dir(ref(Srv,SSHRef), Path),S};
handle_msg({open,Srv,File,Mode}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:open(ref(Srv,SSHRef), File, Mode),S};
handle_msg({opendir,Srv,Path}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:opendir(ref(Srv,SSHRef), Path),S};
handle_msg({close,Srv,Handle}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:close(ref(Srv,SSHRef), Handle),S};
handle_msg({read,Srv,Handle,Len}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:read(ref(Srv,SSHRef), Handle, Len),S};
handle_msg({pread,Srv,Handle,Position,Length}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:pread(ref(Srv,SSHRef),Handle,Position,Length),S};
handle_msg({aread,Srv,Handle,Len}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:aread(ref(Srv,SSHRef), Handle, Len),S};
handle_msg({apread,Srv,Handle,Position,Length}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:apread(ref(Srv,SSHRef), Handle, Position, Length),S};
handle_msg({write,Srv,Handle,Data}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:write(ref(Srv,SSHRef), Handle, Data),S};
handle_msg({pwrite,Srv,Handle,Position,Data}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:pwrite(ref(Srv,SSHRef), Handle, Position, Data),S};
handle_msg({awrite,Srv,Handle,Data}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:awrite(ref(Srv,SSHRef), Handle, Data),S};
handle_msg({apwrite,Srv,Handle,Position,Data}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:apwrite(ref(Srv,SSHRef), Handle, Position, Data),S};
handle_msg({position,Srv,Handle,Location}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:position(ref(Srv,SSHRef), Handle, Location),S};
handle_msg({read_file_info,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:read_file_info(ref(Srv,SSHRef), Name),S};
handle_msg({get_file_info,Srv,Handle}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:get_file_info(ref(Srv,SSHRef), Handle),S};
handle_msg({read_link_info,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:read_link_info(ref(Srv,SSHRef), Name),S};
handle_msg({write_file_info,Srv,Name,Info}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:write_file_info(ref(Srv,SSHRef), Name, Info),S};
handle_msg({read_link,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:read_link(ref(Srv,SSHRef), Name),S};
handle_msg({make_symlink,Srv,Name,Target}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:make_symlink(ref(Srv,SSHRef), Name, Target),S};
handle_msg({rename,Srv,OldName,NewName}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:rename(ref(Srv,SSHRef), OldName, NewName),S};
handle_msg({delete,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:delete(ref(Srv,SSHRef), Name),S};
handle_msg({make_dir,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:make_dir(ref(Srv,SSHRef), Name),S};
handle_msg({del_dir,Srv,Name}=Cmd, S=#state{ssh_ref=SSHRef}) ->
try_log(heading(sftp,S#state.target),
- "SSH Ref: ~p, Server: ~p~nCmd: ~p",
+ "SSH Ref: ~p, Server: ~p~nCmd: ~tp",
[SSHRef,ref(Srv,SSHRef),mod(Cmd)]),
{ssh_sftp:del_dir(ref(Srv,SSHRef), Name),S}.
-%% @hidden
reconnect(_Addr,_State) ->
{error,no_reconnection_of_ssh}.
-%% @hidden
close(SSHRef) ->
disconnect(SSHRef).
-%% @hidden
terminate(SSHRef, State) ->
case State#state.conn_type of
ssh ->
@@ -1243,8 +727,6 @@ terminate(SSHRef, State) ->
%%%=================================================================
%%% Internal functions
-%%%-----------------------------------------------------------------
-%%%
do_recv_response(SSH, Chn, Data, End, Timeout) ->
receive
{ssh_cm, SSH, {open,Chn,RemoteChn,{session}}} ->
@@ -1258,7 +740,7 @@ do_recv_response(SSH, Chn, Data, End, Timeout) ->
{ssh_cm, SSH, {data,Chn,_,NewData}} ->
ssh_connection:adjust_window(SSH, Chn, size(NewData)),
- debug("RECVD~n~p", [binary_to_list(NewData)]),
+ debug("RECVD~n~tp", [binary_to_list(NewData)]),
DataAcc = Data ++ binary_to_list(NewData),
if is_function(End) ->
case End(DataAcc) of
@@ -1311,7 +793,7 @@ do_recv_response(SSH, Chn, Data, End, Timeout) ->
%% {ok,WCh};
Other ->
- debug("UNEXPECTED MESSAGE~n~p ~p~n~p", [SSH,Chn,Other]),
+ debug("UNEXPECTED MESSAGE~n~p ~p~n~tp", [SSH,Chn,Other]),
do_recv_response(SSH, Chn, Data, End, Timeout)
after Timeout ->
@@ -1323,8 +805,6 @@ do_recv_response(SSH, Chn, Data, End, Timeout) ->
end
end.
-%%%-----------------------------------------------------------------
-%%%
get_handle(SSH) when is_pid(SSH) ->
{ok,SSH};
get_handle(SSH) ->
@@ -1337,8 +817,6 @@ get_handle(SSH) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%%
call(SSH, Msg) ->
call(SSH, Msg, infinity).
@@ -1350,29 +828,19 @@ call(SSH, Msg, Timeout) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%%
ref(sftp, SSHRef) -> SSHRef;
ref(Server, _) -> Server.
-%%%-----------------------------------------------------------------
-%%%
mod(Cmd) ->
[Op,_Server|Args] = tuple_to_list(Cmd),
list_to_tuple([Op|Args]).
-%%%-----------------------------------------------------------------
-%%%
heading(Function, Ref) ->
- io_lib:format("ct_ssh:~w ~p",[Function,Ref]).
+ io_lib:format("ct_ssh:~tw ~tp",[Function,Ref]).
-%%%-----------------------------------------------------------------
-%%%
log(Heading, Str, Args) ->
ct_gen_conn:log(Heading, Str, Args).
-%%%-----------------------------------------------------------------
-%%%
try_log(Heading, Str, Args) ->
try_log(Heading, Str, Args, infinity).
@@ -1386,8 +854,6 @@ try_log(Heading, Str, Args, Timeout) ->
ok
end.
-%%%-----------------------------------------------------------------
-%%%
debug(Str) ->
debug(Str, []).
diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl
index 844f53731e..f9abecfd38 100644
--- a/lib/common_test/src/ct_telnet.erl
+++ b/lib/common_test/src/ct_telnet.erl
@@ -1,145 +1,22 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2018. 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
%%
-%% %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 `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 10 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}]}.</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 ===
+%% 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.
%%
-%% 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
-
-%% @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.
+%% %CopyrightEnd%
%%
-%% @see unix_telnet
-module(ct_telnet).
@@ -181,22 +58,12 @@
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}
-%%% @equiv open(Name,telnet)
open(Name) ->
open(Name,telnet).
-%%%-----------------------------------------------------------------
-%%% @spec open(Name,ConnType) -> {ok,Handle} | {error,Reason}
-%%% 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) ->
case ct_util:get_key_from_name(Name) of
{ok, unix} -> % unix host
@@ -207,48 +74,13 @@ open(Name,ConnType) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @spec open(KeyOrName,ConnType,TargetMod) ->
-%%% {ok,Handle} | {error,Reason}
-%%% @equiv open(KeyOrName,ConnType,TargetMod,[])
open(KeyOrName,ConnType,TargetMod) ->
open(KeyOrName,ConnType,TargetMod,KeyOrName).
-%%%-----------------------------------------------------------------
-%%% @spec open(KeyOrName,ConnType,TargetMod,Extra) ->
-%%% {ok,Handle} | {error,Reason}
-%%% KeyOrName = Key | Name
-%%% Key = atom()
-%%% Name = ct:target_name()
-%%% ConnType = connection_type()
-%%% TargetMod = atom()
-%%% Extra = term()
-%%% Handle = handle()
-%%% Reason = term()
-%%%
-%%% @doc Open a telnet connection to the specified target host.
-%%%
-%%% <p>The target data must exist in a configuration file. The connection
-%%% may be associated with either <code>Name</code> and/or the returned
-%%% <code>Handle</code>. To allocate a name for the target,
-%%% use <code>ct:require/2</code> in a test case, or use a
-%%% <code>require</code> statement in the suite info function
-%%% (<code>suite/0</code>), or in a test case info function.
-%%% If you want the connection to be associated with <code>Handle</code> only
-%%% (in case you need to open multiple connections to a host for example),
-%%% simply use <code>Key</code>, the configuration variable name, to
-%%% specify the target. Note that a connection that has no associated target
-%%% name can only be closed with the handle value.</p>
-%%%
-%%% <p><code>TargetMod</code> is a module which exports the functions
-%%% <code>connect(Ip,Port,KeepAlive,Extra)</code> and <code>get_prompt_regexp()</code>
-%%% for the given <code>TargetType</code> (e.g. <code>unix_telnet</code>).</p>
-%%%
-%%% @see ct:require/2
open(KeyOrName,ConnType,TargetMod,Extra) ->
case ct:get_config({KeyOrName,ConnType}) of
undefined ->
- log(undefined,open,"Failed: ~p",[{not_available,KeyOrName}]),
+ log(undefined,open,"Failed: ~tp",[{not_available,KeyOrName}]),
{error,{not_available,KeyOrName,ConnType}};
Addr ->
Addr1 =
@@ -270,7 +102,7 @@ open(KeyOrName,ConnType,TargetMod,Extra) ->
end;
Bool -> Bool
end,
- log(undefined,open,"Connecting to ~p(~p)",
+ log(undefined,open,"Connecting to ~tp(~tp)",
[KeyOrName,Addr1]),
Reconnect =
case ct:get_config({telnet_settings,reconnection_attempts}) of
@@ -284,17 +116,6 @@ open(KeyOrName,ConnType,TargetMod,Extra) ->
{old,true}])
end.
-%%%-----------------------------------------------------------------
-%%% @spec close(Connection) -> ok | {error,Reason}
-%%% Connection = ct_telnet:connection()
-%%% Reason = term()
-%%%
-%%% @doc Close the telnet connection and stop the process managing it.
-%%%
-%%% <p>A connection may be associated with a target name and/or a handle.
-%%% If <code>Connection</code> has no associated target name, it may only
-%%% be closed with the handle value (see the <code>open/4</code>
-%%% function).</p>
close(Connection) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -312,30 +133,10 @@ close(Connection) ->
%%%=================================================================
%%% Test suite interface
%%%-----------------------------------------------------------------
-%%% @spec cmd(Connection,Cmd) -> {ok,Data} | {error,Reason}
-%%% @equiv cmd(Connection,Cmd,[])
+
cmd(Connection,Cmd) ->
cmd(Connection,Cmd,[]).
-%%%-----------------------------------------------------------------
-%%% @spec cmd(Connection,Cmd,Opts) -> {ok,Data} | {error,Reason}
-%%% Connection = ct_telnet:connection()
-%%% Cmd = string()
-%%% Opts = [Opt]
-%%% Opt = {timeout,timeout()} | {newline,boolean()}
-%%% Data = [string()]
-%%% Reason = term()
-%%% @doc Send a command via telnet and wait for prompt.
-%%%
-%%% This function will by default add a newline to the end of the
-%%% 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).
-%%%
-%%% 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.
+
cmd(Connection,Cmd,Opts) when is_list(Opts) ->
case check_cmd_opts(Opts) of
ok ->
@@ -360,42 +161,13 @@ check_cmd_opts([]) ->
check_cmd_opts(Opts) ->
check_send_opts(Opts).
-%%%-----------------------------------------------------------------
-%%% @spec cmdf(Connection,CmdFormat,Args) -> {ok,Data} | {error,Reason}
-%%% @equiv cmdf(Connection,CmdFormat,Args,[])
cmdf(Connection,CmdFormat,Args) ->
cmdf(Connection,CmdFormat,Args,[]).
-%%%-----------------------------------------------------------------
-%%% @spec cmdf(Connection,CmdFormat,Args,Opts) -> {ok,Data} | {error,Reason}
-%%% Connection = ct_telnet:connection()
-%%% CmdFormat = string()
-%%% Args = list()
-%%% 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).
-%%%
-%%% See {@link cmd/3} further description.
+
cmdf(Connection,CmdFormat,Args,Opts) when is_list(Args) ->
Cmd = lists:flatten(io_lib:format(CmdFormat,Args)),
cmd(Connection,Cmd,Opts).
-%%%-----------------------------------------------------------------
-%%% @spec get_data(Connection) -> {ok,Data} | {error,Reason}
-%%% Connection = ct_telnet:connection()
-%%% Data = [string()]
-%%% 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} ->
@@ -404,29 +176,9 @@ get_data(Connection) ->
Error
end.
-%%%-----------------------------------------------------------------
-%%% @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,Opts) ->
case check_send_opts(Opts) of
ok ->
@@ -447,142 +199,16 @@ check_send_opts([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,Opts) when is_list(Args) ->
Cmd = lists:flatten(io_lib:format(CmdFormat,Args)),
send(Connection,Cmd,Opts).
-%%%-----------------------------------------------------------------
-%%% @spec expect(Connection,Patterns) -> term()
-%%% @equiv expect(Connections,Patterns,[])
expect(Connection,Patterns) ->
expect(Connection,Patterns,[]).
-%%%-----------------------------------------------------------------
-%%% @spec expect(Connection,Patterns,Opts) -> {ok,Match} |
-%%% {ok,MatchList,HaltReason} |
-%%% {error,Reason}
-%%% Connection = ct_telnet:connection()
-%%% Patterns = Pattern | [Pattern]
-%%% Pattern = string() | {Tag,string()} | prompt | {prompt,Prompt}
-%%% Prompt = string()
-%%% Tag = term()
-%%% Opts = [Opt]
-%%% 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]
-%%% Match = RxMatch | {Tag,RxMatch} | {prompt,Prompt}
-%%% RxMatch = [string()]
-%%% HaltReason = done | Match
-%%% Reason = timeout | {prompt,Prompt}
-%%%
-%%% @doc Get data from telnet and wait for the expected pattern.
-%%%
-%%% <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>
-%%% where <code>FullMatch</code> is the string matched by the whole
-%%% regular expression and <code>SubMatchN</code> is the string that
-%%% matched subexpression no <code>N</code>. Subexpressions are
-%%% denoted with '(' ')' in the regular expression</p>
-%%%
-%%% <p>If a <code>Tag</code> is given, the returned <code>Match</code>
-%%% will also include the matched <code>Tag</code>. Else, only
-%%% <code>RxMatch</code> is returned.</p>
-%%%
-%%% <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>IdleTimeout</code> milliseconds. Default
-%%% timeout is 10 seconds.</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
-%%% is useful if data sent by the server could include a pattern that
-%%% would match the prompt regexp (as returned by
-%%% <code>TargedMod:get_prompt_regexp/0</code>), but which should not
-%%% cause the function to return.</p>
-%%%
-%%% <p>If the <code>no_prompt_check</code> option is used,
-%%% <code>ct_telnet</code> will not search for a prompt at all. This
-%%% is useful if, for instance, the <code>Pattern</code> itself
-%%% matches the prompt.</p>
-%%%
-%%% <p>The <code>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
-%%% will return with <code>HaltReason = done</code>.</p>
-%%%
-%%% <p>The <code>sequence</code> option indicates that all patterns
-%%% shall be matched in a sequence. A match will not be concluded
-%%% untill all patterns are matched.</p>
-%%%
-%%% <p>Both <code>repeat</code> and <code>sequence</code> can be
-%%% interrupted by one or more <code>HaltPatterns</code>. When
-%%% <code>sequence</code> or <code>repeat</code> is used, there will
-%%% always be a <code>MatchList</code> returned, i.e. a list of
-%%% <code>Match</code> instead of only one <code>Match</code>. There
-%%% will also be a <code>HaltReason</code> returned.</p>
-%%%
-%%% <p><underline>Examples:</underline><br/>
-%%% <code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],</code>
-%%% <code>[sequence,{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match
-%%% "ABC" first and then "XYZ", but if "NNN" appears the function will
-%%% return <code>{error,{nnn,["NNN"]}}</code>. If both "ABC" and "XYZ"
-%%% are matched, the function will return
-%%% <code>{ok,[AbcMatch,XyzMatch]}</code>.</p>
-%%%
-%%% <p><code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],</code>
-%%% <code>[{repeat,2},{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match
-%%% "ABC" or "XYZ" twice. If "NNN" appears the function will return
-%%% with <code>HaltReason = {nnn,["NNN"]}</code>.</p>
-%%%
-%%% <p>The <code>repeat</code> and <code>sequence</code> options can be
-%%% combined in order to match a sequence multiple times.</p>
expect(Connection,Patterns,Opts) ->
case get_handle(Connection) of
{ok,Pid} ->
@@ -593,7 +219,7 @@ expect(Connection,Patterns,Opts) ->
%%%=================================================================
%%% Callback functions
-%% @hidden
+
init(Name,{Ip,Port,Type},{TargetMod,KeepAlive,Extra}) ->
S0 = case ct:get_config(telnet_settings) of
undefined ->
@@ -601,8 +227,18 @@ init(Name,{Ip,Port,Type},{TargetMod,KeepAlive,Extra}) ->
Settings ->
set_telnet_defaults(Settings,#state{})
end,
- case catch TargetMod:connect(Name,Ip,Port,S0#state.conn_to,
- KeepAlive,Extra) of
+ %% Handle old user versions of TargetMod
+ _ = code:ensure_loaded(TargetMod),
+ try
+ case erlang:function_exported(TargetMod,connect,7) of
+ true ->
+ TargetMod:connect(Name,Ip,Port,S0#state.conn_to,
+ KeepAlive,S0#state.tcp_nodelay,Extra);
+ false ->
+ TargetMod:connect(Name,Ip,Port,S0#state.conn_to,
+ KeepAlive,Extra)
+ end
+ of
{ok,TelnPid} ->
put({ct_telnet_pid2name,TelnPid},Name),
S1 = S0#state{host=Ip,
@@ -624,15 +260,18 @@ init(Name,{Ip,Port,Type},{TargetMod,KeepAlive,Extra}) ->
"Connection timeout: ~p\n"
"Keep alive: ~w\n"
"Poll limit: ~w\n"
- "Poll interval: ~w",
+ "Poll interval: ~w\n"
+ "TCP nodelay: ~w",
[Ip,Port,S1#state.com_to,S1#state.reconns,
S1#state.reconn_int,S1#state.conn_to,KeepAlive,
- S1#state.poll_limit,S1#state.poll_interval]),
+ S1#state.poll_limit,S1#state.poll_interval,
+ S1#state.tcp_nodelay]),
{ok,TelnPid,S1};
- {'EXIT',Reason} ->
- {error,Reason};
Error ->
Error
+ catch
+ _:Reason ->
+ {error,Reason}
end.
type(telnet) -> ip;
@@ -652,17 +291,18 @@ set_telnet_defaults([{poll_limit,PL}|Ss],S) ->
set_telnet_defaults(Ss,S#state{poll_limit=PL});
set_telnet_defaults([{poll_interval,PI}|Ss],S) ->
set_telnet_defaults(Ss,S#state{poll_interval=PI});
+set_telnet_defaults([{tcp_nodelay,NoDelay}|Ss],S) ->
+ set_telnet_defaults(Ss,S#state{tcp_nodelay=NoDelay});
set_telnet_defaults([Unknown|Ss],S) ->
force_log(S,error,
- "Bad element in telnet_settings: ~p",[Unknown]),
+ "Bad element in telnet_settings: ~tp",[Unknown]),
set_telnet_defaults(Ss,S);
set_telnet_defaults([],S) ->
S.
-%% @hidden
handle_msg({cmd,Cmd,Opts},State) ->
start_gen_log(heading(cmd,State#state.name)),
- log(State,cmd,"Cmd: ~p",[Cmd]),
+ log(State,cmd,"Cmd: ~tp",[Cmd]),
%% whatever is in the buffer from previous operations
%% will be ignored as we go ahead with this telnet cmd
@@ -670,7 +310,7 @@ handle_msg({cmd,Cmd,Opts},State) ->
debug_cont_gen_log("Throwing Buffer:",[]),
debug_log_lines(State#state.buffer),
- case {State#state.type,State#state.prompt} of
+ _ = case {State#state.type,State#state.prompt} of
{ts,_} ->
silent_teln_expect(State#state.name,
State#state.teln_pid,
@@ -697,7 +337,7 @@ handle_msg({cmd,Cmd,Opts},State) ->
case teln_cmd(State#state.teln_pid, Cmd, State#state.prx,
Newline, TO) of
{ok,Data,_PromptType,Rest} ->
- log(State,recv,"Return: ~p",[{ok,Data}]),
+ log(State,recv,"Return: ~tp",[{ok,Data}]),
{{ok,Data},Rest,true};
Error ->
Retry = {retry,{Error,
@@ -705,19 +345,19 @@ handle_msg({cmd,Cmd,Opts},State) ->
State#state.type},
State#state.teln_pid,
{cmd,Cmd,Opts}}},
- log(State,recv,"Return: ~p",[Error]),
+ log(State,recv,"Return: ~tp",[Error]),
{Retry,[],false}
end,
end_gen_log(),
{Return,State#state{buffer=NewBuffer,prompt=Prompt}};
handle_msg({send,Cmd,Opts},State) ->
start_gen_log(heading(send,State#state.name)),
- log(State,send,"Sending: ~p",[Cmd]),
+ log(State,send,"Sending: ~tp",[Cmd]),
debug_cont_gen_log("Throwing Buffer:",[]),
debug_log_lines(State#state.buffer),
- case {State#state.type,State#state.prompt} of
+ _ = case {State#state.type,State#state.prompt} of
{ts,_} ->
silent_teln_expect(State#state.name,
State#state.teln_pid,
@@ -744,12 +384,12 @@ handle_msg(get_data,State) ->
log(State,cmd,"Reading data...",[]),
{ok,Data,Buffer} = teln_get_all_data(State,State#state.buffer,[],[],
State#state.poll_limit),
- log(State,recv,"Return: ~p",[{ok,Data}]),
+ log(State,recv,"Return: ~tp",[{ok,Data}]),
end_gen_log(),
{{ok,Data},State#state{buffer=Buffer}};
handle_msg({expect,Pattern,Opts},State) ->
start_gen_log(heading(expect,State#state.name)),
- log(State,expect,"Expect: ~p\nOpts = ~p\n",[Pattern,Opts]),
+ log(State,expect,"Expect: ~tp\nOpts = ~tp\n",[Pattern,Opts]),
{Return,NewBuffer,Prompt} =
case teln_expect(State#state.name,
State#state.teln_pid,
@@ -761,15 +401,15 @@ handle_msg({expect,Pattern,Opts},State) ->
P = check_if_prompt_was_reached(Data,[]),
{{ok,Data},Rest,P};
{ok,Data,HaltReason,Rest} ->
- force_log(State,expect,"HaltReason: ~p",[HaltReason]),
+ force_log(State,expect,"HaltReason: ~tp",[HaltReason]),
P = check_if_prompt_was_reached(Data,HaltReason),
{{ok,Data,HaltReason},Rest,P};
{error,Reason,Rest} ->
- force_log(State,expect,"Expect failed\n~p",[{error,Reason}]),
+ force_log(State,expect,"Expect failed\n~tp",[{error,Reason}]),
P = check_if_prompt_was_reached([],Reason),
{{error,Reason},Rest,P};
{error,Reason} ->
- force_log(State,expect,"Expect failed\n~p",[{error,Reason}]),
+ force_log(State,expect,"Expect failed\n~tp",[{error,Reason}]),
P = check_if_prompt_was_reached([],Reason),
{{error,Reason},[],P}
end,
@@ -785,7 +425,6 @@ handle_msg({expect,Pattern,Opts},State) ->
{Return1,State#state{buffer=NewBuffer,prompt=Prompt}}.
-%% @hidden
reconnect({Ip,Port,_Type},State) ->
reconnect(Ip,Port,State#state.reconns,State).
reconnect(Ip,Port,N,State=#state{name=Name,
@@ -793,8 +432,17 @@ reconnect(Ip,Port,N,State=#state{name=Name,
keep_alive=KeepAlive,
extra=Extra,
conn_to=ConnTo,
- reconn_int=ReconnInt}) ->
- case TargetMod:connect(Name,Ip,Port,ConnTo,KeepAlive,Extra) of
+ reconn_int=ReconnInt,
+ tcp_nodelay=NoDelay}) ->
+ %% Handle old user versions of TargetMod
+ ConnResult =
+ case erlang:function_exported(TargetMod,connect,7) of
+ true ->
+ TargetMod:connect(Name,Ip,Port,ConnTo,KeepAlive,NoDelay,Extra);
+ false ->
+ TargetMod:connect(Name,Ip,Port,ConnTo,KeepAlive,Extra)
+ end,
+ case ConnResult of
{ok,NewPid} ->
put({ct_telnet_pid2name,NewPid},Name),
{ok, NewPid, State#state{teln_pid=NewPid}};
@@ -807,7 +455,6 @@ reconnect(Ip,Port,N,State=#state{name=Name,
end.
-%% @hidden
terminate(TelnPid,State) ->
Result = ct_telnet_client:close(TelnPid),
log(State,close,"Telnet connection for ~w closed.",[TelnPid]),
@@ -869,13 +516,12 @@ check_if_prompt_was_reached(_,_) ->
heading(Action,undefined) ->
io_lib:format("~w ~w",[?MODULE,Action]);
heading(Action,Name) ->
- io_lib:format("~w ~w for ~p",[?MODULE,Action,Name]).
+ io_lib:format("~w ~w for ~tp",[?MODULE,Action,Name]).
force_log(State,Action,String,Args) ->
log(State,Action,String,Args,true).
%%%-----------------------------------------------------------------
-%%% @hidden
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) ->
@@ -884,7 +530,6 @@ log(TelnPid,Action,String,Args) when is_pid(TelnPid) ->
log(#state{teln_pid=TelnPid},Action,String,Args,false).
%%%-----------------------------------------------------------------
-%%% @hidden
log(undefined,String,Args) ->
log(#state{},undefined,String,Args,false);
log(Name,String,Args) when is_atom(Name) ->
@@ -893,7 +538,6 @@ log(TelnPid,String,Args) when is_pid(TelnPid) ->
log(#state{teln_pid=TelnPid},undefined,String,Args).
%%%-----------------------------------------------------------------
-%%% @hidden
log(#state{name=Name,teln_pid=TelnPid,host=Host,port=Port},
Action,String,Args,ForcePrint) ->
Name1 = if Name == undefined -> get({ct_telnet_pid2name,TelnPid});
@@ -927,7 +571,7 @@ log(#state{name=Name,teln_pid=TelnPid,host=Host,port=Port},
true ->
ok;
false ->
- ct_gen_conn:cont_log(String,Args)
+ ct_gen_conn:cont_log_no_timestamp(String,Args)
end;
ForcePrint == true ->
@@ -938,13 +582,12 @@ log(#state{name=Name,teln_pid=TelnPid,host=Host,port=Port},
%% called
ct_gen_conn:log(heading(Action,Name1),String,Args);
false ->
- ct_gen_conn:cont_log(String,Args)
+ ct_gen_conn:cont_log_no_timestamp(String,Args)
end
end
end.
%%%-----------------------------------------------------------------
-%%% @hidden
start_gen_log(Heading) ->
%% check if output is suppressed
case ct_util:is_silenced(telnet) of
@@ -953,7 +596,6 @@ start_gen_log(Heading) ->
end.
%%%-----------------------------------------------------------------
-%%% @hidden
end_gen_log() ->
%% check if output is suppressed
case ct_util:is_silenced(telnet) of
@@ -961,7 +603,6 @@ end_gen_log() ->
false -> ct_gen_conn:end_log()
end.
-%%% @hidden
%% Debug printouts.
debug_cont_gen_log(Str,Args) ->
Old = put(silent,true),
@@ -1011,8 +652,7 @@ teln_get_all_data(State=#state{teln_pid=Pid,prx=Prx},Data,Acc,LastLine,Polls) ->
found_prompt=false,
prompt_check=true}).
-%% @hidden
-%% @doc Externally the silent_teln_expect function shall only be used
+%% 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(Name,Pid,Data,Pattern,Prx,Opts) ->
@@ -1021,7 +661,7 @@ silent_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:
@@ -1030,9 +670,9 @@ silent_teln_expect(Name,Pid,Data,Pattern,Prx,Opts) ->
%% 2) Sequence: Several patterns are given, and they are matched in
%% the order they appear in the pattern list.
%% 3a) Repeat (single): 1) is repeated either N times or until a halt
-%% condition is fullfilled.
+%% condition is fulfilled.
%% 3b) Repeat (sequence): 2) is repeated either N times or until a
-%% halt condition is fullfilled.
+%% halt condition is fulfilled.
teln_expect(Name,Pid,Data,Pattern0,Prx,Opts) ->
HaltPatterns =
case get_ignore_prompt(Opts) of
@@ -1176,7 +816,7 @@ wait_for_prompt2(Prompt, Pattern, Opts) ->
{true,Pattern1,Opts1}.
%% Repeat either single or sequence. All match results are accumulated
-%% and returned when a halt condition is fulllfilled.
+%% and returned when a halt condition is fulfilled.
repeat_expect(_Name,_Pid,Rest,_Pattern,Acc,#eo{repeat=0}) ->
{ok,lists:reverse(Acc),done,Rest};
repeat_expect(Name,Pid,Data,Pattern,Acc,EO) ->
@@ -1197,7 +837,6 @@ teln_expect1(Name,Pid,Data,Pattern,Acc,EO=#eo{idle_timeout=IdleTO,
EOMod = if TotalTO /= infinity -> EO#eo{total_timeout=trunc(TotalTO)};
true -> EO
end,
-
ExpectFun = case EOMod#eo.seq of
true -> fun() ->
seq_expect(Name,Pid,Data,Pattern,Acc,EOMod)
@@ -1220,38 +859,34 @@ teln_expect1(Name,Pid,Data,Pattern,Acc,EO=#eo{idle_timeout=IdleTO,
true ->
IdleTO
end,
+ {PatOrPats1,Acc1,Rest1} = case NotFinished of
+ {nomatch,Rest0} ->
+ %% one expect
+ {Pattern,[],Rest0};
+ {continue,Pats0,Acc0,Rest0} ->
+ %% sequence
+ {Pats0,Acc0,Rest0}
+ end,
case timer:tc(ct_gen_conn, do_within_time, [Fun,BreakAfter]) of
- {_,{error,Reason}} ->
+ {_,{error,Reason}} ->
%% A timeout will occur when the telnet connection
%% is idle for EO#eo.idle_timeout milliseconds.
+ if Rest1 /= [] ->
+ log(name_or_pid(Name,Pid)," ~ts",[Rest1]);
+ true ->
+ ok
+ end,
{error,Reason};
{_,{ok,Data1}} when TotalTO == infinity ->
- case NotFinished of
- {nomatch,Rest} ->
- %% One expect
- teln_expect1(Name,Pid,Rest++Data1,
- Pattern,[],EOMod);
- {continue,Patterns1,Acc1,Rest} ->
- %% Sequence
- teln_expect1(Name,Pid,Rest++Data1,
- Patterns1,Acc1,EOMod)
- end;
+ teln_expect1(Name,Pid,Rest1++Data1,PatOrPats1,Acc1,EOMod);
{Elapsed,{ok,Data1}} ->
TVal = TotalTO - (Elapsed/1000),
if TVal =< 0 ->
{error,timeout};
true ->
EO1 = EO#eo{total_timeout = TVal},
- case NotFinished of
- {nomatch,Rest} ->
- %% One expect
- teln_expect1(Name,Pid,Rest++Data1,
- Pattern,[],EO1);
- {continue,Patterns1,Acc1,Rest} ->
- %% Sequence
- teln_expect1(Name,Pid,Rest++Data1,
- Patterns1,Acc1,EO1)
- end
+ teln_expect1(Name,Pid,Rest1++Data1,
+ PatOrPats1,Acc1,EO1)
end
end
end.
@@ -1272,7 +907,7 @@ get_data1(Pid) ->
%% one_expect: split data chunk at prompts
one_expect(Name,Pid,Data,Pattern,EO) when EO#eo.prompt_check==false ->
-% io:format("Raw Data ~p Pattern ~p EO ~p ",[Data,Pattern,EO]),
+% io:format("Raw Data ~tp Pattern ~tp EO ~tp ",[Data,Pattern,EO]),
one_expect1(Name,Pid,Data,Pattern,[],EO#eo{found_prompt=false});
one_expect(Name,Pid,Data,Pattern,EO) ->
case match_prompt(Data,EO#eo.prx) of
@@ -1320,7 +955,7 @@ one_expect1(Name,Pid,Data,Pattern,Rest,EO) ->
%% 2) Sequence.
%% 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 the first pattern in the list.
%% When a match is found, the match result is accumulated, and we keep
%% searching for the next pattern in the list.
@@ -1389,14 +1024,14 @@ match_lines(Name,Pid,Data,Patterns,EO) ->
case one_line(Data,[]) of
{noline,Rest} when FoundPrompt=/=false ->
%% This is the line including the prompt
- case match_line(Name,Pid,Rest,Patterns,FoundPrompt,EO) of
+ case match_line(Name,Pid,Rest,Patterns,FoundPrompt,false,EO) of
nomatch ->
{nomatch,prompt};
{Tag,Match} ->
{Tag,Match,[]}
end;
{noline,Rest} when EO#eo.prompt_check==false ->
- case match_line(Name,Pid,Rest,Patterns,false,EO) of
+ case match_line(Name,Pid,Rest,Patterns,false,false,EO) of
nomatch ->
{nomatch,Rest};
{Tag,Match} ->
@@ -1405,7 +1040,7 @@ match_lines(Name,Pid,Data,Patterns,EO) ->
{noline,Rest} ->
{nomatch,Rest};
{Line,Rest} ->
- case match_line(Name,Pid,Line,Patterns,false,EO) of
+ case match_line(Name,Pid,Line,Patterns,false,true,EO) of
nomatch ->
match_lines(Name,Pid,Rest,Patterns,EO);
{Tag,Match} ->
@@ -1413,45 +1048,50 @@ match_lines(Name,Pid,Data,Patterns,EO) ->
end
end.
-
%% For one line, match each pattern
-match_line(Name,Pid,Line,Patterns,FoundPrompt,EO) ->
- match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,match).
+match_line(Name,Pid,Line,Patterns,FoundPrompt,Terminated,EO) ->
+ match_line(Name,Pid,Line,Patterns,FoundPrompt,Terminated,EO,match).
-match_line(Name,Pid,Line,[prompt|Patterns],false,EO,RetTag) ->
- match_line(Name,Pid,Line,Patterns,false,EO,RetTag);
-match_line(Name,Pid,Line,[prompt|_Patterns],FoundPrompt,_EO,RetTag) ->
+match_line(Name,Pid,Line,[prompt|Patterns],false,Term,EO,RetTag) ->
+ match_line(Name,Pid,Line,Patterns,false,Term,EO,RetTag);
+match_line(Name,Pid,Line,[prompt|_Patterns],FoundPrompt,_Term,_EO,RetTag) ->
log(name_or_pid(Name,Pid)," ~ts",[Line]),
log(name_or_pid(Name,Pid),"PROMPT: ~ts",[FoundPrompt]),
{RetTag,{prompt,FoundPrompt}};
-match_line(Name,Pid,Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_EO,RetTag)
- when PromptType==FoundPrompt ->
+match_line(Name,Pid,Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_Term,
+ _EO,RetTag) when PromptType==FoundPrompt ->
log(name_or_pid(Name,Pid)," ~ts",[Line]),
log(name_or_pid(Name,Pid),"PROMPT: ~ts",[FoundPrompt]),
{RetTag,{prompt,FoundPrompt}};
-match_line(Name,Pid,Line,[{prompt,PromptType}|Patterns],FoundPrompt,EO,RetTag)
+match_line(Name,Pid,Line,[{prompt,PromptType}|Patterns],FoundPrompt,Term,
+ EO,RetTag)
when PromptType=/=FoundPrompt ->
- match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag);
-match_line(Name,Pid,Line,[{Tag,Pattern}|Patterns],FoundPrompt,EO,RetTag) ->
- case re:run(Line,Pattern,[{capture,all,list}]) of
+ match_line(Name,Pid,Line,Patterns,FoundPrompt,Term,EO,RetTag);
+match_line(Name,Pid,Line,[{Tag,Pattern}|Patterns],FoundPrompt,Term,EO,RetTag) ->
+ case re:run(Line,Pattern,[{capture,all,list},unicode]) of
nomatch ->
- match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag);
+ match_line(Name,Pid,Line,Patterns,FoundPrompt,Term,EO,RetTag);
{match,Match} ->
log(name_or_pid(Name,Pid),"MATCH: ~ts",[Line]),
{RetTag,{Tag,Match}}
end;
-match_line(Name,Pid,Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) ->
- case re:run(Line,Pattern,[{capture,all,list}]) of
+match_line(Name,Pid,Line,[Pattern|Patterns],FoundPrompt,Term,EO,RetTag) ->
+ case re:run(Line,Pattern,[{capture,all,list},unicode]) of
nomatch ->
- match_line(Name,Pid,Line,Patterns,FoundPrompt,EO,RetTag);
+ match_line(Name,Pid,Line,Patterns,FoundPrompt,Term,EO,RetTag);
{match,Match} ->
log(name_or_pid(Name,Pid),"MATCH: ~ts",[Line]),
{RetTag,Match}
end;
-match_line(Name,Pid,Line,[],FoundPrompt,EO,match) ->
- match_line(Name,Pid,Line,EO#eo.haltpatterns,FoundPrompt,EO,halt);
-match_line(Name,Pid,Line,[],_FoundPrompt,_EO,halt) ->
+match_line(Name,Pid,Line,[],FoundPrompt,Term,EO,match) ->
+ match_line(Name,Pid,Line,EO#eo.haltpatterns,FoundPrompt,Term,EO,halt);
+%% print any terminated line that can not be matched
+match_line(Name,Pid,Line,[],_FoundPrompt,true,_EO,halt) ->
log(name_or_pid(Name,Pid)," ~ts",[Line]),
+ nomatch;
+%% if there's no line termination, Line is saved as Rest (above) and will
+%% be printed later
+match_line(_Name,_Pid,_Line,[],_FoundPrompt,false,_EO,halt) ->
nomatch.
one_line([$\n|Rest],Line) ->
@@ -1503,8 +1143,6 @@ add_tabs([],[$\n|Acc],LastLine) ->
add_tabs([],[],LastLine) ->
{[],lists:reverse(LastLine)}.
-
-%%% @hidden
teln_receive_until_prompt(Pid,Prx,Timeout) ->
Fun = fun() -> teln_receive_until_prompt(Pid,Prx,[],[]) end,
ct_gen_conn:do_within_time(Fun, Timeout).
@@ -1548,7 +1186,7 @@ split_lines([],Line,Lines) ->
match_prompt(Str,Prx) ->
match_prompt(Str,Prx,[]).
match_prompt(Str,Prx,Acc) ->
- case re:run(Str,Prx) of
+ case re:run(Str,Prx,[unicode]) of
nomatch ->
noprompt;
{match,[{Start,Len}]} ->
diff --git a/lib/common_test/src/ct_telnet_client.erl b/lib/common_test/src/ct_telnet_client.erl
index 0c448e5b35..76e4b9ea70 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-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2017. 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%
%%
@@ -34,12 +35,12 @@
%%-define(debug, true).
--export([open/2, open/3, open/4, open/5, close/1]).
+-export([open/2, open/3, open/4, open/5, open/6, close/1]).
-export([send_data/2, send_data/3, get_data/1]).
-define(TELNET_PORT, 23).
-define(OPEN_TIMEOUT,10000).
--define(IDLE_TIMEOUT,10000).
+-define(IDLE_TIMEOUT,8000).
%% telnet control characters
-define(SE, 240).
@@ -69,19 +70,22 @@
-record(state,{conn_name, get_data, keep_alive=true, log_pos=1}).
open(Server, ConnName) ->
- open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT, true, ConnName).
+ open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT, true, false, ConnName).
open(Server, Port, ConnName) ->
- open(Server, Port, ?OPEN_TIMEOUT, true, ConnName).
+ open(Server, Port, ?OPEN_TIMEOUT, true, false, ConnName).
open(Server, Port, Timeout, ConnName) ->
- open(Server, Port, Timeout, true, ConnName).
+ open(Server, Port, Timeout, true, false, ConnName).
open(Server, Port, Timeout, KeepAlive, ConnName) ->
+ open(Server, Port, Timeout, KeepAlive, false, ConnName).
+
+open(Server, Port, Timeout, KeepAlive, NoDelay, ConnName) ->
Self = self(),
Pid = spawn(fun() ->
init(Self, Server, Port, Timeout,
- KeepAlive, ConnName)
+ KeepAlive, NoDelay, ConnName)
end),
receive
{open,Pid} ->
@@ -113,10 +117,11 @@ get_data(Pid) ->
%%%-----------------------------------------------------------------
%%% Internal functions
-init(Parent, Server, Port, Timeout, KeepAlive, ConnName) ->
- case gen_tcp:connect(Server, Port, [list,{packet,0}], Timeout) of
+init(Parent, Server, Port, Timeout, KeepAlive, NoDelay, ConnName) ->
+ ct_util:mark_process(),
+ case gen_tcp:connect(Server, Port, [list,{packet,0},{nodelay,NoDelay}], Timeout) of
{ok,Sock} ->
- dbg("~p connected to: ~p (port: ~w, keep_alive: ~w)\n",
+ dbg("~tp connected to: ~tp (port: ~w, keep_alive: ~w)\n",
[ConnName,Server,Port,KeepAlive]),
send([?IAC,?DO,?SUPPRESS_GO_AHEAD], Sock, ConnName),
Parent ! {open,self()},
@@ -268,7 +273,7 @@ send(Data, Sock, ConnName) ->
_:_ -> ok
end
end,
- gen_tcp:send(Sock, Data),
+ ok = gen_tcp:send(Sock, Data),
ok.
%% [IAC,IAC] = buffer data value 255
@@ -280,7 +285,7 @@ check_msg(Sock, [?IAC | Cs], Acc) ->
case get_cmd(Cs) of
{Cmd,Cs1} ->
cmd_dbg("Got",Cmd),
- respond_cmd(Cmd, Sock),
+ ok = respond_cmd(Cmd, Sock),
check_msg(Sock, Cs1, Acc);
error ->
Acc
@@ -393,7 +398,7 @@ cmd_dbg(Prefix,Cmd) ->
end.
timestamp() ->
- {MS,S,US} = now(),
+ {MS,S,US} = os:timestamp(),
{{Year,Month,Day}, {Hour,Min,Sec}} =
calendar:now_to_local_time({MS,S,US}),
MilliSec = trunc(US/1000),
diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl
index 10a9bdac67..2c18caf18f 100644
--- a/lib/common_test/src/ct_testspec.erl
+++ b/lib/common_test/src/ct_testspec.erl
@@ -1,31 +1,31 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2018. 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 Framework functions handling test specifications.
-%%%
-%%% <p>This module exports functions that are used within CT to
-%%% scan and parse test specifikations.</p>
-module(ct_testspec).
-export([prepare_tests/1, prepare_tests/2,
collect_tests_from_list/2, collect_tests_from_list/3,
- collect_tests_from_file/2, collect_tests_from_file/3]).
+ collect_tests_from_file/2, collect_tests_from_file/3,
+ get_tests/1]).
+
+-export([testspec_rec2list/1, testspec_rec2list/2]).
-include("ct_util.hrl").
-define(testspec_fields, record_info(fields, testspec)).
@@ -67,13 +67,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 +104,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 +141,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 +157,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 +184,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 +226,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) ->
@@ -321,7 +340,7 @@ create_spec_tree([Spec|Specs],TS,JoinWithNext,Known) ->
create_spec_tree(Specs,TS,JoinWithNext,Known)};
{error,Reason} ->
ReasonStr =
- lists:flatten(io_lib:format("~s",
+ lists:flatten(io_lib:format("~ts",
[file:format_error(Reason)])),
throw({error,{SpecAbsName,ReasonStr}})
end
@@ -428,6 +447,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
@@ -513,7 +533,7 @@ replace_names_in_elems([],Modified,_Defs) ->
replace_names_in_string(Term,Defs=[{Name,Replacement=[Ch|_]}|Ds])
when is_integer(Ch) ->
try re:replace(Term,[$'|atom_to_list(Name)]++"'",
- Replacement,[{return,list}]) of
+ Replacement,[{return,list},unicode]) of
Term -> % no match, proceed
replace_names_in_string(Term,Ds);
Term1 ->
@@ -545,7 +565,7 @@ replace_names_in_node1(NodeStr,Defs=[{Name,Replacement}|Ds]) ->
replace_names_in_node1(NodeStr,Ds);
true ->
case re:replace(NodeStr,atom_to_list(Name),
- ReplStr,[{return,list}]) of
+ ReplStr,[{return,list},unicode]) of
NodeStr -> % no match, proceed
replace_names_in_node1(NodeStr,Ds);
NodeStr1 ->
@@ -780,6 +800,31 @@ list_nodes(#testspec{nodes=NodeRefs}) ->
lists:map(fun({_Ref,Node}) -> Node end, NodeRefs).
+%%%-----------------------------------------------------------------
+%%% Parse the given test specs and return the complete set of specs
+%%% and tests to run/skip.
+%%% [Spec1,Spec2,...] means create separate tests per spec
+%%% [[Spec1,Spec2,...]] means merge all specs into one
+-spec get_tests(Specs) -> {ok,[{Specs,Tests}]} | {error,Reason} when
+ Specs :: [string()] | [[string()]],
+ Tests :: {Node,Run,Skip},
+ Node :: atom(),
+ Run :: {Dir,Suites,Cases},
+ Skip :: {Dir,Suites,Comment} | {Dir,Suites,Cases,Comment},
+ Dir :: string(),
+ Suites :: atom | [atom()] | all,
+ Cases :: atom | [atom()] | all,
+ Comment :: string(),
+ Reason :: term().
+
+get_tests(Specs) ->
+ case collect_tests_from_file(Specs,true) of
+ Tests when is_list(Tests) ->
+ {ok,[{S,prepare_tests(R)} || {S,R} <- Tests]};
+ Error ->
+ Error
+ end.
+
%% -----------------------------------------------------
%% / \
%% | When adding test/config terms, remember to update |
@@ -973,7 +1018,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 +1047,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 +1059,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;
@@ -1048,7 +1097,7 @@ check_term(Term) when is_tuple(Term) ->
true ->
io:format("~nSuspicious term, "
"please check:~n"
- "~p~n", [Term]),
+ "~tp~n", [Term]),
invalid;
false ->
invalid
@@ -1105,6 +1154,11 @@ handle_data(verbosity,Node,VLvls,_Spec) when is_list(VLvls) ->
VLvls1 = lists:map(fun(VLvl = {_Cat,_Lvl}) -> VLvl;
(Lvl) -> {'$unspecified',Lvl} end, VLvls),
[{Node,VLvls1}];
+handle_data(multiply_timetraps,Node,Mult,_Spec) when is_integer(Mult) ->
+ [{Node,Mult}];
+handle_data(scale_timetraps,Node,Scale,_Spec) when Scale == true;
+ Scale == false ->
+ [{Node,Scale}];
handle_data(silent_connections,Node,all,_Spec) ->
[{Node,[all]}];
handle_data(silent_connections,Node,Conn,_Spec) when is_atom(Conn) ->
@@ -1119,9 +1173,12 @@ should_be_added(Tag,Node,_Data,Spec) ->
if
%% list terms *without* possible duplicates here
Tag == logdir; Tag == logopts;
- Tag == basic_html; Tag == label;
- Tag == auto_compile; Tag == abort_if_missing_suites;
+ Tag == basic_html; Tag == esc_chars;
+ Tag == label; Tag == auto_compile;
+ Tag == abort_if_missing_suites;
Tag == stylesheet; Tag == verbosity;
+ Tag == multiply_timetraps;
+ Tag == scale_timetraps;
Tag == silent_connections ->
lists:keymember(ref2node(Node,Spec#testspec.nodes),1,
read_field(Spec,Tag)) == false;
@@ -1149,6 +1206,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 ->
@@ -1232,7 +1307,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.
@@ -1257,7 +1332,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 ->
@@ -1287,7 +1362,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.
@@ -1344,9 +1419,14 @@ skip_groups1(Suite,Groups,Cmt,Suites0) ->
case lists:keysearch(Suite,1,Suites0) of
{value,{Suite,GrAndCases0}} ->
GrAndCases1 = GrAndCases0 ++ SkipGroups,
- insert_in_order({Suite,GrAndCases1},Suites0);
+ insert_in_order({Suite,GrAndCases1},Suites0,replace);
false ->
- insert_in_order({Suite,SkipGroups},Suites0)
+ case Suites0 of
+ [{all,_}=All|Skips]->
+ [All|Skips++[{Suite,SkipGroups}]];
+ _ ->
+ insert_in_order({Suite,SkipGroups},Suites0,replace)
+ end
end.
skip_cases(Node,Dir,Suite,Cases,Cmt,Tests,false) when is_list(Cases) ->
@@ -1355,7 +1435,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}) ->
@@ -1376,32 +1456,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,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({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) ->
@@ -1476,6 +1579,8 @@ valid_terms() ->
{logopts,3},
{basic_html,2},
{basic_html,3},
+ {esc_chars,2},
+ {esc_chars,3},
{verbosity,2},
{verbosity,3},
{silent_connections,2},
diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl
index 56027586d1..d8fd401a64 100644
--- a/lib/common_test/src/ct_util.erl
+++ b/lib/common_test/src/ct_util.erl
@@ -1,27 +1,28 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2003-2018. 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 Framework Utilities.
+%%% Common Test Framework Utilities.
%%%
-%%% <p>This is a support module for the Common Test Framework. It
+%%% This is a support module for the Common Test Framework. It
%%% implements the process ct_util_server which acts like a data
-%%% holder for suite, configuration and connection data.</p>
+%%% holder for suite, configuration and connection data.
%%%
-module(ct_util).
@@ -64,6 +65,9 @@
-export([warn_duplicates/1]).
+-export([mark_process/0, mark_process/1, is_marked/1, is_marked/2,
+ remaining_test_procs/0]).
+
-export([get_profile_data/0, get_profile_data/1,
get_profile_data/2, open_url/3]).
@@ -79,20 +83,20 @@
%%%-----------------------------------------------------------------
start() ->
start(normal, ".", ?default_verbosity).
-%%% @spec start(Mode) -> Pid | exit(Error)
+%%% -spec start(Mode) -> Pid | exit(Error)
%%% Mode = normal | interactive
%%% Pid = pid()
%%%
-%%% @doc Start start the ct_util_server process
+%%% Start start the ct_util_server process
%%% (tool-internal use only).
%%%
-%%% <p>This function is called from ct_run.erl. It starts and initiates
-%%% the <code>ct_util_server</code></p>
+%%% This function is called from ct_run.erl. It starts and initiates
+%%% the ct_util_server
%%%
-%%% <p>Returns the process identity of the
-%%% <code>ct_util_server</code>.</p>
+%%% Returns the process identity of the
+%%% ct_util_server.
%%%
-%%% @see ct
+%%% See ct.
start(LogDir) when is_list(LogDir) ->
start(normal, LogDir, ?default_verbosity);
start(Mode) ->
@@ -125,19 +129,20 @@ start(Mode, LogDir, Verbosity) ->
do_start(Parent, Mode, LogDir, Verbosity) ->
process_flag(trap_exit,true),
register(ct_util_server,self()),
+ mark_process(),
create_table(?conn_table,#conn.handle),
create_table(?board_table,2),
create_table(?suite_table,#suite_data.key),
create_table(?verbosity_table,1),
- [ets:insert(?verbosity_table,{Cat,Lvl}) || {Cat,Lvl} <- Verbosity],
+ _ = [ets:insert(?verbosity_table,{Cat,Lvl}) || {Cat,Lvl} <- Verbosity],
{ok,StartDir} = file:get_cwd(),
case file:set_cwd(LogDir) of
ok -> ok;
E -> exit(E)
end,
- DoExit = fun(Reason) -> file:set_cwd(StartDir), exit(Reason) end,
+ DoExit = fun(Reason) -> ok = file:set_cwd(StartDir), exit(Reason) end,
Opts = case read_opts() of
{ok,Opts1} ->
Opts1;
@@ -168,7 +173,7 @@ do_start(Parent, Mode, LogDir, Verbosity) ->
end,
%% add user event handlers
- case lists:keysearch(event_handler,1,Opts) of
+ _ = case lists:keysearch(event_handler,1,Opts) of
{value,{_,Handlers}} ->
Add = fun({H,Args}) ->
case catch gen_event:add_handler(?CT_EVMGR_REF,H,Args) of
@@ -187,6 +192,8 @@ do_start(Parent, Mode, LogDir, Verbosity) ->
ok
end,
+ ct_default_gl:start_link(group_leader()),
+
{StartTime,TestLogDir} = ct_logs:init(Mode, Verbosity),
ct_event:notify(#event{name=test_start,
@@ -194,26 +201,18 @@ do_start(Parent, Mode, LogDir, Verbosity) ->
data={StartTime,
lists:flatten(TestLogDir)}}),
%% Initialize ct_hooks
- try ct_hooks:init(Opts) of
+ _ = try ct_hooks:init(Opts) of
ok ->
Parent ! {self(),started};
{fail,CTHReason} ->
- ErrorInfo = if is_atom(CTHReason) ->
- io_lib:format("{~p,~p}",
- [CTHReason,
- erlang:get_stacktrace()]);
- true ->
- CTHReason
- end,
- ct_logs:tc_print('Suite Callback',ErrorInfo,[]),
+ ct_logs:tc_print('Suite Callback',CTHReason,[]),
self() ! {{stop,{self(),{user_error,CTHReason}}},
{Parent,make_ref()}}
catch
- _:CTHReason ->
+ _:CTHReason:StackTrace ->
ErrorInfo = if is_atom(CTHReason) ->
- io_lib:format("{~p,~p}",
- [CTHReason,
- erlang:get_stacktrace()]);
+ io_lib:format("{~tp,~tp}",
+ [CTHReason, StackTrace]);
true ->
CTHReason
end,
@@ -227,7 +226,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
@@ -458,6 +458,7 @@ loop(Mode,TestData,StartDir) ->
error:badarg -> []
end,
ct_hooks:terminate(Callbacks),
+
close_connections(ets:tab2list(?conn_table)),
ets:delete(?conn_table),
ets:delete(?board_table),
@@ -471,7 +472,8 @@ loop(Mode,TestData,StartDir) ->
ct_logs:close(Info, StartDir),
ct_event:stop(),
ct_config:stop(),
- file:set_cwd(StartDir),
+ ct_default_gl:stop(),
+ ok = file:set_cwd(StartDir),
return(From, Info);
{Ref, _Msg} when is_reference(Ref) ->
%% This clause is used when doing cast operations.
@@ -484,15 +486,17 @@ loop(Mode,TestData,StartDir) ->
{'EXIT',Pid,Reason} ->
case ets:lookup(?conn_table,Pid) of
[#conn{address=A,callback=CB}] ->
+ ErrorStr = io_lib:format("~tp", [Reason]),
+ ErrorHtml = ct_logs:escape_chars(ErrorStr),
%% A connection crashed - remove the connection but don't die
ct_logs:tc_log_async(ct_error_notify,
?MAX_IMPORTANCE,
"CT Error Notification",
"Connection process died: "
- "Pid: ~w, Address: ~p, "
+ "Pid: ~w, Address: ~tp, "
"Callback: ~w\n"
- "Reason: ~p\n\n",
- [Pid,A,CB,Reason]),
+ "Reason: ~ts\n\n",
+ [Pid,A,CB,ErrorHtml]),
catch CB:close(Pid),
%% in case CB:close failed to do this:
unregister_connection(Pid),
@@ -500,8 +504,8 @@ loop(Mode,TestData,StartDir) ->
_ ->
%% Let process crash in case of error, this shouldn't happen!
io:format("\n\nct_util_server got EXIT "
- "from ~w: ~p\n\n", [Pid,Reason]),
- file:set_cwd(StartDir),
+ "from ~w: ~tp\n\n", [Pid,Reason]),
+ ok = file:set_cwd(StartDir),
exit(Reason)
end
end.
@@ -516,19 +520,19 @@ get_key_from_name(Name)->
ct_config:get_key_from_name(Name).
%%%-----------------------------------------------------------------
-%%% @spec register_connection(TargetName,Address,Callback,Handle) ->
+%%% -spec register_connection(TargetName,Address,Callback,Handle) ->
%%% ok | {error,Reason}
%%% TargetName = ct:target_name()
%%% Address = term()
%%% Callback = atom()
%%% Handle = term
%%%
-%%% @doc Register a new connection (tool-internal use only).
+%%% Register a new connection (tool-internal use only).
%%%
-%%% <p>This function can be called when a new connection is
+%%% This function can be called when a new connection is
%%% established. The connection data is stored in the connection
%%% table, and ct_util will close all registered connections when the
-%%% test is finished by calling <code>Callback:close/1</code>.</p>
+%%% test is finished by calling Callback:close/1.
register_connection(TargetName,Address,Callback,Handle) ->
%% If TargetName is a registered alias for a config
%% variable, use it as reference for the connection,
@@ -549,28 +553,28 @@ register_connection(TargetName,Address,Callback,Handle) ->
ok.
%%%-----------------------------------------------------------------
-%%% @spec unregister_connection(Handle) -> ok
+%%% -spec unregister_connection(Handle) -> ok
%%% Handle = term
%%%
-%%% @doc Unregister a connection (tool-internal use only).
+%%% Unregister a connection (tool-internal use only).
%%%
-%%% <p>This function should be called when a registered connection is
+%%% This function should be called when a registered connection is
%%% closed. It removes the connection data from the connection
-%%% table.</p>
+%%% table.
unregister_connection(Handle) ->
ets:delete(?conn_table,Handle),
ok.
%%%-----------------------------------------------------------------
-%%% @spec does_connection_exist(TargetName,Address,Callback) ->
+%%% -spec does_connection_exist(TargetName,Address,Callback) ->
%%% {ok,Handle} | false
%%% TargetName = ct:target_name()
%%% Address = address
%%% Callback = atom()
%%% Handle = term()
%%%
-%%% @doc Check if a connection already exists.
+%%% Check if a connection already exists.
does_connection_exist(TargetName,Address,Callback) ->
case ct_config:get_key_from_name(TargetName) of
{ok,_Key} ->
@@ -590,7 +594,7 @@ does_connection_exist(TargetName,Address,Callback) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec get_connection(TargetName,Callback) ->
+%%% -spec get_connection(TargetName,Callback) ->
%%% {ok,Connection} | {error,Reason}
%%% TargetName = ct:target_name()
%%% Callback = atom()
@@ -598,8 +602,8 @@ does_connection_exist(TargetName,Address,Callback) ->
%%% Handle = term()
%%% Address = term()
%%%
-%%% @doc Return the connection for <code>Callback</code> on the
-%%% given target (<code>TargetName</code>).
+%%% Return the connection for Callback on the
+%%% given target (TargetName).
get_connection(TargetName,Callback) ->
%% check that TargetName is a registered alias
case ct_config:get_key_from_name(TargetName) of
@@ -620,7 +624,7 @@ get_connection(TargetName,Callback) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec get_connections(ConnPid) ->
+%%% -spec get_connections(ConnPid) ->
%%% {ok,Connections} | {error,Reason}
%%% Connections = [Connection]
%%% Connection = {TargetName,Handle,Callback,Address}
@@ -629,8 +633,8 @@ get_connection(TargetName,Callback) ->
%%% Callback = atom()
%%% Address = term()
%%%
-%%% @doc Get data for all connections associated with a particular
-%%% connection pid (see Callback:init/3).
+%%% Get data for all connections associated with a particular
+%%% connection pid (see Callback:init/3).
get_connections(ConnPid) ->
Conns = ets:tab2list(?conn_table),
lists:flatmap(fun(#conn{targetref=TargetName,
@@ -650,8 +654,7 @@ get_connections(ConnPid) ->
end, Conns).
%%%-----------------------------------------------------------------
-%%% @hidden
-%%% @equiv ct:get_target_name/1
+%%% Equivalent to ct:get_target_name/1
get_target_name(Handle) ->
case ets:select(?conn_table,[{#conn{handle=Handle,targetref='$1',_='_'},
[],
@@ -663,17 +666,14 @@ get_target_name(Handle) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec close_connections() -> ok
+%%% -spec close_connections() -> ok
%%%
-%%% @doc Close all open connections.
+%%% Close all open connections.
close_connections() ->
close_connections(ets:tab2list(?conn_table)),
ok.
%%%-----------------------------------------------------------------
-%%% @spec
-%%%
-%%% @doc
override_silence_all_connections() ->
Protocols = [telnet,ftp,rpc,snmp,ssh],
override_silence_connections(Protocols),
@@ -734,12 +734,12 @@ reset_silent_connections() ->
%%%-----------------------------------------------------------------
-%%% @spec stop(Info) -> ok
+%%% -spec stop(Info) -> ok
%%%
-%%% @doc Stop the ct_util_server and close all existing connections
+%%% Stop the ct_util_server and close all existing connections
%%% (tool-internal use only).
%%%
-%%% @see ct
+%%% See ct.
stop(Info) ->
case whereis(ct_util_server) of
undefined ->
@@ -753,26 +753,25 @@ stop(Info) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec update_last_run_index() -> ok
+%%% -spec update_last_run_index() -> ok
%%%
-%%% @doc Update <code>ct_run.&lt;timestamp&gt;/index.html</code>
+%%% Update ct_run.<timestamp>/index.html
%%% (tool-internal use only).
update_last_run_index() ->
call(update_last_run_index).
%%%-----------------------------------------------------------------
-%%% @spec get_mode() -> Mode
+%%% -spec get_mode() -> Mode
%%% Mode = normal | interactive
%%%
-%%% @doc Return the current mode of the ct_util_server
+%%% Return the current mode of the ct_util_server
%%% (tool-internal use only).
get_mode() ->
call(get_mode).
%%%-----------------------------------------------------------------
-%%% @hidden
-%%% @equiv ct:listenv/1
+%%% Equivalent to ct:listenv/1
listenv(Telnet) ->
case ct_telnet:send(Telnet,"listenv") of
ok ->
@@ -786,33 +785,32 @@ listenv(Telnet) ->
end.
%%%-----------------------------------------------------------------
-%%% @hidden
-%%% @equiv ct:parse_table/1
+%%% Equivalent to ct:parse_table/1
parse_table(Data) ->
{Heading, Rest} = get_headings(Data),
Lines = parse_row(Rest,[],size(Heading)),
{Heading,Lines}.
get_headings(["|" ++ Headings | Rest]) ->
- {remove_space(string:tokens(Headings, "|"),[]), Rest};
+ {remove_space(string:lexemes(Headings, "|"),[]), Rest};
get_headings([_ | Rest]) ->
get_headings(Rest);
get_headings([]) ->
{{},[]}.
parse_row(["|" ++ _ = Row | T], Rows, NumCols) when NumCols > 1 ->
- case string:tokens(Row, "|") of
+ case string:lexemes(Row, "|") of
Values when length(Values) =:= NumCols ->
parse_row(T,[remove_space(Values,[])|Rows], NumCols);
Values when length(Values) < NumCols ->
parse_row([Row ++"\n"++ hd(T) | tl(T)], Rows, NumCols)
end;
-parse_row(["|" ++ _ = Row | T], Rows, 1 = NumCols) ->
- case string:rchr(Row, $|) of
- 1 ->
+parse_row(["|" ++ X = Row | T], Rows, 1 = NumCols) ->
+ case string:find(X, [$|]) of
+ nomatch ->
parse_row([Row ++"\n"++hd(T) | tl(T)], Rows, NumCols);
_Else ->
- parse_row(T, [remove_space(string:tokens(Row,"|"),[])|Rows],
+ parse_row(T, [remove_space(string:lexemes(Row,"|"),[])|Rows],
NumCols)
end;
parse_row([_Skip | T], Rows, NumCols) ->
@@ -821,22 +819,16 @@ parse_row([], Rows, _NumCols) ->
lists:reverse(Rows).
remove_space([Str|Rest],Acc) ->
- remove_space(Rest,[string:strip(string:strip(Str),both,$')|Acc]);
+ remove_space(Rest,[string:trim(string:trim(Str,both,[$\s]),both,[$'])|Acc]);
remove_space([],Acc) ->
list_to_tuple(lists:reverse(Acc)).
%%%-----------------------------------------------------------------
-%%% @spec
-%%%
-%%% @doc
is_test_dir(Dir) ->
- lists:last(string:tokens(filename:basename(Dir), "_")) == "test".
+ lists:last(string:lexemes(filename:basename(Dir), "_")) == "test".
%%%-----------------------------------------------------------------
-%%% @spec
-%%%
-%%% @doc
get_testdir(Dir, all) ->
Abs = abs_name(Dir),
case is_test_dir(Abs) of
@@ -880,9 +872,6 @@ get_testdir(Dir, _) ->
get_testdir(Dir, all).
%%%-----------------------------------------------------------------
-%%% @spec
-%%%
-%%% @doc
get_attached(TCPid) ->
case dbg_iserver:safe_call({get_attpid,TCPid}) of
{ok,AttPid} when is_pid(AttPid) ->
@@ -892,9 +881,6 @@ get_attached(TCPid) ->
end.
%%%-----------------------------------------------------------------
-%%% @spec
-%%%
-%%% @doc
kill_attached(undefined,_AttPid) ->
ok;
kill_attached(_TCPid,undefined) ->
@@ -909,9 +895,6 @@ kill_attached(TCPid,AttPid) ->
%%%-----------------------------------------------------------------
-%%% @spec
-%%%
-%%% @doc
warn_duplicates(Suites) ->
Warn =
fun(Mod) ->
@@ -921,7 +904,8 @@ warn_duplicates(Suites) ->
[] ->
ok;
_ ->
- io:format(user,"~nWARNING! Deprecated function: ~w:sequences/0.~n"
+ io:format(?def_gl,
+ "~nWARNING! Deprecated function: ~w:sequences/0.~n"
" Use group with sequence property instead.~n",[Mod])
end
end,
@@ -929,9 +913,67 @@ warn_duplicates(Suites) ->
ok.
%%%-----------------------------------------------------------------
-%%% @spec
-%%%
-%%% @doc
+mark_process() ->
+ mark_process(system).
+
+mark_process(Type) ->
+ put(ct_process_type, Type).
+
+is_marked(Pid) ->
+ is_marked(Pid, system).
+
+is_marked(Pid, Type) ->
+ case process_info(Pid, dictionary) of
+ {dictionary,List} ->
+ Type == proplists:get_value(ct_process_type, List);
+ undefined ->
+ false
+ end.
+
+remaining_test_procs() ->
+ Procs = processes(),
+ {SharedGL,OtherGLs,Procs2} =
+ lists:foldl(
+ fun(Pid, ProcTypes = {Shared,Other,Procs1}) ->
+ case is_marked(Pid, group_leader) of
+ true ->
+ if not is_pid(Shared) ->
+ case test_server_io:get_gl(true) of
+ Pid ->
+ {Pid,Other,
+ lists:delete(Pid,Procs1)};
+ _ ->
+ {Shared,[Pid|Other],Procs1}
+ end;
+ true -> % SharedGL already found
+ {Shared,[Pid|Other],Procs1}
+ end;
+ false ->
+ case is_marked(Pid) of
+ true ->
+ {Shared,Other,lists:delete(Pid,Procs1)};
+ false ->
+ ProcTypes
+ end
+ end
+ end, {undefined,[],Procs}, Procs),
+
+ AllGLs = [SharedGL | OtherGLs],
+ TestProcs =
+ lists:flatmap(fun(Pid) ->
+ case process_info(Pid, group_leader) of
+ {group_leader,GL} ->
+ case lists:member(GL, AllGLs) of
+ true -> [{Pid,GL}];
+ false -> []
+ end;
+ undefined ->
+ []
+ end
+ end, Procs2),
+ {TestProcs, SharedGL, OtherGLs}.
+
+%%%-----------------------------------------------------------------
get_profile_data() ->
get_profile_data(all).
@@ -975,12 +1017,12 @@ get_profile_data(Profile, Key, StartDir) ->
end,
case Result of
{error,enoent} when Profile /= default ->
- io:format(user, "~nERROR! Missing profile file ~p~n", [File]),
+ io:format(?def_gl, "~nERROR! Missing profile file ~tp~n", [File]),
undefined;
{error,enoent} when Profile == default ->
undefined;
{error,Reason} ->
- io:format(user,"~nERROR! Error in profile file ~p: ~p~n",
+ io:format(?def_gl,"~nERROR! Error in profile file ~tp: ~tp~n",
[WhichFile,Reason]),
undefined;
{ok,Data} ->
@@ -990,8 +1032,8 @@ get_profile_data(Profile, Key, StartDir) ->
_ when is_list(Data) ->
Data;
_ ->
- io:format(user,
- "~nERROR! Invalid profile data in ~p~n",
+ io:format(?def_gl,
+ "~nERROR! Invalid profile data in ~tp~n",
[WhichFile]),
[]
end,
@@ -1031,10 +1073,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).
@@ -1070,15 +1114,15 @@ abs_name2([],Acc) ->
open_url(iexplore, Args, URL) ->
{ok,R} = win32reg:open([read]),
ok = win32reg:change_key(R,"applications\\iexplore.exe\\shell\\open\\command"),
- case win32reg:values(R) of
+ _ = case win32reg:values(R) of
{ok, Paths} ->
Path = proplists:get_value(default, Paths),
- [Cmd | _] = string:tokens(Path, "%"),
+ [Cmd | _] = string:lexemes(Path, "%"),
Cmd1 = Cmd ++ " " ++ Args ++ " " ++ URL,
- io:format(user, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd1]),
+ io:format(?def_gl, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd1]),
open_port({spawn,Cmd1}, []);
_ ->
- io:format("~nNo path to iexplore.exe~n",[])
+ io:format(?def_gl, "~nNo path to iexplore.exe~n",[])
end,
win32reg:close(R),
ok;
@@ -1088,6 +1132,6 @@ open_url(Prog, Args, URL) ->
is_list(Prog) -> Prog
end,
Cmd = ProgStr ++ " " ++ Args ++ " " ++ URL,
- io:format(user, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd]),
+ io:format(?def_gl, "~nOpening ~ts with command:~n ~ts~n", [URL,Cmd]),
open_port({spawn,Cmd},[]),
ok.
diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl
index 845bb55486..039c8168ec 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%
%%
@@ -22,6 +23,7 @@
-define(board_table,ct_boards).
-define(suite_table,ct_suite_data).
-define(verbosity_table,ct_verbosity_table).
+-define(def_gl, ct_default_gl).
-record(conn, {handle,
targetref,
@@ -36,6 +38,7 @@
logdir=["."],
logopts=[],
basic_html=[],
+ esc_chars=[],
verbosity=[],
silent_connections=[],
cover=[],
@@ -55,6 +58,7 @@
create_priv_dir=[],
alias=[],
tests=[],
+ unknown=[],
merge_tests=true}).
-record(cover, {app=none,
diff --git a/lib/common_test/src/ct_webtool.erl b/lib/common_test/src/ct_webtool.erl
index b67a7c2a92..32d4255217 100644
--- a/lib/common_test/src/ct_webtool.erl
+++ b/lib/common_test/src/ct_webtool.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2001-2010. All Rights Reserved.
+%% Copyright Ericsson AB 2001-2018. 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%
%%
@@ -83,27 +84,27 @@
%% Function = {FunctionName,Arity} | FunctionName |
%% {Module, FunctionName, Arity} | {Module,FunctionName}
debug(F) ->
- ttb:tracer(all,[{file,"webtool.trc"}]), % tracing all nodes
- ttb:p(all,[call,timestamp]),
+ {ok, _} = ttb:tracer(all,[{file,"webtool.trc"}]), % tracing all nodes
+ {ok, _} = ttb:p(all,[call,timestamp]),
MS = [{'_',[],[{return_trace},{message,{caller}}]}],
- tp(F,MS),
- ttb:ctp(?MODULE,stop_debug), % don't want tracing of the stop_debug func
+ _ = tp(F,MS),
+ {ok, _} = ttb:ctp(?MODULE,stop_debug), % don't want tracing of the stop_debug func
ok.
tp(local,MS) -> % all functions
ttb:tpl(?MODULE,MS);
tp(global,MS) -> % all exported functions
ttb:tp(?MODULE,MS);
tp([{M,F,A}|T],MS) -> % Other module
- ttb:tpl(M,F,A,MS),
+ {ok, _} = ttb:tpl(M,F,A,MS),
tp(T,MS);
tp([{M,F}|T],MS) when is_atom(F) -> % Other module
- ttb:tpl(M,F,MS),
+ {ok, _} = ttb:tpl(M,F,MS),
tp(T,MS);
tp([{F,A}|T],MS) -> % function/arity
- ttb:tpl(?MODULE,F,A,MS),
+ {ok, _} = ttb:tpl(?MODULE,F,A,MS),
tp(T,MS);
tp([F|T],MS) -> % function
- ttb:tpl(?MODULE,F,MS),
+ {ok, _} = ttb:tpl(?MODULE,F,MS),
tp(T,MS);
tp([],_MS) ->
ok.
@@ -111,18 +112,18 @@ stop_debug() ->
ttb:stop([format]).
debug_app(Mod) ->
- ttb:tracer(all,[{file,"webtool_app.trc"},{handler,{fun out/4,true}}]),
- ttb:p(all,[call,timestamp]),
+ {ok, _} = ttb:tracer(all,[{file,"webtool_app.trc"},{handler,{fun out/4,true}}]),
+ {ok, _} = ttb:p(all,[call,timestamp]),
MS = [{'_',[],[{return_trace},{message,{caller}}]}],
- ttb:tp(Mod,MS),
+ {ok, _} = ttb:tp(Mod,MS),
ok.
out(_,{trace_ts,Pid,call,MFA={M,F,A},{W,_,_},TS},_,S)
when W==webtool;W==mod_esi->
- io:format("~w: (~p)~ncall ~s~n", [TS,Pid,ffunc(MFA)]),
+ io:format("~w: (~p)~ncall ~ts~n", [TS,Pid,ffunc(MFA)]),
[{M,F,length(A)}|S];
out(_,{trace_ts,Pid,return_from,MFA,R,TS},_,[MFA|S]) ->
- io:format("~w: (~p)~nreturned from ~s -> ~p~n", [TS,Pid,ffunc(MFA),R]),
+ io:format("~w: (~p)~nreturned from ~ts -> ~tp~n", [TS,Pid,ffunc(MFA),R]),
S;
out(_,_,_,_) ->
ok.
@@ -144,7 +145,7 @@ script_start([App]) ->
script_start([App,DefaultBrowser]);
script_start([App,Browser]) ->
io:format("Starting webtool...\n"),
- start(),
+ {ok, _} = start(),
AvailableApps = get_applications(),
{OSType,_} = os:type(),
case lists:keysearch(App,1,AvailableApps) of
@@ -158,19 +159,19 @@ script_start([App,Browser]) ->
_ ->
"http://localhost:" ++ PortStr ++ "/" ++ StartPage
end,
- case Browser of
+ _ = case Browser of
none ->
ok;
iexplore when OSType == win32->
io:format("Starting internet explorer...\n"),
{ok,R} = win32reg:open(""),
Key="\\local_machine\\SOFTWARE\\Microsoft\\IE Setup\\Setup",
- win32reg:change_key(R,Key),
+ ok = win32reg:change_key(R,Key),
{ok,Val} = win32reg:value(R,"Path"),
IExplore=filename:join(win32reg:expand(Val),"iexplore.exe"),
os:cmd("\"" ++ IExplore ++ "\" " ++ Url);
_ when OSType == win32 ->
- io:format("Starting ~w...\n",[Browser]),
+ io:format("Starting ~tw...\n",[Browser]),
os:cmd("\"" ++ atom_to_list(Browser) ++ "\" " ++ Url);
B when B==firefox; B==mozilla ->
io:format("Sending URL to ~w...",[Browser]),
@@ -185,7 +186,7 @@ script_start([App,Browser]) ->
{Port,{exit_status,_Error}} ->
io:format(" not running, starting ~w...\n",
[Browser]),
- os:cmd(BStr ++ " " ++ Url),
+ _ = os:cmd(BStr ++ " " ++ Url),
ok
after ?SEND_URL_TIMEOUT ->
io:format(" failed, starting ~w...\n",[Browser]),
@@ -193,7 +194,7 @@ script_start([App,Browser]) ->
os:cmd(BStr ++ " " ++ Url)
end;
_ ->
- io:format("Starting ~w...\n",[Browser]),
+ io:format("Starting ~tw...\n",[Browser]),
os:cmd(atom_to_list(Browser) ++ " " ++ Url)
end,
ok;
@@ -205,7 +206,7 @@ script_start([App,Browser]) ->
usage() ->
io:format("Starting webtool...\n"),
- start(),
+ {ok, _} = start(),
Apps = lists:map(fun({A,_}) -> A end,get_applications()),
io:format(
"\nUsage: start_webtool application [ browser ]\n"
@@ -253,7 +254,12 @@ start(Path,Port) when is_integer(Port)->
start(Path,Data0)->
Data = Data0 ++ rest_of_standard_data(),
- gen_server:start({local,ct_web_tool},ct_webtool,{Path,Data},[]).
+ case gen_server:start({local,ct_web_tool},ct_webtool,{Path,Data},[]) of
+ {error, {already_started, Pid}} ->
+ {ok, Pid};
+ Else ->
+ Else
+ end.
stop()->
gen_server:call(ct_web_tool,stoppit).
@@ -337,6 +343,7 @@ code_change(_,State,_)->
% Start the gen_server
%----------------------------------------------------------------------
init({Path,Config})->
+ ct_util:mark_process(),
case filelib:is_dir(Path) of
true ->
{ok, Table} = get_tool_files_data(),
@@ -373,7 +380,7 @@ print_url(ConfigData)->
Server=proplists:get_value(server_name,ConfigData,"undefined"),
Port=proplists:get_value(port,ConfigData,"undefined"),
{A,B,C,D}=proplists:get_value(bind_address,ConfigData,"undefined"),
- io:format("WebTool is available at http://~s:~w/~n",[Server,Port]),
+ io:format("WebTool is available at http://~ts:~w/~n",[Server,Port]),
io:format("Or http://~w.~w.~w.~w:~w/~n",[A,B,C,D,Port]).
@@ -764,7 +771,7 @@ fill_out(Nr)->
%Controls whether the user selected a tool to start
%----------------------------------------------------------------------
get_tools(Input)->
- case httpd:parse_query(Input) of
+ case uri_string:dissect_query(Input) of
[]->
no_tools;
Tools->
@@ -853,8 +860,8 @@ handle_app({Name,{start,{func,Start,Stop}}},Data,_Pid,Cmd)->
%%! Here the tool disappears from the webtool interface!!
io:format("\n=======ERROR (webtool, line ~w) =======\n"
"Could not start application \'~p\'\n\n"
- "~w:~w(~s) ->\n"
- "~p\n\n",
+ "~w:~tw(~ts) ->\n"
+ "~tp\n\n",
[?LINE,Name,M,F,format_args(A),Exit]),
ets:delete(Data,Name);
_OK->
@@ -877,16 +884,16 @@ handle_app({Name,{start,{child,ChildSpec}}},Data,Pid,Cmd)->
%%! Here the tool disappears from the webtool interface!!
io:format("\n=======ERROR (webtool, line ~w) =======\n"
"Could not start application \'~p\'\n\n"
- "supervisor:start_child(~p,~p) ->\n"
- "~p\n\n",
+ "supervisor:start_child(~p,~tp) ->\n"
+ "~tp\n\n",
[?LINE,Name,Pid,ChildSpec,{error,Reason}]),
ets:delete(Data,Name);
Error ->
%%! Here the tool disappears from the webtool interface!!
io:format("\n=======ERROR (webtool, line ~w) =======\n"
"Could not start application \'~p\'\n\n"
- "supervisor:start_child(~p,~p) ->\n"
- "~p\n\n",
+ "supervisor:start_child(~p,~tp) ->\n"
+ "~tp\n\n",
[?LINE,Name,Pid,ChildSpec,Error]),
ets:delete(Data,Name)
end;
@@ -918,7 +925,7 @@ handle_app({Name,{start,{app,Real_name}}},Data,_Pid,Cmd)->
io:format("\n=======ERROR (webtool, line ~w) =======\n"
"Could not start application \'~p\'\n\n"
"application:start(~p,~p) ->\n"
- "~p\n\n",
+ "~tp\n\n",
[?LINE,Name,Real_name,temporary,Error]),
ets:delete(Data,Name)
end;
@@ -934,7 +941,7 @@ handle_app({Name,Incorrect},Data,_Pid,Cmd)->
%%! Here the tool disappears from the webtool interface!!
io:format("\n=======ERROR (webtool, line ~w) =======\n"
"Could not ~w application \'~p\'\n\n"
- "Incorrect data: ~p\n\n",
+ "Incorrect data: ~tp\n\n",
[?LINE,Cmd,Name,Incorrect]),
ets:delete(Data,Name).
@@ -1196,12 +1203,12 @@ filter_tool_files(Dir,[File|Rest]) ->
%%%-----------------------------------------------------------------
%%% format functions
ffunc({M,F,A}) when is_list(A) ->
- io_lib:format("~w:~w(~s)\n",[M,F,format_args(A)]);
+ io_lib:format("~w:~tw(~ts)\n",[M,F,format_args(A)]);
ffunc({M,F,A}) when is_integer(A) ->
- io_lib:format("~w:~w/~w\n",[M,F,A]).
+ io_lib:format("~w:~tw/~w\n",[M,F,A]).
format_args([]) ->
"";
format_args(Args) ->
- Str = lists:append(["~p"|lists:duplicate(length(Args)-1,",~p")]),
+ Str = lists:append(["~tp"|lists:duplicate(length(Args)-1,",~tp")]),
io_lib:format(Str,Args).
diff --git a/lib/common_test/src/ct_webtool_sup.erl b/lib/common_test/src/ct_webtool_sup.erl
index 1d612a2d18..04fbbf8745 100644
--- a/lib/common_test/src/ct_webtool_sup.erl
+++ b/lib/common_test/src/ct_webtool_sup.erl
@@ -1,18 +1,19 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2001-2009. All Rights Reserved.
+%% Copyright Ericsson AB 2001-2018. 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%
%%
@@ -45,6 +46,7 @@ stop(Pid)->
%% {error, Reason}
%%----------------------------------------------------------------------
init(_StartArgs) ->
+ ct_util:mark_process(),
%%Child1 =
%%Child2 ={webcover_backend,{webcover_backend,start_link,[]},permanent,2000,worker,[webcover_backend]},
%%{ok,{{simple_one_for_one,5,10},[Child1]}}.
diff --git a/lib/common_test/src/cth_conn_log.erl b/lib/common_test/src/cth_conn_log.erl
index 1e60f2751e..7b6f03311a 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-2017. 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%
%%
@@ -23,11 +24,11 @@
%%
%% suite() ->
%% [{ct_hooks, [{cth_conn_log,
-%% [{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}]}].
+%% [{conn_mod(),hook_options()}]}]}].
%%
%% or specified in a configuration file:
%%
-%% {ct_conn_log,[{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}.
+%% {ct_conn_log,[{conn_mod(),hook_options()}]}.
%%
%% The conn_mod() is the common test module implementing the protocol,
%% e.g. ct_netconfc, ct_telnet, etc. This module must log by calling
@@ -53,32 +54,21 @@
-include_lib("common_test/include/ct.hrl").
-export([init/2,
- pre_init_per_testcase/3,
- post_end_per_testcase/4]).
-
-%%----------------------------------------------------------------------
-%% Exported types
-%%----------------------------------------------------------------------
--export_type([hook_options/0,
- log_type/0,
- conn_mod/0]).
+ pre_init_per_testcase/4,
+ post_end_per_testcase/5]).
%%----------------------------------------------------------------------
%% Type declarations
%%----------------------------------------------------------------------
--type hook_options() :: [hook_option()].
-%% Options that can be given to `cth_conn_log' in the `ct_hook' statement.
--type hook_option() :: {log_type,log_type()} |
- {hosts,[ct_gen_conn:key_or_name()]}.
--type log_type() :: raw | pretty | html | silent.
--type conn_mod() :: ct_netconfc | ct_telnet.
+-type hook_options() :: ct:conn_log_options().
+-type log_type() :: ct:conn_log_type().
+-type conn_mod() :: ct:conn_log_mod().
%%----------------------------------------------------------------------
-spec init(Id, HookOpts) -> Result when
Id :: term(),
HookOpts :: hook_options(),
- Result :: {ok,[{conn_mod(),
- {log_type(),[ct_gen_conn:key_or_name()]}}]}.
+ Result :: {ok,[{conn_mod(),{log_type(),[ct:key_or_name()]}}]}.
init(_Id, HookOpts) ->
ConfOpts = ct:get_config(ct_conn_log,[]),
{ok,merge_log_info(ConfOpts,HookOpts)}.
@@ -103,7 +93,7 @@ get_log_opts(Mod,Opts) ->
Hosts = proplists:get_value(hosts,Opts,[]),
{LogType,Hosts}.
-pre_init_per_testcase(TestCase,Config,CthState) ->
+pre_init_per_testcase(_Suite,TestCase,Config,CthState) ->
Logs =
lists:map(
fun({ConnMod,{LogType,Hosts}}) ->
@@ -126,12 +116,12 @@ pre_init_per_testcase(TestCase,Config,CthState) ->
"<table borders=1>"
"<b>" ++ ConnModStr ++ " logs:</b>\n" ++
[io_lib:format(
- "<tr><td>~p</td><td><a href=\"~ts\">~ts</a>"
+ "<tr><td>~tp</td><td><a href=\"~ts\">~ts</a>"
"</td></tr>",
[S,ct_logs:uri(L),filename:basename(L)])
|| {S,L} <- Ls] ++
"</table>",
- io:format(Str,[]),
+ ct:log(Str,[],[no_css]),
{ConnMod,{LogType,Ls}};
_ ->
{ConnMod,{LogType,[]}}
@@ -157,7 +147,7 @@ pre_init_per_testcase(TestCase,Config,CthState) ->
ct_util:update_testdata(?MODULE, Update, [create]),
{Config,CthState}.
-post_end_per_testcase(TestCase,_Config,Return,CthState) ->
+post_end_per_testcase(_Suite,TestCase,_Config,Return,CthState) ->
Update =
fun(PrevUsers) ->
case lists:delete(TestCase, PrevUsers) of
@@ -169,7 +159,7 @@ post_end_per_testcase(TestCase,_Config,Return,CthState) ->
end,
case ct_util:update_testdata(?MODULE, Update) of
deleted ->
- [ct_util:delete_testdata({?MODULE,ConnMod}) ||
+ _ = [ct_util:delete_testdata({?MODULE,ConnMod}) ||
{ConnMod,_} <- CthState],
error_logger:delete_report_handler(ct_conn_log_h);
{error,no_response} ->
diff --git a/lib/common_test/src/cth_log_redirect.erl b/lib/common_test/src/cth_log_redirect.erl
index 61700a2032..4980d1ee4b 100644
--- a/lib/common_test/src/cth_log_redirect.erl
+++ b/lib/common_test/src/cth_log_redirect.erl
@@ -1,47 +1,49 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2011-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2011-2018. 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%
%%
-module(cth_log_redirect).
-%%% @doc Common Test Framework functions handling test specifications.
+%%% Common Test Framework functions handling test specifications.
%%%
-%%% <p>This module redirects sasl and error logger info to common test log.</p>
-%%% @end
-
+%%% This module redirects sasl and error logger info to common test log.
%% CTH Callbacks
-export([id/1, init/2,
pre_init_per_suite/3, pre_end_per_suite/3, post_end_per_suite/4,
- pre_init_per_group/3, post_init_per_group/4,
- pre_end_per_group/3, post_end_per_group/4,
- pre_init_per_testcase/3, post_end_per_testcase/4]).
+ pre_init_per_group/4, post_init_per_group/5,
+ pre_end_per_group/4, post_end_per_group/5,
+ pre_init_per_testcase/4, post_init_per_testcase/5,
+ pre_end_per_testcase/4, post_end_per_testcase/5]).
-%% Event handler Callbacks
--export([init/1,
- handle_event/2, handle_call/2, handle_info/2,
- terminate/1, terminate/2, code_change/3]).
+%% Logger handler and gen_server callbacks
+-export([log/2,
+ init/1,
+ handle_cast/2, handle_call/3,
+ terminate/1, terminate/2]).
%% Other
-export([handle_remote_events/1]).
-include("ct.hrl").
+-include("../../kernel/src/logger_internal.hrl").
--behaviour(gen_event).
+-behaviour(gen_server).
-record(eh_state, {log_func,
curr_suite,
@@ -54,7 +56,8 @@ id(_Opts) ->
?MODULE.
init(?MODULE, _Opts) ->
- error_logger:add_report_handler(?MODULE),
+ ct_util:mark_process(),
+ ok = start_log_handler(),
tc_log_async.
pre_init_per_suite(Suite, Config, State) ->
@@ -69,11 +72,11 @@ post_end_per_suite(_Suite, Config, Return, State) ->
set_curr_func(undefined, Config),
{Return, State}.
-pre_init_per_group(Group, Config, State) ->
+pre_init_per_group(_Suite, Group, Config, State) ->
set_curr_func({group,Group,init_per_group}, Config),
{Config, State}.
-post_init_per_group(Group, Config, Result, tc_log_async) when is_list(Config) ->
+post_init_per_group(_Suite, Group, Config, Result, tc_log_async) when is_list(Config) ->
case lists:member(parallel,proplists:get_value(
tc_group_properties,Config,[])) of
true ->
@@ -81,134 +84,176 @@ post_init_per_group(Group, Config, Result, tc_log_async) when is_list(Config) ->
false ->
{Result, tc_log_async}
end;
-post_init_per_group(_Group, _Config, Result, State) ->
+post_init_per_group(_Suite, _Group, _Config, Result, State) ->
{Result, State}.
-pre_init_per_testcase(TC, Config, State) ->
+pre_init_per_testcase(_Suite, TC, Config, State) ->
set_curr_func(TC, Config),
{Config, State}.
-post_end_per_testcase(_TC, _Config, Result, State) ->
+post_init_per_testcase(_Suite, _TC, _Config, Return, State) ->
+ {Return, State}.
+
+pre_end_per_testcase(_Suite, _TC, Config, State) ->
+ {Config, State}.
+
+post_end_per_testcase(_Suite, _TC, _Config, Result, State) ->
%% Make sure that the event queue is flushed
%% before ending this test case.
- gen_event:call(error_logger, ?MODULE, flush, 300000),
+ gen_server:call(?MODULE, flush, 300000),
{Result, State}.
-pre_end_per_group(Group, Config, {tc_log, Group}) ->
+pre_end_per_group(_Suite, Group, Config, {tc_log, Group}) ->
set_curr_func({group,Group,end_per_group}, Config),
{Config, set_log_func(tc_log_async)};
-pre_end_per_group(Group, Config, State) ->
+pre_end_per_group(_Suite, Group, Config, State) ->
set_curr_func({group,Group,end_per_group}, Config),
{Config, State}.
-post_end_per_group(_Group, Config, Return, State) ->
+post_end_per_group(_Suite, _Group, Config, Return, State) ->
set_curr_func({group,undefined}, Config),
{Return, State}.
-%% Copied and modified from sasl_report_tty_h.erl
-init(_Type) ->
- {ok, #eh_state{log_func = tc_log_async}}.
-
-handle_event({_Type,GL,_Msg}, #eh_state{handle_remote_events = false} = State)
- when node(GL) /= node() ->
- {ok, State};
-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)),
- if is_list(SReport) ->
- SaslHeader = format_header(State),
- ct_logs:LogFunc(sasl, ?STD_IMPORTANCE, SaslHeader, SReport, []);
- true -> %% Report is an atom if no logging is to be done
- ignore
- end
- end,
- EReport = error_logger_tty_h:write_event(
- tag_event(Event),io_lib),
- if is_list(EReport) ->
- ErrHeader = format_header(State),
- ct_logs:LogFunc(error_logger, ?STD_IMPORTANCE, ErrHeader, EReport, []);
- true -> %% Report is an atom if no logging is to be done
- ignore
+start_log_handler() ->
+ case whereis(?MODULE) of
+ undefined ->
+ ChildSpec =
+ #{id=>?MODULE,
+ start=>{gen_server,start_link,[{local,?MODULE},?MODULE,[],[]]},
+ restart=>transient,
+ shutdown=>2000,
+ type=>worker,
+ modules=>[?MODULE]},
+ {ok,_} = supervisor:start_child(logger_sup,ChildSpec);
+ _Pid ->
+ ok
end,
- {ok, State}.
+ ok = logger:add_handler(?MODULE,?MODULE,
+ #{level=>info,
+ formatter=>{?DEFAULT_FORMATTER,
+ ?DEFAULT_FORMAT_CONFIG}}).
-handle_info({'EXIT',User,killed}, State) ->
- case whereis(user) of
- %% init:stop/1/2 has been called, let's finish!
+init([]) ->
+ {ok, #eh_state{log_func = tc_log_async}}.
+
+log(#{msg:={report,Msg},meta:=#{domain:=[otp,sasl]}}=Log,Config) ->
+ case whereis(sasl_sup) of
undefined ->
- remove_handler;
- User ->
- remove_handler;
- _ ->
- {ok,State}
+ ok; % sasl application is not started
+ _Else ->
+ Level =
+ case application:get_env(sasl, errlog_type) of
+ {ok,error} ->
+ error;
+ {ok,_} ->
+ info;
+ undefined ->
+ info
+ end,
+ case Level of
+ error ->
+ case Msg of
+ #{label:={_,progress}} ->
+ ok;
+ _ ->
+ do_log(add_log_category(Log,sasl),Config)
+ end;
+ _ ->
+ do_log(add_log_category(Log,sasl),Config)
+ end
end;
+log(#{meta:=#{domain:=[otp]}}=Log,Config) ->
+ do_log(add_log_category(Log,error_logger),Config);
+log(#{meta:=#{domain:=_}},_) ->
+ ok;
+log(Log,Config) ->
+ do_log(add_log_category(Log,error_logger),Config).
-handle_info(_, State) ->
- {ok,State}.
+add_log_category(#{meta:=Meta}=Log,Category) ->
+ Log#{meta=>Meta#{?MODULE=>#{category=>Category}}}.
-handle_call(flush,State) ->
- {ok, ok, State};
+do_log(Log,Config) ->
+ gen_server:call(?MODULE,{log,Log,Config}).
-handle_call({set_curr_func,{group,Group,Conf},Config},
- State) when is_list(Config) ->
+handle_cast(_, State) ->
+ {noreply,State}.
+
+handle_call({log,#{meta:=#{gl:=GL}},_}, _From,
+ #eh_state{handle_remote_events=false}=State)
+ when node(GL) /= node() ->
+ {reply, ok, State};
+
+handle_call({log,
+ #{meta:=#{?MODULE:=#{category:=Category}}}=Log,
+ #{formatter:={Formatter,FConfig}}},
+ _From,
+ #eh_state{log_func=LogFunc}=State) ->
+ Header = format_header(State),
+ String = Formatter:format(Log,FConfig),
+ case LogFunc of
+ tc_log ->
+ ct_logs:tc_log(Category, ?STD_IMPORTANCE,
+ Header, String, [], []);
+ tc_log_async ->
+ ct_logs:tc_log_async(sasl, ?STD_IMPORTANCE,
+ Header, String, [])
+ end,
+ {reply,ok,State};
+
+handle_call(flush,_From,State) ->
+ {reply, ok, State};
+
+handle_call({set_curr_func,{group,Group,Conf},Config},_From,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}.
+ {reply, ok, State#eh_state{curr_group = Group,
+ curr_func = Conf,
+ parallel_tcs = Parallel}};
+handle_call({set_curr_func,{group,Group,Conf},_SkipOrFail}, _From, State) ->
+ {reply, ok, State#eh_state{curr_group = Group,
+ curr_func = Conf,
+ parallel_tcs = false}};
+handle_call({set_curr_func,{group,undefined},_Config}, _From, State) ->
+ {reply, ok, State#eh_state{curr_group = undefined,
+ curr_func = undefined,
+ parallel_tcs = false}};
+handle_call({set_curr_func,{Suite,Conf},_Config}, _From, State) ->
+ {reply, ok, State#eh_state{curr_suite = Suite,
+ curr_func = Conf,
+ parallel_tcs = false}};
+handle_call({set_curr_func,undefined,_Config}, _From, State) ->
+ {reply, ok, State#eh_state{curr_suite = undefined,
+ curr_func = undefined,
+ parallel_tcs = false}};
+handle_call({set_curr_func,TC,_Config}, _From, State) ->
+ {reply, ok, State#eh_state{curr_func = TC}};
+
+handle_call({set_logfunc,NewLogFunc}, _From, State) ->
+ {reply, NewLogFunc, State#eh_state{log_func = NewLogFunc}};
+
+handle_call({handle_remote_events,Bool}, _From, State) ->
+ {reply, ok, State#eh_state{handle_remote_events = Bool}}.
terminate(_) ->
- error_logger:delete_report_handler(?MODULE),
- [].
+ _ = logger:remove_handler(?MODULE),
+ _ = supervisor:terminate_child(logger_sup,?MODULE),
+ _ = supervisor:delete_child(logger_sup,?MODULE),
+ ok.
terminate(_Arg, _State) ->
ok.
-tag_event(Event) ->
- {calendar:local_time(), Event}.
-
set_curr_func(CurrFunc, Config) ->
- gen_event:call(error_logger, ?MODULE, {set_curr_func, CurrFunc, Config}).
+ gen_server:call(?MODULE, {set_curr_func, CurrFunc, Config}).
set_log_func(Func) ->
- gen_event:call(error_logger, ?MODULE, {set_logfunc, Func}).
+ gen_server:call(?MODULE, {set_logfunc, Func}).
handle_remote_events(Bool) ->
- gen_event:call(error_logger, ?MODULE, {handle_remote_events, Bool}).
+ gen_server:call(?MODULE, {handle_remote_events, Bool}).
%%%-----------------------------------------------------------------
@@ -225,27 +270,24 @@ format_header(#eh_state{curr_suite = Suite,
format_header(#eh_state{curr_suite = Suite,
curr_group = undefined,
curr_func = TcOrConf}) ->
- io_lib:format("System report during ~w:~w/1",
+ io_lib:format("System report during ~w:~tw/1",
[Suite,TcOrConf]);
format_header(#eh_state{curr_suite = Suite,
curr_group = Group,
curr_func = Conf}) when Conf == init_per_group;
Conf == end_per_group ->
- io_lib:format("System report during ~w:~w/2 for ~w",
+ io_lib:format("System report during ~w:~w/2 for ~tw",
[Suite,Conf,Group]);
format_header(#eh_state{curr_suite = Suite,
curr_group = Group,
parallel_tcs = true}) ->
- io_lib:format("System report during ~w in ~w",
+ io_lib:format("System report during ~tw in ~w",
[Group,Suite]);
format_header(#eh_state{curr_suite = Suite,
curr_group = Group,
curr_func = TC}) ->
- io_lib:format("System report during ~w:~w/1 in ~w",
+ io_lib:format("System report during ~w:~tw/1 in ~tw",
[Suite,TC,Group]).
-
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
diff --git a/lib/common_test/src/cth_surefire.erl b/lib/common_test/src/cth_surefire.erl
index bb12171ea7..b0742717ae 100644
--- a/lib/common_test/src/cth_surefire.erl
+++ b/lib/common_test/src/cth_surefire.erl
@@ -1,26 +1,27 @@
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2012-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2012-2018. 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 Framework functions handling test specifications.
+%%% Common Test Framework functions handling test specifications.
%%%
-%%% <p>This module creates a junit report of the test run if plugged in
-%%% as a suite_callback.</p>
+%%% This module creates a junit report of the test run if plugged in
+%%% as a suite_callback.
-module(cth_surefire).
@@ -32,16 +33,16 @@
-export([pre_end_per_suite/3]).
-export([post_end_per_suite/4]).
--export([pre_init_per_group/3]).
--export([post_init_per_group/4]).
--export([pre_end_per_group/3]).
--export([post_end_per_group/4]).
+-export([pre_init_per_group/4]).
+-export([post_init_per_group/5]).
+-export([pre_end_per_group/4]).
+-export([post_end_per_group/5]).
--export([pre_init_per_testcase/3]).
--export([post_end_per_testcase/4]).
+-export([pre_init_per_testcase/4]).
+-export([post_end_per_testcase/5]).
--export([on_tc_fail/3]).
--export([on_tc_skip/3]).
+-export([on_tc_fail/4]).
+-export([on_tc_skip/4]).
-export([terminate/1]).
@@ -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,11 +80,12 @@ 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) when is_tuple(SkipOrFail) ->
+pre_init_per_suite(Suite,SkipOrFail,#state{ test_cases = [] } = State)
+ when is_tuple(SkipOrFail) ->
{SkipOrFail, init_tc(State#state{curr_suite = Suite,
- curr_suite_ts = now()},
+ curr_suite_ts = ?now},
SkipOrFail) };
pre_init_per_suite(Suite,Config,#state{ test_cases = [] } = State) ->
TcLog = proplists:get_value(tc_logfile,Config),
@@ -96,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) ->
@@ -112,40 +116,39 @@ pre_end_per_suite(_Suite,Config,State) ->
post_end_per_suite(_Suite,Config,Result,State) ->
{Result, end_tc(end_per_suite,Config,Result,State)}.
-pre_init_per_group(Group,Config,State) ->
+pre_init_per_group(_Suite,Group,Config,State) ->
{Config, init_tc(State#state{ curr_group = [Group|State#state.curr_group]},
Config)}.
-post_init_per_group(_Group,Config,Result,State) ->
+post_init_per_group(_Suite,_Group,Config,Result,State) ->
{Result, end_tc(init_per_group,Config,Result,State)}.
-pre_end_per_group(_Group,Config,State) ->
+pre_end_per_group(_Suite,_Group,Config,State) ->
{Config, init_tc(State, Config)}.
-post_end_per_group(_Group,Config,Result,State) ->
+post_end_per_group(_Suite,_Group,Config,Result,State) ->
NewState = end_tc(end_per_group, Config, Result, State),
{Result, NewState#state{ curr_group = tl(NewState#state.curr_group)}}.
-pre_init_per_testcase(_TC,Config,State) ->
+pre_init_per_testcase(_Suite,_TC,Config,State) ->
{Config, init_tc(State, Config)}.
-post_end_per_testcase(TC,Config,Result,State) ->
+post_end_per_testcase(_Suite,TC,Config,Result,State) ->
{Result, end_tc(TC,Config, Result,State)}.
-on_tc_fail(_TC, _Res, State = #state{test_cases = []}) ->
+on_tc_fail(_Suite,_TC, _Res, State = #state{test_cases = []}) ->
State;
-on_tc_fail(_TC, Res, State) ->
+on_tc_fail(_Suite,_TC, Res, State) ->
TCs = State#state.test_cases,
TC = hd(TCs),
NewTC = TC#testcase{
result =
- {fail,lists:flatten(io_lib:format("~p",[Res]))} },
+ {fail,lists:flatten(io_lib:format("~tp",[Res]))} },
State#state{ test_cases = [NewTC | tl(TCs)]}.
-on_tc_skip({ConfigFunc,_GrName},{Type,_Reason} = Res, State0)
- when Type == tc_auto_skip; Type == tc_user_skip ->
- on_tc_skip(ConfigFunc, Res, State0);
-on_tc_skip(Tc,{Type,_Reason} = Res, State0) when Type == tc_auto_skip ->
+on_tc_skip(Suite,{ConfigFunc,_GrName}, Res, State) ->
+ on_tc_skip(Suite,ConfigFunc, Res, State);
+on_tc_skip(Suite,Tc, Res, State0) ->
TcStr = atom_to_list(Tc),
State =
case State0#state.test_cases of
@@ -154,24 +157,20 @@ on_tc_skip(Tc,{Type,_Reason} = Res, State0) when Type == tc_auto_skip ->
_ ->
State0
end,
- do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(State,[])));
-on_tc_skip(_Tc, _Res, State = #state{test_cases = []}) ->
- State;
-on_tc_skip(_Tc, Res, State) ->
- do_tc_skip(Res, State).
+ do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(set_suite(Suite,State),[]))).
do_tc_skip(Res, State) ->
TCs = State#state.test_cases,
TC = hd(TCs),
NewTC = TC#testcase{
result =
- {skipped,lists:flatten(io_lib:format("~p",[Res]))} },
+ {skipped,lists:flatten(io_lib:format("~tp",[Res]))} },
State#state{ test_cases = [NewTC | tl(TCs)]}.
init_tc(State, Config) when is_list(Config) == false ->
- 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) ->
@@ -185,16 +184,15 @@ end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite,
Log =
case Log0 of
"" ->
- LowerSuiteName = string:to_lower(atom_to_list(Suite)),
+ LowerSuiteName = string:lowercase(atom_to_list(Suite)),
filename:join(CurrLogDir,LowerSuiteName++"."++Name++".html");
_ ->
Log0
end,
Url = make_url(UrlBase,Log),
ClassName = atom_to_list(Suite),
- PGroup = string:join([ atom_to_list(Group)||
- Group <- lists:reverse(Groups)],"."),
- TimeTakes = io_lib:format("~f",[timer:now_diff(now(),TS) / 1000000]),
+ PGroup = lists:concat(lists:join(".",lists:reverse(Groups))),
+ 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),
@@ -205,11 +203,17 @@ end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite,
result = passed }|
State#state.test_cases],
tc_log = ""}. % so old tc_log is not set if next is on_tc_skip
+
+set_suite(Suite,#state{curr_suite=undefined}=State) ->
+ State#state{curr_suite=Suite, curr_suite_ts=?now};
+set_suite(_,State) ->
+ State.
+
close_suite(#state{ test_cases = [] } = State) ->
State;
close_suite(#state{ test_cases = TCs, url_base = UrlBase } = State) ->
{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),
@@ -224,7 +228,8 @@ close_suite(#state{ test_cases = TCs, url_base = UrlBase } = State) ->
testcases = lists:reverse(TCs),
log = SuiteLog,
url = SuiteUrl},
- State#state{ test_cases = [],
+ State#state{ curr_suite = undefined,
+ test_cases = [],
test_suites = [Suite | State#state.test_suites]}.
terminate(State = #state{ test_cases = [] }) ->
@@ -311,9 +316,9 @@ make_url(undefined,_) ->
make_url(_,[]) ->
undefined;
make_url(UrlBase0,Log) ->
- UrlBase = string:strip(UrlBase0,right,$/),
+ UrlBase = string:trim(UrlBase0,trailing,[$/]),
RelativeLog = get_relative_log_url(Log),
- string:join([UrlBase,RelativeLog],"/").
+ lists:flatten(lists:join($/,[UrlBase,RelativeLog])).
get_test_root(Log) ->
LogParts = filename:split(Log),
@@ -323,7 +328,7 @@ get_relative_log_url(Log) ->
LogParts = filename:split(Log),
Start = length(LogParts)-?log_depth,
Length = ?log_depth+1,
- string:join(lists:sublist(LogParts,Start,Length),"/").
+ lists:flatten(lists:join($/,lists:sublist(LogParts,Start,Length))).
count_tcs([#testcase{name=ConfCase}|TCs],Ok,F,S)
when ConfCase=="init_per_suite";
diff --git a/lib/common_test/src/erl2html2.erl b/lib/common_test/src/erl2html2.erl
new file mode 100644
index 0000000000..7f3eb699de
--- /dev/null
+++ b/lib/common_test/src/erl2html2.erl
@@ -0,0 +1,313 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1997-2016. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%%------------------------------------------------------------------
+%%% Purpose:Convert Erlang files to html.
+%%%------------------------------------------------------------------
+
+-module(erl2html2).
+-export([convert/3, convert/4]).
+
+convert([], _Dest, _InclPath) -> % Fake clause.
+ ok;
+convert(File, Dest, InclPath) ->
+ %% The generated code uses the BGCOLOR attribute in the
+ %% BODY tag, which wasn't valid until HTML 3.2. Also,
+ %% good HTML should either override all colour attributes
+ %% or none of them -- *never* just a few.
+ %%
+ %% FIXME: The colours should *really* be set with
+ %% stylesheets...
+ %%
+ %% The html file is written with the same encoding as the input file.
+ Encoding = encoding(File),
+ Header = ["<!DOCTYPE HTML PUBLIC "
+ "\"-//W3C//DTD HTML 3.2 Final//EN\">\n"
+ "<!-- autogenerated by '",atom_to_list(?MODULE),"'. -->\n"
+ "<html>\n"
+ "<head>\n"
+ "<meta http-equiv=\"Content-Type\" content=\"text/html;"
+ "charset=",html_encoding(Encoding),"\"/></meta>\n"
+ "<title>", to_raw_list(File,Encoding), "</title>\n"
+ "</head>\n\n"
+ "<body bgcolor=\"white\" text=\"black\""
+ " link=\"blue\" vlink=\"purple\" alink=\"red\">\n"],
+ convert(File, Dest, InclPath, Header).
+
+
+convert(File, Dest, InclPath, Header) ->
+ %% statistics(runtime),
+ case parse_file(File, InclPath) of
+ {ok,Functions} ->
+ %% {_, Time1} = statistics(runtime),
+ %% io:format("Parsed file in ~.2f Seconds.~n",[Time1/1000]),
+ case file:open(File,[raw,{read_ahead,10000}]) of
+ {ok,SFd} ->
+ case file:open(Dest,[write,raw]) of
+ {ok,DFd} ->
+ ok = file:write(DFd,[Header,"<pre>\n"]),
+ _Lines = build_html(SFd,DFd,encoding(File),Functions),
+ ok = file:write(DFd,["</pre>\n",footer(),
+ "</body>\n</html>\n"]),
+ %% {_, Time2} = statistics(runtime),
+ %% io:format("Converted ~p lines in ~.2f Seconds.~n",
+ %% [_Lines, Time2/1000]),
+ ok = file:close(SFd),
+ ok = file:close(DFd),
+ ok;
+ Error ->
+ Error
+ end;
+ Error ->
+ Error
+ end;
+ Error ->
+ Error
+ end.
+
+%%%-----------------------------------------------------------------
+%%% Parse the input file to get the line numbers for all function
+%%% definitions. This will be used when creating link targets for each
+%%% function in build_html/5.
+%%%
+%%% All function clauses are also marked in order to allow
+%%% possibly_enhance/2 to write these in bold.
+%%%
+%%% Use expanded preprocessor directives if possible (epp). Only if
+%%% this fails, fall back on using non-expanded code (epp_dodger).
+
+parse_file(File, InclPath) ->
+ case epp:open(File, InclPath, []) of
+ {ok,Epp} ->
+ try parse_preprocessed_file(Epp,File,false) of
+ Forms ->
+ epp:close(Epp),
+ {ok,Forms}
+ catch
+ _:{error,_Reason,true} ->
+ parse_non_preprocessed_file(File);
+ _:{error,_Reason,false} ->
+ {ok,[]}
+ end;
+ Error = {error,_} ->
+ Error
+ end.
+
+parse_preprocessed_file(Epp, File, InCorrectFile) ->
+ case epp:parse_erl_form(Epp) of
+ {ok,Form} ->
+ case Form of
+ {attribute,_,file,{File,_}} ->
+ parse_preprocessed_file(Epp, File, true);
+ {attribute,_,file,{_OtherFile,_}} ->
+ parse_preprocessed_file(Epp, File, false);
+ {function,L,F,A,Cs} when InCorrectFile ->
+ {CLs,LastCL} = find_clause_lines(Cs, []),
+ %% tl(CLs) cause we know the start line already
+ [{atom_to_list(F),A,get_line(L),LastCL} | tl(CLs)] ++
+ parse_preprocessed_file(Epp, File, true);
+ _ ->
+ parse_preprocessed_file(Epp, File, InCorrectFile)
+ end;
+ {error,Reason={_L,epp,{undefined,_Macro,none}}} ->
+ throw({error,Reason,InCorrectFile});
+ {error,_Reason} ->
+ parse_preprocessed_file(Epp, File, InCorrectFile);
+ {warning,_} ->
+ parse_preprocessed_file(Epp, File, InCorrectFile);
+ {eof,_Location} ->
+ []
+ end.
+
+parse_non_preprocessed_file(File) ->
+ case file:open(File, []) of
+ {ok,Epp} ->
+ Forms = parse_non_preprocessed_file(Epp, File, 1),
+ ok = file:close(Epp),
+ {ok,Forms};
+ Error = {error,_E} ->
+ Error
+ end.
+
+parse_non_preprocessed_file(Epp, File, Location) ->
+ case epp_dodger:parse_form(Epp, Location) of
+ {ok,Tree,Location1} ->
+ try erl_syntax:revert(Tree) of
+ {function,L,F,A,Cs} ->
+ {CLs,LastCL} = find_clause_lines(Cs, []),
+ %% tl(CLs) cause we know the start line already
+ [{atom_to_list(F),A,get_line(L),LastCL} | tl(CLs)] ++
+ parse_non_preprocessed_file(Epp, File, Location1);
+ _ ->
+ parse_non_preprocessed_file(Epp, File, Location1)
+ catch
+ _:_ -> parse_non_preprocessed_file(Epp, File, Location1)
+ end;
+ {error,_E,Location1} ->
+ parse_non_preprocessed_file(Epp, File, Location1);
+ {eof,_Location} ->
+ []
+ end.
+
+get_line(Anno) ->
+ erl_anno:line(Anno).
+
+%%%-----------------------------------------------------------------
+%%% Find the line number of the last expression in the function
+find_clause_lines([{clause,CL,_Params,_Op,Exprs}], CLs) -> % last clause
+ case classify_exprs(Exprs) of
+ {anno, Anno} ->
+ {lists:reverse([{clause,get_line(CL)}|CLs]), get_line(Anno)};
+ {tree, Exprs1} ->
+ find_clause_lines([{clause,CL,undefined,undefined,Exprs1}], CLs);
+ unknown ->
+ {lists:reverse([{clause,get_line(CL)}|CLs]), get_line(CL)}
+ end;
+find_clause_lines([{clause,CL,_Params,_Op,_Exprs} | Cs], CLs) ->
+ find_clause_lines(Cs, [{clause,get_line(CL)}|CLs]).
+
+classify_exprs(Exprs) ->
+ case tuple_to_list(lists:last(Exprs)) of
+ [macro,{_var,Anno,_MACRO} | _] ->
+ {anno, Anno};
+ [T,ExprAnno | Exprs1] ->
+ case erl_anno:is_anno(ExprAnno) of
+ true ->
+ {anno, ExprAnno};
+ false when T =:= tree ->
+ {tree, Exprs1};
+ false ->
+ unknown
+ end
+ end.
+
+%%%-----------------------------------------------------------------
+%%% Add a link target for each line and one for each function definition.
+build_html(SFd,DFd,Encoding,FuncsAndCs) ->
+ build_html(SFd,DFd,Encoding,file:read_line(SFd),1,FuncsAndCs,
+ false,undefined).
+
+%% line of last expression in function found
+build_html(SFd,DFd,Enc,{ok,Str},LastL,FuncsAndCs,_IsFuncDef,{F,LastL}) ->
+ LastLineLink = test_server_ctrl:uri_encode(F++"-last_expr",utf8),
+ ok = file:write(DFd,["<a name=\"",
+ to_raw_list(LastLineLink,Enc),"\"/>"]),
+ build_html(SFd,DFd,Enc,{ok,Str},LastL,FuncsAndCs,true,undefined);
+%% function start line found
+build_html(SFd,DFd,Enc,{ok,Str},L0,[{F,A,L0,LastL}|FuncsAndCs],
+ _IsFuncDef,_FAndLastL) ->
+ FALink = test_server_ctrl:uri_encode(F++"-"++integer_to_list(A),utf8),
+ ok = file:write(DFd,["<a name=\"",to_raw_list(FALink,Enc),"\"/>"]),
+ build_html(SFd,DFd,Enc,{ok,Str},L0,FuncsAndCs,true,{F,LastL});
+build_html(SFd,DFd,Enc,{ok,Str},L,[{clause,L}|FuncsAndCs],
+ _IsFuncDef,FAndLastL) ->
+ build_html(SFd,DFd,Enc,{ok,Str},L,FuncsAndCs,true,FAndLastL);
+build_html(SFd,DFd,Enc,{ok,Str},L,FuncsAndCs,IsFuncDef,FAndLastL) ->
+ LStr = line_number(L),
+ Str1 = line(Str,IsFuncDef),
+ ok = file:write(DFd,[LStr,Str1]),
+ build_html(SFd,DFd,Enc,file:read_line(SFd),L+1,FuncsAndCs,false,FAndLastL);
+build_html(_SFd,_DFd,_Enc,eof,L,_FuncsAndCs,_IsFuncDef,_FAndLastL) ->
+ L.
+
+line_number(L) ->
+ LStr = integer_to_list(L),
+ Pred =
+ case length(LStr) of
+ Length when Length < 5 ->
+ lists:duplicate(5-Length,$\s);
+ _ ->
+ []
+ end,
+ ["<a name=\"",LStr,"\"/>",Pred,LStr,": "].
+
+line(Str,IsFuncDef) ->
+ Str1 = htmlize(Str),
+ possibly_enhance(Str1,IsFuncDef).
+
+%%%-----------------------------------------------------------------
+%%% Substitute special characters that should not appear in HTML
+htmlize([$<|Str]) ->
+ [$&,$l,$t,$;|htmlize(Str)];
+htmlize([$>|Str]) ->
+ [$&,$g,$t,$;|htmlize(Str)];
+htmlize([$&|Str]) ->
+ [$&,$a,$m,$p,$;|htmlize(Str)];
+htmlize([$"|Str]) ->
+ [$&,$q,$u,$o,$t,$;|htmlize(Str)];
+htmlize([Ch|Str]) ->
+ [Ch|htmlize(Str)];
+htmlize([]) ->
+ [].
+
+%%%-----------------------------------------------------------------
+%%% Write comments in italic and function definitions in bold.
+possibly_enhance(Str,true) ->
+ case lists:splitwith(fun($() -> false; (_) -> true end, Str) of
+ {_,[]} -> Str;
+ {F,A} -> ["<b>",F,"</b>",A]
+ end;
+possibly_enhance([$%|_]=Str,_) ->
+ ["<i>",Str--"\n","</i>","\n"];
+possibly_enhance([$-|_]=Str,_) ->
+ possibly_enhance(Str,true);
+possibly_enhance(Str,false) ->
+ Str.
+
+%%%-----------------------------------------------------------------
+%%% End of the file
+footer() ->
+ "".
+
+%%%-----------------------------------------------------------------
+%%% Read encoding from source file
+encoding(File) ->
+ case epp:read_encoding(File) of
+ none ->
+ epp:default_encoding();
+ E ->
+ E
+ end.
+
+%%%-----------------------------------------------------------------
+%%% Covert encoding atom to string for use in HTML header
+html_encoding(latin1) ->
+ "iso-8859-1";
+html_encoding(utf8) ->
+ "utf-8".
+
+%%%-----------------------------------------------------------------
+%%% Convert a string to a list of raw printable characters in the
+%%% given encoding. This is necessary since the files (source and
+%%% destination) are both opened in raw mode (default encoding). Byte
+%%% by byte is read from source and written to the destination. This
+%%% conversion is needed when printing data that is not first read
+%%% from the source.
+%%%
+%%% Example: if the encoding of the file is utf8, and we have a string
+%%% containing "Ã¥" = [229], then we need to convert this to [195,165]
+%%% before writing. Note that this conversion is only necessary
+%%% because the destination file is not (necessarily) opened with utf8
+%%% encoding - it is opened with default encoding in order to allow
+%%% raw file mode and byte by byte copying from source.
+to_raw_list(X,latin1) when is_list(X) ->
+ X;
+to_raw_list(X,utf8) when is_list(X) ->
+ binary_to_list(unicode:characters_to_binary(X)).
diff --git a/lib/common_test/src/test_server.erl b/lib/common_test/src/test_server.erl
new file mode 100644
index 0000000000..a896a0551b
--- /dev/null
+++ b/lib/common_test/src/test_server.erl
@@ -0,0 +1,2807 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1996-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+-module(test_server).
+
+-define(DEFAULT_TIMETRAP_SECS, 60).
+
+%%% TEST_SERVER_CTRL INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([run_test_case_apply/1,init_target_info/0,init_valgrind/0]).
+-export([cover_compile/1,cover_analyse/2]).
+
+%%% TEST_SERVER_SUP INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([get_loc/1,set_tc_state/1]).
+
+%%% TEST SUITE INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([lookup_config/2]).
+-export([fail/0,fail/1,format/1,format/2,format/3]).
+-export([capture_start/0,capture_stop/0,capture_get/0]).
+-export([messages_get/0]).
+-export([permit_io/2]).
+-export([hours/1,minutes/1,seconds/1,sleep/1,adjusted_sleep/1,timecall/3]).
+-export([timetrap_scale_factor/0,timetrap/1,get_timetrap_info/0,
+ timetrap_cancel/1,timetrap_cancel/0]).
+-export([m_out_of_n/3,do_times/4,do_times/2]).
+-export([call_crash/3,call_crash/4,call_crash/5]).
+-export([temp_name/1]).
+-export([start_node/3, stop_node/1, wait_for_node/1, is_release_available/1]).
+-export([app_test/1, app_test/2, appup_test/1]).
+-export([is_native/1]).
+-export([comment/1, make_priv_dir/0]).
+-export([os_type/0]).
+-export([run_on_shielded_node/2]).
+-export([is_cover/0,is_debug/0,is_commercial/0]).
+
+-export([break/1,break/2,break/3,continue/0,continue/1]).
+
+%%% DEBUGGER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([valgrind_new_leaks/0, valgrind_format/2,
+ is_valgrind/0]).
+
+%%% PRIVATE EXPORTED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-include("test_server_internal.hrl").
+-include_lib("kernel/include/file.hrl").
+
+init_target_info() ->
+ [$.|Emu] = code:objfile_extension(),
+ {_, OTPRel} = init:script_id(),
+ #target_info{os_family=test_server_sup:get_os_family(),
+ os_type=os:type(),
+ version=erlang:system_info(version),
+ system_version=erlang:system_info(system_version),
+ root_dir=code:root_dir(),
+ emulator=Emu,
+ otp_release=OTPRel,
+ username=test_server_sup:get_username(),
+ cookie=atom_to_list(erlang:get_cookie())}.
+
+init_valgrind() ->
+ valgrind_new_leaks().
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% cover_compile(#cover{app=App,incl=Include,excl=Exclude,cross=Cross}) ->
+%% {ok,#cover{mods=AnalyseModules}} | {error,Reason}
+%%
+%% App = atom() , name of application to be compiled
+%% Exclude = [atom()], list of modules to exclude
+%% Include = [atom()], list of modules outside of App that should be included
+%% in the cover compilation
+%% Cross = [atoms()], list of modules outside of App shat should be included
+%% in the cover compilation, but that shall not be part of
+%% the cover analysis for this application.
+%% AnalyseModules = [atom()], list of successfully compiled modules
+%%
+%% Cover compile the given application. Return {ok,CoverInfo} if
+%% compilation succeeds, else (if application is not found and there
+%% are no modules to compile) {error,application_not_found}.
+
+cover_compile(CoverInfo=#cover{app=none,incl=Include,cross=Cross}) ->
+ CrossMods = lists:flatmap(fun({_,M}) -> M end,Cross),
+ CompileMods = Include++CrossMods,
+ case length(CompileMods) of
+ 0 ->
+ io:fwrite("WARNING: No modules to cover compile!\n\n",[]),
+ {ok, _} = start_cover(), % start cover server anyway
+ {ok,CoverInfo#cover{mods=[]}};
+ N ->
+ io:fwrite("Cover compiling ~w modules - "
+ "this may take some time... ",[N]),
+ do_cover_compile(CompileMods),
+ io:fwrite("done\n\n",[]),
+ {ok,CoverInfo#cover{mods=Include}}
+ end;
+cover_compile(CoverInfo=#cover{app=App,excl=all,incl=Include,cross=Cross}) ->
+ CrossMods = lists:flatmap(fun({_,M}) -> M end,Cross),
+ CompileMods = Include++CrossMods,
+ case length(CompileMods) of
+ 0 ->
+ io:fwrite("WARNING: No modules to cover compile!\n\n",[]),
+ {ok, _} = start_cover(), % start cover server anyway
+ {ok,CoverInfo#cover{mods=[]}};
+ N ->
+ io:fwrite("Cover compiling '~w' (~w files) - "
+ "this may take some time... ",[App,N]),
+ io:format("\nWARNING: All modules in \'~w\' are excluded\n"
+ "Only cover compiling modules in include list "
+ "and the modules\nin the cross cover file:\n"
+ "~tp\n", [App,CompileMods]),
+ do_cover_compile(CompileMods),
+ io:fwrite("done\n\n",[]),
+ {ok,CoverInfo#cover{mods=Include}}
+ end;
+cover_compile(CoverInfo=#cover{app=App,excl=Exclude,
+ incl=Include,cross=Cross}) ->
+ CrossMods = lists:flatmap(fun({_,M}) -> M end,Cross),
+ case code:lib_dir(App) of
+ {error,bad_name} ->
+ case Include++CrossMods of
+ [] ->
+ io:format("\nWARNING: Can't find lib_dir for \'~w\'\n"
+ "Not cover compiling!\n\n",[App]),
+ {error,application_not_found};
+ CompileMods ->
+ io:fwrite("Cover compiling '~w' (~w files) - "
+ "this may take some time... ",
+ [App,length(CompileMods)]),
+ io:format("\nWARNING: Can't find lib_dir for \'~w\'\n"
+ "Only cover compiling modules in include list: "
+ "~tp\n", [App,Include]),
+ do_cover_compile(CompileMods),
+ io:fwrite("done\n\n",[]),
+ {ok,CoverInfo#cover{mods=Include}}
+ end;
+ LibDir ->
+ EbinDir = filename:join([LibDir,"ebin"]),
+ WC = filename:join(EbinDir,"*.beam"),
+ AllMods = module_names(filelib:wildcard(WC)),
+ AnalyseMods = (AllMods ++ Include) -- Exclude,
+ CompileMods = AnalyseMods ++ CrossMods,
+ case length(CompileMods) of
+ 0 ->
+ io:fwrite("WARNING: No modules to cover compile!\n\n",[]),
+ {ok, _} = start_cover(), % start cover server anyway
+ {ok,CoverInfo#cover{mods=[]}};
+ N ->
+ io:fwrite("Cover compiling '~w' (~w files) - "
+ "this may take some time... ",[App,N]),
+ do_cover_compile(CompileMods),
+ io:fwrite("done\n\n",[]),
+ {ok,CoverInfo#cover{mods=AnalyseMods}}
+ end
+ end.
+
+
+module_names(Beams) ->
+ [list_to_atom(filename:basename(filename:rootname(Beam))) || Beam <- Beams].
+
+
+do_cover_compile(Modules) ->
+ {ok, _} = start_cover(),
+ Sticky = prepare_cover_compile(Modules,[]),
+ R = cover:compile_beam(Modules),
+ _ = [warn_compile(Error) || Error <- R,element(1,Error)=/=ok],
+ _ = [code:stick_mod(M) || M <- Sticky],
+ ok.
+
+warn_compile({error,{Reason,Module}}) ->
+ io:fwrite("\nWARNING: Could not cover compile ~ts: ~tp\n",
+ [Module,{error,Reason}]).
+
+%% Make sure all modules are loaded and unstick if sticky
+prepare_cover_compile([M|Ms],Sticky) ->
+ case {code:is_sticky(M),code:is_loaded(M)} of
+ {true,_} ->
+ code:unstick_mod(M),
+ prepare_cover_compile(Ms,[M|Sticky]);
+ {false,false} ->
+ case code:load_file(M) of
+ {module,_} ->
+ prepare_cover_compile([M|Ms],Sticky);
+ Error ->
+ io:fwrite("\nWARNING: Could not load ~w: ~tp\n",[M,Error]),
+ prepare_cover_compile(Ms,Sticky)
+ end;
+ {false,_} ->
+ prepare_cover_compile(Ms,Sticky)
+ end;
+prepare_cover_compile([],Sticky) ->
+ Sticky.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% cover_analyse(Dir,#cover{level=Analyse,mods=Modules,stop=Stop) ->
+%% [{M,{Cov,NotCov,Details}}]
+%%
+%% Dir = string()
+%% Analyse = details | overview
+%% Modules = [atom()], the modules to analyse
+%%
+%% Cover analysis. If Analyse==details analyse_to_file is used.
+%%
+%% If Analyse==overview analyse_to_file is not used, only an overview
+%% containing the number of covered/not covered lines in each module.
+%%
+%% Also, cover data will be exported to a file called all.coverdata in
+%% the given directory.
+%%
+%% Finally, if Stop==true, then cover will be stopped after the
+%% analysis is completed. Stopping cover causes the original (non
+%% cover compiled) modules to be loaded back in. If a process at this
+%% point is still running old code of any of the cover compiled
+%% modules, meaning that is has not done any fully qualified function
+%% call after the cover compilation, the process will now be
+%% killed. To avoid this scenario, it is possible to set Stop=false,
+%% which means that the modules will stay cover compiled. Note that
+%% this is only recommended if the erlang node is being terminated
+%% after the test is completed.
+cover_analyse(Dir,#cover{level=Analyse,mods=Modules,stop=Stop}) ->
+ io:fwrite(user, "Cover analysing... ", []),
+ {ATFOk,ATFFail} =
+ case Analyse of
+ details ->
+ case cover:export(filename:join(Dir,"all.coverdata")) of
+ ok ->
+ {result,Ok1,Fail1} =
+ cover:analyse_to_file(Modules,[{outdir,Dir},html]),
+ {lists:map(fun(OutFile) ->
+ M = list_to_atom(
+ filename:basename(
+ filename:rootname(OutFile,
+ ".COVER.html")
+ )
+ ),
+ {M,{file,OutFile}}
+ end, Ok1),
+ lists:map(fun({Reason,M}) ->
+ {M,{error,Reason}}
+ end, Fail1)};
+ Error ->
+ {[],lists:map(fun(M) -> {M,Error} end, Modules)}
+ end;
+ overview ->
+ case cover:export(filename:join(Dir,"all.coverdata")) of
+ ok ->
+ {[],lists:map(fun(M) -> {M,undefined} end, Modules)};
+ Error ->
+ {[],lists:map(fun(M) -> {M,Error} end, Modules)}
+ end
+ end,
+ {result,AOk,AFail} = cover:analyse(Modules,module),
+ R0 = merge_analysis_results(AOk,ATFOk++ATFFail,[]) ++
+ [{M,{error,Reason}} || {Reason,M} <- AFail],
+ R = lists:sort(R0),
+ io:fwrite(user, "done\n\n", []),
+
+ case Stop of
+ true ->
+ Sticky = unstick_all_sticky(node()),
+ cover:stop(),
+ stick_all_sticky(node(),Sticky);
+ false ->
+ ok
+ end,
+ R.
+
+merge_analysis_results([{M,{Cov,NotCov}}|T],ATF,Acc) ->
+ case lists:keytake(M,1,ATF) of
+ {value,{_,R},ATF1} ->
+ merge_analysis_results(T,ATF1,[{M,{Cov,NotCov,R}}|Acc]);
+ false ->
+ merge_analysis_results(T,ATF,Acc)
+ end;
+merge_analysis_results([],_,Acc) ->
+ Acc.
+
+do_cover_for_node(Node,CoverFunc) ->
+ do_cover_for_node(Node,CoverFunc,true).
+do_cover_for_node(Node,CoverFunc,StickUnstick) ->
+ %% In case a slave node is starting another slave node! I.e. this
+ %% function is executed on a slave node - then the cover function
+ %% must be executed on the master node. This is for instance the
+ %% case in test_server's own tests.
+ MainCoverNode = cover:get_main_node(),
+ Sticky =
+ if StickUnstick -> unstick_all_sticky(MainCoverNode,Node);
+ true -> ok
+ end,
+ rpc:call(MainCoverNode,cover,CoverFunc,[Node]),
+ if StickUnstick -> stick_all_sticky(Node,Sticky);
+ true -> ok
+ end.
+
+unstick_all_sticky(Node) ->
+ unstick_all_sticky(node(),Node).
+unstick_all_sticky(MainCoverNode,Node) ->
+ lists:filter(
+ fun(M) ->
+ case code:is_sticky(M) of
+ true ->
+ rpc:call(Node,code,unstick_mod,[M]),
+ true;
+ false ->
+ false
+ end
+ end,
+ rpc:call(MainCoverNode,cover,modules,[])).
+
+stick_all_sticky(Node,Sticky) ->
+ lists:foreach(
+ fun(M) ->
+ rpc:call(Node,code,stick_mod,[M])
+ end,
+ Sticky).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% run_test_case_apply(Mod,Func,Args,Name,RunInit,TimetrapData) ->
+%% {Time,Value,Loc,Opts,Comment} | {died,Reason,unknown,Comment}
+%%
+%% Time = float() (seconds)
+%% Value = term()
+%% Loc = term()
+%% Comment = string()
+%% Reason = term()
+%%
+%% Spawns off a process (case process) that actually runs the test suite.
+%% The case process will have the job process as group leader, which makes
+%% it possible to capture all it's output from io:format/2, etc.
+%%
+%% The job process then sits down and waits for news from the case process.
+%%
+%% Returns a tuple with the time spent (in seconds) in the test case,
+%% the return value from the test case or an {'EXIT',Reason} if the case
+%% failed, Loc points out where the test case crashed (if it did). Loc
+%% is either the name of the function, or {<Module>,<Line>} of the last
+%% line executed that had a ?line macro. If the test case did execute
+%% erase/0 or similar, it may be empty. Comment is the last comment added
+%% by test_server:comment/1, the reason if test_server:fail has been
+%% called or the comment given by the return value {comment,Comment} from
+%% a test case.
+%%
+%% {died,Reason,unknown,Comment} is returned if the test case was killed
+%% by some other process. Reason is the kill reason provided.
+%%
+%% TimetrapData = {MultiplyTimetrap,ScaleTimetrap}, which indicates a
+%% possible extension of all timetraps. Timetraps will be multiplied by
+%% MultiplyTimetrap. If it is infinity, no timetraps will be started at all.
+%% ScaleTimetrap indicates if test_server should attemp to automatically
+%% compensate timetraps for runtime delays introduced by e.g. tools like
+%% cover.
+
+run_test_case_apply({CaseNum,Mod,Func,Args,Name,RunInit,TimetrapData}) ->
+ case is_valgrind() of
+ false ->
+ ok;
+ true ->
+ valgrind_format("Test case #~w ~w:~w/1", [CaseNum, Mod, Func]),
+ os:putenv("VALGRIND_LOGFILE_INFIX",atom_to_list(Mod)++"."++
+ atom_to_list(Func)++"-")
+ end,
+ ProcBef = erlang:system_info(process_count),
+ Result = run_test_case_apply(Mod, Func, Args, Name, RunInit,
+ TimetrapData),
+ ProcAft = erlang:system_info(process_count),
+ valgrind_new_leaks(),
+ DetFail = get(test_server_detected_fail),
+ {Result,DetFail,ProcBef,ProcAft}.
+
+-type tc_status() :: 'starting' | 'running' | 'init_per_testcase' |
+ 'end_per_testcase' | {'framework',atom(),atom()} |
+ 'tc'.
+-record(st,
+ {
+ ref :: reference(),
+ pid :: pid(),
+ mf :: {atom(),atom()},
+ last_known_loc :: term(),
+ status :: tc_status() | 'undefined',
+ ret_val :: term(),
+ comment :: list(char()),
+ timeout :: non_neg_integer() | 'infinity',
+ config :: list() | 'undefined',
+ end_conf_pid :: pid() | 'undefined'
+ }).
+
+run_test_case_apply(Mod, Func, Args, Name, RunInit, TimetrapData) ->
+ print_timestamp(minor,"Started at "),
+ print(minor, "", [], internal_raw),
+ TCCallback = get(test_server_testcase_callback),
+ LogOpts = get(test_server_logopts),
+ Ref = make_ref(),
+ Pid =
+ spawn_link(
+ run_test_case_eval_fun(Mod, Func, Args, Name, Ref,
+ RunInit, TimetrapData,
+ LogOpts, TCCallback)),
+ put(test_server_detected_fail, []),
+ St = #st{ref=Ref,pid=Pid,mf={Mod,Func},last_known_loc=unknown,
+ status=starting,ret_val=[],comment="",timeout=infinity,
+ config=hd(Args)},
+ ct_util:mark_process(),
+ run_test_case_msgloop(St).
+
+%% Ugly bug (pre R5A):
+%% If this process (group leader of the test case) terminates before
+%% all messages have been replied back to the io server, the io server
+%% hangs. Fixed by the 20 milli timeout check here, and by using monitor in
+%% io.erl.
+%%
+%% A test case is known to have failed if it returns {'EXIT', _} tuple,
+%% or sends a message {failed, File, Line} to it's group_leader
+%%
+run_test_case_msgloop(#st{ref=Ref,pid=Pid,end_conf_pid=EndConfPid0}=St0) ->
+ receive
+ {set_tc_state=Tag,From,{Status,Config0}} ->
+ Config = case Config0 of
+ unknown -> St0#st.config;
+ _ -> Config0
+ end,
+ St = St0#st{status=Status,config=Config},
+ From ! {self(),Tag,ok},
+ run_test_case_msgloop(St);
+ {abort_current_testcase,_,_}=Abort when St0#st.status =:= starting ->
+ %% we're in init phase, must must postpone this operation
+ %% until test case execution is in progress (or FW:init_tc
+ %% gets killed)
+ self() ! Abort,
+ erlang:yield(),
+ run_test_case_msgloop(St0);
+ {abort_current_testcase,Reason,From} ->
+ Line = case is_process_alive(Pid) of
+ true -> get_loc(Pid);
+ false -> unknown
+ end,
+ Mon = erlang:monitor(process, Pid),
+ exit(Pid,{testcase_aborted,Reason,Line}),
+ erlang:yield(),
+ From ! {self(),abort_current_testcase,ok},
+ St = receive
+ {'DOWN', Mon, process, Pid, _} ->
+ St0
+ after 10000 ->
+ %% Pid is probably trapping exits, hit it harder...
+ exit(Pid, kill),
+ %% here's the only place we know Reason, so we save
+ %% it as a comment, potentially replacing user data
+ Error = lists:flatten(io_lib:format("Aborted: ~tp",
+ [Reason])),
+ Error1 = lists:flatten([string:trim(S,leading,"\s") ||
+ S <- string:lexemes(Error,
+ [$\n])]),
+ ErrorLength = string:length(Error1),
+ Comment = if ErrorLength > 63 ->
+ string:slice(Error1,0,60) ++ "...";
+ true ->
+ Error1
+ end,
+ St0#st{comment=Comment}
+ end,
+ run_test_case_msgloop(St);
+ {sync_apply,From,MFA} ->
+ do_sync_apply(false,From,MFA),
+ run_test_case_msgloop(St0);
+ {sync_apply_proxy,Proxy,From,MFA} ->
+ do_sync_apply(Proxy,From,MFA),
+ run_test_case_msgloop(St0);
+ {comment,NewComment0} ->
+ NewComment1 = test_server_ctrl:to_string(NewComment0),
+ NewComment = test_server_sup:framework_call(format_comment,
+ [NewComment1],
+ NewComment1),
+ run_test_case_msgloop(St0#st{comment=NewComment});
+ {read_comment,From} ->
+ From ! {self(),read_comment,St0#st.comment},
+ run_test_case_msgloop(St0);
+ {make_priv_dir,From} ->
+ Config = case St0#st.config of
+ undefined -> [];
+ Config0 -> Config0
+ end,
+ Result =
+ case proplists:get_value(priv_dir, Config) of
+ undefined ->
+ {error,no_priv_dir_in_config};
+ PrivDir ->
+ case file:make_dir(PrivDir) of
+ ok ->
+ ok;
+ {error, eexist} ->
+ ok;
+ MkDirError ->
+ {error,{MkDirError,PrivDir}}
+ end
+ end,
+ From ! {self(),make_priv_dir,Result},
+ run_test_case_msgloop(St0);
+ {'EXIT',Pid,{Ref,Time,Value,Loc,Opts}} ->
+ RetVal = {Time/1000000,Value,Loc,Opts},
+ St = setup_termination(RetVal, St0#st{config=undefined}),
+ run_test_case_msgloop(St);
+ {'EXIT',Pid,Reason} ->
+ %% This exit typically happens when an unknown external process
+ %% has caused a test case process to terminate (e.g. if a linked
+ %% process has crashed).
+ St =
+ case Reason of
+ {What,[Loc0={_M,_F,A,[{file,_}|_]}|_]} when
+ is_integer(A) ->
+ Loc = rewrite_loc_item(Loc0),
+ handle_tc_exit(What, St0#st{last_known_loc=[Loc]});
+ {What,[Details,Loc0={_M,_F,A,[{file,_}|_]}|_]} when
+ is_integer(A) ->
+ Loc = rewrite_loc_item(Loc0),
+ handle_tc_exit({What,Details}, St0#st{last_known_loc=[Loc]});
+ _ ->
+ handle_tc_exit(Reason, St0)
+ end,
+ run_test_case_msgloop(St);
+ {EndConfPid0,{call_end_conf,Data,_Result}} ->
+ #st{mf={Mod,Func},config=CurrConf} = St0,
+ case CurrConf of
+ _ when is_list(CurrConf) ->
+ {_Mod,_Func,TCPid,TCExitReason,Loc} = Data,
+ spawn_fw_call(Mod,Func,CurrConf,TCPid,
+ TCExitReason,Loc,self()),
+ St = St0#st{config=undefined,end_conf_pid=undefined},
+ run_test_case_msgloop(St);
+ _ ->
+ run_test_case_msgloop(St0)
+ end;
+ {_FwCallPid,fw_notify_done,{T,Value,Loc,Opts,AddToComment}} ->
+ %% the framework has been notified, we're finished
+ RetVal = {T,Value,Loc,Opts},
+ Comment0 = St0#st.comment,
+ Comment = case AddToComment of
+ undefined ->
+ Comment0;
+ _ ->
+ if Comment0 =:= "" ->
+ AddToComment;
+ true ->
+ Comment0 ++
+ test_server_ctrl:xhtml("<br>",
+ "<br />") ++
+ AddToComment
+ end
+ end,
+ St = setup_termination(RetVal, St0#st{comment=Comment,
+ config=undefined}),
+ run_test_case_msgloop(St);
+ {'EXIT',_FwCallPid,{fw_notify_done,Func,Error}} ->
+ %% a framework function failed
+ CB = os:getenv("TEST_SERVER_FRAMEWORK"),
+ Loc = case CB of
+ FW when FW =:= false; FW =:= "undefined" ->
+ [{test_server,Func}];
+ _ ->
+ [{list_to_atom(CB),Func}]
+ end,
+ RetVal = {died,{framework_error,Loc,Error},Loc},
+ St = setup_termination(RetVal, St0#st{comment="Framework error",
+ config=undefined}),
+ run_test_case_msgloop(St);
+ {failed,File,Line} ->
+ put(test_server_detected_fail,
+ [{File, Line}| get(test_server_detected_fail)]),
+ run_test_case_msgloop(St0);
+
+ {user_timetrap,Pid,_TrapTime,StartTime,E={user_timetrap_error,_},_} ->
+ case update_user_timetraps(Pid, StartTime) of
+ proceed ->
+ self() ! {abort_current_testcase,E,Pid},
+ ok;
+ ignore ->
+ ok
+ end,
+ run_test_case_msgloop(St0);
+ {user_timetrap,Pid,TrapTime,StartTime,ElapsedTime,Scale} ->
+ %% a user timetrap is triggered, ignore it if new
+ %% timetrap has been started since
+ case update_user_timetraps(Pid, StartTime) of
+ proceed ->
+ TotalTime = if is_integer(TrapTime) ->
+ TrapTime + ElapsedTime;
+ true ->
+ TrapTime
+ end,
+ _ = timetrap(TrapTime, TotalTime, Pid, Scale),
+ ok;
+ ignore ->
+ ok
+ end,
+ run_test_case_msgloop(St0);
+ {timetrap_cancel_one,Handle,_From} ->
+ timetrap_cancel_one(Handle, false),
+ run_test_case_msgloop(St0);
+ {timetrap_cancel_all,TCPid,_From} ->
+ timetrap_cancel_all(TCPid, false),
+ run_test_case_msgloop(St0);
+ {get_timetrap_info,From,TCPid} ->
+ Info = get_timetrap_info(TCPid, false),
+ From ! {self(),get_timetrap_info,Info},
+ run_test_case_msgloop(St0);
+ _Other when not is_tuple(_Other) ->
+ %% ignore anything not generated by test server
+ run_test_case_msgloop(St0);
+ _Other when element(1, _Other) /= 'EXIT',
+ element(1, _Other) /= started,
+ element(1, _Other) /= finished,
+ element(1, _Other) /= print ->
+ %% ignore anything not generated by test server
+ run_test_case_msgloop(St0)
+ after St0#st.timeout ->
+ #st{ret_val=RetVal,comment=Comment} = St0,
+ erlang:append_element(RetVal, Comment)
+ end.
+
+setup_termination(RetVal, #st{pid=Pid}=St) ->
+ timetrap_cancel_all(Pid, false),
+ St#st{ret_val=RetVal,timeout=20}.
+
+set_tc_state(State) ->
+ set_tc_state(State,unknown).
+set_tc_state(State, Config) ->
+ tc_supervisor_req(set_tc_state, {State,Config}).
+
+handle_tc_exit(killed, St) ->
+ %% probably the result of an exit(TestCase,kill) call, which is the
+ %% only way to abort a testcase process that traps exits
+ %% (see abort_current_testcase).
+ #st{config=Config,mf={Mod,Func},pid=Pid} = St,
+ Msg = testcase_aborted_or_killed,
+ spawn_fw_call(Mod, Func, Config, Pid, Msg, unknown, self()),
+ St;
+handle_tc_exit({testcase_aborted,{user_timetrap_error,_}=Msg,_}, St) ->
+ #st{config=Config,mf={Mod,Func},pid=Pid} = St,
+ spawn_fw_call(Mod, Func, Config, Pid, Msg, unknown, self()),
+ St;
+handle_tc_exit(Reason, #st{status={framework,FwMod,FwFunc},
+ config=Config,pid=Pid}=St) ->
+ R = case Reason of
+ {timetrap_timeout,TVal,_} ->
+ {timetrap,TVal};
+ {testcase_aborted=E,AbortReason,_} ->
+ {E,AbortReason};
+ {fw_error,{FwMod,FwFunc,FwError}} ->
+ FwError;
+ Other ->
+ Other
+ end,
+ Error = {framework_error,R},
+ spawn_fw_call(FwMod, FwFunc, Config, Pid, Error, unknown, self()),
+ St;
+handle_tc_exit(Reason, #st{status=tc,config=Config0,mf={Mod,Func},pid=Pid}=St)
+ when is_list(Config0) ->
+ {R,Loc1,F} = case Reason of
+ {timetrap_timeout=E,TVal,Loc0} ->
+ {{E,TVal},Loc0,E};
+ {testcase_aborted=E,AbortReason,Loc0} ->
+ Msg = {E,AbortReason},
+ {Msg,Loc0,Msg};
+ Other ->
+ {{'EXIT',Other},unknown,Other}
+ end,
+ Timeout = end_conf_timeout(Reason, St),
+ Config = [{tc_status,{failed,F}}|Config0],
+ EndConfPid = call_end_conf(Mod, Func, Pid, R, Loc1, Config, Timeout),
+ St#st{end_conf_pid=EndConfPid};
+handle_tc_exit(Reason, #st{config=Config,mf={Mod,Func0},pid=Pid,
+ status=Status}=St) ->
+ {R,Loc1} = case Reason of
+ {timetrap_timeout=E,TVal,Loc0} ->
+ {{E,TVal},Loc0};
+ {testcase_aborted=E,AbortReason,Loc0} ->
+ {{E,AbortReason},Loc0};
+ Other ->
+ {{'EXIT',Other},St#st.last_known_loc}
+ end,
+ Func = case Status of
+ init_per_testcase=F -> {F,Func0};
+ end_per_testcase=F -> {F,Func0};
+ _ -> Func0
+ end,
+ spawn_fw_call(Mod, Func, Config, Pid, R, Loc1, self()),
+ St.
+
+end_conf_timeout({timetrap_timeout,Timeout,_}, _) ->
+ Timeout;
+end_conf_timeout(_, #st{config=Config}) when is_list(Config) ->
+ proplists:get_value(default_timeout, Config, ?DEFAULT_TIMETRAP_SECS*1000);
+end_conf_timeout(_, _) ->
+ ?DEFAULT_TIMETRAP_SECS*1000.
+
+call_end_conf(Mod,Func,TCPid,TCExitReason,Loc,Conf,TVal) ->
+ Starter = self(),
+ Data = {Mod,Func,TCPid,TCExitReason,Loc},
+ case erlang:function_exported(Mod,end_per_testcase,2) of
+ false ->
+ spawn_link(fun() ->
+ Starter ! {self(),{call_end_conf,Data,ok}}
+ end);
+ true ->
+ do_call_end_conf(Starter,Mod,Func,Data,TCExitReason,Conf,TVal)
+ end.
+
+do_call_end_conf(Starter,Mod,Func,Data,TCExitReason,Conf,TVal) ->
+ EndConfProc =
+ fun() ->
+ process_flag(trap_exit,true), % to catch timetraps
+ Supervisor = self(),
+ EndConfApply =
+ fun() ->
+ _ = timetrap(TVal),
+ %% We can't handle fails or skips here
+ %% (neither input nor output). The error can
+ %% be read from Conf though (tc_status).
+ EndConf =
+ case do_init_tc_call(Mod,{end_per_testcase,Func},
+ [Conf],
+ {TCExitReason,[Conf]}) of
+ {_,[EPTCInit]} when is_list(EPTCInit) ->
+ EPTCInit;
+ _ ->
+ Conf
+ end,
+ try apply(Mod,end_per_testcase,[Func,EndConf]) of
+ _ -> ok
+ catch
+ _:Error ->
+ timer:sleep(1),
+ print_end_conf_result(Mod,Func,Conf,
+ "crashed",Error)
+ end,
+ Supervisor ! {self(),end_conf}
+ end,
+ Pid = spawn_link(EndConfApply),
+ receive
+ {Pid,end_conf} ->
+ Starter ! {self(),{call_end_conf,Data,ok}};
+ {'EXIT',Pid,Reason} ->
+ print_end_conf_result(Mod,Func,Conf,"failed",Reason),
+ Starter ! {self(),{call_end_conf,Data,{error,Reason}}};
+ {'EXIT',_OtherPid,Reason} ->
+ %% Probably the parent - not much to do about that
+ exit(Reason)
+ end
+ end,
+ spawn_link(EndConfProc).
+
+print_end_conf_result(Mod,Func,Conf,Cause,Error) ->
+ Str2Print =
+ fun(NoHTML) when NoHTML == stdout; NoHTML == major ->
+ io_lib:format("WARNING! "
+ "~w:end_per_testcase(~tw, ~tp)"
+ " ~s!\n\tReason: ~tp\n",
+ [Mod,Func,Conf,Cause,Error]);
+ (minor) ->
+ ErrorStr = test_server_ctrl:escape_chars(Error),
+ io_lib:format("WARNING! "
+ "~w:end_per_testcase(~tw, ~tp)"
+ " ~s!\n\tReason: ~ts\n",
+ [Mod,Func,Conf,Cause,ErrorStr])
+ end,
+ group_leader() ! {printout,12,Str2Print},
+ ok.
+
+
+spawn_fw_call(Mod,IPTC={init_per_testcase,Func},CurrConf,Pid,
+ Why,Loc,SendTo) ->
+ FwCall =
+ fun() ->
+ ct_util:mark_process(),
+ Skip = {skip,{failed,{Mod,init_per_testcase,Why}}},
+ %% if init_per_testcase fails, the test case
+ %% should be skipped
+ try begin do_end_tc_call(Mod,IPTC, {Pid,Skip,[CurrConf]}, Why),
+ do_init_tc_call(Mod,{end_per_testcase_not_run,Func},
+ [CurrConf],{ok,[CurrConf]}),
+ do_end_tc_call(Mod,{end_per_testcase_not_run,Func},
+ {Pid,Skip,[CurrConf]}, Why) end of
+ _ -> ok
+ catch
+ _:FwEndTCErr ->
+ exit({fw_notify_done,end_tc,FwEndTCErr})
+ end,
+ Time = case Why of
+ {timetrap_timeout,TVal} -> TVal/1000;
+ _ -> died
+ end,
+ group_leader() ! {printout,12,
+ "ERROR! ~w:init_per_testcase(~tw, ~tp)"
+ " failed!\n\tReason: ~tp\n",
+ [Mod,Func,CurrConf,Why]},
+ %% finished, report back
+ SendTo ! {self(),fw_notify_done,{Time,Skip,Loc,[],undefined}}
+ end,
+ spawn_link(FwCall);
+
+spawn_fw_call(Mod,EPTC={end_per_testcase,Func},EndConf,Pid,
+ Why,_Loc,SendTo) ->
+ FwCall =
+ fun() ->
+ ct_util:mark_process(),
+ {RetVal,Report} =
+ case proplists:get_value(tc_status, EndConf) of
+ undefined ->
+ E = {failed,{Mod,end_per_testcase,Why}},
+ {E,E};
+ E = {failed,Reason} ->
+ {E,{error,Reason}};
+ Result ->
+ E = {failed,{Mod,end_per_testcase,Why}},
+ {Result,E}
+ end,
+ {Time,Warn} =
+ case Why of
+ {timetrap_timeout,TVal} ->
+ group_leader() !
+ {printout,12,
+ "WARNING! ~w:end_per_testcase(~tw, ~tp)"
+ " failed!\n\tReason: timetrap timeout"
+ " after ~w ms!\n", [Mod,Func,EndConf,TVal]},
+ W = "<font color=\"red\">"
+ "WARNING: end_per_testcase timed out!</font>",
+ {TVal/1000,W};
+ _ ->
+ group_leader() !
+ {printout,12,
+ "WARNING! ~w:end_per_testcase(~tw, ~tp)"
+ " failed!\n\tReason: ~tp\n",
+ [Mod,Func,EndConf,Why]},
+ W = "<font color=\"red\">"
+ "WARNING: end_per_testcase failed!</font>",
+ {died,W}
+ end,
+ try do_end_tc_call(Mod,EPTC,{Pid,Report,[EndConf]}, Why) of
+ _ -> ok
+ catch
+ _:FwEndTCErr ->
+ exit({fw_notify_done,end_tc,FwEndTCErr})
+ end,
+ FailLoc = proplists:get_value(tc_fail_loc, EndConf),
+ %% finished, report back (if end_per_testcase fails, a warning
+ %% should be printed as part of the comment)
+ SendTo ! {self(),fw_notify_done,
+ {Time,RetVal,FailLoc,[],Warn}}
+ end,
+ spawn_link(FwCall);
+
+spawn_fw_call(FwMod,FwFunc,_,_Pid,{framework_error,FwError},_,SendTo) ->
+ FwCall =
+ fun() ->
+ ct_util:mark_process(),
+ test_server_sup:framework_call(report, [framework_error,
+ {{FwMod,FwFunc},
+ FwError}]),
+ Comment =
+ lists:flatten(
+ io_lib:format("<font color=\"red\">"
+ "WARNING! ~w:~tw failed!</font>",
+ [FwMod,FwFunc])),
+ %% finished, report back
+ SendTo ! {self(),fw_notify_done,
+ {died,{error,{FwMod,FwFunc,FwError}},
+ {FwMod,FwFunc},[],Comment}}
+ end,
+ spawn_link(FwCall);
+
+spawn_fw_call(Mod,Func,CurrConf,Pid,Error,Loc,SendTo) ->
+ ct_util:mark_process(),
+ {Func1,EndTCFunc} = case Func of
+ CF when CF == init_per_suite; CF == end_per_suite;
+ CF == init_per_group; CF == end_per_group ->
+ {CF,CF};
+ TC -> {TC,{end_per_testcase,TC}}
+ end,
+ FwCall =
+ fun() ->
+ try fw_error_notify(Mod,Func1,[],
+ Error,Loc) of
+ _ -> ok
+ catch
+ _:FwErrorNotifyErr ->
+ exit({fw_notify_done,error_notification,
+ FwErrorNotifyErr})
+ end,
+ Conf = [{tc_status,{failed,Error}}|CurrConf],
+ try do_end_tc_call(Mod,EndTCFunc,{Pid,Error,[Conf]},Error) of
+ _ -> ok
+ catch
+ _:FwEndTCErr ->
+ exit({fw_notify_done,end_tc,FwEndTCErr})
+ end,
+ %% finished, report back
+ SendTo ! {self(),fw_notify_done,{died,Error,Loc,[],undefined}}
+ end,
+ spawn_link(FwCall).
+
+%% The job proxy process forwards messages between the test case
+%% process on a shielded node (and its descendants) and the job process.
+%%
+%% The job proxy process have to be started by the test-case process
+%% on the shielded node!
+start_job_proxy() ->
+ group_leader(spawn(fun () -> job_proxy_msgloop() end), self()), ok.
+
+%% The io_reply_proxy is not the most satisfying solution but it works...
+io_reply_proxy(ReplyTo) ->
+ ct_util:mark_process(),
+ receive
+ IoReply when is_tuple(IoReply),
+ element(1, IoReply) == io_reply ->
+ ReplyTo ! IoReply;
+ _ ->
+ io_reply_proxy(ReplyTo)
+ end.
+
+job_proxy_msgloop() ->
+ ct_util:mark_process(),
+ receive
+
+ %%
+ %% Messages that need intervention by proxy...
+ %%
+
+ %% io stuff ...
+ IoReq when tuple_size(IoReq) >= 2,
+ element(1, IoReq) == io_request ->
+
+ ReplyProxy = spawn(fun () -> io_reply_proxy(element(2, IoReq)) end),
+ group_leader() ! setelement(2, IoReq, ReplyProxy);
+
+ %% test_server stuff...
+ {sync_apply, From, MFA} ->
+ group_leader() ! {sync_apply_proxy, self(), From, MFA};
+ {sync_result_proxy, To, Result} ->
+ To ! {sync_result, Result};
+
+ %%
+ %% Messages that need no intervention by proxy...
+ %%
+ Msg ->
+ group_leader() ! Msg
+ end,
+ job_proxy_msgloop().
+
+-spec run_test_case_eval_fun(_, _, _, _, _, _, _, _, _) ->
+ fun(() -> no_return()).
+run_test_case_eval_fun(Mod, Func, Args, Name, Ref, RunInit,
+ TimetrapData, LogOpts, TCCallback) ->
+ fun () ->
+ run_test_case_eval(Mod, Func, Args, Name, Ref,
+ RunInit, TimetrapData,
+ LogOpts, TCCallback)
+ end.
+
+%% A test case is known to have failed if it returns {'EXIT', _} tuple,
+%% or sends a message {failed, File, Line} to it's group_leader
+
+run_test_case_eval(Mod, Func, Args0, Name, Ref, RunInit,
+ TimetrapData, LogOpts, TCCallback) ->
+ put(test_server_multiply_timetraps, TimetrapData),
+ put(test_server_logopts, LogOpts),
+ Where = [{Mod,Func}],
+ put(test_server_loc, Where),
+
+ FWInitFunc = case RunInit of
+ run_init -> {init_per_testcase,Func};
+ _ -> Func
+ end,
+
+ FWInitResult0 = do_init_tc_call(Mod,FWInitFunc,Args0,{ok,Args0}),
+
+ set_tc_state(running),
+ {{Time,Value},Loc,Opts} =
+ case FWInitResult0 of
+ {ok,Args} ->
+ run_test_case_eval1(Mod, Func, Args, Name, RunInit, TCCallback);
+ Error = {error,_Reason} ->
+ NewResult = do_end_tc_call(Mod,FWInitFunc, {Error,Args0},
+ {auto_skip,{failed,Error}}),
+ {{0,NewResult},Where,[]};
+ {fail,Reason} ->
+ Conf = [{tc_status,{failed,Reason}} | hd(Args0)],
+ fw_error_notify(Mod, Func, Conf, Reason),
+ NewResult = do_end_tc_call(Mod,FWInitFunc,
+ {{error,Reason},[Conf]},
+ {fail,Reason}),
+ {{0,NewResult},Where,[]};
+ Skip = {SkipType,_Reason} when SkipType == skip;
+ SkipType == skipped ->
+ NewResult = do_end_tc_call(Mod,FWInitFunc,
+ {Skip,Args0}, Skip),
+ {{0,NewResult},Where,[]};
+ AutoSkip = {auto_skip,_Reason} ->
+ %% special case where a conf case "pretends" to be skipped
+ NewResult =
+ do_end_tc_call(Mod,FWInitFunc, {AutoSkip,Args0}, AutoSkip),
+ {{0,NewResult},Where,[]}
+ end,
+ exit({Ref,Time,Value,Loc,Opts}).
+
+run_test_case_eval1(Mod, Func, Args, Name, RunInit, TCCallback) ->
+ case RunInit of
+ run_init ->
+ set_tc_state(init_per_testcase, hd(Args)),
+ ensure_timetrap(Args),
+ case init_per_testcase(Mod, Func, Args) of
+ Skip = {SkipType,Reason} when SkipType == skip;
+ SkipType == skipped ->
+ Line = get_loc(),
+ Conf = [{tc_status,{skipped,Reason}}|hd(Args)],
+ NewRes = do_end_tc_call(Mod,{init_per_testcase,Func},
+ {Skip,[Conf]}, Skip),
+ {{0,NewRes},Line,[]};
+ {skip_and_save,Reason,SaveCfg} ->
+ Line = get_loc(),
+ Conf = [{tc_status,{skipped,Reason}},
+ {save_config,SaveCfg}|hd(Args)],
+ NewRes = do_end_tc_call(Mod,{init_per_testcase,Func},
+ {{skip,Reason},[Conf]},
+ {skip,Reason}),
+ {{0,NewRes},Line,[]};
+ FailTC = {fail,Reason} -> % user fails the testcase
+ EndConf = [{tc_status,{failed,Reason}} | hd(Args)],
+ fw_error_notify(Mod, Func, EndConf, Reason),
+ NewRes = do_end_tc_call(Mod,{init_per_testcase,Func},
+ {{error,Reason},[EndConf]},
+ FailTC),
+ {{0,NewRes},[{Mod,Func}],[]};
+ {ok,NewConf} ->
+ IPTCEndRes = do_end_tc_call(Mod,{init_per_testcase,Func},
+ {ok,[NewConf]}, NewConf),
+ {{T,Return},Loc,NewConf1} =
+ if not is_list(IPTCEndRes) ->
+ %% received skip or fail, not config
+ {{0,IPTCEndRes},undefined,NewConf};
+ true ->
+ %% call user callback function if defined
+ NewConfUC =
+ user_callback(TCCallback, Mod, Func,
+ init, IPTCEndRes),
+ %% save current state in controller loop
+ set_tc_state(tc, NewConfUC),
+ %% execute the test case
+ {ts_tc(Mod,Func,[NewConfUC]),get_loc(),NewConfUC}
+ end,
+ {EndConf,TSReturn,FWReturn} =
+ case Return of
+ {E,TCError} when E=='EXIT' ; E==failed ->
+ fw_error_notify(Mod, Func, NewConf1,
+ TCError, Loc),
+ {[{tc_status,{failed,TCError}},
+ {tc_fail_loc,Loc}|NewConf1],
+ Return,{error,TCError}};
+ SaveCfg={save_config,_} ->
+ {[{tc_status,ok},SaveCfg|NewConf1],Return,ok};
+ {skip_and_save,Why,SaveCfg} ->
+ Skip = {skip,Why},
+ {[{tc_status,{skipped,Why}},
+ {save_config,SaveCfg}|NewConf1],
+ Skip,Skip};
+ {SkipType,Why} when SkipType == skip;
+ SkipType == skipped ->
+ {[{tc_status,{skipped,Why}}|NewConf1],Return,
+ Return};
+ _ ->
+ {[{tc_status,ok}|NewConf1],Return,ok}
+ end,
+ %% call user callback function if defined
+ EndConf1 =
+ user_callback(TCCallback, Mod, Func, 'end', EndConf),
+
+ %% We can't handle fails or skips here
+ EndConf2 =
+ case do_init_tc_call(Mod,{end_per_testcase,Func},
+ [EndConf1],{ok,[EndConf1]}) of
+ {ok,[EPTCInitRes]} when is_list(EPTCInitRes) ->
+ EPTCInitRes;
+ _ ->
+ EndConf1
+ end,
+
+ %% update current state in controller loop
+ {FWReturn1,TSReturn1,EndConf3} =
+ case end_per_testcase(Mod, Func, EndConf2) of
+ SaveCfg1={save_config,_} ->
+ {FWReturn,TSReturn,
+ [SaveCfg1|lists:keydelete(save_config,1,
+ EndConf2)]};
+ {fail,ReasonToFail} ->
+ %% user has failed the testcase
+ fw_error_notify(Mod, Func, EndConf2,
+ ReasonToFail),
+ {{error,ReasonToFail},
+ {failed,ReasonToFail},
+ EndConf2};
+ {failed,{_,end_per_testcase,_}} = Failure when
+ FWReturn == ok ->
+ %% unexpected termination in end_per_testcase
+ %% report this as the result to the framework
+ {Failure,TSReturn,EndConf2};
+ _ ->
+ %% test case result should be reported to
+ %% framework no matter the status of
+ %% end_per_testcase
+ {FWReturn,TSReturn,EndConf2}
+ end,
+ %% clear current state in controller loop
+ case do_end_tc_call(Mod,{end_per_testcase,Func},
+ {FWReturn1,[EndConf3]}, TSReturn1) of
+ {failed,Reason} = NewReturn ->
+ fw_error_notify(Mod,Func,EndConf3, Reason),
+ {{T,NewReturn},[{Mod,Func}],[]};
+ NewReturn ->
+ {{T,NewReturn},Loc,[]}
+ end
+ end;
+ skip_init ->
+ set_tc_state(running, hd(Args)),
+ %% call user callback function if defined
+ Args1 = user_callback(TCCallback, Mod, Func, init, Args),
+ ensure_timetrap(Args1),
+ %% ts_tc does a catch
+ %% if this is a named conf group, the test case (init or end conf)
+ %% should be called with the name as the first argument
+ Args2 = if Name == undefined -> Args1;
+ true -> [Name | Args1]
+ end,
+ %% execute the conf test case
+ {{T,Return},Loc} = {ts_tc(Mod, Func, Args2),get_loc()},
+ %% call user callback function if defined
+ Return1 = user_callback(TCCallback, Mod, Func, 'end', Return),
+ {Return2,Opts} = process_return_val([Return1], Mod, Func,
+ Args1, [{Mod,Func}], Return1),
+ {{T,Return2},Loc,Opts}
+ end.
+
+do_init_tc_call(Mod, Func, Res, Return) ->
+ test_server_sup:framework_call(init_tc,[Mod,Func,Res],Return).
+
+do_end_tc_call(Mod, IPTC={init_per_testcase,Func}, Res, Return) ->
+ case Return of
+ {NOk,_} when NOk == auto_skip; NOk == fail;
+ NOk == skip ; NOk == skipped ->
+ {_,Args} = Res,
+ IPTCEndRes =
+ case do_end_tc_call1(Mod, IPTC, Res, Return) of
+ IPTCEndConfig when is_list(IPTCEndConfig) ->
+ IPTCEndConfig;
+ _ ->
+ Args
+ end,
+ EPTCInitRes =
+ case do_init_tc_call(Mod,{end_per_testcase_not_run,Func},
+ IPTCEndRes,Return) of
+ {ok,EPTCInitConfig} when is_list(EPTCInitConfig) ->
+ {Return,EPTCInitConfig};
+ _ ->
+ {Return,IPTCEndRes}
+ end,
+ do_end_tc_call1(Mod, {end_per_testcase_not_run,Func},
+ EPTCInitRes, Return);
+ _Ok ->
+ do_end_tc_call1(Mod, IPTC, Res, Return)
+ end;
+do_end_tc_call(Mod, Func, Res, Return) ->
+ do_end_tc_call1(Mod, Func, Res, Return).
+
+do_end_tc_call1(Mod, Func, Res, Return) ->
+ FwMod = os:getenv("TEST_SERVER_FRAMEWORK"),
+ Ref = make_ref(),
+ if FwMod == "ct_framework" ; FwMod == "undefined"; FwMod == false ->
+ case test_server_sup:framework_call(
+ end_tc, [Mod,Func,Res, Return], ok) of
+ {fail,FWReason} ->
+ {failed,FWReason};
+ ok ->
+ case Return of
+ {fail,Reason} ->
+ {failed,Reason};
+ Return ->
+ Return
+ end;
+ NewReturn ->
+ NewReturn
+ end;
+ true ->
+ case test_server_sup:framework_call(FwMod, end_tc,
+ [Mod,Func,Res], Ref) of
+ {fail,FWReason} ->
+ {failed,FWReason};
+ _Else ->
+ Return
+ end
+ end.
+
+%% the return value is a list and we have to check if it contains
+%% the result of an end conf case or if it's a Config list
+process_return_val([Return], M,F,A, Loc, Final) when is_list(Return) ->
+ ReturnTags = [skip,skip_and_save,save_config,comment,return_group_result],
+ %% check if all elements in the list are valid end conf return value tuples
+ case lists:all(fun(Val) when is_tuple(Val) ->
+ lists:any(fun(T) -> T == element(1, Val) end,
+ ReturnTags);
+ (ok) ->
+ true;
+ (_) ->
+ false
+ end, Return) of
+ true -> % must be return value from end conf case
+ process_return_val1(Return, M,F,A, Loc, Final, []);
+ false -> % must be Config value from init conf case
+ case do_end_tc_call(M, F, {ok,A}, Return) of
+ {failed, FWReason} = Failed ->
+ fw_error_notify(M,F,A, FWReason),
+ {Failed, []};
+ NewReturn ->
+ {NewReturn, []}
+ end
+ end;
+%% the return value is not a list, so it's the return value from an
+%% end conf case or it's a dummy value that can be ignored
+process_return_val(Return, M,F,A, Loc, Final) ->
+ process_return_val1(Return, M,F,A, Loc, Final, []).
+
+process_return_val1([Failed={E,TCError}|_], M,F,A=[Args], Loc, _, SaveOpts)
+ when E=='EXIT';
+ E==failed ->
+ fw_error_notify(M,F,A, TCError, Loc),
+ case do_end_tc_call(M,F, {{error,TCError},
+ [[{tc_status,{failed,TCError}}|Args]]},
+ Failed) of
+ {failed,FWReason} ->
+ {{failed,FWReason},SaveOpts};
+ NewReturn ->
+ {NewReturn,SaveOpts}
+ end;
+process_return_val1([SaveCfg={save_config,_}|Opts], M,F,[Args],
+ Loc, Final, SaveOpts) ->
+ process_return_val1(Opts, M,F,[[SaveCfg|Args]], Loc, Final, SaveOpts);
+process_return_val1([{skip_and_save,Why,SaveCfg}|Opts], M,F,[Args],
+ Loc, _, SaveOpts) ->
+ process_return_val1(Opts, M,F,[[{save_config,SaveCfg}|Args]],
+ Loc, {skip,Why}, SaveOpts);
+process_return_val1([GR={return_group_result,_}|Opts], M,F,A,
+ Loc, Final, SaveOpts) ->
+ process_return_val1(Opts, M,F,A, Loc, Final, [GR|SaveOpts]);
+process_return_val1([RetVal={Tag,_}|Opts], M,F,A,
+ Loc, _, SaveOpts) when Tag==skip;
+ Tag==comment ->
+ process_return_val1(Opts, M,F,A, Loc, RetVal, SaveOpts);
+process_return_val1([_|Opts], M,F,A, Loc, Final, SaveOpts) ->
+ process_return_val1(Opts, M,F,A, Loc, Final, SaveOpts);
+process_return_val1([], M,F,A, _Loc, Final, SaveOpts) ->
+ case do_end_tc_call(M,F, {Final,A}, Final) of
+ {failed,FWReason} ->
+ {{failed,FWReason},SaveOpts};
+ NewReturn ->
+ {NewReturn,lists:reverse(SaveOpts)}
+ end.
+
+user_callback(undefined, _, _, _, Args) ->
+ Args;
+user_callback({CBMod,CBFunc}, Mod, Func, InitOrEnd,
+ [Args]) when is_list(Args) ->
+ case catch apply(CBMod, CBFunc, [InitOrEnd,Mod,Func,Args]) of
+ Args1 when is_list(Args1) ->
+ [Args1];
+ _ ->
+ [Args]
+ end;
+user_callback({CBMod,CBFunc}, Mod, Func, InitOrEnd, Args) ->
+ case catch apply(CBMod, CBFunc, [InitOrEnd,Mod,Func,Args]) of
+ Args1 when is_list(Args1) ->
+ Args1;
+ _ ->
+ Args
+ end.
+
+init_per_testcase(Mod, Func, Args) ->
+ case code:is_loaded(Mod) of
+ false ->
+ _ = code:load_file(Mod),
+ ok;
+ _ -> ok
+ end,
+ case erlang:function_exported(Mod, init_per_testcase, 2) of
+ true ->
+ do_init_per_testcase(Mod, [Func|Args]);
+ false ->
+ %% Optional init_per_testcase is not defined -- keep quiet.
+ [Config] = Args,
+ {ok, Config}
+ end.
+
+do_init_per_testcase(Mod, Args) ->
+ try apply(Mod, init_per_testcase, Args) of
+ {Skip,Reason} when Skip =:= skip; Skip =:= skipped ->
+ {skip,Reason};
+ {skip_and_save,_,_}=Res ->
+ Res;
+ NewConf when is_list(NewConf) ->
+ case lists:filter(fun(T) when is_tuple(T) -> false;
+ (_) -> true end, NewConf) of
+ [] ->
+ {ok,NewConf};
+ Bad ->
+ group_leader() ! {printout,12,
+ "ERROR! init_per_testcase has returned "
+ "bad elements in Config: ~tp\n",[Bad]},
+ {skip,{failed,{Mod,init_per_testcase,bad_return}}}
+ end;
+ {fail,_Reason}=Res ->
+ Res;
+ _Other ->
+ group_leader() ! {printout,12,
+ "ERROR! init_per_testcase did not return "
+ "a Config list.\n",[]},
+ {skip,{failed,{Mod,init_per_testcase,bad_return}}}
+ catch
+ throw:{Skip,Reason} when Skip =:= skip; Skip =:= skipped ->
+ {skip,Reason};
+ exit:{Skip,Reason} when Skip =:= skip; Skip =:= skipped ->
+ {skip,Reason};
+ throw:Other:Stk ->
+ set_loc(Stk),
+ Line = get_loc(),
+ print_init_conf_result(Line,"thrown",Other),
+ {skip,{failed,{Mod,init_per_testcase,Other}}};
+ _:Reason0:Stk ->
+ Reason = {Reason0,Stk},
+ set_loc(Stk),
+ Line = get_loc(),
+ print_init_conf_result(Line,"crashed",Reason),
+ {skip,{failed,{Mod,init_per_testcase,Reason}}}
+ end.
+
+print_init_conf_result(Line,Cause,Reason) ->
+ FormattedLoc = test_server_sup:format_loc(Line),
+ Str2Print =
+ fun(NoHTML) when NoHTML == stdout; NoHTML == major ->
+ io_lib:format("ERROR! init_per_testcase ~s!\n"
+ "\tLocation: ~tp\n\tReason: ~tp\n",
+ [Cause,Line,Reason]);
+ (minor) ->
+ ReasonStr = test_server_ctrl:escape_chars(Reason),
+ io_lib:format("ERROR! init_per_testcase ~s!\n"
+ "\tLocation: ~ts\n\tReason: ~ts\n",
+ [Cause,FormattedLoc,ReasonStr])
+ end,
+ group_leader() ! {printout,12,Str2Print},
+ ok.
+
+
+end_per_testcase(Mod, Func, Conf) ->
+ case erlang:function_exported(Mod,end_per_testcase,2) of
+ true ->
+ do_end_per_testcase(Mod,end_per_testcase,Func,Conf);
+ false ->
+ %% Backwards compatibility!
+ case erlang:function_exported(Mod,fin_per_testcase,2) of
+ true ->
+ do_end_per_testcase(Mod,fin_per_testcase,Func,Conf);
+ false ->
+ ok
+ end
+ end.
+
+do_end_per_testcase(Mod,EndFunc,Func,Conf) ->
+ set_tc_state(end_per_testcase, Conf),
+ try Mod:EndFunc(Func, Conf) of
+ {save_config,_}=SaveCfg ->
+ SaveCfg;
+ {fail,_}=Fail ->
+ Fail;
+ _ ->
+ ok
+ catch
+ throw:Other:Stk ->
+ Comment0 = case read_comment() of
+ "" -> "";
+ Cmt -> Cmt ++ test_server_ctrl:xhtml("<br>",
+ "<br />")
+ end,
+ set_loc(Stk),
+ 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 ->
+ set_loc(Stk),
+ Why = case Class of
+ exit -> {'EXIT',Reason};
+ error -> {'EXIT',{Reason,Stk}}
+ end,
+ Comment0 = case read_comment() of
+ "" -> "";
+ Cmt -> Cmt ++ test_server_ctrl:xhtml("<br>",
+ "<br />")
+ end,
+ comment(io_lib:format("~ts<font color=\"red\">"
+ "WARNING: ~w crashed!"
+ "</font>\n",[Comment0,EndFunc])),
+ print_end_tc_warning(EndFunc,Reason,"crashed",get_loc()),
+ {failed,{Mod,end_per_testcase,Why}}
+ end.
+
+print_end_tc_warning(EndFunc,Reason,Cause,Loc) ->
+ FormattedLoc = test_server_sup:format_loc(Loc),
+ Str2Print =
+ fun(NoHTML) when NoHTML == stdout; NoHTML == major ->
+ io_lib:format("WARNING: ~w ~s!\n"
+ "Reason: ~tp\nLine: ~tp\n",
+ [EndFunc,Cause,Reason,Loc]);
+ (minor) ->
+ ReasonStr = test_server_ctrl:escape_chars(Reason),
+ io_lib:format("WARNING: ~w ~s!\n"
+ "Reason: ~ts\nLine: ~ts\n",
+ [EndFunc,Cause,ReasonStr,FormattedLoc])
+ end,
+ group_leader() ! {printout,12,Str2Print},
+ ok.
+
+get_loc() ->
+ get(test_server_loc).
+
+get_loc(Pid) ->
+ [{current_stacktrace,Stk0},{dictionary,Dict}] =
+ process_info(Pid, [current_stacktrace,dictionary]),
+ lists:foreach(fun({Key,Val}) -> put(Key, Val) end, Dict),
+ Stk = [rewrite_loc_item(Loc) || Loc <- Stk0],
+ case get(test_server_loc) of
+ [{Suite,Case}] ->
+ %% Location info unknown, check if {Suite,Case,Line}
+ %% is available in stacktrace and if so, use stacktrace
+ %% instead of current test_server_loc.
+ %% If location is the last expression in a test case
+ %% function, the info is not available due to tail call
+ %% elimination. We need to check if the test case has been
+ %% called by ts_tc/3 and, if so, insert the test case info
+ %% at that position.
+ case [match || {S,C,_L} <- Stk, S == Suite, C == Case] of
+ [match|_] ->
+ put(test_server_loc, Stk);
+ _ ->
+ {PreTC,PostTC} =
+ lists:splitwith(fun({test_server,ts_tc,_}) ->
+ false;
+ (_) ->
+ true
+ end, Stk),
+ if PostTC == [] ->
+ ok;
+ true ->
+ put(test_server_loc,
+ PreTC++[{Suite,Case,last_expr} | PostTC])
+ end
+ end;
+ _ ->
+ put(test_server_loc, Stk)
+ end,
+ get_loc().
+
+fw_error_notify(Mod, Func, Args, Error) ->
+ test_server_sup:framework_call(error_notification,
+ [Mod,Func,[Args],
+ {Error,unknown}]).
+fw_error_notify(Mod, Func, Args, Error, Loc) ->
+ test_server_sup:framework_call(error_notification,
+ [Mod,Func,[Args],
+ {Error,Loc}]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% print(Detail,Format,Args,Printer) -> ok
+%% Detail = integer()
+%% Format = string()
+%% Args = [term()]
+%%
+%% Just like io:format, except that depending on the Detail value, the output
+%% is directed to console, major and/or minor log files.
+
+%% print(Detail,Format,Args) ->
+%% test_server_ctrl:print(Detail, Format, Args).
+
+print(Detail,Format,Args,Printer) ->
+ test_server_ctrl:print(Detail, Format, Args, Printer).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% print_timsteamp(Detail,Leader) -> ok
+%%
+%% Prints Leader followed by a time stamp (date and time). Depending on
+%% the Detail value, the output is directed to console, major and/or minor
+%% log files.
+
+print_timestamp(Detail,Leader) ->
+ test_server_ctrl:print_timestamp(Detail, Leader).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% lookup_config(Key,Config) -> {value,{Key,Value}} | undefined
+%% Key = term()
+%% Value = term()
+%% Config = [{Key,Value},...]
+%%
+%% Looks up a specific key in the config list, and returns the value
+%% of the associated key, or undefined if the key doesn't exist.
+
+lookup_config(Key,Config) ->
+ case lists:keysearch(Key,1,Config) of
+ {value,{Key,Val}} ->
+ Val;
+ _ ->
+ io:format("Could not find element ~tp in Config.~n",[Key]),
+ undefined
+ end.
+
+%%
+%% IMPORTANT: get_loc/1 uses the name of this function when analysing
+%% stack traces. If the name changes, get_loc/1 must be updated!
+%%
+ts_tc(M, F, A) ->
+ Before = erlang:monotonic_time(),
+ Result = try
+ apply(M, F, A)
+ catch
+ throw:{skip, Reason} -> {skip, Reason};
+ throw:{skipped, Reason} -> {skip, Reason};
+ exit:{skip, Reason} -> {skip, Reason};
+ exit:{skipped, Reason} -> {skip, Reason};
+ Type:Reason:Stk ->
+ set_loc(Stk),
+ case Type of
+ throw ->
+ {failed,{thrown,Reason}};
+ error ->
+ {'EXIT',{Reason,Stk}};
+ exit ->
+ {'EXIT',Reason}
+ end
+ end,
+ After = erlang:monotonic_time(),
+ Elapsed = erlang:convert_time_unit(After-Before, native, micro_seconds),
+ {Elapsed, Result}.
+
+set_loc(Stk) ->
+ Loc = case [rewrite_loc_item(I) || {_,_,_,_}=I <- Stk] of
+ [{M,F,0}|Stack] ->
+ [{M,F}|Stack];
+ Other ->
+ Other
+ end,
+ put(test_server_loc, Loc).
+
+rewrite_loc_item({M,F,_,Loc}) ->
+ {M,F,proplists:get_value(line, Loc, 0)}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TEST SUITE SUPPORT FUNCTIONS %%
+%% %%
+%% Note: Some of these functions have been moved to test_server_sup %%
+%% in an attempt to keep this modules small (yeah, right!) %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% format(Format) -> IoLibReturn
+%% format(Detail,Format) -> IoLibReturn
+%% format(Format,Args) -> IoLibReturn
+%% format(Detail,Format,Args) -> IoLibReturn
+%% Detail = integer()
+%% Format = string()
+%% Args = [term(),...]
+%% IoLibReturn = term()
+%%
+%% Logs the Format string and Args, similar to io:format/1/2 etc. If
+%% Detail is not specified, the default detail level (which is 50) is used.
+%% Which log files the string will be logged in depends on the thresholds
+%% set with set_levels/3. Typically with default detail level, only the
+%% minor log file is used.
+format(Format) ->
+ format(minor, Format, []).
+
+format(major, Format) ->
+ format(major, Format, []);
+format(minor, Format) ->
+ format(minor, Format, []);
+format(Detail, Format) when is_integer(Detail) ->
+ format(Detail, Format, []);
+format(Format, Args) ->
+ format(minor, Format, Args).
+
+format(Detail, Format, Args) ->
+ Str =
+ case catch io_lib:format(Format,Args) of
+ {'EXIT',_} ->
+ io_lib:format("illegal format; ~tp with args ~tp.\n",
+ [Format,Args]);
+ Valid -> Valid
+ end,
+ log({Detail, Str}).
+
+log(Msg) ->
+ group_leader() ! {structured_io, self(), Msg},
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% capture_start() -> ok
+%% capture_stop() -> ok
+%%
+%% Starts/stops capturing all output from io:format, and similar. Capturing
+%% output doesn't stop output from happening. It just makes it possible
+%% to retrieve the output using capture_get/0.
+%% Starting and stopping capture doesn't affect already captured output.
+%% All output is stored as messages in the message queue until retrieved
+
+capture_start() ->
+ group_leader() ! {capture,self()},
+ ok.
+
+capture_stop() ->
+ group_leader() ! {capture,false},
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% capture_get() -> Output
+%% Output = [string(),...]
+%%
+%% Retrieves all the captured output since last call to capture_get/0.
+%% Note that since output arrive as messages to the process, it takes
+%% a short while from the call to io:format until all output is available
+%% by capture_get/0. It is not necessary to call capture_stop/0 before
+%% retreiving the output.
+capture_get() ->
+ test_server_sup:capture_get([]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% messages_get() -> Messages
+%% Messages = [term(),...]
+%%
+%% Returns all messages in the message queue.
+messages_get() ->
+ test_server_sup:messages_get([]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% permit_io(GroupLeader, FromPid) -> ok
+%%
+%% Make sure proceeding IO from FromPid won't get rejected
+permit_io(GroupLeader, FromPid) ->
+ GroupLeader ! {permit_io,FromPid},
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% sleep(Time) -> ok
+%% Time = integer() | float() | infinity
+%%
+%% Sleeps the specified number of milliseconds. This sleep also accepts
+%% floating point numbers (which are truncated) and the atom 'infinity'.
+sleep(infinity) ->
+ receive
+ after infinity ->
+ ok
+ end;
+sleep(MSecs) ->
+ receive
+ after trunc(MSecs) ->
+ ok
+ end,
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% adjusted_sleep(Time) -> ok
+%% Time = integer() | float() | infinity
+%%
+%% Sleeps the specified number of milliseconds, multiplied by the
+%% 'multiply_timetraps' value (if set) and possibly also automatically scaled
+%% up if 'scale_timetraps' is set to true (which is default).
+%% This function also accepts floating point numbers (which are truncated) and
+%% the atom 'infinity'.
+adjusted_sleep(infinity) ->
+ receive
+ after infinity ->
+ ok
+ end;
+adjusted_sleep(MSecs) ->
+ {Multiplier,ScaleFactor} =
+ case test_server_ctrl:get_timetrap_parameters() of
+ {undefined,undefined} ->
+ {1,1};
+ {undefined,false} ->
+ {1,1};
+ {undefined,true} ->
+ {1,timetrap_scale_factor()};
+ {infinity,_} ->
+ {infinity,1};
+ {Mult,undefined} ->
+ {Mult,1};
+ {Mult,false} ->
+ {Mult,1};
+ {Mult,true} ->
+ {Mult,timetrap_scale_factor()}
+ end,
+ receive
+ after trunc(MSecs*Multiplier*ScaleFactor) ->
+ ok
+ end,
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% fail(Reason) -> exit({suite_failed,Reason})
+%%
+%% Immediately calls exit. Included because test suites are easier
+%% to read when using this function, rather than exit directly.
+fail(Reason) ->
+ comment(cast_to_list(Reason)),
+ try
+ exit({suite_failed,Reason})
+ catch
+ Class:R:Stacktrace ->
+ case Stacktrace of
+ [{?MODULE,fail,1,_}|Stk] -> ok;
+ Stk -> ok
+ end,
+ erlang:raise(Class, R, Stk)
+ end.
+
+cast_to_list(X) when is_list(X) -> X;
+cast_to_list(X) when is_atom(X) -> atom_to_list(X);
+cast_to_list(X) -> lists:flatten(io_lib:format("~tp", [X])).
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% fail() -> exit(suite_failed)
+%%
+%% Immediately calls exit. Included because test suites are easier
+%% to read when using this function, rather than exit directly.
+fail() ->
+ try
+ exit(suite_failed)
+ catch
+ Class:R:Stacktrace ->
+ case Stacktrace of
+ [{?MODULE,fail,0,_}|Stk] -> ok;
+ Stk -> ok
+ end,
+ erlang:raise(Class, R, Stk)
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% break(Comment) -> ok
+%%
+%% Break a test case so part of the test can be done manually.
+%% Use continue/0 to continue.
+break(Comment) ->
+ break(?MODULE, Comment).
+
+break(CBM, Comment) ->
+ break(CBM, '', Comment).
+
+break(CBM, TestCase, Comment) ->
+ timetrap_cancel(),
+ {TCName,CntArg,PName} =
+ if TestCase == '' ->
+ {"", "", test_server_break_process};
+ true ->
+ Str = atom_to_list(TestCase),
+ {[32 | Str], Str,
+ list_to_atom("test_server_break_process_" ++ Str)}
+ end,
+ io:format(user,
+ "\n\n\n--- SEMIAUTOMATIC TESTING ---"
+ "\nThe test case~ts executes on process ~w"
+ "\n\n\n~ts"
+ "\n\n\n-----------------------------\n\n"
+ "Continue with --> ~w:continue(~ts).\n",
+ [TCName,self(),Comment,CBM,CntArg]),
+ case whereis(PName) of
+ undefined ->
+ spawn_break_process(self(), PName);
+ OldBreakProcess ->
+ OldBreakProcess ! cancel,
+ spawn_break_process(self(), PName)
+ end,
+ receive continue -> ok end.
+
+spawn_break_process(Pid, PName) ->
+ spawn(fun() ->
+ register(PName, self()),
+ ct_util:mark_process(),
+ receive
+ continue -> continue(Pid);
+ cancel -> ok
+ end
+ end).
+
+continue() ->
+ case whereis(test_server_break_process) of
+ undefined -> ok;
+ BreakProcess -> BreakProcess ! continue
+ end.
+
+continue(TestCase) when is_atom(TestCase) ->
+ PName = list_to_atom("test_server_break_process_" ++
+ atom_to_list(TestCase)),
+ case whereis(PName) of
+ undefined -> ok;
+ BreakProcess -> BreakProcess ! continue
+ end;
+
+continue(Pid) when is_pid(Pid) ->
+ Pid ! continue.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timetrap_scale_factor() -> Factor
+%%
+%% Returns the amount to scale timetraps with.
+
+%% {X, fun() -> check() end} <- multiply scale with X if Fun() is true
+timetrap_scale_factor() ->
+ timetrap_scale_factor([
+ { 2, fun() -> has_lock_checking() end},
+ { 3, fun() -> has_superfluous_schedulers() end},
+ { 6, fun() -> is_debug() end},
+ {10, fun() -> is_cover() end},
+ {10, fun() -> is_valgrind() end}
+ ]).
+
+timetrap_scale_factor(Scales) ->
+ %% The fun in {S, Fun} a filter input to the list comprehension
+ lists:foldl(fun(S,O) -> O*S end, 1, [ S || {S,F} <- Scales, F()]).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timetrap(Timeout) -> Handle
+%% Handle = term()
+%%
+%% Creates a time trap, that will kill the calling process if the
+%% trap is not cancelled with timetrap_cancel/1, within Timeout milliseconds.
+timetrap(Timeout) ->
+ MultAndScale =
+ case get(test_server_multiply_timetraps) of
+ undefined -> {fun(T) -> T end, true};
+ {undefined,false} -> {fun(T) -> T end, false};
+ {undefined,_} -> {fun(T) -> T end, true};
+ {infinity,_} -> {fun(_) -> infinity end, false};
+ {Int,Scale} -> {fun(infinity) -> infinity;
+ (T) -> T*Int end, Scale}
+ end,
+ timetrap(Timeout, Timeout, self(), MultAndScale).
+
+%% when the function is called from different process than
+%% the test case, the test_server_multiply_timetraps data
+%% is unknown and must be passed as argument
+timetrap(Timeout, TCPid, MultAndScale) ->
+ timetrap(Timeout, Timeout, TCPid, MultAndScale).
+
+timetrap(Timeout0, TimeToReport0, TCPid, MultAndScale = {Multiplier,Scale}) ->
+ %% the time_ms call will either convert Timeout to ms or spawn a
+ %% user timetrap which sends the result to the IO server process
+ Timeout = time_ms(Timeout0, TCPid, MultAndScale),
+ Timeout1 = Multiplier(Timeout),
+ TimeToReport = if Timeout0 == TimeToReport0 ->
+ Timeout1;
+ true ->
+ %% only convert to ms, don't start a
+ %% user timetrap
+ time_ms_check(TimeToReport0)
+ end,
+ cancel_default_timetrap(self() == TCPid),
+ Handle = case Timeout1 of
+ infinity ->
+ infinity;
+ _ ->
+ spawn_link(test_server_sup,timetrap,[Timeout1,TimeToReport,
+ Scale,TCPid])
+ end,
+
+ %% ERROR! This sets dict on IO process instead of testcase process
+ %% if Timeout is return value from previous user timetrap!!
+
+ case get(test_server_timetraps) of
+ undefined ->
+ put(test_server_timetraps,[{Handle,TCPid,{TimeToReport,Scale}}]);
+ List ->
+ List1 = lists:delete({infinity,TCPid,{infinity,false}}, List),
+ put(test_server_timetraps,[{Handle,TCPid,
+ {TimeToReport,Scale}}|List1])
+ end,
+ Handle.
+
+ensure_timetrap(Config) ->
+ case get(test_server_timetraps) of
+ [_|_] ->
+ ok;
+ _ ->
+ case get(test_server_default_timetrap) of
+ undefined -> ok;
+ Garbage ->
+ erase(test_server_default_timetrap),
+ format("=== WARNING: garbage in "
+ "test_server_default_timetrap: ~tp~n",
+ [Garbage])
+ end,
+ DTmo = case lists:keysearch(default_timeout,1,Config) of
+ {value,{default_timeout,Tmo}} -> Tmo;
+ _ -> ?DEFAULT_TIMETRAP_SECS
+ end,
+ format("=== test_server setting default "
+ "timetrap of ~p seconds~n",
+ [DTmo]),
+ put(test_server_default_timetrap, timetrap(seconds(DTmo)))
+ end.
+
+%% executing on IO process, no default timetrap ever set here
+cancel_default_timetrap(false) ->
+ ok;
+cancel_default_timetrap(true) ->
+ case get(test_server_default_timetrap) of
+ undefined ->
+ ok;
+ TimeTrap when is_pid(TimeTrap) ->
+ timetrap_cancel(TimeTrap),
+ erase(test_server_default_timetrap),
+ format("=== test_server canceled default timetrap "
+ "since another timetrap was set~n"),
+ ok;
+ Garbage ->
+ erase(test_server_default_timetrap),
+ format("=== WARNING: garbage in "
+ "test_server_default_timetrap: ~tp~n",
+ [Garbage]),
+ error
+ end.
+
+time_ms({hours,N}, _, _) -> hours(N);
+time_ms({minutes,N}, _, _) -> minutes(N);
+time_ms({seconds,N}, _, _) -> seconds(N);
+time_ms({Other,_N}, _, _) ->
+ format("=== ERROR: Invalid time specification: ~tp. "
+ "Should be seconds, minutes, or hours.~n", [Other]),
+ exit({invalid_time_format,Other});
+time_ms(Ms, _, _) when is_integer(Ms) -> Ms;
+time_ms(infinity, _, _) -> infinity;
+time_ms(Fun, TCPid, MultAndScale) when is_function(Fun) ->
+ time_ms_apply(Fun, TCPid, MultAndScale);
+time_ms({M,F,A}=MFA, TCPid, MultAndScale) when is_atom(M),
+ is_atom(F),
+ is_list(A) ->
+ time_ms_apply(MFA, TCPid, MultAndScale);
+time_ms(Other, _, _) -> exit({invalid_time_format,Other}).
+
+time_ms_check(MFA = {M,F,A}) when is_atom(M), is_atom(F), is_list(A) ->
+ MFA;
+time_ms_check(Fun) when is_function(Fun) ->
+ Fun;
+time_ms_check(Other) ->
+ time_ms(Other, undefined, undefined).
+
+time_ms_apply(Func, TCPid, MultAndScale) ->
+ {_,GL} = process_info(TCPid, group_leader),
+ WhoAmI = self(), % either TC or IO server
+ T0 = erlang:monotonic_time(),
+ UserTTSup =
+ spawn(fun() ->
+ user_timetrap_supervisor(Func, WhoAmI, TCPid,
+ GL, T0, MultAndScale)
+ end),
+ receive
+ {UserTTSup,infinity} ->
+ %% remember the user timetrap so that it can be cancelled
+ save_user_timetrap(TCPid, UserTTSup, T0),
+ %% we need to make sure the user timetrap function
+ %% gets time to execute and return
+ timetrap(infinity, TCPid, MultAndScale)
+ after 5000 ->
+ exit(UserTTSup, kill),
+ if WhoAmI /= GL ->
+ exit({user_timetrap_error,time_ms_apply});
+ true ->
+ format("=== ERROR: User timetrap execution failed!", []),
+ ignore
+ end
+ end.
+
+user_timetrap_supervisor(Func, Spawner, TCPid, GL, T0, MultAndScale) ->
+ process_flag(trap_exit, true),
+ ct_util:mark_process(),
+ Spawner ! {self(),infinity},
+ MonRef = monitor(process, TCPid),
+ UserTTSup = self(),
+ group_leader(GL, UserTTSup),
+ UserTT = spawn_link(fun() -> call_user_timetrap(Func, UserTTSup) end),
+ receive
+ {UserTT,Result} ->
+ demonitor(MonRef, [flush]),
+ T1 = erlang:monotonic_time(),
+ Elapsed = erlang:convert_time_unit(T1-T0, native, milli_seconds),
+ try time_ms_check(Result) of
+ TimeVal ->
+ %% this is the new timetrap value to set (return value
+ %% from a fun or an MFA)
+ GL ! {user_timetrap,TCPid,TimeVal,T0,Elapsed,MultAndScale}
+ catch _:_ ->
+ %% when other than a legal timetrap value is returned
+ %% which will be the normal case for user timetraps
+ GL ! {user_timetrap,TCPid,0,T0,Elapsed,MultAndScale}
+ end;
+ {'EXIT',UserTT,Error} when Error /= normal ->
+ demonitor(MonRef, [flush]),
+ GL ! {user_timetrap,TCPid,0,T0,{user_timetrap_error,Error},
+ MultAndScale};
+ {'DOWN',MonRef,_,_,_} ->
+ demonitor(MonRef, [flush]),
+ exit(UserTT, kill)
+ end.
+
+call_user_timetrap(Func, Sup) when is_function(Func) ->
+ try Func() of
+ Result ->
+ Sup ! {self(),Result}
+ catch _:Error:Stk ->
+ exit({Error,Stk})
+ end;
+call_user_timetrap({M,F,A}, Sup) ->
+ try apply(M,F,A) of
+ Result ->
+ Sup ! {self(),Result}
+ catch _:Error:Stk ->
+ exit({Error,Stk})
+ end.
+
+save_user_timetrap(TCPid, UserTTSup, StartTime) ->
+ %% save pid of user timetrap supervisor process so that
+ %% it may be stopped even before the timetrap func has returned
+ NewUserTT = {TCPid,{UserTTSup,StartTime}},
+ case get(test_server_user_timetrap) of
+ undefined ->
+ put(test_server_user_timetrap, [NewUserTT]);
+ UserTTSups ->
+ case proplists:get_value(TCPid, UserTTSups) of
+ undefined ->
+ put(test_server_user_timetrap,
+ [NewUserTT | UserTTSups]);
+ PrevTTSup ->
+ %% remove prev user timetrap
+ remove_user_timetrap(PrevTTSup),
+ put(test_server_user_timetrap,
+ [NewUserTT | proplists:delete(TCPid,
+ UserTTSups)])
+ end
+ end.
+
+update_user_timetraps(TCPid, StartTime) ->
+ %% called when a user timetrap is triggered
+ case get(test_server_user_timetrap) of
+ undefined ->
+ proceed;
+ UserTTs ->
+ case proplists:get_value(TCPid, UserTTs) of
+ {_UserTTSup,StartTime} -> % same timetrap
+ put(test_server_user_timetrap,
+ proplists:delete(TCPid, UserTTs)),
+ proceed;
+ {OtherUserTTSup,OtherStartTime} ->
+ case OtherStartTime - StartTime of
+ Diff when Diff >= 0 ->
+ ignore;
+ _ ->
+ exit(OtherUserTTSup, kill),
+ put(test_server_user_timetrap,
+ proplists:delete(TCPid, UserTTs)),
+ proceed
+ end;
+ undefined ->
+ proceed
+ end
+ end.
+
+remove_user_timetrap(TTSup) ->
+ exit(TTSup, kill).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timetrap_cancel(Handle) -> ok
+%% Handle = term()
+%%
+%% Cancels a time trap.
+timetrap_cancel(Handle) ->
+ timetrap_cancel_one(Handle, true).
+
+timetrap_cancel_one(infinity, _SendToServer) ->
+ ok;
+timetrap_cancel_one(Handle, SendToServer) ->
+ case get(test_server_timetraps) of
+ undefined ->
+ ok;
+ [{Handle,_,_}] ->
+ erase(test_server_timetraps);
+ Timers ->
+ case lists:keysearch(Handle, 1, Timers) of
+ {value,_} ->
+ put(test_server_timetraps,
+ lists:keydelete(Handle, 1, Timers));
+ false when SendToServer == true ->
+ group_leader() ! {timetrap_cancel_one,Handle,self()};
+ false ->
+ ok
+ end
+ end,
+ test_server_sup:timetrap_cancel(Handle).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timetrap_cancel() -> ok
+%%
+%% Cancels timetrap for current test case.
+timetrap_cancel() ->
+ timetrap_cancel_all(self(), true).
+
+timetrap_cancel_all(TCPid, SendToServer) ->
+ case get(test_server_timetraps) of
+ undefined ->
+ ok;
+ Timers ->
+ [timetrap_cancel_one(Handle, false) ||
+ {Handle,Pid,_} <- Timers, Pid == TCPid],
+ ok
+ end,
+ case get(test_server_user_timetrap) of
+ undefined ->
+ ok;
+ UserTTs ->
+ case proplists:get_value(TCPid, UserTTs) of
+ {UserTTSup,_StartTime} ->
+ remove_user_timetrap(UserTTSup),
+ put(test_server_user_timetrap,
+ proplists:delete(TCPid, UserTTs)),
+ ok;
+ undefined ->
+ ok
+ end
+ end,
+ if SendToServer == true ->
+ group_leader() ! {timetrap_cancel_all,TCPid,self()},
+ ok;
+ true ->
+ ok
+ end,
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% get_timetrap_info() -> {Timeout,Scale} | undefined
+%%
+%% Read timetrap info for current test case
+get_timetrap_info() ->
+ get_timetrap_info(self(), true).
+
+get_timetrap_info(TCPid, SendToServer) ->
+ case get(test_server_timetraps) of
+ undefined ->
+ undefined;
+ Timers ->
+ case [Info || {Handle,Pid,Info} <- Timers,
+ Pid == TCPid, Handle /= infinity] of
+ [{TVal,true}|_] ->
+ {TVal,{true,test_server:timetrap_scale_factor()}};
+ [{TVal,false}|_] ->
+ {TVal,{false,1}};
+ [] when SendToServer == true ->
+ case tc_supervisor_req({get_timetrap_info,TCPid}) of
+ {TVal,true} ->
+ {TVal,{true,test_server:timetrap_scale_factor()}};
+ {TVal,false} ->
+ {TVal,{false,1}};
+ Error ->
+ Error
+ end;
+ [] ->
+ undefined
+ end
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% hours(N) -> Milliseconds
+%% minutes(N) -> Milliseconds
+%% seconds(N) -> Milliseconds
+%% N = integer() | float()
+%% Milliseconds = integer()
+%%
+%% Transforms the named units to milliseconds. Fractions in the input
+%% are accepted. The output is an integer.
+hours(N) -> trunc(N * 1000 * 60 * 60).
+minutes(N) -> trunc(N * 1000 * 60).
+seconds(N) -> trunc(N * 1000).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% tc_supervisor_req(Tag) -> Result
+%% tc_supervisor_req(Tag, Msg) -> Result
+%%
+
+tc_supervisor_req(Tag) ->
+ Pid = test_server_gl:get_tc_supervisor(group_leader()),
+ Pid ! {Tag,self()},
+ receive
+ {Pid,Tag,Result} ->
+ Result
+ after 5000 ->
+ error(no_answer_from_tc_supervisor)
+ end.
+
+tc_supervisor_req(Tag, Msg) ->
+ Pid = test_server_gl:get_tc_supervisor(group_leader()),
+ Pid ! {Tag,self(),Msg},
+ receive
+ {Pid,Tag,Result} ->
+ Result
+ after 5000 ->
+ error(no_answer_from_tc_supervisor)
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timecall(M,F,A) -> {Time,Val}
+%% Time = float()
+%%
+%% Measures the time spent evaluating MFA. The measurement is done with
+%% erlang:now/0, and should have pretty good accuracy on most platforms.
+%% The function is not evaluated in a catch context.
+timecall(M, F, A) ->
+ test_server_sup:timecall(M,F,A).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% do_times(N,M,F,A) -> ok
+%% do_times(N,Fun) ->
+%% N = integer()
+%% Fun = fun() -> void()
+%%
+%% Evaluates MFA or Fun N times, and returns ok.
+do_times(N,M,F,A) when N>0 ->
+ apply(M,F,A),
+ do_times(N-1,M,F,A);
+do_times(0,_,_,_) ->
+ ok.
+
+do_times(N,Fun) when N>0 ->
+ Fun(),
+ do_times(N-1,Fun);
+do_times(0,_) ->
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% m_out_of_n(M,N,Fun) -> ok | exit({m_out_of_n_failed,{R,left_to_do}})
+%% M = integer()
+%% N = integer()
+%% Fun = fun() -> void()
+%% R = integer()
+%%
+%% Repeats evaluating the given function until it succeeded (didn't crash)
+%% M times. If, after N times, M successful attempts have not been
+%% accomplished, the process crashes with reason {m_out_of_n_failed
+%% {R,left_to_do}}, where R indicates how many cases that remained to be
+%% successfully completed.
+%%
+%% For example:
+%% m_out_of_n(1,4,fun() -> tricky_test_case() end)
+%% Tries to run tricky_test_case() up to 4 times,
+%% and is happy if it succeeds once.
+%%
+%% m_out_of_n(7,8,fun() -> clock_sanity_check() end)
+%% Tries running clock_sanity_check() up to 8
+%% times and allows the function to fail once.
+%% This might be useful if clock_sanity_check/0
+%% is known to fail if the clock crosses an hour
+%% boundary during the test (and the up to 8
+%% test runs could never cross 2 boundaries)
+m_out_of_n(0,_,_) ->
+ ok;
+m_out_of_n(M,0,_) ->
+ exit({m_out_of_n_failed,{M,left_to_do}});
+m_out_of_n(M,N,Fun) ->
+ case catch Fun() of
+ {'EXIT',_} ->
+ m_out_of_n(M,N-1,Fun);
+ _Other ->
+ m_out_of_n(M-1,N-1,Fun)
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%call_crash(M,F,A)
+%%call_crash(Time,M,F,A)
+%%call_crash(Time,Crash,M,F,A)
+%% M - atom()
+%% F - atom()
+%% A - [term()]
+%% Time - integer() in milliseconds.
+%% Crash - term()
+%%
+%% Spaws a new process that calls MFA. The call is considered
+%% successful if the call crashes with the given reason (Crash),
+%% or any other reason if Crash is not specified.
+%% ** The call must terminate withing the given Time (defaults
+%% to infinity), or it is considered a failure (exit with reason
+%% 'call_crash_timeout' is generated).
+
+call_crash(M,F,A) ->
+ call_crash(infinity,M,F,A).
+call_crash(Time,M,F,A) ->
+ call_crash(Time,any,M,F,A).
+call_crash(Time,Crash,M,F,A) ->
+ test_server_sup:call_crash(Time,Crash,M,F,A).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% start_node(SlaveName, Type, Options) ->
+%% {ok, Slave} | {error, Reason}
+%%
+%% SlaveName = string(), atom().
+%% Type = slave | peer
+%% Options = [{tuple(), term()}]
+%%
+%% OptionList is a tuplelist wich may contain one
+%% or more of these members:
+%%
+%% Slave and Peer:
+%% {remote, true} - Start the node on a remote host. If not specified,
+%% the node will be started on the local host (with
+%% some exceptions, for instance VxWorks,
+%% where all nodes are started on a remote host).
+%% {args, Arguments} - Arguments passed directly to the node.
+%% {cleanup, false} - Nodes started with this option will not be killed
+%% by the test server after completion of the test case
+%% Therefore it is IMPORTANT that the USER terminates
+%% the node!!
+%% {erl, ReleaseList} - Use an Erlang emulator determined by ReleaseList
+%% when starting nodes, instead of the same emulator
+%% as the test server is running. ReleaseList is a list
+%% of specifiers, where a specifier is either
+%% {release, Rel}, {prog, Prog}, or 'this'. Rel is
+%% either the name of a release, e.g., "r7a" or
+%% 'latest'. 'this' means using the same emulator as
+%% the test server. Prog is the name of an emulator
+%% executable. If the list has more than one element,
+%% one of them is picked randomly. (Only
+%% works on Solaris and Linux, and the test
+%% server gives warnings when it notices that
+%% nodes are not of the same version as
+%% itself.)
+%%
+%% Peer only:
+%% {wait, false} - Don't wait for the node to be started.
+%% {fail_on_error, false} - Returns {error, Reason} rather than failing
+%% the test case. This option can only be used with
+%% peer nodes.
+%% Note that slave nodes always act as if they had
+%% fail_on_error==false.
+%%
+
+start_node(Name, Type, Options) ->
+ lists:foreach(
+ fun(N) ->
+ case firstname(N) of
+ Name ->
+ format("=== WARNING: Trying to start node \'~w\' when node"
+ " with same first name exists: ~w", [Name, N]);
+ _other -> ok
+ end
+ end,
+ nodes()),
+
+ group_leader() ! {sync_apply,
+ self(),
+ {test_server_ctrl,start_node,[Name,Type,Options]}},
+ Result = receive {sync_result,R} -> R end,
+
+ case Result of
+ {ok,Node} ->
+
+ %% Cannot run cover on shielded node or on a node started
+ %% by a shielded node.
+ Cover = case is_cover(Node) of
+ true ->
+ proplists:get_value(start_cover,Options,true);
+ false ->
+ false
+ end,
+
+ net_adm:ping(Node),
+ case Cover of
+ true ->
+ do_cover_for_node(Node,start);
+ _ ->
+ ok
+ end,
+ {ok,Node};
+ {fail,Reason} -> fail(Reason);
+ Error -> Error
+ end.
+
+firstname(N) ->
+ list_to_atom(upto($@,atom_to_list(N))).
+
+%% This should!!! crash if H is not member in list.
+upto(H, [H | _T]) -> [];
+upto(H, [X | T]) -> [X | upto(H,T)].
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% wait_for_node(Name) -> ok | {error,timeout}
+%%
+%% If a node is started with the options {wait,false}, this function
+%% can be used to wait for the node to come up from the
+%% test server point of view (i.e. wait until it has contacted
+%% the test server controller after startup)
+wait_for_node(Slave) ->
+ group_leader() ! {sync_apply,
+ self(),
+ {test_server_ctrl,wait_for_node,[Slave]}},
+ Result = receive {sync_result,R} -> R end,
+ case Result of
+ ok ->
+ net_adm:ping(Slave),
+ case is_cover(Slave) of
+ true ->
+ do_cover_for_node(Slave,start);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ Result.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% stop_node(Name) -> true|false
+%%
+%% Kills a (remote) node.
+%% Also inform test_server_ctrl so it can clean up!
+stop_node(Slave) ->
+ Cover = is_cover(Slave),
+ if Cover -> do_cover_for_node(Slave,flush,false);
+ true -> ok
+ end,
+ group_leader() ! {sync_apply,self(),{test_server_ctrl,stop_node,[Slave]}},
+ Result = receive {sync_result,R} -> R end,
+ case Result of
+ ok ->
+ erlang:monitor_node(Slave, true),
+ slave:stop(Slave),
+ receive
+ {nodedown, Slave} ->
+ format(minor, "Stopped slave node: ~w", [Slave]),
+ format(major, "=node_stop ~w", [Slave]),
+ if Cover -> do_cover_for_node(Slave,stop,false);
+ true -> ok
+ end,
+ true
+ after 30000 ->
+ format("=== WARNING: Node ~w does not seem to terminate.",
+ [Slave]),
+ erlang:monitor_node(Slave, false),
+ receive {nodedown, Slave} -> ok after 0 -> ok end,
+ false
+ end;
+ {error, _Reason} ->
+ %% Either, the node is already dead or it was started
+ %% with the {cleanup,false} option, or it was started
+ %% in some other way than test_server:start_node/3
+ format("=== WARNING: Attempt to stop a nonexisting slavenode (~w)~n"
+ "=== Trying to kill it anyway!!!",
+ [Slave]),
+ case net_adm:ping(Slave)of
+ pong ->
+ erlang:monitor_node(Slave, true),
+ slave:stop(Slave),
+ receive
+ {nodedown, Slave} ->
+ format(minor, "Stopped slave node: ~w", [Slave]),
+ format(major, "=node_stop ~w", [Slave]),
+ if Cover -> do_cover_for_node(Slave,stop,false);
+ true -> ok
+ end,
+ true
+ after 30000 ->
+ format("=== WARNING: Node ~w does not seem to terminate.",
+ [Slave]),
+ erlang:monitor_node(Slave, false),
+ receive {nodedown, Slave} -> ok after 0 -> ok end,
+ false
+ end;
+ pang ->
+ if Cover -> do_cover_for_node(Slave,stop,false);
+ true -> ok
+ end,
+ false
+ end
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% is_release_available(Release) -> true | false
+%% Release -> string()
+%%
+%% Test if a release (such as "r10b") is available to be
+%% started using start_node/3.
+
+is_release_available(Release) ->
+ group_leader() ! {sync_apply,
+ self(),
+ {test_server_ctrl,is_release_available,[Release]}},
+ receive {sync_result,R} -> R end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% run_on_shielded_node(Fun, CArgs) -> term()
+%% Fun -> function()
+%% CArg -> list()
+%%
+%%
+%% Fun is executed in a process on a temporarily created
+%% hidden node. Communication with the job process goes
+%% via a job proxy process on the hidden node, i.e. the
+%% group leader of the test case process is the job proxy
+%% process. This makes it possible to start nodes from the
+%% hidden node that are unaware of the test server node.
+%% Without the job proxy process all processes would have
+%% a process residing on the test_server node as group_leader.
+%%
+%% Fun - Function to execute
+%% CArg - Extra command line arguments to use when starting
+%% the shielded node.
+%%
+%% If Fun is successfully executed, the result is returned.
+%%
+
+run_on_shielded_node(Fun, CArgs) when is_function(Fun), is_list(CArgs) ->
+ Nr = erlang:unique_integer([positive]),
+ Name = "shielded_node-" ++ integer_to_list(Nr),
+ Node = case start_node(Name, slave, [{args, "-hidden " ++ CArgs}]) of
+ {ok, N} -> N;
+ Err -> fail({failed_to_start_shielded_node, Err})
+ end,
+ Master = self(),
+ Ref = make_ref(),
+ Slave = spawn(Node, start_job_proxy_fun(Master, Fun)),
+ MRef = erlang:monitor(process, Slave),
+ Slave ! Ref,
+ receive
+ {'DOWN', MRef, _, _, Info} ->
+ stop_node(Node),
+ fail(Info);
+ {Ref, Res} ->
+ stop_node(Node),
+ receive
+ {'DOWN', MRef, _, _, _} ->
+ Res
+ end
+ end.
+
+-spec start_job_proxy_fun(_, _) -> fun(() -> no_return()).
+start_job_proxy_fun(Master, Fun) ->
+ fun () ->
+ ct_util:mark_process(),
+ _ = start_job_proxy(),
+ receive
+ Ref ->
+ Master ! {Ref, Fun()},
+ ok
+ end,
+ receive after infinity -> infinity end
+ end.
+
+%% Return true if Name or node() is a shielded node
+is_shielded(Name) ->
+ case {cast_to_list(Name),atom_to_list(node())} of
+ {"shielded_node"++_,_} -> true;
+ {_,"shielded_node"++_} -> true;
+ _ -> false
+ end.
+
+same_version(Name) ->
+ ThisVersion = erlang:system_info(version),
+ OtherVersion = rpc:call(Name, erlang, system_info, [version]),
+ ThisVersion =:= OtherVersion.
+
+is_cover(Name) ->
+ case is_cover() of
+ true ->
+ not is_shielded(Name) andalso same_version(Name);
+ false ->
+ false
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% temp_name(Stem) -> string()
+%% Stem = string()
+%%
+%% Create a unique file name, based on (starting with) Stem.
+%% A filename of the form <Stem><Number> is generated, and the
+%% function checks that that file doesn't already exist.
+temp_name(Stem) ->
+ Num = erlang:unique_integer([positive]),
+ RandomName = Stem ++ integer_to_list(Num),
+ {ok,Files} = file:list_dir(filename:dirname(Stem)),
+ case lists:member(RandomName,Files) of
+ true ->
+ %% oh, already exists - bad luck. Try again.
+ temp_name(Stem); %% recursively try again
+ false ->
+ RandomName
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% app_test/1
+%%
+app_test(App) ->
+ app_test(App, pedantic).
+app_test(App, Mode) ->
+ test_server_sup:app_test(App, Mode).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% appup_test/1
+%%
+appup_test(App) ->
+ test_server_sup:appup_test(App).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% is_native(Mod) -> true | false
+%%
+%% Checks wether the module is natively compiled or not.
+
+is_native(Mod) ->
+ (catch Mod:module_info(native)) =:= true.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% comment(String) -> ok
+%%
+%% The given String will occur in the comment field
+%% of the table on the test suite result page. If
+%% called several times, only the last comment is
+%% printed.
+%% comment/1 is also overwritten by the return value
+%% {comment,Comment} or fail/1 (which prints Reason
+%% as a comment).
+comment(String) ->
+ group_leader() ! {comment,String},
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% read_comment() -> string()
+%%
+%% Read the current comment string stored in
+%% state during test case execution.
+read_comment() ->
+ tc_supervisor_req(read_comment).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% make_priv_dir() -> ok
+%%
+%% Order test server to create the private directory
+%% for the current test case.
+make_priv_dir() ->
+ tc_supervisor_req(make_priv_dir).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% os_type() -> OsType
+%%
+%% Returns the OsType of the target node. OsType is
+%% the same as returned from os:type()
+os_type() ->
+ os:type().
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% is_cover() -> boolean()
+%%
+%% Returns true if cover is running, else false
+is_cover() ->
+ case whereis(cover_server) of
+ undefined -> false;
+ _ -> true
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% is_debug() -> boolean()
+%%
+%% Returns true if the emulator is debug-compiled, false otherwise.
+is_debug() ->
+ case catch erlang:system_info(debug_compiled) of
+ {'EXIT', _} ->
+ case string:find(erlang:system_info(system_version), "debug") of
+ nomatch -> false;
+ _ -> true
+ 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:find(erlang:system_info(system_version), "source") of
+ nomatch -> true;
+ _ -> false
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% is_valgrind() -> boolean()
+%%
+%% Returns true if valgrind is running, else false
+is_valgrind() ->
+ case catch erlang:system_info({valgrind, running}) of
+ {'EXIT', _} -> false;
+ Res -> Res
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% DEBUGGER INTERFACE %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% valgrind_new_leaks() -> ok
+%%
+%% Checks for new memory leaks if Valgrind is active.
+valgrind_new_leaks() ->
+ catch erlang:system_info({valgrind, memory}),
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% valgrind_format(Format, Args) -> ok
+%% Format = string()
+%% Args = lists()
+%%
+%% Outputs the formatted string to Valgrind's logfile,if Valgrind is active.
+valgrind_format(Format, Args) ->
+ (catch erlang:system_info({valgrind, io_lib:format(Format, Args)})),
+ ok.
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% Apply given function and reply to caller or proxy.
+%%
+do_sync_apply(Proxy, From, {M,F,A}) ->
+ Result = apply(M, F, A),
+ if is_pid(Proxy) ->
+ Proxy ! {sync_result_proxy,From,Result},
+ ok;
+ true ->
+ From ! {sync_result,Result},
+ ok
+ end.
+
+start_cover() ->
+ case cover:start() of
+ {error, {already_started, Pid}} ->
+ {ok, Pid};
+ Else ->
+ Else
+ end.
+
diff --git a/lib/common_test/src/test_server_ctrl.erl b/lib/common_test/src/test_server_ctrl.erl
new file mode 100644
index 0000000000..8bd6cd583a
--- /dev/null
+++ b/lib/common_test/src/test_server_ctrl.erl
@@ -0,0 +1,5763 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2002-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+-module(test_server_ctrl).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% %%
+%% The Erlang Test Server %%
+%% %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% MODULE DEPENDENCIES:
+%% HARD TO REMOVE: erlang, lists, io_lib, gen_server, file, io, string,
+%% code, ets, rpc, gen_tcp, inet, erl_tar, sets,
+%% test_server, test_server_sup, test_server_node
+%% EASIER TO REMOVE: filename, filelib, lib, re
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%% SUPERVISOR INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([start/0, start/1, start_link/1, stop/0]).
+
+%%% OPERATOR INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([add_spec/1, add_dir/2, add_dir/3]).
+-export([add_module/1, add_module/2,
+ add_conf/3,
+ add_case/2, add_case/3, add_cases/2, add_cases/3]).
+-export([add_dir_with_skip/3, add_dir_with_skip/4, add_tests_with_skip/3]).
+-export([add_module_with_skip/2, add_module_with_skip/3,
+ add_conf_with_skip/4,
+ add_case_with_skip/3, add_case_with_skip/4,
+ add_cases_with_skip/3, add_cases_with_skip/4]).
+-export([jobs/0, run_test/1, wait_finish/0, idle_notify/1,
+ abort_current_testcase/1, abort/0]).
+-export([start_get_totals/1, stop_get_totals/0]).
+-export([reject_io_reqs/1, get_levels/0, set_levels/3]).
+-export([multiply_timetraps/1, scale_timetraps/1, get_timetrap_parameters/0]).
+-export([create_priv_dir/1]).
+-export([cover/1, cover/2, cover/3,
+ cover_compile/7, cover_analyse/2, cross_cover_analyse/2,
+ trc/1, stop_trace/0]).
+-export([testcase_callback/1]).
+-export([set_random_seed/1]).
+-export([kill_slavenodes/0]).
+
+%%% TEST_SERVER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([print/2, print/3, print/4, print_timestamp/2]).
+-export([start_node/3, stop_node/1, wait_for_node/1, is_release_available/1]).
+-export([format/1, format/2, format/3, to_string/1]).
+-export([get_target_info/0]).
+-export([get_hosts/0]).
+-export([node_started/1]).
+-export([uri_encode/1,uri_encode/2]).
+
+%%% DEBUGGER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([i/0, p/1, p/3, pi/2, pi/4, t/0, t/1]).
+
+%%% PRIVATE EXPORTED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([init/1, terminate/2]).
+-export([handle_call/3, handle_cast/2, handle_info/2]).
+-export([do_test_cases/4]).
+-export([do_spec/2, do_spec_list/2]).
+-export([xhtml/2, escape_chars/1]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-include("test_server_internal.hrl").
+-include_lib("kernel/include/file.hrl").
+-define(suite_ext, "_SUITE").
+-define(log_ext, ".log.html").
+-define(src_listing_ext, ".src.html").
+-define(logdir_ext, ".logs").
+-define(data_dir_suffix, "_data/").
+-define(suitelog_name, "suite.log").
+-define(suitelog_latest_name, "suite.log.latest").
+-define(coverlog_name, "cover.html").
+-define(raw_coverlog_name, "cover.log").
+-define(cross_coverlog_name, "cross_cover.html").
+-define(raw_cross_coverlog_name, "cross_cover.log").
+-define(cross_cover_info, "cross_cover.info").
+-define(cover_total, "total_cover.log").
+-define(unexpected_io_log, "unexpected_io.log.html").
+-define(last_file, "last_name").
+-define(last_link, "last_link").
+-define(last_test, "last_test").
+-define(html_ext, ".html").
+-define(now, os:timestamp()).
+
+-define(void_fun, fun() -> ok end).
+-define(mod_result(X), if X == skip -> skipped;
+ X == auto_skip -> skipped;
+ true -> X end).
+
+-define(auto_skip_color, "#FFA64D").
+-define(user_skip_color, "#FF8000").
+-define(sortable_table_name, "SortableTable").
+
+-record(state,{jobs=[], levels={1,19,10}, reject_io_reqs=false,
+ multiply_timetraps=1, scale_timetraps=true,
+ create_priv_dir=auto_per_run, finish=false,
+ target_info, trc=false, cover=false, wait_for_node=[],
+ testcase_callback=undefined, idle_notify=[],
+ get_totals=false, random_seed=undefined}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% OPERATOR INTERFACE
+
+add_dir(Name, Job=[Dir|_Dirs]) when is_list(Dir) ->
+ add_job(cast_to_list(Name),
+ lists:map(fun(D)-> {dir,cast_to_list(D)} end, Job));
+add_dir(Name, Dir) ->
+ add_job(cast_to_list(Name), {dir,cast_to_list(Dir)}).
+
+add_dir(Name, Job=[Dir|_Dirs], Pattern) when is_list(Dir) ->
+ add_job(cast_to_list(Name),
+ lists:map(fun(D)-> {dir,cast_to_list(D),
+ cast_to_list(Pattern)} end, Job));
+add_dir(Name, Dir, Pattern) ->
+ add_job(cast_to_list(Name), {dir,cast_to_list(Dir),cast_to_list(Pattern)}).
+
+add_module(Mod) when is_atom(Mod) ->
+ add_job(atom_to_list(Mod), {Mod,all}).
+
+add_module(Name, Mods) when is_list(Mods) ->
+ add_job(cast_to_list(Name), lists:map(fun(Mod) -> {Mod,all} end, Mods)).
+
+add_conf(Name, Mod, Conf) when is_tuple(Conf) ->
+ add_job(cast_to_list(Name), {Mod,[Conf]});
+
+add_conf(Name, Mod, Confs) when is_list(Confs) ->
+ add_job(cast_to_list(Name), {Mod,Confs}).
+
+add_case(Mod, Case) when is_atom(Mod), is_atom(Case) ->
+ add_job(atom_to_list(Mod), {Mod,Case}).
+
+add_case(Name, Mod, Case) when is_atom(Mod), is_atom(Case) ->
+ add_job(Name, {Mod,Case}).
+
+add_cases(Mod, Cases) when is_atom(Mod), is_list(Cases) ->
+ add_job(atom_to_list(Mod), {Mod,Cases}).
+
+add_cases(Name, Mod, Cases) when is_atom(Mod), is_list(Cases) ->
+ add_job(Name, {Mod,Cases}).
+
+add_spec(Spec) ->
+ Name = filename:rootname(Spec, ".spec"),
+ case filelib:is_file(Spec) of
+ true -> add_job(Name, {spec,Spec});
+ false -> {error,nofile}
+ end.
+
+%% This version of the interface is to be used if there are
+%% suites or cases that should be skipped.
+
+add_dir_with_skip(Name, Job=[Dir|_Dirs], Skip) when is_list(Dir) ->
+ add_job(cast_to_list(Name),
+ lists:map(fun(D)-> {dir,cast_to_list(D)} end, Job),
+ Skip);
+add_dir_with_skip(Name, Dir, Skip) ->
+ add_job(cast_to_list(Name), {dir,cast_to_list(Dir)}, Skip).
+
+add_dir_with_skip(Name, Job=[Dir|_Dirs], Pattern, Skip) when is_list(Dir) ->
+ add_job(cast_to_list(Name),
+ lists:map(fun(D)-> {dir,cast_to_list(D),
+ cast_to_list(Pattern)} end, Job),
+ Skip);
+add_dir_with_skip(Name, Dir, Pattern, Skip) ->
+ add_job(cast_to_list(Name),
+ {dir,cast_to_list(Dir),cast_to_list(Pattern)}, Skip).
+
+add_module_with_skip(Mod, Skip) when is_atom(Mod) ->
+ add_job(atom_to_list(Mod), {Mod,all}, Skip).
+
+add_module_with_skip(Name, Mods, Skip) when is_list(Mods) ->
+ add_job(cast_to_list(Name), lists:map(fun(Mod) -> {Mod,all} end, Mods), Skip).
+
+add_conf_with_skip(Name, Mod, Conf, Skip) when is_tuple(Conf) ->
+ add_job(cast_to_list(Name), {Mod,[Conf]}, Skip);
+
+add_conf_with_skip(Name, Mod, Confs, Skip) when is_list(Confs) ->
+ add_job(cast_to_list(Name), {Mod,Confs}, Skip).
+
+add_case_with_skip(Mod, Case, Skip) when is_atom(Mod), is_atom(Case) ->
+ add_job(atom_to_list(Mod), {Mod,Case}, Skip).
+
+add_case_with_skip(Name, Mod, Case, Skip) when is_atom(Mod), is_atom(Case) ->
+ add_job(Name, {Mod,Case}, Skip).
+
+add_cases_with_skip(Mod, Cases, Skip) when is_atom(Mod), is_list(Cases) ->
+ add_job(atom_to_list(Mod), {Mod,Cases}, Skip).
+
+add_cases_with_skip(Name, Mod, Cases, Skip) when is_atom(Mod), is_list(Cases) ->
+ add_job(Name, {Mod,Cases}, Skip).
+
+add_tests_with_skip(LogDir, Tests, Skip) ->
+ add_job(LogDir,
+ lists:map(fun({Dir,all,all}) ->
+ {Dir,{dir,Dir}};
+ ({Dir,Mods,all}) ->
+ {Dir,lists:map(fun(M) -> {M,all} end, Mods)};
+ ({Dir,Mod,Cases}) ->
+ {Dir,{Mod,Cases}}
+ end, Tests),
+ Skip).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% COMMAND LINE INTERFACE
+
+parse_cmd_line(Cmds) ->
+ parse_cmd_line(Cmds, [], [], local, false, false, undefined).
+
+parse_cmd_line(['SPEC',Spec|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ case file:consult(Spec) of
+ {ok, TermList} ->
+ Name = filename:rootname(Spec),
+ parse_cmd_line(Cmds, TermList++SpecList, [Name|Names], Param,
+ Trc, Cov, TCCB);
+ {error,Reason} ->
+ io:format("Can't open ~tw: ~tp\n",[Spec, file:format_error(Reason)]),
+ parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, TCCB)
+ end;
+parse_cmd_line(['NAME',Name|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ parse_cmd_line(Cmds, SpecList, [{name,atom_to_list(Name)}|Names],
+ Param, Trc, Cov, TCCB);
+parse_cmd_line(['SKIPMOD',Mod|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ parse_cmd_line(Cmds, [{skip,{Mod,"by command line"}}|SpecList], Names,
+ Param, Trc, Cov, TCCB);
+parse_cmd_line(['SKIPCASE',Mod,Case|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ parse_cmd_line(Cmds, [{skip,{Mod,Case,"by command line"}}|SpecList], Names,
+ Param, Trc, Cov, TCCB);
+parse_cmd_line(['DIR',Dir|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ Name = filename:basename(Dir),
+ parse_cmd_line(Cmds, [{topcase,{dir,Name}}|SpecList], [Name|Names],
+ Param, Trc, Cov, TCCB);
+parse_cmd_line(['MODULE',Mod|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ parse_cmd_line(Cmds,[{topcase,{Mod,all}}|SpecList],[atom_to_list(Mod)|Names],
+ Param, Trc, Cov, TCCB);
+parse_cmd_line(['CASE',Mod,Case|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ parse_cmd_line(Cmds,[{topcase,{Mod,Case}}|SpecList],[atom_to_list(Mod)|Names],
+ Param, Trc, Cov, TCCB);
+parse_cmd_line(['TRACE',Trc|Cmds], SpecList, Names, Param, _Trc, Cov, TCCB) ->
+ parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, TCCB);
+parse_cmd_line(['COVER',App,CF,Analyse|Cmds], SpecList, Names, Param, Trc, _Cov, TCCB) ->
+ parse_cmd_line(Cmds, SpecList, Names, Param, Trc, {{App,CF}, Analyse}, TCCB);
+parse_cmd_line(['TESTCASE_CALLBACK',Mod,Func|Cmds], SpecList, Names, Param, Trc, Cov, _) ->
+ parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, {Mod,Func});
+parse_cmd_line([Obj|_Cmds], _SpecList, _Names, _Param, _Trc, _Cov, _TCCB) ->
+ io:format("~w: Bad argument: ~tw\n", [?MODULE,Obj]),
+ io:format(" Use the `ts' module to start tests.\n", []),
+ io:format(" (If you ARE using `ts', there is a bug in `ts'.)\n", []),
+ halt(1);
+parse_cmd_line([], SpecList, Names, Param, Trc, Cov, TCCB) ->
+ NameList = lists:reverse(Names, ["suite"]),
+ Name = case lists:keysearch(name, 1, NameList) of
+ {value,{name,N}} -> N;
+ false -> hd(NameList)
+ end,
+ {lists:reverse(SpecList), Name, Param, Trc, Cov, TCCB}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% cast_to_list(X) -> string()
+%% X = list() | atom() | void()
+%% Returns a string representation of whatever was input
+
+cast_to_list(X) when is_list(X) -> X;
+cast_to_list(X) when is_atom(X) -> atom_to_list(X);
+cast_to_list(X) -> lists:flatten(io_lib:format("~tw", [X])).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% START INTERFACE
+
+%% Kept for backwards compatibility
+start(_) ->
+ start().
+start_link(_) ->
+ start_link().
+
+
+start() ->
+ case gen_server:start({local,?MODULE}, ?MODULE, [], []) of
+ {error, {already_started, Pid}} ->
+ {ok, Pid};
+ Other ->
+ Other
+ end.
+
+start_link() ->
+ case gen_server:start_link({local,?MODULE}, ?MODULE, [], []) of
+ {error, {already_started, Pid}} ->
+ {ok, Pid};
+ Other ->
+ Other
+ end.
+
+run_test(CommandLine) ->
+ process_flag(trap_exit,true),
+ {SpecList,Name,Param,Trc,Cov,TCCB} = parse_cmd_line(CommandLine),
+ {ok,_TSPid} = start_link(Param),
+ case Trc of
+ false -> ok;
+ File -> trc(File)
+ end,
+ case Cov of
+ false -> ok;
+ {{App,CoverFile},Analyse} -> cover(App, maybe_file(CoverFile), Analyse)
+ end,
+ testcase_callback(TCCB),
+ add_job(Name, {command_line,SpecList}),
+
+ wait_finish().
+
+%% Converted CoverFile to a string unless it is 'none'
+maybe_file(none) ->
+ none;
+maybe_file(CoverFile) ->
+ atom_to_list(CoverFile).
+
+idle_notify(Fun) ->
+ {ok, Pid} = controller_call({idle_notify,Fun}),
+ Pid.
+
+start_get_totals(Fun) ->
+ {ok, Pid} = controller_call({start_get_totals,Fun}),
+ Pid.
+
+stop_get_totals() ->
+ ok = controller_call(stop_get_totals),
+ ok.
+
+wait_finish() ->
+ OldTrap = process_flag(trap_exit, true),
+ {ok, Pid} = finish(true),
+ link(Pid),
+ receive
+ {'EXIT',Pid,_} ->
+ ok
+ end,
+ process_flag(trap_exit, OldTrap),
+ ok.
+
+abort_current_testcase(Reason) ->
+ controller_call({abort_current_testcase,Reason}).
+
+abort() ->
+ OldTrap = process_flag(trap_exit, true),
+ {ok, Pid} = finish(abort),
+ link(Pid),
+ receive
+ {'EXIT',Pid,_} ->
+ ok
+ end,
+ process_flag(trap_exit, OldTrap),
+ ok.
+
+finish(Abort) ->
+ controller_call({finish,Abort}).
+
+stop() ->
+ controller_call(stop).
+
+jobs() ->
+ controller_call(jobs).
+
+get_levels() ->
+ controller_call(get_levels).
+
+set_levels(Show, Major, Minor) ->
+ controller_call({set_levels,Show,Major,Minor}).
+
+reject_io_reqs(Bool) ->
+ controller_call({reject_io_reqs,Bool}).
+
+multiply_timetraps(N) ->
+ controller_call({multiply_timetraps,N}).
+
+scale_timetraps(Bool) ->
+ controller_call({scale_timetraps,Bool}).
+
+get_timetrap_parameters() ->
+ controller_call(get_timetrap_parameters).
+
+create_priv_dir(Value) ->
+ controller_call({create_priv_dir,Value}).
+
+trc(TraceFile) ->
+ controller_call({trace,TraceFile}, 2*?ACCEPT_TIMEOUT).
+
+stop_trace() ->
+ controller_call(stop_trace).
+
+node_started(Node) ->
+ gen_server:cast(?MODULE, {node_started,Node}).
+
+cover(App, Analyse) when is_atom(App) ->
+ cover(App, none, Analyse);
+cover(CoverFile, Analyse) ->
+ cover(none, CoverFile, Analyse).
+cover(App, CoverFile, Analyse) ->
+ {Excl,Incl,Cross} = read_cover_file(CoverFile),
+ CoverInfo = #cover{app=App,
+ file=CoverFile,
+ excl=Excl,
+ incl=Incl,
+ cross=Cross,
+ level=Analyse},
+ controller_call({cover,CoverInfo}).
+
+cover(CoverInfo) ->
+ controller_call({cover,CoverInfo}).
+
+cover_compile(App,File,Excl,Incl,Cross,Analyse,Stop) ->
+ cover_compile(#cover{app=App,
+ file=File,
+ excl=Excl,
+ incl=Incl,
+ cross=Cross,
+ level=Analyse,
+ stop=Stop}).
+
+testcase_callback(ModFunc) ->
+ controller_call({testcase_callback,ModFunc}).
+
+set_random_seed(Seed) ->
+ controller_call({set_random_seed,Seed}).
+
+kill_slavenodes() ->
+ controller_call(kill_slavenodes).
+
+get_hosts() ->
+ get(test_server_hosts).
+
+%%--------------------------------------------------------------------
+
+add_job(Name, TopCase) ->
+ add_job(Name, TopCase, []).
+
+add_job(Name, TopCase, Skip) ->
+ SuiteName =
+ case Name of
+ "." -> "current_dir";
+ ".." -> "parent_dir";
+ Other -> Other
+ end,
+ Dir = filename:absname(SuiteName),
+ controller_call({add_job,Dir,SuiteName,TopCase,Skip}).
+
+controller_call(Arg) ->
+ case catch gen_server:call(?MODULE, Arg, infinity) of
+ {'EXIT',{{badarg,_},{gen_server,call,_}}} ->
+ exit(test_server_ctrl_not_running);
+ {'EXIT',Reason} ->
+ exit(Reason);
+ Other ->
+ Other
+ end.
+controller_call(Arg, Timeout) ->
+ case catch gen_server:call(?MODULE, Arg, Timeout) of
+ {'EXIT',{{badarg,_},{gen_server,call,_}}} ->
+ exit(test_server_ctrl_not_running);
+ {'EXIT',Reason} ->
+ exit(Reason);
+ Other ->
+ Other
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% init([])
+%%
+%% init() is the init function of the test_server's gen_server.
+%%
+init([]) ->
+ case os:getenv("TEST_SERVER_CALL_TRACE") of
+ false ->
+ ok;
+ "" ->
+ ok;
+ TraceSpec ->
+ test_server_sup:call_trace(TraceSpec)
+ end,
+ process_flag(trap_exit, true),
+ %% copy format_exception setting from init arg to application environment
+ case init:get_argument(test_server_format_exception) of
+ {ok,[[TSFE]]} ->
+ application:set_env(test_server, format_exception, list_to_atom(TSFE));
+ _ ->
+ ok
+ end,
+ test_server_sup:cleanup_crash_dumps(),
+ test_server_sup:util_start(),
+ State = #state{jobs=[],finish=false},
+ TI0 = test_server:init_target_info(),
+ TargetHost = test_server_sup:hoststr(),
+ TI = TI0#target_info{host=TargetHost,
+ naming=naming(),
+ master=TargetHost},
+ _ = ets:new(slave_tab, [named_table,set,public,{keypos,2}]),
+ set_hosts([TI#target_info.host]),
+ {ok,State#state{target_info=TI}}.
+
+naming() ->
+ case lists:member($., test_server_sup:hoststr()) of
+ true -> "-name";
+ false -> "-sname"
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(kill_slavenodes, From, State) -> ok
+%%
+%% Kill all slave nodes that remain after a test case
+%% is completed.
+%%
+handle_call(kill_slavenodes, _From, State) ->
+ Nodes = test_server_node:kill_nodes(),
+ {reply, Nodes, State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({set_hosts, HostList}, From, State) -> ok
+%%
+%% Set the global hostlist.
+%%
+handle_call({set_hosts, Hosts}, _From, State) ->
+ set_hosts(Hosts),
+ {reply, ok, State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(get_hosts, From, State) -> [Hosts]
+%%
+%% Returns the lists of hosts that the test server
+%% can use for slave nodes. This is primarily used
+%% for nodename generation.
+%%
+handle_call(get_hosts, _From, State) ->
+ Hosts = get_hosts(),
+ {reply, Hosts, State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({add_job,Dir,Name,TopCase,Skip}, _, State) ->
+%% ok | {error,Reason}
+%%
+%% Dir = string()
+%% Name = string()
+%% TopCase = term()
+%% Skip = [SkipItem]
+%% SkipItem = {Mod,Comment} | {Mod,Case,Comment} | {Mod,Cases,Comment}
+%% Mod = Case = atom()
+%% Comment = string()
+%% Cases = [Case]
+%%
+%% Adds a job to the job queue. The name of the job is Name. A log directory
+%% will be created in Dir/Name.logs. TopCase may be anything that
+%% collect_cases/3 accepts, plus the following:
+%%
+%% {spec,SpecName} executes the named test suite specification file. Commands
+%% in the file should be in the format accepted by do_spec_list/1.
+%%
+%% {command_line,SpecList} executes the list of specification instructions
+%% supplied, which should be in the format accepted by do_spec_list/1.
+
+handle_call({add_job,Dir,Name,TopCase,Skip}, _From, State) ->
+ LogDir = Dir ++ ?logdir_ext,
+ ExtraTools =
+ case State#state.cover of
+ false -> [];
+ CoverInfo -> [{cover,CoverInfo}]
+ end,
+ ExtraTools1 =
+ case State#state.random_seed of
+ undefined -> ExtraTools;
+ Seed -> [{random_seed,Seed}|ExtraTools]
+ end,
+ case lists:keysearch(Name, 1, State#state.jobs) of
+ false ->
+ case TopCase of
+ {spec,SpecName} ->
+ Pid = spawn_tester(
+ ?MODULE, do_spec,
+ [SpecName,{State#state.multiply_timetraps,
+ State#state.scale_timetraps}],
+ LogDir, Name, State#state.levels,
+ State#state.reject_io_reqs,
+ State#state.create_priv_dir,
+ State#state.testcase_callback, ExtraTools1),
+ NewJobs = [{Name,Pid}|State#state.jobs],
+ {reply, ok, State#state{jobs=NewJobs}};
+ {command_line,SpecList} ->
+ Pid = spawn_tester(
+ ?MODULE, do_spec_list,
+ [SpecList,{State#state.multiply_timetraps,
+ State#state.scale_timetraps}],
+ LogDir, Name, State#state.levels,
+ State#state.reject_io_reqs,
+ State#state.create_priv_dir,
+ State#state.testcase_callback, ExtraTools1),
+ NewJobs = [{Name,Pid}|State#state.jobs],
+ {reply, ok, State#state{jobs=NewJobs}};
+ TopCase ->
+ case State#state.get_totals of
+ {CliPid,Fun} ->
+ Result = count_test_cases(TopCase, Skip),
+ Fun(CliPid, Result),
+ {reply, ok, State};
+ _ ->
+ Cfg = make_config([]),
+ Pid = spawn_tester(
+ ?MODULE, do_test_cases,
+ [TopCase,Skip,Cfg,
+ {State#state.multiply_timetraps,
+ State#state.scale_timetraps}],
+ LogDir, Name, State#state.levels,
+ State#state.reject_io_reqs,
+ State#state.create_priv_dir,
+ State#state.testcase_callback, ExtraTools1),
+ NewJobs = [{Name,Pid}|State#state.jobs],
+ {reply, ok, State#state{jobs=NewJobs}}
+ end
+ end;
+ _ ->
+ {reply,{error,name_already_in_use},State}
+ end;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(jobs, _, State) -> JobList
+%% JobList = [{Name,Pid}, ...]
+%% Name = string()
+%% Pid = pid()
+%%
+%% Return the list of current jobs.
+
+handle_call(jobs, _From, State) ->
+ {reply,State#state.jobs,State};
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({abort_current_testcase,Reason}, _, State) -> Result
+%% Reason = term()
+%% Result = ok | {error,no_testcase_running}
+%%
+%% Attempts to abort the test case that's currently running.
+
+handle_call({abort_current_testcase,Reason}, _From, State) ->
+ case State#state.jobs of
+ [{_,Pid}|_] ->
+ Pid ! {abort_current_testcase,Reason,self()},
+ receive
+ {Pid,abort_current_testcase,Result} ->
+ {reply, Result, State}
+ after 10000 ->
+ {reply, {error,no_testcase_running}, State}
+ end;
+ _ ->
+ {reply, {error,no_testcase_running}, State}
+ end;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({finish,Fini}, _, State) -> {ok,Pid}
+%% Fini = true | abort
+%%
+%% Tells the test_server to stop as soon as there are no test suites
+%% running. Immediately if none are running. Abort is handled as soon
+%% as current test finishes.
+
+handle_call({finish,Fini}, _From, State) ->
+ case State#state.jobs of
+ [] ->
+ lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Fini) end,
+ State#state.idle_notify),
+ State2 = State#state{finish=false},
+ {stop,shutdown,{ok,self()}, State2};
+ _SomeJobs ->
+ State2 = State#state{finish=Fini},
+ {reply, {ok,self()}, State2}
+ end;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({idle_notify,Fun}, From, State) -> {ok,Pid}
+%%
+%% Lets a test client subscribe to receive a notification when the
+%% test server becomes idle (can be used to syncronize jobs).
+%% test_server calls Fun(From) when idle.
+
+handle_call({idle_notify,Fun}, {Cli,_Ref}, State) ->
+ case State#state.jobs of
+ [] -> self() ! report_idle;
+ _ -> ok
+ end,
+ Subscribed = State#state.idle_notify,
+ {reply, {ok,self()}, State#state{idle_notify=[{Cli,Fun}|Subscribed]}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(start_get_totals, From, State) -> {ok,Pid}
+%%
+%% Switch on the mode where the test server will only
+%% report back the number of tests it would execute
+%% given some subsequent jobs.
+
+handle_call({start_get_totals,Fun}, {Cli,_Ref}, State) ->
+ {reply, {ok,self()}, State#state{get_totals={Cli,Fun}}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(stop_get_totals, From, State) -> ok
+%%
+%% Lets a test client subscribe to receive a notification when the
+%% test server becomes idle (can be used to syncronize jobs).
+%% test_server calls Fun(From) when idle.
+
+handle_call(stop_get_totals, {_Cli,_Ref}, State) ->
+ {reply, ok, State#state{get_totals=false}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(get_levels, _, State) -> {Show,Major,Minor}
+%% Show = integer()
+%% Major = integer()
+%% Minor = integer()
+%%
+%% Returns a 3-tuple with the logging thresholds.
+%% All output and information from a test suite is tagged with a detail
+%% level. Lower values are more "important". Text that is output using
+%% io:format or similar is automatically tagged with detail level 50.
+%%
+%% All output with detail level:
+%% less or equal to Show is displayed on the screen (default 1)
+%% less or equal to Major is logged in the major log file (default 19)
+%% greater or equal to Minor is logged in the minor log files (default 10)
+
+handle_call(get_levels, _From, State) ->
+ {reply,State#state.levels,State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({set_levels,Show,Major,Minor}, _, State) -> ok
+%% Show = integer()
+%% Major = integer()
+%% Minor = integer()
+%%
+%% Sets the logging thresholds, see handle_call(get_levels,...) above.
+
+handle_call({set_levels,Show,Major,Minor}, _From, State) ->
+ {reply,ok,State#state{levels={Show,Major,Minor}}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({reject_io_reqs,Bool}, _, State) -> ok
+%% Bool = bool()
+%%
+%% May be used to switch off stdout printouts to the minor log file
+
+handle_call({reject_io_reqs,Bool}, _From, State) ->
+ {reply,ok,State#state{reject_io_reqs=Bool}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({multiply_timetraps,N}, _, State) -> ok
+%% N = integer() | infinity
+%%
+%% Multiplies all timetraps set by test cases with N
+
+handle_call({multiply_timetraps,N}, _From, State) ->
+ {reply,ok,State#state{multiply_timetraps=N}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({scale_timetraps,Bool}, _, State) -> ok
+%% Bool = true | false
+%%
+%% Specifies if test_server should scale the timetrap value
+%% automatically if e.g. cover is running.
+
+handle_call({scale_timetraps,Bool}, _From, State) ->
+ {reply,ok,State#state{scale_timetraps=Bool}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(get_timetrap_parameters, _, State) -> {Multiplier,Scale}
+%% Multiplier = integer() | infinity
+%% Scale = true | false
+%%
+%% Returns the parameter values that affect timetraps.
+
+handle_call(get_timetrap_parameters, _From, State) ->
+ {reply,{State#state.multiply_timetraps,State#state.scale_timetraps},State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({trace,TraceFile}, _, State) -> ok | {error,Reason}
+%%
+%% Starts a separate node (trace control node) which
+%% starts tracing on target and all slave nodes
+%%
+%% TraceFile is a text file with elements of type
+%% {Trace,Mod,TracePattern}.
+%% {Trace,Mod,Func,TracePattern}.
+%% {Trace,Mod,Func,Arity,TracePattern}.
+%%
+%% Trace = tp | tpl; local or global call trace
+%% Mod,Func = atom(), Arity=integer(); defines what to trace
+%% TracePattern = [] | match_spec()
+%%
+%% The 'call' trace flag is set on all processes, and then
+%% the given trace patterns are set.
+
+handle_call({trace,TraceFile}, _From, State=#state{trc=false}) ->
+ TI = State#state.target_info,
+ case test_server_node:start_tracer_node(TraceFile, TI) of
+ {ok,Tracer} -> {reply,ok,State#state{trc=Tracer}};
+ Error -> {reply,Error,State}
+ end;
+handle_call({trace,_TraceFile}, _From, State) ->
+ {reply,{error,already_tracing},State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(stop_trace, _, State) -> ok | {error,Reason}
+%%
+%% Stops tracing on target and all slave nodes and
+%% terminates trace control node
+
+handle_call(stop_trace, _From, State=#state{trc=false}) ->
+ {reply,{error,not_tracing},State};
+handle_call(stop_trace, _From, State) ->
+ R = test_server_node:stop_tracer_node(State#state.trc),
+ {reply,R,State#state{trc=false}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({cover,CoverInfo}, _, State) -> ok | {error,Reason}
+%%
+%% Set specification of cover analysis to be used when running tests
+%% (see start_extra_tools/1 and stop_extra_tools/1)
+
+handle_call({cover,CoverInfo}, _From, State) ->
+ {reply,ok,State#state{cover=CoverInfo}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({create_priv_dir,Value}, _, State) -> ok | {error,Reason}
+%%
+%% Set create_priv_dir to either auto_per_run (create common priv dir once
+%% per test run), manual_per_tc (the priv dir name will be unique for each
+%% test case, but the user has to call test_server:make_priv_dir/0 to create
+%% it), or auto_per_tc (unique priv dir created automatically for each test
+%% case).
+
+handle_call({create_priv_dir,Value}, _From, State) ->
+ {reply,ok,State#state{create_priv_dir=Value}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({testcase_callback,{Mod,Func}}, _, State) -> ok | {error,Reason}
+%%
+%% Add a callback function that will be called before and after every
+%% test case (on the test case process):
+%%
+%% Mod:Func(Suite,TestCase,InitOrEnd,Config)
+%%
+%% InitOrEnd = init | 'end'.
+
+handle_call({testcase_callback,ModFunc}, _From, State) ->
+ case ModFunc of
+ {Mod,Func} ->
+ _ = case code:is_loaded(Mod) of
+ {file,_} ->
+ ok;
+ false ->
+ code:load_file(Mod)
+ end,
+ case erlang:function_exported(Mod,Func,4) of
+ true ->
+ ok;
+ false ->
+ io:format(user,
+ "WARNING! Callback function ~w:~tw/4 undefined.~n~n",
+ [Mod,Func])
+ end;
+ _ ->
+ ok
+ end,
+ {reply,ok,State#state{testcase_callback=ModFunc}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({set_random_seed,Seed}, _, State) -> ok | {error,Reason}
+%%
+%% Let operator set a random seed value to be used e.g. for shuffling
+%% test cases.
+
+handle_call({set_random_seed,Seed}, _From, State) ->
+ {reply,ok,State#state{random_seed=Seed}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(stop, _, State) -> ok
+%%
+%% Stops the test server immediately.
+%% Some cleanup is done by terminate/2
+
+handle_call(stop, _From, State) ->
+ {stop, shutdown, ok, State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call(get_target_info, _, State) -> TI
+%%
+%% TI = #target_info{}
+%%
+%% Returns information about target
+
+handle_call(get_target_info, _From, State) ->
+ {reply, State#state.target_info, State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({start_node,Name,Type,Options}, _, State) ->
+%% ok | {error,Reason}
+%%
+%% Starts a new node (slave or peer)
+
+handle_call({start_node, Name, Type, Options}, From, State) ->
+ %% test_server_ctrl does gen_server:reply/2 explicitly
+ test_server_node:start_node(Name, Type, Options, From,
+ State#state.target_info),
+ {noreply,State};
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({wait_for_node,Node}, _, State) -> ok
+%%
+%% Waits for a new node to take contact. Used if
+%% node is started with option {wait,false}
+
+handle_call({wait_for_node, Node}, From, State) ->
+ NewWaitList =
+ case ets:lookup(slave_tab,Node) of
+ [] ->
+ [{Node,From}|State#state.wait_for_node];
+ _ ->
+ gen_server:reply(From,ok),
+ State#state.wait_for_node
+ end,
+ {noreply,State#state{wait_for_node=NewWaitList}};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({stop_node,Name}, _, State) -> ok | {error,Reason}
+%%
+%% Stops a slave or peer node. This is actually only some cleanup
+%% - the node is really stopped by test_server when this returns.
+
+handle_call({stop_node, Name}, _From, State) ->
+ R = test_server_node:stop_node(Name),
+ {reply, R, State};
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_call({is_release_available,Name}, _, State) -> ok | {error,Reason}
+%%
+%% Tests if the release is available.
+
+handle_call({is_release_available, Release}, _From, State) ->
+ R = test_server_node:is_release_available(Release),
+ {reply, R, State}.
+
+%%--------------------------------------------------------------------
+set_hosts(Hosts) ->
+ put(test_server_hosts, Hosts).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_cast({node_started,Name}, _, State)
+%%
+%% Called by test_server_node when a slave/peer node is fully started.
+
+handle_cast({node_started,Node}, State) ->
+ case State#state.trc of
+ false -> ok;
+ Trc -> test_server_node:trace_nodes(Trc, [Node])
+ end,
+ NewWaitList =
+ case lists:keysearch(Node,1,State#state.wait_for_node) of
+ {value,{Node,From}} ->
+ gen_server:reply(From, ok),
+ lists:keydelete(Node, 1, State#state.wait_for_node);
+ false ->
+ State#state.wait_for_node
+ end,
+ {noreply, State#state{wait_for_node=NewWaitList}}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_info({'EXIT',Pid,Reason}, State)
+%% Pid = pid()
+%% Reason = term()
+%%
+%% Handles exit messages from linked processes. Only test suites are
+%% expected to be linked. When a test suite terminates, it is removed
+%% from the job queue.
+
+handle_info(report_idle, State) ->
+ Finish = State#state.finish,
+ lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end,
+ State#state.idle_notify),
+ {noreply,State#state{idle_notify=[]}};
+
+
+handle_info({'EXIT',Pid,Reason}, State) ->
+ case lists:keysearch(Pid,2,State#state.jobs) of
+ false ->
+ %% not our problem
+ {noreply,State};
+ {value,{Name,_}} ->
+ NewJobs = lists:keydelete(Pid, 2, State#state.jobs),
+ case Reason of
+ normal ->
+ fine;
+ killed ->
+ io:format("Suite ~ts was killed\n", [Name]);
+ _Other ->
+ io:format("Suite ~ts was killed with reason ~tp\n",
+ [Name,Reason])
+ end,
+ State2 = State#state{jobs=NewJobs},
+ Finish = State2#state.finish,
+ case NewJobs of
+ [] ->
+ lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end,
+ State2#state.idle_notify),
+ case Finish of
+ false ->
+ {noreply,State2#state{idle_notify=[]}};
+ _ -> % true | abort
+ %% test_server:finish() has been called and
+ %% there are no jobs in the job queue =>
+ %% stop the test_server_ctrl
+ {stop,shutdown,State2#state{finish=false}}
+ end;
+ _ -> % pending jobs
+ case Finish of
+ abort -> % abort test now!
+ lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end,
+ State2#state.idle_notify),
+ {stop,shutdown,State2#state{finish=false}};
+ _ -> % true | false
+ {noreply, State2}
+ end
+ end
+ end;
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_info({tcp_closed,Sock}, State)
+%%
+%% A Socket was closed. This indicates that a node died.
+%% This can be
+%% *Slave or peer node started by a test suite
+%% *Trace controll node
+
+handle_info({tcp_closed,Sock}, State=#state{trc=Sock}) ->
+ %% Tracer node died - can't really do anything
+ %%! Maybe print something???
+ {noreply,State#state{trc=false}};
+handle_info({tcp_closed,Sock}, State) ->
+ test_server_node:nodedown(Sock),
+ {noreply,State};
+handle_info(_, State) ->
+ %% dummy; accept all, do nothing.
+ {noreply, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% terminate(Reason, State) -> ok
+%% Reason = term()
+%%
+%% Cleans up when the test_server is terminating. Kills the running
+%% test suites (if any) and any possible remainting slave node
+
+terminate(_Reason, State) ->
+ test_server_sup:util_stop(),
+ case State#state.trc of
+ false -> ok;
+ Sock -> test_server_node:stop_tracer_node(Sock)
+ end,
+ ok = kill_all_jobs(State#state.jobs),
+ _ = test_server_node:kill_nodes(),
+ ok.
+
+kill_all_jobs([{_Name,JobPid}|Jobs]) ->
+ exit(JobPid, kill),
+ kill_all_jobs(Jobs);
+kill_all_jobs([]) ->
+ ok.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%----------------------- INTERNAL FUNCTIONS -----------------------%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% spawn_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs,
+%% CreatePrivDir, TestCaseCallback, ExtraTools) -> Pid
+%% Mod = atom()
+%% Func = atom()
+%% Args = [term(),...]
+%% Dir = string()
+%% Name = string()
+%% Levels = {integer(),integer(),integer()}
+%% RejectIoReqs = bool()
+%% CreatePrivDir = auto_per_run | manual_per_tc | auto_per_tc
+%% TestCaseCallback = {CBMod,CBFunc} | undefined
+%% ExtraTools = [ExtraTool,...]
+%% ExtraTool = CoverInfo | TraceInfo | RandomSeed
+%%
+%% Spawns a test suite execute-process, just an ordinary spawn, except
+%% that it will set a lot of dictionary information before starting the
+%% named function. Also, the execution is timed and protected by a catch.
+%% When the named function is done executing, a summary of the results
+%% is printed to the log files.
+
+spawn_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs,
+ CreatePrivDir, TCCallback, ExtraTools) ->
+ spawn_link(fun() ->
+ init_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs,
+ CreatePrivDir, TCCallback, ExtraTools)
+ end).
+
+init_tester(Mod, Func, Args, Dir, Name, {_,_,MinLev}=Levels,
+ RejectIoReqs, CreatePrivDir, TCCallback, ExtraTools) ->
+ process_flag(trap_exit, true),
+ _ = test_server_io:start_link(),
+ put(app, common_test),
+ put(test_server_name, Name),
+ put(test_server_dir, Dir),
+ put(test_server_total_time, 0),
+ put(test_server_ok, 0),
+ put(test_server_failed, 0),
+ put(test_server_skipped, {0,0}),
+ put(test_server_minor_level, MinLev),
+ put(test_server_create_priv_dir, CreatePrivDir),
+ put(test_server_random_seed, proplists:get_value(random_seed, ExtraTools)),
+ put(test_server_testcase_callback, TCCallback),
+ case os:getenv("TEST_SERVER_FRAMEWORK") of
+ FW when FW =:= false; FW =:= "undefined" ->
+ put(test_server_framework, '$none');
+ FW ->
+ put(test_server_framework_name, list_to_atom(FW)),
+ case os:getenv("TEST_SERVER_FRAMEWORK_NAME") of
+ FWName when FWName =:= false; FWName =:= "undefined" ->
+ put(test_server_framework_name, '$none');
+ FWName ->
+ put(test_server_framework_name, list_to_atom(FWName))
+ end
+ end,
+
+ %% before first print, read and set logging options
+ FWLogDir =
+ case test_server_sup:framework_call(get_log_dir, [], []) of
+ {ok,FwDir} -> FwDir;
+ _ -> filename:dirname(Dir)
+ end,
+ put(test_server_framework_logdir, FWLogDir),
+ LogOpts = test_server_sup:framework_call(get_logopts, [], []),
+ put(test_server_logopts, LogOpts),
+
+ StartedExtraTools = start_extra_tools(ExtraTools),
+
+ test_server_io:set_job_name(Name),
+ test_server_io:set_gl_props([{levels,Levels},
+ {auto_nl,not lists:member(no_nl, LogOpts)},
+ {reject_io_reqs,RejectIoReqs}]),
+ group_leader(test_server_io:get_gl(true), self()),
+ {TimeMy,Result} = ts_tc(Mod, Func, Args),
+ set_io_buffering(undefined),
+ test_server_io:set_job_name(undefined),
+ catch stop_extra_tools(StartedExtraTools),
+ case Result of
+ {'EXIT',test_suites_done} ->
+ ok;
+ {'EXIT',_Pid,Reason} ->
+ print(1, "EXIT, reason ~tp", [Reason]);
+ {'EXIT',Reason} ->
+ report_severe_error(Reason),
+ print(1, "EXIT, reason ~tp", [Reason])
+ end,
+ Time = TimeMy/1000000,
+ SuccessStr =
+ case get(test_server_failed) of
+ 0 -> "Ok";
+ _ -> "FAILED"
+ end,
+ {SkippedN,SkipStr} =
+ case get(test_server_skipped) of
+ {0,0} ->
+ {0,""};
+ {USkipped,ASkipped} ->
+ Skipped = USkipped+ASkipped,
+ {Skipped,io_lib:format(", ~w Skipped", [Skipped])}
+ end,
+ OkN = get(test_server_ok),
+ FailedN = get(test_server_failed),
+ print(html,"\n</tbody>\n<tfoot>\n"
+ "<tr><td></td><td><b>TOTAL</b></td><td></td><td></td><td></td>"
+ "<td>~.3fs</td><td><b>~ts</b></td><td>~w Ok, ~w Failed~ts of ~w</td></tr>\n"
+ "</tfoot>\n",
+ [Time,SuccessStr,OkN,FailedN,SkipStr,OkN+FailedN+SkippedN]),
+
+ test_server_io:stop([major,html,unexpected_io]),
+ {UnexpectedIoName,UnexpectedIoFooter} = get(test_server_unexpected_footer),
+ {ok,UnexpectedIoFd} = open_html_file(UnexpectedIoName, [append]),
+ io:put_chars(UnexpectedIoFd, "\n</pre>\n"++UnexpectedIoFooter),
+ ok = file:close(UnexpectedIoFd).
+
+report_severe_error(Reason) ->
+ test_server_sup:framework_call(report, [severe_error,Reason]).
+
+ts_tc(M,F,A) ->
+ Before = erlang:monotonic_time(),
+ Result = (catch apply(M, F, A)),
+ After = erlang:monotonic_time(),
+ Elapsed = erlang:convert_time_unit(After-Before,
+ native,
+ micro_seconds),
+ {Elapsed, Result}.
+
+start_extra_tools(ExtraTools) ->
+ start_extra_tools(ExtraTools, []).
+start_extra_tools([{cover,CoverInfo} | ExtraTools], Started) ->
+ case start_cover(CoverInfo) of
+ {ok,NewCoverInfo} ->
+ start_extra_tools(ExtraTools,[{cover,NewCoverInfo}|Started]);
+ {error,_} ->
+ start_extra_tools(ExtraTools, Started)
+ end;
+start_extra_tools([_ | ExtraTools], Started) ->
+ start_extra_tools(ExtraTools, Started);
+start_extra_tools([], Started) ->
+ Started.
+
+stop_extra_tools(ExtraTools) ->
+ TestDir = get(test_server_log_dir_base),
+ case lists:keymember(cover, 1, ExtraTools) of
+ false ->
+ write_default_coverlog(TestDir);
+ true ->
+ ok
+ end,
+ stop_extra_tools(ExtraTools, TestDir).
+
+stop_extra_tools([{cover,CoverInfo}|ExtraTools], TestDir) ->
+ stop_cover(CoverInfo,TestDir),
+ stop_extra_tools(ExtraTools, TestDir);
+%%stop_extra_tools([_ | ExtraTools], TestDir) ->
+%% stop_extra_tools(ExtraTools, TestDir);
+stop_extra_tools([], _) ->
+ ok.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% do_spec(SpecName, TimetrapSpec) -> {error,Reason} | exit(Result)
+%% SpecName = string()
+%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap}
+%% MultiplyTimetrap = integer() | infinity
+%% ScaleTimetrap = bool()
+%%
+%% Reads the named test suite specification file, and executes it.
+%%
+%% This function is meant to be called by a process created by
+%% spawn_tester/10, which sets up some necessary dictionary values.
+
+do_spec(SpecName, TimetrapSpec) when is_list(SpecName) ->
+ case file:consult(SpecName) of
+ {ok,TermList} ->
+ do_spec_list(TermList,TimetrapSpec);
+ {error,Reason} ->
+ io:format("Can't open ~ts: ~tp\n", [SpecName,Reason]),
+ {error,{cant_open_spec,Reason}}
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% do_spec_list(TermList, TimetrapSpec) -> exit(Result)
+%% TermList = [term()|...]
+%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap}
+%% MultiplyTimetrap = integer() | infinity
+%% ScaleTimetrap = bool()
+%%
+%% Executes a list of test suite specification commands. The following
+%% commands are available, and may occur zero or more times (if several,
+%% the contents is appended):
+%%
+%% {topcase,TopCase} Specifies top level test goals. TopCase has the syntax
+%% specified by collect_cases/3.
+%%
+%% {skip,Skip} Specifies test cases to skip, and lists requirements that
+%% cannot be granted during the test run. Skip has the syntax specified
+%% by collect_cases/3.
+%%
+%% {nodes,Nodes} Lists node names avaliable to the test suites. Nodes have
+%% the syntax specified by collect_cases/3.
+%%
+%% {require_nodenames, Num} Specifies how many nodenames the test suite will
+%% need. Theese are automaticly generated and inserted into the Config by the
+%% test_server. The caller may specify other hosts to run theese nodes by
+%% using the {hosts, Hosts} option. If there are no hosts specified, all
+%% nodenames will be generated from the local host.
+%%
+%% {hosts, Hosts} Specifies a list of available hosts on which to start
+%% slave nodes. It is used when the {remote, true} option is given to the
+%% test_server:start_node/3 function. Also, if {require_nodenames, Num} is
+%% contained in the TermList, the generated nodenames will be spread over
+%% all hosts given in this Hosts list. The hostnames are given as atoms or
+%% strings.
+%%
+%% {diskless, true}</c></tag> is kept for backwards compatiblilty and
+%% should not be used. Use a configuration test case instead.
+%%
+%% This function is meant to be called by a process created by
+%% spawn_tester/10, which sets up some necessary dictionary values.
+
+do_spec_list(TermList0, TimetrapSpec) ->
+ Nodes = [],
+ TermList =
+ case lists:keysearch(hosts, 1, TermList0) of
+ {value, {hosts, Hosts0}} ->
+ Hosts = lists:map(fun(H) -> cast_to_list(H) end, Hosts0),
+ controller_call({set_hosts, Hosts}),
+ lists:keydelete(hosts, 1, TermList0);
+ _ ->
+ TermList0
+ end,
+ DefaultConfig = make_config([{nodes,Nodes}]),
+ {TopCases,SkipList,Config} = do_spec_terms(TermList, [], [], DefaultConfig),
+ do_test_cases(TopCases, SkipList, Config, TimetrapSpec).
+
+do_spec_terms([], TopCases, SkipList, Config) ->
+ {TopCases,SkipList,Config};
+do_spec_terms([{topcase,TopCase}|Terms], TopCases, SkipList, Config) ->
+ do_spec_terms(Terms,[TopCase|TopCases], SkipList, Config);
+do_spec_terms([{skip,Skip}|Terms], TopCases, SkipList, Config) ->
+ do_spec_terms(Terms, TopCases, [Skip|SkipList], Config);
+do_spec_terms([{nodes,Nodes}|Terms], TopCases, SkipList, Config) ->
+ do_spec_terms(Terms, TopCases, SkipList,
+ update_config(Config, {nodes,Nodes}));
+do_spec_terms([{diskless,How}|Terms], TopCases, SkipList, Config) ->
+ do_spec_terms(Terms, TopCases, SkipList,
+ update_config(Config, {diskless,How}));
+do_spec_terms([{config,MoreConfig}|Terms], TopCases, SkipList, Config) ->
+ do_spec_terms(Terms, TopCases, SkipList, Config++MoreConfig);
+do_spec_terms([{default_timeout,Tmo}|Terms], TopCases, SkipList, Config) ->
+ do_spec_terms(Terms, TopCases, SkipList,
+ update_config(Config, {default_timeout,Tmo}));
+
+do_spec_terms([{require_nodenames,NumNames}|Terms], TopCases, SkipList, Config) ->
+ NodeNames0=generate_nodenames(NumNames),
+ NodeNames=lists:delete([], NodeNames0),
+ do_spec_terms(Terms, TopCases, SkipList,
+ update_config(Config, {nodenames,NodeNames}));
+do_spec_terms([Other|Terms], TopCases, SkipList, Config) ->
+ io:format("** WARNING: Spec file contains unknown directive ~tp\n",
+ [Other]),
+ do_spec_terms(Terms, TopCases, SkipList, Config).
+
+
+
+generate_nodenames(Num) ->
+ Hosts = case controller_call(get_hosts) of
+ [] ->
+ TI = controller_call(get_target_info),
+ [TI#target_info.host];
+ List ->
+ List
+ end,
+ generate_nodenames2(Num, Hosts, []).
+
+generate_nodenames2(0, _Hosts, Acc) ->
+ Acc;
+generate_nodenames2(N, Hosts, Acc) ->
+ Host=lists:nth((N rem (length(Hosts)))+1, Hosts),
+ Name=list_to_atom(temp_nodename("nod", []) ++ "@" ++ Host),
+ generate_nodenames2(N-1, Hosts, [Name|Acc]).
+
+temp_nodename([], Acc) ->
+ lists:flatten(Acc);
+temp_nodename([Chr|Base], Acc) ->
+ {A,B,C} = ?now,
+ New = [Chr | integer_to_list(Chr bxor A bxor B+A bxor C+B)],
+ temp_nodename(Base, [New|Acc]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% count_test_cases(TopCases, SkipCases) -> {Suites,NoOfCases} | error
+%% TopCases = term() (See collect_cases/3)
+%% SkipCases = term() (See collect_cases/3)
+%% Suites = list()
+%% NoOfCases = integer() | unknown
+%%
+%% Counts the test cases that are about to run and returns that number.
+%% If there's a conf group in TestSpec with a repeat property, the total number
+%% of cases can not be calculated and NoOfCases = unknown.
+count_test_cases(TopCases, SkipCases) when is_list(TopCases) ->
+ case collect_all_cases(TopCases, SkipCases) of
+ {error,_Why} = Error ->
+ Error;
+ TestSpec ->
+ {get_suites(TestSpec, []),
+ case remove_conf(TestSpec) of
+ {repeats,_} ->
+ unknown;
+ TestSpec1 ->
+ length(TestSpec1)
+ end}
+ end;
+
+count_test_cases(TopCase, SkipCases) ->
+ count_test_cases([TopCase], SkipCases).
+
+
+remove_conf(Cases) ->
+ remove_conf(Cases, [], false).
+
+remove_conf([{conf, _Ref, Props, _MF}|Cases], NoConf, Repeats) ->
+ case get_repeat(Props) of
+ undefined ->
+ remove_conf(Cases, NoConf, Repeats);
+ {_RepType,1} ->
+ remove_conf(Cases, NoConf, Repeats);
+ _ ->
+ remove_conf(Cases, NoConf, true)
+ end;
+remove_conf([{make,_Ref,_MF}|Cases], NoConf, Repeats) ->
+ remove_conf(Cases, NoConf, Repeats);
+remove_conf([{skip_case,{{_M,all},_Cmt},_Mode}|Cases], NoConf, Repeats) ->
+ remove_conf(Cases, NoConf, Repeats);
+remove_conf([{skip_case,{Type,_Ref,_MF,_Cmt}}|Cases],
+ NoConf, Repeats) when Type==conf;
+ Type==make ->
+ remove_conf(Cases, NoConf, Repeats);
+remove_conf([{skip_case,{Type,_Ref,_MF,_Cmt},_Mode}|Cases],
+ NoConf, Repeats) when Type==conf;
+ Type==make ->
+ remove_conf(Cases, NoConf, Repeats);
+remove_conf([C={Mod,error_in_suite,_}|Cases], NoConf, Repeats) ->
+ FwMod = get_fw_mod(?MODULE),
+ if Mod == FwMod ->
+ remove_conf(Cases, NoConf, Repeats);
+ true ->
+ remove_conf(Cases, [C|NoConf], Repeats)
+ end;
+remove_conf([C|Cases], NoConf, Repeats) ->
+ remove_conf(Cases, [C|NoConf], Repeats);
+remove_conf([], NoConf, true) ->
+ {repeats,lists:reverse(NoConf)};
+remove_conf([], NoConf, false) ->
+ lists:reverse(NoConf).
+
+get_suites([{skip_case,{{Mod,_F},_Cmt},_Mode}|Tests], Mods) when is_atom(Mod) ->
+ case add_mod(Mod, Mods) of
+ true -> get_suites(Tests, [Mod|Mods]);
+ false -> get_suites(Tests, Mods)
+ end;
+get_suites([{Mod,_Case}|Tests], Mods) when is_atom(Mod) ->
+ case add_mod(Mod, Mods) of
+ true -> get_suites(Tests, [Mod|Mods]);
+ false -> get_suites(Tests, Mods)
+ end;
+get_suites([{Mod,_Func,_Args}|Tests], Mods) when is_atom(Mod) ->
+ case add_mod(Mod, Mods) of
+ true -> get_suites(Tests, [Mod|Mods]);
+ false -> get_suites(Tests, Mods)
+ end;
+get_suites([_|Tests], Mods) ->
+ get_suites(Tests, Mods);
+
+get_suites([], Mods) ->
+ lists:reverse(Mods).
+
+add_mod(Mod, Mods) ->
+ case lists:reverse(atom_to_list(Mod)) of
+ "ETIUS_" ++ _ -> % test suite
+ case lists:member(Mod, Mods) of
+ true -> false;
+ false -> true
+ end;
+ _ ->
+ false
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% do_test_cases(TopCases, SkipCases, Config, TimetrapSpec) ->
+%% exit(Result)
+%%
+%% TopCases = term() (See collect_cases/3)
+%% SkipCases = term() (See collect_cases/3)
+%% Config = term() (See collect_cases/3)
+%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap}
+%% MultiplyTimetrap = integer() | infinity
+%% ScaleTimetrap = bool()
+%%
+%% Initializes and starts the test run, for "ordinary" test suites.
+%% Creates log directories and log files, inserts initial timestamps and
+%% configuration information into the log files.
+%%
+%% This function is meant to be called by a process created by
+%% spawn_tester/10, which sets up some necessary dictionary values.
+do_test_cases(TopCases, SkipCases,
+ Config, MultiplyTimetrap) when is_integer(MultiplyTimetrap);
+ MultiplyTimetrap == infinity ->
+ do_test_cases(TopCases, SkipCases, Config, {MultiplyTimetrap,true});
+
+do_test_cases(TopCases, SkipCases,
+ Config, TimetrapData) when is_list(TopCases),
+ is_tuple(TimetrapData) ->
+ {ok,TestDir} = start_log_file(),
+ FwMod = get_fw_mod(?MODULE),
+ case collect_all_cases(TopCases, SkipCases) of
+ {error,Why} ->
+ print(1, "Error starting: ~tp", [Why]),
+ exit(test_suites_done);
+ TestSpec0 ->
+ N = case remove_conf(TestSpec0) of
+ {repeats,_} -> unknown;
+ TS -> length(TS)
+ end,
+ put(test_server_cases, N),
+ put(test_server_case_num, 0),
+
+ TestSpec =
+ add_init_and_end_per_suite(TestSpec0, undefined, undefined, FwMod),
+
+ TI = get_target_info(),
+ print(1, "Starting test~ts",
+ [print_if_known(N, {", ~w test cases",[N]},
+ {" (with repeated test cases)",[]})]),
+ Test = get(test_server_name),
+ TestName = if is_list(Test) ->
+ lists:flatten(io_lib:format("~ts", [Test]));
+ true ->
+ lists:flatten(io_lib:format("~tp", [Test]))
+ end,
+ TestDescr = "Test " ++ TestName ++ " results",
+
+ test_server_sup:framework_call(report, [tests_start,{Test,N}]),
+
+ {Header,Footer} =
+ case test_server_sup:framework_call(get_html_wrapper,
+ [TestDescr,true,TestDir,
+ {[],[2,3,4,7,8],[1,6]}], "") of
+ Empty when (Empty == "") ; (element(2,Empty) == "") ->
+ put(basic_html, true),
+ {[html_header(TestDescr),
+ "<h2>Results for test ", TestName, "</h2>\n"],
+ "\n</body>\n</html>\n"};
+ {basic_html,Html0,Html1} ->
+ put(basic_html, true),
+ {Html0++["<h1>Results for <i>",TestName,"</i></h1>\n"],
+ Html1};
+ {xhtml,Html0,Html1} ->
+ put(basic_html, false),
+ {Html0++["<h1>Results for <i>",TestName,"</i></h1>\n"],
+ Html1}
+ end,
+
+ print(html, Header),
+
+ print(html, xhtml("<p>", "<h4>")),
+ print_timestamp(html, "Test started at "),
+ print(html, xhtml("</p>", "</h4>")),
+
+ print(html, xhtml("\n<p><b>Host info:</b><br>\n",
+ "\n<p><b>Host info:</b><br />\n")),
+ print_who(test_server_sup:hoststr(), test_server_sup:get_username()),
+ print(html, xhtml("<br>Used Erlang v~ts in <tt>~ts</tt></p>\n",
+ "<br />Used Erlang v~ts in \"~ts\"</p>\n"),
+ [erlang:system_info(version), code:root_dir()]),
+
+ if FwMod == ?MODULE ->
+ print(html, xhtml("\n<p><b>Target Info:</b><br>\n",
+ "\n<p><b>Target Info:</b><br />\n")),
+ print_who(TI#target_info.host, TI#target_info.username),
+ print(html,xhtml("<br>Used Erlang v~ts in <tt>~ts</tt></p>\n",
+ "<br />Used Erlang v~ts in \"~ts\"</p>\n"),
+ [TI#target_info.version, TI#target_info.root_dir]);
+ true ->
+ case test_server_sup:framework_call(target_info, []) of
+ TargetInfo when is_list(TargetInfo),
+ length(TargetInfo) > 0 ->
+ print(html, xhtml("\n<p><b>Target info:</b><br>\n",
+ "\n<p><b>Target info:</b><br />\n")),
+ print(html, "~ts</p>\n", [TargetInfo]);
+ _ ->
+ ok
+ end
+ end,
+ CoverLog =
+ case get(test_server_cover_log_dir) of
+ undefined ->
+ ?coverlog_name;
+ AbsLogDir ->
+ AbsLog = filename:join(AbsLogDir,?coverlog_name),
+ make_relative(AbsLog, TestDir)
+ end,
+ print(html,
+ "<p><ul>\n"
+ "<li><a href=\"~ts\">Full textual log</a></li>\n"
+ "<li><a href=\"~ts\">Coverage log</a></li>\n"
+ "<li><a href=\"~ts\">Unexpected I/O log</a></li>\n</ul></p>\n",
+ [?suitelog_name,CoverLog,?unexpected_io_log]),
+ print(html,
+ "<p>~ts</p>\n" ++
+ xhtml(["<table bgcolor=\"white\" border=\"3\" cellpadding=\"5\">\n",
+ "<thead>\n"],
+ ["<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n"]) ++
+ "<tr><th>Num</th><th>Module</th><th>Group</th>" ++
+ "<th>Case</th><th>Log</th><th>Time</th><th>Result</th>" ++
+ "<th>Comment</th></tr>\n</thead>\n<tbody>\n",
+ [print_if_known(N, {"<i>Executing <b>~w</b> test cases...</i>"
+ ++ xhtml("\n<br>\n", "\n<br />\n"),[N]},
+ {"",[]})]),
+
+ print(major, "=cases ~w", [get(test_server_cases)]),
+ print(major, "=user ~ts", [TI#target_info.username]),
+ print(major, "=host ~ts", [TI#target_info.host]),
+
+ %% If there are no hosts specified,use only the local host
+ case controller_call(get_hosts) of
+ [] ->
+ print(major, "=hosts ~ts", [TI#target_info.host]),
+ controller_call({set_hosts, [TI#target_info.host]});
+ Hosts ->
+ Str = lists:flatten(lists:map(fun(X) -> [X," "] end, Hosts)),
+ print(major, "=hosts ~ts", [Str])
+ end,
+ print(major, "=emulator_vsn ~ts", [TI#target_info.version]),
+ print(major, "=emulator ~ts", [TI#target_info.emulator]),
+ print(major, "=otp_release ~ts", [TI#target_info.otp_release]),
+ print(major, "=started ~s",
+ [lists:flatten(timestamp_get(""))]),
+
+ test_server_io:set_footer(Footer),
+
+ run_test_cases(TestSpec, Config, TimetrapData)
+ end;
+
+do_test_cases(TopCase, SkipCases, Config, TimetrapSpec) ->
+ %% when not list(TopCase)
+ do_test_cases([TopCase], SkipCases, Config, TimetrapSpec).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% start_log_file() -> {ok,TestDirName} | exit({Error,Reason})
+%% Stem = string()
+%%
+%% Creates the log directories, the major log file and the html log file.
+%% The log files are initialized with some header information.
+%%
+%% The name of the log directory will be <Name>.logs/run.<Date>/ where
+%% Name is the test suite name and Date is the current date and time.
+
+start_log_file() ->
+ Dir = get(test_server_dir),
+ case file:make_dir(Dir) of
+ ok ->
+ ok;
+ {error, eexist} ->
+ ok;
+ MkDirError ->
+ log_file_error(MkDirError, Dir)
+ end,
+ TestDir = timestamp_filename_get(filename:join(Dir, "run.")),
+ TestDir1 =
+ case file:make_dir(TestDir) of
+ ok ->
+ TestDir;
+ {error,eexist} ->
+ timer:sleep(1000),
+ %% we need min 1 second between timestamps unfortunately
+ TestDirX = timestamp_filename_get(filename:join(Dir, "run.")),
+ case file:make_dir(TestDirX) of
+ ok ->
+ TestDirX;
+ MkDirError2 ->
+ log_file_error(MkDirError2, TestDirX)
+ end;
+ MkDirError2 ->
+ log_file_error(MkDirError2, TestDir)
+ end,
+ FilenameMode = file:native_name_encoding(),
+ ok = write_file(filename:join(Dir, ?last_file),
+ TestDir1 ++ "\n",
+ FilenameMode),
+ ok = write_file(?last_file, TestDir1 ++ "\n", FilenameMode),
+ put(test_server_log_dir_base,TestDir1),
+
+ MajorName = filename:join(TestDir1, ?suitelog_name),
+ HtmlName = MajorName ++ ?html_ext,
+ UnexpectedName = filename:join(TestDir1, ?unexpected_io_log),
+
+ {ok,Major} = open_utf8_file(MajorName),
+ {ok,Html} = open_html_file(HtmlName),
+
+ {UnexpHeader,UnexpFooter} =
+ case test_server_sup:framework_call(get_html_wrapper,
+ ["Unexpected I/O log",false,
+ TestDir, undefined],"") of
+ UEmpty when (UEmpty == "") ; (element(2,UEmpty) == "") ->
+ {html_header("Unexpected I/O log"),"\n</body>\n</html>\n"};
+ {basic_html,UH,UF} ->
+ {UH,UF};
+ {xhtml,UH,UF} ->
+ {UH,UF}
+ end,
+
+ {ok,Unexpected} = open_html_file(UnexpectedName),
+ io:put_chars(Unexpected, [UnexpHeader,
+ xhtml("<br>\n<h2>Unexpected I/O</h2>",
+ "<br />\n<h3>Unexpected I/O</h3>"),
+ "\n<pre>\n"]),
+ put(test_server_unexpected_footer,{UnexpectedName,UnexpFooter}),
+
+ test_server_io:set_fd(major, Major),
+ test_server_io:set_fd(html, Html),
+ test_server_io:set_fd(unexpected_io, Unexpected),
+
+ %% we must assume the redirection file (to the latest suite index) can
+ %% be stored on the level above the log directory of the current test
+ TopDir = filename:dirname(get(test_server_framework_logdir)),
+ RedirectLink = filename:join(TopDir, ?suitelog_latest_name ++ ?html_ext),
+ make_html_link(RedirectLink, HtmlName, redirect),
+
+ make_html_link(filename:absname(?last_test ++ ?html_ext),
+ HtmlName, filename:basename(Dir)),
+ LinkName = filename:join(Dir, ?last_link),
+ make_html_link(LinkName ++ ?html_ext, HtmlName,
+ filename:basename(Dir)),
+
+ PrivDir = filename:join(TestDir1, ?priv_dir),
+ ok = file:make_dir(PrivDir),
+ put(test_server_priv_dir,PrivDir++"/"),
+ print_timestamp(major, "Suite started at "),
+
+ LogInfo = [{topdir,Dir},{rundir,lists:flatten(TestDir1)}],
+ test_server_sup:framework_call(report, [loginfo,LogInfo]),
+ {ok,TestDir1}.
+
+log_file_error(Error, Dir) ->
+ exit({cannot_create_log_dir,{Error,lists:flatten(Dir)}}).
+
+make_html_link(LinkName, Target, Explanation) ->
+ %% if possible use a relative reference to Target.
+ TargetL = filename:split(Target),
+ PwdL = filename:split(filename:dirname(LinkName)),
+ Href = case lists:prefix(PwdL, TargetL) of
+ true ->
+ uri_encode(filename:join(lists:nthtail(length(PwdL),TargetL)));
+ false ->
+ "file:" ++ uri_encode(Target)
+ end,
+ H = if Explanation == redirect ->
+ Meta = ["<meta http-equiv=\"refresh\" "
+ "content=\"0; url=", Href, "\" />\n"],
+ [html_header("redirect", Meta), "</html>\n"];
+ true ->
+ [html_header(Explanation),
+ "<h1>Last test</h1>\n"
+ "<a href=\"",Href,"\">",Explanation,"</a>\n"
+ "</body>\n</html>\n"]
+ end,
+ ok = write_html_file(LinkName, H).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% start_minor_log_file(Mod, Func, ParallelTC) -> AbsName
+%% Mod = atom()
+%% Func = atom()
+%% ParallelTC = bool()
+%% AbsName = string()
+%%
+%% Create a minor log file for the test case Mod,Func,Args. The log file
+%% will be stored in the log directory under the name <Mod>.<Func>.html.
+%% Some header info will also be inserted into the log file. If the test
+%% case runs in a parallel group, then to avoid clashing file names if the
+%% case is executed more than once, the name <Mod>.<Func>.<Timestamp>.html
+%% is used.
+
+start_minor_log_file(Mod, Func, ParallelTC) ->
+ MFA = {Mod,Func,1},
+ LogDir = get(test_server_log_dir_base),
+ Name = minor_log_file_name(Mod,Func),
+ AbsName = filename:join(LogDir, Name),
+ case (ParallelTC orelse (element(1,file:read_file_info(AbsName))==ok)) of
+ false -> %% normal case, unique name
+ start_minor_log_file1(Mod, Func, LogDir, AbsName, MFA);
+ true -> %% special case, duplicate names
+ Tag = test_server_sup:unique_name(),
+ Name1 = minor_log_file_name(Mod,Func,[$.|Tag]),
+ AbsName1 = filename:join(LogDir, Name1),
+ start_minor_log_file1(Mod, Func, LogDir, AbsName1, MFA)
+ end.
+
+start_minor_log_file1(Mod, Func, LogDir, AbsName, MFA) ->
+ {ok,Fd} = open_html_file(AbsName),
+ Lev = get(test_server_minor_level)+1000, %% far down in the minor levels
+ put(test_server_minor_fd, Fd),
+ test_server_gl:set_minor_fd(group_leader(), Fd, MFA),
+
+ TestDescr = io_lib:format("Test ~w:~tw result", [Mod,Func]),
+ {Header,Footer} =
+ case test_server_sup:framework_call(get_html_wrapper,
+ [TestDescr,false,
+ filename:dirname(AbsName),
+ undefined], "") of
+ Empty when (Empty == "") ; (element(2,Empty) == "") ->
+ put(basic_html, true),
+ {html_header(TestDescr), "\n</body>\n</html>\n"};
+ {basic_html,Html0,Html1} ->
+ put(basic_html, true),
+ {Html0,Html1};
+ {xhtml,Html0,Html1} ->
+ put(basic_html, false),
+ {Html0,Html1}
+ end,
+ put(test_server_minor_footer, Footer),
+ io:put_chars(Fd, Header),
+
+ io:put_chars(Fd, "<a name=\"top\"></a>"),
+ io:put_chars(Fd, "<pre>\n"),
+
+ SrcListing = downcase(atom_to_list(Mod)) ++ ?src_listing_ext,
+
+ case get_fw_mod(?MODULE) of
+ Mod when Func == error_in_suite ->
+ ok;
+ _ ->
+ {Info,Arity} =
+ if Func == init_per_suite; Func == end_per_suite ->
+ {"Config function: ", 1};
+ Func == init_per_group; Func == end_per_group ->
+ {"Config function: ", 2};
+ true ->
+ {"Test case: ", 1}
+ end,
+
+ case {filelib:is_file(filename:join(LogDir, SrcListing)),
+ lists:member(no_src, get(test_server_logopts))} of
+ {true,false} ->
+ print(Lev, ["$tc_html",
+ Info ++ "<a href=\"~ts#~ts\">~w:~tw/~w</a> "
+ "(click for source code)\n"],
+ [uri_encode(SrcListing),
+ uri_encode(atom_to_list(Func)++"-1",utf8),
+ Mod,Func,Arity]);
+ _ ->
+ print(Lev, ["$tc_html",Info ++ "~w:~tw/~w\n"], [Mod,Func,Arity])
+ end
+ end,
+
+ AbsName.
+
+stop_minor_log_file() ->
+ test_server_gl:unset_minor_fd(group_leader()),
+ Fd = get(test_server_minor_fd),
+ Footer = get(test_server_minor_footer),
+ io:put_chars(Fd, "</pre>\n" ++ Footer),
+ ok = file:close(Fd),
+ put(test_server_minor_fd, undefined).
+
+minor_log_file_name(Mod,Func) ->
+ minor_log_file_name(Mod,Func,"").
+minor_log_file_name(Mod,Func,Tag) ->
+ Name =
+ downcase(
+ lists:flatten(
+ io_lib:format("~w.~tw~s~s", [Mod,Func,Tag,?html_ext]))),
+ Ok = file:native_name_encoding()==utf8
+ orelse io_lib:printable_latin1_list(Name),
+ if Ok -> Name;
+ true -> exit({error,unicode_name_on_latin1_file_system})
+ end.
+
+downcase(S) -> downcase(S, []).
+downcase([Uc|Rest], Result) when $A =< Uc, Uc =< $Z ->
+ downcase(Rest, [Uc-$A+$a|Result]);
+downcase([C|Rest], Result) ->
+ downcase(Rest, [C|Result]);
+downcase([], Result) ->
+ lists:reverse(Result).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% html_convert_modules(TestSpec, Config) -> ok
+%% Isolate the modules affected by TestSpec and
+%% make sure they are converted to html.
+%%
+%% Errors are silently ignored.
+
+html_convert_modules(TestSpec, _Config, FwMod) ->
+ Mods = html_isolate_modules(TestSpec, FwMod),
+ html_convert_modules(Mods),
+ copy_html_files(get(test_server_dir), get(test_server_log_dir_base)).
+
+%% Retrieve a list of modules out of the test spec.
+html_isolate_modules(List, FwMod) ->
+ html_isolate_modules(List, sets:new(), FwMod).
+
+html_isolate_modules([], Set, _) -> sets:to_list(Set);
+html_isolate_modules([{skip_case,{_Case,_Cmt},_Mode}|Cases], Set, FwMod) ->
+ html_isolate_modules(Cases, Set, FwMod);
+html_isolate_modules([{conf,_Ref,Props,{FwMod,_Func}}|Cases], Set, FwMod) ->
+ Set1 = case proplists:get_value(suite, Props) of
+ undefined -> Set;
+ Mod -> sets:add_element(Mod, Set)
+ end,
+ html_isolate_modules(Cases, Set1, FwMod);
+html_isolate_modules([{conf,_Ref,_Props,{Mod,_Func}}|Cases], Set, FwMod) ->
+ html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod);
+html_isolate_modules([{skip_case,{conf,_Ref,{FwMod,_Func},_Cmt},Mode}|Cases],
+ Set, FwMod) ->
+ Set1 = case proplists:get_value(suite, get_props(Mode)) of
+ undefined -> Set;
+ Mod -> sets:add_element(Mod, Set)
+ end,
+ html_isolate_modules(Cases, Set1, FwMod);
+html_isolate_modules([{skip_case,{conf,_Ref,{Mod,_Func},_Cmt},_Props}|Cases],
+ Set, FwMod) ->
+ html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod);
+html_isolate_modules([{Mod,_Case}|Cases], Set, FwMod) ->
+ html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod);
+html_isolate_modules([{Mod,_Case,_Args}|Cases], Set, FwMod) ->
+ html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod).
+
+%% Given a list of modules, convert each module's source code to HTML.
+html_convert_modules([Mod|Mods]) ->
+ case code:which(Mod) of
+ Path when is_list(Path) ->
+ SrcFile = filename:rootname(Path) ++ ".erl",
+ FoundSrcFile =
+ case file:read_file_info(SrcFile) of
+ {ok,SInfo} ->
+ {SrcFile,SInfo};
+ {error,_} ->
+ ModInfo = Mod:module_info(compile),
+ case proplists:get_value(source, ModInfo) of
+ undefined ->
+ undefined;
+ OtherSrcFile ->
+ case file:read_file_info(OtherSrcFile) of
+ {ok,SInfo} ->
+ {OtherSrcFile,SInfo};
+ {error,_} ->
+ undefined
+ end
+ end
+ end,
+ case FoundSrcFile of
+ undefined ->
+ html_convert_modules(Mods);
+ {SrcFile1,SrcFileInfo} ->
+ DestDir = get(test_server_dir),
+ Name = atom_to_list(Mod),
+ DestFile = filename:join(DestDir,
+ downcase(Name)++?src_listing_ext),
+ _ = html_possibly_convert(SrcFile1, SrcFileInfo, DestFile),
+ html_convert_modules(Mods)
+ end;
+ _Other ->
+ html_convert_modules(Mods)
+ end;
+html_convert_modules([]) -> ok.
+
+%% Convert source code to HTML if possible and needed.
+html_possibly_convert(Src, SrcInfo, Dest) ->
+ case file:read_file_info(Dest) of
+ {ok,DestInfo} when DestInfo#file_info.mtime >= SrcInfo#file_info.mtime ->
+ ok; % dest file up to date
+ _ ->
+ InclPath = case application:get_env(test_server, include) of
+ {ok,Incls} -> Incls;
+ _ -> []
+ end,
+
+ OutDir = get(test_server_log_dir_base),
+ case test_server_sup:framework_call(get_html_wrapper,
+ ["Module "++Src,false,
+ OutDir,undefined,
+ encoding(Src)], "") of
+ Empty when (Empty == "") ; (element(2,Empty) == "") ->
+ erl2html2:convert(Src, Dest, InclPath);
+ {_,Header,_} ->
+ erl2html2:convert(Src, Dest, InclPath, Header)
+ end
+ end.
+
+%% Copy all HTML files in InDir to OutDir.
+copy_html_files(InDir, OutDir) ->
+ Files = filelib:wildcard(filename:join(InDir, "*" ++ ?src_listing_ext)),
+ lists:foreach(fun (Src) -> copy_html_file(Src, OutDir) end, Files).
+
+copy_html_file(Src, DestDir) ->
+ Dest = filename:join(DestDir, filename:basename(Src)),
+ case file:read_file(Src) of
+ {ok,Bin} ->
+ ok = write_binary_file(Dest, Bin);
+ {error,_Reason} ->
+ io:format("File ~ts: read failed\n", [Src])
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% add_init_and_end_per_suite(TestSpec, Mod, Ref, FwMod) -> NewTestSpec
+%%
+%% Expands TestSpec with an initial init_per_suite, and a final
+%% end_per_suite element, per each discovered suite in the list.
+
+add_init_and_end_per_suite([{make,_,_}=Case|Cases], LastMod, LastRef, FwMod) ->
+ [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)];
+add_init_and_end_per_suite([{skip_case,{{Mod,all},_},_}=Case|Cases], LastMod,
+ LastRef, FwMod) when Mod =/= LastMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_end_per_suite_and_skip(LastMod, LastRef, Mod, FwMod),
+ PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+add_init_and_end_per_suite([{skip_case,{{Mod,_},_Cmt},_Mode}=Case|Cases],
+ LastMod, LastRef, FwMod) when Mod =/= LastMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod),
+ PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+add_init_and_end_per_suite([{skip_case,{conf,_,{Mod,_},_},_}=Case|Cases],
+ LastMod, LastRef, FwMod) when Mod =/= LastMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod),
+ PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+add_init_and_end_per_suite([{skip_case,{conf,_,{Mod,_},_}}=Case|Cases], LastMod,
+ LastRef, FwMod) when Mod =/= LastMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod),
+ PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+add_init_and_end_per_suite([{conf,Ref,Props,{FwMod,Func}}=Case|Cases], LastMod,
+ LastRef, FwMod) ->
+ %% if Mod == FwMod, this conf test is (probably) a test case group where
+ %% the init- and end-functions are missing in the suite, and if so,
+ %% the suite name should be stored as {suite,Suite} in Props
+ case proplists:get_value(suite, Props) of
+ Suite when Suite =/= undefined, Suite =/= LastMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_init_and_end_per_suite(LastMod, LastRef, Suite, FwMod),
+ Case1 = {conf,Ref,[{suite,NextMod}|proplists:delete(suite,Props)],
+ {FwMod,Func}},
+ PreCases ++ [Case1|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+ _ ->
+ [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]
+ end;
+add_init_and_end_per_suite([{conf,_,_,{Mod,_}}=Case|Cases], LastMod,
+ LastRef, FwMod) when Mod =/= LastMod, Mod =/= FwMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod),
+ PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+add_init_and_end_per_suite([SkipCase|Cases], LastMod, LastRef, FwMod)
+ when element(1,SkipCase) == skip_case; element(1,SkipCase) == auto_skip_case->
+ [SkipCase|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)];
+add_init_and_end_per_suite([{conf,_,_,_}=Case|Cases], LastMod, LastRef, FwMod) ->
+ [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)];
+add_init_and_end_per_suite([{Mod,_}=Case|Cases], LastMod, LastRef, FwMod)
+ when Mod =/= LastMod, Mod =/= FwMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod),
+ PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+add_init_and_end_per_suite([{Mod,_,_}=Case|Cases], LastMod, LastRef, FwMod)
+ when Mod =/= LastMod, Mod =/= FwMod ->
+ {PreCases, NextMod, NextRef} =
+ do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod),
+ PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod,
+ NextRef, FwMod)];
+add_init_and_end_per_suite([Case|Cases], LastMod, LastRef, FwMod)->
+ [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)];
+add_init_and_end_per_suite([], _LastMod, undefined, _FwMod) ->
+ [];
+add_init_and_end_per_suite([], _LastMod, skipped_suite, _FwMod) ->
+ [];
+add_init_and_end_per_suite([], LastMod, LastRef, FwMod) ->
+ %% we'll add end_per_suite here even if it's not exported
+ %% (and simply let the call fail if it's missing)
+ case {erlang:function_exported(LastMod, end_per_suite, 1),
+ erlang:function_exported(LastMod, init_per_suite, 1)} of
+ {false,false} ->
+ %% let's call a "fake" end_per_suite if it exists
+ case erlang:function_exported(FwMod, end_per_suite, 1) of
+ true ->
+ [{conf,LastRef,[{suite,LastMod}],{FwMod,end_per_suite}}];
+ false ->
+ [{conf,LastRef,[],{LastMod,end_per_suite}}]
+ end;
+ _ ->
+ %% If any of these exist, the other should too
+ %% (required and documented). If it isn't, it will fail
+ %% with reason 'undef'.
+ [{conf,LastRef,[],{LastMod,end_per_suite}}]
+ end.
+
+do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod) ->
+ _ = case code:is_loaded(Mod) of
+ false -> code:load_file(Mod);
+ _ -> ok
+ end,
+ {Init,NextMod,NextRef} =
+ case {erlang:function_exported(Mod, init_per_suite, 1),
+ erlang:function_exported(Mod, end_per_suite, 1)} of
+ {false,false} ->
+ %% let's call a "fake" init_per_suite if it exists
+ case erlang:function_exported(FwMod, init_per_suite, 1) of
+ true ->
+ Ref = make_ref(),
+ {[{conf,Ref,[{suite,Mod}],
+ {FwMod,init_per_suite}}],Mod,Ref};
+ false ->
+ {[],Mod,undefined}
+ end;
+ _ ->
+ %% If any of these exist, the other should too
+ %% (required and documented). If it isn't, it will fail
+ %% with reason 'undef'.
+ Ref = make_ref(),
+ {[{conf,Ref,[],{Mod,init_per_suite}}],Mod,Ref}
+ end,
+ Cases =
+ if LastRef==undefined ->
+ Init;
+ LastRef==skipped_suite ->
+ Init;
+ true ->
+ %% we'll add end_per_suite here even if it's not exported
+ %% (and simply let the call fail if it's missing)
+ case {erlang:function_exported(LastMod, end_per_suite, 1),
+ erlang:function_exported(LastMod, init_per_suite, 1)} of
+ {false,false} ->
+ %% let's call a "fake" end_per_suite if it exists
+ case erlang:function_exported(FwMod, end_per_suite, 1) of
+ true ->
+ [{conf,LastRef,[{suite,Mod}],
+ {FwMod,end_per_suite}}|Init];
+ false ->
+ [{conf,LastRef,[],{LastMod,end_per_suite}}|Init]
+ end;
+ _ ->
+ %% If any of these exist, the other should too
+ %% (required and documented). If it isn't, it will fail
+ %% with reason 'undef'.
+ [{conf,LastRef,[],{LastMod,end_per_suite}}|Init]
+ end
+ end,
+ {Cases,NextMod,NextRef}.
+
+do_add_end_per_suite_and_skip(LastMod, LastRef, Mod, FwMod) ->
+ case LastRef of
+ No when No==undefined ; No==skipped_suite ->
+ {[],Mod,skipped_suite};
+ _Ref ->
+ case {erlang:function_exported(LastMod, end_per_suite, 1),
+ erlang:function_exported(LastMod, init_per_suite, 1)} of
+ {false,false} ->
+ case erlang:function_exported(FwMod, end_per_suite, 1) of
+ true ->
+ %% let's call "fake" end_per_suite if it exists
+ {[{conf,LastRef,[],{FwMod,end_per_suite}}],
+ Mod,skipped_suite};
+ false ->
+ {[{conf,LastRef,[],{LastMod,end_per_suite}}],
+ Mod,skipped_suite}
+ end;
+ _ ->
+ %% If any of these exist, the other should too
+ %% (required and documented). If it isn't, it will fail
+ %% with reason 'undef'.
+ {[{conf,LastRef,[],{LastMod,end_per_suite}}],
+ Mod,skipped_suite}
+ end
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% run_test_cases(TestSpec, Config, TimetrapData) -> exit(Result)
+%%
+%% Runs the specified tests, then displays/logs the summary.
+
+run_test_cases(TestSpec, Config, TimetrapData) ->
+ test_server:init_valgrind(),
+ case lists:member(no_src, get(test_server_logopts)) of
+ true ->
+ ok;
+ false ->
+ FwMod = get_fw_mod(?MODULE),
+ html_convert_modules(TestSpec, Config, FwMod)
+ end,
+
+ run_test_cases_loop(TestSpec, [Config], TimetrapData, [], []),
+
+ {AllSkippedN,UserSkipN,AutoSkipN,SkipStr} =
+ case get(test_server_skipped) of
+ {0,0} -> {0,0,0,""};
+ {US,AS} -> {US+AS,US,AS,io_lib:format(", ~w skipped", [US+AS])}
+ end,
+ OkN = get(test_server_ok),
+ FailedN = get(test_server_failed),
+ print(1, "TEST COMPLETE, ~w ok, ~w failed~ts of ~w test cases\n",
+ [OkN,FailedN,SkipStr,OkN+FailedN+AllSkippedN]),
+ test_server_sup:framework_call(report, [tests_done,
+ {OkN,FailedN,{UserSkipN,AutoSkipN}}]),
+ print(major, "=finished ~s", [lists:flatten(timestamp_get(""))]),
+ print(major, "=failed ~w", [FailedN]),
+ print(major, "=successful ~w", [OkN]),
+ print(major, "=user_skipped ~w", [UserSkipN]),
+ print(major, "=auto_skipped ~w", [AutoSkipN]),
+ exit(test_suites_done).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% run_test_cases_loop(TestCases, Config, TimetrapData, Mode, Status) -> ok
+%% TestCases = [Test,...]
+%% Config = [[{Key,Val},...],...]
+%% TimetrapData = {MultiplyTimetrap,ScaleTimetrap}
+%% MultiplyTimetrap = integer() | infinity
+%% ScaleTimetrap = bool()
+%% Mode = [{Ref,[Prop,..],StartTime}]
+%% Ref = reference()
+%% Prop = {name,Name} | sequence | parallel |
+%% shuffle | {shuffle,Seed} |
+%% repeat | {repeat,N} |
+%% repeat_until_all_ok | {repeat_until_all_ok,N} |
+%% repeat_until_any_ok | {repeat_until_any_ok,N} |
+%% repeat_until_any_fail | {repeat_until_any_fail,N} |
+%% repeat_until_all_fail | {repeat_until_all_fail,N}
+%% Status = [{Ref,{{Ok,Skipped,Failed},CopiedCases}}]
+%% Ok = Skipped = Failed = [Case,...]
+%%
+%% Execute the TestCases under configuration Config. Config is a list
+%% of lists, where hd(Config) holds the config tuples for the current
+%% conf case and tl(Config) is the data for the higher level conf cases.
+%% Config data is "inherited" from top to nested conf cases, but
+%% never the other way around. if length(Config) == 1, Config contains
+%% only the initial config data for the suite.
+%%
+%% Test may be one of the following:
+%%
+%% {conf,Ref,Props,{Mod,Func}} Mod:Func is a configuration modification
+%% function, call it with the current configuration as argument. It will
+%% return a new configuration.
+%%
+%% {make,Ref,{Mod,Func,Args}} Mod:Func is a make function, and it is called
+%% with the given arguments.
+%%
+%% {Mod,Case} This is a normal test case. Determine the correct
+%% configuration, and insert {Mod,Case,Config} as head of the list,
+%% then reiterate.
+%%
+%% {Mod,Case,Args} A test case with predefined argument (usually a normal
+%% test case which just got a fresh configuration (see above)).
+%%
+%% {skip_case,{conf,Ref,Case,Comment}} An init conf case gets skipped
+%% by the user. This will also cause the end conf case to be skipped.
+%% Note that it is not possible to skip an end conf case directly (it
+%% can only be skipped indirectly by a skipped init conf case). The
+%% comment (which gets printed in the log files) describes why the case
+%% was skipped.
+%%
+%% {skip_case,{Case,Comment},Mode} A normal test case skipped by the user.
+%% The comment (which gets printed in the log files) describes why the
+%% case was skipped.
+%%
+%% {auto_skip_case,{conf,Ref,Case,Comment},Mode} This is the result of
+%% an end conf case being automatically skipped due to a failing init
+%% conf case. It could also be a nested conf case that gets skipped
+%% because of a failed or skipped top level conf.
+%%
+%% {auto_skip_case,{Case,Comment},Mode} This is a normal test case which
+%% gets automatically skipped because of a failing init conf case or
+%% because of a failing previous test case in a sequence.
+%%
+%% -------------------------------------------------------------------
+%% Description of IO handling during execution of parallel test cases:
+%% -------------------------------------------------------------------
+%%
+%% A conf group can have an associated list of properties. If the
+%% parallel property is specified for a group, it means the test cases
+%% should be spawned and run in parallel rather than called sequentially
+%% (which is always the default mode). Test cases that execute in parallel
+%% also write to their respective minor log files in parallel. Printouts
+%% to common log files, such as the summary html file and the major log
+%% file on text format, still have to be processed sequentially. For this
+%% reason, the Mode argument specifies if a parallel group is currently
+%% being executed.
+%%
+%% The low-level mechanism for buffering IO for the common log files
+%% is handled by the test_server_io module. Buffering is turned on by
+%% test_server_io:start_transaction/0 and off by calling
+%% test_server_io:end_transaction/0. The buffered data for the transaction
+%% can printed by calling test_server_io:print_buffered/1.
+%%
+%% This module is responsible for turning on IO buffering and to later
+%% test_server_io:print_buffered/1 to print the data. To help with this,
+%% two variables in the process dictionary are used:
+%% 'test_server_common_io_handler' and 'test_server_queued_io'. The values
+%% are set to as following:
+%%
+%% 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:lexemes(NumStr, [$ ,$:,$,])])
+ end,
+ {shuffle_cases(Ref, Cs0, UseSeed),{shuffle,UseSeed}}
+ end;
+ not StartConf ->
+ {Cs0,undefined}
+ end,
+
+ %% if this is a start conf we check if Props specifies repeat and if so
+ %% we copy the group and carry the copy until the end conf where we
+ %% decide to perform the repetition or not
+ {Repeating,Status2,Cases,ReportRepeatStop} =
+ if StartConf ->
+ case get_repeat(Props) of
+ undefined ->
+ %% we *must* have a status entry for every conf since we
+ %% will continously update status with test case results
+ %% without knowing the Ref (but update hd(Status))
+ {false,new_status(Ref, Status1),Cases1,?void_fun};
+ {_RepType,N} when N =< 1 ->
+ {false,new_status(Ref, Status1),Cases1,?void_fun};
+ _ ->
+ {Copied,_} = copy_cases(Ref, make_ref(), Cs1),
+ {true,new_status(Ref, Copied, Status1),Cases1,?void_fun}
+ end;
+ not StartConf ->
+ RepVal = get_repeat(get_props(Mode0)),
+ ReportStop =
+ fun() ->
+ print(minor, "~n*** Stopping repeat operation ~w", [RepVal]),
+ print(1, "Stopping repeat operation ~w", [RepVal])
+ end,
+ CopiedCases = get_copied_cases(Status1),
+ EndStatus = delete_status(Ref, Status1),
+ %% check in Mode0 if this is a repeat conf
+ case RepVal of
+ undefined ->
+ {false,EndStatus,Cases1,?void_fun};
+ {_RepType,N} when N =< 1 ->
+ {false,EndStatus,Cases1,?void_fun};
+ {repeat,_} ->
+ {true,EndStatus,CopiedCases++Cases1,?void_fun};
+ {repeat_until_all_ok,_} ->
+ {RestCs,Fun} = case get_tc_results(Status1) of
+ {_,_,[]} ->
+ {Cases1,ReportStop};
+ _ ->
+ {CopiedCases++Cases1,?void_fun}
+ end,
+ {true,EndStatus,RestCs,Fun};
+ {repeat_until_any_ok,_} ->
+ {RestCs,Fun} = case get_tc_results(Status1) of
+ {Ok,_,_Fails} when length(Ok) > 0 ->
+ {Cases1,ReportStop};
+ _ ->
+ {CopiedCases++Cases1,?void_fun}
+ end,
+ {true,EndStatus,RestCs,Fun};
+ {repeat_until_any_fail,_} ->
+ {RestCs,Fun} = case get_tc_results(Status1) of
+ {_,_,Fails} when length(Fails) > 0 ->
+ {Cases1,ReportStop};
+ _ ->
+ {CopiedCases++Cases1,?void_fun}
+ end,
+ {true,EndStatus,RestCs,Fun};
+ {repeat_until_all_fail,_} ->
+ {RestCs,Fun} = case get_tc_results(Status1) of
+ {[],_,_} ->
+ {Cases1,ReportStop};
+ _ ->
+ {CopiedCases++Cases1,?void_fun}
+ end,
+ {true,EndStatus,RestCs,Fun}
+ end
+ end,
+
+ ReportAbortRepeat = fun(What) when Repeating ->
+ print(minor, "~n*** Aborting repeat operation "
+ "(configuration case ~w)", [What]),
+ print(1, "Aborting repeat operation "
+ "(configuration case ~w)", [What]);
+ (_) -> ok
+ end,
+ CfgProps = if StartConf ->
+ if Shuffle == undefined ->
+ [{tc_group_properties,Props}];
+ true ->
+ [{tc_group_properties,
+ [Shuffle|delete_shuffle(Props)]}]
+ end;
+ not StartConf ->
+ {TcOk,TcSkip,TcFail} = get_tc_results(Status1),
+ [{tc_group_properties,get_props(Mode0)},
+ {tc_group_result,[{ok,TcOk},
+ {skipped,TcSkip},
+ {failed,TcFail}]}]
+ end,
+
+ SuiteName = proplists:get_value(suite, Props),
+ case get(test_server_create_priv_dir) of
+ auto_per_run -> % use common priv_dir
+ TSDirs = [{priv_dir,get(test_server_priv_dir)},
+ {data_dir,get_data_dir(Mod, SuiteName)}];
+ _ ->
+ TSDirs = [{data_dir,get_data_dir(Mod, SuiteName)}]
+ end,
+
+ ActualCfg =
+ if not StartConf ->
+ update_config(hd(Config), TSDirs ++ CfgProps);
+ true ->
+ GroupPath = lists:flatmap(fun({_Ref,[],_T}) -> [];
+ ({_Ref,GrProps,_T}) -> [GrProps]
+ end, Mode0),
+ update_config(hd(Config),
+ TSDirs ++ [{tc_group_path,GroupPath} | CfgProps])
+ end,
+
+ CurrMode = curr_mode(Ref, Mode0, Mode),
+ ConfCaseResult = run_test_case(Ref, 0, Mod, Func, [ActualCfg], skip_init,
+ TimetrapData, CurrMode),
+
+ case ConfCaseResult of
+ {_,NewCfg,_} when Func == init_per_suite, is_list(NewCfg) ->
+ %% check that init_per_suite returned data on correct format
+ case lists:filter(fun({_,_}) -> false;
+ (_) -> true end, NewCfg) of
+ [] ->
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases, [NewCfg|Config],
+ TimetrapData, Mode, Status2);
+ Bad ->
+ print(minor,
+ "~n*** ~tw returned bad elements in Config: ~tp.~n",
+ [Func,Bad]),
+ Reason = {failed,{Mod,init_per_suite,bad_return}},
+ Cases2 = skip_cases_upto(Ref, Cases, Reason, conf, CurrMode,
+ auto_skip_case),
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases2, Config, TimetrapData, Mode,
+ delete_status(Ref, Status2))
+ end;
+ {_,NewCfg,_} when StartConf, is_list(NewCfg) ->
+ print_conf_time(ConfTime),
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases, [NewCfg|Config], TimetrapData, Mode, Status2);
+ {_,{framework_error,{FwMod,FwFunc},Reason},_} ->
+ print(minor, "~n*** ~w failed in ~tw. Reason: ~tp~n",
+ [FwMod,FwFunc,Reason]),
+ print(1, "~w failed in ~tw. Reason: ~tp~n", [FwMod,FwFunc,Reason]),
+ exit(framework_error);
+ {_,Fail,_} when element(1,Fail) == 'EXIT';
+ element(1,Fail) == timetrap_timeout;
+ element(1,Fail) == user_timetrap_error;
+ element(1,Fail) == failed ->
+ {Cases2,Config1,Status3} =
+ if StartConf ->
+ ReportAbortRepeat(failed),
+ print(minor, "~n*** ~tw failed.~n"
+ " Skipping all cases.", [Func]),
+ Reason = {failed,{Mod,Func,Fail}},
+ {skip_cases_upto(Ref, Cases, Reason, conf, CurrMode,
+ auto_skip_case),
+ Config,
+ update_status(failed, group_result, get_name(Mode),
+ delete_status(Ref, Status2))};
+ not StartConf ->
+ ReportRepeatStop(),
+ print_conf_time(ConfTime),
+ {Cases,tl(Config),delete_status(Ref, Status2)}
+ end,
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases2, Config1, TimetrapData, Mode, Status3);
+
+ {_,{auto_skip,SkipReason},_} ->
+ %% this case can only happen if the framework (not the user)
+ %% decides to skip execution of a conf function
+ {Cases2,Config1,Status3} =
+ if StartConf ->
+ ReportAbortRepeat(auto_skipped),
+ print(minor, "~n*** ~tw auto skipped.~n"
+ " Skipping all cases.", [Func]),
+ {skip_cases_upto(Ref, Cases, SkipReason, conf, CurrMode,
+ auto_skip_case),
+ Config,
+ delete_status(Ref, Status2)};
+ not StartConf ->
+ ReportRepeatStop(),
+ print_conf_time(ConfTime),
+ {Cases,tl(Config),delete_status(Ref, Status2)}
+ end,
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases2, Config1, TimetrapData, Mode, Status3);
+
+ {_,{Skip,Reason},_} when StartConf and ((Skip==skip) or (Skip==skipped)) ->
+ ReportAbortRepeat(skipped),
+ print(minor, "~n*** ~tw skipped.~n"
+ " Skipping all cases.", [Func]),
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(skip_cases_upto(Ref, Cases, Reason, conf,
+ CurrMode, skip_case),
+ [hd(Config)|Config], TimetrapData, Mode,
+ delete_status(Ref, Status2));
+ {_,{skip_and_save,Reason,_SavedConfig},_} when StartConf ->
+ ReportAbortRepeat(skipped),
+ print(minor, "~n*** ~tw skipped.~n"
+ " Skipping all cases.", [Func]),
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(skip_cases_upto(Ref, Cases, Reason, conf,
+ CurrMode, skip_case),
+ [hd(Config)|Config], TimetrapData, Mode,
+ delete_status(Ref, Status2));
+ {_,_Other,_} when Func == init_per_suite ->
+ print(minor, "~n*** init_per_suite failed to return a Config list.~n", []),
+ Reason = {failed,{Mod,init_per_suite,bad_return}},
+ Cases2 = skip_cases_upto(Ref, Cases, Reason, conf, CurrMode,
+ auto_skip_case),
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases2, Config, TimetrapData, Mode,
+ delete_status(Ref, Status2));
+ {_,_Other,_} when StartConf ->
+ print_conf_time(ConfTime),
+ set_io_buffering(IOHandler),
+ ReportRepeatStop(),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases, [hd(Config)|Config], TimetrapData,
+ Mode, Status2);
+ {_,_EndConfRetVal,Opts} ->
+ %% Check if return_group_result is set (ok, skipped or failed) and
+ %% if so:
+ %% 1) *If* the parent group is a sequence, skip all proceeding tests
+ %% in that group.
+ %% 2) Return the value to the group "above" so that result may be
+ %% used for evaluating a 'repeat_until_*' property.
+ GrName = get_name(Mode0, Func),
+ {Cases2,Status3} =
+ case lists:keysearch(return_group_result, 1, Opts) of
+ {value,{_,failed}} ->
+ case {curr_ref(Mode),check_prop(sequence, Mode)} of
+ {ParentRef,ParentRef} ->
+ Reason = {group_result,GrName,failed},
+ {skip_cases_upto(ParentRef, Cases, Reason, tc,
+ Mode, auto_skip_case),
+ update_status(failed, group_result, GrName,
+ delete_status(Ref, Status2))};
+ _ ->
+ {Cases,update_status(failed, group_result, GrName,
+ delete_status(Ref, Status2))}
+ end;
+ {value,{_,GroupResult}} ->
+ {Cases,update_status(GroupResult, group_result, GrName,
+ delete_status(Ref, Status2))};
+ false ->
+ {Cases,update_status(ok, group_result, GrName,
+ delete_status(Ref, Status2))}
+ end,
+ print_conf_time(ConfTime),
+ ReportRepeatStop(),
+ set_io_buffering(IOHandler),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases2, tl(Config), TimetrapData,
+ Mode, Status3)
+ end;
+
+run_test_cases_loop([{make,Ref,{Mod,Func,Args}}|Cases0], Config, TimetrapData,
+ Mode, Status) ->
+ case run_test_case(Ref, 0, Mod, Func, Args, skip_init, TimetrapData) of
+ {_,Why={'EXIT',_},_} ->
+ print(minor, "~n*** ~tw failed.~n"
+ " Skipping all cases.", [Func]),
+ Reason = {failed,{Mod,Func,Why}},
+ Cases = skip_cases_upto(Ref, Cases0, Reason, conf, Mode,
+ auto_skip_case),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status);
+ {_,_Whatever,_} ->
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases0, Config, TimetrapData, Mode, Status)
+ end;
+
+run_test_cases_loop([{conf,_Ref,_Props,_X}=Conf|_Cases0],
+ Config, _TimetrapData, _Mode, _Status) ->
+ erlang:error(badarg, [Conf,Config]);
+
+run_test_cases_loop([{Mod,Case}|Cases], Config, TimetrapData, Mode, Status) ->
+ ActualCfg =
+ case get(test_server_create_priv_dir) of
+ auto_per_run ->
+ update_config(hd(Config), [{priv_dir,get(test_server_priv_dir)},
+ {data_dir,get_data_dir(Mod)}]);
+ _ ->
+ update_config(hd(Config), [{data_dir,get_data_dir(Mod)}])
+ end,
+ run_test_cases_loop([{Mod,Case,[ActualCfg]}|Cases], Config,
+ TimetrapData, Mode, Status);
+
+run_test_cases_loop([{Mod,Func,Args}|Cases], Config, TimetrapData, Mode, Status) ->
+ {Num,RunInit} =
+ case FwMod = get_fw_mod(?MODULE) of
+ Mod when Func == error_in_suite ->
+ {-1,skip_init};
+ _ ->
+ {put(test_server_case_num, get(test_server_case_num)+1),
+ run_init}
+ end,
+
+ %% check the current execution mode and save info about the case if
+ %% detected that printouts to common log files is handled later
+
+ case check_prop(parallel, Mode) =:= false andalso is_io_buffered() of
+ true ->
+ %% sequential test case nested in a parallel group;
+ %% io is buffered, so we must queue this test case
+ queue_test_case_io(undefined, self(), Num+1, Mod, Func);
+ false ->
+ ok
+ end,
+
+ case run_test_case(undefined, Num+1, Mod, Func, Args,
+ RunInit, TimetrapData, Mode) of
+ %% callback to framework module failed, exit immediately
+ {_,{framework_error,{FwMod,FwFunc},Reason},_} ->
+ print(minor, "~n*** ~w failed in ~tw. Reason: ~tp~n",
+ [FwMod,FwFunc,Reason]),
+ print(1, "~w failed in ~tw. Reason: ~tp~n", [FwMod,FwFunc,Reason]),
+ stop_minor_log_file(),
+ exit(framework_error);
+ %% sequential execution of test case finished
+ {Time,RetVal,_} ->
+ RetTag =
+ if is_tuple(RetVal) -> element(1,RetVal);
+ true -> undefined
+ end,
+ {Failed,Status1} =
+ case RetTag of
+ Skip when Skip==skip; Skip==skipped ->
+ {false,update_status(skipped, Mod, Func, Status)};
+ Fail when Fail=='EXIT'; Fail==failed ->
+ {true,update_status(failed, Mod, Func, Status)};
+ _ when Time==died, RetVal=/=ok ->
+ {true,update_status(failed, Mod, Func, Status)};
+ _ ->
+ {false,update_status(ok, Mod, Func, Status)}
+ end,
+ case check_prop(sequence, Mode) of
+ false ->
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status1);
+ Ref ->
+ %% the case is in a sequence; we must check the result and
+ %% determine if the following cases should run or be skipped
+ if not Failed -> % proceed with next case
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status1);
+ true -> % skip rest of cases in sequence
+ print(minor, "~n*** ~tw failed.~n"
+ " Skipping all other cases in sequence.",
+ [Func]),
+ Reason = {failed,{Mod,Func}},
+ Cases2 = skip_cases_upto(Ref, Cases, Reason, tc,
+ Mode, auto_skip_case),
+ stop_minor_log_file(),
+ run_test_cases_loop(Cases2, Config, TimetrapData, Mode, Status1)
+ end
+ end;
+ %% the test case is being executed in parallel with the main process (and
+ %% other test cases) and Pid is the dedicated process executing the case
+ Pid ->
+ %% io from Pid will be buffered by the test_server_io process and
+ %% handled later, so we have to save info about the case
+ queue_test_case_io(undefined, Pid, Num+1, Mod, Func),
+ run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status)
+ end;
+
+%% TestSpec processing finished
+run_test_cases_loop([], _Config, _TimetrapData, _, _) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% various help functions
+
+new_status(Ref, Status) ->
+ [{Ref,{{[],[],[]},[]}} | Status].
+
+new_status(Ref, CopiedCases, Status) ->
+ [{Ref,{{[],[],[]},CopiedCases}} | Status].
+
+delete_status(Ref, Status) ->
+ lists:keydelete(Ref, 1, Status).
+
+update_status(ok, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) ->
+ [{Ref,{{Ok++[{Mod,Func}],Skip,Fail},Cs}} | Status];
+
+update_status(skipped, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) ->
+ [{Ref,{{Ok,Skip++[{Mod,Func}],Fail},Cs}} | Status];
+
+update_status(failed, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) ->
+ [{Ref,{{Ok,Skip,Fail++[{Mod,Func}]},Cs}} | Status];
+
+update_status(_, _, _, []) ->
+ [].
+
+update_status(Ref, {Ok,Skip,Fail}, [{Ref,{{Ok0,Skip0,Fail0},Cs}} | Status]) ->
+ [{Ref,{{Ok0++Ok,Skip0++Skip,Fail0++Fail},Cs}} | Status].
+
+get_copied_cases([{_,{_,Cases}} | _Status]) ->
+ Cases.
+
+get_tc_results([{_,{OkSkipFail,_}} | _Status]) ->
+ OkSkipFail;
+get_tc_results([]) -> % in case init_per_suite crashed
+ {[],[],[]}.
+
+conf(Ref, Props) ->
+ {Ref,Props,?now}.
+
+curr_ref([{Ref,_Props,_}|_]) ->
+ Ref;
+curr_ref([]) ->
+ undefined.
+
+curr_mode(Ref, Mode0, Mode1) ->
+ case curr_ref(Mode1) of
+ Ref -> Mode1;
+ _ -> Mode0
+ end.
+
+get_props([{_,Props,_} | _]) ->
+ Props;
+get_props([]) ->
+ [].
+
+check_prop(_Attrib, []) ->
+ false;
+check_prop(Attrib, [{Ref,Props,_}|_]) ->
+ case lists:member(Attrib, Props) of
+ true -> Ref;
+ false -> false
+ end.
+
+check_props(Attrib, Mode) ->
+ case [R || {R,Ps,_} <- Mode, lists:member(Attrib, Ps)] of
+ [] -> false;
+ [Ref|_] -> Ref
+ end.
+
+get_name(Mode, Def) ->
+ case get_name(Mode) of
+ undefined -> Def;
+ Name -> Name
+ end.
+
+get_name([{_Ref,Props,_}|_]) ->
+ proplists:get_value(name, Props);
+get_name([]) ->
+ undefined.
+
+conf_start(Ref, Mode) ->
+ case lists:keysearch(Ref, 1, Mode) of
+ {value,{_,_,T}} -> T;
+ false -> 0
+ end.
+
+
+get_data_dir(Mod) ->
+ get_data_dir(Mod, undefined).
+
+get_data_dir(Mod, Suite) ->
+ UseMod = if Suite == undefined -> Mod;
+ true -> Suite
+ end,
+ case code:which(UseMod) of
+ non_existing ->
+ print(12, "The module ~w is not loaded", [Mod]),
+ [];
+ cover_compiled ->
+ MainCoverNode = cover:get_main_node(),
+ {file,File} = rpc:call(MainCoverNode,cover,is_compiled,[UseMod]),
+ do_get_data_dir(UseMod,File);
+ FullPath ->
+ do_get_data_dir(UseMod,FullPath)
+ end.
+
+do_get_data_dir(Mod,File) ->
+ filename:dirname(File) ++ "/" ++ atom_to_list(Mod) ++ ?data_dir_suffix.
+
+print_conf_time(0) ->
+ ok;
+print_conf_time(ConfTime) ->
+ print(major, "=group_time ~.3fs", [ConfTime]),
+ print(minor, "~n=== Total execution time of group: ~.3fs~n", [ConfTime]).
+
+print_props([]) ->
+ ok;
+print_props(Props) ->
+ print(major, "=group_props ~tp", [Props]),
+ print(minor, "Group properties: ~tp~n", [Props]).
+
+%% repeat N times: {repeat,N}
+%% repeat N times or until all successful: {repeat_until_all_ok,N}
+%% repeat N times or until at least one successful: {repeat_until_any_ok,N}
+%% repeat N times or until at least one case fails: {repeat_until_any_fail,N}
+%% repeat N times or until all fails: {repeat_until_all_fail,N}
+%% N = integer() | forever
+get_repeat(Props) ->
+ get_prop([repeat,repeat_until_all_ok,repeat_until_any_ok,
+ repeat_until_any_fail,repeat_until_all_fail], forever, Props).
+
+update_repeat(Props) ->
+ case get_repeat(Props) of
+ undefined ->
+ Props;
+ {RepType,N} ->
+ Props1 =
+ if N == forever ->
+ [{RepType,N}|lists:keydelete(RepType, 1, Props)];
+ N < 3 ->
+ lists:keydelete(RepType, 1, Props);
+ N >= 3 ->
+ [{RepType,N-1}|lists:keydelete(RepType, 1, Props)]
+ end,
+ %% if shuffle is used in combination with repeat, a new
+ %% seed shouldn't be set every new turn
+ case get_shuffle(Props1) of
+ undefined ->
+ Props1;
+ _ ->
+ [{shuffle,repeated}|delete_shuffle(Props1)]
+ end
+ end.
+
+get_shuffle(Props) ->
+ get_prop([shuffle], ?now, Props).
+
+delete_shuffle(Props) ->
+ delete_prop([shuffle], Props).
+
+%% Return {Item,Value} if found, else if Item alone
+%% is found, return {Item,Default}
+get_prop([Item|Items], Default, Props) ->
+ case lists:keysearch(Item, 1, Props) of
+ {value,R} ->
+ R;
+ false ->
+ case lists:member(Item, Props) of
+ true ->
+ {Item,Default};
+ false ->
+ get_prop(Items, Default, Props)
+ end
+ end;
+get_prop([], _Def, _Props) ->
+ undefined.
+
+delete_prop([Item|Items], Props) ->
+ Props1 = lists:delete(Item, lists:keydelete(Item, 1, Props)),
+ delete_prop(Items, Props1);
+delete_prop([], Props) ->
+ Props.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% shuffle_cases(Ref, Cases, Seed) -> Cases1
+%%
+%% Shuffles the order of Cases.
+
+shuffle_cases(Ref, Cases, undefined) ->
+ shuffle_cases(Ref, Cases, rand:seed_s(exsplus));
+
+shuffle_cases(Ref, [{conf,Ref,_,_}=Start | Cases], Seed0) ->
+ {N,CasesToShuffle,Rest} = cases_to_shuffle(Ref, Cases),
+ Seed = case Seed0 of
+ {X,Y,Z} when is_integer(X+Y+Z) ->
+ rand:seed(exsplus, Seed0);
+ _ ->
+ Seed0
+ end,
+ ShuffledCases = random_order(N, rand:uniform_s(N, Seed), CasesToShuffle, []),
+ [Start|ShuffledCases] ++ Rest.
+
+cases_to_shuffle(Ref, Cases) ->
+ cases_to_shuffle(Ref, Cases, 1, []).
+
+cases_to_shuffle(Ref, [{conf,Ref,_,_} | _]=Cs, N, Ix) -> % end
+ {N-1,Ix,Cs};
+cases_to_shuffle(Ref, [{skip_case,{_,Ref,_,_},_} | _]=Cs, N, Ix) -> % end
+ {N-1,Ix,Cs};
+
+cases_to_shuffle(Ref, [{conf,Ref1,_,_}=C | Cs], N, Ix) -> % nested group
+ {Cs1,Rest} = get_subcases(Ref1, Cs, []),
+ cases_to_shuffle(Ref, Rest, N+1, [{N,[C|Cs1]} | Ix]);
+cases_to_shuffle(Ref, [{skip_case,{_,Ref1,_,_},_}=C | Cs], N, Ix) -> % nested group
+ {Cs1,Rest} = get_subcases(Ref1, Cs, []),
+ cases_to_shuffle(Ref, Rest, N+1, [{N,[C|Cs1]} | Ix]);
+
+cases_to_shuffle(Ref, [C | Cs], N, Ix) ->
+ cases_to_shuffle(Ref, Cs, N+1, [{N,[C]} | Ix]).
+
+get_subcases(SubRef, [{conf,SubRef,_,_}=C | Cs], SubCs) ->
+ {lists:reverse([C|SubCs]),Cs};
+get_subcases(SubRef, [{skip_case,{_,SubRef,_,_},_}=C | Cs], SubCs) ->
+ {lists:reverse([C|SubCs]),Cs};
+get_subcases(SubRef, [C|Cs], SubCs) ->
+ get_subcases(SubRef, Cs, [C|SubCs]).
+
+random_order(1, {_Pos,Seed}, [{_Ix,CaseOrGroup}], Shuffled) ->
+ %% save current seed to be used if test cases are repeated
+ put(test_server_curr_random_seed, Seed),
+ Shuffled++CaseOrGroup;
+random_order(N, {Pos,NewSeed}, IxCases, Shuffled) ->
+ {First,[{_Ix,CaseOrGroup}|Rest]} = lists:split(Pos-1, IxCases),
+ random_order(N-1, rand:uniform_s(N-1, NewSeed),
+ First++Rest, Shuffled++CaseOrGroup).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% skip_case(Type, Ref, CaseNum, Case, Comment, SendSync) -> {Mod,Func}
+%%
+%% Prints info about a skipped case in the major and html log files.
+%% SendSync determines if start and finished messages must be sent so
+%% that the printouts can be buffered and handled in order with io from
+%% parallel processes.
+skip_case(Type, Ref, CaseNum, Case, Comment, SendSync, Mode) ->
+ MF = {Mod,Func} = case Case of
+ {M,F,_A} -> {M,F};
+ {M,F} -> {M,F}
+ end,
+ if SendSync ->
+ queue_test_case_io(Ref, self(), CaseNum, Mod, Func),
+ self() ! {started,Ref,self(),CaseNum,Mod,Func},
+ test_server_io:start_transaction(),
+ skip_case1(Type, CaseNum, Mod, Func, Comment, Mode),
+ test_server_io:end_transaction(),
+ self() ! {finished,Ref,self(),CaseNum,Mod,Func,skipped,{0,skipped,[]}};
+ not SendSync ->
+ skip_case1(Type, CaseNum, Mod, Func, Comment, Mode)
+ end,
+ MF.
+
+skip_case1(Type, CaseNum, Mod, Func, Comment, Mode) ->
+ {{Col0,Col1},_} = get_font_style((CaseNum > 0), Mode),
+ ResultCol = if Type == auto -> ?auto_skip_color;
+ Type == user -> ?user_skip_color
+ end,
+ print(major, "~n=case ~w:~tw", [Mod,Func]),
+ GroupName = case get_name(Mode) of
+ undefined ->
+ "";
+ GrName ->
+ GrName1 = cast_to_list(GrName),
+ print(major, "=group_props ~tp", [[{name,GrName1}]]),
+ GrName1
+ end,
+ print(major, "=started ~s", [lists:flatten(timestamp_get(""))]),
+ Comment1 = reason_to_string(Comment),
+ if Type == auto ->
+ print(major, "=result auto_skipped: ~ts", [Comment1]);
+ Type == user ->
+ print(major, "=result skipped: ~ts", [Comment1])
+ end,
+ if CaseNum == 0 ->
+ print(2,"*** Skipping ~tw ***", [{Mod,Func}]);
+ true ->
+ print(2,"*** Skipping test case #~w ~tw ***", [CaseNum,{Mod,Func}])
+ end,
+ TR = xhtml("<tr valign=\"top\">", ["<tr class=\"",odd_or_even(),"\">"]),
+ GroupName = case get_name(Mode) of
+ undefined -> "";
+ Name -> cast_to_list(Name)
+ end,
+ print(html,
+ TR ++ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>"
+ "<td>" ++ Col0 ++ "~w" ++ Col1 ++ "</td>"
+ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>"
+ "<td>" ++ Col0 ++ "~tw" ++ Col1 ++ "</td>"
+ "<td>" ++ Col0 ++ "< >" ++ Col1 ++ "</td>"
+ "<td>" ++ Col0 ++ "0.000s" ++ Col1 ++ "</td>"
+ "<td><font color=\"~ts\">SKIPPED</font></td>"
+ "<td>~ts</td></tr>\n",
+ [num2str(CaseNum),fw_name(Mod),GroupName,Func,ResultCol,Comment1]),
+
+ if CaseNum > 0 ->
+ {US,AS} = get(test_server_skipped),
+ case Type of
+ user -> put(test_server_skipped, {US+1,AS});
+ auto -> put(test_server_skipped, {US,AS+1})
+ end,
+ put(test_server_case_num, CaseNum);
+ true -> % conf
+ ok
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% skip_cases_upto(Ref, Cases, Reason, Origin, Mode, SkipType) -> Cases1
+%%
+%% SkipType = skip_case | auto_skip_case
+%% Mark all cases tagged with Ref as skipped.
+
+skip_cases_upto(Ref, Cases, Reason, Origin, Mode, SkipType) ->
+ {_,Modified,Rest} =
+ modify_cases_upto(Ref, {skip,Reason,Origin,Mode,SkipType}, Cases),
+ Modified++Rest.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% copy_cases(OrigRef, NewRef, Cases) -> Cases1
+%%
+%% Copy the test cases marked with OrigRef and tag the copies with NewRef.
+%% The start conf case copy will also get its repeat property updated.
+
+copy_cases(OrigRef, NewRef, Cases) ->
+ {Original,Altered,Rest} = modify_cases_upto(OrigRef, {copy,NewRef}, Cases),
+ {Altered,Original++Altered++Rest}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% modify_cases_upto(Ref, ModOp, Cases) -> {Original,Altered,Remaining}
+%%
+%% ModOp = {skip,Reason,Origin,Mode} | {copy,NewRef}
+%% Origin = conf | tc
+%%
+%% Modifies Cases according to ModOp and returns the original elements,
+%% the modified versions of these elements and the remaining (untouched)
+%% cases.
+
+modify_cases_upto(Ref, ModOp, Cases) ->
+ {Original,Altered,Rest} = modify_cases_upto(Ref, ModOp, Cases, [], []),
+ {lists:reverse(Original),lists:reverse(Altered),Rest}.
+
+%% first case of a copy operation is the start conf
+modify_cases_upto(Ref, {copy,NewRef}=Op, [{conf,Ref,Props,MF}=C|T], Orig, Alt) ->
+ modify_cases_upto(Ref, Op, T, [C|Orig], [{conf,NewRef,update_repeat(Props),MF}|Alt]);
+
+modify_cases_upto(Ref, ModOp, Cases, Orig, Alt) ->
+ %% we need to check if there's an end conf case with the
+ %% same ref in the list, if not, this *is* an end conf case
+ case lists:any(fun({_,R,_,_}) when R == Ref -> true;
+ ({_,R,_}) when R == Ref -> true;
+ ({skip_case,{_,R,_,_},_}) when R == Ref -> true;
+ ({skip_case,{_,R,_,_}}) when R == Ref -> true;
+ (_) -> false
+ end, Cases) of
+ true ->
+ modify_cases_upto1(Ref, ModOp, Cases, Orig, Alt);
+ false ->
+ {[],[],Cases}
+ end.
+
+%% next case is a conf with same ref, must be end conf = we're done
+modify_cases_upto1(Ref, {skip,Reason,conf,Mode,skip_case},
+ [{conf,Ref,_Props,MF}|T], Orig, Alt) ->
+ {Orig,[{skip_case,{conf,Ref,MF,Reason},Mode}|Alt],T};
+modify_cases_upto1(Ref, {skip,Reason,conf,Mode,auto_skip_case},
+ [{conf,Ref,_Props,MF}|T], Orig, Alt) ->
+ {Orig,[{auto_skip_case,{conf,Ref,MF,Reason},Mode}|Alt],T};
+modify_cases_upto1(Ref, {copy,NewRef}, [{conf,Ref,Props,MF}=C|T], Orig, Alt) ->
+ {[C|Orig],[{conf,NewRef,update_repeat(Props),MF}|Alt],T};
+
+%% we've skipped all remaining cases in a sequence
+modify_cases_upto1(Ref, {skip,_,tc,_,_},
+ [{conf,Ref,_Props,_MF}|_]=Cs, Orig, Alt) ->
+ {Orig,Alt,Cs};
+
+%% next is a make case
+modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType},
+ [{make,Ref,MF}|T], Orig, Alt) ->
+ {Orig,[{SkipType,{make,Ref,MF,Reason},Mode}|Alt],T};
+modify_cases_upto1(Ref, {copy,NewRef}, [{make,Ref,MF}=M|T], Orig, Alt) ->
+ {[M|Orig],[{make,NewRef,MF}|Alt],T};
+
+%% next case is a user skipped end conf with the same ref = we're done
+modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType},
+ [{skip_case,{Type,Ref,MF,_Cmt},_}|T], Orig, Alt) ->
+ {Orig,[{SkipType,{Type,Ref,MF,Reason},Mode}|Alt],T};
+modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType},
+ [{skip_case,{Type,Ref,MF,_Cmt}}|T], Orig, Alt) ->
+ {Orig,[{SkipType,{Type,Ref,MF,Reason},Mode}|Alt],T};
+modify_cases_upto1(Ref, {copy,NewRef},
+ [{skip_case,{Type,Ref,MF,Cmt},Mode}=C|T], Orig, Alt) ->
+ {[C|Orig],[{skip_case,{Type,NewRef,MF,Cmt},Mode}|Alt],T};
+modify_cases_upto1(Ref, {copy,NewRef},
+ [{skip_case,{Type,Ref,MF,Cmt}}=C|T], Orig, Alt) ->
+ {[C|Orig],[{skip_case,{Type,NewRef,MF,Cmt}}|Alt],T};
+
+%% next is a skip_case, could be one test case or 'all' in suite, we must proceed
+modify_cases_upto1(Ref, ModOp, [{skip_case,{_F,_Cmt},_Mode}=MF|T], Orig, Alt) ->
+ modify_cases_upto1(Ref, ModOp, T, [MF|Orig], [MF|Alt]);
+
+%% next is a normal case (possibly in a sequence), mark as skipped, or copy, and proceed
+modify_cases_upto1(Ref, {skip,Reason,_,Mode,skip_case}=Op,
+ [{_M,_F}=MF|T], Orig, Alt) ->
+ modify_cases_upto1(Ref, Op, T, Orig, [{skip_case,{MF,Reason},Mode}|Alt]);
+modify_cases_upto1(Ref, {skip,Reason,_,Mode,auto_skip_case}=Op,
+ [{_M,_F}=MF|T], Orig, Alt) ->
+ modify_cases_upto1(Ref, Op, T, Orig, [{auto_skip_case,{MF,Reason},Mode}|Alt]);
+modify_cases_upto1(Ref, CopyOp, [{_M,_F}=MF|T], Orig, Alt) ->
+ modify_cases_upto1(Ref, CopyOp, T, [MF|Orig], [MF|Alt]);
+
+%% next is a conf case, modify the Mode arg to keep track of sub groups
+modify_cases_upto1(Ref, {skip,Reason,FType,Mode,SkipType},
+ [{conf,OtherRef,Props,_MF}|T], Orig, Alt) ->
+ case hd(Mode) of
+ {OtherRef,_,_} -> % end conf
+ modify_cases_upto1(Ref, {skip,Reason,FType,tl(Mode),SkipType},
+ T, Orig, Alt);
+ _ -> % start conf
+ Mode1 = [conf(OtherRef,Props)|Mode],
+ modify_cases_upto1(Ref, {skip,Reason,FType,Mode1,SkipType},
+ T, Orig, Alt)
+ end;
+
+%% next is some other case, ignore or copy
+modify_cases_upto1(Ref, {skip,_,_,_,_}=Op, [_Other|T], Orig, Alt) ->
+ modify_cases_upto1(Ref, Op, T, Orig, Alt);
+modify_cases_upto1(Ref, CopyOp, [C|T], Orig, Alt) ->
+ modify_cases_upto1(Ref, CopyOp, T, [C|Orig], [C|Alt]).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% set_io_buffering(IOHandler) -> PrevIOHandler
+%%
+%% Save info about current process (always the main process) buffering
+%% io printout messages from parallel test case processes (*and* possibly
+%% also the main process).
+
+set_io_buffering(IOHandler) ->
+ put(test_server_common_io_handler, IOHandler).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% is_io_buffered() -> true|false
+%%
+%% Test whether is being buffered.
+
+is_io_buffered() ->
+ get(test_server_common_io_handler) =/= undefined.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% queue_test_case_io(Pid, Num, Mod, Func) -> ok
+%%
+%% Save info about test case that gets its io buffered. This can
+%% be a parallel test case or it can be a test case (conf or normal)
+%% that belongs to a group nested under a parallel group. The queue
+%% is processed after io buffering is disabled. See run_test_cases_loop/4
+%% and handle_test_case_io_and_status/0 for more info.
+
+queue_test_case_io(Ref, Pid, Num, Mod, Func) ->
+ Entry = {Ref,Pid,Num,Mod,Func},
+ %% the order of the test cases is very important!
+ put(test_server_queued_io,
+ get(test_server_queued_io)++[Entry]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% wait_for_cases(Ref) -> {Ok,Skipped,Failed}
+%%
+%% At the end of a nested parallel group, we have to wait for the test
+%% cases to terminate before we can go on (since test cases never execute
+%% in parallel with the end conf case of the group). When a top level
+%% parallel group is finished, buffered io messages must be handled and
+%% this is taken care of by handle_test_case_io_and_status/0.
+
+wait_for_cases(Ref) ->
+ case get(test_server_queued_io) of
+ [] ->
+ {[],[],[]};
+ Cases ->
+ [_Start|TCs] =
+ lists:dropwhile(fun({R,_,_,_,_}) when R == Ref -> false;
+ (_) -> true
+ end, Cases),
+ wait_and_resend(Ref, TCs, [],[],[])
+ end.
+
+wait_and_resend(Ref, [{OtherRef,_,0,_,_}|Ps],
+ Ok,Skip,Fail) when is_reference(OtherRef),
+ OtherRef /= Ref ->
+ %% ignore cases that belong to nested group
+ Ps1 = rm_cases_upto(OtherRef, Ps),
+ wait_and_resend(Ref, Ps1, Ok,Skip,Fail);
+
+wait_and_resend(Ref, [{_,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) ->
+ receive
+ {finished,_Ref,CurrPid,CaseNum,Mod,Func,Result,_RetVal} = Msg ->
+ %% resend message to main process so that it can be used
+ %% to test_server_io:print_buffered/1 later
+ self() ! Msg,
+ MF = {Mod,Func},
+ {Ok1,Skip1,Fail1} =
+ case Result of
+ ok -> {[MF|Ok],Skip,Fail};
+ skipped -> {Ok,[MF|Skip],Fail};
+ failed -> {Ok,Skip,[MF|Fail]}
+ end,
+ wait_and_resend(Ref, Ps, Ok1,Skip1,Fail1);
+ {'EXIT',CurrPid,Reason} when Reason /= normal ->
+ %% unexpected termination of test case process
+ {value,{_,_,CaseNum,Mod,Func}} = lists:keysearch(CurrPid, 2, Cases),
+ print(1, "Error! Process for test case #~w (~w:~tw) died! Reason: ~tp",
+ [CaseNum, Mod, Func, Reason]),
+ exit({unexpected_termination,{CaseNum,Mod,Func},{CurrPid,Reason}})
+ end;
+
+wait_and_resend(_, [], Ok,Skip,Fail) ->
+ {lists:reverse(Ok),lists:reverse(Skip),lists:reverse(Fail)}.
+
+rm_cases_upto(Ref, [{Ref,_,0,_,_}|Ps]) ->
+ Ps;
+rm_cases_upto(Ref, [_|Ps]) ->
+ rm_cases_upto(Ref, Ps).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% handle_test_case_io_and_status() -> [Ok,Skipped,Failed}
+%%
+%% Each parallel test case process prints to its own minor log file during
+%% execution. The common log files (major, html etc) must however be
+%% written to sequentially. This is handled by calling
+%% test_server_io:start_transaction/0 to tell the test_server_io process
+%% to buffer all print requests.
+%%
+%% An io session is always started with a
+%% {started,Ref,Pid,Num,Mod,Func} message (and
+%% test_server_io:start_transaction/0 will be called) and terminated
+%% with {finished,Ref,Pid,Num,Mod,Func,Result,RetVal} (and
+%% test_server_io:end_transaction/0 will be called). The result
+%% shipped with the finished message from a parallel process is used
+%% to update status data of the current test run. An 'EXIT' message
+%% from each parallel test case process (after finishing and
+%% terminating) is also received and handled here.
+%%
+%% During execution of a parallel group, any cases (conf or normal)
+%% belonging to a nested group will also get its io printouts buffered.
+%% This is necessary to get the major and html log files written in
+%% correct sequence. This function handles also the print messages
+%% generated by nested group cases that have been executed sequentially
+%% by the main process (note that these cases do not generate 'EXIT'
+%% messages, only 'start' and 'finished' messages).
+%%
+%% See the header comment for run_test_cases_loop/4 for more
+%% info about IO handling.
+%%
+%% Note: It is important that the type of messages handled here
+%% do not get consumed by test_server:run_test_case_msgloop/5
+%% during the test case execution (e.g. in the catch clause of
+%% the receive)!
+
+handle_test_case_io_and_status() ->
+ case get(test_server_queued_io) of
+ [] ->
+ {[],[],[]};
+ Cases ->
+ %% Cases = [{Ref,Pid,CaseNum,Mod,Func} | ...]
+ Result = handle_io_and_exit_loop([], Cases, [],[],[]),
+ Main = self(),
+ %% flush normal exit messages
+ lists:foreach(fun({_,Pid,_,_,_}) when Pid /= Main ->
+ receive
+ {'EXIT',Pid,normal} -> ok
+ after
+ 1000 -> ok
+ end;
+ (_) ->
+ ok
+ end, Cases),
+ Result
+ end.
+
+%% Handle cases (without Ref) that belong to the top parallel group (i.e. when Refs = [])
+handle_io_and_exit_loop([], [{undefined,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) ->
+ %% retrieve the start message for the current io session (= testcase)
+ receive
+ {started,_,CurrPid,CaseNum,Mod,Func} ->
+ {Ok1,Skip1,Fail1} =
+ case handle_io_and_exits(self(), CurrPid, CaseNum, Mod, Func, Cases) of
+ {ok,MF} -> {[MF|Ok],Skip,Fail};
+ {skipped,MF} -> {Ok,[MF|Skip],Fail};
+ {failed,MF} -> {Ok,Skip,[MF|Fail]}
+ end,
+ handle_io_and_exit_loop([], Ps, Ok1,Skip1,Fail1)
+ after
+ 1000 ->
+ exit({testcase_failed_to_start,Mod,Func})
+ end;
+
+%% Handle cases that belong to groups nested under top parallel group
+handle_io_and_exit_loop(Refs, [{Ref,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) ->
+ receive
+ {started,_,CurrPid,CaseNum,Mod,Func} ->
+ _ = handle_io_and_exits(self(), CurrPid, CaseNum, Mod, Func, Cases),
+ Refs1 =
+ case Refs of
+ [Ref|Rs] -> % must be end conf case for subgroup
+ Rs;
+ _ when is_reference(Ref) -> % must be start of new subgroup
+ [Ref|Refs];
+ _ -> % must be normal subgroup testcase
+ Refs
+ end,
+ handle_io_and_exit_loop(Refs1, Ps, Ok,Skip,Fail)
+ after
+ 1000 ->
+ exit({testcase_failed_to_start,Mod,Func})
+ end;
+
+handle_io_and_exit_loop(_, [], Ok,Skip,Fail) ->
+ {lists:reverse(Ok),lists:reverse(Skip),lists:reverse(Fail)}.
+
+handle_io_and_exits(Main, CurrPid, CaseNum, Mod, Func, Cases) ->
+ receive
+ {abort_current_testcase=Tag,_Reason,From} ->
+ %% If a parallel group is executing, there is no unique
+ %% current test case, so we must generate an error.
+ From ! {self(),Tag,{error,parallel_group}},
+ handle_io_and_exits(Main, CurrPid, CaseNum, Mod, Func, Cases);
+ %% end of io session from test case executed by main process
+ {finished,_,Main,CaseNum,Mod,Func,Result,_RetVal} ->
+ test_server_io:print_buffered(CurrPid),
+ {Result,{Mod,Func}};
+ %% end of io session from test case executed by parallel process
+ {finished,_,CurrPid,CaseNum,Mod,Func,Result,RetVal} ->
+ test_server_io:print_buffered(CurrPid),
+ case Result of
+ ok ->
+ put(test_server_ok, get(test_server_ok)+1);
+ failed ->
+ put(test_server_failed, get(test_server_failed)+1);
+ skipped ->
+ SkipCounters =
+ update_skip_counters(RetVal, get(test_server_skipped)),
+ put(test_server_skipped, SkipCounters)
+ end,
+ {Result,{Mod,Func}};
+
+ %% unexpected termination of test case process
+ {'EXIT',TCPid,Reason} when Reason /= normal ->
+ test_server_io:print_buffered(CurrPid),
+ {value,{_,_,Num,M,F}} = lists:keysearch(TCPid, 2, Cases),
+ print(1, "Error! Process for test case #~w (~w:~tw) died! Reason: ~tp",
+ [Num, M, F, Reason]),
+ exit({unexpected_termination,{Num,M,F},{TCPid,Reason}})
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% run_test_case(Ref, Num, Mod, Func, Args, RunInit,
+%% TimetrapData, Mode) -> RetVal
+%%
+%% Creates the minor log file and inserts some test case specific headers
+%% and footers into the log files. Then the test case is executed and the
+%% result is printed to the log files (also info about lingering processes
+%% & slave nodes in the system is presented).
+%%
+%% RunInit decides if the per test case init is to be run (true for all
+%% but conf cases).
+%%
+%% Mode specifies if the test case should be executed by a dedicated,
+%% parallel, process rather than sequentially by the main process. If
+%% the former, the new process is spawned and the dictionary of the main
+%% process is copied to the test case process.
+%%
+%% RetVal is the result of executing the test case. It contains info
+%% about the execution time and the return value of the test case function.
+
+run_test_case(Ref, Num, Mod, Func, Args, RunInit, TimetrapData) ->
+ ok = file:set_cwd(filename:dirname(get(test_server_dir))),
+ run_test_case1(Ref, Num, Mod, Func, Args, RunInit,
+ TimetrapData, [], self()).
+
+run_test_case(Ref, Num, Mod, Func, Args, skip_init, TimetrapData, Mode) ->
+ %% a conf case is always executed by the main process
+ run_test_case1(Ref, Num, Mod, Func, Args, skip_init,
+ TimetrapData, Mode, self());
+
+run_test_case(Ref, Num, Mod, Func, Args, RunInit, TimetrapData, Mode) ->
+ ok = file:set_cwd(filename:dirname(get(test_server_dir))),
+ Main = self(),
+ case check_prop(parallel, Mode) of
+ false ->
+ %% this is a sequential test case
+ run_test_case1(Ref, Num, Mod, Func, Args, RunInit,
+ TimetrapData, Mode, Main);
+ _Ref ->
+ %% this a parallel test case, spawn the new process
+ Dictionary = get(),
+ {dictionary,Dictionary} = process_info(self(), dictionary),
+ spawn_link(
+ fun() ->
+ process_flag(trap_exit, true),
+ ct_util:mark_process(),
+ _ = [put(Key, Val) || {Key,Val} <- Dictionary],
+ set_io_buffering({tc,Main}),
+ run_test_case1(Ref, Num, Mod, Func, Args, RunInit,
+ TimetrapData, Mode, Main)
+ end)
+ end.
+
+run_test_case1(Ref, Num, Mod, Func, Args, RunInit,
+ TimetrapData, Mode, Main) ->
+ group_leader(test_server_io:get_gl(Main == self()), self()),
+
+ %% if io is being buffered, send start io session message
+ %% (no matter if case runs on parallel or main process)
+ case is_io_buffered() of
+ false -> ok;
+ true ->
+ test_server_io:start_transaction(),
+ Main ! {started,Ref,self(),Num,Mod,Func},
+ ok
+ end,
+ TSDir = get(test_server_dir),
+
+ print(major, "=case ~w:~tw", [Mod, Func]),
+ MinorName = start_minor_log_file(Mod, Func, self() /= Main),
+ MinorBase = filename:basename(MinorName),
+ print(major, "=logfile ~ts", [filename:basename(MinorName)]),
+
+ UpdatedArgs =
+ %% maybe create unique private directory for test case or config func
+ case get(test_server_create_priv_dir) of
+ auto_per_run ->
+ update_config(hd(Args), [{tc_logfile,MinorName}]);
+ PrivDirMode ->
+ %% create unique private directory for test case
+ RunDir = filename:dirname(MinorName),
+ Ext =
+ if Num == 0 ->
+ Int = erlang:unique_integer([positive,monotonic]),
+ lists:flatten(io_lib:format(".cfg.~w", [Int]));
+ true ->
+ lists:flatten(io_lib:format(".~w", [Num]))
+ end,
+ PrivDir = filename:join(RunDir, ?priv_dir) ++ Ext,
+ if PrivDirMode == auto_per_tc ->
+ ok = file:make_dir(PrivDir);
+ PrivDirMode == manual_per_tc ->
+ ok
+ end,
+ update_config(hd(Args), [{priv_dir,PrivDir++"/"},
+ {tc_logfile,MinorName}])
+ end,
+ GrName = get_name(Mode),
+ test_server_sup:framework_call(report,
+ [tc_start,{{Mod,{Func,GrName}},
+ MinorName}]),
+
+ {ok,Cwd} = file:get_cwd(),
+ Args2Print = if is_list(UpdatedArgs) ->
+ lists:keydelete(tc_group_result, 1, UpdatedArgs);
+ true ->
+ UpdatedArgs
+ end,
+ if RunInit == skip_init ->
+ print_props(get_props(Mode));
+ true ->
+ ok
+ end,
+
+ print(minor,
+ escape_chars(io_lib:format("Config value:\n\n ~tp\n", [Args2Print])),
+ []),
+ print(minor, "Current directory is ~tp\n", [Cwd]),
+
+ GrNameStr = case GrName of
+ undefined -> "";
+ Name -> cast_to_list(Name)
+ end,
+ print(major, "=started ~s", [lists:flatten(timestamp_get(""))]),
+ {{Col0,Col1},Style} = get_font_style((RunInit==run_init), Mode),
+ TR = xhtml("<tr valign=\"top\">", ["<tr class=\"",odd_or_even(),"\">"]),
+ EncMinorBase = uri_encode(MinorBase),
+ print(html, TR ++ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>"
+ "<td>" ++ Col0 ++ "~w" ++ Col1 ++ "</td>"
+ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>"
+ "<td><a href=\"~ts\">~tw</a></td>"
+ "<td><a href=\"~ts#top\">&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(Num, Mod, Func, [UpdatedArgs], GrName,
+ RunInit, TimetrapData),
+ {Time,RetVal,Loc,Opts,Comment} =
+ case Result of
+ Normal={_Time,_RetVal,_Loc,_Opts,_Comment} -> Normal;
+ {died,DReason,DLoc,DCmt} -> {died,DReason,DLoc,[],DCmt}
+ end,
+
+ print(minor, "<a name=\"end\"></a>", [], internal_raw),
+ print(minor, "\n", [], internal_raw),
+ print_timestamp(minor, "Ended at "),
+ print(major, "=ended ~s", [lists:flatten(timestamp_get(""))]),
+
+ do_unless_parallel(Main, fun() -> file:set_cwd(filename:dirname(TSDir)) end),
+
+ %% call the appropriate progress function clause to print the results to log
+ Status =
+ case {Time,RetVal} of
+ {died,{timetrap_timeout,TimetrapTimeout}} ->
+ progress(failed, Num, Mod, Func, GrName, Loc,
+ timetrap_timeout, TimetrapTimeout, Comment, Style);
+ {died,{Skip,Reason}} when Skip==skip; Skip==skipped ->
+ %% died in init_per_testcase
+ progress(skip, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {died,Reason} when Reason=/=ok ->
+ %% (If Reason==ok it means that process died in
+ %% end_per_testcase after successfully completing the
+ %% test case itself - then we shall not fail, but a
+ %% warning will be issued in the comment field.)
+ progress(failed, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {_,{'EXIT',{Skip,Reason}}} when Skip==skip; Skip==skipped;
+ Skip==auto_skip ->
+ progress(skip, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {_,{'EXIT',_Pid,{Skip,Reason}}} when Skip==skip; Skip==skipped ->
+ progress(skip, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {_,{'EXIT',_Pid,Reason}} ->
+ progress(failed, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {_,{'EXIT',Reason}} ->
+ progress(failed, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {_,{Fail,Reason}} when Fail =:= fail; Fail =:= failed ->
+ progress(failed, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {_,Reason={auto_skip,_Why}} ->
+ progress(skip, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {_,{Skip,Reason}} when Skip==skip; Skip==skipped ->
+ progress(skip, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style);
+ {Time,RetVal} ->
+ case DetectedFail of
+ [] ->
+ progress(ok, Num, Mod, Func, GrName, Loc, RetVal,
+ Time, Comment, Style);
+
+ Reason ->
+ progress(failed, Num, Mod, Func, GrName, Loc, Reason,
+ Time, Comment, Style)
+ end
+ end,
+ %% if the test case was executed sequentially, this updates the
+ %% status count on the main process (status of parallel test cases
+ %% is updated later by the handle_test_case_io_and_status/0 function)
+ case {RunInit,Status} of
+ {skip_init,_} -> % conf doesn't count
+ ok;
+ {_,ok} ->
+ put(test_server_ok, get(test_server_ok)+1);
+ {_,failed} ->
+ put(test_server_failed, get(test_server_failed)+1);
+ {_,skip} ->
+ {US,AS} = get(test_server_skipped),
+ put(test_server_skipped, {US+1,AS});
+ {_,auto_skip} ->
+ {US,AS} = get(test_server_skipped),
+ put(test_server_skipped, {US,AS+1})
+ end,
+ %% only if test case execution is sequential do we care about the
+ %% remaining processes and slave nodes count
+ case self() of
+ Main ->
+ case test_server_sup:framework_call(warn, [processes], true) of
+ true ->
+ if ProcsBefore < ProcsAfter ->
+ print(minor,
+ "WARNING: ~w more processes in system after test case",
+ [ProcsAfter-ProcsBefore]);
+ ProcsBefore > ProcsAfter ->
+ print(minor,
+ "WARNING: ~w less processes in system after test case",
+ [ProcsBefore-ProcsAfter]);
+ true -> ok
+ end;
+ false ->
+ ok
+ end,
+ case test_server_sup:framework_call(warn, [nodes], true) of
+ true ->
+ case catch controller_call(kill_slavenodes) of
+ {'EXIT',_} = Exit ->
+ print(minor,
+ "WARNING: There might be slavenodes left in the"
+ " system. I tried to kill them, but I failed: ~tp\n",
+ [Exit]);
+ [] -> ok;
+ List ->
+ print(minor, "WARNING: ~w slave nodes in system after test"++
+ "case. Tried to killed them.~n"++
+ " Names:~tp",
+ [length(List),List])
+ end;
+ false ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ %% if the test case was executed sequentially, this updates the execution
+ %% time count on the main process (adding execution time of parallel test
+ %% case groups is done in run_test_cases_loop/4)
+ if is_number(Time) ->
+ put(test_server_total_time, get(test_server_total_time)+Time);
+ true ->
+ ok
+ end,
+ test_server_sup:check_new_crash_dumps(),
+
+ %% if io is being buffered, send finished message
+ %% (no matter if case runs on parallel or main process)
+ case is_io_buffered() of
+ false ->
+ ok;
+ true ->
+ test_server_io:end_transaction(),
+ Main ! {finished,Ref,self(),Num,Mod,Func,
+ ?mod_result(Status),{Time,RetVal,Opts}},
+ ok
+ end,
+ {Time,RetVal,Opts}.
+
+
+%%--------------------------------------------------------------------
+%% various help functions
+
+%% Call Action if we are running on the main process (not parallel).
+do_unless_parallel(Main, Action) when is_function(Action, 0) ->
+ case self() of
+ Main -> Action();
+ _ -> ok
+ end.
+
+num2str(0) -> "";
+num2str(N) -> integer_to_list(N).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% progress(Result, CaseNum, Mod, Func, Location, Reason, Time,
+%% Comment, TimeFormat) -> Result
+%%
+%% Prints the result of the test case to log file.
+%% Note: Strings that are to be written to the minor log must
+%% be prefixed with "=== " here, or the indentation will be wrong.
+
+progress(skip, CaseNum, Mod, Func, GrName, Loc, Reason, Time,
+ Comment, {St0,St1}) ->
+ {Reason1,{Color,Ret,ReportTag}} =
+ if_auto_skip(Reason,
+ fun() -> {?auto_skip_color,auto_skip,auto_skipped} end,
+ fun() -> {?user_skip_color,skip,skipped} end),
+ print(major, "=result ~w: ~tp", [ReportTag,Reason1]),
+ print(1, "*** SKIPPED ~ts ***",
+ [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]),
+ test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName},
+ {ReportTag,Reason1}}]),
+ TimeStr = io_lib:format(if is_float(Time) -> "~.3fs";
+ true -> "~w"
+ end, [Time]),
+ ReasonStr = escape_chars(reason_to_string(Reason1)),
+ ReasonStr1 = lists:flatten([string:trim(S,leading,"\s") ||
+ S <- string:lexemes(ReasonStr,[$\n])]),
+ ReasonLength = string:length(ReasonStr1),
+ ReasonStr2 =
+ if ReasonLength > 80 ->
+ string:slice(ReasonStr1, 0, 77) ++ "...";
+ true ->
+ ReasonStr1
+ end,
+ Comment1 = case Comment of
+ "" -> "";
+ _ -> xhtml("<br>(","<br />(") ++ to_string(Comment) ++ ")"
+ end,
+ print(html,
+ "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>"
+ "<td><font color=\"~ts\">SKIPPED</font></td>"
+ "<td>~ts~ts</td></tr>\n",
+ [TimeStr,Color,ReasonStr2,Comment1]),
+ FormatLoc = test_server_sup:format_loc(Loc),
+ print(minor, "=== Location: ~ts", [FormatLoc]),
+ print(minor, "=== Reason: ~ts", [ReasonStr1]),
+ Ret;
+
+progress(failed, CaseNum, Mod, Func, GrName, Loc, timetrap_timeout, T,
+ Comment0, {St0,St1}) ->
+ print(major, "=result failed: timeout, ~tp", [Loc]),
+ print(1, "*** FAILED ~ts ***",
+ [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]),
+ test_server_sup:framework_call(report,
+ [tc_done,{Mod,{Func,GrName},
+ {failed,timetrap_timeout}}]),
+ FormatLastLoc = test_server_sup:format_loc(get_last_loc(Loc)),
+ ErrorReason = io_lib:format("{timetrap_timeout,~ts}", [FormatLastLoc]),
+ Comment =
+ case Comment0 of
+ "" -> "<font color=\"red\">" ++ ErrorReason ++ "</font>";
+ _ -> "<font color=\"red\">" ++ ErrorReason ++
+ xhtml("</font><br>","</font><br />") ++ to_string(Comment0)
+ end,
+ print(html,
+ "<td>" ++ St0 ++ "~.3fs" ++ St1 ++ "</td>"
+ "<td><font color=\"red\">FAILED</font></td>"
+ "<td>~ts</td></tr>\n",
+ [T/1000,Comment]),
+ FormatLoc = test_server_sup:format_loc(Loc),
+ print(minor, "=== Location: ~ts", [FormatLoc]),
+ print(minor, "=== Reason: timetrap timeout", []),
+ failed;
+
+progress(failed, CaseNum, Mod, Func, GrName, Loc, {testcase_aborted,Reason}, _T,
+ Comment0, {St0,St1}) ->
+ print(major, "=result failed: testcase_aborted, ~tp", [Loc]),
+ print(1, "*** FAILED ~ts ***",
+ [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]),
+ test_server_sup:framework_call(report,
+ [tc_done,{Mod,{Func,GrName},
+ {failed,testcase_aborted}}]),
+ FormatLastLoc = test_server_sup:format_loc(get_last_loc(Loc)),
+ ErrorReason = io_lib:format("{testcase_aborted,~ts}", [FormatLastLoc]),
+ Comment =
+ case Comment0 of
+ "" -> "<font color=\"red\">" ++ ErrorReason ++ "</font>";
+ _ -> "<font color=\"red\">" ++ ErrorReason ++
+ xhtml("</font><br>","</font><br />") ++ to_string(Comment0)
+ end,
+ print(html,
+ "<td>" ++ St0 ++ "died" ++ St1 ++ "</td>"
+ "<td><font color=\"red\">FAILED</font></td>"
+ "<td>~ts</td></tr>\n",
+ [Comment]),
+ FormatLoc = test_server_sup:format_loc(Loc),
+ print(minor, "=== Location: ~ts", [FormatLoc]),
+ print(minor,
+ escape_chars(io_lib:format("=== Reason: {testcase_aborted,~tp}",
+ [Reason])),
+ []),
+ failed;
+
+progress(failed, CaseNum, Mod, Func, GrName, unknown, Reason, Time,
+ Comment0, {St0,St1}) ->
+ print(major, "=result failed: ~tp, ~w", [Reason,unknown_location]),
+ print(1, "*** FAILED ~ts ***",
+ [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]),
+ test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName},
+ {failed,Reason}}]),
+ TimeStr = io_lib:format(if is_float(Time) -> "~.3fs";
+ true -> "~w"
+ end, [Time]),
+ ErrorReason = escape_chars(lists:flatten(io_lib:format("~tp", [Reason]))),
+ ErrorReason1 = lists:flatten([string:trim(S,leading,"\s") ||
+ S <- string:lexemes(ErrorReason,[$\n])]),
+ ErrorReasonLength = string:length(ErrorReason1),
+ ErrorReason2 =
+ if ErrorReasonLength > 63 ->
+ string:slice(ErrorReason1, 0, 60) ++ "...";
+ true ->
+ ErrorReason1
+ end,
+ Comment =
+ case Comment0 of
+ "" -> "<font color=\"red\">" ++ ErrorReason2 ++ "</font>";
+ _ -> "<font color=\"red\">" ++ ErrorReason2 ++
+ xhtml("</font><br>","</font><br />") ++
+ to_string(Comment0)
+ end,
+ print(html,
+ "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>"
+ "<td><font color=\"red\">FAILED</font></td>"
+ "<td>~ts</td></tr>\n",
+ [TimeStr,Comment]),
+ print(minor, "=== Location: ~w", [unknown]),
+ {FStr,FormattedReason} = format_exception(Reason),
+ print(minor,
+ escape_chars(io_lib:format("=== Reason: " ++ FStr, [FormattedReason])),
+ []),
+ failed;
+
+progress(failed, CaseNum, Mod, Func, GrName, Loc, Reason, Time,
+ Comment0, {St0,St1}) ->
+ {LocMaj,LocMin} = if Func == error_in_suite ->
+ case get_fw_mod(undefined) of
+ Mod -> {unknown_location,unknown};
+ _ -> {Loc,Loc}
+ end;
+ true -> {Loc,Loc}
+ end,
+ print(major, "=result failed: ~tp, ~tp", [Reason,LocMaj]),
+ print(1, "*** FAILED ~ts ***",
+ [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]),
+ test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName},
+ {failed,Reason}}]),
+ TimeStr = io_lib:format(if is_float(Time) -> "~.3fs";
+ true -> "~w"
+ end, [Time]),
+ Comment =
+ case Comment0 of
+ "" -> "";
+ _ -> xhtml("<br>","<br />") ++ to_string(Comment0)
+ end,
+ FormatLastLoc = test_server_sup:format_loc(get_last_loc(LocMaj)),
+ print(html,
+ "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>"
+ "<td><font color=\"red\">FAILED</font></td>"
+ "<td><font color=\"red\">~ts</font>~ts</td></tr>\n",
+ [TimeStr,FormatLastLoc,Comment]),
+ FormatLoc = test_server_sup:format_loc(LocMin),
+ print(minor, "=== Location: ~ts", [FormatLoc]),
+ {FStr,FormattedReason} = format_exception(Reason),
+ print(minor, "=== Reason: " ++
+ escape_chars(io_lib:format(FStr, [FormattedReason])), []),
+ failed;
+
+progress(ok, _CaseNum, Mod, Func, GrName, _Loc, RetVal, Time,
+ Comment0, {St0,St1}) ->
+ print(minor, "successfully completed test case", []),
+ test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName},ok}]),
+ TimeStr = io_lib:format(if is_float(Time) -> "~.3fs";
+ true -> "~w"
+ end, [Time]),
+ Comment =
+ case RetVal of
+ {comment,RetComment} ->
+ String = to_string(RetComment),
+ HtmlCmt = test_server_sup:framework_call(format_comment,
+ [String],
+ String),
+ print(major, "=result ok: ~ts", [String]),
+ "<td>" ++ HtmlCmt ++ "</td>";
+ _ ->
+ print(major, "=result ok", []),
+ case Comment0 of
+ "" -> "<td></td>";
+ _ -> "<td>" ++ to_string(Comment0) ++ "</td>"
+ end
+ end,
+ print(major, "=elapsed ~p", [Time]),
+ print(html,
+ "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>"
+ "<td><font color=\"green\">Ok</font></td>"
+ "~ts</tr>\n",
+ [TimeStr,Comment]),
+ print(minor,
+ escape_chars(io_lib:format("=== Returned value: ~tp", [RetVal])),
+ []),
+ ok.
+
+%%--------------------------------------------------------------------
+%% various help functions
+escape_chars(Term) when not is_list(Term), not is_binary(Term) ->
+ esc_chars_in_list(io_lib:format("~tp", [Term]));
+escape_chars(List = [Term | _]) when not is_list(Term), not is_integer(Term) ->
+ esc_chars_in_list(io_lib:format("~tp", [List]));
+escape_chars(List) ->
+ esc_chars_in_list(List).
+
+esc_chars_in_list([Bin | Io]) when is_binary(Bin) ->
+ [Bin | esc_chars_in_list(Io)];
+esc_chars_in_list([List | Io]) when is_list(List) ->
+ [esc_chars_in_list(List) | esc_chars_in_list(Io)];
+esc_chars_in_list([$< | Io]) ->
+ ["&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("~tw", [{Mod,Func}]);
+get_info_str(_Mod,_Func, CaseNum, unknown) ->
+ "test case " ++ integer_to_list(CaseNum);
+get_info_str(_Mod,_Func, CaseNum, Cases) ->
+ "test case " ++ integer_to_list(CaseNum) ++
+ " of " ++ integer_to_list(Cases).
+
+print_if_known(Known, {SK,AK}, {SU,AU}) ->
+ {S,A} = if Known == unknown -> {SU,AU};
+ true -> {SK,AK}
+ end,
+ io_lib:format(S, A).
+
+to_string(Term) when is_list(Term) ->
+ case (catch io_lib:format("~ts", [Term])) of
+ {'EXIT',_} -> lists:flatten(io_lib:format("~tp", [Term]));
+ String -> lists:flatten(String)
+ end;
+to_string(Term) ->
+ lists:flatten(io_lib:format("~tp", [Term])).
+
+get_last_loc(Loc) when is_tuple(Loc) ->
+ Loc;
+get_last_loc([Loc|_]) when is_tuple(Loc) ->
+ [Loc];
+get_last_loc(Loc) ->
+ Loc.
+
+reason_to_string({failed,{_,FailFunc,bad_return}}) ->
+ atom_to_list(FailFunc) ++ " bad return value";
+reason_to_string({failed,{_,FailFunc,{timetrap_timeout,_}}}) ->
+ atom_to_list(FailFunc) ++ " timed out";
+reason_to_string(FWInitFail = {failed,{_CB,init_tc,_Reason}}) ->
+ to_string(FWInitFail);
+reason_to_string({failed,{_,FailFunc,_}}) ->
+ atom_to_list(FailFunc) ++ " failed";
+reason_to_string(Other) ->
+ to_string(Other).
+
+%get_font_style(Prop) ->
+% {Col,St0,St1} = get_font_style1(Prop),
+% {{"<font color="++Col++">","</font>"},
+% {"<font color="++Col++">"++St0,St1++"</font>"}}.
+
+get_font_style(NormalCase, Mode) ->
+ Prop = if not NormalCase ->
+ default;
+ true ->
+ case check_prop(parallel, Mode) of
+ false ->
+ case check_prop(sequence, Mode) of
+ false ->
+ default;
+ _ ->
+ sequence
+ end;
+ _ ->
+ parallel
+ end
+ end,
+ {Col,St0,St1} = get_font_style1(Prop),
+ {{"<font color="++Col++">","</font>"},
+ {"<font color="++Col++">"++St0,St1++"</font>"}}.
+
+get_font_style1(parallel) ->
+ {"\"darkslategray\"","<i>","</i>"};
+get_font_style1(sequence) ->
+% {"\"darkolivegreen\"","",""};
+ {"\"saddlebrown\"","",""};
+get_font_style1(default) ->
+ {"\"black\"","",""}.
+%%get_font_style1(skipped) ->
+%% {"\"lightgray\"","",""}.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% format_exception({Error,Stack}) -> {CtrlSeq,Term}
+%%
+%% The default behaviour is that error information gets formatted
+%% (like in the erlang shell) before printed to the minor log file.
+%% The framework application can switch this feature off by setting
+%% *its* application environment variable 'format_exception' to false.
+%% It is also possible to switch formatting off by starting the
+%% test_server node with init argument 'test_server_format_exception'
+%% set to false.
+
+format_exception(Reason={_Error,Stack}) when is_list(Stack) ->
+ case get_fw_mod(undefined) of
+ undefined ->
+ case application:get_env(test_server, format_exception) of
+ {ok,false} ->
+ {"~tp",Reason};
+ _ ->
+ do_format_exception(Reason)
+ end;
+ FW ->
+ case application:get_env(FW, format_exception) of
+ {ok,false} ->
+ {"~tp",Reason};
+ _ ->
+ do_format_exception(Reason)
+ end
+ end;
+format_exception(Error) ->
+ format_exception({Error,[]}).
+
+do_format_exception(Reason={Error,Stack}) ->
+ StackFun = fun(_, _, _) -> false end,
+ PF = fun(Term, I) ->
+ io_lib:format("~." ++ integer_to_list(I) ++ "tp", [Term])
+ end,
+ case catch erl_error:format_exception(1, error, Error, Stack, StackFun, PF, utf8) of
+ {'EXIT',_R} ->
+ {"~tp",Reason};
+ Formatted ->
+ Formatted1 = re:replace(Formatted, "exception error: ", "", [{return,list},unicode]),
+ {"~ts",lists:flatten(Formatted1)}
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% run_test_case_apply(CaseNum, Mod, Func, Args, Name, RunInit,
+%% TimetrapData) ->
+%% {{Time,RetVal,Loc,Opts,Comment},DetectedFail,ProcessesBefore,ProcessesAfter} |
+%% {{died,Reason,unknown,Comment},DetectedFail,ProcessesBefore,ProcessesAfter}
+%% Name = atom()
+%% Time = float() (seconds)
+%% RetVal = term()
+%% Loc = term()
+%% Comment = string()
+%% Reason = term()
+%% DetectedFail = [{File,Line}]
+%% ProcessesBefore = ProcessesAfter = integer()
+%%
+
+run_test_case_apply(CaseNum, Mod, Func, Args, Name, RunInit,
+ TimetrapData) ->
+ test_server:run_test_case_apply({CaseNum,Mod,Func,Args,Name,RunInit,
+ TimetrapData}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% print(Detail, Format, Args) -> ok
+%% Detail = integer()
+%% Format = string()
+%% Args = [term()]
+%%
+%% Just like io:format, except that depending on the Detail value, the output
+%% is directed to console, major and/or minor log files.
+
+print(Detail, Format) ->
+ print(Detail, Format, []).
+
+print(Detail, Format, Args) ->
+ print(Detail, Format, Args, internal).
+
+print(Detail, ["$tc_html",Format], Args, Printer) ->
+ Msg = io_lib:format(Format, Args),
+ print_or_buffer(Detail, ["$tc_html",Msg], Printer);
+
+print(Detail, Format, Args, Printer) ->
+ Msg = io_lib:format(Format, Args),
+ print_or_buffer(Detail, Msg, Printer).
+
+print_or_buffer(Detail, Msg, Printer) ->
+ test_server_gl:print(group_leader(), Detail, Msg, Printer).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% print_timestamp(Detail, Leader) -> ok
+%%
+%% Prints Leader followed by a time stamp (date and time). Depending on
+%% the Detail value, the output is directed to console, major and/or minor
+%% log files.
+
+print_timestamp(Detail, Leader) ->
+ print(Detail, timestamp_get(Leader), []).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% print_who(Host, User) -> ok
+%%
+%% Logs who runs the suite.
+
+print_who(Host, User) ->
+ UserStr = case User of
+ "" -> "";
+ _ -> " by " ++ User
+ end,
+ print(html, "Run~ts on ~ts", [UserStr,Host]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% format(Format) -> IoLibReturn
+%% format(Detail, Format) -> IoLibReturn
+%% format(Format, Args) -> IoLibReturn
+%% format(Detail, Format, Args) -> IoLibReturn
+%%
+%% Detail = integer()
+%% Format = string()
+%% Args = [term(),...]
+%% IoLibReturn = term()
+%%
+%% Logs the Format string and Args, similar to io:format/1/2 etc. If
+%% Detail is not specified, the default detail level (which is 50) is used.
+%% Which log files the string will be logged in depends on the thresholds
+%% set with set_levels/3. Typically with default detail level, only the
+%% minor log file is used.
+
+format(Format) ->
+ format(minor, Format, []).
+
+format(major, Format) ->
+ format(major, Format, []);
+format(minor, Format) ->
+ format(minor, Format, []);
+format(Detail, Format) when is_integer(Detail) ->
+ format(Detail, Format, []);
+format(Format, Args) ->
+ format(minor, Format, Args).
+
+format(Detail, Format, Args) ->
+ Str =
+ case catch io_lib:format(Format, Args) of
+ {'EXIT',_} ->
+ io_lib:format("illegal format; ~tp with args ~tp.\n",
+ [Format,Args]);
+ Valid -> Valid
+ end,
+ print_or_buffer(Detail, Str, self()).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% xhtml(BasicHtml, XHtml) -> BasicHtml | XHtml
+%%
+xhtml(HTML, XHTML) ->
+ case get(basic_html) of
+ true -> HTML;
+ _ -> XHTML
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% odd_or_even() -> "odd" | "even"
+%%
+odd_or_even() ->
+ case get(odd_or_even) of
+ even ->
+ put(odd_or_even, odd),
+ "even";
+ _ ->
+ put(odd_or_even, even),
+ "odd"
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timestamp_filename_get(Leader) -> string()
+%% Leader = string()
+%%
+%% Returns a string consisting of Leader concatenated with the current
+%% date and time. The resulting string is suitable as a filename.
+timestamp_filename_get(Leader) ->
+ timestamp_get_internal(Leader,
+ "~ts~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w").
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timestamp_get(Leader) -> string()
+%% Leader = string()
+%%
+%% Returns a string consisting of Leader concatenated with the current
+%% date and time. The resulting string is suitable for display.
+timestamp_get(Leader) ->
+ timestamp_get_internal(Leader,
+ "~ts~w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w").
+
+timestamp_get_internal(Leader, Format) ->
+ {YY,MM,DD,H,M,S} = time_get(),
+ io_lib:format(Format, [Leader,YY,MM,DD,H,M,S]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% time_get() -> {YY,MM,DD,H,M,S}
+%% YY = integer()
+%% MM = integer()
+%% DD = integer()
+%% H = integer()
+%% M = integer()
+%% S = integer()
+%%
+%% Returns the current Year,Month,Day,Hours,Minutes,Seconds.
+%% The function checks that the date doesn't wrap while calling
+%% getting the time.
+time_get() ->
+ {YY,MM,DD} = date(),
+ {H,M,S} = time(),
+ case date() of
+ {YY,MM,DD} ->
+ {YY,MM,DD,H,M,S};
+ _NewDay ->
+ %% date changed between call to date() and time(), try again
+ time_get()
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% make_config(Config) -> NewConfig
+%% Config = [{Key,Value},...]
+%% NewConfig = [{Key,Value},...]
+%%
+%% Creates a configuration list (currently returns it's input)
+
+make_config(Initial) ->
+ Initial.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% update_config(Config, Update) -> NewConfig
+%% Config = [{Key,Value},...]
+%% Update = [{Key,Value},...] | {Key,Value}
+%% NewConfig = [{Key,Value},...]
+%%
+%% Adds or replaces the key-value pairs in config with those in update.
+%% Returns the updated list.
+
+update_config(Config, {Key,Val}) ->
+ case lists:keymember(Key, 1, Config) of
+ true ->
+ lists:keyreplace(Key, 1, Config, {Key,Val});
+ false ->
+ [{Key,Val}|Config]
+ end;
+update_config(Config, [Assoc|Assocs]) ->
+ NewConfig = update_config(Config, Assoc),
+ update_config(NewConfig, Assocs);
+update_config(Config, []) ->
+ Config.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% collect_cases(CurMod, TopCase, SkipList) ->
+%% BasicCaseList | {error,Reason}
+%%
+%% CurMod = atom()
+%% TopCase = term()
+%% SkipList = [term(),...]
+%% BasicCaseList = [term(),...]
+%%
+%% Parses the given test goal(s) in TopCase, and transforms them to a
+%% simple list of test cases to call, when executing the test suite.
+%%
+%% CurMod is the "current" module, that is, the module the last instruction
+%% was read from. May be be set to 'none' initially.
+%%
+%% SkipList is the list of test cases to skip and requirements to deny.
+%%
+%% The BasicCaseList is built out of TopCase, which may be any of the
+%% following terms:
+%%
+%% [] Nothing is added
+%% List list() The list is decomposed, and each element is
+%% treated according to this table
+%% Case atom() CurMod:Case(suite) is called
+%% {module,Case} CurMod:Case(suite) is called
+%% {Module,Case} Module:Case(suite) is called
+%% {module,Module,Case} Module:Case(suite) is called
+%% {module,Module,Case,Args} Module:Case is called with Args as arguments
+%% {dir,Dir} All modules *_SUITE in the named directory
+%% are listed, and each Module:all(suite) is called
+%% {dir,Dir,Pattern} All modules <Pattern>_SUITE in the named dir
+%% are listed, and each Module:all(suite) is called
+%% {conf,InitMF,Cases,FinMF}
+%% {conf,Props,InitMF,Cases,FinMF}
+%% InitMF is placed in the BasicCaseList, then
+%% Cases is treated according to this table, then
+%% FinMF is placed in the BasicCaseList. InitMF
+%% and FinMF are configuration manipulation
+%% functions. See below.
+%% {make,InitMFA,Cases,FinMFA}
+%% InitMFA is placed in the BasicCaseList, then
+%% Cases is treated according to this table, then
+%% FinMFA is placed in the BasicCaseList. InitMFA
+%% and FinMFA are make/unmake functions. If InitMFA
+%% fails, Cases are not run.
+%%
+%% When a function is called, above, it means that the function is invoked
+%% and the return is expected to be:
+%%
+%% [] Leaf case
+%% {req,ReqList} Kept for backwards compatibility - same as []
+%% {req,ReqList,Cases} Kept for backwards compatibility -
+%% Cases parsed recursively with collect_cases/3
+%% Cases (list) Recursively parsed with collect_cases/3
+%%
+%% Leaf cases are added to the BasicCaseList as Module:Case(Config). Each
+%% case is checked against the SkipList. If present, a skip instruction
+%% is inserted instead, which only prints the case name and the reason
+%% why the case was skipped in the log files.
+%%
+%% Configuration manipulation functions are called with the current
+%% configuration list as only argument, and are expected to return a new
+%% configuration list. Such a pair of function may, for example, start a
+%% server and stop it after a serie of test cases.
+%%
+%% SkipCases is expected to be in the format:
+%%
+%% Other Recursively parsed with collect_cases/3
+%% {Mod,Comment} Skip Mod, with Comment
+%% {Mod,Funcs,Comment} Skip listed functions in Mod with Comment
+%% {Mod,Func,Comment} Skip named function in Mod with Comment
+%%
+-record(cc, {mod, % current module
+ skip}). % skip list
+
+collect_all_cases(Top, Skip) when is_list(Skip) ->
+ Result =
+ case collect_cases(Top, #cc{mod=[],skip=Skip}, []) of
+ {ok,Cases,_St} -> Cases;
+ Other -> Other
+ end,
+ Result.
+
+
+collect_cases([], St, _) -> {ok,[],St};
+collect_cases([Case|Cs0], St0, Mode) ->
+ case collect_cases(Case, St0, Mode) of
+ {ok,FlatCases1,St1} ->
+ case collect_cases(Cs0, St1, Mode) of
+ {ok,FlatCases2,St} ->
+ {ok,FlatCases1 ++ FlatCases2,St};
+ {error,_Reason} = Error -> Error
+ end;
+ {error,_Reason} = Error -> Error
+ end;
+
+
+collect_cases({module,Case}, St, Mode) when is_atom(Case), is_atom(St#cc.mod) ->
+ collect_case({St#cc.mod,Case}, St, Mode);
+collect_cases({module,Mod,Case}, St, Mode) ->
+ collect_case({Mod,Case}, St, Mode);
+collect_cases({module,Mod,Case,Args}, St, Mode) ->
+ collect_case({Mod,Case,Args}, St, Mode);
+
+collect_cases({dir,SubDir}, St, Mode) ->
+ collect_files(SubDir, "*_SUITE", St, Mode);
+collect_cases({dir,SubDir,Pattern}, St, Mode) ->
+ collect_files(SubDir, Pattern++"*", St, Mode);
+
+collect_cases({conf,InitF,CaseList,FinMF}, St, Mode) when is_atom(InitF) ->
+ collect_cases({conf,[],{St#cc.mod,InitF},CaseList,FinMF}, St, Mode);
+collect_cases({conf,InitMF,CaseList,FinF}, St, Mode) when is_atom(FinF) ->
+ collect_cases({conf,[],InitMF,CaseList,{St#cc.mod,FinF}}, St, Mode);
+collect_cases({conf,InitMF,CaseList,FinMF}, St0, Mode) ->
+ collect_cases({conf,[],InitMF,CaseList,FinMF}, St0, Mode);
+collect_cases({conf,Props,InitF,CaseList,FinMF}, St, Mode) when is_atom(InitF) ->
+ case init_props(Props) of
+ {error,_} ->
+ {ok,[],St};
+ Props1 ->
+ collect_cases({conf,Props1,{St#cc.mod,InitF},CaseList,FinMF},
+ St, Mode)
+ end;
+collect_cases({conf,Props,InitMF,CaseList,FinF}, St, Mode) when is_atom(FinF) ->
+ case init_props(Props) of
+ {error,_} ->
+ {ok,[],St};
+ Props1 ->
+ collect_cases({conf,Props1,InitMF,CaseList,{St#cc.mod,FinF}},
+ St, Mode)
+ end;
+collect_cases({conf,Props,InitMF,CaseList,FinMF} = Conf, St, Mode) ->
+ case init_props(Props) of
+ {error,_} ->
+ {ok,[],St};
+ Props1 ->
+ Ref = make_ref(),
+ Skips = St#cc.skip,
+ Props2 = [{suite,St#cc.mod} | lists:delete(suite,Props1)],
+ Mode1 = [{Ref,Props2,undefined} | Mode],
+ case in_skip_list({St#cc.mod,Conf}, Skips) of
+ {true,Comment} -> % conf init skipped
+ {ok,[{skip_case,{conf,Ref,InitMF,Comment},Mode1} |
+ [] ++ [{conf,Ref,[],FinMF}]],St};
+ {true,Name,Comment} when is_atom(Name) -> % all cases skipped
+ case collect_cases(CaseList, St, Mode1) of
+ {ok,[],_St} = Empty ->
+ Empty;
+ {ok,FlatCases,St1} ->
+ Cases2Skip = FlatCases ++ [{conf,Ref,
+ keep_name(Props1),
+ FinMF}],
+ Skipped = skip_cases_upto(Ref, Cases2Skip, Comment,
+ conf, Mode1, skip_case),
+ {ok,[{skip_case,{conf,Ref,InitMF,Comment},Mode1} |
+ Skipped],St1};
+ {error,_Reason} = Error ->
+ Error
+ end;
+ {true,ToSkip,_} when is_list(ToSkip) -> % some cases skipped
+ case collect_cases(CaseList,
+ St#cc{skip=ToSkip++Skips}, Mode1) of
+ {ok,[],_St} = Empty ->
+ Empty;
+ {ok,FlatCases,St1} ->
+ {ok,[{conf,Ref,Props1,InitMF} |
+ FlatCases ++ [{conf,Ref,
+ keep_name(Props1),
+ FinMF}]],St1#cc{skip=Skips}};
+ {error,_Reason} = Error ->
+ Error
+ end;
+ false ->
+ case collect_cases(CaseList, St, Mode1) of
+ {ok,[],_St} = Empty ->
+ Empty;
+ {ok,FlatCases,St1} ->
+ {ok,[{conf,Ref,Props1,InitMF} |
+ FlatCases ++ [{conf,Ref,
+ keep_name(Props1),
+ FinMF}]],St1};
+ {error,_Reason} = Error ->
+ Error
+ end
+ end
+ end;
+
+collect_cases({make,InitMFA,CaseList,FinMFA}, St0, Mode) ->
+ case collect_cases(CaseList, St0, Mode) of
+ {ok,[],_St} = Empty -> Empty;
+ {ok,FlatCases,St} ->
+ Ref = make_ref(),
+ {ok,[{make,Ref,InitMFA}|FlatCases ++
+ [{make,Ref,FinMFA}]],St};
+ {error,_Reason} = Error -> Error
+ end;
+
+collect_cases({Module, Cases}, St, Mode) when is_list(Cases) ->
+ case (catch collect_case(Cases, St#cc{mod=Module}, [], Mode)) of
+ Result = {ok,_,_} ->
+ Result;
+ Other ->
+ {error,Other}
+ end;
+
+collect_cases({_Mod,_Case}=Spec, St, Mode) ->
+ collect_case(Spec, St, Mode);
+
+collect_cases({_Mod,_Case,_Args}=Spec, St, Mode) ->
+ collect_case(Spec, St, Mode);
+collect_cases(Case, St, Mode) when is_atom(Case), is_atom(St#cc.mod) ->
+ collect_case({St#cc.mod,Case}, St, Mode);
+collect_cases(Other, St, _Mode) ->
+ {error,{bad_subtest_spec,St#cc.mod,Other}}.
+
+collect_case({Mod,{conf,_,_,_,_}=Conf}, St, Mode) ->
+ collect_case_invoke(Mod, Conf, [], St, Mode);
+
+collect_case(MFA, St, Mode) ->
+ case in_skip_list(MFA, St#cc.skip) of
+ {true,Comment} when Comment /= make_failed ->
+ {ok,[{skip_case,{MFA,Comment},Mode}],St};
+ _ ->
+ case MFA of
+ {Mod,Case} -> collect_case_invoke(Mod, Case, MFA, St, Mode);
+ {_Mod,_Case,_Args} -> {ok,[MFA],St}
+ end
+ end.
+
+collect_case([], St, Acc, _Mode) ->
+ {ok, Acc, St};
+
+collect_case([Case | Cases], St, Acc, Mode) ->
+ {ok, FlatCases, NewSt} = collect_case({St#cc.mod, Case}, St, Mode),
+ collect_case(Cases, NewSt, Acc ++ FlatCases, Mode).
+
+collect_case_invoke(Mod, Case, MFA, St, Mode) ->
+ case get_fw_mod(undefined) of
+ undefined ->
+ case catch apply(Mod, Case, [suite]) of
+ {'EXIT',_} ->
+ {ok,[MFA],St};
+ Suite ->
+ collect_subcases(Mod, Case, MFA, St, Suite, Mode)
+ end;
+ _ ->
+ Suite = test_server_sup:framework_call(get_suite,
+ [Mod,Case],
+ []),
+ collect_subcases(Mod, Case, MFA, St, Suite, Mode)
+ end.
+
+collect_subcases(Mod, Case, MFA, St, Suite, Mode) ->
+ case Suite of
+ [] when Case == all -> {ok,[],St};
+ [] when element(1, Case) == conf -> {ok,[],St};
+ [] -> {ok,[MFA],St};
+%%%! --- START Kept for backwards compatibility ---
+%%%! Requirements are not used
+ {req,ReqList} ->
+ collect_case_deny(Mod, Case, MFA, ReqList, [], St, Mode);
+ {req,ReqList,SubCases} ->
+ collect_case_deny(Mod, Case, MFA, ReqList, SubCases, St, Mode);
+%%%! --- END Kept for backwards compatibility ---
+ {Skip,Reason} when Skip==skip; Skip==skipped ->
+ {ok,[{skip_case,{MFA,Reason},Mode}],St};
+ {error,Reason} ->
+ throw(Reason);
+ SubCases ->
+ collect_case_subcases(Mod, Case, SubCases, St, Mode)
+ end.
+
+collect_case_subcases(Mod, Case, SubCases, St0, Mode) ->
+ OldMod = St0#cc.mod,
+ case collect_cases(SubCases, St0#cc{mod=Mod}, Mode) of
+ {ok,FlatCases,St} ->
+ {ok,FlatCases,St#cc{mod=OldMod}};
+ {error,Reason} ->
+ {error,{{Mod,Case},Reason}}
+ end.
+
+collect_files(Dir, Pattern, St, Mode) ->
+ {ok,Cwd} = file:get_cwd(),
+ Dir1 = filename:join(Cwd, Dir),
+ Wc = filename:join([Dir1,Pattern++"{.erl,"++code:objfile_extension()++"}"]),
+ case catch filelib:wildcard(Wc) of
+ {'EXIT', Reason} ->
+ io:format("Could not collect files: ~tp~n", [Reason]),
+ {error,{collect_fail,Dir,Pattern}};
+ Files ->
+ %% convert to module names and remove duplicates
+ Mods = lists:foldl(fun(File, Acc) ->
+ Mod = fullname_to_mod(File),
+ case lists:member(Mod, Acc) of
+ true -> Acc;
+ false -> [Mod | Acc]
+ end
+ end, [], Files),
+ Tests = [{Mod,all} || Mod <- lists:sort(Mods)],
+ collect_cases(Tests, St, Mode)
+ end.
+
+fullname_to_mod(Path) when is_list(Path) ->
+ %% If this is called with a binary, then we are probably in +fnu
+ %% mode and have found a beam file with name encoded as latin1. We
+ %% will let this crash since it can not work to load such a module
+ %% anyway. It should be removed or renamed!
+ list_to_atom(filename:rootname(filename:basename(Path))).
+
+collect_case_deny(Mod, Case, MFA, ReqList, SubCases, St, Mode) ->
+ case {check_deny(ReqList, St#cc.skip),SubCases} of
+ {{denied,Comment},_SubCases} ->
+ {ok,[{skip_case,{MFA,Comment},Mode}],St};
+ {granted,[]} ->
+ {ok,[MFA],St};
+ {granted,SubCases} ->
+ collect_case_subcases(Mod, Case, SubCases, St, Mode)
+ end.
+
+check_deny([Req|Reqs], DenyList) ->
+ case check_deny_req(Req, DenyList) of
+ {denied,_Comment}=Denied -> Denied;
+ granted -> check_deny(Reqs, DenyList)
+ end;
+check_deny([], _DenyList) -> granted;
+check_deny(Req, DenyList) -> check_deny([Req], DenyList).
+
+check_deny_req({Req,Val}, DenyList) ->
+ %%io:format("ValCheck ~p=~p in ~p\n", [Req,Val,DenyList]),
+ case lists:keysearch(Req, 1, DenyList) of
+ {value,{_Req,DenyVal}} when Val >= DenyVal ->
+ {denied,io_lib:format("Requirement ~tp=~tp", [Req,Val])};
+ _ ->
+ check_deny_req(Req, DenyList)
+ end;
+check_deny_req(Req, DenyList) ->
+ case lists:member(Req, DenyList) of
+ true -> {denied,io_lib:format("Requirement ~tp", [Req])};
+ false -> granted
+ end.
+
+in_skip_list({Mod,{conf,Props,InitMF,_CaseList,_FinMF}}, SkipList) ->
+ case in_skip_list(InitMF, SkipList) of
+ {true,_} = Yes ->
+ Yes;
+ _ ->
+ case proplists:get_value(name, Props) of
+ undefined ->
+ false;
+ Name ->
+ ToSkip =
+ lists:flatmap(
+ fun({M,{conf,SProps,_,SCaseList,_},Cmt}) when
+ M == Mod ->
+ case proplists:get_value(name, SProps) of
+ all ->
+ [{M,all,Cmt}];
+ Name ->
+ case SCaseList of
+ all ->
+ [{M,all,Cmt}];
+ _ ->
+ [{M,F,Cmt} || F <- SCaseList]
+ end;
+ _ ->
+ []
+ end;
+ (_) ->
+ []
+ end, SkipList),
+ case ToSkip of
+ [] ->
+ false;
+ _ ->
+ case lists:keysearch(all, 2, ToSkip) of
+ {value,{_,_,Cmt}} -> {true,Name,Cmt};
+ _ -> {true,ToSkip,""}
+ end
+ end
+ end
+ end;
+
+in_skip_list({Mod,Func,_Args}, SkipList) ->
+ in_skip_list({Mod,Func}, SkipList);
+in_skip_list({Mod,Func}, [{Mod,Funcs,Comment}|SkipList]) when is_list(Funcs) ->
+ case lists:member(Func, Funcs) of
+ true ->
+ {true,Comment};
+ _ ->
+ in_skip_list({Mod,Func}, SkipList)
+ end;
+in_skip_list({Mod,Func}, [{Mod,Func,Comment}|_SkipList]) ->
+ {true,Comment};
+in_skip_list({Mod,_Func}, [{Mod,Comment}|_SkipList]) ->
+ {true,Comment};
+in_skip_list({Mod,Func}, [_|SkipList]) ->
+ in_skip_list({Mod,Func}, SkipList);
+in_skip_list(_, []) ->
+ false.
+
+%% remove unnecessary properties
+init_props(Props) ->
+ case get_repeat(Props) of
+ Repeat = {_RepType,N} when N < 2 ->
+ if N == 0 ->
+ {error,{invalid_property,Repeat}};
+ true ->
+ lists:delete(Repeat, Props)
+ end;
+ _ ->
+ Props
+ end.
+
+keep_name(Props) ->
+ lists:filter(fun({name,_}) -> true;
+ ({suite,_}) -> true;
+ (_) -> false end, Props).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Node handling functions %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% get_target_info() -> #target_info
+%%
+%% Returns a record containing system information for target
+
+get_target_info() ->
+ controller_call(get_target_info).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% start_node(SlaveName, Type, Options) ->
+%% {ok, Slave} | {error, Reason}
+%%
+%% Called by test_server. See test_server:start_node/3 for details
+
+start_node(Name, Type, Options) ->
+ T = 10 * ?ACCEPT_TIMEOUT * test_server:timetrap_scale_factor(),
+ format(minor, "Attempt to start ~w node ~tp with options ~tp",
+ [Type, Name, Options]),
+ case controller_call({start_node,Name,Type,Options}, T) of
+ {{ok,Nodename}, Host, Cmd, Info, Warning} ->
+ format(minor,
+ "Successfully started node ~w on ~tp with command: ~ts",
+ [Nodename, Host, Cmd]),
+ format(major, "=node_start ~w", [Nodename]),
+ case Info of
+ [] -> ok;
+ _ -> format(minor, Info)
+ end,
+ case Warning of
+ [] -> ok;
+ _ ->
+ format(1, Warning),
+ format(minor, Warning)
+ end,
+ {ok, Nodename};
+ {fail,{Ret, Host, Cmd}} ->
+ format(minor,
+ "Failed to start node ~tp on ~tp with command: ~ts~n"
+ "Reason: ~tp",
+ [Name, Host, Cmd, Ret]),
+ {fail,Ret};
+ {Ret, undefined, undefined} ->
+ format(minor, "Failed to start node ~tp: ~tp", [Name,Ret]),
+ Ret;
+ {Ret, Host, Cmd} ->
+ format(minor,
+ "Failed to start node ~tp on ~tp with command: ~ts~n"
+ "Reason: ~tp",
+ [Name, Host, Cmd, Ret]),
+ Ret
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% wait_for_node(Node) -> ok | {error,timeout}
+%%
+%% Wait for a slave/peer node which has been started with
+%% the option {wait,false}. This function returns when
+%% when the new node has contacted test_server_ctrl again
+
+wait_for_node(Slave) ->
+ T = 10000 * test_server:timetrap_scale_factor(),
+ case catch controller_call({wait_for_node,Slave},T) of
+ {'EXIT',{timeout,_}} -> {error,timeout};
+ ok -> ok
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% is_release_available(Release) -> true | false
+%% Release -> string()
+%%
+%% Test if a release (such as "r10b") is available to be
+%% started using start_node/3.
+
+is_release_available(Release) ->
+ controller_call({is_release_available,Release}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% stop_node(Name) -> ok | {error,Reason}
+%%
+%% Clean up - test_server will stop this node
+
+stop_node(Slave) ->
+ controller_call({stop_node,Slave}).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% DEBUGGER INTERFACE %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+i() ->
+ hformat("Pid", "Initial Call", "Current Function", "Reducts", "Msgs"),
+ Line=lists:duplicate(27, "-"),
+ hformat(Line, Line, Line, Line, Line),
+ display_info(processes(), 0, 0).
+
+p(A,B,C) ->
+ pinfo(ts_pid(A,B,C)).
+p(X) when is_atom(X) ->
+ pinfo(whereis(X));
+p({A,B,C}) ->
+ pinfo(ts_pid(A,B,C));
+p(X) ->
+ pinfo(X).
+
+t() ->
+ t(wall_clock).
+t(X) ->
+ element(1, statistics(X)).
+
+pi(Item,X) ->
+ lists:keysearch(Item,1,p(X)).
+pi(Item,A,B,C) ->
+ lists:keysearch(Item,1,p(A,B,C)).
+
+%% c:pid/3
+ts_pid(X,Y,Z) when is_integer(X), is_integer(Y), is_integer(Z) ->
+ list_to_pid("<" ++ integer_to_list(X) ++ "." ++
+ integer_to_list(Y) ++ "." ++
+ integer_to_list(Z) ++ ">").
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% display_info(Pids, Reductions, Messages) -> void
+%% Pids = [pid(),...]
+%% Reductions = integer()
+%% Messaged = integer()
+%%
+%% Displays info, similar to c:i() about the processes in the list Pids.
+%% Also counts the total number of reductions and msgs for the listed
+%% processes, if called with Reductions = Messages = 0.
+
+display_info([Pid|T], R, M) ->
+ case pinfo(Pid) of
+ undefined ->
+ display_info(T, R, M);
+ Info ->
+ Call = fetch(initial_call, Info),
+ Curr = case fetch(current_function, Info) of
+ {Mod,F,Args} when is_list(Args) ->
+ {Mod,F,length(Args)};
+ Other ->
+ Other
+ end,
+ Reds = fetch(reductions, Info),
+ LM = fetch(message_queue_len, Info),
+ pformat(io_lib:format("~w", [Pid]),
+ io_lib:format("~tw", [Call]),
+ io_lib:format("~tw", [Curr]), Reds, LM),
+ display_info(T, R+Reds, M + LM)
+ end;
+display_info([], R, M) ->
+ Line=lists:duplicate(27, "-"),
+ hformat(Line, Line, Line, Line, Line),
+ pformat("Total", "", "", R, M).
+
+hformat(A1, A2, A3, A4, A5) ->
+ io:format("~-10s ~-27s ~-27s ~8s ~4s~n", [A1,A2,A3,A4,A5]).
+
+pformat(A1, A2, A3, A4, A5) ->
+ io:format("~-10s ~-27s ~-27s ~8w ~4w~n", [A1,A2,A3,A4,A5]).
+
+fetch(Key, Info) ->
+ case lists:keysearch(Key, 1, Info) of
+ {value, {_, Val}} ->
+ Val;
+ _ ->
+ 0
+ end.
+
+pinfo(P) ->
+ Node = node(),
+ case node(P) of
+ Node ->
+ process_info(P);
+ _ ->
+ rpc:call(node(P),erlang,process_info,[P])
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Support functions for COVER %%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% A module is included in the cover analysis if
+%% - it belongs to the tested application and is not listed in the
+%% {exclude,List} part of the App.cover file
+%% - it does not belong to the application, but is listed in the
+%% {include,List} part of the App.cover file
+%% - it does not belong to the application, but is listed in the
+%% {cross,[{Tag,List}]} part of the App.cover file
+%%
+%% The modules listed in the 'cross' part of the cover file are
+%% modules that are heavily used by other tests than the one where
+%% they are explicitly tested. They should then be listed as 'cross'
+%% in the cover file for the test where they are used but do not
+%% belong.
+%%
+%% After all tests are completed, the these modules can be analysed
+%% with coverage data from all tests where they are compiled - see
+%% cross_cover_analyse/2. The result is stored in a file called
+%% cross_cover.html in the run.<timestamp> directory of the
+%% test the modules belong to.
+%%
+%% Example:
+%% If the module m1 belongs to system s1 but is heavily used also in
+%% the tests for another system s2, then the cover files for the two
+%% systems could be like this:
+%%
+%% s1.cover:
+%% {include,[m1]}.
+%%
+%% s2.cover:
+%% {include,[....]}. % modules belonging to system s2
+%% {cross,[{s1,[m1]}]}.
+%%
+%% When the tests for both s1 and s2 are completed, run
+%% cross_cover_analyse(Level,[{s1,S1LogDir},{s2,S2LogDir}]), and
+%% the accumulated cover data for m1 will be written to
+%% S1LogDir/[run.<timestamp>/]cross_cover.html
+%%
+%% S1LogDir and S2LogDir are either the run.<timestamp> directories
+%% for the two tests, or the parent directory of these, in which case
+%% the latest run.<timestamp> directory will be chosen.
+%%
+%% Note that the m1 module will also be presented in the normal
+%% coverage log for s1 (due to the include statement in s1.cover), but
+%% that only includes the coverage achieved by the s1 test itself.
+%%
+%% The Tag in the 'cross' statement in the cover file has no other
+%% purpose than mapping the list of modules ([m1] in the example
+%% above) to the correct log directory where it should be included in
+%% the cross_cover.html file (S1LogDir in the example above).
+%% I.e. the value of the Tag has no meaning, it could be foo as well
+%% as s1 above, as long as the same Tag is used in the cover file and
+%% in the call to cross_cover_analyse/2.
+
+
+%% Cover compilation
+%% The compilation is executed on the target node
+start_cover(#cover{}=CoverInfo) ->
+ cover_compile(CoverInfo);
+start_cover({log,CoverLogDir}=CoverInfo) ->
+ %% Cover is controlled by the framework - here's the log
+ put(test_server_cover_log_dir,CoverLogDir),
+ {ok,CoverInfo}.
+
+cover_compile(CoverInfo) ->
+ test_server:cover_compile(CoverInfo).
+
+%% Read the coverfile for an application and return a list of modules
+%% that are members of the application but shall not be compiled
+%% (Exclude), and a list of modules that are not members of the
+%% application but shall be compiled (Include).
+read_cover_file(none) ->
+ {[],[],[]};
+read_cover_file(CoverFile) ->
+ case file:consult(CoverFile) of
+ {ok,List} ->
+ case check_cover_file(List, [], [], []) of
+ {ok,Exclude,Include,Cross} -> {Exclude,Include,Cross};
+ error ->
+ io:fwrite("Faulty format of CoverFile ~tp\n", [CoverFile]),
+ {[],[],[]}
+ end;
+ {error,Reason} ->
+ io:fwrite("Can't read CoverFile ~ts\nReason: ~tp\n",
+ [CoverFile,Reason]),
+ {[],[],[]}
+ end.
+
+check_cover_file([{exclude,all}|Rest], _, Include, Cross) ->
+ check_cover_file(Rest, all, Include, Cross);
+check_cover_file([{exclude,Exclude}|Rest], _, Include, Cross) ->
+ case lists:all(fun(M) -> is_atom(M) end, Exclude) of
+ true ->
+ check_cover_file(Rest, Exclude, Include, Cross);
+ false ->
+ error
+ end;
+check_cover_file([{include,Include}|Rest], Exclude, _, Cross) ->
+ case lists:all(fun(M) -> is_atom(M) end, Include) of
+ true ->
+ check_cover_file(Rest, Exclude, Include, Cross);
+ false ->
+ error
+ end;
+check_cover_file([{cross,Cross}|Rest], Exclude, Include, _) ->
+ case check_cross(Cross) of
+ true ->
+ check_cover_file(Rest, Exclude, Include, Cross);
+ false ->
+ error
+ end;
+check_cover_file([], Exclude, Include, Cross) ->
+ {ok,Exclude,Include,Cross}.
+
+check_cross([{Tag,Modules}|Rest]) ->
+ case lists:all(fun(M) -> is_atom(M) end, [Tag|Modules]) of
+ true ->
+ check_cross(Rest);
+ false ->
+ false
+ end;
+check_cross([]) ->
+ true.
+
+
+%% Cover analysis, per application
+%% This analysis is executed on the target node once the test is
+%% completed for an application. This is not the same as the cross
+%% cover analysis, which can be executed on any node after the tests
+%% are finshed.
+%%
+%% This per application analysis writes the file cover.html in the
+%% application's run.<timestamp> directory.
+stop_cover(#cover{}=CoverInfo, TestDir) ->
+ cover_analyse(CoverInfo, TestDir),
+ ok;
+stop_cover(_CoverInfo, _TestDir) ->
+ %% Cover is probably controlled by the framework
+ ok.
+
+make_relative(AbsDir, VsDir) ->
+ DirTokens = filename:split(AbsDir),
+ VsTokens = filename:split(VsDir),
+ filename:join(make_relative1(DirTokens, VsTokens)).
+
+make_relative1([T | DirTs], [T | VsTs]) ->
+ make_relative1(DirTs, VsTs);
+make_relative1(Last = [_File], []) ->
+ Last;
+make_relative1(Last = [_File], VsTs) ->
+ Ups = ["../" || _ <- VsTs],
+ Ups ++ Last;
+make_relative1(DirTs, []) ->
+ DirTs;
+make_relative1(DirTs, VsTs) ->
+ Ups = ["../" || _ <- VsTs],
+ Ups ++ DirTs.
+
+
+cover_analyse(CoverInfo, TestDir) ->
+ write_default_cross_coverlog(TestDir),
+
+ {ok,CoverLog} = open_html_file(filename:join(TestDir, ?coverlog_name)),
+ write_coverlog_header(CoverLog),
+ #cover{app=App,
+ file=CoverFile,
+ excl=Excluded,
+ cross=Cross} = CoverInfo,
+ io:fwrite(CoverLog, "<h1>Coverage for application '~w'</h1>\n", [App]),
+ io:fwrite(CoverLog,
+ "<p><a href=\"~ts\">Coverdata collected over all tests</a></p>",
+ [?cross_coverlog_name]),
+
+ io:fwrite(CoverLog, "<p>CoverFile: <code>~tp</code>\n", [CoverFile]),
+ ok = write_cross_cover_info(TestDir,Cross),
+
+ case length(cover:imported_modules()) of
+ Imps when Imps > 0 ->
+ io:fwrite(CoverLog,
+ "<p>Analysis includes data from ~w imported module(s).\n",
+ [Imps]);
+ _ ->
+ ok
+ end,
+
+ io:fwrite(CoverLog, "<p>Excluded module(s): <code>~tp</code>\n", [Excluded]),
+
+ Coverage = test_server:cover_analyse(TestDir, CoverInfo),
+ ok = write_binary_file(filename:join(TestDir,?raw_coverlog_name),
+ term_to_binary(Coverage)),
+
+ case lists:filter(fun({_M,{_,_,_}}) -> false;
+ (_) -> true
+ end, Coverage) of
+ [] ->
+ ok;
+ Bad ->
+ io:fwrite(CoverLog, "<p>Analysis failed for ~w module(s): "
+ "<code>~w</code>\n",
+ [length(Bad),[BadM || {BadM,{_,_Why}} <- Bad]])
+ end,
+
+ TotPercent = write_cover_result_table(CoverLog, Coverage),
+ ok = write_binary_file(filename:join(TestDir, ?cover_total),
+ term_to_binary(TotPercent)).
+
+%% Cover analysis - accumulated over multiple tests
+%% This can be executed on any node after all tests are finished.
+%% Analyse = overview | details
+%% TagDirs = [{Tag,Dir}]
+%% Tag = atom(), identifier
+%% Dir = string(), the log directory for Tag, it can be a
+%% run.<timestamp> directory or the parent directory of
+%% such (in which case the latest run.<timestamp> directory
+%% is used)
+cross_cover_analyse(Analyse, TagDirs0) ->
+ TagDirs = get_latest_run_dirs(TagDirs0),
+ TagMods = get_all_cross_info(TagDirs,[]),
+ TagDirMods = add_cross_modules(TagMods,TagDirs),
+ CoverdataFiles = get_coverdata_files(TagDirMods),
+ lists:foreach(fun(CDF) -> cover:import(CDF) end, CoverdataFiles),
+ io:fwrite("Cover analysing...\n", []),
+ DetailsFun =
+ case Analyse of
+ details ->
+ fun(Dir,M) ->
+ OutFile = filename:join(Dir,
+ atom_to_list(M) ++
+ ".CROSS_COVER.html"),
+ case cover:analyse_to_file(M, OutFile, [html]) of
+ {ok,_} ->
+ {file,OutFile};
+ Error ->
+ Error
+ end
+ end;
+ _ ->
+ fun(_,_) -> undefined end
+ end,
+ Coverage = analyse_tests(TagDirMods, DetailsFun, []),
+ cover:stop(),
+ write_cross_cover_logs(Coverage,TagDirMods).
+
+write_cross_cover_info(_Dir,[]) ->
+ ok;
+write_cross_cover_info(Dir,Cross) ->
+ write_binary_file(filename:join(Dir,?cross_cover_info),
+ term_to_binary(Cross)).
+
+%% For each test from which there are cross cover analysed
+%% modules, write a cross cover log (cross_cover.html).
+write_cross_cover_logs([{Tag,Coverage}|T],TagDirMods) ->
+ case lists:keyfind(Tag,1,TagDirMods) of
+ {_,Dir,Mods} when Mods=/=[] ->
+ ok = write_binary_file(filename:join(Dir,?raw_cross_coverlog_name),
+ term_to_binary(Coverage)),
+ CoverLogName = filename:join(Dir,?cross_coverlog_name),
+ {ok,CoverLog} = open_html_file(CoverLogName),
+ write_coverlog_header(CoverLog),
+ io:fwrite(CoverLog,
+ "<h1>Coverage results for \'~w\' from all tests</h1>\n",
+ [Tag]),
+ write_cover_result_table(CoverLog, Coverage),
+ io:fwrite("Written file ~tp\n", [CoverLogName]);
+ _ ->
+ ok
+ end,
+ write_cross_cover_logs(T,TagDirMods);
+write_cross_cover_logs([],_) ->
+ io:fwrite("done\n", []).
+
+%% Get the latest run.<timestamp> directories
+get_latest_run_dirs([{Tag,Dir}|Rest]) ->
+ [{Tag,get_latest_run_dir(Dir)} | get_latest_run_dirs(Rest)];
+get_latest_run_dirs([]) ->
+ [].
+
+get_latest_run_dir(Dir) ->
+ case filelib:wildcard(filename:join(Dir,"run.[1-2]*")) of
+ [] ->
+ Dir;
+ [H|T] ->
+ get_latest_dir(T,H)
+ end.
+
+get_latest_dir([H|T],Latest) when H>Latest ->
+ get_latest_dir(T,H);
+get_latest_dir([_|T],Latest) ->
+ get_latest_dir(T,Latest);
+get_latest_dir([],Latest) ->
+ Latest.
+
+get_all_cross_info([{_Tag,Dir}|Rest],Acc) ->
+ case file:read_file(filename:join(Dir,?cross_cover_info)) of
+ {ok,Bin} ->
+ TagMods = binary_to_term(Bin),
+ get_all_cross_info(Rest,TagMods++Acc);
+ _ ->
+ get_all_cross_info(Rest,Acc)
+ end;
+get_all_cross_info([],Acc) ->
+ Acc.
+
+%% Associate the cross cover modules with their log directories
+add_cross_modules(TagMods,TagDirs)->
+ do_add_cross_modules(TagMods,[{Tag,Dir,[]} || {Tag,Dir} <- TagDirs]).
+do_add_cross_modules([{Tag,Mods1}|TagMods],TagDirMods)->
+ NewTagDirMods =
+ case lists:keytake(Tag,1,TagDirMods) of
+ {value,{Tag,Dir,Mods},Rest} ->
+ [{Tag,Dir,lists:umerge(lists:sort(Mods1),Mods)}|Rest];
+ false ->
+ TagDirMods
+ end,
+ do_add_cross_modules(TagMods,NewTagDirMods);
+do_add_cross_modules([],TagDirMods) ->
+ %% Just to get the modules in the same order as in the normal cover log
+ [{Tag,Dir,lists:reverse(Mods)} || {Tag,Dir,Mods} <- TagDirMods].
+
+%% Find all exported coverdata files.
+get_coverdata_files(TagDirMods) ->
+ lists:flatmap(
+ fun({_,LatestDir,_}) ->
+ filelib:wildcard(filename:join(LatestDir,"all.coverdata"))
+ end,
+ TagDirMods).
+
+
+%% For each test, analyse all modules
+%% Used for cross cover analysis.
+analyse_tests([{Tag,LastTest,Modules}|T], DetailsFun, Acc) ->
+ Cov = analyse_modules(LastTest, Modules, DetailsFun, []),
+ analyse_tests(T, DetailsFun, [{Tag,Cov}|Acc]);
+analyse_tests([], _DetailsFun, Acc) ->
+ Acc.
+
+%% Analyse each module
+%% Used for cross cover analysis.
+analyse_modules(Dir, [M|Modules], DetailsFun, Acc) ->
+ {ok,{M,{Cov,NotCov}}} = cover:analyse(M, module),
+ Acc1 = [{M,{Cov,NotCov,DetailsFun(Dir,M)}}|Acc],
+ analyse_modules(Dir, Modules, DetailsFun, Acc1);
+analyse_modules(_Dir, [], _DetailsFun, Acc) ->
+ Acc.
+
+
+%% Support functions for writing the cover logs (both cross and normal)
+write_coverlog_header(CoverLog) ->
+ case catch io:put_chars(CoverLog,html_header("Coverage results")) of
+ {'EXIT',Reason} ->
+ io:format("\n\nERROR: Could not write normal heading in coverlog.\n"
+ "CoverLog: ~tw\n"
+ "Reason: ~tp\n",
+ [CoverLog,Reason]),
+ io:format(CoverLog,"<html><body>\n", []);
+ _ ->
+ ok
+ end.
+
+
+format_analyse(M,Cov,NotCov,undefined) ->
+ io_lib:fwrite("<tr><td>~w</td>"
+ "<td align=right>~w %</td>"
+ "<td align=right>~w</td>"
+ "<td align=right>~w</td></tr>\n",
+ [M,pc(Cov,NotCov),Cov,NotCov]);
+format_analyse(M,Cov,NotCov,{file,File}) ->
+ io_lib:fwrite("<tr><td><a href=\"~ts\">~w</a></td>"
+ "<td align=right>~w %</td>"
+ "<td align=right>~w</td>"
+ "<td align=right>~w</td></tr>\n",
+ [uri_encode(filename:basename(File)),
+ M,pc(Cov,NotCov),Cov,NotCov]);
+format_analyse(M,Cov,NotCov,{lines,Lines}) ->
+ CoverOutName = atom_to_list(M)++".COVER.html",
+ {ok,CoverOut} = open_html_file(CoverOutName),
+ write_not_covered(CoverOut,M,Lines),
+ ok = file:close(CoverOut),
+ io_lib:fwrite("<tr><td><a href=\"~ts\">~w</a></td>"
+ "<td align=right>~w %</td>"
+ "<td align=right>~w</td>"
+ "<td align=right>~w</td></tr>\n",
+ [uri_encode(CoverOutName),M,pc(Cov,NotCov),Cov,NotCov]);
+format_analyse(M,Cov,NotCov,{error,_}) ->
+ io_lib:fwrite("<tr><td>~w</td>"
+ "<td align=right>~w %</td>"
+ "<td align=right>~w</td>"
+ "<td align=right>~w</td></tr>\n",
+ [M,pc(Cov,NotCov),Cov,NotCov]).
+
+
+pc(0,0) ->
+ 0;
+pc(Cov,NotCov) ->
+ round(Cov/(Cov+NotCov)*100).
+
+
+write_not_covered(CoverOut,M,Lines) ->
+ io:put_chars(CoverOut,html_header("Coverage results for "++atom_to_list(M))),
+ io:fwrite(CoverOut,
+ "The following lines in module ~w are not covered:\n"
+ "<table border=3 cellpadding=5>\n"
+ "<th>Line Number</th>\n",
+ [M]),
+ lists:foreach(fun({{_M,Line},{0,1}}) ->
+ io:fwrite(CoverOut,"<tr><td>~w</td></tr>\n", [Line]);
+ (_) ->
+ ok
+ end,
+ Lines),
+ io:put_chars(CoverOut,"</table>\n</body>\n</html>\n").
+
+
+write_default_coverlog(TestDir) ->
+ {ok,CoverLog} = open_html_file(filename:join(TestDir,?coverlog_name)),
+ write_coverlog_header(CoverLog),
+ io:put_chars(CoverLog,"Cover tool is not used\n</body></html>\n"),
+ ok = file:close(CoverLog).
+
+write_default_cross_coverlog(TestDir) ->
+ {ok,CrossCoverLog} =
+ open_html_file(filename:join(TestDir,?cross_coverlog_name)),
+ write_coverlog_header(CrossCoverLog),
+ io:put_chars(CrossCoverLog,
+ ["No cross cover modules exist for this application,",
+ xhtml("<br>","<br />"),
+ "or cross cover analysis is not completed.\n"
+ "</body></html>\n"]),
+ ok = file:close(CrossCoverLog).
+
+write_cover_result_table(CoverLog,Coverage) ->
+ io:fwrite(CoverLog,
+ "<p><table border=3 cellpadding=5>\n"
+ "<tr><th>Module</th><th>Covered (%)</th><th>Covered (Lines)</th>"
+ "<th>Not covered (Lines)</th>\n",
+ []),
+ {TotCov,TotNotCov} =
+ lists:foldl(fun({M,{Cov,NotCov,Details}},{AccCov,AccNotCov}) ->
+ Str = format_analyse(M,Cov,NotCov,Details),
+ io:fwrite(CoverLog,"~ts", [Str]),
+ {AccCov+Cov,AccNotCov+NotCov};
+ ({_M,{error,_Reason}},{AccCov,AccNotCov}) ->
+ {AccCov,AccNotCov}
+ end,
+ {0,0},
+ Coverage),
+ TotPercent = pc(TotCov,TotNotCov),
+ io:fwrite(CoverLog,
+ "<tr><th align=left>Total</th><th align=right>~w %</th>"
+ "<th align=right>~w</th><th align=right>~w</th></tr>\n"
+ "</table>\n"
+ "</body>\n"
+ "</html>\n",
+ [TotPercent,TotCov,TotNotCov]),
+ ok = file:close(CoverLog),
+ TotPercent.
+
+
+%%%-----------------------------------------------------------------
+%%% Support functions for writing files
+
+%% HTML files are always written with utf8 encoding
+html_header(Title) ->
+ ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n"
+ "<!-- autogenerated by '", atom_to_list(?MODULE), "'. -->\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>", Title, "</title>\n"
+ "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n"
+ "<meta http-equiv=\"content-type\" content=\"text/html; "
+ "charset=utf-8\"></meta>\n"
+ "</head>\n"
+ "<body bgcolor=\"white\" text=\"black\" "
+ "link=\"blue\" vlink=\"purple\" alink=\"red\">\n"].
+
+html_header(Title, Meta) ->
+ ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n"
+ "<!-- autogenerated by '", atom_to_list(?MODULE), "'. -->\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>", Title, "</title>\n"] ++ Meta ++ ["</head>\n"].
+
+open_html_file(File) ->
+ open_utf8_file(File).
+
+open_html_file(File,Opts) ->
+ open_utf8_file(File,Opts).
+
+write_html_file(File,Content) ->
+ write_file(File,Content,utf8).
+
+%% The 'major' log file, which is a pure text file is also written
+%% with utf8 encoding
+open_utf8_file(File) ->
+ case file:open(File,AllOpts=[write,{encoding,utf8}]) of
+ {error,Reason} -> {error,{Reason,{File,AllOpts}}};
+ Result -> Result
+ end.
+
+open_utf8_file(File,Opts) ->
+ case file:open(File,AllOpts=[{encoding,utf8}|Opts]) of
+ {error,Reason} -> {error,{Reason,{File,AllOpts}}};
+ Result -> Result
+ end.
+
+%% Write a file with specified encoding
+write_file(File,Content,latin1) ->
+ file:write_file(File,Content);
+write_file(File,Content,utf8) ->
+ write_binary_file(File,unicode:characters_to_binary(Content)).
+
+%% Write a file with only binary data
+write_binary_file(File,Content) ->
+ file:write_file(File,Content).
+
+%% Encoding of hyperlinks in HTML files
+uri_encode(File) ->
+ Encoding = file:native_name_encoding(),
+ uri_encode(File,Encoding).
+
+uri_encode(File,Encoding) ->
+ Components = filename:split(File),
+ filename:join([uri_encode_comp(C,Encoding) || C <- Components]).
+
+%% Encode the reference to a "filename of the given encoding" so it
+%% can be inserted in a utf8 encoded HTML file.
+%% This does almost the same as http_uri:encode/1, except
+%% 1. it does not convert @, : and / (in order to preserve nodename and c:/)
+%% 2. if the file name is in latin1, it also encodes all
+%% characters >127 - i.e. latin1 but not ASCII.
+uri_encode_comp([Char|Chars],Encoding) ->
+ Reserved = sets:is_element(Char, reserved()),
+ case (Char>127 andalso Encoding==latin1) orelse Reserved of
+ true ->
+ [ $% | integer_to_list(Char, 16)] ++
+ uri_encode_comp(Chars,Encoding);
+ false ->
+ [Char | uri_encode_comp(Chars,Encoding)]
+ end;
+uri_encode_comp([],_) ->
+ [].
+
+%% Copied from http_uri.erl, but slightly modified
+%% (not converting @, : and /)
+reserved() ->
+ sets:from_list([$;, $&, $=, $+, $,, $?,
+ $#, $[, $], $<, $>, $\", ${, $}, $|,
+ $\\, $', $^, $%, $ ]).
+
+encoding(File) ->
+ case epp:read_encoding(File) of
+ none ->
+ epp:default_encoding();
+ E ->
+ E
+ end.
diff --git a/lib/common_test/src/test_server_gl.erl b/lib/common_test/src/test_server_gl.erl
new file mode 100644
index 0000000000..24dd5cd54c
--- /dev/null
+++ b/lib/common_test/src/test_server_gl.erl
@@ -0,0 +1,362 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2012-2017. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%% This module implements group leader processes for test cases.
+%% Each group leader process handles output to the minor log file for
+%% a test case, and calls test_server_io to handle output to the common
+%% log files. The group leader processes are created and destroyed
+%% through the test_server_io module/process.
+
+-module(test_server_gl).
+-export([start_link/1,stop/1,set_minor_fd/3,unset_minor_fd/1,
+ get_tc_supervisor/1,print/4,set_props/2]).
+
+-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2]).
+
+-record(st, {tc_supervisor :: 'none'|pid(), %Test case supervisor
+ tc :: mfa() | 'undefined', %Current test case MFA
+ minor :: 'none'|pid(), %Minor fd
+ minor_monitor, %Monitor ref for minor fd
+ tsio_monitor, %Monitor red for controlling proc
+ capture :: 'none'|pid(), %Capture output
+ reject_io :: boolean(), %Reject I/O requests...
+ permit_io, %... and exceptions
+ auto_nl=true :: boolean(), %Automatically add NL
+ levels, %{Stdout,Major,Minor}
+ escape_chars=true %Switch escaping HTML on/off
+ }).
+
+%% start_link()
+%% Start a new group leader process. Only to be called by
+%% the test_server_io process.
+
+start_link(TSIO) ->
+ case gen_server:start_link(?MODULE, [TSIO], []) of
+ {ok,Pid} ->
+ {ok,Pid};
+ Other ->
+ Other
+ end.
+
+
+%% stop(Pid)
+%% Stop a group leader process. Only to be called by
+%% the test_server_io process.
+
+stop(GL) ->
+ gen_server:cast(GL, stop).
+
+
+%% set_minor_fd(GL, Fd, MFA)
+%% GL = Pid for the group leader process
+%% Fd = file descriptor for the minor log file
+%% MFA = {M,F,A} for the test case owning the minor log file
+%%
+%% Register the file descriptor for the minor log file. Subsequent
+%% IO directed to the minor log file will be written to this file.
+%% Also register the currently executing process at the testcase
+%% supervisor corresponding to this group leader process.
+
+set_minor_fd(GL, Fd, MFA) ->
+ req(GL, {set_minor_fd,Fd,MFA,self()}).
+
+
+%% unset_minor_fd(GL, Fd, MFA)
+%% GL = Pid for the group leader process
+%%
+%% Unregister the file descriptor for minor log file (typically
+%% because the test case has ended the minor log file is about
+%% to be closed). Subsequent IO (for example, by a process spawned
+%% by the testcase process) will go to the unexpected_io log file.
+
+unset_minor_fd(GL) ->
+ req(GL, unset_minor_fd).
+
+
+%% get_tc_supervisor(GL)
+%% GL = Pid for the group leader process
+%%
+%% Return the Pid for the process that supervises the test case
+%% that has this group leader.
+
+get_tc_supervisor(GL) ->
+ req(GL, get_tc_supervisor).
+
+
+%% print(GL, Detail, Format, Args) -> ok
+%% GL = Pid for the group leader process
+%% Detail = integer() | minor | major | html | stdout
+%% Msg = iodata()
+%% Printer = internal | pid()
+%%
+%% Print a message to one of the log files. If Detail is an integer,
+%% it will be compared to the levels (set by set_props/2) to
+%% determine which log file(s) that are to receive the output. If
+%% Detail is an atom, the value of the atom will directly determine
+%% which log file to use. IO to the minor log file will be handled
+%% directly by this group leader process (printing to the file set by
+%% set_minor_fd/3), and all other IO will be handled by calling
+%% test_server_io:print/3.
+
+print(GL, Detail, Msg, Printer) ->
+ req(GL, {print,Detail,Msg,Printer}).
+
+
+%% set_props(GL, [PropertyTuple])
+%% GL = Pid for the group leader process
+%% PropertyTuple = {levels,{Show,Major,Minor}} |
+%% {auto_nl,boolean()} |
+%% {reject_io_reqs,boolean()}
+%%
+%% Set properties for this group leader process.
+
+set_props(GL, PropList) ->
+ req(GL, {set_props,PropList}).
+
+%%% Internal functions.
+
+init([TSIO]) ->
+ ct_util:mark_process(group_leader),
+ EscChars = case application:get_env(test_server, esc_chars) of
+ {ok,ECBool} -> ECBool;
+ _ -> true
+ end,
+ Ref = erlang:monitor(process, TSIO),
+ {ok,#st{tc_supervisor=none,
+ minor=none,
+ minor_monitor=none,
+ tsio_monitor=Ref,
+ capture=none,
+ reject_io=false,
+ permit_io=gb_sets:empty(),
+ auto_nl=true,
+ levels={1,19,10},
+ escape_chars=EscChars
+ }}.
+
+req(GL, Req) ->
+ gen_server:call(GL, Req, infinity).
+
+handle_call(get_tc_supervisor, _From, #st{tc_supervisor=Pid}=St) ->
+ {reply,Pid,St};
+handle_call({set_minor_fd,Fd,MFA,Supervisor}, _From, St) ->
+ Ref = erlang:monitor(process, Fd),
+ {reply,ok,St#st{tc=MFA,minor=Fd,minor_monitor=Ref,
+ tc_supervisor=Supervisor}};
+handle_call(unset_minor_fd, _From, St) ->
+ {reply,ok,St#st{minor=none,tc_supervisor=none}};
+handle_call({set_props,PropList}, _From, St) ->
+ {reply,ok,do_set_props(PropList, St)};
+handle_call({print,Detail,Msg,Printer}, {From,_}, St) ->
+ output(Detail, Msg, Printer, From, St),
+ {reply,ok,St}.
+
+handle_cast(stop, St) ->
+ {stop,normal,St}.
+
+handle_info({'DOWN',Ref,process,_,Reason}=D, #st{minor_monitor=Ref}=St) ->
+ case Reason of
+ normal -> ok;
+ _ ->
+ Data = io_lib:format("=== WARNING === TC: ~tw\n"
+ "Got down from minor Fd ~w: ~tw\n\n",
+ [St#st.tc,St#st.minor,D]),
+ test_server_io:print_unexpected(Data)
+ end,
+ {noreply,St#st{minor=none,minor_monitor=none}};
+handle_info({'DOWN',Ref,process,_,_}, #st{tsio_monitor=Ref}=St) ->
+ %% controlling process (test_server_io) terminated, we're done
+ {stop,normal,St};
+handle_info({permit_io,Pid}, #st{permit_io=P}=St) ->
+ {noreply,St#st{permit_io=gb_sets:add(Pid, P)}};
+handle_info({capture,Cap0}, St) ->
+ Cap = case Cap0 of
+ false -> none;
+ Pid when is_pid(Cap0) -> Pid
+ end,
+ {noreply,St#st{capture=Cap}};
+handle_info({io_request,From,ReplyAs,Req}=IoReq, St) ->
+ _ = try io_req(Req, From, St) of
+ passthrough ->
+ group_leader() ! IoReq;
+ {EscapeHtml,Data} ->
+ case is_io_permitted(From, St) of
+ false ->
+ ok;
+ true ->
+ case St of
+ #st{capture=none} ->
+ ok;
+ #st{capture=CapturePid} ->
+ CapturePid ! {captured,Data},
+ ok
+ end,
+ case EscapeHtml andalso St#st.escape_chars of
+ true ->
+ output(minor, test_server_ctrl:escape_chars(Data),
+ From, From, St);
+ false ->
+ output(minor, Data, From, From, St)
+ end
+ end,
+ From ! {io_reply,ReplyAs,ok}
+ catch
+ _:_ ->
+ From ! {io_reply,ReplyAs,{error,arguments}}
+ end,
+ {noreply,St};
+handle_info({structured_io,ClientPid,{Detail,Str}}, St) ->
+ output(Detail, Str, ClientPid, ClientPid, St),
+ {noreply,St};
+handle_info({printout,Detail,["$tc_html",Format],Args}, St) ->
+ Str = io_lib:format(Format, Args),
+ output(Detail, ["$tc_html",Str], internal, none, St),
+ {noreply,St};
+handle_info({printout,Detail,Fun}, St) when is_function(Fun)->
+ output(Detail, Fun, internal, none, St),
+ {noreply,St};
+handle_info({printout,Detail,Format,Args}, St) ->
+ Str = io_lib:format(Format, Args),
+ if not St#st.escape_chars ->
+ output(Detail, ["$tc_html",Str], internal, none, St);
+ true ->
+ output(Detail, Str, internal, none, St)
+ end,
+ {noreply,St};
+handle_info(Msg, #st{tc_supervisor=Pid}=St) when is_pid(Pid) ->
+ %% The process overseeing the testcase process also used to be
+ %% the group leader; thus, it is widely expected that it can be
+ %% reached by sending a message to the group leader. Therefore
+ %% we'll need to forward any non-recognized messaged to the test
+ %% case supervisor.
+ Pid ! Msg,
+ {noreply,St};
+handle_info(_Msg, #st{}=St) ->
+ %% There is no known supervisor process. Ignore this message.
+ {noreply,St}.
+
+terminate(_, _) ->
+ ok.
+
+do_set_props([{levels,Levels}|Ps], St) ->
+ do_set_props(Ps, St#st{levels=Levels});
+do_set_props([{auto_nl,AutoNL}|Ps], St) ->
+ do_set_props(Ps, St#st{auto_nl=AutoNL});
+do_set_props([{reject_io_reqs,Bool}|Ps], St) ->
+ do_set_props(Ps, St#st{reject_io=Bool});
+do_set_props([], St) -> St.
+
+io_req({put_chars,Enc,Str}, _, _) when Enc =:= latin1; Enc =:= unicode ->
+ case Str of
+ ["$tc_html",Str0] ->
+ {false,unicode:characters_to_list(Str0, Enc)};
+ _ ->
+ {true,unicode:characters_to_list(Str, Enc)}
+ end;
+io_req({put_chars,Encoding,Mod,Func,[Format,Args]}, _, _) ->
+ case Format of
+ ["$tc_html",Format0] ->
+ Str = Mod:Func(Format0, Args),
+ {false,unicode:characters_to_list(Str, Encoding)};
+ _ ->
+ Str = Mod:Func(Format, Args),
+ {true,unicode:characters_to_list(Str, Encoding)}
+ end;
+io_req(_, _, _) -> passthrough.
+
+output(Level, StrOrFun, Sender, From, St) when is_integer(Level) ->
+ case selected_by_level(Level, stdout, St) of
+ true when hd(StrOrFun) == "$tc_html" ->
+ output(stdout, tl(StrOrFun), Sender, From, St);
+ true when is_function(StrOrFun) ->
+ output(stdout, StrOrFun(stdout), Sender, From, St);
+ true ->
+ output(stdout, StrOrFun, Sender, From, St);
+ false ->
+ ok
+ end,
+ case selected_by_level(Level, major, St) of
+ true when hd(StrOrFun) == "$tc_html" ->
+ output(major, tl(StrOrFun), Sender, From, St);
+ true when is_function(StrOrFun) ->
+ output(major, StrOrFun(major), Sender, From, St);
+ true ->
+ output(major, StrOrFun, Sender, From, St);
+ false ->
+ ok
+ end,
+ case selected_by_level(Level, minor, St) of
+ true when hd(StrOrFun) == "$tc_html" ->
+ output(minor, tl(StrOrFun), Sender, From, St);
+ true when is_function(StrOrFun) ->
+ output(minor, StrOrFun(minor), Sender, From, St);
+ true ->
+ output(minor, test_server_ctrl:escape_chars(StrOrFun),
+ Sender, From, St);
+ false ->
+ ok
+ end;
+output(stdout, Str, _Sender, From, St) ->
+ output_to_file(stdout, Str, From, St);
+output(html, Str, _Sender, From, St) ->
+ output_to_file(html, Str, From, St);
+output(Level, Str, Sender, From, St) when is_atom(Level) ->
+ output_to_file(Level, dress_output(Str, Sender, St), From, St).
+
+output_to_file(minor, Data0, From, #st{tc={M,F,A},minor=none}) ->
+ Data = [io_lib:format("=== ~w:~tw/~w\n", [M,F,A]),Data0],
+ test_server_io:print(From, unexpected_io, Data),
+ ok;
+output_to_file(minor, Data, From, #st{tc=TC,minor=Fd}) ->
+ try
+ io:put_chars(Fd, Data)
+ catch
+ Type:Reason ->
+ Data1 =
+ [io_lib:format("=== ERROR === TC: ~tw\n"
+ "Failed to write to minor Fd: ~w\n"
+ "Type: ~w\n"
+ "Reason: ~tw\n",
+ [TC,Fd,Type,Reason]),
+ Data,"\n"],
+ test_server_io:print(From, unexpected_io, Data1)
+ end;
+output_to_file(Detail, Data, From, _) ->
+ test_server_io:print(From, Detail, Data).
+
+is_io_permitted(From, #st{reject_io=true,permit_io=P}) ->
+ gb_sets:is_member(From, P);
+is_io_permitted(_, #st{reject_io=false}) -> true.
+
+selected_by_level(Level, stdout, #st{levels={Stdout,_,_}}) ->
+ Level =< Stdout;
+selected_by_level(Level, major, #st{levels={_,Major,_}}) ->
+ Level =< Major;
+selected_by_level(Level, minor, #st{levels={_,_,Minor}}) ->
+ Level >= Minor.
+
+dress_output([$=|_]=Str, internal, _) ->
+ [Str,$\n];
+dress_output(Str, internal, _) ->
+ ["=== ",Str,$\n];
+dress_output(Str, _, #st{auto_nl=AutoNL}) ->
+ case AutoNL of
+ true -> [Str,$\n];
+ false -> Str
+ end.
diff --git a/lib/common_test/src/test_server_internal.hrl b/lib/common_test/src/test_server_internal.hrl
new file mode 100644
index 0000000000..f0c1876dd6
--- /dev/null
+++ b/lib/common_test/src/test_server_internal.hrl
@@ -0,0 +1,60 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2002-2016. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+-define(priv_dir,"log_private").
+-define(MAIN_PORT,3289).
+-define(ACCEPT_TIMEOUT,20000).
+
+%% Target information generated by test_server:init_target_info/0
+%% Once initiated, this information will never change!!
+-record(target_info, {os_family, % atom(); win32 | unix
+ os_type, % result of os:type()
+ host, % string(); the name of the target machine
+ version, % string()
+ system_version, % string()
+ root_dir, % string()
+ emulator, % string()
+ otp_release, % string()
+ username, % string()
+ cookie, % string(); Cookie for target node
+ naming, % string(); "-name" | "-sname"
+ master}). % string(); Was used for OSE's master
+ % node for main target and slave nodes.
+ % For other platforms the target node
+ % itself is master for slave nodes
+
+%% Temporary information generated by test_server_ctrl:read_parameters/X
+%% This information is used when starting the main target, and for
+%% initiating the #target_info record.
+-record(par, {type,
+ target,
+ naming,
+ master,
+ cookie}).
+
+
+-record(cover, {app, % application; Name | none
+ file, % cover spec file
+ incl, % explicitly include modules
+ excl, % explicitly exclude modules
+ level, % analyse level; details | overview
+ mods, % actually cover compiled modules
+ stop=true, % stop cover after analyse; boolean()
+ cross}).% cross cover analyse info
diff --git a/lib/common_test/src/test_server_io.erl b/lib/common_test/src/test_server_io.erl
new file mode 100644
index 0000000000..ef31521950
--- /dev/null
+++ b/lib/common_test/src/test_server_io.erl
@@ -0,0 +1,458 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2012-2017. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%% This module implements a process with the registered name 'test_server_io',
+%% which has two main responsibilities:
+%%
+%% * Manage group leader processes (see the test_server_gl module)
+%% for test cases. A group_leader process is obtained by calling
+%% get_gl/1. Group leader processes will be kept alive as along as
+%% the 'test_server_io' process is alive.
+%%
+%% * Handle output to the common log files (stdout, major, html,
+%% unexpected_io).
+%%
+
+-module(test_server_io).
+-export([start_link/0,stop/1,get_gl/1,set_fd/2,
+ start_transaction/0,end_transaction/0,
+ print_buffered/1,print/3,print_unexpected/1,
+ set_footer/1,set_job_name/1,set_gl_props/1,
+ reset_state/0,finish/0]).
+
+-export([init/1,handle_call/3,handle_info/2,terminate/2]).
+
+-record(st, {fds, % Singleton fds (gb_tree)
+ tags=[], % Known tag types
+ shared_gl :: pid(), % Shared group leader
+ gls, % Group leaders (gb_set)
+ io_buffering=false, % I/O buffering
+ buffered, % Buffered I/O requests
+ html_footer, % HTML footer
+ job_name, % Name of current job.
+ gl_props, % Properties for GL
+ phase, % Indicates current mode
+ offline_buffer, % Buffer I/O during startup
+ stopping, % Reply to when process stopped
+ pending_ops % Perform when process idle
+ }).
+
+start_link() ->
+ case whereis(?MODULE) of
+ undefined ->
+ case gen_server:start_link({local,?MODULE}, ?MODULE, [], []) of
+ {ok,Pid} ->
+ {ok,Pid};
+ Other ->
+ Other
+ end;
+ Pid ->
+ %% already running, reset the state
+ reset_state(),
+ {ok,Pid}
+ end.
+
+stop(FilesToClose) ->
+ OldGL = group_leader(),
+ group_leader(self(), self()),
+ req({stop,FilesToClose}),
+ group_leader(OldGL, self()),
+ ok.
+
+finish() ->
+ req(finish).
+
+%% get_gl(Shared) -> Pid
+%% Shared = boolean()
+%% Pid = pid()
+%%
+%% Return a group leader (a process using the test_server_gl module).
+%% If Shared is true, the shared group leader is returned (suitable for
+%% running sequential test cases), otherwise a new group leader process
+%% is spawned. Group leader processes will live until the
+%% 'test_server_io' process is stopped.
+
+get_gl(Shared) when is_boolean(Shared) ->
+ req({get_gl,Shared}).
+
+%% set_fd(Tag, Fd) -> ok.
+%% Tag = major | html | unexpected_io
+%% Fd = a file descriptor (as returned by file:open/2)
+%%
+%% Associate a file descriptor with the given Tag. This
+%% Tag can later be used in when calling to print/3.
+
+set_fd(Tag, Fd) ->
+ req({set_fd,Tag,Fd}).
+
+%% start_transaction()
+%%
+%% Subsequent calls to print/3 from the process executing start_transaction/0
+%% will cause the messages to be buffered instead of printed directly.
+
+start_transaction() ->
+ req({start_transaction,self()}).
+
+%% end_transaction()
+%%
+%% End the transaction started by start_transaction/0. Subsequent calls to
+%% print/3 will cause the message to be printed directly.
+
+end_transaction() ->
+ req({end_transaction,self()}).
+
+%% print(From, Tag, Msg)
+%% From = pid()
+%% Tag = stdout, or any tag that has been registered using set_fd/2
+%% Msg = string or iolist
+%%
+%% Either print Msg to the file identified by Tag, or buffer the message
+%% start_transaction/0 has been called from the process From.
+%%
+%% NOTE: The tags have various special meanings. For example, 'html'
+%% is assumed to be a HTML file.
+
+print(From, Tag, Msg) ->
+ req({print,From,Tag,Msg}).
+
+%% print_buffered(Pid)
+%% Pid = pid()
+%%
+%% Print all messages buffered in the *first* transaction buffered for Pid.
+%% (If start_transaction/0 and end_transaction/0 has been called N times,
+%% print_buffered/1 must be called N times to print all transactions.)
+
+print_buffered(Pid) ->
+ req({print_buffered,Pid}).
+
+%% print_unexpected(Msg)
+%% Msg = string or iolist
+%%
+%% Print the given string in the unexpected_io log.
+
+print_unexpected(Msg) ->
+ print(xxxFrom,unexpected_io,Msg).
+
+%% set_footer(IoData)
+%%
+%% Set a footer for the file associated with the 'html' tag.
+%% It will be used by print/3 to print a footer for the HTML file.
+
+set_footer(Footer) ->
+ req({set_footer,Footer}).
+
+%% set_job_name(Name)
+%%
+%% Set a name for the currently running job. The name will be used
+%% when printing to 'stdout'.
+%%
+
+set_job_name(Name) ->
+ req({set_job_name,Name}).
+
+%% set_gl_props(PropList)
+%%
+%% Set properties for group leader processes. When a group_leader process
+%% is created, test_server_gl:set_props(PropList) will be called.
+
+set_gl_props(PropList) ->
+ req({set_gl_props,PropList}).
+
+%% reset_state
+%%
+%% Reset the initial state
+reset_state() ->
+ req(reset_state).
+
+%%% Internal functions.
+
+init([]) ->
+ process_flag(trap_exit, true),
+ ct_util:mark_process(),
+ Empty = gb_trees:empty(),
+ {ok,Shared} = test_server_gl:start_link(self()),
+ {ok,#st{fds=Empty,shared_gl=Shared,gls=gb_sets:empty(),
+ io_buffering=gb_sets:empty(),
+ buffered=Empty,
+ html_footer="</body>\n</html>\n",
+ job_name="<name not set>",
+ gl_props=[],
+ phase=starting,
+ offline_buffer=[],
+ pending_ops=[]}}.
+
+req(Req) ->
+ gen_server:call(?MODULE, Req, infinity).
+
+handle_call({get_gl,false}, _From, #st{gls=Gls,gl_props=Props}=St) ->
+ {ok,Pid} = test_server_gl:start_link(self()),
+ test_server_gl:set_props(Pid, Props),
+ {reply,Pid,St#st{gls=gb_sets:insert(Pid, Gls)}};
+handle_call({get_gl,true}, _From, #st{shared_gl=Shared}=St) ->
+ {reply,Shared,St};
+handle_call({set_fd,Tag,Fd}, _From, #st{fds=Fds0,tags=Tags0,
+ offline_buffer=OfflineBuff}=St) ->
+ Fds = gb_trees:enter(Tag, Fd, Fds0),
+ St1 = St#st{fds=Fds,tags=[Tag|lists:delete(Tag, Tags0)]},
+ OfflineBuff1 =
+ if OfflineBuff == [] ->
+ [];
+ true ->
+ %% Fd ready, print anything buffered for associated Tag
+ lists:filtermap(fun({T,From,Str}) when T == Tag ->
+ _ = output(From, Tag, Str, St1),
+ false;
+ (_) ->
+ true
+ end, lists:reverse(OfflineBuff))
+ end,
+ {reply,ok,St1#st{phase=started,
+ offline_buffer=lists:reverse(OfflineBuff1)}};
+handle_call({start_transaction,Pid}, _From, #st{io_buffering=Buffer0,
+ buffered=Buf0}=St) ->
+ Buf = case gb_trees:is_defined(Pid, Buf0) of
+ false -> gb_trees:insert(Pid, queue:new(), Buf0);
+ true -> Buf0
+ end,
+ Buffer = gb_sets:add(Pid, Buffer0),
+ {reply,ok,St#st{io_buffering=Buffer,buffered=Buf}};
+handle_call({print,From,Tag,Str}, _From, St0) ->
+ St = output(From, Tag, Str, St0),
+ {reply,ok,St};
+handle_call({end_transaction,Pid}, _From, #st{io_buffering=Buffer0,
+ buffered=Buffered0}=St0) ->
+ Q0 = gb_trees:get(Pid, Buffered0),
+ Q = queue:in(eot, Q0),
+ Buffered = gb_trees:update(Pid, Q, Buffered0),
+ Buffer = gb_sets:delete_any(Pid, Buffer0),
+ St = St0#st{io_buffering=Buffer,buffered=Buffered},
+ {reply,ok,St};
+handle_call({print_buffered,Pid}, _From, #st{buffered=Buffered0}=St0) ->
+ Q0 = gb_trees:get(Pid, Buffered0),
+ Q = do_print_buffered(Q0, St0),
+ Buffered = gb_trees:update(Pid, Q, Buffered0),
+ St = St0#st{buffered=Buffered},
+ {reply,ok,St};
+handle_call({set_footer,Footer}, _From, St) ->
+ {reply,ok,St#st{html_footer=Footer}};
+handle_call({set_job_name,Name}, _From, St) ->
+ {reply,ok,St#st{job_name=Name}};
+handle_call({set_gl_props,Props}, _From, #st{shared_gl=Shared}=St) ->
+ test_server_gl:set_props(Shared, Props),
+ {reply,ok,St#st{gl_props=Props}};
+handle_call(reset_state, From, #st{phase=stopping,pending_ops=Ops}=St) ->
+ %% can't reset during stopping phase, save op for later
+ Op = fun(NewSt) ->
+ {_,Result,NewSt1} = handle_call(reset_state, From, NewSt),
+ {Result,NewSt1}
+ end,
+ {noreply,St#st{pending_ops=[{From,Op}|Ops]}};
+handle_call(reset_state, _From, #st{fds=Fds,tags=Tags,shared_gl=Shared0,gls=Gls,
+ offline_buffer=OfflineBuff}) ->
+ %% close open log files
+ lists:foreach(fun(Tag) ->
+ case gb_trees:lookup(Tag, Fds) of
+ none ->
+ ok;
+ {value,Fd} ->
+ file:close(Fd)
+ end
+ end, Tags),
+ test_server_gl:stop(Shared0),
+ GlList = gb_sets:to_list(Gls),
+ _ = [test_server_gl:stop(GL) || GL <- GlList],
+ timer:sleep(100),
+ case lists:filter(fun(GlPid) -> is_process_alive(GlPid) end, GlList) of
+ [] ->
+ ok;
+ _ ->
+ timer:sleep(2000),
+ [exit(GL, kill) || GL <- GlList],
+ ok
+ end,
+ Empty = gb_trees:empty(),
+ {ok,Shared} = test_server_gl:start_link(self()),
+ {reply,ok,#st{fds=Empty,shared_gl=Shared,gls=gb_sets:empty(),
+ io_buffering=gb_sets:empty(),
+ buffered=Empty,
+ html_footer="</body>\n</html>\n",
+ job_name="<name not set>",
+ gl_props=[],
+ phase=starting,
+ offline_buffer=OfflineBuff,
+ pending_ops=[]}};
+handle_call({stop,FdTags}, From, #st{fds=Fds0,tags=Tags0,
+ shared_gl=SGL,gls=Gls0}=St0) ->
+ St = St0#st{gls=gb_sets:insert(SGL, Gls0),phase=stopping,stopping=From},
+ gc(St),
+ %% close open log files
+ {Fds1,Tags1} = lists:foldl(fun(Tag, {Fds,Tags}) ->
+ case gb_trees:lookup(Tag, Fds) of
+ none ->
+ {Fds,Tags};
+ {value,Fd} ->
+ _ = file:close(Fd),
+ {gb_trees:delete(Tag, Fds),
+ lists:delete(Tag, Tags)}
+ end
+ end, {Fds0,Tags0}, FdTags),
+ %% Give the users of the surviving group leaders some
+ %% time to finish.
+ erlang:send_after(1000, self(), stop_group_leaders),
+ {noreply,St#st{fds=Fds1,tags=Tags1}};
+handle_call(finish, From, St) ->
+ gen_server:reply(From, ok),
+ {stop,normal,St}.
+
+handle_info({'EXIT',Pid,normal}, #st{gls=Gls0,stopping=From}=St) ->
+ Gls = gb_sets:delete_any(Pid, Gls0),
+ case gb_sets:is_empty(Gls) andalso From =/= undefined of
+ true ->
+ %% No more group leaders left.
+ gen_server:reply(From, ok),
+ {noreply,St#st{gls=Gls,phase=stopping,stopping=undefined}};
+ false ->
+ %% Wait for more group leaders to finish.
+ {noreply,St#st{gls=Gls,phase=stopping}}
+ end;
+handle_info({'EXIT',Pid,killed}, #st{gls=Gls0}=St) ->
+ %% forced termination of group leader
+ {noreply,St#st{gls=gb_sets:delete_any(Pid, Gls0)}};
+handle_info({'EXIT',_Pid,Reason}, _St) ->
+ exit(Reason);
+handle_info(stop_group_leaders, #st{gls=Gls}=St) ->
+ %% Stop the remaining group leaders.
+ GlPids = gb_sets:to_list(Gls),
+ _ = [test_server_gl:stop(GL) || GL <- GlPids],
+ timer:sleep(100),
+ Wait =
+ case lists:filter(fun(GlPid) -> is_process_alive(GlPid) end, GlPids) of
+ [] -> 0;
+ _ -> 2000
+ end,
+ erlang:send_after(Wait, self(), kill_group_leaders),
+ {noreply,St};
+handle_info(kill_group_leaders, #st{gls=Gls,stopping=From,
+ pending_ops=Ops}=St) ->
+ _ = [exit(GL, kill) || GL <- gb_sets:to_list(Gls)],
+ if From /= undefined ->
+ gen_server:reply(From, ok);
+ true -> % reply has been sent already
+ ok
+ end,
+ %% we're idle, check if any ops are pending
+ St1 = lists:foldr(fun({ReplyTo,Op},NewSt) ->
+ {Result,NewSt1} = Op(NewSt),
+ gen_server:reply(ReplyTo, Result),
+ NewSt1
+ end, St#st{phase=idle,pending_ops=[]}, Ops),
+ {noreply,St1};
+handle_info(Other, St) ->
+ io:format("Ignoring: ~tp\n", [Other]),
+ {noreply,St}.
+
+terminate(_, _) ->
+ ok.
+
+output(From, Tag, Str, #st{io_buffering=Buffered,buffered=Buf0,
+ phase=Phase,offline_buffer=OfflineBuff}=St) ->
+ case gb_sets:is_member(From, Buffered) of
+ false ->
+ case do_output(Tag, Str, Phase, St) of
+ buffer when length(OfflineBuff)>500 ->
+ %% something's wrong, clear buffer
+ St#st{offline_buffer=[]};
+ buffer ->
+ St#st{offline_buffer=[{Tag,From,Str}|OfflineBuff]};
+ _ ->
+ St
+ end;
+ true ->
+ Q0 = gb_trees:get(From, Buf0),
+ Q = queue:in({Tag,Str}, Q0),
+ Buf = gb_trees:update(From, Q, Buf0),
+ St#st{buffered=Buf}
+ end.
+
+do_output(stdout, Str, _, #st{job_name=undefined}) ->
+ io:put_chars(Str);
+do_output(stdout, Str0, _, #st{job_name=Name}) ->
+ Str = io_lib:format("Testing ~ts: ~ts\n", [Name,Str0]),
+ io:put_chars(Str);
+do_output(Tag, Str, Phase, #st{fds=Fds}=St) ->
+ case gb_trees:lookup(Tag, Fds) of
+ none when Phase /= started ->
+ buffer;
+ none ->
+ S = io_lib:format("\n*** ERROR: ~w, line ~w: No known '~tp' log file\n",
+ [?MODULE,?LINE,Tag]),
+ do_output(stdout, [S,Str], Phase, St);
+ {value,Fd} ->
+ try
+ io:put_chars(Fd, Str),
+ case Tag of
+ html -> finalise_table(Fd, St);
+ _ -> ok
+ end
+ catch _:Error ->
+ S = io_lib:format("\n*** ERROR: ~w, line ~w: Error writing to "
+ "log file '~tp': ~tp\n",
+ [?MODULE,?LINE,Tag,Error]),
+ do_output(stdout, [S,Str], Phase, St)
+ end
+ end.
+
+finalise_table(Fd, #st{html_footer=Footer}) ->
+ case file:position(Fd, {cur,0}) of
+ {ok,Pos} ->
+ %% We are writing to a seekable file. Finalise so
+ %% we get complete valid (and viewable) HTML code.
+ %% Then rewind to overwrite the finalising code.
+ io:put_chars(Fd, ["\n</table>\n",Footer]),
+ file:position(Fd, Pos);
+ {error,epipe} ->
+ %% The file is not seekable. We cannot erase what
+ %% we've already written --- so the reader will
+ %% have to wait until we're done.
+ ok
+ end.
+
+do_print_buffered(Q0, St) ->
+ Item = queue:get(Q0),
+ Q = queue:drop(Q0),
+ case Item of
+ eot ->
+ Q;
+ {Tag,Str} ->
+ _ = do_output(Tag, Str, undefined, St),
+ do_print_buffered(Q, St)
+ end.
+
+gc(#st{gls=Gls0}) ->
+ InUse0 = [begin
+ case process_info(P, group_leader) of
+ {group_leader,GL} -> GL;
+ undefined -> undefined
+ end
+ end || P <- processes()],
+ InUse = ordsets:from_list(InUse0),
+ Gls = gb_sets:to_list(Gls0),
+ NotUsed = ordsets:subtract(Gls, InUse),
+ _ = [test_server_gl:stop(Pid) || Pid <- NotUsed],
+ ok.
diff --git a/lib/common_test/src/test_server_node.erl b/lib/common_test/src/test_server_node.erl
new file mode 100644
index 0000000000..ea7ad8538e
--- /dev/null
+++ b/lib/common_test/src/test_server_node.erl
@@ -0,0 +1,769 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2002-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+-module(test_server_node).
+-compile(r16).
+
+%%%
+%%% The same compiled code for this module must be possible to load
+%%% in R16B and later.
+%%%
+
+%% Test Controller interface
+-export([is_release_available/1]).
+-export([start_tracer_node/2,trace_nodes/2,stop_tracer_node/1]).
+-export([start_node/5, stop_node/1]).
+-export([kill_nodes/0, nodedown/1]).
+%% Internal export
+-export([node_started/1,trc/1,handle_debug/4]).
+
+-include("test_server_internal.hrl").
+-record(slave_info, {name,socket,client}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% All code in this module executes on the test_server_ctrl process %%%
+%%% except for node_started/1 and trc/1 which execute on a new node. %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+is_release_available(Rel) when is_atom(Rel) ->
+ is_release_available(atom_to_list(Rel));
+is_release_available(Rel) ->
+ case os:type() of
+ {unix,_} ->
+ Erl = find_release(Rel),
+ case Erl of
+ none -> false;
+ _ -> filelib:is_regular(Erl)
+ end;
+ _ ->
+ false
+ end.
+
+nodedown(Sock) ->
+ Match = #slave_info{name='$1',socket=Sock,client='$2',_='_'},
+ case ets:match(slave_tab,Match) of
+ [[Node,_Client]] -> % Slave node died
+ gen_tcp:close(Sock),
+ ets:delete(slave_tab,Node),
+ slave_died;
+ [] ->
+ ok
+ end.
+
+
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% Start trace node
+%%%
+start_tracer_node(TraceFile,TI) ->
+ Match = #slave_info{name='$1',_='_'},
+ SlaveNodes = lists:map(fun([N]) -> [" ",N] end,
+ ets:match(slave_tab,Match)),
+ TargetNode = node(),
+ Cookie = TI#target_info.cookie,
+ {ok,LSock} = gen_tcp:listen(0,[binary,{reuseaddr,true},{packet,2}]),
+ {ok,TracePort} = inet:port(LSock),
+ Prog = quote_progname(pick_erl_program(default)),
+ Cmd = lists:concat([Prog, " -sname tracer -hidden -setcookie ", Cookie,
+ " -s ", ?MODULE, " trc ", TraceFile, " ",
+ TracePort, " ", TI#target_info.os_family]),
+ spawn(fun() -> print_data(open_port({spawn,Cmd},[stream])) end),
+%! open_port({spawn,Cmd},[stream]),
+ case gen_tcp:accept(LSock,?ACCEPT_TIMEOUT) of
+ {ok,Sock} ->
+ gen_tcp:close(LSock),
+ receive
+ {tcp,Sock,Result} when is_binary(Result) ->
+ case unpack(Result) of
+ error ->
+ gen_tcp:close(Sock),
+ {error,timeout};
+ {ok,started} ->
+ trace_nodes(Sock,[TargetNode | SlaveNodes]),
+ {ok,Sock};
+ {ok,Error} -> Error
+ end;
+ {tcp_closed,Sock} ->
+ gen_tcp:close(Sock),
+ {error,could_not_start_tracernode}
+ after ?ACCEPT_TIMEOUT ->
+ gen_tcp:close(Sock),
+ {error,timeout}
+ end;
+ Error ->
+ gen_tcp:close(LSock),
+ {error,{could_not_start_tracernode,Error}}
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% Start a tracer on each of these nodes and set flags and patterns
+%%%
+trace_nodes(Sock,Nodes) ->
+ Bin = term_to_binary({add_nodes,Nodes}),
+ ok = gen_tcp:send(Sock, tag_trace_message(Bin)),
+ receive_ack(Sock).
+
+
+receive_ack(Sock) ->
+ receive
+ {tcp,Sock,Bin} when is_binary(Bin) ->
+ case unpack(Bin) of
+ error -> receive_ack(Sock);
+ {ok,_} -> ok
+ end;
+ _ ->
+ receive_ack(Sock)
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% Stop trace node
+%%%
+stop_tracer_node(Sock) ->
+ Bin = term_to_binary(id(stop)),
+ ok = gen_tcp:send(Sock, tag_trace_message(Bin)),
+ receive {tcp_closed,Sock} -> gen_tcp:close(Sock) end,
+ ok.
+
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% trc([TraceFile,Nodes]) -> ok
+%%
+%% Start tracing on the given nodes
+%%
+%% This function executes on the new node
+%%
+trc([TraceFile, PortAtom, Type]) ->
+ {Result,Patterns} =
+ case file:consult(TraceFile) of
+ {ok,TI} ->
+ Pat = parse_trace_info(lists:flatten(TI)),
+ {started,Pat};
+ Error ->
+ {Error,[]}
+ end,
+ Port = list_to_integer(atom_to_list(PortAtom)),
+ case catch gen_tcp:connect("localhost", Port, [binary,
+ {reuseaddr,true},
+ {packet,2}]) of
+ {ok,Sock} ->
+ BinResult = term_to_binary(Result),
+ ok = gen_tcp:send(Sock,tag_trace_message(BinResult)),
+ trc_loop(Sock,Patterns,Type);
+ _else ->
+ ok
+ end,
+ erlang:halt().
+trc_loop(Sock,Patterns,Type) ->
+ receive
+ {tcp,Sock,Bin} ->
+ case unpack(Bin) of
+ error ->
+ ttb:stop(),
+ gen_tcp:close(Sock);
+ {ok,{add_nodes,Nodes}} ->
+ add_nodes(Nodes,Patterns,Type),
+ Bin = term_to_binary(id(ok)),
+ ok = gen_tcp:send(Sock, tag_trace_message(Bin)),
+ trc_loop(Sock,Patterns,Type);
+ {ok,stop} ->
+ ttb:stop(),
+ gen_tcp:close(Sock)
+ end;
+ {tcp_closed,Sock} ->
+ ttb:stop(),
+ gen_tcp:close(Sock)
+ end.
+add_nodes(Nodes,Patterns,_Type) ->
+ {ok, _} = ttb:tracer(Nodes,[{file,{local, test_server}},
+ {handler, {{?MODULE,handle_debug},initial}}]),
+ {ok, _} = ttb:p(all,[call,timestamp]),
+ lists:foreach(fun({TP,M,F,A,Pat}) -> ttb:TP(M,F,A,Pat);
+ ({CTP,M,F,A}) -> ttb:CTP(M,F,A)
+ end,
+ Patterns).
+
+parse_trace_info([{TP,M,Pat}|Pats]) when TP=:=tp; TP=:=tpl ->
+ [{TP,M,'_','_',Pat}|parse_trace_info(Pats)];
+parse_trace_info([{TP,M,F,Pat}|Pats]) when TP=:=tp; TP=:=tpl ->
+ [{TP,M,F,'_',Pat}|parse_trace_info(Pats)];
+parse_trace_info([{TP,M,F,A,Pat}|Pats]) when TP=:=tp; TP=:=tpl ->
+ [{TP,M,F,A,Pat}|parse_trace_info(Pats)];
+parse_trace_info([CTP|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg ->
+ [{CTP,'_','_','_'}|parse_trace_info(Pats)];
+parse_trace_info([{CTP,M}|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg ->
+ [{CTP,M,'_','_'}|parse_trace_info(Pats)];
+parse_trace_info([{CTP,M,F}|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg ->
+ [{CTP,M,F,'_'}|parse_trace_info(Pats)];
+parse_trace_info([{CTP,M,F,A}|Pats]) when CTP=:=ctp; CTP=:=ctpl; CTP=:=ctpg ->
+ [{CTP,M,F,A}|parse_trace_info(Pats)];
+parse_trace_info([]) ->
+ [];
+parse_trace_info([_other|Pats]) -> % ignore
+ parse_trace_info(Pats).
+
+handle_debug(Out,Trace,TI,initial) ->
+ handle_debug(Out,Trace,TI,0);
+handle_debug(_Out,end_of_trace,_TI,N) ->
+ N;
+handle_debug(Out,Trace,_TI,N) ->
+ print_trc(Out,Trace,N),
+ N+1.
+
+print_trc(Out,{trace_ts,P,call,{M,F,A},C,Ts},N) ->
+ io:format(Out,
+ "~w: ~s~n"
+ "Process : ~w~n"
+ "Call : ~w:~tw/~w~n"
+ "Arguments : ~tp~n"
+ "Caller : ~tw~n~n",
+ [N,ts(Ts),P,M,F,length(A),A,C]);
+print_trc(Out,{trace_ts,P,call,{M,F,A},Ts},N) ->
+ io:format(Out,
+ "~w: ~s~n"
+ "Process : ~w~n"
+ "Call : ~w:~tw/~w~n"
+ "Arguments : ~tp~n~n",
+ [N,ts(Ts),P,M,F,length(A),A]);
+print_trc(Out,{trace_ts,P,return_from,{M,F,A},R,Ts},N) ->
+ io:format(Out,
+ "~w: ~s~n"
+ "Process : ~w~n"
+ "Return from : ~w:~tw/~w~n"
+ "Return value : ~tp~n~n",
+ [N,ts(Ts),P,M,F,A,R]);
+print_trc(Out,{drop,X},N) ->
+ io:format(Out,
+ "~w: Tracer dropped ~w messages - too busy~n~n",
+ [N,X]);
+print_trc(Out,Trace,N) ->
+ Ts = element(size(Trace),Trace),
+ io:format(Out,
+ "~w: ~s~n"
+ "Trace : ~tp~n~n",
+ [N,ts(Ts),Trace]).
+ts({_, _, Micro} = Now) ->
+ {{Y,M,D},{H,Min,S}} = calendar:now_to_local_time(Now),
+ io_lib:format("~4.4.0w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w,~6.6.0w",
+ [Y,M,D,H,Min,S,Micro]).
+
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% Start slave/peer nodes (initiated by test_server:start_node/5)
+%%%
+start_node(SlaveName, slave, Options, From, TI) when is_list(SlaveName) ->
+ start_node_slave(list_to_atom(SlaveName), Options, From, TI);
+start_node(SlaveName, slave, Options, From, TI) ->
+ start_node_slave(SlaveName, Options, From, TI);
+start_node(SlaveName, peer, Options, From, TI) when is_atom(SlaveName) ->
+ start_node_peer(atom_to_list(SlaveName), Options, From, TI);
+start_node(SlaveName, peer, Options, From, TI) ->
+ start_node_peer(SlaveName, Options, From, TI);
+start_node(_SlaveName, _Type, _Options, _From, _TI) ->
+ not_implemented_yet.
+
+%%
+%% Peer nodes are always started on the same host as test_server_ctrl
+%%
+%% (Socket communication is used since in early days the test target
+%% and the test server controller node could be on different hosts and
+%% the target could not know the controller node via erlang
+%% distribution)
+%%
+start_node_peer(SlaveName, OptList, From, TI) ->
+ SuppliedArgs = start_node_get_option_value(args, OptList, []),
+ Cleanup = start_node_get_option_value(cleanup, OptList, true),
+ HostStr = test_server_sup:hoststr(),
+ {ok,LSock} = gen_tcp:listen(0,[binary,
+ {reuseaddr,true},
+ {packet,2}]),
+ {ok,WaitPort} = inet:port(LSock),
+ NodeStarted = lists:concat([" -s ", ?MODULE, " node_started ",
+ HostStr, " ", WaitPort]),
+
+ % Support for erl_crash_dump files..
+ CrashDir = test_server_sup:crash_dump_dir(),
+ CrashFile = filename:join([CrashDir,
+ "erl_crash_dump."++cast_to_list(SlaveName)]),
+ CrashArgs = lists:concat([" -env ERL_CRASH_DUMP \"",CrashFile,"\" "]),
+ FailOnError = start_node_get_option_value(fail_on_error, OptList, true),
+ Prog0 = start_node_get_option_value(erl, OptList, default),
+ Prog = quote_progname(pick_erl_program(Prog0)),
+ Args =
+ case string:find(SuppliedArgs,"-setcookie") of
+ nomatch ->
+ "-setcookie " ++ TI#target_info.cookie ++ " " ++ SuppliedArgs;
+ _ ->
+ SuppliedArgs
+ end,
+ Cmd = lists:concat([Prog,
+ " -detached ",
+ TI#target_info.naming, " ", SlaveName,
+ NodeStarted,
+ CrashArgs,
+ " ", Args]),
+ Opts = case start_node_get_option_value(env, OptList, []) of
+ [] -> [];
+ Env -> [{env, Env}]
+ end,
+ %% peer is always started on localhost
+ %%
+ %% Bad environment can cause open port to fail. If this happens,
+ %% we ignore it and let the testcase handle the situation...
+ catch open_port({spawn, Cmd}, [stream|Opts]),
+
+ Tmo = 60000 * test_server:timetrap_scale_factor(),
+
+ case start_node_get_option_value(wait, OptList, true) of
+ true ->
+ Ret = wait_for_node_started(LSock,Tmo,undefined,Cleanup,TI,self()),
+ case {Ret,FailOnError} of
+ {{{ok, Node}, Warning},_} ->
+ gen_server:reply(From,{{ok,Node},HostStr,Cmd,[],Warning});
+ {_,false} ->
+ gen_server:reply(From,{Ret, HostStr, Cmd});
+ {_,true} ->
+ gen_server:reply(From,{fail,{Ret, HostStr, Cmd}})
+ end;
+ false ->
+ Nodename = list_to_atom(SlaveName ++ "@" ++ HostStr),
+ I = "=== Not waiting for node",
+ gen_server:reply(From,{{ok, Nodename}, HostStr, Cmd, I, []}),
+ Self = self(),
+ spawn_link(wait_for_node_started_fun(LSock,Tmo,Cleanup,TI,Self)),
+ ok
+ end.
+
+-spec wait_for_node_started_fun(_, _, _, _, _) -> fun(() -> no_return()).
+wait_for_node_started_fun(LSock, Tmo, Cleanup, TI, Self) ->
+ fun() ->
+ {{ok, _}, _} = wait_for_node_started(LSock,Tmo,undefined,
+ Cleanup,TI,Self),
+ receive after infinity -> ok end
+ end.
+
+%%
+%% Slave nodes are started on a remote host if
+%% - the option remote is given when calling test_server:start_node/3
+%%
+start_node_slave(SlaveName, OptList, From, _TI) ->
+ SuppliedArgs = start_node_get_option_value(args, OptList, []),
+ Cleanup = start_node_get_option_value(cleanup, OptList, true),
+
+ CrashDir = test_server_sup:crash_dump_dir(),
+ CrashFile = filename:join([CrashDir,
+ "erl_crash_dump."++cast_to_list(SlaveName)]),
+ CrashArgs = lists:concat([" -env ERL_CRASH_DUMP \"",CrashFile,"\" "]),
+ Args = lists:concat([" ", SuppliedArgs, CrashArgs]),
+
+ Prog0 = start_node_get_option_value(erl, OptList, default),
+ Prog = pick_erl_program(Prog0),
+ Ret =
+ case start_which_node(OptList) of
+ {error,Reason} -> {{error,Reason},undefined,undefined};
+ Host0 -> do_start_node_slave(Host0,SlaveName,Args,Prog,Cleanup)
+ end,
+ gen_server:reply(From,Ret).
+
+
+do_start_node_slave(Host0, SlaveName, Args, Prog, Cleanup) ->
+ Host =
+ case Host0 of
+ local -> test_server_sup:hoststr();
+ _ -> cast_to_list(Host0)
+ end,
+ Cmd = Prog ++ " " ++ Args,
+ case slave:start(Host, SlaveName, Args, no_link, Prog) of
+ {ok,Nodename} ->
+ case Cleanup of
+ true -> ets:insert(slave_tab,#slave_info{name=Nodename});
+ false -> ok
+ end,
+ {{ok,Nodename}, Host, Cmd, [], []};
+ Ret ->
+ {Ret, Host, Cmd}
+ end.
+
+
+wait_for_node_started(LSock,Timeout,Client,Cleanup,TI,CtrlPid) ->
+ case gen_tcp:accept(LSock,Timeout) of
+ {ok,Sock} ->
+ gen_tcp:close(LSock),
+ receive
+ {tcp,Sock,Started0} when is_binary(Started0) ->
+ case unpack(Started0) of
+ error ->
+ gen_tcp:close(Sock),
+ {error, connection_closed};
+ {ok,Started} ->
+ Version = TI#target_info.otp_release,
+ VsnStr = TI#target_info.system_version,
+ {ok,Nodename, W} =
+ handle_start_node_return(Version,
+ VsnStr,
+ Started),
+ case Cleanup of
+ true ->
+ ets:insert(slave_tab,#slave_info{name=Nodename,
+ socket=Sock,
+ client=Client});
+ false -> ok
+ end,
+ ok = gen_tcp:controlling_process(Sock,CtrlPid),
+ test_server_ctrl:node_started(Nodename),
+ {{ok,Nodename},W}
+ end;
+ {tcp_closed,Sock} ->
+ gen_tcp:close(Sock),
+ {error, connection_closed}
+ after Timeout ->
+ gen_tcp:close(Sock),
+ {error, timeout}
+ end;
+ {error,Reason} ->
+ gen_tcp:close(LSock),
+ {error, {no_connection,Reason}}
+ end.
+
+
+
+handle_start_node_return(Version,VsnStr,{started, Node, Version, VsnStr}) ->
+ {ok, Node, []};
+handle_start_node_return(Version,VsnStr,{started, Node, OVersion, OVsnStr}) ->
+ Str = io_lib:format("WARNING: Started node "
+ "reports different system "
+ "version than current node! "
+ "Current node version: ~p, ~p "
+ "Started node version: ~p, ~p",
+ [Version, VsnStr,
+ OVersion, OVsnStr]),
+ Str1 = lists:flatten(Str),
+ {ok, Node, Str1}.
+
+
+%%
+%% This function executes on the new node
+%%
+node_started([Host,PortAtom]) ->
+ %% Must spawn a new process because the boot process should not
+ %% hang forever!!
+ spawn(node_started_fun(Host,PortAtom)).
+
+-spec node_started_fun(_, _) -> fun(() -> no_return()).
+node_started_fun(Host,PortAtom) ->
+ fun() -> node_started(Host,PortAtom) end.
+
+%% This process hangs forever, just waiting for the socket to be
+%% closed and terminating the node
+node_started(Host,PortAtom) ->
+ {_, Version} = init:script_id(),
+ VsnStr = erlang:system_info(system_version),
+ Port = list_to_integer(atom_to_list(PortAtom)),
+ case catch gen_tcp:connect(Host,Port, [binary,
+ {reuseaddr,true},
+ {packet,2}]) of
+
+ {ok,Sock} ->
+ Started = term_to_binary({started, node(), Version, VsnStr}),
+ ok = gen_tcp:send(Sock, tag_trace_message(Started)),
+ receive _Anyting ->
+ gen_tcp:close(Sock),
+ erlang:halt()
+ end;
+ _else ->
+ erlang:halt()
+ end.
+
+
+-compile({inline, [tag_trace_message/1]}).
+-dialyzer({no_improper_lists, tag_trace_message/1}).
+tag_trace_message(M) ->
+ [1|M].
+
+% start_which_node(Optlist) -> hostname
+start_which_node(Optlist) ->
+ case start_node_get_option_value(remote, Optlist) of
+ undefined ->
+ local;
+ true ->
+ case find_remote_host() of
+ {error, Other} ->
+ {error, Other};
+ RHost ->
+ RHost
+ end
+ end.
+
+find_remote_host() ->
+ HostList=test_server_ctrl:get_hosts(),
+ case lists:delete(test_server_sup:hoststr(), HostList) of
+ [] ->
+ {error, no_remote_hosts};
+ [RHost|_Rest] ->
+ RHost
+ end.
+
+start_node_get_option_value(Key, List) ->
+ start_node_get_option_value(Key, List, undefined).
+
+start_node_get_option_value(Key, List, Default) ->
+ case lists:keysearch(Key, 1, List) of
+ {value, {Key, Value}} ->
+ Value;
+ false ->
+ Default
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% stop_node(Name) -> ok | {error,Reason}
+%%
+%% Clean up - test_server will stop this node
+stop_node(Name) ->
+ case ets:lookup(slave_tab,Name) of
+ [#slave_info{}] ->
+ ets:delete(slave_tab,Name),
+ ok;
+ [] ->
+ {error, not_a_slavenode}
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% kill_nodes() -> ok
+%%
+%% Brutally kill all slavenodes that were not stopped by test_server
+kill_nodes() ->
+ case ets:match_object(slave_tab,'_') of
+ [] -> [];
+ List ->
+ lists:map(fun(SI) -> kill_node(SI) end, List)
+ end.
+
+kill_node(SI) ->
+ Name = SI#slave_info.name,
+ ets:delete(slave_tab,Name),
+ case SI#slave_info.socket of
+ undefined ->
+ catch rpc:call(Name,erlang,halt,[]);
+ Sock ->
+ gen_tcp:close(Sock)
+ end,
+ Name.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% cast_to_list(X) -> string()
+%%% X = list() | atom() | void()
+%%% Returns a string representation of whatever was input
+
+cast_to_list(X) when is_list(X) -> X;
+cast_to_list(X) when is_atom(X) -> atom_to_list(X);
+cast_to_list(X) -> lists:flatten(io_lib:format("~tw", [X])).
+
+
+%%% L contains elements of the forms
+%%% {prog, String}
+%%% {release, Rel} where Rel = String | latest | previous
+%%% this
+%%%
+pick_erl_program(default) ->
+ ct:get_progname();
+pick_erl_program(L) ->
+ P = random_element(L),
+ case P of
+ {prog, S} ->
+ S;
+ {release, S} ->
+ find_release(S);
+ this ->
+ ct:get_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 ct:get_progname()
+%% could then typically be "/<full_path_to>/cerl -gcov").
+quote_progname(Progname) ->
+ do_quote_progname(string:lexemes(Progname," ")).
+
+do_quote_progname([Prog]) ->
+ "\""++Prog++"\"";
+do_quote_progname([Prog,Arg|Args]) ->
+ case os:find_executable(Prog) of
+ false ->
+ do_quote_progname([Prog++" "++Arg | Args]);
+ _ ->
+ %% this one has an executable - we assume the rest are arguments
+ "\""++Prog++"\""++
+ lists:flatten(lists:map(fun(X) -> [" ",X] end, [Arg|Args]))
+ end.
+
+random_element(L) ->
+ lists:nth(rand:uniform(length(L)), L).
+
+find_release(latest) ->
+ "/usr/local/otp/releases/latest/bin/erl";
+find_release(previous) ->
+ "kaka";
+find_release(Rel) ->
+ find_release(os:type(), Rel).
+
+find_release({unix,sunos}, Rel) ->
+ case os:cmd("uname -p") of
+ "sparc" ++ _ ->
+ "/usr/local/otp/releases/otp_beam_solaris8_" ++ Rel ++ "/bin/erl";
+ _ ->
+ none
+ end;
+find_release({unix,linux}, Rel) ->
+ Candidates = find_rel_linux(Rel),
+ case lists:dropwhile(fun(N) ->
+ not filelib:is_regular(N)
+ end, Candidates) of
+ [] -> none;
+ [Erl|_] -> Erl
+ end;
+find_release(_, _) -> none.
+
+find_rel_linux(Rel) ->
+ case suse_release() of
+ none -> [];
+ SuseRel -> find_rel_suse(Rel, SuseRel)
+ end.
+
+find_rel_suse(Rel, SuseRel) ->
+ Root = "/usr/local/otp/releases/sles",
+ case SuseRel of
+ "11" ->
+ %% Try both SuSE 11, SuSE 10 and SuSe 9 in that order.
+ find_rel_suse_1(Rel, Root++"11") ++
+ find_rel_suse_1(Rel, Root++"10") ++
+ find_rel_suse_1(Rel, Root++"9");
+ "10" ->
+ %% Try both SuSE 10 and SuSe 9 in that order.
+ find_rel_suse_1(Rel, Root++"10") ++
+ find_rel_suse_1(Rel, Root++"9");
+ "9" ->
+ find_rel_suse_1(Rel, Root++"9");
+ _ ->
+ []
+ end.
+
+find_rel_suse_1(Rel, RootWc) ->
+ case erlang:system_info(wordsize) of
+ 4 ->
+ find_rel_suse_2(Rel, RootWc++"_32");
+ 8 ->
+ find_rel_suse_2(Rel, RootWc++"_64") ++
+ find_rel_suse_2(Rel, RootWc++"_32")
+ end.
+
+find_rel_suse_2(Rel, RootWc) ->
+ RelDir = filename:dirname(RootWc),
+ Pat = filename:basename(RootWc ++ "_" ++ Rel) ++ ".*",
+ case file:list_dir(RelDir) of
+ {ok,Dirs} ->
+ case lists:filter(fun(Dir) ->
+ case re:run(Dir, Pat, [unicode]) of
+ nomatch -> false;
+ _ -> true
+ end
+ end, Dirs) of
+ [] ->
+ [];
+ [R|_] ->
+ [filename:join([RelDir,R,"bin","erl"])]
+ end;
+ _ ->
+ []
+ end.
+
+%% suse_release() -> VersionString | none.
+%% Return the major SuSE version number for this platform or
+%% 'none' if this is not a SuSE platform.
+suse_release() ->
+ case file:open("/etc/SuSE-release", [read]) of
+ {ok,Fd} ->
+ try
+ suse_release(Fd)
+ after
+ file:close(Fd)
+ end;
+ {error,_} -> none
+ end.
+
+suse_release(Fd) ->
+ case io:get_line(Fd, '') of
+ eof -> none;
+ Line when is_list(Line) ->
+ case re:run(Line, "^VERSION\\s*=\\s*(\\d+)\s*",
+ [{capture,all_but_first,list}]) of
+ nomatch ->
+ suse_release(Fd);
+ {match,[Version]} ->
+ Version
+ end
+ end.
+
+unpack(Bin) ->
+ {One,Term} = split_binary(Bin, 1),
+ case binary_to_list(One) of
+ [1] ->
+ case catch {ok,binary_to_term(Term)} of
+ {'EXIT',_} -> error;
+ {ok,_}=Res -> Res
+ end;
+ _ -> error
+ end.
+
+id(I) -> I.
+
+print_data(Port) ->
+ ct_util:mark_process(),
+ receive
+ {Port, {data, Bytes}} ->
+ io:put_chars(Bytes),
+ print_data(Port);
+ {Port, eof} ->
+ Port ! {self(), close},
+ receive
+ {Port, closed} ->
+ true
+ end,
+ receive
+ {'EXIT', Port, _} ->
+ ok
+ after 1 -> % force context switch
+ ok
+ end
+ end.
diff --git a/lib/common_test/src/test_server_sup.erl b/lib/common_test/src/test_server_sup.erl
new file mode 100644
index 0000000000..26e7534c6c
--- /dev/null
+++ b/lib/common_test/src/test_server_sup.erl
@@ -0,0 +1,947 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1998-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%%-------------------------------------------------------------------
+%%% Purpose: Test server support functions.
+%%%-------------------------------------------------------------------
+-module(test_server_sup).
+-export([timetrap/2, timetrap/3, timetrap/4,
+ timetrap_cancel/1, capture_get/1, messages_get/1,
+ timecall/3, call_crash/5, app_test/2, check_new_crash_dumps/0,
+ cleanup_crash_dumps/0, crash_dump_dir/0, tar_crash_dumps/0,
+ get_username/0, get_os_family/0,
+ hostatom/0, hostatom/1, hoststr/0, hoststr/1,
+ framework_call/2,framework_call/3,framework_call/4,
+ format_loc/1,
+ util_start/0, util_stop/0, unique_name/0,
+ call_trace/1,
+ appup_test/1]).
+-include("test_server_internal.hrl").
+-define(crash_dump_tar,"crash_dumps.tar.gz").
+-define(src_listing_ext, ".src.html").
+-record(util_state, {starter, latest_name}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timetrap(Timeout,Scale,Pid) -> Handle
+%% Handle = term()
+%%
+%% Creates a time trap, that will kill the given process if the
+%% trap is not cancelled with timetrap_cancel/1, within Timeout
+%% milliseconds.
+%% Scale says if the time should be scaled up to compensate for
+%% delays during the test (e.g. if cover is running).
+
+timetrap(Timeout0, Pid) ->
+ timetrap(Timeout0, Timeout0, true, Pid).
+
+timetrap(Timeout0, Scale, Pid) ->
+ timetrap(Timeout0, Timeout0, Scale, Pid).
+
+timetrap(Timeout0, ReportTVal, Scale, Pid) ->
+ process_flag(priority, max),
+ ct_util:mark_process(),
+ Timeout = if not Scale -> Timeout0;
+ true -> test_server:timetrap_scale_factor() * Timeout0
+ end,
+ TruncTO = trunc(Timeout),
+ receive
+ after TruncTO ->
+ kill_the_process(Pid, Timeout0, TruncTO, ReportTVal)
+ end.
+
+kill_the_process(Pid, Timeout0, TruncTO, ReportTVal) ->
+ case is_process_alive(Pid) of
+ true ->
+ TimeToReport = if Timeout0 == ReportTVal -> TruncTO;
+ true -> ReportTVal end,
+ MFLs = test_server:get_loc(Pid),
+ Mon = erlang:monitor(process, Pid),
+ Trap = {timetrap_timeout,TimeToReport,MFLs},
+ exit(Pid, Trap),
+ receive
+ {'DOWN', Mon, process, Pid, _} ->
+ ok
+ after 10000 ->
+ %% Pid is probably trapping exits, hit it harder...
+ catch error_logger:warning_msg(
+ "Testcase process ~w not "
+ "responding to timetrap "
+ "timeout:~n"
+ " ~tp.~n"
+ "Killing testcase...~n",
+ [Pid, Trap]),
+ exit(Pid, kill)
+ end;
+ false ->
+ ok
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% timetrap_cancel(Handle) -> ok
+%% Handle = term()
+%%
+%% Cancels a time trap.
+timetrap_cancel(Handle) ->
+ unlink(Handle),
+ MonRef = erlang:monitor(process, Handle),
+ exit(Handle, kill),
+ receive {'DOWN',MonRef,_,_,_} -> ok
+ after
+ 2000 ->
+ erlang:demonitor(MonRef, [flush]),
+ ok
+ end.
+
+capture_get(Msgs) ->
+ receive
+ {captured,Msg} ->
+ capture_get([Msg|Msgs])
+ after 0 ->
+ lists:reverse(Msgs)
+ end.
+
+messages_get(Msgs) ->
+ receive
+ Msg ->
+ messages_get([Msg|Msgs])
+ after 0 ->
+ lists:reverse(Msgs)
+ end.
+
+timecall(M, F, A) ->
+ {Elapsed, Val} = timer:tc(M, F, A),
+ {Elapsed / 1000000, Val}.
+
+
+call_crash(Time,Crash,M,F,A) ->
+ OldTrapExit = process_flag(trap_exit,true),
+ Pid = spawn_link(M,F,A),
+ Answer =
+ receive
+ {'EXIT',Crash} ->
+ ok;
+ {'EXIT',Pid,Crash} ->
+ ok;
+ {'EXIT',_Reason} when Crash==any ->
+ ok;
+ {'EXIT',Pid,_Reason} when Crash==any ->
+ ok;
+ {'EXIT',Reason} ->
+ test_server:format(12, "Wrong crash reason. Wanted ~tp, got ~tp.",
+ [Crash, Reason]),
+ exit({wrong_crash_reason,Reason});
+ {'EXIT',Pid,Reason} ->
+ test_server:format(12, "Wrong crash reason. Wanted ~tp, got ~tp.",
+ [Crash, Reason]),
+ exit({wrong_crash_reason,Reason});
+ {'EXIT',OtherPid,Reason} when OldTrapExit == false ->
+ exit({'EXIT',OtherPid,Reason})
+ after do_trunc(Time) ->
+ exit(call_crash_timeout)
+ end,
+ process_flag(trap_exit,OldTrapExit),
+ Answer.
+
+do_trunc(infinity) -> infinity;
+do_trunc(T) -> trunc(T).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% app_test/2
+%%
+%% Checks one applications .app file for obvious errors.
+%% Checks..
+%% * .. required fields
+%% * .. that all modules specified actually exists
+%% * .. that all requires applications exists
+%% * .. that no module included in the application has export_all
+%% * .. that all modules in the ebin/ dir is included
+%% (This only produce a warning, as all modules does not
+%% have to be included (If the `pedantic' option isn't used))
+app_test(Application, Mode) ->
+ case is_app(Application) of
+ {ok, AppFile} ->
+ do_app_tests(AppFile, Application, Mode);
+ Error ->
+ test_server:fail(Error)
+ end.
+
+is_app(Application) ->
+ case file:consult(filename:join([code:lib_dir(Application),"ebin",
+ atom_to_list(Application)++".app"])) of
+ {ok, [{application, Application, AppFile}] } ->
+ {ok, AppFile};
+ _ ->
+ test_server:format(minor,
+ "Application (.app) file not found, "
+ "or it has very bad syntax.~n"),
+ {error, not_an_application}
+ end.
+
+
+do_app_tests(AppFile, AppName, Mode) ->
+ DictList=
+ [
+ {missing_fields, []},
+ {missing_mods, []},
+ {superfluous_mods_in_ebin, []},
+ {export_all_mods, []},
+ {missing_apps, []}
+ ],
+ fill_dictionary(DictList),
+
+ %% An appfile must (?) have some fields..
+ check_fields([description, modules, registered, applications], AppFile),
+
+ %% Check for missing and extra modules.
+ {value, {modules, Mods}}=lists:keysearch(modules, 1, AppFile),
+ EBinList=lists:sort(get_ebin_modnames(AppName)),
+ {Missing, Extra} = common(lists:sort(Mods), EBinList),
+ put(superfluous_mods_in_ebin, Extra),
+ put(missing_mods, Missing),
+
+ %% Check that no modules in the application has export_all.
+ app_check_export_all(Mods),
+
+ %% Check that all specified applications exists.
+ {value, {applications, Apps}}=
+ lists:keysearch(applications, 1, AppFile),
+ check_apps(Apps),
+
+ A=check_dict(missing_fields, "Inconsistent app file, "
+ "missing fields"),
+ B=check_dict(missing_mods, "Inconsistent app file, "
+ "missing modules"),
+ C=check_dict_tolerant(superfluous_mods_in_ebin, "Inconsistent app file, "
+ "Modules not included in app file.", Mode),
+ D=check_dict(export_all_mods, "Inconsistent app file, "
+ "Modules have `export_all'."),
+ E=check_dict(missing_apps, "Inconsistent app file, "
+ "missing applications."),
+
+ erase_dictionary(DictList),
+ case A+B+C+D+E of
+ 5 ->
+ ok;
+ _ ->
+ test_server:fail()
+ end.
+
+app_check_export_all([]) ->
+ ok;
+app_check_export_all([Mod|Mods]) ->
+ case catch apply(Mod, module_info, [compile]) of
+ {'EXIT', {undef,_}} ->
+ app_check_export_all(Mods);
+ COpts ->
+ case lists:keysearch(options, 1, COpts) of
+ false ->
+ app_check_export_all(Mods);
+ {value, {options, List}} ->
+ case lists:member(export_all, List) of
+ true ->
+ put(export_all_mods, [Mod|get(export_all_mods)]),
+ app_check_export_all(Mods);
+ false ->
+ app_check_export_all(Mods)
+ end
+ end
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% appup_test/1
+%%
+%% Checks one applications .appup file for obvious errors.
+%% Checks..
+%% * .. syntax
+%% * .. that version in app file matches appup file version
+%% * .. validity of appup instructions
+%%
+%% For library application this function checks that the proper
+%% 'restart_application' upgrade and downgrade clauses exist.
+appup_test(Application) ->
+ case is_app(Application) of
+ {ok, AppFile} ->
+ case is_appup(Application, proplists:get_value(vsn, AppFile)) of
+ {ok, Up, Down} ->
+ StartMod = proplists:get_value(mod, AppFile),
+ Modules = proplists:get_value(modules, AppFile),
+ do_appup_tests(StartMod, Application, Up, Down, Modules);
+ Error ->
+ test_server:fail(Error)
+ end;
+ Error ->
+ test_server:fail(Error)
+ end.
+
+is_appup(Application, Version) ->
+ AppupFile = atom_to_list(Application) ++ ".appup",
+ AppupPath = filename:join([code:lib_dir(Application), "ebin", AppupFile]),
+ case file:consult(AppupPath) of
+ {ok, [{Version, Up, Down}]} when is_list(Up), is_list(Down) ->
+ {ok, Up, Down};
+ _ ->
+ test_server:format(
+ minor,
+ "Application upgrade (.appup) file not found, "
+ "or it has very bad syntax.~n"),
+ {error, appup_not_readable}
+ end.
+
+do_appup_tests(undefined, Application, Up, Down, _Modules) ->
+ %% library application
+ case Up of
+ [{<<".*">>, [{restart_application, Application}]}] ->
+ case Down of
+ [{<<".*">>, [{restart_application, Application}]}] ->
+ ok;
+ _ ->
+ test_server:format(
+ minor,
+ "Library application needs restart_application "
+ "downgrade instruction.~n"),
+ {error, library_downgrade_instruction_malformed}
+ end;
+ _ ->
+ test_server:format(
+ minor,
+ "Library application needs restart_application "
+ "upgrade instruction.~n"),
+ {error, library_upgrade_instruction_malformed}
+ end;
+do_appup_tests(_, _Application, Up, Down, Modules) ->
+ %% normal application
+ case check_appup_clauses_plausible(Up, up, Modules) of
+ ok ->
+ case check_appup_clauses_plausible(Down, down, Modules) of
+ ok ->
+ test_server:format(minor, "OK~n");
+ Error ->
+ test_server:format(minor, "ERROR ~tp~n", [Error]),
+ test_server:fail(Error)
+ end;
+ Error ->
+ test_server:format(minor, "ERROR ~tp~n", [Error]),
+ test_server:fail(Error)
+ end.
+
+check_appup_clauses_plausible([], _Direction, _Modules) ->
+ ok;
+check_appup_clauses_plausible([{Re, Instrs} | Rest], Direction, Modules)
+ when is_binary(Re) ->
+ case re:compile(Re,[unicode]) of
+ {ok, _} ->
+ case check_appup_instructions(Instrs, Direction, Modules) of
+ ok ->
+ check_appup_clauses_plausible(Rest, Direction, Modules);
+ Error ->
+ Error
+ end;
+ {error, Error} ->
+ {error, {version_regex_malformed, Re, Error}}
+ end;
+check_appup_clauses_plausible([{V, Instrs} | Rest], Direction, Modules)
+ when is_list(V) ->
+ case check_appup_instructions(Instrs, Direction, Modules) of
+ ok ->
+ check_appup_clauses_plausible(Rest, Direction, Modules);
+ Error ->
+ Error
+ end;
+check_appup_clauses_plausible(Clause, _Direction, _Modules) ->
+ {error, {clause_malformed, Clause}}.
+
+check_appup_instructions(Instrs, Direction, Modules) ->
+ case check_instructions(Direction, Instrs, Instrs, [], [], Modules) of
+ {_Good, []} ->
+ ok;
+ {_, Bad} ->
+ {error, {bad_instructions, Bad}}
+ end.
+
+check_instructions(_, [], _, Good, Bad, _) ->
+ {lists:reverse(Good), lists:reverse(Bad)};
+check_instructions(UpDown, [Instr | Rest], All, Good, Bad, Modules) ->
+ case catch check_instruction(UpDown, Instr, All, Modules) of
+ ok ->
+ check_instructions(UpDown, Rest, All, [Instr | Good], Bad, Modules);
+ {error, Reason} ->
+ NewBad = [{Instr, Reason} | Bad],
+ check_instructions(UpDown, Rest, All, Good, NewBad, Modules)
+ end.
+
+check_instruction(up, {add_module, Module}, _, Modules) ->
+ %% A new module is added
+ check_module(Module, Modules);
+check_instruction(down, {add_module, Module}, _, Modules) ->
+ %% An old module is re-added
+ case (catch check_module(Module, Modules)) of
+ {error, {unknown_module, Module, Modules}} -> ok;
+ ok -> throw({error, {existing_readded_module, Module}})
+ end;
+check_instruction(_, {load_module, Module}, _, Modules) ->
+ check_module(Module, Modules);
+check_instruction(_, {load_module, Module, DepMods}, _, Modules) ->
+ check_module(Module, Modules),
+ check_depend(DepMods);
+check_instruction(_, {load_module, Module, Pre, Post, DepMods}, _, Modules) ->
+ check_module(Module, Modules),
+ check_depend(DepMods),
+ check_purge(Pre),
+ check_purge(Post);
+check_instruction(up, {delete_module, Module}, _, Modules) ->
+ case (catch check_module(Module, Modules)) of
+ {error, {unknown_module, Module, Modules}} ->
+ ok;
+ ok ->
+ throw({error,{existing_module_deleted, Module}})
+ end;
+check_instruction(down, {delete_module, Module}, _, Modules) ->
+ check_module(Module, Modules);
+check_instruction(_, {update, Module}, _, Modules) ->
+ check_module(Module, Modules);
+check_instruction(_, {update, Module, supervisor}, _, Modules) ->
+ check_module(Module, Modules);
+check_instruction(_, {update, Module, DepMods}, _, Modules)
+ when is_list(DepMods) ->
+ check_module(Module, Modules);
+check_instruction(_, {update, Module, Change}, _, Modules) ->
+ check_module(Module, Modules),
+ check_change(Change);
+check_instruction(_, {update, Module, Change, DepMods}, _, Modules) ->
+ check_module(Module, Modules),
+ check_change(Change),
+ check_depend(DepMods);
+check_instruction(_, {update, Module, Change, Pre, Post, DepMods}, _, Modules) ->
+ check_module(Module, Modules),
+ check_change(Change),
+ check_purge(Pre),
+ check_purge(Post),
+ check_depend(DepMods);
+check_instruction(_,
+ {update, Module, Timeout, Change, Pre, Post, DepMods},
+ _,
+ Modules) ->
+ check_module(Module, Modules),
+ check_timeout(Timeout),
+ check_change(Change),
+ check_purge(Pre),
+ check_purge(Post),
+ check_depend(DepMods);
+check_instruction(_,
+ {update, Module, ModType, Timeout, Change, Pre, Post, DepMods},
+ _,
+ Modules) ->
+ check_module(Module, Modules),
+ check_mod_type(ModType),
+ check_timeout(Timeout),
+ check_change(Change),
+ check_purge(Pre),
+ check_purge(Post),
+ check_depend(DepMods);
+check_instruction(_, {restart_application, Application}, _, _) ->
+ check_application(Application);
+check_instruction(_, {remove_application, Application}, _, _) ->
+ check_application(Application);
+check_instruction(_, {add_application, Application}, _, _) ->
+ check_application(Application);
+check_instruction(_, {add_application, Application, Type}, _, _) ->
+ check_application(Application),
+ check_restart_type(Type);
+check_instruction(_, Instr, _, _) ->
+ throw({error, {low_level_or_invalid_instruction, Instr}}).
+
+check_module(Module, Modules) ->
+ case {is_atom(Module), lists:member(Module, Modules)} of
+ {true, true} -> ok;
+ {true, false} -> throw({error, {unknown_module, Module}});
+ {false, _} -> throw({error, {bad_module, Module}})
+ end.
+
+check_application(App) ->
+ case is_atom(App) of
+ true -> ok;
+ false -> throw({error, {bad_application, App}})
+ end.
+
+check_depend(Dep) when is_list(Dep) -> ok;
+check_depend(Dep) -> throw({error, {bad_depend, Dep}}).
+
+check_restart_type(permanent) -> ok;
+check_restart_type(transient) -> ok;
+check_restart_type(temporary) -> ok;
+check_restart_type(load) -> ok;
+check_restart_type(none) -> ok;
+check_restart_type(Type) -> throw({error, {bad_restart_type, Type}}).
+
+check_timeout(T) when is_integer(T), T > 0 -> ok;
+check_timeout(default) -> ok;
+check_timeout(infinity) -> ok;
+check_timeout(T) -> throw({error, {bad_timeout, T}}).
+
+check_mod_type(static) -> ok;
+check_mod_type(dynamic) -> ok;
+check_mod_type(Type) -> throw({error, {bad_mod_type, Type}}).
+
+check_purge(soft_purge) -> ok;
+check_purge(brutal_purge) -> ok;
+check_purge(Purge) -> throw({error, {bad_purge, Purge}}).
+
+check_change(soft) -> ok;
+check_change({advanced, _}) -> ok;
+check_change(Change) -> throw({error, {bad_change, Change}}).
+
+%% Given two sorted lists, L1 and L2, returns {NotInL2, NotInL1},
+%% NotInL2 is the elements of L1 which don't occurr in L2,
+%% NotInL1 is the elements of L2 which don't ocurr in L1.
+
+common(L1, L2) ->
+ common(L1, L2, [], []).
+
+common([X|Rest1], [X|Rest2], A1, A2) ->
+ common(Rest1, Rest2, A1, A2);
+common([X|Rest1], [Y|Rest2], A1, A2) when X < Y ->
+ common(Rest1, [Y|Rest2], [X|A1], A2);
+common([X|Rest1], [Y|Rest2], A1, A2) ->
+ common([X|Rest1], Rest2, A1, [Y|A2]);
+common([], L, A1, A2) ->
+ {A1, L++A2};
+common(L, [], A1, A2) ->
+ {L++A1, A2}.
+
+check_apps([]) ->
+ ok;
+check_apps([App|Apps]) ->
+ case is_app(App) of
+ {ok, _AppFile} ->
+ ok;
+ {error, _} ->
+ put(missing_apps, [App|get(missing_apps)])
+ end,
+ check_apps(Apps).
+
+check_fields([], _AppFile) ->
+ ok;
+check_fields([L|Ls], AppFile) ->
+ check_field(L, AppFile),
+ check_fields(Ls, AppFile).
+
+check_field(FieldName, AppFile) ->
+ case lists:keymember(FieldName, 1, AppFile) of
+ true ->
+ ok;
+ false ->
+ put(missing_fields, [FieldName|get(missing_fields)]),
+ ok
+ end.
+
+check_dict(Dict, Reason) ->
+ case get(Dict) of
+ [] ->
+ 1; % All ok.
+ List ->
+ io:format("** ~ts (~ts) ->~n~tp~n",[Reason, Dict, List]),
+ 0
+ end.
+
+check_dict_tolerant(Dict, Reason, Mode) ->
+ case get(Dict) of
+ [] ->
+ 1; % All ok.
+ List ->
+ io:format("** ~ts (~ts) ->~n~tp~n",[Reason, Dict, List]),
+ case Mode of
+ pedantic ->
+ 0;
+ _ ->
+ 1
+ end
+ end.
+
+get_ebin_modnames(AppName) ->
+ Wc=filename:join([code:lib_dir(AppName),"ebin",
+ "*"++code:objfile_extension()]),
+ TheFun=fun(X, Acc) ->
+ [list_to_atom(filename:rootname(
+ filename:basename(X)))|Acc] end,
+ _Files=lists:foldl(TheFun, [], filelib:wildcard(Wc)).
+
+%%
+%% This function removes any erl_crash_dump* files found in the
+%% test server directory. Done only once when the test server
+%% is started.
+%%
+cleanup_crash_dumps() ->
+ Dir = crash_dump_dir(),
+ Dumps = filelib:wildcard(filename:join(Dir, "erl_crash_dump*")),
+ delete_files(Dumps).
+
+crash_dump_dir() ->
+ %% If no framework is known, then we use current working directory
+ %% - in most cases that will be the same as the default log
+ %% directory.
+ {ok,Dir} = test_server_sup:framework_call(get_log_dir,[],file:get_cwd()),
+ Dir.
+
+tar_crash_dumps() ->
+ Dir = crash_dump_dir(),
+ case filelib:wildcard(filename:join(Dir, "erl_crash_dump*")) of
+ [] -> {error,no_crash_dumps};
+ Dumps ->
+ TarFileName = filename:join(Dir,?crash_dump_tar),
+ {ok,Tar} = erl_tar:open(TarFileName,[write,compressed]),
+ lists:foreach(
+ fun(File) ->
+ ok = erl_tar:add(Tar,File,filename:basename(File),[])
+ end,
+ Dumps),
+ ok = erl_tar:close(Tar),
+ delete_files(Dumps),
+ {ok,TarFileName}
+ end.
+
+
+check_new_crash_dumps() ->
+ Dir = crash_dump_dir(),
+ Dumps = filelib:wildcard(filename:join(Dir, "erl_crash_dump*")),
+ case length(Dumps) of
+ 0 ->
+ ok;
+ Num ->
+ test_server_ctrl:format(minor,
+ "Found ~w crash dumps:~n", [Num]),
+ append_files_to_logfile(Dumps),
+ delete_files(Dumps)
+ end.
+
+append_files_to_logfile([]) -> ok;
+append_files_to_logfile([File|Files]) ->
+ NodeName=from($., File),
+ test_server_ctrl:format(minor, "Crash dump from node ~tp:~n",[NodeName]),
+ Fd=get(test_server_minor_fd),
+ case file:read_file(File) of
+ {ok, Bin} ->
+ case file:write(Fd, Bin) of
+ ok ->
+ ok;
+ {error,Error} ->
+ %% Write failed. The following io:format/3 will probably also
+ %% fail, but in that case it will throw an exception so that
+ %% we will be aware of the problem.
+ io:format(Fd, "Unable to write the crash dump "
+ "to this file: ~tp~n", [file:format_error(Error)])
+ end;
+ _Error ->
+ io:format(Fd, "Failed to read: ~ts\n", [File])
+ end,
+ append_files_to_logfile(Files).
+
+delete_files([]) -> ok;
+delete_files([File|Files]) ->
+ io:format("Deleting file: ~ts~n", [File]),
+ case file:delete(File) of
+ {error, _} ->
+ case file:rename(File, File++".old") of
+ {error, Error} ->
+ io:format("Could neither delete nor rename file "
+ "~ts: ~ts.~n", [File, Error]);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ delete_files(Files).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% erase_dictionary(Vars) -> ok
+%% Vars = [atom(),...]
+%%
+%% Takes a list of dictionary keys, KeyVals, erases
+%% each key and returns ok.
+erase_dictionary([{Var, _Val}|Vars]) ->
+ erase(Var),
+ erase_dictionary(Vars);
+erase_dictionary([]) ->
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% fill_dictionary(KeyVals) -> void()
+%% KeyVals = [{atom(),term()},...]
+%%
+%% Takes each Key-Value pair, and inserts it in the process dictionary.
+fill_dictionary([{Var,Val}|Vars]) ->
+ put(Var,Val),
+ fill_dictionary(Vars);
+fill_dictionary([]) ->
+ [].
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% get_username() -> UserName
+%%
+%% Returns the current user
+get_username() ->
+ getenv_any(["USER","USERNAME"]).
+
+getenv_any([Key|Rest]) ->
+ case catch os:getenv(Key) of
+ String when is_list(String) -> String;
+ false -> getenv_any(Rest)
+ end;
+getenv_any([]) -> "".
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% get_os_family() -> OsFamily
+%%
+%% Returns the OS family
+get_os_family() ->
+ {OsFamily,_OsName} = os:type(),
+ OsFamily.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% hostatom()/hostatom(Node) -> Host; atom()
+%% hoststr() | hoststr(Node) -> Host; string()
+%%
+%% Returns the OS family
+hostatom() ->
+ hostatom(node()).
+hostatom(Node) ->
+ list_to_atom(hoststr(Node)).
+hoststr() ->
+ hoststr(node()).
+hoststr(Node) when is_atom(Node) ->
+ hoststr(atom_to_list(Node));
+hoststr(Node) when is_list(Node) ->
+ from($@, Node).
+
+from(H, [H | T]) -> T;
+from(H, [_ | T]) -> from(H, T);
+from(_H, []) -> [].
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% framework_call(Callback,Func,Args,DefaultReturn) -> Return | DefaultReturn
+%%
+%% Calls the given Func in Callback
+framework_call(Func,Args) ->
+ framework_call(Func,Args,ok).
+framework_call(Func,Args,DefaultReturn) ->
+ CB = os:getenv("TEST_SERVER_FRAMEWORK"),
+ framework_call(CB,Func,Args,DefaultReturn).
+framework_call(FW,_Func,_Args,DefaultReturn)
+ when FW =:= false; FW =:= "undefined" ->
+ DefaultReturn;
+framework_call(Callback,Func,Args,DefaultReturn) ->
+ Mod = list_to_atom(Callback),
+ _ = case code:is_loaded(Mod) of
+ false -> code:load_file(Mod);
+ _ -> ok
+ end,
+ case erlang:function_exported(Mod,Func,length(Args)) of
+ true ->
+ EH = fun(Reason) -> exit({fw_error,{Mod,Func,Reason}}) end,
+ SetTcState = case Func of
+ end_tc -> true;
+ init_tc -> true;
+ _ -> false
+ end,
+ case SetTcState of
+ true ->
+ test_server:set_tc_state({framework,Mod,Func});
+ false ->
+ ok
+ end,
+ ct_util:mark_process(),
+ try apply(Mod,Func,Args) of
+ Result ->
+ Result
+ catch
+ exit:Why ->
+ EH(Why);
+ error:Why:Stacktrace ->
+ EH({Why,Stacktrace});
+ throw:Why ->
+ EH(Why)
+ end;
+ false ->
+ DefaultReturn
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% format_loc(Loc) -> string()
+%%
+%% Formats the printout of the line of code read from
+%% process dictionary (test_server_loc). Adds link to
+%% correct line in source code.
+format_loc([{Mod,Func,Line}]) ->
+ [format_loc1({Mod,Func,Line})];
+format_loc([{Mod,Func,Line}|Rest]) ->
+ ["[",format_loc1({Mod,Func,Line}),",\n"|format_loc1(Rest)];
+format_loc([{Mod,LineOrFunc}]) ->
+ format_loc({Mod,LineOrFunc});
+format_loc({Mod,Func}) when is_atom(Func) ->
+ io_lib:format("{~w,~tw}",[Mod,Func]);
+format_loc(Loc) ->
+ io_lib:format("~tp",[Loc]).
+
+format_loc1([{Mod,Func,Line}]) ->
+ [" ",format_loc1({Mod,Func,Line}),"]"];
+format_loc1([{Mod,Func,Line}|Rest]) ->
+ [" ",format_loc1({Mod,Func,Line}),",\n"|format_loc1(Rest)];
+format_loc1({Mod,Func,Line}) ->
+ ModStr = atom_to_list(Mod),
+ case {lists:member(no_src, get(test_server_logopts)),
+ lists:reverse(ModStr)} of
+ {false,[$E,$T,$I,$U,$S,$_|_]} ->
+ Link = if is_integer(Line) ->
+ integer_to_list(Line);
+ Line == last_expr ->
+ list_to_atom(atom_to_list(Func)++"-last_expr");
+ is_atom(Line) ->
+ atom_to_list(Line);
+ true ->
+ Line
+ end,
+ io_lib:format("{~w,~tw,<a href=\"~ts~ts#~ts\">~tw</a>}",
+ [Mod,Func,
+ test_server_ctrl:uri_encode(downcase(ModStr)),
+ ?src_listing_ext,Link,Line]);
+ _ ->
+ io_lib:format("{~w,~tw,~tw}",[Mod,Func,Line])
+ end.
+
+downcase(S) -> downcase(S, []).
+downcase([Uc|Rest], Result) when $A =< Uc, Uc =< $Z ->
+ downcase(Rest, [Uc-$A+$a|Result]);
+downcase([C|Rest], Result) ->
+ downcase(Rest, [C|Result]);
+downcase([], Result) ->
+ lists:reverse(Result).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% util_start() -> ok
+%%
+%% Start local utility process
+util_start() ->
+ Starter = self(),
+ case whereis(?MODULE) of
+ undefined ->
+ spawn_link(fun() ->
+ register(?MODULE, self()),
+ put(app, common_test),
+ util_loop(#util_state{starter=Starter})
+ end),
+ ok;
+ _Pid ->
+ ok
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% util_stop() -> ok
+%%
+%% Stop local utility process
+util_stop() ->
+ try (?MODULE ! {self(),stop}) of
+ _ ->
+ receive {?MODULE,stopped} -> ok
+ after 5000 -> exit(whereis(?MODULE), kill)
+ end
+ catch
+ _:_ ->
+ ok
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% unique_name() -> string()
+%%
+unique_name() ->
+ ?MODULE ! {self(),unique_name},
+ receive {?MODULE,Name} -> Name
+ after 5000 -> exit({?MODULE,no_util_process})
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% util_loop(State) -> ok
+%%
+util_loop(State) ->
+ receive
+ {From,unique_name} ->
+ Nr = erlang:unique_integer([positive]),
+ Name = integer_to_list(Nr),
+ if Name == State#util_state.latest_name ->
+ timer:sleep(1),
+ self() ! {From,unique_name},
+ util_loop(State);
+ true ->
+ From ! {?MODULE,Name},
+ util_loop(State#util_state{latest_name = Name})
+ end;
+ {From,stop} ->
+ catch unlink(State#util_state.starter),
+ From ! {?MODULE,stopped},
+ ok
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% call_trace(TraceSpecFile) -> ok
+%%
+%% Read terms on format {m,Mod} | {f,Mod,Func}
+%% from TraceSpecFile and enable call trace for
+%% specified functions.
+call_trace(TraceSpec) ->
+ case catch try_call_trace(TraceSpec) of
+ {'EXIT',Reason} ->
+ erlang:display(Reason),
+ exit(Reason);
+ Ok ->
+ Ok
+ end.
+
+try_call_trace(TraceSpec) ->
+ case file:consult(TraceSpec) of
+ {ok,Terms} ->
+ dbg:tracer(),
+ %% dbg:p(self(), [p, m, sos, call]),
+ dbg:p(self(), [sos, call]),
+ lists:foreach(fun({m,M}) ->
+ case dbg:tpl(M,[{'_',[],[{return_trace}]}]) of
+ {error,What} -> exit({error,{tracing_failed,What}});
+ _ -> ok
+ end;
+ ({f,M,F}) ->
+ case dbg:tpl(M,F,[{'_',[],[{return_trace}]}]) of
+ {error,What} -> exit({error,{tracing_failed,What}});
+ _ -> ok
+ end;
+ (Huh) ->
+ exit({error,{unrecognized_trace_term,Huh}})
+ end, Terms),
+ ok;
+ {_,Error} ->
+ exit({error,{tracing_failed,TraceSpec,Error}})
+ end.
+
diff --git a/lib/common_test/src/unix_telnet.erl b/lib/common_test/src/unix_telnet.erl
index 09b6fd1510..7a210237a8 100644
--- a/lib/common_test/src/unix_telnet.erl
+++ b/lib/common_test/src/unix_telnet.erl
@@ -1,114 +1,55 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2014. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2018. 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 connecting to a telnet
-%%% server on a unix host.
-%%%
-%%% <p>It requires the following entry in the config file:</p>
-%%% <pre>
-%%% {unix,[{telnet,HostNameOrIpAddress},
-%%% {port,PortNum}, % optional
-%%% {username,UserName},
-%%% {password,Password},
-%%% {keep_alive,Bool}]}. % optional</pre>
-%%%
-%%% <p>To communicate via telnet to the host specified by
-%%% <code>HostNameOrIpAddress</code>, use the interface functions in
-%%% <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]}}].</pre>
-%%% <p>or</p>
-%%% <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
-%%% particular connection as described here. It may be disabled for all connections
-%%% using <c>telnet_settings</c> (see <c>ct_telnet</c>).</p>
-%%%
-%%% <p>Note that the <code>{port,PortNum}</code> tuple is optional and if
-%%% omitted, default telnet port 23 will be used. Also the <c>keep_alive</c> tuple
-%%% is optional, and the value defauls to true (enabled).</p>
-%%%
-%%% @see ct
-%%% @see ct_telnet
-module(unix_telnet).
--compile(export_all).
-
%% Callbacks for ct_telnet.erl
--export([connect/6,get_prompt_regexp/0]).
+-export([connect/7,get_prompt_regexp/0]).
-import(ct_telnet,[start_gen_log/1,log/4,end_gen_log/0]).
-define(username,"login: ").
-define(password,"Password: ").
-define(prx,"login: |Password: |\\\$ |> ").
-%%%-----------------------------------------------------------------
-%%% @spec get_prompt_regexp() -> PromptRegexp
-%%% PromptRegexp = ct_telnet:prompt_regexp()
-%%%
-%%% @doc Callback for ct_telnet.erl.
-%%%
-%%% <p>Return a suitable regexp string that will match common
-%%% prompts for users on unix hosts.</p>
get_prompt_regexp() ->
?prx.
-
-%%%-----------------------------------------------------------------
-%%% @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 = 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(ConnName,Ip,Port,Timeout,KeepAlive,Extra) ->
+connect(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay,Extra) ->
case Extra of
{Username,Password} ->
- connect1(ConnName,Ip,Port,Timeout,KeepAlive,
+ connect1(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay,
Username,Password);
KeyOrName ->
case get_username_and_password(KeyOrName) of
{ok,{Username,Password}} ->
- connect1(ConnName,Ip,Port,Timeout,KeepAlive,
+ connect1(ConnName,Ip,Port,Timeout,KeepAlive,TCPNoDelay,
Username,Password);
Error ->
Error
end
end.
-connect1(Name,Ip,Port,Timeout,KeepAlive,Username,Password) ->
+connect1(Name,Ip,Port,Timeout,KeepAlive,TCPNoDelay,Username,Password) ->
start_gen_log("unix_telnet connect"),
Result =
- case ct_telnet_client:open(Ip,Port,Timeout,KeepAlive,Name) of
+ case ct_telnet_client:open(Ip,Port,Timeout,KeepAlive,TCPNoDelay,Name) of
{ok,Pid} ->
case ct_telnet:silent_teln_expect(Name,Pid,[],
[prompt],?prx,[]) of
@@ -120,7 +61,8 @@ connect1(Name,Ip,Port,Timeout,KeepAlive,Username,Password) ->
prompt,?prx,[]) of
{ok,{prompt,?password},_} ->
ok = ct_telnet_client:send_data(Pid,Password),
- Stars = lists:duplicate(length(Password),$*),
+ Stars =
+ lists:duplicate(string:length(Password),$*),
log(Name,send,"Password: ~s",[Stars]),
% ok = ct_telnet_client:send_data(Pid,""),
case ct_telnet:silent_teln_expect(Name,Pid,[],
@@ -131,25 +73,25 @@ connect1(Name,Ip,Port,Timeout,KeepAlive,Username,Password) ->
Prompt=/=?password ->
{ok,Pid};
Error ->
- log(Name,recv,"Password failed\n~p\n",
+ log(Name,recv,"Password failed\n~tp\n",
[Error]),
{error,Error}
end;
Error ->
- log(Name,recv,"Login to ~p:~p failed\n~p\n",[Ip,Port,Error]),
+ log(Name,recv,"Login to ~p:~p failed\n~tp\n",[Ip,Port,Error]),
{error,Error}
end;
{ok,[{prompt,_OtherPrompt1},{prompt,_OtherPrompt2}],_} ->
{ok,Pid};
Error ->
log(Name,conn_error,
- "Did not get expected prompt from ~p:~p\n~p\n",
+ "Did not get expected prompt from ~p:~p\n~tp\n",
[Ip,Port,Error]),
{error,Error}
end;
Error ->
log(Name,conn_error,
- "Could not open telnet connection to ~p:~p\n~p\n",
+ "Could not open telnet connection to ~p:~p\n~tp\n",
[Ip,Port,Error]),
Error
end,
diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl
index ab13e7d0ee..38e549d2d6 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-2018. 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,7 +64,7 @@
%%%-----------------------------------------------------------------
%%% User API
start() ->
- ct_webtool:start(),
+ {ok, _} = ct_webtool:start(),
ct_webtool:start_tools([],"app=vts").
init_data(ConfigFiles,EvHandlers,LogDir,LogOpts,Tests) ->
@@ -156,6 +157,7 @@ test_info(_VtsPid,Type,Data) ->
init(Parent) ->
register(?MODULE,self()),
process_flag(trap_exit,true),
+ ct_util:mark_process(),
Parent ! {self(),started},
{ok,Cwd} = file:get_cwd(),
InitState = #state{start_dir=Cwd},
@@ -168,7 +170,7 @@ loop(State) ->
NewState = State#state{config=Config,event_handler=EvHandlers,
current_log_dir=LogDir,
logopts=LogOpts,tests=Tests},
- ct_install(NewState),
+ _ = ct_install(NewState),
return(From,ok),
loop(NewState);
{start_page,From} ->
@@ -191,12 +193,12 @@ loop(State) ->
loop(State);
{{add_config_file,Input},From} ->
{Return,State1} = add_config_file1(Input,State),
- ct_install(State1),
+ _ = ct_install(State1),
return(From,Return),
loop(State1);
{{remove_config_file,Input},From} ->
{Return,State1} = remove_config_file1(Input,State),
- ct_install(State1),
+ _ = ct_install(State1),
return(From,Return),
loop(State1);
{run_frame,From} ->
@@ -232,7 +234,7 @@ loop(State) ->
return(From,result_summary_frame1(State)),
loop(State);
stop_reload_results ->
- file:set_cwd(State#state.start_dir),
+ ok = file:set_cwd(State#state.start_dir),
loop(State#state{reload_results=false});
{no_result_log_frame,From} ->
return(From,no_result_log_frame1()),
@@ -249,7 +251,7 @@ loop(State) ->
{'EXIT',Pid,Reason} ->
case State#state.test_runner of
Pid ->
- io:format("Test run error: ~p\n",[Reason]),
+ io:format("Test run error: ~tp\n",[Reason]),
loop(State);
_ ->
loop(State)
@@ -276,13 +278,14 @@ call(Msg) ->
end.
return({To,Ref},Result) ->
- To ! {Ref, Result}.
-
+ To ! {Ref, Result},
+ ok.
run_test1(State=#state{tests=Tests,current_log_dir=LogDir,
logopts=LogOpts}) ->
Self=self(),
RunTest = fun() ->
+ ct_util:mark_process(),
case ct_run:do_run(Tests,[],LogDir,LogOpts) of
{error,_Reason} ->
aborted();
@@ -310,7 +313,6 @@ run_test1(State=#state{tests=Tests,current_log_dir=LogDir,
ct_install(#state{config=Config,event_handler=EvHandlers,
current_log_dir=LogDir}) ->
ct_run:install([{config,Config},{event_handler,EvHandlers}],LogDir).
-
%%%-----------------------------------------------------------------
%%% HTML
start_page1() ->
@@ -548,10 +550,10 @@ case_select(Dir,Suite,Case,N) ->
end,
case MakeResult of
ok ->
- code:add_pathz(Dir),
+ true = code:add_pathz(Dir),
case catch apply(Suite,all,[]) of
{'EXIT',Reason} ->
- io:format("\n~p\n",[Reason]),
+ io:format("\n~tp\n",[Reason]),
red(["COULD NOT READ TESTCASES!!",br(),
"See erlang shell for info"]);
{skip,_Reason} ->
@@ -754,7 +756,7 @@ report1(tests_start,{TestName,_N},State) ->
end,
State#state{testruns=TestRuns};
report1(tests_done,{_Ok,_Fail,_Skip},State) ->
- timer:send_after(5000, self(),stop_reload_results),
+ {ok, _} = timer:send_after(5000, self(),stop_reload_results),
State#state{running=State#state.running-1,reload_results=true};
report1(tc_start,{_Suite,_Case},State) ->
State;
@@ -917,7 +919,7 @@ get_input_data(Input,Key)->
end.
parse(Input) ->
- httpd:parse_query(Input).
+ uri_string:dissect_query(Input).
vts_integer_to_list(X) when is_atom(X) ->
atom_to_list(X);