aboutsummaryrefslogtreecommitdiffstats
path: root/lib/common_test/src
diff options
context:
space:
mode:
authorErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
committerErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
commit84adefa331c4159d432d22840663c38f155cd4c1 (patch)
treebff9a9c66adda4df2106dfd0e5c053ab182a12bd /lib/common_test/src
downloadotp-84adefa331c4159d432d22840663c38f155cd4c1.tar.gz
otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.bz2
otp-84adefa331c4159d432d22840663c38f155cd4c1.zip
The R13B03 release.OTP_R13B03
Diffstat (limited to 'lib/common_test/src')
-rw-r--r--lib/common_test/src/Makefile141
-rw-r--r--lib/common_test/src/common_test.app.src55
-rw-r--r--lib/common_test/src/common_test.appup.src1
-rw-r--r--lib/common_test/src/ct.erl789
-rw-r--r--lib/common_test/src/ct_cover.erl368
-rw-r--r--lib/common_test/src/ct_event.erl269
-rw-r--r--lib/common_test/src/ct_framework.erl1059
-rw-r--r--lib/common_test/src/ct_ftp.erl380
-rw-r--r--lib/common_test/src/ct_gen_conn.erl286
-rw-r--r--lib/common_test/src/ct_line.erl266
-rw-r--r--lib/common_test/src/ct_logs.erl1606
-rw-r--r--lib/common_test/src/ct_make.erl344
-rw-r--r--lib/common_test/src/ct_master.erl696
-rw-r--r--lib/common_test/src/ct_master_event.erl179
-rw-r--r--lib/common_test/src/ct_master_logs.erl454
-rw-r--r--lib/common_test/src/ct_master_status.erl124
-rw-r--r--lib/common_test/src/ct_repeat.erl263
-rw-r--r--lib/common_test/src/ct_rpc.erl204
-rw-r--r--lib/common_test/src/ct_run.erl1812
-rw-r--r--lib/common_test/src/ct_snmp.erl771
-rw-r--r--lib/common_test/src/ct_ssh.erl1346
-rw-r--r--lib/common_test/src/ct_telnet.erl1166
-rw-r--r--lib/common_test/src/ct_telnet_client.erl304
-rw-r--r--lib/common_test/src/ct_testspec.erl780
-rw-r--r--lib/common_test/src/ct_util.erl1303
-rw-r--r--lib/common_test/src/ct_util.hrl51
-rw-r--r--lib/common_test/src/unix_telnet.erl152
-rw-r--r--lib/common_test/src/vts.erl882
28 files changed, 16051 insertions, 0 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile
new file mode 100644
index 0000000000..e7e2d1275d
--- /dev/null
+++ b/lib/common_test/src/Makefile
@@ -0,0 +1,141 @@
+#
+# %CopyrightBegin%
+#
+# Copyright Ericsson AB 2003-2009. All Rights Reserved.
+#
+# The contents of this file are subject to the Erlang Public License,
+# Version 1.1, (the "License"); you may not use this file except in
+# compliance with the License. You should have received a copy of the
+# Erlang Public License along with this software. If not, it can be
+# retrieved online at http://www.erlang.org/.
+#
+# Software distributed under the License is distributed on an "AS IS"
+# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+# the License for the specific language governing rights and limitations
+# under the License.
+#
+# %CopyrightEnd%
+#
+
+include $(ERL_TOP)/make/target.mk
+
+# ----------------------------------------------------
+# Configuration info.
+# ----------------------------------------------------
+include $(ERL_TOP)/make/$(TARGET)/otp.mk
+
+# ----------------------------------------------------
+# Application version
+# ----------------------------------------------------
+include ../vsn.mk
+VSN=$(COMMON_TEST_VSN)
+
+# ----------------------------------------------------
+# Release directory specification
+# ----------------------------------------------------
+RELSYSDIR = $(RELEASE_PATH)/lib/common_test-$(VSN)
+
+# ----------------------------------------------------
+# Target Specs
+# ----------------------------------------------------
+
+MODULES= \
+ ct_line \
+ ct \
+ ct_logs \
+ ct_framework \
+ ct_ftp \
+ ct_ssh \
+ ct_snmp \
+ ct_gen_conn \
+ ct_rpc \
+ ct_run \
+ ct_master \
+ ct_telnet \
+ ct_util \
+ ct_cover \
+ ct_testspec \
+ ct_event \
+ ct_master_event \
+ ct_master_logs \
+ ct_master_status \
+ ct_repeat \
+ ct_telnet_client \
+ ct_make \
+ vts \
+ unix_telnet
+
+TARGET_MODULES= $(MODULES:%=$(EBIN)/%)
+
+ERL_FILES= $(MODULES:=.erl)
+HRL_FILES = \
+ ct_util.hrl
+EXTERNAL_HRL_FILES = \
+ ../include/ct.hrl \
+ ../include/ct_event.hrl
+
+EXTERNAL_INC_PATH = ../include
+
+DTD_FILES = \
+ mp.dtd
+
+# ----------------------------------------------------
+# 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
+
+# ----------------------------------------------------
+# Targets
+# ----------------------------------------------------
+TARGET_FILES = \
+ $(GEN_ERL_FILES:%.erl=$(EBIN)/%.$(EMULATOR)) \
+ $(MODULES:%=$(EBIN)/%.$(EMULATOR)) \
+ $(APP_TARGET) $(APPUP_TARGET)
+
+APP_FILE= common_test.app
+APP_SRC= $(APP_FILE).src
+APP_TARGET= $(EBIN)/$(APP_FILE)
+
+APPUP_FILE= common_test.appup
+APPUP_SRC= $(APPUP_FILE).src
+APPUP_TARGET= $(EBIN)/$(APPUP_FILE)
+
+tests debug opt: $(TARGET_FILES)
+
+docs:
+
+clean:
+ rm -f $(TARGET_FILES) $(GEN_FILES)
+ rm -f core
+
+# ----------------------------------------------------
+# Special Build Targets
+# ----------------------------------------------------
+$(APP_TARGET): $(APP_SRC) ../vsn.mk
+ sed -e 's;%VSN%;$(VSN);' $< > $@
+
+$(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk
+ sed -e 's;%VSN%;$(VSN);' $< > $@
+
+# ----------------------------------------------------
+# Release Target
+# ----------------------------------------------------
+include $(ERL_TOP)/make/otp_release_targets.mk
+
+release_spec: opt
+ $(INSTALL_DIR) $(RELSYSDIR)/src
+ $(INSTALL_DATA) $(ERL_FILES) $(HRL_FILES) $(RELSYSDIR)/src
+ $(INSTALL_DIR) $(RELSYSDIR)/ebin
+ $(INSTALL_DATA) $(TARGET_FILES) $(RELSYSDIR)/ebin
+ $(INSTALL_DIR) $(RELSYSDIR)/include
+ $(INSTALL_DATA) $(EXTERNAL_HRL_FILES) $(RELSYSDIR)/include
+
+release_tests_spec: opt
+ $(INSTALL_DIR) $(RELEASE_PATH)/common_test_test
+ $(INSTALL_DATA) $(ERL_FILES) \
+ $(HRL_FILES) $(TARGET_FILES) $(RELEASE_PATH)/common_test_test
+
+release_docs_spec: docs
+
diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src
new file mode 100644
index 0000000000..7b72932ad4
--- /dev/null
+++ b/lib/common_test/src/common_test.app.src
@@ -0,0 +1,55 @@
+% This is an -*- erlang -*- file.
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+
+{application, common_test,
+ [{description, "The OTP Common Test application"},
+ {vsn, "%VSN%"},
+ {modules, [ct_cover,
+ ct,
+ ct_event,
+ ct_framework,
+ ct_ftp,
+ ct_gen_conn,
+ ct_line,
+ ct_logs,
+ ct_make,
+ ct_master,
+ ct_master_event,
+ ct_master_logs,
+ ct_master_status,
+ ct_repeat,
+ ct_rpc,
+ ct_run,
+ ct_snmp,
+ ct_ssh,
+ ct_telnet_client,
+ ct_telnet,
+ ct_testspec,
+ ct_util,
+ unix_telnet,
+ vts
+ ]},
+ {registered, [ct_logs,
+ ct_util_server,
+ ct_make_ref,
+ vts,
+ ct_master,
+ ct_master_logs]},
+ {applications, [kernel,stdlib]},
+ {env, []}]}.
+
diff --git a/lib/common_test/src/common_test.appup.src b/lib/common_test/src/common_test.appup.src
new file mode 100644
index 0000000000..0fbe5f23f7
--- /dev/null
+++ b/lib/common_test/src/common_test.appup.src
@@ -0,0 +1 @@
+{"%VSN%",[],[]}. \ No newline at end of file
diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl
new file mode 100644
index 0000000000..8ae041e5b4
--- /dev/null
+++ b/lib/common_test/src/ct.erl
@@ -0,0 +1,789 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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 <code>config</code> macro is defined in <code>ct.hrl</code>. This
+%%% macro should be used to retrieve information from the
+%%% <code>Config</code> 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 <code>Config</code>
+%%% variable supplied to the test case.</p>
+%%%
+%%% <p>Possible configuration variables include:</p>
+%%% <ul>
+%%% <li><code>data_dir</code> - Data file directory.</li>
+%%% <li><code>priv_dir</code> - Scratch file directory.</li>
+%%% <li>Whatever added by <code>init_per_suite/1</code> or
+%%% <code>init_per_testcase/2</code> in the test suite.</li>
+%%% </ul>
+
+%%% @type var_name() = atom(). A variable name which is specified when
+%%% <code>ct:require/2</code> is called,
+%%% e.g. <code>ct:require(mynodename,{node,[telnet]})</code>
+%%%
+%%% @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).
+
+%% Command line user interface for running tests
+-export([install/1, run/1, run/2, run/3,
+ run_test/1, run_testspec/1, step/3, step/4,
+ start_interactive/0, stop_interactive/0]).
+
+%% Test suite API
+-export([require/1, require/2,
+ get_config/1, get_config/2, get_config/3,
+ log/1, log/2, log/3,
+ print/1, print/2, print/3,
+ pal/1, pal/2, pal/3,
+ fail/1, comment/1,
+ testcases/2, userdata/2, userdata/3]).
+
+%% Other interface functions
+-export([get_status/0, abort_current_testcase/1,
+ encrypt_config_file/2, encrypt_config_file/3,
+ decrypt_config_file/2, decrypt_config_file/3]).
+
+
+-export([get_target_name/1]).
+-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/>
+%%% <code>install([{config,["config_node.ctc","config_user.ctc"]}])</code>.</p>
+%%%
+%%% <p>Note that this function is automatically run by the
+%%% <code>run_test</code> script.</p>
+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 testcase(s).
+%%%
+%%% <p>Requires that <code>ct:install/1</code> has been run first.</p>
+%%%
+%%% <p>Suites (*_SUITE.erl) files must be stored in
+%%% <code>TestDir</code> or <code>TestDir/test</code>. 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 testcases 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 testcases in all suites in the given directories.
+%%% @see run/3.
+run(TestDirs) ->
+ ct_run:run(TestDirs).
+
+%%%-----------------------------------------------------------------
+%%% @spec run_test(Opts) -> Result
+%%% Opts = [OptTuples]
+%%% OptTuples = {config,CfgFiles} | {dir,TestDirs} | {suite,Suites} |
+%%% {testcase,Cases} | {group,Groups} | {spec,TestSpecs} |
+%%% {allow_user_terms,Bool} | {logdir,LogDir} |
+%%% {silent_connections,Conns} | {cover,CoverSpecFile} |
+%%% {step,StepOpts} | {event_handler,EventHandlers} | {include,InclDirs} |
+%%% {auto_compile,Bool} | {repeat,N} | {duration,DurTime} |
+%%% {until,StopTime} | {force_stop,Bool} | {decrypt,DecryptKeyOrFile} |
+%%% {refresh_logs,LogDir} | {basic_html,Bool}
+%%% CfgFiles = [string()] | string()
+%%% TestDirs = [string()] | string()
+%%% Suites = [string()] | string()
+%%% Cases = [atom()] | atom()
+%%% Groups = [atom()] | atom()
+%%% TestSpecs = [string()] | string()
+%%% LogDir = string()
+%%% Conns = all | [atom()]
+%%% CoverSpecFile = string()
+%%% StepOpts = [StepOpt] | []
+%%% StepOpt = config | keep_inactive
+%%% EventHandlers = EH | [EH]
+%%% EH = atom() | {atom(),InitArgs} | {[atom()],InitArgs}
+%%% InitArgs = [term()]
+%%% InclDirs = [string()] | string()
+%%% N = integer()
+%%% DurTime = string(HHMMSS)
+%%% StopTime = string(YYMoMoDDHHMMSS) | string(HHMMSS)
+%%% DecryptKeyOrFile = {key,DecryptKey} | {file,DecryptFile}
+%%% DecryptKey = string()
+%%% DecryptFile = string()
+%%% Result = [TestResult] | {error,Reason}
+%%% @doc Run tests as specified by the combination of options in <code>Opts</code>.
+%%% The options are the same as those used with the <code>run_test</code> script.
+%%% Note that here a <code>TestDir</code> can be used to point out the path to
+%%% a <code>Suite</code>. Note also that the option <code>testcase</code>
+%%% corresponds to the <code>-case</code> option in the <code>run_test</code>
+%%% script. Configuration files specified in <code>Opts</code> will be
+%%% installed automatically at startup.
+run_test(Opts) ->
+ ct_run:run_test(Opts).
+
+%%%-----------------------------------------------------------------
+%%% @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.
+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
+%%% <code>config</code> option has been given, breakpoints will
+%%% be set also on the configuration functions in <code>Suite</code>.
+%%% @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 unix command line with <code>run_test -shell
+%%% [-config File...]</code>.</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 <code>ct:require/2</code>.</p>
+%%%
+%%% <p>Example:<br/>
+%%% <code>&gt; ct:require(unix_telnet, unix).</code><br/>
+%%% <code>ok</code><br/>
+%%% <code>&gt; ct_telnet:open(unix_telnet).</code><br/>
+%%% <code>{ok,&lt;0.105.0&gt;}</code><br/>
+%%% <code>&gt; ct_telnet:cmd(unix_telnet, "ls .").</code><br/>
+%%% <code>{ok,["ls","file1 ...",...]}</code></p>
+start_interactive() ->
+ ct_util:start(interactive).
+
+%%%-----------------------------------------------------------------
+%%% @spec stop_interactive() -> ok
+%%%
+%%% @doc Exit the interactive mode.
+%%% @see start_interactive/0
+stop_interactive() ->
+ ct_util:stop(normal).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% MISC INTERFACE
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%-----------------------------------------------------------------
+%%% @spec require(Required) -> ok | {error,Reason}
+%%% Required = Key | {Key,SubKeys}
+%%% Key = atom()
+%%% SubKeys = SubKey | [SubKey]
+%%% SubKey = atom()
+%%%
+%%% @doc Check if the required configuration is available.
+%%%
+%%% <p>Example: require the variable <code>myvar</code>:<br/>
+%%% <code>ok = ct:require(myvar)</code></p>
+%%%
+%%% <p>In this case the config file must at least contain:</p>
+%%% <pre>
+%%% {myvar,Value}.</pre>
+%%%
+%%% <p>Example: require the variable <code>myvar</code> with
+%%% subvariable <code>sub1</code>:<br/>
+%%% <code>ok = ct:require({myvar,sub1})</code></p>
+%%%
+%%% <p>In this case the config file must at least contain:</p>
+%%% <pre>
+%%% {myvar,[{sub1,Value}]}.</pre>
+%%%
+%%% @see require/2
+%%% @see get_config/1
+%%% @see get_config/2
+%%% @see get_config/3
+require(Required) ->
+ ct_util:require(Required).
+
+%%%-----------------------------------------------------------------
+%%% @spec require(Name,Required) -> ok | {error,Reason}
+%%% Name = atom()
+%%% Required = Key | {Key,SubKeys}
+%%% Key = atom()
+%%% SubKeys = SubKey | [SubKey]
+%%% SubKey = atom()
+%%%
+%%% @doc Check if the required configuration is available, and give it
+%%% a name.
+%%%
+%%% <p>If the requested data is available, the main entry will be
+%%% associated with <code>Name</code> so that the value of the element
+%%% can be read with <code>get_config/1,2</code> provided
+%%% <code>Name</code> instead of the <code>Key</code>.</p>
+%%%
+%%% <p>Example: Require one node with a telnet connection and an
+%%% ftp connection. Name the node <code>a</code>:<br/> <code>ok =
+%%% ct:require(a,{node,[telnet,ftp]}).</code><br/> All references
+%%% to this node may then use the node name. E.g. you can fetch a
+%%% file over ftp like this:<br/>
+%%% <code>ok = ct:ftp_get(a,RemoteFile,LocalFile).</code></p>
+%%%
+%%% <p>For this to work, the config file must at least contain:</p>
+%%% <pre>
+%%% {node,[{telnet,IpAddr},
+%%% {ftp,IpAddr}]}.</pre>
+%%%
+%%% @see require/1
+%%% @see get_config/1
+%%% @see get_config/2
+%%% @see get_config/3
+require(Name,Required) ->
+ ct_util:require(Name,Required).
+
+%%%-----------------------------------------------------------------
+%%% @spec get_config(Required) -> Value
+%%% @equiv get_config(Required,undefined,[])
+get_config(Required) ->
+ ct_util:get_config(Required,undefined,[]).
+
+%%%-----------------------------------------------------------------
+%%% @spec get_config(Required,Default) -> Value
+%%% @equiv get_config(Required,Default,[])
+get_config(Required,Default) ->
+ ct_util:get_config(Required,Default,[]).
+
+%%%-----------------------------------------------------------------
+%%% @spec get_config(Required,Default,Opts) -> ValueOrElement
+%%% Required = KeyOrName | {KeyOrName,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 <code>require/2</code> or a
+%%% require statement).</p>
+%%%
+%%% <p>Example, given the following config file:</p>
+%%% <pre>
+%%% {unix,[{telnet,IpAddr},
+%%% {username,Username},
+%%% {password,Password}]}.</pre>
+%%% <p><code>get_config(unix,Default) ->
+%%% [{telnet,IpAddr},
+%%% {username,Username},
+%%% {password,Password}]</code><br/>
+%%% <code>get_config({unix,telnet},Default) -> IpAddr</code><br/>
+%%% <code>get_config({unix,ftp},Default) -> Default</code><br/>
+%%% <code>get_config(unknownkey,Default) -> Default</code></p>
+%%%
+%%% <p>If a config variable key has been associated with a name (by
+%%% means of <code>require/2</code> or a require statement), the name
+%%% may be used instead of the key to read the value:</p>
+%%%
+%%% <p><code>require(myhost,unix) -> ok</code><br/>
+%%% <code>get_config(myhost,Default) ->
+%%% [{telnet,IpAddr},
+%%% {username,Username},
+%%% {password,Password}]</code></p>
+%%%
+%%% <p>If a config variable is defined in multiple files and you want to
+%%% access all possible values, use the <code>all</code> 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 <code>element</code> option.
+%%% The returned elements will then be on the form <code>{KeyOrName,Value}</code>,
+%%% or (in case a subkey has been specified)
+%%% <code>{{KeyOrName,SubKey},Value}</code></p>
+%%%
+%%% @see get_config/1
+%%% @see get_config/2
+%%% @see require/1
+%%% @see require/2
+get_config(Required,Default,Opts) ->
+ ct_util:get_config(Required,Default,Opts).
+
+%%%-----------------------------------------------------------------
+%%% @spec log(Format) -> ok
+%%% @equiv log(default,Format,[])
+log(Format) ->
+ log(default,Format,[]).
+
+%%%-----------------------------------------------------------------
+%%% @spec log(X1,X2) -> ok
+%%% X1 = Category | Format
+%%% X2 = Format | Args
+%%% @equiv log(Category,Format,Args)
+log(X1,X2) ->
+ {Category,Format,Args} =
+ if is_atom(X1) -> {X1,X2,[]};
+ is_list(X1) -> {default,X1,X2}
+ end,
+ log(Category,Format,Args).
+
+%%%-----------------------------------------------------------------
+%%% @spec log(Category,Format,Args) -> ok
+%%% Category = atom()
+%%% Format = string()
+%%% Args = list()
+%%%
+%%% @doc Printout from a testcase to the log.
+%%%
+%%% <p>This function is meant for printing stuff directly from a
+%%% testcase (i.e. not from within the CT framework) in the test
+%%% log.</p>
+%%%
+%%% <p>Default <code>Category</code> is <code>default</code> and
+%%% default <code>Args</code> is <code>[]</code>.</p>
+log(Category,Format,Args) ->
+ ct_logs:tc_log(Category,Format,Args).
+
+
+%%%-----------------------------------------------------------------
+%%% @spec print(Format) -> ok
+%%% @equiv print(default,Format,[])
+print(Format) ->
+ print(default,Format,[]).
+
+%%%-----------------------------------------------------------------
+%%% @equiv print(Category,Format,Args)
+print(X1,X2) ->
+ {Category,Format,Args} =
+ if is_atom(X1) -> {X1,X2,[]};
+ is_list(X1) -> {default,X1,X2}
+ end,
+ print(Category,Format,Args).
+
+%%%-----------------------------------------------------------------
+%%% @spec print(Category,Format,Args) -> ok
+%%% Category = atom()
+%%% Format = string()
+%%% Args = list()
+%%%
+%%% @doc Printout from a testcase to the console.
+%%%
+%%% <p>This function is meant for printing stuff from a testcase on
+%%% the console.</p>
+%%%
+%%% <p>Default <code>Category</code> is <code>default</code> and
+%%% default <code>Args</code> is <code>[]</code>.</p>
+print(Category,Format,Args) ->
+ ct_logs:tc_print(Category,Format,Args).
+
+
+%%%-----------------------------------------------------------------
+%%% @spec pal(Format) -> ok
+%%% @equiv pal(default,Format,[])
+pal(Format) ->
+ pal(default,Format,[]).
+
+%%%-----------------------------------------------------------------
+%%% @spec pal(X1,X2) -> ok
+%%% X1 = Category | Format
+%%% X2 = Format | Args
+%%% @equiv pal(Category,Format,Args)
+pal(X1,X2) ->
+ {Category,Format,Args} =
+ if is_atom(X1) -> {X1,X2,[]};
+ is_list(X1) -> {default,X1,X2}
+ end,
+ pal(Category,Format,Args).
+
+%%%-----------------------------------------------------------------
+%%% @spec pal(Category,Format,Args) -> ok
+%%% Category = atom()
+%%% Format = string()
+%%% Args = list()
+%%%
+%%% @doc Print and log from a testcase.
+%%%
+%%% <p>This function is meant for printing stuff from a testcase both
+%%% in the log and on the console.</p>
+%%%
+%%% <p>Default <code>Category</code> is <code>default</code> and
+%%% default <code>Args</code> is <code>[]</code>.</p>
+pal(Category,Format,Args) ->
+ ct_logs:tc_pal(Category,Format,Args).
+
+
+%%%-----------------------------------------------------------------
+%%% @spec fail(Reason) -> void()
+%%% Reason = term()
+%%%
+%%% @doc Terminate a test case with the given error
+%%% <code>Reason</code>.
+fail(Reason) ->
+ exit({test_case_failed,Reason}).
+
+%%%-----------------------------------------------------------------
+%%% @spec comment(Comment) -> void()
+%%% Comment = term()
+%%%
+%%% @doc Print the given <code>Comment</code> in the comment field of
+%%% the table on the test suite result page.
+%%%
+%%% <p>If called several times, only the last comment is printed.
+%%% <code>comment/1</code> is also overwritten by the return value
+%%% <code>{comment,Comment}</code> or by the function
+%%% <code>fail/1</code> (which prints <code>Reason</code> as a
+%%% comment).</p>
+comment(Comment) when is_list(Comment) ->
+ Formatted =
+ case (catch io_lib:format("~s",[Comment])) of
+ {'EXIT',_} -> % it's a list not a string
+ io_lib:format("~p",[Comment]);
+ String ->
+ String
+ end,
+ send_html_comment(lists:flatten(Formatted));
+comment(Comment) ->
+ Formatted = io_lib:format("~p",[Comment]),
+ send_html_comment(lists:flatten(Formatted)).
+
+send_html_comment(Comment) ->
+ Html = "<font color=\"green\">" ++ Comment ++ "</font>",
+ ct_util:set_testdata({comment,Html}),
+ test_server:comment(Html).
+
+
+%%%-----------------------------------------------------------------
+%%% @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
+%%% <code>select</code> command in SQL. The returned
+%%% <code>Table</code> is a list of tuples, where each tuple is a row
+%%% in the table.</p>
+%%%
+%%% <p><code>Heading</code> is a tuple of strings representing the
+%%% headings of each column in the table.</p>
+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 testcases in the specified suite.
+testcases(TestDir, Suite) ->
+ case make_and_load(TestDir, Suite) of
+ E = {error,_} ->
+ E;
+ _ ->
+ case (catch Suite:all()) of
+ {'EXIT',Reason} ->
+ {error,Reason};
+ TCs ->
+ TCs
+ end
+ end.
+
+make_and_load(Dir, Suite) ->
+ EnvInclude =
+ case os:getenv("CT_INCLUDE_PATH") of
+ false -> [];
+ CtInclPath -> string:tokens(CtInclPath, [$:,$ ,$,])
+ end,
+ StartInclude =
+ case init:get_argument(include) of
+ {ok,[Dirs]} -> Dirs;
+ _ -> []
+ end,
+ UserInclude = EnvInclude ++ StartInclude,
+ case ct_run:run_make(Dir, Suite, UserInclude) of
+ MErr = {error,_} ->
+ MErr;
+ _ ->
+ TestDir = ct_util:get_testdir(Dir, Suite),
+ File = filename:join(TestDir, atom_to_list(Suite)),
+ case code:soft_purge(Suite) of
+ true ->
+ code:load_abs(File);
+ false -> % will use loaded
+ {module,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 <code>userdata</code>
+%%% in the list of tuples returned from <code>Suite:suite/0</code>.
+userdata(TestDir, Suite) ->
+ case make_and_load(TestDir, Suite) of
+ E = {error,_} ->
+ E;
+ _ ->
+ Info = (catch Suite:suite()),
+ get_userdata(Info, "suite/0")
+ end.
+
+get_userdata({'EXIT',{undef,_}}, Spec) ->
+ {error,list_to_atom(Spec ++ " is not defined")};
+get_userdata({'EXIT',Reason}, Spec) ->
+ {error,{list_to_atom("error in " ++ Spec),Reason}};
+get_userdata(List, _) when is_list(List) ->
+ Fun = fun({userdata,Data}, Acc) -> [Data | Acc];
+ (_, Acc) -> Acc
+ end,
+ case lists:foldl(Fun, [], List) of
+ Terms ->
+ lists:flatten(lists:reverse(Terms))
+ end;
+get_userdata(_BadTerm, Spec) ->
+ {error,list_to_atom(Spec ++ " must return a list")}.
+
+%%%-----------------------------------------------------------------
+%%% @spec userdata(TestDir, Suite, Case) -> TCUserData | {error,Reason}
+%%% TestDir = string()
+%%% Suite = atom()
+%%% Case = atom()
+%%% TCUserData = [term()]
+%%% Reason = term()
+%%%
+%%% @doc Returns any data specified with the tag <code>userdata</code>
+%%% in the list of tuples returned from <code>Suite:Case/0</code>.
+userdata(TestDir, Suite, Case) ->
+ case make_and_load(TestDir, Suite) of
+ E = {error,_} ->
+ E;
+ _ ->
+ Info = (catch apply(Suite, Case, [])),
+ get_userdata(Info, atom_to_list(Case)++"/0")
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @spec get_status() -> TestStatus | {error,Reason}
+%%% TestStatus = [StatusElem]
+%%% StatusElem = {current,{Suite,TestCase}} | {successful,Successful} |
+%%% {failed,Failed} | {skipped,Skipped} | {total,Total}
+%%% 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, as well as counters for
+%%% successful, failed, skipped, and total test cases so far.
+get_status() ->
+ case get_testdata(curr_tc) of
+ {ok,TestCase} ->
+ case get_testdata(stats) of
+ {ok,{Ok,Failed,Skipped={UserSkipped,AutoSkipped}}} ->
+ [{current,TestCase},
+ {successful,Ok},
+ {failed,Failed},
+ {skipped,Skipped},
+ {total,Ok+Failed+UserSkipped+AutoSkipped}];
+ Err1 -> Err1
+ end;
+ Err2 -> Err2
+ end.
+
+get_testdata(Key) ->
+ case catch ct_util:get_testdata(Key) of
+ Error = {error,_Reason} ->
+ Error;
+ {'EXIT',_Reason} ->
+ no_tests_running;
+ Data ->
+ {ok,Data}
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec abort_current_testcase(Reason) -> ok | {error,no_testcase_running}
+%%% Reason = term()
+%%%
+%%% @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><code>Reason</code>, 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 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 <code>EncryptFileName</code>. The key,
+%%% a string, must be available in a text file named
+%%% <code>.ct_config.crypt</code> 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 <code>crypto</code> application for details on DES3
+%%% encryption/decryption.</p>
+encrypt_config_file(SrcFileName, EncryptFileName) ->
+ ct_util: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 <code>EncryptFileName</code>.
+%%% The encryption key to use is either the value in
+%%% <code>{key,Key}</code> or the value stored in the file specified
+%%% by <code>{file,File}</code>.</p>
+%%% <p>See the Common Test User's Guide for information about using
+%%% encrypted config files when running tests.</p>
+%%% <p>See the <code>crypto</code> application for details on DES3
+%%% encryption/decryption.</p>
+encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile) ->
+ ct_util: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 <code>EncryptFileName</code>, previously
+%%% generated with <code>encrypt_config_file/2/3</code>. The original
+%%% file contents is saved in the target file. The encryption key, a
+%%% string, must be available in a text file named
+%%% <code>.ct_config.crypt</code> 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_util: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 <code>EncryptFileName</code>, previously
+%%% generated with <code>encrypt_config_file/2/3</code>. 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_util:decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile).
+
diff --git a/lib/common_test/src/ct_cover.erl b/lib/common_test/src/ct_cover.erl
new file mode 100644
index 0000000000..d39f50ba00
--- /dev/null
+++ b/lib/common_test/src/ct_cover.erl
@@ -0,0 +1,368 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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]).
+
+-include("ct_util.hrl").
+
+-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) ->
+ case whereis(cover_server) of
+ undefined ->
+ {error,cover_not_running};
+ _ ->
+ {File,Nodes0,Import,Export,AppInfo} = ct_util:get_testdata(cover),
+ Nodes1 = [Node || Node <- Nodes,
+ lists:member(Node,Nodes0) == false],
+ ct_logs:log("COVER INFO",
+ "Adding nodes to cover test: ~w", [Nodes1]),
+ case cover:start(Nodes1) of
+ Result = {ok,_} ->
+ ct_util:set_testdata({cover,{File,Nodes1++Nodes0,
+ Import,Export,AppInfo}}),
+
+ Result;
+ Error ->
+ Error
+ 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) ->
+ case whereis(cover_server) of
+ undefined ->
+ {error,cover_not_running};
+ _ ->
+ {File,Nodes0,Import,Export,AppInfo} = ct_util:get_testdata(cover),
+ ToRemove = [Node || Node <- Nodes, lists:member(Node,Nodes0)],
+ ct_logs:log("COVER INFO",
+ "Removing nodes from cover test: ~w", [ToRemove]),
+ case cover:stop(ToRemove) of
+ ok ->
+ Nodes1 = lists:foldl(fun(N,Deleted) ->
+ lists:delete(N,Deleted)
+ end, Nodes0, ToRemove),
+ ct_util:set_testdata({cover,{File,Nodes1,
+ Import,Export,AppInfo}}),
+ ok;
+ Error ->
+ Error
+ end
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+
+%% Read cover specification file and return the parsed info.
+%% -> CoverSpec: {CoverFile,Nodes,Import,Export,AppCoverInfo}
+get_spec(File) ->
+ catch get_spec_test(File).
+
+get_spec_test(File) ->
+ FullName = filename:absname(File),
+ case filelib:is_file(FullName) of
+ true ->
+ case file:consult(FullName) of
+ {ok,Terms} ->
+ Import =
+ case lists:keysearch(import, 1, Terms) of
+ {value,{_,Imps=[S|_]}} when is_list(S) ->
+ ImpsFN = lists:map(fun(F) ->
+ filename:absname(F)
+ end, Imps),
+ test_files(ImpsFN, ImpsFN);
+ {value,{_,Imp=[IC|_]}} when is_integer(IC) ->
+ ImpFN = filename:absname(Imp),
+ test_files([ImpFN], [ImpFN]);
+ _ ->
+ []
+ end,
+ Export =
+ case lists:keysearch(export, 1, Terms) of
+ {value,{_,Exp=[EC|_]}} when is_integer(EC) ->
+ filename:absname(Exp);
+ {value,{_,[Exp]}} ->
+ filename:absname(Exp);
+ _ ->
+ []
+ end,
+ Nodes =
+ case lists:keysearch(nodes, 1, Terms) of
+ {value,{_,Ns}} ->
+ Ns;
+ _ ->
+ []
+ end,
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %% NOTE! We can read specifications with multiple %%
+ %% apps, but since we don't have support for %%
+ %% running cover on more than one app at a time, %%
+ %% we just allow 1 app per spec for now. %%
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ case collect_apps(Terms, []) of
+ Res when Res == [] ; length(Res) == 1 -> % 1 app = ok
+ Apps = case Res of
+ [] -> [#cover{app=none, level=details}];
+ _ -> Res
+ end,
+ case get_cover_opts(Apps, Terms, []) of
+ E = {error,_} ->
+ E;
+ [CoverSpec] ->
+ CoverSpec1 = remove_excludes_and_dups(CoverSpec),
+ {FullName,Nodes,Import,Export,CoverSpec1};
+ _ ->
+ {error,multiple_apps_in_cover_spec}
+ end;
+ Apps when is_list(Apps) ->
+ {error,multiple_apps_in_cover_spec}
+ end;
+ Error -> % file:consult/1 fails
+ {error,{invalid_cover_spec,Error}}
+ end;
+ false ->
+ {error,{cant_read_cover_spec_file,FullName}}
+ end.
+
+collect_apps([{level,Level}|Ts], Apps) ->
+ collect_apps(Ts, [#cover{app=none, level=Level}|Apps]);
+collect_apps([{incl_app,App,Level}|Ts], Apps) ->
+ collect_apps(Ts, [#cover{app=App, level=Level}|Apps]);
+collect_apps([_|Ts], Apps) ->
+ collect_apps(Ts, Apps);
+collect_apps([], Apps) ->
+ Apps.
+
+%% get_cover_opts(Terms) -> AppCoverInfo
+%% AppCoverInfo: [#cover{app=App,...}]
+
+get_cover_opts([App | Apps], Terms, CoverInfo) ->
+ case get_app_info(App, Terms) of
+ E = {error,_} -> E;
+ AppInfo ->
+ AppInfo1 = files2mods(AppInfo),
+ get_cover_opts(Apps, Terms, [AppInfo1|CoverInfo])
+ end;
+get_cover_opts([], _, CoverInfo) ->
+ lists:reverse(CoverInfo).
+
+%% get_app_info(App, Terms) -> App1
+
+get_app_info(App=#cover{app=none}, [{incl_dirs,Dirs}|Terms]) ->
+ get_app_info(App, [{incl_dirs,none,Dirs}|Terms]);
+get_app_info(App=#cover{app=Name}, [{incl_dirs,Name,Dirs}|Terms]) ->
+ case get_files(Dirs, ".beam", false, []) of
+ E = {error,_} -> E;
+ Mods1 ->
+ Mods = App#cover.incl_mods,
+ get_app_info(App#cover{incl_mods=Mods++Mods1},Terms)
+ end;
+
+get_app_info(App=#cover{app=none}, [{incl_dirs_r,Dirs}|Terms]) ->
+ get_app_info(App, [{incl_dirs_r,none,Dirs}|Terms]);
+get_app_info(App=#cover{app=Name}, [{incl_dirs_r,Name,Dirs}|Terms]) ->
+ case get_files(Dirs, ".beam", true, []) of
+ E = {error,_} -> E;
+ Mods1 ->
+ Mods = App#cover.incl_mods,
+ get_app_info(App#cover{incl_mods=Mods++Mods1},Terms)
+ end;
+
+get_app_info(App=#cover{app=none}, [{incl_mods,Mods1}|Terms]) ->
+ get_app_info(App, [{incl_mods,none,Mods1}|Terms]);
+get_app_info(App=#cover{app=Name}, [{incl_mods,Name,Mods1}|Terms]) ->
+ Mods = App#cover.incl_mods,
+ get_app_info(App#cover{incl_mods=Mods++Mods1},Terms);
+
+get_app_info(App=#cover{app=none}, [{excl_dirs,Dirs}|Terms]) ->
+ get_app_info(App, [{excl_dirs,none,Dirs}|Terms]);
+get_app_info(App=#cover{app=Name}, [{excl_dirs,Name,Dirs}|Terms]) ->
+ case get_files(Dirs, ".beam", false, []) of
+ E = {error,_} -> E;
+ Mods1 ->
+ Mods = App#cover.excl_mods,
+ get_app_info(App#cover{excl_mods=Mods++Mods1},Terms)
+ end;
+
+get_app_info(App=#cover{app=none}, [{excl_dirs_r,Dirs}|Terms]) ->
+ get_app_info(App, [{excl_dirs_r,none,Dirs}|Terms]);
+get_app_info(App=#cover{app=Name}, [{excl_dirs_r,Name,Dirs}|Terms]) ->
+ case get_files(Dirs, ".beam", true, []) of
+ E = {error,_} -> E;
+ Mods1 ->
+ Mods = App#cover.excl_mods,
+ get_app_info(App#cover{excl_mods=Mods++Mods1},Terms)
+ end;
+
+get_app_info(App=#cover{app=none}, [{excl_mods,Mods1}|Terms]) ->
+ get_app_info(App, [{excl_mods,none,Mods1}|Terms]);
+get_app_info(App=#cover{app=Name}, [{excl_mods,Name,Mods1}|Terms]) ->
+ Mods = App#cover.excl_mods,
+ get_app_info(App#cover{excl_mods=Mods++Mods1},Terms);
+
+get_app_info(App=#cover{app=Name}, [{cross_apps,Name,AppMods1}|Terms]) ->
+ AppMods = App#cover.cross,
+ get_app_info(App#cover{cross=AppMods++AppMods1},Terms);
+
+get_app_info(App=#cover{app=none}, [{src_dirs,Dirs}|Terms]) ->
+ get_app_info(App, [{src_dirs,none,Dirs}|Terms]);
+get_app_info(App=#cover{app=Name}, [{src_dirs,Name,Dirs}|Terms]) ->
+ case get_files(Dirs, ".erl", false, []) of
+ E = {error,_} -> E;
+ Src1 ->
+ Src = App#cover.src,
+ get_app_info(App#cover{src=Src++Src1},Terms)
+ end;
+
+get_app_info(App=#cover{app=none}, [{src_dirs_r,Dirs}|Terms]) ->
+ get_app_info(App, [{src_dirs_r,none,Dirs}|Terms]);
+get_app_info(App=#cover{app=Name}, [{src_dirs_r,Name,Dirs}|Terms]) ->
+ case get_files(Dirs, ".erl", true, []) of
+ E = {error,_} -> E;
+ Src1 ->
+ Src = App#cover.src,
+ get_app_info(App#cover{src=Src++Src1},Terms)
+ end;
+
+get_app_info(App=#cover{app=none}, [{src_files,Src1}|Terms]) ->
+ get_app_info(App, [{src_files,none,Src1}|Terms]);
+get_app_info(App=#cover{app=Name}, [{src_files,Name,Src1}|Terms]) ->
+ Src = App#cover.src,
+ get_app_info(App#cover{src=Src++Src1},Terms);
+
+get_app_info(App, [_|Terms]) ->
+ get_app_info(App, Terms);
+
+get_app_info(App, []) ->
+ App.
+
+%% get_files(...)
+
+get_files([Dir|Dirs], Ext, Recurse, Files) ->
+ case file:list_dir(Dir) of
+ {ok,Entries} ->
+ {SubDirs,Matches} = analyse_files(Entries, Dir, Ext, [], []),
+ if Recurse == false ->
+ get_files(Dirs, Ext, Recurse, Files++Matches);
+ true ->
+ Files1 = get_files(SubDirs, Ext, Recurse, Files++Matches),
+ get_files(Dirs, Ext, Recurse, Files1)
+ end;
+ {error,Reason} ->
+ {error,{Reason,Dir}}
+ end;
+get_files([], _Ext, _R, Files) ->
+ Files.
+
+%% analyse_files(...)
+
+analyse_files([F|Fs], Dir, Ext, Dirs, Matches) ->
+ Fullname = filename:absname(F, Dir),
+ {ok,Info} = file:read_file_info(Fullname),
+ case Info#file_info.type of
+ directory ->
+ analyse_files(Fs, Dir, Ext,
+ [Fullname|Dirs], Matches);
+ _ ->
+ case filename:extension(Fullname) of
+ ".beam" when Ext == ".beam" ->
+ %% File = {file,Dir,filename:rootname(F)},
+ Mod = list_to_atom(filename:rootname(F)),
+ analyse_files(Fs, Dir, Ext, Dirs,
+ [Mod|Matches]);
+ ".erl" when Ext == ".erl" ->
+ analyse_files(Fs, Dir, Ext, Dirs,
+ [Fullname|Matches]);
+ _ ->
+ analyse_files(Fs, Dir, Ext, Dirs, Matches)
+ end
+ end;
+analyse_files([], _Dir, _Ext, Dirs, Matches) ->
+ {Dirs,Matches}.
+
+
+test_files([F|Fs], Ret) ->
+ case filelib:is_file(F) of
+ true ->
+ test_files(Fs, Ret);
+ false ->
+ throw({error,{invalid_cover_file,F}})
+ end;
+test_files([], Ret) ->
+ Ret.
+
+remove_excludes_and_dups(CoverData=#cover{excl_mods=Excl,incl_mods=Incl}) ->
+ Incl1 = [Mod || Mod <- Incl, lists:member(Mod, Excl) == false],
+ %% delete duplicates and sort
+ Incl2 = lists:sort(lists:foldl(fun(M,L) ->
+ case lists:member(M,L) of
+ true -> L;
+ false -> [M|L]
+ end
+ end, [], Incl1)),
+ CoverData#cover{incl_mods=Incl2}.
+
+
+files2mods(Info=#cover{excl_mods=ExclFs,
+ incl_mods=InclFs,
+ cross=CrossFs}) ->
+ Info#cover{excl_mods=files2mods1(ExclFs),
+ incl_mods=files2mods1(InclFs),
+ cross=files2mods1(CrossFs)}.
+
+files2mods1([M|Fs]) when is_atom(M) ->
+ [M|files2mods1(Fs)];
+files2mods1([F|Fs]) when is_list(F) ->
+ M = filename:rootname(filename:basename(F)),
+ [list_to_atom(M)|files2mods1(Fs)];
+files2mods1([]) ->
+ [].
diff --git a/lib/common_test/src/ct_event.erl b/lib/common_test/src/ct_event.erl
new file mode 100644
index 0000000000..3e79898ad1
--- /dev/null
+++ b/lib/common_test/src/ct_event.erl
@@ -0,0 +1,269 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Common Test Framework Event Handler
+%%%
+%%% <p>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>
+-module(ct_event).
+
+-behaviour(gen_event).
+
+%% API
+-export([start_link/0, add_handler/0, add_handler/1, stop/0]).
+-export([notify/1, sync_notify/1]).
+-export([is_alive/0]).
+
+%% gen_event callbacks
+-export([init/1, handle_event/2, handle_call/2,
+ handle_info/2, terminate/2, code_change/3]).
+
+-include("ct_event.hrl").
+-include("ct_util.hrl").
+
+%% receivers = [{RecvTag,Pid}]
+-record(state, {receivers=[]}).
+
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | {error,Error}
+%% Description: Creates an event manager.
+%%--------------------------------------------------------------------
+start_link() ->
+ gen_event:start_link({local,?CT_EVMGR}).
+
+%%--------------------------------------------------------------------
+%% Function: add_handler() -> ok | {'EXIT',Reason} | term()
+%% Description: Adds an event handler
+%%--------------------------------------------------------------------
+add_handler() ->
+ gen_event:add_handler(?CT_EVMGR_REF,?MODULE,[]).
+add_handler(Args) ->
+ gen_event:add_handler(?CT_EVMGR_REF,?MODULE,Args).
+
+%%--------------------------------------------------------------------
+%% Function: stop() -> ok
+%% Description: Stops the event manager
+%%--------------------------------------------------------------------
+stop() ->
+ case whereis(?CT_EVMGR) of
+ undefined ->
+ ok;
+ _Pid ->
+ gen_event:stop(?CT_EVMGR_REF)
+ end.
+
+%%--------------------------------------------------------------------
+%% Function: notify(Event) -> ok
+%% Description: Asynchronous notification to event manager.
+%%--------------------------------------------------------------------
+notify(Event) ->
+ case catch gen_event:notify(?CT_EVMGR_REF,Event) of
+ {'EXIT',Reason} ->
+ {error,{notify,Reason}};
+ Result ->
+ Result
+ end.
+
+%%--------------------------------------------------------------------
+%% Function: sync_notify(Event) -> ok
+%% Description: Synchronous notification to event manager.
+%%--------------------------------------------------------------------
+sync_notify(Event) ->
+ case catch gen_event:sync_notify(?CT_EVMGR_REF,Event) of
+ {'EXIT',Reason} ->
+ {error,{sync_notify,Reason}};
+ Result ->
+ Result
+ end.
+
+%%--------------------------------------------------------------------
+%% Function: is_alive() -> true | false
+%% Description: Check if Event Manager is alive.
+%%--------------------------------------------------------------------
+is_alive() ->
+ case whereis(?CT_EVMGR) of
+ undefined ->
+ false;
+ _Pid ->
+ true
+ end.
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler.
+%%--------------------------------------------------------------------
+init(RecvPids) ->
+ %% RecvPids = [{RecvTag,Pid}]
+ {ok,#state{receivers=RecvPids}}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%% {swap_handler, Args1, State1, Mod2, Args2} |
+%% remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%--------------------------------------------------------------------
+handle_event(Event,State=#state{receivers=RecvPids}) ->
+ print("~n=== ~w ===~n", [?MODULE]),
+ print("~p: ~p~n", [Event#event.name,Event#event.data]),
+ lists:foreach(fun(Recv) -> report_event(Recv,Event) end, RecvPids),
+ {ok,State}.
+
+%%============================== EVENTS ==============================
+%%
+%% Name = test_start
+%% Data = {StartTime,LogDir}
+%%
+%% Name = start_info
+%% Data = {Tests,Suites,Cases}
+%% Tests = Suites = Cases = integer()
+%%
+%% Name = test_done
+%% Data = EndTime
+%%
+%% Name = start_make
+%% Data = Dir
+%%
+%% Name = finished_make
+%% Data = Dir
+%%
+%% Name = tc_start
+%% Data = {Suite,CaseOrGroup}
+%% CaseOrGroup = atom() | {Conf,GroupName,GroupProperties}
+%% Conf = init_per_group | end_per_group
+%% GroupName = atom()
+%% GroupProperties = list()
+%%
+%% Name = tc_done
+%% Data = {Suite,CaseOrGroup,Result}
+%% CaseOrGroup = atom() | {Conf,GroupName,GroupProperties}
+%% Conf = init_per_group | end_per_group
+%% GroupName = atom()
+%% GroupProperties = list()
+%% Result = ok | {skipped,Reason} | {failed,Reason}
+%%
+%% Name = tc_user_skip
+%% Data = {Suite,Case,Comment}
+%% Comment = string()
+%%
+%% Name = tc_auto_skip
+%% Data = {Suite,Case,Comment}
+%% Comment = string()
+%%
+%% Name = test_stats
+%% Data = {Ok,Failed,Skipped}
+%% Ok = Failed = integer()
+%% Skipped = {UserSkipped,AutoSkipped}
+%% UserSkipped = AutoSkipped = integer()
+%%
+%% Name = start_logging
+%% Data = CtRunDir
+%%
+%% Name = stop_logging
+%% Data = []
+%%
+%% Name = start_write_file
+%% Data = FullNameFile
+%%
+%% Name = finished_write_file
+%% Data = FullNameFile
+%%
+%% Name =
+%% Data =
+%%
+
+%% report to master
+report_event({master,Master},E=#event{name=_Name,node=_Node,data=_Data}) ->
+ ct_master:status(Master,E);
+
+%% report to VTS
+report_event({vts,VTS},#event{name=Name,node=_Node,data=Data}) ->
+ if Name == start_info ;
+ Name == test_stats ;
+ Name == test_done ->
+ vts:test_info(VTS,Name,Data);
+ true ->
+ ok
+ end.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%% {swap_handler, Reply, Args1, State1,
+%% Mod2, Args2} |
+%% {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Req, State) ->
+ Reply = ok,
+ {ok, Reply, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%% {swap_handler, Args1, State1, Mod2, Args2} |
+%% remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% 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.
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+print(_Str,_Args) ->
+% io:format(_Str,_Args),
+ ok.
diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl
new file mode 100644
index 0000000000..8456251b29
--- /dev/null
+++ b/lib/common_test/src/ct_framework.erl
@@ -0,0 +1,1059 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Common Test Framework callback module.
+%%%
+%%% <p>This module exports framework callback functions which are
+%%% called from the test_server.</p>
+
+-module(ct_framework).
+
+-export([init_tc/3, end_tc/3, get_suite/2, report/2, warn/1]).
+-export([error_notification/4]).
+
+-export([error_in_suite/1]).
+
+-include("ct_event.hrl").
+-include("ct_util.hrl").
+
+%%%-----------------------------------------------------------------
+%%% @spec init_tc(Mod,Func,Args) -> {ok,NewArgs} | {error,Reason} |
+%%% {skip,Reason} | {auto_skip,Reason}
+%%% Mod = atom()
+%%% Func = atom()
+%%% Args = list()
+%%% NewArgs = list()
+%%% Reason = term()
+%%%
+%%% @doc Test server framework callback, called by the test_server
+%%% when a new test case is started.
+init_tc(Mod,Func,Config) ->
+ %% check if previous testcase was interpreted and has left
+ %% a "dead" trace window behind - if so, kill it
+ case ct_util:get_testdata(interpret) of
+ {What,kill,{TCPid,AttPid}} ->
+ ct_util:kill_attached(TCPid,AttPid),
+ ct_util:set_testdata({interpret,{What,kill,{undefined,undefined}}});
+ _ ->
+ ok
+ end,
+
+ %% check if we need to add defaults explicitly because
+ %% there's no init_per_suite exported from Mod
+ {InitFailed,DoInit} =
+ case ct_util:get_testdata(curr_tc) of
+ {Mod,{suite0_failed,_}=Failure} ->
+ {Failure,false};
+ {Mod,_} ->
+ {false,false};
+ _ when Func == init_per_suite ->
+ {false,false};
+ _ ->
+ {false,true}
+ end,
+ case InitFailed of
+ false ->
+ ct_util:set_testdata({curr_tc,{Mod,Func}}),
+ case ct_util:read_suite_data({seq,Mod,Func}) of
+ undefined ->
+ init_tc1(Mod,Func,Config,DoInit);
+ Seq when is_atom(Seq) ->
+ case ct_util:read_suite_data({seq,Mod,Seq}) of
+ [Func|TCs] -> % this is the 1st case in Seq
+ %% make sure no cases in this seq are marked as failed
+ %% from an earlier execution in the same suite
+ lists:foreach(fun(TC) ->
+ ct_util:save_suite_data({seq,Mod,TC},Seq)
+ end, TCs);
+ _ ->
+ ok
+ end,
+ init_tc1(Mod,Func,Config,DoInit);
+ {failed,Seq,BadFunc} ->
+ {skip,{sequence_failed,Seq,BadFunc}}
+ end;
+ {_,{require,Reason}} ->
+ {skip,{require_failed_in_suite0,Reason}};
+ _ ->
+ {skip,InitFailed}
+ end.
+
+init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) ->
+ Config1 =
+ case ct_util:read_suite_data(last_saved_config) of
+ {{Mod,LastFunc},SavedConfig} -> % last testcase
+ [{saved_config,{LastFunc,SavedConfig}} |
+ lists:keydelete(saved_config,1,Config0)];
+ {{LastSuite,InitOrEnd},SavedConfig} when InitOrEnd == init_per_suite ;
+ InitOrEnd == end_per_suite -> % last suite
+ [{saved_config,{LastSuite,SavedConfig}} |
+ lists:keydelete(saved_config,1,Config0)];
+ undefined ->
+ lists:keydelete(saved_config,1,Config0)
+ end,
+ ct_util:delete_suite_data(last_saved_config),
+ Config = lists:keydelete(watchdog,1,Config1),
+ if Func /= init_per_suite, DoInit /= true ->
+ ok;
+ true ->
+ %% delete all default values used in previous suite
+ ct_util:delete_default_config(suite),
+ %% release all name -> key bindings (once per suite)
+ ct_util:release_allocated()
+ end,
+ TestCaseInfo =
+ case catch apply(Mod,Func,[]) of
+ Result when is_list(Result) -> Result;
+ _ -> []
+ end,
+ %% clear all config data default values set by previous
+ %% testcase info function (these should only survive the
+ %% testcase, not the whole suite)
+ ct_util:delete_default_config(testcase),
+ case add_defaults(Mod,Func,TestCaseInfo,DoInit) of
+ Error = {suite0_failed,_} ->
+ ct_logs:init_tc(),
+ FuncSpec = group_or_func(Func,Config0),
+ ct_event:notify(#event{name=tc_start,
+ node=node(),
+ data={Mod,FuncSpec}}),
+ ct_util:set_testdata({curr_tc,{Mod,Error}}),
+ {error,Error};
+ {SuiteInfo,MergeResult} ->
+ case MergeResult of
+ {error,Reason} when DoInit == false ->
+ ct_logs:init_tc(),
+ FuncSpec = group_or_func(Func,Config0),
+ ct_event:notify(#event{name=tc_start,
+ node=node(),
+ data={Mod,FuncSpec}}),
+ {skip,Reason};
+ _ ->
+ init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit)
+ end
+ end;
+init_tc1(_Mod,_Func,Args,_DoInit) ->
+ {ok,Args}.
+
+init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) ->
+ %% if first testcase fails when there's no init_per_suite
+ %% we must do suite/0 configurations before skipping test
+ MergedInfo =
+ case MergeResult of
+ {error,_} when DoInit == true ->
+ SuiteInfo;
+ _ ->
+ MergeResult
+ end,
+ %% timetrap must be handled before require
+ MergedInfo1 = timetrap_first(MergedInfo, [], []),
+ %% tell logger to use specified style sheet
+ case lists:keysearch(stylesheet,1,MergedInfo++Config) of
+ {value,{stylesheet,SSFile}} ->
+ ct_logs:set_stylesheet(Func,add_data_dir(SSFile,Config));
+ _ ->
+ case ct_util:get_testdata(stylesheet) of
+ undefined ->
+ ct_logs:clear_stylesheet(Func);
+ SSFile ->
+ ct_logs:set_stylesheet(Func,SSFile)
+ end
+ end,
+ %% suppress output for connections (Conns is a
+ %% list of {Type,Bool} tuples, e.g. {telnet,true}),
+ case ct_util:get_overridden_silenced_connections() of
+ undefined ->
+ case lists:keysearch(silent_connections,1,MergedInfo++Config) of
+ {value,{silent_connections,Conns}} ->
+ ct_util:silence_connections(Conns);
+ _ ->
+ ok
+ end;
+ Conns ->
+ ct_util:silence_connections(Conns)
+ end,
+
+ ct_logs:init_tc(),
+ FuncSpec = group_or_func(Func,Config),
+ ct_event:notify(#event{name=tc_start,
+ node=node(),
+ data={Mod,FuncSpec}}),
+
+ case configure(MergedInfo1,MergedInfo1,SuiteInfo,{Func,DoInit},Config) of
+ {suite0_failed,Reason} ->
+ ct_util:set_testdata({curr_tc,{Mod,{suite0_failed,{require,Reason}}}}),
+ {skip,{require_failed_in_suite0,Reason}};
+ {error,Reason} ->
+ {auto_skip,{require_failed,Reason}};
+ FinalConfig ->
+ case MergeResult of
+ {error,Reason} ->
+ %% suite0 configure finished now, report that
+ %% first test case actually failed
+ {skip,Reason};
+ _ ->
+ case get('$test_server_framework_test') of
+ undefined ->
+ FinalConfig;
+ Fun ->
+ Fun(init_tc, FinalConfig)
+ end
+ end
+ end.
+
+
+add_defaults(Mod,Func,FuncInfo,DoInit) ->
+ case (catch Mod:suite()) of
+ {'EXIT',{undef,_}} ->
+ SuiteInfo = merge_with_suite_defaults(Mod,[]),
+ case add_defaults1(Mod,Func,FuncInfo,SuiteInfo,DoInit) of
+ Error = {error,_} -> {SuiteInfo,Error};
+ MergedInfo -> {SuiteInfo,MergedInfo}
+ end;
+ {'EXIT',Reason} ->
+ {suite0_failed,{exited,Reason}};
+ SuiteInfo when is_list(SuiteInfo) ->
+ case lists:all(fun(E) when is_tuple(E) -> true;
+ (_) -> false
+ end, SuiteInfo) of
+ true ->
+ SuiteInfo1 = merge_with_suite_defaults(Mod,SuiteInfo),
+ case add_defaults1(Mod,Func,FuncInfo,SuiteInfo1,DoInit) of
+ Error = {error,_} -> {SuiteInfo1,Error};
+ MergedInfo -> {SuiteInfo1,MergedInfo}
+ end;
+ false ->
+ {suite0_failed,bad_return_value}
+ end;
+ _ ->
+ {suite0_failed,bad_return_value}
+ end.
+
+add_defaults1(_Mod,init_per_suite,[],SuiteInfo,_) ->
+ SuiteInfo;
+
+add_defaults1(Mod,Func,FuncInfo,SuiteInfo,DoInit) ->
+ %% mustn't re-require suite variables in test case info function,
+ %% can result in weird behaviour (suite values get overwritten)
+ SuiteReqs =
+ [SDDef || SDDef <- SuiteInfo,
+ require == element(1,SDDef)],
+ case [element(2,Clash) || Clash <- SuiteReqs,
+ true == lists:keymember(element(2,Clash),2,FuncInfo)] of
+ [] ->
+ add_defaults2(Mod,Func,FuncInfo,SuiteInfo,SuiteReqs,DoInit);
+ Clashes ->
+ {error,{config_name_already_in_use,Clashes}}
+ end.
+
+add_defaults2(_Mod,_Func,FuncInfo,SuiteInfo,_,false) ->
+ %% include require elements from test case info, but not from suite/0
+ %% (since we've already required those vars)
+ FuncInfo ++
+ [SFDef || SFDef <- SuiteInfo,
+ require /= element(1,SFDef),
+ false == lists:keymember(element(1,SFDef),1,FuncInfo)];
+
+add_defaults2(_Mod,_Func,FuncInfo,SuiteInfo,SuiteReqs,true) ->
+ %% We must include require elements from suite/0 here since
+ %% there's no init_per_suite call before this first test case.
+ %% Let other test case info elements override those from suite/0.
+ FuncInfo ++ SuiteReqs ++
+ [SDDef || SDDef <- SuiteInfo,
+ require /= element(1,SDDef),
+ false == lists:keymember(element(1,SDDef),1,FuncInfo)].
+
+merge_with_suite_defaults(Mod,SuiteInfo) ->
+ case lists:keysearch(suite_defaults,1,Mod:module_info(attributes)) of
+ {value,{suite_defaults,Defaults}} ->
+ SDReqs =
+ [SDDef || SDDef <- Defaults,
+ require == element(1,SDDef),
+ false == lists:keymember(element(2,SDDef),2,
+ SuiteInfo)],
+ SuiteInfo ++ SDReqs ++
+ [SDDef || SDDef <- Defaults,
+ require /= element(1,SDDef),
+ false == lists:keymember(element(1,SDDef),1,
+ SuiteInfo)];
+ false ->
+ SuiteInfo
+ end.
+
+timetrap_first([Trap = {timetrap,_} | Rest],Info,Found) ->
+ timetrap_first(Rest,Info,[Trap | Found]);
+timetrap_first([Other | Rest],Info,Found) ->
+ timetrap_first(Rest,[Other | Info],Found);
+timetrap_first([],Info,[]) ->
+ [{timetrap,{minutes,30}} | lists:reverse(Info)];
+timetrap_first([],Info,Found) ->
+ lists:reverse(Found) ++ lists:reverse(Info).
+
+configure([{require,Required}|Rest],Info,SuiteInfo,Scope,Config) ->
+ case ct:require(Required) of
+ ok ->
+ configure(Rest,Info,SuiteInfo,Scope,Config);
+ Error = {error,Reason} ->
+ case required_default('_UNDEF',Required,Info,SuiteInfo,Scope) of
+ ok ->
+ configure(Rest,Info,SuiteInfo,Scope,Config);
+ _ ->
+ case lists:keymember(Required,2,SuiteInfo) of
+ true ->
+ {suite0_failed,Reason};
+ false ->
+ Error
+ end
+ end
+ end;
+configure([{require,Name,Required}|Rest],Info,SuiteInfo,Scope,Config) ->
+ case ct:require(Name,Required) of
+ ok ->
+ configure(Rest,Info,SuiteInfo,Scope,Config);
+ Error = {error,Reason} ->
+ case required_default(Name,Required,Info,SuiteInfo,Scope) of
+ ok ->
+ configure(Rest,Info,SuiteInfo,Scope,Config);
+ _ ->
+ case lists:keymember(Name,2,SuiteInfo) of
+ true ->
+ {suite0_failed,Reason};
+ false ->
+ Error
+ end
+ end
+ end;
+configure([{timetrap,off}|Rest],Info,SuiteInfo,Scope,Config) ->
+ configure(Rest,Info,SuiteInfo,Scope,Config);
+configure([{timetrap,Time}|Rest],Info,SuiteInfo,Scope,Config) ->
+ Dog = test_server:timetrap(Time),
+ configure(Rest,Info,SuiteInfo,Scope,[{watchdog,Dog}|Config]);
+configure([_|Rest],Info,SuiteInfo,Scope,Config) ->
+ configure(Rest,Info,SuiteInfo,Scope,Config);
+configure([],_,_,_,Config) ->
+ {ok,[Config]}.
+
+%% the require element in Info may come from suite/0 and
+%% should be scoped 'suite', or come from the testcase info
+%% function and should then be scoped 'testcase'
+required_default(Name,Key,Info,SuiteInfo,{Func,true}) ->
+ case try_set_default(Name,Key,SuiteInfo,suite) of
+ ok ->
+ ok;
+ _ ->
+ required_default(Name,Key,Info,[],{Func,false})
+ end;
+required_default(Name,Key,Info,_,{init_per_suite,_}) ->
+ try_set_default(Name,Key,Info,suite);
+required_default(Name,Key,Info,_,_) ->
+ try_set_default(Name,Key,Info,testcase).
+
+try_set_default(Name,Key,Info,Where) ->
+ CfgElems =
+ case lists:keysearch(Name,1,Info) of
+ {value,{Name,Val}} ->
+ [Val];
+ false ->
+ case catch [{Key,element(3,Elem)} || Elem <- Info,
+ element(1,Elem)==default_config,
+ element(2,Elem)==Key] of
+ {'EXIT',_} -> [];
+ Result -> Result
+ end
+ end,
+ case {Name,CfgElems} of
+ {_,[]} ->
+ no_default;
+ {'_UNDEF',_} ->
+ [ct_util:set_default_config([CfgVal],Where) || CfgVal <- CfgElems],
+ ok;
+ _ ->
+ [ct_util:set_default_config(Name,[CfgVal],Where) || CfgVal <- CfgElems],
+ ok
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @spec end_tc(Mod,Func,Args) -> {ok,NewArgs}| {error,Reason} |
+%%% {skip,Reason} | {auto_skip,Reason}
+%%% Mod = atom()
+%%% Func = atom()
+%%% Args = list()
+%%% NewArgs = list()
+%%% Reason = term()
+%%%
+%%% @doc Test server framework callback, called by the test_server
+%%% when a test case is finished.
+end_tc(?MODULE,error_in_suite,_) -> % bad start!
+ ok;
+end_tc(Mod,Func,{TCPid,Result,[Args]}) when is_pid(TCPid) ->
+ end_tc(Mod,Func,TCPid,Result,Args);
+end_tc(Mod,Func,{Result,[Args]}) ->
+ end_tc(Mod,Func,self(),Result,Args).
+
+end_tc(Mod,Func,TCPid,Result,Args) ->
+ case lists:keysearch(watchdog,1,Args) of
+ {value,{watchdog,Dog}} -> test_server:timetrap_cancel(Dog);
+ false -> ok
+ end,
+
+ %% save the testcase process pid so that it can be used
+ %% to look up the attached trace window later
+ case ct_util:get_testdata(interpret) of
+ {What,kill,_} ->
+ AttPid = ct_util:get_attached(self()),
+ ct_util:set_testdata({interpret,{What,kill,{self(),AttPid}}});
+ _ ->
+ ok
+ end,
+
+ ct_util:delete_testdata(comment),
+ ct_util:delete_suite_data(last_saved_config),
+ FuncSpec =
+ case group_or_func(Func,Args) of
+ {_,GroupName,_Props} = Group ->
+ case lists:keysearch(save_config,1,Args) of
+ {value,{save_config,SaveConfig}} ->
+ ct_util:save_suite_data(last_saved_config,
+ {Mod,{group,GroupName}},SaveConfig),
+ Group;
+ false ->
+ Group
+ end;
+ _ ->
+ case lists:keysearch(save_config,1,Args) of
+ {value,{save_config,SaveConfig}} ->
+ ct_util:save_suite_data(last_saved_config,
+ {Mod,Func},SaveConfig),
+ Func;
+ false ->
+ Func
+ end
+ end,
+ ct_util:reset_silent_connections(),
+
+ %% send sync notification so that event handlers may print
+ %% in the log file before it gets closed
+ ct_event:sync_notify(#event{name=tc_done,
+ node=node(),
+ data={Mod,FuncSpec,tag(Result)}}),
+ case Result of
+ {skip,{sequence_failed,_,_}} ->
+ %% ct_logs:init_tc is never called for a skipped test case
+ %% in a failing sequence, so neither should end_tc
+ ok;
+ _ ->
+ case ct_logs:end_tc(TCPid) of
+ {error,Reason} ->
+ exit({error,{logger,Reason}});
+ _ ->
+ ok
+ end
+ end,
+ case Func of
+ end_per_suite ->
+ ct_util:match_delete_suite_data({seq,Mod,'_'});
+ _ ->
+ ok
+ end,
+ case get('$test_server_framework_test') of
+ undefined ->
+ ok;
+ Fun ->
+ Fun(end_tc, ok)
+ end.
+
+%% {error,Reason} | {skip,Reason} | {timetrap_timeout,TVal} |
+%% {testcase_aborted,Reason} | testcase_aborted_or_killed |
+%% {'EXIT',Reason} | Other (ignored return value, e.g. 'ok')
+tag({STag,Reason}) when STag == skip; STag == skipped ->
+ {skipped,Reason};
+tag(E = {ETag,_}) when ETag == error; ETag == 'EXIT';
+ ETag == timetrap_timeout;
+ ETag == testcase_aborted ->
+ {failed,E};
+tag(E = testcase_aborted_or_killed) ->
+ {failed,E};
+tag(Other) ->
+ Other.
+
+%%%-----------------------------------------------------------------
+%%% @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.
+error_notification(Mod,Func,_Args,{Error,Loc}) ->
+ ErrSpec = case Error of
+ {What={_E,_R},Trace} when is_list(Trace) ->
+ What;
+ What ->
+ What
+ end,
+ ErrStr = case ErrSpec of
+ {badmatch,Descr} ->
+ Descr1 = lists:flatten(io_lib:format("~P",[Descr,10])),
+ if length(Descr1) > 50 ->
+ Descr2 = string:substr(Descr1,1,50),
+ io_lib:format("{badmatch,~s...}",[Descr2]);
+ true ->
+ io_lib:format("{badmatch,~s}",[Descr1])
+ end;
+ {test_case_failed,Reason} ->
+ case (catch io_lib:format("{test_case_failed,~s}", [Reason])) of
+ {'EXIT',_} ->
+ io_lib:format("{test_case_failed,~p}", [Reason]);
+ Result -> Result
+ end;
+ {Spec,_Reason} when is_atom(Spec) ->
+ io_lib:format("~w", [Spec]);
+ Other ->
+ io_lib:format("~P", [Other,5])
+ end,
+ ErrorHtml = "<font color=\"brown\">" ++ ErrStr ++ "</font>",
+ case {Mod,Error} of
+ %% some notifications come from the main test_server process
+ %% and for these cases the existing comment may not be modified
+ {_,{timetrap_timeout,_TVal}} ->
+ ok;
+ {_,{testcase_aborted,_Info}} ->
+ ok;
+ {_,testcase_aborted_or_killed} ->
+ ok;
+ {undefined,_OtherError} ->
+ ok;
+ _ ->
+ %% this notification comes from the test case process, so
+ %% we can add error info to comment with test_server:comment/1
+ case ct_util:get_testdata(comment) of
+ undefined ->
+ test_server:comment(ErrorHtml);
+ Comment ->
+ CommentHtml =
+ "<font color=\"green\">" ++ "(" ++ "</font>"
+ ++ Comment ++
+ "<font color=\"green\">" ++ ")" ++ "</font>",
+ Str = io_lib:format("~s ~s", [ErrorHtml,CommentHtml]),
+ test_server:comment(Str)
+ end
+ end,
+
+ io:format(user, "~n- - - - - - - - - - - - - - - - "
+ "- - - - - - - - - -~n", []),
+ case Loc of
+ %% we don't use the line parse transform as we compile this
+ %% module so location will be on form {M,F}
+ [{?MODULE,error_in_suite}] ->
+ io:format(user, "Error in suite detected: ~s", [ErrStr]);
+
+ unknown ->
+ io:format(user, "Error detected: ~s", [ErrStr]);
+
+ %% if a function specified by all/0 does not exist, we
+ %% pick up undef here
+ [{LastMod,LastFunc}] ->
+ io:format(user, "~w:~w could not be executed~n",
+ [LastMod,LastFunc]),
+ io:format(user, "Reason: ~s", [ErrStr]);
+
+ [{LastMod,LastFunc,LastLine}|_] ->
+ %% print error to console, we are only
+ %% interested in the last executed expression
+ io:format(user, "~w:~w failed on line ~w~n",
+ [LastMod,LastFunc,LastLine]),
+ io:format(user, "Reason: ~s", [ErrStr]),
+
+ case ct_util:read_suite_data({seq,Mod,Func}) of
+ undefined ->
+ ok;
+ Seq ->
+ SeqTCs = ct_util:read_suite_data({seq,Mod,Seq}),
+ mark_as_failed(Seq,Mod,Func,SeqTCs)
+ end
+ end,
+ io:format(user, "~n- - - - - - - - - - - - - - - - "
+ "- - - - - - - - - -~n~n", []),
+ ok.
+
+%% cases in seq that have already run
+mark_as_failed(Seq,Mod,Func,[Func|TCs]) ->
+ mark_as_failed1(Seq,Mod,Func,TCs);
+mark_as_failed(Seq,Mod,Func,[_TC|TCs]) ->
+ mark_as_failed(Seq,Mod,Func,TCs);
+mark_as_failed(_,_,_,[]) ->
+ ok;
+mark_as_failed(_,_,_,undefined) ->
+ ok.
+
+%% mark rest of cases in seq to be skipped
+mark_as_failed1(Seq,Mod,Func,[TC|TCs]) ->
+ ct_util:save_suite_data({seq,Mod,TC},{failed,Seq,Func}),
+ mark_as_failed1(Seq,Mod,Func,TCs);
+mark_as_failed1(_,_,_,[]) ->
+ ok.
+
+group_or_func(Func, Config) when Func == init_per_group;
+ Func == end_per_group ->
+ case proplists:get_value(tc_group_properties,Config) of
+ undefined ->
+ {Func,unknown,[]};
+ GrProps ->
+ GrName = proplists:get_value(name,GrProps),
+ {Func,GrName,proplists:delete(name,GrProps)}
+ end;
+group_or_func(Func, _Config) ->
+ Func.
+
+%%%-----------------------------------------------------------------
+%%% @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.
+
+get_suite(Mod, all) ->
+ case catch apply(Mod, groups, []) of
+ {'EXIT',_} ->
+ get_all(Mod, []);
+ GroupDefs when is_list(GroupDefs) ->
+ case catch check_groups(Mod, GroupDefs) of
+ {error,_} = Error ->
+ %% this makes test_server call error_in_suite as first
+ %% (and only) test case so we can report Error properly
+ [{?MODULE,error_in_suite,[[Error]]}];
+ ConfTests ->
+ get_all(Mod, ConfTests)
+ end;
+ _ ->
+ E = "Bad return value from "++atom_to_list(Mod)++":groups/0",
+ [{?MODULE,error_in_suite,[[{error,list_to_atom(E)}]]}]
+ end;
+
+%%!============================================================
+%%! Note: The handling of sequences in get_suite/2 and get_all/2
+%%! is deprecated and should be removed after OTP R13!
+%%!============================================================
+
+get_suite(Mod, Name) ->
+ %% Name may be name of a group or a test case. If it's a group,
+ %% it should be expanded to list of cases (in a conf term)
+ case catch apply(Mod, groups, []) of
+ {'EXIT',_} ->
+ get_seq(Mod, Name);
+ GroupDefs when is_list(GroupDefs) ->
+ case catch check_groups(Mod, GroupDefs) of
+ {error,_} = Error ->
+ %% this makes test_server call error_in_suite as first
+ %% (and only) test case so we can report Error properly
+ [{?MODULE,error_in_suite,[[Error]]}];
+ ConfTests ->
+ FindConf = fun({conf,Props,_,_,_}) ->
+ case proplists:get_value(name, Props) of
+ Name -> true;
+ _ -> false
+ end
+ end,
+ case lists:filter(FindConf, ConfTests) of
+ [] -> % must be a test case
+ get_seq(Mod, Name);
+ [ConfTest|_] ->
+ ConfTest
+ end
+ end;
+ _ ->
+ E = "Bad return value from "++atom_to_list(Mod)++":groups/0",
+ [{?MODULE,error_in_suite,[[{error,list_to_atom(E)}]]}]
+ end.
+
+check_groups(_Mod, []) ->
+ [];
+check_groups(Mod, Defs) ->
+ check_groups(Mod, Defs, Defs, []).
+
+check_groups(Mod, [TC | Gs], Defs, Levels) when is_atom(TC), length(Levels)>0 ->
+ [TC | check_groups(Mod, Gs, Defs, Levels)];
+
+check_groups(Mod, [{group,SubName} | Gs], Defs, Levels) when is_atom(SubName) ->
+ case lists:member(SubName, Levels) of
+ true ->
+ E = "Cyclic reference to group "++atom_to_list(SubName)++
+ " in "++atom_to_list(Mod)++":groups/0",
+ throw({error,list_to_atom(E)});
+ false ->
+ case find_group(Mod, SubName, Defs) of
+ {error,_} = Error ->
+ throw(Error);
+ G ->
+ [check_groups(Mod, [G], Defs, Levels) |
+ check_groups(Mod, Gs, Defs, Levels)]
+ end
+ end;
+
+check_groups(Mod, [{Name,Tests} | Gs], Defs, Levels) when is_atom(Name),
+ is_list(Tests) ->
+ check_groups(Mod, [{Name,[],Tests} | Gs], Defs, Levels);
+
+check_groups(Mod, [{Name,Props,Tests} | Gs], Defs, Levels) when is_atom(Name),
+ is_list(Props),
+ is_list(Tests) ->
+ {TestSpec,Levels1} =
+ case Levels of
+ [] ->
+ {check_groups(Mod, Tests, Defs, [Name]),[]};
+ _ ->
+ {check_groups(Mod, Tests, Defs, [Name|Levels]),Levels}
+ end,
+ [make_conf(Mod, Name, Props, TestSpec) |
+ check_groups(Mod, Gs, Defs, Levels1)];
+
+check_groups(Mod, [BadTerm | _Gs], _Defs, Levels) ->
+ Where = if length(Levels) == 0 ->
+ atom_to_list(Mod)++":groups/0";
+ true ->
+ "group "++atom_to_list(lists:last(Levels))++
+ " in "++atom_to_list(Mod)++":groups/0"
+ end,
+ Term = io_lib:format("~p", [BadTerm]),
+ E = "Bad term "++lists:flatten(Term)++" in "++Where,
+ throw({error,list_to_atom(E)});
+
+check_groups(_Mod, [], _Defs, _) ->
+ [].
+
+find_group(Mod, Name, Defs) ->
+ case lists:keysearch(Name, 1, Defs) of
+ {value,Def} ->
+ Def;
+ false ->
+ E = "Invalid group "++atom_to_list(Name)++
+ " in "++atom_to_list(Mod)++":groups/0",
+ throw({error,list_to_atom(E)})
+ end.
+
+make_conf(Mod, Name, Props, TestSpec) ->
+ {conf,[{name,Name}|Props],
+ {Mod,init_per_group},TestSpec,{Mod,end_per_group}}.
+
+
+get_all(Mod, ConfTests) ->
+ case catch apply(Mod, all, []) of
+ {'EXIT',_} ->
+ Reason =
+ list_to_atom(atom_to_list(Mod)++":all/0 is missing"),
+ %% 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}]]}];
+ AllTCs when is_list(AllTCs) ->
+ case catch save_seqs(Mod,AllTCs) of
+ {error,What} ->
+ [{?MODULE,error_in_suite,[[{error,What}]]}];
+ SeqsAndTCs ->
+ %% expand group references in all() using ConfTests
+ Expand =
+ fun({group,Name}) ->
+ FindConf =
+ fun({conf,Props,_,_,_}) ->
+ case proplists:get_value(name, Props) of
+ Name -> true;
+ _ -> false
+ end
+ end,
+ case lists:filter(FindConf, ConfTests) of
+ [ConfTest|_] ->
+ ConfTest;
+ [] ->
+ E = "Invalid reference to group "++
+ atom_to_list(Name)++" in "++
+ atom_to_list(Mod)++":all/0",
+ throw({error,list_to_atom(E)})
+ end;
+ (SeqOrTC) -> SeqOrTC
+ end,
+ case catch lists:map(Expand, SeqsAndTCs) of
+ {error,_} = Error ->
+ [{?MODULE,error_in_suite,[[Error]]}];
+ Tests ->
+ Tests
+ end
+ end;
+ Skip = {skip,_Reason} ->
+ Skip;
+ _ ->
+ Reason =
+ list_to_atom("Bad return value from "++atom_to_list(Mod)++":all/0"),
+ [{?MODULE,error_in_suite,[[{error,Reason}]]}]
+ end.
+
+
+%%!============================================================
+%%! The support for sequences by means of using sequences/0
+%%! will be removed in OTP R14. The code below is only kept
+%%! for backwards compatibility. From OTP R13 groups with
+%%! sequence property should be used instead!
+%%!============================================================
+%%!============================================================
+%%! START OF DEPRECATED SUPPORT FOR SEQUENCES --->
+
+get_seq(Mod, Func) ->
+ case ct_util:read_suite_data({seq,Mod,Func}) of
+ undefined ->
+ case catch apply(Mod,sequences,[]) of
+ {'EXIT',_} ->
+ [];
+ Seqs ->
+ case lists:keysearch(Func,1,Seqs) of
+ {value,{Func,SeqTCs}} ->
+ case catch save_seq(Mod,Func,SeqTCs) of
+ {error,What} ->
+ [{?MODULE,error_in_suite,[[{error,What}]]}];
+ _ ->
+ SeqTCs
+ end;
+ false ->
+ []
+ end
+ end;
+ TCs when is_list(TCs) ->
+ TCs;
+ _ ->
+ []
+ end.
+
+save_seqs(Mod,AllTCs) ->
+ case lists:keymember(sequence,1,AllTCs) of
+ true ->
+ case catch apply(Mod,sequences,[]) of
+ {'EXIT',_} ->
+ Reason = list_to_atom(atom_to_list(Mod)++
+ ":sequences/0 is missing"),
+ throw({error,Reason});
+ Seqs ->
+ save_seqs(Mod,AllTCs,Seqs,AllTCs)
+ end;
+ false ->
+ AllTCs
+ end.
+
+save_seqs(Mod,[{sequence,Seq}|TCs],Seqs,All) ->
+ case lists:keysearch(Seq,1,Seqs) of
+ {value,{Seq,SeqTCs}} ->
+ save_seq(Mod,Seq,SeqTCs,All),
+ [Seq|save_seqs(Mod,TCs,Seqs,All)];
+ false ->
+ Reason = list_to_atom(
+ atom_to_list(Seq)++" is missing in "++
+ atom_to_list(Mod)),
+ throw({error,Reason})
+ end;
+save_seqs(Mod,[TC|TCs],Seqs,All) ->
+ [TC|save_seqs(Mod,TCs,Seqs,All)];
+save_seqs(_,[],_,_) ->
+ [].
+
+save_seq(Mod,Seq,SeqTCs) ->
+ save_seq(Mod,Seq,SeqTCs,apply(Mod,all,[])).
+
+save_seq(Mod,Seq,SeqTCs,All) ->
+ check_private(Seq,SeqTCs,All),
+ check_multiple(Mod,Seq,SeqTCs),
+ ct_util:save_suite_data({seq,Mod,Seq},SeqTCs),
+ lists:foreach(fun(TC) ->
+ ct_util:save_suite_data({seq,Mod,TC},Seq)
+ end, SeqTCs).
+
+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]),
+ throw({error,list_to_atom(lists:flatten(Reason))});
+ true ->
+ ok
+ end.
+
+check_multiple(Mod,Seq,TCs) ->
+ Bad = lists:filter(fun(TC) ->
+ case ct_util:read_suite_data({seq,Mod,TC}) of
+ Seq1 when Seq1 /= undefined, Seq1 /= Seq ->
+ true;
+
+ _ -> false
+ end
+ end,TCs),
+ if Bad /= [] ->
+ Reason = io_lib:format("test cases found in multiple sequences: "
+ "~p",[Bad]),
+ throw({error,list_to_atom(lists:flatten(Reason))});
+ true ->
+ ok
+ end.
+
+%%! <--- END OF DEPRECATED SUPPORT FOR SEQUENCES
+%%!============================================================
+
+%% let test_server call this function as a testcase only so that
+%% the user may see info about what's missing in the suite
+error_in_suite(Config) ->
+ Reason = test_server:lookup_config(error,Config),
+ exit(Reason).
+
+%%%-----------------------------------------------------------------
+%%% @spec report(What,Data) -> ok
+report(What,Data) ->
+ case What of
+ tests_start ->
+ case ct_util:get_testdata(cover) of
+ undefined ->
+ ok;
+ {_CovFile,_CovNodes,CovImport,CovExport,_CovAppData} ->
+ %% Always import cover data from files specified by CovImport
+ %% if no CovExport defined. If CovExport is defined, only
+ %% import from CovImport files initially, then use CovExport
+ %% to pass coverdata between proceeding tests (in the same run).
+ Imps =
+ case CovExport of
+ [] -> % don't export data between tests
+ CovImport;
+ _ ->
+ case filelib:is_file(CovExport) of
+ true ->
+ [CovExport];
+ false ->
+ CovImport
+ end
+ end,
+ lists:foreach(
+ fun(Imp) ->
+ case cover:import(Imp) of
+ ok ->
+ ok;
+ {error,Reason} ->
+ ct_logs:log("COVER INFO",
+ "Importing cover data from: ~s fails! "
+ "Reason: ~p", [Imp,Reason])
+ end
+ end, Imps)
+ end;
+ tests_done ->
+ ok;
+ tc_start ->
+ ok;
+ tc_done ->
+ {_Suite,Case,Result} = Data,
+ case {Case,Result} of
+ {init_per_suite,_} ->
+ ok;
+ {end_per_suite,_} ->
+ ok;
+ {init_per_group,_} ->
+ ok;
+ {end_per_group,_} ->
+ ok;
+ {_,ok} ->
+ add_to_stats(ok);
+ {_,{skipped,{failed,{_,init_per_testcase,_}}}} ->
+ add_to_stats(auto_skipped);
+ {_,{skipped,{require_failed,_}}} ->
+ add_to_stats(auto_skipped);
+ {_,{skipped,_}} ->
+ add_to_stats(user_skipped);
+ {_,{FailOrSkip,_Reason}} ->
+ add_to_stats(FailOrSkip)
+ end;
+ tc_user_skip ->
+ %% test case specified as skipped in testspec
+ %% Data = {Suite,Case,Comment}
+ ct_event:sync_notify(#event{name=tc_user_skip,
+ node=node(),
+ data=Data}),
+ add_to_stats(user_skipped);
+ tc_auto_skip ->
+ %% test case skipped because of error in init_per_suite
+ %% Data = {Suite,Case,Comment}
+
+ {_Suite,Case,_Result} = Data,
+
+ %% this test case does not have a log, so printouts
+ %% from event handlers should end up in the main log
+ ct_event:sync_notify(#event{name=tc_auto_skip,
+ node=node(),
+ data=Data}),
+ if Case /= end_per_suite, Case /= end_per_group ->
+ add_to_stats(auto_skipped);
+ true ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ catch vts:report(What,Data).
+
+add_to_stats(Result) ->
+ Update = fun({Ok,Failed,Skipped={UserSkipped,AutoSkipped}}) ->
+ Stats =
+ case Result of
+ ok ->
+ {Ok+1,Failed,Skipped};
+ failed ->
+ {Ok,Failed+1,Skipped};
+ skipped ->
+ {Ok,Failed,{UserSkipped+1,AutoSkipped}};
+ user_skipped ->
+ {Ok,Failed,{UserSkipped+1,AutoSkipped}};
+ auto_skipped ->
+ {Ok,Failed,{UserSkipped,AutoSkipped+1}}
+ end,
+ ct_event:sync_notify(#event{name=test_stats,
+ node=node(),
+ data=Stats}),
+ Stats
+ end,
+ ct_util:update_testdata(stats, Update).
+
+%%%-----------------------------------------------------------------
+%%% @spec warn(What) -> true | false
+warn(What) when What==nodes; What==processes ->
+ false;
+warn(_What) ->
+ true.
+
+%%%-----------------------------------------------------------------
+%%% @spec add_data_dir(File0) -> File1
+add_data_dir(File,Config) when is_atom(File) ->
+ add_data_dir(atom_to_list(File),Config);
+
+add_data_dir(File,Config) when is_list(File) ->
+ case filename:split(File) of
+ [File] ->
+ %% no user path, add data dir
+ case lists:keysearch(data_dir,1,Config) of
+ {value,{data_dir,DataDir}} ->
+ filename:join(DataDir,File);
+ _ ->
+ File
+ end;
+ _ ->
+ File
+ end.
+
+
diff --git a/lib/common_test/src/ct_ftp.erl b/lib/common_test/src/ct_ftp.erl
new file mode 100644
index 0000000000..5db73066a3
--- /dev/null
+++ b/lib/common_test/src/ct_ftp.erl
@@ -0,0 +1,380 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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
+-export([get/3,put/3, open/1,close/1, send/2,send/3,
+ recv/2,recv/3, cd/2, ls/2, type/2, delete/2]).
+
+%% Callbacks
+-export([init/3,handle_msg/2,reconnect/2,terminate/2]).
+
+-include("ct_util.hrl").
+
+-record(state,{ftp_pid,target_name}).
+
+-define(DEFAULT_PORT,21).
+
+%%%=================================================================
+%%% 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>
+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
+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>
+open(KeyOrName) ->
+ case ct_util:get_key_from_name(KeyOrName) of
+ {ok,node} ->
+ open(KeyOrName,"erlang","x");
+ _ ->
+ case ct:get_config(KeyOrName) of
+ undefined ->
+ log(heading(open,KeyOrName),"Failed: ~p\n",
+ [{not_available,KeyOrName}]),
+ {error,{not_available,KeyOrName}};
+ _ ->
+ case ct:get_config({KeyOrName,username}) of
+ undefined ->
+ log(heading(open,KeyOrName),"Failed: ~p\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",
+ [{not_available,{KeyOrName,password}}]),
+ {error,{not_available,{KeyOrName,password}}};
+ Password ->
+ open(KeyOrName,Username,Password)
+ end
+ end
+ end
+ end.
+
+open(KeyOrName,Username,Password) ->
+ log(heading(open,KeyOrName),"",[]),
+ case ct:get_config({KeyOrName,ftp}) of
+ undefined ->
+ log(heading(open,KeyOrName),"Failed: ~p\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} ->
+ call(Pid,{send,LocalFile,RemoteFile});
+ Error ->
+ 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} ->
+ call(Pid,{recv,RemoteFile,LocalFile});
+ Error ->
+ 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} ->
+ call(Pid,{cd,Dir});
+ Error ->
+ 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} ->
+ call(Pid,{ls,Dir});
+ Error ->
+ 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} ->
+ call(Pid,{type,Type});
+ Error ->
+ 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} ->
+ call(Pid,{delete,File});
+ Error ->
+ Error
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec close(Connection) -> ok | {error,Reason}
+%%% Connection = connection()
+%%%
+%%% @doc Close the FTP connection.
+close(Connection) ->
+ case get_handle(Connection) of
+ {ok,Pid} ->
+ ct_gen_conn:stop(Pid);
+ Error ->
+ Error
+ end.
+
+
+%%%=================================================================
+%%% 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),$*)]),
+ {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
+ {ok,FtpPid} ->
+ case ftp:user(FtpPid,Username,Password) of
+ ok ->
+ {ok,FtpPid};
+ {error,Reason} ->
+ {error,{user,Reason}}
+ end;
+ {error,Reason} ->
+ {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]),
+ 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]),
+ 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]),
+ 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]),
+ 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]),
+ 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]),
+ 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).
+
+
+%%%=================================================================
+%%% Internal function
+get_handle(Pid) when is_pid(Pid) ->
+ {ok,Pid};
+get_handle(Name) ->
+ case ct_util:get_connections(Name,?MODULE) of
+ {ok,[{Pid,_}|_]} ->
+ {ok,Pid};
+ {ok,[]} ->
+ open(Name);
+ Error ->
+ Error
+ end.
+
+full_addr({Ip,Port}) ->
+ {Ip,Port};
+full_addr(Ip) ->
+ {Ip,?DEFAULT_PORT}.
+
+call(Pid,Msg) ->
+ ct_gen_conn:call(Pid,Msg).
+
+
+heading(Function,Name) ->
+ io_lib:format("ct_ftp:~w ~p",[Function,Name]).
+
+log(Heading,Str,Args) ->
+ ct_gen_conn:log(Heading,Str,Args).
+
+
+open_and_do(Name,Fun) ->
+ case open(Name) of
+ {ok,Ftp} ->
+ R = Fun(Ftp),
+ close(Ftp),
+ R;
+ Error ->
+ Error
+ end.
+
+
diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl
new file mode 100644
index 0000000000..a31e57c7ea
--- /dev/null
+++ b/lib/common_test/src/ct_gen_conn.erl
@@ -0,0 +1,286 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Generic connection owner process.
+%%%
+%%% @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]).
+-export([call/2, do_within_time/2]).
+
+-ifdef(debug).
+-define(dbg,true).
+-else.
+-define(dbg,false).
+-endif.
+
+-record(gen_opts,{callback,
+ name,
+ address,
+ init_data,
+ conn_pid,
+ cb_state,
+ ct_util_server}).
+
+%%%-----------------------------------------------------------------
+%%% @spec start(Name,Address,InitData,CallbackMod) ->
+%%% {ok,Handle} | {error,Reason}
+%%% Name = term()
+%%% CallbackMod = atom()
+%%% InitData = term()
+%%% Address = term()
+%%%
+%%% @doc Open a connection and start the generic connection owner process.
+%%%
+%%% <p>The <code>CallbackMod</code> 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>
+start(Name,Address,InitData,CallbackMod) ->
+ case ct_util:does_connection_exist(Name,Address,CallbackMod) of
+ {ok,Pid} ->
+ log("ct_gen_conn:start","Using existing connection!\n",[]),
+ {ok,Pid};
+ false ->
+ Self = self(),
+ Pid = spawn(fun() ->
+ init_gen(Self, #gen_opts{callback=CallbackMod,
+ name=Name,
+ address=Address,
+ init_data=InitData})
+ end),
+ MRef = erlang:monitor(process,Pid),
+ receive
+ {connected,Pid} ->
+ erlang:demonitor(MRef),
+ ct_util:register_connection(Name,Address,CallbackMod,Pid),
+ {ok,Pid};
+ {Error,Pid} ->
+ receive {'DOWN',MRef,process,_,_} -> ok end,
+ Error;
+ {'DOWN',MRef,process,_,Reason} ->
+ log("ct_gen_conn:start",
+ "Connection process died: ~p\n",
+ [Reason]),
+ {error,{connection_process_died,Reason}}
+ end
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @spec stop(Handle) -> ok
+%%% Handle = handle()
+%%%
+%%% @doc Close the telnet connection and stop the process managing it.
+stop(Pid) ->
+ call(Pid,stop).
+
+%%%-----------------------------------------------------------------
+%%% @spec log(Heading,Format,Args) -> ok
+%%%
+%%% @doc 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
+%%%
+%%% @doc 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
+%%%
+%%% @doc 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
+%%%
+%%% @doc 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}
+%%% Fun = function()
+%%% Timeout = integer()
+%%%
+%%% @doc 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>
+%%%
+%%% <p>The execution is also interrupted if the connection is
+%%% closed.</p>
+do_within_time(Fun,Timeout) ->
+ Self = self(),
+ Silent = get(silent),
+ TmpPid = spawn_link(fun() -> put(silent,Silent),
+ R = Fun(),
+ Self ! {self(),R}
+ end),
+ ConnPid = get(conn_pid),
+ receive
+ {TmpPid,Result} ->
+ Result;
+ {'EXIT',ConnPid,_Reason}=M ->
+ unlink(TmpPid),
+ exit(TmpPid,kill),
+ self() ! M,
+ {error,connection_closed}
+ after
+ Timeout ->
+ exit(TmpPid,kill),
+ receive
+ {TmpPid,Result} ->
+ %% TmpPid just managed to send the result at the same time
+ %% as the timeout expired.
+ receive {'EXIT',TmpPid,_reason} -> ok end,
+ Result;
+ {'EXIT',TmpPid,killed} ->
+ %% TmpPid did not send the result before the timeout expired.
+ {error,timeout}
+ end
+ end.
+
+%%%=================================================================
+%%% Internal functions
+call(Pid,Msg) ->
+ MRef = erlang:monitor(process,Pid),
+ Ref = make_ref(),
+ Pid ! {Msg,{self(),Ref}},
+ receive
+ {Ref, Result} ->
+ erlang:demonitor(MRef),
+ case Result of
+ {retry,_Data} ->
+ call(Pid,Result);
+ Other ->
+ Other
+ end;
+ {'DOWN',MRef,process,_,Reason} ->
+ {error,{process_down,Pid,Reason}}
+ end.
+
+return({To,Ref},Result) ->
+ To ! {Ref, Result}.
+
+init_gen(Parent,Opts) ->
+ process_flag(trap_exit,true),
+ CtUtilServer = whereis(ct_util_server),
+ link(CtUtilServer),
+ put(silent,false),
+ case catch (Opts#gen_opts.callback):init(Opts#gen_opts.name,
+ Opts#gen_opts.address,
+ Opts#gen_opts.init_data) of
+ {ok,ConnPid,State} when is_pid(ConnPid) ->
+ link(ConnPid),
+ put(conn_pid,ConnPid),
+ Parent ! {connected,self()},
+ loop(Opts#gen_opts{conn_pid=ConnPid,
+ cb_state=State,
+ ct_util_server=CtUtilServer});
+ {error,Reason} ->
+ Parent ! {{error,Reason},self()}
+ end.
+
+loop(Opts) ->
+ receive
+ {'EXIT',Pid,Reason} when Pid==Opts#gen_opts.conn_pid ->
+ log("Connection down!\nOpening new!","Reason: ~p\nAddress: ~p\n",
+ [Reason,Opts#gen_opts.address]),
+ case reconnect(Opts) of
+ {ok, NewPid, NewState} ->
+ link(NewPid),
+ put(conn_pid,NewPid),
+ loop(Opts#gen_opts{conn_pid=NewPid,cb_state=NewState});
+ Error ->
+ ct_util:unregister_connection(self()),
+ log("Reconnect failed. Giving up!","Reason: ~p\n",[Error])
+ end;
+ {'EXIT',Pid,Reason} ->
+ case Opts#gen_opts.ct_util_server of
+ Pid ->
+ exit(Reason);
+ _ ->
+ 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),
+ return(From,ok),
+ ok;
+ {{retry,{Error,_Name,CPid,_Msg}}, From} when CPid == Opts#gen_opts.conn_pid ->
+ %% only retry if failure is because of a reconnection
+ Return = case Error of
+ {error,_} -> Error;
+ Reason -> {error,Reason}
+ end,
+ return(From, Return),
+ loop(Opts);
+ {{retry,{_Error,_Name,_CPid,Msg}}, From} ->
+ log("Rerunning command","Connection reestablished. Rerunning command...",[]),
+ {Return,NewState} =
+ (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state),
+ return(From, Return),
+ loop(Opts#gen_opts{cb_state=NewState});
+ {Msg,From={Pid,_Ref}} when is_pid(Pid) ->
+ {Return,NewState} =
+ (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state),
+ return(From, Return),
+ loop(Opts#gen_opts{cb_state=NewState})
+ end.
+
+nozero({ok,S}) when is_list(S) ->
+ {ok,[C || C <- S,
+ C=/=0,
+ C=/=13]};
+nozero(M) ->
+ M.
+
+reconnect(Opts) ->
+ (Opts#gen_opts.callback):reconnect(Opts#gen_opts.address,
+ Opts#gen_opts.cb_state).
+
+
+log(Func,Args) ->
+ case get(silent) of
+ true when not ?dbg->
+ ok;
+ _ ->
+ apply(ct_logs,Func,Args)
+ end.
+
+
diff --git a/lib/common_test/src/ct_line.erl b/lib/common_test/src/ct_line.erl
new file mode 100644
index 0000000000..4af9da5463
--- /dev/null
+++ b/lib/common_test/src/ct_line.erl
@@ -0,0 +1,266 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Parse transform for inserting line numbers
+
+-module(ct_line).
+
+-record(vars, {module, % atom() Module name
+ vsn, % atom()
+
+ init_info=[], % [{M,F,A,C,L}]
+
+ function, % atom()
+ arity, % int()
+ clause, % int()
+ lines, % [int()]
+ depth, % int()
+ is_guard=false % boolean
+ }).
+
+-export([parse_transform/2,
+ line/1]).
+
+line(LOC={{Mod,Func},_Line}) ->
+ Lines = case get(test_server_loc) of
+ [{{Mod,Func},_}|Ls] ->
+ Ls;
+ Ls when is_list(Ls) ->
+ case length(Ls) of
+ 10 ->
+ [_|T]=lists:reverse(Ls),
+ lists:reverse(T);
+ _ ->
+ Ls
+ end;
+ _ ->
+ []
+ end,
+ put(test_server_loc,[LOC|Lines]).
+
+parse_transform(Forms, _Options) ->
+ transform(Forms, _Options).
+
+%% forms(Fs) -> lists:map(fun (F) -> form(F) end, Fs).
+
+transform(Forms, _Options)->
+ Vars0 = #vars{},
+ {ok, MungedForms, _Vars} = transform(Forms, [], Vars0),
+ MungedForms.
+
+
+transform([Form|Forms], MungedForms, Vars) ->
+ case munge(Form, Vars) of
+ ignore ->
+ transform(Forms, MungedForms, Vars);
+ {MungedForm, Vars2} ->
+ transform(Forms, [MungedForm|MungedForms], Vars2)
+ end;
+transform([], MungedForms, Vars) ->
+ {ok, lists:reverse(MungedForms), Vars}.
+
+%% This code traverses the abstract code, stored as the abstract_code
+%% chunk in the BEAM file, as described in absform(3) for Erlang/OTP R8B
+%% (Vsn=abstract_v2).
+%% The abstract format after preprocessing differs slightly from the abstract
+%% format given eg using epp:parse_form, this has been noted in comments.
+munge(Form={attribute,_,module,Module}, Vars) ->
+ Vars2 = Vars#vars{module=Module},
+ {Form, Vars2};
+
+munge({function,0,module_info,_Arity,_Clauses}, _Vars) ->
+ ignore; % module_info will be added again when the forms are recompiled
+munge({function,Line,Function,Arity,Clauses}, Vars) ->
+ Vars2 = Vars#vars{function=Function,
+ arity=Arity,
+ clause=1,
+ lines=[],
+ depth=1},
+ {MungedClauses, Vars3} = munge_clauses(Clauses, Vars2, []),
+ {{function,Line,Function,Arity,MungedClauses}, Vars3};
+munge(Form, Vars) -> % attributes
+ {Form, Vars}.
+
+munge_clauses([{clause,Line,Pattern,Guards,Body}|Clauses], Vars, MClauses) ->
+ {MungedGuards, _Vars} = munge_exprs(Guards, Vars#vars{is_guard=true},[]),
+
+ case Vars#vars.depth of
+ 1 -> % function clause
+ {MungedBody, Vars2} = munge_body(Body, Vars#vars{depth=2}, []),
+ ClauseInfo = {Vars2#vars.module,
+ Vars2#vars.function,
+ Vars2#vars.arity,
+ Vars2#vars.clause,
+ length(Vars2#vars.lines)},
+ InitInfo = [ClauseInfo | Vars2#vars.init_info],
+ Vars3 = Vars2#vars{init_info=InitInfo,
+ clause=(Vars2#vars.clause)+1,
+ lines=[],
+ depth=1},
+ munge_clauses(Clauses, Vars3,
+ [{clause,Line,Pattern,MungedGuards,MungedBody}|
+ MClauses]);
+
+ 2 -> % receive-, case- or if clause
+ {MungedBody, Vars2} = munge_body(Body, Vars, []),
+ munge_clauses(Clauses, Vars2,
+ [{clause,Line,Pattern,MungedGuards,MungedBody}|
+ MClauses])
+ end;
+munge_clauses([], Vars, MungedClauses) ->
+ {lists:reverse(MungedClauses), Vars}.
+
+munge_body([Expr|Body], Vars, MungedBody) ->
+ %% Here is the place to add a call to cover:bump/6!
+ Line = element(2, Expr),
+ Lines = Vars#vars.lines,
+ case lists:member(Line,Lines) of
+ true -> % already a bump at this line!
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ munge_body(Body, Vars2, [MungedExpr|MungedBody]);
+ false ->
+ Bump = {call, 0, {remote,0,{atom,0,?MODULE},{atom,0,line}},
+ [{tuple,0,[{tuple,0,[{atom,0,Vars#vars.module},
+ {atom, 0, Vars#vars.function}]},
+ {integer, 0, Line}]}]},
+ Lines2 = [Line|Lines],
+
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars#vars{lines=Lines2}),
+ munge_body(Body, Vars2, [MungedExpr,Bump|MungedBody])
+ end;
+munge_body([], Vars, MungedBody) ->
+ {lists:reverse(MungedBody), Vars}.
+
+munge_expr({match,Line,ExprL,ExprR}, Vars) ->
+ {MungedExprL, Vars2} = munge_expr(ExprL, Vars),
+ {MungedExprR, Vars3} = munge_expr(ExprR, Vars2),
+ {{match,Line,MungedExprL,MungedExprR}, Vars3};
+munge_expr({tuple,Line,Exprs}, Vars) ->
+ {MungedExprs, Vars2} = munge_exprs(Exprs, Vars, []),
+ {{tuple,Line,MungedExprs}, Vars2};
+munge_expr({record,Line,Expr,Exprs}, Vars) ->
+ %% Only for Vsn=raw_abstract_v1
+ {MungedExprName, Vars2} = munge_expr(Expr, Vars),
+ {MungedExprFields, Vars3} = munge_exprs(Exprs, Vars2, []),
+ {{record,Line,MungedExprName,MungedExprFields}, Vars3};
+munge_expr({record_field,Line,ExprL,ExprR}, Vars) ->
+ %% Only for Vsn=raw_abstract_v1
+ {MungedExprL, Vars2} = munge_expr(ExprL, Vars),
+ {MungedExprR, Vars3} = munge_expr(ExprR, Vars2),
+ {{record_field,Line,MungedExprL,MungedExprR}, Vars3};
+munge_expr({cons,Line,ExprH,ExprT}, Vars) ->
+ {MungedExprH, Vars2} = munge_expr(ExprH, Vars),
+ {MungedExprT, Vars3} = munge_expr(ExprT, Vars2),
+ {{cons,Line,MungedExprH,MungedExprT}, Vars3};
+munge_expr({op,Line,Op,ExprL,ExprR}, Vars) ->
+ {MungedExprL, Vars2} = munge_expr(ExprL, Vars),
+ {MungedExprR, Vars3} = munge_expr(ExprR, Vars2),
+ {{op,Line,Op,MungedExprL,MungedExprR}, Vars3};
+munge_expr({op,Line,Op,Expr}, Vars) ->
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ {{op,Line,Op,MungedExpr}, Vars2};
+munge_expr({'catch',Line,Expr}, Vars) ->
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ {{'catch',Line,MungedExpr}, Vars2};
+munge_expr({call,Line1,{remote,Line2,ExprM,ExprF},Exprs},
+ Vars) when Vars#vars.is_guard==false->
+ {MungedExprM, Vars2} = munge_expr(ExprM, Vars),
+ {MungedExprF, Vars3} = munge_expr(ExprF, Vars2),
+ {MungedExprs, Vars4} = munge_exprs(Exprs, Vars3, []),
+ {{call,Line1,{remote,Line2,MungedExprM,MungedExprF},MungedExprs}, Vars4};
+munge_expr({call,Line1,{remote,_Line2,_ExprM,ExprF},Exprs},
+ Vars) when Vars#vars.is_guard==true ->
+ %% Difference in abstract format after preprocessing: BIF calls in guards
+ %% are translated to {remote,...} (which is not allowed as source form)
+ %% NOT NECESSARY FOR Vsn=raw_abstract_v1
+ munge_expr({call,Line1,ExprF,Exprs}, Vars);
+munge_expr({call,Line,Expr,Exprs}, Vars) ->
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ {MungedExprs, Vars3} = munge_exprs(Exprs, Vars2, []),
+ {{call,Line,MungedExpr,MungedExprs}, Vars3};
+munge_expr({lc,Line,Expr,LC}, Vars) ->
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ {MungedLC, Vars3} = munge_lc(LC, Vars2, []),
+ {{lc,Line,MungedExpr,MungedLC}, Vars3};
+munge_expr({block,Line,Body}, Vars) ->
+ {MungedBody, Vars2} = munge_body(Body, Vars, []),
+ {{block,Line,MungedBody}, Vars2};
+munge_expr({'if',Line,Clauses}, Vars) ->
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars, []),
+ {{'if',Line,MungedClauses}, Vars2};
+munge_expr({'case',Line,Expr,Clauses}, Vars) ->
+ {MungedExpr,Vars2} = munge_expr(Expr,Vars),
+ {MungedClauses,Vars3} = munge_clauses(Clauses, Vars2, []),
+ {{'case',Line,MungedExpr,MungedClauses}, Vars3};
+munge_expr({'receive',Line,Clauses}, Vars) ->
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars, []),
+ {{'receive',Line,MungedClauses}, Vars2};
+munge_expr({'receive',Line,Clauses,Expr,Body}, Vars) ->
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars, []),
+ {MungedExpr, Vars3} = munge_expr(Expr, Vars2),
+ {MungedBody, Vars4} = munge_body(Body, Vars3, []),
+ {{'receive',Line,MungedClauses,MungedExpr,MungedBody}, Vars4};
+munge_expr({'try',Line,Exprs,Clauses,CatchClauses}, Vars) ->
+ {MungedExprs, Vars1} = munge_exprs(Exprs, Vars, []),
+ {MungedClauses, Vars2} = munge_clauses(Clauses, Vars1, []),
+ {MungedCatchClauses, Vars3} = munge_clauses(CatchClauses, Vars2, []),
+ {{'try',Line,MungedExprs,MungedClauses,MungedCatchClauses}, Vars3};
+%% Difference in abstract format after preprocessing: Funs get an extra
+%% element Extra.
+%% NOT NECESSARY FOR Vsn=raw_abstract_v1
+munge_expr({'fun',Line,{function,Name,Arity},_Extra}, Vars) ->
+ {{'fun',Line,{function,Name,Arity}}, Vars};
+munge_expr({'fun',Line,{clauses,Clauses},_Extra}, Vars) ->
+ {MungedClauses,Vars2}=munge_clauses(Clauses, Vars, []),
+ {{'fun',Line,{clauses,MungedClauses}}, Vars2};
+munge_expr({'fun',Line,{clauses,Clauses}}, Vars) ->
+ %% Only for Vsn=raw_abstract_v1
+ {MungedClauses,Vars2}=munge_clauses(Clauses, Vars, []),
+ {{'fun',Line,{clauses,MungedClauses}}, Vars2};
+munge_expr(Form, Vars) -> % var|char|integer|float|string|atom|nil|bin|eof
+ {Form, Vars}.
+
+munge_exprs([Expr|Exprs], Vars, MungedExprs) when Vars#vars.is_guard==true,
+ is_list(Expr) ->
+ {MungedExpr, _Vars} = munge_exprs(Expr, Vars, []),
+ munge_exprs(Exprs, Vars, [MungedExpr|MungedExprs]);
+munge_exprs([Expr|Exprs], Vars, MungedExprs) ->
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ munge_exprs(Exprs, Vars2, [MungedExpr|MungedExprs]);
+munge_exprs([], Vars, MungedExprs) ->
+ {lists:reverse(MungedExprs), Vars}.
+
+munge_lc([{generate,Line,Pattern,Expr}|LC], Vars, MungedLC) ->
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ munge_lc(LC, Vars2, [{generate,Line,Pattern,MungedExpr}|MungedLC]);
+munge_lc([Expr|LC], Vars, MungedLC) ->
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars),
+ munge_lc(LC, Vars2, [MungedExpr|MungedLC]);
+munge_lc([], Vars, MungedLC) ->
+ {lists:reverse(MungedLC), Vars}.
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl
new file mode 100644
index 0000000000..bd1a89ae1f
--- /dev/null
+++ b/lib/common_test/src/ct_logs.erl
@@ -0,0 +1,1606 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Logging functionality for Common Test Framework.
+%%%
+%%% <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>
+
+-module(ct_logs).
+
+-export([init/1,close/1,init_tc/0,end_tc/1]).
+-export([get_log_dir/0,log/3,start_log/1,cont_log/2,end_log/0]).
+-export([set_stylesheet/2,clear_stylesheet/1]).
+-export([add_external_logs/1,add_link/3]).
+-export([make_last_run_index/0]).
+-export([make_all_suites_index/1,make_all_runs_index/1]).
+
+%% Logging stuff directly from testcase
+-export([tc_log/3,tc_print/3,tc_pal/3]).
+
+%% Simulate logger process for use without ct environment running
+-export([simulate/0]).
+
+-include("ct_event.hrl").
+-include("ct_util.hrl").
+-include_lib("kernel/include/file.hrl").
+
+-define(suitelog_name,"suite.log").
+-define(run_summary, "suite.summary").
+-define(logdir_ext, ".logs").
+-define(ct_log_name, "ctlog.html").
+-define(all_runs_name, "all_runs.html").
+-define(index_name, "index.html").
+-define(totals_name, "totals.info").
+
+-define(table_color1,"#ADD8E6").
+-define(table_color2,"#E4F0FE").
+-define(table_color3,"#F0F8FF").
+
+-define(testname_width, 70).
+
+-define(abs(Name), filename:absname(Name)).
+
+%%%-----------------------------------------------------------------
+%%% @spec init(Mode) -> Result
+%%% Mode = normal | interactive
+%%% Result = {StartTime,LogDir}
+%%% StartTime = term()
+%%% LogDir = string()
+%%%
+%%% @doc 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>
+%%%
+init(Mode) ->
+ Self = self(),
+ Pid = spawn_link(fun() -> logger(Self,Mode) end),
+ MRef = erlang:monitor(process,Pid),
+ receive
+ {started,Pid,Result} ->
+ erlang:demonitor(MRef),
+ Result;
+ {'DOWN',MRef,process,_,Reason} ->
+ exit({could_not_start_process,?MODULE,Reason})
+ end.
+
+make_dirname({{YY,MM,DD},{H,M,S}}) ->
+ io_lib:format(logdir_node_prefix()++".~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w",
+ [YY,MM,DD,H,M,S]).
+
+logdir_prefix() ->
+ "ct_run".
+logdir_node_prefix() ->
+ logdir_prefix()++"."++atom_to_list(node()).
+
+%%%-----------------------------------------------------------------
+%%% @spec close(How) -> ok
+%%%
+%%% @doc Create index pages with test results and close the CT Log
+%%% (tool-internal use only).
+close(How) ->
+ make_last_run_index(),
+
+ ct_event:notify(#event{name=stop_logging,node=node(),data=[]}),
+
+ case whereis(?MODULE) of
+ Pid when is_pid(Pid) ->
+ MRef = erlang:monitor(process,Pid),
+ ?MODULE ! stop,
+ receive
+ {'DOWN',MRef,process,_,_} ->
+ ok
+ end;
+ undefined ->
+ ok
+ end,
+
+ if How == clean ->
+ case cleanup() of
+ ok ->
+ ok;
+ Error ->
+ io:format("Warning! Cleanup failed: ~p~n", [Error])
+ end;
+ true ->
+ file:set_cwd("..")
+ end,
+
+ make_all_suites_index(stop),
+ make_all_runs_index(stop),
+
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec set_stylesheet(TC,SSFile) -> ok
+set_stylesheet(TC, SSFile) ->
+ cast({set_stylesheet,TC,SSFile}).
+
+%%%-----------------------------------------------------------------
+%%% @spec clear_stylesheet(TC) -> ok
+clear_stylesheet(TC) ->
+ cast({clear_stylesheet,TC}).
+
+%%%-----------------------------------------------------------------
+%%% @spec get_log_dir() -> {ok,Dir} | {error,Reason}
+get_log_dir() ->
+ call(get_log_dir).
+
+%%%-----------------------------------------------------------------
+%%% make_last_run_index() -> ok
+make_last_run_index() ->
+ call(make_last_run_index).
+
+call(Msg) ->
+ case whereis(?MODULE) of
+ undefined ->
+ {error,does_not_exist};
+ Pid ->
+ MRef = erlang:monitor(process,Pid),
+ Ref = make_ref(),
+ ?MODULE ! {Msg,{self(),Ref}},
+ receive
+ {Ref, Result} ->
+ erlang:demonitor(MRef),
+ Result;
+ {'DOWN',MRef,process,_,Reason} ->
+ {error,{process_down,?MODULE,Reason}}
+ end
+ end.
+
+return({To,Ref},Result) ->
+ To ! {Ref, Result}.
+
+cast(Msg) ->
+ case whereis(?MODULE) of
+ undefined ->
+ {error,does_not_exist};
+ _Pid ->
+ ?MODULE ! Msg
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @spec init_tc() -> ok
+%%%
+%%% @doc Test case initiation (tool-internal use only).
+%%%
+%%% <p>This function is called by ct_framework:init_tc/3</p>
+init_tc() ->
+ call({init_tc,self(),group_leader()}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec end_tc(TCPid) -> ok | {error,Reason}
+%%%
+%%% @doc Test case clean up (tool-internal use only).
+%%%
+%%% <p>This function is called by ct_framework:end_tc/3</p>
+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 log(Heading,Format,Args) -> ok
+%%%
+%%% @doc 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>
+%%%
+%%% <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>
+log(Heading,Format,Args) ->
+ cast({log,self(),group_leader(),
+ [{int_header(),[log_timestamp(now()),Heading]},
+ {Format,Args},
+ {int_footer(),[]}]}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec start_log(Heading) -> ok
+%%%
+%%% @doc 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>
+%%%
+%%% <p>For information about the parameters, see <code>log/3</code>.</p>
+%%%
+%%% @see log/3
+%%% @see cont_log/2
+%%% @see end_log/0
+start_log(Heading) ->
+ cast({log,self(),group_leader(),
+ [{int_header(),[log_timestamp(now()),Heading]}]}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec cont_log(Format,Args) -> ok
+%%%
+%%% @doc Adds information about an activity (tool-internal use only).
+%%%
+%%% @see start_log/1
+%%% @see end_log/0
+cont_log([],[]) ->
+ ok;
+cont_log(Format,Args) ->
+ maybe_log_timestamp(),
+ cast({log,self(),group_leader(),[{Format,Args}]}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec end_log() -> ok
+%%%
+%%% @doc Ends the logging of an activity (tool-internal use only).
+%%%
+%%% @see start_log/1
+%%% @see cont_log/2
+end_log() ->
+ cast({log,self(),group_leader(),[{int_footer(), []}]}),
+ 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
+%%% log.
+%%%
+%%% <p>The given <code>Logs</code> must exist in the priv dir of the
+%%% calling test suite.</p>
+add_external_logs(Logs) ->
+ start_log("External Logs"),
+ [cont_log("<a href=~p>~s</a>\n",
+ [filename:join("log_private",Log),Log]) || Log <- Logs],
+ end_log().
+
+%%%-----------------------------------------------------------------
+%%% @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
+%%% calling test suite.
+add_link(Heading,File,Type) ->
+ log(Heading,"<a href=~p type=~p>~s</a>\n",
+ [filename:join("log_private",File),Type,File]).
+
+
+
+%%%-----------------------------------------------------------------
+%%% @spec tc_log(Category,Format,Args) -> ok
+%%% Category = atom()
+%%% Format = string()
+%%% Args = list()
+%%%
+%%% @doc Printout from a testcase.
+%%%
+%%% <p>This function is called by <code>ct</code> when logging
+%%% stuff directly from a testcase (i.e. not from within the CT
+%%% framework).</p>
+tc_log(Category,Format,Args) ->
+ cast({log,self(),group_leader(),[{div_header(Category),[]},
+ {Format,Args},
+ {div_footer(),[]}]}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec tc_print(Category,Format,Args) -> ok
+%%% Category = atom()
+%%% Format = string()
+%%% Args = list()
+%%%
+%%% @doc Console printout from a testcase.
+%%%
+%%% <p>This function is called by <code>ct</code> when printing
+%%% stuff a testcase on the user console.</p>
+tc_print(Category,Format,Args) ->
+ print_heading(Category),
+ io:format(user,Format,Args),
+ io:format(user,"\n\n",[]),
+ ok.
+
+print_heading(default) ->
+ io:format(user,
+ "----------------------------------------------------\n~s\n",
+ [log_timestamp(now())]);
+print_heading(Category) ->
+ io:format(user,
+ "----------------------------------------------------\n~s ~w\n",
+ [log_timestamp(now()),Category]).
+
+
+%%%-----------------------------------------------------------------
+%%% @spec tc_pal(Category,Format,Args) -> ok
+%%% Category = atom()
+%%% Format = string()
+%%% Args = list()
+%%%
+%%% @doc Print and log from a testcase.
+%%%
+%%% <p>This function is called by <code>ct</code> when logging
+%%% stuff directly from a testcase. The info is written both in the
+%%% log and on the console.</p>
+tc_pal(Category,Format,Args) ->
+ tc_print(Category,Format,Args),
+ cast({log,self(),group_leader(),[{div_header(Category),[]},
+ {Format,Args},
+ {div_footer(),[]}]}),
+ ok.
+
+
+%%%=================================================================
+%%% Internal functions
+int_header() ->
+ "<div class=\"ct_internal\"><b>*** CT ~s *** ~s</b>".
+int_footer() ->
+ "</div>".
+
+div_header(Class) ->
+ "<div class=\"" ++ atom_to_list(Class) ++ "\"><b>*** User " ++
+ log_timestamp(now()) ++ " ***</b>".
+div_footer() ->
+ "</div>".
+
+
+maybe_log_timestamp() ->
+ {MS,S,US} = now(),
+ case get(log_timestamp) of
+ {MS,S,_} ->
+ ok;
+ _ ->
+ cast({log,self(),group_leader(),
+ [{"<i>~s</i>",[log_timestamp({MS,S,US})]}]})
+ end.
+
+log_timestamp(Now) ->
+ put(log_timestamp,Now),
+ {_,{H,M,S}} = calendar:now_to_local_time(Now),
+ lists:flatten(io_lib:format("~2.2.0w:~2.2.0w:~2.2.0w",
+ [H,M,S])).
+
+%%%-----------------------------------------------------------------
+%%% The logger server
+-record(logger_state,{parent,
+ log_dir,
+ start_time,
+ orig_GL,
+ ct_log_fd,
+ tc_groupleaders,
+ stylesheet}).
+
+logger(Parent,Mode) ->
+ register(?MODULE,self()),
+
+ %%! Below is a temporary workaround for the limitation of
+ %%! max one test run per second.
+ %%! --->
+ Time0 = calendar:local_time(),
+ Dir0 = make_dirname(Time0),
+ {Time,Dir} =
+ case filelib:is_dir(Dir0) of
+ true ->
+ timer:sleep(1000),
+ Time1 = calendar:local_time(),
+ Dir1 = make_dirname(Time1),
+
+ {Time1,Dir1};
+ false ->
+ {Time0,Dir0}
+ end,
+ %%! <---
+
+ file:make_dir(Dir),
+ ct_event:notify(#event{name=start_logging,node=node(),
+ data=?abs(Dir)}),
+ make_all_suites_index(start),
+ make_all_runs_index(start),
+ case Mode of
+ interactive -> interactive_link();
+ _ -> ok
+ end,
+ file:set_cwd(Dir),
+ make_last_run_index(Time),
+ CtLogFd = open_ctlog(),
+ io:format(CtLogFd,int_header()++int_footer(),
+ [log_timestamp(now()),"Common Test Logger started"]),
+ Parent ! {started,self(),{Time,filename:absname("")}},
+ set_evmgr_gl(CtLogFd),
+ logger_loop(#logger_state{parent=Parent,
+ log_dir=Dir,
+ start_time=Time,
+ orig_GL=group_leader(),
+ ct_log_fd=CtLogFd,
+ tc_groupleaders=[]}).
+
+logger_loop(State) ->
+ receive
+ {log,Pid,GL,List} ->
+ case get_groupleader(Pid,GL,State) of
+ {tc_log,TCGL,TCGLs} ->
+ case erlang:is_process_alive(TCGL) of
+ true ->
+ %% we have to build one io-list of all strings
+ %% before printing, or other io printouts (made in
+ %% parallel) may get printed between this header
+ %% and footer
+ Fun =
+ fun({Str,Args},IoList) ->
+ case catch io_lib:format(Str,Args) of
+ {'EXIT',_Reason} ->
+ Fd = State#logger_state.ct_log_fd,
+ io:format(Fd,
+ "Logging fails! Str: ~p, Args: ~p~n",
+ [Str,Args]),
+ %% stop the testcase, we need to see the fault
+ exit(Pid,logging_failed),
+ ok;
+ IoStr when IoList == [] ->
+ [IoStr];
+ IoStr ->
+ [IoList,"\n",IoStr]
+ end
+ end,
+ io:format(TCGL,"~s",[lists:foldl(Fun,[],List)]),
+ logger_loop(State#logger_state{tc_groupleaders=TCGLs});
+ false ->
+ %% Group leader is dead, so write to the CtLog instead
+ Fd = State#logger_state.ct_log_fd,
+ [begin io:format(Fd,Str,Args),io:nl(Fd) end ||
+ {Str,Args} <- List],
+ logger_loop(State)
+ end;
+ {ct_log,Fd,TCGLs} ->
+ [begin io:format(Fd,Str,Args),io:nl(Fd) end || {Str,Args} <- List],
+ logger_loop(State#logger_state{tc_groupleaders=TCGLs})
+ end;
+ {{init_tc,TCPid,GL},From} ->
+ print_style(GL, State#logger_state.stylesheet),
+ set_evmgr_gl(GL),
+ TCGLs = add_tc_gl(TCPid,GL,State),
+ return(From,ok),
+ logger_loop(State#logger_state{tc_groupleaders=TCGLs});
+ {{end_tc,TCPid},From} ->
+ set_evmgr_gl(State#logger_state.ct_log_fd),
+ return(From,ok),
+ logger_loop(State#logger_state{tc_groupleaders=rm_tc_gl(TCPid,State)});
+ {get_log_dir,From} ->
+ return(From,{ok,State#logger_state.log_dir}),
+ logger_loop(State);
+ {make_last_run_index,From} ->
+ make_last_run_index(State#logger_state.start_time),
+ return(From,State#logger_state.log_dir),
+ logger_loop(State);
+ {set_stylesheet,_,SSFile} when State#logger_state.stylesheet == SSFile ->
+ logger_loop(State);
+ {set_stylesheet,TC,SSFile} ->
+ Fd = State#logger_state.ct_log_fd,
+ io:format(Fd, "~p uses external style sheet: ~s~n", [TC,SSFile]),
+ logger_loop(State#logger_state{stylesheet=SSFile});
+ {clear_stylesheet,_} when State#logger_state.stylesheet == undefined ->
+ logger_loop(State);
+ {clear_stylesheet,_} ->
+ logger_loop(State#logger_state{stylesheet=undefined});
+ stop ->
+ io:format(State#logger_state.ct_log_fd,
+ int_header()++int_footer(),
+ [log_timestamp(now()),"Common Test Logger finished"]),
+ close_ctlog(State#logger_state.ct_log_fd),
+ ok
+ end.
+
+%% #logger_state.tc_groupleaders == [{Pid,{Type,GLPid}},...]
+%% Type = tc | io
+%%
+%% Pid can either be a test case process (tc), an IO process (io)
+%% spawned by a test case process, or a common test process (never
+%% registered by an init_tc msg). An IO process gets registered the
+%% first time it sends data and will be stored in the list until the
+%% last TC process associated with the same group leader gets
+%% unregistered.
+%%
+%% If a process that has not been spawned by a test case process
+%% sends a log request, the data will be printed to a test case
+%% log file *if* there exists one registered process only in the
+%% tc_groupleaders list. If multiple test case processes are
+%% running, the data gets printed to the CT framework log instead.
+%%
+%% Note that an external process must not be registered as an IO
+%% process since it could then accidentally be associated with
+%% the first test case process that starts in a group of parallel
+%% cases (if the log request would come in between the registration
+%% of the first and second test case process).
+
+get_groupleader(Pid,GL,State) ->
+ TCGLs = State#logger_state.tc_groupleaders,
+ %% check if Pid is registered either as a TC or IO process
+ case proplists:get_value(Pid,TCGLs) of
+ undefined ->
+ %% this could be a process spawned by the test case process,
+ %% if so they have the same registered group leader
+ case lists:keysearch({tc,GL},2,TCGLs) of
+ {value,_} ->
+ %% register the io process
+ {tc_log,GL,[{Pid,{io,GL}}|TCGLs]};
+ false ->
+ %% check if only one test case is executing,
+ %% if so return the group leader for it
+ case [TCGL || {_,{Type,TCGL}} <- TCGLs, Type == tc] of
+ [TCGL] ->
+ %% an external process sending the log
+ %% request, don't register
+ {tc_log,TCGL,TCGLs};
+ _ ->
+ {ct_log,State#logger_state.ct_log_fd,TCGLs}
+ end
+ end;
+ {_,GL} ->
+ {tc_log,GL,TCGLs};
+ _ ->
+ %% special case where a test case io process has changed
+ %% its group leader to an non-registered GL process
+ TCGLs1 = proplists:delete(Pid,TCGLs),
+ case [TCGL || {_,{Type,TCGL}} <- TCGLs1, Type == tc] of
+ [TCGL] ->
+ {tc_log,TCGL,TCGLs1};
+ _ ->
+ {ct_log,State#logger_state.ct_log_fd,TCGLs1}
+ end
+ end.
+
+add_tc_gl(TCPid,GL,State) ->
+ TCGLs = State#logger_state.tc_groupleaders,
+ [{TCPid,{tc,GL}} | lists:keydelete(TCPid,1,TCGLs)].
+
+rm_tc_gl(TCPid,State) ->
+ TCGLs = State#logger_state.tc_groupleaders,
+ case proplists:get_value(TCPid,TCGLs) of
+ {tc,GL} ->
+ TCGLs1 = lists:keydelete(TCPid,1,TCGLs),
+ case lists:keysearch({tc,GL},2,TCGLs1) of
+ {value,_} ->
+ %% test cases using GL remain, keep associated IO processes
+ TCGLs1;
+ false ->
+ %% last test case using GL, delete all associated IO processes
+ lists:filter(fun({_,{io,GLPid}}) when GL == GLPid -> false;
+ (_) -> true
+ end, TCGLs1)
+ end;
+ _ ->
+ %% add_tc_gl has not been called for this Pid, ignore
+ TCGLs
+ end.
+
+set_evmgr_gl(GL) ->
+ case whereis(?CT_EVMGR_REF) of
+ undefined -> ok;
+ EvMgrPid -> group_leader(GL,EvMgrPid)
+ end.
+
+open_ctlog() ->
+ {ok,Fd} = file:open(?ct_log_name,[write]),
+ io:format(Fd,header("Common Test Framework"),[]),
+ case file:consult(ct_run:variables_file_name("../")) of
+ {ok,Vars} ->
+ io:format(Fd, config_table(Vars), []);
+ {error,Reason} ->
+ {ok,Cwd} = file:get_cwd(),
+ Dir = filename:dirname(Cwd),
+ Variables = ct_run:variables_file_name(Dir),
+ io:format(Fd,
+ "Can not read the file \'~s\' Reason: ~w\n"
+ "No configuration found for test!!\n",
+ [Variables,Reason])
+ end,
+ print_style(Fd,undefined),
+ io:format(Fd,
+ "<br><br><h2>Progress Log</h2>\n"
+ "<pre>\n",[]),
+ Fd.
+
+print_style(Fd,undefined) ->
+ io:format(Fd,
+ "<style>\n"
+ "div.ct_internal { background:lightgrey; color:black }\n"
+ "div.default { background:lightgreen; color:black }\n"
+ "</style>\n",
+ []);
+
+print_style(Fd,StyleSheet) ->
+ case file:read_file(StyleSheet) of
+ {ok,Bin} ->
+ Str = binary_to_list(Bin),
+ Pos0 = case string:str(Str,"<style>") of
+ 0 -> string:str(Str,"<STYLE>");
+ N0 -> N0
+ end,
+ case Pos0 of
+ 0 -> print_style_error(Fd,StyleSheet,missing_style_tag);
+ _ ->
+ Pos1 = case string:str(Str,"</style>") of
+ 0 -> string:str(Str,"</STYLE>");
+ N1 -> N1
+ end,
+ case Pos1 of
+ 0 ->
+ print_style_error(Fd,StyleSheet,missing_style_end_tag);
+ _ ->
+ Style = string:sub_string(Str,Pos0,Pos1+7),
+ io:format(Fd,"~s\n",[Style])
+ end
+ end;
+ {error,Reason} ->
+ print_style_error(Fd,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 ~s: ~p -->\n",
+ [StyleSheet,Reason]),
+ print_style(Fd,undefined).
+
+close_ctlog(Fd) ->
+ io:format(Fd,"</pre>",[]),
+ io:format(Fd,footer(),[]),
+ file:close(Fd).
+
+
+
+%%%-----------------------------------------------------------------
+%%% Make an index page for the last run
+make_last_run_index(StartTime) ->
+ IndexName = ?index_name,
+ AbsIndexName = ?abs(IndexName),
+ case catch make_last_run_index1(StartTime,IndexName) of
+ {'EXIT', Reason} ->
+ io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"),
+ io:format("~p~n", [Reason]),
+ {error, Reason};
+ {error, Reason} ->
+ io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"),
+ io:format("~p~n", [Reason]),
+ {error, Reason};
+ ok ->
+% io:put_chars("done\n"),
+ ok;
+ Err ->
+ io:format("Unknown internal error while updating ~s. "
+ "Please report.\n(Err: ~p, ID: 1)",
+ [AbsIndexName,Err]),
+ {error, Err}
+ end.
+
+make_last_run_index1(StartTime,IndexName) ->
+ %% this manoeuvre is to ensure the tests get logged
+ %% in correct order of time (the 1 sec resolution
+ %% of the dirnames may be too big)
+ Logs1 =
+ case filelib:wildcard([$*|?logdir_ext]) of
+ [Log] -> % first test
+ [Log];
+ Logs ->
+ case read_totals_file(?totals_name) of
+ {_Node,Logs0,_Totals} ->
+ insert_dirs(Logs,Logs0);
+ _ ->
+ %% someone deleted the totals file!?
+ Logs
+ end
+ end,
+ Missing =
+ case file:read_file(?missing_suites_info) of
+ {ok,Bin} -> binary_to_term(Bin);
+ _ -> []
+ end,
+ {ok,Index0,Totals} = make_last_run_index(Logs1, index_header(StartTime),
+ 0, 0, 0, 0, 0, Missing),
+ %% write current Totals to file, later to be used in all_runs log
+ write_totals_file(?totals_name,Logs1,Totals),
+ Index = [Index0|index_footer()],
+ case force_write_file(IndexName, Index) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ {error,{index_write_error, Reason}}
+ end.
+
+insert_dirs([NewDir|NewDirs],Dirs) ->
+ Dirs1 = insert_dir(NewDir,Dirs),
+ insert_dirs(NewDirs,Dirs1);
+insert_dirs([],Dirs) ->
+ Dirs.
+insert_dir(D,Dirs=[D|_]) ->
+ Dirs;
+insert_dir(D,[D1|Ds]) ->
+ [D1|insert_dir(D,Ds)];
+insert_dir(D,[]) ->
+ [D].
+
+make_last_run_index([Name|Rest], Result, TotSucc, TotFail, UserSkip, AutoSkip,
+ TotNotBuilt, Missing) ->
+ case last_test(Name) of
+ false ->
+ %% Silently skip.
+ make_last_run_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip,
+ TotNotBuilt, Missing);
+ LastLogDir ->
+ SuiteName = filename:rootname(filename:basename(Name)),
+ case make_one_index_entry(SuiteName, LastLogDir, false, Missing) of
+ {Result1,Succ,Fail,USkip,ASkip,NotBuilt} ->
+ %% for backwards compatibility
+ AutoSkip1 = case catch AutoSkip+ASkip of
+ {'EXIT',_} -> undefined;
+ Res -> Res
+ end,
+ make_last_run_index(Rest, [Result|Result1], TotSucc+Succ,
+ TotFail+Fail, UserSkip+USkip, AutoSkip1,
+ TotNotBuilt+NotBuilt, Missing);
+ error ->
+ make_last_run_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip,
+ TotNotBuilt, Missing)
+ end
+ end;
+make_last_run_index([], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, _) ->
+ {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, false)],
+ {TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}}.
+
+make_one_index_entry(SuiteName, LogDir, All, Missing) ->
+ case count_cases(LogDir) of
+ {Succ,Fail,UserSkip,AutoSkip} ->
+ NotBuilt = not_built(SuiteName, LogDir, All, Missing),
+ NewResult = make_one_index_entry1(SuiteName, LogDir, Succ, Fail,
+ UserSkip, AutoSkip, NotBuilt, All),
+ {NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt};
+ error ->
+ error
+ end.
+
+make_one_index_entry1(SuiteName, Link, Success, Fail, UserSkip, AutoSkip,
+ NotBuilt, All) ->
+ LogFile = filename:join(Link, ?suitelog_name ++ ".html"),
+ CrashDumpName = SuiteName ++ "_erl_crash.dump",
+ CrashDumpLink =
+ case filelib:is_file(CrashDumpName) of
+ true ->
+ ["&nbsp;<A HREF=\"", CrashDumpName,
+ "\">(CrashDump)</A>"];
+ false ->
+ ""
+ end,
+ {Timestamp,Node,AllInfo} =
+ case All of
+ {true,OldRuns} ->
+ [_Prefix,NodeOrDate|_] = string:tokens(Link,"."),
+ Node1 = case string:chr(NodeOrDate,$@) of
+ 0 -> "-";
+ _ -> NodeOrDate
+ end,
+ N = ["<TD ALIGN=right>",Node1,"</TD>\n"],
+ CtRunDir = filename:dirname(filename:dirname(Link)),
+ T = ["<TD>",timestamp(CtRunDir),"</TD>\n"],
+ CtLogFile = filename:join(CtRunDir,?ct_log_name),
+ OldRunsLink =
+ case OldRuns of
+ [] -> "none";
+ _ -> "<A HREF=\""++?all_runs_name++"\">Old Runs</A>"
+ end,
+ A=["<TD><A HREF=\"",CtLogFile,"\">CT Log</A></TD>\n",
+ "<TD>",OldRunsLink,"</TD>\n"],
+ {T,N,A};
+ false ->
+ {"","",""}
+ end,
+ NotBuiltStr =
+ if NotBuilt == 0 ->
+ ["<TD ALIGN=right>",integer_to_list(NotBuilt),"</TD>\n"];
+ true ->
+ ["<TD ALIGN=right><A HREF=\"",?ct_log_name,"\">",
+ integer_to_list(NotBuilt),"</A></TD>\n"]
+ end,
+ FailStr =
+ if Fail > 0 ->
+ ["<FONT color=\"red\">",
+ integer_to_list(Fail),"</FONT>"];
+ true ->
+ integer_to_list(Fail)
+ end,
+ {AllSkip,UserSkipStr,AutoSkipStr} =
+ if AutoSkip == undefined -> {UserSkip,"?","?"};
+ true ->
+ ASStr = if AutoSkip > 0 ->
+ ["<FONT color=\"brown\">",
+ integer_to_list(AutoSkip),"</FONT>"];
+ true -> integer_to_list(AutoSkip)
+ end,
+ {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr}
+ end,
+ ["<TR valign=top>\n",
+ "<TD><A HREF=\"",LogFile,"\">",SuiteName,"</A>",CrashDumpLink,"</TD>\n",
+ Timestamp,
+ "<TD ALIGN=right>",integer_to_list(Success),"</TD>\n",
+ "<TD ALIGN=right>",FailStr,"</TD>\n",
+ "<TD ALIGN=right>",integer_to_list(AllSkip),
+ " (",UserSkipStr,"/",AutoSkipStr,")</TD>\n",
+ NotBuiltStr,
+ Node,
+ AllInfo,
+ "</TR>\n"].
+total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) ->
+ {TimestampCell,AllInfo} =
+ case All of
+ true ->
+ {"<TD>&nbsp;</TD>\n","<TD>&nbsp;</TD>\n<TD>&nbsp;</TD>\n"};
+ false ->
+ {"",""}
+ end,
+
+ {AllSkip,UserSkipStr,AutoSkipStr} =
+ if AutoSkip == undefined -> {UserSkip,"?","?"};
+ true -> {UserSkip+AutoSkip,
+ integer_to_list(UserSkip),integer_to_list(AutoSkip)}
+ end,
+ ["<TR valign=top>\n",
+ "<TD><B>Total</B></TD>",
+ TimestampCell,
+ "<TD ALIGN=right><B>",integer_to_list(Success),"<B></TD>\n",
+ "<TD ALIGN=right><B>",integer_to_list(Fail),"<B></TD>\n",
+ "<TD ALIGN=right>",integer_to_list(AllSkip),
+ " (",UserSkipStr,"/",AutoSkipStr,")</TD>\n",
+ "<TD ALIGN=right><B>",integer_to_list(NotBuilt),"<B></TD>\n",
+ AllInfo,
+ "</TR>\n"].
+
+not_built(_BaseName,_LogDir,_All,[]) ->
+ 0;
+not_built(BaseName,_LogDir,_All,Missing) ->
+ %% find out how many suites didn't compile
+ %% BaseName =
+ %% Top.ObjDir | Top.ObjDir.suites | Top.ObjDir.Suite |
+ %% Top.ObjDir.Suite.cases | Top.ObjDir.Suite.Case
+ Failed =
+ case string:tokens(BaseName,".") of
+ [T,O] when is_list(T) -> % all under Top.ObjDir
+ locate_info({T,O},all,Missing);
+ [T,O,"suites"] ->
+ locate_info({T,O},suites,Missing);
+ [T,O,S] ->
+ locate_info({T,O},list_to_atom(S),Missing);
+ [T,O,S,_] ->
+ locate_info({T,O},list_to_atom(S),Missing);
+ _ -> % old format - don't crash
+ []
+ end,
+ length(Failed).
+
+locate_info(Path={Top,Obj},AllOrSuite,[{{Dir,Suite},Failed}|Errors]) ->
+ case lists:reverse(filename:split(Dir)) of
+ ["test",Obj,Top|_] ->
+ get_missing_suites(AllOrSuite,{Suite,Failed}) ++
+ locate_info(Path,AllOrSuite,Errors);
+ [Obj,Top|_] ->
+ get_missing_suites(AllOrSuite,{Suite,Failed}) ++
+ locate_info(Path,AllOrSuite,Errors);
+ _ ->
+ locate_info(Path,AllOrSuite,Errors)
+ end;
+locate_info(_,_,[]) ->
+ [].
+
+get_missing_suites(all,{"all",Failed}) ->
+ Failed;
+get_missing_suites(suites,{_Suite,Failed}) ->
+ Failed;
+get_missing_suites(Suite,{Suite,Failed}) ->
+ Failed;
+get_missing_suites(_,_) ->
+ [].
+
+term_to_text(Term) ->
+ lists:flatten(io_lib:format("~p.\n", [Term])).
+
+
+%%% Headers and footers.
+
+index_header(StartTime) ->
+ [header("Test Results " ++ format_time(StartTime)) |
+ ["<CENTER>\n",
+ "<P><A HREF=\"",?ct_log_name,"\">Common Test Framework Log</A></P>",
+ "<TABLE border=\"3\" cellpadding=\"5\" "
+ "BGCOLOR=\"",?table_color3,"\">\n"
+ "<th><B>Name</B></th>\n",
+ "<th><font color=\"",?table_color3,"\">_</font>Ok"
+ "<font color=\"",?table_color3,"\">_</font></th>\n"
+ "<th>Failed</th>\n",
+ "<th>Skipped<br>(User/Auto)</th>\n"
+ "<th>Missing<br>Suites</th>\n"
+ "\n"]].
+
+all_suites_index_header() ->
+ [header("Test Results") |
+ ["<CENTER>\n",
+ "<A HREF=\"",?all_runs_name,"\">All Test Runs in this directory</A>\n",
+ "<br><br>\n",
+ "<TABLE border=\"3\" cellpadding=\"5\" "
+ "BGCOLOR=\"",?table_color2,"\">\n"
+ "<th>Name</th>\n",
+ "<th>Test Run Started</th>\n",
+ "<th><font color=\"",?table_color2,"\">_</font>Ok"
+ "<font color=\"",?table_color2,"\">_</font></th>\n"
+ "<th>Failed</th>\n",
+ "<th>Skipped<br>(User/Auto)</th>\n"
+ "<th>Missing<br>Suites</th>\n"
+ "<th>Node</th>\n",
+ "<th>CT Log</th>\n",
+ "<th>Old Runs</th>\n",
+ "\n"]].
+
+all_runs_header() ->
+ [header("All test runs in current directory") |
+ ["<CENTER><TABLE border=\"3\" cellpadding=\"5\" "
+ "BGCOLOR=\"",?table_color1,"\">\n"
+ "<th><B>History</B></th>\n"
+ "<th><B>Node</B></th>\n"
+ "<th>Tests</th>\n"
+ "<th><B>Names</B></th>\n"
+ "<th>Total</th>\n"
+ "<th><font color=\"",?table_color1,"\">_</font>Ok"
+ "<font color=\"",?table_color1,"\">_</font></th>\n"
+ "<th>Failed</th>\n"
+ "<th>Skipped<br>(User/Auto)</th>\n"
+ "<th>Missing<br>Suites</th>\n"
+ "\n"]].
+
+header(Title) ->
+ ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n"
+ "<!-- autogenerated by '"++atom_to_list(?MODULE)++"'. -->\n"
+ "<HTML>\n",
+ "<HEAD>\n",
+
+ "<TITLE>" ++ Title ++ "</TITLE>\n",
+ "<META HTTP-EQUIV=\"CACHE-CONTROL\" CONTENT=\"NO-CACHE\">\n",
+
+ "</HEAD>\n",
+
+ body_tag(),
+
+ "<!-- ---- DOCUMENT TITLE ---- -->\n",
+
+ "<CENTER>\n",
+ "<H1>" ++ Title ++ "</H1>\n",
+ "</CENTER>\n",
+
+ "<!-- ---- CONTENT ---- -->\n"].
+
+index_footer() ->
+ ["</TABLE>\n"
+ "</CENTER>\n" | footer()].
+
+footer() ->
+ ["<P><CENTER>\n"
+ "<HR>\n"
+ "<P><FONT SIZE=-1>\n"
+ "Copyright &copy; ", year(),
+ " <A HREF=\"http://erlang.ericsson.se\">Open Telecom Platform</A><BR>\n"
+ "Updated: <!date>", current_time(), "<!/date><BR>\n"
+ "</FONT>\n"
+ "</CENTER>\n"
+ "</body>\n"].
+
+
+body_tag() ->
+ "<body bgcolor=\"#FFFFFF\" text=\"#000000\" link=\"#0000FF\""
+ "vlink=\"#800080\" alink=\"#FF0000\">\n".
+
+current_time() ->
+ format_time(calendar:local_time()).
+
+format_time({{Y, Mon, D}, {H, Min, S}}) ->
+ Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)),
+ lists:flatten(io_lib:format("~s ~s ~p ~w ~2.2.0w:~2.2.0w:~2.2.0w",
+ [Weekday, month(Mon), D, Y, H, Min, S])).
+
+weekday(1) -> "Mon";
+weekday(2) -> "Tue";
+weekday(3) -> "Wed";
+weekday(4) -> "Thu";
+weekday(5) -> "Fri";
+weekday(6) -> "Sat";
+weekday(7) -> "Sun".
+
+month(1) -> "Jan";
+month(2) -> "Feb";
+month(3) -> "Mar";
+month(4) -> "Apr";
+month(5) -> "May";
+month(6) -> "Jun";
+month(7) -> "Jul";
+month(8) -> "Aug";
+month(9) -> "Sep";
+month(10) -> "Oct";
+month(11) -> "Nov";
+month(12) -> "Dec".
+
+year() ->
+ {Y, _, _} = date(),
+ integer_to_list(Y).
+
+
+%% Count test cases in the given directory (a directory of the type
+%% run.1997-08-04_09.58.52).
+
+count_cases(Dir) ->
+ SumFile = filename:join(Dir, ?run_summary),
+ case read_summary(SumFile, [summary]) of
+ {ok, [{Succ,Fail,Skip}]} ->
+ {Succ,Fail,Skip,undefined};
+ {ok, [Summary]} ->
+ Summary;
+ {error, _} ->
+ LogFile = filename:join(Dir, ?suitelog_name),
+ case file:read_file(LogFile) of
+ {ok, Bin} ->
+ case count_cases1(binary_to_list(Bin),
+ {undefined,undefined,undefined,undefined}) of
+ {error,not_complete} ->
+ %% The test is not complete - dont write summary
+ %% file yet.
+ {0,0,0,0};
+ Summary ->
+ write_summary(SumFile, Summary),
+ Summary
+ end;
+ {error, _Reason} ->
+ io:format("\nFailed to read ~p (skipped)\n", [LogFile]),
+ error
+ end
+ end.
+
+write_summary(Name, Summary) ->
+ File = [term_to_text({summary, Summary})],
+ force_write_file(Name, File).
+
+read_summary(Name, Keys) ->
+ case file:consult(Name) of
+ {ok, []} ->
+ {error, "Empty summary file"};
+ {ok, Terms} ->
+ {ok, lists:map(fun(Key) -> {value, {_, Value}} =
+ lists:keysearch(Key, 1, Terms),
+ Value end,
+ Keys)};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+count_cases1("=failed" ++ Rest, {Success, _Fail, UserSkip,AutoSkip}) ->
+ {NextLine, Count} = get_number(Rest),
+ count_cases1(NextLine, {Success, Count, UserSkip,AutoSkip});
+count_cases1("=successful" ++ Rest, {_Success, Fail, UserSkip,AutoSkip}) ->
+ {NextLine, Count} = get_number(Rest),
+ count_cases1(NextLine, {Count, Fail, UserSkip,AutoSkip});
+count_cases1("=skipped" ++ Rest, {Success, Fail, _UserSkip,_AutoSkip}) ->
+ {NextLine, Count} = get_number(Rest),
+ count_cases1(NextLine, {Success, Fail, Count,undefined});
+count_cases1("=user_skipped" ++ Rest, {Success, Fail, _UserSkip,AutoSkip}) ->
+ {NextLine, Count} = get_number(Rest),
+ count_cases1(NextLine, {Success, Fail, Count,AutoSkip});
+count_cases1("=auto_skipped" ++ Rest, {Success, Fail, UserSkip,_AutoSkip}) ->
+ {NextLine, Count} = get_number(Rest),
+ count_cases1(NextLine, {Success, Fail, UserSkip,Count});
+count_cases1([], {Su,F,USk,_ASk}) when Su==undefined;F==undefined;
+ USk==undefined ->
+ {error,not_complete};
+count_cases1([], Counters) ->
+ Counters;
+count_cases1(Other, Counters) ->
+ count_cases1(skip_to_nl(Other), Counters).
+
+get_number([$\s|Rest]) ->
+ get_number(Rest);
+get_number([Digit|Rest]) when $0 =< Digit, Digit =< $9 ->
+ get_number(Rest, Digit-$0).
+
+get_number([Digit|Rest], Acc) when $0 =< Digit, Digit =< $9 ->
+ get_number(Rest, Acc*10+Digit-$0);
+get_number([$\n|Rest], Acc) ->
+ {Rest, Acc};
+get_number([_|Rest], Acc) ->
+ get_number(Rest, Acc).
+
+skip_to_nl([$\n|Rest]) ->
+ Rest;
+skip_to_nl([_|Rest]) ->
+ skip_to_nl(Rest);
+skip_to_nl([]) ->
+ [].
+
+
+config_table(Vars) ->
+ [config_table_header()|config_table1(Vars)].
+
+config_table_header() ->
+ ["<h2>Configuration</h2>\n",
+ "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color1,
+ "\"\n",
+ "<tr><th>Key</th><th>Value</th></tr>\n"].
+
+config_table1([{Key,Value}|Vars]) ->
+ ["<tr><td>", atom_to_list(Key), "</td>\n",
+ "<td><pre>",io_lib:format("~p",[Value]),"</pre></td></tr>\n" |
+ config_table1(Vars)];
+config_table1([]) ->
+ ["</table>\n"].
+
+
+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 ++ "... ")
+ end,
+ Dirs = filelib:wildcard(logdir_prefix()++"*.*"),
+ DirsSorted = (catch sort_all_runs(Dirs)),
+ Header = all_runs_header(),
+ BasicHtml = basic_html(),
+ Index = [runentry(Dir, BasicHtml) || Dir <- DirsSorted],
+ Result = file:write_file(AbsName,Header++Index++index_footer()),
+ if When == start -> ok;
+ true -> io:put_chars("done\n")
+ end,
+ notify_and_unlock_file(AbsName),
+ Result.
+
+sort_all_runs(Dirs) ->
+ %% sort on time string, always last and on the format:
+ %% "YYYY-MM-DD_HH.MM.SS"
+ KeyList =
+ lists:map(fun(Dir) ->
+ case lists:reverse(string:tokens(Dir,[$.,$_])) of
+ [SS,MM,HH,Date|_] ->
+ {{Date,HH,MM,SS},Dir};
+ _Other ->
+ throw(Dirs)
+ end
+ end,Dirs),
+ lists:reverse(lists:map(fun({_,Dir}) ->
+ Dir
+ end,lists:keysort(1,KeyList))).
+
+
+interactive_link() ->
+ [Dir|_] = lists:reverse(filelib:wildcard(logdir_prefix()++"*.*")),
+ CtLog = filename:join(Dir,"ctlog.html"),
+ Body = ["Log from last interactive run: <A HREF=\"",CtLog,"\">",
+ timestamp(Dir),"</A>"],
+ file:write_file("last_interactive.html",Body),
+ io:format("~n~nUpdated ~s\n"
+ "Any CT activities will be logged here\n",
+ [?abs("last_interactive.html")]).
+
+runentry(Dir, BasicHtml) ->
+ TotalsFile = filename:join(Dir,?totals_name),
+ TotalsStr =
+ case read_totals_file(TotalsFile) of
+ {Node,Logs,{TotSucc,TotFail,UserSkip,AutoSkip,NotBuilt}} ->
+ TotFailStr =
+ if TotFail > 0 ->
+ ["<FONT color=\"red\">",
+ integer_to_list(TotFail),"</FONT>"];
+ true ->
+ integer_to_list(TotFail)
+ end,
+ {AllSkip,UserSkipStr,AutoSkipStr} =
+ if AutoSkip == undefined -> {UserSkip,"?","?"};
+ true ->
+ ASStr = if AutoSkip > 0 ->
+ ["<FONT color=\"brown\">",
+ integer_to_list(AutoSkip),"</FONT>"];
+ true -> integer_to_list(AutoSkip)
+ end,
+ {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr}
+ end,
+ NoOfTests = case length(Logs) of
+ 0 -> "-";
+ N -> integer_to_list(N)
+ end,
+ StripExt =
+ fun(File) ->
+ string:sub_string(File,1,
+ length(File)-
+ length(?logdir_ext)) ++ ", "
+ end,
+ Polish = fun(S) -> case lists:reverse(S) of
+ [32,$,|Rev] -> lists:reverse(Rev);
+ [$,|Rev] -> lists:reverse(Rev);
+ _ -> S
+ end
+ end,
+ TestNames = Polish(lists:flatten(lists:map(StripExt,Logs))),
+ TestNamesTrunc =
+ if TestNames=="" ->
+ "";
+ length(TestNames) < ?testname_width ->
+ TestNames;
+ true ->
+ Trunc = Polish(string:substr(TestNames,1,?testname_width-3)),
+ lists:flatten(io_lib:format("~s...",[Trunc]))
+ end,
+ Total = TotSucc+TotFail+AllSkip,
+ A = ["<TD ALIGN=center><FONT SIZE=-1>",Node,"</FONT></TD>\n",
+ "<TD ALIGN=right>",NoOfTests,"</TD>\n"],
+ B = if BasicHtml ->
+ ["<TD ALIGN=center><FONT SIZE=-1>",TestNamesTrunc,"</FONT></TD>\n"];
+ true ->
+ ["<TD ALIGN=center TITLE='",TestNames,"'><FONT SIZE=-1> ",
+ TestNamesTrunc,"</FONT></TD>\n"]
+ end,
+ C = ["<TD ALIGN=right>",integer_to_list(Total),"</TD>\n",
+ "<TD ALIGN=right>",integer_to_list(TotSucc),"</TD>\n",
+ "<TD ALIGN=right>",TotFailStr,"</TD>\n",
+ "<TD ALIGN=right>",integer_to_list(AllSkip),
+ " (",UserSkipStr,"/",AutoSkipStr,")</TD>\n",
+ "<TD ALIGN=right>",integer_to_list(NotBuilt),"</TD>\n"],
+ A++B++C;
+ _ ->
+ ["<TD ALIGN=center><FONT size=-1 color=\"red\">",
+ "Test data missing or corrupt","</FONT></TD>\n"]
+ end,
+ Index = filename:join(Dir,?index_name),
+ ["<TR>\n"
+ "<TD><A HREF=\"",Index,"\">",timestamp(Dir),"</A>",TotalsStr,"</TD>\n"
+ "</TR>\n"].
+
+write_totals_file(Name,Logs,Totals) ->
+ AbsName = ?abs(Name),
+ notify_and_lock_file(AbsName),
+ force_write_file(AbsName,
+ term_to_binary({atom_to_list(node()),
+ Logs,Totals})),
+ notify_and_unlock_file(AbsName).
+
+read_totals_file(Name) ->
+ AbsName = ?abs(Name),
+ notify_and_lock_file(AbsName),
+ Result =
+ case file:read_file(AbsName) of
+ {ok,Bin} ->
+ case catch binary_to_term(Bin) of
+ {'EXIT',_Reason} -> % corrupt file
+ {"-",[],undefined};
+ R = {Node,Ls,Tot} ->
+ case Tot of
+ {_,_,_,_,_} -> % latest format
+ R;
+ {TotSucc,TotFail,AllSkip,NotBuilt} ->
+ {Node,Ls,{TotSucc,TotFail,AllSkip,undefined,NotBuilt}}
+ end;
+ %% for backwards compatibility
+ {Ls,Tot} -> {"-",Ls,Tot};
+ Tot -> {"-",[],Tot}
+ end;
+ Error ->
+ Error
+ end,
+ notify_and_unlock_file(AbsName),
+ Result.
+
+force_write_file(Name,Contents) ->
+ force_delete(Name),
+ file:write_file(Name,Contents).
+
+force_delete(Name) ->
+ case file:delete(Name) of
+ {error,eacces} ->
+ force_rename(Name,Name++".old.",0);
+ Other ->
+ Other
+ end.
+
+force_rename(From,To,Number) ->
+ Dest = [To|integer_to_list(Number)],
+ case file:read_file_info(Dest) of
+ {ok,_} ->
+ force_rename(From,To,Number+1);
+ {error,_} ->
+ file:rename(From,Dest)
+ end.
+
+
+timestamp(Dir) ->
+ TsR = lists:reverse(string:tokens(Dir,".-_")),
+ [S,Min,H,D,M,Y] = [list_to_integer(N) || N <- lists:sublist(TsR,6)],
+ format_time({{Y,M,D},{H,Min,S}}).
+
+make_all_suites_index(When) ->
+ AbsIndexName = ?abs(?index_name),
+ notify_and_lock_file(AbsIndexName),
+ LogDirs = filelib:wildcard(logdir_prefix()++".*/*"++?logdir_ext),
+ Sorted = sort_logdirs(LogDirs,[]),
+ Result = make_all_suites_index1(When,Sorted),
+ notify_and_unlock_file(AbsIndexName),
+ Result.
+
+sort_logdirs([Dir|Dirs],Groups) ->
+ TestName = filename:rootname(filename:basename(Dir)),
+ case filelib:wildcard(filename:join(Dir,"run.*")) of
+ [RunDir] ->
+ Groups1 = insert_test(TestName,{filename:basename(RunDir),RunDir},Groups),
+ sort_logdirs(Dirs,Groups1);
+ _ -> % ignore missing run directory
+ sort_logdirs(Dirs,Groups)
+ end;
+sort_logdirs([],Groups) ->
+ lists:keysort(1,sort_each_group(Groups)).
+
+insert_test(Test,IxDir,[{Test,IxDirs}|Groups]) ->
+ [{Test,[IxDir|IxDirs]}|Groups];
+insert_test(Test,IxDir,[]) ->
+ [{Test,[IxDir]}];
+insert_test(Test,IxDir,[TestDir|Groups]) ->
+ [TestDir|insert_test(Test,IxDir,Groups)].
+
+sort_each_group([{Test,IxDirs}|Groups]) ->
+ Sorted = lists:reverse([Dir || {_,Dir} <- lists:keysort(1,IxDirs)]),
+ [{Test,Sorted}| sort_each_group(Groups)];
+sort_each_group([]) ->
+ [].
+
+make_all_suites_index1(When,AllSuitesLogDirs) ->
+ IndexName = ?index_name,
+ AbsIndexName = ?abs(IndexName),
+ if When == start -> ok;
+ true -> io:put_chars("Updating " ++ AbsIndexName ++ "... ")
+ end,
+ case catch make_all_suites_index2(IndexName,AllSuitesLogDirs) of
+ {'EXIT', Reason} ->
+ io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"),
+ io:format("~p~n", [Reason]),
+ {error, Reason};
+ {error, Reason} ->
+ io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"),
+ io:format("~p~n", [Reason]),
+ {error, Reason};
+ ok ->
+ if When == start -> ok;
+ true -> io:put_chars("done\n")
+ end,
+ ok;
+ Err ->
+ io:format("Unknown internal error while updating ~s. "
+ "Please report.\n(Err: ~p, ID: 1)",
+ [AbsIndexName,Err]),
+ {error, Err}
+ end.
+
+make_all_suites_index2(IndexName,AllSuitesLogDirs) ->
+ {ok,Index0,_Totals} = make_all_suites_index3(AllSuitesLogDirs,
+ all_suites_index_header(),
+ 0, 0, 0, 0, 0),
+ Index = [Index0|index_footer()],
+ case force_write_file(IndexName, Index) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ {error,{index_write_error, Reason}}
+ end.
+
+make_all_suites_index3([{SuiteName,[LastLogDir|OldDirs]}|Rest],
+ Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt) ->
+ [EntryDir|_] = filename:split(LastLogDir),
+ Missing =
+ case file:read_file(filename:join(EntryDir,?missing_suites_info)) of
+ {ok,Bin} -> binary_to_term(Bin);
+ _ -> []
+ end,
+ case make_one_index_entry(SuiteName, LastLogDir, {true,OldDirs}, Missing) of
+ {Result1,Succ,Fail,USkip,ASkip,NotBuilt} ->
+ %% for backwards compatibility
+ AutoSkip1 = case catch AutoSkip+ASkip of
+ {'EXIT',_} -> undefined;
+ Res -> Res
+ end,
+ make_all_suites_index3(Rest, [Result|Result1], TotSucc+Succ,
+ TotFail+Fail, UserSkip+USkip, AutoSkip1,
+ TotNotBuilt+NotBuilt);
+ error ->
+ make_all_suites_index3(Rest, Result, TotSucc, TotFail,
+ UserSkip, AutoSkip, TotNotBuilt)
+ end;
+make_all_suites_index3([], Result, TotSucc, TotFail, UserSkip, AutoSkip,
+ TotNotBuilt) ->
+ {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt,true)],
+ {TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}}.
+
+
+%%-----------------------------------------------------------------
+%% Remove log files.
+%% Cwd should always be set to the root logdir when finished.
+cleanup() ->
+ {ok,Cwd} = file:get_cwd(),
+ ok = file:set_cwd("../"),
+ {ok,Top} = file:get_cwd(),
+ Result =
+ case catch try_cleanup(Cwd) of
+ ok ->
+ ok;
+ {'EXIT',Reason} ->
+ {error,Reason};
+ Error ->
+ {error,Error}
+ end,
+ ok = file:set_cwd(Top),
+ Result.
+
+try_cleanup(CTRunDir) ->
+ %% ensure we're removing the ct_run directory
+ case lists:reverse(filename:split(CTRunDir)) of
+ [[$c,$t,$_,$r,$u,$n,$.|_]|_] ->
+ case filelib:wildcard(filename:join(CTRunDir,"ct_run.*")) of
+ [] -> % "double check"
+ rm_dir(CTRunDir);
+ _ ->
+ unknown_logdir
+ end;
+ _ ->
+ unknown_logdir
+ end.
+
+rm_dir(Dir) ->
+ case file:list_dir(Dir) of
+ {error,Errno} ->
+ exit({ls_failed,Dir,Errno});
+ {ok,Files} ->
+ rm_files([filename:join(Dir, F) || F <- Files]),
+ case file:del_dir(Dir) of
+ {error,Errno} ->
+ exit({rmdir_failed,Errno});
+ ok ->
+ ok
+ end
+ end.
+
+rm_files([F | Fs]) ->
+ Base = filename:basename(F),
+ if Base == "." ; Base == ".." ->
+ rm_files(Fs);
+ true ->
+ case file:read_file_info(F) of
+ {ok,#file_info{type=directory}} ->
+ rm_dir(F),
+ rm_files(Fs);
+ {ok,_Regular} ->
+ case file:delete(F) of
+ ok ->
+ rm_files(Fs);
+ {error,Errno} ->
+ exit({del_failed,F,Errno})
+ end
+ end
+ end;
+rm_files([]) ->
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec simulate() -> pid()
+%%%
+%%% @doc Simulate the logger process.
+%%%
+%%% <p>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>
+simulate() ->
+ cast(stop),
+ S = self(),
+ Pid = spawn(fun() ->
+ register(?MODULE,self()),
+ S ! {self(),started},
+ simulate_logger_loop()
+ end),
+ receive {Pid,started} -> Pid end.
+
+
+simulate_logger_loop() ->
+ receive
+ {log,_,_,List} ->
+ S = [[io_lib:format(Str,Args),io_lib:nl()] || {Str,Args} <- List],
+ io:format("~s",[S]),
+ simulate_logger_loop();
+ stop ->
+ ok
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec notify_and_lock_file(Files) -> ok
+%%%
+%%% @doc
+%%%
+notify_and_lock_file(File) ->
+ case ct_event:is_alive() of
+ true ->
+ ct_event:sync_notify(#event{name=start_write_file,
+ node=node(),
+ data=File});
+ false ->
+ ok
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec notify_and_unlock_file(Files) -> ok
+%%%
+%%% @doc
+%%%
+notify_and_unlock_file(File) ->
+ case ct_event:is_alive() of
+ true ->
+ ct_event:sync_notify(#event{name=finished_write_file,
+ node=node(),
+ data=File});
+ false ->
+ ok
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec last_test(Dir) -> string() | false
+%%%
+%%% @doc
+%%%
+last_test(Dir) ->
+ last_test(filelib:wildcard(filename:join(Dir, "run.[1-2]*")), false).
+
+last_test([Run|Rest], false) ->
+ last_test(Rest, Run);
+last_test([Run|Rest], Latest) when Run > Latest ->
+ last_test(Rest, Run);
+last_test([_|Rest], Latest) ->
+ last_test(Rest, Latest);
+last_test([], Latest) ->
+ Latest.
+
+%%%-----------------------------------------------------------------
+%%% @spec basic_html() -> true | false
+%%%
+%%% @doc
+%%%
+basic_html() ->
+ case application:get_env(common_test, basic_html) of
+ {ok,true} ->
+ true;
+ _ ->
+ false
+ end.
diff --git a/lib/common_test/src/ct_make.erl b/lib/common_test/src/ct_make.erl
new file mode 100644
index 0000000000..233e45248e
--- /dev/null
+++ b/lib/common_test/src/ct_make.erl
@@ -0,0 +1,344 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%
+%% Purpose : Basic make facility for Common Test
+%%
+%% Compares date stamps of .erl and Object files - recompiles when
+%% necessary.
+%% Files to be checked are contained in a file 'Emakefile'
+%% If Emakefile is missing the current directory is used.
+%%
+
+-module(ct_make).
+
+-export([all/0,all/1,files/1,files/2]).
+
+-include_lib("kernel/include/file.hrl").
+
+-define(MakeOpts,[noexec,load,netload,noload]).
+
+all() ->
+ all([]).
+
+all(Options) ->
+ {MakeOpts,CompileOpts} = sort_options(Options,[],[]),
+ case read_emakefile('Emakefile',CompileOpts) of
+ Files when is_list(Files) ->
+ do_make_files(Files,MakeOpts);
+ error ->
+ {error,[]}
+ end.
+
+files(Fs) ->
+ files(Fs, []).
+
+files(Fs0, Options) ->
+ Fs = [filename:rootname(F,".erl") || F <- Fs0],
+ {MakeOpts,CompileOpts} = sort_options(Options,[],[]),
+ case get_opts_from_emakefile(Fs,'Emakefile',CompileOpts) of
+ Files when is_list(Files) ->
+ do_make_files(Files,MakeOpts);
+ error ->
+ {error,[]}
+ end.
+
+do_make_files(Fs, Opts) ->
+ process(Fs, lists:member(noexec, Opts), load_opt(Opts), []).
+
+
+sort_options([H|T],Make,Comp) ->
+ case lists:member(H,?MakeOpts) of
+ true ->
+ sort_options(T,[H|Make],Comp);
+ false ->
+ sort_options(T,Make,[H|Comp])
+ end;
+sort_options([],Make,Comp) ->
+ {Make,lists:reverse(Comp)}.
+
+%%% Reads the given Emakefile and returns a list of tuples: {Mods,Opts}
+%%% Mods is a list of module names (strings)
+%%% Opts is a list of options to be used when compiling Mods
+%%%
+%%% Emakefile can contain elements like this:
+%%% Mod.
+%%% {Mod,Opts}.
+%%% Mod is a module name which might include '*' as wildcard
+%%% or a list of such module names
+%%%
+%%% These elements are converted to [{ModList,OptList},...]
+%%% ModList is a list of modulenames (strings)
+read_emakefile(Emakefile,Opts) ->
+ case file:consult(Emakefile) of
+ {ok,Emake} ->
+ transform(Emake,Opts,[],[]);
+ {error,enoent} ->
+ %% No Emakefile found - return all modules in current
+ %% directory and the options given at command line
+ Mods = [filename:rootname(F) || F <- filelib:wildcard("*.erl")],
+ [{Mods, Opts}];
+ {error,Other} ->
+ io:format("make: Trouble reading 'Emakefile':~n~p~n",[Other]),
+ error
+ end.
+
+transform([{Mod,ModOpts}|Emake],Opts,Files,Already) ->
+ case expand(Mod,Already) of
+ [] ->
+ transform(Emake,Opts,Files,Already);
+ Mods ->
+ transform(Emake,Opts,[{Mods,ModOpts++Opts}|Files],Mods++Already)
+ end;
+transform([Mod|Emake],Opts,Files,Already) ->
+ case expand(Mod,Already) of
+ [] ->
+ transform(Emake,Opts,Files,Already);
+ Mods ->
+ transform(Emake,Opts,[{Mods,Opts}|Files],Mods++Already)
+ end;
+transform([],_Opts,Files,_Already) ->
+ lists:reverse(Files).
+
+expand(Mod,Already) when is_atom(Mod) ->
+ expand(atom_to_list(Mod),Already);
+expand(Mods,Already) when is_list(Mods), not is_integer(hd(Mods)) ->
+ lists:concat([expand(Mod,Already) || Mod <- Mods]);
+expand(Mod,Already) ->
+ case lists:member($*,Mod) of
+ true ->
+ Fun = fun(F,Acc) ->
+ M = filename:rootname(F),
+ case lists:member(M,Already) of
+ true -> Acc;
+ false -> [M|Acc]
+ end
+ end,
+ lists:foldl(Fun, [], filelib:wildcard(Mod++".erl"));
+ false ->
+ Mod2 = filename:rootname(Mod, ".erl"),
+ case lists:member(Mod2,Already) of
+ true -> [];
+ false -> [Mod2]
+ end
+ end.
+
+%%% Reads the given Emakefile to see if there are any specific compile
+%%% options given for the modules.
+get_opts_from_emakefile(Mods,Emakefile,Opts) ->
+ case file:consult(Emakefile) of
+ {ok,Emake} ->
+ Modsandopts = transform(Emake,Opts,[],[]),
+ ModStrings = [coerce_2_list(M) || M <- Mods],
+ get_opts_from_emakefile2(Modsandopts,ModStrings,Opts,[]);
+ {error,enoent} ->
+ [{Mods, Opts}];
+ {error,Other} ->
+ io:format("make: Trouble reading 'Emakefile':~n~p~n",[Other]),
+ error
+ end.
+
+get_opts_from_emakefile2([{MakefileMods,O}|Rest],Mods,Opts,Result) ->
+ case members(Mods,MakefileMods,[],Mods) of
+ {[],_} ->
+ get_opts_from_emakefile2(Rest,Mods,Opts,Result);
+ {I,RestOfMods} ->
+ get_opts_from_emakefile2(Rest,RestOfMods,Opts,[{I,O}|Result])
+ end;
+get_opts_from_emakefile2([],[],_Opts,Result) ->
+ Result;
+get_opts_from_emakefile2([],RestOfMods,Opts,Result) ->
+ [{RestOfMods,Opts}|Result].
+
+members([H|T],MakefileMods,I,Rest) ->
+ case lists:member(H,MakefileMods) of
+ true ->
+ members(T,MakefileMods,[H|I],lists:delete(H,Rest));
+ false ->
+ members(T,MakefileMods,I,Rest)
+ end;
+members([],_MakefileMods,I,Rest) ->
+ {I,Rest}.
+
+
+%% Any flags that are not recognixed as make flags are passed directly
+%% to the compiler.
+%% So for example make:all([load,debug_info]) will make everything
+%% with the debug_info flag and load it.
+
+load_opt(Opts) ->
+ case lists:member(netload,Opts) of
+ true ->
+ netload;
+ false ->
+ case lists:member(load,Opts) of
+ true ->
+ load;
+ _ ->
+ noload
+ end
+ end.
+
+
+process([{[],_Opts}|Rest], NoExec, Load, Result) ->
+ process(Rest, NoExec, Load, Result);
+process([{[H|T],Opts}|Rest], NoExec, Load, Result) ->
+ case recompilep(coerce_2_list(H), NoExec, Load, Opts) of
+ error ->
+ process([{T,Opts}|Rest], NoExec, Load, [{H,error}|Result]);
+ Info ->
+ process([{T,Opts}|Rest], NoExec, Load, [{H,Info}|Result])
+ end;
+process([], NoExec, _Load, Result) ->
+ if not NoExec ->
+ case lists:keysearch(error, 2, Result) of
+ {value,_} ->
+ {error,Result};
+ false ->
+ {up_to_date,Result}
+ end;
+ true ->
+ Result
+ end.
+
+recompilep(File, NoExec, Load, Opts) ->
+ ObjName = lists:append(filename:basename(File),
+ code:objfile_extension()),
+ ObjFile = case lists:keysearch(outdir,1,Opts) of
+ {value,{outdir,OutDir}} ->
+ filename:join(coerce_2_list(OutDir),ObjName);
+ false ->
+ ObjName
+ end,
+ case exists(ObjFile) of
+ true ->
+ recompilep1(File, NoExec, Load, Opts, ObjFile);
+ false ->
+ recompile(File, NoExec, Load, Opts)
+ end.
+
+recompilep1(File, NoExec, Load, Opts, ObjFile) ->
+ {ok, Erl} = file:read_file_info(lists:append(File, ".erl")),
+ {ok, Obj} = file:read_file_info(ObjFile),
+ case {readable(Erl), writable(Obj)} of
+ {true, true} ->
+ recompilep1(Erl, Obj, File, NoExec, Load, Opts);
+ _ ->
+ error
+ end.
+
+recompilep1(#file_info{mtime=Te},
+ #file_info{mtime=To}, File, NoExec, Load, Opts) when Te>To ->
+ recompile(File, NoExec, Load, Opts);
+recompilep1(_Erl, #file_info{mtime=To}, File, NoExec, Load, Opts) ->
+ recompile2(To, File, NoExec, Load, Opts).
+
+%% recompile2(ObjMTime, File, NoExec, Load, Opts)
+%% Check if file is of a later date than include files.
+recompile2(ObjMTime, File, NoExec, Load, Opts) ->
+ IncludePath = include_opt(Opts),
+ case check_includes(lists:append(File, ".erl"), IncludePath, ObjMTime) of
+ true ->
+ recompile(File, NoExec, Load, Opts);
+ false ->
+ up_to_date
+ end.
+
+include_opt([{i,Path}|Rest]) ->
+ [Path|include_opt(Rest)];
+include_opt([_First|Rest]) ->
+ include_opt(Rest);
+include_opt([]) ->
+ [].
+
+%% recompile(File, NoExec, Load, Opts)
+%% Actually recompile and load the file, depending on the flags.
+%% Where load can be netload | load | noload
+
+recompile(File, NoExec, Load, Opts) ->
+ case do_recompile(File, NoExec, Load, Opts) of
+ {ok,_} -> ok;
+ Other -> Other
+ end.
+
+do_recompile(_File, true, _Load, _Opts) ->
+ out_of_date;
+do_recompile(File, false, noload, Opts) ->
+ io:format("Recompile: ~s\n",[File]),
+ compile:file(File, [report_errors, report_warnings, error_summary |Opts]);
+do_recompile(File, false, load, Opts) ->
+ io:format("Recompile: ~s\n",[File]),
+ c:c(File, Opts);
+do_recompile(File, false, netload, Opts) ->
+ io:format("Recompile: ~s\n",[File]),
+ c:nc(File, Opts).
+
+exists(File) ->
+ case file:read_file_info(File) of
+ {ok, _} ->
+ true;
+ _ ->
+ false
+ end.
+
+readable(#file_info{access=read_write}) -> true;
+readable(#file_info{access=read}) -> true;
+readable(_) -> false.
+
+writable(#file_info{access=read_write}) -> true;
+writable(#file_info{access=write}) -> true;
+writable(_) -> false.
+
+coerce_2_list(X) when is_atom(X) ->
+ atom_to_list(X);
+coerce_2_list(X) ->
+ X.
+
+%%% If you an include file is found with a modification
+%%% time larger than the modification time of the object
+%%% file, return true. Otherwise return false.
+check_includes(File, IncludePath, ObjMTime) ->
+ Path = [filename:dirname(File)|IncludePath],
+ case epp:open(File, Path, []) of
+ {ok, Epp} ->
+ check_includes2(Epp, File, ObjMTime);
+ _Error ->
+ false
+ end.
+
+check_includes2(Epp, File, ObjMTime) ->
+ case epp:parse_erl_form(Epp) of
+ {ok, {attribute, 1, file, {File, 1}}} ->
+ check_includes2(Epp, File, ObjMTime);
+ {ok, {attribute, 1, file, {IncFile, 1}}} ->
+ case file:read_file_info(IncFile) of
+ {ok, #file_info{mtime=MTime}} when MTime>ObjMTime ->
+ epp:close(Epp),
+ true;
+ _ ->
+ check_includes2(Epp, File, ObjMTime)
+ end;
+ {ok, _} ->
+ check_includes2(Epp, File, ObjMTime);
+ {eof, _} ->
+ epp:close(Epp),
+ false;
+ {error, _Error} ->
+ check_includes2(Epp, File, ObjMTime)
+ end.
diff --git a/lib/common_test/src/ct_master.erl b/lib/common_test/src/ct_master.erl
new file mode 100644
index 0000000000..7eb2c3cfef
--- /dev/null
+++ b/lib/common_test/src/ct_master.erl
@@ -0,0 +1,696 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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([abort/0,abort/1,progress/0]).
+
+-export([init_master/6, init_node_ctrl/3]).
+
+-export([status/2]).
+
+-include("ct_event.hrl").
+-include("ct_util.hrl").
+
+-record(state, {node_ctrl_pids=[],
+ logdirs=[],
+ results=[],
+ locks=[],
+ 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}
+%%% 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) ->
+ TS1 =
+ case TS of
+ List=[S|_] when is_list(S) -> List;
+ Spec -> [Spec]
+ end,
+ Result =
+ case catch ct_testspec:collect_tests_from_file(TS1,InclNodes,AllowUserTerms) of
+ {error,Reason} ->
+ {error,Reason};
+ TSRec=#testspec{logdir=AllLogDirs,
+ config=AllCfgFiles,
+ event_handler=AllEvHs} ->
+ RunSkipPerNode = ct_testspec:prepare_tests(TSRec),
+ RunSkipPerNode2 = exclude_nodes(ExclNodes,RunSkipPerNode),
+ run_all(RunSkipPerNode2,AllLogDirs,AllCfgFiles,AllEvHs,[],[],TS1)
+ end,
+ [{TS,Result} | run(TestSpecs,AllowUserTerms,InclNodes,ExclNodes)];
+run([],_,_,_) ->
+ [];
+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) ->
+ run([TS],false,[],[]).
+
+
+exclude_nodes([ExclNode|ExNs],RunSkipPerNode) ->
+ exclude_nodes(ExNs,lists:keydelete(ExclNode,1,RunSkipPerNode));
+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) ->
+ TS1 =
+ case TS of
+ [List|_] when is_list(List) -> List;
+ Spec -> [Spec]
+ end,
+ Result =
+ case catch ct_testspec:collect_tests_from_file(TS1,[Node],AllowUserTerms) of
+ {error,Reason} ->
+ {error,Reason};
+ TSRec=#testspec{logdir=AllLogDirs,
+ config=AllCfgFiles,
+ event_handler=AllEvHs} ->
+ {Run,Skip} = ct_testspec:prepare_tests(TSRec,Node),
+ run_all([{Node,Run,Skip}],AllLogDirs,AllCfgFiles,AllEvHs,[],[],TS1)
+ end,
+ [{TS,Result} | run_on_node(TestSpecs,AllowUserTerms,Node)];
+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).
+
+
+
+run_all([{Node,Run,Skip}|Rest],AllLogDirs,AllCfgFiles,AllEvHs,NodeOpts,LogDirs,Specs) ->
+ LogDir =
+ lists:foldl(fun({N,Dir},_Found) when N == Node ->
+ Dir;
+ ({_N,_Dir},Found) ->
+ Found;
+ (Dir,".") ->
+ Dir;
+ (_Dir,Found) ->
+ Found
+ end,".",AllLogDirs),
+ CfgFiles =
+ lists:foldr(fun({N,F},Fs) when N == Node -> [F|Fs];
+ ({_N,_F},Fs) -> Fs;
+ (F,Fs) -> [F|Fs]
+ end,[],AllCfgFiles),
+ EvHs =
+ lists:foldr(fun({N,H,A},Hs) when N == Node -> [{H,A}|Hs];
+ ({_N,_H,_A},Hs) -> Hs;
+ ({H,A},Hs) -> [{H,A}|Hs]
+ end,[],AllEvHs),
+ NO = {Node,[{prepared_tests,{Run,Skip},Specs},
+ {logdir,LogDir},
+ {config,CfgFiles},
+ {event_handler,EvHs}]},
+ run_all(Rest,AllLogDirs,AllCfgFiles,AllEvHs,[NO|NodeOpts],[LogDir|LogDirs],Specs);
+run_all([],AllLogDirs,_,AllEvHs,NodeOpts,LogDirs,Specs) ->
+ Handlers = [{H,A} || {Master,H,A} <- AllEvHs, Master == master],
+ MasterLogDir = case lists:keysearch(master,1,AllLogDirs) of
+ {value,{_,Dir}} -> Dir;
+ false -> "."
+ end,
+ log(tty,"Master Logdir","~s",[MasterLogDir]),
+ start_master(lists:reverse(NodeOpts),Handlers,MasterLogDir,LogDirs,Specs),
+ 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).
+
+
+%%%-----------------------------------------------------------------
+%%% MASTER, runs on central controlling node.
+%%%-----------------------------------------------------------------
+start_master(NodeOptsList) ->
+ start_master(NodeOptsList,[],".",[],[]).
+
+start_master(NodeOptsList,EvHandlers,MasterLogDir,LogDirs,Specs) ->
+ Master = spawn_link(?MODULE,init_master,[self(),NodeOptsList,EvHandlers,
+ MasterLogDir,LogDirs,Specs]),
+ receive
+ {Master,Result} -> Result
+ end.
+
+%%% @hidden
+init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,Specs) ->
+ case whereis(ct_master) of
+ undefined ->
+ register(ct_master,self()),
+ ok;
+ _Pid ->
+ io:format("~nWarning: ct_master already running!~n"),
+ exit(aborted)
+% case io:get_line('[y/n]>') of
+% "y\n" ->
+% ok;
+% "n\n" ->
+% exit(aborted);
+% _ ->
+% init_master(NodeOptsList,LogDirs)
+% end
+ end,
+
+ %% start master logger
+ {MLPid,_} = ct_master_logs:start(MasterLogDir,
+ [N || {N,_} <- NodeOptsList]),
+ log(all,"Master Logger process started","~w",[MLPid]),
+ case Specs of
+ [] -> ok;
+ _ ->
+ SpecsStr = lists:map(fun(Name) ->
+ Name ++ " "
+ end,Specs),
+ ct_master_logs:log("Test Specification file(s)","~s",
+ [lists:flatten(SpecsStr)])
+ end,
+
+ %% start master event manager and add default handler
+ ct_master_event:start_link(),
+ ct_master_event:add_handler(),
+ %% add user handlers for master event manager
+ Add = fun({H,Args}) ->
+ log(all,"Adding Event Handler","~p",[H]),
+ case gen_event:add_handler(?CT_MEVMGR_REF,H,Args) of
+ ok -> ok;
+ {'EXIT',Why} -> exit(Why);
+ Other -> exit({event_handler,Other})
+ end
+ end,
+ lists:foreach(Add,EvHandlers),
+
+ %% double check event manager is started and registered
+ case whereis(?CT_MEVMGR) of
+ undefined ->
+ exit({?CT_MEVMGR,undefined});
+ Pid when is_pid(Pid) ->
+ ok
+ end,
+ init_master1(Parent,NodeOptsList,LogDirs).
+
+init_master1(Parent,NodeOptsList,LogDirs) ->
+ {Inaccessible,NodeOptsList1} = ping_nodes(NodeOptsList,[],[]),
+ case Inaccessible of
+ [] ->
+ init_master2(Parent,NodeOptsList,LogDirs);
+ _ ->
+ io:format("~nThe following nodes are inaccessible: ~p~n~n",
+ [Inaccessible]),
+ io:format("Proceed(p), Rescan(r) or Abort(a)? "),
+ case io:get_line('[p/r/a]>') of
+ "p\n" ->
+ log(html,"Inaccessible Nodes",
+ "Proceeding without: ~p",[Inaccessible]),
+ init_master2(Parent,NodeOptsList1,LogDirs);
+ "r\n" ->
+ init_master1(Parent,NodeOptsList,LogDirs);
+ _ ->
+ log(html,"Aborting Tests","",[]),
+ ct_master_event:stop(),
+ ct_master_logs:stop(),
+ exit(aborted)
+ end
+ end.
+
+init_master2(Parent,NodeOptsList,LogDirs) ->
+ process_flag(trap_exit,true),
+ Cookie = erlang:get_cookie(),
+ log(all,"Cookie","~w",[Cookie]),
+ log(all,"Starting Tests",
+ "Tests starting on: ~p",[[N || {N,_} <- NodeOptsList]]),
+ SpawnAndMon =
+ fun({Node,Opts}) ->
+ monitor_node(Node,true),
+ log(all,"Test Info","Starting test(s) on ~p...",[Node]),
+ {spawn_link(Node,?MODULE,init_node_ctrl,[self(),Cookie,Opts]),Node}
+ end,
+ NodeCtrlPids = lists:map(SpawnAndMon,NodeOptsList),
+ Result = master_loop(#state{node_ctrl_pids=NodeCtrlPids,
+ logdirs=LogDirs}),
+ Parent ! {self(),Result}.
+
+master_loop(#state{node_ctrl_pids=[],
+ logdirs=LogDirs,
+ results=Finished}) ->
+ Str =
+ lists:map(fun({Node,Result}) ->
+ io_lib:format("~-40.40.*s~p\n",[$_,atom_to_list(Node),Result])
+ end,lists:reverse(Finished)),
+ log(all,"TEST RESULTS",Str,[]),
+ log(all,"Info","Updating log files",[]),
+ refresh_logs(LogDirs,[]),
+
+ ct_master_event:stop(),
+ ct_master_logs:stop(),
+ ok;
+
+master_loop(State=#state{node_ctrl_pids=NodeCtrlPids,
+ results=Results,
+ locks=Locks,
+ blocked=Blocked}) ->
+ receive
+ {'EXIT',Pid,Reason} ->
+ case get_node(Pid,NodeCtrlPids) of
+ {Node,NodeCtrlPids1} ->
+ monitor_node(Node,false),
+ case Reason of
+ normal ->
+ log(all,"Test Info",
+ "Test(s) on node ~w finished.",[Node]),
+ master_loop(State#state{node_ctrl_pids=NodeCtrlPids1});
+ Bad ->
+ Error =
+ case Bad of
+ What when What=/=killed,is_atom(What) ->
+ {error,Bad};
+ _ ->
+ Bad
+ end,
+ log(all,"Test Info",
+ "Test on node ~w failed! Reason: ~p",[Node,Error]),
+ {Locks1,Blocked1} =
+ update_queue(exit,Node,Locks,Blocked),
+ master_loop(State#state{node_ctrl_pids=NodeCtrlPids1,
+ results=[{Node,Error}|Results],
+ locks=Locks1,
+ blocked=Blocked1})
+ end;
+ undefined ->
+ %% ignore (but report) exit from master_logger etc
+ log(all,"Test Info",
+ "Warning! Process ~p has terminated. Reason: ~p",
+ [Pid,Reason]),
+ master_loop(State)
+ end;
+
+ {nodedown,Node} ->
+ case get_pid(Node,NodeCtrlPids) of
+ {_Pid,NodeCtrlPids1} ->
+ monitor_node(Node,false),
+ log(all,"Test Info","No connection to testnode ~w!",[Node]),
+ {Locks1,Blocked1} =
+ update_queue(exit,Node,Locks,Blocked),
+ master_loop(State#state{node_ctrl_pids=NodeCtrlPids1,
+ results=[{Node,nodedown}|Results],
+ locks=Locks1,
+ blocked=Blocked1});
+ undefined ->
+ master_loop(State)
+ end;
+
+ {Pid,{result,Result}} ->
+ {Node,_} = get_node(Pid,NodeCtrlPids),
+ master_loop(State#state{results=[{Node,Result}|Results]});
+
+ {call,progress,From} ->
+ reply(master_progress(NodeCtrlPids,Results),From),
+ master_loop(State);
+
+ {call,abort,From} ->
+ lists:foreach(fun({Pid,Node}) ->
+ log(all,"Test Info",
+ "Aborting tests on ~w",[Node]),
+ exit(Pid,kill)
+ end,NodeCtrlPids),
+ reply(ok,From),
+ master_loop(State);
+
+ {call,{abort,Nodes},From} ->
+ lists:foreach(fun(Node) ->
+ case lists:keysearch(Node,2,NodeCtrlPids) of
+ {value,{Pid,Node}} ->
+ log(all,"Test Info",
+ "Aborting tests on ~w",[Node]),
+ exit(Pid,kill);
+ false ->
+ ok
+ end
+ end,Nodes),
+ reply(ok,From),
+ master_loop(State);
+
+ {call,#event{name=Name,node=Node,data=Data},From} ->
+ {Op,Lock} =
+ case Name of
+ start_make ->
+ {take,{make,Data}};
+ finished_make ->
+ {release,{make,Data}};
+ start_write_file ->
+ {take,{write_file,Data}};
+ finished_write_file ->
+ {release,{write_file,Data}}
+ end,
+ {Locks1,Blocked1} =
+ update_queue(Op,Node,From,Lock,Locks,Blocked),
+ if Op == release -> reply(ok,From);
+ true -> ok
+ end,
+ master_loop(State#state{locks=Locks1,
+ blocked=Blocked1});
+
+ {cast,Event} when is_record(Event,event) ->
+ ct_master_event:notify(Event),
+ master_loop(State)
+
+ end.
+
+
+update_queue(take,Node,From,Lock={Op,Resource},Locks,Blocked) ->
+ %% Locks: [{{Operation,Resource},Node},...]
+ %% Blocked: [{{Operation,Resource},Node,WaitingPid},...]
+ case lists:keysearch(Lock,1,Locks) of
+ {value,{_Lock,Owner}} -> % other node has lock
+ log(html,"Lock Info","Node ~p blocked on ~w by ~w. Resource: ~p",
+ [Node,Op,Owner,Resource]),
+ Blocked1 = Blocked ++ [{Lock,Node,From}],
+ {Locks,Blocked1};
+ false -> % go ahead
+ Locks1 = [{Lock,Node}|Locks],
+ reply(ok,From),
+ {Locks1,Blocked}
+ end;
+
+update_queue(release,Node,_From,Lock={Op,Resource},Locks,Blocked) ->
+ Locks1 = lists:delete({Lock,Node},Locks),
+ case lists:keysearch(Lock,1,Blocked) of
+ {value,E={Lock,SomeNode,WaitingPid}} ->
+ Blocked1 = lists:delete(E,Blocked),
+ log(html,"Lock Info","Node ~p proceeds with ~w. Resource: ~p",
+ [SomeNode,Op,Resource]),
+ reply(ok,WaitingPid), % waiting process may start
+ {Locks1,Blocked1};
+ false ->
+ {Locks1,Blocked}
+ end.
+
+update_queue(exit,Node,Locks,Blocked) ->
+ NodeLocks = lists:foldl(fun({L,N},Ls) when N == Node ->
+ [L|Ls];
+ (_,Ls) ->
+ Ls
+ end,[],Locks),
+ release_locks(Node,NodeLocks,Locks,Blocked).
+
+release_locks(Node,[Lock|Ls],Locks,Blocked) ->
+ {Locks1,Blocked1} = update_queue(release,Node,undefined,Lock,Locks,Blocked),
+ release_locks(Node,Ls,Locks1,Blocked1);
+release_locks(_,[],Locks,Blocked) ->
+ {Locks,Blocked}.
+
+get_node(Pid,NodeCtrlPids) ->
+ case lists:keysearch(Pid,1,NodeCtrlPids) of
+ {value,{Pid,Node}} ->
+ {Node,lists:keydelete(Pid,1,NodeCtrlPids)};
+ false ->
+ undefined
+ end.
+
+get_pid(Node,NodeCtrlPids) ->
+ case lists:keysearch(Node,2,NodeCtrlPids) of
+ {value,{Pid,Node}} ->
+ {Pid,lists:keydelete(Node,2,NodeCtrlPids)};
+ false ->
+ undefined
+ end.
+
+ping_nodes([NO={Node,_Opts}|NOs],Inaccessible,NodeOpts) ->
+ case net_adm:ping(Node) of
+ pong ->
+ ping_nodes(NOs,Inaccessible,[NO|NodeOpts]);
+ _ ->
+ ping_nodes(NOs,[Node|Inaccessible],NodeOpts)
+ end;
+ping_nodes([],Inaccessible,NodeOpts) ->
+ {lists:reverse(Inaccessible),lists:reverse(NodeOpts)}.
+
+master_progress(NodeCtrlPids,Results) ->
+ Results ++ lists:map(fun({_Pid,Node}) ->
+ {Node,ongoing}
+ end,NodeCtrlPids).
+
+%% refresh those dirs where more than one node has written logs
+refresh_logs([D|Dirs],Refreshed) ->
+ case lists:member(D,Dirs) of
+ true ->
+ case lists:keymember(D,1,Refreshed) of
+ true ->
+ refresh_logs(Dirs,Refreshed);
+ false ->
+ {ok,Cwd} = file:get_cwd(),
+ case catch ct_run:refresh_logs(D) of
+ {'EXIT',Reason} ->
+ file:set_cwd(Cwd),
+ refresh_logs(Dirs,[{D,{error,Reason}}|Refreshed]);
+ Result ->
+ refresh_logs(Dirs,[{D,Result}|Refreshed])
+ end
+ end;
+ false ->
+ refresh_logs(Dirs,Refreshed)
+ end;
+refresh_logs([],Refreshed) ->
+ Str =
+ lists:map(fun({D,Result}) ->
+ io_lib:format("Refreshing logs in ~p... ~p",[D,Result])
+ end,Refreshed),
+ log(all,"Info",Str,[]).
+
+%%%-----------------------------------------------------------------
+%%% 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),
+ MasterNode = node(MasterPid),
+ group_leader(whereis(user),self()),
+ io:format("~n********** node_ctrl process ~p started on ~p **********~n",
+ [self(),node()]),
+ %% initially this node must have the same cookie as the master node
+ %% but now we set it explicitly for the connection so that test suites
+ %% can change the cookie for the node if they wish
+ case erlang:get_cookie() of
+ Cookie -> % first time or cookie not changed
+ erlang:set_cookie(node(MasterPid),Cookie);
+ _ ->
+ ok
+ end,
+ case whereis(ct_util_server) of
+ undefined -> ok;
+ Pid -> exit(Pid,kill)
+ end,
+
+ %% start a local event manager
+ ct_event:start_link(),
+ ct_event:add_handler([{master,MasterPid}]),
+
+ %% log("Running test with options: ~p~n", [Opts]),
+ Result = case (catch ct:run_test(Opts)) of
+ ok -> finished_ok;
+ Other -> Other
+ end,
+
+ %% stop local event manager
+ ct_event:stop(),
+
+ case net_adm:ping(MasterNode) of
+ pong ->
+ MasterPid ! {self(),{result,Result}};
+ pang ->
+ io:format("Warning! Connection to master node ~p is lost. "
+ "Can't report result!~n~n", [MasterNode])
+ end.
+
+%%%-----------------------------------------------------------------
+%%% Event handling
+%%%-----------------------------------------------------------------
+%%% @hidden
+status(MasterPid,Event=#event{name=start_make}) ->
+ call(MasterPid,Event);
+status(MasterPid,Event=#event{name=finished_make}) ->
+ call(MasterPid,Event);
+status(MasterPid,Event=#event{name=start_write_file}) ->
+ call(MasterPid,Event);
+status(MasterPid,Event=#event{name=finished_write_file}) ->
+ call(MasterPid,Event);
+status(MasterPid,Event) ->
+ cast(MasterPid,Event).
+
+%%%-----------------------------------------------------------------
+%%% Internal
+%%%-----------------------------------------------------------------
+
+log(To,Heading,Str,Args) ->
+ if To == all ; To == tty ->
+ Str1 = ["=== ",Heading," ===\n",io_lib:format(Str,Args),"\n"],
+ io:format(Str1,[]);
+ true ->
+ ok
+ end,
+ if To == all ; To == html ->
+ ct_master_logs:log(Heading,Str,Args);
+ true ->
+ ok
+ end.
+
+
+call(Msg) ->
+ call(whereis(ct_master),Msg).
+
+call(undefined,_Msg) ->
+ {error,not_running};
+
+call(Pid,Msg) ->
+ Ref = erlang:monitor(process,Pid),
+ Pid ! {call,Msg,self()},
+ Return = receive
+ {Pid,Result} ->
+ Result;
+ {'DOWN', Ref, _, _, _} ->
+ {error,master_died}
+ end,
+ erlang:demonitor(Ref),
+ Return.
+
+reply(Result,To) ->
+ To ! {self(),Result},
+ ok.
+
+%cast(Msg) ->
+% cast(whereis(ct_master),Msg).
+
+cast(undefined,_Msg) ->
+ {error,not_running};
+
+cast(Pid,Msg) ->
+ Pid ! {cast,Msg},
+ ok.
diff --git a/lib/common_test/src/ct_master_event.erl b/lib/common_test/src/ct_master_event.erl
new file mode 100644
index 0000000000..a70baefaaf
--- /dev/null
+++ b/lib/common_test/src/ct_master_event.erl
@@ -0,0 +1,179 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Common Test Framework Event Handler
+%%%
+%%% <p>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>
+-module(ct_master_event).
+
+-behaviour(gen_event).
+
+%% API
+-export([start_link/0, add_handler/0, add_handler/1, stop/0]).
+-export([notify/1, sync_notify/1]).
+
+%% gen_event callbacks
+-export([init/1, handle_event/2, handle_call/2,
+ handle_info/2, terminate/2, code_change/3]).
+
+-include("ct_event.hrl").
+-include("ct_util.hrl").
+
+
+-record(state, {}).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | {error,Error}
+%% Description: Creates an event manager.
+%%--------------------------------------------------------------------
+start_link() ->
+ gen_event:start_link({local,?CT_MEVMGR}).
+
+%%--------------------------------------------------------------------
+%% Function: add_handler() -> ok | {'EXIT',Reason} | term()
+%% Description: Adds an event handler
+%%--------------------------------------------------------------------
+add_handler() ->
+ gen_event:add_handler(?CT_MEVMGR_REF,?MODULE,[]).
+add_handler(Args) ->
+ gen_event:add_handler(?CT_MEVMGR_REF,?MODULE,Args).
+
+%%--------------------------------------------------------------------
+%% Function: stop() -> ok
+%% Description: Stops the event manager
+%%--------------------------------------------------------------------
+stop() ->
+ flush(),
+ gen_event:stop(?CT_MEVMGR_REF).
+
+flush() ->
+ case gen_event:call(?CT_MEVMGR_REF,?MODULE,flush) of
+ flushing ->
+ timer:sleep(1),
+ flush();
+ done ->
+ ok
+ end.
+
+%%--------------------------------------------------------------------
+%% Function: notify(Event) -> ok
+%% Description: Asynchronous notification to event manager.
+%%--------------------------------------------------------------------
+notify(Event) ->
+ gen_event:notify(?CT_MEVMGR_REF,Event).
+
+%%--------------------------------------------------------------------
+%% Function: sync_notify(Event) -> ok
+%% Description: Synchronous notification to event manager.
+%%--------------------------------------------------------------------
+sync_notify(Event) ->
+ gen_event:sync_notify(?CT_MEVMGR_REF,Event).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler.
+%%--------------------------------------------------------------------
+init(_) ->
+ ct_master_logs:log("CT Master Event Handler started","",[]),
+ {ok,#state{}}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%% {swap_handler, Args1, State1, Mod2, Args2} |
+%% remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%--------------------------------------------------------------------
+handle_event(#event{name=start_logging,node=Node,data=RunDir},State) ->
+ ct_master_logs:log("CT Master Event Handler","Got ~s from ~p",[RunDir,Node]),
+ ct_master_logs:nodedir(Node,RunDir),
+ {ok,State};
+
+handle_event(#event{name=Name,node=Node,data=Data},State) ->
+ print("~n=== ~w ===~n", [?MODULE]),
+ print("~p on ~p: ~p~n", [Name,Node,Data]),
+ {ok,State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%% {swap_handler, Reply, Args1, State1,
+%% Mod2, Args2} |
+%% {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(flush,State) ->
+ case process_info(self(),message_queue_len) of
+ {message_queue_len,0} ->
+ {ok,done,State};
+ _ ->
+ {ok,flushing,State}
+ end.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%% {swap_handler, Args1, State1, Mod2, Args2} |
+%% remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info,State) ->
+ {ok,State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% 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.
+%%--------------------------------------------------------------------
+terminate(_Reason,_State) ->
+ ct_master_logs:log("CT Master Event Handler stopping","",[]),
+ ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn,State,_Extra) ->
+ {ok,State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+print(_Str,_Args) ->
+% io:format(_Str,_Args),
+ ok.
diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl
new file mode 100644
index 0000000000..63f60b1182
--- /dev/null
+++ b/lib/common_test/src/ct_master_logs.erl
@@ -0,0 +1,454 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Logging functionality for Common Test Master.
+%%%
+%%% <p>This module implements a logger for the master
+%%% node.</p>
+-module(ct_master_logs).
+
+-export([start/2, make_all_runs_index/0, log/3, nodedir/2, stop/0]).
+
+-record(state, {log_fd, start_time, logdir, rundir,
+ nodedir_ix_fd, nodes, nodedirs=[]}).
+
+-define(ct_master_log_name, "ct_master_log.html").
+-define(all_runs_name, "master_runs.html").
+-define(nodedir_index_name, "index.html").
+-define(details_file_name,"details.info").
+-define(table_color,"lightblue").
+
+%%%--------------------------------------------------------------------
+%%% API
+%%%--------------------------------------------------------------------
+
+start(LogDir,Nodes) ->
+ Self = self(),
+ Pid = spawn_link(fun() -> init(Self,LogDir,Nodes) end),
+ MRef = erlang:monitor(process,Pid),
+ receive
+ {started,Pid,Result} ->
+ erlang:demonitor(MRef),
+ {Pid,Result};
+ {'DOWN',MRef,process,_,Reason} ->
+ exit({could_not_start_process,?MODULE,Reason})
+ end.
+
+log(Heading,Format,Args) ->
+ cast({log,self(),[{int_header(),[log_timestamp(now()),Heading]},
+ {Format,Args},
+ {int_footer(),[]}]}),
+ ok.
+
+make_all_runs_index() ->
+ call(make_all_runs_index).
+
+nodedir(Node,RunDir) ->
+ call({nodedir,Node,RunDir}).
+
+stop() ->
+ case whereis(?MODULE) of
+ Pid when is_pid(Pid) ->
+ MRef = erlang:monitor(process,Pid),
+ ?MODULE ! stop,
+ receive
+ {'DOWN',MRef,process,_,_} ->
+ ok
+ end;
+ undefined ->
+ ok
+ end,
+ ok.
+
+%%%--------------------------------------------------------------------
+%%% Logger process
+%%%--------------------------------------------------------------------
+
+init(Parent,LogDir,Nodes) ->
+ register(?MODULE,self()),
+ Time = calendar:local_time(),
+ RunDir = make_dirname(Time),
+ RunDirAbs = filename:join(LogDir,RunDir),
+ file:make_dir(RunDirAbs),
+ write_details_file(RunDirAbs,{node(),Nodes}),
+ 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,"~s\n",[NodeStr]),
+ io:format(CtLogFd,int_footer()++"\n",[]),
+
+ NodeDirIxFd = open_nodedir_index(RunDirAbs,Time),
+ Parent ! {started,self(),{Time,RunDirAbs}},
+ loop(#state{log_fd=CtLogFd,
+ start_time=Time,
+ logdir=LogDir,
+ rundir=RunDirAbs,
+ nodedir_ix_fd=NodeDirIxFd,
+ nodes=Nodes,
+ nodedirs=lists:map(fun(N) ->
+ {N,""}
+ end,Nodes)}).
+
+loop(State) ->
+ receive
+ {log,_From,List} ->
+ Fd = State#state.log_fd,
+ Fun =
+ fun({Str,Args}) ->
+ case catch io:format(Fd,Str++"\n",Args) of
+ {'EXIT',Reason} ->
+ io:format(Fd,
+ "Logging fails! Str: ~p, Args: ~p~n",
+ [Str,Args]),
+ exit({logging_failed,Reason}),
+ ok;
+ _ ->
+ ok
+ end
+ end,
+ lists:foreach(Fun,List),
+ loop(State);
+ {make_all_runs_index,From} ->
+ make_all_runs_index(State#state.logdir),
+ return(From,State#state.logdir),
+ loop(State);
+ {{nodedir,Node,RunDir},From} ->
+ print_nodedir(Node,RunDir,State#state.nodedir_ix_fd),
+ return(From,ok),
+ loop(State);
+ stop ->
+ 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),
+ ok
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% Master Log functions %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+open_ct_master_log(Dir) ->
+ FullName = filename:join(Dir,?ct_master_log_name),
+ {ok,Fd} = file:open(FullName,[write]),
+ io:format(Fd,header("Common Test Master Log"),[]),
+ %% maybe add config info here later
+ io:format(Fd, config_table([]), []),
+ io:format(Fd,
+ "<style>\n"
+ "div.ct_internal { background:lightgrey; color:black }\n"
+ "div.default { background:lightgreen; color:black }\n"
+ "</style>\n",
+ []),
+ io:format(Fd,
+ "<br><h2>Progress Log</h2>\n"
+ "<pre>\n",[]),
+ Fd.
+
+close_ct_master_log(Fd) ->
+ io:format(Fd,"</pre>",[]),
+ io:format(Fd,footer(),[]),
+ file:close(Fd).
+
+config_table(Vars) ->
+ [config_table_header()|config_table1(Vars)].
+
+config_table_header() ->
+ ["<h2>Configuration</h2>\n",
+ "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color,
+ "\"\n",
+ "<tr><th>Key</th><th>Value</th></tr>\n"].
+
+%%
+%% keep for possible later use
+%%
+%%config_table1([{Key,Value}|Vars]) ->
+%% ["<tr><td>", atom_to_list(Key), "</td>\n",
+%% "<td><pre>",io_lib:format("~p",[Value]),"</pre></td></tr>\n" |
+%% config_table1(Vars)];
+
+config_table1([]) ->
+ ["</table>\n"].
+
+int_header() ->
+ "<div class=\"ct_internal\"><b>*** CT MASTER ~s *** ~s</b>".
+int_footer() ->
+ "</div>".
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% NodeDir Index functions %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+open_nodedir_index(Dir,StartTime) ->
+ FullName = filename:join(Dir,?nodedir_index_name),
+ {ok,Fd} = file:open(FullName,[write]),
+ io:format(Fd,nodedir_index_header(StartTime),[]),
+ Fd.
+
+print_nodedir(Node,RunDir,Fd) ->
+ Index = filename:join(RunDir,"index.html"),
+ io:format(Fd,
+ ["<TR>\n"
+ "<TD ALIGN=center>",atom_to_list(Node),"</TD>\n",
+ "<TD ALIGN=left><A HREF=\"",Index,"\">",Index,"</A></TD>\n",
+ "</TR>\n"],[]),
+ ok.
+
+close_nodedir_index(Fd) ->
+ io:format(Fd,index_footer(),[]),
+ file:close(Fd).
+
+nodedir_index_header(StartTime) ->
+ [header("Log Files " ++ format_time(StartTime)) |
+ ["<CENTER>\n",
+ "<P><A HREF=\"",?ct_master_log_name,"\">Common Test Master Log</A></P>",
+ "<TABLE border=\"3\" cellpadding=\"5\" ",
+ "BGCOLOR=\"",?table_color,"\">\n",
+ "<th><B>Node</B></th>\n",
+ "<th><B>Log</B></th>\n",
+ "\n"]].
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% All Run Index functions %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+make_all_runs_index(LogDir) ->
+ FullName = filename:join(LogDir,?all_runs_name),
+ Match = filename:join(LogDir,logdir_prefix()++"*.*"),
+ Dirs = filelib:wildcard(Match),
+ DirsSorted = (catch sort_all_runs(Dirs)),
+ Header = all_runs_header(),
+ Index = [runentry(Dir) || Dir <- DirsSorted],
+ Result = file:write_file(FullName,Header++Index++index_footer()),
+ Result.
+
+sort_all_runs(Dirs) ->
+ %% sort on time string, always last and on the format:
+ %% "YYYY-MM-DD_HH.MM.SS"
+ KeyList =
+ lists:map(fun(Dir) ->
+ case lists:reverse(string:tokens(Dir,[$.,$_])) of
+ [SS,MM,HH,Date|_] ->
+ {{Date,HH,MM,SS},Dir};
+ _Other ->
+ throw(Dirs)
+ end
+ end,Dirs),
+ lists:reverse(lists:map(fun({_,Dir}) ->
+ Dir
+ end,lists:keysort(1,KeyList))).
+
+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;
+ _Error ->
+ {"unknown",""}
+ end,
+ Index = filename:join(Dir,?nodedir_index_name),
+ ["<TR>\n"
+ "<TD ALIGN=center><A HREF=\"",Index,"\">",timestamp(Dir),"</A></TD>\n",
+ "<TD ALIGN=center>",MasterStr,"</TD>\n",
+ "<TD ALIGN=center>",NodesStr,"</TD>\n",
+ "</TR>\n"].
+
+all_runs_header() ->
+ [header("Master Test Runs") |
+ ["<CENTER>\n",
+ "<TABLE border=\"3\" cellpadding=\"5\" "
+ "BGCOLOR=\"",?table_color,"\">\n"
+ "<th><B>History</B></th>\n"
+ "<th><B>Master Host</B></th>\n"
+ "<th><B>Test Nodes</B></th>\n"
+ "\n"]].
+
+timestamp(Dir) ->
+ [S,Min,H,D,M,Y|_] = lists:reverse(string:tokens(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}}).
+
+write_details_file(Dir,Details) ->
+ FullName = filename:join(Dir,?details_file_name),
+ force_write_file(FullName,term_to_binary(Details)).
+
+read_details_file(Dir) ->
+ FullName = filename:join(Dir,?details_file_name),
+ case file:read_file(FullName) of
+ {ok,Bin} ->
+ binary_to_term(Bin);
+ Error ->
+ Error
+ end.
+
+%%%--------------------------------------------------------------------
+%%% Internal functions
+%%%--------------------------------------------------------------------
+
+header(Title) ->
+ ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n"
+ "<!-- autogenerated by '"++atom_to_list(?MODULE)++"'. -->\n"
+ "<HTML>\n",
+ "<HEAD>\n",
+
+ "<TITLE>" ++ Title ++ "</TITLE>\n",
+ "<META HTTP-EQUIV=\"CACHE-CONTROL\" CONTENT=\"NO-CACHE\">\n",
+
+ "</HEAD>\n",
+
+ body_tag(),
+
+ "<!-- ---- DOCUMENT TITLE ---- -->\n",
+
+ "<CENTER>\n",
+ "<H1>" ++ Title ++ "</H1>\n",
+ "</CENTER>\n",
+
+ "<!-- ---- CONTENT ---- -->\n"].
+
+index_footer() ->
+ ["</TABLE>\n"
+ "</CENTER>\n" | footer()].
+
+footer() ->
+ ["<P><CENTER>\n"
+ "<HR>\n"
+ "<P><FONT SIZE=-1>\n"
+ "Copyright &copy; ", year(),
+ " <A HREF=\"http://erlang.ericsson.se\">Open Telecom Platform</A><BR>\n"
+ "Updated: <!date>", current_time(), "<!/date><BR>\n"
+ "</FONT>\n"
+ "</CENTER>\n"
+ "</body>\n"].
+
+body_tag() ->
+ "<body bgcolor=\"#FFFFFF\" text=\"#000000\" link=\"#0000FF\""
+ "vlink=\"#800080\" alink=\"#FF0000\">\n".
+
+current_time() ->
+ format_time(calendar:local_time()).
+
+format_time({{Y, Mon, D}, {H, Min, S}}) ->
+ Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)),
+ lists:flatten(io_lib:format("~s ~s ~p ~w ~2.2.0w:~2.2.0w:~2.2.0w",
+ [Weekday, month(Mon), D, Y, H, Min, S])).
+
+weekday(1) -> "Mon";
+weekday(2) -> "Tue";
+weekday(3) -> "Wed";
+weekday(4) -> "Thu";
+weekday(5) -> "Fri";
+weekday(6) -> "Sat";
+weekday(7) -> "Sun".
+
+month(1) -> "Jan";
+month(2) -> "Feb";
+month(3) -> "Mar";
+month(4) -> "Apr";
+month(5) -> "May";
+month(6) -> "Jun";
+month(7) -> "Jul";
+month(8) -> "Aug";
+month(9) -> "Sep";
+month(10) -> "Oct";
+month(11) -> "Nov";
+month(12) -> "Dec".
+
+year() ->
+ {Y, _, _} = date(),
+ integer_to_list(Y).
+
+
+make_dirname({{YY,MM,DD},{H,M,S}}) ->
+ io_lib:format(logdir_prefix()++".~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w",
+ [YY,MM,DD,H,M,S]).
+
+logdir_prefix() ->
+ "ct_master_run".
+
+log_timestamp(Now) ->
+ put(log_timestamp,Now),
+ {_,{H,M,S}} = calendar:now_to_local_time(Now),
+ lists:flatten(io_lib:format("~2.2.0w:~2.2.0w:~2.2.0w",
+ [H,M,S])).
+
+force_write_file(Name,Contents) ->
+ force_delete(Name),
+ file:write_file(Name,Contents).
+
+force_delete(Name) ->
+ case file:delete(Name) of
+ {error,eacces} ->
+ force_rename(Name,Name++".old.",0);
+ Other ->
+ Other
+ end.
+
+force_rename(From,To,Number) ->
+ Dest = [To|integer_to_list(Number)],
+ case file:read_file_info(Dest) of
+ {ok,_} ->
+ force_rename(From,To,Number+1);
+ {error,_} ->
+ file:rename(From,Dest)
+ end.
+
+call(Msg) ->
+ case whereis(?MODULE) of
+ undefined ->
+ {error,does_not_exist};
+ Pid ->
+ MRef = erlang:monitor(process,Pid),
+ Ref = make_ref(),
+ ?MODULE ! {Msg,{self(),Ref}},
+ receive
+ {Ref, Result} ->
+ erlang:demonitor(MRef),
+ Result;
+ {'DOWN',MRef,process,_,Reason} ->
+ {error,{process_down,?MODULE,Reason}}
+ end
+ end.
+
+return({To,Ref},Result) ->
+ To ! {Ref, Result}.
+
+cast(Msg) ->
+ case whereis(?MODULE) of
+ undefined ->
+ {error,does_not_exist};
+ _Pid ->
+ ?MODULE ! Msg
+ end.
diff --git a/lib/common_test/src/ct_master_status.erl b/lib/common_test/src/ct_master_status.erl
new file mode 100644
index 0000000000..76060fb7bb
--- /dev/null
+++ b/lib/common_test/src/ct_master_status.erl
@@ -0,0 +1,124 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Logging functionality for Common Test Master.
+%%%
+%%% <p>This module keeps a list of <code>{Node,Status}</code>
+%%% 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>
+-module(ct_master_status).
+
+-behaviour(gen_event).
+
+-export([]).
+
+%% gen_event callbacks
+-export([init/1, handle_event/2, handle_call/2,
+ handle_info/2, terminate/2, code_change/3]).
+
+-include("ct_event.hrl").
+
+-record(state, {status=[]}).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler.
+%%--------------------------------------------------------------------
+init(_) ->
+ {ok,#state{}}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%% {swap_handler, Args1, State1, Mod2, Args2} |
+%% remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%--------------------------------------------------------------------
+
+%%============================== EVENTS ==============================
+%%
+%% Events documented in ct_event, plus:
+%%
+%% Name = pending_make
+%% Data = {Node,Dir}
+%%
+%% Name = go_ahead_make
+%% Data = {Node,Dir}
+%%
+handle_event(#event{name=Name,node=Node,data=Data},State) ->
+ print("~n=== ~w ===~n", [?MODULE]),
+ print("~p on ~p: ~p~n", [Name,Node,Data]),
+ {ok,State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%% {swap_handler, Reply, Args1, State1,
+%% Mod2, Args2} |
+%% {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Req, State) ->
+ Reply = ok,
+ {ok, Reply, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%% {swap_handler, Args1, State1, Mod2, Args2} |
+%% remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% 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.
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+print(_Str,_Args) ->
+ ok.
+
diff --git a/lib/common_test/src/ct_repeat.erl b/lib/common_test/src/ct_repeat.erl
new file mode 100644
index 0000000000..7ac6e045d7
--- /dev/null
+++ b/lib/common_test/src/ct_repeat.erl
@@ -0,0 +1,263 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2007-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Common Test Framework module that handles repeated test runs
+%%%
+%%% <p>This module exports functions for repeating tests. The following
+%%% script flags (or equivalent ct:run_test/1 options) are supported:
+%%% -until <StopTime>, StopTime = YYMoMoDDHHMMSS | HHMMSS
+%%% -duration <DurTime>, DurTime = HHMMSS
+%%% -force_stop
+%%% -repeat <N>, N = integer()</p>
+
+-module(ct_repeat).
+
+%% Script interface
+-export([loop_test/2]).
+-export([log_loop_info/1]).
+
+%%----------------------------------------------------------
+%% Flags:
+%%----------------------------------------------------------
+
+loop_test(If,Args) when is_list(Args) ->
+ {ok,Cwd} = file:get_cwd(),
+ case get_loop_info(Args) of
+ no_loop ->
+ false;
+ {error,E} ->
+ io:format("Common Test error: ~p\n\n",[E]),
+ 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],
+ loop(If,repeat,0,N,undefined,Args1,undefined),
+ file:set_cwd(Cwd);
+ {stop_time,StopTime} ->
+ case remaining_time(StopTime) of
+ 0 ->
+ io:format("\nCommon Test: No time left to run tests.\n\n",[]),
+ ok;
+ Secs ->
+ io:format("\nCommon Test: Will repeat tests for ~s.\n\n",
+ [ts(Secs)]),
+ TPid =
+ case lists:keymember(force_stop,1,Args) of
+ true ->
+ CtrlPid = self(),
+ spawn(fun() -> stop_after(CtrlPid,Secs) end);
+ false ->
+ undefined
+ end,
+ Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args],
+ loop(If,stop_time,0,Secs,StopTime,Args1,TPid)
+ end,
+ file:set_cwd(Cwd)
+ end.
+
+loop(_,repeat,N,N,_,_Args,_) ->
+ ok;
+
+loop(If,Type,N,Data0,Data1,Args,TPid) ->
+ Pid = spawn_tester(If,self(),Args),
+ receive
+ {'EXIT',Pid,Reason} ->
+ io:format("Test run crashed! This could be an internal error "
+ "- please report!\n\n"
+ "~p\n\n",[Reason]),
+ cancel(TPid),
+ {error,Reason};
+ {Pid,{error,Reason}} ->
+ io:format("\nTest run failed!\nReason: ~p\n\n",[Reason]),
+ cancel(TPid),
+ {error,Reason};
+ {Pid,Result} ->
+ if Type == repeat ->
+ io:format("\nTest run ~w(~w) complete.\n\n",[N+1,Data0]),
+ lists:keydelete(loop_info,1,Args),
+ Args1 = [{loop_info,[{repeat,N+2,Data0}]} | Args],
+ loop(If,repeat,N+1,Data0,Data1,Args1,TPid);
+ Type == stop_time ->
+ case remaining_time(Data1) of
+ 0 ->
+ io:format("\nTest time (~s) has run out.\n\n",[ts(Data0)]),
+ cancel(TPid),
+ Result;
+ Secs ->
+ io:format("\n~s of test time remaining, "
+ "starting run #~w...\n\n",[ts(Secs),N+2]),
+ lists:keydelete(loop_info,1,Args),
+ ST = {stop_time,Data0,Data1,N+2},
+ Args1 = [{loop_info,[ST]} | Args],
+ loop(If,stop_time,N+1,Data0,Data1,Args1,TPid)
+ end
+ end
+ end.
+
+spawn_tester(script,Ctrl,Args) ->
+ spawn_link(fun() -> ct_run:script_start1(Ctrl,Args) end);
+
+spawn_tester(func,Ctrl,Opts) ->
+ Tester = fun() ->
+ case catch ct_run:run_test1(Opts) of
+ {'EXIT',Reason} ->
+ exit(Reason);
+ Result ->
+ Ctrl ! {self(),Result}
+ end
+ end,
+ spawn_link(Tester).
+
+remaining_time(StopTime) ->
+ Now = calendar:datetime_to_gregorian_seconds(calendar:local_time()),
+ Diff = StopTime - Now,
+ if Diff > 0 ->
+ Diff;
+ true ->
+ 0
+ end.
+
+get_loop_info(Args) when is_list(Args) ->
+ case lists:keysearch(until,1,Args) of
+ {value,{until,Time}} ->
+ Time1 = delistify(Time),
+ case catch get_stop_time(until,Time1) of
+ {'EXIT',_} ->
+ {error,{bad_time_format,Time1}};
+ Stop ->
+ {stop_time,Stop}
+ end;
+ false ->
+ case lists:keysearch(duration,1,Args) of
+ {value,{duration,Time}} ->
+ Time1 = delistify(Time),
+ case catch get_stop_time(duration,Time1) of
+ {'EXIT',_} ->
+ {error,{bad_time_format,Time1}};
+ Stop ->
+ {stop_time,Stop}
+ end;
+ false ->
+ case lists:keysearch(repeat,1,Args) of
+ {value,{repeat,R}} ->
+ case R of
+ N when is_integer(N), N>0 ->
+ {repeat,N};
+ [Str] ->
+ case catch list_to_integer(Str) of
+ N when is_integer(N), N>0 ->
+ {repeat,N};
+ _ ->
+ {error,{invalid_repeat_value,Str}}
+ end;
+ _ ->
+ {error,{invalid_repeat_value,R}}
+ end;
+ false ->
+ no_loop
+ end
+ end
+ end.
+
+get_stop_time(until,[Y1,Y2,Mo1,Mo2,D1,D2,H1,H2,Mi1,Mi2,S1,S2]) ->
+ Date =
+ case [Mo1,Mo2] of
+ "00" ->
+ date();
+ _ ->
+ Y = list_to_integer([Y1,Y2]),
+ Mo = list_to_integer([Mo1,Mo2]),
+ D = list_to_integer([D1,D2]),
+ {YNow,_,_} = date(),
+ Dec = trunc(YNow/100),
+ Year =
+ if Y < (YNow-Dec*100) -> (Dec+1)*100 + Y;
+ true -> Dec*100 + Y
+ end,
+ {Year,Mo,D}
+ end,
+ Time = {list_to_integer([H1,H2]),
+ list_to_integer([Mi1,Mi2]),
+ list_to_integer([S1,S2])},
+ calendar:datetime_to_gregorian_seconds({Date,Time});
+
+get_stop_time(until,Time) ->
+ get_stop_time(until,"000000"++Time);
+
+get_stop_time(duration,[H1,H2,Mi1,Mi2,S1,S2]) ->
+ Secs =
+ list_to_integer([H1,H2]) * 3600 +
+ list_to_integer([Mi1,Mi2]) * 60 +
+ list_to_integer([S1,S2]),
+ calendar:datetime_to_gregorian_seconds(calendar:local_time()) + Secs.
+
+cancel(Pid) ->
+ catch exit(Pid,kill).
+
+%% After Secs, abort will make the test_server finish the current
+%% job, then empty the job queue and stop.
+stop_after(_CtrlPid,Secs) ->
+ timer:sleep(Secs*1000),
+ test_server_ctrl:abort().
+
+%% Callback from ct_run to print loop info to system log.
+log_loop_info(Args) ->
+ case lists:keysearch(loop_info,1,Args) of
+ false ->
+ ok;
+ {value,{_,[{repeat,C,N}]}} ->
+ ct_logs:log("Test loop info","Test run ~w of ~w",[C,N]);
+ {value,{_,[{stop_time,Secs0,StopTime,N}]}} ->
+ LogStr1 =
+ case lists:keysearch(duration,1,Args) of
+ {value,{_,Dur}} ->
+ io_lib:format("Specified test duration: ~s (~w secs)\n",
+ [delistify(Dur),Secs0]);
+ _ ->
+ case lists:keysearch(until,1,Args) of
+ {value,{_,Until}} ->
+ io_lib:format("Specified end time: ~s (duration ~w secs)\n",
+ [delistify(Until),Secs0]);
+ _ ->
+ ok
+ end
+ end,
+ LogStr2 = io_lib:format("Test run #~w\n", [N]),
+ Secs = remaining_time(StopTime),
+ LogStr3 =
+ io_lib:format("Test time remaining: ~w secs (~w%)\n",
+ [Secs,trunc((Secs/Secs0)*100)]),
+ LogStr4 =
+ case lists:keymember(force_stop,1,Args) of
+ true ->
+ io_lib:format("force_stop is enabled",[]);
+ _ ->
+ ""
+ end,
+ ct_logs:log("Test loop info",LogStr1++LogStr2++LogStr3++LogStr4,[])
+ end.
+
+ts(Secs) ->
+ integer_to_list(Secs) ++ " secs".
+
+delistify([X]) ->
+ X;
+delistify(X) ->
+ X.
diff --git a/lib/common_test/src/ct_rpc.erl b/lib/common_test/src/ct_rpc.erl
new file mode 100644
index 0000000000..03d95d1408
--- /dev/null
+++ b/lib/common_test/src/ct_rpc.erl
@@ -0,0 +1,204 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Common Test specific layer on Erlang/OTP rpc.
+
+-module(ct_rpc).
+
+%%% API
+-export([app_node/2, app_node/3, app_node/4,
+ call/4, call/5, call/6, cast/4, cast/5]).
+
+%%%=========================================================================
+%%% 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});
+
+%% Variable _Candidates is a workaround for the strange edoc behavior
+%% of creating the spec: app_node(App, Nodes::Candidates) -> NodeName
+%% if it does not exist.
+app_node(App, _Candidates = [CandidateNode | Nodes], FailOnBadRPC, Cookie) ->
+ Cookie0 = set_the_cookie(Cookie),
+ Result = rpc:call(CandidateNode, application, which_applications, []),
+ set_the_cookie(Cookie0),
+ case Result of
+ {badrpc,Reason} when FailOnBadRPC == true ->
+ ct:fail({Reason,CandidateNode});
+ {badrpc,_} when FailOnBadRPC == false ->
+ app_node(App, Nodes, FailOnBadRPC);
+ Apps ->
+ case lists:keysearch(App, 1, Apps) of
+ {value, _} ->
+ CandidateNode;
+ _ ->
+ app_node(App, Nodes, FailOnBadRPC)
+ 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),
+ 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),
+ ok.
+
+
+%%%---------- Internal -----------
+
+%%% @hidden
+set_the_cookie([]) ->
+ [];
+set_the_cookie(Cookie) ->
+ Cookie0 = erlang:get_cookie(),
+ erlang:set_cookie(node(),Cookie),
+ Cookie0.
diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl
new file mode 100644
index 0000000000..a1e2358578
--- /dev/null
+++ b/lib/common_test/src/ct_run.erl
@@ -0,0 +1,1812 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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
+-export([script_start/0,script_usage/0]).
+
+%% User interface
+-export([install/1,install/2,run/1,run/2,run/3,run_test/1,
+ run_testspec/1,step/3,step/4,refresh_logs/1]).
+
+
+%% Exported for VTS
+-export([run_make/3,do_run/3,tests/1,tests/2,tests/3]).
+
+
+%% Misc internal functions
+-export([variables_file_name/1,script_start1/2,run_test1/1]).
+
+-include("ct_event.hrl").
+-include("ct_util.hrl").
+
+-define(abs(Name), filename:absname(Name)).
+-define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)).
+
+%%%-----------------------------------------------------------------
+%%% @spec script_start() -> void()
+%%%
+%%% @doc Start tests via the run_test script.
+%%%
+%%% <p>Example:<br/><code>./run_test -config config.ctc -dir
+%%% $TEST_DIR</code></p>
+%%%
+%%% <p>Example:<br/><code>./run_test -config config.ctc -suite
+%%% $SUITE_PATH/$SUITE_NAME [-case $CASE_NAME]</code></p>
+%%%
+script_start() ->
+ process_flag(trap_exit, true),
+ Args = merge_arguments(init:get_arguments()),
+ Tracing = start_trace(Args),
+ Res =
+ case ct_repeat:loop_test(script, Args) of
+ false ->
+ {ok,Cwd} = file:get_cwd(),
+ CTVsn =
+ case filename:basename(code:lib_dir(common_test)) of
+ CTBase when is_list(CTBase) ->
+ case string:tokens(CTBase, "-") of
+ ["common_test",Vsn] -> " v"++Vsn;
+ _ -> ""
+ end
+ end,
+ io:format("~nCommon Test~s starting (cwd is ~s)~n~n", [CTVsn,Cwd]),
+ Self = self(),
+ Pid = spawn_link(fun() -> script_start1(Self, Args) end),
+ receive
+ {'EXIT',Pid,Reason} ->
+ case Reason of
+ {user_error,What} ->
+ io:format("\nTest run failed!\nReason: ~p\n\n", [What]),
+ {error,What};
+ _ ->
+ io:format("Test run crashed! This could be an internal error "
+ "- please report!\n\n"
+ "~p\n\n", [Reason]),
+ {error,Reason}
+ end;
+ {Pid,{error,Reason}} ->
+ io:format("\nTest run failed! Reason:\n~p\n\n",[Reason]),
+ {error,Reason};
+ {Pid,Result} ->
+ Result
+ end;
+ Result ->
+ Result
+ end,
+ stop_trace(Tracing),
+ Res.
+
+script_start1(Parent, Args) ->
+ case lists:keymember(preload, 1, Args) of
+ true -> preload();
+ false -> ok
+ end,
+
+ VtsOrShell =
+ case lists:keymember(vts, 1, Args) of
+ true ->
+ vts;
+ false ->
+ case lists:keymember(shell, 1, Args) of
+ true -> shell;
+ false -> false
+ end
+ end,
+ LogDir =
+ case lists:keysearch(logdir, 1, Args) of
+ {value,{logdir,[LogD]}} -> LogD;
+ false -> "."
+ end,
+ EvHandlers =
+ case lists:keysearch(event_handler, 1, Args) of
+ {value,{event_handler,Handlers}} ->
+ lists:map(fun(H) -> {list_to_atom(H),[]} end, Handlers);
+ false ->
+ []
+ end,
+ Cover =
+ case lists:keysearch(cover, 1, Args) of
+ {value,{cover,CoverFile}} ->
+ {cover,?abs(CoverFile)};
+ false ->
+ false
+ end,
+
+ case lists:keysearch(ct_decrypt_key, 1, Args) of
+ {value,{_,[DecryptKey]}} ->
+ application:set_env(common_test, decrypt, {key,DecryptKey});
+ false ->
+ case lists:keysearch(ct_decrypt_file, 1, Args) of
+ {value,{_,[DecryptFile]}} ->
+ application:set_env(common_test, decrypt,
+ {file,filename:absname(DecryptFile)});
+ false ->
+ application:unset_env(common_test, decrypt)
+ end
+ end,
+
+ case lists:keysearch(no_auto_compile, 1, Args) of
+ {value,_} ->
+ application:set_env(common_test, auto_compile, false);
+ false ->
+ application:set_env(common_test, auto_compile, true),
+
+ InclDirs =
+ case lists:keysearch(include,1,Args) of
+ {value,{include,Incl}} when is_list(hd(Incl)) ->
+ Incl;
+ {value,{include,Incl}} when is_list(Incl) ->
+ [Incl];
+ false ->
+ []
+ end,
+ case os:getenv("CT_INCLUDE_PATH") of
+ false ->
+ application:set_env(common_test, include, InclDirs);
+ CtInclPath ->
+ InclDirs1 = string:tokens(CtInclPath,[$:,$ ,$,]),
+ application:set_env(common_test, include, InclDirs1++InclDirs)
+ end
+ end,
+
+ case lists:keysearch(basic_html, 1, Args) of
+ {value,_} ->
+ application:set_env(common_test, basic_html, true);
+ false ->
+ application:set_env(common_test, basic_html, false)
+ end,
+
+ Result =
+ case lists:keysearch(refresh_logs, 1, Args) of
+ {value,{refresh_logs,Refresh}} ->
+ LogDir1 = case Refresh of
+ [] -> LogDir;
+ [RefreshDir] -> ?abs(RefreshDir)
+ end,
+ {ok,Cwd} = file:get_cwd(),
+ file:set_cwd(LogDir1),
+ timer:sleep(500), % give the shell time to print version etc
+ io:nl(),
+ case catch ct_logs:make_all_suites_index(refresh) of
+ {'EXIT',ASReason} ->
+ 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),
+ {error,{all_runs_index,ARReason}};
+ _ ->
+ file:set_cwd(Cwd),
+ io:format("Logs in ~s refreshed!~n~n", [LogDir1]),
+ timer:sleep(500), % time to flush io before quitting
+ ok
+ end
+ end;
+ false ->
+ case lists:keysearch(ct_config, 1, Args) of
+ {value,{ct_config,ConfigFiles}} ->
+ case lists:keysearch(spec, 1, Args) of
+ false ->
+ case get_configfiles(ConfigFiles, [], LogDir,
+ EvHandlers) of
+ ok ->
+ script_start2(VtsOrShell, ConfigFiles,
+ EvHandlers, Args, LogDir,
+ Cover);
+ Error ->
+ Error
+ end;
+ _ ->
+ script_start2(VtsOrShell, ConfigFiles,
+ EvHandlers, Args, LogDir, Cover)
+ end;
+ false ->
+ case install([{config,[]},
+ {event_handler,EvHandlers}],
+ LogDir) of
+ ok ->
+ script_start2(VtsOrShell, [], EvHandlers,
+ Args, LogDir, Cover);
+ Error ->
+ Error
+ end
+ end
+ end,
+ Parent ! {self(), Result}.
+
+get_configfiles([File|Files], Acc, LogDir, EvHandlers) ->
+ case filelib:is_file(File) of
+ true ->
+ get_configfiles(Files, [?abs(File)|Acc],
+ LogDir, EvHandlers);
+ false ->
+ {error,{cant_read_config_file,File}}
+ end;
+get_configfiles([], Acc, LogDir, EvHandlers) ->
+ install([{config,lists:reverse(Acc)}, {event_handler,EvHandlers}], LogDir).
+
+script_start2(false, ConfigFiles, EvHandlers, Args, LogDir, Cover) ->
+ case lists:keysearch(spec, 1, Args) of
+ {value,{spec,[]}} ->
+ {error,no_testspec_specified};
+ {value,{spec,Specs}} ->
+ Relaxed = lists:keymember(allow_user_terms, 1, Args),
+ %% using testspec as input for test
+ case catch ct_testspec:collect_tests_from_file(Specs, Relaxed) of
+ {error,Reason} ->
+ {error,Reason};
+ TS ->
+ {LogDir1,TSCoverFile,ConfigFiles1,EvHandlers1,Include1} =
+ get_data_for_node(TS,node()),
+ UserInclude =
+ case application:get_env(common_test, include) of
+ {ok,Include} -> Include++Include1;
+ _ -> Include1
+ end,
+ application:set_env(common_test, include, UserInclude),
+ LogDir2 = which_logdir(LogDir,LogDir1),
+ CoverOpt = case {Cover,TSCoverFile} of
+ {false,undef} -> [];
+ {_,undef} -> [Cover];
+ {false,_} -> [{cover,TSCoverFile}]
+ end,
+ case get_configfiles(ConfigFiles++ConfigFiles1,
+ [], LogDir2,
+ EvHandlers++EvHandlers1) of
+ ok ->
+ {Run,Skip} = ct_testspec:prepare_tests(TS, node()),
+ do_run(Run, Skip, CoverOpt, Args, LogDir2);
+ Error ->
+ Error
+ end
+ end;
+ false ->
+ script_start3(false, ConfigFiles, EvHandlers, Args, LogDir, Cover)
+ end;
+script_start2(VtsOrShell, ConfigFiles, EvHandlers, Args, LogDir, Cover) ->
+ script_start3(VtsOrShell, ConfigFiles, EvHandlers, Args, LogDir, Cover).
+
+script_start3(VtsOrShell, ConfigFiles, EvHandlers, Args, LogDir, Cover) ->
+ case lists:keysearch(dir, 1, Args) of
+ {value,{dir,[]}} ->
+ {error,no_dir_specified};
+ {value,{dir,Dirs}} ->
+ script_start4(VtsOrShell, ConfigFiles, EvHandlers, tests(Dirs),
+ Cover, Args, LogDir);
+ false ->
+ case lists:keysearch(suite, 1, Args) of
+ {value,{suite,[]}} ->
+ {error,no_suite_specified};
+ {value,{suite,Suites}} ->
+ StepOrCover =
+ case lists:keysearch(step, 1, Args) of
+ {value,Step} -> Step;
+ false -> Cover
+ end,
+ S2M = fun(S) ->
+ {filename:dirname(S),
+ list_to_atom(
+ filename:rootname(filename:basename(S)))}
+ end,
+ DirMods = lists:map(S2M, Suites),
+ {Specified,GroupsAndCases} =
+ case {lists:keysearch(group, 1, Args),
+ lists:keysearch('case', 1, Args)} of
+ {{value,{_,Gs}},{value,{_,Cs}}} -> {true,Gs++Cs};
+ {{value,{_,Gs}},_} -> {true,Gs};
+ {_,{value,{_,Cs}}} -> {true,Cs};
+ _ -> {false,[]}
+ end,
+ if Specified, length(GroupsAndCases) == 0 ->
+ {error,no_case_or_group_specified};
+ Specified, length(DirMods) > 1 ->
+ {error,multiple_suites_and_cases};
+ length(GroupsAndCases) > 0, length(DirMods) == 1 ->
+ GsAndCs = lists:map(fun(C) -> list_to_atom(C) end,
+ GroupsAndCases),
+ script_start4(VtsOrShell, ConfigFiles, EvHandlers,
+ tests(DirMods, GsAndCs),
+ StepOrCover, Args, LogDir);
+ not Specified, length(DirMods) > 0 ->
+ script_start4(VtsOrShell, ConfigFiles, EvHandlers,
+ tests(DirMods),
+ StepOrCover, Args, LogDir);
+ true ->
+ {error,incorrect_suite_and_case_options}
+ end;
+ false when VtsOrShell=/=false ->
+ script_start4(VtsOrShell, ConfigFiles, EvHandlers,
+ [], Cover, Args, LogDir);
+ false ->
+ script_usage(),
+ {error,incorrect_usage}
+ end
+ end.
+
+script_start4(vts, ConfigFiles, EvHandlers, Tests, false, _Args, LogDir) ->
+ vts:init_data(ConfigFiles, EvHandlers, ?abs(LogDir), Tests);
+script_start4(shell, ConfigFiles, EvHandlers, _Tests, false, Args, LogDir) ->
+ Opts = [{config,ConfigFiles},{event_handler,EvHandlers}],
+ if ConfigFiles == [] ->
+ ok;
+ true ->
+ io:format("\nInstalling: ~p\n\n", [ConfigFiles])
+ end,
+ case install(Opts) of
+ ok ->
+ ct_util:start(interactive, LogDir),
+ log_ts_names(Args),
+ io:nl(),
+ ok;
+ Error ->
+ Error
+ end;
+script_start4(vts, _CfgFs, _EvHs, _Tests, _Cover={cover,_}, _Args, _LogDir) ->
+ %% Add support later (maybe).
+ script_usage(),
+ erlang:halt();
+script_start4(shell, _CfgFs, _EvHs, _Tests, _Cover={cover,_}, _Args, _LogDir) ->
+ %% Add support later (maybe).
+ script_usage();
+script_start4(false, _CfgFs, _EvHs, Tests, Cover={cover,_}, Args, LogDir) ->
+ do_run(Tests, [], [Cover], Args, LogDir);
+script_start4(false, _ConfigFiles, _EvHandlers, Tests, false, Args, LogDir) ->
+ do_run(Tests, [], [], Args, LogDir);
+script_start4(false, _ConfigFiles, _EvHandlers, Test, Step, Args, LogDir) ->
+ do_run(Test, [], [Step], Args, LogDir);
+script_start4(vts, _ConfigFiles, _EvHandlers, _Test, _Step, _Args, _LogDir) ->
+ script_usage(),
+ erlang:halt();
+script_start4(shell, _ConfigFiles, _EvHandlers, _Test, _Step, _Args, _LogDir) ->
+ script_usage().
+
+%%%-----------------------------------------------------------------
+%%% @spec script_usage() -> ok
+%%% @doc Print script usage information for <code>run_test</code>.
+script_usage() ->
+ io:format("\n\nUsage:\n\n"),
+ io:format("Run tests in web based GUI:\n\n"
+ "\trun_test -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[-include InclDir1 InclDir2 .. InclDirN]"
+ "\n\t[-no_auto_compile]"
+ "\n\t[-basic_html]\n\n"),
+ io:format("Run tests from command line:\n\n"
+ "\trun_test [-dir TestDir1 TestDir2 .. TestDirN] |"
+ "\n\t[-suite Suite1 Suite2 .. SuiteN [-case Case1 Case2 .. CaseN]]"
+ "\n\t[-step [config | keep_inactive]]"
+ "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
+ "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
+ "\n\t[-logdir LogDir]"
+ "\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
+ "\n\t[-stylesheet CSSFile]"
+ "\n\t[-cover CoverCfgFile]"
+ "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
+ "\n\t[-include InclDir1 InclDir2 .. InclDirN]"
+ "\n\t[-no_auto_compile]"
+ "\n\t[-basic_html]"
+ "\n\t[-repeat N [-force_stop]] |"
+ "\n\t[-duration HHMMSS [-force_stop]] |"
+ "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop]]\n\n"),
+ io:format("Run tests using test specification:\n\n"
+ "\trun_test -spec TestSpec1 TestSpec2 .. TestSpecN"
+ "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
+ "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
+ "\n\t[-logdir LogDir]"
+ "\n\t[-allow_user_terms]"
+ "\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
+ "\n\t[-stylesheet CSSFile]"
+ "\n\t[-cover CoverCfgFile]"
+ "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
+ "\n\t[-include InclDir1 InclDir2 .. InclDirN]"
+ "\n\t[-no_auto_compile]"
+ "\n\t[-basic_html]"
+ "\n\t[-repeat N [-force_stop]] |"
+ "\n\t[-duration HHMMSS [-force_stop]] |"
+ "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop]]\n\n"),
+ io:format("Refresh the HTML index files:\n\n"
+ "\trun_test -refresh_logs [LogDir]"
+ "[-logdir LogDir] "
+ "[-basic_html]\n\n"),
+ io:format("Run CT in interactive mode:\n\n"
+ "\trun_test -shell"
+ "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
+ "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]\n\n").
+
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:install/1
+install(Opts) ->
+ install(Opts, ".").
+
+install(Opts, LogDir) ->
+ case application:get_env(common_test, decrypt) of
+ {ok,_} ->
+ ok;
+ _ ->
+ case lists:keysearch(decrypt, 1, Opts) of
+ {value,{_,KeyOrFile}} ->
+ application:set_env(common_test, decrypt, KeyOrFile);
+ false ->
+ application:unset_env(common_test, decrypt)
+ end
+ end,
+ case whereis(ct_util_server) of
+ undefined ->
+ VarFile = variables_file_name(LogDir),
+ case file:open(VarFile, [write]) of
+ {ok,Fd} ->
+ [io:format(Fd, "~p.\n", [Opt]) || Opt <- Opts],
+ file:close(Fd),
+ ok;
+ {error,Reason} ->
+ io:format("CT failed to install configuration data. Please "
+ "verify that the log directory exists and that "
+ "write permission is set.\n\n", []),
+ {error,{VarFile,Reason}}
+ end;
+ _ ->
+ io:format("It is not possible to install CT while running "
+ "in interactive mode.\n"
+ "To exit this mode, run ct:stop_interactive().\n"
+ "To enter the interactive mode again, "
+ "run ct:start_interactive()\n\n", []),
+ {error,interactive_mode}
+ end.
+
+variables_file_name(Dir) ->
+ filename:join(Dir, "variables-"++atom_to_list(node())).
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:run_test/1
+
+%% Opts = [OptTuples]
+%% OptTuples = {config,CfgFiles} | {dir,TestDirs} | {suite,Suites} |
+%% {testcase,Cases} | {spec,TestSpecs} | {allow_user_terms,Bool} |
+%% {logdir,LogDir} | {cover,CoverSpecFile} | {step,StepOpts} |
+%% {silent_connections,Conns} | {event_handler,EventHandlers} |
+%% {include,InclDirs} | {auto_compile,Bool} |
+%% {repeat,N} | {duration,DurTime} | {until,StopTime} | {force_stop,Bool} |
+%% {decrypt,KeyOrFile}
+
+run_test(Opt) when is_tuple(Opt) ->
+ run_test([Opt]);
+
+run_test(Opts) when is_list(Opts) ->
+ case lists:keysearch(refresh_logs, 1, Opts) of
+ {value,{_,RefreshDir}} ->
+ refresh_logs(?abs(RefreshDir)),
+ ok;
+ false ->
+ Tracing = start_trace(Opts),
+ {ok,Cwd} = file:get_cwd(),
+ io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]),
+ Res =
+ case ct_repeat:loop_test(func, Opts) of
+ false ->
+ case catch run_test1(Opts) of
+ {'EXIT',Reason} ->
+ file:set_cwd(Cwd),
+ {error,Reason};
+ Result ->
+ Result
+ end;
+ Result ->
+ Result
+ end,
+ stop_trace(Tracing),
+ Res
+ end.
+
+run_test1(Opts) ->
+ LogDir =
+ case lists:keysearch(logdir, 1, Opts) of
+ {value,{_,LD}} when is_list(LD) -> LD;
+ false -> "."
+ end,
+ CfgFiles =
+ case lists:keysearch(config, 1, Opts) of
+ {value,{_,Files=[File|_]}} when is_list(File) ->
+ Files;
+ {value,{_,File=[C|_]}} when is_integer(C) ->
+ [File];
+ {value,{_,[]}} ->
+ [];
+ false ->
+ []
+ end,
+ EvHandlers =
+ case lists:keysearch(event_handler, 1, Opts) of
+ {value,{_,H}} when is_atom(H) ->
+ [{H,[]}];
+ {value,{_,H}} ->
+ Hs =
+ if is_tuple(H) -> [H];
+ is_list(H) -> H;
+ true -> []
+ end,
+ lists:flatten(
+ lists:map(fun(EH) when is_atom(EH) ->
+ {EH,[]};
+ ({HL,Args}) when is_list(HL) ->
+ [{EH,Args} || EH <- HL];
+ ({EH,Args}) when is_atom(EH) ->
+ {EH,Args};
+ (_) ->
+ []
+ end, Hs));
+ _ ->
+ []
+ end,
+ SilentConns =
+ case lists:keysearch(silent_connections, 1, Opts) of
+ {value,{_,all}} ->
+ [];
+ {value,{_,Conns}} ->
+ Conns;
+ _ ->
+ undefined
+ end,
+ Cover =
+ case lists:keysearch(cover, 1, Opts) of
+ {value,{_,CoverFile}} ->
+ [{cover,?abs(CoverFile)}];
+ _ ->
+ []
+ end,
+ Include =
+ case lists:keysearch(auto_compile, 1, Opts) of
+ {value,{auto_compile,ACBool}} ->
+ application:set_env(common_test, auto_compile, ACBool),
+ [];
+ _ ->
+ application:set_env(common_test, auto_compile, true),
+ InclDirs =
+ case lists:keysearch(include, 1, Opts) of
+ {value,{include,Incl}} when is_list(hd(Incl)) ->
+ Incl;
+ {value,{include,Incl}} when is_list(Incl) ->
+ [Incl];
+ false ->
+ []
+ end,
+ case os:getenv("CT_INCLUDE_PATH") of
+ false ->
+ application:set_env(common_test, include, InclDirs),
+ InclDirs;
+ CtInclPath ->
+ InclDirs1 = string:tokens(CtInclPath, [$:,$ ,$,]),
+ AllInclDirs = InclDirs1++InclDirs,
+ application:set_env(common_test, include, AllInclDirs),
+ AllInclDirs
+ end
+ end,
+
+ case lists:keysearch(decrypt, 1, Opts) of
+ {value,{_,Key={key,_}}} ->
+ application:set_env(common_test, decrypt, Key);
+ {value,{_,{file,KeyFile}}} ->
+ application:set_env(common_test, decrypt, {file,filename:absname(KeyFile)});
+ false ->
+ application:unset_env(common_test, decrypt)
+ end,
+
+ case lists:keysearch(basic_html, 1, Opts) of
+ {value,{basic_html,BasicHtmlBool}} ->
+ application:set_env(common_test, basic_html, BasicHtmlBool);
+ _ ->
+ application:set_env(common_test, basic_html, false)
+ end,
+
+ case lists:keysearch(spec, 1, Opts) of
+ {value,{_,Specs}} ->
+ Relaxed =
+ case lists:keysearch(allow_user_terms, 1, Opts) of
+ {value,{_,true}} -> true;
+ _ -> false
+ end,
+ %% using testspec(s) as input for test
+ run_spec_file(LogDir, CfgFiles, EvHandlers, Include, Specs, Relaxed, Cover,
+ replace_opt([{silent_connections,SilentConns}], Opts));
+ false ->
+ case lists:keysearch(prepared_tests, 1, Opts) of
+ {value,{_,{Run,Skip},Specs}} -> % use prepared tests
+ run_prepared(LogDir, CfgFiles, EvHandlers,
+ Run, Skip, Cover,
+ replace_opt([{silent_connections,SilentConns},
+ {spec,Specs}],Opts));
+ false -> % use dir|suite|case
+ StepOrCover =
+ case lists:keysearch(step, 1, Opts) of
+ {value,Step} -> [Step];
+ false -> Cover
+ end,
+ run_dir(LogDir, CfgFiles, EvHandlers, StepOrCover,
+ replace_opt([{silent_connections,SilentConns}], Opts))
+ end
+ end.
+
+replace_opt([O={Key,_Val}|Os], Opts) ->
+ [O | replace_opt(Os, lists:keydelete(Key, 1, Opts))];
+replace_opt([], Opts) ->
+ Opts.
+
+run_spec_file(LogDir, CfgFiles, EvHandlers, Include, Specs, Relaxed, Cover, Opts) ->
+ Specs1 = case Specs of
+ [X|_] when is_integer(X) -> [Specs];
+ _ -> Specs
+ end,
+ AbsSpecs = lists:map(fun(SF) -> ?abs(SF) end, Specs1),
+ log_ts_names(AbsSpecs),
+ case catch ct_testspec:collect_tests_from_file(AbsSpecs, Relaxed) of
+ {error,CTReason} ->
+ exit(CTReason);
+ TS ->
+ {LogDir1,TSCoverFile,CfgFiles1,EvHandlers1,Include1} =
+ get_data_for_node(TS, node()),
+ application:set_env(common_test, include, Include++Include1),
+ LogDir2 = which_logdir(LogDir, LogDir1),
+ CoverOpt = case {Cover,TSCoverFile} of
+ {[],undef} -> [];
+ {_,undef} -> Cover;
+ {[],_} -> [{cover,TSCoverFile}]
+ end,
+ case get_configfiles(CfgFiles++CfgFiles1, [], LogDir2,
+ EvHandlers++EvHandlers1) of
+ ok ->
+ {Run,Skip} = ct_testspec:prepare_tests(TS, node()),
+ do_run(Run, Skip, CoverOpt,
+ replace_opt([{spec,AbsSpecs}], Opts),
+ LogDir2);
+ {error,GCFReason} ->
+ exit(GCFReason)
+ end
+ end.
+
+run_prepared(LogDir, CfgFiles, EvHandlers, Run, Skip, Cover, Opts) ->
+ case get_configfiles(CfgFiles, [], LogDir, EvHandlers) of
+ ok ->
+ do_run(Run, Skip, Cover, Opts, LogDir);
+ {error,Reason} ->
+ exit(Reason)
+ end.
+
+run_dir(LogDir, CfgFiles, EvHandlers, StepOrCover, Opts) ->
+ AbsCfgFiles =
+ lists:map(fun(F) ->
+ AbsName = ?abs(F),
+ case filelib:is_file(AbsName) of
+ true -> AbsName;
+ false -> exit({no_such_file,AbsName})
+ end
+ end, CfgFiles),
+
+ case install([{config,AbsCfgFiles},{event_handler,EvHandlers}], LogDir) of
+ ok -> ok;
+ {error,IReason} -> exit(IReason)
+ end,
+ case lists:keysearch(dir,1,Opts) of
+ {value,{_,Dirs=[Dir|_]}} when not is_integer(Dir),
+ length(Dirs)>1 ->
+ %% multiple dirs (no suite)
+ do_run(tests(Dirs), [], StepOrCover, Opts, LogDir);
+ false -> % no dir
+ %% fun for converting suite name to {Dir,Mod} tuple
+ S2M = fun(S) when is_list(S) ->
+ {filename:dirname(S),
+ list_to_atom(filename:rootname(filename:basename(S)))};
+ (A) ->
+ {".",A}
+ end,
+ case lists:keysearch(suite, 1, Opts) of
+ {value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) ->
+ {Dir,Mod} = S2M(Suite),
+ case listify(proplists:get_value(group, Opts, [])) ++
+ listify(proplists:get_value(testcase, Opts, [])) of
+ [] ->
+ do_run(tests(Dir, listify(Mod)), [], StepOrCover, Opts, LogDir);
+ GsAndCs ->
+ do_run(tests(Dir, Mod, GsAndCs), [], StepOrCover, Opts, LogDir)
+ end;
+ {value,{_,Suites}} ->
+ do_run(tests(lists:map(S2M, Suites)), [], StepOrCover, Opts, LogDir);
+ _ ->
+ exit(no_tests_specified)
+ end;
+ {value,{_,Dir}} ->
+ case lists:keysearch(suite, 1, Opts) of
+ {value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) ->
+ Mod = if is_atom(Suite) -> Suite;
+ true -> list_to_atom(Suite)
+ end,
+ case listify(proplists:get_value(group, Opts, [])) ++
+ listify(proplists:get_value(testcase, Opts, [])) of
+ [] ->
+ do_run(tests(Dir, listify(Mod)), [], StepOrCover, Opts, LogDir);
+ GsAndCs ->
+ do_run(tests(Dir, Mod, GsAndCs), [], StepOrCover, Opts, LogDir)
+ end;
+ {value,{_,Suites=[Suite|_]}} when is_list(Suite) ->
+ Mods = lists:map(fun(Str) -> list_to_atom(Str) end, Suites),
+ do_run(tests(delistify(Dir), Mods), [], StepOrCover, Opts, LogDir);
+ {value,{_,Suites}} ->
+ do_run(tests(delistify(Dir), Suites), [], StepOrCover, Opts, LogDir);
+ false -> % no suite, only dir
+ do_run(tests(listify(Dir)), [], StepOrCover, Opts, LogDir)
+ end
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%%
+
+%% using testspec(s) as input for test
+run_testspec(TestSpec) ->
+ {ok,Cwd} = file:get_cwd(),
+ io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]),
+ case catch run_testspec1(TestSpec) of
+ {'EXIT',Reason} ->
+ file:set_cwd(Cwd),
+ {error,Reason};
+ Result ->
+ Result
+ end.
+
+run_testspec1(TestSpec) ->
+ case ct_testspec:collect_tests_from_list(TestSpec,false) of
+ {error,CTReason} ->
+ exit(CTReason);
+ TS ->
+ {LogDir,TSCoverFile,CfgFiles,EvHandlers,Include} =
+ get_data_for_node(TS,node()),
+ case os:getenv("CT_INCLUDE_PATH") of
+ false ->
+ application:set_env(common_test, include, Include);
+ CtInclPath ->
+ EnvInclude = string:tokens(CtInclPath, [$:,$ ,$,]),
+ application:set_env(common_test, include, EnvInclude++Include)
+ end,
+ CoverOpt = if TSCoverFile == undef -> [];
+ true -> [{cover,TSCoverFile}]
+ end,
+ case get_configfiles(CfgFiles,[],LogDir,EvHandlers) of
+ ok ->
+ {Run,Skip} = ct_testspec:prepare_tests(TS,node()),
+ do_run(Run,Skip,CoverOpt,[],LogDir);
+ {error,GCFReason} ->
+ exit(GCFReason)
+ end
+ end.
+
+
+get_data_for_node(#testspec{logdir=LogDirs,
+ cover=CoverFs,
+ config=Cfgs,
+ event_handler=EvHs,
+ include=Incl}, Node) ->
+ LogDir = case lists:keysearch(Node,1,LogDirs) of
+ {value,{Node,Dir}} -> Dir;
+ false -> "."
+ end,
+ Cover = case lists:keysearch(Node,1,CoverFs) of
+ {value,{Node,CovFile}} -> CovFile;
+ false -> undef
+ end,
+ ConfigFiles = [F || {N,F} <- Cfgs, N==Node],
+ EvHandlers = [{H,A} || {N,H,A} <- EvHs, N==Node],
+ Include = [I || {N,I} <- Incl, N==Node],
+ {LogDir,Cover,ConfigFiles,EvHandlers,Include}.
+
+
+refresh_logs(LogDir) ->
+ {ok,Cwd} = file:get_cwd(),
+ case file:set_cwd(LogDir) of
+ E = {error,_Reason} ->
+ E;
+ _ ->
+ case catch ct_logs:make_all_suites_index(refresh) of
+ {'EXIT',ASReason} ->
+ 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),
+ {error,{all_runs_index,ARReason}};
+ _ ->
+ file:set_cwd(Cwd),
+ io:format("Logs in ~s refreshed!~n",[LogDir]),
+ ok
+ end
+ end
+ end.
+
+which_logdir(".",Dir) ->
+ Dir;
+which_logdir(Dir,_) ->
+ Dir.
+
+listify([C|_]=Str) when is_integer(C) -> [Str];
+listify(L) when is_list(L) -> L;
+listify(E) -> [E].
+
+delistify([E]) -> E;
+delistify(E) -> E.
+
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:run/3
+run(TestDir, Suite, Cases) ->
+ install([]),
+ do_run(tests(TestDir, Suite, Cases), []).
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:run/2
+run(TestDir, Suite) when is_list(TestDir), is_integer(hd(TestDir)) ->
+ install([]),
+ do_run(tests(TestDir, Suite), []).
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:run/1
+run(TestDirs) ->
+ install([]),
+ do_run(tests(TestDirs), []).
+
+
+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([{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(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)) ->
+ tests([TestDir]);
+tests(TestDirs) when is_list(TestDirs), is_list(hd(TestDirs)) ->
+ [{?testdir(TestDir,all),all,all} || TestDir <- TestDirs].
+
+do_run(Tests, Opt) ->
+ do_run(Tests, [], Opt, [], ".").
+
+do_run(Tests, Opt, LogDir) ->
+ do_run(Tests, [], Opt, [], LogDir).
+
+do_run(Tests, Skip, Opt, Args, LogDir) ->
+ case code:which(test_server) of
+ non_existing ->
+ exit({error,no_path_to_test_server});
+ _ ->
+ Opt1 =
+ case lists:keysearch(cover, 1, Opt) of
+ {value,{_,CoverFile}} ->
+ case ct_cover:get_spec(CoverFile) of
+ {error,Reason} ->
+ exit({error,Reason});
+ Spec ->
+ [{cover_spec,Spec} |
+ lists:keydelete(cover, 1, Opt)]
+ end;
+ _ ->
+ Opt
+ end,
+ %% This env variable is used by test_server to determine
+ %% which framework it runs under.
+ case os:getenv("TEST_SERVER_FRAMEWORK") of
+ false ->
+ os:putenv("TEST_SERVER_FRAMEWORK", "ct_framework");
+ "ct_framework" ->
+ ok;
+ Other ->
+ erlang:display(list_to_atom("Note: TEST_SERVER_FRAMEWORK = " ++ Other))
+ end,
+ case ct_util:start(LogDir) of
+ {error,interactive_mode} ->
+ io:format("CT is started in interactive mode. "
+ "To exit this mode, run ct:stop_interactive().\n"
+ "To enter the interactive mode again, "
+ "run ct:start_interactive()\n\n",[]),
+ {error,interactive_mode};
+
+ _Pid ->
+ %% save style sheet info
+ case lists:keysearch(stylesheet, 1, Args) of
+ {value,{_,SSFile}} ->
+ ct_util:set_testdata({stylesheet,SSFile});
+ _ ->
+ ct_util:set_testdata({stylesheet,undefined})
+ end,
+
+ case lists:keysearch(silent_connections, 1, Args) of
+ {value,{silent_connections,undefined}} ->
+ ok;
+ {value,{silent_connections,[]}} ->
+ Conns = ct_util:override_silence_all_connections(),
+ ct_logs:log("Silent connections", "~p", [Conns]);
+ {value,{silent_connections,Cs}} ->
+ Conns = lists:map(fun(S) when is_list(S) ->
+ list_to_atom(S);
+ (A) -> A
+ end, Cs),
+ ct_util:override_silence_connections(Conns),
+ ct_logs:log("Silent connections", "~p", [Conns]);
+ _ ->
+ ok
+ end,
+ log_ts_names(Args),
+ TestSuites = suite_tuples(Tests),
+
+ {SuiteMakeErrors,AllMakeErrors} =
+ case application:get_env(common_test, auto_compile) of
+ {ok,false} ->
+ SuitesNotFound = verify_suites(TestSuites),
+ {SuitesNotFound,SuitesNotFound};
+ _ ->
+ {SuiteErrs,HelpErrs} = auto_compile(TestSuites),
+ {SuiteErrs,SuiteErrs++HelpErrs}
+ end,
+
+ case continue(AllMakeErrors) of
+ true ->
+ SavedErrors = save_make_errors(SuiteMakeErrors),
+ ct_repeat:log_loop_info(Args),
+ {Tests1,Skip1} = final_tests(Tests,[],Skip,SavedErrors),
+ R = do_run_test(Tests1, Skip1, Opt1),
+ ct_util:stop(normal),
+ R;
+ false ->
+ io:nl(),
+ ct_util:stop(clean),
+ BadMods =
+ lists:foldr(
+ fun({{_,_},Ms}, Acc) ->
+ Ms ++ lists:foldl(
+ fun(M, Acc1) ->
+ lists:delete(M, Acc1)
+ end, Acc, Ms)
+ end, [], AllMakeErrors),
+ {error,{make_failed,BadMods}}
+ end
+ end
+ end.
+
+%% attempt to compile the modules specified in TestSuites
+auto_compile(TestSuites) ->
+ io:format("~nCommon Test: Running make in test directories...~n"),
+ UserInclude =
+ 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 ||
+ UserInclDir <- UserInclDirs];
+ _ ->
+ []
+ end,
+ SuiteMakeErrors =
+ lists:flatmap(fun({TestDir,Suite} = TS) ->
+ case run_make(suites, TestDir, Suite, UserInclude) of
+ {error,{make_failed,Bad}} ->
+ [{TS,Bad}];
+ {error,_} ->
+ [{TS,[filename:join(TestDir,"*_SUITE")]}];
+ _ ->
+ []
+ end
+ end, TestSuites),
+
+ %% try to compile other modules than SUITEs in the test directories
+ {_,HelpMakeErrors} =
+ lists:foldl(
+ fun({Dir,Suite}, {Done,Failed}) ->
+ case lists:member(Dir, Done) of
+ false ->
+ Failed1 =
+ case run_make(helpmods, Dir, Suite, UserInclude) of
+ {error,{make_failed,BadMods}} ->
+ [{{Dir,all},BadMods}|Failed];
+ {error,_} ->
+ [{{Dir,all},[Dir]}|Failed];
+ _ ->
+ Failed
+ end,
+ {[Dir|Done],Failed1};
+ true -> % already visited
+ {Done,Failed}
+ end
+ end, {[],[]}, TestSuites),
+ {SuiteMakeErrors,lists:reverse(HelpMakeErrors)}.
+
+%% verify that specified test suites exist (if auto compile is disabled)
+verify_suites(TestSuites) ->
+ io:nl(),
+ Verify =
+ fun({Dir,Suite},NotFound) ->
+ case locate_test_dir(Dir, Suite) of
+ {ok,TestDir} ->
+ if Suite == all ->
+ NotFound;
+ true ->
+ Beam = filename:join(TestDir, atom_to_list(Suite)++".beam"),
+ case filelib:is_regular(Beam) of
+ true ->
+ NotFound;
+ false ->
+ Name = filename:join(TestDir, atom_to_list(Suite)),
+ io:format("Suite ~w not found in directory ~s~n",
+ [Suite,TestDir]),
+ [{{Dir,Suite},[Name]} | NotFound]
+ end
+ end;
+ {error,_Reason} ->
+ io:format("Directory ~s is invalid~n", [Dir]),
+ Name = filename:join(Dir, atom_to_list(Suite)),
+ [{{Dir,Suite},[Name]} | NotFound]
+ end
+ end,
+ lists:reverse(lists:foldl(Verify, [], TestSuites)).
+
+
+save_make_errors([]) ->
+ [];
+save_make_errors(Errors) ->
+ Suites = get_bad_suites(Errors,[]),
+ ct_logs:log("MAKE RESULTS",
+ "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)),
+ Errors.
+
+get_bad_suites([{{_TestDir,_Suite},Failed}|Errors], BadSuites) ->
+ get_bad_suites(Errors,BadSuites++Failed);
+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 ->
+ do_run([{TestDir,Suite,Case}], [{step,Opts}]).
+
+
+%%%-----------------------------------------------------------------
+%%% Internal
+suite_tuples([{TestDir,Suites,_} | Tests]) when is_list(Suites) ->
+ lists:map(fun(S) -> {TestDir,S} end, Suites) ++ suite_tuples(Tests);
+suite_tuples([{TestDir,Suite,_} | Tests]) when is_atom(Suite) ->
+ [{TestDir,Suite} | suite_tuples(Tests)];
+suite_tuples([]) ->
+ [].
+
+final_tests([{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,
+ S == S1, TD == TestDir],
+ Final1 = [{TestDir,S,all} || S <- Suites],
+ final_tests(Tests, lists:reverse(Final1)++Final, Skip++Skip1, Bad);
+
+final_tests([{TestDir,all,all}|Tests], Final, Skip, Bad) ->
+ MissingSuites =
+ case lists:keysearch({TestDir,all}, 1, Bad) of
+ {value,{_,Failed}} ->
+ [list_to_atom(filename:basename(F)) || F <- Failed];
+ false ->
+ []
+ end,
+ Missing = [{TestDir,S,"Make failed"} || S <- MissingSuites],
+ Final1 = [{TestDir,all,all}|Final],
+ final_tests(Tests, Final1, Skip++Missing, Bad);
+
+final_tests([{TestDir,Suite,Cases}|Tests],
+ Final, Skip, Bad) when Cases==[]; Cases==all ->
+ final_tests([{TestDir,[Suite],all}|Tests], Final, Skip, Bad);
+
+final_tests([{TestDir,Suite,Cases}|Tests], Final, Skip, Bad) ->
+ case lists:keymember({TestDir,Suite}, 1, Bad) of
+ false ->
+ Do = {TestDir,Suite,Cases},
+ final_tests(Tests, [Do|Final], Skip, Bad);
+ true ->
+ Do = {TestDir,Suite,Cases},
+ Skip1 = Skip ++ [{TestDir,Suite,Cases,"Make failed"}],
+ final_tests(Tests, [Do|Final], Skip1, Bad)
+ end;
+
+final_tests([], Final, Skip, _Bad) ->
+ {lists:reverse(Final),Skip}.
+
+continue([]) ->
+ true;
+continue(_MakeErrors) ->
+ io:nl(),
+ OldGl = group_leader(),
+ case set_group_leader_same_as_shell() of
+ true ->
+ S = self(),
+ io:format("Failed to compile or locate one or more test suites\n"
+ "Press \'c\' to continue or \'a\' to abort.\n"
+ "Will continue in 15 seconds if no answer is given!\n"),
+ Pid = spawn(fun() ->
+ case io:get_line('(c/a) ') of
+ "c\n" ->
+ S ! true;
+ _ ->
+ S ! false
+ end
+ end),
+ group_leader(OldGl, self()),
+ receive R when R==true; R==false ->
+ R
+ after 15000 ->
+ exit(Pid, kill),
+ io:format("... timeout - continuing!!\n"),
+ true
+ end;
+ false -> % no shell process to use
+ true
+ end.
+
+set_group_leader_same_as_shell() ->
+ %%! Locate the shell process... UGLY!!!
+ GS2or3 = fun(P) ->
+ case process_info(P,initial_call) of
+ {initial_call,{group,server,X}} when X == 2 ; X == 3 ->
+ true;
+ _ ->
+ false
+ end
+ end,
+ case [P || P <- processes(), GS2or3(P),
+ true == lists:keymember(shell,1,element(2,process_info(P,dictionary)))] of
+ [GL|_] ->
+ group_leader(GL, self());
+ [] ->
+ false
+ end.
+
+check_and_add([{TestDir0,M,_} | Tests], Added) ->
+ case locate_test_dir(TestDir0, M) of
+ {ok,TestDir} ->
+ case lists:member(TestDir, Added) of
+ true ->
+ check_and_add(Tests, Added);
+ false ->
+ true = code:add_patha(TestDir),
+ check_and_add(Tests, [TestDir|Added])
+ end;
+ {error,_} ->
+ {error,{invalid_directory,TestDir0}}
+ end;
+check_and_add([], _) ->
+ ok.
+
+do_run_test(Tests, Skip, Opt) ->
+ case check_and_add(Tests, []) of
+ ok ->
+ ct_util:set_testdata({stats,{0,0,{0,0}}}),
+ ct_util:set_testdata({cover,undefined}),
+ test_server_ctrl:start_link(local),
+ case lists:keysearch(cover_spec, 1, Opt) of
+ {value,{_,CovData={CovFile,
+ CovNodes,
+ _CovImport,
+ CovExport,
+ #cover{app = CovApp,
+ level = CovLevel,
+ excl_mods = CovExcl,
+ incl_mods = CovIncl,
+ cross = CovCross,
+ src = _CovSrc}}}} ->
+ ct_logs:log("COVER INFO","Using cover specification file: ~s~n"
+ "App: ~w~n"
+ "Cross cover: ~w~n"
+ "Including ~w modules~n"
+ "Excluding ~w modules",
+ [CovFile,CovApp,CovCross,length(CovIncl),length(CovExcl)]),
+
+ %% cover export file will be used for export and import
+ %% between tests so make sure it doesn't exist initially
+ case filelib:is_file(CovExport) of
+ true ->
+ DelResult = file:delete(CovExport),
+ ct_logs:log("COVER INFO",
+ "Warning! Export file ~s already exists. "
+ "Deleting with result: ~p",
+ [CovExport,DelResult]);
+ false ->
+ ok
+ end,
+
+ %% tell test_server which modules should be cover compiled
+ %% note that actual compilation is done when tests start
+ test_server_ctrl:cover(CovApp, CovFile, CovExcl, CovIncl,
+ CovCross, CovExport, CovLevel),
+ %% save cover data (used e.g. to add nodes dynamically)
+ ct_util:set_testdata({cover,CovData}),
+ %% start cover on specified nodes
+ if (CovNodes /= []) and (CovNodes /= undefined) ->
+ ct_logs:log("COVER INFO",
+ "Nodes included in cover session: ~w",
+ [CovNodes]),
+ cover:start(CovNodes);
+ true ->
+ ok
+ end,
+ true;
+ _ ->
+ false
+ end,
+ %% let test_server expand the test tuples and count no of cases
+ {Suites,NoOfCases} = count_test_cases(Tests, Skip),
+ Suites1 = delete_dups(Suites),
+ NoOfTests = length(Tests),
+ NoOfSuites = length(Suites1),
+ ct_util:warn_duplicates(Suites1),
+ {ok,Cwd} = file:get_cwd(),
+ io:format("~nCWD set to: ~p~n", [Cwd]),
+ if NoOfCases == unknown ->
+ io:format("~nTEST INFO: ~w test(s), ~w suite(s)~n~n",
+ [NoOfTests,NoOfSuites]),
+ ct_logs:log("TEST INFO","~w test(s), ~w suite(s)",
+ [NoOfTests,NoOfSuites]);
+ true ->
+ io:format("~nTEST INFO: ~w test(s), ~w case(s) in ~w suite(s)~n~n",
+ [NoOfTests,NoOfCases,NoOfSuites]),
+ ct_logs:log("TEST INFO","~w test(s), ~w case(s) in ~w suite(s)",
+ [NoOfTests,NoOfCases,NoOfSuites])
+ end,
+ ct_event:notify(#event{name=start_info,
+ node=node(),
+ data={NoOfTests,NoOfSuites,NoOfCases}}),
+ CleanUp = add_jobs(Tests, Skip, Opt, []),
+ unlink(whereis(test_server_ctrl)),
+ catch test_server_ctrl:wait_finish(),
+ %% check if last testcase has left a "dead" trace window
+ %% behind, and if so, kill it
+ case ct_util:get_testdata(interpret) of
+ {_What,kill,{TCPid,AttPid}} ->
+ ct_util:kill_attached(TCPid, AttPid);
+ _ ->
+ ok
+ end,
+ lists:foreach(fun(Suite) ->
+ maybe_cleanup_interpret(Suite, Opt)
+ end, CleanUp);
+ Error ->
+ Error
+ end.
+
+delete_dups([S | Suites]) ->
+ Suites1 = lists:delete(S, Suites),
+ [S | delete_dups(Suites1)];
+delete_dups([]) ->
+ [].
+
+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, [], []),
+ {Suites,NoOfCases} = count_test_cases1(length(Tests), 0, [], Ref),
+ erlang:demonitor(Ref),
+ test_server_ctrl:stop_get_totals(),
+ {Suites,NoOfCases}.
+
+count_test_cases1(0, N, Suites, _) ->
+ {lists:flatten(Suites), N};
+count_test_cases1(Jobs, N, Suites, Ref) ->
+ receive
+ {no_of_cases,{Ss,N1}} ->
+ count_test_cases1(Jobs-1, add_known(N,N1), [Ss|Suites], Ref);
+ {'DOWN', Ref, _, _, _} ->
+ {[],0}
+ end.
+
+add_known(unknown, _) ->
+ unknown;
+add_known(_, unknown) ->
+ unknown;
+add_known(N, N1) ->
+ N+N1.
+
+add_jobs([{TestDir,all,_}|Tests], Skip, Opt, CleanUp) ->
+ Name = get_name(TestDir),
+ case catch test_server_ctrl:add_dir_with_skip(Name, TestDir,
+ skiplist(TestDir,Skip)) of
+ {'EXIT',_} ->
+ CleanUp;
+ _ ->
+ wait_for_idle(),
+ add_jobs(Tests, Skip, Opt, CleanUp)
+ end;
+add_jobs([{TestDir,[Suite],all}|Tests], Skip, Opt, CleanUp) when is_atom(Suite) ->
+ add_jobs([{TestDir,Suite,all}|Tests], Skip, Opt, CleanUp);
+add_jobs([{TestDir,Suites,all}|Tests], Skip, Opt, CleanUp) when is_list(Suites) ->
+ Name = get_name(TestDir) ++ ".suites",
+ case catch test_server_ctrl:add_module_with_skip(Name, Suites,
+ skiplist(TestDir,Skip)) of
+ {'EXIT',_} ->
+ CleanUp;
+ _ ->
+ wait_for_idle(),
+ add_jobs(Tests, Skip, Opt, CleanUp)
+ end;
+add_jobs([{TestDir,Suite,all}|Tests], Skip, Opt, CleanUp) ->
+ case maybe_interpret(Suite, all, Opt) of
+ ok ->
+ Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite),
+ case catch test_server_ctrl:add_module_with_skip(Name, [Suite],
+ skiplist(TestDir,Skip)) of
+ {'EXIT',_} ->
+ CleanUp;
+ _ ->
+ wait_for_idle(),
+ add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
+ end;
+ Error ->
+ Error
+ end;
+add_jobs([{TestDir,Suite,[Case]}|Tests], Skip, Opt, CleanUp) when is_atom(Case) ->
+ add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opt, CleanUp);
+add_jobs([{TestDir,Suite,Cases}|Tests], Skip, Opt, CleanUp) when is_list(Cases) ->
+ case maybe_interpret(Suite, Cases, Opt) of
+ ok ->
+ Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite) ++ ".cases",
+ case catch test_server_ctrl:add_cases_with_skip(Name, Suite, Cases,
+ skiplist(TestDir,Skip)) of
+ {'EXIT',_} ->
+ CleanUp;
+ _ ->
+ wait_for_idle(),
+ add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
+ end;
+ Error ->
+ Error
+ end;
+add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opt, CleanUp) when is_atom(Case) ->
+ case maybe_interpret(Suite, Case, Opt) of
+ ok ->
+ Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite) ++ "." ++
+ atom_to_list(Case),
+ case catch test_server_ctrl:add_case_with_skip(Name, Suite, Case,
+ skiplist(TestDir,Skip)) of
+ {'EXIT',_} ->
+ CleanUp;
+ _ ->
+ wait_for_idle(),
+ add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
+ end;
+ Error ->
+ Error
+ end;
+add_jobs([], _, _, CleanUp) ->
+ CleanUp.
+
+wait_for_idle() ->
+ ct_util:update_last_run_index(),
+ Notify = fun(Me) -> Me ! idle end,
+ case catch test_server_ctrl:idle_notify(Notify) of
+ {'EXIT',_} ->
+ error;
+ TSPid ->
+ %% so we don't hang forever if test_server dies
+ Ref = erlang:monitor(process, TSPid),
+ Result = receive
+ idle -> ok;
+ {'DOWN', Ref, _, _, _} -> error
+ end,
+ erlang:demonitor(Ref),
+ ct_util:update_last_run_index(),
+ Result
+ end.
+
+skiplist(Dir, [{Dir,all,Cmt}|Skip]) ->
+ %% we need to turn 'all' into list of modules since
+ %% test_server doesn't do skips on Dir level
+ Ss = filelib:wildcard(filename:join(Dir, "*_SUITE.beam")),
+ [{list_to_atom(filename:basename(S,".beam")),Cmt} || S <- Ss] ++ skiplist(Dir,Skip);
+skiplist(Dir, [{Dir,S,Cmt}|Skip]) ->
+ [{S,Cmt} | skiplist(Dir, Skip)];
+skiplist(Dir, [{Dir,S,C,Cmt}|Skip]) ->
+ [{S,C,Cmt} | skiplist(Dir, Skip)];
+skiplist(Dir, [_|Skip]) ->
+ skiplist(Dir, Skip);
+skiplist(_Dir, []) ->
+ [].
+
+get_name(Dir) ->
+ TestDir =
+ case filename:basename(Dir) of
+ "test" ->
+ filename:dirname(Dir);
+ _ ->
+ Dir
+ end,
+ Base = filename:basename(TestDir),
+ case filename:basename(filename:dirname(TestDir)) of
+ "" ->
+ Base;
+ TopDir ->
+ TopDir ++ "." ++ Base
+ end.
+
+
+run_make(TestDir, Mod, UserInclude) ->
+ run_make(suites, TestDir, Mod, UserInclude).
+
+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) ->
+ case locate_test_dir(TestDir0, Mod) of
+ {ok,TestDir} ->
+ %% send a start_make notification which may suspend
+ %% the process if some other node is compiling files
+ %% in the same directory
+ ct_event:sync_notify(#event{name=start_make,
+ node=node(),
+ 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},
+ {i,XmerlInclude},
+ debug_info],
+ Result =
+ if Mod == all ; Targets == helpmods ->
+ case (catch ct_make:all([noexec])) of
+ {'EXIT',_} = Failure ->
+ Failure;
+ MakeInfo ->
+ FileTest = fun(F, suites) -> is_suite(F);
+ (F, helpmods) -> not is_suite(F);
+ (_, _) -> true end,
+ Files = lists:flatmap(fun({F,out_of_date}) ->
+ case FileTest(F, Targets) of
+ true -> [F];
+ false -> []
+ end;
+ (_) ->
+ []
+ end, MakeInfo),
+ (catch ct_make:files(Files, [load|ErlFlags]))
+ end;
+ true ->
+ (catch ct_make:files([Mod], [load|ErlFlags]))
+ end,
+
+ ok = file:set_cwd(Cwd),
+ %% send finished_make notification
+ ct_event:notify(#event{name=finished_make,
+ node=node(),
+ data=TestDir}),
+ case Result of
+ {up_to_date,_} ->
+ ok;
+ {'EXIT',Reason} ->
+ io:format("{error,{make_crashed,~p}\n", [Reason]),
+ {error,{make_crashed,TestDir,Reason}};
+ {error,ModInfo} ->
+ io:format("{error,make_failed}\n", []),
+ Bad = [filename:join(TestDir, M) || {M,R} <- ModInfo,
+ R == error],
+ {error,{make_failed,Bad}}
+ end;
+ {error,_} ->
+ io:format("{error,{invalid_directory,~p}}\n", [TestDir0]),
+ {error,{invalid_directory,TestDir0}}
+ end.
+
+get_dir(App, Dir) ->
+ filename:join(code:lib_dir(App), Dir).
+
+maybe_interpret(Suite, Cases, [{step,StepOpts}]) ->
+ %% if other suite has run before this one, check if last testcase
+ %% has left a "dead" trace window behind, and if so, kill it
+ case ct_util:get_testdata(interpret) of
+ {_What,kill,{TCPid,AttPid}} ->
+ ct_util:kill_attached(TCPid, AttPid);
+ _ ->
+ ok
+ end,
+ maybe_interpret1(Suite, Cases, StepOpts);
+maybe_interpret(_, _, _) ->
+ ok.
+
+maybe_interpret1(Suite, all, StepOpts) ->
+ case i:ii(Suite) of
+ {module,_} ->
+ i:iaa([break]),
+ case get_all_testcases(Suite) of
+ {error,_} ->
+ {error,no_testcases_found};
+ Cases ->
+ maybe_interpret2(Suite, Cases, StepOpts)
+ end;
+ error ->
+ {error,could_not_interpret_module}
+ end;
+maybe_interpret1(Suite, Case, StepOpts) when is_atom(Case) ->
+ maybe_interpret1(Suite, [Case], StepOpts);
+maybe_interpret1(Suite, Cases, StepOpts) when is_list(Cases) ->
+ case i:ii(Suite) of
+ {module,_} ->
+ i:iaa([break]),
+ maybe_interpret2(Suite, Cases, StepOpts);
+ error ->
+ {error,could_not_interpret_module}
+ end.
+
+maybe_interpret2(Suite, Cases, StepOpts) ->
+ set_break_on_config(Suite, StepOpts),
+ [i:ib(Suite, Case, 1) || Case <- Cases],
+ test_server_ctrl:multiply_timetraps(infinity),
+ WinOp = case lists:member(keep_inactive, ensure_atom(StepOpts)) of
+ true -> no_kill;
+ false -> kill
+ end,
+ ct_util:set_testdata({interpret,{{Suite,Cases},WinOp,
+ {undefined,undefined}}}),
+ ok.
+
+set_break_on_config(Suite, StepOpts) ->
+ case lists:member(config, ensure_atom(StepOpts)) of
+ true ->
+ i:ib(Suite, init_per_suite, 1),
+ i:ib(Suite, init_per_testcase, 2),
+ i:ib(Suite, end_per_testcase, 2),
+ i:ib(Suite, end_per_suite, 1);
+ false ->
+ ok
+ end.
+
+maybe_cleanup_interpret(Suite, [{step,_}]) ->
+ i:iq(Suite);
+maybe_cleanup_interpret(_, _) ->
+ ok.
+
+log_ts_names(Args) ->
+ case lists:keysearch(spec, 1, Args) of
+ {value,{_,Specs}} ->
+ List = lists:map(fun(Name) ->
+ Name ++ " "
+ end, Specs),
+ ct_logs:log("Test Specification file(s)", "~s",
+ [lists:flatten(List)]);
+ _ ->
+ ok
+ end.
+
+merge_arguments(Args) ->
+ merge_arguments(Args, []).
+
+merge_arguments([LogDir={logdir,_}|Args], Merged) ->
+ merge_arguments(Args, handle_arg(replace, LogDir, Merged));
+merge_arguments([CoverFile={cover,_}|Args], Merged) ->
+ merge_arguments(Args, handle_arg(replace, CoverFile, Merged));
+merge_arguments([Arg={_,_}|Args], Merged) ->
+ merge_arguments(Args, handle_arg(merge, Arg, Merged));
+merge_arguments([], Merged) ->
+ Merged.
+
+handle_arg(replace, {Key,Elems}, [{Key,_}|Merged]) ->
+ [{Key,Elems}|Merged];
+handle_arg(merge, {Key,Elems}, [{Key,PrevElems}|Merged]) ->
+ [{Key,PrevElems++Elems}|Merged];
+handle_arg(Op, Arg, [Other|Merged]) ->
+ [Other|handle_arg(Op, Arg, Merged)];
+handle_arg(_,Arg,[]) ->
+ [Arg].
+
+locate_test_dir(Dir, Suite) ->
+ TestDir = case ct_util:is_test_dir(Dir) of
+ true -> Dir;
+ false -> ct_util:get_testdir(Dir, Suite)
+ end,
+ case filelib:is_dir(TestDir) of
+ true -> {ok,TestDir};
+ false -> {error,invalid}
+ end.
+
+is_suite(Mod) when is_atom(Mod) ->
+ is_suite(atom_to_list(Mod));
+is_suite(ModOrFile) when is_list(ModOrFile) ->
+ case lists:reverse(filename:basename(ModOrFile, ".erl")) of
+ [$E,$T,$I,$U,$S,$_|_] ->
+ true;
+ _ ->
+ case lists:reverse(filename:basename(ModOrFile, ".beam")) of
+ [$E,$T,$I,$U,$S,$_|_] ->
+ true;
+ _ ->
+ false
+ end
+ end.
+
+get_all_testcases(Suite) ->
+ %%! this needs to be updated to handle testcase groups later!!
+ case catch Suite:all() of
+ {'EXIT',Why} ->
+ {error,Why};
+ {skip,_} ->
+ [];
+ Cases ->
+ AllCases =
+ lists:foldl(fun({sequence,SeqName}, All) ->
+ case catch Suite:sequences() of
+ {'EXIT',_} ->
+ All;
+ Seqs ->
+ case proplists:get_value(SeqName, Seqs) of
+ undefined ->
+ All;
+ SeqCases ->
+ lists:reverse(SeqCases) ++ All
+ end
+ end;
+ (Case,All) ->
+ [Case|All]
+ end, [], Cases),
+ lists:reverse(AllCases)
+ end.
+
+
+%% Internal tracing support. If {ct_trace,TraceSpec} is present, the
+%% TraceSpec file will be consulted and dbg used to trace function
+%% calls during test run. Expected terms in TraceSpec:
+%% {m,Mod} or {f,Mod,Func}.
+start_trace(Args) ->
+ case lists:keysearch(ct_trace,1,Args) of
+ {value,{ct_trace,File}} ->
+ TraceSpec = delistify(File),
+ case file:consult(TraceSpec) of
+ {ok,Terms} ->
+ case catch do_trace(Terms) of
+ ok ->
+ true;
+ {_,Error} ->
+ io:format("Warning! Tracing not started. Reason: ~p~n~n",
+ [Error]),
+ false
+ end;
+ {_,Error} ->
+ io:format("Warning! Tracing not started. Reason: ~p~n~n",
+ [Error]),
+ false
+ end;
+ false ->
+ false
+ end.
+
+do_trace(Terms) ->
+ dbg:tracer(),
+ 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.
+
+stop_trace(true) ->
+ dbg:stop_clear();
+stop_trace(false) ->
+ ok.
+
+preload() ->
+ io:format("~nLoading Common Test and Test Server modules...~n~n"),
+ preload_mod([ct_logs,
+ ct_make,
+ ct_telnet,
+ ct,
+ ct_master,
+ ct_testspec,
+ ct_cover,
+ ct_master_event,
+ ct_util,
+ ct_event,
+ ct_master_logs,
+ ct_framework,
+ teln,
+ ct_ftp,
+ ct_rpc,
+ unix_telnet,
+ ct_gen_conn,
+ ct_line,
+ ct_snmp,
+ test_server_sup,
+ test_server,
+ test_server_ctrl,
+ test_server_h,
+ test_server_line,
+ test_server_node]).
+
+preload_mod([M|Ms]) ->
+ case code:is_loaded(M) of
+ false ->
+ {module,M} = code:load_file(M),
+ preload_mod(Ms);
+ _ ->
+ ok
+ end;
+preload_mod([]) ->
+ ok.
+
+ensure_atom(Atom) when is_atom(Atom) ->
+ Atom;
+ensure_atom(String) when is_list(String), is_integer(hd(String)) ->
+ list_to_atom(String);
+ensure_atom(List) when is_list(List) ->
+ [ensure_atom(Item) || Item <- List];
+ensure_atom(Other) ->
+ Other.
+
diff --git a/lib/common_test/src/ct_snmp.erl b/lib/common_test/src/ct_snmp.erl
new file mode 100644
index 0000000000..7ff88ad7d3
--- /dev/null
+++ b/lib/common_test/src/ct_snmp.erl
@@ -0,0 +1,771 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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() = string()
+%%% @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").
+
+%%% API
+-export([start/2, start/3, stop/1, get_values/3, get_next_values/3, set_values/4,
+ set_info/1, register_users/2, register_agents/2, register_usm_users/2,
+ unregister_users/1, unregister_agents/1, update_usm_users/2,
+ load_mibs/1]).
+
+%% Manager values
+-define(CT_SNMP_LOG_FILE, "ct_snmp_set.log").
+-define(MGR_PORT, 5000).
+-define(MAX_MSG_SIZE, 484).
+-define(ENGINE_ID, "mgrEngine").
+
+%% Agent values
+-define(AGENT_ENGINE_ID, "agentEngine").
+-define(TRAP_UDP, 5000).
+-define(AGENT_UDP, 4000).
+-define(CONF_FILE_VER, [v2]).
+-define(AGENT_MAX_MSG_SIZE, 484).
+-define(AGENT_NOTIFY_TYPE, trap).
+-define(AGENT_SEC_TYPE, none).
+-define(AGENT_PASSWD, "").
+%%%=========================================================================
+%%% 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),
+
+ SysName = ct:get_config({MgrAgentConfName, agent_sysname}, "ct_test"),
+ {ok, HostName} = inet:gethostname(),
+ {ok, Addr} = inet:getaddr(HostName, inet),
+ IP = tuple_to_list(Addr),
+ AgentManagerIP = ct:get_config({MgrAgentConfName, agent_manager_ip}, IP),
+
+ prepare_snmp_env(),
+ setup_agent(StartAgent, MgrAgentConfName, SnmpAppConfName,
+ Config, SysName, AgentManagerIP, IP),
+ setup_manager(StartManager, MgrAgentConfName, SnmpAppConfName,
+ Config, AgentManagerIP),
+ application:start(snmp),
+
+ manager_register(StartManager, MgrAgentConfName).
+
+%%% @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),
+ MgrDir = filename:join(PrivDir,"mgr"),
+ ConfDir = filename:join(PrivDir, "conf"),
+ DbDir = filename:join(PrivDir,"db"),
+ catch del_dir(MgrDir),
+ catch del_dir(ConfDir),
+ 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, AgentIp, AgentUdpPort | _] =
+ agent_conf(Agent, MgrAgentConfName),
+ {ok, SnmpReply, _} =
+ snmpm:g(Uid, AgentIp, AgentUdpPort, 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, AgentIp, AgentUdpPort | _] =
+ agent_conf(Agent, MgrAgentConfName),
+ {ok, SnmpReply, _} =
+ snmpm:gn(Uid, AgentIp, AgentUdpPort, 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, AgentIp, AgentUdpPort | _] =
+ agent_conf(Agent, MgrAgentConfName),
+ Oids = lists:map(fun({Oid, _, _}) -> Oid end, VarsAndVals),
+ {ok, SnmpGetReply, _} =
+ snmpm:g(Uid, AgentIp, AgentUdpPort, Oids),
+ {ok, SnmpSetReply, _} =
+ snmpm:s(Uid, AgentIp, AgentUdpPort, VarsAndVals),
+ case SnmpSetReply of
+ {noError, 0, _} when PrivDir /= false ->
+ log(PrivDir, Agent, SnmpGetReply, VarsAndVals);
+ _ ->
+ set_failed_or_user_did_not_want_to_log
+ 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),
+ 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
+register_users(MgrAgentConfName, Users) ->
+ {snmp, SnmpVals} = ct:get_config(MgrAgentConfName),
+ NewSnmpVals = lists:keyreplace(users, 1, SnmpVals, {users, Users}),
+ ct_util:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
+ setup_users(Users).
+
+%%% @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
+register_agents(MgrAgentConfName, ManagedAgents) ->
+ {snmp, SnmpVals} = ct:get_config(MgrAgentConfName),
+ NewSnmpVals = lists:keyreplace(managed_agents, 1, SnmpVals,
+ {managed_agents, ManagedAgents}),
+ ct_util:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
+ setup_managed_agents(ManagedAgents).
+
+%%% @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
+register_usm_users(MgrAgentConfName, UsmUsers) ->
+ {snmp, SnmpVals} = ct:get_config(MgrAgentConfName),
+ NewSnmpVals = lists:keyreplace(users, 1, SnmpVals, {usm_users, UsmUsers}),
+ ct_util:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
+ EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID),
+ setup_usm_users(UsmUsers, EngineID).
+
+%%% @spec unregister_users(MgrAgentConfName) -> ok | {error, Reason}
+%%%
+%%% MgrAgentConfName = atom()
+%%% Reason = term()
+%%%
+%%% @doc Removes information added when calling register_users/2.
+unregister_users(MgrAgentConfName) ->
+ Users = lists:map(fun({UserName, _}) -> UserName end,
+ ct:get_config({MgrAgentConfName, users})),
+ {snmp, SnmpVals} = ct:get_config(MgrAgentConfName),
+ NewSnmpVals = lists:keyreplace(users, 1, SnmpVals, {users, []}),
+ ct_util:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
+ takedown_users(Users).
+
+%%% @spec unregister_agents(MgrAgentConfName) -> ok | {error, Reason}
+%%%
+%%% MgrAgentConfName = atom()
+%%% Reason = term()
+%%%
+%%% @doc Removes information added when calling register_agents/2.
+unregister_agents(MgrAgentConfName) ->
+ ManagedAgents = lists:map(fun({_, [Uid, AgentIP, AgentPort, _]}) ->
+ {Uid, AgentIP, AgentPort}
+ end,
+ ct:get_config({MgrAgentConfName, managed_agents})),
+ {snmp, SnmpVals} = ct:get_config(MgrAgentConfName),
+ NewSnmpVals = lists:keyreplace(managed_agents, 1, SnmpVals,
+ {managed_agents, []}),
+ ct_util:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
+ takedown_managed_agents(ManagedAgents).
+
+
+%%% @spec update_usm_users(MgrAgentConfName, UsmUsers) -> ok | {error, Reason}
+%%%
+%%% MgrAgentConfName = atom()
+%%% UsmUsers = usm_users()
+%%% Reason = term()
+%%%
+%%% @doc Alters information added when calling register_usm_users/2.
+update_usm_users(MgrAgentConfName, UsmUsers) ->
+
+ {snmp, SnmpVals} = ct:get_config(MgrAgentConfName),
+ NewSnmpVals = lists:keyreplace(usm_users, 1, SnmpVals,
+ {usm_users, UsmUsers}),
+ ct_util:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
+ EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID),
+ do_update_usm_users(UsmUsers, EngineID).
+
+%%% @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).
+
+
+%%%========================================================================
+%%% Internal functions
+%%%========================================================================
+prepare_snmp_env() ->
+ %% To make sure application:set_env is not overwritten by any
+ %% app-file settings.
+ application:load(snmp),
+
+ %% Fix for older versions of snmp where there are some
+ %% inappropriate default values for alway starting an
+ %% agent.
+ application:unset_env(snmp, agent).
+%%%---------------------------------------------------------------------------
+setup_manager(false, _, _, _, _) ->
+ ok;
+setup_manager(true, MgrConfName, SnmpConfName, Config, IP) ->
+ PrivDir = ?config(priv_dir, Config),
+ MaxMsgSize = ct:get_config({MgrConfName,max_msg_size}, ?MAX_MSG_SIZE),
+ Port = ct:get_config({MgrConfName,mgr_port}, ?MGR_PORT),
+ EngineID = ct:get_config({MgrConfName,engine_id}, ?ENGINE_ID),
+ MgrDir = filename:join(PrivDir,"mgr"),
+ %%% Users, Agents and Usms are in test suites register after the
+ %%% snmp application is started.
+ Users = [],
+ Agents = [],
+ Usms = [],
+ file:make_dir(MgrDir),
+
+ snmp_config:write_manager_snmp_files(MgrDir, IP, Port, MaxMsgSize,
+ EngineID, Users, Agents, Usms),
+ SnmpEnv = merge_snmp_conf([{config, [{dir, MgrDir},{db_dir, MgrDir},
+ {verbosity, trace}]},
+ {server, [{verbosity, trace}]},
+ {net_if, [{verbosity, trace}]},
+ {versions, [v1, v2, v3]}],
+ ct:get_config({SnmpConfName,manager})),
+ application:set_env(snmp, manager, SnmpEnv).
+%%%---------------------------------------------------------------------------
+setup_agent(false,_, _, _, _, _, _) ->
+ ok;
+setup_agent(true, AgentConfName, SnmpConfName,
+ Config, SysName, ManagerIP, AgentIP) ->
+ application:start(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),
+ AgentUdp = ct:get_config({AgentConfName, agent_udp}, ?AGENT_UDP),
+ NotifType = ct:get_config({AgentConfName, agent_notify_type},
+ ?AGENT_NOTIFY_TYPE),
+ SecType = ct:get_config({AgentConfName, agent_sec_type}, ?AGENT_SEC_TYPE),
+ Passwd = ct:get_config({AgentConfName, agent_passwd}, ?AGENT_PASSWD),
+ AgentEngineID = ct:get_config({AgentConfName, agent_engine_id},
+ ?AGENT_ENGINE_ID),
+ AgentMaxMsgSize = ct:get_config({AgentConfName, agent_max_msg_size},
+ ?MAX_MSG_SIZE),
+
+ ConfDir = filename:join(PrivDir, "conf"),
+ DbDir = filename:join(PrivDir,"db"),
+ file:make_dir(ConfDir),
+ file:make_dir(DbDir),
+ snmp_config:write_agent_snmp_files(ConfDir, Vsns, ManagerIP, TrapUdp,
+ AgentIP, AgentUdp, SysName,
+ atom_to_list(NotifType),
+ SecType, Passwd, AgentEngineID,
+ AgentMaxMsgSize),
+
+ override_default_configuration(Config, AgentConfName),
+
+ SnmpEnv = merge_snmp_conf([{db_dir, DbDir},
+ {config, [{dir, ConfDir},
+ {verbosity, trace}]},
+ {agent_type, master},
+ {agent_verbosity, trace},
+ {net_if, [{verbosity, trace}]}],
+ ct:get_config({SnmpConfName,agent})),
+ application:set_env(snmp, agent, SnmpEnv).
+%%%---------------------------------------------------------------------------
+merge_snmp_conf(Defaults, undefined) ->
+ Defaults;
+merge_snmp_conf([Def={Key,DefList=[P|_]}|DefParams], UserParams) when is_tuple(P) ->
+ case lists:keysearch(Key, 1, UserParams) of
+ false ->
+ [Def | merge_snmp_conf(DefParams, UserParams)];
+ {value,{Key,UserList}} ->
+ DefList1 = [{SubKey,Val} || {SubKey,Val} <- DefList,
+ lists:keysearch(SubKey, 1, UserList) == false],
+ [{Key,DefList1++UserList} | merge_snmp_conf(DefParams,
+ lists:keydelete(Key, 1, UserParams))]
+ end;
+merge_snmp_conf([Def={Key,_}|DefParams], UserParams) ->
+ case lists:keysearch(Key, 1, UserParams) of
+ false ->
+ [Def | merge_snmp_conf(DefParams, UserParams)];
+ {value,_} ->
+ merge_snmp_conf(DefParams, UserParams)
+ end;
+merge_snmp_conf([], UserParams) ->
+ UserParams.
+
+
+%%%---------------------------------------------------------------------------
+manager_register(false, _) ->
+ ok;
+manager_register(true, MgrAgentConfName) ->
+ Agents = ct:get_config({MgrAgentConfName, managed_agents}, []),
+ Users = ct:get_config({MgrAgentConfName, users}, []),
+ UsmUsers = ct:get_config({MgrAgentConfName, usm_users}, []),
+ EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID),
+
+ setup_usm_users(UsmUsers, EngineID),
+ setup_users(Users),
+ setup_managed_agents(Agents).
+
+%%%---------------------------------------------------------------------------
+setup_users(Users) ->
+ lists:foreach(fun({Id, [Module, Data]}) ->
+ snmpm:register_user(Id, Module, Data)
+ end, Users).
+%%%---------------------------------------------------------------------------
+setup_managed_agents([]) ->
+ ok;
+
+setup_managed_agents([{_, [Uid, AgentIp, AgentUdpPort, AgentConf]} |
+ Rest]) ->
+ NewAgentIp = case AgentIp of
+ IpTuple when is_tuple(IpTuple) ->
+ IpTuple;
+ HostName when is_list(HostName) ->
+ {ok,Hostent} = inet:gethostbyname(HostName),
+ [IpTuple|_] = Hostent#hostent.h_addr_list,
+ IpTuple
+ end,
+ ok = snmpm:register_agent(Uid, NewAgentIp, AgentUdpPort),
+ lists:foreach(fun({Item, Val}) ->
+ snmpm:update_agent_info(Uid, NewAgentIp,
+ AgentUdpPort, Item, Val)
+ end, AgentConf),
+ setup_managed_agents(Rest).
+%%%---------------------------------------------------------------------------
+setup_usm_users(UsmUsers, EngineID)->
+ lists:foreach(fun({UsmUser, Conf}) ->
+ snmpm:register_usm_user(EngineID, UsmUser, Conf)
+ end, UsmUsers).
+%%%---------------------------------------------------------------------------
+takedown_users(Users) ->
+ lists:foreach(fun({Id}) ->
+ snmpm:unregister_user(Id)
+ end, Users).
+%%%---------------------------------------------------------------------------
+takedown_managed_agents([{Uid, AgentIp, AgentUdpPort} |
+ Rest]) ->
+ NewAgentIp = case AgentIp of
+ IpTuple when is_tuple(IpTuple) ->
+ IpTuple;
+ HostName when is_list(HostName) ->
+ {ok,Hostent} = inet:gethostbyname(HostName),
+ [IpTuple|_] = Hostent#hostent.h_addr_list,
+ IpTuple
+ end,
+ ok = snmpm:unregister_agent(Uid, NewAgentIp, AgentUdpPort),
+ takedown_managed_agents(Rest);
+
+takedown_managed_agents([]) ->
+ ok.
+%%%---------------------------------------------------------------------------
+do_update_usm_users(UsmUsers, EngineID) ->
+ lists:foreach(fun({UsmUser, {Item, Val}}) ->
+ snmpm:update_usm_user_info(EngineID, UsmUser,
+ Item, Val)
+ end, UsmUsers).
+%%%---------------------------------------------------------------------------
+log(PrivDir, Agent, {_, _, Varbinds}, NewVarsAndVals) ->
+
+ Fun = fun(#varbind{oid = Oid, variabletype = Type, value = Value}) ->
+ {Oid, Type, Value}
+ end,
+ OldVarsAndVals = lists:map(Fun, Varbinds),
+
+ 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.
+%%%---------------------------------------------------------------------------
+del_dir(Dir) ->
+ {ok, Files} = file:list_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.
+%%%---------------------------------------------------------------------------
+agent_conf(Agent, MgrAgentConfName) ->
+ Agents = ct:get_config({MgrAgentConfName, managed_agents}),
+ case lists:keysearch(Agent, 1, Agents) of
+ {value, {Agent, AgentConf}} ->
+ AgentConf;
+ _ ->
+ exit({error, {unknown_agent, Agent, Agents}})
+ end.
+%%%---------------------------------------------------------------------------
+override_default_configuration(Config, MgrAgentConfName) ->
+ override_contexts(Config,
+ ct:get_config({MgrAgentConfName, agent_contexts}, undefined)),
+ override_community(Config,
+ ct:get_config({MgrAgentConfName, agent_community}, undefined)),
+ override_sysinfo(Config,
+ ct:get_config({MgrAgentConfName, agent_sysinfo}, undefined)),
+ override_vacm(Config,
+ ct:get_config({MgrAgentConfName, agent_vacm}, undefined)),
+ override_usm(Config,
+ ct:get_config({MgrAgentConfName, agent_usm}, undefined)),
+ override_notify(Config,
+ ct:get_config({MgrAgentConfName, agent_notify_def}, undefined)),
+ override_target_address(Config,
+ ct:get_config({MgrAgentConfName,
+ agent_target_address_def},
+ undefined)),
+ override_target_params(Config,
+ ct:get_config({MgrAgentConfName, agent_target_param_def},
+ undefined)).
+
+%%%---------------------------------------------------------------------------
+override_contexts(_, undefined) ->
+ ok;
+
+override_contexts(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, ContextInfo} = file:consult(FullPathFile),
+ override_contexts(Config, ContextInfo);
+
+override_contexts(Config, Contexts) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"context.conf"),
+ file:delete(File),
+ snmp_config:write_agent_context_config(Dir, "", Contexts).
+
+%%%---------------------------------------------------------------------------
+override_sysinfo(_, undefined) ->
+ ok;
+
+override_sysinfo(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, SysInfo} = file:consult(FullPathFile),
+ override_sysinfo(Config, SysInfo);
+
+override_sysinfo(Config, SysInfo) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"standard.conf"),
+ file:delete(File),
+ snmp_config:write_agent_standard_config(Dir, "", SysInfo).
+
+%%%---------------------------------------------------------------------------
+override_target_address(_, undefined) ->
+ ok;
+override_target_address(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, TargetAddressConf} = file:consult(FullPathFile),
+ override_target_address(Config, TargetAddressConf);
+
+override_target_address(Config, TargetAddressConf) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"target_addr.conf"),
+ file:delete(File),
+ snmp_config:write_agent_target_addr_config(Dir, "", TargetAddressConf).
+
+
+%%%---------------------------------------------------------------------------
+override_target_params(_, undefined) ->
+ ok;
+override_target_params(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, TargetParamsConf} = file:consult(FullPathFile),
+ override_target_params(Config, TargetParamsConf);
+
+override_target_params(Config, TargetParamsConf) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"target_params.conf"),
+ file:delete(File),
+ snmp_config:write_agent_target_params_config(Dir, "", TargetParamsConf).
+
+%%%---------------------------------------------------------------------------
+override_notify(_, undefined) ->
+ ok;
+override_notify(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, NotifyConf} = file:consult(FullPathFile),
+ override_notify(Config, NotifyConf);
+
+override_notify(Config, NotifyConf) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"notify.conf"),
+ file:delete(File),
+ snmp_config:write_agent_notify_config(Dir, "", NotifyConf).
+
+%%%---------------------------------------------------------------------------
+override_usm(_, undefined) ->
+ ok;
+override_usm(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, UsmConf} = file:consult(FullPathFile),
+ override_usm(Config, UsmConf);
+
+override_usm(Config, UsmConf) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"usm.conf"),
+ file:delete(File),
+ snmp_config:write_agent_usm_config(Dir, "", UsmConf).
+
+%%%--------------------------------------------------------------------------
+override_community(_, undefined) ->
+ ok;
+override_community(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, CommunityConf} = file:consult(FullPathFile),
+ override_community(Config, CommunityConf);
+
+override_community(Config, CommunityConf) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"community.conf"),
+ file:delete(File),
+ snmp_config:write_agent_community_config(Dir, "", CommunityConf).
+
+%%%---------------------------------------------------------------------------
+
+override_vacm(_, undefined) ->
+ ok;
+override_vacm(Config, {data_dir_file, File}) ->
+ Dir = ?config(data_dir, Config),
+ FullPathFile = filename:join(Dir, File),
+ {ok, VacmConf} = file:consult(FullPathFile),
+ override_vacm(Config, VacmConf);
+
+override_vacm(Config, VacmConf) ->
+ Dir = ?config(priv_dir, Config),
+ File = filename:join(Dir,"vacm.conf"),
+ file:delete(File),
+ snmp_config:write_agent_vacm_config(Dir, "", VacmConf).
diff --git a/lib/common_test/src/ct_ssh.erl b/lib/common_test/src/ct_ssh.erl
new file mode 100644
index 0000000000..f2b25b1fcd
--- /dev/null
+++ b/lib/common_test/src/ct_ssh.erl
@@ -0,0 +1,1346 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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
+-export([connect/1, connect/2, connect/3,
+ disconnect/1,
+ session_open/1, session_open/2,
+ session_close/2,
+ send/3, send/4, send/5,
+ receive_response/2, receive_response/3, receive_response/4,
+ 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]).
+
+%% STFP Functions
+-export([sftp_connect/1,
+
+ read_file/2, write_file/3, list_dir/2, open/3, opendir/2,
+ close/2, read/3, pread/4, aread/3, apread/4, write/3,
+ pwrite/4, awrite/3, apwrite/4, position/3, read_file_info/2,
+ get_file_info/2, read_link_info/2, write_file_info/3,
+ read_link/2, make_symlink/3, rename/3, delete/2, make_dir/2,
+ del_dir/2,
+
+ read_file/3, write_file/4, list_dir/3, open/4, opendir/3,
+ close/3, read/4, pread/5, aread/4, apread/5, write/4,
+ pwrite/5, awrite/4, apwrite/5, position/4, read_file_info/3,
+ get_file_info/3, read_link_info/3, write_file_info/4,
+ read_link/3, make_symlink/4, rename/4, delete/3, make_dir/3,
+ del_dir/3]).
+
+%% Callbacks
+-export([init/3, handle_msg/2, reconnect/2, terminate/2, close/1]).
+
+-define(DEFAULT_TIMEOUT, 10000).
+
+-record(state, {ssh_ref, conn_type, target}).
+
+
+%%%-----------------------------------------------------------------
+%%%------------------------ 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>).</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>
+connect(KeyOrName, ConnType, ExtraOpts) ->
+ case ct:get_config(KeyOrName) of
+ undefined ->
+ log(heading(connect,KeyOrName), "Failed: ~p\n",
+ [{not_available,KeyOrName}]),
+ {error,{not_available,KeyOrName}};
+ SSHData ->
+ AllOpts = ExtraOpts++SSHData,
+ {ConnType1,Addr,AllOpts1} =
+ case ConnType of
+ host ->
+ case proplists:get_value(ssh, AllOpts) of
+ undefined ->
+ case proplists:get_value(sftp, AllOpts) of
+ undefined ->
+ log(heading(connect,KeyOrName),
+ "No host information specified!\n",[]);
+ SFTPAddr ->
+ {sftp,SFTPAddr,AllOpts}
+ end;
+ SSHAddr ->
+ {ssh,SSHAddr,AllOpts}
+ end;
+ _ ->
+ case proplists:get_value(ConnType, AllOpts) of
+ undefined when ConnType == ssh ->
+ case proplists:get_value(sftp, AllOpts) of
+ undefined ->
+ {ssh,undefined,AllOpts};
+ SFTPAddr ->
+ log(heading(connect,KeyOrName),
+ "Note: Opening ssh connection to sftp host.\n",
+ []),
+ {ssh,SFTPAddr,
+ [{ssh,SFTPAddr}|proplists:delete(sftp, AllOpts)]}
+ end;
+ undefined when ConnType == sftp ->
+ case proplists:get_value(ssh, AllOpts) of
+ undefined ->
+ {sftp,undefined,AllOpts};
+ SSHAddr ->
+ log(heading(connect,KeyOrName),
+ "Note: Opening sftp connection to ssh host.\n",
+ []),
+ {sftp,SSHAddr,
+ [{sftp,SSHAddr}|proplists:delete(ssh, AllOpts)]}
+ end;
+ SSHorSFTPAddr ->
+ {ConnType,SSHorSFTPAddr,AllOpts}
+ end
+ end,
+ case {Addr,proplists:get_value(port, AllOpts1)} of
+ {undefined,_} ->
+ log(heading(connect,KeyOrName), "Failed: ~p\n",
+ [{not_available,{KeyOrName,ConnType1}}]),
+ {error,{not_available,{KeyOrName,ConnType1}}};
+ {_,undefined} ->
+ log(heading(connect,KeyOrName),
+ "Opening ~w connection to ~p:22\n",
+ [ConnType1,Addr]),
+ ct_gen_conn:start(KeyOrName, {ConnType1,Addr,22},
+ AllOpts1, ?MODULE);
+ {_,Port} ->
+ log(heading(connect,KeyOrName),
+ "Opening ~w connection to ~p:~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} ->
+ log(heading(disconnect,SSH), "Handle: ~p", [Pid]),
+ case ct_gen_conn:stop(Pid) of
+ {error,{process_down,Pid,noproc}} ->
+ {error,already_closed};
+ Result ->
+ Result
+ end;
+ Error ->
+ 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}).
+
+
+%%%-----------------------------------------------------------------
+%%%------------------------ 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}).
+
+
+%%%=================================================================
+%%% Callback functions
+%%%=================================================================
+
+%% @hidden
+init(KeyOrName, {ConnType,Addr,Port}, AllOpts) ->
+ User = proplists:get_value(user, AllOpts),
+ Password = case proplists:get_value(password, AllOpts) of
+ undefined -> "";
+ Pwd -> Pwd
+ end,
+ AllOpts1 = case proplists:get_value(connect_timeout, AllOpts) of
+ undefined ->
+ [{connect_timeout,?DEFAULT_TIMEOUT}|AllOpts];
+ _ ->
+ AllOpts
+ end,
+ Options =
+ lists:foldl(fun({ssh,_},Opts) -> Opts;
+ ({sftp,_},Opts) -> Opts;
+ ({port,_},Opts) -> Opts;
+ ({silently_accept_hosts,_},Opts) -> Opts;
+ ({user_interaction,_},Opts) -> Opts;
+ (Opt={Key,_},Opts) ->
+ case lists:keymember(Key, 1, Opts) of
+ true -> Opts;
+ false -> [Opt|Opts]
+ end;
+ (_,Opts) -> Opts
+ end, [], AllOpts1),
+ FinalOptions = [{silently_accept_hosts,true},
+ {user_interaction,false} | Options],
+ crypto:start(),
+ ssh:start(),
+ Result = case ConnType of
+ ssh ->
+ ssh:connect(Addr, Port, FinalOptions);
+ sftp ->
+ ssh_sftp:connect(Addr, Port, FinalOptions)
+ end,
+ case Result of
+ {ok,SSHRef} ->
+ log(heading(init,KeyOrName),
+ "Opened ~w connection:\nHost: ~p (~p)\nUser: ~p\nPassword: ~p\n",
+ [ConnType,Addr,Port,User,lists:duplicate(length(Password),$*)]),
+ {ok,SSHRef,#state{ssh_ref=SSHRef, conn_type=ConnType,
+ target=KeyOrName}};
+ Error ->
+ Error
+ end.
+
+%% @hidden
+handle_msg(sftp_connect, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ log(heading(sftp_connect,Target), "SSH Ref: ~p", [SSHRef]),
+ {ssh_sftp:connect(SSHRef),State};
+
+handle_msg({session_open,TO}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ log(heading(session_open,Target), "SSH Ref: ~p, Timeout: ~p", [SSHRef,TO]),
+ {ssh_connection:session_channel(SSHRef, TO),State};
+
+handle_msg({session_close,Chn}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ log(heading(session_close,Target), "SSH Ref: ~p, Chn: ~p", [SSHRef,Chn]),
+ {ssh_connection:close(SSHRef, Chn),State};
+
+handle_msg({exec,Chn,Command,TO}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ Chn1 =
+ if Chn == undefined ->
+ log(heading(exec,Target),
+ "Opening channel for exec, SSH Ref: ~p", [SSHRef]),
+ case ssh_connection:session_channel(SSHRef, TO) of
+ {ok,C} -> C;
+ CErr -> CErr
+ end;
+ true ->
+ Chn
+ end,
+ case Chn1 of
+ {error,_} = ChnError ->
+ log(heading(exec,Target), "Opening channel failed: ~p", [ChnError]),
+ {ChnError,State};
+ _ ->
+ log(heading(exec,Target),
+ "SSH Ref: ~p, Chn: ~p, Command: ~p, Timeout: ~p",
+ [SSHRef,Chn1,Command,TO]),
+ case ssh_connection:exec(SSHRef, Chn1, Command, TO) of
+ success ->
+ Result = do_recv_response(SSHRef, Chn1, [], close, TO),
+ ssh_connection:close(SSHRef, Chn1),
+ {Result,State};
+ Other ->
+ {{error,Other},State}
+ end
+ end;
+
+handle_msg({receive_response,Chn,End,TO}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ log(heading(receive_response,Target),
+ "SSH Ref: ~p, Chn: ~p, Timeout: ~p", [SSHRef,Chn,TO]),
+ Result = do_recv_response(SSHRef, Chn, [], End, TO),
+ {Result,State};
+
+handle_msg({send,Chn,Type,Data,TO}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ log(heading(send,Target),
+ "SSH Ref: ~p, Chn: ~p, Type: ~p, Timeout: ~p~n"
+ "Data: ~p", [SSHRef,Chn,Type,TO,Data]),
+ Result = ssh_connection:send(SSHRef, Chn, Type, Data, TO),
+ {Result,State};
+
+handle_msg({send_and_receive,Chn,Type,Data,End,TO}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ log(heading(send_and_receive,Target),
+ "SSH Ref: ~p, Chn: ~p, Type: ~p, Timeout: ~p~n"
+ "Data: ~p", [SSHRef,Chn,Type,TO,Data]),
+ case ssh_connection:send(SSHRef, Chn, Type, Data, TO) of
+ ok ->
+ Result = do_recv_response(SSHRef, Chn, [], End, TO),
+ {Result,State};
+ Error ->
+ {Error,State}
+ end;
+
+handle_msg({subsystem,Chn,Subsystem,TO}, State) ->
+ #state{ssh_ref=SSHRef, target=Target} = State,
+ log(heading(subsystem,Target),
+ "SSH Ref: ~p, Chn: ~p, Subsys: ~p, Timeout: ~p",
+ [SSHRef,Chn,Subsystem,TO]),
+ Result = ssh_connection:subsystem(SSHRef, Chn, Subsystem, TO),
+ {Result,State};
+
+%% --- SFTP Commands ---
+
+handle_msg({read_file,Srv,File}=Cmd, S=#state{ssh_ref=SSHRef}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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}) ->
+ log(heading(sftp,S#state.target),
+ "SSH Ref: ~p, Server: ~p~nCmd: ~p", [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 ->
+ log(heading(disconnect_ssh,State#state.target),
+ "SSH Ref: ~p",[SSHRef]),
+ ssh:close(SSHRef);
+ sftp ->
+ log(heading(disconnect_sftp,State#state.target),
+ "SFTP Ref: ~p",[SSHRef]),
+ ssh_sftp:stop(SSHRef)
+ end.
+
+
+%%%=================================================================
+%%% Internal functions
+
+%%%-----------------------------------------------------------------
+%%%
+do_recv_response(SSH, Chn, Data, End, Timeout) ->
+ receive
+
+ {ssh_cm, SSH, {open,Chn,RemoteChn,{session}}} ->
+ debug("RECVD open"),
+ {ok,{open,Chn,RemoteChn,{session}}};
+
+ {ssh_cm, SSH, {closed,Chn}} ->
+ ssh_connection:close(SSH, Chn),
+ debug("CLSD~n~p ~p", [SSH,Chn]),
+ {ok,Data};
+
+ {ssh_cm, SSH, {data,Chn,_,NewData}} ->
+ ssh_connection:adjust_window(SSH, Chn, size(NewData)),
+ debug("RECVD~n~p", [binary_to_list(NewData)]),
+ DataAcc = Data ++ binary_to_list(NewData),
+ if is_function(End) ->
+ case End(DataAcc) of
+ true ->
+ {ok,DataAcc};
+ false ->
+ do_recv_response(SSH, Chn, DataAcc, End, Timeout)
+ end;
+ true ->
+ do_recv_response(SSH, Chn, DataAcc, End, Timeout)
+ end;
+
+ {ssh_cm, SSH, {eof,Chn}} ->
+ debug("RECVD EOF~n~p ~p", [SSH,Chn]),
+ {ok,Data};
+
+ {ssh_cm, SSH, {exit_signal,Chn,Signal,Err,_Lang}} ->
+ debug("RECVD exit_signal~n~p ~p~n~p ~p", [SSH,Chn,Signal,Err]),
+ do_recv_response(SSH, Chn, Data, End, Timeout);
+%% {ok,{exit_signal,Chn,Signal,Err,_Lang}};
+
+ {ssh_cm, SSH, {exit_status,Chn,Status}} ->
+ debug("RECVD exit_status~n~p ~p~n~p", [SSH,Chn,Status]),
+ do_recv_response(SSH, Chn, Data, End, Timeout);
+%% {ok,{exit_status,Chn,_Status}};
+
+
+%% --- INTERACTIVE MESSAGES - NOT HANDLED ---
+%%
+%% {ssh_cm, SSH, {subsystem,Chn,WantReply,Name}} ->
+%% debug("RECVD SUBS WNTRPLY~n~p ~p~n~p~n~p",
+%% [SSH,Chn,WantReply]),
+%% ssh_connection:reply_request(SSH, WantReply, success, Chn),
+%% do_recv_response(SSH, Chn, Data, End, Timeout);
+
+%% {ssh_cm, SSH, {shell,WantReply}} ->
+%% debug("RECVD SHELL WNTRPLY~n~p ~p~n~p~n~p",
+%% [SSH,Chn,WantReply]),
+%% ssh_connection:reply_request(SSH, WantReply, success, Chn),
+%% do_recv_response(SSH,Chn,Data,End,Timeout);
+
+%% {ssh_cm, SSH, {pty,Chn,WantReply,Pty}} ->
+%% debug("RECVD PTY WNTRPLY~n~p ~p~n~p~n~p",
+%% [SSH,Chn,WantReply,Pty]),
+%% ssh_connection:reply_request(SSH, WantReply, success, Chn),
+%% do_recv_response(SSH, Chn, Data, End, Timeout);
+
+%% {ssh_cm, SSH, WCh={window_change,_Chn,_Width,_Height,_PixWidth,_PixHeight}} ->
+%% debug("RECVD WINCH"),
+%% {ok,WCh};
+
+ Other ->
+ debug("UNEXPECTED MESSAGE~n~p ~p~n~p", [SSH,Chn,Other]),
+ do_recv_response(SSH, Chn, Data, End, Timeout)
+
+ after Timeout ->
+ case End of
+ timeout ->
+ {ok,Data};
+ _ ->
+ {timeout,Data}
+ end
+ end.
+
+%%%-----------------------------------------------------------------
+%%%
+get_handle(SSH) when is_pid(SSH) ->
+ {ok,SSH};
+get_handle(SSH) ->
+ case ct_util:get_connections(SSH, ?MODULE) of
+ {ok,[{Pid,_}]} ->
+ {ok,Pid};
+ {ok,[]} ->
+ connect(SSH);
+ Error ->
+ Error
+ end.
+
+%%%-----------------------------------------------------------------
+%%%
+call(SSH, Msg) ->
+ case get_handle(SSH) of
+ {ok,Pid} ->
+ ct_gen_conn:call(Pid, Msg);
+ Error ->
+ 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]).
+
+%%%-----------------------------------------------------------------
+%%%
+log(Heading, Str, Args) ->
+ ct_gen_conn:log(Heading, Str, Args).
+
+
+%%%-----------------------------------------------------------------
+%%%
+debug(Str) ->
+ debug(Str, []).
+
+debug(_Str, _Args) ->
+%% io:format("~n--- ct_ssh debug ---~n" ++ _Str ++ "~n", _Args),
+ ok.
diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl
new file mode 100644
index 0000000000..c19d312f01
--- /dev/null
+++ b/lib/common_test/src/ct_telnet.erl
@@ -0,0 +1,1166 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc 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 <code>unix_telnet</code> manual page for information
+%%% about how ct_telnet may be used specifically with 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)
+%%% </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}]}.
+%%% </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.</p>
+
+%%% @type connection_type() = telnet | ts1 | ts2
+
+%%% @type connection() = handle() |
+%%% {ct:target_name(),connection_type()} | ct:target_name()
+
+%%% @type handle() = ct_gen_conn:handle(). Handle for a
+%%% specific telnet connection.
+
+%%% @type prompt_regexp() = string(). A regular expression which
+%%% matches all possible prompts for a specific type of target. The
+%%% regexp must not have any groups i.e. when matching, re:run/3 shall
+%%% return a list with one single element.
+%%%
+%%% @see unix_telnet
+
+-module(ct_telnet).
+
+-compile(export_all).
+
+-export([open/1, open/2, open/3, open/4, close/1]).
+-export([cmd/2, cmd/3, cmdf/3, cmdf/4, get_data/1,
+ send/2, sendf/3, expect/2, expect/3]).
+
+%% Callbacks
+-export([init/3,handle_msg/2,reconnect/2,terminate/2]).
+
+%% Tool internals
+-export([silent_teln_expect/5, teln_receive_until_prompt/3,
+ start_log/1, log/3, cont_log/2, end_log/0,
+ try_start_log/1, try_log/3, try_cont_log/2, try_end_log/0]).
+
+
+-define(RECONNS,3).
+-define(RECONN_TIMEOUT,5000).
+-define(DEFAULT_TIMEOUT,10000).
+-define(DEFAULT_PORT,23).
+
+-include("ct_util.hrl").
+
+-record(state,{teln_pid,
+ prx,
+ type,
+ buffer=[],
+ prompt=false,
+ name,
+ target_mod,extra,
+ conn_to=?DEFAULT_TIMEOUT,
+ com_to=?DEFAULT_TIMEOUT,
+ reconns=?RECONNS,
+ reconn_int=?RECONN_TIMEOUT}).
+
+%%%-----------------------------------------------------------------
+%%% @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()
+%%%
+%%% @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
+ open(Name, ConnType, unix_telnet, Name);
+ {ok, Key} -> % any other, e.g. interwatch (iw), etc.
+ open(Name, ConnType, Key, Name);
+ Error ->
+ 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()
+%%%
+%%% @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,Extra)</code> and <code>get_prompt_regexp()</code>
+%%% for the given <code>TargetType</code> (e.g. <code>unix_telnet</code>).</p>
+open(KeyOrName,ConnType,TargetMod,Extra) ->
+ case ct:get_config({KeyOrName,ConnType}) of
+ undefined ->
+ log(heading(open,{KeyOrName,ConnType}),"Failed: ~p",
+ [{not_available,KeyOrName}]),
+ {error,{not_available,KeyOrName,ConnType}};
+ Addr ->
+ Addr1 =
+ case Addr of
+ {_IP,_Port} ->
+ Addr;
+ IP ->
+ case ct:get_config({KeyOrName,port}) of
+ undefined -> IP;
+ P -> {IP,P}
+ end
+ end,
+ log(heading(open,{KeyOrName,ConnType}),"Opening connection to: ~p",[Addr1]),
+ ct_gen_conn:start(KeyOrName,full_addr(Addr1,ConnType),
+ {TargetMod,Extra},?MODULE)
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec close(Connection) -> ok | {error,Reason}
+%%% Connection = ct_telnet:connection()
+%%%
+%%% @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} ->
+ log("ct_telnet:close","Handle: ~p",[Pid]),
+ case ct_gen_conn:stop(Pid) of
+ {error,{process_down,Pid,noproc}} ->
+ {error,already_closed};
+ Result ->
+ Result
+ end;
+ Error ->
+ Error
+ end.
+
+%%%=================================================================
+%%% Test suite interface
+%%%-----------------------------------------------------------------
+%%% @spec cmd(Connection,Cmd) -> {ok,Data} | {error,Reason}
+%%% @equiv cmd(Connection,Cmd,DefaultTimeout)
+cmd(Connection,Cmd) ->
+ cmd(Connection,Cmd,default).
+%%%-----------------------------------------------------------------
+%%% @spec cmd(Connection,Cmd,Timeout) -> {ok,Data} | {error,Reason}
+%%% Connection = ct_telnet:connection()
+%%% Cmd = string()
+%%% Timeout = integer()
+%%% Data = [string()]
+%%% @doc Send a command via telnet and wait for prompt.
+cmd(Connection,Cmd,Timeout) ->
+ case get_handle(Connection) of
+ {ok,Pid} ->
+ call(Pid,{cmd,Cmd,Timeout});
+ Error ->
+ Error
+ end.
+%%%-----------------------------------------------------------------
+%%% @spec cmdf(Connection,CmdFormat,Args) -> {ok,Data} | {error,Reason}
+%%% @equiv cmdf(Connection,CmdFormat,Args,DefaultTimeout)
+cmdf(Connection,CmdFormat,Args) ->
+ cmdf(Connection,CmdFormat,Args,default).
+%%%-----------------------------------------------------------------
+%%% @spec cmdf(Connection,CmdFormat,Args,Timeout) -> {ok,Data} | {error,Reason}
+%%% Connection = ct_telnet:connection()
+%%% CmdFormat = string()
+%%% Args = list()
+%%% Timeout = integer()
+%%% Data = [string()]
+%%% @doc Send a telnet command and wait for prompt
+%%% (uses a format string and list of arguments to build the command).
+%%%-----------------------------------------------------------------
+cmdf(Connection,CmdFormat,Args,Timeout) when is_list(Args) ->
+ Cmd = lists:flatten(io_lib:format(CmdFormat,Args)),
+ cmd(Connection,Cmd,Timeout).
+
+%%%-----------------------------------------------------------------
+%%% @spec get_data(Connection) -> {ok,Data} | {error,Reason}
+%%% Connection = ct_telnet:connection()
+%%% Data = [string()]
+%%% @doc Get all data which has been received by the telnet client
+%%% since last command was sent.
+get_data(Connection) ->
+ case get_handle(Connection) of
+ {ok,Pid} ->
+ call(Pid,get_data);
+ Error ->
+ Error
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec send(Connection,Cmd) -> ok | {error,Reason}
+%%% Connection = ct_telnet:connection()
+%%% Cmd = string()
+%%% @doc Send a telnet command and return immediately.
+%%%
+%%% <p>The resulting output from the command can be read with
+%%% <code>get_data/1</code> or <code>expect/2/3</code>.</p>
+send(Connection,Cmd) ->
+ case get_handle(Connection) of
+ {ok,Pid} ->
+ call(Pid,{send,Cmd});
+ Error ->
+ Error
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec sendf(Connection,CmdFormat,Args) -> ok | {error,Reason}
+%%% Connection = ct_telnet:connection()
+%%% CmdFormat = string()
+%%% Args = list()
+%%% @doc Send a telnet command and return immediately (uses a format
+%%% string and a list of arguments to build the command).
+sendf(Connection,CmdFormat,Args) when is_list(Args) ->
+ Cmd = lists:flatten(io_lib:format(CmdFormat,Args)),
+ send(Connection,Cmd).
+
+%%%-----------------------------------------------------------------
+%%% @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 = {timeout,Timeout} | repeat | {repeat,N} | sequence |
+%%% {halt,HaltPatterns} | ignore_prompt
+%%% Timeout = 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. If more
+%%% than one pattern is given, the function returns when the first
+%%% match is found.</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 function will always return when a prompt is found, unless
+%%% the <code>ignore_prompt</code> options is used.</p>
+%%%
+%%% <p>The <code>timeout</code> option indicates that the function
+%%% shall return if the telnet client is idle (i.e. if no data is
+%%% received) for more than <code>Timeout</code> milliseconds. Default
+%%% timeout is 10 seconds.</p>
+%%%
+%%% <p>The <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"}],
+%%% [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"}],
+%%% [{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} ->
+ call(Pid,{expect,Patterns,Opts});
+ Error ->
+ Error
+ end.
+
+%%%=================================================================
+%%% Callback functions
+%% @hidden
+init(Name,{Ip,Port,Type},{TargetMod,Extra}) ->
+ S0 = case ct:get_config(telnet_settings) of
+ undefined ->
+ #state{};
+ Settings ->
+ set_telnet_defaults(Settings,#state{})
+ end,
+ case catch TargetMod:connect(Ip,Port,S0#state.conn_to,Extra) of
+ {ok,TelnPid} ->
+ log(heading(init,{Name,Type}),
+ "Opened telnet connection\n"
+ "IP: ~p\n"
+ "Port: ~p\n"
+ "Command timeout: ~p\n"
+ "Reconnection attempts: ~p\n"
+ "Reconnection interval: ~p\n"
+ "Connection timeout: ~p",
+ [Ip,Port,S0#state.com_to,S0#state.reconns,
+ S0#state.reconn_int,S0#state.conn_to]),
+ {ok,TelnPid,S0#state{teln_pid=TelnPid,
+ type=type(Type),
+ name={Name,Type},
+ target_mod=TargetMod,
+ extra=Extra,
+ prx=TargetMod:get_prompt_regexp()}};
+ {'EXIT',Reason} ->
+ {error,Reason};
+ Error ->
+ Error
+ end.
+
+type(telnet) -> ip;
+type(TS) when TS==ts1;TS==ts2 -> ts.
+
+set_telnet_defaults([{connect_timeout,CnTo}|Ss],S) ->
+ set_telnet_defaults(Ss,S#state{conn_to=CnTo});
+set_telnet_defaults([{command_timeout,CmTo}|Ss],S) ->
+ set_telnet_defaults(Ss,S#state{com_to=CmTo});
+set_telnet_defaults([{reconnection_attempts,Rs}|Ss],S) ->
+ set_telnet_defaults(Ss,S#state{reconns=Rs});
+set_telnet_defaults([{reconnection_interval,RInt}|Ss],S) ->
+ set_telnet_defaults(Ss,S#state{reconn_int=RInt});
+set_telnet_defaults([],S) ->
+ S.
+
+%% @hidden
+handle_msg({cmd,Cmd,Timeout},State) ->
+ try_start_log(heading(cmd,State#state.name)),
+ try_cont_log("Cmd: ~p", [Cmd]),
+ debug_cont_log("Throwing Buffer:",[]),
+ debug_log_lines(State#state.buffer),
+ case {State#state.type,State#state.prompt} of
+ {ts,_} ->
+ silent_teln_expect(State#state.teln_pid,
+ State#state.buffer,
+ prompt,
+ State#state.prx,
+ [{timeout,2000}]);
+ {ip,false} ->
+ silent_teln_expect(State#state.teln_pid,
+ State#state.buffer,
+ prompt,
+ State#state.prx,
+ [{timeout,200}]);
+ {ip,true} ->
+ ok
+ end,
+ TO = if Timeout == default -> State#state.com_to;
+ true -> Timeout
+ end,
+ {Return,NewBuffer,Prompt} =
+ case teln_cmd(State#state.teln_pid, Cmd, State#state.prx, TO) of
+ {ok,Data,_PromptType,Rest} ->
+ try_cont_log("Return: ~p", [{ok,Data}]),
+ {{ok,Data},Rest,true};
+ Error ->
+ Retry = {retry,{Error,State#state.name,State#state.teln_pid,
+ {cmd,Cmd,TO}}},
+ try_cont_log("Return: ~p", [Error]),
+ {Retry,[],false}
+ end,
+ try_end_log(),
+ {Return,State#state{buffer=NewBuffer,prompt=Prompt}};
+handle_msg({send,Cmd},State) ->
+ try_log(heading(send,State#state.name),"Cmd: ~p",[Cmd]),
+ debug_cont_log("Throwing Buffer:",[]),
+ debug_log_lines(State#state.buffer),
+ case {State#state.type,State#state.prompt} of
+ {ts,_} ->
+ silent_teln_expect(State#state.teln_pid,
+ State#state.buffer,
+ prompt,
+ State#state.prx,
+ [{timeout,2000}]);
+ {ip,false} ->
+ silent_teln_expect(State#state.teln_pid,
+ State#state.buffer,
+ prompt,
+ State#state.prx,
+ [{timeout,200}]);
+ {ip,true} ->
+ ok
+ end,
+ ct_telnet_client:send_data(State#state.teln_pid,Cmd),
+ {ok,State#state{buffer=[],prompt=false}};
+handle_msg(get_data,State) ->
+ try_start_log(heading(get_data,State#state.name)),
+ {ok,Data,Buffer} = teln_get_all_data(State#state.teln_pid,
+ State#state.prx,
+ State#state.buffer,
+ [],[]),
+ try_cont_log("Return: ~p",[{ok,Data}]),
+ try_end_log(),
+ {{ok,Data},State#state{buffer=Buffer}};
+handle_msg({expect,Pattern,Opts},State) ->
+ try_start_log(heading(expect,State#state.name)),
+ try_cont_log("Expect: ~p\nOpts=~p\n",[Pattern,Opts]),
+ {Return,NewBuffer,Prompt} =
+ case teln_expect(State#state.teln_pid,
+ State#state.buffer,
+ Pattern,
+ State#state.prx,
+ Opts) of
+ {ok,Data,Rest} ->
+ P = check_if_prompt_was_reached(Data,[]),
+ {{ok,Data},Rest,P};
+ {ok,Data,HaltReason,Rest} ->
+ force_cont_log("HaltReason: ~p",
+ [HaltReason]),
+ P = check_if_prompt_was_reached(Data,HaltReason),
+ {{ok,Data,HaltReason},Rest,P};
+ {error,Reason,Rest} ->
+ force_cont_log("Expect failed\n~p",[{error,Reason}]),
+ P = check_if_prompt_was_reached([],Reason),
+ {{error,Reason},Rest,P};
+ {error,Reason} ->
+ force_cont_log("Expect failed\n~p",[{error,Reason}]),
+ P = check_if_prompt_was_reached([],Reason),
+ {{error,Reason},[],P}
+ end,
+ try_end_log(),
+ Return1 = case Return of
+ {error,_} -> {retry,{Return,State#state.name,
+ State#state.teln_pid,
+ {expect,Pattern,Opts}}};
+ _ -> Return
+ end,
+ {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{target_mod=TargetMod,
+ extra=Extra,
+ conn_to=ConnTo,
+ reconn_int=ReconnInt}) ->
+ case TargetMod:connect(Ip,Port,ConnTo,Extra) of
+ {ok, NewPid} ->
+ {ok, NewPid, State#state{teln_pid=NewPid}};
+ Error when N==0 ->
+ Error;
+ _Error ->
+ log("Reconnect failed!","Retries left: ~p",[N]),
+ timer:sleep(ReconnInt),
+ reconnect(Ip,Port,N-1,State)
+ end.
+
+
+%% @hidden
+terminate(TelnPid,State) ->
+ log(heading(terminate,State#state.name),
+ "Closing telnet connection.\nId: ~p",
+ [TelnPid]),
+ ct_telnet_client:close(TelnPid).
+
+
+%%%=================================================================
+%%% Internal function
+get_handle(Pid) when is_pid(Pid) ->
+ {ok,Pid};
+get_handle({Name,Type}) when Type==telnet;Type==ts1;Type==ts2 ->
+ case ct_util:get_connections(Name,?MODULE) of
+ {ok,Conns} when Conns /= [] ->
+ case get_handle(Type,Conns) of
+ {ok,Pid} ->
+ {ok,Pid};
+ _Error ->
+ case ct_util:get_key_from_name(Name) of
+ {ok,node} ->
+ open(Name,Type,ct_telnet_cello_node);
+ {ok,unix} -> % unix host
+ open(Name,Type,unix_telnet,Name);
+ {ok,Key} -> % any other, e.g. interwatch (iw)
+ open(Name,Type,Key,Name);
+ Error ->
+ Error
+ end
+ end;
+ {ok,[]} ->
+ {error,already_closed};
+ Error ->
+ Error
+ end;
+get_handle(Name) ->
+ get_handle({Name,telnet}).
+
+get_handle(Type,[{Pid,{_,_,Type}}|_]) ->
+ {ok,Pid};
+get_handle(Type,[_H|T]) ->
+ get_handle(Type,T);
+get_handle(Type,[]) ->
+ {error,{no_such_connection,Type}}.
+
+full_addr({Ip,Port},Type) ->
+ {Ip,Port,Type};
+full_addr(Ip,Type) ->
+ {Ip,?DEFAULT_PORT,Type}.
+
+call(Pid,Msg) ->
+ ct_gen_conn:call(Pid,Msg).
+
+check_if_prompt_was_reached({prompt,_},_) ->
+ true;
+check_if_prompt_was_reached(_,{prompt,_}) ->
+ true;
+check_if_prompt_was_reached(Data,_) when is_list(Data) ->
+ lists:keymember(prompt,1,Data);
+check_if_prompt_was_reached(_,_) ->
+ false.
+
+%tc(Fun) ->
+% Before = erlang:now(),
+% Val = Fun(),
+% After = erlang:now(),
+% {now_diff(After, Before), Val}.
+%now_diff({A2, B2, C2}, {A1, B1, C1}) ->
+% ((A2-A1)*1000000 + B2-B1)*1000000 + C2-C1.
+
+heading(Function,Name) ->
+ io_lib:format("~w:~w ~p",[?MODULE,Function,Name]).
+
+%%% @hidden
+%% Functions for regular (unconditional) logging, to be
+%% used during connect, reconnect, disconnect etc.
+log(Heading,Str,Args) ->
+ ct_gen_conn:log(Heading,Str,Args).
+%%% @hidden
+start_log(Heading) ->
+ ct_gen_conn:start_log(Heading).
+cont_log(Str,Args) ->
+ ct_gen_conn:cont_log(Str,Args).
+end_log() ->
+ ct_gen_conn:end_log().
+
+%%% @hidden
+%% Functions for conditional logging, to be used by
+%% cmd, send, receive, expect etc (this output may be
+%% silenced by user).
+try_start_log(Heading) ->
+ do_try_log(start_log,[Heading]).
+%%% @hidden
+try_end_log() ->
+ do_try_log(end_log,[]).
+
+%%% @hidden
+try_log(Heading,Str,Args) ->
+ do_try_log(log,[Heading,Str,Args]).
+
+%%% @hidden
+try_cont_log(Str,Args) ->
+ do_try_log(cont_log,[Str,Args]).
+
+%%% @hidden
+do_try_log(Func,Args) ->
+ %% check if output is suppressed
+ case ct_util:is_silenced(telnet) of
+ true ->
+ ok;
+ false ->
+ apply(ct_gen_conn,Func,Args)
+ end.
+
+%%% @hidden
+%% Functions that will force printout even if ct_telnet
+%% output has been silenced, to be used for error printouts.
+force_cont_log(Str,Args) ->
+ case ct_util:is_silenced(telnet) of
+ true ->
+ %% call log/3 now instead of cont_log/2 since
+ %% start_log/1 will not have been previously called
+ log("ct_telnet info",Str,Args);
+ false ->
+ cont_log(Str,Args)
+ end.
+
+%%% @hidden
+%% Debug printouts.
+debug_cont_log(Str,Args) ->
+ Old = put(silent,true),
+ cont_log(Str,Args),
+ put(silent,Old).
+
+
+
+%%%=================================================================
+%%% Abstraction layer on top of ct_telnet_client.erl
+teln_cmd(Pid,Cmd,Prx,Timeout) ->
+ ct_telnet_client:send_data(Pid,Cmd),
+ teln_receive_until_prompt(Pid,Prx,Timeout).
+
+
+teln_get_all_data(Pid,Prx,Data,Acc,LastLine) ->
+ case check_for_prompt(Prx,lists:reverse(LastLine) ++ Data) of
+ {prompt,Lines,_PromptType,Rest} ->
+ teln_get_all_data(Pid,Prx,Rest,[Lines|Acc],[]);
+ {noprompt,Lines,LastLine1} ->
+ case ct_telnet_client:get_data(Pid) of
+ {ok,[]} ->
+ {ok,lists:reverse(lists:append([Lines|Acc])),
+ lists:reverse(LastLine1)};
+ {ok,Data1} ->
+ teln_get_all_data(Pid,Prx,Data1,[Lines|Acc],LastLine1)
+ end
+ end.
+
+%% Expect options record
+-record(eo,{teln_pid,
+ prx,
+ timeout,
+ haltpatterns=[],
+ seq=false,
+ repeat=false,
+ found_prompt=false}).
+
+%% @hidden
+%% @doc Externally the silent_teln_expect function shall only be used
+%% by the TargetModule, i.e. the target specific module which
+%% implements connect/2 and get_prompt_regexp/0.
+silent_teln_expect(Pid,Data,Pattern,Prx,Opts) ->
+ Old = put(silent,true),
+ try_cont_log("silent_teln_expect/5, Pattern = ~p",[Pattern]),
+ Result = teln_expect(Pid,Data,Pattern,Prx,Opts),
+ try_cont_log("silent_teln_expect -> ~p\n",[Result]),
+ put(silent,Old),
+ Result.
+
+%% teln_expect/5
+%%
+%% This function implements the expect functionality over telnet. In
+%% general there are three possible ways to go:
+%% 1) Single: One or more patterns are given, and the function return
+%% when one of the patterns are matched.
+%% 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.
+%% 3b) Repeat (sequence): 2) is repeated either N times or until a
+%% halt condition is fullfilled.
+teln_expect(Pid,Data,Pattern0,Prx,Opts) -> HaltPatterns = case
+ get_ignore_prompt(Opts) of true -> get_haltpatterns(Opts); false
+ -> [prompt | get_haltpatterns(Opts)] end,
+
+ Seq = get_seq(Opts),
+ Pattern = convert_pattern(Pattern0,Seq),
+
+ Timeout = get_timeout(Opts),
+
+ EO = #eo{teln_pid=Pid,
+ prx=Prx,
+ timeout=Timeout,
+ seq=Seq,
+ haltpatterns=HaltPatterns},
+
+ case get_repeat(Opts) of
+ false ->
+ case teln_expect1(Data,Pattern,[],EO) of
+ {ok,Matched,Rest} ->
+ {ok,Matched,Rest};
+ {halt,Why,Rest} ->
+ {error,Why,Rest};
+ {error,Reason} ->
+ {error,Reason}
+ end;
+ N ->
+ EO1 = EO#eo{repeat=N},
+ repeat_expect(Data,Pattern,[],EO1)
+ end.
+
+convert_pattern(Pattern,Seq)
+ when is_list(Pattern) and not is_integer(hd(Pattern)) ->
+ case Seq of
+ true -> Pattern;
+ false -> rm_dupl(Pattern,[])
+ end;
+convert_pattern(Pattern,_Seq) ->
+ [Pattern].
+
+rm_dupl([P|Ps],Acc) ->
+ case lists:member(P,Acc) of
+ true ->
+ rm_dupl(Ps,Acc);
+ false ->
+ rm_dupl(Ps,[P|Acc])
+ end;
+rm_dupl([],Acc) ->
+ lists:reverse(Acc).
+
+get_timeout(Opts) ->
+ case lists:keysearch(timeout,1,Opts) of
+ {value,{timeout,T}} -> T;
+ false -> ?DEFAULT_TIMEOUT
+ end.
+get_repeat(Opts) ->
+ case lists:keysearch(repeat,1,Opts) of
+ {value,{repeat,N}} when is_integer(N) ->
+ N;
+ false ->
+ case lists:member(repeat,Opts) of
+ true ->
+ -1;
+ false ->
+ false
+ end
+ end.
+get_seq(Opts) ->
+ lists:member(sequence,Opts).
+get_haltpatterns(Opts) ->
+ case lists:keysearch(halt,1,Opts) of
+ {value,{halt,HaltPatterns}} ->
+ convert_pattern(HaltPatterns,false);
+ false ->
+ []
+ end.
+get_ignore_prompt(Opts) ->
+ lists:member(ignore_prompt,Opts).
+
+%% Repeat either single or sequence. All match results are accumulated
+%% and returned when a halt condition is fulllfilled.
+repeat_expect(Rest,_Pattern,Acc,#eo{repeat=0}) ->
+ {ok,lists:reverse(Acc),done,Rest};
+repeat_expect(Data,Pattern,Acc,EO) ->
+ case teln_expect1(Data,Pattern,[],EO) of
+ {ok,Matched,Rest} ->
+ EO1 = EO#eo{repeat=EO#eo.repeat-1},
+ repeat_expect(Rest,Pattern,[Matched|Acc],EO1);
+ {halt,Why,Rest} ->
+ {ok,lists:reverse(Acc),Why,Rest};
+ {error,Reason} ->
+ {error,Reason}
+ end.
+
+teln_expect1(Data,Pattern,Acc,EO) ->
+ ExpectFun = case EO#eo.seq of
+ true -> fun() -> seq_expect(Data,Pattern,Acc,EO) end;
+ false -> fun() -> one_expect(Data,Pattern,EO) end
+ end,
+ case ExpectFun() of
+ {match,Match,Rest} ->
+ {ok,Match,Rest};
+ {halt,Why,Rest} ->
+ {halt,Why,Rest};
+ NotFinished ->
+ %% Get more data
+ Fun = fun() -> get_data1(EO#eo.teln_pid) end,
+ case ct_gen_conn:do_within_time(Fun, EO#eo.timeout) of
+ {error,Reason} ->
+ %% A timeout will occur when the telnet connection
+ %% is idle for EO#eo.timeout milliseconds.
+ {error,Reason};
+ {ok,Data1} ->
+ case NotFinished of
+ {nomatch,Rest} ->
+ %% One expect
+ teln_expect1(Rest++Data1,Pattern,[],EO);
+ {continue,Patterns1,Acc1,Rest} ->
+ %% Sequence
+ teln_expect1(Rest++Data1,Patterns1,Acc1,EO)
+ end
+ end
+ end.
+
+get_data1(Pid) ->
+ case ct_telnet_client:get_data(Pid) of
+ {ok,[]} ->
+ get_data1(Pid);
+ {ok,Data} ->
+ {ok,Data}
+ end.
+
+%% 1) Single expect.
+%% First the whole data chunk is searched for a prompt (to avoid doing
+%% a regexp match for the prompt at each line).
+%% If we are searching for anyting else, the datachunk is split into
+%% lines and each line is matched against each pattern.
+
+%% one_expect: split data chunk at prompts
+one_expect(Data,Pattern,EO) ->
+ case match_prompt(Data,EO#eo.prx) of
+ {prompt,UptoPrompt,PromptType,Rest} ->
+ case Pattern of
+ [Prompt] when Prompt==prompt; Prompt=={prompt,PromptType} ->
+ %% Only searching for prompt
+ log_lines(UptoPrompt),
+ try_cont_log("<b>PROMPT:</b> ~s", [PromptType]),
+ {match,{prompt,PromptType},Rest};
+ [{prompt,_OtherPromptType}] ->
+ %% Only searching for one specific prompt, not thisone
+ log_lines(UptoPrompt),
+ {nomatch,Rest};
+ _ ->
+ one_expect1(UptoPrompt,Pattern,Rest,
+ EO#eo{found_prompt=PromptType})
+ end;
+ noprompt ->
+ case Pattern of
+ [Prompt] when Prompt==prompt; element(1,Prompt)==prompt ->
+ %% Only searching for prompt
+ LastLine = log_lines_not_last(Data),
+ {nomatch,LastLine};
+ _ ->
+ one_expect1(Data,Pattern,[],EO#eo{found_prompt=false})
+ end
+ end.
+
+remove_zero(List) ->
+ [Ch || Ch <- List, Ch=/=0, Ch=/=13].
+
+%% one_expect1: split data chunk at lines
+one_expect1(Data,Pattern,Rest,EO) ->
+ case match_lines(Data,Pattern,EO) of
+ {match,Match,MatchRest} ->
+ {match,Match,MatchRest++Rest};
+ {nomatch,prompt} ->
+ one_expect(Rest,Pattern,EO);
+ {nomatch,NoMatchRest} ->
+ {nomatch,NoMatchRest++Rest};
+ {halt,Why,HaltRest} ->
+ {halt,Why,HaltRest++Rest}
+ end.
+
+
+%% 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
+%% 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.
+
+%% seq_expect: Split data chunk at prompts
+seq_expect(Data,[],Acc,_EO) ->
+ {match,lists:reverse(Acc),Data};
+seq_expect([],Patterns,Acc,_EO) ->
+ {continue,Patterns,lists:reverse(Acc),[]};
+seq_expect(Data,Patterns,Acc,EO) ->
+ case match_prompt(Data,EO#eo.prx) of
+ {prompt,UptoPrompt,PromptType,Rest} ->
+ seq_expect1(UptoPrompt,Patterns,Acc,Rest,
+ EO#eo{found_prompt=PromptType});
+ noprompt ->
+ seq_expect1(Data,Patterns,Acc,[],EO#eo{found_prompt=false})
+ end.
+
+%% seq_expect1: For one prompt-chunk, match each pattern - line by
+%% line if it is other than the prompt we are seaching for.
+seq_expect1(Data,[prompt|Patterns],Acc,Rest,EO) ->
+ case EO#eo.found_prompt of
+ false ->
+ LastLine = log_lines_not_last(Data),
+ %% Rest==[] because no prompt is found
+ {continue,[prompt|Patterns],Acc,LastLine};
+ PromptType ->
+ log_lines(Data),
+ try_cont_log("<b>PROMPT:</b> ~s", [PromptType]),
+ seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO)
+ end;
+seq_expect1(Data,[{prompt,PromptType}|Patterns],Acc,Rest,EO) ->
+ case EO#eo.found_prompt of
+ false ->
+ LastLine = log_lines_not_last(Data),
+ %% Rest==[] because no prompt is found
+ {continue,[{prompt,PromptType}|Patterns],Acc,LastLine};
+ PromptType ->
+ log_lines(Data),
+ try_cont_log("<b>PROMPT:</b> ~s", [PromptType]),
+ seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO);
+ _OtherPromptType ->
+ log_lines(Data),
+ seq_expect(Rest,[{prompt,PromptType}|Patterns],Acc,EO)
+ end;
+seq_expect1(Data,[Pattern|Patterns],Acc,Rest,EO) ->
+ case match_lines(Data,[Pattern],EO) of
+ {match,Match,MatchRest} ->
+ seq_expect1(MatchRest,Patterns,[Match|Acc],Rest,EO);
+ {nomatch,prompt} ->
+ seq_expect(Rest,[Pattern|Patterns],Acc,EO);
+ {nomatch,NoMatchRest} when Rest==[] ->
+ %% The data did not end with a prompt
+ {continue,[Pattern|Patterns],Acc,NoMatchRest};
+ {halt,Why,HaltRest} ->
+ {halt,Why,HaltRest++Rest}
+ end;
+seq_expect1(Data,[],Acc,Rest,_EO) ->
+ {match,lists:reverse(Acc),Data++Rest}.
+
+%% Split prompt-chunk at lines
+match_lines(Data,Patterns,EO) ->
+ FoundPrompt = EO#eo.found_prompt,
+ case one_line(Data,[]) of
+ {noline,Rest} when FoundPrompt=/=false ->
+ %% This is the line including the prompt
+ case match_line(Rest,Patterns,FoundPrompt,EO) of
+ nomatch ->
+ {nomatch,prompt};
+ {Tag,Match} ->
+ {Tag,Match,[]}
+ end;
+ {noline,Rest} ->
+ {nomatch,Rest};
+ {Line,Rest} ->
+ case match_line(Line,Patterns,false,EO) of
+ nomatch ->
+ match_lines(Rest,Patterns,EO);
+ {Tag,Match} ->
+ {Tag,Match,Rest}
+ end
+ end.
+
+
+%% For one line, match each pattern
+match_line(Line,Patterns,FoundPrompt,EO) ->
+ match_line(Line,Patterns,FoundPrompt,EO,match).
+
+match_line(Line,[prompt|Patterns],false,EO,RetTag) ->
+ match_line(Line,Patterns,false,EO,RetTag);
+match_line(Line,[prompt|_Patterns],FoundPrompt,_EO,RetTag) ->
+ try_cont_log(" ~s", [Line]),
+ try_cont_log("<b>PROMPT:</b> ~s", [FoundPrompt]),
+ {RetTag,{prompt,FoundPrompt}};
+match_line(Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_EO,RetTag)
+ when PromptType==FoundPrompt ->
+ try_cont_log(" ~s", [Line]),
+ try_cont_log("<b>PROMPT:</b> ~s", [FoundPrompt]),
+ {RetTag,{prompt,FoundPrompt}};
+match_line(Line,[{prompt,PromptType}|Patterns],FoundPrompt,EO,RetTag)
+ when PromptType=/=FoundPrompt ->
+ match_line(Line,Patterns,FoundPrompt,EO,RetTag);
+match_line(Line,[{Tag,Pattern}|Patterns],FoundPrompt,EO,RetTag) ->
+ case re:run(Line,Pattern,[{capture,all,list}]) of
+ nomatch ->
+ match_line(Line,Patterns,FoundPrompt,EO,RetTag);
+ {match,Match} ->
+ try_cont_log("<b>MATCH:</b> ~s", [Line]),
+ {RetTag,{Tag,Match}}
+ end;
+match_line(Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) ->
+ case re:run(Line,Pattern,[{capture,all,list}]) of
+ nomatch ->
+ match_line(Line,Patterns,FoundPrompt,EO,RetTag);
+ {match,Match} ->
+ try_cont_log("<b>MATCH:</b> ~s", [Line]),
+ {RetTag,Match}
+ end;
+match_line(Line,[],FoundPrompt,EO,match) ->
+ match_line(Line,EO#eo.haltpatterns,FoundPrompt,EO,halt);
+match_line(Line,[],_FoundPrompt,_EO,halt) ->
+ try_cont_log(" ~s", [Line]),
+ nomatch.
+
+one_line([$\n|Rest],Line) ->
+ {lists:reverse(Line),Rest};
+one_line([$\r|Rest],Line) ->
+ one_line(Rest,Line);
+one_line([0|Rest],Line) ->
+ one_line(Rest,Line);
+one_line([Char|Rest],Line) ->
+ one_line(Rest,[Char|Line]);
+one_line([],Line) ->
+ {noline,lists:reverse(Line)}.
+
+debug_log_lines(String) ->
+ Old = put(silent,true),
+ log_lines(String),
+ put(silent,Old).
+
+log_lines(String) ->
+ case log_lines_not_last(String) of
+ [] ->
+ ok;
+ LastLine ->
+ try_cont_log(" ~s", [LastLine])
+ end.
+
+log_lines_not_last(String) ->
+ case add_tabs(String,[],[]) of
+ {[],LastLine} ->
+ LastLine;
+ {String1,LastLine} ->
+ try_cont_log("~s",[String1]),
+ LastLine
+ end.
+
+add_tabs([0|Rest],Acc,LastLine) ->
+ add_tabs(Rest,Acc,LastLine);
+add_tabs([$\r|Rest],Acc,LastLine) ->
+ add_tabs(Rest,Acc,LastLine);
+add_tabs([$\n|Rest],Acc,LastLine) ->
+ add_tabs(Rest,[$\n|LastLine] ++ [$\s,$\s,$\s,$\s,$\s,$\s,$\s|Acc],[]);
+add_tabs([Ch|Rest],Acc,LastLine) ->
+ add_tabs(Rest,Acc,[Ch|LastLine]);
+add_tabs([],[$\n|Acc],LastLine) ->
+ {lists:reverse(Acc),lists:reverse(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).
+
+teln_receive_until_prompt(Pid,Prx,Acc,LastLine) ->
+ {ok,Data} = ct_telnet_client:get_data(Pid),
+ case check_for_prompt(Prx,LastLine ++ Data) of
+ {prompt,Lines,PromptType,Rest} ->
+ Return = lists:reverse(lists:append([Lines|Acc])),
+ {ok,Return,PromptType,Rest};
+ {noprompt,Lines,LastLine1} ->
+ teln_receive_until_prompt(Pid,Prx,[Lines|Acc],LastLine1)
+ end.
+
+check_for_prompt(Prx,Data) ->
+ case match_prompt(Data,Prx) of
+ {prompt,UptoPrompt,PromptType,Rest} ->
+ {RevLines,LastLine} = split_lines(UptoPrompt),
+ {prompt,[LastLine|RevLines],PromptType,Rest};
+ noprompt ->
+ {RevLines,Rest} = split_lines(Data),
+ {noprompt,RevLines,Rest}
+ end.
+
+split_lines(String) ->
+ split_lines(String,[],[]).
+split_lines([$\n|Rest],Line,Lines) ->
+ split_lines(Rest,[],[lists:reverse(Line)|Lines]);
+split_lines([$\r|Rest],Line,Lines) ->
+ split_lines(Rest,Line,Lines);
+split_lines([0|Rest],Line,Lines) ->
+ split_lines(Rest,Line,Lines);
+split_lines([Char|Rest],Line,Lines) ->
+ split_lines(Rest,[Char|Line],Lines);
+split_lines([],Line,Lines) ->
+ {Lines,lists:reverse(Line)}.
+
+
+match_prompt(Str,Prx) ->
+ match_prompt(Str,Prx,[]).
+match_prompt(Str,Prx,Acc) ->
+ case re:run(Str,Prx) of
+ nomatch ->
+ noprompt;
+ {match,[{Start,Len}]} ->
+ case split_prompt_string(Str,Start+1,Start+Len,1,[],[]) of
+ {noprompt,Done,Rest} ->
+ match_prompt(Rest,Prx,Done);
+ {prompt,UptoPrompt,Prompt,Rest} ->
+ {prompt,lists:reverse(UptoPrompt++Acc),
+ lists:reverse(Prompt),Rest}
+ end
+ end.
+
+split_prompt_string([Ch|Str],Start,End,N,UptoPrompt,Prompt) when N<Start ->
+ split_prompt_string(Str,Start,End,N+1,[Ch|UptoPrompt],Prompt);
+split_prompt_string([Ch|Str],Start,End,N,UptoPrompt,Prompt)
+ when N>=Start, N<End->
+ split_prompt_string(Str,Start,End,N+1,UptoPrompt,[Ch|Prompt]);
+split_prompt_string([Ch|Rest],_Start,End,N,UptoPrompt,Prompt) when N==End ->
+ case UptoPrompt of
+ [$",$=,$T,$P,$M,$O,$R,$P|_] ->
+ %% This is a line from "listenv", it is not a real prompt
+ {noprompt,[Ch|Prompt]++UptoPrompt,Rest};
+ [$\s,$t,$s,$a|_] when Prompt==":nigol" ->
+ %% This is probably the "Last login:" statement which is
+ %% written when telnet connection is openend.
+ {noprompt,[Ch|Prompt]++UptoPrompt,Rest};
+ _ ->
+ {prompt,[Ch|Prompt]++UptoPrompt,[Ch|Prompt],Rest}
+ end.
diff --git a/lib/common_test/src/ct_telnet_client.erl b/lib/common_test/src/ct_telnet_client.erl
new file mode 100644
index 0000000000..e460a50eac
--- /dev/null
+++ b/lib/common_test/src/ct_telnet_client.erl
@@ -0,0 +1,304 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%---------------------------------------------------------------
+%% Basic negotiated options with Telnet (RFC 854)
+%%
+%% Side A request: I WILL set option Opt.
+%% Side B answer: DO go ahead, or no, DON'T set it.
+%%
+%% Side A request: Please DO set this option.
+%% Side B answer: Ok I WILL, or no, I WON'T set it.
+%%
+%% "Enable option" requests may be rejected.
+%% "Disable option" requests must not.
+%%---------------------------------------------------------------
+
+-module(ct_telnet_client).
+
+-export([open/1, open/2, open/3, close/1]).
+-export([send_data/2, get_data/1]).
+
+-define(DBG, false).
+
+-define(TELNET_PORT, 23).
+-define(OPEN_TIMEOUT,10000).
+
+%% telnet control characters
+-define(SE, 240).
+-define(NOP, 241).
+-define(DM, 242).
+-define(BRK, 243).
+-define(IP, 244).
+-define(AO, 245).
+-define(AYT, 246).
+-define(EC, 247).
+-define(EL, 248).
+-define(GA, 249).
+-define(SB, 250).
+-define(WILL, 251).
+-define(WONT, 252).
+-define(DO, 253).
+-define(DONT, 254).
+-define(IAC, 255).
+
+%% telnet options
+-define(BINARY, 0).
+-define(ECHO, 1).
+-define(SUPPRESS_GO_AHEAD, 3).
+-define(TERMINAL_TYPE, 24).
+-define(WINDOW_SIZE, 31).
+
+-record(state,{get_data}).
+
+open(Server) ->
+ open(Server, ?TELNET_PORT, ?OPEN_TIMEOUT).
+
+open(Server, Port) ->
+ open(Server, Port, ?OPEN_TIMEOUT).
+
+open(Server, Port, Timeout) ->
+ Self = self(),
+ Pid = spawn(fun() -> init(Self, Server, Port, Timeout) end),
+ receive
+ {open,Pid} ->
+ {ok,Pid};
+ {Error,Pid} ->
+ Error
+ end.
+
+close(Pid) ->
+ Pid ! close.
+
+send_data(Pid, Data) ->
+ Pid ! {send_data, Data++"\n"},
+ ok.
+
+get_data(Pid) ->
+ Pid ! {get_data, self()},
+ receive
+ {data,Data} ->
+ {ok, Data}
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% Internal functions
+init(Parent, Server, Port, Timeout) ->
+ case gen_tcp:connect(Server, Port, [list,{packet,0}], Timeout) of
+ {ok,Sock} ->
+ dbg("Connected to: ~p\n", [Server]),
+ send([?IAC,?DO,?SUPPRESS_GO_AHEAD], Sock),
+ Parent ! {open,self()},
+ loop(#state{get_data=10}, Sock, []),
+ gen_tcp:close(Sock);
+ Error ->
+ Parent ! {Error,self()}
+ end.
+
+
+
+loop(State, Sock, Acc) ->
+ receive
+ {tcp_closed,_} ->
+ dbg("Connection closed\n", []),
+ receive
+ {get_data,Pid} ->
+ Pid ! closed
+ after 100 ->
+ ok
+ end;
+ {tcp,_,Msg0} ->
+ dbg("tcp msg: ~p~n",[Msg0]),
+ Msg = check_msg(Sock,Msg0,[]),
+ loop(State, Sock, [Msg | Acc]);
+ {send_data,Data} ->
+ send(Data, Sock),
+ loop(State, Sock, Acc);
+ {get_data,Pid} ->
+ NewState =
+ case Acc of
+ [] ->
+ dbg("get_data nodata\n",[]),
+ erlang:send_after(100,self(),{get_data_delayed,Pid}),
+ State#state{get_data=State#state.get_data - 1};
+ _ ->
+ Pid ! {data,lists:reverse(lists:append(Acc))},
+ State
+ end,
+ loop(NewState, Sock, []);
+ {get_data_delayed,Pid} ->
+ NewState = case State#state.get_data of
+ 0 ->
+ send([?IAC,?DO,?NOP], Sock),
+ dbg("delayed after 1000\n",[]),
+ State#state{get_data=10};
+ _ ->
+ State
+ end,
+ NewAcc =
+ case erlang:is_process_alive(Pid) of
+ true ->
+ Pid ! {data,lists:reverse(lists:append(Acc))},
+ [];
+ false ->
+ Acc
+ end,
+ loop(NewState, Sock, NewAcc);
+
+ close ->
+ dbg("Closing connection\n", []),
+ gen_tcp:close(Sock),
+ ok
+ after 1000 ->
+ case Acc of
+ [] -> % no data buffered
+ send([?IAC,?DO,?NOP], Sock),
+ dbg("after 1000\n",[]);
+ _ ->
+ true
+ end,
+ loop(State, Sock, Acc)
+ end.
+
+send(Data, Sock) ->
+ dbg("Sending: ~p\n", [Data]),
+ gen_tcp:send(Sock, Data),
+ ok.
+
+check_msg(Sock,[?IAC | Cs], Acc) ->
+ case get_cmd(Cs) of
+ {Cmd,Cs1} ->
+ dbg("Got ", []),
+ cmd_dbg(Cmd),
+ respond_cmd(Cmd, Sock),
+ check_msg(Sock, Cs1, Acc);
+ error ->
+ Acc
+ end;
+check_msg(Sock,[H|T],Acc) ->
+ check_msg(Sock,T,[H|Acc]);
+check_msg(_Sock,[],Acc) ->
+ Acc.
+
+%% Positive responses (WILL and DO).
+
+respond_cmd([?WILL,?ECHO], Sock) ->
+ R = [?IAC,?DO,?ECHO],
+ cmd_dbg(R),
+ gen_tcp:send(Sock, R);
+
+respond_cmd([?DO,?ECHO], Sock) ->
+ R = [?IAC,?WILL,?ECHO],
+ cmd_dbg(R),
+ gen_tcp:send(Sock, R);
+
+%% Answers from server
+
+respond_cmd([?WILL,?SUPPRESS_GO_AHEAD], _Sock) ->
+ dbg("Server will suppress-go-ahead\n", []);
+
+respond_cmd([?WONT,?SUPPRESS_GO_AHEAD], _Sock) ->
+ dbg("Warning! Server won't suppress-go-ahead\n", []);
+
+respond_cmd([?DONT | _Opt], _Sock) -> % server ack?
+ ok;
+respond_cmd([?WONT | _Opt], _Sock) -> % server ack?
+ ok;
+
+%% Negative responses (WON'T and DON'T). These are default!
+
+respond_cmd([?WILL,Opt], Sock) ->
+ R = [?IAC,?DONT,Opt],
+ cmd_dbg(R),
+ gen_tcp:send(Sock, R);
+
+respond_cmd([?DO | Opt], Sock) ->
+ R = [?IAC,?WONT | Opt],
+ cmd_dbg(R),
+ gen_tcp:send(Sock, R);
+
+%% Unexpected messages.
+
+respond_cmd([Cmd | Opt], _Sock) when Cmd >= 240, Cmd =< 255 ->
+ dbg("Received cmd: ~w. Ignored!\n", [[Cmd | Opt]]);
+
+respond_cmd([Cmd | Opt], _Sock) ->
+ dbg("WARNING: Received unknown cmd: ~w. Ignored!\n", [[Cmd | Opt]]).
+
+
+get_cmd([Cmd | Rest]) when Cmd == ?SB ->
+ get_subcmd(Rest, []);
+
+get_cmd([Cmd,Opt | Rest]) ->
+ {[Cmd,Opt], Rest};
+
+get_cmd(_Other) ->
+ error.
+
+get_subcmd([?SE | Rest], Acc) ->
+ {[?SE | lists:reverse(Acc)], Rest};
+
+get_subcmd([Opt | Rest], Acc) ->
+ get_subcmd(Rest, [Opt | Acc]).
+
+
+dbg(_Str,_Args) -> ok.
+% if ?DBG -> io:format(_Str,_Args);
+% true -> ok
+% end.
+
+cmd_dbg(_Cmd) -> ok.
+% if ?DBG ->
+% case _Cmd of
+% [?IAC|Cmd1] ->
+% cmd_dbg(Cmd1);
+% [Ctrl|Opts] ->
+% CtrlStr =
+% case Ctrl of
+% ?DO -> "DO";
+% ?DONT -> "DONT";
+% ?WILL -> "WILL";
+% ?WONT -> "WONT";
+% _ -> "CMD"
+% end,
+% Opts1 =
+% case Opts of
+% [Opt] -> Opt;
+% _ -> Opts
+% end,
+% io:format("~s(~w): ~w\n", [CtrlStr,Ctrl,Opts1]);
+% Any ->
+% io:format("Unexpected in cmd_dbg:~n~w~n",[Any])
+% end;
+% true -> ok
+% end.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl
new file mode 100644
index 0000000000..21a2f82a54
--- /dev/null
+++ b/lib/common_test/src/ct_testspec.erl
@@ -0,0 +1,780 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Common Test Framework functions handlig test specifikations.
+%%%
+%%% <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]).
+
+-include("ct_util.hrl").
+
+%%%------------------------------------------------------------------
+%%% NOTE:
+%%% Multiple testspecs may be used as input with the result that
+%%% the data is merged. It's in this case up to the user to ensure
+%%% there are no clashes in any "global" variables, such as logdir.
+%%%-------------------------------------------------------------------
+
+%%%-------------------------------------------------------------------
+%%% prepare_tests/2 compiles the testspec data into a list of tests
+%%% to be run and a list of tests to be skipped, either for one
+%%% particular node or for all nodes.
+%%%-------------------------------------------------------------------
+
+%%%-------------------------------------------------------------------
+%%% Version 1 - extract and return all tests and skips for Node
+%%% (incl all_nodes)
+%%%-------------------------------------------------------------------
+prepare_tests(TestSpec,Node) when is_record(TestSpec,testspec), is_atom(Node) ->
+ case lists:keysearch(Node,1,prepare_tests(TestSpec)) of
+ {value,{Node,Run,Skip}} ->
+ {Run,Skip};
+ false ->
+ {[],[]}
+ end.
+
+%%%-------------------------------------------------------------------
+%%% Version 2 - create and return a list of {Node,Run,Skip} tuples,
+%%% one for each node specified in the test specification.
+%%% The tuples in the Run list will have the form {Dir,Suites,Cases}
+%%% and the tuples in the Skip list will have the form
+%%% {Dir,Suites,Comment} or {Dir,Suite,Cases,Comment}.
+%%%-------------------------------------------------------------------
+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),
+ %% Get all Skip entries sorted per node basis.
+ NodeList2 = skip_per_node(Skip,NodeList1),
+ %% Change representation.
+ Result=
+ lists:map(fun({Node,{Run1,Skip1}}) ->
+ Run2 = lists:map(fun({D,{Ss,Cs}}) ->
+ {D,Ss,Cs}
+ end, Run1),
+ Skip2 = lists:map(fun({D,{Ss,Cmt}}) ->
+ {D,Ss,Cmt};
+ ({D,{S,Cs,Cmt}}) ->
+ {D,S,Cs,Cmt}
+ end, Skip1),
+ {Node,Run2,Skip2}
+ end, NodeList2),
+ Result.
+
+%% run_per_node/2 takes the Run list as input and returns a list
+%% of {Node,RunPerNode,[]} tuples where the tests have been sorted
+%% on a per node basis.
+run_per_node([{{Node,Dir},Test}|Ts],Result) ->
+ {value,{Node,{Run,Skip}}} = lists:keysearch(Node,1,Result),
+ Run1 = merge_tests(Dir,Test,Run),
+ run_per_node(Ts,insert_in_order({Node,{Run1,Skip}},Result));
+run_per_node([],Result) ->
+ Result.
+
+merge_tests(Dir,Test={all,_},TestDirs) ->
+ %% overwrite all previous entries for Dir
+ TestDirs1 = lists:filter(fun({D,_}) when D==Dir ->
+ false;
+ (_) ->
+ true
+ end,TestDirs),
+ insert_in_order({Dir,Test},TestDirs1);
+merge_tests(Dir,Test={Suite,all},TestDirs) ->
+ TestDirs1 = lists:filter(fun({D,{S,_}}) when D==Dir,S==Suite ->
+ false;
+ (_) ->
+ true
+ end,TestDirs),
+ TestDirs1++[{Dir,Test}];
+merge_tests(Dir,Test,TestDirs) ->
+ merge_suites(Dir,Test,TestDirs).
+
+merge_suites(Dir,{Suite,Cases},[{Dir,{Suite,Cases0}}|Dirs]) ->
+ Cases1 = insert_in_order(Cases,Cases0),
+ [{Dir,{Suite,Cases1}}|Dirs];
+merge_suites(Dir,Test,[Other|Dirs]) ->
+ [Other|merge_suites(Dir,Test,Dirs)];
+merge_suites(Dir,Test,[]) ->
+ [{Dir,Test}].
+
+%% skip_per_node/2 takes the Skip list as input and returns a list
+%% of {Node,RunPerNode,SkipPerNode} tuples where the skips have been
+%% sorted on a per node basis.
+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([],Result) ->
+ Result.
+
+%% get_run_and_skip/3 takes a list of test terms as input and sorts
+%% them into a list of Run tests and a list of Skip entries. The
+%% elements all have the form
+%%
+%% {{Node,Dir},TestData}
+%%
+%% TestData has the form:
+%%
+%% Run entry: {Suite,Cases}
+%%
+%% Skip entry: {Suites,Comment} or {Suite,Cases,Comment}
+%%
+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
+ Skipped = get_skipped_suites(Node,TestDir,Suites),
+ %% note: this adds an 'all' test even if only skip is specified,
+ %% probably a good thing cause it gets logged as skipped then
+ get_run_and_skip(Tests,
+ [[{{Node,TestDir},{all,all}}]|Run],
+ [Skipped|Skip]);
+ false ->
+ {R,S} = prepare_suites(Node,TestDir,Suites,[],[]),
+ get_run_and_skip(Tests,[R|Run],[S|Skip])
+ end;
+get_run_and_skip([],Run,Skip) ->
+ {lists:flatten(lists:reverse(Run)),
+ lists:flatten(lists:reverse(Skip))}.
+
+prepare_suites(Node,Dir,[{Suite,Cases}|Suites],Run,Skip) ->
+ case lists:member(all,Cases) of
+ true -> % all Cases in Suite
+ Skipped = get_skipped_cases(Node,Dir,Suite,Cases),
+ %% note: this adds an 'all' test even if only skip is specified
+ prepare_suites(Node,Dir,Suites,
+ [[{{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])
+ end;
+prepare_suites(_Node,_Dir,[],Run,Skip) ->
+ {lists:flatten(lists:reverse(Run)),
+ lists:flatten(lists:reverse(Skip))}.
+
+prepare_cases(Node,Dir,Suite,Cases) ->
+ case get_skipped_cases(Node,Dir,Suite,Cases) of
+ SkipAll=[{{Node,Dir},{Suite,_Cmt}}] -> % all cases to be skipped
+ %% note: this adds an 'all' test even if only skip is specified
+ {[{{Node,Dir},{Suite,all}}],SkipAll};
+ Skipped ->
+ %% note: this adds a test even if only skip is specified
+ PrepC = lists:foldr(fun({C,{skip,_Cmt}},Acc) ->
+ case lists:member(C,Cases) of
+ true ->
+ Acc;
+ false ->
+ [C|Acc]
+ end;
+ (C,Acc) -> [C|Acc]
+ end, [], Cases),
+ {{{Node,Dir},{Suite,PrepC}},Skipped}
+ end.
+
+get_skipped_suites(Node,Dir,Suites) ->
+ lists:flatten(get_skipped_suites1(Node,Dir,Suites)).
+
+get_skipped_suites1(Node,Dir,[{Suite,Cases}|Suites]) ->
+ SkippedCases = get_skipped_cases(Node,Dir,Suite,Cases),
+ [SkippedCases|get_skipped_suites1(Node,Dir,Suites)];
+get_skipped_suites1(_,_,[]) ->
+ [].
+
+get_skipped_cases(Node,Dir,Suite,Cases) ->
+ case lists:keysearch(all,1,Cases) of
+ {value,{all,{skip,Cmt}}} ->
+ [{{Node,Dir},{Suite,Cmt}}];
+ false ->
+ get_skipped_cases1(Node,Dir,Suite,Cases)
+ end.
+
+get_skipped_cases1(Node,Dir,Suite,[{Case,{skip,Cmt}}|Cs]) ->
+ [{{Node,Dir},{Suite,Case,Cmt}}|get_skipped_cases1(Node,Dir,Suite,Cs)];
+get_skipped_cases1(Node,Dir,Suite,[_Case|Cs]) ->
+ get_skipped_cases1(Node,Dir,Suite,Cs);
+get_skipped_cases1(_,_,_,[]) ->
+ [].
+
+%%% collect_tests_from_file reads a testspec file and returns a record
+%%% containing the data found.
+collect_tests_from_file(Specs, Relaxed) ->
+ collect_tests_from_file(Specs,[node()],Relaxed).
+
+collect_tests_from_file(Specs,Nodes,Relaxed) when is_list(Nodes) ->
+ NodeRefs = lists:map(fun(N) -> {undefined,N} end, Nodes),
+ catch collect_tests_from_file1(Specs,#testspec{nodes=NodeRefs},Relaxed).
+
+collect_tests_from_file1([Spec|Specs],TestSpec,Relaxed) ->
+ case file:consult(Spec) of
+ {ok,Terms} ->
+ TestSpec1 = collect_tests(Terms,TestSpec,Relaxed),
+ collect_tests_from_file1(Specs,TestSpec1,Relaxed);
+ {error,Reason} ->
+ throw({error,{Spec,Reason}})
+ end;
+collect_tests_from_file1([],TS=#testspec{config=Cfgs,event_handler=EvHs,
+ include=Incl,tests=Tests},_) ->
+ TS#testspec{config=lists:reverse(Cfgs),
+ event_handler=lists:reverse(EvHs),
+ include=lists:reverse(Incl),
+ tests=lists:flatten(Tests)}.
+
+collect_tests_from_list(Terms,Relaxed) ->
+ collect_tests_from_list(Terms,[node()],Relaxed).
+
+collect_tests_from_list(Terms,Nodes,Relaxed) when is_list(Nodes) ->
+ NodeRefs = lists:map(fun(N) -> {undefined,N} end, Nodes),
+ case catch collect_tests(Terms,#testspec{nodes=NodeRefs},Relaxed) of
+ E = {error,_} ->
+ E;
+ TS ->
+ #testspec{config=Cfgs,event_handler=EvHs,include=Incl,tests=Tests} = TS,
+ TS#testspec{config=lists:reverse(Cfgs),
+ event_handler=lists:reverse(EvHs),
+ include=lists:reverse(Incl),
+ tests=lists:flatten(Tests)}
+ end.
+
+collect_tests(Terms,TestSpec,Relaxed) ->
+ put(relaxed,Relaxed),
+ TestSpec1 = get_global(Terms,TestSpec),
+ TestSpec2 = get_all_nodes(Terms,TestSpec1),
+ add_tests(Terms,TestSpec2).
+
+get_global([{alias,Ref,Dir}|Ts],Spec=#testspec{alias=Refs}) ->
+ get_global(Ts,Spec#testspec{alias=[{Ref,get_absname(Dir)}|Refs]});
+get_global([{node,Ref,Node}|Ts],Spec=#testspec{nodes=Refs}) ->
+ get_global(Ts,Spec#testspec{nodes=[{Ref,Node}|lists:keydelete(Node,2,Refs)]});
+get_global([_|Ts],Spec) -> get_global(Ts,Spec);
+get_global([],Spec) -> Spec.
+
+get_absname(TestDir) ->
+ AbsName = filename:absname(TestDir),
+ TestDirName = filename:basename(AbsName),
+ Path = filename:dirname(AbsName),
+ TopDir = filename:basename(Path),
+ Path1 =
+ case TopDir of
+ "." ->
+ [_|Rev] = lists:reverse(filename:split(Path)),
+ filename:join(lists:reverse(Rev));
+ ".." ->
+ [_,_|Rev] = lists:reverse(filename:split(Path)),
+ filename:join(lists:reverse(Rev));
+ _ ->
+ Path
+ end,
+ filename:join(Path1,TestDirName).
+
+%% go through all tests and register all nodes found
+get_all_nodes([{suites,Nodes,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{suites,Node,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([{cases,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{cases,Node,_,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([{skip_suites,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{skip_suites,Node,_,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([{skip_cases,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{skip_cases,Node,_,_,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([_|Ts],Spec) ->
+ get_all_nodes(Ts,Spec);
+get_all_nodes([],Spec) ->
+ Spec.
+
+save_nodes(Nodes,Spec=#testspec{nodes=NodeRefs}) ->
+ NodeRefs1 =
+ lists:foldr(fun(all_nodes,NR) ->
+ NR;
+ (Node,NR) ->
+ case lists:keymember(Node,1,NR) of
+ true ->
+ NR;
+ false ->
+ case lists:keymember(Node,2,NR) of
+ true ->
+ NR;
+ false ->
+ [{undefined,Node}|NR]
+ end
+ end
+ end,NodeRefs,Nodes),
+ Spec#testspec{nodes=NodeRefs1}.
+
+list_nodes(#testspec{nodes=NodeRefs}) ->
+ lists:map(fun({_Ref,Node}) -> Node end, NodeRefs).
+
+%% Associate a "global" logdir with all nodes
+%% except those with specific logdir, e.g:
+%% ["/tmp/logdir",{ct1@finwe,"/tmp/logdir2"}]
+%% means all nodes should write to /tmp/logdir
+%% except ct1@finwe that should use /tmp/logdir2.
+
+%% --- logdir ---
+add_tests([{logdir,all_nodes,Dir}|Ts],Spec) ->
+ Dirs = Spec#testspec.logdir,
+ Tests = [{logdir,N,Dir} || N <- list_nodes(Spec),
+ lists:keymember(ref2node(N,Spec#testspec.nodes),
+ 1,Dirs) == false],
+ add_tests(Tests++Ts,Spec);
+add_tests([{logdir,Nodes,Dir}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,logdir,[Dir],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{logdir,Node,Dir}|Ts],Spec) ->
+ Dirs = Spec#testspec.logdir,
+ Dirs1 = [{ref2node(Node,Spec#testspec.nodes),Dir} |
+ lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,Dirs)],
+ add_tests(Ts,Spec#testspec{logdir=Dirs1});
+add_tests([{logdir,Dir}|Ts],Spec) ->
+ add_tests([{logdir,all_nodes,Dir}|Ts],Spec);
+
+%% --- cover ---
+add_tests([{cover,all_nodes,File}|Ts],Spec) ->
+ Tests = lists:map(fun(N) -> {cover,N,File} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{cover,Nodes,File}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,cover,[File],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{cover,Node,File}|Ts],Spec) ->
+ CoverFs = Spec#testspec.cover,
+ CoverFs1 = [{ref2node(Node,Spec#testspec.nodes),File} |
+ lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,CoverFs)],
+ add_tests(Ts,Spec#testspec{cover=CoverFs1});
+add_tests([{cover,File}|Ts],Spec) ->
+ add_tests([{cover,all_nodes,File}|Ts],Spec);
+
+%% --- config ---
+add_tests([{config,all_nodes,Files}|Ts],Spec) ->
+ Tests = lists:map(fun(N) -> {config,N,Files} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{config,Nodes,Files}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,config,[Files],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{config,Node,[F|Fs]}|Ts],Spec) when is_list(F) ->
+ Cfgs = Spec#testspec.config,
+ Node1 = ref2node(Node,Spec#testspec.nodes),
+ add_tests([{config,Node,Fs}|Ts],Spec#testspec{config=[{Node1,F}|Cfgs]});
+add_tests([{config,_Node,[]}|Ts],Spec) ->
+ add_tests(Ts,Spec);
+add_tests([{config,Node,F}|Ts],Spec) ->
+ add_tests([{config,Node,[F]}|Ts],Spec);
+add_tests([{config,Files}|Ts],Spec) ->
+ add_tests([{config,all_nodes,Files}|Ts],Spec);
+
+%% --- event_handler ---
+add_tests([{event_handler,all_nodes,Hs}|Ts],Spec) ->
+ Tests = lists:map(fun(N) -> {event_handler,N,Hs,[]} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{event_handler,all_nodes,Hs,Args}|Ts],Spec) when is_list(Args) ->
+ Tests = lists:map(fun(N) -> {event_handler,N,Hs,Args} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{event_handler,Hs}|Ts],Spec) ->
+ add_tests([{event_handler,all_nodes,Hs,[]}|Ts],Spec);
+add_tests([{event_handler,HsOrNodes,HsOrArgs}|Ts],Spec) ->
+ case is_noderef(HsOrNodes,Spec#testspec.nodes) of
+ true -> % HsOrNodes == Nodes, HsOrArgs == Hs
+ case {HsOrNodes,HsOrArgs} of
+ {Nodes,Hs} when is_list(Nodes) ->
+ Ts1 = separate(Nodes,event_handler,[Hs,[]],Ts,
+ Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+ {_Node,[]} ->
+ add_tests(Ts,Spec);
+ {Node,HOrHs} ->
+ EvHs = Spec#testspec.event_handler,
+ Node1 = ref2node(Node,Spec#testspec.nodes),
+ case HOrHs of
+ [H|Hs] when is_atom(H) ->
+ add_tests([{event_handler,Node,Hs}|Ts],
+ Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]});
+ H when is_atom(H) ->
+ add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]})
+ end
+ end;
+ false -> % HsOrNodes == Hs, HsOrArgs == Args
+ add_tests([{event_handler,all_nodes,HsOrNodes,HsOrArgs}|Ts],Spec)
+ end;
+add_tests([{event_handler,Nodes,Hs,Args}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,event_handler,[Hs,Args],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{event_handler,Node,[H|Hs],Args}|Ts],Spec) when is_atom(H) ->
+ EvHs = Spec#testspec.event_handler,
+ Node1 = ref2node(Node,Spec#testspec.nodes),
+ add_tests([{event_handler,Node,Hs,Args}|Ts],
+ Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]});
+add_tests([{event_handler,_Node,[],_Args}|Ts],Spec) ->
+ add_tests(Ts,Spec);
+add_tests([{event_handler,Node,H,Args}|Ts],Spec) when is_atom(H) ->
+ EvHs = Spec#testspec.event_handler,
+ Node1 = ref2node(Node,Spec#testspec.nodes),
+ add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]});
+
+%% --- include ---
+add_tests([{include,all_nodes,InclDirs}|Ts],Spec) ->
+ Tests = lists:map(fun(N) -> {include,N,InclDirs} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{include,Nodes,InclDirs}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,include,[InclDirs],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{include,Node,[D|Ds]}|Ts],Spec) when is_list(D) ->
+ Dirs = Spec#testspec.include,
+ Node1 = ref2node(Node,Spec#testspec.nodes),
+ add_tests([{include,Node,Ds}|Ts],Spec#testspec{include=[{Node1,D}|Dirs]});
+add_tests([{include,_Node,[]}|Ts],Spec) ->
+ add_tests(Ts,Spec);
+add_tests([{include,Node,D}|Ts],Spec) ->
+ add_tests([{include,Node,[D]}|Ts],Spec);
+add_tests([{include,InclDirs}|Ts],Spec) ->
+ add_tests([{include,all_nodes,InclDirs}|Ts],Spec);
+
+%% --- suites ---
+add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec) ->
+ add_tests([{suites,list_nodes(Spec),Dir,Ss}|Ts],Spec);
+add_tests([{suites,Dir,Ss}|Ts],Spec) ->
+ add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec);
+add_tests([{suites,Nodes,Dir,Ss}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,suites,[Dir,Ss],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{suites,Node,Dir,Ss}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = insert_suites(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Ss,Tests),
+ add_tests(Ts,Spec#testspec{tests=Tests1});
+
+%% --- cases ---
+add_tests([{cases,all_nodes,Dir,Suite,Cs}|Ts],Spec) ->
+ add_tests([{cases,list_nodes(Spec),Dir,Suite,Cs}|Ts],Spec);
+add_tests([{cases,Dir,Suite,Cs}|Ts],Spec) ->
+ add_tests([{cases,all_nodes,Dir,Suite,Cs}|Ts],Spec);
+add_tests([{cases,Nodes,Dir,Suite,Cs}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,cases,[Dir,Suite,Cs],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{cases,Node,Dir,Suite,Cs}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = insert_cases(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Suite,Cs,Tests),
+ add_tests(Ts,Spec#testspec{tests=Tests1});
+
+%% --- skip_suites ---
+add_tests([{skip_suites,all_nodes,Dir,Ss,Cmt}|Ts],Spec) ->
+ add_tests([{skip_suites,list_nodes(Spec),Dir,Ss,Cmt}|Ts],Spec);
+add_tests([{skip_suites,Dir,Ss,Cmt}|Ts],Spec) ->
+ add_tests([{skip_suites,all_nodes,Dir,Ss,Cmt}|Ts],Spec);
+add_tests([{skip_suites,Nodes,Dir,Ss,Cmt}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,skip_suites,[Dir,Ss,Cmt],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = skip_suites(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Ss,Cmt,Tests),
+ add_tests(Ts,Spec#testspec{tests=Tests1});
+
+%% --- skip_cases ---
+add_tests([{skip_cases,all_nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) ->
+ add_tests([{skip_cases,list_nodes(Spec),Dir,Suite,Cs,Cmt}|Ts],Spec);
+add_tests([{skip_cases,Dir,Suite,Cs,Cmt}|Ts],Spec) ->
+ add_tests([{skip_cases,all_nodes,Dir,Suite,Cs,Cmt}|Ts],Spec);
+add_tests([{skip_cases,Nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,skip_cases,[Dir,Suite,Cs,Cmt],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{skip_cases,Node,Dir,Suite,Cs,Cmt}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = skip_cases(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Suite,Cs,Cmt,Tests),
+ add_tests(Ts,Spec#testspec{tests=Tests1});
+
+%% --- handled/errors ---
+add_tests([{alias,_,_}|Ts],Spec) -> % handled
+ add_tests(Ts,Spec);
+
+add_tests([{node,_,_}|Ts],Spec) -> % handled
+ add_tests(Ts,Spec);
+
+%% check if it's a CT term that has bad format or if the user seems to
+%% have added something of his/her own, which we'll let pass if relaxed
+%% mode is enabled.
+add_tests([Other|Ts],Spec) when is_tuple(Other) ->
+ [Name|_] = tuple_to_list(Other),
+ case lists:keymember(Name,1,valid_terms()) of
+ true -> % halt
+ throw({error,{bad_term_in_spec,Other}});
+ false -> % ignore
+ case get(relaxed) of
+ true ->
+ %% warn if name resembles a CT term
+ case resembles_ct_term(Name,size(Other)) of
+ true ->
+ io:format("~nSuspicious term, please check:~n"
+ "~p~n", [Other]);
+ false ->
+ ok
+ end,
+ add_tests(Ts,Spec);
+ false ->
+ throw({error,{undefined_term_in_spec,Other}})
+ end
+ end;
+
+add_tests([Other|Ts],Spec) ->
+ case get(relaxed) of
+ true ->
+ add_tests(Ts,Spec);
+ false ->
+ throw({error,{undefined_term_in_spec,Other}})
+ end;
+
+add_tests([],Spec) -> % done
+ Spec.
+
+separate(Nodes,Tag,Data,Tests,Refs) ->
+ Separated = separate(Nodes,Tag,Data,Refs),
+ Separated ++ Tests.
+separate([N|Ns],Tag,Data,Refs) ->
+ [list_to_tuple([Tag,ref2node(N,Refs)|Data])|separate(Ns,Tag,Data,Refs)];
+separate([],_,_,_) ->
+ [].
+
+
+%% Representation:
+%% {{Node,Dir},[{Suite1,[case11,case12,...]},{Suite2,[case21,case22,...]},...]}
+%% {{Node,Dir},[{Suite1,{skip,Cmt}},{Suite2,[{case21,{skip,Cmt}},case22,...]},...]}
+
+insert_suites(Node,Dir,[S|Ss],Tests) ->
+ Tests1 = insert_cases(Node,Dir,S,all,Tests),
+ insert_suites(Node,Dir,Ss,Tests1);
+insert_suites(_Node,_Dir,[],Tests) ->
+ Tests;
+insert_suites(Node,Dir,S,Tests) ->
+ insert_suites(Node,Dir,[S],Tests).
+
+insert_cases(Node,Dir,Suite,Cases,Tests) when is_list(Cases) ->
+ case lists:keysearch({Node,Dir},1,Tests) of
+ {value,{{Node,Dir},[{all,_}]}} ->
+ Tests;
+ {value,{{Node,Dir},Suites0}} ->
+ Suites1 = insert_cases1(Suite,Cases,Suites0),
+ insert_in_order({{Node,Dir},Suites1},Tests);
+ false ->
+ insert_in_order({{Node,Dir},[{Suite,Cases}]},Tests)
+ end;
+insert_cases(Node,Dir,Suite,Case,Tests) when is_atom(Case) ->
+ insert_cases(Node,Dir,Suite,[Case],Tests).
+
+insert_cases1(_Suite,_Cases,all) ->
+ all;
+insert_cases1(Suite,Cases,Suites0) ->
+ case lists:keysearch(Suite,1,Suites0) of
+ {value,{Suite,all}} ->
+ Suites0;
+ {value,{Suite,Cases0}} ->
+ Cases1 = insert_in_order(Cases,Cases0),
+ insert_in_order({Suite,Cases1},Suites0);
+ false ->
+ insert_in_order({Suite,Cases},Suites0)
+ end.
+
+skip_suites(Node,Dir,[S|Ss],Cmt,Tests) ->
+ Tests1 = skip_cases(Node,Dir,S,all,Cmt,Tests),
+ skip_suites(Node,Dir,Ss,Cmt,Tests1);
+skip_suites(_Node,_Dir,[],_Cmt,Tests) ->
+ Tests;
+skip_suites(Node,Dir,S,Cmt,Tests) ->
+ skip_suites(Node,Dir,[S],Cmt,Tests).
+
+skip_cases(Node,Dir,Suite,Cases,Cmt,Tests) when is_list(Cases) ->
+ Suites =
+ case lists:keysearch({Node,Dir},1,Tests) of
+ {value,{{Node,Dir},Suites0}} ->
+ Suites0;
+ false ->
+ []
+ end,
+ Suites1 = skip_cases1(Suite,Cases,Cmt,Suites),
+ insert_in_order({{Node,Dir},Suites1},Tests);
+skip_cases(Node,Dir,Suite,Case,Cmt,Tests) when is_atom(Case) ->
+ skip_cases(Node,Dir,Suite,[Case],Cmt,Tests).
+
+skip_cases1(Suite,Cases,Cmt,Suites0) ->
+ SkipCases = lists:map(fun(C) ->
+ {C,{skip,Cmt}}
+ end,Cases),
+ case lists:keysearch(Suite,1,Suites0) of
+ {value,{Suite,Cases0}} ->
+ Cases1 = Cases0 ++ SkipCases,
+ insert_in_order({Suite,Cases1},Suites0);
+ false ->
+ insert_in_order({Suite,SkipCases},Suites0)
+ end.
+
+insert_in_order([E|Es],List) ->
+ List1 = insert_elem(E,List,[]),
+ insert_in_order(Es,List1);
+insert_in_order([],List) ->
+ List;
+insert_in_order(E,List) ->
+ insert_elem(E,List,[]).
+
+%% replace an existing entry (same key) or add last in list
+insert_elem({Key,_}=E,[{Key,_}|Rest],SoFar) ->
+ lists:reverse([E|SoFar]) ++ Rest;
+insert_elem({E,_},[E|Rest],SoFar) ->
+ lists:reverse([E|SoFar]) ++ Rest;
+insert_elem(E,[E|Rest],SoFar) ->
+ lists:reverse([E|SoFar]) ++ Rest;
+insert_elem(E,[E1|Rest],SoFar) ->
+ insert_elem(E,Rest,[E1|SoFar]);
+insert_elem(E,[],SoFar) ->
+ lists:reverse([E|SoFar]).
+
+ref2node(all_nodes,_Refs) ->
+ all_nodes;
+ref2node(master,_Refs) ->
+ master;
+ref2node(RefOrNode,Refs) ->
+ case string:chr(atom_to_list(RefOrNode),$@) of
+ 0 -> % a ref
+ case lists:keysearch(RefOrNode,1,Refs) of
+ {value,{RefOrNode,Node}} ->
+ Node;
+ false ->
+ throw({error,{noderef_missing,RefOrNode}})
+ end;
+ _ -> % a node
+ RefOrNode
+ end.
+
+ref2dir(Ref,Refs) when is_atom(Ref) ->
+ case lists:keysearch(Ref,1,Refs) of
+ {value,{Ref,Dir}} ->
+ Dir;
+ false ->
+ throw({error,{alias_missing,Ref}})
+ end;
+ref2dir(Dir,_) when is_list(Dir) ->
+ Dir.
+
+is_noderef(What,Nodes) when is_atom(What) ->
+ is_noderef([What],Nodes);
+is_noderef([master|_],_Nodes) ->
+ true;
+is_noderef([What|_],Nodes) ->
+ case lists:keymember(What,1,Nodes) or
+ lists:keymember(What,2,Nodes) of
+ true ->
+ true;
+ false ->
+ false
+ end;
+is_noderef([],_) ->
+ false.
+
+valid_terms() ->
+ [
+ {node,3},
+ {cover,2},
+ {cover,3},
+ {config,2},
+ {config,3},
+ {alias,3},
+ {logdir,2},
+ {logdir,3},
+ {event_handler,2},
+ {event_handler,3},
+ {event_handler,4},
+ {include,2},
+ {include,3},
+
+ {suites,3},
+ {suites,4},
+ {cases,4},
+ {cases,5},
+ {skip_suites,4},
+ {skip_suites,5},
+ {skip_cases,5},
+ {skip_cases,6}
+ ].
+
+%% this function "guesses" if the user has misspelled a term name
+resembles_ct_term(Name,Size) when is_atom(Name) ->
+ resembles_ct_term2(atom_to_list(Name),Size);
+resembles_ct_term(_Name,_) ->
+ false.
+
+resembles_ct_term2(Name,Size) when length(Name) > 3 ->
+ CTTerms = [{atom_to_list(Tag),Sz} || {Tag,Sz} <- valid_terms()],
+ compare_names(Name,Size,CTTerms);
+resembles_ct_term2(_,_) ->
+ false.
+
+compare_names(Name,Size,[{Term,Sz}|Ts]) ->
+ if abs(Size-Sz) > 0 ->
+ compare_names(Name,Size,Ts);
+ true ->
+ Diff = abs(length(Name)-length(Term)),
+ if Diff > 1 ->
+ compare_names(Name,Size,Ts);
+ true ->
+ Common = common_letters(Name,Term,0),
+ Bad = abs(length(Name)-Common),
+ if Bad > 2 ->
+ compare_names(Name,Size,Ts);
+ true ->
+ true
+ end
+ end
+ end;
+compare_names(_,_,[]) ->
+ false.
+
+common_letters(_,[],Count) ->
+ Count;
+common_letters([L|Ls],Term,Count) ->
+ case lists:member(L,Term) of
+ true ->
+ Term1 = lists:delete(L,Term),
+ common_letters(Ls,Term1,Count+1);
+ false ->
+ common_letters(Ls,Term,Count)
+ end;
+common_letters([],_,Count) ->
+ Count.
+
+
+
+
diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl
new file mode 100644
index 0000000000..ba3d789f8d
--- /dev/null
+++ b/lib/common_test/src/ct_util.erl
@@ -0,0 +1,1303 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Common Test Framework Utilities.
+%%%
+%%% <p>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>
+%%%
+-module(ct_util).
+
+-export([start/0,start/1,start/2,stop/1,update_last_run_index/0]).
+
+-export([register_connection/4,unregister_connection/1,
+ does_connection_exist/3,get_key_from_name/1]).
+
+-export([require/1, require/2, get_config/1, get_config/2, get_config/3,
+ set_default_config/2, set_default_config/3, delete_default_config/1,
+ get_all_config/0, update_config/2,
+ release_allocated/0, close_connections/0]).
+
+-export([save_suite_data/3, save_suite_data/2, read_suite_data/1,
+ delete_suite_data/0, delete_suite_data/1, match_delete_suite_data/1,
+ delete_testdata/0, delete_testdata/1, set_testdata/1, get_testdata/1,
+ update_testdata/2]).
+
+-export([override_silence_all_connections/0, override_silence_connections/1,
+ get_overridden_silenced_connections/0,
+ delete_overridden_silenced_connections/0,
+ silence_all_connections/0, silence_connections/1, is_silenced/1,
+ reset_silent_connections/0]).
+
+-export([set_cwd/1, reset_cwd/0]).
+
+-export([parse_table/1]).
+
+-export([listenv/1]).
+
+-export([get_target_name/1, get_connections/2]).
+
+-export([is_test_dir/1, get_testdir/2]).
+
+-export([encrypt_config_file/2, encrypt_config_file/3,
+ decrypt_config_file/2, decrypt_config_file/3]).
+
+-export([kill_attached/2, get_attached/1]).
+
+-export([warn_duplicates/1]).
+
+-include("ct_event.hrl").
+-include("ct_util.hrl").
+
+-record(ct_conf,{key,value,ref,name='_UNDEF',default=false}).
+%% default = {true,suite} | {true,testcase} | false
+
+-record(suite_data, {key,name,value}).
+
+-define(cryptfile, ".ct_config.crypt").
+
+%%%-----------------------------------------------------------------
+%%% @spec start(Mode) -> Pid | exit(Error)
+%%% Mode = normal | interactive
+%%% Pid = pid()
+%%%
+%%% @doc 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>
+%%%
+%%% <p>Returns the process identity of the
+%%% <code>ct_util_server</code>.</p>
+%%%
+%%% @see ct
+start() ->
+ start(normal,".").
+
+start(LogDir) when is_list(LogDir) ->
+ start(normal,LogDir);
+start(Mode) ->
+ start(Mode,".").
+
+start(Mode,LogDir) ->
+ case whereis(ct_util_server) of
+ undefined ->
+ S = self(),
+ Pid = spawn_link(fun() -> do_start(S,Mode,LogDir) end),
+ receive
+ {Pid,started} -> Pid;
+ {Pid,Error} -> exit(Error)
+ end;
+ Pid ->
+ case get_mode() of
+ interactive when Mode==interactive ->
+ Pid;
+ interactive ->
+ {error,interactive_mode};
+ _OtherMode ->
+ Pid
+ end
+ end.
+
+do_start(Parent,Mode,LogDir) ->
+ process_flag(trap_exit,true),
+ register(ct_util_server,self()),
+ create_table(?attr_table,bag,#ct_conf.key),
+ create_table(?conn_table,#conn.handle),
+ create_table(?board_table,2),
+ create_table(?suite_table,#suite_data.key),
+ {ok,StartDir} = file:get_cwd(),
+ case file:set_cwd(LogDir) of
+ ok -> ok;
+ E -> exit(E)
+ end,
+ Opts = case read_opts() of
+ {ok,Opts1} ->
+ Opts1;
+ Error ->
+ Parent ! {self(),Error},
+ exit(Error)
+ end,
+
+ %% start an event manager (if not already started by master)
+ case ct_event:start_link() of
+ {error,{already_started,_}} ->
+ ok;
+ _ ->
+ case whereis(vts) of
+ undefined ->
+ ct_event:add_handler();
+ VtsPid ->
+ ct_event:add_handler([{vts,VtsPid}])
+ end
+ end,
+ case read_config_files(Opts) of
+ ok ->
+ %% add user handlers
+ 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
+ ok -> ok;
+ {'EXIT',Why} -> exit(Why);
+ Other -> exit({event_handler,Other})
+ end
+ end,
+ case catch lists:foreach(Add,Handlers) of
+ {'EXIT',Reason} ->
+ Parent ! {self(),Reason};
+ _ ->
+ ok
+ end;
+ false ->
+ ok
+ end,
+ {StartTime,TestLogDir} = ct_logs:init(Mode),
+ ct_event:notify(#event{name=test_start,
+ node=node(),
+ data={StartTime,
+ lists:flatten(TestLogDir)}}),
+ Parent ! {self(),started},
+ loop(Mode,[],StartDir);
+ ReadError ->
+ Parent ! {self(),ReadError},
+ exit(ReadError)
+ end.
+
+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}]).
+
+read_opts() ->
+ case file:consult(ct_run:variables_file_name("./")) of
+ {ok,Opts} ->
+ {ok,Opts};
+ {error,enoent} ->
+ {error,not_installed};
+ Error ->
+ {error,{bad_installation,Error}}
+ end.
+
+read_config_files(Opts) ->
+ ConfigFiles =
+ lists:foldl(fun({config,Files},Acc) ->
+ Acc ++ Files;
+ (_,Acc) ->
+ Acc
+ end,[],Opts),
+ read_config_files1(ConfigFiles).
+
+read_config_files1([ConfigFile|Files]) ->
+ case file:consult(ConfigFile) of
+ {ok,Config} ->
+ set_config(Config),
+ read_config_files1(Files);
+ {error,enoent} ->
+ {user_error,{config_file_error,ConfigFile,enoent}};
+ {error,Reason} ->
+ Key =
+ case application:get_env(common_test, decrypt) of
+ {ok,KeyOrFile} ->
+ case KeyOrFile of
+ {key,K} ->
+ K;
+ {file,F} ->
+ get_crypt_key_from_file(F)
+ end;
+ _ ->
+ get_crypt_key_from_file()
+ end,
+ case Key of
+ {error,no_crypt_file} ->
+ {user_error,{config_file_error,ConfigFile,Reason}};
+ {error,CryptError} ->
+ {user_error,{decrypt_file_error,ConfigFile,CryptError}};
+ _ when is_list(Key) ->
+ case decrypt_config_file(ConfigFile, undefined, {key,Key}) of
+ {ok,CfgBin} ->
+ case read_config_terms(CfgBin) of
+ {error,ReadFail} ->
+ {user_error,{config_file_error,ConfigFile,ReadFail}};
+ Config ->
+ set_config(Config),
+ read_config_files1(Files)
+ end;
+ {error,DecryptFail} ->
+ {user_error,{decrypt_config_error,ConfigFile,DecryptFail}}
+ end;
+ _ ->
+ {user_error,{bad_decrypt_key,ConfigFile,Key}}
+ end
+ end;
+read_config_files1([]) ->
+ ok.
+
+read_config_terms(Bin) when is_binary(Bin) ->
+ case catch binary_to_list(Bin) of
+ {'EXIT',_} ->
+ {error,invalid_textfile};
+ Lines ->
+ read_config_terms(Lines)
+ end;
+read_config_terms(Lines) when is_list(Lines) ->
+ read_config_terms1(erl_scan:tokens([], Lines, 0), 1, [], []).
+
+read_config_terms1({done,{ok,Ts,EL},Rest}, L, Terms, _) ->
+ case erl_parse:parse_term(Ts) of
+ {ok,Term} when Rest == [] ->
+ lists:reverse([Term|Terms]);
+ {ok,Term} ->
+ read_config_terms1(erl_scan:tokens([], Rest, 0),
+ EL+1, [Term|Terms], Rest);
+ _ ->
+ {error,{bad_term,{L,EL}}}
+ end;
+read_config_terms1({done,{eof,_},_}, _, Terms, Rest) when Rest == [] ->
+ lists:reverse(Terms);
+read_config_terms1({done,{eof,EL},_}, L, _, _) ->
+ {error,{bad_term,{L,EL}}};
+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
+ [] ->
+ lists:reverse(Terms);
+ _ ->
+ {error,{bad_term,L}}
+ end.
+
+set_default_config(NewConfig, Scope) ->
+ call({set_default_config, {NewConfig, Scope}}).
+
+set_default_config(Name, NewConfig, Scope) ->
+ call({set_default_config, {Name, NewConfig, Scope}}).
+
+delete_default_config(Scope) ->
+ call({delete_default_config, Scope}).
+
+update_config(Name, Config) ->
+ call({update_config, {Name, Config}}).
+
+save_suite_data(Key, Value) ->
+ call({save_suite_data, {Key, undefined, Value}}).
+
+save_suite_data(Key, Name, Value) ->
+ call({save_suite_data, {Key, Name, Value}}).
+
+read_suite_data(Key) ->
+ call({read_suite_data, Key}).
+
+delete_suite_data() ->
+ call({delete_suite_data, all}).
+
+delete_suite_data(Key) ->
+ call({delete_suite_data, Key}).
+
+match_delete_suite_data(KeyPat) ->
+ call({match_delete_suite_data, KeyPat}).
+
+delete_testdata() ->
+ call(delete_testdata).
+
+delete_testdata(Key) ->
+ call({delete_testdata, Key}).
+
+update_testdata(Key, Fun) ->
+ call({update_testdata, Key, Fun}).
+
+set_testdata(TestData) ->
+ call({set_testdata, TestData}).
+
+get_testdata(Key) ->
+ call({get_testdata, Key}).
+
+set_cwd(Dir) ->
+ call({set_cwd,Dir}).
+
+reset_cwd() ->
+ call(reset_cwd).
+
+loop(Mode,TestData,StartDir) ->
+ receive
+ {update_last_run_index,From} ->
+ ct_logs:make_last_run_index(),
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {{require,Name,Tag,SubTags},From} ->
+ Result = do_require(Name,Tag,SubTags),
+ return(From,Result),
+ loop(Mode,TestData,StartDir);
+ {{set_default_config,{Config,Scope}},From} ->
+ set_config(Config,{true,Scope}),
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {{set_default_config,{Name,Config,Scope}},From} ->
+ set_config(Name,Config,{true,Scope}),
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {{delete_default_config,Scope},From} ->
+ delete_config({true,Scope}),
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {{update_config,{Name,NewConfig}},From} ->
+ update_conf(Name,NewConfig),
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {{save_suite_data,{Key,Name,Value}},From} ->
+ ets:insert(?suite_table, #suite_data{key=Key,
+ name=Name,
+ value=Value}),
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {{read_suite_data,Key},From} ->
+ case ets:lookup(?suite_table, Key) of
+ [#suite_data{key=Key,name=undefined,value=Value}] ->
+ return(From,Value);
+ [#suite_data{key=Key,name=Name,value=Value}] ->
+ return(From,{Name,Value});
+ _ ->
+ return(From,undefined)
+ end,
+ loop(Mode,TestData,StartDir);
+ {{delete_suite_data,Key},From} ->
+ if Key == all ->
+ ets:delete_all_objects(?suite_table);
+ true ->
+ ets:delete(?suite_table, Key)
+ end,
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {{match_delete_suite_data,KeyPat},From} ->
+ ets:match_delete(?suite_table, #suite_data{key=KeyPat,
+ name='_',
+ value='_'}),
+ return(From,ok),
+ loop(Mode,TestData,StartDir);
+ {delete_testdata,From} ->
+ return(From,ok),
+ loop(From,[],StartDir);
+ {{delete_testdata,Key},From} ->
+ TestData1 = lists:keydelete(Key,1,TestData),
+ return(From,ok),
+ loop(From,TestData1,StartDir);
+ {{set_testdata,New = {Key,_Val}},From} ->
+ TestData1 = lists:keydelete(Key,1,TestData),
+ return(From,ok),
+ loop(Mode,[New|TestData1],StartDir);
+ {{get_testdata,Key},From} ->
+ case lists:keysearch(Key,1,TestData) of
+ {value,{Key,Val}} ->
+ return(From,Val);
+ _ ->
+ return(From,undefined)
+ end,
+ loop(From,TestData,StartDir);
+ {{update_testdata,Key,Fun},From} ->
+ TestData1 =
+ case lists:keysearch(Key,1,TestData) of
+ {value,{Key,Val}} ->
+ NewVal = Fun(Val),
+ return(From,NewVal),
+ [{Key,NewVal}|lists:keydelete(Key,1,TestData)];
+ _ ->
+ return(From,undefined),
+ TestData
+ end,
+ loop(From,TestData1,StartDir);
+ {{set_cwd,Dir},From} ->
+ return(From,file:set_cwd(Dir)),
+ loop(From,TestData,StartDir);
+ {reset_cwd,From} ->
+ return(From,file:set_cwd(StartDir)),
+ loop(From,TestData,StartDir);
+ {{stop,How},From} ->
+ Time = calendar:local_time(),
+ ct_event:sync_notify(#event{name=test_done,
+ node=node(),
+ data=Time}),
+ ets:delete(?attr_table),
+ close_connections(ets:tab2list(?conn_table)),
+ ets:delete(?conn_table),
+ ets:delete(?board_table),
+ ets:delete(?suite_table),
+ ct_logs:close(How),
+ file:set_cwd(StartDir),
+ ct_event:stop(),
+ return(From,ok);
+ {get_mode,From} ->
+ return(From,Mode),
+ loop(Mode,TestData,StartDir);
+ {'EXIT',_Pid,normal} ->
+ loop(Mode,TestData,StartDir);
+ {'EXIT',Pid,Reason} ->
+ %% Let process crash in case of error, this shouldn't happen!
+ io:format("\n\nct_util_server got EXIT from ~p: ~p\n\n",
+ [Pid,Reason]),
+ file:set_cwd(StartDir),
+ exit(Reason)
+ end.
+
+
+close_connections([#conn{handle=Handle,callback=CB}|Conns]) ->
+ CB:close(Handle),
+ close_connections(Conns);
+close_connections([]) ->
+ ok.
+
+
+%%%-----------------------------------------------------------------
+%%% @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).
+%%%
+%%% <p>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>
+register_connection(TargetName,Address,Callback,Handle) ->
+ TargetRef =
+ case get_ref_from_name(TargetName) of
+ {ok,Ref} ->
+ Ref;
+ _ ->
+ %% no config name associated with connection,
+ %% use handle for identification instead
+ Handle
+ end,
+ ets:insert(?conn_table,#conn{handle=Handle,
+ targetref=TargetRef,
+ address=Address,
+ callback=Callback}),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec unregister_connection(Handle) -> ok
+%%% Handle = term
+%%%
+%%% @doc Unregister a connection (tool-internal use only).
+%%%
+%%% <p>This function should be called when a registered connection is
+%%% closed. It removes the connection data from the connection
+%%% table.</p>
+unregister_connection(Handle) ->
+ ets:delete(?conn_table,Handle),
+ ok.
+
+
+%%%-----------------------------------------------------------------
+%%% @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.
+does_connection_exist(TargetName,Address,Callback) ->
+ case get_ref_from_name(TargetName) of
+ {ok,TargetRef} ->
+ case ets:select(?conn_table,[{#conn{handle='$1',
+ targetref=TargetRef,
+ address=Address,
+ callback=Callback},
+ [],
+ ['$1']}]) of
+ [Handle] ->
+ {ok,Handle};
+ [] ->
+ false
+ end;
+ _ ->
+ false
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec get_connections(TargetName,Callback) ->
+%%% {ok,Connections} | {error,Reason}
+%%% TargetName = ct:target_name()
+%%% Callback = atom()
+%%% Connections = [Connection]
+%%% Connection = {Handle,Address}
+%%% Handle = term()
+%%% Address = term()
+%%%
+%%% @doc Return all connections for the <code>Callback</code> on the
+%%% given target (<code>TargetName</code>).
+get_connections(TargetName,Callback) ->
+ case get_ref_from_name(TargetName) of
+ {ok,Ref} ->
+ {ok,ets:select(?conn_table,[{#conn{handle='$1',
+ address='$2',
+ targetref=Ref,
+ callback=Callback},
+ [],
+ [{{'$1','$2'}}]}])};
+ Error ->
+ Error
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:get_target_name/1
+get_target_name(ConnPid) ->
+ case ets:select(?conn_table,[{#conn{handle=ConnPid,targetref='$1',_='_'},
+ [],
+ ['$1']}]) of
+ [TargetRef] ->
+ get_name_from_ref(TargetRef);
+ [] ->
+ {error,{unknown_connection,ConnPid}}
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:require/1
+require(Key) when is_atom(Key) ->
+ require({Key,[]});
+require({Key,SubKeys}) when is_atom(Key) ->
+ allocate('_UNDEF',Key,to_list(SubKeys));
+require(Key) ->
+ {error,{invalid,Key}}.
+
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:require/2
+require(Name,Key) when is_atom(Key) ->
+ require(Name,{Key,[]});
+require(Name,{Key,SubKeys}) when is_atom(Name), is_atom(Key) ->
+ call({require,Name,Key,to_list(SubKeys)});
+require(Name,Keys) ->
+ {error,{invalid,{Name,Keys}}}.
+
+to_list(X) when is_list(X) -> X;
+to_list(X) -> [X].
+
+do_require(Name,Key,SubKeys) when is_list(SubKeys) ->
+ case get_key_from_name(Name) of
+ {error,_} ->
+ allocate(Name,Key,SubKeys);
+ {ok,Key} ->
+ %% already allocated - check that it has all required subkeys
+ Vals = [Val || {_Ref,Val} <- lookup_name(Name)],
+ case get_subconfig(SubKeys,Vals) of
+ {ok,_SubMapped} ->
+ ok;
+ Error ->
+ Error
+ end;
+ {ok,OtherKey} ->
+ {error,{name_in_use,Name,OtherKey}}
+ end.
+
+allocate(Name,Key,SubKeys) ->
+ case ets:match_object(?attr_table,#ct_conf{key=Key,name='_UNDEF',_='_'}) of
+ [] ->
+ {error,{not_available,Key}};
+ Available ->
+ case allocate_subconfig(Name,SubKeys,Available,false) of
+ ok ->
+ ok;
+ Error ->
+ Error
+ end
+ end.
+
+allocate_subconfig(Name,SubKeys,[C=#ct_conf{value=Value}|Rest],Found) ->
+ case do_get_config(SubKeys,Value,[]) of
+ {ok,_SubMapped} ->
+ ets:insert(?attr_table,C#ct_conf{name=Name}),
+ allocate_subconfig(Name,SubKeys,Rest,true);
+ _Error ->
+ allocate_subconfig(Name,SubKeys,Rest,Found)
+ end;
+allocate_subconfig(_Name,_SubKeys,[],true) ->
+ ok;
+allocate_subconfig(_Name,SubKeys,[],false) ->
+ {error,{not_available,SubKeys}}.
+
+
+
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:get_config/1
+get_config(KeyOrName) ->
+ get_config(KeyOrName,undefined,[]).
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:get_config/2
+get_config(KeyOrName,Default) ->
+ get_config(KeyOrName,Default,[]).
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:get_config/3
+get_config(KeyOrName,Default,Opts) when is_atom(KeyOrName) ->
+ case lookup_config(KeyOrName) of
+ [] ->
+ Default;
+ [{_Ref,Val}|_] = Vals ->
+ case {lists:member(all,Opts),lists:member(element,Opts)} of
+ {true,true} ->
+ [{KeyOrName,V} || {_R,V} <- lists:sort(Vals)];
+ {true,false} ->
+ [V || {_R,V} <- lists:sort(Vals)];
+ {false,true} ->
+ {KeyOrName,Val};
+ {false,false} ->
+ Val
+ end
+ end;
+
+get_config({KeyOrName,SubKey},Default,Opts) ->
+ case lookup_config(KeyOrName) of
+ [] ->
+ Default;
+ Vals ->
+ Vals1 = case [Val || {_Ref,Val} <- lists:sort(Vals)] of
+ Result=[L|_] when is_list(L) ->
+ case L of
+ [{_,_}|_] ->
+ Result;
+ _ ->
+ []
+ end;
+ _ ->
+ []
+ end,
+ case get_subconfig([SubKey],Vals1,[],Opts) of
+ {ok,[{_,SubVal}|_]=SubVals} ->
+ case {lists:member(all,Opts),lists:member(element,Opts)} of
+ {true,true} ->
+ [{{KeyOrName,SubKey},Val} || {_,Val} <- SubVals];
+ {true,false} ->
+ [Val || {_SubKey,Val} <- SubVals];
+ {false,true} ->
+ {{KeyOrName,SubKey},SubVal};
+ {false,false} ->
+ SubVal
+ end;
+ _ ->
+ Default
+ end
+ end.
+
+
+get_subconfig(SubKeys,Values) ->
+ get_subconfig(SubKeys,Values,[],[]).
+
+get_subconfig(SubKeys,[Value|Rest],Mapped,Opts) ->
+ case do_get_config(SubKeys,Value,[]) of
+ {ok,SubMapped} ->
+ case lists:member(all,Opts) of
+ true ->
+ get_subconfig(SubKeys,Rest,Mapped++SubMapped,Opts);
+ false ->
+ {ok,SubMapped}
+ end;
+ _Error ->
+ get_subconfig(SubKeys,Rest,Mapped,Opts)
+ end;
+get_subconfig(SubKeys,[],[],_) ->
+ {error,{not_available,SubKeys}};
+get_subconfig(_SubKeys,[],Mapped,_) ->
+ {ok,Mapped}.
+
+do_get_config([Key|Required],Available,Mapped) ->
+ case lists:keysearch(Key,1,Available) of
+ {value,{Key,Value}} ->
+ NewAvailable = lists:keydelete(Key,1,Available),
+ NewMapped = [{Key,Value}|Mapped],
+ do_get_config(Required,NewAvailable,NewMapped);
+ false ->
+ {error,{not_available,Key}}
+ end;
+do_get_config([],_Available,Mapped) ->
+ {ok,lists:reverse(Mapped)}.
+
+get_all_config() ->
+ ets:select(?attr_table,[{#ct_conf{name='$1',key='$2',value='$3',
+ default='$4',_='_'},
+ [],
+ [{{'$1','$2','$3','$4'}}]}]).
+
+lookup_config(KeyOrName) ->
+ case lookup_name(KeyOrName) of
+ [] ->
+ lookup_key(KeyOrName);
+ Values ->
+ Values
+ end.
+
+lookup_name(Name) ->
+ ets:select(?attr_table,[{#ct_conf{ref='$1',value='$2',name=Name,_='_'},
+ [],
+ [{{'$1','$2'}}]}]).
+lookup_key(Key) ->
+ ets:select(?attr_table,[{#ct_conf{key=Key,ref='$1',value='$2',name='_UNDEF',_='_'},
+ [],
+ [{{'$1','$2'}}]}]).
+
+set_config(Config) ->
+ set_config('_UNDEF',Config,false).
+
+set_config(Config,Default) ->
+ set_config('_UNDEF',Config,Default).
+
+set_config(Name,Config,Default) ->
+ [ets:insert(?attr_table,
+ #ct_conf{key=Key,value=Val,ref=ct_make_ref(),
+ name=Name,default=Default}) ||
+ {Key,Val} <- Config].
+
+delete_config(Default) ->
+ ets:match_delete(?attr_table,#ct_conf{default=Default,_='_'}),
+ ok.
+
+
+%%%-----------------------------------------------------------------
+%%% @spec release_allocated() -> ok
+%%%
+%%% @doc Release all allocated resources, but don't take down any
+%%% connections.
+release_allocated() ->
+ Allocated = ets:select(?attr_table,[{#ct_conf{name='$1',_='_'},
+ [{'=/=','$1','_UNDEF'}],
+ ['$_']}]),
+ release_allocated(Allocated).
+release_allocated([H|T]) ->
+ ets:delete_object(?attr_table,H),
+ ets:insert(?attr_table,H#ct_conf{name='_UNDEF'}),
+ release_allocated(T);
+release_allocated([]) ->
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+update_conf(Name, NewConfig) ->
+ Old = ets:select(?attr_table,[{#ct_conf{name=Name,_='_'},[],['$_']}]),
+ lists:foreach(fun(OldElem) ->
+ NewElem = OldElem#ct_conf{value=NewConfig},
+ ets:delete_object(?attr_table, OldElem),
+ ets:insert(?attr_table, NewElem)
+ end, Old),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec close_connections() -> ok
+%%%
+%%% @doc Close all open connections.
+close_connections() ->
+ close_connections(ets:tab2list(?conn_table)),
+ ok.
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+override_silence_all_connections() ->
+ Protocols = [telnet,ftp,rpc,snmp],
+ override_silence_connections(Protocols),
+ Protocols.
+
+override_silence_connections(Conns) when is_list(Conns) ->
+ Conns1 = lists:map(fun({C,B}) -> {C,B};
+ (C) -> {C,true}
+ end, Conns),
+ set_testdata({override_silent_connections,Conns1}).
+
+get_overridden_silenced_connections() ->
+ case get_testdata(override_silent_connections) of
+ {error,_} ->
+ undefined;
+ Conns -> % list() or undefined
+ Conns
+ end.
+
+delete_overridden_silenced_connections() ->
+ delete_testdata(override_silent_connections).
+
+silence_all_connections() ->
+ Protocols = [telnet,ftp,rpc,snmp],
+ silence_connections(Protocols),
+ Protocols.
+
+silence_connections(Conn) when is_tuple(Conn) ->
+ silence_connections([Conn]);
+silence_connections(Conn) when is_atom(Conn) ->
+ silence_connections([{Conn,true}]);
+silence_connections(Conns) when is_list(Conns) ->
+ Conns1 = lists:map(fun({C,B}) -> {C,B};
+ (C) -> {C,true}
+ end, Conns),
+ set_testdata({silent_connections,Conns1}).
+
+is_silenced(Conn) ->
+ case get_testdata(silent_connections) of
+ Conns when is_list(Conns) ->
+ case lists:keysearch(Conn,1,Conns) of
+ {value,{Conn,true}} ->
+ true;
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end.
+
+reset_silent_connections() ->
+ delete_testdata(silent_connections).
+
+
+%%%-----------------------------------------------------------------
+%%% @spec stop(How) -> ok
+%%%
+%%% @doc Stop the ct_util_server and close all existing connections
+%%% (tool-internal use only).
+%%%
+%%% @see ct
+stop(How) ->
+ case whereis(ct_util_server) of
+ undefined -> ok;
+ _ -> call({stop,How})
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec update_last_run_index() -> ok
+%%%
+%%% @doc Update <code>ct_run.&lt;timestamp&gt;/index.html</code>
+%%% (tool-internal use only).
+update_last_run_index() ->
+ call(update_last_run_index).
+
+
+%%%-----------------------------------------------------------------
+%%% @spec get_mode() -> Mode
+%%% Mode = normal | interactive
+%%%
+%%% @doc Return the current mode of the ct_util_server
+%%% (tool-internal use only).
+get_mode() ->
+ call(get_mode).
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:listenv/1
+listenv(Telnet) ->
+ case ct_telnet:send(Telnet,"listenv") of
+ ok ->
+ {ok,Data,_} = ct_telnet:expect(Telnet,
+ ["(^.+)=(.*$)"],
+ [{timeout,seconds(3)},
+ repeat]),
+ {ok,[{Name,Val} || [_,Name,Val] <- Data]};
+ {error,Reason} ->
+ {error,{could_not_send_command,Telnet,"listenv",Reason}}
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @equiv ct:parse_table/1
+parse_table(Data) ->
+ [Heading|Lines]=
+ [remove_space(string:tokens(L, "|"),[]) || L <- Data, hd(L)==$|],
+ {Heading,Lines}.
+
+remove_space([Str|Rest],Acc) ->
+ remove_space(Rest,[string:strip(string:strip(Str),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".
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+get_testdir(Dir, all) ->
+ Abs = abs_name(Dir),
+ case is_test_dir(Abs) of
+ true ->
+ Abs;
+ false ->
+ AbsTest = filename:join(Abs, "test"),
+ case filelib:is_dir(AbsTest) of
+ true -> AbsTest;
+ false -> Abs
+ end
+ end;
+
+get_testdir(Dir, [Suite | _]) when is_atom(Suite) ->
+ get_testdir(Dir, atom_to_list(Suite));
+
+get_testdir(Dir, [Suite | _]) when is_list(Suite) ->
+ get_testdir(Dir, Suite);
+
+get_testdir(Dir, Suite) when is_atom(Suite) ->
+ get_testdir(Dir, atom_to_list(Suite));
+
+get_testdir(Dir, Suite) when is_list(Suite) ->
+ Abs = abs_name(Dir),
+ case is_test_dir(Abs) of
+ true ->
+ Abs;
+ false ->
+ AbsTest = filename:join(Abs, "test"),
+ Mod = case filename:extension(Suite) of
+ ".erl" -> Suite;
+ _ -> Suite ++ ".erl"
+ end,
+ case filelib:is_file(filename:join(AbsTest, Mod)) of
+ true -> AbsTest;
+ false -> Abs
+ end
+ end;
+
+get_testdir(Dir, _) ->
+ get_testdir(Dir, all).
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+encrypt_config_file(SrcFileName, EncryptFileName) ->
+ case get_crypt_key_from_file() of
+ {error,_} = E ->
+ E;
+ Key ->
+ encrypt_config_file(SrcFileName, EncryptFileName, {key,Key})
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+encrypt_config_file(SrcFileName, EncryptFileName, {file,KeyFile}) ->
+ case get_crypt_key_from_file(KeyFile) of
+ {error,_} = E ->
+ E;
+ Key ->
+ encrypt_config_file(SrcFileName, EncryptFileName, {key,Key})
+ end;
+
+encrypt_config_file(SrcFileName, EncryptFileName, {key,Key}) ->
+ crypto:start(),
+ {K1,K2,K3,IVec} = make_crypto_key(Key),
+ case file:read_file(SrcFileName) of
+ {ok,Bin0} ->
+ Bin1 = term_to_binary({SrcFileName,Bin0}),
+ Bin2 = case byte_size(Bin1) rem 8 of
+ 0 -> Bin1;
+ N -> list_to_binary([Bin1,random_bytes(8-N)])
+ end,
+ EncBin = crypto:des3_cbc_encrypt(K1, K2, K3, IVec, Bin2),
+ case file:write_file(EncryptFileName, EncBin) of
+ ok ->
+ io:format("~s --(encrypt)--> ~s~n",
+ [SrcFileName,EncryptFileName]),
+ ok;
+ {error,Reason} ->
+ {error,{Reason,EncryptFileName}}
+ end;
+ {error,Reason} ->
+ {error,{Reason,SrcFileName}}
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+decrypt_config_file(EncryptFileName, TargetFileName) ->
+ case get_crypt_key_from_file() of
+ {error,_} = E ->
+ E;
+ Key ->
+ decrypt_config_file(EncryptFileName, TargetFileName, {key,Key})
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+decrypt_config_file(EncryptFileName, TargetFileName, {file,KeyFile}) ->
+ case get_crypt_key_from_file(KeyFile) of
+ {error,_} = E ->
+ E;
+ Key ->
+ decrypt_config_file(EncryptFileName, TargetFileName, {key,Key})
+ end;
+
+decrypt_config_file(EncryptFileName, TargetFileName, {key,Key}) ->
+ crypto:start(),
+ {K1,K2,K3,IVec} = make_crypto_key(Key),
+ case file:read_file(EncryptFileName) of
+ {ok,Bin} ->
+ DecBin = crypto:des3_cbc_decrypt(K1, K2, K3, IVec, Bin),
+ case catch binary_to_term(DecBin) of
+ {'EXIT',_} ->
+ {error,bad_file};
+ {_SrcFile,SrcBin} ->
+ case TargetFileName of
+ undefined ->
+ {ok,SrcBin};
+ _ ->
+ case file:write_file(TargetFileName, SrcBin) of
+ ok ->
+ io:format("~s --(decrypt)--> ~s~n",
+ [EncryptFileName,TargetFileName]),
+ ok;
+ {error,Reason} ->
+ {error,{Reason,TargetFileName}}
+ end
+ end
+ end;
+ {error,Reason} ->
+ {error,{Reason,EncryptFileName}}
+ end.
+
+
+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
+ [Key] ->
+ Key;
+ _ ->
+ {error,{bad_crypt_file,File}}
+ end;
+ {error,Reason} ->
+ {error,{Reason,File}}
+ end.
+
+get_crypt_key_from_file() ->
+ CwdFile = filename:join(".",?cryptfile),
+ {Result,FullName} =
+ case file:read_file(CwdFile) of
+ {ok,Bin} ->
+ {Bin,CwdFile};
+ _ ->
+ case init:get_argument(home) of
+ {ok,[[Home]]} ->
+ HomeFile = filename:join(Home,?cryptfile),
+ case file:read_file(HomeFile) of
+ {ok,Bin} ->
+ {Bin,HomeFile};
+ _ ->
+ {{error,no_crypt_file},noent}
+ end;
+ _ ->
+ {{error,no_crypt_file},noent}
+ end
+ end,
+ case FullName of
+ noent ->
+ Result;
+ _ ->
+ case catch string:tokens(binary_to_list(Result), [$\n,$\r]) of
+ [Key] ->
+ io:format("~nCrypt key file: ~s~n", [FullName]),
+ Key;
+ _ ->
+ {error,{bad_crypt_file,FullName}}
+ end
+ end.
+
+make_crypto_key(String) ->
+ <<K1:8/binary,K2:8/binary>> = First = erlang:md5(String),
+ <<K3:8/binary,IVec:8/binary>> = erlang:md5([First|lists:reverse(String)]),
+ {K1,K2,K3,IVec}.
+
+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]).
+
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+get_attached(TCPid) ->
+ case dbg_iserver:safe_call({get_attpid,TCPid}) of
+ {ok,AttPid} when is_pid(AttPid) ->
+ AttPid;
+ _ ->
+ undefined
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+kill_attached(undefined,_AttPid) ->
+ ok;
+kill_attached(_TCPid,undefined) ->
+ ok;
+kill_attached(TCPid,AttPid) ->
+ case process_info(TCPid) of
+ undefined ->
+ exit(AttPid,kill);
+ _ ->
+ ok
+ end.
+
+
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+warn_duplicates(Suites) ->
+ Warn =
+ fun(Mod) ->
+ case catch apply(Mod,sequences,[]) of
+ {'EXIT',_} ->
+ ok;
+ [] ->
+ ok;
+ _ ->
+ io:format(user,"~nWARNING! Deprecated function: ~w:sequences/0.~n"
+ " Use group with sequence property instead.~n",[Mod])
+ end
+ end,
+ lists:foreach(Warn, Suites),
+ ok.
+
+
+%%%-----------------------------------------------------------------
+%%% Internal functions
+call(Msg) ->
+ MRef = erlang:monitor(process,whereis(ct_util_server)),
+ Ref = make_ref(),
+ ct_util_server ! {Msg,{self(),Ref}},
+ receive
+ {Ref, Result} ->
+ erlang:demonitor(MRef),
+ Result;
+ {'DOWN',MRef,process,_,Reason} ->
+ {error,{ct_util_server_down,Reason}}
+ end.
+
+return({To,Ref},Result) ->
+ To ! {Ref, Result}.
+
+seconds(T) ->
+ test_server:seconds(T).
+
+ct_make_ref() ->
+ Pid = case whereis(ct_make_ref) of
+ undefined ->
+ spawn_link(fun() -> ct_make_ref_init() end);
+ P ->
+ P
+ end,
+ Pid ! {self(),ref_req},
+ receive
+ {Pid,Ref} -> Ref
+ end.
+
+ct_make_ref_init() ->
+ register(ct_make_ref,self()),
+ ct_make_ref_loop(0).
+
+ct_make_ref_loop(N) ->
+ receive
+ {From,ref_req} ->
+ From ! {self(),N},
+ ct_make_ref_loop(N+1)
+ end.
+
+get_ref_from_name(Name) ->
+ case ets:select(?attr_table,[{#ct_conf{name=Name,ref='$1',_='_'},
+ [],
+ ['$1']}]) of
+ [Ref] ->
+ {ok,Ref};
+ _ ->
+ {error,{no_such_name,Name}}
+ end.
+
+get_name_from_ref(Ref) ->
+ case ets:select(?attr_table,[{#ct_conf{name='$1',ref=Ref,_='_'},
+ [],
+ ['$1']}]) of
+ [Name] ->
+ {ok,Name};
+ _ ->
+ {error,{no_such_ref,Ref}}
+ end.
+
+get_key_from_name(Name) ->
+ case ets:select(?attr_table,[{#ct_conf{name=Name,key='$1',_='_'},
+ [],
+ ['$1']}]) of
+ [Key|_] ->
+ {ok,Key};
+ _ ->
+ {error,{no_such_name,Name}}
+ end.
+
+
+abs_name(Dir0) ->
+ Abs = filename:absname(Dir0),
+ Dir = case lists:reverse(Abs) of
+ [$/|Rest] -> lists:reverse(Rest);
+ _ -> Abs
+ end,
+ abs_name1(Dir,[]).
+
+abs_name1([Drv,$:,$/],Acc) ->
+ Split = [[Drv,$:,$/]|Acc],
+ abs_name2(Split,[]);
+abs_name1("/",Acc) ->
+ Split = ["/"|Acc],
+ abs_name2(Split,[]);
+abs_name1(Dir,Acc) ->
+ abs_name1(filename:dirname(Dir),[filename:basename(Dir)|Acc]).
+
+abs_name2([".."|T],[_|Acc]) ->
+ abs_name2(T,Acc);
+abs_name2(["."|T],Acc) ->
+ abs_name2(T,Acc);
+abs_name2([H|T],Acc) ->
+ abs_name2(T,[H|Acc]);
+abs_name2([],Acc) ->
+ filename:join(lists:reverse(Acc)).
diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl
new file mode 100644
index 0000000000..94ae2625cf
--- /dev/null
+++ b/lib/common_test/src/ct_util.hrl
@@ -0,0 +1,51 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+-define(attr_table,ct_attributes).
+-define(conn_table,ct_connections).
+-define(board_table,ct_boards).
+-define(suite_table,ct_suite_data).
+
+-record(conn, {handle,
+ targetref,
+ address,
+ callback}).
+
+-record(testspec, {nodes=[],
+ logdir=["."],
+ cover=[],
+ config=[],
+ event_handler=[],
+ include=[],
+ alias=[],
+ tests=[]}).
+
+-record(cover, {app=none,
+ level=details,
+ excl_mods=[],
+ incl_mods=[],
+ cross=[],
+ src=[]}).
+
+-define(CT_EVMGR, ct_event).
+-define(CT_EVMGR_REF, ct_event).
+-define(CT_MEVMGR, ct_master_event).
+-define(CT_MEVMGR_REF, ct_master_event).
+
+-define(missing_suites_info, "missing_suites.info").
diff --git a/lib/common_test/src/unix_telnet.erl b/lib/common_test/src/unix_telnet.erl
new file mode 100644
index 0000000000..14a70e9d22
--- /dev/null
+++ b/lib/common_test/src/unix_telnet.erl
@@ -0,0 +1,152 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%% @doc Callback module for ct_telnet for talking telnet
+%%% to a unix host.
+%%%
+%%% <p>It requires the following entry in the config file:</p>
+%%% <pre>
+%%% {unix,[{telnet,HostNameOrIpAddress},
+%%% {port,PortNum},
+%%% {username,UserName},
+%%% {password,Password}]}.
+%%% </pre>
+%%%
+%%% <p>To talk telnet to the host specified by
+%%% <code>HostNameOrIpAddress</code>, use the interface functions in
+%%% <code>ct</code>, e.g. <code>open(Name), cmd(Name,Cmd), ...</code>.</p>
+%%%
+%%% <p><code>Name</code> is the name you allocated to the unix host in
+%%% your <code>require</code> statement. E.g.</p>
+%%% <pre> suite() -> [{require,Name,{unix,[telnet,username,password]}}].</pre>
+%%% <p>or</p>
+%%% <pre> ct:require(Name,{unix,[telnet,username,password]}).</pre>
+%%%
+%%% <p>Note that the <code>{port,PortNum}</code> tuple is optional and if
+%%% omitted, default telnet port 23 will be used.</p>
+%%%
+%%% @see ct
+%%% @see ct_telnet
+-module(unix_telnet).
+
+-compile(export_all).
+
+%% Callbacks for ct_telnet.erl
+-export([connect/4,get_prompt_regexp/0]).
+-import(ct_telnet,[start_log/1,cont_log/2,end_log/0]).
+
+-define(username,"login: ").
+-define(password,"Password: ").
+-define(prx,"login: |Password: |\\\$ |> ").
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @spec get_prompt_regexp() -> PromptRegexp
+%%% PromptRegexp = ct_telnet:prompt_regexp()
+%%%
+%%% @doc Callback for ct_telnet.erl.
+%%%
+%%% <p>Return the prompt regexp for telnet connections to the
+%%% interwatch instrument.</p>
+get_prompt_regexp() ->
+ ?prx.
+
+
+%%%-----------------------------------------------------------------
+%%% @hidden
+%%% @spec connect(Ip,Port,Timeout,Extra) -> {ok,Handle} | {error,Reason}
+%%% Ip = string() | {integer(),integer(),integer(),integer()}
+%%% Port = integer()
+%%% Timeout = integer()
+%%% Extra = {Username,Password}
+%%% Username = string()
+%%% Password = string()
+%%% Handle = ct_telnet:handle()
+%%%
+%%% @doc Callback for ct_telnet.erl.
+%%%
+%%% <p>Setup telnet connection to a UNIX host.</p>
+connect(Ip,Port,Timeout,Extra) ->
+ case Extra of
+ {Username,Password} ->
+ connect(Ip,Port,Timeout,Username,Password);
+ Name ->
+ case get_username_and_password(Name) of
+ {ok,{Username,Password}} ->
+ connect(Ip,Port,Timeout,Username,Password);
+ Error ->
+ Error
+ end
+ end.
+
+connect(Ip,Port,Timeout,Username,Password) ->
+ start_log("unix_telnet:connect"),
+ Result =
+ case ct_telnet_client:open(Ip,Port,Timeout) of
+ {ok,Pid} ->
+ case ct_telnet:silent_teln_expect(Pid,[],[prompt],?prx,[]) of
+ {ok,{prompt,?username},_} ->
+ ok = ct_telnet_client:send_data(Pid,Username),
+ cont_log("Username: ~s",[Username]),
+ case ct_telnet:silent_teln_expect(Pid,[],prompt,?prx,[]) of
+ {ok,{prompt,?password},_} ->
+ ok = ct_telnet_client:send_data(Pid,Password),
+ Stars = lists:duplicate(length(Password),$*),
+ cont_log("Password: ~s",[Stars]),
+ ok = ct_telnet_client:send_data(Pid,""),
+ case ct_telnet:silent_teln_expect(Pid,[],prompt,
+ ?prx,[]) of
+ {ok,{prompt,Prompt},_}
+ when Prompt=/=?username, Prompt=/=?password ->
+ {ok,Pid};
+ Error ->
+ cont_log("Password failed\n~p\n",
+ [Error]),
+ {error,Error}
+ end;
+ Error ->
+ cont_log("Login failed\n~p\n",[Error]),
+ {error,Error}
+ end;
+ {ok,[{prompt,_OtherPrompt1},{prompt,_OtherPrompt2}],_} ->
+ {ok,Pid};
+ Error ->
+ cont_log("Did not get expected prompt\n~p\n",[Error]),
+ {error,Error}
+ end;
+ Error ->
+ cont_log("Could not open telnet connection\n~p\n",[Error]),
+ Error
+ end,
+ end_log(),
+ Result.
+
+get_username_and_password(Name) ->
+ case ct:get_config({Name,username}) of
+ undefined ->
+ {error,{no_username,Name}};
+ Username ->
+ case ct:get_config({Name,password}) of
+ undefined ->
+ {error,{no_password,Name}};
+ Password ->
+ {ok,{Username,Password}}
+ end
+ end.
+
diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl
new file mode 100644
index 0000000000..ad4845a7c3
--- /dev/null
+++ b/lib/common_test/src/vts.erl
@@ -0,0 +1,882 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+-module(vts).
+
+-export([start/0,
+ init_data/4,
+ stop/0,
+ report/2]).
+
+-export([config_data/0,
+ start_link/0]).
+
+-export([start_page/2,
+ title_frame/2,
+ menu_frame/2,
+ welcome_frame/2,
+ config_frame/2,
+ add_config_file/2,
+ remove_config_file/2,
+ run_frame/2,
+ add_test_dir/2,
+ remove_test_dir/2,
+ select_case/2,
+ select_suite/2,
+ run_test/2,
+ result_frameset/2,
+ result_summary_frame/2,
+ no_result_log_frame/2,
+ redirect_to_result_log_frame/2]).
+
+-export([test_info/3]).
+
+-define(START_PAGE,"/vts_erl/vts/start_page").
+
+-define(tests,vts_tests).
+
+%% Colors
+-define(INFO_BG_COLOR,"#C0C0EA").
+
+-record(state,{tests=[],config=[],event_handler=[],test_runner,
+ running=0,reload_results=false,start_dir,current_log_dir,
+ total=0,ok=0,fail=0,skip=0,testruns=[]}).
+
+
+%%%-----------------------------------------------------------------
+%%% User API
+start() ->
+ webtool:start(),
+ webtool:start_tools([],"app=vts").
+
+init_data(ConfigFiles,EvHandlers,LogDir,Tests) ->
+ call({init_data,ConfigFiles,EvHandlers,LogDir,Tests}).
+
+stop() ->
+ webtool:stop_tools([],"app=vts"),
+ webtool:stop().
+
+report(What,Data) ->
+ call({report,What,Data}).
+
+%%%-----------------------------------------------------------------
+%%% Return config data used by webtool
+config_data() ->
+ {ok,LogDir} =
+ case lists:keysearch(logdir,1,init:get_arguments()) of
+ {value,{logdir,[LogD]}} -> {ok,filename:absname(LogD)};
+ false -> file:get_cwd()
+ end,
+ {vts,
+ [{web_data,{"VisualTestServer",?START_PAGE}},
+ {alias,{erl_alias,"/vts_erl",[?MODULE]}},
+ {alias,{"/log_dir",LogDir}},
+ {start,{child,{{local,?MODULE},
+ {?MODULE,start_link,[]},
+ permanent,100,worker,[?MODULE]}}}
+ ]}.
+
+start_link() ->
+ case whereis(?MODULE) of
+ undefined ->
+ Self = self(),
+ Pid = spawn_link(fun() -> init(Self) end),
+ MRef = erlang:monitor(process,Pid),
+ receive
+ {Pid,started} ->
+ erlang:demonitor(MRef),
+ {ok,Pid};
+ {'DOWN',MRef,process,_,Reason} ->
+ {error,{vts,died,Reason}}
+ end;
+ Pid ->
+ {ok,Pid}
+ end.
+
+start_page(_Env,_Input) ->
+ call(start_page).
+title_frame(_Env,_Input) ->
+ call(title_frame).
+welcome_frame(_Env,_Input) ->
+ call(welcome_frame).
+menu_frame(_Env,_Input) ->
+ call(menu_frame).
+config_frame(_Env,_Input) ->
+ call(config_frame).
+add_config_file(_Env,Input) ->
+ call({add_config_file,Input}).
+remove_config_file(_Env,Input) ->
+ call({remove_config_file,Input}).
+run_frame(_Env,_Input) ->
+ call(run_frame).
+add_test_dir(_Env,Input) ->
+ call({add_test_dir,Input}).
+remove_test_dir(_Env,Input) ->
+ call({remove_test_dir,Input}).
+select_suite(_Env,Input) ->
+ call({select_suite,Input}).
+select_case(_Env,Input) ->
+ call({select_case,Input}).
+run_test(_Env,_Input) ->
+ call(run_test).
+result_frameset(_Env,_Input) ->
+ call(result_frameset).
+redirect_to_result_log_frame(_Env,_Input) ->
+ call(redirect_to_result_log_frame).
+result_summary_frame(_Env,_Input) ->
+ call(result_summary_frame).
+no_result_log_frame(_Env,_Input) ->
+ call(no_result_log_frame).
+
+aborted() ->
+ call(aborted).
+
+test_info(_VtsPid,Type,Data) ->
+ call({test_info,Type,Data}).
+
+init(Parent) ->
+ register(?MODULE,self()),
+ process_flag(trap_exit,true),
+ Parent ! {self(),started},
+ {ok,Cwd} = file:get_cwd(),
+ InitState = #state{start_dir=Cwd},
+ loop(InitState).
+
+loop(State) ->
+ receive
+ {{init_data,ConfigFiles,EvHandlers,LogDir,Tests},From} ->
+ ct_install(State),
+ return(From,ok),
+ loop(#state{config=ConfigFiles,event_handler=EvHandlers,
+ current_log_dir=LogDir,tests=Tests});
+ {start_page,From} ->
+ return(From,start_page1()),
+ loop(State);
+ {title_frame,From} ->
+ return(From,title_frame1()),
+ loop(State);
+ {welcome_frame,From} ->
+ return(From,welcome_frame1()),
+ loop(State);
+ {menu_frame,From} ->
+ return(From,menu_frame1()),
+ loop(State);
+ {config_frame,From} ->
+ return(From,config_frame1(State)),
+ loop(State);
+ {{add_config_file,Input},From} ->
+ {Return,State1} = add_config_file1(Input,State),
+ ct_install(State1),
+ return(From,Return),
+ loop(State1);
+ {{remove_config_file,Input},From} ->
+ {Return,State1} = remove_config_file1(Input,State),
+ ct_install(State1),
+ return(From,Return),
+ loop(State1);
+ {run_frame,From} ->
+ return(From,run_frame1(State)),
+ loop(State);
+ {{add_test_dir,Input},From} ->
+ {Return,State1} = add_test_dir1(Input,State),
+ return(From,Return),
+ loop(State1);
+ {{remove_test_dir,Input},From} ->
+ {Return,State1} = remove_test_dir1(Input,State),
+ return(From,Return),
+ loop(State1);
+ {{select_suite,Input},From} ->
+ {Return,State1} = select_suite1(Input,State),
+ return(From,Return),
+ loop(State1);
+ {{select_case,Input},From} ->
+ {Return,State1} = select_case1(Input,State),
+ return(From,Return),
+ loop(State1);
+ {run_test,From} ->
+ State1 = run_test1(State),
+ return(From,redirect_to_result_frameset1()),
+ loop(State1);
+ {result_frameset,From} ->
+ return(From,result_frameset1(State)),
+ loop(State);
+ {redirect_to_result_log_frame,From} ->
+ return(From,redirect_to_result_log_frame1(State)),
+ loop(State);
+ {result_summary_frame,From} ->
+ return(From,result_summary_frame1(State)),
+ loop(State);
+ stop_reload_results ->
+ file:set_cwd(State#state.start_dir),
+ loop(State#state{reload_results=false});
+ {no_result_log_frame,From} ->
+ return(From,no_result_log_frame1()),
+ loop(State);
+ {aborted,From} ->
+ return(From,ok),
+ loop(State#state{test_runner=undefined,running=0});
+ {{report,What,Data},From} ->
+ State1 = report1(What,Data,State),
+ return(From,ok),
+ loop(State1);
+ {stop,From} ->
+ return(From,ok);
+ {'EXIT',Pid,Reason} ->
+ case State#state.test_runner of
+ Pid -> io:format("ERROR: test runner crashed: ~p\n",[Reason]);
+ _ -> ignore
+ end,
+ loop(State);
+ {{test_info,_Type,_Data},From} ->
+ return(From,ok),
+ loop(State)
+ end.
+
+call(Msg) ->
+ case whereis(?MODULE) of
+ undefined -> {error,no_proc};
+ Pid ->
+ MRef = erlang:monitor(process,Pid),
+ Ref = make_ref(),
+ Pid ! {Msg,{self(),Ref}},
+ receive
+ {Ref, Result} ->
+ erlang:demonitor(MRef),
+ Result;
+ {'DOWN',MRef,process,_,Reason} ->
+ {error,{process_down,Pid,Reason}}
+ end
+ end.
+
+return({To,Ref},Result) ->
+ To ! {Ref, Result}.
+
+
+run_test1(State=#state{tests=Tests,current_log_dir=LogDir}) ->
+ Self=self(),
+ RunTest = fun() ->
+ case ct_run:do_run(Tests,[],LogDir) of
+ {error,_Reason} ->
+ aborted();
+ _ ->
+ ok
+ end,
+ unlink(Self)
+ end,
+
+ Pid = spawn_link(RunTest),
+
+ Total =
+ receive
+ {{test_info,start_info,{_,_,Cases}},From} ->
+ return(From,ok),
+ Cases;
+ EXIT = {'EXIT',_,_} ->
+ self() ! EXIT
+ after 30000 ->
+ 0
+ end,
+ State#state{test_runner=Pid,running=length(Tests),
+ total=Total,ok=0,fail=0,skip=0,testruns=[]}.
+
+
+ct_install(#state{config=Config,event_handler=EvHandlers,
+ current_log_dir=LogDir}) ->
+ ct_run:install([{config,Config},{event_handler,EvHandlers}],LogDir).
+
+%%%-----------------------------------------------------------------
+%%% HTML
+start_page1() ->
+ header("Visual Test Server Start Page",start_page_frameset()).
+
+start_page_frameset() ->
+ frameset(
+ "ROWS=\"60,*\"",
+ [frame(["NAME=\"title\" SRC=\"./title_frame\""]),
+ frameset(
+ "COLS=\"150,*\"",
+ [frame(["NAME=\"menu\" SRC=\"./menu_frame\""]),
+ frame(["NAME=\"main\" SRC=\"./welcome_frame\""])])]).
+
+
+title_frame1() ->
+ header(body("BGCOLOR=lightgrey TEXT=darkgreen",title_body())).
+
+title_body() ->
+ p("ALIGN=center",font("SIZE=\"+3\"",b("Visual Test Server"))).
+
+welcome_frame1() ->
+ header(body(welcome_body())).
+
+welcome_body() ->
+ table(
+ "WIDTH=100% HEIGHT=60%",
+ [tr("VALIGN=middle",
+ td("ALIGN=center",
+ font("SIZE=6",
+ ["Welcome to the",br(),
+ "Visual Test Server"])))]).
+
+menu_frame1() ->
+ header(body(menu_body())).
+
+menu_body() ->
+ [h2("Content"),
+ ul([
+ li(href(["TARGET=\"main\""],"./config_frame","Config")),
+ li(href(["TARGET=\"main\""],"./run_frame","Run")),
+ li(href(["TARGET=\"main\""],"./result_frameset","Result"))
+ ]),
+ h2("Logs"),
+ ul([
+ li(href(["TARGET=\"new\""],"/log_dir/index.html","Last Runs")),
+ li(href(["TARGET=\"new\""],"/log_dir/all_runs.html","All Runs"))
+ ])
+ ].
+
+config_frame1(State) ->
+ header("Config",body(config_body(State))).
+
+config_body(State) ->
+ Entry = [input("TYPE=file NAME=browse SIZE=40"),
+ input("TYPE=hidden NAME=file")],
+ AddForm =
+ form(
+ "NAME=read_file_form METHOD=post ACTION=\"./add_config_file\"",
+ table(
+ "BORDER=0",
+ [tr(
+ [td(Entry),
+ td("ALIGN=center",
+ input("TYPE=submit onClick=\"file.value=browse.value;\""
+ " VALUE=\"Add\""))])])),
+ {Text,RemoveForm} =
+ case State#state.config of
+ [] ->
+ T = "To be able to run any tests, one or more configuration "
+ "files must be added. Enter the name of the configuration "
+ "file below and click the \"Add\" button.",
+ R = "",
+ {T,R};
+ Files ->
+ T = "The currently known configuration files are listed below. "
+ "To add a file, type the filename in the entry and "
+ "click the \"Add\" button. "
+ "To remove a file, select it and click the \"Remove\" "
+ "button.",
+ ConfigFiles = [option(File) || File <- Files],
+ Select = select("NAME=file TITLE=\"Select Config File\""
+ " MULTIPLE=true",
+ ConfigFiles),
+ R =
+ form(["NAME=remove_config METHOD=post ",
+ "ACTION=\"./remove_config_file\""],
+ table(
+ "BORDER=0",
+ [tr(td("ALIGN=center",Select)),
+ tr(td("ALIGN=center",
+ input("TYPE=submit VALUE=\"Remove\"")))])),
+ {T,R}
+ end,
+
+ [h1("ALIGN=center","Config"),
+ table(
+ "WIDTH=600 ALIGN=center CELLPADDING=5",
+ [tr(td(["BGCOLOR=",?INFO_BG_COLOR],Text)),
+ tr(td("ALIGN=center",AddForm)),
+ tr(td("ALIGN=center",RemoveForm))])].
+
+
+add_config_file1(Input,State) ->
+ State1 =
+ case get_input_data(Input,"file") of
+ "" -> State;
+ File -> State#state{config=[File|State#state.config]}
+ end,
+ Return = config_frame1(State1),
+ {Return,State1}.
+
+remove_config_file1(Input,State) ->
+ Files = get_all_input_data(Input,"file"),
+ State1 = State#state{config=State#state.config--Files},
+ Return = config_frame1(State1),
+ {Return,State1}.
+
+
+
+run_frame1(State) ->
+ header("Run Test",body(run_body(State))).
+
+run_body(#state{running=Running}) when Running>0 ->
+ [h1("ALIGN=center","Run Test"),
+ p(["Test are ongoing: ",href("./result_frameset","Results")])];
+run_body(State) ->
+ ConfigList = ul([li(File) || File <- State#state.config]),
+ ConfigFiles = [h3("Config Files"),
+ ConfigList],
+
+ AddDirForm =
+ form(
+ "NAME=add_dir_form METHOD=post ACTION=\"./add_test_dir\"",
+ table(
+ "BORDER=0",
+ [tr(td("COLSPAN=2","Enter test directory")),
+ tr(
+ [td(input("TYPE=text NAME=dir SIZE=40")),
+ td("ALIGN=center",
+ input("TYPE=submit onClick=\"dir.value=browse.value;\""
+ " VALUE=\"Add Test Dir\""))])])),
+
+ {LoadedTestsTable,Submit} =
+ case create_testdir_entries(State#state.tests,1) of
+ [] -> {"",""};
+ TestDirs ->
+ Heading = tr([th(""),
+ th("ALIGN=left","Directory"),
+ th("ALIGN=left","Suite"),
+ th("ALIGN=left","Case")]),
+ {table("CELLPADDING=5",[Heading,TestDirs]),
+ submit_button()}
+ end,
+
+ %% It should be ok to have no config-file!
+ Body =
+ %% case State#state.config of %% [] -> %% p("ALIGN=center",
+ %% href("./config_frame","Please select one or
+ %% more config files")); %% _ ->
+ table(
+ "WIDTH=100%",
+ [tr(td(ConfigFiles)),
+ tr(td("")),
+ tr(td(AddDirForm)),
+ tr(td("")),
+ tr(td(LoadedTestsTable)),
+ tr(td(Submit))]),
+ %% end,
+
+ [h1("ALIGN=center","Run Test"), Body].
+
+create_testdir_entries([{Dir,Suite,Case}|Tests],N) ->
+ [testdir_entry(Dir,Suite,Case,N)|create_testdir_entries(Tests,N+1)];
+create_testdir_entries([],_N) ->
+ [].
+
+testdir_entry(Dir,Suite,Case,N) ->
+ NStr = integer_to_list(N),
+ tr([td(delete_button(NStr)),
+ td(Dir),
+ td(suite_select(Dir,Suite,NStr)),
+ td(case_select(Dir,Suite,Case,NStr))]).
+
+delete_button(N) ->
+ form(["NAME=remove_dir_form METHOD=post ACTION=\"./remove_test_dir\""],
+ [input(["TYPE=hidden NAME=dir VALUE=\'",N,"\'"]),
+ input(["TYPE=submit VALUE=X"])]).
+
+suite_select(Dir,Suite,N) ->
+ case filelib:wildcard(filename:join(Dir,"*_SUITE.erl")) of
+ [] ->
+ select("NAME=suite TITLE=\"Select suite\"","");
+ Suites0 ->
+ Suites = [filename:basename(filename:rootname(S)) || S <- Suites0],
+ select("NAME=suite TITLE=\"Select suite\"",
+ options(["all"|Suites],atom_to_list(Suite),N,"select_suite"))
+ end.
+
+case_select(_Dir,all,_,N) ->
+ select("NAME=case TITLE=\"Select case\"",
+ options(["all"],"all",N,"select_case"));
+case_select(Dir,Suite,Case,N) ->
+ MakeResult =
+ case application:get_env(common_test, auto_compile) of
+ {ok,false} ->
+ ok;
+ _ ->
+ UserInclude =
+ case application:get_env(common_test, include) of
+ {ok,UserInclDirs} when length(UserInclDirs) > 0 ->
+ [{i,UserInclDir} || UserInclDir <- UserInclDirs];
+ _ ->
+ []
+ end,
+ ct_run:run_make(Dir,Suite,UserInclude)
+ end,
+ case MakeResult of
+ ok ->
+ code:add_pathz(Dir),
+ case catch apply(Suite,all,[]) of
+ {'EXIT',Reason} ->
+ io:format("\n~p\n",[Reason]),
+ red(["COULD NOT READ TESTCASES!!",br(),
+ "See erlang shell for info"]);
+ {skip,_Reason} ->
+ select("NAME=case TITLE=\"Select case\"",
+ options(["all"],"all",N,"select_case"));
+ AllCasesAtoms ->
+ AllCases = [atom_to_list(C) || C <- AllCasesAtoms,
+ is_atom(C)],
+ select("NAME=case TITLE=\"Select case\"",
+ options(["all"|AllCases],atom_to_list(Case),
+ N,"select_case"))
+ end;
+ _Error ->
+ red(["COMPILATION ERROR!!",br(),
+ "See erlang shell for info",br(),
+ "Reload this page when errors are fixed"])
+ end.
+
+
+options([Selected|Elements],Selected,N,Func) ->
+ [option(["SELECTED ",
+ "onClick=\"document.location.href=\'./",Func,"?n=",N,
+ "&selected=",Selected,"\';\""],
+ Selected)|
+ options(Elements,Selected,N,Func)];
+options([Element|Elements],Selected,N,Func) ->
+ [option(["onClick=\"document.location.href=\'./",Func,"?n=",N,
+ "&selected=",Element,"\';\""],
+ Element)|
+ options(Elements,Selected,N,Func)];
+options([],_Selected,_N,_Func) ->
+ [].
+
+add_test_dir1(Input,State) ->
+ State1 =
+ case get_input_data(Input,"dir") of
+ "" -> State;
+ Dir0 ->
+ Dir = case ct_util:is_test_dir(Dir0) of
+ true ->
+ Dir0;
+ false -> filename:join(Dir0,"test")
+ end,
+ case filelib:is_dir(Dir) of
+ true ->
+ Test = ct_run:tests(Dir),
+ State#state{tests=State#state.tests++Test};
+ false ->
+ State
+ end
+ end,
+ Return = run_frame1(State1),
+ {Return,State1}.
+
+
+
+remove_test_dir1(Input,State) ->
+ N = list_to_integer(get_input_data(Input,"dir")),
+ State1 = State#state{tests=delete_test(N,State#state.tests)},
+ Return = run_frame1(State1),
+ {Return,State1}.
+
+delete_test(1,[_|T]) ->
+ T;
+delete_test(N,[H|T]) ->
+ [H|delete_test(N-1,T)].
+
+select_suite1(Input,State) ->
+ N = list_to_integer(get_input_data(Input,"n")),
+ Suite = list_to_atom(get_input_data(Input,"selected")),
+ Tests1 = replace_suite(N,Suite,State#state.tests),
+ State1 = State#state{tests=Tests1},
+ Return = run_frame1(State1),
+ {Return,State1}.
+
+replace_suite(1,Suite,[{Dir,_,_}|T]) ->
+ [Test] = ct_run:tests(Dir,Suite),
+ [Test|T];
+replace_suite(N,Suite,[H|T]) ->
+ [H|replace_suite(N-1,Suite,T)].
+
+select_case1(Input,State) ->
+ N = list_to_integer(get_input_data(Input,"n")),
+ Case = list_to_atom(get_input_data(Input,"selected")),
+ Tests1 = replace_case(N,Case,State#state.tests),
+ State1 = State#state{tests=Tests1},
+ Return = run_frame1(State1),
+ {Return,State1}.
+
+replace_case(1,Case,[{Dir,Suite,_}|T]) ->
+ [Test] = ct_run:tests(Dir,Suite,Case),
+ [Test|T];
+replace_case(N,Case,[H|T]) ->
+ [H|replace_case(N-1,Case,T)].
+
+
+submit_button() ->
+ form(["NAME=run_test_form METHOD=post ACTION=\"./run_test\""],
+ [input("TYPE=submit VALUE=\"Run Test\"")]).
+
+
+redirect_to_result_frameset1() ->
+ Head =
+ ["<META HTTP-EQUIV=\"refresh\" CONTENT=\"1; URL=./result_frameset\">"],
+ [header("",Head,body("Please wait..."))].
+
+result_frameset1(State) ->
+ header("Results",result_frameset2(State)).
+
+result_frameset2(State) ->
+ ResultLog =
+ case {State#state.current_log_dir,State#state.running} of
+ {undefined,0} ->
+ "./no_result_log_frame";
+ {undefined,_} ->
+ "./redirect_to_result_log_frame";
+ {_Dir,0} ->
+ filename:join(["/log_dir","index.html"]);
+ {_Dir,_} ->
+ {_,CurrentLog} = hd(State#state.testruns),
+ CurrentLog
+ end,
+ frameset(
+ "COLS=\"200,*\"",
+ [frame(["NAME=\"result_summary\" SRC=\"./result_summary_frame\""]),
+ frame(["NAME=\"result_log\" SRC=\"",ResultLog,"\""])]).
+
+redirect_to_result_log_frame1(State) ->
+ ResultLog =
+ case {State#state.testruns,State#state.running} of
+ {[],0} ->
+ "./no_result_log_frame";
+ {[],_} ->
+ "./redirect_to_result_log_frame";
+ {[{_,CurrentLog}|_],_} ->
+ CurrentLog
+ end,
+ Head = ["<META HTTP-EQUIV=\"refresh\" CONTENT=\"1; URL=",ResultLog,"\">"],
+ [header("",Head,body("Please wait..."))].
+
+result_summary_frame1(State) ->
+ case {State#state.running,State#state.reload_results} of
+ {0,false} ->
+ header("Result Summary",body(result_summary_body(State)));
+ _ ->
+ Head =
+ "<SCRIPT LANGUAGE=\"JavaScript1.2\">\n"
+ "\n"
+ "function startReloadInterval() {\n"
+ " intervalId = setInterval(\"reloadPage()\",5000)\n"
+ "}\n"
+ "\n"
+ "function reloadPage() {\n"
+ " location.reload()\n"
+ " parent.result_log.location.reload()\n"
+% " parent.result_log.scrollBy(0, window.innerHeight)\n"
+ "}\n"
+ "</SCRIPT>\n",
+ header("Result Summary",Head,
+ body("onLoad=\"startReloadInterval()\" BGCOLOR=\"#FFFFFF\"",
+ result_summary_body(State)))
+ end.
+
+result_summary_body(State) ->
+ N = State#state.ok + State#state.fail + State#state.skip,
+ [h2("Result Summary"),
+ p([b(integer_to_list(N))," cases executed (of ",
+ b(integer_to_list(State#state.total)),")"]),
+ p([green([b(integer_to_list(State#state.ok))," successful"]),br(),
+ red([b(integer_to_list(State#state.fail))," failed"]),br(),
+ orange([b(integer_to_list(State#state.skip))," skipped"])]),
+ executed_test_list(State)].
+
+executed_test_list(#state{testruns=[]}) ->
+ [];
+executed_test_list(#state{testruns=TestRuns}) ->
+ [h2("Executed Tests"),
+ table(
+ "",
+ [tr(td(href("TARGET=\"result_log\"",Log,Name))) ||
+ {Name,Log} <- lists:reverse(TestRuns)])].
+
+
+no_result_log_frame1() ->
+ header("Test Results",body(no_result_log_body())).
+
+no_result_log_body() ->
+ [h1("ALIGN=center","Test Results"),
+ p(["There are currently no test results available. ",
+ br(),href("TARGET=\"main\"","./run_frame","You can run tests here")])].
+
+report1(tests_start,{TestName,_N},State) ->
+ {ok,LogDir} = ct_logs:get_log_dir(),
+ TestRuns =
+ case State#state.testruns of
+ [{TestName,_}|_]=TR ->
+ TR;
+ TR ->
+ [{TestName,get_test_log(TestName,LogDir)}|TR]
+ end,
+ State#state{testruns=TestRuns};
+report1(tests_done,{_Ok,_Fail,_Skip},State) ->
+ 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;
+report1(tc_done,{_Suite,init_per_suite,_},State) ->
+ State;
+report1(tc_done,{_Suite,end_per_suite,_},State) ->
+ State;
+report1(tc_done,{_Suite,_Case,ok},State) ->
+ State#state{ok=State#state.ok+1};
+report1(tc_done,{_Suite,_Case,{failed,_Reason}},State) ->
+ State#state{fail=State#state.fail+1};
+report1(tc_done,{_Suite,_Case,{skipped,_Reason}},State) ->
+ State#state{skip=State#state.skip+1};
+report1(tc_user_skip,{_Suite,_Case,_Reason},State) ->
+ State#state{skip=State#state.skip+1}.
+
+get_test_log(TestName,LogDir) ->
+ [Log] =
+ filelib:wildcard(
+ filename:join([TestName++".logs","run*","suite.log.html"])),
+ filename:join(["/log_dir",LogDir,Log]).
+
+
+
+%get_description(Suite,Case) ->
+% case erlang:function_exported(Suite,Case,0) of
+% true ->
+% case catch apply(Suite,Case,[]) of
+% {'EXIT',_Reason} ->
+% "-";
+% Info ->
+% case lists:keysearch(doc,1,Info) of
+% {value,{doc,Doc}} when is_list(Doc) ->
+% Doc;
+% _ ->
+% "-"
+% end
+% end;
+% false ->
+% "-"
+% end.
+
+%%%-----------------------------------------------------------------
+%%% Internal library
+header(Body) ->
+ header("","",Body).
+header(Title,Body) ->
+ header(Title,"",Body).
+header(Title,Head,Body) ->
+ ["Pragma:no-cache\r\n",
+ "Content-type: text/html\r\n\r\n",
+ html_header(Title,Head,Body)].
+
+html_header(Title,Head,Body) ->
+ ["<HTML>\n",
+ "<HEAD>\n",
+ "<TITLE>", Title, "</TITLE>\n",
+ Head,
+ "</HEAD>\n",
+ Body,
+ "</HTML>"].
+
+body(Text) ->
+ ["<BODY BGCOLOR=\"#FFFFFF\">\n",Text,"<\BODY>\n"].
+body(Args,Text) ->
+ ["<BODY ", Args, ">\n", Text,"<\BODY>\n"].
+
+
+frameset(Args,Frames) ->
+ ["<FRAMESET ",Args,">\n", Frames, "\n</FRAMESET>\n"].
+frame(Args) ->
+ ["<FRAME ",Args, ">\n"].
+
+table(Args,Text) ->
+ ["<TABLE ", Args, ">\n", Text, "\n</TABLE>\n"].
+tr(Text) ->
+ ["<TR>\n", Text, "\n</TR>\n"].
+tr(Args,Text) ->
+ ["<TR ", Args, ">\n", Text, "\n</TR>\n"].
+th(Text) ->
+ ["<TH>", Text, "</TH>"].
+th(Args,Text) ->
+ ["<TH ", Args, ">\n", Text, "\n</TH>\n"].
+td(Text) ->
+ ["<TD>", Text, "</TD>"].
+td(Args,Text) ->
+ ["<TD ", Args, ">", Text, "</TD>"].
+
+b(Text) ->
+ ["<B>",Text,"</B>"].
+%em(Text) ->
+% ["<EM>",Text,"</EM>\n"].
+%pre(Text) ->
+% ["<PRE>",Text,"</PRE>"].
+href(Link,Text) ->
+ ["<A HREF=\"",Link,"\">",Text,"</A>"].
+href(Args,Link,Text) ->
+ ["<A HREF=\"",Link,"\" ",Args,">",Text,"</A>"].
+form(Args,Text) ->
+ ["<FORM ",Args,">\n",Text,"\n</FORM>\n"].
+input(Args) ->
+ ["<INPUT ", Args, ">\n"].
+select(Args,Text) ->
+ ["<SELECT ", Args, ">\n", Text, "\n</SELECT>\n"].
+option(Text) ->
+ ["<OPTION>\n", Text, "\n</OPTION>\n"].
+option(Args,Text) ->
+ ["<OPTION ", Args, ">\n", Text, "\n</OPTION>\n"].
+h1(Args,Text) ->
+ ["<H1 ", Args, ">",Text,"</H1>\n"].
+h2(Text) ->
+ ["<H2>",Text,"</H2>\n"].
+h3(Text) ->
+ ["<H3>",Text,"</H3>\n"].
+font(Args,Text) ->
+ ["<FONT ",Args,">\n",Text,"\n</FONT>\n"].
+p(Text) ->
+ ["<P>",Text,"</P>\n"].
+p(Args, Text) ->
+ ["<P ", Args, ">",Text,"</P>\n"].
+ul(Text) ->
+ ["<UL>", Text, "</UL>\n"].
+li(Text) ->
+ ["<LI>", Text, "</LI>\n"].
+br() ->
+ "<BR>\n".
+
+red(Text) -> color(red,Text).
+green(Text) -> color(green,Text).
+orange(Text) -> color(orange,Text).
+color(Color,Text) when is_atom(Color) ->
+ font(["COLOR=",atom_to_list(Color)],Text).
+
+get_all_input_data(Input,Key)->
+ List = parse(Input),
+ get_all_input_data(List,Key,[]).
+get_all_input_data([{Key,Value}|List],Key,Acc) ->
+ get_all_input_data(List,Key,[Value|Acc]);
+get_all_input_data([{_OtherKey,_Value}|List],Key,Acc) ->
+ get_all_input_data(List,Key,Acc);
+get_all_input_data([],_Key,Acc) ->
+ Acc.
+
+get_input_data(Input,Key)->
+ case lists:keysearch(Key,1,parse(Input)) of
+ {value,{Key,Value}} ->
+ Value;
+ false ->
+ undefined
+ end.
+
+parse(Input) ->
+ httpd:parse_query(Input).
+