diff options
author | Erlang/OTP <[email protected]> | 2009-11-20 14:54:40 +0000 |
---|---|---|
committer | Erlang/OTP <[email protected]> | 2009-11-20 14:54:40 +0000 |
commit | 84adefa331c4159d432d22840663c38f155cd4c1 (patch) | |
tree | bff9a9c66adda4df2106dfd0e5c053ab182a12bd /lib/common_test/src | |
download | otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.gz otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.bz2 otp-84adefa331c4159d432d22840663c38f155cd4c1.zip |
The R13B03 release.OTP_R13B03
Diffstat (limited to 'lib/common_test/src')
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>> ct:require(unix_telnet, unix).</code><br/> +%%% <code>ok</code><br/> +%%% <code>> ct_telnet:open(unix_telnet).</code><br/> +%%% <code>{ok,<0.105.0>}</code><br/> +%%% <code>> 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.<timestamp> 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 -> + [" <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> </TD>\n","<TD> </TD>\n<TD> </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 © ", 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 © ", 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.<timestamp>/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). + |