aboutsummaryrefslogtreecommitdiffstats
path: root/lib/common_test/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common_test/src')
-rw-r--r--lib/common_test/src/Makefile16
-rw-r--r--lib/common_test/src/common_test.app.src17
-rw-r--r--lib/common_test/src/ct.erl141
-rw-r--r--lib/common_test/src/ct_config.erl786
-rw-r--r--lib/common_test/src/ct_config_plain.erl109
-rw-r--r--lib/common_test/src/ct_config_xml.erl118
-rw-r--r--lib/common_test/src/ct_framework.erl43
-rw-r--r--lib/common_test/src/ct_gen_conn.erl14
-rw-r--r--lib/common_test/src/ct_logs.erl16
-rw-r--r--lib/common_test/src/ct_master.erl142
-rw-r--r--lib/common_test/src/ct_master_logs.erl14
-rw-r--r--lib/common_test/src/ct_repeat.erl12
-rw-r--r--lib/common_test/src/ct_run.erl1682
-rw-r--r--lib/common_test/src/ct_slave.erl439
-rw-r--r--lib/common_test/src/ct_snmp.erl22
-rw-r--r--lib/common_test/src/ct_telnet_client.erl61
-rw-r--r--lib/common_test/src/ct_testspec.erl315
-rw-r--r--lib/common_test/src/ct_util.erl653
-rw-r--r--lib/common_test/src/ct_util.hrl5
-rw-r--r--lib/common_test/src/vts.erl24
20 files changed, 3157 insertions, 1472 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile
index e7e2d1275d..027667e6b0 100644
--- a/lib/common_test/src/Makefile
+++ b/lib/common_test/src/Makefile
@@ -1,19 +1,19 @@
#
# %CopyrightBegin%
-#
-# Copyright Ericsson AB 2003-2009. All Rights Reserved.
-#
+#
+# Copyright Ericsson AB 2003-2010. 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%
#
@@ -63,7 +63,11 @@ MODULES= \
ct_telnet_client \
ct_make \
vts \
- unix_telnet
+ unix_telnet \
+ ct_config \
+ ct_config_plain \
+ ct_config_xml \
+ ct_slave
TARGET_MODULES= $(MODULES:%=$(EBIN)/%)
diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src
index 7b72932ad4..b42173f412 100644
--- a/lib/common_test/src/common_test.app.src
+++ b/lib/common_test/src/common_test.app.src
@@ -1,19 +1,19 @@
% This is an -*- erlang -*- file.
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2009-2010. 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,
@@ -42,10 +42,15 @@
ct_testspec,
ct_util,
unix_telnet,
- vts
+ vts,
+ ct_config,
+ ct_config_plain,
+ ct_config_xml,
+ ct_slave
]},
{registered, [ct_logs,
ct_util_server,
+ ct_config_server,
ct_make_ref,
vts,
ct_master,
diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl
index 8ae041e5b4..0d82a86e7d 100644
--- a/lib/common_test/src/ct.erl
+++ b/lib/common_test/src/ct.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2003-2010. 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%
%%
@@ -59,18 +59,22 @@
%% Test suite API
-export([require/1, require/2,
get_config/1, get_config/2, get_config/3,
+ reload_config/1,
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]).
+ testcases/2, userdata/2, userdata/3,
+ timetrap/1, sleep/1]).
+
+%% New API for manipulating with config handlers
+-export([add_config/2, remove_config/2]).
%% 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]).
@@ -93,7 +97,7 @@
%%% <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>
+%%% <code>run_test</code> program.</p>
install(Opts) ->
ct_run:install(Opts).
@@ -135,14 +139,20 @@ run(TestDirs) ->
%%% @spec run_test(Opts) -> Result
%%% Opts = [OptTuples]
%%% OptTuples = {config,CfgFiles} | {dir,TestDirs} | {suite,Suites} |
-%%% {testcase,Cases} | {group,Groups} | {spec,TestSpecs} |
+%%% {userconfig, UserConfig} |
+%%% {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} |
+%%% {silent_connections,Conns} | {stylesheet,CSSFile} |
+%%% {cover,CoverSpecFile} | {step,StepOpts} |
+%%% {event_handler,EventHandlers} | {include,InclDirs} |
+%%% {auto_compile,Bool} | {multiply_timetraps,M} | {scale_timetraps,Bool} |
+%%% {repeat,N} | {duration,DurTime} | {until,StopTime} |
+%%% {force_stop,Bool} | {decrypt,DecryptKeyOrFile} |
%%% {refresh_logs,LogDir} | {basic_html,Bool}
%%% CfgFiles = [string()] | string()
+%%% UserConfig = [{CallbackMod,CfgStrings}] | {CallbackMod,CfgStrings}
+%%% CallbackMod = atom()
+%%% CfgStrings = [string()] | string()
%%% TestDirs = [string()] | string()
%%% Suites = [string()] | string()
%%% Cases = [atom()] | atom()
@@ -150,6 +160,7 @@ run(TestDirs) ->
%%% TestSpecs = [string()] | string()
%%% LogDir = string()
%%% Conns = all | [atom()]
+%%% CSSFile = string()
%%% CoverSpecFile = string()
%%% StepOpts = [StepOpt] | []
%%% StepOpt = config | keep_inactive
@@ -157,6 +168,7 @@ run(TestDirs) ->
%%% EH = atom() | {atom(),InitArgs} | {[atom()],InitArgs}
%%% InitArgs = [term()]
%%% InclDirs = [string()] | string()
+%%% M = integer()
%%% N = integer()
%%% DurTime = string(HHMMSS)
%%% StopTime = string(YYMoMoDDHHMMSS) | string(HHMMSS)
@@ -165,11 +177,11 @@ run(TestDirs) ->
%%% 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.
+%%% The options are the same as those used with the <code>run_test</code> program.
%%% 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
+%%% program. Configuration files specified in <code>Opts</code> will be
%%% installed automatically at startup.
run_test(Opts) ->
ct_run:run_test(Opts).
@@ -211,7 +223,7 @@ step(TestDir,Suite,Case,Opts) ->
%%%
%%% <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
+%%% started from the OS command line with <code>run_test -shell
%%% [-config File...]</code>.</p>
%%%
%%% <p>If any functions using "required config data" (e.g. telnet or
@@ -269,7 +281,7 @@ stop_interactive() ->
%%% @see get_config/2
%%% @see get_config/3
require(Required) ->
- ct_util:require(Required).
+ ct_config:require(Required).
%%%-----------------------------------------------------------------
%%% @spec require(Name,Required) -> ok | {error,Reason}
@@ -304,19 +316,19 @@ require(Required) ->
%%% @see get_config/2
%%% @see get_config/3
require(Name,Required) ->
- ct_util:require(Name,Required).
+ ct_config:require(Name,Required).
%%%-----------------------------------------------------------------
%%% @spec get_config(Required) -> Value
%%% @equiv get_config(Required,undefined,[])
get_config(Required) ->
- ct_util:get_config(Required,undefined,[]).
+ ct_config: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,[]).
+ ct_config:get_config(Required,Default,[]).
%%%-----------------------------------------------------------------
%%% @spec get_config(Required,Default,Opts) -> ValueOrElement
@@ -375,7 +387,26 @@ get_config(Required,Default) ->
%%% @see require/1
%%% @see require/2
get_config(Required,Default,Opts) ->
- ct_util:get_config(Required,Default,Opts).
+ ct_config:get_config(Required,Default,Opts).
+
+%%%-----------------------------------------------------------------
+%%% @spec reload_config(Required) -> ValueOrElement
+%%% Required = KeyOrName | {KeyOrName,SubKey}
+%%% KeyOrName = atom()
+%%% SubKey = atom()
+%%% ValueOrElement = term()
+%%%
+%%% @doc Reload config file which contains specified configuration key.
+%%%
+%%% <p>This function performs updating of the configuration data from which the
+%%% given configuration variable was read, and returns the (possibly) new
+%%% value of this variable.</p>
+%%% <p>Note that if some variables were present in the configuration but are not loaded
+%%% using this function, they will be removed from the configuration table together
+%%% with their aliases.</p>
+%%%
+reload_config(Required)->
+ ct_config:reload_config(Required).
%%%-----------------------------------------------------------------
%%% @spec log(Format) -> ok
@@ -734,7 +765,7 @@ abort_current_testcase(Reason) ->
%%% <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).
+ ct_config:encrypt_config_file(SrcFileName, EncryptFileName).
%%%-----------------------------------------------------------------
%%% @spec encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile) ->
@@ -754,7 +785,7 @@ encrypt_config_file(SrcFileName, EncryptFileName) ->
%%% <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).
+ ct_config:encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile).
%%%-----------------------------------------------------------------
%%% @spec decrypt_config_file(EncryptFileName, TargetFileName) ->
@@ -770,7 +801,7 @@ encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile) ->
%%% <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).
+ ct_config:decrypt_config_file(EncryptFileName, TargetFileName).
%%%-----------------------------------------------------------------
%%% @spec decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile) ->
@@ -785,5 +816,65 @@ decrypt_config_file(EncryptFileName, TargetFileName) ->
%%% 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).
+ ct_config:decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile).
+
+%%%-----------------------------------------------------------------
+%%% @spec add_config(Callback, Config) -> ok | {error, Reason}
+%%% Callback = atom()
+%%% Config = string()
+%%% Reason = term()
+%%%
+%%% @doc <p>This function loads configuration variables using the
+%%% given callback module and configuration string. Callback module
+%%% should be either loaded or present in the code part. Loaded
+%%% configuration variables can later be removed using
+%%% <code>remove_config/2</code> function.</p>
+add_config(Callback, Config)->
+ ct_config:add_config(Callback, Config).
+
+%%%-----------------------------------------------------------------
+%%% @spec remove_config(Callback, Config) -> ok
+%%% Callback = atom()
+%%% Config = string()
+%%% Reason = term()
+%%%
+%%% @doc <p>This function removes configuration variables (together with
+%%% their aliases) which were loaded with specified callback module and
+%%% configuration string.</p>
+remove_config(Callback, Config) ->
+ ct_config:remove_config(Callback, Config).
+
+%%%-----------------------------------------------------------------
+%%% @spec timetrap(Time) -> ok
+%%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity
+%%% Hours = integer()
+%%% Mins = integer()
+%%% Secs = integer()
+%%% Millisecs = integer() | float()
+%%%
+%%% @doc <p>Use this function to set a new timetrap for the running test case.</p>
+timetrap(Time) ->
+ test_server:timetrap(Time).
+
+%%%-----------------------------------------------------------------
+%%% @spec sleep(Time) -> ok
+%%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity
+%%% Hours = integer()
+%%% Mins = integer()
+%%% Secs = integer()
+%%% Millisecs = integer() | float()
+%%%
+%%% @doc <p>This function, similar to <c>timer:sleep/1</c>, suspends the test
+%%% case for specified time. However, this function also multiplies
+%%% <c>Time</c> with the 'multiply_timetraps' value (if set) and under
+%%% certain circumstances also scales up the time automatically
+%%% if 'scale_timetraps' is set to true (default is false).</p>
+sleep({hours,Hs}) ->
+ sleep(trunc(Hs * 1000 * 60 * 60));
+sleep({minutes,Ms}) ->
+ sleep(trunc(Ms * 1000 * 60));
+sleep({seconds,Ss}) ->
+ sleep(trunc(Ss * 1000));
+sleep(Time) ->
+ test_server:adjusted_sleep(Time).
diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl
new file mode 100644
index 0000000000..dc6fcc66e5
--- /dev/null
+++ b/lib/common_test/src/ct_config.erl
@@ -0,0 +1,786 @@
+%%--------------------------------------------------------------------
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2010. 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%
+%%----------------------------------------------------------------------
+%% File : ct_config.erl
+%% Description : CT module for reading and manipulating of configuration
+%% data
+%%
+%% Created : 15 February 2010
+%%----------------------------------------------------------------------
+-module(ct_config).
+
+-export([start/1, stop/0]).
+
+-export([read_config_files/1,
+ get_config_file_list/1]).
+
+-export([require/1, require/2]).
+
+-export([get_config/1, get_config/2, get_config/3,
+ get_all_config/0]).
+
+-export([set_default_config/2, set_default_config/3]).
+
+-export([delete_default_config/1]).
+
+-export([reload_config/1, update_config/2]).
+
+-export([release_allocated/0]).
+
+-export([encrypt_config_file/2, encrypt_config_file/3,
+ decrypt_config_file/2, decrypt_config_file/3,
+ get_crypt_key_from_file/0, get_crypt_key_from_file/1]).
+
+-export([get_ref_from_name/1, get_name_from_ref/1, get_key_from_name/1]).
+
+-export([check_config_files/1, prepare_config_list/1]).
+
+-export([add_config/2, remove_config/2]).
+
+-include("ct_util.hrl").
+
+-define(cryptfile, ".ct_config.crypt").
+
+-record(ct_conf,{key,value,handler,config,ref,name='_UNDEF',default=false}).
+
+start(Mode) ->
+ case whereis(ct_config_server) of
+ undefined ->
+ Me = self(),
+ Pid = spawn_link(fun() -> do_start(Me) end),
+ receive
+ {Pid,started} -> Pid;
+ {Pid,Error} -> exit(Error)
+ end;
+ Pid ->
+ case ct_util:get_mode() of
+ interactive when Mode==interactive ->
+ Pid;
+ interactive ->
+ {error,interactive_mode};
+ _OtherMode ->
+ Pid
+ end
+ end.
+
+do_start(Parent) ->
+ process_flag(trap_exit,true),
+ register(ct_config_server,self()),
+ ct_util:create_table(?attr_table,bag,#ct_conf.key),
+ {ok,StartDir} = file:get_cwd(),
+ Opts = case ct_util:read_opts() of
+ {ok,Opts1} ->
+ Opts1;
+ Error ->
+ Parent ! {self(),Error},
+ exit(Error)
+ end,
+ case read_config_files(Opts) of
+ ok ->
+ Parent ! {self(),started},
+ loop(StartDir);
+ ReadError ->
+ Parent ! {self(),ReadError},
+ exit(ReadError)
+ end.
+
+stop() ->
+ case whereis(ct_config_server) of
+ undefined -> ok;
+ _ -> call({stop})
+ end.
+
+call(Msg) ->
+ MRef = erlang:monitor(process, whereis(ct_config_server)),
+ Ref = make_ref(),
+ ct_config_server ! {Msg,{self(),Ref}},
+ receive
+ {Ref, Result} ->
+ erlang:demonitor(MRef, [flush]),
+ Result;
+ {'DOWN',MRef,process,_,Reason} ->
+ {error,{ct_util_server_down,Reason}}
+ end.
+
+return({To,Ref},Result) ->
+ To ! {Ref, Result}.
+
+loop(StartDir) ->
+ receive
+ {{require,Name,Tag,SubTags},From} ->
+ Result = do_require(Name,Tag,SubTags),
+ return(From,Result),
+ loop(StartDir);
+ {{set_default_config,{Config,Scope}},From} ->
+ set_config(Config,{true,Scope}),
+ return(From,ok),
+ loop(StartDir);
+ {{set_default_config,{Name,Config,Scope}},From} ->
+ set_config(Name,Config,{true,Scope}),
+ return(From,ok),
+ loop(StartDir);
+ {{delete_default_config,Scope},From} ->
+ delete_config({true,Scope}),
+ return(From,ok),
+ loop(StartDir);
+ {{update_config,{Name,NewConfig}},From} ->
+ update_conf(Name,NewConfig),
+ return(From,ok),
+ loop(StartDir);
+ {{reload_config, KeyOrName},From}->
+ NewValue = reload_conf(KeyOrName),
+ return(From, NewValue),
+ loop(StartDir);
+ {{stop},From} ->
+ ets:delete(?attr_table),
+ file:set_cwd(StartDir),
+ return(From,ok)
+ 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}}).
+
+reload_config(KeyOrName) ->
+ call({reload_config, KeyOrName}).
+
+process_default_configs(Opts) ->
+ 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.
+
+process_user_configs(Opts, Acc) ->
+ case lists:keytake(userconfig, 1, Opts) of
+ false ->
+ lists:reverse(Acc);
+ {value, {userconfig, Config=[{_,_}|_]}, NewOpts} ->
+ Acc1 = lists:map(fun({_Callback, []}=Cfg) ->
+ Cfg;
+ ({Callback, Files=[File|_]}) when is_list(File) ->
+ {Callback, Files};
+ ({Callback, File=[C|_]}) when is_integer(C) ->
+ {Callback, [File]}
+ end, Config),
+ process_user_configs(NewOpts, lists:reverse(Acc1)++Acc);
+ {value, {userconfig, {Callback, []}}, NewOpts} ->
+ process_user_configs(NewOpts, [{Callback, []} | Acc]);
+ {value, {userconfig, {Callback, Files=[File|_]}}, NewOpts} when is_list(File) ->
+ process_user_configs(NewOpts, [{Callback, Files} | Acc]);
+ {value, {userconfig, {Callback, File=[C|_]}}, NewOpts} when is_integer(C) ->
+ process_user_configs(NewOpts, [{Callback, [File]} | Acc])
+ end.
+
+get_config_file_list(Opts) ->
+ DefaultConfigs = process_default_configs(Opts),
+ CfgFiles =
+ if
+ DefaultConfigs == []->
+ [];
+ true->
+ [{?ct_config_txt, DefaultConfigs}]
+ end ++
+ process_user_configs(Opts, []),
+ CfgFiles.
+
+read_config_files(Opts) ->
+ AddCallback = fun(CallBack, []) ->
+ [{CallBack, []}];
+ (CallBack, [F|_]=Files) when is_integer(F) ->
+ [{CallBack, Files}];
+ (CallBack, [F|_]=Files) when is_list(F) ->
+ lists:map(fun(X) -> {CallBack, X} end, Files)
+ end,
+ ConfigFiles = case lists:keyfind(config, 1, Opts) of
+ {config, ConfigLists}->
+ lists:foldr(fun({Callback,Files}, Acc) ->
+ AddCallback(Callback,Files) ++ Acc
+ end,
+ [],
+ ConfigLists);
+ false->
+ []
+ end,
+ read_config_files_int(ConfigFiles, fun store_config/3).
+
+read_config_files_int([{Callback, File}|Files], FunToSave) ->
+ case Callback:read_config(File) of
+ {ok, Config} ->
+ FunToSave(Config, Callback, File),
+ read_config_files_int(Files, FunToSave);
+ {error, ErrorName, ErrorDetail}->
+ {user_error, {ErrorName, File, ErrorDetail}}
+ end;
+read_config_files_int([], _FunToSave) ->
+ ok.
+
+store_config(Config, Callback, File) ->
+ [ets:insert(?attr_table,
+ #ct_conf{key=Key,
+ value=Val,
+ handler=Callback,
+ config=File,
+ ref=ct_util:ct_make_ref(),
+ default=false}) ||
+ {Key,Val} <- Config].
+
+keyfindall(Key, Pos, List) ->
+ [E || E <- List, element(Pos, E) =:= Key].
+
+rewrite_config(Config, Callback, File) ->
+ OldRows = ets:match_object(?attr_table,
+ #ct_conf{handler=Callback,
+ config=File,_='_'}),
+ ets:match_delete(?attr_table,
+ #ct_conf{handler=Callback,
+ config=File,_='_'}),
+ Updater = fun({Key, Value}) ->
+ case keyfindall(Key, #ct_conf.key, OldRows) of
+ []->
+ ets:insert(?attr_table,
+ #ct_conf{key=Key,
+ value=Value,
+ handler=Callback,
+ config=File,
+ ref=ct_util:ct_make_ref()});
+ RowsToUpdate ->
+ Inserter = fun(Row) ->
+ ets:insert(?attr_table,
+ Row#ct_conf{value=Value,
+ ref=ct_util:ct_make_ref()})
+ end,
+ lists:foreach(Inserter, RowsToUpdate)
+ end
+ end,
+ [Updater({Key, Value})||{Key, Value}<-Config].
+
+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_util:ct_make_ref(),
+ name=Name,default=Default}) ||
+ {Key,Val} <- Config].
+
+get_config(KeyOrName) ->
+ get_config(KeyOrName,undefined,[]).
+
+get_config(KeyOrName,Default) ->
+ get_config(KeyOrName,Default,[]).
+
+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'}}]}]).
+
+lookup_handler_for_config({Key, _Subkey}) ->
+ lookup_handler_for_config(Key);
+lookup_handler_for_config(KeyOrName) ->
+ case lookup_handler_for_name(KeyOrName) of
+ [] ->
+ lookup_handler_for_key(KeyOrName);
+ Values ->
+ Values
+ end.
+
+lookup_handler_for_name(Name) ->
+ ets:select(?attr_table,[{#ct_conf{handler='$1',config='$2',name=Name,_='_'},
+ [],
+ [{{'$1','$2'}}]}]).
+
+lookup_handler_for_key(Key) ->
+ ets:select(?attr_table,[{#ct_conf{handler='$1',config='$2',key=Key,_='_'},
+ [],
+ [{{'$1','$2'}}]}]).
+
+
+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.
+
+reload_conf(KeyOrName) ->
+ case lookup_handler_for_config(KeyOrName) of
+ []->
+ undefined;
+ HandlerList->
+ HandlerList2 = lists:usort(HandlerList),
+ read_config_files_int(HandlerList2, fun rewrite_config/3),
+ get_config(KeyOrName)
+ end.
+
+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.
+
+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}}.
+
+delete_config(Default) ->
+ ets:match_delete(?attr_table,#ct_conf{default=Default,_='_'}),
+ ok.
+
+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}}.
+
+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.
+
+encrypt_config_file(SrcFileName, EncryptFileName) ->
+ case get_crypt_key_from_file() of
+ {error,_} = E ->
+ E;
+ Key ->
+ encrypt_config_file(SrcFileName, EncryptFileName, {key,Key})
+ 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.
+
+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.
+
+decrypt_config_file(EncryptFileName, TargetFileName) ->
+ case get_crypt_key_from_file() of
+ {error,_} = E ->
+ E;
+ Key ->
+ decrypt_config_file(EncryptFileName, TargetFileName, {key,Key})
+ end.
+
+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]).
+
+check_callback_load(Callback) ->
+ case code:is_loaded(Callback) of
+ {file, _Filename}->
+ check_exports(Callback);
+ false->
+ case code:load_file(Callback) of
+ {module, Callback}->
+ check_exports(Callback);
+ {error, Error}->
+ {error, Error}
+ end
+ end.
+
+check_exports(Callback) ->
+ Fs = Callback:module_info(exports),
+ case {lists:member({check_parameter,1},Fs),
+ lists:member({read_config,1},Fs)} of
+ {true, true} ->
+ {ok, Callback};
+ _ ->
+ {error, missing_callback_functions}
+ end.
+
+check_config_files(Configs) ->
+ ConfigChecker = fun
+ ({Callback, [F|_R]=Files}) ->
+ case check_callback_load(Callback) of
+ {ok, Callback} ->
+ if is_integer(F) ->
+ Callback:check_parameter(Files);
+ is_list(F) ->
+ lists:map(fun(File) ->
+ Callback:check_parameter(File)
+ end,
+ Files)
+ end;
+ {error, Why}->
+ {error, {callback, {Callback,Why}}}
+ end;
+ ({Callback, []}) ->
+ case check_callback_load(Callback) of
+ {ok, Callback}->
+ Callback:check_parameter([]);
+ {error, Why}->
+ {error, {callback, {Callback,Why}}}
+ end
+ end,
+ lists:keysearch(error, 1, lists:flatten(lists:map(ConfigChecker, Configs))).
+
+prepare_user_configs([ConfigString|UserConfigs], Acc, new) ->
+ prepare_user_configs(UserConfigs,
+ [{list_to_atom(ConfigString), []}|Acc],
+ cur);
+prepare_user_configs(["and"|UserConfigs], Acc, _) ->
+ prepare_user_configs(UserConfigs, Acc, new);
+prepare_user_configs([ConfigString|UserConfigs], [{LastMod, LastList}|Acc], cur) ->
+ prepare_user_configs(UserConfigs,
+ [{LastMod, [ConfigString|LastList]}|Acc],
+ cur);
+prepare_user_configs([], Acc, _) ->
+ Acc.
+
+prepare_config_list(Args) ->
+ ConfigFiles = case lists:keysearch(ct_config, 1, Args) of
+ {value,{ct_config,Files}}->
+ [{?ct_config_txt,[filename:absname(F) || F <- Files]}];
+ false->
+ []
+ end,
+ UserConfigs = case lists:keysearch(userconfig, 1, Args) of
+ {value,{userconfig,UserConfigFiles}}->
+ prepare_user_configs(UserConfigFiles, [], new);
+ false->
+ []
+ end,
+ ConfigFiles ++ UserConfigs.
+
+% TODO: add logging of the loaded configuration file to the CT FW log!!!
+add_config(Callback, []) ->
+ read_config_files_int([{Callback, []}], fun store_config/3);
+add_config(Callback, [File|_Files]=Config) when is_list(File) ->
+ lists:foreach(fun(CfgStr) ->
+ read_config_files_int([{Callback, CfgStr}], fun store_config/3) end,
+ Config);
+add_config(Callback, [C|_]=Config) when is_integer(C) ->
+ read_config_files_int([{Callback, Config}], fun store_config/3),
+ ok.
+
+remove_config(Callback, Config) ->
+ ets:match_delete(?attr_table,
+ #ct_conf{handler=Callback,
+ config=Config,_='_'}),
+ ok.
diff --git a/lib/common_test/src/ct_config_plain.erl b/lib/common_test/src/ct_config_plain.erl
new file mode 100644
index 0000000000..3fbc8af9fb
--- /dev/null
+++ b/lib/common_test/src/ct_config_plain.erl
@@ -0,0 +1,109 @@
+%%--------------------------------------------------------------------
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2010. 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%
+%%----------------------------------------------------------------------
+%% File : ct_config_plain.erl
+%% Description : CT callback module for reading configs from text files
+%%
+%% Created : 15 February 2010
+%%----------------------------------------------------------------------
+-module(ct_config_plain).
+-export([read_config/1, check_parameter/1]).
+
+read_config(ConfigFile) ->
+ case file:consult(ConfigFile) of
+ {ok,Config} ->
+ {ok, Config};
+ {error,enoent} ->
+ {error, config_file_error, enoent};
+ {error,Reason} ->
+ Key =
+ case application:get_env(common_test, decrypt) of
+ {ok,KeyOrFile} ->
+ case KeyOrFile of
+ {key,K} ->
+ K;
+ {file,F} ->
+ ct_config:get_crypt_key_from_file(F)
+ end;
+ _ ->
+ ct_config:get_crypt_key_from_file()
+ end,
+ case Key of
+ {error,no_crypt_file} ->
+ {error, config_file_error, Reason};
+ {error,CryptError} ->
+ {error, decrypt_file_error, CryptError};
+ _ when is_list(Key) ->
+ case ct_config:decrypt_config_file(ConfigFile, undefined, {key,Key}) of
+ {ok,CfgBin} ->
+ case read_config_terms(CfgBin) of
+ {error,ReadFail} ->
+ {error, config_file_error, ReadFail};
+ Config ->
+ {ok, Config}
+ end;
+ {error,DecryptFail} ->
+ {error, decrypt_config_error, DecryptFail}
+ end;
+ _ ->
+ {error, bad_decrypt_key, Key}
+ end
+ end.
+
+% check if config file exists
+check_parameter(File)->
+ case filelib:is_file(File) of
+ true->
+ {ok, {file, File}};
+ false->
+ {error, {nofile, File}}
+ end.
+
+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.
diff --git a/lib/common_test/src/ct_config_xml.erl b/lib/common_test/src/ct_config_xml.erl
new file mode 100644
index 0000000000..8a6e75e635
--- /dev/null
+++ b/lib/common_test/src/ct_config_xml.erl
@@ -0,0 +1,118 @@
+%%--------------------------------------------------------------------
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2010. 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%
+%%----------------------------------------------------------------------
+%% File : ct_config_xml.erl
+%% Description : CT callback module for reading configs from XML files
+%%
+%% Created : 16 February 2010
+%%----------------------------------------------------------------------
+-module(ct_config_xml).
+-export([read_config/1, check_parameter/1]).
+
+% read config file
+read_config(ConfigFile) ->
+ case catch do_read_xml_config(ConfigFile) of
+ {ok, Config}->
+ {ok, Config};
+ {error, Error, ErroneousString}->
+ {error, Error, ErroneousString}
+ end.
+
+% check file exists
+check_parameter(File)->
+ case filelib:is_file(File) of
+ true->
+ {ok, {file, File}};
+ false->
+ {error, {nofile, File}}
+ end.
+
+% actual reading of the config
+do_read_xml_config(ConfigFile)->
+ case catch xmerl_sax_parser:file(ConfigFile,
+ [{event_fun, fun event/3},
+ {event_state, []}]) of
+ {ok, EntityList, _}->
+ {ok, lists:reverse(transform_entity_list(EntityList))};
+ Oops->
+ {error, parsing_failed, Oops}
+ end.
+
+% event callback for xmerl_sax_parser
+event(Event, _LineNo, State) ->
+ tag(Event, State).
+
+% document start
+tag(startDocument, State) ->
+ State;
+
+% start of the config
+tag({startElement, _Uri, "config", _QName, _Attributes}, []) ->
+ [{"config", []}];
+
+% start tag
+tag({startElement, _Uri, Name, _QName, _Attributes}, Tags) ->
+ [{Name, []}|Tags];
+
+% value
+tag({characters, String}, [{Tag, _Value}|Tags]) ->
+ [{Tag, String}|Tags];
+
+% end tag
+tag({endElement, _Uri, _Name, _QName},
+ [Entity, {PrevEntityTag, PrevEntityValue}|Tags]) ->
+ NewHead = {PrevEntityTag, [Entity|PrevEntityValue]},
+ [NewHead|Tags];
+
+% end of the config
+tag({endElement, _Uri, "config", _QName}, [{"config", Config}]) ->
+ Config;
+
+% end of document, return result
+tag(endDocument, {_Tags, Result}) ->
+ Result;
+
+% default
+tag(_El, State) ->
+ State.
+
+% transform of the ugly deeply nested entity list to the key-value "tree"
+transform_entity_list(EntityList)->
+ lists:map(fun transform_entity/1, EntityList).
+
+% transform entity from {list(), list()} to {atom(), term()}
+transform_entity({Tag, [Value|Rest]}) when
+ is_tuple(Value)->
+ {list_to_atom(Tag), transform_entity_list(lists:reverse([Value|Rest]))};
+transform_entity({Tag, String})->
+ case list_to_term(String) of
+ {ok, Value}->
+ {list_to_atom(Tag), Value};
+ Error->
+ throw(Error)
+ end.
+
+% transform a string with Erlang terms
+list_to_term(String) ->
+ {ok, T, _} = erl_scan:string(String++"."),
+ case catch erl_parse:parse_term(T) of
+ {ok, Term} ->
+ {ok, Term};
+ Error ->
+ {error, Error, String}
+ end.
diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl
index ed8b564921..3dd1026f13 100644
--- a/lib/common_test/src/ct_framework.erl
+++ b/lib/common_test/src/ct_framework.erl
@@ -113,9 +113,9 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) ->
ok;
true ->
%% delete all default values used in previous suite
- ct_util:delete_default_config(suite),
+ ct_config:delete_default_config(suite),
%% release all name -> key bindings (once per suite)
- ct_util:release_allocated()
+ ct_config:release_allocated()
end,
TestCaseInfo =
case catch apply(Mod,Func,[]) of
@@ -125,7 +125,7 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) ->
%% 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),
+ ct_config:delete_default_config(testcase),
case add_defaults(Mod,Func,TestCaseInfo,DoInit) of
Error = {suite0_failed,_} ->
ct_logs:init_tc(),
@@ -161,6 +161,7 @@ init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) ->
_ ->
MergeResult
end,
+
%% timetrap must be handled before require
MergedInfo1 = timetrap_first(MergedInfo, [], []),
%% tell logger to use specified style sheet
@@ -244,8 +245,8 @@ add_defaults(Mod,Func,FuncInfo,DoInit) ->
_ ->
{suite0_failed,bad_return_value}
end.
-
-add_defaults1(_Mod,init_per_suite,[],SuiteInfo,_) ->
+
+add_defaults1(_Mod,init_per_suite,[],SuiteInfo,_DoInit) ->
SuiteInfo;
add_defaults1(Mod,Func,FuncInfo,SuiteInfo,DoInit) ->
@@ -253,15 +254,27 @@ add_defaults1(Mod,Func,FuncInfo,SuiteInfo,DoInit) ->
%% 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
+ ((require == element(1,SDDef)) or
+ (default_config == element(1,SDDef)))],
+ FuncReqs =
+ [FIDef || FIDef <- FuncInfo,
+ require == element(1,FIDef)],
+ case [element(2,Clash) || Clash <- SuiteReqs,
+ require == element(1, Clash),
+ true == lists:keymember(element(2,Clash),2,
+ FuncReqs)] of
[] ->
add_defaults2(Mod,Func,FuncInfo,SuiteInfo,SuiteReqs,DoInit);
Clashes ->
{error,{config_name_already_in_use,Clashes}}
end.
+add_defaults2(Mod,init_per_suite,IPSInfo,SuiteInfo,SuiteReqs,false) ->
+ %% not common practise to use a test case info function for
+ %% init_per_suite (usually handled by suite/0), but let's support
+ %% it just in case...
+ add_defaults2(Mod,init_per_suite,IPSInfo,SuiteInfo,SuiteReqs,true);
+
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)
@@ -381,10 +394,10 @@ try_set_default(Name,Key,Info,Where) ->
{_,[]} ->
no_default;
{'_UNDEF',_} ->
- [ct_util:set_default_config([CfgVal],Where) || CfgVal <- CfgElems],
+ [ct_config:set_default_config([CfgVal],Where) || CfgVal <- CfgElems],
ok;
_ ->
- [ct_util:set_default_config(Name,[CfgVal],Where) || CfgVal <- CfgElems],
+ [ct_config:set_default_config(Name,[CfgVal],Where) || CfgVal <- CfgElems],
ok
end.
@@ -631,7 +644,7 @@ group_or_func(Func, _Config) ->
%%% and every test case. If the former, all test cases in the suite
%%% should be returned.
-get_suite(Mod, all) ->
+get_suite(Mod, all) ->
case catch apply(Mod, groups, []) of
{'EXIT',_} ->
get_all(Mod, []);
@@ -667,12 +680,18 @@ get_suite(Mod, Name) ->
%% (and only) test case so we can report Error properly
[{?MODULE,error_in_suite,[[Error]]}];
ConfTests ->
+
+ %%! --- Thu Jun 3 19:13:22 2010 --- peppe was here!
+ %%! HEERE!
+ %%! Must be able to search recursively for group Name,
+ %%! this only handles top level groups!
+
FindConf = fun({conf,Props,_,_,_}) ->
case proplists:get_value(name, Props) of
Name -> true;
_ -> false
end
- end,
+ end,
case lists:filter(FindConf, ConfTests) of
[] -> % must be a test case
get_seq(Mod, Name);
diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl
index a31e57c7ea..5aab4dd2dd 100644
--- a/lib/common_test/src/ct_gen_conn.erl
+++ b/lib/common_test/src/ct_gen_conn.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2003-2010. 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%
%%
@@ -76,7 +76,7 @@ start(Name,Address,InitData,CallbackMod) ->
MRef = erlang:monitor(process,Pid),
receive
{connected,Pid} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
ct_util:register_connection(Name,Address,CallbackMod,Pid),
{ok,Pid};
{Error,Pid} ->
@@ -182,7 +182,7 @@ call(Pid,Msg) ->
Pid ! {Msg,{self(),Ref}},
receive
{Ref, Result} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
case Result of
{retry,_Data} ->
call(Pid,Result);
diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl
index bd1a89ae1f..5683d06aa7 100644
--- a/lib/common_test/src/ct_logs.erl
+++ b/lib/common_test/src/ct_logs.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2003-2010. 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%
%%
@@ -80,7 +80,7 @@ init(Mode) ->
MRef = erlang:monitor(process,Pid),
receive
{started,Pid,Result} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
Result;
{'DOWN',MRef,process,_,Reason} ->
exit({could_not_start_process,?MODULE,Reason})
@@ -163,7 +163,7 @@ call(Msg) ->
?MODULE ! {Msg,{self(),Ref}},
receive
{Ref, Result} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
Result;
{'DOWN',MRef,process,_,Reason} ->
{error,{process_down,?MODULE,Reason}}
@@ -505,7 +505,7 @@ logger_loop(State) ->
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]),
+ io:format(Fd, "~p loading 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);
diff --git a/lib/common_test/src/ct_master.erl b/lib/common_test/src/ct_master.erl
index 7eb2c3cfef..42e4cf08f4 100644
--- a/lib/common_test/src/ct_master.erl
+++ b/lib/common_test/src/ct_master.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2006-2010. 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%
%%
@@ -28,7 +28,7 @@
-export([abort/0,abort/1,progress/0]).
--export([init_master/6, init_node_ctrl/3]).
+-export([init_master/7, init_node_ctrl/3]).
-export([status/2]).
@@ -49,7 +49,8 @@
%%% 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}
+%%% {silent_connections,Conns} | {cover,CoverSpecFile} |
+%%% {userconfig, UserCfgFiles}
%%% CfgFiles = string() | [string()]
%%% TestDirs = string() | [string()]
%%% Suites = atom() | [atom()]
@@ -98,11 +99,14 @@ run([TS|TestSpecs],AllowUserTerms,InclNodes,ExclNodes) when is_list(TS),
{error,Reason} ->
{error,Reason};
TSRec=#testspec{logdir=AllLogDirs,
- config=AllCfgFiles,
+ config=StdCfgFiles,
+ userconfig=UserCfgFiles,
+ init=AllInitOpts,
event_handler=AllEvHs} ->
+ AllCfgFiles = {StdCfgFiles, UserCfgFiles},
RunSkipPerNode = ct_testspec:prepare_tests(TSRec),
RunSkipPerNode2 = exclude_nodes(ExclNodes,RunSkipPerNode),
- run_all(RunSkipPerNode2,AllLogDirs,AllCfgFiles,AllEvHs,[],[],TS1)
+ run_all(RunSkipPerNode2,AllLogDirs,AllCfgFiles,AllEvHs,[],[],AllInitOpts,TS1)
end,
[{TS,Result} | run(TestSpecs,AllowUserTerms,InclNodes,ExclNodes)];
run([],_,_,_) ->
@@ -157,10 +161,13 @@ run_on_node([TS|TestSpecs],AllowUserTerms,Node) when is_list(TS),is_atom(Node) -
{error,Reason} ->
{error,Reason};
TSRec=#testspec{logdir=AllLogDirs,
- config=AllCfgFiles,
+ config=StdCfgFiles,
+ init=AllInitOpts,
+ userconfig=UserCfgFiles,
event_handler=AllEvHs} ->
+ AllCfgFiles = {StdCfgFiles, UserCfgFiles},
{Run,Skip} = ct_testspec:prepare_tests(TSRec,Node),
- run_all([{Node,Run,Skip}],AllLogDirs,AllCfgFiles,AllEvHs,[],[],TS1)
+ run_all([{Node,Run,Skip}],AllLogDirs,AllCfgFiles,AllEvHs,[],[],AllInitOpts,TS1)
end,
[{TS,Result} | run_on_node(TestSpecs,AllowUserTerms,Node)];
run_on_node([],_,_) ->
@@ -180,7 +187,9 @@ run_on_node(TestSpecs,Node) ->
-run_all([{Node,Run,Skip}|Rest],AllLogDirs,AllCfgFiles,AllEvHs,NodeOpts,LogDirs,Specs) ->
+run_all([{Node,Run,Skip}|Rest],AllLogDirs,
+ {AllStdCfgFiles, AllUserCfgFiles}=AllCfgFiles,
+ AllEvHs,NodeOpts,LogDirs,InitOptions,Specs) ->
LogDir =
lists:foldl(fun({N,Dir},_Found) when N == Node ->
Dir;
@@ -191,29 +200,36 @@ run_all([{Node,Run,Skip}|Rest],AllLogDirs,AllCfgFiles,AllEvHs,NodeOpts,LogDirs,S
(_Dir,Found) ->
Found
end,".",AllLogDirs),
- CfgFiles =
+
+ StdCfgFiles =
lists:foldr(fun({N,F},Fs) when N == Node -> [F|Fs];
({_N,_F},Fs) -> Fs;
(F,Fs) -> [F|Fs]
- end,[],AllCfgFiles),
+ end,[],AllStdCfgFiles),
+ UserCfgFiles =
+ lists:foldr(fun({N,F},Fs) when N == Node -> [{userconfig, F}|Fs];
+ ({_N,_F},Fs) -> Fs;
+ (F,Fs) -> [{userconfig, F}|Fs]
+ end,[],AllUserCfgFiles),
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) ->
+ {config,StdCfgFiles},
+ {event_handler,EvHs}] ++ UserCfgFiles},
+ run_all(Rest,AllLogDirs,AllCfgFiles,AllEvHs,[NO|NodeOpts],[LogDir|LogDirs],InitOptions,Specs);
+run_all([],AllLogDirs,_,AllEvHs,NodeOpts,LogDirs,InitOptions,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),
+ start_master(lists:reverse(NodeOpts),Handlers,MasterLogDir,LogDirs,InitOptions,Specs),
ok.
@@ -251,17 +267,17 @@ progress() ->
%%% MASTER, runs on central controlling node.
%%%-----------------------------------------------------------------
start_master(NodeOptsList) ->
- start_master(NodeOptsList,[],".",[],[]).
+ start_master(NodeOptsList,[],".",[],[],[]).
-start_master(NodeOptsList,EvHandlers,MasterLogDir,LogDirs,Specs) ->
+start_master(NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Specs) ->
Master = spawn_link(?MODULE,init_master,[self(),NodeOptsList,EvHandlers,
- MasterLogDir,LogDirs,Specs]),
+ MasterLogDir,LogDirs,InitOptions,Specs]),
receive
{Master,Result} -> Result
end.
%%% @hidden
-init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,Specs) ->
+init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,InitOptions,Specs) ->
case whereis(ct_master) of
undefined ->
register(ct_master,self()),
@@ -314,10 +330,10 @@ init_master(Parent,NodeOptsList,EvHandlers,MasterLogDir,LogDirs,Specs) ->
Pid when is_pid(Pid) ->
ok
end,
- init_master1(Parent,NodeOptsList,LogDirs).
+ init_master1(Parent,NodeOptsList,InitOptions,LogDirs).
-init_master1(Parent,NodeOptsList,LogDirs) ->
- {Inaccessible,NodeOptsList1} = ping_nodes(NodeOptsList,[],[]),
+init_master1(Parent,NodeOptsList,InitOptions,LogDirs) ->
+ {Inaccessible,NodeOptsList1,InitOptions1} = init_nodes(NodeOptsList,InitOptions),
case Inaccessible of
[] ->
init_master2(Parent,NodeOptsList,LogDirs);
@@ -331,7 +347,7 @@ init_master1(Parent,NodeOptsList,LogDirs) ->
"Proceeding without: ~p",[Inaccessible]),
init_master2(Parent,NodeOptsList1,LogDirs);
"r\n" ->
- init_master1(Parent,NodeOptsList,LogDirs);
+ init_master1(Parent,NodeOptsList,InitOptions1,LogDirs);
_ ->
log(html,"Aborting Tests","",[]),
ct_master_event:stop(),
@@ -542,6 +558,9 @@ get_pid(Node,NodeCtrlPids) ->
undefined
end.
+ping_nodes(NodeOptions)->
+ ping_nodes(NodeOptions, [], []).
+
ping_nodes([NO={Node,_Opts}|NOs],Inaccessible,NodeOpts) ->
case net_adm:ping(Node) of
pong ->
@@ -678,13 +697,80 @@ call(Pid,Msg) ->
{'DOWN', Ref, _, _, _} ->
{error,master_died}
end,
- erlang:demonitor(Ref),
+ erlang:demonitor(Ref, [flush]),
Return.
reply(Result,To) ->
To ! {self(),Result},
ok.
+init_nodes(NodeOptions, InitOptions)->
+ ping_nodes(NodeOptions),
+ start_nodes(InitOptions),
+ eval_on_nodes(InitOptions),
+ {Inaccessible, NodeOptions1}=ping_nodes(NodeOptions),
+ InitOptions1 = filter_accessible(InitOptions, Inaccessible),
+ {Inaccessible, NodeOptions1, InitOptions1}.
+
+% only nodes which are inaccessible now, should be initiated later
+filter_accessible(InitOptions, Inaccessible)->
+ [{Node,Option}||{Node,Option}<-InitOptions, lists:member(Node, Inaccessible)].
+
+start_nodes(InitOptions)->
+ lists:foreach(fun({NodeName, Options})->
+ [NodeS,HostS]=string:tokens(atom_to_list(NodeName), "@"),
+ Node=list_to_atom(NodeS),
+ Host=list_to_atom(HostS),
+ HasNodeStart = lists:keymember(node_start, 1, Options),
+ IsAlive = lists:member(NodeName, nodes()),
+ case {HasNodeStart, IsAlive} of
+ {false, false}->
+ io:format("WARNING: Node ~p is not alive but has no node_start option~n", [NodeName]);
+ {false, true}->
+ io:format("Node ~p is alive~n", [NodeName]);
+ {true, false}->
+ {node_start, NodeStart} = lists:keyfind(node_start, 1, Options),
+ {value, {callback_module, Callback}, NodeStart2}=
+ lists:keytake(callback_module, 1, NodeStart),
+ case Callback:start(Host, Node, NodeStart2) of
+ {ok, NodeName} ->
+ io:format("Node ~p started successfully with callback ~p~n", [NodeName,Callback]);
+ {error, Reason, _NodeName} ->
+ io:format("Failed to start node ~p with callback ~p! Reason: ~p~n", [NodeName, Callback, Reason])
+ end;
+ {true, true}->
+ io:format("WARNING: Node ~p is alive but has node_start option~n", [NodeName])
+ end
+ end,
+ InitOptions).
+
+eval_on_nodes(InitOptions)->
+ lists:foreach(fun({NodeName, Options})->
+ HasEval = lists:keymember(eval, 1, Options),
+ IsAlive = lists:member(NodeName, nodes()),
+ case {HasEval, IsAlive} of
+ {false,_}->
+ ok;
+ {true,false}->
+ io:format("WARNING: Node ~p is not alive but has eval option ~n", [NodeName]);
+ {true,true}->
+ {eval, MFAs} = lists:keyfind(eval, 1, Options),
+ evaluate(NodeName, MFAs)
+ end
+ end,
+ InitOptions).
+
+evaluate(Node, [{M,F,A}|MFAs])->
+ case rpc:call(Node, M, F, A) of
+ {badrpc,Reason}->
+ io:format("WARNING: Failed to call ~p:~p/~p on node ~p due to ~p~n", [M,F,length(A),Node,Reason]);
+ Result->
+ io:format("Called ~p:~p/~p on node ~p, result: ~p~n", [M,F,length(A),Node,Result])
+ end,
+ evaluate(Node, MFAs);
+evaluate(_Node, [])->
+ ok.
+
%cast(Msg) ->
% cast(whereis(ct_master),Msg).
diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl
index 63f60b1182..244faace06 100644
--- a/lib/common_test/src/ct_master_logs.erl
+++ b/lib/common_test/src/ct_master_logs.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2006-2010. 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%
%%
@@ -44,7 +44,7 @@ start(LogDir,Nodes) ->
MRef = erlang:monitor(process,Pid),
receive
{started,Pid,Result} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
{Pid,Result};
{'DOWN',MRef,process,_,Reason} ->
exit({could_not_start_process,?MODULE,Reason})
@@ -435,7 +435,7 @@ call(Msg) ->
?MODULE ! {Msg,{self(),Ref}},
receive
{Ref, Result} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
Result;
{'DOWN',MRef,process,_,Reason} ->
{error,{process_down,?MODULE,Reason}}
diff --git a/lib/common_test/src/ct_repeat.erl b/lib/common_test/src/ct_repeat.erl
index 7ac6e045d7..be3c485b75 100644
--- a/lib/common_test/src/ct_repeat.erl
+++ b/lib/common_test/src/ct_repeat.erl
@@ -1,26 +1,26 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2007-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2007-2010. 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:
+%%% start flags (or equivalent ct:run_test/1 options) are supported:
%%% -until <StopTime>, StopTime = YYMoMoDDHHMMSS | HHMMSS
%%% -duration <DurTime>, DurTime = HHMMSS
%%% -force_stop
diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl
index 6b1063f74c..6a9c42d1b9 100644
--- a/lib/common_test/src/ct_run.erl
+++ b/lib/common_test/src/ct_run.erl
@@ -24,7 +24,6 @@
-module(ct_run).
-
%% Script interface
-export([script_start/0,script_usage/0]).
@@ -46,11 +45,27 @@
-define(abs(Name), filename:absname(Name)).
-define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)).
+-record(opts, {vts,
+ shell,
+ cover,
+ coverspec,
+ step,
+ logdir,
+ config = [],
+ event_handlers = [],
+ include = [],
+ silent_connections,
+ stylesheet,
+ multiply_timetraps = 1,
+ scale_timetraps = false,
+ testspecs = [],
+ tests}).
+
%%%-----------------------------------------------------------------
%%% @spec script_start() -> void()
%%%
-%%% @doc Start tests via the run_test script.
-%%%
+%%% @doc Start tests via the run_test program or script.
+%%%
%%% <p>Example:<br/><code>./run_test -config config.ctc -dir
%%% $TEST_DIR</code></p>
%%%
@@ -59,13 +74,53 @@
%%%
script_start() ->
process_flag(trap_exit, true),
- Args = merge_arguments(init:get_arguments()),
+ Init = init:get_arguments(),
+ CtArgs = lists:takewhile(fun({ct_erl_args,_}) -> false;
+ (_) -> true end, Init),
+
+ %% convert relative dirs added with pa or pz (pre erl_args on
+ %% the run_test command line) to absolute so that app modules
+ %% can be found even after CT changes CWD to logdir
+ rel_to_abs(CtArgs),
+
+ Args =
+ case application:get_env(common_test, run_test_start_opts) of
+ {ok,EnvStartOpts} ->
+ FlagFilter = fun(Flags) ->
+ lists:filter(fun({root,_}) -> false;
+ ({progname,_}) -> false;
+ ({home,_}) -> false;
+ ({noshell,_}) -> false;
+ ({noinput,_}) -> false;
+ (_) -> true
+ end, Flags)
+ end,
+ %% used for purpose of testing the run_test interface
+ io:format(user, "~n-------------------- START ARGS --------------------~n", []),
+ io:format(user, "--- Init args:~n~p~n", [FlagFilter(Init)]),
+ io:format(user, "--- CT args:~n~p~n", [FlagFilter(CtArgs)]),
+ EnvArgs = opts2args(EnvStartOpts),
+ io:format(user, "--- Env opts -> args:~n~p~n =>~n~p~n",
+ [EnvStartOpts,EnvArgs]),
+ Merged = merge_arguments(CtArgs ++ EnvArgs),
+ io:format(user, "--- Merged args:~n~p~n", [FlagFilter(Merged)]),
+ io:format(user, "----------------------------------------------------~n~n", []),
+ Merged;
+ _ ->
+ merge_arguments(CtArgs)
+ end,
+ case proplists:get_value(help, Args) of
+ undefined -> script_start(Args);
+ _ -> script_usage()
+ end.
+
+script_start(Args) ->
Tracing = start_trace(Args),
- Res =
+ Res =
case ct_repeat:loop_test(script, Args) of
- false ->
+ false ->
{ok,Cwd} = file:get_cwd(),
- CTVsn =
+ CTVsn =
case filename:basename(code:lib_dir(common_test)) of
CTBase when is_list(CTBase) ->
case string:tokens(CTBase, "-") of
@@ -76,7 +131,7 @@ script_start() ->
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
+ receive
{'EXIT',Pid,Reason} ->
case Reason of
{user_error,What} ->
@@ -101,293 +156,320 @@ script_start() ->
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]}} ->
+ %% read general start flags
+ Vts = get_start_opt(vts, true, Args),
+ Shell = get_start_opt(shell, true, Args),
+ Cover = get_start_opt(cover, fun([CoverFile]) -> ?abs(CoverFile) end, Args),
+ LogDir = get_start_opt(logdir, fun([LogD]) -> LogD end, ".", Args),
+ MultTT = get_start_opt(multiply_timetraps,
+ fun([MT]) -> list_to_integer(MT) end, 1, Args),
+ ScaleTT = get_start_opt(scale_timetraps,
+ fun([CT]) -> list_to_atom(CT);
+ ([]) -> true
+ end, false, Args),
+ EvHandlers = event_handler_args2opts(Args),
+
+ %% check flags and set corresponding application env variables
+
+ %% ct_decrypt_key | ct_decrypt_file
+ case proplists:get_value(ct_decrypt_key, Args) of
+ [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 ->
+ undefined ->
+ case proplists:get_value(ct_decrypt_file, Args) of
+ [DecryptFile] ->
+ application:set_env(common_test, decrypt,
+ {file,?abs(DecryptFile)});
+ undefined ->
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];
+ %% no_auto_compile + include
+ IncludeDirs =
+ case proplists:get_value(no_auto_compile, Args) of
+ undefined ->
+ application:set_env(common_test, auto_compile, true),
+ InclDirs =
+ case proplists:get_value(include, Args) of
+ Incl when is_list(hd(Incl)) ->
+ Incl;
+ Incl when is_list(Incl) ->
+ [Incl];
+ undefined ->
+ []
+ end,
+ case os:getenv("CT_INCLUDE_PATH") of
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
+ application:set_env(common_test, include, InclDirs),
+ InclDirs;
+ CtInclPath ->
+ AllInclDirs =
+ string:tokens(CtInclPath,[$:,$ ,$,]) ++ InclDirs,
+ application:set_env(common_test, include, AllInclDirs),
+ AllInclDirs
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
+ _ ->
+ application:set_env(common_test, auto_compile, false),
+ []
end,
+ %% silent connections
+ SilentConns =
+ get_start_opt(silent_connections,
+ fun(["all"]) -> [];
+ (Conns) -> [list_to_atom(Conn) || Conn <- Conns]
+ end, Args),
+ %% stylesheet
+ Stylesheet = get_start_opt(stylesheet,
+ fun([SS]) -> ?abs(SS) end, Args),
+ %% basic_html - used by ct_logs
+ case proplists:get_value(basic_html, Args) of
+ undefined ->
+ application:set_env(common_test, basic_html, false);
+ _ ->
+ application:set_env(common_test, basic_html, true)
+ end,
+
+ StartOpts = #opts{vts = Vts, shell = Shell, cover = Cover,
+ logdir = LogDir, event_handlers = EvHandlers,
+ include = IncludeDirs,
+ silent_connections = SilentConns,
+ stylesheet = Stylesheet,
+ multiply_timetraps = MultTT,
+ scale_timetraps = ScaleTT},
+
+ %% check if log files should be refreshed or go on to run tests...
+ Result = run_or_refresh(StartOpts, Args),
+ %% send final results to starting process waiting in script_start/0
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).
+run_or_refresh(StartOpts = #opts{logdir = LogDir}, Args) ->
+ case proplists:get_value(refresh_logs, Args) of
+ undefined ->
+ script_start2(StartOpts, Args);
+ Refresh ->
+ LogDir1 = case Refresh of
+ [] -> which(logdir,LogDir);
+ [RefreshDir] -> ?abs(RefreshDir)
+ end,
+ {ok,Cwd} = file:get_cwd(),
+ file:set_cwd(LogDir1),
+ %% give the shell time to print version etc
+ timer:sleep(500),
+ 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
+ end.
-script_start2(false, ConfigFiles, EvHandlers, Args, LogDir, Cover) ->
- case lists:keysearch(spec, 1, Args) of
- {value,{spec,[]}} ->
+script_start2(StartOpts = #opts{vts = undefined,
+ shell = undefined}, Args) ->
+ TestSpec = proplists:get_value(spec, Args),
+ {Terms,Opts} =
+ case TestSpec of
+ Specs when Specs =/= [], Specs =/= undefined ->
+ %% using testspec as input for test
+ Relaxed = get_start_opt(allow_user_terms, true, false, Args),
+ case catch ct_testspec:collect_tests_from_file(Specs, Relaxed) of
+ {E,Reason} when E == error ; E == 'EXIT' ->
+ {{error,Reason},StartOpts};
+ TS ->
+ SpecStartOpts = get_data_for_node(TS, node()),
+
+ LogDir = choose_val(StartOpts#opts.logdir,
+ SpecStartOpts#opts.logdir),
+
+ Cover = choose_val(StartOpts#opts.cover,
+ SpecStartOpts#opts.cover),
+ MultTT = choose_val(StartOpts#opts.multiply_timetraps,
+ SpecStartOpts#opts.multiply_timetraps),
+ ScaleTT = choose_val(StartOpts#opts.scale_timetraps,
+ SpecStartOpts#opts.scale_timetraps),
+ AllEvHs = merge_vals([StartOpts#opts.event_handlers,
+ SpecStartOpts#opts.event_handlers]),
+ AllInclude = merge_vals([StartOpts#opts.include,
+ SpecStartOpts#opts.include]),
+ application:set_env(common_test, include, AllInclude),
+
+ {TS,StartOpts#opts{testspecs = Specs,
+ cover = Cover,
+ logdir = LogDir,
+ config = SpecStartOpts#opts.config,
+ event_handlers = AllEvHs,
+ include = AllInclude,
+ multiply_timetraps = MultTT,
+ scale_timetraps = ScaleTT}}
+ end;
+ _ ->
+ {undefined,StartOpts}
+ end,
+ %% read config/userconfig from start flags
+ InitConfig = ct_config:prepare_config_list(Args),
+ case {TestSpec,Terms} of
+ {_,{error,_}=Error} ->
+ Error;
+ {[],_} ->
{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
+ {undefined,_} -> % no testspec used
+ case check_and_install_configfiles(InitConfig,
+ which(logdir,Opts#opts.logdir),
+ Opts#opts.event_handlers) of
+ ok -> % go on read tests from start flags
+ script_start3(Opts#opts{config=InitConfig}, Args);
+ Error ->
+ Error
end;
- false ->
- script_start3(false, ConfigFiles, EvHandlers, Args, LogDir, Cover)
+ {_,_} -> % testspec used
+ %% merge config from start flags with config from testspec
+ AllConfig = merge_vals([InitConfig, Opts#opts.config]),
+ case check_and_install_configfiles(AllConfig,
+ which(logdir,Opts#opts.logdir),
+ Opts#opts.event_handlers) of
+ ok -> % read tests from spec
+ {Run,Skip} = ct_testspec:prepare_tests(Terms, node()),
+ do_run(Run, Skip, Opts#opts{config=AllConfig}, Args);
+ Error ->
+ Error
+ end
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);
+script_start2(StartOpts, Args) ->
+ %% read config/userconfig from start flags
+ InitConfig = ct_config:prepare_config_list(Args),
+ case check_and_install_configfiles(InitConfig,
+ which(logdir,StartOpts#opts.logdir),
+ StartOpts#opts.event_handlers) of
+ ok -> % go on read tests from start flags
+ script_start3(StartOpts#opts{config=InitConfig}, Args);
+ Error ->
+ Error
+ end.
+
+check_and_install_configfiles(Configs, LogDir, EvHandlers) ->
+ case ct_config:check_config_files(Configs) of
false ->
- case lists:keysearch(suite, 1, Args) of
- {value,{suite,[]}} ->
+ install([{config,Configs},
+ {event_handler,EvHandlers}], LogDir);
+ {value,{error,{nofile,File}}} ->
+ {error,{cant_read_config_file,File}};
+ {value,{error,{wrong_config,Message}}}->
+ {error,{wrong_config,Message}};
+ {value,{error,{callback,Info}}} ->
+ {error,{cant_load_callback_module,Info}}
+ end.
+
+script_start3(StartOpts, Args) ->
+ case proplists:get_value(dir, Args) of
+ [] ->
+ {error,no_dir_specified};
+ Dirs when is_list(Dirs) ->
+ script_start4(StartOpts#opts{tests = tests(Dirs)}, Args);
+ undefined ->
+ case proplists:get_value(suite, Args) of
+ [] ->
{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 ->
+ Suites when is_list(Suites) ->
+ StartOpts1 =
+ get_start_opt(step,
+ fun(Step) ->
+ StartOpts#opts{step = Step,
+ cover = undefined}
+ end, StartOpts, Args),
+ DirMods = [suite_to_test(S) || S <- Suites],
+ case groups_and_cases(proplists:get_value(group, Args),
+ proplists:get_value(testcase, Args)) of
+ Error = {error,_} ->
+ Error;
+ [] when DirMods =/= [] ->
+ Ts = tests(DirMods),
+ script_start4(StartOpts1#opts{tests = Ts}, Args);
+ GroupsAndCases when length(DirMods) == 1 ->
+ Ts = tests(DirMods, GroupsAndCases),
+ script_start4(StartOpts1#opts{tests = Ts}, Args);
+ [_,_|_] when 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}
+ _ ->
+ {error,incorrect_suite_option}
end;
- false when VtsOrShell=/=false ->
- script_start4(VtsOrShell, ConfigFiles, EvHandlers,
- [], Cover, Args, LogDir);
- false ->
- script_usage(),
- {error,incorrect_usage}
+ undefined ->
+ if StartOpts#opts.vts ; StartOpts#opts.shell ->
+ script_start4(StartOpts#opts{tests = []}, Args);
+ true ->
+ script_usage(),
+ {error,incorrect_usage}
+ end
end
end.
-script_start4(vts, ConfigFiles, EvHandlers, Tests, false, _Args, LogDir) ->
+script_start4(#opts{vts = true, config = Config, event_handlers = EvHandlers,
+ tests = Tests, logdir = LogDir}, _Args) ->
+ ConfigFiles =
+ lists:foldl(fun({ct_config_plain,CfgFiles}, AllFiles) when
+ is_list(hd(CfgFiles)) ->
+ AllFiles ++ CfgFiles;
+ ({ct_config_plain,CfgFile}, AllFiles) when
+ is_integer(hd(CfgFile)) ->
+ AllFiles ++ [CfgFile];
+ (_, AllFiles) ->
+ AllFiles
+ end, [], Config),
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 == [] ->
+
+script_start4(#opts{shell = true, config = Config, event_handlers = EvHandlers,
+ logdir = LogDir, testspecs = Specs}, _Args) ->
+ InstallOpts = [{config,Config},{event_handler,EvHandlers}],
+ if Config == [] ->
ok;
true ->
- io:format("\nInstalling: ~p\n\n", [ConfigFiles])
+ io:format("\nInstalling: ~p\n\n", [Config])
end,
- case install(Opts) of
+ case install(InstallOpts) of
ok ->
ct_util:start(interactive, LogDir),
- log_ts_names(Args),
+ log_ts_names(Specs),
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(),
+
+script_start4(#opts{vts = true, cover = Cover}, _) ->
+ case Cover of
+ undefined ->
+ script_usage();
+ _ ->
+ %% Add support later (maybe).
+ io:format("\nCan't run cover in vts mode.\n\n", [])
+ end,
erlang:halt();
-script_start4(shell, _ConfigFiles, _EvHandlers, _Test, _Step, _Args, _LogDir) ->
- script_usage().
+
+script_start4(#opts{shell = true, cover = Cover}, _) ->
+ case Cover of
+ undefined ->
+ script_usage();
+ _ ->
+ %% Add support later (maybe).
+ io:format("\nCan't run cover in interactive mode.\n\n", [])
+ end;
+
+script_start4(Opts = #opts{tests = Tests}, Args) ->
+ do_run(Tests, [], Opts, Args).
%%%-----------------------------------------------------------------
%%% @spec script_usage() -> ok
-%%% @doc Print script usage information for <code>run_test</code>.
+%%% @doc Print 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"
@@ -396,24 +478,29 @@ script_usage() ->
"\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[-include InclDir1 InclDir2 .. InclDirN]"
"\n\t[-no_auto_compile]"
+ "\n\t[-multiply_timetraps N]"
+ "\n\t[-scale_timetraps]"
"\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[-userconfig CallbackModule ConfigFile1 .. 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[-include InclDir1 InclDir2 .. InclDirN]"
"\n\t[-no_auto_compile]"
- "\n\t[-basic_html]"
- "\n\t[-repeat N [-force_stop]] |"
+ "\n\t[-multiply_timetraps N]"
+ "\n\t[-scale_timetraps]"
+ "\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"
@@ -426,10 +513,12 @@ script_usage() ->
"\n\t[-stylesheet CSSFile]"
"\n\t[-cover CoverCfgFile]"
"\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
- "\n\t[-include InclDir1 InclDir2 .. InclDirN]"
+ "\n\t[-include InclDir1 InclDir2 .. InclDirN]"
"\n\t[-no_auto_compile]"
- "\n\t[-basic_html]"
- "\n\t[-repeat N [-force_stop]] |"
+ "\n\t[-multiply_timetraps N]"
+ "\n\t[-scale_timetraps]"
+ "\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"
@@ -440,7 +529,6 @@ script_usage() ->
"\trun_test -shell"
"\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
"\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]\n\n").
-
%%%-----------------------------------------------------------------
%%% @hidden
@@ -468,7 +556,7 @@ install(Opts, LogDir) ->
[io:format(Fd, "~p.\n", [Opt]) || Opt <- Opts],
file:close(Fd),
ok;
- {error,Reason} ->
+ {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", []),
@@ -487,69 +575,58 @@ variables_file_name(Dir) ->
filename:join(Dir, "variables-"++atom_to_list(node())).
%%%-----------------------------------------------------------------
-%%% @hidden
+%%% @spec run_test(Opts) -> Result
+%%% Opts = [tuple()]
+%%% Result = [TestResult] | {error,Reason}
+%%%
+%%% @doc Start tests from the erlang shell or from an erlang program.
%%% @equiv ct:run_test/1
+%%%-----------------------------------------------------------------
-%% 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),
+run_test(StartOpt) when is_tuple(StartOpt) ->
+ run_test([StartOpt]);
+
+run_test(StartOpts) when is_list(StartOpts) ->
+ case proplists:get_value(refresh_logs, StartOpts) of
+ undefined ->
+ Tracing = start_trace(StartOpts),
{ok,Cwd} = file:get_cwd(),
- io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]),
+ io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]),
Res =
- case ct_repeat:loop_test(func, Opts) of
+ case ct_repeat:loop_test(func, StartOpts) of
false ->
- case catch run_test1(Opts) of
- {'EXIT',Reason} ->
+ case catch run_test1(StartOpts) of
+ {'EXIT',Reason} ->
file:set_cwd(Cwd),
{error,Reason};
- Result ->
+ Result ->
Result
end;
Result ->
Result
end,
stop_trace(Tracing),
- Res
+ Res;
+ RefreshDir ->
+ refresh_logs(?abs(RefreshDir)),
+ ok
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,
+run_test1(StartOpts) ->
+ %% logdir
+ LogDir = get_start_opt(logdir, fun(LD) when is_list(LD) -> LD end,
+ ".", StartOpts),
+ %% config & userconfig
+ CfgFiles = ct_config:get_config_file_list(StartOpts),
+
+ %% event handlers
EvHandlers =
- case lists:keysearch(event_handler, 1, Opts) of
- {value,{_,H}} when is_atom(H) ->
+ case proplists:get_value(event_handler, StartOpts) of
+ undefined ->
+ [];
+ H when is_atom(H) ->
[{H,[]}];
- {value,{_,H}} ->
+ H ->
Hs =
if is_tuple(H) -> [H];
is_list(H) -> H;
@@ -564,41 +641,39 @@ run_test1(Opts) ->
{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, Hs))
end,
+
+ %% silent connections
+ SilentConns = get_start_opt(silent_connections,
+ fun(all) -> [];
+ (Conns) -> Conns
+ end, StartOpts),
+ %% stylesheet
+ Stylesheet = get_start_opt(stylesheet,
+ fun(SS) -> ?abs(SS) end,
+ StartOpts),
+ %% code coverage
+ Cover = get_start_opt(cover,
+ fun(CoverFile) -> ?abs(CoverFile) end, StartOpts),
+
+ %% timetrap manipulation
+ MultiplyTT = get_start_opt(multiply_timetraps, value, 1, StartOpts),
+ ScaleTT = get_start_opt(scale_timetraps, value, false, StartOpts),
+
+ %% auto compile & include files
Include =
- case lists:keysearch(auto_compile, 1, Opts) of
- {value,{auto_compile,ACBool}} ->
- application:set_env(common_test, auto_compile, ACBool),
- [];
- _ ->
+ case proplists:get_value(auto_compile, StartOpts) of
+ undefined ->
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 ->
- []
+ case proplists:get_value(include, StartOpts) of
+ undefined ->
+ [];
+ Incl when is_list(hd(Incl)) ->
+ Incl;
+ Incl when is_list(Incl) ->
+ [Incl]
end,
case os:getenv("CT_INCLUDE_PATH") of
false ->
@@ -609,117 +684,165 @@ run_test1(Opts) ->
AllInclDirs = InclDirs1++InclDirs,
application:set_env(common_test, include, AllInclDirs),
AllInclDirs
- end
+ end;
+ ACBool ->
+ application:set_env(common_test, auto_compile, ACBool),
+ []
end,
- case lists:keysearch(decrypt, 1, Opts) of
- {value,{_,Key={key,_}}} ->
+ %% decrypt config file
+ case proplists:get_value(decrypt, StartOpts) of
+ undefined ->
+ application:unset_env(common_test, decrypt);
+ 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)
+ {file,KeyFile} ->
+ application:set_env(common_test, decrypt, {file,?abs(KeyFile)})
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)
+ %% basic html - used by ct_logs
+ case proplists:get_value(basic_html, StartOpts) of
+ undefined ->
+ application:set_env(common_test, basic_html, false);
+ BasicHtmlBool ->
+ application:set_env(common_test, basic_html, BasicHtmlBool)
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,
+ %% stepped execution
+ Step = get_start_opt(step, value, StartOpts),
+
+ Opts = #opts{cover = Cover, step = Step, logdir = LogDir, config = CfgFiles,
+ event_handlers = EvHandlers, include = Include,
+ silent_connections = SilentConns,
+ stylesheet = Stylesheet,
+ multiply_timetraps = MultiplyTT,
+ scale_timetraps = ScaleTT},
+
+ %% test specification
+ case proplists:get_value(spec, StartOpts) of
+ undefined ->
+ case proplists:get_value(prepared_tests, StartOpts) of
+ undefined -> % use dir|suite|case
+ run_dir(Opts, StartOpts);
+ {{Run,Skip},Specs} -> % use prepared tests
+ run_prepared(Run, Skip, Opts#opts{testspecs = Specs}, StartOpts)
+ end;
+ Specs ->
+ Relaxed = get_start_opt(allow_user_terms, value, false, StartOpts),
%% 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
+ run_spec_file(Relaxed, Opts#opts{testspecs = Specs}, StartOpts)
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) ->
+run_spec_file(Relaxed,
+ Opts = #opts{testspecs = Specs, config = CfgFiles},
+ StartOpts) ->
Specs1 = case Specs of
[X|_] when is_integer(X) -> [Specs];
_ -> Specs
end,
- AbsSpecs = lists:map(fun(SF) -> ?abs(SF) end, Specs1),
+ 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} ->
+ {Error,CTReason} when Error == error ; Error == 'EXIT' ->
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
+ SpecOpts = get_data_for_node(TS, node()),
+ LogDir = choose_val(Opts#opts.logdir,
+ SpecOpts#opts.logdir),
+ AllConfig = merge_vals([CfgFiles, SpecOpts#opts.config]),
+ Cover = choose_val(Opts#opts.cover,
+ SpecOpts#opts.cover),
+ MultTT = choose_val(Opts#opts.multiply_timetraps,
+ SpecOpts#opts.multiply_timetraps),
+ ScaleTT = choose_val(Opts#opts.scale_timetraps,
+ SpecOpts#opts.scale_timetraps),
+ AllEvHs = merge_vals([Opts#opts.event_handlers,
+ SpecOpts#opts.event_handlers]),
+ AllInclude = merge_vals([Opts#opts.include,
+ SpecOpts#opts.include]),
+ application:set_env(common_test, include, AllInclude),
+
+ case check_and_install_configfiles(AllConfig,
+ which(logdir,LogDir),
+ AllEvHs) of
ok ->
+ Opts1 = Opts#opts{cover = Cover,
+ logdir = LogDir,
+ config = AllConfig,
+ event_handlers = AllEvHs,
+ include = AllInclude,
+ testspecs = AbsSpecs,
+ multiply_timetraps = MultTT,
+ scale_timetraps = ScaleTT},
{Run,Skip} = ct_testspec:prepare_tests(TS, node()),
- do_run(Run, Skip, CoverOpt,
- replace_opt([{spec,AbsSpecs}], Opts),
- LogDir2);
+ do_run(Run, Skip, Opts1, StartOpts);
{error,GCFReason} ->
exit(GCFReason)
end
end.
-run_prepared(LogDir, CfgFiles, EvHandlers, Run, Skip, Cover, Opts) ->
- case get_configfiles(CfgFiles, [], LogDir, EvHandlers) of
+run_prepared(Run, Skip, Opts = #opts{logdir = LogDir,
+ config = CfgFiles,
+ event_handlers = EvHandlers},
+ StartOpts) ->
+ case check_and_install_configfiles(CfgFiles, LogDir, EvHandlers) of
ok ->
- do_run(Run, Skip, Cover, Opts, LogDir);
+ do_run(Run, Skip, Opts, StartOpts);
{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),
+ end.
+check_config_file(Callback, File)->
+ case code:is_loaded(Callback) of
+ false ->
+ case code:load_file(Callback) of
+ {module,_} -> ok;
+ {error,Why} -> exit({cant_load_callback_module,Why})
+ end;
+ _ ->
+ ok
+ end,
+ case Callback:check_parameter(File) of
+ {ok,{file,File}}->
+ ?abs(File);
+ {ok,{config,_}}->
+ File;
+ {error,{wrong_config,Message}}->
+ exit({wrong_config,{Callback,Message}});
+ {error,{nofile,File}}->
+ exit({no_such_file,?abs(File)})
+ end.
+
+run_dir(Opts = #opts{logdir = LogDir,
+ config = CfgFiles,
+ event_handlers = EvHandlers}, StartOpts) ->
+ AbsCfgFiles =
+ lists:map(fun({Callback,FileList})->
+ case code:is_loaded(Callback) of
+ {file,_Path}->
+ ok;
+ false ->
+ case code:load_file(Callback) of
+ {module,Callback}->
+ ok;
+ {error,_}->
+ exit({no_such_module,Callback})
+ end
+ end,
+ {Callback,
+ lists:map(fun(File)->
+ check_config_file(Callback, File)
+ end, FileList)}
+ 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
+ case lists:keysearch(dir, 1, StartOpts) of
{value,{_,Dirs=[Dir|_]}} when not is_integer(Dir),
length(Dirs)>1 ->
%% multiple dirs (no suite)
- do_run(tests(Dirs), [], StepOrCover, Opts, LogDir);
+ do_run(tests(Dirs), [], Opts, StartOpts);
false -> % no dir
%% fun for converting suite name to {Dir,Mod} tuple
S2M = fun(S) when is_list(S) ->
@@ -728,105 +851,120 @@ run_dir(LogDir, CfgFiles, EvHandlers, StepOrCover, Opts) ->
(A) ->
{".",A}
end,
- case lists:keysearch(suite, 1, Opts) of
+ case lists:keysearch(suite, 1, StartOpts) 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
+ case listify(proplists:get_value(group, StartOpts, [])) ++
+ listify(proplists:get_value(testcase, StartOpts, [])) of
[] ->
- do_run(tests(Dir, listify(Mod)), [], StepOrCover, Opts, LogDir);
+ do_run(tests(Dir, listify(Mod)), [], Opts, StartOpts);
GsAndCs ->
- do_run(tests(Dir, Mod, GsAndCs), [], StepOrCover, Opts, LogDir)
+ do_run(tests(Dir, Mod, GsAndCs), [], Opts, StartOpts)
end;
{value,{_,Suites}} ->
- do_run(tests(lists:map(S2M, Suites)), [], StepOrCover, Opts, LogDir);
+ do_run(tests(lists:map(S2M, Suites)), [], Opts, StartOpts);
_ ->
exit(no_tests_specified)
- end;
+ end;
{value,{_,Dir}} ->
- case lists:keysearch(suite, 1, Opts) of
+ case lists:keysearch(suite, 1, StartOpts) of
{value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) ->
- Mod = if is_atom(Suite) -> Suite;
- true -> list_to_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
+ case listify(proplists:get_value(group, StartOpts, [])) ++
+ listify(proplists:get_value(testcase, StartOpts, [])) of
[] ->
- do_run(tests(Dir, listify(Mod)), [], StepOrCover, Opts, LogDir);
+ do_run(tests(Dir, listify(Mod)), [], Opts, StartOpts);
GsAndCs ->
- do_run(tests(Dir, Mod, GsAndCs), [], StepOrCover, Opts, LogDir)
+ do_run(tests(Dir, Mod, GsAndCs), [], Opts, StartOpts)
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);
+ do_run(tests(delistify(Dir), Mods), [], Opts, StartOpts);
{value,{_,Suites}} ->
- do_run(tests(delistify(Dir), Suites), [], StepOrCover, Opts, LogDir);
+ do_run(tests(delistify(Dir), Suites), [], Opts, StartOpts);
false -> % no suite, only dir
- do_run(tests(listify(Dir)), [], StepOrCover, Opts, LogDir)
- end
+ do_run(tests(listify(Dir)), [], Opts, StartOpts)
+ end
end.
%%%-----------------------------------------------------------------
-%%% @hidden
+%%% @spec run_testspec(TestSpec) -> Result
+%%% TestSpec = [term()]
%%%
+%%% @doc Run test specified by <code>TestSpec</code>. The terms are
+%%% the same as those used in test specification files.
+%%% @equiv ct:run_testspec/1
+%%%-----------------------------------------------------------------
-%% 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} ->
+ {'EXIT',Reason} ->
file:set_cwd(Cwd),
{error,Reason};
- Result ->
+ Result ->
Result
end.
run_testspec1(TestSpec) ->
- case ct_testspec:collect_tests_from_list(TestSpec,false) of
- {error,CTReason} ->
+ case catch ct_testspec:collect_tests_from_list(TestSpec, false) of
+ {E,CTReason} when E == error ; E == 'EXIT' ->
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
+ Opts = get_data_for_node(TS, node()),
+
+ AllInclude =
+ case os:getenv("CT_INCLUDE_PATH") of
+ false ->
+ Opts#opts.include;
+ CtInclPath ->
+ EnvInclude = string:tokens(CtInclPath, [$:,$ ,$,]),
+ EnvInclude++Opts#opts.include
+ end,
+ application:set_env(common_test, include, AllInclude),
+
+ case check_and_install_configfiles(Opts#opts.config,
+ which(logdir,Opts#opts.logdir),
+ Opts#opts.event_handlers) of
ok ->
- {Run,Skip} = ct_testspec:prepare_tests(TS,node()),
- do_run(Run,Skip,CoverOpt,[],LogDir);
+ Opts1 = Opts#opts{testspecs = [TestSpec],
+ include = AllInclude},
+ {Run,Skip} = ct_testspec:prepare_tests(TS, node()),
+ do_run(Run, Skip, Opts1, []);
{error,GCFReason} ->
exit(GCFReason)
end
end.
-
get_data_for_node(#testspec{logdir=LogDirs,
cover=CoverFs,
config=Cfgs,
+ userconfig=UsrCfgs,
event_handler=EvHs,
- include=Incl}, Node) ->
- LogDir = case lists:keysearch(Node,1,LogDirs) of
- {value,{Node,Dir}} -> Dir;
- false -> "."
+ include=Incl,
+ multiply_timetraps=MTs,
+ scale_timetraps=STs}, Node) ->
+ LogDir = case proplists:get_value(Node, LogDirs) of
+ undefined -> ".";
+ Dir -> Dir
end,
- Cover = case lists:keysearch(Node,1,CoverFs) of
- {value,{Node,CovFile}} -> CovFile;
- false -> undef
- end,
- ConfigFiles = [F || {N,F} <- Cfgs, N==Node],
+ Cover = proplists:get_value(Node, CoverFs),
+ MT = proplists:get_value(Node, MTs),
+ ST = proplists:get_value(Node, STs),
+ ConfigFiles = [{?ct_config_txt,F} || {N,F} <- Cfgs, N==Node] ++
+ [CBF || {N,CBF} <- UsrCfgs, N==Node],
EvHandlers = [{H,A} || {N,H,A} <- EvHs, N==Node],
Include = [I || {N,I} <- Incl, N==Node],
- {LogDir,Cover,ConfigFiles,EvHandlers,Include}.
-
+ #opts{logdir = LogDir,
+ cover = Cover,
+ config = ConfigFiles,
+ event_handlers = EvHandlers,
+ include = Include,
+ multiply_timetraps = MT,
+ scale_timetraps = ST}.
refresh_logs(LogDir) ->
{ok,Cwd} = file:get_cwd(),
@@ -851,11 +989,27 @@ refresh_logs(LogDir) ->
end
end.
-which_logdir(".",Dir) ->
+which(logdir, undefined) ->
+ ".";
+which(logdir, Dir) ->
Dir;
-which_logdir(Dir,_) ->
- Dir.
-
+which(multiply_timetraps, undefined) ->
+ 1;
+which(multiply_timetraps, MT) ->
+ MT;
+which(scale_timetraps, undefined) ->
+ false;
+which(scale_timetraps, ST) ->
+ ST.
+
+choose_val(undefined, V1) ->
+ V1;
+choose_val(V0, _V1) ->
+ V0.
+
+merge_vals(Vs) ->
+ lists:append(Vs).
+
listify([C|_]=Str) when is_integer(C) -> [Str];
listify(L) when is_list(L) -> L;
listify(E) -> [E].
@@ -885,6 +1039,22 @@ run(TestDirs) ->
install([]),
do_run(tests(TestDirs), []).
+suite_to_test(Suite) ->
+ {filename:dirname(Suite),list_to_atom(filename:rootname(filename:basename(Suite)))}.
+
+groups_and_cases(Gs, Cs) when ((Gs == undefined) or (Gs == [])) and
+ ((Cs == undefined) or (Cs == [])) ->
+ [];
+groups_and_cases(Gs, Cs) when Gs == undefined ; Gs == [] ->
+ [list_to_atom(C) || C <- Cs];
+groups_and_cases(Gs, Cs) when Cs == undefined ; Cs == [] ->
+ [{list_to_atom(G),all} || G <- Gs];
+groups_and_cases([G], Cs) ->
+ [{list_to_atom(G),[list_to_atom(C) || C <- Cs]}];
+groups_and_cases([_,_|_] , Cs) when Cs =/= [] ->
+ {error,multiple_groups_and_cases};
+groups_and_cases(_Gs, _Cs) ->
+ {error,incorrect_group_or_case_option}.
tests(TestDir, Suites, []) when is_list(TestDir), is_integer(hd(TestDir)) ->
[{?testdir(TestDir,Suites),ensure_atom(Suites),all}];
@@ -901,30 +1071,42 @@ tests(TestDir) when is_list(TestDir), is_integer(hd(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, Misc) when is_list(Misc) ->
+ do_run(Tests, Misc, ".").
-do_run(Tests, Opt, LogDir) ->
- do_run(Tests, [], Opt, [], LogDir).
+do_run(Tests, Misc, LogDir) when is_list(Misc) ->
+ Opts =
+ case proplists:get_value(step, Misc) of
+ undefined ->
+ #opts{};
+ StepOpts ->
+ #opts{step = StepOpts}
+ end,
+ Opts1 =
+ case proplists:get_value(cover, Misc) of
+ undefined ->
+ Opts;
+ CoverFile ->
+ Opts#opts{cover = CoverFile}
+ end,
+ do_run(Tests, [], Opts1#opts{logdir = LogDir}, []).
-do_run(Tests, Skip, Opt, Args, LogDir) ->
+do_run(Tests, Skip, Opts, Args) ->
+ #opts{cover = Cover} = Opts,
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,
+ Opts1 = if Cover == undefined ->
+ Opts;
+ true ->
+ case ct_cover:get_spec(Cover) of
+ {error,Reason} ->
+ exit({error,Reason});
+ CoverSpec ->
+ Opts#opts{coverspec = CoverSpec}
+ end
+ end,
%% This env variable is used by test_server to determine
%% which framework it runs under.
case os:getenv("TEST_SERVER_FRAMEWORK") of
@@ -935,50 +1117,39 @@ do_run(Tests, Skip, Opt, Args, LogDir) ->
Other ->
erlang:display(list_to_atom("Note: TEST_SERVER_FRAMEWORK = " ++ Other))
end,
- case ct_util:start(LogDir) of
+ case ct_util:start(Opts#opts.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,[]}} ->
+ %% save stylesheet info
+ ct_util:set_testdata({stylesheet,Opts#opts.stylesheet}),
+ %% enable silent connections
+ case Opts#opts.silent_connections of
+ [] ->
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),
+ Conns when is_list(Conns) ->
ct_util:override_silence_connections(Conns),
ct_logs:log("Silent connections", "~p", [Conns]);
_ ->
ok
end,
- log_ts_names(Args),
+ log_ts_names(Opts1#opts.testspecs),
TestSuites = suite_tuples(Tests),
- {SuiteMakeErrors,AllMakeErrors} =
+ {_TestSuites1,SuiteMakeErrors,AllMakeErrors} =
case application:get_env(common_test, auto_compile) of
{ok,false} ->
- SuitesNotFound = verify_suites(TestSuites),
- {SuitesNotFound,SuitesNotFound};
+ {TestSuites1,SuitesNotFound} =
+ verify_suites(TestSuites),
+ {TestSuites1,SuitesNotFound,SuitesNotFound};
_ ->
{SuiteErrs,HelpErrs} = auto_compile(TestSuites),
- {SuiteErrs,SuiteErrs++HelpErrs}
+ {TestSuites,SuiteErrs,SuiteErrs++HelpErrs}
end,
case continue(AllMakeErrors) of
@@ -986,7 +1157,7 @@ do_run(Tests, Skip, Opt, Args, LogDir) ->
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),
+ R = do_run_test(Tests1, Skip1, Opts1),
ct_util:stop(normal),
R;
false ->
@@ -1012,7 +1183,7 @@ auto_compile(TestSuites) ->
case application:get_env(common_test, include) of
{ok,UserInclDirs} when length(UserInclDirs) > 0 ->
io:format("Including the following directories:~n"),
- [begin io:format("~p~n",[UserInclDir]), {i,UserInclDir} end ||
+ [begin io:format("~p~n",[UserInclDir]), {i,UserInclDir} end ||
UserInclDir <- UserInclDirs];
_ ->
[]
@@ -1020,11 +1191,11 @@ auto_compile(TestSuites) ->
SuiteMakeErrors =
lists:flatmap(fun({TestDir,Suite} = TS) ->
case run_make(suites, TestDir, Suite, UserInclude) of
- {error,{make_failed,Bad}} ->
+ {error,{make_failed,Bad}} ->
[{TS,Bad}];
- {error,_} ->
+ {error,_} ->
[{TS,[filename:join(TestDir,"*_SUITE")]}];
- _ ->
+ _ ->
[]
end
end, TestSuites),
@@ -1048,39 +1219,63 @@ auto_compile(TestSuites) ->
true -> % already visited
{Done,Failed}
end
- end, {[],[]}, TestSuites),
+ 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) ->
+ Verify =
+ fun({Dir,Suite}=DS,{Found,NotFound}) ->
case locate_test_dir(Dir, Suite) of
{ok,TestDir} ->
if Suite == all ->
- NotFound;
+ {[DS|Found],NotFound};
true ->
- Beam = filename:join(TestDir, atom_to_list(Suite)++".beam"),
+ 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]
+ true ->
+ {[DS|Found],NotFound};
+ false ->
+ case code:is_loaded(Suite) of
+ {file,SuiteFile} ->
+ %% test suite is already loaded and
+ %% since auto_compile == false,
+ %% let's assume the user has
+ %% loaded the beam file explicitly
+ ActualDir = filename:dirname(SuiteFile),
+ {[{ActualDir,Suite}|Found],NotFound};
+ false ->
+ Name =
+ filename:join(TestDir,
+ atom_to_list(Suite)),
+ io:format(user,
+ "Suite ~w not found"
+ "in directory ~s~n",
+ [Suite,TestDir]),
+ {Found,[{DS,[Name]}|NotFound]}
+ end
end
end;
{error,_Reason} ->
- io:format("Directory ~s is invalid~n", [Dir]),
- Name = filename:join(Dir, atom_to_list(Suite)),
- [{{Dir,Suite},[Name]} | NotFound]
+ case code:is_loaded(Suite) of
+ {file,SuiteFile} ->
+ %% test suite is already loaded and since
+ %% auto_compile == false, let's assume the
+ %% user has loaded the beam file explicitly
+ ActualDir = filename:dirname(SuiteFile),
+ {[{ActualDir,Suite}|Found],NotFound};
+ false ->
+ io:format(user, "Directory ~s is invalid~n", [Dir]),
+ Name = filename:join(Dir, atom_to_list(Suite)),
+ {Found,[{DS,[Name]}|NotFound]}
+ end
end
end,
- lists:reverse(lists:foldl(Verify, [], TestSuites)).
-
-
+ {ActualFound,Missing} = lists:foldl(Verify, {[],[]}, TestSuites),
+ {lists:reverse(ActualFound),lists:reverse(Missing)}.
+
save_make_errors([]) ->
[];
save_make_errors(Errors) ->
@@ -1096,7 +1291,7 @@ get_bad_suites([{{_TestDir,_Suite},Failed}|Errors], BadSuites) ->
get_bad_suites([], BadSuites) ->
BadSuites.
-
+
%%%-----------------------------------------------------------------
%%% @hidden
@@ -1107,7 +1302,7 @@ step(TestDir, Suite, Case) ->
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:step/4
-step(TestDir, Suite, Case, Opts) when is_list(TestDir), is_atom(Suite), is_atom(Case),
+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}]).
@@ -1140,7 +1335,7 @@ final_tests([{TestDir,Suites,_}|Tests],
Skip1 = [{TD,S,"Make failed"} || {{TD,S},_} <- Bad, S1 <- Suites,
S == S1, TD == TestDir],
- Final1 = [{TestDir,S,all} || S <- Suites],
+ 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) ->
@@ -1173,7 +1368,7 @@ final_tests([{TestDir,Suite,Cases}|Tests], Final, Skip, Bad) ->
final_tests([], Final, Skip, _Bad) ->
{lists:reverse(Final),Skip}.
-continue([]) ->
+continue([]) ->
true;
continue(_MakeErrors) ->
io:nl(),
@@ -1214,7 +1409,7 @@ set_group_leader_same_as_shell() ->
false
end
end,
- case [P || P <- processes(), GS2or3(P),
+ case [P || P <- processes(), GS2or3(P),
true == lists:keymember(shell,1,element(2,process_info(P,dictionary)))] of
[GL|_] ->
group_leader(GL, self());
@@ -1238,29 +1433,29 @@ check_and_add([{TestDir0,M,_} | Tests], Added) ->
check_and_add([], _) ->
ok.
-do_run_test(Tests, Skip, Opt) ->
+do_run_test(Tests, Skip, Opts) ->
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}}}} ->
+ case Opts#opts.coverspec of
+ CovData={CovFile,
+ CovNodes,
+ _CovImport,
+ CovExport,
+ #cover{app = CovApp,
+ level = CovLevel,
+ excl_mods = CovExcl,
+ incl_mods = CovIncl,
+ cross = CovCross,
+ src = _CovSrc}} ->
ct_logs:log("COVER INFO","Using cover specification file: ~s~n"
"App: ~w~n"
"Cross cover: ~w~n"
"Including ~w modules~n"
"Excluding ~w modules",
- [CovFile,CovApp,CovCross,length(CovIncl),length(CovExcl)]),
+ [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
@@ -1293,33 +1488,37 @@ do_run_test(Tests, Skip, Opt) ->
true;
_ ->
false
- end,
+ 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),
+ 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",
+ 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)",
+ 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",
+ 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)",
+ ct_logs:log("TEST INFO","~w test(s), ~w case(s) in ~w suite(s)",
[NoOfTests,NoOfCases,NoOfSuites])
end,
+
+ test_server_ctrl:multiply_timetraps(Opts#opts.multiply_timetraps),
+ test_server_ctrl:scale_timetraps(Opts#opts.scale_timetraps),
+
ct_event:notify(#event{name=start_info,
node=node(),
data={NoOfTests,NoOfSuites,NoOfCases}}),
- CleanUp = add_jobs(Tests, Skip, Opt, []),
+ CleanUp = add_jobs(Tests, Skip, Opts, []),
unlink(whereis(test_server_ctrl)),
- catch test_server_ctrl:wait_finish(),
- %% check if last testcase has left a "dead" trace window
+ 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}} ->
@@ -1327,8 +1526,8 @@ do_run_test(Tests, Skip, Opt) ->
_ ->
ok
end,
- lists:foreach(fun(Suite) ->
- maybe_cleanup_interpret(Suite, Opt)
+ lists:foreach(fun(Suite) ->
+ maybe_cleanup_interpret(Suite, Opts#opts.step)
end, CleanUp);
Error ->
Error
@@ -1344,9 +1543,9 @@ 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, [], []),
+ add_jobs(Tests, Skip, #opts{}, []),
{Suites,NoOfCases} = count_test_cases1(length(Tests), 0, [], Ref),
- erlang:demonitor(Ref),
+ erlang:demonitor(Ref, [flush]),
test_server_ctrl:stop_get_totals(),
{Suites,NoOfCases}.
@@ -1354,11 +1553,11 @@ count_test_cases1(0, N, Suites, _) ->
{lists:flatten(Suites), N};
count_test_cases1(Jobs, N, Suites, Ref) ->
receive
- {no_of_cases,{Ss,N1}} ->
+ {no_of_cases,{Ss,N1}} ->
count_test_cases1(Jobs-1, add_known(N,N1), [Ss|Suites], Ref);
- {'DOWN', Ref, _, _, _} ->
+ {'DOWN', Ref, _, _, _} ->
{[],0}
- end.
+ end.
add_known(unknown, _) ->
unknown;
@@ -1367,72 +1566,86 @@ add_known(_, unknown) ->
add_known(N, N1) ->
N+N1.
-add_jobs([{TestDir,all,_}|Tests], Skip, Opt, CleanUp) ->
+add_jobs([{TestDir,all,_}|Tests], Skip, Opts, CleanUp) ->
Name = get_name(TestDir),
case catch test_server_ctrl:add_dir_with_skip(Name, TestDir,
skiplist(TestDir,Skip)) of
- {'EXIT',_} ->
+ {'EXIT',_} ->
CleanUp;
_ ->
wait_for_idle(),
- add_jobs(Tests, Skip, Opt, CleanUp)
+ add_jobs(Tests, Skip, Opts, 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) ->
+add_jobs([{TestDir,[Suite],all}|Tests], Skip, Opts, CleanUp) when is_atom(Suite) ->
+ add_jobs([{TestDir,Suite,all}|Tests], Skip, Opts, CleanUp);
+add_jobs([{TestDir,Suites,all}|Tests], Skip, Opts, 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',_} ->
+ {'EXIT',_} ->
CleanUp;
_ ->
wait_for_idle(),
- add_jobs(Tests, Skip, Opt, CleanUp)
+ add_jobs(Tests, Skip, Opts, CleanUp)
end;
-add_jobs([{TestDir,Suite,all}|Tests], Skip, Opt, CleanUp) ->
- case maybe_interpret(Suite, all, Opt) of
+add_jobs([{TestDir,Suite,all}|Tests], Skip, Opts, CleanUp) ->
+ case maybe_interpret(Suite, all, Opts) 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',_} ->
+ {'EXIT',_} ->
CleanUp;
_ ->
wait_for_idle(),
- add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
+ add_jobs(Tests, Skip, Opts, [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
+
+%% group
+add_jobs([{TestDir,Suite,[{GroupName,_Cases}]}|Tests], Skip, Opts, CleanUp) when
+ is_atom(GroupName) ->
+ add_jobs([{TestDir,Suite,GroupName}|Tests], Skip, Opts, CleanUp);
+add_jobs([{TestDir,Suite,{GroupName,_Cases}}|Tests], Skip, Opts, CleanUp) when
+ is_atom(GroupName) ->
+ add_jobs([{TestDir,Suite,GroupName}|Tests], Skip, Opts, CleanUp);
+
+%% test case
+add_jobs([{TestDir,Suite,[Case]}|Tests], Skip, Opts, CleanUp) when is_atom(Case) ->
+ add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opts, CleanUp);
+
+add_jobs([{TestDir,Suite,Cases}|Tests], Skip, Opts, CleanUp) when is_list(Cases) ->
+ Cases1 = lists:map(fun({GroupName,_}) when is_atom(GroupName) -> GroupName;
+ (Case) -> Case
+ end, Cases),
+ case maybe_interpret(Suite, Cases1, Opts) of
ok ->
Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite) ++ ".cases",
- case catch test_server_ctrl:add_cases_with_skip(Name, Suite, Cases,
+ case catch test_server_ctrl:add_cases_with_skip(Name, Suite, Cases1,
skiplist(TestDir,Skip)) of
- {'EXIT',_} ->
+ {'EXIT',_} ->
CleanUp;
_ ->
wait_for_idle(),
- add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
+ add_jobs(Tests, Skip, Opts, [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
+add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opts, CleanUp) when is_atom(Case) ->
+ case maybe_interpret(Suite, Case, Opts) of
ok ->
- Name = get_name(TestDir) ++ "." ++ atom_to_list(Suite) ++ "." ++
+ 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',_} ->
+ {'EXIT',_} ->
CleanUp;
_ ->
wait_for_idle(),
- add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
+ add_jobs(Tests, Skip, Opts, [Suite|CleanUp])
end;
Error ->
Error
@@ -1453,7 +1666,7 @@ wait_for_idle() ->
idle -> ok;
{'DOWN', Ref, _, _, _} -> error
end,
- erlang:demonitor(Ref),
+ erlang:demonitor(Ref, [flush]),
ct_util:update_last_run_index(),
Result
end.
@@ -1482,7 +1695,7 @@ get_name(Dir) ->
end,
Base = filename:basename(TestDir),
case filename:basename(filename:dirname(TestDir)) of
- "" ->
+ "" ->
Base;
TopDir ->
TopDir ++ "." ++ Base
@@ -1513,15 +1726,15 @@ run_make(Targets, TestDir0, Mod, UserInclude) ->
{i,CtInclude},
{i,XmerlInclude},
debug_info],
- Result =
+ Result =
if Mod == all ; Targets == helpmods ->
case (catch ct_make:all([noexec|ErlFlags])) of
- {'EXIT',_} = Failure ->
+ {'EXIT',_} = Failure ->
Failure;
MakeInfo ->
FileTest = fun(F, suites) -> is_suite(F);
- (F, helpmods) -> not is_suite(F);
- (_, _) -> true end,
+ (F, helpmods) -> not is_suite(F)
+ end,
Files = lists:flatmap(fun({F,out_of_date}) ->
case FileTest(F, Targets) of
true -> [F];
@@ -1535,7 +1748,7 @@ run_make(Targets, TestDir0, Mod, UserInclude) ->
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,
@@ -1549,7 +1762,7 @@ run_make(Targets, TestDir0, Mod, UserInclude) ->
{error,{make_crashed,TestDir,Reason}};
{error,ModInfo} ->
io:format("{error,make_failed}\n", []),
- Bad = [filename:join(TestDir, M) || {M,R} <- ModInfo,
+ Bad = [filename:join(TestDir, M) || {M,R} <- ModInfo,
R == error],
{error,{make_failed,Bad}}
end;
@@ -1561,8 +1774,8 @@ run_make(Targets, TestDir0, Mod, UserInclude) ->
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
+maybe_interpret(Suite, Cases, #opts{step = StepOpts}) when StepOpts =/= undefined ->
+ %% 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}} ->
@@ -1605,7 +1818,7 @@ maybe_interpret2(Suite, Cases, StepOpts) ->
WinOp = case lists:member(keep_inactive, ensure_atom(StepOpts)) of
true -> no_kill;
false -> kill
- end,
+ end,
ct_util:set_testdata({interpret,{{Suite,Cases},WinOp,
{undefined,undefined}}}),
ok.
@@ -1621,37 +1834,44 @@ set_break_on_config(Suite, StepOpts) ->
ok
end.
-maybe_cleanup_interpret(Suite, [{step,_}]) ->
- i:iq(Suite);
-maybe_cleanup_interpret(_, _) ->
- ok.
+maybe_cleanup_interpret(_, undefined) ->
+ ok;
+maybe_cleanup_interpret(Suite, _) ->
+ i:iq(Suite).
+
+log_ts_names([]) ->
+ ok;
+log_ts_names(Specs) ->
+ List = lists:map(fun(Name) ->
+ Name ++ " "
+ end, Specs),
+ ct_logs:log("Test Specification file(s)", "~s",
+ [lists:flatten(List)]).
-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([{'case',TC}|Args], Merged) ->
+ merge_arguments(Args, handle_arg(merge, {testcase,TC}, 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, {event_handler_init,Elems}, [{event_handler_init,PrevElems}|Merged]) ->
+ [{event_handler_init,PrevElems++["add"|Elems]}|Merged];
+handle_arg(merge, {userconfig,Elems}, [{userconfig,PrevElems}|Merged]) ->
+ [{userconfig,PrevElems++["add"|Elems]}|Merged];
handle_arg(merge, {Key,Elems}, [{Key,PrevElems}|Merged]) ->
[{Key,PrevElems++Elems}|Merged];
handle_arg(Op, Arg, [Other|Merged]) ->
@@ -1659,6 +1879,164 @@ handle_arg(Op, Arg, [Other|Merged]) ->
handle_arg(_,Arg,[]) ->
[Arg].
+get_start_opt(Key, IfExists, Args) ->
+ get_start_opt(Key, IfExists, undefined, Args).
+
+get_start_opt(Key, IfExists, IfNotExists, Args) ->
+ case lists:keysearch(Key, 1, Args) of
+ {value,{Key,Val}} when is_function(IfExists) ->
+ IfExists(Val);
+ {value,{Key,Val}} when IfExists == value ->
+ Val;
+ {value,{Key,_Val}} ->
+ IfExists;
+ _ when is_function(IfNotExists) ->
+ IfNotExists();
+ _ ->
+ IfNotExists
+ end.
+
+event_handler_args2opts(Args) ->
+ case proplists:get_value(event_handler, Args) of
+ undefined ->
+ event_handler_args2opts([], Args);
+ EHs ->
+ event_handler_args2opts([{list_to_atom(EH),[]} || EH <- EHs], Args)
+ end.
+event_handler_args2opts(Default, Args) ->
+ case proplists:get_value(event_handler_init, Args) of
+ undefined ->
+ Default;
+ EHs ->
+ event_handler_init_args2opts(EHs)
+ end.
+event_handler_init_args2opts([EH, Arg, "and" | EHs]) ->
+ [{list_to_atom(EH),lists:flatten(io_lib:format("~s",[Arg]))} |
+ event_handler_init_args2opts(EHs)];
+event_handler_init_args2opts([EH, Arg]) ->
+ [{list_to_atom(EH),lists:flatten(io_lib:format("~s",[Arg]))}];
+event_handler_init_args2opts([]) ->
+ [].
+
+%% This function reads pa and pz arguments, converts dirs from relative
+%% to absolute, and re-inserts them in the code path. The order of the
+%% dirs in the code path remain the same. Note however that since this
+%% function is only used for arguments "pre run_test erl_args", the order
+%% relative dirs "post run_test erl_args" is not kept!
+rel_to_abs(CtArgs) ->
+ {PA,PZ} = get_pa_pz(CtArgs, [], []),
+ io:format(user, "~n", []),
+ [begin
+ code:del_path(filename:basename(D)),
+ Abs = filename:absname(D),
+ code:add_pathz(Abs),
+ if D /= Abs ->
+ io:format(user, "Converting ~p to ~p and re-inserting "
+ "with add_pathz/1~n",
+ [D, Abs]);
+ true ->
+ ok
+ end
+ end || D <- PZ],
+ [begin
+ code:del_path(filename:basename(D)),
+ Abs = filename:absname(D),
+ code:add_patha(Abs),
+ if D /= Abs ->
+ io:format(user, "Converting ~p to ~p and re-inserting "
+ "with add_patha/1~n",
+ [D, Abs]);
+ true ->ok
+ end
+ end || D <- PA],
+ io:format(user, "~n", []).
+
+get_pa_pz([{pa,Dirs} | Args], PA, PZ) ->
+ get_pa_pz(Args, PA ++ Dirs, PZ);
+get_pa_pz([{pz,Dirs} | Args], PA, PZ) ->
+ get_pa_pz(Args, PA, PZ ++ Dirs);
+get_pa_pz([_ | Args], PA, PZ) ->
+ get_pa_pz(Args, PA, PZ);
+get_pa_pz([], PA, PZ) ->
+ {PA,PZ}.
+
+%% This function translates ct:run_test/1 start options
+%% to run_test start arguments (on the init arguments format) -
+%% this is useful mainly for testing the ct_run start functions.
+opts2args(EnvStartOpts) ->
+ lists:flatmap(fun({config,CfgFiles}) ->
+ [{ct_config,[CfgFiles]}];
+ ({userconfig,{CBM,CfgStr=[X|_]}}) when is_integer(X) ->
+ [{userconfig,[atom_to_list(CBM),CfgStr]}];
+ ({userconfig,{CBM,CfgStrs}}) when is_list(CfgStrs) ->
+ [{userconfig,[atom_to_list(CBM) | CfgStrs]}];
+ ({userconfig,UserCfg}) when is_list(UserCfg) ->
+ Strs =
+ lists:map(fun({CBM,CfgStr=[X|_]}) when is_integer(X) ->
+ [atom_to_list(CBM),CfgStr,"and"];
+ ({CBM,CfgStrs}) when is_list(CfgStrs) ->
+ [atom_to_list(CBM) | CfgStrs] ++ ["and"]
+ end, UserCfg),
+ [_LastAnd|StrsR] = lists:reverse(lists:flatten(Strs)),
+ [{userconfig,lists:reverse(StrsR)}];
+ ({testcase,Case}) when is_atom(Case) ->
+ [{'case',[atom_to_list(Case)]}];
+ ({testcase,Cases}) ->
+ [{'case',[atom_to_list(C) || C <- Cases]}];
+ ({'case',Cases}) ->
+ [{'case',[atom_to_list(C) || C <- Cases]}];
+ ({allow_user_terms,true}) ->
+ [{allow_user_terms,[]}];
+ ({allow_user_terms,false}) ->
+ [];
+ ({auto_compile,false}) ->
+ [{no_auto_compile,[]}];
+ ({auto_compile,true}) ->
+ [];
+ ({scale_timetraps,true}) ->
+ [{scale_timetraps,[]}];
+ ({scale_timetraps,false}) ->
+ [];
+ ({force_stop,true}) ->
+ [{force_stop,[]}];
+ ({force_stop,false}) ->
+ [];
+ ({decrypt,{key,Key}}) ->
+ [{ct_decrypt_key,[Key]}];
+ ({decrypt,{file,File}}) ->
+ [{ct_decrypt_file,[File]}];
+ ({basic_html,true}) ->
+ ({basic_html,[]});
+ ({basic_html,false}) ->
+ [];
+ ({event_handler,EH}) when is_atom(EH) ->
+ [{event_handler,[atom_to_list(EH)]}];
+ ({event_handler,EHs}) when is_list(EHs) ->
+ [{event_handler,[atom_to_list(EH) || EH <- EHs]}];
+ ({event_handler,{EH,Arg}}) when is_atom(EH) ->
+ ArgStr = lists:flatten(io_lib:format("~p", [Arg])),
+ [{event_handler_init,[atom_to_list(EH),ArgStr]}];
+ ({event_handler,{EHs,Arg}}) when is_list(EHs) ->
+ ArgStr = lists:flatten(io_lib:format("~p", [Arg])),
+ Strs = lists:map(fun(EH) ->
+ [atom_to_list(EH),ArgStr,"and"]
+ end, EHs),
+ [_LastAnd|StrsR] = lists:reverse(lists:flatten(Strs)),
+ [{event_handler_init,lists:reverse(StrsR)}];
+ ({Opt,As=[A|_]}) when is_atom(A) ->
+ [{Opt,[atom_to_list(Atom) || Atom <- As]}];
+ ({Opt,Strs=[S|_]}) when is_list(S) ->
+ [{Opt,Strs}];
+ ({Opt,A}) when is_atom(A) ->
+ [{Opt,[atom_to_list(A)]}];
+ ({Opt,I}) when is_integer(I) ->
+ [{Opt,[integer_to_list(I)]}];
+ ({Opt,S}) when is_list(S) ->
+ [{Opt,[S]}];
+ (Opt) ->
+ Opt
+ end, EnvStartOpts).
+
locate_test_dir(Dir, Suite) ->
TestDir = case ct_util:is_test_dir(Dir) of
true -> Dir;
@@ -1723,18 +2101,18 @@ start_trace(Args) ->
case file:consult(TraceSpec) of
{ok,Terms} ->
case catch do_trace(Terms) of
- ok ->
+ ok ->
true;
{_,Error} ->
io:format("Warning! Tracing not started. Reason: ~p~n~n",
[Error]),
false
- end;
+ end;
{_,Error} ->
io:format("Warning! Tracing not started. Reason: ~p~n~n",
[Error]),
false
- end;
+ end;
false ->
false
end.
@@ -1746,61 +2124,22 @@ do_trace(Terms) ->
case dbg:tpl(M,[{'_',[],[{return_trace}]}]) of
{error,What} -> exit({error,{tracing_failed,What}});
_ -> ok
- end;
+ end;
({f,M,F}) ->
case dbg:tpl(M,F,[{'_',[],[{return_trace}]}]) of
{error,What} -> exit({error,{tracing_failed,What}});
_ -> ok
- end;
+ 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)) ->
@@ -1809,4 +2148,3 @@ ensure_atom(List) when is_list(List) ->
[ensure_atom(Item) || Item <- List];
ensure_atom(Other) ->
Other.
-
diff --git a/lib/common_test/src/ct_slave.erl b/lib/common_test/src/ct_slave.erl
new file mode 100644
index 0000000000..d2a491e079
--- /dev/null
+++ b/lib/common_test/src/ct_slave.erl
@@ -0,0 +1,439 @@
+%%--------------------------------------------------------------------
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2010. 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 for starting and stopping nodes for
+%%% Large Scale Testing.
+%%%
+%%% <p>This module exports functions which are used by the Common Test Master
+%%% to start and stop "slave" nodes. It is the default callback module for the
+%%% <code>{init, node_start}</code> term of the Test Specification.</p>
+
+%%----------------------------------------------------------------------
+%% File : ct_slave.erl
+%% Description : CT module for starting nodes for large-scale testing.
+%%
+%% Created : 7 April 2010
+%%----------------------------------------------------------------------
+-module(ct_slave).
+
+-export([start/1, start/2, start/3, stop/1, stop/2]).
+
+-export([slave_started/2, slave_ready/2, monitor_master/1]).
+
+-record(options, {username, password, boot_timeout, init_timeout,
+ startup_timeout, startup_functions, monitor_master,
+ kill_if_fail, erl_flags}).
+
+%%%-----------------------------------------------------------------
+%%% @spec start(Node) -> Result
+%%% Node = atom()
+%%% Result = {ok, NodeName} |
+%%% {error, already_started, NodeName} |
+%%% {error, started_not_connected, NodeName} |
+%%% {error, boot_timeout, NodeName} |
+%%% {error, init_timeout, NodeName} |
+%%% {error, startup_timeout, NodeName} |
+%%% {error, not_alive, NodeName}
+%%% NodeName = atom()
+%%% @doc Starts an Erlang node with name <code>Node</code> on the local host.
+%%% @see start/3
+start(Node)->
+ start(gethostname(), Node).
+
+%%%-----------------------------------------------------------------
+%%% @spec start(Host, Node) -> Result
+%%% Node = atom()
+%%% Host = atom()
+%%% Result = {ok, NodeName} |
+%%% {error, already_started, NodeName} |
+%%% {error, started_not_connected, NodeName} |
+%%% {error, boot_timeout, NodeName} |
+%%% {error, init_timeout, NodeName} |
+%%% {error, startup_timeout, NodeName} |
+%%% {error, not_alive, NodeName}
+%%% NodeName = atom()
+%%% @doc Starts an Erlang node with name <code>Node</code> on host
+%%% <code>Host</code> with the default options.
+%%% @see start/3
+start(Host, Node)->
+ start(Host, Node, []).
+
+%%%-----------------------------------------------------------------
+%%% @spec start(Host, Node, Opts) -> Result
+%%% Node = atom()
+%%% Host = atom()
+%%% Opts = [OptTuples]
+%%% OptTuples = {username, Username} |
+%%% {password, Password} |
+%%% {boot_timeout, BootTimeout} | {init_timeout, InitTimeout} |
+%%% {startup_timeout, StartupTimeout} |
+%%% {startup_functions, StartupFunctions} |
+%%% {monitor_master, Monitor} |
+%%% {kill_if_fail, KillIfFail} |
+%%% {erl_flags, ErlangFlags}
+%%% Username = string()
+%%% Password = string()
+%%% BootTimeout = integer()
+%%% InitTimeout = integer()
+%%% StartupTimeout = integer()
+%%% StartupFunctions = [StartupFunctionSpec]
+%%% StartupFunctionSpec = {Module, Function, Arguments}
+%%% Module = atom()
+%%% Function = atom()
+%%% Arguments = [term]
+%%% Monitor = bool()
+%%% KillIfFail = bool()
+%%% ErlangFlags = string()
+%%% Result = {ok, NodeName} | {error, already_started, NodeName} |
+%%% {error, started_not_connected, NodeName} |
+%%% {error, boot_timeout, NodeName} |
+%%% {error, init_timeout, NodeName} |
+%%% {error, startup_timeout, NodeName} |
+%%% {error, not_alive, NodeName}
+%%% NodeName = atom()
+%%% @doc Starts an Erlang node with name <code>Node</code> on host
+%%% <code>Host</code> as specified by the combination of options in
+%%% <code>Opts</code>.
+%%%
+%%% <p>Options <code>Username</code> and <code>Password</code> will be used
+%%% to log in onto the remote host <code>Host</code>.
+%%% Username, if omitted, defaults to the current user name,
+%%% and password is empty by default.</p>
+%%%
+%%% <p>A list of functions specified in the <code>Startup</code> option will be
+%%% executed after startup of the node. Note that all used modules should be
+%%% present in the code path on the <code>Host</code>.</p>
+%%%
+%%% <p>The timeouts are applied as follows:
+%%% <list>
+%%% <item>
+%%% <code>BootTimeout</code> - time to start the Erlang node, in seconds.
+%%% Defaults to 3 seconds. If node does not become pingable within this time,
+%%% the result <code>{error, boot_timeout, NodeName}</code> is returned;
+%%% </item>
+%%% <item>
+%%% <code>InitTimeout</code> - time to wait for the node until it calls the
+%%% internal callback function informing master about successfull startup.
+%%% Defaults to one second.
+%%% In case of timed out message the result
+%%% <code>{error, init_timeout, NodeName}</code> is returned;
+%%% </item>
+%%% <item>
+%%% <code>StartupTimeout</code> - time to wait intil the node finishes to run
+%%% the <code>StartupFunctions</code>. Defaults to one second.
+%%% If this timeout occurs, the result
+%%% <code>{error, startup_timeout, NodeName}</code> is returned.
+%%% </item>
+%%% </list></p>
+%%%
+%%% <p>Option <code>monitor_master</code> specifies, if the slave node should be
+%%% stopped in case of master node stop. Defaults to false.</p>
+%%%
+%%% <p>Option <code>kill_if_fail</code> specifies, if the slave node should be
+%%% killed in case of a timeout during initialization or startup.
+%%% Defaults to true. Note that node also may be still alive it the boot
+%%% timeout occurred, but it will not be killed in this case.</p>
+%%%
+%%% <p>Option <code>erlang_flags</code> specifies, which flags will be added
+%%% to the parameters of the <code>erl</code> executable.</p>
+%%%
+%%% <p>Special return values are:
+%%% <list>
+%%% <item><code>{error, already_started, NodeName}</code> - if the node with
+%%% the given name is already started on a given host;</item>
+%%% <item><code>{error, started_not_connected, NodeName}</code> - if node is
+%%% started, but not connected to the master node.</item>
+%%% <item><code>{error, not_alive, NodeName}</code> - if node on which the
+%%% <code>ct_slave:start/3</code> is called, is not alive. Note that
+%%% <code>NodeName</code> is the name of current node in this case.</item>
+%%% </list></p>
+%%%
+start(Host, Node, Options)->
+ ENode = enodename(Host, Node),
+ case erlang:is_alive() of
+ false->
+ {error, not_alive, node()};
+ true->
+ case is_started(ENode) of
+ false->
+ OptionsRec = fetch_options(Options),
+ do_start(Host, Node, OptionsRec);
+ {true, not_connected}->
+ {error, started_not_connected, ENode};
+ {true, connected}->
+ {error, already_started, ENode}
+ end
+ end.
+
+%%% @spec stop(Node) -> Result
+%%% Node = atom()
+%%% Result = {ok, NodeName} |
+%%% {error, not_started, NodeName} |
+%%% {error, not_connected, NodeName} |
+%%% {error, stop_timeout, NodeName}
+%%% NodeName = atom()
+%%% @doc Stops the running Erlang node with name <code>Node</code> on
+%%% the localhost.
+stop(Node)->
+ stop(gethostname(), Node).
+
+%%% @spec stop(Host, Node) -> Result
+%%% Host = atom()
+%%% Node = atom()
+%%% Result = {ok, NodeName} |
+%%% {error, not_started, NodeName} |
+%%% {error, not_connected, NodeName} |
+%%% {error, stop_timeout, NodeName}
+%%% NodeName = atom()
+%%% @doc Stops the running Erlang node with name <code>Node</code> on
+%%% host <code>Host</code>.
+stop(Host, Node)->
+ ENode = enodename(Host, Node),
+ case is_started(ENode) of
+ {true, connected}->
+ do_stop(ENode);
+ {true, not_connected}->
+ {error, not_connected, ENode};
+ false->
+ {error, not_started, ENode}
+ end.
+
+%%% fetch an option value from the tagged tuple list with default
+get_option_value(Key, OptionList, Default)->
+ case lists:keyfind(Key, 1, OptionList) of
+ false->
+ Default;
+ {Key, Value}->
+ Value
+ end.
+
+%%% convert option list to the option record, fill all defaults
+fetch_options(Options)->
+ UserName = get_option_value(username, Options, []),
+ Password = get_option_value(password, Options, []),
+ BootTimeout = get_option_value(boot_timeout, Options, 3),
+ InitTimeout = get_option_value(init_timeout, Options, 1),
+ StartupTimeout = get_option_value(startup_timeout, Options, 1),
+ StartupFunctions = get_option_value(startup_functions, Options, []),
+ Monitor = get_option_value(monitor_master, Options, false),
+ KillIfFail = get_option_value(kill_if_fail, Options, true),
+ ErlFlags = get_option_value(erl_flags, Options, []),
+ #options{username=UserName, password=Password,
+ boot_timeout=BootTimeout, init_timeout=InitTimeout,
+ startup_timeout=StartupTimeout, startup_functions=StartupFunctions,
+ monitor_master=Monitor, kill_if_fail=KillIfFail, erl_flags=ErlFlags}.
+
+% send a message when slave node is started
+% @hidden
+slave_started(ENode, MasterPid)->
+ MasterPid ! {node_started, ENode},
+ ok.
+
+% send a message when slave node has finished startup
+% @hidden
+slave_ready(ENode, MasterPid)->
+ MasterPid ! {node_ready, ENode},
+ ok.
+
+% start monitoring of the master node
+% @hidden
+monitor_master(MasterNode)->
+ spawn(fun()->monitor_master_int(MasterNode) end).
+
+% code of the masterdeath-waiter process
+monitor_master_int(MasterNode)->
+ erlang:monitor_node(MasterNode, true),
+ receive
+ {nodedown, MasterNode}->
+ init:stop()
+ end.
+
+% check if node is listed in the nodes()
+is_connected(ENode)->
+ [N||N<-nodes(), N==ENode] == [ENode].
+
+% check if node is alive (ping and disconnect if pingable)
+is_started(ENode)->
+ case is_connected(ENode) of
+ true->
+ {true, connected};
+ false->
+ case net_adm:ping(ENode) of
+ pang->
+ false;
+ pong->
+ erlang:disconnect_node(ENode),
+ {true, not_connected}
+ end
+ end.
+
+% make a Erlang node name from name and hostname
+enodename(Host, Node)->
+ list_to_atom(atom_to_list(Node)++"@"++atom_to_list(Host)).
+
+% performs actual start of the "slave" node
+do_start(Host, Node, Options)->
+ ENode = enodename(Host, Node),
+ Functions =
+ lists:concat([[{ct_slave, slave_started, [ENode, self()]}],
+ Options#options.startup_functions,
+ [{ct_slave, slave_ready, [ENode, self()]}]]),
+ Functions2 = if
+ Options#options.monitor_master->
+ [{ct_slave, monitor_master, [node()]}|Functions];
+ true->
+ Functions
+ end,
+ MasterHost = gethostname(),
+ if
+ MasterHost == Host ->
+ spawn_local_node(Node, Options);
+ true->
+ spawn_remote_node(Host, Node, Options)
+ end,
+ BootTimeout = Options#options.boot_timeout,
+ InitTimeout = Options#options.init_timeout,
+ StartupTimeout = Options#options.startup_timeout,
+ Result = case wait_for_node_alive(ENode, BootTimeout) of
+ pong->
+ call_functions(ENode, Functions2),
+ receive
+ {node_started, ENode}->
+ receive
+ {node_ready, ENode}->
+ {ok, ENode}
+ after StartupTimeout*1000->
+ {error, startup_timeout, ENode}
+ end
+ after InitTimeout*1000 ->
+ {error, init_timeout, ENode}
+ end;
+ pang->
+ {error, boot_timeout, ENode}
+ end,
+ case Result of
+ {ok, ENode}->
+ ok;
+ {error, Timeout, ENode}
+ when ((Timeout==init_timeout) or (Timeout==startup_timeout)) and
+ Options#options.kill_if_fail->
+ do_stop(ENode);
+ _-> ok
+ end,
+ Result.
+
+% are we using fully qualified hostnames
+long_or_short()->
+ case net_kernel:longnames() of
+ true->
+ " -name ";
+ false->
+ " -sname "
+ end.
+
+% get the localhost's name, depending on the using name policy
+gethostname()->
+ Hostname = case net_kernel:longnames() of
+ true->
+ net_adm:localhost();
+ _->
+ {ok, Name}=inet:gethostname(),
+ Name
+ end,
+ list_to_atom(Hostname).
+
+% get cmd for starting Erlang
+get_cmd(Node, Flags)->
+ Cookie = erlang:get_cookie(),
+ "erl -detached -noinput -setcookie "++ atom_to_list(Cookie) ++
+ long_or_short() ++ atom_to_list(Node) ++ " " ++ Flags.
+
+% spawn node locally
+spawn_local_node(Node, Options)->
+ ErlFlags = Options#options.erl_flags,
+ Cmd = get_cmd(Node, ErlFlags),
+ open_port({spawn, Cmd}, [stream]).
+
+% start crypto and ssh if not yet started
+check_for_ssh_running()->
+ case application:get_application(crypto) of
+ undefined->
+ application:start(crypto),
+ case application:get_application(ssh) of
+ undefined->
+ application:start(ssh);
+ {ok, ssh}->
+ ok
+ end;
+ {ok, crypto}->
+ ok
+ end.
+
+% spawn node remotely
+spawn_remote_node(Host, Node, Options)->
+ Username = Options#options.username,
+ Password = Options#options.password,
+ ErlFlags = Options#options.erl_flags,
+ SSHOptions = case {Username, Password} of
+ {[], []}->
+ [];
+ {_, []}->
+ [{user, Username}];
+ {_, _}->
+ [{user, Username}, {password, Password}]
+ end ++ [{silently_accept_hosts, true}],
+ check_for_ssh_running(),
+ {ok, SSHConnRef} = ssh:connect(atom_to_list(Host), 22, SSHOptions),
+ {ok, SSHChannelId} = ssh_connection:session_channel(SSHConnRef, infinity),
+ ssh_connection:exec(SSHConnRef, SSHChannelId, get_cmd(Node, ErlFlags), infinity).
+
+% call functions on a remote Erlang node
+call_functions(_Node, [])->
+ ok;
+call_functions(Node, [{M, F, A}|Functions])->
+ rpc:call(Node, M, F, A),
+ call_functions(Node, Functions).
+
+% wait N seconds until node is pingable
+wait_for_node_alive(_Node, 0)->
+ pang;
+wait_for_node_alive(Node, N)->
+ timer:sleep(1000),
+ case net_adm:ping(Node) of
+ pong->
+ pong;
+ pang->
+ wait_for_node_alive(Node, N-1)
+ end.
+
+% call init:stop on a remote node
+do_stop(ENode)->
+ spawn(ENode, init, stop, []),
+ wait_for_node_dead(ENode, 5).
+
+% wait N seconds until node is disconnected
+wait_for_node_dead(Node, 0)->
+ {error, stop_timeout, Node};
+wait_for_node_dead(Node, N)->
+ timer:sleep(1000),
+ case lists:member(Node, nodes()) of
+ true->
+ wait_for_node_dead(Node, N-1);
+ false->
+ {ok, Node}
+ end.
diff --git a/lib/common_test/src/ct_snmp.erl b/lib/common_test/src/ct_snmp.erl
index 7ff88ad7d3..8fe63e8ed1 100644
--- a/lib/common_test/src/ct_snmp.erl
+++ b/lib/common_test/src/ct_snmp.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2004-2010. 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%
%%
@@ -332,7 +332,7 @@ set_info(Config) ->
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}),
+ ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
setup_users(Users).
%%% @spec register_agents(MgrAgentConfName, ManagedAgents) -> ok | {error, Reason}
@@ -347,7 +347,7 @@ 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}),
+ ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
setup_managed_agents(ManagedAgents).
%%% @spec register_usm_users(MgrAgentConfName, UsmUsers) -> ok | {error, Reason}
@@ -361,7 +361,7 @@ register_agents(MgrAgentConfName, ManagedAgents) ->
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}),
+ ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID),
setup_usm_users(UsmUsers, EngineID).
@@ -376,7 +376,7 @@ unregister_users(MgrAgentConfName) ->
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}),
+ ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
takedown_users(Users).
%%% @spec unregister_agents(MgrAgentConfName) -> ok | {error, Reason}
@@ -393,7 +393,7 @@ unregister_agents(MgrAgentConfName) ->
{snmp, SnmpVals} = ct:get_config(MgrAgentConfName),
NewSnmpVals = lists:keyreplace(managed_agents, 1, SnmpVals,
{managed_agents, []}),
- ct_util:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
+ ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
takedown_managed_agents(ManagedAgents).
@@ -409,7 +409,7 @@ 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}),
+ ct_config:update_config(MgrAgentConfName, {snmp, NewSnmpVals}),
EngineID = ct:get_config({MgrAgentConfName, engine_id}, ?ENGINE_ID),
do_update_usm_users(UsmUsers, EngineID).
diff --git a/lib/common_test/src/ct_telnet_client.erl b/lib/common_test/src/ct_telnet_client.erl
index 1a12c5e343..d703b39ac5 100644
--- a/lib/common_test/src/ct_telnet_client.erl
+++ b/lib/common_test/src/ct_telnet_client.erl
@@ -35,8 +35,6 @@
-export([open/1, open/2, open/3, open/4, close/1]).
-export([send_data/2, get_data/1]).
--define(DBG, false).
-
-define(TELNET_PORT, 23).
-define(OPEN_TIMEOUT,10000).
-define(IDLE_TIMEOUT,10000).
@@ -287,35 +285,38 @@ get_subcmd([?SE | Rest], Acc) ->
get_subcmd([Opt | Rest], Acc) ->
get_subcmd(Rest, [Opt | Acc]).
-
+-ifdef(debug).
dbg(_Str,_Args) ->
- if ?DBG -> io:format(_Str,_Args);
- true -> ok
+ io:format(_Str,_Args).
+
+cmd_dbg(_Cmd) ->
+ case _Cmd of
+ [?IAC|Cmd1] ->
+ cmd_dbg(Cmd1);
+ [Ctrl|Opts] ->
+ CtrlStr =
+ case Ctrl of
+ ?DO -> "DO";
+ ?DONT -> "DONT";
+ ?WILL -> "WILL";
+ ?WONT -> "WONT";
+ ?NOP -> "NOP";
+ _ -> "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.
+-else.
+dbg(_Str,_Args) ->
+ ok.
+
cmd_dbg(_Cmd) ->
- if ?DBG ->
- case _Cmd of
- [?IAC|Cmd1] ->
- cmd_dbg(Cmd1);
- [Ctrl|Opts] ->
- CtrlStr =
- case Ctrl of
- ?DO -> "DO";
- ?DONT -> "DONT";
- ?WILL -> "WILL";
- ?WONT -> "WONT";
- ?NOP -> "NOP";
- _ -> "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.
+ ok.
+-endif.
diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl
index 4378ec5a52..0f68b062f6 100644
--- a/lib/common_test/src/ct_testspec.erl
+++ b/lib/common_test/src/ct_testspec.erl
@@ -17,7 +17,7 @@
%% %CopyrightEnd%
%%
-%%% @doc Common Test Framework functions handlig test specifikations.
+%%% @doc Common Test Framework functions handling test specifications.
%%%
%%% <p>This module exports functions that are used within CT to
%%% scan and parse test specifikations.</p>
@@ -270,33 +270,8 @@ collect_tests(Terms,TestSpec,Relaxed) ->
put(relaxed,Relaxed),
TestSpec1 = get_global(Terms,TestSpec),
TestSpec2 = get_all_nodes(Terms,TestSpec1),
- case catch evaluate(Terms,TestSpec2) of
- {error,{Node,{M,F,A},Reason}} ->
- io:format("Error! Common Test failed to evaluate ~w:~w/~w on ~w. "
- "Reason: ~p~n~n", [M,F,A,Node,Reason]);
- _ -> ok
- end,
- add_tests(Terms,TestSpec2).
-
-evaluate([{eval,NodeRef,{M,F,Args}}|Ts],Spec) ->
- Node = ref2node(NodeRef,Spec#testspec.nodes),
- case rpc:call(Node,M,F,Args) of
- {badrpc,Reason} ->
- throw({error,{Node,{M,F,length(Args)},Reason}});
- _ ->
- ok
- end,
- evaluate(Ts,Spec);
-evaluate([{eval,{M,F,Args}}|Ts],Spec) ->
- case catch apply(M,F,Args) of
- {'EXIT',Reason} ->
- throw({error,{node(),{M,F,length(Args)},Reason}});
- _ ->
- ok
- end,
- evaluate(Ts,Spec);
-evaluate([],_Spec) ->
- ok.
+ {Terms2, TestSpec3} = filter_init_terms(Terms, [], TestSpec2),
+ add_tests(Terms2,TestSpec3).
get_global([{alias,Ref,Dir}|Ts],Spec=#testspec{alias=Refs}) ->
get_global(Ts,Spec#testspec{alias=[{Ref,get_absdir(Dir,Spec)}|Refs]});
@@ -305,6 +280,26 @@ get_global([{node,Ref,Node}|Ts],Spec=#testspec{nodes=Refs}) ->
get_global([_|Ts],Spec) -> get_global(Ts,Spec);
get_global([],Spec) -> Spec.
+get_absfile(Callback, FullName,#testspec{spec_dir=SpecDir}) ->
+ % we need to temporary switch to new cwd here, because
+ % otherwise config files cannot be found
+ {ok, OldWd} = file:get_cwd(),
+ ok = file:set_cwd(SpecDir),
+ R = Callback:check_parameter(FullName),
+ ok = file:set_cwd(OldWd),
+ case R of
+ {ok, {file, FullName}}->
+ File = filename:basename(FullName),
+ Dir = get_absname(filename:dirname(FullName),SpecDir),
+ filename:join(Dir,File);
+ {ok, {config, FullName}}->
+ FullName;
+ {error, {nofile, FullName}}->
+ FullName;
+ {error, {wrong_config, FullName}}->
+ FullName
+ end.
+
get_absfile(FullName,#testspec{spec_dir=SpecDir}) ->
File = filename:basename(FullName),
Dir = get_absname(filename:dirname(FullName),SpecDir),
@@ -353,6 +348,68 @@ get_all_nodes([_|Ts],Spec) ->
get_all_nodes([],Spec) ->
Spec.
+filter_init_terms([{init, InitOptions}|Ts], NewTerms, Spec)->
+ filter_init_terms([{init, list_nodes(Spec), InitOptions}|Ts], NewTerms, Spec);
+filter_init_terms([{init, NodeRef, InitOptions}|Ts], NewTerms, Spec)
+ when is_atom(NodeRef)->
+ filter_init_terms([{init, [NodeRef], InitOptions}|Ts], NewTerms, Spec);
+filter_init_terms([{init, NodeRefs, InitOption}|Ts], NewTerms, Spec) when is_tuple(InitOption) ->
+ filter_init_terms([{init, NodeRefs, [InitOption]}|Ts], NewTerms, Spec);
+filter_init_terms([{init, [NodeRef|NodeRefs], InitOptions}|Ts], NewTerms, Spec=#testspec{init=InitData})->
+ NodeStartOptions = case lists:keyfind(node_start, 1, InitOptions) of
+ {node_start, NSOptions}->
+ case lists:keyfind(callback_module, 1, NSOptions) of
+ {callback_module, _Callback}->
+ NSOptions;
+ false->
+ [{callback_module, ct_slave}|NSOptions]
+ end;
+ false->
+ []
+ end,
+ EvalTerms = case lists:keyfind(eval, 1, InitOptions) of
+ {eval, MFA} when is_tuple(MFA)->
+ [MFA];
+ {eval, MFAs} when is_list(MFAs)->
+ MFAs;
+ false->
+ []
+ end,
+ Node = ref2node(NodeRef,Spec#testspec.nodes),
+ InitData2 = add_option({node_start, NodeStartOptions}, Node, InitData, true),
+ InitData3 = add_option({eval, EvalTerms}, Node, InitData2, false),
+ filter_init_terms([{init, NodeRefs, InitOptions}|Ts], NewTerms, Spec#testspec{init=InitData3});
+filter_init_terms([{init, [], _}|Ts], NewTerms, Spec)->
+ filter_init_terms(Ts, NewTerms, Spec);
+filter_init_terms([Term|Ts], NewTerms, Spec)->
+ filter_init_terms(Ts, [Term|NewTerms], Spec);
+filter_init_terms([], NewTerms, Spec)->
+ {lists:reverse(NewTerms), Spec}.
+
+add_option([], _, List, _)->
+ List;
+add_option({Key, Value}, Node, List, WarnIfExists) when is_list(Value)->
+ OldOptions = case lists:keyfind(Node, 1, List) of
+ {Node, Options}->
+ Options;
+ false->
+ []
+ end,
+ NewOption = case lists:keyfind(Key, 1, OldOptions) of
+ {Key, OldOption} when WarnIfExists, OldOption/=[]->
+ io:format("There is an option ~w=~w already defined for node ~p, skipping new ~w~n",
+ [Key, OldOption, Node, Value]),
+ OldOption;
+ {Key, OldOption}->
+ OldOption ++ Value;
+ false->
+ Value
+ end,
+ lists:keystore(Node, 1, List,
+ {Node, lists:keystore(Key, 1, OldOptions, {Key, NewOption})});
+add_option({Key, Value}, Node, List, WarnIfExists)->
+ add_option({Key, [Value]}, Node, List, WarnIfExists).
+
save_nodes(Nodes,Spec=#testspec{nodes=NodeRefs}) ->
NodeRefs1 =
lists:foldr(fun(all_nodes,NR) ->
@@ -415,6 +472,36 @@ add_tests([{cover,Node,File}|Ts],Spec) ->
add_tests([{cover,File}|Ts],Spec) ->
add_tests([{cover,all_nodes,File}|Ts],Spec);
+%% --- multiply_timetraps ---
+add_tests([{multiply_timetraps,all_nodes,MT}|Ts],Spec) ->
+ Tests = lists:map(fun(N) -> {multiply_timetraps,N,MT} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{multiply_timetraps,Nodes,MT}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,multiply_timetraps,[MT],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{multiply_timetraps,Node,MT}|Ts],Spec) ->
+ MTs = Spec#testspec.multiply_timetraps,
+ MTs1 = [{ref2node(Node,Spec#testspec.nodes),MT} |
+ lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,MTs)],
+ add_tests(Ts,Spec#testspec{multiply_timetraps=MTs1});
+add_tests([{multiply_timetraps,MT}|Ts],Spec) ->
+ add_tests([{multiply_timetraps,all_nodes,MT}|Ts],Spec);
+
+%% --- scale_timetraps ---
+add_tests([{scale_timetraps,all_nodes,ST}|Ts],Spec) ->
+ Tests = lists:map(fun(N) -> {scale_timetraps,N,ST} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{scale_timetraps,Nodes,ST}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,scale_timetraps,[ST],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{scale_timetraps,Node,ST}|Ts],Spec) ->
+ STs = Spec#testspec.scale_timetraps,
+ STs1 = [{ref2node(Node,Spec#testspec.nodes),ST} |
+ lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,STs)],
+ add_tests(Ts,Spec#testspec{scale_timetraps=STs1});
+add_tests([{scale_timetraps,ST}|Ts],Spec) ->
+ add_tests([{scale_timetraps,all_nodes,ST}|Ts],Spec);
+
%% --- config ---
add_tests([{config,all_nodes,Files}|Ts],Spec) ->
Tests = lists:map(fun(N) -> {config,N,Files} end, list_nodes(Spec)),
@@ -434,6 +521,27 @@ add_tests([{config,Node,F}|Ts],Spec) ->
add_tests([{config,Files}|Ts],Spec) ->
add_tests([{config,all_nodes,Files}|Ts],Spec);
+
+%% --- userconfig ---
+add_tests([{userconfig,all_nodes,CBF}|Ts],Spec) ->
+ Tests = lists:map(fun(N) -> {userconfig,N,CBF} end, list_nodes(Spec)),
+ add_tests(Tests++Ts,Spec);
+add_tests([{userconfig,Nodes,CBF}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,userconfig,[CBF],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{userconfig,Node,[{Callback, Config}|CBF]}|Ts],Spec) ->
+ Cfgs = Spec#testspec.userconfig,
+ Node1 = ref2node(Node,Spec#testspec.nodes),
+ add_tests([{userconfig,Node,CBF}|Ts],
+ Spec#testspec{userconfig=[{Node1,{Callback,
+ get_absfile(Callback, Config ,Spec)}}|Cfgs]});
+add_tests([{userconfig,_Node,[]}|Ts],Spec) ->
+ add_tests(Ts,Spec);
+add_tests([{userconfig,Node,CBF}|Ts],Spec) ->
+ add_tests([{userconfig,Node,[CBF]}|Ts],Spec);
+add_tests([{userconfig,CBF}|Ts],Spec) ->
+ add_tests([{userconfig,all_nodes,CBF}|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)),
@@ -516,6 +624,38 @@ add_tests([{suites,Node,Dir,Ss}|Ts],Spec) ->
Ss,Tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
+%% --- groups ---
+%% Later make it possible to specify group execution properties
+%% that will override thse in the suite. Also make it possible
+%% create dynamic groups in specification, i.e. to group test cases
+%% by means of groups defined only in the test specification.
+add_tests([{groups,all_nodes,Dir,Suite,Gs}|Ts],Spec) ->
+ add_tests([{groups,list_nodes(Spec),Dir,Suite,Gs}|Ts],Spec);
+add_tests([{groups,all_nodes,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) ->
+ add_tests([{groups,list_nodes(Spec),Dir,Suite,Gs,{cases,TCs}}|Ts],Spec);
+add_tests([{groups,Dir,Suite,Gs}|Ts],Spec) ->
+ add_tests([{groups,all_nodes,Dir,Suite,Gs}|Ts],Spec);
+add_tests([{groups,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) ->
+ add_tests([{groups,all_nodes,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec);
+add_tests([{groups,Nodes,Dir,Suite,Gs}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,groups,[Dir,Suite,Gs],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{groups,Nodes,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,groups,[Dir,Suite,Gs,{cases,TCs}],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{groups,Node,Dir,Suite,Gs}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = insert_groups(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Suite,Gs,all,Tests),
+ add_tests(Ts,Spec#testspec{tests=Tests1});
+add_tests([{groups,Node,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = insert_groups(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Suite,Gs,TCs,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);
@@ -546,6 +686,34 @@ add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) ->
Ss,Cmt,Tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
+%% --- skip_groups ---
+add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,Cmt}|Ts],Spec) ->
+ add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,Cmt}|Ts],Spec);
+add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) ->
+ add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec);
+add_tests([{skip_groups,Dir,Suite,Gs,Cmt}|Ts],Spec) ->
+ add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,Cmt}|Ts],Spec);
+add_tests([{skip_groups,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) ->
+ add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec);
+add_tests([{skip_groups,Nodes,Dir,Suite,Gs,Cmt}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,Cmt],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{skip_groups,Nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,{cases,TCs},Cmt],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{skip_groups,Node,Dir,Suite,Gs,Cmt}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = skip_groups(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Suite,Gs,all,Cmt,Tests),
+ add_tests(Ts,Spec#testspec{tests=Tests1});
+add_tests([{skip_groups,Node,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) ->
+ Tests = Spec#testspec.tests,
+ Tests1 = skip_groups(ref2node(Node,Spec#testspec.nodes),
+ ref2dir(Dir,Spec#testspec.alias),
+ Suite,Gs,TCs,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);
@@ -614,8 +782,11 @@ separate([],_,_,_) ->
%% Representation:
-%% {{Node,Dir},[{Suite1,[case11,case12,...]},{Suite2,[case21,case22,...]},...]}
-%% {{Node,Dir},[{Suite1,{skip,Cmt}},{Suite2,[{case21,{skip,Cmt}},case22,...]},...]}
+%% {{Node,Dir},[{Suite1,[GrOrCase11,GrOrCase12,...]},
+%% {Suite2,[GrOrCase21,GrOrCase22,...]},...]}
+%% {{Node,Dir},[{Suite1,{skip,Cmt}},
+%% {Suite2,[{GrOrCase21,{skip,Cmt}},GrOrCase22,...]},...]}
+%% GrOrCase = {GroupName,[Case1,Case2,...]} | Case
insert_suites(Node,Dir,[S|Ss],Tests) ->
Tests1 = insert_cases(Node,Dir,S,all,Tests),
@@ -625,6 +796,54 @@ insert_suites(_Node,_Dir,[],Tests) ->
insert_suites(Node,Dir,S,Tests) ->
insert_suites(Node,Dir,[S],Tests).
+insert_groups(Node,Dir,Suite,Group,Cases,Tests) when is_atom(Group) ->
+ insert_groups(Node,Dir,Suite,[Group],Cases,Tests);
+insert_groups(Node,Dir,Suite,Groups,Cases,Tests) when
+ ((Cases == all) or is_list(Cases)) and is_list(Groups) ->
+ case lists:keysearch({Node,Dir},1,Tests) of
+ {value,{{Node,Dir},[{all,_}]}} ->
+ Tests;
+ {value,{{Node,Dir},Suites0}} ->
+ Suites1 = insert_groups1(Suite,
+ [{Gr,Cases} || Gr <- Groups],
+ Suites0),
+ insert_in_order({{Node,Dir},Suites1},Tests);
+ false ->
+ Groups1 = [{Gr,Cases} || Gr <- Groups],
+ insert_in_order({{Node,Dir},[{Suite,Groups1}]},Tests)
+ end;
+insert_groups(Node,Dir,Suite,Groups,Case,Tests) when is_atom(Case) ->
+ Cases = if Case == all -> all; true -> [Case] end,
+ insert_groups(Node,Dir,Suite,Groups,Cases,Tests).
+
+insert_groups1(_Suite,_Groups,all) ->
+ all;
+insert_groups1(Suite,Groups,Suites0) ->
+ case lists:keysearch(Suite,1,Suites0) of
+ {value,{Suite,all}} ->
+ Suites0;
+ {value,{Suite,GrAndCases0}} ->
+ GrAndCases = insert_groups2(Groups,GrAndCases0),
+ insert_in_order({Suite,GrAndCases},Suites0);
+ false ->
+ insert_in_order({Suite,Groups},Suites0)
+ end.
+
+insert_groups2(_Groups,all) ->
+ all;
+insert_groups2([Group={GrName,Cases}|Groups],GrAndCases) ->
+ case lists:keysearch(GrName,1,GrAndCases) of
+ {value,{GrName,all}} ->
+ GrAndCases;
+ {value,{GrName,Cases0}} ->
+ Cases1 = insert_in_order(Cases,Cases0),
+ insert_groups2(Groups,insert_in_order({GrName,Cases1},GrAndCases));
+ false ->
+ insert_groups2(Groups,insert_in_order(Group,GrAndCases))
+ end;
+insert_groups2([],GrAndCases) ->
+ GrAndCases.
+
insert_cases(Node,Dir,Suite,Cases,Tests) when is_list(Cases) ->
case lists:keysearch({Node,Dir},1,Tests) of
{value,{{Node,Dir},[{all,_}]}} ->
@@ -659,6 +878,35 @@ skip_suites(_Node,_Dir,[],_Cmt,Tests) ->
skip_suites(Node,Dir,S,Cmt,Tests) ->
skip_suites(Node,Dir,[S],Cmt,Tests).
+skip_groups(Node,Dir,Suite,Group,Case,Cmt,Tests) when is_atom(Group) ->
+ skip_groups(Node,Dir,Suite,[Group],[Case],Cmt,Tests);
+skip_groups(Node,Dir,Suite,Groups,Cases,Cmt,Tests) when
+ ((Cases == all) or is_list(Cases)) and is_list(Groups) ->
+ Suites =
+ case lists:keysearch({Node,Dir},1,Tests) of
+ {value,{{Node,Dir},Suites0}} ->
+ Suites0;
+ false ->
+ []
+ end,
+ Suites1 = skip_groups1(Suite,[{Gr,Cases} || Gr <- Groups],Cmt,Suites),
+ insert_in_order({{Node,Dir},Suites1},Tests);
+skip_groups(Node,Dir,Suite,Groups,Case,Cmt,Tests) when is_atom(Case) ->
+ Cases = if Case == all -> all; true -> [Case] end,
+ skip_groups(Node,Dir,Suite,Groups,Cases,Cmt,Tests).
+
+skip_groups1(Suite,Groups,Cmt,Suites0) ->
+ SkipGroups = lists:map(fun(Group) ->
+ {Group,{skip,Cmt}}
+ end,Groups),
+ case lists:keysearch(Suite,1,Suites0) of
+ {value,{Suite,GrAndCases0}} ->
+ GrAndCases1 = GrAndCases0 ++ SkipGroups,
+ insert_in_order({Suite,GrAndCases1},Suites0);
+ false ->
+ insert_in_order({Suite,SkipGroups},Suites0)
+ end.
+
skip_cases(Node,Dir,Suite,Cases,Cmt,Tests) when is_list(Cases) ->
Suites =
case lists:keysearch({Node,Dir},1,Tests) of
@@ -753,6 +1001,8 @@ valid_terms() ->
{cover,3},
{config,2},
{config,3},
+ {userconfig, 2},
+ {userconfig, 3},
{alias,3},
{logdir,2},
{logdir,3},
@@ -761,7 +1011,6 @@ valid_terms() ->
{event_handler,4},
{include,2},
{include,3},
-
{suites,3},
{suites,4},
{cases,4},
@@ -816,7 +1065,3 @@ common_letters([L|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
index ba3d789f8d..eddaf4c8b9 100644
--- a/lib/common_test/src/ct_util.erl
+++ b/lib/common_test/src/ct_util.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2003-2010. 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%
%%
@@ -30,10 +30,7 @@
-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([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,
@@ -46,6 +43,8 @@
silence_all_connections/0, silence_connections/1, is_silenced/1,
reset_silent_connections/0]).
+-export([get_mode/0, create_table/3, read_opts/0]).
+
-export([set_cwd/1, reset_cwd/0]).
-export([parse_table/1]).
@@ -56,23 +55,15 @@
-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([kill_attached/2, get_attached/1, ct_make_ref/0]).
-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
@@ -119,7 +110,6 @@ start(Mode,LogDir) ->
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),
@@ -135,7 +125,6 @@ do_start(Parent,Mode,LogDir) ->
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,_}} ->
@@ -148,38 +137,34 @@ do_start(Parent,Mode,LogDir) ->
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 ->
+ %% start ct_config server
+ ct_config:start(Mode),
+ %% add user event 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,
- {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.
+ 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).
create_table(TableName,KeyPos) ->
create_table(TableName,set,KeyPos).
@@ -197,106 +182,6 @@ read_opts() ->
{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}}).
@@ -342,26 +227,6 @@ loop(Mode,TestData,StartDir) ->
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,
@@ -434,14 +299,14 @@ loop(Mode,TestData,StartDir) ->
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(),
+ ct_config:stop(),
+ file:set_cwd(StartDir),
return(From,ok);
{get_mode,From} ->
return(From,Mode),
@@ -463,6 +328,8 @@ close_connections([#conn{handle=Handle,callback=CB}|Conns]) ->
close_connections([]) ->
ok.
+get_key_from_name(Name)->
+ ct_config:get_key_from_name(Name).
%%%-----------------------------------------------------------------
%%% @spec register_connection(TargetName,Address,Callback,Handle) ->
@@ -480,7 +347,7 @@ close_connections([]) ->
%%% 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
+ case ct_config:get_ref_from_name(TargetName) of
{ok,Ref} ->
Ref;
_ ->
@@ -518,7 +385,7 @@ unregister_connection(Handle) ->
%%%
%%% @doc Check if a connection already exists.
does_connection_exist(TargetName,Address,Callback) ->
- case get_ref_from_name(TargetName) of
+ case ct_config:get_ref_from_name(TargetName) of
{ok,TargetRef} ->
case ets:select(?conn_table,[{#conn{handle='$1',
targetref=TargetRef,
@@ -548,7 +415,7 @@ does_connection_exist(TargetName,Address,Callback) ->
%%% @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
+ case ct_config:get_ref_from_name(TargetName) of
{ok,Ref} ->
{ok,ets:select(?conn_table,[{#conn{handle='$1',
address='$2',
@@ -568,250 +435,11 @@ get_target_name(ConnPid) ->
[],
['$1']}]) of
[TargetRef] ->
- get_name_from_ref(TargetRef);
+ ct_config: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
%%%
@@ -991,166 +619,6 @@ get_testdir(Dir, Suite) when is_list(Suite) ->
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
@@ -1210,7 +678,7 @@ call(Msg) ->
ct_util_server ! {Msg,{self(),Ref}},
receive
{Ref, Result} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
Result;
{'DOWN',MRef,process,_,Reason} ->
{error,{ct_util_server_down,Reason}}
@@ -1244,37 +712,6 @@ ct_make_ref_loop(N) ->
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),
diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl
index c1dc14f943..54eed29415 100644
--- a/lib/common_test/src/ct_util.hrl
+++ b/lib/common_test/src/ct_util.hrl
@@ -29,11 +29,15 @@
-record(testspec, {spec_dir,
nodes=[],
+ init=[],
logdir=["."],
cover=[],
config=[],
+ userconfig=[],
event_handler=[],
include=[],
+ multiply_timetraps=[],
+ scale_timetraps=[],
alias=[],
tests=[]}).
@@ -50,3 +54,4 @@
-define(CT_MEVMGR_REF, ct_master_event).
-define(missing_suites_info, "missing_suites.info").
+-define(ct_config_txt, ct_config_plain).
diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl
index ad4845a7c3..2ee982d726 100644
--- a/lib/common_test/src/vts.erl
+++ b/lib/common_test/src/vts.erl
@@ -1,19 +1,19 @@
%%
%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2003-2009. All Rights Reserved.
-%%
+%%
+%% Copyright Ericsson AB 2003-2010. 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%
%%
@@ -100,7 +100,7 @@ start_link() ->
MRef = erlang:monitor(process,Pid),
receive
{Pid,started} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
{ok,Pid};
{'DOWN',MRef,process,_,Reason} ->
{error,{vts,died,Reason}}
@@ -160,11 +160,13 @@ init(Parent) ->
loop(State) ->
receive
- {{init_data,ConfigFiles,EvHandlers,LogDir,Tests},From} ->
- ct_install(State),
+ {{init_data,Config,EvHandlers,LogDir,Tests},From} ->
+ %% ct:pal("State#state.current_log_dir=~p", [State#state.current_log_dir]),
+ NewState = State#state{config=Config,event_handler=EvHandlers,
+ current_log_dir=LogDir,tests=Tests},
+ ct_install(NewState),
return(From,ok),
- loop(#state{config=ConfigFiles,event_handler=EvHandlers,
- current_log_dir=LogDir,tests=Tests});
+ loop(NewState);
{start_page,From} ->
return(From,start_page1()),
loop(State);
@@ -257,7 +259,7 @@ call(Msg) ->
Pid ! {Msg,{self(),Ref}},
receive
{Ref, Result} ->
- erlang:demonitor(MRef),
+ erlang:demonitor(MRef, [flush]),
Result;
{'DOWN',MRef,process,_,Reason} ->
{error,{process_down,Pid,Reason}}