%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2003-2012. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
%%% @doc Main user interface for the Common Test framework.
%%%
%%% <p> This module implements the command line interface for running
%%% tests and some basic functions for common test case issues
%%% such as configuration and logging. </p>
%%%
%%% <p><strong>Test Suite Support Macros</strong></p>
%%%
%%% <p>The <code>config</code> macro is defined in <code>ct.hrl</code>. This
%%% macro should be used to retrieve information from the
%%% <code>Config</code> variable sent to all test cases. It is used with two
%%% arguments, where the first is the name of the configuration
%%% variable you wish to retrieve, and the second is the <code>Config</code>
%%% variable supplied to the test case.</p>
%%%
%%% <p>Possible configuration variables include:</p>
%%% <ul>
%%% <li><code>data_dir</code> - Data file directory.</li>
%%% <li><code>priv_dir</code> - Scratch file directory.</li>
%%% <li>Whatever added by <code>init_per_suite/1</code> or
%%% <code>init_per_testcase/2</code> in the test suite.</li>
%%% </ul>
%%% @type var_name() = atom(). A variable name which is specified when
%%% <code>ct:require/2</code> is called,
%%% e.g. <code>ct:require(mynodename,{node,[telnet]})</code>
%%%
%%% @type target_name() = var_name(). The name of a target.
%%%
%%% @type handle() = ct_gen_conn:handle() | term(). The identity of a
%%% specific connection.
-module(ct).
%% Command line user interface for running tests
-export([install/1, run/1, run/2, run/3,
run_test/1, run_testspec/1, step/3, step/4,
start_interactive/0, stop_interactive/0]).
%% Test suite API
-export([require/1, require/2,
get_config/1, get_config/2, get_config/3,
reload_config/1,
log/1, log/2, log/3,
print/1, print/2, print/3,
pal/1, pal/2, pal/3,
capture_start/0, capture_stop/0, capture_get/0, capture_get/1,
fail/1, fail/2, comment/1, comment/2, make_priv_dir/0,
testcases/2, userdata/2, userdata/3,
timetrap/1, get_timetrap_info/0, sleep/1,
notify/2, sync_notify/2]).
%% 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]).
%%%-----------------------------------------------------------------
%%% @spec install(Opts) -> ok | {error,Reason}
%%% Opts = [Opt]
%%% Opt = {config,ConfigFiles} | {event_handler,Modules} |
%%% {decrypt,KeyOrFile}
%%% ConfigFiles = [ConfigFile]
%%% ConfigFile = string()
%%% Modules = [atom()]
%%% KeyOrFile = {key,Key} | {file,KeyFile}
%%% Key = string()
%%% KeyFile = string()
%%% @doc Install config files and event handlers.
%%%
%%% <p>Run this function once before first test.</p>
%%%
%%% <p>Example:<br/>
%%% <code>install([{config,["config_node.ctc","config_user.ctc"]}])</code>.</p>
%%%
%%% <p>Note that this function is automatically run by the
%%% <code>ct_run</code> program.</p>
install(Opts) ->
ct_run:install(Opts).
%%%-----------------------------------------------------------------
%%% @spec run(TestDir,Suite,Cases) -> Result
%%% TestDir = string()
%%% Suite = atom()
%%% Cases = atom() | [atom()]
%%% Result = [TestResult] | {error,Reason}
%%%
%%% @doc Run the given test case(s).
%%%
%%% <p>Requires that <code>ct:install/1</code> has been run first.</p>
%%%
%%% <p>Suites (*_SUITE.erl) files must be stored in
%%% <code>TestDir</code> or <code>TestDir/test</code>. All suites
%%% will be compiled when test is run.</p>
run(TestDir,Suite,Cases) ->
ct_run:run(TestDir,Suite,Cases).
%%%-----------------------------------------------------------------
%%% @spec run(TestDir,Suite) -> Result
%%%
%%% @doc Run all test cases in the given suite.
%%% @see run/3.
run(TestDir,Suite) ->
ct_run:run(TestDir,Suite).
%%%-----------------------------------------------------------------
%%% @spec run(TestDirs) -> Result
%%% TestDirs = TestDir | [TestDir]
%%%
%%% @doc Run all test cases in all suites in the given directories.
%%% @see run/3.
run(TestDirs) ->
ct_run:run(TestDirs).
%%%-----------------------------------------------------------------
%%% @spec run_test(Opts) -> Result
%%% Opts = [OptTuples]
%%% OptTuples = {dir,TestDirs} | {suite,Suites} | {group,Groups} |
%%% {testcase,Cases} | {spec,TestSpecs} | {label,Label} |
%%% {config,CfgFiles} | {userconfig, UserConfig} |
%%% {allow_user_terms,Bool} | {logdir,LogDir} |
%%% {silent_connections,Conns} | {stylesheet,CSSFile} |
%%% {cover,CoverSpecFile} | {step,StepOpts} |
%%% {event_handler,EventHandlers} | {include,InclDirs} |
%%% {auto_compile,Bool} | {create_priv_dir,CreatePrivDir} |
%%% {multiply_timetraps,M} | {scale_timetraps,Bool} |
%%% {repeat,N} | {duration,DurTime} | {until,StopTime} |
%%% {force_stop,Bool} | {decrypt,DecryptKeyOrFile} |
%%% {refresh_logs,LogDir} | {logopts,LogOpts} | {basic_html,Bool} |
%%% {ct_hooks, CTHs} | {enable_builtin_hooks,Bool}
%%% TestDirs = [string()] | string()
%%% Suites = [string()] | [atom()] | string() | atom()
%%% Cases = [atom()] | atom()
%%% Groups = [atom()] | atom()
%%% TestSpecs = [string()] | string()
%%% Label = string() | atom()
%%% CfgFiles = [string()] | string()
%%% UserConfig = [{CallbackMod,CfgStrings}] | {CallbackMod,CfgStrings}
%%% CallbackMod = atom()
%%% CfgStrings = [string()] | string()
%%% LogDir = string()
%%% Conns = all | [atom()]
%%% CSSFile = string()
%%% CoverSpecFile = string()
%%% StepOpts = [StepOpt] | []
%%% StepOpt = config | keep_inactive
%%% EventHandlers = EH | [EH]
%%% EH = atom() | {atom(),InitArgs} | {[atom()],InitArgs}
%%% InitArgs = [term()]
%%% InclDirs = [string()] | string()
%%% CreatePrivDir = auto_per_run | auto_per_tc | manual_per_tc
%%% M = integer()
%%% N = integer()
%%% DurTime = string(HHMMSS)
%%% StopTime = string(YYMoMoDDHHMMSS) | string(HHMMSS)
%%% DecryptKeyOrFile = {key,DecryptKey} | {file,DecryptFile}
%%% DecryptKey = string()
%%% DecryptFile = string()
%%% LogOpts = [LogOpt]
%%% LogOpt = no_nl | no_src
%%% CTHs = [CTHModule | {CTHModule, CTHInitArgs}]
%%% CTHModule = atom()
%%% CTHInitArgs = term()
%%% 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
%%% <seealso marker="ct_run#ct_run"><code>ct_run</code></seealso> 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>ct_run</code>
%%% program. Configuration files specified in <code>Opts</code> will be
%%% installed automatically at startup.
run_test(Opts) ->
ct_run:run_test(Opts).
%%%-----------------------------------------------------------------
%%% @spec run_testspec(TestSpec) -> Result
%%% TestSpec = [term()]
%%% @doc Run test specified by <code>TestSpec</code>. The terms are
%%% the same as those used in test specification files.
run_testspec(TestSpec) ->
ct_run:run_testspec(TestSpec).
%%%-----------------------------------------------------------------
%%% @spec step(TestDir,Suite,Case) -> Result
%%% Case = atom()
%%%
%%% @doc Step through a test case with the debugger.
%%% @see run/3
step(TestDir,Suite,Case) ->
ct_run:step(TestDir,Suite,Case).
%%%-----------------------------------------------------------------
%%% @spec step(TestDir,Suite,Case,Opts) -> Result
%%% Case = atom()
%%% Opts = [Opt] | []
%%% Opt = config | keep_inactive
%%%
%%% @doc Step through a test case with the debugger. If the
%%% <code>config</code> option has been given, breakpoints will
%%% be set also on the configuration functions in <code>Suite</code>.
%%% @see run/3
step(TestDir,Suite,Case,Opts) ->
ct_run:step(TestDir,Suite,Case,Opts).
%%%-----------------------------------------------------------------
%%% @spec start_interactive() -> ok
%%%
%%% @doc Start CT in interactive mode.
%%%
%%% <p>From this mode all test case support functions can be executed
%%% directly from the erlang shell. The interactive mode can also be
%%% started from the OS command line with <code>ct_run -shell
%%% [-config File...]</code>.</p>
%%%
%%% <p>If any functions using "required config data" (e.g. telnet or
%%% ftp functions) are to be called from the erlang shell, config data
%%% must first be required with <code>ct:require/2</code>.</p>
%%%
%%% <p>Example:<br/>
%%% <code>> ct:require(unix_telnet, unix).</code><br/>
%%% <code>ok</code><br/>
%%% <code>> ct_telnet:open(unix_telnet).</code><br/>
%%% <code>{ok,<0.105.0>}</code><br/>
%%% <code>> ct_telnet:cmd(unix_telnet, "ls .").</code><br/>
%%% <code>{ok,["ls","file1 ...",...]}</code></p>
start_interactive() ->
ct_util:start(interactive).
%%%-----------------------------------------------------------------
%%% @spec stop_interactive() -> ok
%%%
%%% @doc Exit the interactive mode.
%%% @see start_interactive/0
stop_interactive() ->
ct_util:stop(normal).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% MISC INTERFACE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%-----------------------------------------------------------------
%%% @spec require(Required) -> ok | {error,Reason}
%%% Required = Key | {Key,SubKeys} | {Key,SubKey,SubKeys}
%%% Key = atom()
%%% SubKeys = SubKey | [SubKey]
%%% SubKey = atom()
%%%
%%% @doc Check if the required configuration is available. It is possible
%%% to specify arbitrarily deep tuples as <c>Required</c>. Note that it is
%%% only the last element of the tuple which can be a list of <c>SubKey</c>s.
%%%
%%% <p>Example 1: require the variable <code>myvar</code>:</p>
%%% <pre>ok = ct:require(myvar).</pre>
%%%
%%% <p>In this case the config file must at least contain:</p>
%%% <pre>{myvar,Value}.</pre>
%%%
%%% <p>Example 2: require the key <code>myvar</code> with
%%% subkeys <code>sub1</code> and <code>sub2</code>:</p>
%%% <pre>ok = ct:require({myvar,[sub1,sub2]}).</pre>
%%%
%%% <p>In this case the config file must at least contain:</p>
%%% <pre>{myvar,[{sub1,Value},{sub2,Value}]}.</pre>
%%%
%%% <p>Example 3: require the key <code>myvar</code> with
%%% subkey <code>sub1</code> with <code>subsub1</code>:</p>
%%% <pre>ok = ct:require({myvar,sub1,sub2}).</pre>
%%%
%%% <p>In this case the config file must at least contain:</p>
%%% <pre>{myvar,[{sub1,[{sub2,Value}]}]}.</pre>
%%%
%%% @see require/2
%%% @see get_config/1
%%% @see get_config/2
%%% @see get_config/3
require(Required) ->
ct_config:require(Required).
%%%-----------------------------------------------------------------
%%% @spec require(Name,Required) -> ok | {error,Reason}
%%% Name = atom()
%%% Required = Key | {Key,SubKey} | {Key,SubKey,SubKey}
%%% SubKey = Key
%%% Key = atom()
%%%
%%% @doc Check if the required configuration is available, and give it
%%% a name. The semantics for <c>Required</c> is the same as in
%%% <c>required/1</c> except that it is not possible to specify a list
%%% of <c>SubKey</c>s.
%%%
%%% <p>If the requested data is available, the sub entry will be
%%% associated with <code>Name</code> so that the value of the element
%%% can be read with <code>get_config/1,2</code> provided
%%% <code>Name</code> instead of the whole <code>Required</code> term.</p>
%%%
%%% <p>Example: Require one node with a telnet connection and an
%%% ftp connection. Name the node <code>a</code>:
%%% <pre>ok = ct:require(a,{machine,node}).</pre>
%%% All references to this node may then use the node name.
%%% E.g. you can fetch a file over ftp like this:</p>
%%% <pre>ok = ct:ftp_get(a,RemoteFile,LocalFile).</pre>
%%%
%%% <p>For this to work, the config file must at least contain:</p>
%%% <pre>{machine,[{node,[{telnet,IpAddr},{ftp,IpAddr}]}]}.</pre>
%%%
%%% <note>The behaviour of this function changed radically in common_test
%%% 1.6.2. In order too keep some backwards compatability it is still possible
%%% to do: <br/><c>ct:require(a,{node,[telnet,ftp]}).</c><br/>
%%% This will associate the name <c>a</c> with the top level <c>node</c> entry.
%%% For this to work, the config file must at least contain:<br/>
%%% <c>{node,[{telnet,IpAddr},{ftp,IpAddr}]}.</c></note>
%%%
%%% @see require/1
%%% @see get_config/1
%%% @see get_config/2
%%% @see get_config/3
require(Name,Required) ->
ct_config:require(Name,Required).
%%%-----------------------------------------------------------------
%%% @spec get_config(Required) -> Value
%%% @equiv get_config(Required,undefined,[])
get_config(Required) ->
ct_config:get_config(Required,undefined,[]).
%%%-----------------------------------------------------------------
%%% @spec get_config(Required,Default) -> Value
%%% @equiv get_config(Required,Default,[])
get_config(Required,Default) ->
ct_config:get_config(Required,Default,[]).
%%%-----------------------------------------------------------------
%%% @spec get_config(Required,Default,Opts) -> ValueOrElement
%%% Required = KeyOrName | {KeyOrName,SubKey} | {KeyOrName,SubKey,SubKey}
%%% KeyOrName = atom()
%%% SubKey = atom()
%%% Default = term()
%%% Opts = [Opt] | []
%%% Opt = element | all
%%% ValueOrElement = term() | Default
%%%
%%% @doc Read config data values.
%%%
%%% <p>This function returns the matching value(s) or config element(s),
%%% given a config variable key or its associated name
%%% (if one has been specified with <code>require/2</code> or a
%%% require statement).</p>
%%%
%%% <p>Example, given the following config file:</p>
%%% <pre>
%%% {unix,[{telnet,IpAddr},
%%% {user,[{username,Username},
%%% {password,Password}]}]}.</pre>
%%% <p><code>ct:get_config(unix,Default) ->
%%% [{telnet,IpAddr},
%%% {user, [{username,Username},
%%% {password,Password}]}]</code><br/>
%%% <code>ct:get_config({unix,telnet},Default) -> IpAddr</code><br/>
%%% <code>ct:get_config({unix,user,username},Default) -> Username</code><br/>
%%% <code>ct:get_config({unix,ftp},Default) -> Default</code><br/>
%%% <code>ct:get_config(unknownkey,Default) -> Default</code></p>
%%%
%%% <p>If a config variable key has been associated with a name (by
%%% means of <code>require/2</code> or a require statement), the name
%%% may be used instead of the key to read the value:</p>
%%%
%%% <p><code>ct:require(myuser,{unix,user}) -> ok.</code><br/>
%%% <code>ct:get_config(myuser,Default) ->
%%% [{username,Username},
%%% {password,Password}]</code></p>
%%%
%%% <p>If a config variable is defined in multiple files and you want to
%%% access all possible values, use the <code>all</code> option. The
%%% values will be returned in a list and the order of the elements
%%% corresponds to the order that the config files were specified at
%%% startup.</p>
%%%
%%% <p>If you want config elements (key-value tuples) returned as result
%%% instead of values, use the <code>element</code> option.
%%% The returned elements will then be on the form <code>{Required,Value}</code></p>
%%%
%%% @see get_config/1
%%% @see get_config/2
%%% @see require/1
%%% @see require/2
get_config(Required,Default,Opts) ->
ct_config:get_config(Required,Default,Opts).
%%%-----------------------------------------------------------------
%%% @spec reload_config(Required) -> ValueOrElement
%%% Required = KeyOrName | {KeyOrName,SubKey} | {KeyOrName,SubKey,SubKey}
%%% KeyOrName = atom()
%%% SubKey = atom()
%%% ValueOrElement = term()
%%%
%%% @doc Reload config file which contains specified configuration key.
%%%
%%% <p>This function performs updating of the configuration data from which the
%%% given configuration variable was read, and returns the (possibly) new
%%% value of this variable.</p>
%%% <p>Note that if some variables were present in the configuration but are not loaded
%%% using this function, they will be removed from the configuration table together
%%% with their aliases.</p>
%%%
reload_config(Required)->
ct_config:reload_config(Required).
%%%-----------------------------------------------------------------
%%% @spec log(Format) -> ok
%%% @equiv log(default,Format,[])
log(Format) ->
log(default,Format,[]).
%%%-----------------------------------------------------------------
%%% @spec log(X1,X2) -> ok
%%% X1 = Category | Format
%%% X2 = Format | Args
%%% @equiv log(Category,Format,Args)
log(X1,X2) ->
{Category,Format,Args} =
if is_atom(X1) -> {X1,X2,[]};
is_list(X1) -> {default,X1,X2}
end,
log(Category,Format,Args).
%%%-----------------------------------------------------------------
%%% @spec log(Category,Format,Args) -> ok
%%% Category = atom()
%%% Format = string()
%%% Args = list()
%%%
%%% @doc Printout from a test case to the log file.
%%%
%%% <p>This function is meant for printing a string directly from a
%%% test case to the test case log file.</p>
%%%
%%% <p>Default <code>Category</code> is <code>default</code> and
%%% default <code>Args</code> is <code>[]</code>.</p>
log(Category,Format,Args) ->
ct_logs:tc_log(Category,Format,Args).
%%%-----------------------------------------------------------------
%%% @spec print(Format) -> ok
%%% @equiv print(default,Format,[])
print(Format) ->
print(default,Format,[]).
%%%-----------------------------------------------------------------
%%% @equiv print(Category,Format,Args)
print(X1,X2) ->
{Category,Format,Args} =
if is_atom(X1) -> {X1,X2,[]};
is_list(X1) -> {default,X1,X2}
end,
print(Category,Format,Args).
%%%-----------------------------------------------------------------
%%% @spec print(Category,Format,Args) -> ok
%%% Category = atom()
%%% Format = string()
%%% Args = list()
%%%
%%% @doc Printout from a test case to the console.
%%%
%%% <p>This function is meant for printing a string from a test case
%%% to the console.</p>
%%%
%%% <p>Default <code>Category</code> is <code>default</code> and
%%% default <code>Args</code> is <code>[]</code>.</p>
print(Category,Format,Args) ->
ct_logs:tc_print(Category,Format,Args).
%%%-----------------------------------------------------------------
%%% @spec pal(Format) -> ok
%%% @equiv pal(default,Format,[])
pal(Format) ->
pal(default,Format,[]).
%%%-----------------------------------------------------------------
%%% @spec pal(X1,X2) -> ok
%%% X1 = Category | Format
%%% X2 = Format | Args
%%% @equiv pal(Category,Format,Args)
pal(X1,X2) ->
{Category,Format,Args} =
if is_atom(X1) -> {X1,X2,[]};
is_list(X1) -> {default,X1,X2}
end,
pal(Category,Format,Args).
%%%-----------------------------------------------------------------
%%% @spec pal(Category,Format,Args) -> ok
%%% Category = atom()
%%% Format = string()
%%% Args = list()
%%%
%%% @doc Print and log from a test case.
%%%
%%% <p>This function is meant for printing a string from a test case,
%%% both to the test case log file and to the console.</p>
%%%
%%% <p>Default <code>Category</code> is <code>default</code> and
%%% default <code>Args</code> is <code>[]</code>.</p>
pal(Category,Format,Args) ->
ct_logs:tc_pal(Category,Format,Args).
%%%-----------------------------------------------------------------
%%% @spec capture_start() -> ok
%%%
%%% @doc Start capturing all text strings printed to stdout during
%%% execution of the test case.
%%%
%%% @see capture_stop/0
%%% @see capture_get/1
capture_start() ->
test_server:capture_start().
%%%-----------------------------------------------------------------
%%% @spec capture_stop() -> ok
%%%
%%% @doc Stop capturing text strings (a session started with
%%% <code>capture_start/0</code>).
%%%
%%% @see capture_start/0
%%% @see capture_get/1
capture_stop() ->
test_server:capture_stop().
%%%-----------------------------------------------------------------
%%% @spec capture_get() -> ListOfStrings
%%% ListOfStrings = [string()]
%%%
%%% @equiv capture_get([default])
capture_get() ->
%% remove default log printouts (e.g. ct:log/2 printouts)
capture_get([default]).
%%%-----------------------------------------------------------------
%%% @spec capture_get(ExclCategories) -> ListOfStrings
%%% ExclCategories = [atom()]
%%% ListOfStrings = [string()]
%%%
%%% @doc Return and purge the list of text strings buffered
%%% during the latest session of capturing printouts to stdout.
%%% With <code>ExclCategories</code> it's possible to specify
%%% log categories that should be ignored in <code>ListOfStrings</code>.
%%% If <code>ExclCategories = []</code>, no filtering takes place.
%%%
%%% @see capture_start/0
%%% @see capture_stop/0
%%% @see log/3
capture_get([ExclCat | ExclCategories]) ->
Strs = test_server:capture_get(),
CatsStr = [atom_to_list(ExclCat) |
[[$| | atom_to_list(EC)] || EC <- ExclCategories]],
{ok,MP} = re:compile("<div class=\"(" ++ lists:flatten(CatsStr) ++ ")\">.*"),
lists:flatmap(fun(Str) ->
case re:run(Str, MP) of
{match,_} -> [];
nomatch -> [Str]
end
end, Strs);
capture_get([]) ->
test_server:capture_get().
%%%-----------------------------------------------------------------
%%% @spec fail(Reason) -> void()
%%% Reason = term()
%%%
%%% @doc Terminate a test case with the given error
%%% <code>Reason</code>.
fail(Reason) ->
try
exit({test_case_failed,Reason})
catch
Class:R ->
case erlang:get_stacktrace() of
[{?MODULE,fail,1,_}|Stk] -> ok;
Stk -> ok
end,
erlang:raise(Class, R, Stk)
end.
%%%-----------------------------------------------------------------
%%% @spec fail(Format, Args) -> void()
%%% Format = string()
%%% Args = list()
%%%
%%% @doc Terminate a test case with an error message specified
%%% by a format string and a list of values (used as arguments to
%%% <code>io_lib:format/2</code>).
fail(Format, Args) ->
try io_lib:format(Format, Args) of
Str ->
try
exit({test_case_failed,lists:flatten(Str)})
catch
Class:R ->
case erlang:get_stacktrace() of
[{?MODULE,fail,2,_}|Stk] -> ok;
Stk -> ok
end,
erlang:raise(Class, R, Stk)
end
catch
_:BadArgs ->
exit({BadArgs,{?MODULE,fail,[Format,Args]}})
end.
%%%-----------------------------------------------------------------
%%% @spec comment(Comment) -> void()
%%% Comment = term()
%%%
%%% @doc Print the given <code>Comment</code> in the comment field in
%%% the table on the test suite result page.
%%%
%%% <p>If called several times, only the last comment is printed.
%%% The test case return value <code>{comment,Comment}</code>
%%% overwrites the string set by this function.</p>
comment(Comment) when is_list(Comment) ->
Formatted =
case (catch io_lib:format("~s",[Comment])) of
{'EXIT',_} -> % it's a list not a string
io_lib:format("~p",[Comment]);
String ->
String
end,
send_html_comment(lists:flatten(Formatted));
comment(Comment) ->
Formatted = io_lib:format("~p",[Comment]),
send_html_comment(lists:flatten(Formatted)).
%%%-----------------------------------------------------------------
%%% @spec comment(Format, Args) -> void()
%%% Format = string()
%%% Args = list()
%%%
%%% @doc Print the formatted string in the comment field in
%%% the table on the test suite result page.
%%%
%%% <p>The <code>Format</code> and <code>Args</code> arguments are
%%% used in call to <code>io_lib:format/2</code> in order to create
%%% the comment string. The behaviour of <code>comment/2</code> is
%%% otherwise the same as the <code>comment/1</code> function (see
%%% above for details).</p>
comment(Format, Args) when is_list(Format), is_list(Args) ->
Formatted =
case (catch io_lib:format(Format, Args)) of
{'EXIT',Reason} -> % bad args
exit({Reason,{?MODULE,comment,[Format,Args]}});
String ->
lists:flatten(String)
end,
send_html_comment(Formatted).
send_html_comment(Comment) ->
Html = "<font color=\"green\">" ++ Comment ++ "</font>",
ct_util:set_testdata({comment,Html}),
test_server:comment(Html).
%%%-----------------------------------------------------------------
%%% @spec make_priv_dir() -> ok | {error,Reason}
%%% Reason = term()
%%% @doc If the test has been started with the create_priv_dir
%%% option set to manual_per_tc, in order for the test case to use
%%% the private directory, it must first create it by calling
%%% this function.
make_priv_dir() ->
test_server:make_priv_dir().
%%%-----------------------------------------------------------------
%%% @spec get_target_name(Handle) -> {ok,TargetName} | {error,Reason}
%%% Handle = handle()
%%% TargetName = target_name()
%%% @doc Return the name of the target that the given connection
%%% belongs to.
get_target_name(Handle) ->
ct_util:get_target_name(Handle).
%%%-----------------------------------------------------------------
%%% @spec parse_table(Data) -> {Heading,Table}
%%% Data = [string()]
%%% Heading = tuple()
%%% Table = [tuple()]
%%%
%%% @doc Parse the printout from an SQL table and return a list of tuples.
%%%
%%% <p>The printout to parse would typically be the result of a
%%% <code>select</code> command in SQL. The returned
%%% <code>Table</code> is a list of tuples, where each tuple is a row
%%% in the table.</p>
%%%
%%% <p><code>Heading</code> is a tuple of strings representing the
%%% headings of each column in the table.</p>
parse_table(Data) ->
ct_util:parse_table(Data).
%%%-----------------------------------------------------------------
%%% @spec listenv(Telnet) -> [Env]
%%% Telnet = term()
%%% Env = {Key,Value}
%%% Key = string()
%%% Value = string()
%%%
%%% @doc Performs the listenv command on the given telnet connection
%%% and returns the result as a list of Key-Value pairs.
listenv(Telnet) ->
ct_util:listenv(Telnet).
%%%-----------------------------------------------------------------
%%% @spec testcases(TestDir, Suite) -> Testcases | {error,Reason}
%%% TestDir = string()
%%% Suite = atom()
%%% Testcases = list()
%%% Reason = term()
%%%
%%% @doc Returns all test cases in the specified suite.
testcases(TestDir, Suite) ->
case make_and_load(TestDir, Suite) of
E = {error,_} ->
E;
_ ->
case (catch Suite:all()) of
{'EXIT',Reason} ->
{error,Reason};
TCs ->
TCs
end
end.
make_and_load(Dir, Suite) ->
EnvInclude =
case os:getenv("CT_INCLUDE_PATH") of
false -> [];
CtInclPath -> string:tokens(CtInclPath, [$:,$ ,$,])
end,
StartInclude =
case init:get_argument(include) of
{ok,[Dirs]} -> Dirs;
_ -> []
end,
UserInclude = EnvInclude ++ StartInclude,
case ct_run:run_make(Dir, Suite, UserInclude) of
MErr = {error,_} ->
MErr;
_ ->
TestDir = ct_util:get_testdir(Dir, Suite),
File = filename:join(TestDir, atom_to_list(Suite)),
case code:soft_purge(Suite) of
true ->
code:load_abs(File);
false -> % will use loaded
{module,Suite}
end
end.
%%%-----------------------------------------------------------------
%%% @spec userdata(TestDir, Suite) -> SuiteUserData | {error,Reason}
%%% TestDir = string()
%%% Suite = atom()
%%% SuiteUserData = [term()]
%%% Reason = term()
%%%
%%% @doc Returns any data specified with the tag <code>userdata</code>
%%% in the list of tuples returned from <code>Suite:suite/0</code>.
userdata(TestDir, Suite) ->
case make_and_load(TestDir, Suite) of
E = {error,_} ->
E;
_ ->
Info = (catch Suite:suite()),
get_userdata(Info, "suite/0")
end.
get_userdata({'EXIT',{Undef,_}}, Spec) when Undef == undef;
Undef == function_clause ->
{error,list_to_atom(Spec ++ " is not defined")};
get_userdata({'EXIT',Reason}, Spec) ->
{error,{list_to_atom("error in " ++ Spec),Reason}};
get_userdata(List, _) when is_list(List) ->
Fun = fun({userdata,Data}, Acc) -> [Data | Acc];
(_, Acc) -> Acc
end,
case lists:foldl(Fun, [], List) of
Terms ->
lists:flatten(lists:reverse(Terms))
end;
get_userdata(_BadTerm, Spec) ->
{error,list_to_atom(Spec ++ " must return a list")}.
%%%-----------------------------------------------------------------
%%% @spec userdata(TestDir, Suite, GroupOrCase) -> TCUserData | {error,Reason}
%%% TestDir = string()
%%% Suite = atom()
%%% GroupOrCase = {group,GroupName} | atom()
%%% GroupName = atom()
%%% TCUserData = [term()]
%%% Reason = term()
%%%
%%% @doc Returns any data specified with the tag <code>userdata</code>
%%% in the list of tuples returned from <code>Suite:group(GroupName)</code>
%%% or <code>Suite:Case()</code>.
userdata(TestDir, Suite, {group,GroupName}) ->
case make_and_load(TestDir, Suite) of
E = {error,_} ->
E;
_ ->
Info = (catch apply(Suite, group, [GroupName])),
get_userdata(Info, "group("++atom_to_list(GroupName)++")")
end;
userdata(TestDir, Suite, Case) when is_atom(Case) ->
case make_and_load(TestDir, Suite) of
E = {error,_} ->
E;
_ ->
Info = (catch apply(Suite, Case, [])),
get_userdata(Info, atom_to_list(Case)++"/0")
end.
%%%-----------------------------------------------------------------
%%% @spec get_status() -> TestStatus | {error,Reason} | no_tests_running
%%% TestStatus = [StatusElem]
%%% StatusElem = {current,{Suite,TestCase}} | {successful,Successful} |
%%% {failed,Failed} | {skipped,Skipped} | {total,Total}
%%% Suite = atom()
%%% TestCase = atom()
%%% Successful = integer()
%%% Failed = integer()
%%% Skipped = {UserSkipped,AutoSkipped}
%%% UserSkipped = integer()
%%% AutoSkipped = integer()
%%% Total = integer()
%%% Reason = term()
%%%
%%% @doc Returns status of ongoing test. The returned list contains info about
%%% which test case is currently executing, as well as counters for
%%% successful, failed, skipped, and total test cases so far.
get_status() ->
case get_testdata(curr_tc) of
{ok,TestCase} ->
case get_testdata(stats) of
{ok,{Ok,Failed,Skipped={UserSkipped,AutoSkipped}}} ->
[{current,TestCase},
{successful,Ok},
{failed,Failed},
{skipped,Skipped},
{total,Ok+Failed+UserSkipped+AutoSkipped}];
Err1 -> Err1
end;
Err2 -> Err2
end.
get_testdata(Key) ->
case catch ct_util:get_testdata(Key) of
{error,ct_util_server_not_running} ->
no_tests_running;
Error = {error,_Reason} ->
Error;
{'EXIT',_Reason} ->
no_tests_running;
Data ->
{ok,Data}
end.
%%%-----------------------------------------------------------------
%%% @spec abort_current_testcase(Reason) -> ok | {error,no_testcase_running}
%%% Reason = term()
%%%
%%% @doc <p>When calling this function, the currently executing test case will be aborted.
%%% It is the user's responsibility to know for sure which test case is currently
%%% executing. The function is therefore only safe to call from a function which
%%% has been called (or synchronously invoked) by the test case.</p>
%%%
%%% <p><code>Reason</code>, the reason for aborting the test case, is printed
%%% in the test case log.</p>
abort_current_testcase(Reason) ->
test_server_ctrl:abort_current_testcase(Reason).
%%%-----------------------------------------------------------------
%%% @spec encrypt_config_file(SrcFileName, EncryptFileName) ->
%%% ok | {error,Reason}
%%% SrcFileName = string()
%%% EncryptFileName = string()
%%% Reason = term()
%%%
%%% @doc <p>This function encrypts the source config file with DES3 and
%%% saves the result in file <code>EncryptFileName</code>. The key,
%%% a string, must be available in a text file named
%%% <code>.ct_config.crypt</code> in the current directory, or the
%%% home directory of the user (it is searched for in that order).</p>
%%% <p>See the Common Test User's Guide for information about using
%%% encrypted config files when running tests.</p>
%%% <p>See the <code>crypto</code> application for details on DES3
%%% encryption/decryption.</p>
encrypt_config_file(SrcFileName, EncryptFileName) ->
ct_config:encrypt_config_file(SrcFileName, EncryptFileName).
%%%-----------------------------------------------------------------
%%% @spec encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile) ->
%%% ok | {error,Reason}
%%% SrcFileName = string()
%%% EncryptFileName = string()
%%% KeyOrFile = {key,string()} | {file,string()}
%%% Reason = term()
%%%
%%% @doc <p>This function encrypts the source config file with DES3 and
%%% saves the result in the target file <code>EncryptFileName</code>.
%%% The encryption key to use is either the value in
%%% <code>{key,Key}</code> or the value stored in the file specified
%%% by <code>{file,File}</code>.</p>
%%% <p>See the Common Test User's Guide for information about using
%%% encrypted config files when running tests.</p>
%%% <p>See the <code>crypto</code> application for details on DES3
%%% encryption/decryption.</p>
encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile) ->
ct_config:encrypt_config_file(SrcFileName, EncryptFileName, KeyOrFile).
%%%-----------------------------------------------------------------
%%% @spec decrypt_config_file(EncryptFileName, TargetFileName) ->
%%% ok | {error,Reason}
%%% EncryptFileName = string()
%%% TargetFileName = string()
%%% Reason = term()
%%%
%%% @doc <p>This function decrypts <code>EncryptFileName</code>, previously
%%% generated with <code>encrypt_config_file/2/3</code>. The original
%%% file contents is saved in the target file. The encryption key, a
%%% string, must be available in a text file named
%%% <code>.ct_config.crypt</code> in the current directory, or the
%%% home directory of the user (it is searched for in that order).</p>
decrypt_config_file(EncryptFileName, TargetFileName) ->
ct_config:decrypt_config_file(EncryptFileName, TargetFileName).
%%%-----------------------------------------------------------------
%%% @spec decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile) ->
%%% ok | {error,Reason}
%%% EncryptFileName = string()
%%% TargetFileName = string()
%%% KeyOrFile = {key,string()} | {file,string()}
%%% Reason = term()
%%%
%%% @doc <p>This function decrypts <code>EncryptFileName</code>, previously
%%% generated with <code>encrypt_config_file/2/3</code>. The original
%%% file contents is saved in the target file. The key must have the
%%% the same value as that used for encryption.</p>
decrypt_config_file(EncryptFileName, TargetFileName, KeyOrFile) ->
ct_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 | Func
%%% Hours = integer()
%%% Mins = integer()
%%% Secs = integer()
%%% Millisecs = integer() | float()
%%% Func = {M,F,A} | fun()
%%% M = atom()
%%% F = atom()
%%% A = list()
%%%
%%% @doc <p>Use this function to set a new timetrap for the running test case.
%%% If the argument is <code>Func</code>, the timetrap will be triggered
%%% when this function returns. <code>Func</code> may also return a new
%%% <code>Time</code> value, which in that case will be the value for the
%%% new timetrap.</p>
timetrap(Time) ->
test_server:timetrap_cancel(),
test_server:timetrap(Time).
%%%-----------------------------------------------------------------
%%% @spec get_timetrap_info() -> {Time,Scale}
%%% Time = integer() | infinity
%%% Scale = true | false
%%%
%%% @doc <p>Read info about the timetrap set for the current test case.
%%% <c>Scale</c> indicates if Common Test will attempt to automatically
%%% compensate timetraps for runtime delays introduced by e.g. tools like
%%% cover.</p>
get_timetrap_info() ->
test_server:get_timetrap_info().
%%%-----------------------------------------------------------------
%%% @spec sleep(Time) -> ok
%%% Time = {hours,Hours} | {minutes,Mins} | {seconds,Secs} | Millisecs | infinity
%%% Hours = integer()
%%% Mins = integer()
%%% Secs = integer()
%%% Millisecs = integer() | float()
%%%
%%% @doc <p>This function, similar to <c>timer:sleep/1</c>, suspends the test
%%% case for specified time. However, this function also multiplies
%%% <c>Time</c> with the 'multiply_timetraps' value (if set) and under
%%% certain circumstances also scales up the time automatically
%%% if 'scale_timetraps' is set to true (default is false).</p>
sleep({hours,Hs}) ->
sleep(trunc(Hs * 1000 * 60 * 60));
sleep({minutes,Ms}) ->
sleep(trunc(Ms * 1000 * 60));
sleep({seconds,Ss}) ->
sleep(trunc(Ss * 1000));
sleep(Time) ->
test_server:adjusted_sleep(Time).
%%%-----------------------------------------------------------------
%%% @spec notify(Name,Data) -> ok
%%% Name = atom()
%%% Data = term()
%%%
%%% @doc <p>Sends a asynchronous notification of type <c>Name</c> with
%%% <c>Data</c>to the common_test event manager. This can later be
%%% caught by any installed event manager. </p>
%%% @see //stdlib/gen_event
notify(Name,Data) ->
ct_event:notify(Name, Data).
%%%-----------------------------------------------------------------
%%% @spec sync_notify(Name,Data) -> ok
%%% Name = atom()
%%% Data = term()
%%%
%%% @doc <p>Sends a synchronous notification of type <c>Name</c> with
%%% <c>Data</c>to the common_test event manager. This can later be
%%% caught by any installed event manager. </p>
%%% @see //stdlib/gen_event
sync_notify(Name,Data) ->
ct_event:sync_notify(Name, Data).