aboutsummaryrefslogtreecommitdiffstats
path: root/lib/common_test/src
diff options
context:
space:
mode:
authorLukas Larsson <[email protected]>2011-10-04 10:16:09 +0200
committerLukas Larsson <[email protected]>2011-10-04 10:16:09 +0200
commit2d25f96f904bfa33c4bba4f3026f9fd2117ed8ab (patch)
tree26acbda51fa2ac4dd5f809e400d05aa1021eba20 /lib/common_test/src
parent622daf3b96666f8bbabec44a15b26a188a83b95e (diff)
parent1a42cde6d62a8ec0f9e22215d3c3ec655201c78f (diff)
downloadotp-2d25f96f904bfa33c4bba4f3026f9fd2117ed8ab.tar.gz
otp-2d25f96f904bfa33c4bba4f3026f9fd2117ed8ab.tar.bz2
otp-2d25f96f904bfa33c4bba4f3026f9fd2117ed8ab.zip
Merge branch 'dev' into major
* dev: (38 commits) Update documentation Rid ct_telnet of doc build warnings Create temporary fix for problem with parallel test cases Update primary bootstrap Correct "Missing Suites" link Add documentation on timetraps and start flags Add missing tests for timetrap handling and fix remaining errors Solve problem with ct_init/end_per_group being counted as test cases Fix errors in test suites Fix invalid call to undefined function Fix problem with test_server_ctrl creating invalid conf test Improve info in CT framework log Update vsn.mk for common_test and test_server Enhance logging performance Change order of include files Add link to last executed test suite on index page Fix problem with location value when init config func calls help func Fix crash when CTHook init fails Correct error in test suite Fix error with incorrect notification after end_per_testcase craches ... Conflicts: bootstrap/bin/start.boot bootstrap/bin/start_clean.boot bootstrap/lib/compiler/ebin/beam_asm.beam bootstrap/lib/compiler/ebin/beam_disasm.beam bootstrap/lib/compiler/ebin/compile.beam bootstrap/lib/compiler/ebin/sys_pre_expand.beam bootstrap/lib/kernel/ebin/code.beam bootstrap/lib/kernel/ebin/code_server.beam bootstrap/lib/kernel/ebin/hipe_unified_loader.beam bootstrap/lib/kernel/ebin/inet.beam bootstrap/lib/kernel/ebin/inet_config.beam bootstrap/lib/kernel/ebin/inet_dns.beam bootstrap/lib/stdlib/ebin/beam_lib.beam bootstrap/lib/stdlib/ebin/dets.beam bootstrap/lib/stdlib/ebin/erl_compile.beam bootstrap/lib/stdlib/ebin/erl_internal.beam bootstrap/lib/stdlib/ebin/erl_scan.beam bootstrap/lib/stdlib/ebin/erl_tar.beam bootstrap/lib/stdlib/ebin/io_lib_fread.beam bootstrap/lib/stdlib/ebin/otp_internal.beam bootstrap/lib/stdlib/ebin/sofs.beam bootstrap/lib/stdlib/ebin/supervisor.beam bootstrap/lib/stdlib/ebin/zip.beam lib/common_test/src/ct.erl lib/common_test/src/ct_run.erl lib/common_test/test/ct_error_SUITE.erl lib/common_test/test/ct_repeat_1_SUITE.erl lib/common_test/test/ct_skip_SUITE.erl lib/test_server/src/test_server.erl
Diffstat (limited to 'lib/common_test/src')
-rw-r--r--lib/common_test/src/ct.erl6
-rw-r--r--lib/common_test/src/ct_config.erl40
-rw-r--r--lib/common_test/src/ct_config_plain.erl24
-rw-r--r--lib/common_test/src/ct_config_xml.erl48
-rw-r--r--lib/common_test/src/ct_framework.erl117
-rw-r--r--lib/common_test/src/ct_logs.erl57
-rw-r--r--lib/common_test/src/ct_make.erl2
-rw-r--r--lib/common_test/src/ct_run.erl494
-rw-r--r--lib/common_test/src/ct_telnet.erl9
-rw-r--r--lib/common_test/src/ct_testspec.erl30
-rw-r--r--lib/common_test/src/ct_util.erl137
-rw-r--r--lib/common_test/src/ct_util.hrl4
-rw-r--r--lib/common_test/src/vts.erl133
13 files changed, 788 insertions, 313 deletions
diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl
index 3a96190256..69e15fa246 100644
--- a/lib/common_test/src/ct.erl
+++ b/lib/common_test/src/ct.erl
@@ -148,10 +148,10 @@ run(TestDirs) ->
%%% {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} |
+%%% {refresh_logs,LogDir} | {logopts,LogOpts} | {basic_html,Bool} |
%%% {ct_hooks, CTHs} | {enable_builtin_hooks,Bool}
%%% TestDirs = [string()] | string()
-%%% Suites = [string()] | string()
+%%% Suites = [string()] | [atom()] | string() | atom()
%%% Cases = [atom()] | atom()
%%% Groups = [atom()] | atom()
%%% TestSpecs = [string()] | string()
@@ -177,6 +177,8 @@ run(TestDirs) ->
%%% DecryptKeyOrFile = {key,DecryptKey} | {file,DecryptFile}
%%% DecryptKey = string()
%%% DecryptFile = string()
+%%% LogOpts = [LogOpt]
+%%% LogOpt = no_nl | no_src
%%% CTHs = [CTHModule | {CTHModule, CTHInitArgs}]
%%% CTHModule = atom()
%%% CTHInitArgs = term()
diff --git a/lib/common_test/src/ct_config.erl b/lib/common_test/src/ct_config.erl
index 6b75937668..fc51aea7f3 100644
--- a/lib/common_test/src/ct_config.erl
+++ b/lib/common_test/src/ct_config.erl
@@ -204,9 +204,9 @@ get_config_file_list(Opts) ->
DefaultConfigs = process_default_configs(Opts),
CfgFiles =
if
- DefaultConfigs == []->
+ DefaultConfigs == [] ->
[];
- true->
+ true ->
[{?ct_config_txt, DefaultConfigs}]
end ++
process_user_configs(Opts, []),
@@ -240,12 +240,12 @@ read_config_files(Opts) ->
end,
ConfigFiles = case lists:keyfind(config, 1, Opts) of
- {config,ConfigLists}->
+ {config,ConfigLists} ->
lists:foldr(fun({Callback,Files}, Acc) ->
AddCallback(Callback,Files)
++ Acc
end,[],ConfigLists);
- false->
+ false ->
[]
end,
read_config_files_int(ConfigFiles, fun store_config/3).
@@ -255,7 +255,9 @@ read_config_files_int([{Callback, File}|Files], FunToSave) ->
{ok, Config} ->
FunToSave(Config, Callback, File),
read_config_files_int(Files, FunToSave);
- {error, ErrorName, ErrorDetail}->
+ {error, {ErrorName, ErrorDetail}} ->
+ {user_error, {ErrorName, File, ErrorDetail}};
+ {error, ErrorName, ErrorDetail} ->
{user_error, {ErrorName, File, ErrorDetail}}
end;
read_config_files_int([], _FunToSave) ->
@@ -283,7 +285,7 @@ rewrite_config(Config, Callback, File) ->
config=File,_='_'}),
Updater = fun({Key, Value}) ->
case keyfindall(Key, #ct_conf.key, OldRows) of
- []->
+ [] ->
ets:insert(?attr_table,
#ct_conf{key=Key,
value=Value,
@@ -453,9 +455,9 @@ update_conf(Name, NewConfig) ->
reload_conf(KeyOrName) ->
case lookup_handler_for_config(KeyOrName) of
- []->
+ [] ->
undefined;
- HandlerList->
+ HandlerList ->
HandlerList2 = lists:usort(HandlerList),
read_config_files_int(HandlerList2, fun rewrite_config/3),
get_config(KeyOrName)
@@ -711,13 +713,13 @@ 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}->
+ {file, _Filename} ->
check_exports(Callback);
- false->
+ false ->
case code:load_file(Callback) of
- {module, Callback}->
+ {module, Callback} ->
check_exports(Callback);
- {error, Error}->
+ {error, Error} ->
{error, Error}
end
end.
@@ -745,14 +747,14 @@ check_config_files(Configs) ->
end,
Files)
end;
- {error, Why}->
+ {error, Why} ->
{error, {callback, {Callback,Why}}}
end;
({Callback, []}) ->
case check_callback_load(Callback) of
- {ok, Callback}->
+ {ok, Callback} ->
Callback:check_parameter([]);
- {error, Why}->
+ {error, Why} ->
{error, {callback, {Callback,Why}}}
end
end,
@@ -773,15 +775,15 @@ prepare_user_configs([], Acc, _) ->
prepare_config_list(Args) ->
ConfigFiles = case lists:keysearch(ct_config, 1, Args) of
- {value,{ct_config,Files}}->
+ {value,{ct_config,Files}} ->
[{?ct_config_txt,[filename:absname(F) || F <- Files]}];
- false->
+ false ->
[]
end,
UserConfigs = case lists:keysearch(userconfig, 1, Args) of
- {value,{userconfig,UserConfigFiles}}->
+ {value,{userconfig,UserConfigFiles}} ->
prepare_user_configs(UserConfigFiles, [], new);
- false->
+ false ->
[]
end,
ConfigFiles ++ UserConfigs.
diff --git a/lib/common_test/src/ct_config_plain.erl b/lib/common_test/src/ct_config_plain.erl
index 3fbc8af9fb..6698332379 100644
--- a/lib/common_test/src/ct_config_plain.erl
+++ b/lib/common_test/src/ct_config_plain.erl
@@ -29,7 +29,7 @@ read_config(ConfigFile) ->
{ok,Config} ->
{ok, Config};
{error,enoent} ->
- {error, config_file_error, enoent};
+ {error,{config_file_error,file:format_error(enoent)}};
{error,Reason} ->
Key =
case application:get_env(common_test, decrypt) of
@@ -45,23 +45,27 @@ read_config(ConfigFile) ->
end,
case Key of
{error,no_crypt_file} ->
- {error, config_file_error, Reason};
+ {error,{config_file_error,
+ lists:flatten(
+ io_lib:format("~s",[file:format_error(Reason)]))}};
{error,CryptError} ->
- {error, decrypt_file_error, CryptError};
+ {error,{decrypt_file_error,CryptError}};
_ when is_list(Key) ->
- case ct_config:decrypt_config_file(ConfigFile, undefined, {key,Key}) of
+ 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};
+ {error,{config_file_error,ReadFail}};
Config ->
- {ok, Config}
+ {ok,Config}
end;
{error,DecryptFail} ->
- {error, decrypt_config_error, DecryptFail}
+ {error,{decrypt_config_error,DecryptFail}}
end;
_ ->
- {error, bad_decrypt_key, Key}
+ {error,{bad_decrypt_key,Key}}
end
end.
@@ -69,9 +73,9 @@ read_config(ConfigFile) ->
check_parameter(File)->
case filelib:is_file(File) of
true->
- {ok, {file, File}};
+ {ok,{file,File}};
false->
- {error, {nofile, File}}
+ {error,{nofile,File}}
end.
read_config_terms(Bin) when is_binary(Bin) ->
diff --git a/lib/common_test/src/ct_config_xml.erl b/lib/common_test/src/ct_config_xml.erl
index 8a6e75e635..794174e663 100644
--- a/lib/common_test/src/ct_config_xml.erl
+++ b/lib/common_test/src/ct_config_xml.erl
@@ -27,30 +27,30 @@
% read config file
read_config(ConfigFile) ->
case catch do_read_xml_config(ConfigFile) of
- {ok, Config}->
- {ok, Config};
- {error, Error, ErroneousString}->
- {error, Error, ErroneousString}
+ {ok,Config} ->
+ {ok,Config};
+ Error = {error,_} ->
+ Error
end.
% check file exists
-check_parameter(File)->
+check_parameter(File) ->
case filelib:is_file(File) of
- true->
- {ok, {file, File}};
- false->
- {error, {nofile, File}}
+ true ->
+ {ok,{file,File}};
+ false ->
+ {error,{nofile,File}}
end.
% actual reading of the config
-do_read_xml_config(ConfigFile)->
+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}
+ [{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
@@ -92,18 +92,18 @@ tag(_El, State) ->
State.
% transform of the ugly deeply nested entity list to the key-value "tree"
-transform_entity_list(EntityList)->
+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)->
+ is_tuple(Value) ->
{list_to_atom(Tag), transform_entity_list(lists:reverse([Value|Rest]))};
-transform_entity({Tag, String})->
+transform_entity({Tag, String}) ->
case list_to_term(String) of
- {ok, Value}->
+ {ok, Value} ->
{list_to_atom(Tag), Value};
- Error->
+ Error ->
throw(Error)
end.
@@ -111,8 +111,8 @@ transform_entity({Tag, String})->
list_to_term(String) ->
{ok, T, _} = erl_scan:string(String++"."),
case catch erl_parse:parse_term(T) of
- {ok, Term} ->
- {ok, Term};
+ {ok,Term} ->
+ {ok,Term};
Error ->
- {error, Error, String}
+ {error,{Error,String}}
end.
diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl
index 2ebc6c311a..482c5242ce 100644
--- a/lib/common_test/src/ct_framework.erl
+++ b/lib/common_test/src/ct_framework.erl
@@ -24,10 +24,10 @@
-module(ct_framework).
--export([init_tc/3, end_tc/3, end_tc/4, get_suite/2, report/2, warn/1]).
--export([error_notification/4]).
+-export([init_tc/3, end_tc/3, end_tc/4, get_suite/2, get_all_cases/1]).
+-export([report/2, warn/1, error_notification/4]).
--export([overview_html_header/1]).
+-export([get_logopts/0, format_comment/1, overview_html_header/1]).
-export([error_in_suite/1, ct_init_per_group/2, ct_end_per_group/2]).
@@ -116,7 +116,7 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) ->
Config = lists:keydelete(watchdog,1,Config1),
if Func /= init_per_suite, DoInit /= true ->
ok;
- true ->
+ true ->
%% delete all default values used in previous suite
ct_config:delete_default_config(suite),
%% release all name -> key bindings (once per suite)
@@ -133,7 +133,7 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) ->
ct_config:delete_default_config(testcase),
case add_defaults(Mod,Func,TestCaseInfo,DoInit) of
Error = {suite0_failed,_} ->
- ct_logs:init_tc(),
+ ct_logs:init_tc(false),
FuncSpec = group_or_func(Func,Config0),
ct_event:notify(#event{name=tc_start,
node=node(),
@@ -143,7 +143,7 @@ init_tc1(Mod,Func,[Config0],DoInit) when is_list(Config0) ->
{SuiteInfo,MergeResult} ->
case MergeResult of
{error,Reason} when DoInit == false ->
- ct_logs:init_tc(),
+ ct_logs:init_tc(false),
FuncSpec = group_or_func(Func,Config0),
ct_event:notify(#event{name=tc_start,
node=node(),
@@ -194,19 +194,24 @@ init_tc2(Mod,Func,SuiteInfo,MergeResult,Config,DoInit) ->
Conns ->
ct_util:silence_connections(Conns)
end,
-
- ct_logs:init_tc(),
+ if Func /= init_per_suite, DoInit /= true ->
+ ct_logs:init_tc(false);
+ true ->
+ ct_logs:init_tc(true)
+ end,
FuncSpec = group_or_func(Func,Config),
ct_event:notify(#event{name=tc_start,
node=node(),
data={Mod,FuncSpec}}),
- case configure(MergedInfo1,MergedInfo1,SuiteInfo,{Func,DoInit},Config) of
+ case catch configure(MergedInfo1,MergedInfo1,SuiteInfo,{Func,DoInit},Config) of
{suite0_failed,Reason} ->
ct_util:set_testdata({curr_tc,{Mod,{suite0_failed,{require,Reason}}}}),
{skip,{require_failed_in_suite0,Reason}};
{error,Reason} ->
{auto_skip,{require_failed,Reason}};
+ {'EXIT',Reason} ->
+ {auto_skip,Reason};
{ok, FinalConfig} ->
case MergeResult of
{error,Reason} ->
@@ -245,7 +250,12 @@ add_defaults(Mod,Func,FuncInfo,DoInit) ->
Error = {error,_} -> {SuiteInfo,Error};
MergedInfo -> {SuiteInfo,MergedInfo}
end;
- {'EXIT',Reason} ->
+ {'EXIT',Reason} ->
+ ErrStr = io_lib:format("~n*** ERROR *** "
+ "~w:suite/0 failed: ~p~n",
+ [Mod,Reason]),
+ io:format(ErrStr, []),
+ io:format(user, ErrStr, []),
{suite0_failed,{exited,Reason}};
SuiteInfo when is_list(SuiteInfo) ->
case lists:all(fun(E) when is_tuple(E) -> true;
@@ -261,9 +271,19 @@ add_defaults(Mod,Func,FuncInfo,DoInit) ->
MergedInfo -> {SuiteInfo1,MergedInfo}
end;
false ->
+ ErrStr = io_lib:format("~n*** ERROR *** "
+ "Invalid return value from "
+ "~w:suite/0: ~p~n", [Mod,SuiteInfo]),
+ io:format(ErrStr, []),
+ io:format(user, ErrStr, []),
{suite0_failed,bad_return_value}
end;
- _ ->
+ SuiteInfo ->
+ ErrStr = io_lib:format("~n*** ERROR *** "
+ "Invalid return value from "
+ "~w:suite/0: ~p~n", [Mod,SuiteInfo]),
+ io:format(ErrStr, []),
+ io:format(user, ErrStr, []),
{suite0_failed,bad_return_value}
end.
@@ -451,7 +471,6 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
{value,{watchdog,Dog}} -> test_server:timetrap_cancel(Dog);
false -> ok
end,
-
%% save the testcase process pid so that it can be used
%% to look up the attached trace window later
case ct_util:get_testdata(interpret) of
@@ -461,7 +480,6 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
_ ->
ok
end,
-
ct_util:delete_testdata(comment),
ct_util:delete_suite_data(last_saved_config),
FuncSpec =
@@ -767,6 +785,37 @@ get_suite(Mod, Name) ->
%%%-----------------------------------------------------------------
+get_all_cases(Suite) ->
+ case get_suite(Suite, all) of
+ [{?MODULE,error_in_suite,[[{error,_}=Error]]}] ->
+ Error;
+ [{?MODULE,error_in_suite,[[Error]]}] ->
+ {error,Error};
+ Tests ->
+ Cases = get_all_cases1(Suite, Tests),
+ lists:reverse(
+ lists:foldl(fun(TC, TCs) ->
+ case lists:member(TC, TCs) of
+ true -> TCs;
+ false -> [TC | TCs]
+ end
+ end, [], Cases))
+ end.
+
+get_all_cases1(Suite, [{conf,_Props,_Init,GrTests,_End} | Tests]) ->
+ get_all_cases1(Suite, GrTests) ++ get_all_cases1(Suite, Tests);
+
+get_all_cases1(Suite, [Test | Tests]) when is_atom(Test) ->
+ [{Suite,Test} | get_all_cases1(Suite, Tests)];
+
+get_all_cases1(Suite, [Test | Tests]) ->
+ [Test | get_all_cases1(Suite, Tests)];
+
+get_all_cases1(_, []) ->
+ [].
+
+%%%-----------------------------------------------------------------
+
find_groups(Mod, Name, TCs, GroupDefs) ->
Found = find(Mod, Name, TCs, GroupDefs, [], GroupDefs, false),
trim(Found).
@@ -978,15 +1027,20 @@ make_conf(Mod, Name, Props, TestSpec) ->
_ ->
ok
end,
- {InitConf,EndConf} =
+ {InitConf,EndConf,ExtraProps} =
case erlang:function_exported(Mod,init_per_group,2) of
true ->
- {{Mod,init_per_group},{Mod,end_per_group}};
+ {{Mod,init_per_group},{Mod,end_per_group},[]};
false ->
+ ct_logs:log("TEST INFO", "init_per_group/2 and "
+ "end_per_group/2 missing for group "
+ "~p in ~p, using default.",
+ [Name,Mod]),
{{?MODULE,ct_init_per_group},
- {?MODULE,ct_end_per_group}}
+ {?MODULE,ct_end_per_group},
+ [{suite,Mod}]}
end,
- {conf,[{name,Name}|Props],InitConf,TestSpec,EndConf}.
+ {conf,[{name,Name}|Props++ExtraProps],InitConf,TestSpec,EndConf}.
%%%-----------------------------------------------------------------
@@ -1159,13 +1213,15 @@ error_in_suite(Config) ->
%% if the group config functions are missing in the suite,
%% use these instead
ct_init_per_group(GroupName, Config) ->
- ct_logs:log("WARNING", "init_per_group/2 for ~w missing "
+ ct:comment(io_lib:format("start of ~p", [GroupName])),
+ ct_logs:log("TEST INFO", "init_per_group/2 for ~w missing "
"in suite, using default.",
[GroupName]),
Config.
ct_end_per_group(GroupName, _) ->
- ct_logs:log("WARNING", "end_per_group/2 for ~w missing "
+ ct:comment(io_lib:format("end of ~p", [GroupName])),
+ ct_logs:log("TEST INFO", "end_per_group/2 for ~w missing "
"in suite, using default.",
[GroupName]),
ok.
@@ -1242,12 +1298,20 @@ report(What,Data) ->
ok;
{end_per_group,_} ->
ok;
+ {ct_init_per_group,_} ->
+ ok;
+ {ct_end_per_group,_} ->
+ ok;
{_,ok} ->
add_to_stats(ok);
{_,{skipped,{failed,{_,init_per_testcase,_}}}} ->
add_to_stats(auto_skipped);
{_,{skipped,{require_failed,_}}} ->
add_to_stats(auto_skipped);
+ {_,{skipped,{timetrap_error,_}}} ->
+ add_to_stats(auto_skipped);
+ {_,{skipped,{invalid_time_format,_}}} ->
+ add_to_stats(auto_skipped);
{_,{skipped,_}} ->
add_to_stats(user_skipped);
{_,{SkipOrFail,_Reason}} ->
@@ -1332,6 +1396,21 @@ add_data_dir(File,Config) when is_list(File) ->
end.
%%%-----------------------------------------------------------------
+%%% @spec get_logopts() -> [LogOpt]
+get_logopts() ->
+ case ct_util:get_testdata(logopts) of
+ undefined ->
+ [];
+ LogOpts ->
+ LogOpts
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec format_comment(Comment) -> HtmlComment
+format_comment(Comment) ->
+ "<font color=\"green\">" ++ Comment ++ "</font>".
+
+%%%-----------------------------------------------------------------
%%% @spec overview_html_header(TestName) -> Header
overview_html_header(TestName) ->
TestName1 = lists:flatten(io_lib:format("~p", [TestName])),
diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl
index 6a90441d53..c1523509a5 100644
--- a/lib/common_test/src/ct_logs.erl
+++ b/lib/common_test/src/ct_logs.erl
@@ -28,7 +28,7 @@
-module(ct_logs).
--export([init/1,close/1,init_tc/0,end_tc/1]).
+-export([init/1,close/2,init_tc/1,end_tc/1]).
-export([get_log_dir/0,log/3,start_log/1,cont_log/2,end_log/0]).
-export([set_stylesheet/2,clear_stylesheet/1]).
-export([add_external_logs/1,add_link/3]).
@@ -97,11 +97,11 @@ logdir_node_prefix() ->
logdir_prefix()++"."++atom_to_list(node()).
%%%-----------------------------------------------------------------
-%%% @spec close(Info) -> ok
+%%% @spec close(Info, StartDir) -> ok
%%%
%%% @doc Create index pages with test results and close the CT Log
%%% (tool-internal use only).
-close(Info) ->
+close(Info, StartDir) ->
make_last_run_index(),
ct_event:notify(#event{name=stop_logging,node=node(),data=[]}),
@@ -124,14 +124,29 @@ close(Info) ->
ok;
Error ->
io:format("Warning! Cleanup failed: ~p~n", [Error])
- end;
+ end,
+ make_all_suites_index(stop),
+ make_all_runs_index(stop);
true ->
- file:set_cwd("..")
- end,
-
- make_all_suites_index(stop),
- make_all_runs_index(stop),
-
+ file:set_cwd(".."),
+ make_all_suites_index(stop),
+ make_all_runs_index(stop),
+ case ct_util:get_profile_data(browser, StartDir) of
+ undefined ->
+ ok;
+ BrowserData ->
+ case {proplists:get_value(prog, BrowserData),
+ proplists:get_value(args, BrowserData),
+ proplists:get_value(page, BrowserData)} of
+ {Prog,Args,Page} when is_list(Args),
+ is_list(Page) ->
+ URL = "\"file://" ++ ?abs(Page) ++ "\"",
+ ct_util:open_url(Prog, Args, URL);
+ _ ->
+ ok
+ end
+ end
+ end,
ok.
%%%-----------------------------------------------------------------
@@ -182,15 +197,14 @@ cast(Msg) ->
?MODULE ! Msg
end.
-
%%%-----------------------------------------------------------------
-%%% @spec init_tc() -> ok
+%%% @spec init_tc(RefreshLog) -> ok
%%%
%%% @doc Test case initiation (tool-internal use only).
%%%
%%% <p>This function is called by ct_framework:init_tc/3</p>
-init_tc() ->
- call({init_tc,self(),group_leader()}),
+init_tc(RefreshLog) ->
+ call({init_tc,self(),group_leader(),RefreshLog}),
ok.
%%%-----------------------------------------------------------------
@@ -486,8 +500,8 @@ logger_loop(State) ->
[Str,Args]),
%% stop the testcase, we need
%% to see the fault
- exit(Pid,logging_failed),
- ok;
+ exit(Pid,{log_printout_error,Str,Args}),
+ [];
IoStr when IoList == [] ->
[IoStr];
IoStr ->
@@ -507,10 +521,15 @@ logger_loop(State) ->
[begin io:format(Fd,Str,Args),io:nl(Fd) end || {Str,Args} <- List],
logger_loop(State#logger_state{tc_groupleaders=TCGLs})
end;
- {{init_tc,TCPid,GL},From} ->
+ {{init_tc,TCPid,GL,RefreshLog},From} ->
print_style(GL, State#logger_state.stylesheet),
set_evmgr_gl(GL),
TCGLs = add_tc_gl(TCPid,GL,State),
+ if not RefreshLog ->
+ ok;
+ true ->
+ make_last_run_index(State#logger_state.start_time)
+ end,
return(From,ok),
logger_loop(State#logger_state{tc_groupleaders=TCGLs});
{{end_tc,TCPid},From} ->
@@ -841,6 +860,7 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip,
""
end
end,
+ CtRunDir = filename:dirname(filename:dirname(Link)),
{Lbl,Timestamp,Node,AllInfo} =
case All of
{true,OldRuns} ->
@@ -850,7 +870,6 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip,
_ -> NodeOrDate
end,
N = ["<TD ALIGN=right><FONT SIZE=-1>",Node1,"</FONT></TD>\n"],
- CtRunDir = filename:dirname(filename:dirname(Link)),
L = ["<TD ALIGN=center><FONT SIZE=-1><B>",Label,"</FONT></B></TD>\n"],
T = ["<TD><FONT SIZE=-1>",timestamp(CtRunDir),"</FONT></TD>\n"],
CtLogFile = filename:join(CtRunDir,?ct_log_name),
@@ -869,7 +888,7 @@ make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip,
if NotBuilt == 0 ->
["<TD ALIGN=right>",integer_to_list(NotBuilt),"</TD>\n"];
true ->
- ["<TD ALIGN=right><A HREF=\"",?ct_log_name,"\">",
+ ["<TD ALIGN=right><A HREF=\"",filename:join(CtRunDir,?ct_log_name),"\">",
integer_to_list(NotBuilt),"</A></TD>\n"]
end,
FailStr =
diff --git a/lib/common_test/src/ct_make.erl b/lib/common_test/src/ct_make.erl
index 233e45248e..40e9e99f37 100644
--- a/lib/common_test/src/ct_make.erl
+++ b/lib/common_test/src/ct_make.erl
@@ -177,7 +177,7 @@ members([],_MakefileMods,I,Rest) ->
{I,Rest}.
-%% Any flags that are not recognixed as make flags are passed directly
+%% Any flags that are not recognised as make flags are passed directly
%% to the compiler.
%% So for example make:all([load,debug_info]) will make everything
%% with the debug_info flag and load it.
diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl
index 0715b8abf8..0a9bb5af67 100644
--- a/lib/common_test/src/ct_run.erl
+++ b/lib/common_test/src/ct_run.erl
@@ -33,7 +33,7 @@
%% Exported for VTS
--export([run_make/3,do_run/3,tests/1,tests/2,tests/3]).
+-export([run_make/3,do_run/4,tests/1,tests/2,tests/3]).
%% Misc internal functions
@@ -46,12 +46,14 @@
-define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)).
-record(opts, {label,
+ profile,
vts,
shell,
cover,
coverspec,
step,
logdir,
+ logopts = [],
config = [],
event_handlers = [],
ct_hooks = [],
@@ -157,15 +159,19 @@ script_start(Args) ->
end,
stop_trace(Tracing),
timer:sleep(1000),
+ io:nl(),
Res.
script_start1(Parent, Args) ->
%% read general start flags
Label = get_start_opt(label, fun([Lbl]) -> Lbl end, Args),
+ Profile = get_start_opt(profile, fun([Prof]) -> Prof end, Args),
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),
+ LogOpts = get_start_opt(logopts, fun(Os) -> [list_to_atom(O) || O <- Os] end,
+ [], Args),
MultTT = get_start_opt(multiply_timetraps,
fun([MT]) -> list_to_integer(MT) end, 1, Args),
ScaleTT = get_start_opt(scale_timetraps,
@@ -239,8 +245,10 @@ script_start1(Parent, Args) ->
application:set_env(common_test, basic_html, true)
end,
- StartOpts = #opts{label = Label, vts = Vts, shell = Shell, cover = Cover,
- logdir = LogDir, event_handlers = EvHandlers,
+ StartOpts = #opts{label = Label, profile = Profile,
+ vts = Vts, shell = Shell, cover = Cover,
+ logdir = LogDir, logopts = LogOpts,
+ event_handlers = EvHandlers,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
include = IncludeDirs,
@@ -303,9 +311,15 @@ script_start2(StartOpts = #opts{vts = undefined,
Label = choose_val(StartOpts#opts.label,
SpecStartOpts#opts.label),
+ Profile = choose_val(StartOpts#opts.profile,
+ SpecStartOpts#opts.profile),
+
LogDir = choose_val(StartOpts#opts.logdir,
SpecStartOpts#opts.logdir),
+ AllLogOpts = merge_vals([StartOpts#opts.logopts,
+ SpecStartOpts#opts.logopts]),
+
Cover = choose_val(StartOpts#opts.cover,
SpecStartOpts#opts.cover),
MultTT = choose_val(StartOpts#opts.multiply_timetraps,
@@ -328,9 +342,11 @@ script_start2(StartOpts = #opts{vts = undefined,
application:set_env(common_test, include, AllInclude),
{TS,StartOpts#opts{label = Label,
+ profile = Profile,
testspecs = Specs,
cover = Cover,
logdir = LogDir,
+ logopts = AllLogOpts,
config = SpecStartOpts#opts.config,
event_handlers = AllEvHs,
ct_hooks = AllCTHooks,
@@ -404,50 +420,72 @@ check_and_install_configfiles(
end.
script_start3(StartOpts, Args) ->
- case proplists:get_value(dir, Args) of
- [] ->
+ StartOpts1 = get_start_opt(step,
+ fun(Step) ->
+ StartOpts#opts{step = Step,
+ cover = undefined}
+ end, StartOpts, Args),
+ case {proplists:get_value(dir, Args),
+ proplists:get_value(suite, Args),
+ groups_and_cases(proplists:get_value(group, Args),
+ proplists:get_value(testcase, Args))} of
+ %% flag specified without data
+ {_,_,Error={error,_}} ->
+ Error;
+ {_,[],_} ->
+ {error,no_suite_specified};
+ {[],_,_} ->
{error,no_dir_specified};
- Dirs when is_list(Dirs) ->
+
+ {Dirs,undefined,[]} when is_list(Dirs) ->
script_start4(StartOpts#opts{tests = tests(Dirs)}, Args);
- undefined ->
- case proplists:get_value(suite, Args) of
- [] ->
- {error,no_suite_specified};
- 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};
- _ ->
- {error,incorrect_suite_option}
- end;
- undefined ->
- if StartOpts#opts.vts ; StartOpts#opts.shell ->
- script_start4(StartOpts#opts{tests = []}, Args);
- true ->
- script_usage(),
- {error,incorrect_usage}
- end
+
+ {undefined,Suites,[]} when is_list(Suites) ->
+ Ts = tests([suite_to_test(S) || S <- Suites]),
+ script_start4(StartOpts1#opts{tests = Ts}, Args);
+
+ {undefined,Suite,GsAndCs} when is_list(Suite) ->
+ case [suite_to_test(S) || S <- Suite] of
+ DirMods = [_] ->
+ Ts = tests(DirMods, GsAndCs),
+ script_start4(StartOpts1#opts{tests = Ts}, Args);
+ [_,_|_] ->
+ {error,multiple_suites_and_cases};
+ _ ->
+ {error,incorrect_start_options}
+ end;
+
+ {[_,_|_],Suite,[]} when is_list(Suite) ->
+ {error,multiple_dirs_and_suites};
+
+ {[Dir],Suite,GsAndCs} when is_list(Dir), is_list(Suite) ->
+ case [suite_to_test(Dir,S) || S <- Suite] of
+ DirMods when GsAndCs == [] ->
+ Ts = tests(DirMods),
+ script_start4(StartOpts1#opts{tests = Ts}, Args);
+ DirMods = [_] when GsAndCs /= [] ->
+ Ts = tests(DirMods, GsAndCs),
+ script_start4(StartOpts1#opts{tests = Ts}, Args);
+ [_,_|_] when GsAndCs /= [] ->
+ {error,multiple_suites_and_cases};
+ _ ->
+ {error,incorrect_start_options}
+ end;
+
+ {undefined,undefined,GsAndCs} when GsAndCs /= [] ->
+ {error,incorrect_start_options};
+
+ {undefined,undefined,_} ->
+ if StartOpts#opts.vts ; StartOpts#opts.shell ->
+ script_start4(StartOpts#opts{tests = []}, Args);
+ true ->
+ script_usage(),
+ {error,missing_start_options}
end
end.
script_start4(#opts{vts = true, config = Config, event_handlers = EvHandlers,
- tests = Tests, logdir = LogDir}, _Args) ->
+ tests = Tests, logdir = LogDir, logopts = LogOpts}, _Args) ->
ConfigFiles =
lists:foldl(fun({ct_config_plain,CfgFiles}, AllFiles) when
is_list(hd(CfgFiles)) ->
@@ -458,16 +496,21 @@ script_start4(#opts{vts = true, config = Config, event_handlers = EvHandlers,
(_, AllFiles) ->
AllFiles
end, [], Config),
- vts:init_data(ConfigFiles, EvHandlers, ?abs(LogDir), Tests);
+ vts:init_data(ConfigFiles, EvHandlers, ?abs(LogDir), LogOpts, Tests);
-script_start4(#opts{label = Label, shell = true, config = Config,
+script_start4(#opts{label = Label, profile = Profile,
+ shell = true, config = Config,
event_handlers = EvHandlers,
ct_hooks = CTHooks,
+ logopts = LogOpts,
enable_builtin_hooks = EnableBuiltinHooks,
logdir = LogDir, testspecs = Specs}, _Args) ->
%% label - used by ct_logs
application:set_env(common_test, test_label, Label),
+ %% profile - used in ct_util
+ application:set_env(common_test, profile, Profile),
+
if Config == [] ->
ok;
true ->
@@ -478,6 +521,7 @@ script_start4(#opts{label = Label, shell = true, config = Config,
{enable_builtin_hooks,EnableBuiltinHooks}]) of
ok ->
ct_util:start(interactive, LogDir),
+ ct_util:set_testdata({logopts, LogOpts}),
log_ts_names(Specs),
io:nl(),
ok;
@@ -518,6 +562,7 @@ script_usage() ->
"\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
"\n\t[-dir TestDir1 TestDir2 .. TestDirN] |"
"\n\t[-suite Suite [-case Case]]"
+ "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]"
"\n\t[-include InclDir1 InclDir2 .. InclDirN]"
"\n\t[-no_auto_compile]"
"\n\t[-multiply_timetraps N]"
@@ -534,8 +579,9 @@ script_usage() ->
"\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
"\n\t[-stylesheet CSSFile]"
"\n\t[-cover CoverCfgFile]"
- "\n\t[-event_handler EvHandler1 and EvHandler2 .. EvHandlerN]"
- "\n\t[-ct_hooks CTHook1 and CTHook2 .. CTHookN]"
+ "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
+ "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]"
+ "\n\t[-ct_hooks CTHook1 CTHook2 .. CTHookN]"
"\n\t[-include InclDir1 InclDir2 .. InclDirN]"
"\n\t[-no_auto_compile]"
"\n\t[-multiply_timetraps N]"
@@ -553,8 +599,9 @@ script_usage() ->
"\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
"\n\t[-stylesheet CSSFile]"
"\n\t[-cover CoverCfgFile]"
- "\n\t[-event_handler EvHandler1 and EvHandler2 .. EvHandlerN]"
- "\n\t[-ct_hooks CTHook1 and CTHook2 .. CTHookN]"
+ "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
+ "\n\t[-logopts LogOpt1 LogOpt2 .. LogOptN]"
+ "\n\t[-ct_hooks CTHook1 CTHook2 .. CTHookN]"
"\n\t[-include InclDir1 InclDir2 .. InclDirN]"
"\n\t[-no_auto_compile]"
"\n\t[-multiply_timetraps N]"
@@ -632,6 +679,16 @@ run_test(StartOpt) when is_tuple(StartOpt) ->
run_test([StartOpt]);
run_test(StartOpts) when is_list(StartOpts) ->
+ CTPid = spawn(fun() -> run_test1(StartOpts) end),
+ Ref = monitor(process, CTPid),
+ receive
+ {'DOWN',Ref,process,CTPid,{user_error,Error}} ->
+ Error;
+ {'DOWN',Ref,process,CTPid,Other} ->
+ Other
+ end.
+
+run_test1(StartOpts) when is_list(StartOpts) ->
case proplists:get_value(refresh_logs, StartOpts) of
undefined ->
Tracing = start_trace(StartOpts),
@@ -640,7 +697,7 @@ run_test(StartOpts) when is_list(StartOpts) ->
Res =
case ct_repeat:loop_test(func, StartOpts) of
false ->
- case catch run_test1(StartOpts) of
+ case catch run_test2(StartOpts) of
{'EXIT',Reason} ->
file:set_cwd(Cwd),
{error,Reason};
@@ -651,20 +708,27 @@ run_test(StartOpts) when is_list(StartOpts) ->
Result
end,
stop_trace(Tracing),
- Res;
+ exit(Res);
RefreshDir ->
refresh_logs(?abs(RefreshDir)),
- ok
+ exit(done)
end.
-run_test1(StartOpts) ->
+run_test2(StartOpts) ->
%% label
Label = get_start_opt(label, fun(Lbl) when is_list(Lbl) -> Lbl;
(Lbl) when is_atom(Lbl) -> atom_to_list(Lbl)
end, StartOpts),
+ %% profile
+ Profile = get_start_opt(profile, fun(Prof) when is_list(Prof) -> Prof;
+ (Prof) when is_atom(Prof) -> atom_to_list(Prof)
+ end, StartOpts),
%% logdir
LogDir = get_start_opt(logdir, fun(LD) when is_list(LD) -> LD end,
StartOpts),
+ %% logopts
+ LogOpts = get_start_opt(logopts, value, [], StartOpts),
+
%% config & userconfig
CfgFiles = ct_config:get_config_file_list(StartOpts),
@@ -768,8 +832,9 @@ run_test1(StartOpts) ->
%% stepped execution
Step = get_start_opt(step, value, StartOpts),
- Opts = #opts{label = Label,
- cover = Cover, step = Step, logdir = LogDir, config = CfgFiles,
+ Opts = #opts{label = Label, profile = Profile,
+ cover = Cover, step = Step, logdir = LogDir,
+ logopts = LogOpts, config = CfgFiles,
event_handlers = EvHandlers,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
@@ -811,8 +876,12 @@ run_spec_file(Relaxed,
SpecOpts = get_data_for_node(TS, node()),
Label = choose_val(Opts#opts.label,
SpecOpts#opts.label),
+ Profile = choose_val(Opts#opts.profile,
+ SpecOpts#opts.profile),
LogDir = choose_val(Opts#opts.logdir,
SpecOpts#opts.logdir),
+ AllLogOpts = merge_vals([Opts#opts.logopts,
+ SpecOpts#opts.logopts]),
AllConfig = merge_vals([CfgFiles, SpecOpts#opts.config]),
Cover = choose_val(Opts#opts.cover,
SpecOpts#opts.cover),
@@ -833,8 +902,10 @@ run_spec_file(Relaxed,
application:set_env(common_test, include, AllInclude),
Opts1 = Opts#opts{label = Label,
+ profile = Profile,
cover = Cover,
logdir = which(logdir, LogDir),
+ logopts = AllLogOpts,
config = AllConfig,
event_handlers = AllEvHs,
include = AllInclude,
@@ -848,7 +919,6 @@ run_spec_file(Relaxed,
case check_and_install_configfiles(AllConfig,Opts1#opts.logdir,
Opts1) of
ok ->
-
{Run,Skip} = ct_testspec:prepare_tests(TS, node()),
reformat_result(catch do_run(Run, Skip, Opts1, StartOpts));
{error,GCFReason} ->
@@ -921,67 +991,102 @@ run_dir(Opts = #opts{logdir = LogDir,
ok -> ok;
{error,IReason} -> exit(IReason)
end,
- case lists:keysearch(dir, 1, StartOpts) of
- {value,{_,Dirs=[Dir|_]}} when not is_integer(Dir),
- length(Dirs)>1 ->
- %% multiple dirs (no suite)
- reformat_result(catch do_run(tests(Dirs), [], Opts1, StartOpts));
- false -> % no dir
- %% fun for converting suite name to {Dir,Mod} tuple
- S2M = fun(S) when is_list(S) ->
- {filename:dirname(S),
- list_to_atom(filename:rootname(filename:basename(S)))};
- (A) ->
- {".",A}
- end,
- case lists:keysearch(suite, 1, StartOpts) of
- {value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) ->
- {Dir,Mod} = S2M(Suite),
- case groups_and_cases(proplists:get_value(group, StartOpts),
- proplists:get_value(testcase, StartOpts)) of
- Error = {error,_} ->
- exit(Error);
+ case {proplists:get_value(dir, StartOpts),
+ proplists:get_value(suite, StartOpts),
+ groups_and_cases(proplists:get_value(group, StartOpts),
+ proplists:get_value(testcase, StartOpts))} of
+ %% flag specified without data
+ {_,_,Error={error,_}} ->
+ Error;
+ {_,[],_} ->
+ {error,no_suite_specified};
+ {[],_,_} ->
+ {error,no_dir_specified};
+
+ {Dirs=[Hd|_],undefined,[]} when is_list(Dirs), not is_integer(Hd) ->
+ Dirs1 = [if is_atom(D) -> atom_to_list(D);
+ true -> D end || D <- Dirs],
+ reformat_result(catch do_run(tests(Dirs1), [], Opts1, StartOpts));
+
+ {Dir=[Hd|_],undefined,[]} when is_list(Dir) and is_integer(Hd) ->
+ reformat_result(catch do_run(tests(Dir), [], Opts1, StartOpts));
+
+ {Dir,undefined,[]} when is_atom(Dir) and (Dir /= undefined) ->
+ reformat_result(catch do_run(tests(atom_to_list(Dir)),
+ [], Opts1, StartOpts));
+
+ {undefined,Suites=[Hd|_],[]} when not is_integer(Hd) ->
+ Suites1 = [suite_to_test(S) || S <- Suites],
+ reformat_result(catch do_run(tests(Suites1), [], Opts1, StartOpts));
+
+ {undefined,Suite,[]} when is_atom(Suite) and
+ (Suite /= undefined) ->
+ {Dir,Mod} = suite_to_test(Suite),
+ reformat_result(catch do_run(tests(Dir, Mod), [], Opts1, StartOpts));
+
+ {undefined,Suite,GsAndCs} when is_atom(Suite) and
+ (Suite /= undefined) ->
+ {Dir,Mod} = suite_to_test(Suite),
+ reformat_result(catch do_run(tests(Dir, Mod, GsAndCs),
+ [], Opts1, StartOpts));
+
+ {undefined,[Hd,_|_],_GsAndCs} when not is_integer(Hd) ->
+ exit(multiple_suites_and_cases);
+
+ {undefined,Suite=[Hd|Tl],GsAndCs} when is_integer(Hd) ;
+ (is_list(Hd) and (Tl == [])) ;
+ (is_atom(Hd) and (Tl == [])) ->
+ {Dir,Mod} = suite_to_test(Suite),
+ reformat_result(catch do_run(tests(Dir, Mod, GsAndCs),
+ [], Opts1, StartOpts));
+
+ {[Hd,_|_],_Suites,[]} when is_list(Hd) ; not is_integer(Hd) ->
+ exit(multiple_dirs_and_suites);
+
+ {undefined,undefined,GsAndCs} when GsAndCs /= [] ->
+ exit(incorrect_start_options);
+
+ {Dir,Suite,GsAndCs} when is_integer(hd(Dir)) ;
+ (is_atom(Dir) and (Dir /= undefined)) ;
+ ((length(Dir) == 1) and is_atom(hd(Dir))) ;
+ ((length(Dir) == 1) and is_list(hd(Dir))) ->
+ Dir1 = if is_atom(Dir) -> atom_to_list(Dir);
+ true -> Dir end,
+ if Suite == undefined ->
+ exit(incorrect_start_options);
+
+ is_integer(hd(Suite)) ;
+ (is_atom(Suite) and (Suite /= undefined)) ;
+ ((length(Suite) == 1) and is_atom(hd(Suite))) ;
+ ((length(Suite) == 1) and is_list(hd(Suite))) ->
+ {Dir2,Mod} = suite_to_test(Dir1, Suite),
+ case GsAndCs of
[] ->
- reformat_result(catch do_run(tests(Dir, listify(Mod)),
+ reformat_result(catch do_run(tests(Dir2, Mod),
[], Opts1, StartOpts));
- GsAndCs ->
- reformat_result(catch do_run(tests(Dir, Mod, GsAndCs),
+ _ ->
+ reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs),
[], Opts1, StartOpts))
end;
- {value,{_,Suites}} ->
- reformat_result(catch do_run(tests(lists:map(S2M, Suites)),
- [], Opts1, StartOpts));
- _ ->
- exit(no_tests_specified)
- end;
- {value,{_,Dir}} ->
- 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)
- end,
- case groups_and_cases(proplists:get_value(group, StartOpts),
- proplists:get_value(testcase, StartOpts)) of
- Error = {error,_} ->
- exit(Error);
- [] ->
- reformat_result(catch do_run(tests(Dir, listify(Mod)),
+
+ is_list(Suite) -> % multiple suites
+ case [suite_to_test(Dir1, S) || S <- Suite] of
+ [_,_|_] when GsAndCs /= [] ->
+ exit(multiple_suites_and_cases);
+ [{Dir2,Mod}] when GsAndCs /= [] ->
+ reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs),
[], Opts1, StartOpts));
- GsAndCs ->
- reformat_result(catch do_run(tests(Dir, Mod, GsAndCs),
+ DirMods ->
+ reformat_result(catch do_run(tests(DirMods),
[], Opts1, StartOpts))
- end;
- {value,{_,Suites=[Suite|_]}} when is_list(Suite) ->
- Mods = lists:map(fun(Str) -> list_to_atom(Str) end, Suites),
- reformat_result(catch do_run(tests(delistify(Dir), Mods),
- [], Opts1, StartOpts));
- {value,{_,Suites}} ->
- reformat_result(catch do_run(tests(delistify(Dir), Suites),
- [], Opts1, StartOpts));
- false -> % no suite, only dir
- reformat_result(catch do_run(tests(listify(Dir)),
- [], Opts1, StartOpts))
- end
+ end
+ end;
+
+ {undefined,undefined,[]} ->
+ exit(no_test_specified);
+
+ {Dir,Suite,GsAndCs} ->
+ exit({incorrect_start_options,{Dir,Suite,GsAndCs}})
end.
%%%-----------------------------------------------------------------
@@ -992,19 +1097,38 @@ run_dir(Opts = #opts{logdir = LogDir,
%%% the same as those used in test specification files.
%%% @equiv ct:run_testspec/1
%%%-----------------------------------------------------------------
-
run_testspec(TestSpec) ->
+ CTPid = spawn(fun() -> run_testspec1(TestSpec) end),
+ Ref = monitor(process, CTPid),
+ receive
+ {'DOWN',Ref,process,CTPid,{user_error,Error}} ->
+ Error;
+ {'DOWN',Ref,process,CTPid,Other} ->
+ Other
+ end.
+
+run_testspec1(TestSpec) ->
{ok,Cwd} = file:get_cwd(),
io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]),
- case catch run_testspec1(TestSpec) of
+ case catch run_testspec2(TestSpec) of
{'EXIT',Reason} ->
file:set_cwd(Cwd),
- {error,Reason};
+ exit({error,Reason});
Result ->
- Result
+ exit(Result)
end.
-run_testspec1(TestSpec) ->
+run_testspec2(File) when is_list(File), is_integer(hd(File)) ->
+ case file:read_file_info(File) of
+ {ok,_} ->
+ exit("Bad argument, "
+ "use ct:run_test([{spec," ++ File ++ "}])");
+ _ ->
+ exit("Bad argument, list of tuples expected, "
+ "use ct:run_test/1 for test specification files")
+ end;
+
+run_testspec2(TestSpec) ->
case catch ct_testspec:collect_tests_from_list(TestSpec, false) of
{E,CTReason} when E == error ; E == 'EXIT' ->
exit(CTReason);
@@ -1035,7 +1159,9 @@ run_testspec1(TestSpec) ->
end.
get_data_for_node(#testspec{label = Labels,
+ profile = Profiles,
logdir = LogDirs,
+ logopts = LogOptsList,
cover = CoverFs,
config = Cfgs,
userconfig = UsrCfgs,
@@ -1046,10 +1172,15 @@ get_data_for_node(#testspec{label = Labels,
multiply_timetraps = MTs,
scale_timetraps = STs}, Node) ->
Label = proplists:get_value(Node, Labels),
+ Profile = proplists:get_value(Node, Profiles),
LogDir = case proplists:get_value(Node, LogDirs) of
undefined -> ".";
Dir -> Dir
end,
+ LogOpts = case proplists:get_value(Node, LogOptsList) of
+ undefined -> [];
+ LOs -> LOs
+ end,
Cover = proplists:get_value(Node, CoverFs),
MT = proplists:get_value(Node, MTs),
ST = proplists:get_value(Node, STs),
@@ -1059,7 +1190,9 @@ get_data_for_node(#testspec{label = Labels,
FiltCTHooks = [Hook || {N,Hook} <- CTHooks, N==Node],
Include = [I || {N,I} <- Incl, N==Node],
#opts{label = Label,
+ profile = Profile,
logdir = LogDir,
+ logopts = LogOpts,
cover = Cover,
config = ConfigFiles,
event_handlers = EvHandlers,
@@ -1141,8 +1274,24 @@ reformat_result({user_error,Reason}) ->
reformat_result(Result) ->
Result.
-suite_to_test(Suite) ->
- {filename:dirname(Suite),list_to_atom(filename:rootname(filename:basename(Suite)))}.
+suite_to_test(Suite) when is_atom(Suite) ->
+ suite_to_test(atom_to_list(Suite));
+
+suite_to_test(Suite) when is_list(Suite) ->
+ {filename:dirname(Suite),
+ list_to_atom(filename:rootname(filename:basename(Suite)))}.
+
+suite_to_test(Dir, Suite) when is_atom(Suite) ->
+ suite_to_test(Dir, atom_to_list(Suite));
+
+suite_to_test(Dir, Suite) when is_list(Suite) ->
+ case filename:dirname(Suite) of
+ "." ->
+ {Dir,list_to_atom(filename:rootname(Suite))};
+ DirName -> % ignore Dir
+ File = filename:basename(Suite),
+ {DirName,list_to_atom(filename:rootname(File))}
+ end.
groups_and_cases(Gs, Cs) when ((Gs == undefined) or (Gs == [])) and
((Cs == undefined) or (Cs == [])) ->
@@ -1176,9 +1325,11 @@ tests(TestDirs) when is_list(TestDirs), is_list(hd(TestDirs)) ->
[{?testdir(TestDir,all),all,all} || TestDir <- TestDirs].
do_run(Tests, Misc) when is_list(Misc) ->
- do_run(Tests, Misc, ".").
+ do_run(Tests, Misc, ".", []).
-do_run(Tests, Misc, LogDir) when is_list(Misc) ->
+do_run(Tests, Misc, LogDir, LogOpts) when is_list(Misc),
+ is_list(LogDir),
+ is_list(LogOpts) ->
Opts =
case proplists:get_value(step, Misc) of
undefined ->
@@ -1193,11 +1344,10 @@ do_run(Tests, Misc, LogDir) when is_list(Misc) ->
CoverFile ->
Opts#opts{cover = CoverFile}
end,
- do_run(Tests, [], Opts1#opts{logdir = LogDir}, []).
-
-do_run(Tests, Skip, Opts, Args) ->
- #opts{label = Label, cover = Cover} = Opts,
+ do_run(Tests, [], Opts1#opts{logdir = LogDir}, []);
+do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) ->
+ #opts{label = Label, profile = Profile, cover = Cover} = Opts,
%% label - used by ct_logs
TestLabel =
if Label == undefined -> undefined;
@@ -1207,6 +1357,15 @@ do_run(Tests, Skip, Opts, Args) ->
end,
application:set_env(common_test, test_label, TestLabel),
+ %% profile - used in ct_util
+ TestProfile =
+ if Profile == undefined -> undefined;
+ is_atom(Profile) -> atom_to_list(Profile);
+ is_list(Profile) -> Profile;
+ true -> undefined
+ end,
+ application:set_env(common_test, profile, TestProfile),
+
case code:which(test_server) of
non_existing ->
exit({error,no_path_to_test_server});
@@ -1241,6 +1400,8 @@ do_run(Tests, Skip, Opts, Args) ->
_Pid ->
%% save stylesheet info
ct_util:set_testdata({stylesheet,Opts#opts.stylesheet}),
+ %% save logopts
+ ct_util:set_testdata({logopts,Opts#opts.logopts}),
%% enable silent connections
case Opts#opts.silent_connections of
[] ->
@@ -2008,7 +2169,14 @@ maybe_interpret1(Suite, Cases, StepOpts) when is_list(Cases) ->
maybe_interpret2(Suite, Cases, StepOpts) ->
set_break_on_config(Suite, StepOpts),
- [i:ib(Suite, Case, 1) || Case <- Cases],
+ [begin try i:ib(Suite, Case, 1) of
+ _ -> ok
+ catch
+ _:_Error ->
+ io:format(user, "Invalid breakpoint: ~w:~w/1~n",
+ [Suite,Case])
+ end
+ end || Case <- Cases, is_atom(Case)],
test_server_ctrl:multiply_timetraps(infinity),
WinOp = case lists:member(keep_inactive, ensure_atom(StepOpts)) of
true -> no_kill;
@@ -2021,10 +2189,18 @@ maybe_interpret2(Suite, Cases, StepOpts) ->
set_break_on_config(Suite, StepOpts) ->
case lists:member(config, ensure_atom(StepOpts)) of
true ->
- i:ib(Suite, init_per_suite, 1),
- i:ib(Suite, init_per_testcase, 2),
- i:ib(Suite, end_per_testcase, 2),
- i:ib(Suite, end_per_suite, 1);
+ SetBPIfExists = fun(F,A) ->
+ case erlang:function_exported(Suite, F, A) of
+ true -> i:ib(Suite, F, A);
+ false -> ok
+ end
+ end,
+ SetBPIfExists(init_per_suite, 1),
+ SetBPIfExists(init_per_group, 2),
+ SetBPIfExists(init_per_testcase, 2),
+ SetBPIfExists(end_per_testcase, 2),
+ SetBPIfExists(end_per_group, 2),
+ SetBPIfExists(end_per_suite, 1);
false ->
ok
end.
@@ -2078,6 +2254,15 @@ get_start_opt(Key, IfExists, Args) ->
get_start_opt(Key, IfExists, undefined, Args).
get_start_opt(Key, IfExists, IfNotExists, Args) ->
+ try try_get_start_opt(Key, IfExists, IfNotExists, Args) of
+ Result ->
+ Result
+ catch
+ error:_ ->
+ exit({user_error,{bad_argument,Key}})
+ end.
+
+try_get_start_opt(Key, IfExists, IfNotExists, Args) ->
case lists:keysearch(Key, 1, Args) of
{value,{Key,Val}} when is_function(IfExists) ->
IfExists(Val);
@@ -2252,6 +2437,8 @@ opts2args(EnvStartOpts) ->
end, EHs),
[_LastAnd|StrsR] = lists:reverse(lists:flatten(Strs)),
[{event_handler_init,lists:reverse(StrsR)}];
+ ({logopts,LOs}) when is_list(LOs) ->
+ [{logopts,[atom_to_list(LO) || LO <- LOs]}];
({ct_hooks,[]}) ->
[];
({ct_hooks,CTHs}) when is_list(CTHs) ->
@@ -2315,32 +2502,31 @@ is_suite(ModOrFile) when is_list(ModOrFile) ->
end.
get_all_testcases(Suite) ->
- %%! this needs to be updated to handle testcase groups later!!
- case catch Suite:all() of
- {'EXIT',Why} ->
- {error,Why};
- {skip,_} ->
- [];
- Cases ->
- AllCases =
- lists:foldl(fun({sequence,SeqName}, All) ->
- case catch Suite:sequences() of
- {'EXIT',_} ->
- All;
- Seqs ->
- case proplists:get_value(SeqName, Seqs) of
- undefined ->
- All;
- SeqCases ->
- lists:reverse(SeqCases) ++ All
- end
- end;
- (Case,All) ->
- [Case|All]
- end, [], Cases),
- lists:reverse(AllCases)
+ try ct_framework:get_all_cases(Suite) of
+ {error,_Reason} = Error ->
+ Error;
+ SuiteCases ->
+ Cases = [C || {_S,C} <- SuiteCases],
+ try Suite:sequences() of
+ [] ->
+ Cases;
+ Seqs ->
+ TCs1 = lists:flatten([TCs || {_,TCs} <- Seqs]),
+ lists:reverse(
+ lists:foldl(fun(TC, Acc) ->
+ case lists:member(TC, Acc) of
+ true -> Acc;
+ false -> [TC | Acc]
+ end
+ end, [], Cases ++ TCs1))
+ catch
+ _:_ ->
+ Cases
+ end
+ catch
+ _:Error ->
+ {error,Error}
end.
-
%% Internal tracing support. If {ct_trace,TraceSpec} is present, the
%% TraceSpec file will be consulted and dbg used to trace function
@@ -2361,8 +2547,8 @@ start_trace(Args) ->
false
end;
{_,Error} ->
- io:format("Warning! Tracing not started. Reason: ~p~n~n",
- [Error]),
+ io:format("Warning! Tracing not started. Reason: ~s~n~n",
+ [file:format_error(Error)]),
false
end;
false ->
diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl
index c6f5fd7df4..71a784870c 100644
--- a/lib/common_test/src/ct_telnet.erl
+++ b/lib/common_test/src/ct_telnet.erl
@@ -245,7 +245,6 @@ cmdf(Connection,CmdFormat,Args) ->
%%% Data = [string()]
%%% @doc Send a telnet command and wait for prompt
%%% (uses a format string and list of arguments to build the command).
-%%%-----------------------------------------------------------------
cmdf(Connection,CmdFormat,Args,Timeout) when is_list(Args) ->
Cmd = lists:flatten(io_lib:format(CmdFormat,Args)),
cmd(Connection,Cmd,Timeout).
@@ -360,15 +359,15 @@ expect(Connection,Patterns) ->
%%% will also be a <code>HaltReason</code> returned.</p>
%%%
%%% <p><underline>Examples:</underline><br/>
-%%% <code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],
-%%% [sequence,{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match
+%%% <code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],</code>
+%%% <code>[sequence,{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match
%%% "ABC" first and then "XYZ", but if "NNN" appears the function will
%%% return <code>{error,{nnn,["NNN"]}}</code>. If both "ABC" and "XYZ"
%%% are matched, the function will return
%%% <code>{ok,[AbcMatch,XyzMatch]}</code>.</p>
%%%
-%%% <p><code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],
-%%% [{repeat,2},{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match
+%%% <p><code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}],</code>
+%%% <code>[{repeat,2},{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match
%%% "ABC" or "XYZ" twice. If "NNN" appears the function will return
%%% with <code>HaltReason = {nnn,["NNN"]}</code>.</p>
%%%
diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl
index 96971ccfc7..317910d5c8 100644
--- a/lib/common_test/src/ct_testspec.erl
+++ b/lib/common_test/src/ct_testspec.erl
@@ -249,11 +249,15 @@ collect_tests_from_file1([Spec|Specs],TestSpec,Relaxed) ->
SpecDir = filename:dirname(filename:absname(Spec)),
case file:consult(Spec) of
{ok,Terms} ->
- TestSpec1 = collect_tests(Terms,TestSpec#testspec{spec_dir=SpecDir},
+ TestSpec1 = collect_tests(Terms,
+ TestSpec#testspec{spec_dir=SpecDir},
Relaxed),
collect_tests_from_file1(Specs,TestSpec1,Relaxed);
{error,Reason} ->
- throw({error,{Spec,Reason}})
+ ReasonStr =
+ lists:flatten(io_lib:format("~s",
+ [file:format_error(Reason)])),
+ throw({error,{Spec,ReasonStr}})
end;
collect_tests_from_file1([],TS=#testspec{config=Cfgs,event_handler=EvHs,
include=Incl,tests=Tests},_) ->
@@ -481,6 +485,26 @@ add_tests([{logdir,Node,Dir}|Ts],Spec) ->
add_tests([{logdir,Dir}|Ts],Spec) ->
add_tests([{logdir,all_nodes,Dir}|Ts],Spec);
+%% --- logopts ---
+add_tests([{logopts,all_nodes,Opts}|Ts],Spec) ->
+ LogOpts = Spec#testspec.logopts,
+ Tests = [{logopts,N,Opts} ||
+ N <- list_nodes(Spec),
+ lists:keymember(ref2node(N,Spec#testspec.nodes),1,
+ LogOpts) == false],
+ add_tests(Tests++Ts,Spec);
+add_tests([{logopts,Nodes,Opts}|Ts],Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,logopts,[Opts],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+add_tests([{logopts,Node,Opts}|Ts],Spec) ->
+ LogOpts = Spec#testspec.logopts,
+ LogOpts1 = [{ref2node(Node,Spec#testspec.nodes),Opts} |
+ lists:keydelete(ref2node(Node,Spec#testspec.nodes),
+ 1,LogOpts)],
+ add_tests(Ts,Spec#testspec{logopts=LogOpts1});
+add_tests([{logopts,Opts}|Ts],Spec) ->
+ add_tests([{logopts,all_nodes,Opts}|Ts],Spec);
+
%% --- label ---
add_tests([{label,all_nodes,Lbl}|Ts],Spec) ->
Labels = Spec#testspec.label,
@@ -1101,6 +1125,8 @@ valid_terms() ->
{merge_tests,1},
{logdir,2},
{logdir,3},
+ {logopts,2},
+ {logopts,3},
{label,2},
{label,3},
{event_handler,2},
diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl
index b3e345b4e5..3b6ad6f98d 100644
--- a/lib/common_test/src/ct_util.erl
+++ b/lib/common_test/src/ct_util.erl
@@ -47,7 +47,7 @@
-export([get_mode/0, create_table/3, read_opts/0]).
--export([set_cwd/1, reset_cwd/0]).
+-export([set_cwd/1, reset_cwd/0, get_start_dir/0]).
-export([parse_table/1]).
@@ -61,6 +61,9 @@
-export([warn_duplicates/1]).
+-export([get_profile_data/0, get_profile_data/1,
+ get_profile_data/2, open_url/3]).
+
-include("ct_event.hrl").
-include("ct_util.hrl").
@@ -121,13 +124,15 @@ do_start(Parent,Mode,LogDir) ->
ok -> ok;
E -> exit(E)
end,
+ DoExit = fun(Reason) -> file:set_cwd(StartDir), exit(Reason) end,
Opts = case read_opts() of
{ok,Opts1} ->
Opts1;
Error ->
Parent ! {self(),Error},
- exit(Error)
+ DoExit(Error)
end,
+
%% start an event manager (if not already started by master)
case ct_event:start_link() of
{error,{already_started,_}} ->
@@ -140,16 +145,23 @@ do_start(Parent,Mode,LogDir) ->
ct_event:add_handler([{vts,VtsPid}])
end
end,
+
%% start ct_config server
- ct_config:start(Mode),
+ try ct_config:start(Mode) of
+ _ -> ok
+ catch
+ _Class:CfgError ->
+ DoExit(CfgError)
+ end,
+
%% 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})
+ {'EXIT',Why} -> DoExit(Why);
+ Other -> DoExit({event_handler,Other})
end
end,
case catch lists:foreach(Add,Handlers) of
@@ -168,10 +180,15 @@ do_start(Parent,Mode,LogDir) ->
data={StartTime,
lists:flatten(TestLogDir)}}),
%% Initialize ct_hooks
- case catch ct_hooks:init(Opts) of
+ try ct_hooks:init(Opts) of
ok ->
Parent ! {self(),started};
- {_,CTHReason} ->
+ {fail,CTHReason} ->
+ ct_logs:tc_print('Suite Callback',CTHReason,[]),
+ self() ! {{stop,{self(),{user_error,CTHReason}}},
+ {Parent,make_ref()}}
+ catch
+ _:CTHReason ->
ct_logs:tc_print('Suite Callback',CTHReason,[]),
self() ! {{stop,{self(),{user_error,CTHReason}}},
{Parent,make_ref()}}
@@ -243,6 +260,9 @@ set_cwd(Dir) ->
reset_cwd() ->
call(reset_cwd).
+get_start_dir() ->
+ call(get_start_dir).
+
loop(Mode,TestData,StartDir) ->
receive
{update_last_run_index,From} ->
@@ -319,6 +339,9 @@ loop(Mode,TestData,StartDir) ->
{reset_cwd,From} ->
return(From,file:set_cwd(StartDir)),
loop(From,TestData,StartDir);
+ {get_start_dir,From} ->
+ return(From,StartDir),
+ loop(From,TestData,StartDir);
{{stop,Info},From} ->
Time = calendar:local_time(),
ct_event:sync_notify(#event{name=test_done,
@@ -332,7 +355,7 @@ loop(Mode,TestData,StartDir) ->
ets:delete(?conn_table),
ets:delete(?board_table),
ets:delete(?suite_table),
- ct_logs:close(Info),
+ ct_logs:close(Info, StartDir),
ct_event:stop(),
ct_config:stop(),
file:set_cwd(StartDir),
@@ -727,6 +750,79 @@ warn_duplicates(Suites) ->
lists:foreach(Warn, Suites),
ok.
+%%%-----------------------------------------------------------------
+%%% @spec
+%%%
+%%% @doc
+get_profile_data() ->
+ get_profile_data(all).
+
+get_profile_data(KeyOrStartDir) ->
+ if is_atom(KeyOrStartDir) ->
+ get_profile_data(KeyOrStartDir, get_start_dir());
+ is_list(KeyOrStartDir) ->
+ get_profile_data(all, KeyOrStartDir)
+ end.
+
+get_profile_data(Key, StartDir) ->
+ Profile = case application:get_env(common_test, profile) of
+ {ok,undefined} -> default;
+ {ok,Prof} -> Prof;
+ _ -> default
+ end,
+ get_profile_data(Profile, Key, StartDir).
+
+get_profile_data(Profile, Key, StartDir) ->
+ File = case Profile of
+ default ->
+ ?ct_profile_file;
+ _ when is_list(Profile) ->
+ ?ct_profile_file ++ "." ++ Profile;
+ _ when is_atom(Profile) ->
+ ?ct_profile_file ++ "." ++ atom_to_list(Profile)
+ end,
+ FullNameWD = filename:join(StartDir, File),
+ {WhichFile,Result} =
+ case file:consult(FullNameWD) of
+ {error,enoent} ->
+ case init:get_argument(home) of
+ {ok,[[HomeDir]]} ->
+ FullNameHome = filename:join(HomeDir, File),
+ {FullNameHome,file:consult(FullNameHome)};
+ _ ->
+ {File,{error,enoent}}
+ end;
+ Consulted ->
+ {FullNameWD,Consulted}
+ end,
+ case Result of
+ {error,enoent} when Profile /= default ->
+ io:format(user, "~nERROR! Missing profile file ~p~n", [File]),
+ undefined;
+ {error,enoent} when Profile == default ->
+ undefined;
+ {error,Reason} ->
+ io:format(user,"~nERROR! Error in profile file ~p: ~p~n",
+ [WhichFile,Reason]),
+ undefined;
+ {ok,Data} ->
+ Data1 = case Data of
+ [List] when is_list(List) ->
+ List;
+ _ when is_list(Data) ->
+ Data;
+ _ ->
+ io:format(user,
+ "~nERROR! Invalid profile data in ~p~n",
+ [WhichFile]),
+ []
+ end,
+ if Key == all ->
+ Data1;
+ true ->
+ proplists:get_value(Key, Data)
+ end
+ end.
%%%-----------------------------------------------------------------
%%% Internal functions
@@ -799,3 +895,28 @@ abs_name2([H|T],Acc) ->
abs_name2(T,[H|Acc]);
abs_name2([],Acc) ->
filename:join(lists:reverse(Acc)).
+
+open_url(iexplore, Args, URL) ->
+ {ok,R} = win32reg:open([read]),
+ ok = win32reg:change_key(R,"applications\\iexplore.exe\\shell\\open\\command"),
+ case win32reg:values(R) of
+ {ok, Paths} ->
+ Path = proplists:get_value(default, Paths),
+ [Cmd | _] = string:tokens(Path, "%"),
+ Cmd1 = Cmd ++ " " ++ Args ++ " " ++ URL,
+ io:format(user, "~nOpening ~s with command:~n ~s~n", [URL,Cmd1]),
+ open_port({spawn,Cmd1}, []);
+ _ ->
+ io:format("~nNo path to iexplore.exe~n",[])
+ end,
+ win32reg:close(R),
+ ok;
+
+open_url(Prog, Args, URL) ->
+ ProgStr = if is_atom(Prog) -> atom_to_list(Prog);
+ is_list(Prog) -> Prog
+ end,
+ Cmd = ProgStr ++ " " ++ Args ++ " " ++ URL,
+ io:format(user, "~nOpening ~s with command:~n ~s~n", [URL,Cmd]),
+ open_port({spawn,Cmd},[]),
+ ok.
diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl
index dbe9d9b4e6..bde832811a 100644
--- a/lib/common_test/src/ct_util.hrl
+++ b/lib/common_test/src/ct_util.hrl
@@ -31,7 +31,9 @@
nodes=[],
init=[],
label=[],
+ profile=[],
logdir=["."],
+ logopts=[],
cover=[],
config=[],
userconfig=[],
@@ -59,3 +61,5 @@
-define(missing_suites_info, "missing_suites.info").
-define(ct_config_txt, ct_config_plain).
+
+-define(ct_profile_file, ".common_test").
diff --git a/lib/common_test/src/vts.erl b/lib/common_test/src/vts.erl
index f0bf090804..cc8a932887 100644
--- a/lib/common_test/src/vts.erl
+++ b/lib/common_test/src/vts.erl
@@ -20,7 +20,7 @@
-module(vts).
-export([start/0,
- init_data/4,
+ init_data/5,
stop/0,
report/2]).
@@ -32,6 +32,7 @@
menu_frame/2,
welcome_frame/2,
config_frame/2,
+ browse_config_file/2,
add_config_file/2,
remove_config_file/2,
run_frame/2,
@@ -56,7 +57,7 @@
-record(state,{tests=[],config=[],event_handler=[],test_runner,
running=0,reload_results=false,start_dir,current_log_dir,
- total=0,ok=0,fail=0,skip=0,testruns=[]}).
+ logopts=[],total=0,ok=0,fail=0,skip=0,testruns=[]}).
%%%-----------------------------------------------------------------
@@ -65,8 +66,8 @@ start() ->
webtool:start(),
webtool:start_tools([],"app=vts").
-init_data(ConfigFiles,EvHandlers,LogDir,Tests) ->
- call({init_data,ConfigFiles,EvHandlers,LogDir,Tests}).
+init_data(ConfigFiles,EvHandlers,LogDir,LogOpts,Tests) ->
+ call({init_data,ConfigFiles,EvHandlers,LogDir,LogOpts,Tests}).
stop() ->
webtool:stop_tools([],"app=vts"),
@@ -119,6 +120,8 @@ menu_frame(_Env,_Input) ->
call(menu_frame).
config_frame(_Env,_Input) ->
call(config_frame).
+browse_config_file(_Env,Input) ->
+ call({browse_config_file,Input}).
add_config_file(_Env,Input) ->
call({add_config_file,Input}).
remove_config_file(_Env,Input) ->
@@ -160,10 +163,11 @@ init(Parent) ->
loop(State) ->
receive
- {{init_data,Config,EvHandlers,LogDir,Tests},From} ->
+ {{init_data,Config,EvHandlers,LogDir,LogOpts,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},
+ current_log_dir=LogDir,
+ logopts=LogOpts,tests=Tests},
ct_install(NewState),
return(From,ok),
loop(NewState);
@@ -182,6 +186,9 @@ loop(State) ->
{config_frame,From} ->
return(From,config_frame1(State)),
loop(State);
+ {{browse_config_file,_Input},From} ->
+ return(From,ok),
+ loop(State);
{{add_config_file,Input},From} ->
{Return,State1} = add_config_file1(Input,State),
ct_install(State1),
@@ -241,10 +248,12 @@ loop(State) ->
return(From,ok);
{'EXIT',Pid,Reason} ->
case State#state.test_runner of
- Pid -> io:format("ERROR: test runner crashed: ~p\n",[Reason]);
- _ -> ignore
- end,
- loop(State);
+ Pid ->
+ io:format("Test run error: ~p\n",[Reason]),
+ loop(State);
+ _ ->
+ loop(State)
+ end;
{{test_info,_Type,_Data},From} ->
return(From,ok),
loop(State)
@@ -270,10 +279,11 @@ return({To,Ref},Result) ->
To ! {Ref, Result}.
-run_test1(State=#state{tests=Tests,current_log_dir=LogDir}) ->
+run_test1(State=#state{tests=Tests,current_log_dir=LogDir,
+ logopts=LogOpts}) ->
Self=self(),
RunTest = fun() ->
- case ct_run:do_run(Tests,[],LogDir) of
+ case ct_run:do_run(Tests,[],LogDir,LogOpts) of
{error,_Reason} ->
aborted();
_ ->
@@ -282,17 +292,18 @@ run_test1(State=#state{tests=Tests,current_log_dir=LogDir}) ->
unlink(Self)
end,
Pid = spawn_link(RunTest),
- Total =
+ {Total,Tests1} =
receive
{{test_info,start_info,{_,_,Cases}},From} ->
return(From,ok),
- Cases;
+ {Cases,Tests};
EXIT = {'EXIT',_,_} ->
- self() ! EXIT
+ self() ! EXIT,
+ {0,[]}
after 30000 ->
- 0
+ {0,[]}
end,
- State#state{test_runner=Pid,running=length(Tests),
+ State#state{test_runner=Pid,running=length(Tests1),
total=Total,ok=0,fail=0,skip=0,testruns=[]}.
@@ -356,22 +367,32 @@ config_frame1(State) ->
config_body(State) ->
Entry = [input("TYPE=file NAME=browse SIZE=40"),
input("TYPE=hidden NAME=file")],
+ BrowseForm =
+ form(
+ "NAME=read_file_form METHOD=post ACTION=\"./browse_config_file\"",
+ table(
+ "BORDER=0",
+ [tr(td("1. Locate config file")),
+ tr(td(Entry))])),
AddForm =
form(
- "NAME=read_file_form METHOD=post ACTION=\"./add_config_file\"",
+ "NAME=add_file_form METHOD=post ACTION=\"./add_config_file\"",
table(
"BORDER=0",
- [tr(
- [td(Entry),
+ [tr(td("2. Paste full config file name here")),
+ tr(
+ [td(input("TYPE=text NAME=file SIZE=40")),
td("ALIGN=center",
input("TYPE=submit onClick=\"file.value=browse.value;\""
" VALUE=\"Add\""))])])),
+
{Text,RemoveForm} =
case State#state.config of
[] ->
- T = "To be able to run any tests, one or more configuration "
- "files must be added. Enter the name of the configuration "
- "file below and click the \"Add\" button.",
+ T = "Before running the tests, one or more configuration "
+ "files may be added. Locate the config file, copy its "
+ "full name, paste this into the text field below, then "
+ "click the \"Add\" button.",
R = "",
{T,R};
Files ->
@@ -394,20 +415,24 @@ config_body(State) ->
input("TYPE=submit VALUE=\"Remove\"")))])),
{T,R}
end,
-
+
[h1("ALIGN=center","Config"),
table(
- "WIDTH=600 ALIGN=center CELLPADDING=5",
+ "WIDTH=450 ALIGN=center CELLPADDING=5",
[tr(td(["BGCOLOR=",?INFO_BG_COLOR],Text)),
- tr(td("ALIGN=center",AddForm)),
- tr(td("ALIGN=center",RemoveForm))])].
-
+ tr(td("")),
+ tr(td("")),
+ tr(td("ALIGN=left",BrowseForm)),
+ tr(td("ALIGN=left",AddForm)),
+ tr(td("ALIGN=left",RemoveForm))])].
add_config_file1(Input,State) ->
State1 =
case get_input_data(Input,"file") of
- "" -> State;
- File -> State#state{config=[File|State#state.config]}
+ "" ->
+ State;
+ File ->
+ State#state{config=[File|State#state.config]}
end,
Return = config_frame1(State1),
{Return,State1}.
@@ -427,10 +452,17 @@ run_body(#state{running=Running}) when Running>0 ->
[h1("ALIGN=center","Run Test"),
p(["Test are ongoing: ",href("./result_frameset","Results")])];
run_body(State) ->
- ConfigList = ul([li(File) || File <- State#state.config]),
+ ConfigList =
+ case State#state.config of
+ [] ->
+ ul(["none"]);
+ CfgFiles ->
+ ul([li(File) || File <- CfgFiles])
+ end,
ConfigFiles = [h3("Config Files"),
ConfigList],
-
+ {ok,CWD} = file:get_cwd(),
+ CurrWD = [h3("Current Working Directory"), ul(CWD)],
AddDirForm =
form(
"NAME=add_dir_form METHOD=post ACTION=\"./add_test_dir\"",
@@ -442,7 +474,6 @@ run_body(State) ->
td("ALIGN=center",
input("TYPE=submit onClick=\"dir.value=browse.value;\""
" VALUE=\"Add Test Dir\""))])])),
-
{LoadedTestsTable,Submit} =
case create_testdir_entries(State#state.tests,1) of
[] -> {"",""};
@@ -454,22 +485,20 @@ run_body(State) ->
{table("CELLPADDING=5",[Heading,TestDirs]),
submit_button()}
end,
-
- %% It should be ok to have no config-file!
Body =
- %% case State#state.config of %% [] -> %% p("ALIGN=center",
- %% href("./config_frame","Please select one or
- %% more config files")); %% _ ->
table(
- "WIDTH=100%",
- [tr(td(ConfigFiles)),
+ "WIDTH=450 ALIGN=center",
+ [tr(td("")),
+ tr(td("")),
+ tr(td(ConfigFiles)),
+ tr(td("")),
+ tr(td(CurrWD)),
tr(td("")),
tr(td(AddDirForm)),
tr(td("")),
tr(td(LoadedTestsTable)),
- tr(td(Submit))]),
- %% end,
-
+ tr(td(Submit))
+ ]),
[h1("ALIGN=center","Run Test"), Body].
create_testdir_entries([{Dir,Suite,Case}|Tests],N) ->
@@ -556,18 +585,17 @@ options([Element|Elements],Selected,N,Func) ->
options([],_Selected,_N,_Func) ->
[].
-add_test_dir1(Input,State) ->
+add_test_dir1(Input, State) ->
State1 =
case get_input_data(Input,"dir") of
"" -> State;
Dir0 ->
Dir = case ct_util:is_test_dir(Dir0) of
- true ->
- Dir0;
- false -> filename:join(Dir0,"test")
+ true -> Dir0;
+ false -> ct_util:get_testdir(Dir0, all)
end,
case filelib:is_dir(Dir) of
- true ->
+ true ->
Test = ct_run:tests(Dir),
State#state{tests=State#state.tests++Test};
false ->
@@ -577,8 +605,6 @@ add_test_dir1(Input,State) ->
Return = run_frame1(State1),
{Return,State1}.
-
-
remove_test_dir1(Input,State) ->
N = list_to_integer(get_input_data(Input,"dir")),
State1 = State#state{tests=delete_test(N,State#state.tests)},
@@ -641,6 +667,9 @@ result_frameset2(State) ->
"./redirect_to_result_log_frame";
{_Dir,0} ->
filename:join(["/log_dir","index.html"]);
+ {_Dir,_} when State#state.testruns == [] ->
+ %% crash before first test
+ "./no_result_log_frame";
{_Dir,_} ->
{_,CurrentLog} = hd(State#state.testruns),
CurrentLog
@@ -749,6 +778,8 @@ report1(tc_done,{_Suite,_Case,{skipped,_Reason}},State) ->
State#state{skip=State#state.skip+1};
report1(tc_user_skip,{_Suite,_Case,_Reason},State) ->
State#state{skip=State#state.skip+1};
+report1(tc_auto_skip,{_Suite,_Case,_Reason},State) ->
+ State#state{skip=State#state.skip+1};
report1(loginfo,_,State) ->
State.
@@ -850,6 +881,8 @@ h2(Text) ->
["<H2>",Text,"</H2>\n"].
h3(Text) ->
["<H3>",Text,"</H3>\n"].
+%%h4(Text) ->
+%% ["<H4>",Text,"</H4>\n"].
font(Args,Text) ->
["<FONT ",Args,">\n",Text,"\n</FONT>\n"].
p(Text) ->