aboutsummaryrefslogtreecommitdiffstats
path: root/lib/common_test/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common_test/src')
-rw-r--r--lib/common_test/src/Makefile8
-rw-r--r--lib/common_test/src/common_test.app.src3
-rw-r--r--lib/common_test/src/ct.erl27
-rw-r--r--lib/common_test/src/ct_conn_log_h.erl230
-rw-r--r--lib/common_test/src/ct_event.erl16
-rw-r--r--lib/common_test/src/ct_framework.erl83
-rw-r--r--lib/common_test/src/ct_gen_conn.erl247
-rw-r--r--lib/common_test/src/ct_hooks.erl7
-rw-r--r--lib/common_test/src/ct_logs.erl302
-rw-r--r--lib/common_test/src/ct_master_logs.erl106
-rw-r--r--lib/common_test/src/ct_netconfc.erl1828
-rw-r--r--lib/common_test/src/ct_netconfc.hrl58
-rw-r--r--lib/common_test/src/ct_repeat.erl88
-rw-r--r--lib/common_test/src/ct_run.erl356
-rw-r--r--lib/common_test/src/ct_testspec.erl953
-rw-r--r--lib/common_test/src/ct_util.erl22
-rw-r--r--lib/common_test/src/ct_util.hrl12
-rw-r--r--lib/common_test/src/cth_conn_log.erl124
-rw-r--r--lib/common_test/src/cth_surefire.erl66
19 files changed, 3720 insertions, 816 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile
index 037a686963..f7dce195d7 100644
--- a/lib/common_test/src/Makefile
+++ b/lib/common_test/src/Makefile
@@ -70,14 +70,18 @@ MODULES= \
ct_hooks\
ct_hooks_lock\
cth_log_redirect\
- cth_surefire
+ cth_surefire \
+ ct_netconfc \
+ ct_conn_log_h \
+ cth_conn_log
TARGET_MODULES= $(MODULES:%=$(EBIN)/%)
BEAM_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR))
ERL_FILES= $(MODULES:=.erl)
HRL_FILES = \
- ct_util.hrl
+ ct_util.hrl \
+ ct_netconfc.hrl
EXTERNAL_HRL_FILES = \
../include/ct.hrl \
../include/ct_event.hrl
diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src
index ae9a51faeb..18c1dec784 100644
--- a/lib/common_test/src/common_test.app.src
+++ b/lib/common_test/src/common_test.app.src
@@ -33,6 +33,8 @@
ct_master_event,
ct_master_logs,
ct_master_status,
+ ct_netconfc,
+ ct_conn_log_h,
ct_repeat,
ct_rpc,
ct_run,
@@ -49,6 +51,7 @@
ct_config_xml,
ct_slave,
cth_log_redirect,
+ cth_conn_log,
cth_surefire
]},
{registered, [ct_logs,
diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl
index 22cef776cf..d5938c5db9 100644
--- a/lib/common_test/src/ct.erl
+++ b/lib/common_test/src/ct.erl
@@ -68,7 +68,8 @@
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]).
+ 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]).
@@ -1113,3 +1114,27 @@ 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).
diff --git a/lib/common_test/src/ct_conn_log_h.erl b/lib/common_test/src/ct_conn_log_h.erl
new file mode 100644
index 0000000000..f3b6781971
--- /dev/null
+++ b/lib/common_test/src/ct_conn_log_h.erl
@@ -0,0 +1,230 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 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%
+%%
+-module(ct_conn_log_h).
+
+%%%
+%%% A handler that can be connected to the error_logger event
+%%% handler. Writes all ct connection events. See comments in
+%%% cth_conn_log for more information.
+%%%
+
+-include("ct_util.hrl").
+
+-export([init/1,
+ handle_event/2, handle_call/2, handle_info/2,
+ terminate/2]).
+
+-record(state, {group_leader,logs=[]}).
+
+-define(WIDTH,80).
+
+%%%-----------------------------------------------------------------
+%%% Callbacks
+init({GL,Logs}) ->
+ open_files(Logs,#state{group_leader=GL}).
+
+open_files([{ConnMod,{LogType,Logs}}|T],State) ->
+ case do_open_files(Logs,[]) of
+ {ok,Fds} ->
+ open_files(T,State#state{logs=[{ConnMod,{LogType,Fds}} |
+ State#state.logs]});
+ Error ->
+ Error
+ end;
+open_files([],State) ->
+ {ok,State}.
+
+
+do_open_files([{Tag,File}|Logs],Acc) ->
+ case file:open(File, [write]) of
+ {ok,Fd} ->
+ do_open_files(Logs,[{Tag,Fd}|Acc]);
+ {error,Reason} ->
+ {error,{could_not_open_log,File,Reason}}
+ end;
+do_open_files([],Acc) ->
+ {ok,lists:reverse(Acc)}.
+
+handle_event({_Type, GL, _Msg}, State) when node(GL) /= node() ->
+ {ok, State};
+handle_event({_Type,_GL,{Pid,{ct_connection,Action,ConnName},Report}},State) ->
+ Info = conn_info(Pid,#conn_log{name=ConnName,action=Action}),
+ write_report(now(),Info,Report,State),
+ {ok, State};
+handle_event({_Type,_GL,{Pid,Info=#conn_log{},Report}},State) ->
+ write_report(now(),conn_info(Pid,Info),Report,State),
+ {ok, State};
+handle_event({error_report,_,{Pid,_,[{ct_connection,ConnName}|R]}},State) ->
+ %% Error reports from connection
+ write_error(now(),conn_info(Pid,#conn_log{name=ConnName}),R,State),
+ {ok, State};
+handle_event(_, State) ->
+ {ok, State}.
+
+handle_info(_, State) ->
+ {ok, State}.
+
+handle_call(_Query, State) ->
+ {ok, {error, bad_query}, State}.
+
+terminate(_,#state{logs=Logs}) ->
+ [file:close(Fd) || {_,_,Fds} <- Logs, Fd <- Fds],
+ ok.
+
+
+%%%-----------------------------------------------------------------
+%%% Writing reports
+write_report(Time,#conn_log{module=ConnMod}=Info,Data,State) ->
+ {LogType,Fd} = get_log(Info,State),
+ io:format(Fd,"~n~s~s~s",[format_head(ConnMod,LogType,Time),
+ format_title(LogType,Info),
+ format_data(ConnMod,LogType,Data)]).
+
+write_error(Time,#conn_log{module=ConnMod}=Info,Report,State) ->
+ case get_log(Info,State) of
+ {html,_} ->
+ %% The error will anyway be written in the html log by the
+ %% sasl error handler, so don't write it again.
+ ok;
+ {LogType,Fd} ->
+ io:format(Fd,"~n~s~s~s",[format_head(ConnMod,LogType,Time," ERROR"),
+ format_title(LogType,Info),
+ format_error(LogType,Report)])
+ end.
+
+get_log(Info,State) ->
+ case proplists:get_value(Info#conn_log.module,State#state.logs) of
+ {html,_} ->
+ {html,State#state.group_leader};
+ {LogType,Fds} ->
+ {LogType,get_fd(Info,Fds)};
+ undefined ->
+ {html,State#state.group_leader}
+ end.
+
+get_fd(#conn_log{name=undefined},Fds) ->
+ proplists:get_value(default,Fds);
+get_fd(#conn_log{name=ConnName},Fds) ->
+ case proplists:get_value(ConnName,Fds) of
+ undefined ->
+ proplists:get_value(default,Fds);
+ Fd ->
+ Fd
+ end.
+
+%%%-----------------------------------------------------------------
+%%% Formatting
+format_head(ConnMod,LogType,Time) ->
+ format_head(ConnMod,LogType,Time,"").
+
+format_head(ConnMod,raw,Time,Text) ->
+ io_lib:format("~n~p, ~p~s, ",[now_to_time(Time),ConnMod,Text]);
+format_head(ConnMod,_,Time,Text) ->
+ Head = pad_char_end(?WIDTH,pretty_head(now_to_time(Time),ConnMod,Text),$=),
+ io_lib:format("~n~s",[Head]).
+
+format_title(raw,#conn_log{client=Client}=Info) ->
+ io_lib:format("Client ~p ~s ~s",[Client,actionstr(Info),serverstr(Info)]);
+format_title(_,Info) ->
+ Title = pad_char_end(?WIDTH,pretty_title(Info),$=),
+ io_lib:format("~n~s", [Title]).
+
+format_data(_,_,NoData) when NoData == ""; NoData == <<>> ->
+ "";
+format_data(ConnMod,LogType,Data) ->
+ ConnMod:format_data(LogType,Data).
+
+format_error(raw,Report) ->
+ io_lib:format("~n~p~n",[Report]);
+format_error(pretty,Report) ->
+ [io_lib:format("~n ~p: ~p",[K,V]) || {K,V} <- Report].
+
+
+
+
+%%%-----------------------------------------------------------------
+%%% Helpers
+conn_info(LoggingProc, #conn_log{client=undefined} = ConnInfo) ->
+ conn_info(ConnInfo#conn_log{client=LoggingProc});
+conn_info(_, ConnInfo) ->
+ conn_info(ConnInfo).
+
+conn_info(#conn_log{client=Client, module=undefined} = ConnInfo) ->
+ case ets:lookup(ct_connections,Client) of
+ [#conn{address=Address,callback=Callback}] ->
+ ConnInfo#conn_log{address=Address,module=Callback};
+ [] ->
+ ConnInfo
+ end;
+conn_info(ConnInfo) ->
+ ConnInfo.
+
+
+now_to_time({_,_,MicroS}=Now) ->
+ {calendar:now_to_local_time(Now),MicroS}.
+
+pretty_head({{{Y,Mo,D},{H,Mi,S}},MicroS},ConnMod,Text0) ->
+ Text = string:to_upper(atom_to_list(ConnMod) ++ Text0),
+ io_lib:format("= ~s ==== ~s-~s-~p::~s:~s:~s,~s ",
+ [Text,t(D),month(Mo),Y,t(H),t(Mi),t(S),
+ micro2milli(MicroS)]).
+
+pretty_title(#conn_log{client=Client}=Info) ->
+ io_lib:format("= Client ~p ~s Server ~s ",
+ [Client,actionstr(Info),serverstr(Info)]).
+
+actionstr(#conn_log{action=send}) -> "----->";
+actionstr(#conn_log{action=recv}) -> "<-----";
+actionstr(#conn_log{action=open}) -> "opened session to";
+actionstr(#conn_log{action=close}) -> "closed session to";
+actionstr(_) -> "<---->".
+
+serverstr(#conn_log{name=undefined,address=Address}) ->
+ io_lib:format("~p",[Address]);
+serverstr(#conn_log{name=Alias,address=Address}) ->
+ io_lib:format("~p(~p)",[Alias,Address]).
+
+month(1) -> "Jan";
+month(2) -> "Feb";
+month(3) -> "Mar";
+month(4) -> "Apr";
+month(5) -> "May";
+month(6) -> "Jun";
+month(7) -> "Jul";
+month(8) -> "Aug";
+month(9) -> "Sep";
+month(10) -> "Oct";
+month(11) -> "Nov";
+month(12) -> "Dec".
+
+micro2milli(X) ->
+ pad0(3,integer_to_list(X div 1000)).
+
+t(X) ->
+ pad0(2,integer_to_list(X)).
+
+pad0(N,Str) ->
+ M = length(Str),
+ lists:duplicate(N-M,$0) ++ Str.
+
+pad_char_end(N,Str,Char) ->
+ case length(lists:flatten(Str)) of
+ M when M<N -> Str ++ lists:duplicate(N-M,Char);
+ _ -> Str
+ end.
diff --git a/lib/common_test/src/ct_event.erl b/lib/common_test/src/ct_event.erl
index 3e79898ad1..998be35fda 100644
--- a/lib/common_test/src/ct_event.erl
+++ b/lib/common_test/src/ct_event.erl
@@ -31,7 +31,7 @@
%% API
-export([start_link/0, add_handler/0, add_handler/1, stop/0]).
--export([notify/1, sync_notify/1]).
+-export([notify/1, notify/2, sync_notify/1,sync_notify/2]).
-export([is_alive/0]).
%% gen_event callbacks
@@ -90,6 +90,13 @@ notify(Event) ->
end.
%%--------------------------------------------------------------------
+%% Function: notify(Name,Data) -> ok
+%% Description: Asynchronous notification to event manager.
+%%--------------------------------------------------------------------
+notify(Name, Data) ->
+ notify(#event{ name = Name, data = Data}).
+
+%%--------------------------------------------------------------------
%% Function: sync_notify(Event) -> ok
%% Description: Synchronous notification to event manager.
%%--------------------------------------------------------------------
@@ -102,6 +109,13 @@ sync_notify(Event) ->
end.
%%--------------------------------------------------------------------
+%% Function: sync_notify(Name,Data) -> ok
+%% Description: Synchronous notification to event manager.
+%%--------------------------------------------------------------------
+sync_notify(Name,Data) ->
+ sync_notify(#event{ name = Name, data = Data}).
+
+%%--------------------------------------------------------------------
%% Function: is_alive() -> true | false
%% Description: Check if Event Manager is alive.
%%--------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl
index 11575cd0fb..19a26872ee 100644
--- a/lib/common_test/src/ct_framework.erl
+++ b/lib/common_test/src/ct_framework.erl
@@ -27,7 +27,7 @@
-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([get_logopts/0, format_comment/1, get_html_wrapper/3]).
+-export([get_logopts/0, format_comment/1, get_html_wrapper/4]).
-export([error_in_suite/1, init_per_suite/1, end_per_suite/1,
init_per_group/2, end_per_group/2]).
@@ -204,7 +204,7 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
data={Mod,FuncSpec}}),
case catch configure(MergedInfo,MergedInfo,SuiteInfo,
- FuncSpec,Config) of
+ FuncSpec,[],Config) of
{suite0_failed,Reason} ->
ct_util:set_testdata({curr_tc,{Mod,{suite0_failed,{require,Reason}}}}),
{skip,{require_failed_in_suite0,Reason}};
@@ -212,12 +212,14 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
{auto_skip,{require_failed,Reason}};
{'EXIT',Reason} ->
{auto_skip,Reason};
- {ok,Config1} ->
+ {ok,PostInitHook,Config1} ->
case get('$test_server_framework_test') of
undefined ->
- ct_suite_init(Suite, FuncSpec, Config1);
+ ct_suite_init(Suite, FuncSpec, PostInitHook, Config1);
Fun ->
- case Fun(init_tc, Config1) of
+ PostInitHookResult = do_post_init_hook(PostInitHook,
+ Config1),
+ case Fun(init_tc, [PostInitHookResult ++ Config1]) of
NewConfig when is_list(NewConfig) ->
{ok,NewConfig};
Else ->
@@ -226,14 +228,28 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) ->
end
end.
-ct_suite_init(Suite, Func, [Config]) when is_list(Config) ->
+ct_suite_init(Suite, Func, PostInitHook, Config) when is_list(Config) ->
case ct_hooks:init_tc(Suite, Func, Config) of
NewConfig when is_list(NewConfig) ->
- {ok, [NewConfig]};
+ PostInitHookResult = do_post_init_hook(PostInitHook, NewConfig),
+ {ok, [PostInitHookResult ++ NewConfig]};
Else ->
Else
end.
+do_post_init_hook(PostInitHook, Config) ->
+ lists:flatmap(fun({Tag,Fun}) ->
+ case lists:keysearch(Tag,1,Config) of
+ {value,_} ->
+ [];
+ false ->
+ case Fun() of
+ {error,_} -> [];
+ Result -> [{Tag,Result}]
+ end
+ end
+ end, PostInitHook).
+
add_defaults(Mod,Func, GroupPath) ->
Suite = get_suite_name(Mod, GroupPath),
case (catch Suite:suite()) of
@@ -453,15 +469,16 @@ timetrap_first([],Info,[]) ->
timetrap_first([],Info,Found) ->
?rev(Found) ++ ?rev(Info).
-configure([{require,Required}|Rest],Info,SuiteInfo,Scope,Config) ->
+configure([{require,Required}|Rest],
+ Info,SuiteInfo,Scope,PostInitHook,Config) ->
case ct:require(Required) of
ok ->
- configure(Rest,Info,SuiteInfo,Scope,Config);
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config);
Error = {error,Reason} ->
case required_default('_UNDEF',Required,Info,
SuiteInfo,Scope) of
ok ->
- configure(Rest,Info,SuiteInfo,Scope,Config);
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config);
_ ->
case lists:keymember(Required,2,SuiteInfo) of
true ->
@@ -471,14 +488,15 @@ configure([{require,Required}|Rest],Info,SuiteInfo,Scope,Config) ->
end
end
end;
-configure([{require,Name,Required}|Rest],Info,SuiteInfo,Scope,Config) ->
+configure([{require,Name,Required}|Rest],
+ Info,SuiteInfo,Scope,PostInitHook,Config) ->
case ct:require(Name,Required) of
ok ->
- configure(Rest,Info,SuiteInfo,Scope,Config);
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config);
Error = {error,Reason} ->
case required_default(Name,Required,Info,SuiteInfo,Scope) of
ok ->
- configure(Rest,Info,SuiteInfo,Scope,Config);
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config);
_ ->
case lists:keymember(Name,2,SuiteInfo) of
true ->
@@ -488,17 +506,24 @@ configure([{require,Name,Required}|Rest],Info,SuiteInfo,Scope,Config) ->
end
end
end;
-configure([{timetrap,off}|Rest],Info,SuiteInfo,Scope,Config) ->
- configure(Rest,Info,SuiteInfo,Scope,Config);
-configure([{timetrap,Time}|Rest],Info,SuiteInfo,Scope,Config) ->
- Dog = test_server:timetrap(Time),
- configure(Rest,Info,SuiteInfo,Scope,[{watchdog,Dog}|Config]);
-configure([{ct_hooks, Hook} | Rest], Info, SuiteInfo, Scope, Config) ->
- configure(Rest, Info, SuiteInfo, Scope, [{ct_hooks, Hook} | Config]);
-configure([_|Rest],Info,SuiteInfo,Scope,Config) ->
- configure(Rest,Info,SuiteInfo,Scope,Config);
-configure([],_,_,_,Config) ->
- {ok,[Config]}.
+configure([{timetrap,off}|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) ->
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config);
+configure([{timetrap,Time}|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) ->
+ PostInitHook1 =
+ [{watchdog,fun() -> case test_server:get_timetrap_info() of
+ undefined ->
+ test_server:timetrap(Time);
+ _ ->
+ {error,already_set}
+ end
+ end} | PostInitHook],
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook1,Config);
+configure([{ct_hooks,Hook}|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) ->
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook,[{ct_hooks,Hook}|Config]);
+configure([_|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) ->
+ configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config);
+configure([],_,_,_,PostInitHook,Config) ->
+ {ok,PostInitHook,Config}.
%% the require element in Info may come from suite/0 and
%% should be scoped 'suite', or come from the group info
@@ -562,10 +587,8 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) ->
%% in case Mod == ct_framework, lookup the suite name
Suite = get_suite_name(Mod, Args),
- case lists:keysearch(watchdog,1,Args) of
- {value,{watchdog,Dog}} -> test_server:timetrap_cancel(Dog);
- false -> ok
- end,
+ test_server:timetrap_cancel(),
+
%% 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
@@ -1634,5 +1657,5 @@ format_comment(Comment) ->
%%%-----------------------------------------------------------------
%%% @spec get_html_wrapper(TestName, PrintLabel, Cwd) -> Header
-get_html_wrapper(TestName, PrintLabel, Cwd) ->
- ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd).
+get_html_wrapper(TestName, PrintLabel, Cwd, TableCols) ->
+ ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols).
diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl
index 5aab4dd2dd..5df9127725 100644
--- a/lib/common_test/src/ct_gen_conn.erl
+++ b/lib/common_test/src/ct_gen_conn.erl
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2003-2010. All Rights Reserved.
+%% 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
@@ -27,7 +27,7 @@
-compile(export_all).
-export([start/4, stop/1]).
--export([call/2, do_within_time/2]).
+-export([call/2, call/3, return/2, do_within_time/2]).
-ifdef(debug).
-define(dbg,true).
@@ -39,17 +39,24 @@
name,
address,
init_data,
+ reconnect = true,
+ forward = false,
+ use_existing = true,
+ old = false,
conn_pid,
cb_state,
ct_util_server}).
%%%-----------------------------------------------------------------
-%%% @spec start(Name,Address,InitData,CallbackMod) ->
+%%% @spec start(Address,InitData,CallbackMod,Opts) ->
%%% {ok,Handle} | {error,Reason}
%%% Name = term()
%%% CallbackMod = atom()
%%% InitData = term()
%%% Address = term()
+%%% Opts = [Opt]
+%%% Opt = {name,Name} | {use_existing_connection,boolean()} |
+%%% {reconnect,boolean()} | {forward_messages,boolean()}
%%%
%%% @doc Open a connection and start the generic connection owner process.
%%%
@@ -60,42 +67,59 @@
%%% <code>InitData</code> and returna
%%% <code>{ok,ConnectionPid,State}</code> or
%%% <code>{error,Reason}</code>.</p>
+%%%
+%%% If no name is given, the <code>Name</code> argument in init/3 will
+%%% have the value <code>undefined</code>.
+%%%
+%%% The callback modules must also export
+%%% ```
+%%% handle_msg(Msg,From,State) -> {reply,Reply,State} |
+%%% {noreply,State} |
+%%% {stop,Reply,State}
+%%% terminate(ConnectionPid,State) -> term()
+%%% close(Handle) -> term()
+%%% '''
+%%%
+%%% The <code>close/1</code> callback function is actually a callback
+%%% for ct_util, for closing registered connections when
+%%% ct_util_server is terminated. <code>Handle</code> is the Pid of
+%%% the ct_gen_conn process.
+%%%
+%%% If option <code>reconnect</code> is <code>true</code>, then the
+%%% callback must also export
+%%% ```
+%%% reconnect(Address,State) -> {ok,ConnectionPid,State}
+%%% '''
+%%%
+%%% If option <code>forward_messages</code> is <ocde>true</code>, then
+%%% the callback must also export
+%%% ```
+%%% handle_msg(Msg,State) -> {noreply,State} | {stop,State}
+%%% '''
+%%%
+%%% An old interface still exists. This is used by ct_telnet, ct_ftp
+%%% and ct_ssh. The start function then has an explicit
+%%% <code>Name</code> argument, and no <code>Opts</code> argument. The
+%%% callback must export:
+%%%
+%%% ```
+%%% init(Name,Address,InitData) -> {ok,ConnectionPid,State}
+%%% handle_msg(Msg,State) -> {Reply,State}
+%%% reconnect(Address,State) -> {ok,ConnectionPid,State}
+%%% terminate(ConnectionPid,State) -> term()
+%%% close(Handle) -> term()
+%%% '''
+%%%
+start(Address,InitData,CallbackMod,Opts) when is_list(Opts) ->
+ do_start(Address,InitData,CallbackMod,Opts);
start(Name,Address,InitData,CallbackMod) ->
- case ct_util:does_connection_exist(Name,Address,CallbackMod) of
- {ok,Pid} ->
- log("ct_gen_conn:start","Using existing connection!\n",[]),
- {ok,Pid};
- false ->
- Self = self(),
- Pid = spawn(fun() ->
- init_gen(Self, #gen_opts{callback=CallbackMod,
- name=Name,
- address=Address,
- init_data=InitData})
- end),
- MRef = erlang:monitor(process,Pid),
- receive
- {connected,Pid} ->
- erlang:demonitor(MRef, [flush]),
- ct_util:register_connection(Name,Address,CallbackMod,Pid),
- {ok,Pid};
- {Error,Pid} ->
- receive {'DOWN',MRef,process,_,_} -> ok end,
- Error;
- {'DOWN',MRef,process,_,Reason} ->
- log("ct_gen_conn:start",
- "Connection process died: ~p\n",
- [Reason]),
- {error,{connection_process_died,Reason}}
- end
- end.
-
+ do_start(Address,InitData,CallbackMod,[{name,Name},{old,true}]).
%%%-----------------------------------------------------------------
%%% @spec stop(Handle) -> ok
%%% Handle = handle()
%%%
-%%% @doc Close the telnet connection and stop the process managing it.
+%%% @doc Close the connection and stop the process managing it.
stop(Pid) ->
call(Pid,stop).
@@ -103,7 +127,7 @@ stop(Pid) ->
%%% @spec log(Heading,Format,Args) -> ok
%%%
%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:log/3
+%%% @see ct_logs:log/3
log(Heading,Format,Args) ->
log(log,[Heading,Format,Args]).
@@ -111,7 +135,7 @@ log(Heading,Format,Args) ->
%%% @spec start_log(Heading) -> ok
%%%
%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:start_log/1
+%%% @see ct_logs:start_log/1
start_log(Heading) ->
log(start_log,[Heading]).
@@ -119,7 +143,7 @@ start_log(Heading) ->
%%% @spec cont_log(Format,Args) -> ok
%%%
%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:cont_log/2
+%%% @see ct_logs:cont_log/2
cont_log(Format,Args) ->
log(cont_log,[Format,Args]).
@@ -127,7 +151,7 @@ cont_log(Format,Args) ->
%%% @spec end_log() -> ok
%%%
%%% @doc Log activities on the current connection (tool-internal use only).
-%%% @see ct_logs:end_log/0
+%%% @see ct_logs:end_log/0
end_log() ->
log(end_log,[]).
@@ -148,10 +172,10 @@ do_within_time(Fun,Timeout) ->
Silent = get(silent),
TmpPid = spawn_link(fun() -> put(silent,Silent),
R = Fun(),
- Self ! {self(),R}
+ Self ! {self(),R}
end),
ConnPid = get(conn_pid),
- receive
+ receive
{TmpPid,Result} ->
Result;
{'EXIT',ConnPid,_Reason}=M ->
@@ -159,7 +183,7 @@ do_within_time(Fun,Timeout) ->
exit(TmpPid,kill),
self() ! M,
{error,connection_closed}
- after
+ after
Timeout ->
exit(TmpPid,kill),
receive
@@ -176,12 +200,65 @@ do_within_time(Fun,Timeout) ->
%%%=================================================================
%%% Internal functions
+do_start(Address,InitData,CallbackMod,Opts0) ->
+ Opts = check_opts(Opts0,#gen_opts{callback=CallbackMod,
+ address=Address,
+ init_data=InitData}),
+ case ct_util:does_connection_exist(Opts#gen_opts.name,
+ Address,CallbackMod) of
+ {ok,Pid} when Opts#gen_opts.use_existing ->
+ log("ct_gen_conn:start","Using existing connection!\n",[]),
+ {ok,Pid};
+ {ok,Pid} when not Opts#gen_opts.use_existing ->
+ {error,{connection_exists,Pid}};
+ false ->
+ do_start(Opts)
+ end.
+
+do_start(Opts) ->
+ Self = self(),
+ Pid = spawn(fun() -> init_gen(Self, Opts) end),
+ MRef = erlang:monitor(process,Pid),
+ receive
+ {connected,Pid} ->
+ erlang:demonitor(MRef, [flush]),
+ ct_util:register_connection(Opts#gen_opts.name, Opts#gen_opts.address,
+ Opts#gen_opts.callback, Pid),
+ {ok,Pid};
+ {Error,Pid} ->
+ receive {'DOWN',MRef,process,_,_} -> ok end,
+ Error;
+ {'DOWN',MRef,process,_,Reason} ->
+ log("ct_gen_conn:start",
+ "Connection process died: ~p\n",
+ [Reason]),
+ {error,{connection_process_died,Reason}}
+ end.
+
+check_opts(Opts0) ->
+ check_opts(Opts0,#gen_opts{}).
+
+check_opts([{name,Name}|T],Opts) ->
+ check_opts(T,Opts#gen_opts{name=Name});
+check_opts([{reconnect,Bool}|T],Opts) ->
+ check_opts(T,Opts#gen_opts{reconnect=Bool});
+check_opts([{forward_messages,Bool}|T],Opts) ->
+ check_opts(T,Opts#gen_opts{forward=Bool});
+check_opts([{use_existing_connection,Bool}|T],Opts) ->
+ check_opts(T,Opts#gen_opts{use_existing=Bool});
+check_opts([{old,Bool}|T],Opts) ->
+ check_opts(T,Opts#gen_opts{old=Bool});
+check_opts([],Opts) ->
+ Opts.
+
call(Pid,Msg) ->
+ call(Pid,Msg,infinity).
+call(Pid,Msg,Timeout) ->
MRef = erlang:monitor(process,Pid),
Ref = make_ref(),
Pid ! {Msg,{self(),Ref}},
receive
- {Ref, Result} ->
+ {Ref, Result} ->
erlang:demonitor(MRef, [flush]),
case Result of
{retry,_Data} ->
@@ -189,8 +266,11 @@ call(Pid,Msg) ->
Other ->
Other
end;
- {'DOWN',MRef,process,_,Reason} ->
+ {'DOWN',MRef,process,_,Reason} ->
{error,{process_down,Pid,Reason}}
+ after Timeout ->
+ erlang:demonitor(MRef, [flush]),
+ exit(timeout)
end.
return({To,Ref},Result) ->
@@ -198,36 +278,47 @@ return({To,Ref},Result) ->
init_gen(Parent,Opts) ->
process_flag(trap_exit,true),
- CtUtilServer = whereis(ct_util_server),
- link(CtUtilServer),
put(silent,false),
- case catch (Opts#gen_opts.callback):init(Opts#gen_opts.name,
- Opts#gen_opts.address,
- Opts#gen_opts.init_data) of
+ try (Opts#gen_opts.callback):init(Opts#gen_opts.name,
+ Opts#gen_opts.address,
+ Opts#gen_opts.init_data) of
{ok,ConnPid,State} when is_pid(ConnPid) ->
link(ConnPid),
put(conn_pid,ConnPid),
+ CtUtilServer = whereis(ct_util_server),
+ link(CtUtilServer),
Parent ! {connected,self()},
loop(Opts#gen_opts{conn_pid=ConnPid,
cb_state=State,
ct_util_server=CtUtilServer});
{error,Reason} ->
Parent ! {{error,Reason},self()}
+ catch
+ throw:{error,Reason} ->
+ Parent ! {{error,Reason},self()}
end.
loop(Opts) ->
receive
{'EXIT',Pid,Reason} when Pid==Opts#gen_opts.conn_pid ->
- log("Connection down!\nOpening new!","Reason: ~p\nAddress: ~p\n",
- [Reason,Opts#gen_opts.address]),
- case reconnect(Opts) of
- {ok, NewPid, NewState} ->
- link(NewPid),
- put(conn_pid,NewPid),
- loop(Opts#gen_opts{conn_pid=NewPid,cb_state=NewState});
- Error ->
+ case Opts#gen_opts.reconnect of
+ true ->
+ log("Connection down!\nOpening new!",
+ "Reason: ~p\nAddress: ~p\n",
+ [Reason,Opts#gen_opts.address]),
+ case reconnect(Opts) of
+ {ok, NewPid, NewState} ->
+ link(NewPid),
+ put(conn_pid,NewPid),
+ loop(Opts#gen_opts{conn_pid=NewPid,cb_state=NewState});
+ Error ->
+ ct_util:unregister_connection(self()),
+ log("Reconnect failed. Giving up!","Reason: ~p\n",
+ [Error])
+ end;
+ false ->
ct_util:unregister_connection(self()),
- log("Reconnect failed. Giving up!","Reason: ~p\n",[Error])
+ log("Connection closed!","Reason: ~p\n",[Reason])
end;
{'EXIT',Pid,Reason} ->
case Opts#gen_opts.ct_util_server of
@@ -252,24 +343,40 @@ loop(Opts) ->
loop(Opts);
{{retry,{_Error,_Name,_CPid,Msg}}, From} ->
log("Rerunning command","Connection reestablished. Rerunning command...",[]),
- {Return,NewState} =
+ {Return,NewState} =
(Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state),
return(From, Return),
- loop(Opts#gen_opts{cb_state=NewState});
- {Msg,From={Pid,_Ref}} when is_pid(Pid) ->
- {Return,NewState} =
+ loop(Opts#gen_opts{cb_state=NewState});
+ {Msg,From={Pid,_Ref}} when is_pid(Pid), Opts#gen_opts.old==true ->
+ {Return,NewState} =
(Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state),
return(From, Return),
- loop(Opts#gen_opts{cb_state=NewState})
+ loop(Opts#gen_opts{cb_state=NewState});
+ {Msg,From={Pid,_Ref}} when is_pid(Pid) ->
+ case (Opts#gen_opts.callback):handle_msg(Msg,From,
+ Opts#gen_opts.cb_state) of
+ {reply,Reply,NewState} ->
+ return(From,Reply),
+ loop(Opts#gen_opts{cb_state=NewState});
+ {noreply,NewState} ->
+ loop(Opts#gen_opts{cb_state=NewState});
+ {stop,Reply,NewState} ->
+ ct_util:unregister_connection(self()),
+ (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid,
+ NewState),
+ return(From,Reply)
+ end;
+ Msg when Opts#gen_opts.forward==true ->
+ case (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state) of
+ {noreply,NewState} ->
+ loop(Opts#gen_opts{cb_state=NewState});
+ {stop,NewState} ->
+ ct_util:unregister_connection(self()),
+ (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid,
+ NewState)
+ end
end.
-nozero({ok,S}) when is_list(S) ->
- {ok,[C || C <- S,
- C=/=0,
- C=/=13]};
-nozero(M) ->
- M.
-
reconnect(Opts) ->
(Opts#gen_opts.callback):reconnect(Opts#gen_opts.address,
Opts#gen_opts.cb_state).
@@ -277,10 +384,8 @@ reconnect(Opts) ->
log(Func,Args) ->
case get(silent) of
- true when not ?dbg->
+ true when not ?dbg->
ok;
_ ->
apply(ct_logs,Func,Args)
end.
-
-
diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl
index 0fe6e03079..d0432b604d 100644
--- a/lib/common_test/src/ct_hooks.erl
+++ b/lib/common_test/src/ct_hooks.erl
@@ -192,12 +192,12 @@ call([{Hook, call_id, NextFun} | Rest], Config, Meta, Hooks) ->
case lists:keyfind(NewId, #ct_hook_config.id, Hooks) of
false when NextFun =:= undefined ->
{Hooks ++ [NewHook],
- [{NewId, call_init} | Rest]};
+ Rest ++ [{NewId, call_init}]};
ExistingHook when is_tuple(ExistingHook) ->
{Hooks, Rest};
_ ->
{Hooks ++ [NewHook],
- [{NewId, call_init}, {NewId,NextFun} | Rest]}
+ Rest ++ [{NewId, call_init}, {NewId,NextFun}]}
end,
call(resort(NewRest,NewHooks,Meta), Config, Meta, NewHooks)
catch Error:Reason ->
@@ -353,11 +353,10 @@ pos(Id,[_|Rest],Num) ->
pos(Id,Rest,Num+1).
-
catch_apply(M,F,A, Default) ->
try
apply(M,F,A)
- catch error:Reason ->
+ catch _:Reason ->
case erlang:get_stacktrace() of
%% Return the default if it was the CTH module which did not have the function.
[{M,F,A,_}|_] when Reason == undef ->
diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl
index a07a11eeb0..f942ce6f28 100644
--- a/lib/common_test/src/ct_logs.erl
+++ b/lib/common_test/src/ct_logs.erl
@@ -34,9 +34,10 @@
-export([set_stylesheet/2, clear_stylesheet/1]).
-export([add_external_logs/1, add_link/3]).
-export([make_last_run_index/0]).
--export([make_all_suites_index/1, make_all_runs_index/1]).
--export([get_ts_html_wrapper/3]).
--export([xhtml/2, locate_default_css_file/0, make_relative/1]).
+-export([make_all_suites_index/1,make_all_runs_index/1]).
+-export([get_ts_html_wrapper/4]).
+-export([xhtml/2, locate_priv_file/1, make_relative/1]).
+-export([insert_javascript/1]).
%% Logging stuff directly from testcase
-export([tc_log/3, tc_log/4, tc_log_async/3, tc_print/3, tc_print/4,
@@ -57,7 +58,6 @@
-define(all_runs_name, "all_runs.html").
-define(index_name, "index.html").
-define(totals_name, "totals.info").
--define(css_default, "ct_default.css").
-define(table_color1,"#ADD8E6").
-define(table_color2,"#E4F0FE").
@@ -392,7 +392,7 @@ tc_print(Category,Format,Args) ->
%%% @doc Console printout from a testcase.
%%%
%%% <p>This function is called by <code>ct</code> when printing
-%%% stuff a testcase on the user console.</p>
+%%% stuff from a testcase on the user console.</p>
tc_print(Category,Importance,Format,Args) ->
VLvl = case ct_util:get_testdata({verbosity,Category}) of
undefined ->
@@ -541,26 +541,27 @@ logger(Parent, Mode, Verbosity) ->
%% dir) so logs are independent of Common Test installation
{ok,Cwd} = file:get_cwd(),
CTPath = code:lib_dir(common_test),
- CSSFileSrc = filename:join(filename:join(CTPath, "priv"),
- ?css_default),
- CSSFileDestTop = filename:join(Cwd, ?css_default),
- CSSFileDestRun = filename:join(AbsDir, ?css_default),
- case file:copy(CSSFileSrc, CSSFileDestTop) of
- {error,Reason0} ->
+ PrivFiles = [?css_default,?jquery_script,?tablesorter_script],
+ PrivFilesSrc = [filename:join(filename:join(CTPath, "priv"), F) ||
+ F <- PrivFiles],
+ PrivFilesDestTop = [filename:join(Cwd, F) || F <- PrivFiles],
+ PrivFilesDestRun = [filename:join(AbsDir, F) || F <- PrivFiles],
+ case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of
+ {error,Src1,Dest1,Reason1} ->
io:format(user, "ERROR! "++
- "CSS file ~p could not be copied to ~p. "++
- "Reason: ~p~n",
- [CSSFileSrc,CSSFileDestTop,Reason0]),
- exit({css_file_error,CSSFileDestTop});
- _ ->
- case file:copy(CSSFileSrc, CSSFileDestRun) of
- {error,Reason1} ->
+ "Priv file ~p could not be copied to ~p. "++
+ "Reason: ~p~n",
+ [Src1,Dest1,Reason1]),
+ exit({priv_file_error,Dest1});
+ ok ->
+ case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of
+ {error,Src2,Dest2,Reason2} ->
io:format(user, "ERROR! "++
- "CSS file ~p could not be copied to ~p. "++
- "Reason: ~p~n",
- [CSSFileSrc,CSSFileDestRun,Reason1]),
- exit({css_file_error,CSSFileDestRun});
- _ ->
+ "Priv file ~p could not be copied to ~p. "++
+ "Reason: ~p~n",
+ [Src2,Dest2,Reason2]),
+ exit({priv_file_error,Dest2});
+ ok ->
ok
end
end
@@ -605,6 +606,16 @@ logger(Parent, Mode, Verbosity) ->
tc_groupleaders=[],
async_print_jobs=[]}).
+copy_priv_files([SrcF | SrcFs], [DestF | DestFs]) ->
+ case file:copy(SrcF, DestF) of
+ {error,Reason} ->
+ {error,SrcF,DestF,Reason};
+ _ ->
+ copy_priv_files(SrcFs, DestFs)
+ end;
+copy_priv_files([], []) ->
+ ok.
+
logger_loop(State) ->
receive
{log,SyncOrAsync,Pid,GL,Category,Importance,List} ->
@@ -727,14 +738,24 @@ create_io_fun(FromPid, State) ->
print_to_log(sync, FromPid, TCGL, List, State) ->
IoFun = create_io_fun(FromPid, State),
- io:format(TCGL, "~s", [lists:foldl(IoFun, [], List)]),
+ %% in some situations (exceptions), the printout is made from the
+ %% test server IO process and there's no valid group leader to send to
+ IoProc = if FromPid /= TCGL -> TCGL;
+ true -> State#logger_state.ct_log_fd
+ end,
+ io:format(IoProc, "~s", [lists:foldl(IoFun, [], List)]),
State;
print_to_log(async, FromPid, TCGL, List, State) ->
IoFun = create_io_fun(FromPid, State),
+ %% in some situations (exceptions), the printout is made from the
+ %% test server IO process and there's no valid group leader to send to
+ IoProc = if FromPid /= TCGL -> TCGL;
+ true -> State#logger_state.ct_log_fd
+ end,
Printer = fun() ->
test_server:permit_io(TCGL, self()),
- io:format(TCGL, "~s", [lists:foldl(IoFun, [], List)])
+ io:format(IoProc, "~s", [lists:foldl(IoFun, [], List)])
end,
case State#logger_state.async_print_jobs of
[] ->
@@ -839,7 +860,7 @@ set_evmgr_gl(GL) ->
open_ctlog() ->
{ok,Fd} = file:open(?ct_log_name,[write]),
- io:format(Fd, header("Common Test Framework Log"), []),
+ io:format(Fd, header("Common Test Framework Log", {[],[1,2],[]}), []),
case file:consult(ct_run:variables_file_name("../")) of
{ok,Vars} ->
io:format(Fd, config_table(Vars), []);
@@ -1149,14 +1170,14 @@ total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) ->
integer_to_list(UserSkip),integer_to_list(AutoSkip)}
end,
[xhtml("<tr valign=top>\n",
- ["<tr class=\"",odd_or_even(),"\">\n"]),
+ ["</tbody>\n<tfoot>\n<tr class=\"",odd_or_even(),"\">\n"]),
"<td><b>Total</b></td>\n", Label, TimestampCell,
"<td align=right><b>",integer_to_list(Success),"<b></td>\n",
"<td align=right><b>",integer_to_list(Fail),"<b></td>\n",
"<td align=right>",integer_to_list(AllSkip),
" (",UserSkipStr,"/",AutoSkipStr,")</td>\n",
"<td align=right><b>",integer_to_list(NotBuilt),"<b></td>\n",
- AllInfo, "</tr>\n"].
+ AllInfo, "</tr>\n</tfoot>\n"].
not_built(_BaseName,_LogDir,_All,[]) ->
0;
@@ -1213,10 +1234,12 @@ index_header(Label, StartTime) ->
Head =
case Label of
undefined ->
- header("Test Results", format_time(StartTime));
+ header("Test Results", format_time(StartTime),
+ {[],[1],[2,3,4,5]});
_ ->
header("Test Results for '" ++ Label ++ "'",
- format_time(StartTime))
+ format_time(StartTime),
+ {[],[1],[2,3,4,5]})
end,
[Head |
["<center>\n",
@@ -1228,15 +1251,17 @@ index_header(Label, StartTime) ->
"\">COMMON TEST FRAMEWORK LOG</a>\n</div>"]),
xhtml("<br>\n", "<br /><br /><br />\n"),
xhtml(["<table border=\"3\" cellpadding=\"5\" "
- "bgcolor=\"",?table_color3,"\">\n"], "<table>\n"),
+ "bgcolor=\"",?table_color3,"\">\n"],
+ ["<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n<tr>\n"]),
"<th><b>Test Name</b></th>\n",
xhtml(["<th><font color=\"",?table_color3,"\">_</font>Ok"
"<font color=\"",?table_color3,"\">_</font></th>\n"],
"<th>Ok</th>\n"),
"<th>Failed</th>\n",
"<th>Skipped", xhtml("<br>", "<br />"), "(User/Auto)</th>\n"
- "<th>Missing", xhtml("<br>", "<br />"), "Suites</th>\n"
- "\n"]].
+ "<th>Missing", xhtml("<br>", "<br />"), "Suites</th>\n",
+ xhtml("", "</tr>\n</thead>\n<tbody>\n")]].
all_suites_index_header() ->
{ok,Cwd} = file:get_cwd(),
@@ -1249,12 +1274,14 @@ all_suites_index_header(IndexDir) ->
AllRunsLink = xhtml(["<a href=\"",?all_runs_name,"\">",AllRuns,"</a>\n"],
["<div id=\"button_holder\" class=\"btn\">\n"
"<a href=\"",?all_runs_name,"\">",AllRuns,"</a>\n</div>"]),
- [header("Test Results") |
+ [header("Test Results", {[3],[1,2,8,9,10],[4,5,6,7]}) |
["<center>\n",
AllRunsLink,
xhtml("<br><br>\n", "<br /><br />\n"),
xhtml(["<table border=\"3\" cellpadding=\"5\" "
- "bgcolor=\"",?table_color2,"\">\n"], "<table>\n"),
+ "bgcolor=\"",?table_color2,"\">\n"],
+ ["<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n<tr>\n"]),
"<th>Test Name</th>\n",
"<th>Label</th>\n",
"<th>Test Run Started</th>\n",
@@ -1267,7 +1294,7 @@ all_suites_index_header(IndexDir) ->
"<th>Node</th>\n",
"<th>CT Log</th>\n",
"<th>Old Runs</th>\n",
- "\n"]].
+ xhtml("", "</tr>\n</thead>\n<tbody>\n")]].
all_runs_header() ->
{ok,Cwd} = file:get_cwd(),
@@ -1279,10 +1306,12 @@ all_runs_header() ->
"<a href=\"",?index_name,
"\">TEST INDEX PAGE</a>\n</div>"]),
xhtml("<br>\n", "<br /><br />\n")],
- [header(Title) |
+ [header(Title, {[1],[2,3,5],[4,6,7,8,9,10]}) |
["<center>\n", IxLink,
xhtml(["<table border=\"3\" cellpadding=\"5\" "
- "bgcolor=\"",?table_color1,"\">\n"], "<table>\n"),
+ "bgcolor=\"",?table_color1,"\">\n"],
+ ["<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n<tr>\n"]),
"<th><b>History</b></th>\n"
"<th><b>Node</b></th>\n"
"<th><b>Label</b></th>\n"
@@ -1294,23 +1323,29 @@ all_runs_header() ->
"<th>Ok</th>\n"),
"<th>Failed</th>\n"
"<th>Skipped<br>(User/Auto)</th>\n"
- "<th>Missing<br>Suites</th>\n"
- "\n"]].
+ "<th>Missing<br>Suites</th>\n",
+ xhtml("", "</tr>\n</thead>\n<tbody>\n")]].
-header(Title) ->
- header1(Title, "").
-header(Title, SubTitle) ->
- header1(Title, SubTitle).
+header(Title, TableCols) ->
+ header1(Title, "", TableCols).
+header(Title, SubTitle, TableCols) ->
+ header1(Title, SubTitle, TableCols).
-header1(Title, SubTitle) ->
+header1(Title, SubTitle, TableCols) ->
SubTitleHTML = if SubTitle =/= "" ->
["<center>\n",
"<h3>" ++ SubTitle ++ "</h3>\n",
xhtml("</center>\n<br>\n", "</center>\n<br />\n")];
- true -> xhtml("<br>\n", "<br />\n")
+ true -> xhtml("<br>", "<br />")
end,
CSSFile = xhtml(fun() -> "" end,
- fun() -> make_relative(locate_default_css_file()) end),
+ fun() -> make_relative(locate_priv_file(?css_default)) end),
+ JQueryFile =
+ xhtml(fun() -> "" end,
+ fun() -> make_relative(locate_priv_file(?jquery_script)) end),
+ TableSorterFile =
+ xhtml(fun() -> "" end,
+ fun() -> make_relative(locate_priv_file(?tablesorter_script)) end),
[xhtml(["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n",
"<html>\n"],
["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n",
@@ -1321,7 +1356,17 @@ header1(Title, SubTitle) ->
"<title>" ++ Title ++ " " ++ SubTitle ++ "</title>\n",
"<meta http-equiv=\"cache-control\" content=\"no-cache\">\n",
xhtml("",
- ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">"]),
+ ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">\n"]),
+ xhtml("",
+ ["<script type=\"text/javascript\" src=\"",JQueryFile,
+ "\"></script>\n"]),
+ xhtml("",
+ ["<script type=\"text/javascript\" src=\"",TableSorterFile,
+ "\"></script>\n"]),
+ xhtml(fun() -> "" end,
+ fun() -> insert_javascript({tablesorter,?sortable_table_name,
+ TableCols})
+ end),
"</head>\n",
body_tag(),
"<center>\n",
@@ -1333,6 +1378,10 @@ index_footer() ->
["</table>\n"
"</center>\n" | footer()].
+all_runs_index_footer() ->
+ ["</tbody>\n</table>\n"
+ "</center>\n" | footer()].
+
footer() ->
["<center>\n",
xhtml("<br><br>\n<hr>\n", "<br /><br />\n"),
@@ -1344,7 +1393,8 @@ footer() ->
xhtml("<br>\n", "<br />\n"),
xhtml("</font></p>\n", "</div>\n"),
"</center>\n"
- "</body>\n"].
+ "</body>\n"
+ "</html>\n"].
body_tag() ->
@@ -1360,7 +1410,7 @@ current_time() ->
format_time({{Y, Mon, D}, {H, Min, S}}) ->
Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)),
- lists:flatten(io_lib:format("~s ~s ~p ~w ~2.2.0w:~2.2.0w:~2.2.0w",
+ lists:flatten(io_lib:format("~s ~s ~2.2.0w ~w ~2.2.0w:~2.2.0w:~2.2.0w",
[Weekday, month(Mon), D, Y, H, Min, S])).
weekday(1) -> "Mon";
@@ -1486,8 +1536,12 @@ config_table_header() ->
[
xhtml(["<h2>Configuration</h2>\n"
"<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color1,"\"\n"],
- "<h4>CONFIGURATION</h4>\n<table>\n"),
- "<tr><th>Key</th><th>Value</th></tr>\n"].
+ ["<h4>CONFIGURATION</h4>\n",
+ "<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n"]),
+ "<tr><th>Key</th><th>Value</th></tr>\n",
+ xhtml("", "</thead>\n<tbody>\n")
+ ].
config_table1([{Key,Value}|Vars]) ->
[xhtml(["<tr><td>", atom_to_list(Key), "</td>\n",
@@ -1497,7 +1551,7 @@ config_table1([{Key,Value}|Vars]) ->
"<td>", io_lib:format("~p",[Value]), "</td>\n</tr>\n"]) |
config_table1(Vars)];
config_table1([]) ->
- ["</table>\n"].
+ ["</tbody>\n</table>\n"].
make_all_runs_index(When) ->
@@ -1511,7 +1565,8 @@ make_all_runs_index(When) ->
DirsSorted = (catch sort_all_runs(Dirs)),
Header = all_runs_header(),
Index = [runentry(Dir) || Dir <- DirsSorted],
- Result = file:write_file(AbsName,Header++Index++index_footer()),
+ Result = file:write_file(AbsName,Header++Index++
+ all_runs_index_footer()),
if When == start -> ok;
true -> io:put_chars("done\n")
end,
@@ -2147,34 +2202,34 @@ basic_html() ->
end.
%%%-----------------------------------------------------------------
-%%% @spec locate_default_css_file() -> CSSFile
+%%% @spec locate_priv_file(FileName) -> PrivFile
%%%
%%% @doc
%%%
-locate_default_css_file() ->
+locate_priv_file(FileName) ->
{ok,CWD} = file:get_cwd(),
- CSSFileInCwd = filename:join(CWD, ?css_default),
- case filelib:is_file(CSSFileInCwd) of
+ PrivFileInCwd = filename:join(CWD, FileName),
+ case filelib:is_file(PrivFileInCwd) of
true ->
- CSSFileInCwd;
+ PrivFileInCwd;
false ->
- CSSResultFile =
+ PrivResultFile =
case {whereis(?MODULE),self()} of
{Self,Self} ->
%% executed on the ct_logs process
- filename:join(get(ct_run_dir), ?css_default);
+ filename:join(get(ct_run_dir), FileName);
_ ->
%% executed on other process than ct_logs
{ok,RunDir} = get_log_dir(true),
- filename:join(RunDir, ?css_default)
+ filename:join(RunDir, FileName)
end,
- case filelib:is_file(CSSResultFile) of
+ case filelib:is_file(PrivResultFile) of
true ->
- CSSResultFile;
+ PrivResultFile;
false ->
%% last resort, try use css file in CT installation
CTPath = code:lib_dir(common_test),
- filename:join(filename:join(CTPath, "priv"), ?css_default)
+ filename:join(filename:join(CTPath, "priv"), FileName)
end
end.
@@ -2213,7 +2268,7 @@ make_relative1(DirTs, CwdTs) ->
%%%
%%% @doc
%%%
-get_ts_html_wrapper(TestName, PrintLabel, Cwd) ->
+get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols) ->
TestName1 = if is_list(TestName) ->
lists:flatten(TestName);
true ->
@@ -2273,17 +2328,36 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd) ->
"Open Telecom Platform</a><br />\n",
"Updated: <!date>", current_time(), "<!/date>",
"<br />\n</div>\n"],
- CSSFile = xhtml(fun() -> "" end,
- fun() -> make_relative(locate_default_css_file(), Cwd) end),
+ CSSFile =
+ xhtml(fun() -> "" end,
+ fun() -> make_relative(locate_priv_file(?css_default),
+ Cwd)
+ end),
+ JQueryFile =
+ xhtml(fun() -> "" end,
+ fun() -> make_relative(locate_priv_file(?jquery_script),
+ Cwd)
+ end),
+ TableSorterFile =
+ xhtml(fun() -> "" end,
+ fun() -> make_relative(locate_priv_file(?tablesorter_script),
+ Cwd)
+ end),
+ TableSorterScript =
+ xhtml(fun() -> "" end,
+ fun() -> insert_javascript({tablesorter,
+ ?sortable_table_name,
+ TableCols}) end),
{xhtml,
["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n",
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n",
"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n",
"<head>\n<title>", TestName1, "</title>\n",
"<meta http-equiv=\"cache-control\" content=\"no-cache\">\n",
- "<link rel=\"stylesheet\" href=\"", CSSFile, "\" type=\"text/css\">",
- "</head>\n","<body>\n",
- LabelStr, "\n"],
+ "<link rel=\"stylesheet\" href=\"", CSSFile, "\" type=\"text/css\">\n",
+ "<script type=\"text/javascript\" src=\"", JQueryFile, "\"></script>\n",
+ "<script type=\"text/javascript\" src=\"", TableSorterFile, "\"></script>\n"] ++
+ TableSorterScript ++ ["</head>\n","<body>\n", LabelStr, "\n"],
["<center>\n<br /><hr /><p>\n",
"<a href=\"", AllRuns,
"\">Test run history\n</a> | ",
@@ -2291,3 +2365,89 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd) ->
"\">Top level test index\n</a>\n</p>\n",
Copyright,"</center>\n</body>\n</html>\n"]}
end.
+
+insert_javascript({tablesorter,_TableName,undefined}) ->
+ [];
+
+insert_javascript({tablesorter,TableName,
+ {DateCols,TextCols,ValCols}}) ->
+ Headers =
+ lists:flatten(
+ lists:sort(
+ lists:flatmap(fun({Sorter,Cols}) ->
+ [lists:flatten(
+ io_lib:format(" ~w: "
+ "{ sorter: '~s' },\n",
+ [Col-1,Sorter])) || Col<-Cols]
+ end, [{"CTDateSorter",DateCols},
+ {"CTTextSorter",TextCols},
+ {"CTValSorter",ValCols}]))),
+ Headers1 = string:substr(Headers, 1, length(Headers)-2),
+
+ ["<script type=\"text/javascript\">\n",
+ "// Parser for date format, e.g: Wed Jul 4 2012 11:24:15\n",
+ "var monthNames = {};\n",
+ "monthNames[\"Jan\"] = \"01\"; monthNames[\"Feb\"] = \"02\";\n",
+ "monthNames[\"Mar\"] = \"03\"; monthNames[\"Apr\"] = \"04\";\n",
+ "monthNames[\"May\"] = \"05\"; monthNames[\"Jun\"] = \"06\";\n",
+ "monthNames[\"Jul\"] = \"07\"; monthNames[\"Aug\"] = \"08\";\n",
+ "monthNames[\"Sep\"] = \"09\"; monthNames[\"Oct\"] = \"10\";\n",
+ "monthNames[\"Nov\"] = \"11\"; monthNames[\"Dec\"] = \"12\";\n",
+ "$.tablesorter.addParser({\n",
+ " id: 'CTDateSorter',\n",
+ " is: function(s) {\n",
+ " return false; },\n",
+ " format: function(s) {\n",
+ %% place empty cells, "-" and "?" at the bottom
+ " if (s.length < 2) return 999999999;\n",
+ " else {\n",
+ %% match out each date element
+ " var date = s.match(/(\\w{3})\\s(\\w{3})\\s(\\d{2})\\s(\\d{4})\\s(\\d{2}):(\\d{2}):(\\d{2})/);\n",
+ " var y = date[4]; var mo = monthNames[date[2]]; var d = String(date[3]);\n",
+ " var h = String(date[5]); var mi = String(date[6]); var sec = String(date[7]);\n",
+ " return (parseInt('' + y + mo + d + h + mi + sec)); }},\n",
+ " type: 'numeric' });\n",
+
+ "// Parser for general text format\n",
+ "$.tablesorter.addParser({\n",
+ " id: 'CTTextSorter',\n",
+ " is: function(s) {\n",
+ " return false; },\n",
+ " format: function(s) {\n",
+ %% place empty cells, "?" and "-" at the bottom
+ " if (s.length < 1) return 'zzzzzzzz';\n",
+ " else if (s == \"?\") return 'zzzzzzz';\n",
+ " else if (s == \"-\") return 'zzzzzz';\n",
+ " else if (s == \"FAILED\") return 'A';\n",
+ " else if (s == \"SKIPPED\") return 'B';\n",
+ " else if (s == \"OK\") return 'C';\n",
+ " else return '' + s; },\n",
+ " type: 'text' });\n",
+
+ "// Parser for numerical values\n",
+ "$.tablesorter.addParser({\n",
+ " id: 'CTValSorter',\n",
+ " is: function(s) {\n",
+ " return false; },\n",
+ " format: function(s) {\n"
+ %% place empty cells and "?" at the bottom
+ " if (s.length < 1) return '-2';\n",
+ " else if (s == \"?\") return '-1';\n",
+ %% look for skip value, eg "3 (2/1)"
+ " else if ((s.search(/(\\d{1,})\\s/)) >= 0) {\n",
+ " var num = s.match(/(\\d{1,})\\s/);\n",
+ %% return only the total skip value for sorting
+ " return (parseInt('' + num[1])); }\n",
+ " else if ((s.search(/(\\d{1,})\\.(\\d{3})s/)) >= 0) {\n",
+ " var num = s.match(/(\\d{1,})\\.(\\d{3})/);\n",
+ " if (num[1] == \"0\") return (parseInt('' + num[2]));\n",
+ " else return (parseInt('' + num[1] + num[2])); }\n",
+ " else return '' + s; },\n",
+ " type: 'numeric' });\n",
+
+ "$(document).ready(function() {\n",
+ " $(\"#",TableName,"\").tablesorter({\n",
+ " headers: { \n", Headers1, "\n }\n });\n",
+ " $(\"#",TableName,"\").trigger(\"update\");\n",
+ " $(\"#",TableName,"\").trigger(\"appendCache\");\n",
+ "});\n</script>\n"].
diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl
index 2a951bc5cf..9e61d5b16f 100644
--- a/lib/common_test/src/ct_master_logs.erl
+++ b/lib/common_test/src/ct_master_logs.erl
@@ -26,6 +26,8 @@
-export([start/2, make_all_runs_index/0, log/3, nodedir/2,
stop/0]).
+-include("ct_util.hrl").
+
-record(state, {log_fd, start_time, logdir, rundir,
nodedir_ix_fd, nodes, nodedirs=[]}).
@@ -33,7 +35,6 @@
-define(all_runs_name, "master_runs.html").
-define(nodedir_index_name, "index.html").
-define(details_file_name,"details.info").
--define(css_default, "ct_default.css").
-define(table_color,"lightblue").
%%%--------------------------------------------------------------------
@@ -95,29 +96,30 @@ init(Parent,LogDir,Nodes) ->
put(basic_html, true);
BasicHtml ->
put(basic_html, BasicHtml),
- %% copy stylesheet to log dir (both top dir and test run
+ %% copy priv files to log dir (both top dir and test run
%% dir) so logs are independent of Common Test installation
CTPath = code:lib_dir(common_test),
- CSSFileSrc = filename:join(filename:join(CTPath, "priv"),
- ?css_default),
- CSSFileDestTop = filename:join(LogDir, ?css_default),
- CSSFileDestRun = filename:join(RunDirAbs, ?css_default),
- case file:copy(CSSFileSrc, CSSFileDestTop) of
- {error,Reason0} ->
+ PrivFiles = [?css_default,?jquery_script,?tablesorter_script],
+ PrivFilesSrc = [filename:join(filename:join(CTPath, "priv"), F) ||
+ F <- PrivFiles],
+ PrivFilesDestTop = [filename:join(LogDir, F) || F <- PrivFiles],
+ PrivFilesDestRun = [filename:join(RunDirAbs, F) || F <- PrivFiles],
+ case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of
+ {error,Src1,Dest1,Reason1} ->
io:format(user, "ERROR! "++
- "CSS file ~p could not be copied to ~p. "++
+ "Priv file ~p could not be copied to ~p. "++
"Reason: ~p~n",
- [CSSFileSrc,CSSFileDestTop,Reason0]),
- exit({css_file_error,CSSFileDestTop});
- _ ->
- case file:copy(CSSFileSrc, CSSFileDestRun) of
- {error,Reason1} ->
+ [Src1,Dest1,Reason1]),
+ exit({priv_file_error,Dest1});
+ ok ->
+ case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of
+ {error,Src2,Dest2,Reason2} ->
io:format(user, "ERROR! "++
- "CSS file ~p could not be copied to ~p. "++
+ "Priv file ~p could not be copied to ~p. "++
"Reason: ~p~n",
- [CSSFileSrc,CSSFileDestRun,Reason1]),
- exit({css_file_error,CSSFileDestRun});
- _ ->
+ [Src2,Dest2,Reason2]),
+ exit({priv_file_error,Dest2});
+ ok ->
ok
end
end
@@ -146,6 +148,16 @@ init(Parent,LogDir,Nodes) ->
{N,""}
end,Nodes)}).
+copy_priv_files([SrcF | SrcFs], [DestF | DestFs]) ->
+ case file:copy(SrcF, DestF) of
+ {error,Reason} ->
+ {error,SrcF,DestF,Reason};
+ _ ->
+ copy_priv_files(SrcFs, DestFs)
+ end;
+copy_priv_files([], []) ->
+ ok.
+
loop(State) ->
receive
{log,_From,List} ->
@@ -190,7 +202,7 @@ loop(State) ->
open_ct_master_log(Dir) ->
FullName = filename:join(Dir,?ct_master_log_name),
{ok,Fd} = file:open(FullName,[write]),
- io:format(Fd,header("Common Test Master Log"),[]),
+ io:format(Fd,header("Common Test Master Log", {[],[1,2],[]}),[]),
%% maybe add config info here later
io:format(Fd, config_table([]), []),
io:format(Fd,
@@ -216,11 +228,14 @@ config_table(Vars) ->
config_table_header() ->
["<h2>Configuration</h2>\n",
xhtml(["<table border=\"3\" cellpadding=\"5\" "
- "bgcolor=\"",?table_color,"\"\n"], "<table>\n"),
- "<tr><th>Key</th><th>Value</th></tr>\n"].
+ "bgcolor=\"",?table_color,"\"\n"],
+ ["<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n"]),
+ "<tr><th>Key</th><th>Value</th></tr>\n",
+ xhtml("", "</thead>\n<tbody>\n")].
config_table1([]) ->
- ["</table>\n"].
+ ["</tbody>\n</table>\n"].
int_header() ->
"<div class=\"ct_internal\"><b>*** CT MASTER ~s *** ~s</b>".
@@ -250,14 +265,16 @@ close_nodedir_index(Fd) ->
file:close(Fd).
nodedir_index_header(StartTime) ->
- [header("Log Files " ++ format_time(StartTime)) |
+ [header("Log Files " ++ format_time(StartTime), {[],[1,2],[]}) |
["<center>\n",
"<p><a href=\"",?ct_master_log_name,"\">Common Test Master Log</a></p>",
xhtml(["<table border=\"3\" cellpadding=\"5\" "
- "bgcolor=\"",?table_color,"\">\n"], "<table>\n"),
+ "bgcolor=\"",?table_color,"\">\n"],
+ ["<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n<tr>\n"]),
"<th><b>Node</b></th>\n",
"<th><b>Log</b></th>\n",
- "\n"]].
+ xhtml("", "</tr>\n</thead>\n<tbody>\n")]].
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% All Run Index functions %%%
@@ -315,14 +332,16 @@ runentry(Dir) ->
"</tr>\n"].
all_runs_header() ->
- [header("Master Test Runs") |
+ [header("Master Test Runs", {[1],[2,3],[]}) |
["<center>\n",
xhtml(["<table border=\"3\" cellpadding=\"5\" "
- "bgcolor=\"",?table_color,"\">\n"], "<table>\n"),
+ "bgcolor=\"",?table_color,"\">\n"],
+ ["<table id=\"",?sortable_table_name,"\">\n",
+ "<thead>\n<tr>\n"]),
"<th><b>History</b></th>\n"
"<th><b>Master Host</b></th>\n"
- "<th><b>Test Nodes</b></th>\n"
- "\n"]].
+ "<th><b>Test Nodes</b></th>\n",
+ xhtml("", "</tr></thead>\n<tbody>\n")]].
timestamp(Dir) ->
[S,Min,H,D,M,Y|_] = lists:reverse(string:tokens(Dir,".-_")),
@@ -346,9 +365,16 @@ read_details_file(Dir) ->
%%% Internal functions
%%%--------------------------------------------------------------------
-header(Title) ->
+header(Title, TableCols) ->
CSSFile = xhtml(fun() -> "" end,
- fun() -> make_relative(locate_default_css_file()) end),
+ fun() -> make_relative(locate_priv_file(?css_default)) end),
+ JQueryFile =
+ xhtml(fun() -> "" end,
+ fun() -> make_relative(locate_priv_file(?jquery_script)) end),
+ TableSorterFile =
+ xhtml(fun() -> "" end,
+ fun() -> make_relative(locate_priv_file(?tablesorter_script)) end),
+
[xhtml(["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n",
"<html>\n"],
["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n",
@@ -360,6 +386,16 @@ header(Title) ->
"<meta http-equiv=\"cache-control\" content=\"no-cache\">\n",
xhtml("",
["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">"]),
+ xhtml("",
+ ["<script type=\"text/javascript\" src=\"",JQueryFile,
+ "\"></script>\n"]),
+ xhtml("",
+ ["<script type=\"text/javascript\" src=\"",TableSorterFile,
+ "\"></script>\n"]),
+ xhtml(fun() -> "" end,
+ fun() -> ct_logs:insert_javascript({tablesorter,
+ ?sortable_table_name,
+ TableCols}) end),
"</head>\n",
body_tag(),
"<center>\n",
@@ -367,7 +403,7 @@ header(Title) ->
"</center>\n"].
index_footer() ->
- ["</table>\n"
+ ["</tbody>\n</table>\n"
"</center>\n" | footer()].
footer() ->
@@ -393,7 +429,7 @@ current_time() ->
format_time({{Y, Mon, D}, {H, Min, S}}) ->
Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)),
- lists:flatten(io_lib:format("~s ~s ~p ~w ~2.2.0w:~2.2.0w:~2.2.0w",
+ lists:flatten(io_lib:format("~s ~s ~2.2.0w ~w ~2.2.0w:~2.2.0w:~2.2.0w",
[Weekday, month(Mon), D, Y, H, Min, S])).
weekday(1) -> "Mon";
@@ -446,8 +482,8 @@ basic_html() ->
xhtml(HTML, XHTML) ->
ct_logs:xhtml(HTML, XHTML).
-locate_default_css_file() ->
- ct_logs:locate_default_css_file().
+locate_priv_file(File) ->
+ ct_logs:locate_priv_file(File).
make_relative(Dir) ->
ct_logs:make_relative(Dir).
diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl
new file mode 100644
index 0000000000..d9c4a962dc
--- /dev/null
+++ b/lib/common_test/src/ct_netconfc.erl
@@ -0,0 +1,1828 @@
+%%----------------------------------------------------------------------
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 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%
+%%
+%%----------------------------------------------------------------------
+%% File: ct_netconfc.erl
+%%
+%% Description:
+%% This file contains the Netconf client interface
+%%
+%% @author Support
+%%
+%% @doc Netconf client module.
+%%
+%% <p>The Netconf client is compliant with RFC4741 and RFC4742.</p>
+%%
+%% <p> For each server to test against, the following entry can be
+%% added to a configuration file:</p>
+%%
+%% <p>`{server_id(),options()}.'</p>
+%%
+%% <p> The `server_id()' or an associated `target_name()' (see
+%% {@link ct}) shall then be used in calls to {@link open/2}.</p>
+%%
+%% <p>If no configuration exists for a server, a session can still be
+%% opened by calling {@link open/2} with all necessary options given
+%% in the call. The first argument to {@link open/2} can then be any
+%% atom.</p>
+%%
+%% == Logging ==
+%%
+%% The netconf server uses the `error_logger' for logging of netconf
+%% traffic. A special purpose error handler is implemented in
+%% `ct_conn_log_h'. To use this error handler, add the `cth_conn_log'
+%% hook in your test suite, e.g.
+%%
+%% ```
+%% suite() ->
+%% [{ct_hooks, [{cth_conn_log, [{conn_mod(),hook_options()}]}]}].
+%%'''
+%%
+%% The `conn_mod()' is the name of the common_test module implementing
+%% the connection protocol, e.g. `ct_netconfc'.
+%%
+%% The hook option `log_type' specifies the type of logging:
+%%
+%% <dl>
+%% <dt>`raw'</dt>
+%% <dd>The sent and received netconf data is logged to a separate
+%% text file as is without any formatting. A link to the file is
+%% added to the test case HTML log.</dd>
+%%
+%% <dt>`pretty'</dt>
+%% <dd>The sent and received netconf data is logged to a separate
+%% text file with XML data nicely indented. A link to the file is
+%% added to the test case HTML log.</dd>
+%%
+%% <dt>`html (default)'</dt>
+%% <dd>The sent and received netconf traffic is pretty printed
+%% directly in the test case HTML log.</dd>
+%%
+%% <dt>`silent'</dt>
+%% <dd>Netconf traffic is not logged.</dd>
+%% </dl>
+%%
+%% By default, all netconf traffic is logged in one single log
+%% file. However, it is possible to have different connections logged
+%% in separate files. To do this, use the hook option `hosts' and
+%% list the names of the servers/connections that will be used in the
+%% suite. Note that the connections must be named for this to work,
+%% i.e. they must be opened with {@link open/2}.
+%%
+%% The `hosts' option has no effect if `log_type' is set to `html' or
+%% `silent'.
+%%
+%% The hook options can also be specified in a configuration file with
+%% the configuration variable `ct_conn_log':
+%%
+%% ```
+%% {ct_conn_log,[{conn_mod(),hook_options()}]}.
+%% '''
+%%
+%% For example:
+%%
+%% ```
+%% {ct_conn_log,[{ct_netconfc,[{log_type,pretty},
+%% {hosts,[key_or_name()]}]}]}
+%% '''
+%%
+%% <b>Note</b> that hook options specified in a configuration file
+%% will overwrite the hardcoded hook options in the test suite.
+%%
+%% === Logging example 1 ===
+%%
+%% The following `ct_hooks' statement will cause pretty printing of
+%% netconf traffic to separate logs for the connections named
+%% `nc_server1' and `nc_server2'. Any other connections will be logged
+%% to default netconf log.
+%%
+%% ```
+%% suite() ->
+%% [{ct_hooks, [{cth_conn_log, [{ct_netconfc,[{log_type,pretty}},
+%% {hosts,[nc_server1,nc_server2]}]}
+%% ]}]}].
+%%'''
+%%
+%% Connections must be opened like this:
+%%
+%% ```
+%% open(nc_server1,[...]),
+%% open(nc_server2,[...]).
+%% '''
+%%
+%% === Logging example 2 ===
+%%
+%% The following configuration file will cause raw logging of all
+%% netconf traffic into one single text file.
+%%
+%% ```
+%% {ct_conn_log,[{ct_netconfc,[{log_type,raw}]}]}.
+%% '''
+%%
+%% The `ct_hooks' statement must look like this:
+%%
+%% ```
+%% suite() ->
+%% [{ct_hooks, [{cth_conn_log, []}]}].
+%% '''
+%%
+%% The same `ct_hooks' statement without the configuration file would
+%% cause HTML logging of all netconf connections into the test case
+%% HTML log.
+%%
+%% == Notifications ==
+%%
+%% The netconf client is also compliant with RFC5277 NETCONF Event
+%% Notifications, which defines a mechanism for an asynchronous
+%% message notification delivery service for the netconf protocol.
+%%
+%% Specific functions to support this are {@link
+%% create_subscription/6} and {@link get_event_streams/3}. (The
+%% functions also exist with other arities.)
+%%
+%% @end
+%%----------------------------------------------------------------------
+-module(ct_netconfc).
+
+-include("ct_netconfc.hrl").
+-include("ct_util.hrl").
+-include_lib("xmerl/include/xmerl.hrl").
+
+%%----------------------------------------------------------------------
+%% External exports
+%%----------------------------------------------------------------------
+-export([open/1,
+ open/2,
+ only_open/1,
+ only_open/2,
+ hello/1,
+ hello/2,
+ close_session/1,
+ close_session/2,
+ kill_session/2,
+ kill_session/3,
+ send/2,
+ send/3,
+ send_rpc/2,
+ send_rpc/3,
+ lock/2,
+ lock/3,
+ unlock/2,
+ unlock/3,
+ get/2,
+ get/3,
+ get_config/3,
+ get_config/4,
+ edit_config/3,
+ edit_config/4,
+ delete_config/2,
+ delete_config/3,
+ copy_config/3,
+ copy_config/4,
+ action/2,
+ action/3,
+ create_subscription/1,
+ create_subscription/2,
+ create_subscription/3,
+ create_subscription/4,
+ create_subscription/5,
+ create_subscription/6,
+ get_event_streams/2,
+ get_event_streams/3,
+ get_capabilities/1,
+ get_capabilities/2,
+ get_session_id/1,
+ get_session_id/2]).
+
+%%----------------------------------------------------------------------
+%% Exported types
+%%----------------------------------------------------------------------
+-export_type([hook_options/0,
+ conn_mod/0,
+ log_type/0,
+ key_or_name/0,
+ notification/0]).
+
+%%----------------------------------------------------------------------
+%% Internal exports
+%%----------------------------------------------------------------------
+%% ct_gen_conn callbacks
+-export([init/3,
+ handle_msg/3,
+ handle_msg/2,
+ terminate/2,
+ close/1]).
+
+%% ct_conn_log callback
+-export([format_data/2]).
+
+%%----------------------------------------------------------------------
+%% Internal defines
+%%----------------------------------------------------------------------
+-define(APPLICATION,?MODULE).
+-define(VALID_SSH_OPTS,[user, password, user_dir]).
+-define(DEFAULT_STREAM,"NETCONF").
+
+-define(error(ConnName,Report),
+ error_logger:error_report([{ct_connection,ConnName},
+ {client,self()},
+ {module,?MODULE},
+ {line,?LINE} |
+ Report])).
+
+-define(is_timeout(T), (is_integer(T) orelse T==infinity)).
+-define(is_filter(F),
+ (is_atom(F) orelse (is_tuple(F) andalso is_atom(element(1,F))))).
+-define(is_string(S), (is_list(S) andalso is_integer(hd(S)))).
+
+%%----------------------------------------------------------------------
+%% Records
+%%----------------------------------------------------------------------
+%% Client state
+-record(state, {host,
+ port,
+ connection, % #connection
+ capabilities,
+ session_id,
+ msg_id = 1,
+ hello_status,
+ buff = <<>>,
+ pending = [], % [#pending]
+ event_receiver}).% pid
+
+%% Run-time client options.
+-record(options, {ssh = [], % Options for the ssh application
+ host,
+ port = ?DEFAULT_PORT,
+ timeout = ?DEFAULT_TIMEOUT,
+ name}).
+
+%% Connection reference
+-record(connection, {reference, % {CM,Ch}
+ host,
+ port,
+ name}).
+
+%% Pending replies from server
+-record(pending, {tref, % timer ref (returned from timer:xxx)
+ ref, % pending ref
+ msg_id,
+ op,
+ caller}).% pid which sent the request
+
+%%----------------------------------------------------------------------
+%% Type declarations
+%%----------------------------------------------------------------------
+-type client() :: handle() | server_id() | target_name().
+-type handle() :: term().
+%% An opaque reference for a connection (netconf session). See {@link
+%% ct} for more information.
+
+-type server_id() :: atom().
+%% A `ServerId' which exists in a configuration file.
+-type target_name() :: atom().
+%% A name which is associated to a `server_id()' via a
+%% `require' statement or a call to {@link ct:require/2} in the
+%% test suite.
+-type key_or_name() :: server_id() | target_name().
+
+-type options() :: [option()].
+%% Options used for setting up ssh connection to a netconf server.
+
+-type option() :: {ssh,host()} | {port,inet:port_number()} | {user,string()} |
+ {password,string()} | {user_dir,string()} |
+ {timeout,timeout()}.
+-type host() :: inet:host_name() | inet:ip_address().
+
+-type notification() :: {notification, xml_attributes(), notification_content()}.
+-type notification_content() :: [event_time()|simple_xml()].
+-type event_time() :: {eventTime,xml_attributes(),[xs_datetime()]}.
+
+-type stream_name() :: string().
+-type streams() :: [{stream_name(),[stream_data()]}].
+-type stream_data() :: {description,string()} |
+ {replaySupport,string()} |
+ {replayLogCreationTime,string()} |
+ {replayLogAgedTime,string()}.
+%% See XML Schema for Event Notifications found in RFC5277 for further
+%% detail about the data format for the string values.
+
+-type hook_options() :: [hook_option()].
+%% Options that can be given to `cth_conn_log' in the `ct_hook' statement.
+-type hook_option() :: {log_type,log_type()} |
+ {hosts,[key_or_name()]}.
+-type log_type() :: raw | pretty | html | silent.
+%-type error_handler() :: module().
+-type conn_mod() :: ct_netconfc.
+
+-type error_reason() :: term().
+
+-type simple_xml() :: {xml_tag(), xml_attributes(), xml_content()} |
+ {xml_tag(), xml_content()} |
+ xml_tag().
+%% <p>This type is further described in the documentation for the
+%% <tt>Xmerl</tt> application.</p>
+-type xml_tag() :: atom().
+-type xml_attributes() :: [{xml_attribute_tag(),xml_attribute_value()}].
+-type xml_attribute_tag() :: atom().
+-type xml_attribute_value() :: string().
+-type xml_content() :: [simple_xml() | iolist()].
+-type xpath() :: {xpath,string()}.
+
+-type netconf_db() :: running | startup | candidate.
+-type xs_datetime() :: string().
+%% This date and time identifyer has the same format as the XML type
+%% dateTime and compliant to RFC3339. The format is
+%% ```[-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]'''
+
+%%----------------------------------------------------------------------
+%% External interface functions
+%%----------------------------------------------------------------------
+
+%%----------------------------------------------------------------------
+-spec open(Options) -> Result when
+ Options :: options(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+%% @doc Open a netconf session and exchange `hello' messages.
+%%
+%% If the server options are specified in a configuration file, or if
+%% a named client is needed for logging purposes (see {@section
+%% Logging}) use {@link open/2} instead.
+%%
+%% The opaque `handler()' reference which is returned from this
+%% function is required as client identifier when calling any other
+%% function in this module.
+%%
+%% The `timeout' option (milli seconds) is used when setting up
+%% the ssh connection and when waiting for the hello message from the
+%% server. It is not used for any other purposes during the lifetime
+%% of the connection.
+%%
+%% @end
+%%----------------------------------------------------------------------
+open(Options) ->
+ open(Options,#options{},[],true).
+
+%%----------------------------------------------------------------------
+-spec open(KeyOrName, ExtraOptions) -> Result when
+ KeyOrName :: key_or_name(),
+ ExtraOptions :: options(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+%% @doc Open a named netconf session and exchange `hello' messages.
+%%
+%% If `KeyOrName' is a configured `server_id()' or a
+%% `target_name()' associated with such an ID, then the options
+%% for this server will be fetched from the configuration file.
+%
+%% The `ExtraOptions' argument will be added to the options found in
+%% the configuration file. If the same options are given, the values
+%% from the configuration file will overwrite `ExtraOptions'.
+%%
+%% If the server is not specified in a configuration file, use {@link
+%% open/1} instead.
+%%
+%% The opaque `handle()' reference which is returned from this
+%% function can be used as client identifier when calling any other
+%% function in this module. However, if `KeyOrName' is a
+%% `target_name()', i.e. if the server is named via a call to
+%% `ct:require/2' or a `require' statement in the test
+%% suite, then this name may be used instead of the `handle()'.
+%%
+%% The `timeout' option (milli seconds) is used when setting up
+%% the ssh connection and when waiting for the hello message from the
+%% server. It is not used for any other purposes during the lifetime
+%% of the connection.
+%%
+%% @end
+%%----------------------------------------------------------------------
+open(KeyOrName, ExtraOpts) ->
+ open(KeyOrName, ExtraOpts, true).
+
+open(KeyOrName, ExtraOpts, Hello) ->
+ SortedExtra = lists:keysort(1,ExtraOpts),
+ SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])),
+ AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra),
+ open(AllOpts,#options{name=KeyOrName},[{name,KeyOrName}],Hello).
+
+open(OptList,InitOptRec,NameOpt,Hello) ->
+ case check_options(OptList,undefined,undefined,InitOptRec) of
+ {Host,Port,Options} ->
+ case ct_gen_conn:start({Host,Port},Options,?MODULE,
+ NameOpt ++ [{reconnect,false},
+ {use_existing_connection,false},
+ {forward_messages,true}]) of
+ {ok,Client} when Hello==true ->
+ case hello(Client,Options#options.timeout) of
+ ok ->
+ {ok,Client};
+ Error ->
+ Error
+ end;
+ Other ->
+ Other
+ end;
+ Error ->
+ Error
+ end.
+
+
+%%----------------------------------------------------------------------
+-spec only_open(Options) -> Result when
+ Options :: options(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+%% @doc Open a netconf session, but don't send `hello'.
+%%
+%% As {@link open/1} but does not send a `hello' message.
+%%
+%% @end
+%%----------------------------------------------------------------------
+only_open(Options) ->
+ open(Options,#options{},[],false).
+
+%%----------------------------------------------------------------------
+-spec only_open(KeyOrName,ExtraOptions) -> Result when
+ KeyOrName :: key_or_name(),
+ ExtraOptions :: options(),
+ Result :: {ok,handle()} | {error,error_reason()}.
+%% @doc Open a name netconf session, but don't send `hello'.
+%%
+%% As {@link open/2} but does not send a `hello' message.
+%%
+%% @end
+%%----------------------------------------------------------------------
+only_open(KeyOrName, ExtraOpts) ->
+ open(KeyOrName, ExtraOpts, false).
+
+%%----------------------------------------------------------------------
+%% @spec hello(Client) -> Result
+%% @equiv hello(Client, infinity)
+hello(Client) ->
+ hello(Client,?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec hello(Client,Timeout) -> Result when
+ Client :: handle(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Exchange `hello' messages with the server.
+%%
+%% Sends a `hello' message to the server and waits for the return.
+%%
+%% @end
+%%----------------------------------------------------------------------
+hello(Client,Timeout) ->
+ call(Client, {hello, Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec get_session_id(Client) -> Result
+%% @equiv get_session_id(Client, infinity)
+get_session_id(Client) ->
+ get_session_id(Client, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec get_session_id(Client, Timeout) -> Result when
+ Client :: client(),
+ Timeout :: timeout(),
+ Result :: pos_integer() | {error,error_reason()}.
+%% @doc Returns the session id associated with the given client.
+%%
+%% @end
+%%----------------------------------------------------------------------
+get_session_id(Client, Timeout) ->
+ call(Client, get_session_id, Timeout).
+
+%%----------------------------------------------------------------------
+%% @spec get_capabilities(Client) -> Result
+%% @equiv get_capabilities(Client, infinity)
+get_capabilities(Client) ->
+ get_capabilities(Client, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec get_capabilities(Client, Timeout) -> Result when
+ Client :: client(),
+ Timeout :: timeout(),
+ Result :: [string()] | {error,error_reason()}.
+%% @doc Returns the server side capabilities
+%%
+%% The following capability identifiers, defined in RFC 4741, can be returned:
+%%
+%% <ul>
+%% <li>`"urn:ietf:params:netconf:base:1.0"'</li>
+%% <li>`"urn:ietf:params:netconf:capability:writable-running:1.0"'</li>
+%% <li>`"urn:ietf:params:netconf:capability:candidate:1.0"'</li>
+%% <li>`"urn:ietf:params:netconf:capability:confirmed-commit:1.0"'</li>
+%% <li>`"urn:ietf:params:netconf:capability:rollback-on-error:1.0"'</li>
+%% <li>`"urn:ietf:params:netconf:capability:startup:1.0"'</li>
+%% <li>`"urn:ietf:params:netconf:capability:url:1.0"'</li>
+%% <li>`"urn:ietf:params:netconf:capability:xpath:1.0"'</li>
+%% </ul>
+%%
+%% Note, additional identifiers may exist, e.g. server side namespace.
+%%
+%% @end
+%%----------------------------------------------------------------------
+get_capabilities(Client, Timeout) ->
+ call(Client, get_capabilities, Timeout).
+
+%% @private
+send(Client, SimpleXml) ->
+ send(Client, SimpleXml, ?DEFAULT_TIMEOUT).
+%% @private
+send(Client, SimpleXml, Timeout) ->
+ call(Client,{send, Timeout, SimpleXml}).
+
+%% @private
+send_rpc(Client, SimpleXml) ->
+ send_rpc(Client, SimpleXml, ?DEFAULT_TIMEOUT).
+%% @private
+send_rpc(Client, SimpleXml, Timeout) ->
+ call(Client,{send_rpc, SimpleXml, Timeout}).
+
+
+
+%%----------------------------------------------------------------------
+%% @spec lock(Client, Target) -> Result
+%% @equiv lock(Client, Target, infinity)
+lock(Client, Target) ->
+ lock(Client, Target,?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec lock(Client, Target, Timeout) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Unlock configuration target.
+%%
+%% Which target parameters that can be used depends on if
+%% `:candidate' and/or `:startup' are supported by the
+%% server. If successfull, the configuration system of the device is
+%% not available to other clients (Netconf, CORBA, SNMP etc). Locks
+%% are intended to be short-lived.
+%%
+%% The operations {@link kill_session/2} or {@link kill_session/3} can
+%% be used to force the release of a lock owned by another Netconf
+%% session. How this is achieved by the server side is implementation
+%% specific.
+%%
+%% @end
+%%----------------------------------------------------------------------
+lock(Client, Target, Timeout) ->
+ call(Client,{send_rpc_op,lock,[Target],Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec unlock(Client, Target) -> Result
+%% @equiv unlock(Client, Target, infinity)
+unlock(Client, Target) ->
+ unlock(Client, Target,?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec unlock(Client, Target, Timeout) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Unlock configuration target.
+%%
+%% If the client earlier has aquired a lock, via {@link lock/2} or
+%% {@link lock/3}, this operation release the associated lock. To be
+%% able to access another target than `running', the server must
+%% support `:candidate' and/or `:startup'.
+%%
+%% @end
+%%----------------------------------------------------------------------
+unlock(Client, Target, Timeout) ->
+ call(Client, {send_rpc_op, unlock, [Target], Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec get(Client, Filter) -> Result
+%% @equiv get(Client, Filter, infinity)
+get(Client, Filter) ->
+ get(Client, Filter, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec get(Client, Filter, Timeout) -> Result when
+ Client :: client(),
+ Filter :: simple_xml() | xpath(),
+ Timeout :: timeout(),
+ Result :: {ok,simple_xml()} | {error,error_reason()}.
+%% @doc Get data.
+%%
+%% This operation returns both configuration and state data from the
+%% server.
+%%
+%% Filter type `xpath' can only be used if the server supports
+%% `:xpath'.
+%%
+%% @end
+%%----------------------------------------------------------------------
+get(Client, Filter, Timeout) ->
+ call(Client,{send_rpc_op, get, [Filter], Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec get_config(Client, Source, Filter) -> Result
+%% @equiv get_config(Client, Source, Filter, infinity)
+get_config(Client, Source, Filter) ->
+ get_config(Client, Source, Filter, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec get_config(Client, Source, Filter, Timeout) -> Result when
+ Client :: client(),
+ Source :: netconf_db(),
+ Filter :: simple_xml() | xpath(),
+ Timeout :: timeout(),
+ Result :: {ok,simple_xml()} | {error,error_reason()}.
+%% @doc Get configuration data.
+%%
+%% To be able to access another source than `running', the server
+%% must advertise `:candidate' and/or `:startup'.
+%%
+%% Filter type `xpath' can only be used if the server supports
+%% `:xpath'.
+%%
+%%
+%% @end
+%%----------------------------------------------------------------------
+get_config(Client, Source, Filter, Timeout) ->
+ call(Client, {send_rpc_op, get_config, [Source, Filter], Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec edit_config(Client, Target, Config) -> Result
+%% @equiv edit_config(Client, Target, Config, infinity)
+edit_config(Client, Target, Config) ->
+ edit_config(Client, Target, Config, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec edit_config(Client, Target, Config, Timeout) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Config :: simple_xml(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Edit configuration data.
+%%
+%% Per default only the running target is available, unless the server
+%% include `:candidate' or `:startup' in its list of
+%% capabilities.
+%%
+%% @end
+%%----------------------------------------------------------------------
+edit_config(Client, Target, Config, Timeout) ->
+ call(Client, {send_rpc_op, edit_config, [Target,Config], Timeout}).
+
+
+%%----------------------------------------------------------------------
+%% @spec delete_config(Client, Target) -> Result
+%% @equiv delete_config(Client, Target, infinity)
+delete_config(Client, Target) ->
+ delete_config(Client, Target, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec delete_config(Client, Target, Timeout) -> Result when
+ Client :: client(),
+ Target :: startup | candidate,
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Delete configuration data.
+%%
+%% The running configuration cannot be deleted and `:candidate'
+%% or `:startup' must be advertised by the server.
+%%
+%% @end
+%%----------------------------------------------------------------------
+delete_config(Client, Target, Timeout) when Target == startup;
+ Target == candidate ->
+ call(Client,{send_rpc_op, delete_config, [Target], Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec copy_config(Client, Source, Target) -> Result
+%% @equiv copy_config(Client, Source, Target, infinity)
+copy_config(Client, Source, Target) ->
+ copy_config(Client, Source, Target, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec copy_config(Client, Target, Source, Timeout) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Source :: netconf_db(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Copy configuration data.
+%%
+%% Which source and target options that can be issued depends on the
+%% capabilities supported by the server. I.e. `:candidate' and/or
+%% `:startup' are required.
+%%
+%% @end
+%%----------------------------------------------------------------------
+copy_config(Client, Target, Source, Timeout) ->
+ call(Client,{send_rpc_op, copy_config, [Target, Source], Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec action(Client, Action) -> Result
+%% @equiv action(Client, Action, infinity)
+action(Client,Action) ->
+ action(Client,Action,?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec action(Client, Action, Timeout) -> Result when
+ Client :: client(),
+ Action :: simple_xml(),
+ Timeout :: timeout(),
+ Result :: {ok,simple_xml()} | {error,error_reason()}.
+%% @doc Execute an action.
+%%
+%% @end
+%%----------------------------------------------------------------------
+action(Client,Action,Timeout) ->
+ call(Client,{send_rpc_op, action, [Action], Timeout}).
+
+%%----------------------------------------------------------------------
+create_subscription(Client) ->
+ create_subscription(Client,?DEFAULT_STREAM,?DEFAULT_TIMEOUT).
+
+create_subscription(Client,Timeout)
+ when ?is_timeout(Timeout) ->
+ create_subscription(Client,?DEFAULT_STREAM,Timeout);
+create_subscription(Client,Stream)
+ when is_list(Stream) ->
+ create_subscription(Client,Stream,?DEFAULT_TIMEOUT);
+create_subscription(Client,Filter)
+ when ?is_filter(Filter) ->
+ create_subscription(Client,?DEFAULT_STREAM,Filter,
+ ?DEFAULT_TIMEOUT).
+
+create_subscription(Client,Stream,Timeout)
+ when is_list(Stream) andalso
+ ?is_timeout(Timeout) ->
+ call(Client,{send_rpc_op,{create_subscription,self()},
+ [Stream,undefined,undefined,undefined],
+ Timeout});
+create_subscription(Client,StartTime,StopTime)
+ when is_list(StartTime) andalso
+ is_list(StopTime) ->
+ create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime,
+ ?DEFAULT_TIMEOUT);
+create_subscription(Client,Filter,Timeout)
+ when ?is_filter(Filter) andalso
+ ?is_timeout(Timeout) ->
+ create_subscription(Client,?DEFAULT_STREAM,Filter,Timeout);
+create_subscription(Client,Stream,Filter)
+ when is_list(Stream) andalso
+ ?is_filter(Filter) ->
+ create_subscription(Client,Stream,Filter,?DEFAULT_TIMEOUT).
+
+create_subscription(Client,StartTime,StopTime,Timeout)
+ when is_list(StartTime) andalso
+ is_list(StopTime) andalso
+ ?is_timeout(Timeout) ->
+ create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime,Timeout);
+create_subscription(Client,Stream,StartTime,StopTime)
+ when is_list(Stream) andalso
+ is_list(StartTime) andalso
+ is_list(StopTime) ->
+ create_subscription(Client,Stream,StartTime,StopTime,?DEFAULT_TIMEOUT);
+create_subscription(Client,Filter,StartTime,StopTime)
+ when ?is_filter(Filter) andalso
+ is_list(StartTime) andalso
+ is_list(StopTime) ->
+ create_subscription(Client,?DEFAULT_STREAM,Filter,
+ StartTime,StopTime,?DEFAULT_TIMEOUT);
+create_subscription(Client,Stream,Filter,Timeout)
+ when is_list(Stream) andalso
+ ?is_filter(Filter) andalso
+ ?is_timeout(Timeout) ->
+ call(Client,{send_rpc_op,{create_subscription,self()},
+ [Stream,Filter,undefined,undefined],
+ Timeout}).
+
+create_subscription(Client,Stream,StartTime,StopTime,Timeout)
+ when is_list(Stream) andalso
+ is_list(StartTime) andalso
+ is_list(StopTime) andalso
+ ?is_timeout(Timeout) ->
+ call(Client,{send_rpc_op,{create_subscription,self()},
+ [Stream,undefined,StartTime,StopTime],
+ Timeout});
+create_subscription(Client,Stream,Filter,StartTime,StopTime)
+ when is_list(Stream) andalso
+ ?is_filter(Filter) andalso
+ is_list(StartTime) andalso
+ is_list(StopTime) ->
+ create_subscription(Client,Stream,Filter,StartTime,StopTime,?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec create_subscription(Client, Stream, Filter,StartTime, StopTime, Timeout) ->
+ Result when
+ Client :: client(),
+ Stream :: stream_name(),
+ Filter :: simple_xml(),
+ StartTime :: xs_datetime(),
+ StopTime :: xs_datetime(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Create a subscription for event notifications.
+%%
+%% This function sets up a subscription for netconf event
+%% notifications of the given stream type, matching the given
+%% filter. The calling process will receive notifications as messages
+%% of type `notification()'.
+%%
+%% <dl>
+%% <dt>Stream:</dt>
+%% <dd> An optional parameter that indicates which stream of events
+%% is of interest. If not present, events in the default NETCONF
+%% stream will be sent.</dd>
+%%
+%% <dt>Filter:</dt>
+%% <dd>An optional parameter that indicates which subset of all
+%% possible events is of interest. The format of this parameter is
+%% the same as that of the filter parameter in the NETCONF protocol
+%% operations. If not present, all events not precluded by other
+%% parameters will be sent. See section 3.6 for more information on
+%% filters.</dd>
+%%
+%% <dt>StartTime:</dt>
+%% <dd>An optional parameter used to trigger the replay feature and
+%% indicate that the replay should start at the time specified. If
+%% `StartTime' is not present, this is not a replay subscription.
+%% It is not valid to specify start times that are later than the
+%% current time. If the `StartTime' specified is earlier than the
+%% log can support, the replay will begin with the earliest
+%% available notification. This parameter is of type dateTime and
+%% compliant to [RFC3339]. Implementations must support time
+%% zones.</dd>
+%%
+%% <dt>StopTime:</dt>
+%% <dd>An optional parameter used with the optional replay feature
+%% to indicate the newest notifications of interest. If `StopTime'
+%% is not present, the notifications will continue until the
+%% subscription is terminated. Must be used with and be later than
+%% `StartTime'. Values of `StopTime' in the future are valid. This
+%% parameter is of type dateTime and compliant to [RFC3339].
+%% Implementations must support time zones.</dd>
+%% </dl>
+%%
+%% See RFC5277 for further details about the event notification
+%% mechanism.
+%%
+%% @end
+%%----------------------------------------------------------------------
+create_subscription(Client,Stream,Filter,StartTime,StopTime,Timeout) ->
+ call(Client,{send_rpc_op,{create_subscription, self()},
+ [Stream,Filter,StartTime,StopTime],
+ Timeout}).
+
+%%----------------------------------------------------------------------
+%% @spec get_event_streams(Client, Timeout) -> Result
+%% @equiv get_event_streams(Client, [], Timeout)
+get_event_streams(Client,Timeout) when is_integer(Timeout); Timeout==infinity ->
+ get_event_streams(Client,[],Timeout);
+
+%%----------------------------------------------------------------------
+%% @spec get_event_streams(Client, Streams) -> Result
+%% @equiv get_event_streams(Client, Streams, infinity)
+get_event_streams(Client,Streams) when is_list(Streams) ->
+ get_event_streams(Client,Streams,?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec get_event_streams(Client, Streams, Timeout)
+ -> Result when
+ Client :: client(),
+ Streams :: [stream_name()],
+ Timeout :: timeout(),
+ Result :: {ok,streams()} | {error,error_reason()}.
+%% @doc Send a request to get the given event streams.
+%%
+%% `Streams' is a list of stream names. The following filter will
+%% be sent to the netconf server in a `get' request:
+%%
+%% ```
+%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification">
+%% <streams>
+%% <stream>
+%% <name>StreamName1</name>
+%% </stream>
+%% <stream>
+%% <name>StreamName2</name>
+%% </stream>
+%% ...
+%% </streams>
+%% </netconf>
+%% '''
+%%
+%% If `Streams' is an empty list, ALL streams will be requested
+%% by sending the following filter:
+%%
+%% ```
+%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification">
+%% <streams/>
+%% </netconf>
+%% '''
+%%
+%% If more complex filtering is needed, a use {@link get/2} or {@link
+%% get/3} and specify the exact filter according to XML Schema for
+%% Event Notifications found in RFC5277.
+%%
+%% @end
+%%----------------------------------------------------------------------
+get_event_streams(Client,Streams,Timeout) ->
+ call(Client,{get_event_streams,Streams,Timeout}).
+
+
+%%----------------------------------------------------------------------
+%% @spec close_session(Client) -> Result
+%% @equiv close_session(Client, infinity)
+close_session(Client) ->
+ close_session(Client, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec close_session(Client, Timeout) -> Result when
+ Client :: client(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Request graceful termination of the session associated with the client.
+%%
+%% When a netconf server receives a `close-session' request, it
+%% will gracefully close the session. The server will release any
+%% locks and resources associated with the session and gracefully
+%% close any associated connections. Any NETCONF requests received
+%% after a `close-session' request will be ignored.
+%%
+%% @end
+%%----------------------------------------------------------------------
+close_session(Client, Timeout) ->
+ call(Client,{send_rpc_op, close_session, [], Timeout}).
+
+
+%%----------------------------------------------------------------------
+%% @spec kill_session(Client, SessionId) -> Result
+%% @equiv kill_session(Client, SessionId, infinity)
+kill_session(Client, SessionId) ->
+ kill_session(Client, SessionId, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec kill_session(Client, SessionId, Timeout) -> Result when
+ Client :: client(),
+ SessionId :: pos_integer(),
+ Timeout :: timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc Force termination of the session associated with the supplied
+%% session id.
+%%
+%% The server side shall abort any operations currently in process,
+%% release any locks and resources associated with the session, and
+%% close any associated connections.
+%%
+%% Only if the server is in the confirmed commit phase, the
+%% configuration will be restored to its state before entering the
+%% confirmed commit phase. Otherwise, no configuration roll back will
+%% be performed.
+%%
+%% If the given `SessionId' is equal to the current session id,
+%% an error will be returned.
+%%
+%% @end
+%% ----------------------------------------------------------------------
+kill_session(Client, SessionId, Timeout) ->
+ call(Client,{send_rpc_op, kill_session, [SessionId], Timeout}).
+
+
+%%----------------------------------------------------------------------
+%% Callback functions
+%%----------------------------------------------------------------------
+
+%% @private
+init(_KeyOrName,{_Host,_Port},Options) ->
+ case ssh_open(Options) of
+ {ok, Connection} ->
+ log(Connection,open),
+ {ConnPid,_} = Connection#connection.reference,
+ {ok, ConnPid, #state{connection = Connection}};
+ {error,Reason}->
+ {error,Reason}
+ end.
+
+%% @private
+terminate(_, #state{connection=Connection}) ->
+ ssh_close(Connection),
+ log(Connection,close),
+ ok.
+
+%% @private
+handle_msg({hello,Timeout}, From,
+ #state{connection=Connection,hello_status=HelloStatus} = State) ->
+ case do_send(Connection, client_hello()) of
+ ok ->
+ case HelloStatus of
+ undefined ->
+ {Ref,TRef} = set_request_timer(Timeout),
+ {noreply, State#state{hello_status=#pending{tref=TRef,
+ ref=Ref,
+ caller=From}}};
+ received ->
+ {reply, ok, State#state{hello_status=done}};
+ {error,Reason} ->
+ {stop, {error,Reason}, State}
+ end;
+ Error ->
+ {stop, Error, State}
+ end;
+handle_msg(_, _From, #state{session_id=undefined} = State) ->
+ %% Hello is not yet excanged - this shall never happen
+ {reply,{error,waiting_for_hello},State};
+handle_msg(get_capabilities, _From, #state{capabilities = Caps} = State) ->
+ {reply, Caps, State};
+handle_msg(get_session_id, _From, #state{session_id = Id} = State) ->
+ {reply, Id, State};
+handle_msg({send, Timeout, SimpleXml}, From,
+ #state{connection=Connection,pending=Pending} = State) ->
+ case do_send(Connection, SimpleXml) of
+ ok ->
+ {Ref,TRef} = set_request_timer(Timeout),
+ {noreply, State#state{pending=[#pending{tref=TRef,
+ ref=Ref,
+ caller=From} | Pending]}};
+ Error ->
+ {reply, Error, State}
+ end;
+handle_msg({send_rpc, SimpleXml, Timeout}, From, State) ->
+ do_send_rpc(undefined, SimpleXml, Timeout, From, State);
+handle_msg({send_rpc_op, Op, Data, Timeout}, From, State) ->
+ SimpleXml = encode_rpc_operation(Op,Data),
+ do_send_rpc(Op, SimpleXml, Timeout, From, State);
+handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) ->
+ Filter = {netconf,?NETMOD_NOTIF_NAMESPACE_ATTR,
+ [{streams,[{stream,[{name,[Name]}]} || Name <- Streams]}]},
+ SimpleXml = encode_rpc_operation(get,[Filter]),
+ do_send_rpc(Op, SimpleXml, Timeout, From, State).
+
+handle_msg({ssh_cm, _CM, {data, _Ch, _Type, Data}}, State) ->
+ handle_data(Data, State);
+handle_msg({ssh_cm, _CM, {closed,_Ch}}, State) ->
+ %% This will happen if the server terminates the connection, as in
+ %% kill-session (or if ssh:close is called from somewhere
+ %% unexpected).
+
+ %%! Log this?? - i.e. as server closing the connection
+ %%! Currently the log will say that the client closed the
+ %%! connection - due to terminate/2
+
+ {stop, State};
+handle_msg({Ref,timeout},
+ #state{hello_status=#pending{ref=Ref,caller=Caller}} = State) ->
+ ct_gen_conn:return(Caller,{error,{hello_session_failed,timeout}}),
+ {stop,State#state{hello_status={error,timeout}}};
+handle_msg({Ref,timeout},#state{pending=Pending} = State) ->
+ {value,#pending{caller=Caller},Pending1} =
+ lists:keytake(Ref,#pending.ref,Pending),
+ ct_gen_conn:return(Caller,{error,timeout}),
+ {noreply,State#state{pending=Pending1}}.
+
+%% @private
+%% Called by ct_util_server to close registered connections before terminate.
+close(Client) ->
+ case get_handle(Client) of
+ {ok,Pid} ->
+ case ct_gen_conn:stop(Pid) of
+ {error,{process_down,Pid,noproc}} ->
+ {error,already_closed};
+ Result ->
+ Result
+ end;
+ Error ->
+ Error
+ end.
+
+
+%%----------------------------------------------------------------------
+%% Internal functions
+%%----------------------------------------------------------------------
+call(Client, Msg) ->
+ call(Client, Msg, infinity).
+call(Client, Msg, Timeout) ->
+ case get_handle(Client) of
+ {ok,Pid} ->
+ case ct_gen_conn:call(Pid,Msg,Timeout) of
+ {error,{process_down,Client,noproc}} ->
+ {error,no_such_client};
+ {error,{process_down,Client,normal}} ->
+ {error,closed};
+ {error,{process_down,Client,Reason}} ->
+ {error,{closed,Reason}};
+ Other ->
+ Other
+ end;
+ Error ->
+ Error
+ end.
+
+get_handle(Client) when is_pid(Client) ->
+ {ok,Client};
+get_handle(Client) ->
+ case ct_util:get_connections(Client, ?MODULE) of
+ {ok,[{Pid,_}]} ->
+ {ok,Pid};
+ {ok,[]} ->
+ {error,{no_connection_found,Client}};
+ {ok,Conns} ->
+ {error,{multiple_connections_found,Client,Conns}};
+ Error ->
+ Error
+ end.
+
+check_options([], undefined, _Port, _Options) ->
+ {error, no_host_address};
+check_options([], _Host, undefined, _Options) ->
+ {error, no_port};
+check_options([], Host, Port, Options) ->
+ {Host,Port,Options};
+check_options([{ssh, Host}|T], _, Port, #options{} = Options) ->
+ check_options(T, Host, Port, Options#options{host=Host});
+check_options([{port,Port}|T], Host, _, #options{} = Options) ->
+ check_options(T, Host, Port, Options#options{port=Port});
+check_options([{timeout, Timeout}|T], Host, Port, Options)
+ when is_integer(Timeout); Timeout==infinity ->
+ check_options(T, Host, Port, Options#options{timeout = Timeout});
+check_options([{X,_}=Opt|T], Host, Port, #options{ssh=SshOpts}=Options) ->
+ case lists:member(X,?VALID_SSH_OPTS) of
+ true ->
+ check_options(T, Host, Port, Options#options{ssh=[Opt|SshOpts]});
+ false ->
+ {error, {invalid_option, Opt}}
+ end.
+
+%%%-----------------------------------------------------------------
+set_request_timer(infinity) ->
+ {undefined,undefined};
+set_request_timer(T) ->
+ Ref = make_ref(),
+ {ok,TRef} = timer:send_after(T,{Ref,timeout}),
+ {Ref,TRef}.
+
+
+%%%-----------------------------------------------------------------
+client_hello() ->
+ {hello, ?NETCONF_NAMESPACE_ATTR,
+ [{capabilities,
+ [{capability,[?NETCONF_BASE_CAP++?NETCONF_BASE_CAP_VSN]}]}]}.
+
+%%%-----------------------------------------------------------------
+
+encode_rpc_operation(Lock,[Target]) when Lock==lock; Lock==unlock ->
+ {Lock,[{target,[Target]}]};
+encode_rpc_operation(get,[Filter]) ->
+ {get,filter(Filter)};
+encode_rpc_operation(get_config,[Source,Filter]) ->
+ {'get-config',[{source,[Source]}] ++ filter(Filter)};
+encode_rpc_operation(edit_config,[Target,Config]) ->
+ {'edit-config',[{target,[Target]},{config,[Config]}]};
+encode_rpc_operation(delete_config,[Target]) ->
+ {'delete-config',[{target,[Target]}]};
+encode_rpc_operation(copy_config,[Target,Source]) ->
+ {'copy-config',[{target,[Target]},{source,[Source]}]};
+encode_rpc_operation(action,[Action]) ->
+ {action,?ACTION_NAMESPACE_ATTR,[{data,[Action]}]};
+encode_rpc_operation(kill_session,[SessionId]) ->
+ {'kill-session',[{'session-id',[integer_to_list(SessionId)]}]};
+encode_rpc_operation(close_session,[]) ->
+ 'close-session';
+encode_rpc_operation({create_subscription,_},
+ [Stream,Filter,StartTime,StopTime]) ->
+ {'create-subscription',?NETCONF_NOTIF_NAMESPACE_ATTR,
+ [{stream,[Stream]}] ++
+ filter(Filter) ++
+ maybe_element(startTime,StartTime) ++
+ maybe_element(stopTime,StopTime)}.
+
+filter(undefined) ->
+ [];
+filter({xpath,Filter}) when ?is_string(Filter) ->
+ [{filter,[{type,"xpath"},{select, Filter}],[]}];
+filter(Filter) ->
+ [{filter,[{type,"subtree"}],[Filter]}].
+
+maybe_element(_,undefined) ->
+ [];
+maybe_element(Tag,Value) ->
+ [{Tag,[Value]}].
+
+%%%-----------------------------------------------------------------
+%%% Send XML data to server
+do_send_rpc(PendingOp,SimpleXml,Timeout,Caller,
+ #state{connection=Connection,msg_id=MsgId,pending=Pending} = State) ->
+ case do_send_rpc(Connection, MsgId, SimpleXml) of
+ ok ->
+ {Ref,TRef} = set_request_timer(Timeout),
+ {noreply, State#state{msg_id=MsgId+1,
+ pending=[#pending{tref=TRef,
+ ref=Ref,
+ msg_id=MsgId,
+ op=PendingOp,
+ caller=Caller} | Pending]}};
+ Error ->
+ {reply, Error, State#state{msg_id=MsgId+1}}
+ end.
+
+do_send_rpc(Connection, MsgId, SimpleXml) ->
+ do_send(Connection,
+ {rpc,
+ [{'message-id',MsgId} | ?NETCONF_NAMESPACE_ATTR],
+ [SimpleXml]}).
+
+do_send(Connection, SimpleXml) ->
+ Xml=to_xml_doc(SimpleXml),
+ log(Connection,send,Xml),
+ ssh_send(Connection, Xml).
+
+to_xml_doc(Simple) ->
+ Prolog = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
+ Xml = list_to_binary(xmerl:export_simple([Simple],
+ xmerl_xml,
+ [#xmlAttribute{name=prolog,
+ value=Prolog}])),
+ <<Xml/binary,?END_TAG/binary>>.
+
+%%%-----------------------------------------------------------------
+%%% Parse and handle received XML data
+handle_data(NewData,#state{connection=Connection,buff=Buff} = State) ->
+ log(Connection,recv,NewData),
+ Data = <<Buff/binary,NewData/binary>>,
+ case xmerl_sax_parser:stream(<<>>,
+ [{continuation_fun,fun sax_cont/1},
+ {continuation_state,{Data,Connection,false}},
+ {event_fun,fun sax_event/3},
+ {event_state,[]}]) of
+ {ok, Simple, Rest} ->
+ decode(Simple,State#state{buff=Rest});
+ {fatal_error,_Loc,Reason,_EndTags,_EventState} ->
+ ?error(Connection#connection.name,[{parse_error,Reason},
+ {data,Data}]),
+ case Reason of
+ {could_not_fetch_data,Msg} ->
+ handle_msg(Msg,State#state{buff = <<>>});
+ _Other ->
+ Pending1 =
+ case State#state.pending of
+ [] ->
+ [];
+ Pending ->
+ %% Assuming the first request gets the
+ %% first answer
+ P=#pending{tref=TRef,caller=Caller} =
+ lists:last(Pending),
+ timer:cancel(TRef),
+ Reason1 = {failed_to_parse_received_data,Reason},
+ ct_gen_conn:return(Caller,{error,Reason1}),
+ lists:delete(P,Pending)
+ end,
+ {noreply,State#state{pending=Pending1,buff = <<>>}}
+ end
+ end.
+
+%%%-----------------------------------------------------------------
+%%% Parsing of XML data
+%% Contiuation function for the sax parser
+sax_cont(done) ->
+ {<<>>,done};
+sax_cont({Data,Connection,false}) ->
+ case binary:split(Data,[?END_TAG],[]) of
+ [All] ->
+ %% No end tag found. Remove what could be a part
+ %% of an end tag from the data and save for next
+ %% iteration
+ SafeSize = size(All)-5,
+ <<New:SafeSize/binary,Save:5/binary>> = All,
+ {New,{Save,Connection,true}};
+ [_Msg,_Rest]=Msgs ->
+ %% We have at least one full message. Any excess data will
+ %% be returned from xmerl_sax_parser:stream/2 in the Rest
+ %% parameter.
+ {list_to_binary(Msgs),done}
+ end;
+sax_cont({Data,Connection,true}) ->
+ case ssh_receive_data() of
+ {ok,Bin} ->
+ log(Connection,recv,Bin),
+ sax_cont({<<Data/binary,Bin/binary>>,Connection,false});
+ {error,Reason} ->
+ throw({could_not_fetch_data,Reason})
+ end.
+
+
+
+%% Event function for the sax parser. It builds a simple XML structure.
+%% Care is taken to keep namespace attributes and prefixes as in the original XML.
+sax_event(Event,_Loc,State) ->
+ sax_event(Event,State).
+
+sax_event({startPrefixMapping, Prefix, Uri},Acc) ->
+ %% startPrefixMapping will always come immediately before the
+ %% startElement where the namespace is defined.
+ [{xmlns,{Prefix,Uri}}|Acc];
+sax_event({startElement,_Uri,_Name,QN,Attrs},Acc) ->
+ %% Pick out any namespace attributes inserted due to a
+ %% startPrefixMapping event.The rest of Acc will then be only
+ %% elements.
+ {NsAttrs,NewAcc} = split_attrs_and_elements(Acc,[]),
+ Tag = qn_to_tag(QN),
+ [{Tag,NsAttrs ++ parse_attrs(Attrs),[]}|NewAcc];
+sax_event({endElement,_Uri,_Name,_QN},[{Name,Attrs,Cont},{Parent,PA,PC}|Acc]) ->
+ [{Parent,PA,[{Name,Attrs,lists:reverse(Cont)}|PC]}|Acc];
+sax_event(endDocument,[{Tag,Attrs,Cont}]) ->
+ {Tag,Attrs,lists:reverse(Cont)};
+sax_event({characters,String},[{Name,Attrs,Cont}|Acc]) ->
+ [{Name,Attrs,[String|Cont]}|Acc];
+sax_event(_Event,State) ->
+ State.
+
+split_attrs_and_elements([{xmlns,{Prefix,Uri}}|Rest],Attrs) ->
+ split_attrs_and_elements(Rest,[{xmlnstag(Prefix),Uri}|Attrs]);
+split_attrs_and_elements(Elements,Attrs) ->
+ {Attrs,Elements}.
+
+xmlnstag([]) ->
+ xmlns;
+xmlnstag(Prefix) ->
+ list_to_atom("xmlns:"++Prefix).
+
+qn_to_tag({[],Name}) ->
+ list_to_atom(Name);
+qn_to_tag({Prefix,Name}) ->
+ list_to_atom(Prefix ++ ":" ++ Name).
+
+parse_attrs([{_Uri, [], Name, Value}|Attrs]) ->
+ [{list_to_atom(Name),Value}|parse_attrs(Attrs)];
+parse_attrs([{_Uri, Prefix, Name, Value}|Attrs]) ->
+ [{list_to_atom(Prefix ++ ":" ++ Name),Value}|parse_attrs(Attrs)];
+parse_attrs([]) ->
+ [].
+
+
+%%%-----------------------------------------------------------------
+%%% Decoding of parsed XML data
+decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) ->
+ ConnName = Connection#connection.name,
+ case get_local_name_atom(Tag) of
+ 'rpc-reply' ->
+ case get_msg_id(Attrs) of
+ undefined ->
+ case Pending of
+ [#pending{msg_id=MsgId}] ->
+ ?error(ConnName,[{warning,rpc_reply_missing_msg_id},
+ {assuming,MsgId}]),
+ decode_rpc_reply(MsgId,E,State);
+ _ ->
+ ?error(ConnName,[{error,rpc_reply_missing_msg_id}]),
+ {noreply,State}
+ end;
+ MsgId ->
+ decode_rpc_reply(MsgId,E,State)
+ end;
+ hello ->
+ case State#state.hello_status of
+ undefined ->
+ case decode_hello(E) of
+ {ok,SessionId,Capabilities} ->
+ {noreply,State#state{session_id = SessionId,
+ capabilities = Capabilities,
+ hello_status = received}};
+ {error,Reason} ->
+ {noreply,State#state{hello_status = {error,Reason}}}
+ end;
+ #pending{tref=TRef,caller=Caller} ->
+ timer:cancel(TRef),
+ case decode_hello(E) of
+ {ok,SessionId,Capabilities} ->
+ ct_gen_conn:return(Caller,ok),
+ {noreply,State#state{session_id = SessionId,
+ capabilities = Capabilities,
+ hello_status = done}};
+ {error,Reason} ->
+ ct_gen_conn:return(Caller,{error,Reason}),
+ {stop,State#state{hello_status={error,Reason}}}
+ end;
+ Other ->
+ ?error(ConnName,[{got_unexpected_hello,E},
+ {hello_status,Other}]),
+ {noreply,State}
+ end;
+ notification ->
+ EventReceiver = State#state.event_receiver,
+ EventReceiver ! E,
+ {noreply,State};
+ Other ->
+ %% Result of send/2, when not sending an rpc request - or
+ %% if netconf server sends noise. Can handle this only if
+ %% there is just one pending that matches (i.e. has
+ %% undefined msg_id and op)
+ case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of
+ [#pending{tref=TRef,
+ caller=Caller}] ->
+ timer:cancel(TRef),
+ ct_gen_conn:return(Caller,E),
+ {noreply,State#state{pending=[]}};
+ _ ->
+ ?error(ConnName,[{got_unexpected_msg,Other},
+ {expecting,Pending}]),
+ {noreply,State}
+ end
+
+ end.
+
+get_msg_id(Attrs) ->
+ case lists:keyfind('message-id',1,Attrs) of
+ {_,Str} ->
+ list_to_integer(Str);
+ false ->
+ undefined
+ end.
+
+decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) ->
+ case lists:keytake(MsgId,#pending.msg_id,Pending) of
+ {value, #pending{tref=TRef,op=Op,caller=Caller}, Pending1} ->
+ timer:cancel(TRef),
+ Content = forward_xmlns_attr(Attrs,Content0),
+ {CallerReply,{ServerReply,State2}} =
+ do_decode_rpc_reply(Op,Content,State#state{pending=Pending1}),
+ ct_gen_conn:return(Caller,CallerReply),
+ {ServerReply,State2};
+ false ->
+ %% Result of send/2, when receiving a correct
+ %% rpc-reply. Can handle this only if there is just one
+ %% pending that matches (i.e. has undefined msg_id and op)
+ case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of
+ [#pending{tref=TRef,
+ msg_id=undefined,
+ op=undefined,
+ caller=Caller}] ->
+ timer:cancel(TRef),
+ ct_gen_conn:return(Caller,E),
+ {noreply,State#state{pending=[]}};
+ _ ->
+ ConnName = (State#state.connection)#connection.name,
+ ?error(ConnName,[{got_unexpected_msg_id,MsgId},
+ {expecting,Pending}]),
+ {noreply,State}
+ end
+ end.
+
+do_decode_rpc_reply(Op,Result,State)
+ when Op==lock; Op==unlock; Op==edit_config; Op==delete_config;
+ Op==copy_config; Op==kill_session ->
+ {decode_ok(Result),{noreply,State}};
+do_decode_rpc_reply(Op,Result,State)
+ when Op==get; Op==get_config; Op==action ->
+ {decode_data(Result),{noreply,State}};
+do_decode_rpc_reply(close_session,Result,State) ->
+ case decode_ok(Result) of
+ ok -> {ok,{stop,State}};
+ Other -> {Other,{noreply,State}}
+ end;
+do_decode_rpc_reply({create_subscription,Caller},Result,State) ->
+ case decode_ok(Result) of
+ ok ->
+ {ok,{noreply,State#state{event_receiver=Caller}}};
+ Other ->
+ {Other,{noreply,State}}
+ end;
+do_decode_rpc_reply(get_event_streams,Result,State) ->
+ {decode_streams(decode_data(Result)),{noreply,State}};
+do_decode_rpc_reply(undefined,Result,State) ->
+ {Result,{noreply,State}}.
+
+
+
+decode_ok([{Tag,Attrs,Content}]) ->
+ case get_local_name_atom(Tag) of
+ ok ->
+ ok;
+ 'rpc-error' ->
+ {error,forward_xmlns_attr(Attrs,Content)};
+ _Other ->
+ {error,{unexpected_rpc_reply,[{Tag,Attrs,Content}]}}
+ end;
+decode_ok(Other) ->
+ {error,{unexpected_rpc_reply,Other}}.
+
+decode_data([{Tag,Attrs,Content}]) ->
+ case get_local_name_atom(Tag) of
+ data ->
+ %% Since content of data has nothing from the netconf
+ %% namespace, we remove the parent's xmlns attribute here
+ %% - just to make the result cleaner
+ {ok,forward_xmlns_attr(remove_xmlnsattr_for_tag(Tag,Attrs),Content)};
+ 'rpc-error' ->
+ {error,forward_xmlns_attr(Attrs,Content)};
+ _Other ->
+ {error,{unexpected_rpc_reply,[{Tag,Attrs,Content}]}}
+ end;
+decode_data(Other) ->
+ {error,{unexpected_rpc_reply,Other}}.
+
+get_qualified_name(Tag) ->
+ case string:tokens(atom_to_list(Tag),":") of
+ [TagStr] -> {[],TagStr};
+ [PrefixStr,TagStr] -> {PrefixStr,TagStr}
+ end.
+
+get_local_name_atom(Tag) ->
+ {_,TagStr} = get_qualified_name(Tag),
+ list_to_atom(TagStr).
+
+
+%% Remove the xmlns attr that points to the tag. I.e. if the tag has a
+%% prefix, remove {'xmlns:prefix',_}, else remove default {xmlns,_}.
+remove_xmlnsattr_for_tag(Tag,Attrs) ->
+ {Prefix,_TagStr} = get_qualified_name(Tag),
+ XmlnsTag = xmlnstag(Prefix),
+ case lists:keytake(XmlnsTag,1,Attrs) of
+ {value,_,NoNsAttrs} ->
+ NoNsAttrs;
+ false ->
+ Attrs
+ end.
+
+%% Take all xmlns attributes from the parent's attribute list and
+%% forward into all childrens' attribute lists. But do not overwrite
+%% any.
+forward_xmlns_attr(ParentAttrs,Children) ->
+ do_forward_xmlns_attr(get_all_xmlns_attrs(ParentAttrs,[]),Children).
+
+do_forward_xmlns_attr(XmlnsAttrs,[{ChT,ChA,ChC}|Children]) ->
+ ChA1 = add_xmlns_attrs(XmlnsAttrs,ChA),
+ [{ChT,ChA1,ChC} | do_forward_xmlns_attr(XmlnsAttrs,Children)];
+do_forward_xmlns_attr(_XmlnsAttrs,[]) ->
+ [].
+
+add_xmlns_attrs([{Key,_}=A|XmlnsAttrs],ChA) ->
+ case lists:keymember(Key,1,ChA) of
+ true ->
+ add_xmlns_attrs(XmlnsAttrs,ChA);
+ false ->
+ add_xmlns_attrs(XmlnsAttrs,[A|ChA])
+ end;
+add_xmlns_attrs([],ChA) ->
+ ChA.
+
+get_all_xmlns_attrs([{xmlns,_}=Default|Attrs],XmlnsAttrs) ->
+ get_all_xmlns_attrs(Attrs,[Default|XmlnsAttrs]);
+get_all_xmlns_attrs([{Key,_}=Attr|Attrs],XmlnsAttrs) ->
+ case atom_to_list(Key) of
+ "xmlns:"++_Prefix ->
+ get_all_xmlns_attrs(Attrs,[Attr|XmlnsAttrs]);
+ _ ->
+ get_all_xmlns_attrs(Attrs,XmlnsAttrs)
+ end;
+get_all_xmlns_attrs([],XmlnsAttrs) ->
+ XmlnsAttrs.
+
+
+%% Decode server hello to pick out session id and capabilities
+decode_hello({hello,_Attrs,Hello}) ->
+ case lists:keyfind('session-id',1,Hello) of
+ {'session-id',_,[SessionId]} ->
+ case lists:keyfind(capabilities,1,Hello) of
+ {capabilities,_,Capabilities} ->
+ case decode_caps(Capabilities,[],false) of
+ {ok,Caps} ->
+ {ok,list_to_integer(SessionId),Caps};
+ Error ->
+ Error
+ end;
+ false ->
+ {error,{incorrect_hello,capabilities_not_found}}
+ end;
+ false ->
+ {error,{incorrect_hello,no_session_id_found}}
+ end.
+
+decode_caps([{capability,[],[?NETCONF_BASE_CAP++Vsn=Cap]} |Caps], Acc, _) ->
+ case Vsn of
+ ?NETCONF_BASE_CAP_VSN ->
+ decode_caps(Caps, [Cap|Acc], true);
+ _ ->
+ {error,{incompatible_base_capability_vsn,Vsn}}
+ end;
+decode_caps([{capability,[],[Cap]}|Caps],Acc,Base) ->
+ decode_caps(Caps,[Cap|Acc],Base);
+decode_caps([H|_T],_,_) ->
+ {error,{unexpected_capability_element,H}};
+decode_caps([],_,false) ->
+ {error,{incorrect_hello,no_base_capability_found}};
+decode_caps([],Acc,true) ->
+ {ok,lists:reverse(Acc)}.
+
+
+%% Return a list of {Name,Data}, where data is a {Tag,Value} list for each stream
+decode_streams({error,Reason}) ->
+ {error,Reason};
+decode_streams({ok,[{netconf,_,Streams}]}) ->
+ {ok,decode_streams(Streams)};
+decode_streams([{streams,_,Streams}]) ->
+ decode_streams(Streams);
+decode_streams([{stream,_,Stream} | Streams]) ->
+ {name,_,[Name]} = lists:keyfind(name,1,Stream),
+ [{Name,[{Tag,Value} || {Tag,_,[Value]} <- Stream, Tag /= name]}
+ | decode_streams(Streams)];
+decode_streams([]) ->
+ [].
+
+
+%%%-----------------------------------------------------------------
+%%% Logging
+
+log(Connection,Action) ->
+ log(Connection,Action,<<>>).
+log(#connection{host=Host,port=Port,name=Name},Action,Data) ->
+ error_logger:info_report(#conn_log{client=self(),
+ address={Host,Port},
+ name=Name,
+ action=Action,
+ module=?MODULE},
+ Data).
+
+
+%% Log callback - called from the error handler process
+format_data(raw,Data) ->
+ io_lib:format("~n~s~n",[hide_password(Data)]);
+format_data(pretty,Data) ->
+ io_lib:format("~n~s~n",[indent(Data)]);
+format_data(html,Data) ->
+ io_lib:format("~n~s~n",[html_format(Data)]).
+
+%%%-----------------------------------------------------------------
+%%% Hide password elements from XML data
+hide_password(Bin) ->
+ re:replace(Bin,<<"(<password[^>]*>)[^<]*(</password>)">>,<<"\\1*****\\2">>,
+ [global,{return,binary}]).
+
+%%%-----------------------------------------------------------------
+%%% HTML formatting
+html_format(Bin) ->
+ binary:replace(indent(Bin),<<"<">>,<<"&lt;">>,[global]).
+
+%%%-----------------------------------------------------------------
+%%% Indentation of XML code
+indent(Bin) ->
+ String = normalize(hide_password(Bin)),
+ IndentedString =
+ case erase(part_of_line) of
+ undefined ->
+ indent1(String,[]);
+ Part ->
+ indent1(lists:reverse(Part)++String,erase(indent))
+ end,
+ list_to_binary(IndentedString).
+
+%% Normalizes the XML document by removing all space and newline
+%% between two XML tags.
+%% Returns a list, no matter if the input was a list or a binary.
+normalize(Str) ->
+ re:replace(Str,<<">[ \r\n\t]+<">>,<<"><">>,[global,{return,list}]).
+
+
+indent1("<?"++Rest1,Indent1) ->
+ %% Prolog
+ {Line,Rest2,Indent2} = indent_line(Rest1,Indent1,[$?,$<]),
+ Line++indent1(Rest2,Indent2);
+indent1("</"++Rest1,Indent1) ->
+ %% Stop tag
+ {Line,Rest2,Indent2} = indent_line1(Rest1,Indent1,[$/,$<]),
+ "\n"++Line++indent1(Rest2,Indent2);
+indent1("<"++Rest1,Indent1) ->
+ %% Start- or empty tag
+ put(tag,get_tag(Rest1)),
+ {Line,Rest2,Indent2} = indent_line(Rest1,Indent1,[$<]),
+ "\n"++Line++indent1(Rest2,Indent2);
+indent1([H|T],Indent) ->
+ [H|indent1(T,Indent)];
+indent1([],_Indent) ->
+ [].
+
+indent_line("?>"++Rest,Indent,Line) ->
+ %% Prolog
+ {lists:reverse(Line)++"?>",Rest,Indent};
+indent_line("/></"++Rest,Indent,Line) ->
+ %% Empty tag, and stop of parent tag -> one step out in indentation
+ {Indent++lists:reverse(Line)++"/>","</"++Rest,Indent--" "};
+indent_line("/>"++Rest,Indent,Line) ->
+ %% Empty tag, then probably next tag -> keep indentation
+ {Indent++lists:reverse(Line)++"/>",Rest,Indent};
+indent_line("></"++Rest,Indent,Line) ->
+ LastTag = erase(tag),
+ case get_tag(Rest) of
+ LastTag ->
+ %% Start and stop tag, but no content
+ indent_line1(Rest,Indent,[$/,$<,$>|Line]);
+ _ ->
+ %% Stop tag completed, and then stop tag of parent -> one step out
+ {Indent++lists:reverse(Line)++">","</"++Rest,Indent--" "}
+ end;
+indent_line("><"++Rest,Indent,Line) ->
+ %% Stop tag completed, and new tag comming -> keep indentation
+ {Indent++lists:reverse(Line)++">","<"++Rest," "++Indent};
+indent_line("</"++Rest,Indent,Line) ->
+ %% Stop tag starting -> search for end of this tag
+ indent_line1(Rest,Indent,[$/,$<|Line]);
+indent_line([H|T],Indent,Line) ->
+ indent_line(T,Indent,[H|Line]);
+indent_line([],Indent,Line) ->
+ %% The line is not complete - will be continued later
+ put(part_of_line,Line),
+ put(indent,Indent),
+ {[],[],Indent}.
+
+indent_line1("></"++Rest,Indent,Line) ->
+ %% Stop tag completed, and then stop tag of parent -> one step out
+ {Indent++lists:reverse(Line)++">","</"++Rest,Indent--" "};
+indent_line1(">"++Rest,Indent,Line) ->
+ %% Stop tag completed -> keep indentation
+ {Indent++lists:reverse(Line)++">",Rest,Indent};
+indent_line1([H|T],Indent,Line) ->
+ indent_line1(T,Indent,[H|Line]);
+indent_line1([],Indent,Line) ->
+ %% The line is not complete - will be continued later
+ put(part_of_line,Line),
+ put(indent,Indent),
+ {[],[],Indent}.
+
+get_tag("/>"++_) ->
+ [];
+get_tag(">"++_) ->
+ [];
+get_tag([H|T]) ->
+ [H|get_tag(T)];
+get_tag([]) ->
+ %% The line is not complete - will be continued later.
+ [].
+
+
+%%%-----------------------------------------------------------------
+%%% SSH stuff
+ssh_receive_data() ->
+ receive
+ {ssh_cm, _CM, {data, _Ch, _Type, Data}} ->
+ {ok, Data};
+ {ssh_cm, _CM, {Closed, _Ch}} = X when Closed == closed; Closed == eof ->
+ {error,X};
+ {_Ref,timeout} = X ->
+ {error,X}
+ end.
+
+ssh_open(#options{host=Host,timeout=Timeout,port=Port,ssh=SshOpts,name=Name}) ->
+ case ssh:connect(Host, Port,
+ [{user_interaction,false},
+ {silently_accept_hosts, true}|SshOpts]) of
+ {ok,CM} ->
+ case ssh_connection:session_channel(CM, Timeout) of
+ {ok,Ch} ->
+ case ssh_connection:subsystem(CM, Ch, "netconf", Timeout) of
+ success ->
+ {ok, #connection{reference = {CM,Ch},
+ host = Host,
+ port = Port,
+ name = Name}};
+ failure ->
+ ssh:close(CM),
+ {error,{ssh,could_not_execute_netconf_subsystem}}
+ end;
+ {error, Reason} ->
+ ssh:close(CM),
+ {error,{ssh,could_not_open_channel,Reason}};
+ Other ->
+ %% Bug in ssh?? got {closed,0} here once...
+ {error,{ssh,unexpected_from_session_channel,Other}}
+ end;
+ {error,Reason} ->
+ {error,{ssh,could_not_connect_to_server,Reason}}
+ end.
+
+ssh_send(#connection{reference = {CM,Ch}}, Data) ->
+ case ssh_connection:send(CM, Ch, Data) of
+ ok -> ok;
+ {error,Reason} -> {error,{ssh,failed_to_send_data,Reason}}
+ end.
+
+ssh_close(#connection{reference = {CM,_Ch}}) ->
+ ssh:close(CM).
+
+
+%%----------------------------------------------------------------------
+%% END OF MODULE
+%%----------------------------------------------------------------------
diff --git a/lib/common_test/src/ct_netconfc.hrl b/lib/common_test/src/ct_netconfc.hrl
new file mode 100644
index 0000000000..295a61a98b
--- /dev/null
+++ b/lib/common_test/src/ct_netconfc.hrl
@@ -0,0 +1,58 @@
+%%--------------------------------------------------------------------
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 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%
+%%
+%%----------------------------------------------------------------------
+%% File: ct_netconfc.hrl
+%%
+%% Description:
+%% This file defines constant values and records used by the
+%% netconf client ct_netconfc.
+%%
+%% @author Support
+%% @doc Netconf Client Interface.
+%% @end
+%%----------------------------------------------------------------------
+%%----------------------------------------------------------------------
+
+
+%% Default port number (RFC 4742/IANA).
+-define(DEFAULT_PORT, 830).
+
+%% Default timeout to wait for netconf server to reply to a request
+-define(DEFAULT_TIMEOUT, infinity). %% msec
+
+%% Namespaces
+-define(NETCONF_NAMESPACE_ATTR,[{xmlns,?NETCONF_NAMESPACE}]).
+-define(ACTION_NAMESPACE_ATTR,[{xmlns,?ACTION_NAMESPACE}]).
+-define(NETCONF_NOTIF_NAMESPACE_ATTR,[{xmlns,?NETCONF_NOTIF_NAMESPACE}]).
+-define(NETMOD_NOTIF_NAMESPACE_ATTR,[{xmlns,?NETMOD_NOTIF_NAMESPACE}]).
+
+-define(NETCONF_NAMESPACE,"urn:ietf:params:xml:ns:netconf:base:1.0").
+-define(ACTION_NAMESPACE,"urn:com:ericsson:ecim:1.0").
+-define(NETCONF_NOTIF_NAMESPACE,
+ "urn:ietf:params:xml:ns:netconf:notification:1.0").
+-define(NETMOD_NOTIF_NAMESPACE,"urn:ietf:params:xml:ns:netmod:notification").
+
+%% Capabilities
+-define(NETCONF_BASE_CAP,"urn:ietf:params:netconf:base:").
+-define(NETCONF_BASE_CAP_VSN,"1.0").
+
+%% Misc
+-define(END_TAG,<<"]]>]]>">>).
+
+-define(FORMAT(_F, _A), lists:flatten(io_lib:format(_F, _A))).
diff --git a/lib/common_test/src/ct_repeat.erl b/lib/common_test/src/ct_repeat.erl
index 8ecd82f771..a47309c6ee 100644
--- a/lib/common_test/src/ct_repeat.erl
+++ b/lib/common_test/src/ct_repeat.erl
@@ -41,72 +41,86 @@ loop_test(If,Args) when is_list(Args) ->
case get_loop_info(Args) of
no_loop ->
false;
- {error,E} ->
+ E = {error,_} ->
io:format("Common Test error: ~p\n\n",[E]),
file:set_cwd(Cwd),
E;
{repeat,N} ->
io:format("\nCommon Test: Will repeat tests ~w times.\n\n",[N]),
Args1 = [{loop_info,[{repeat,1,N}]} | Args],
- loop(If,repeat,0,N,undefined,Args1,undefined),
- file:set_cwd(Cwd);
+ Result = loop(If,repeat,0,N,undefined,Args1,undefined,[]),
+ file:set_cwd(Cwd),
+ Result;
{stop_time,StopTime} ->
- case remaining_time(StopTime) of
- 0 ->
- io:format("\nCommon Test: No time left to run tests.\n\n",[]),
- ok;
- Secs ->
- io:format("\nCommon Test: Will repeat tests for ~s.\n\n",
- [ts(Secs)]),
- TPid =
- case lists:keymember(force_stop,1,Args) of
- true ->
- CtrlPid = self(),
- spawn(fun() -> stop_after(CtrlPid,Secs) end);
- false ->
- undefined
- end,
- Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args],
- loop(If,stop_time,0,Secs,StopTime,Args1,TPid)
- end,
- file:set_cwd(Cwd)
+ Result =
+ case remaining_time(StopTime) of
+ 0 ->
+ io:format("\nCommon Test: "
+ "No time left to run tests.\n\n",[]),
+ {error,not_enough_time};
+ Secs ->
+ io:format("\nCommon Test: "
+ "Will repeat tests for ~s.\n\n",[ts(Secs)]),
+ TPid =
+ case lists:keymember(force_stop,1,Args) of
+ true ->
+ CtrlPid = self(),
+ spawn(fun() -> stop_after(CtrlPid,Secs) end);
+ false ->
+ undefined
+ end,
+ Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args],
+ loop(If,stop_time,0,Secs,StopTime,Args1,TPid,[])
+ end,
+ file:set_cwd(Cwd),
+ Result
end.
-loop(_,repeat,N,N,_,_Args,_) ->
- ok;
+loop(_,repeat,N,N,_,_Args,_,AccResult) ->
+ lists:reverse(AccResult);
-loop(If,Type,N,Data0,Data1,Args,TPid) ->
+loop(If,Type,N,Data0,Data1,Args,TPid,AccResult) ->
Pid = spawn_tester(If,self(),Args),
receive
{'EXIT',Pid,Reason} ->
- io:format("Test run crashed! This could be an internal error "
- "- please report!\n\n"
- "~p\n\n",[Reason]),
- cancel(TPid),
- {error,Reason};
+ case Reason of
+ {user_error,What} ->
+ io:format("\nTest run failed!\nReason: ~p\n\n\n", [What]),
+ cancel(TPid),
+ {error,What};
+ _ ->
+ io:format("Test run crashed! This could be an internal error "
+ "- please report!\n\n"
+ "~p\n\n\n",[Reason]),
+ cancel(TPid),
+ {error,Reason}
+ end;
{Pid,{error,Reason}} ->
- io:format("\nTest run failed!\nReason: ~p\n\n",[Reason]),
+ io:format("\nTest run failed!\nReason: ~p\n\n\n",[Reason]),
cancel(TPid),
{error,Reason};
{Pid,Result} ->
if Type == repeat ->
- io:format("\nTest run ~w(~w) complete.\n\n",[N+1,Data0]),
+ io:format("\nTest run ~w(~w) complete.\n\n\n",[N+1,Data0]),
lists:keydelete(loop_info,1,Args),
Args1 = [{loop_info,[{repeat,N+2,Data0}]} | Args],
- loop(If,repeat,N+1,Data0,Data1,Args1,TPid);
+ loop(If,repeat,N+1,Data0,Data1,Args1,TPid,[Result|AccResult]);
Type == stop_time ->
case remaining_time(Data1) of
0 ->
- io:format("\nTest time (~s) has run out.\n\n",[ts(Data0)]),
+ io:format("\nTest time (~s) has run out.\n\n\n",
+ [ts(Data0)]),
cancel(TPid),
- Result;
+ lists:reverse([Result|AccResult]);
Secs ->
io:format("\n~s of test time remaining, "
- "starting run #~w...\n\n",[ts(Secs),N+2]),
+ "starting run #~w...\n\n\n",
+ [ts(Secs),N+2]),
lists:keydelete(loop_info,1,Args),
ST = {stop_time,Data0,Data1,N+2},
Args1 = [{loop_info,[ST]} | Args],
- loop(If,stop_time,N+1,Data0,Data1,Args1,TPid)
+ loop(If,stop_time,N+1,Data0,Data1,Args1,TPid,
+ [Result|AccResult])
end
end
end.
diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl
index 2ca2030723..7c31f88f1f 100644
--- a/lib/common_test/src/ct_run.erl
+++ b/lib/common_test/src/ct_run.erl
@@ -46,6 +46,10 @@
-define(abs(Name), filename:absname(Name)).
-define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)).
+-define(EXIT_STATUS_TEST_SUCCESSFUL, 0).
+-define(EXIT_STATUS_TEST_CASE_FAILED, 1).
+-define(EXIT_STATUS_TEST_RUN_FAILED, 2).
+
-define(default_verbosity, [{default,?MAX_VERBOSITY},
{'$unspecified',?MAX_VERBOSITY}]).
@@ -58,12 +62,14 @@
step,
logdir,
logopts = [],
+ basic_html,
verbosity = [],
config = [],
event_handlers = [],
ct_hooks = [],
enable_builtin_hooks,
include = [],
+ auto_compile,
silent_connections,
stylesheet,
multiply_timetraps = 1,
@@ -107,7 +113,8 @@ script_start() ->
end, Flags)
end,
%% used for purpose of testing the run_test interface
- io:format(user, "~n-------------------- START ARGS --------------------~n", []),
+ io:format(user, "~n-------------------- START ARGS "
+ "--------------------~n", []),
io:format(user, "--- Init args:~n~p~n", [FlagFilter(Init)]),
io:format(user, "--- CT args:~n~p~n", [FlagFilter(CtArgs)]),
EnvArgs = opts2args(EnvStartOpts),
@@ -115,7 +122,8 @@ script_start() ->
[EnvStartOpts,EnvArgs]),
Merged = merge_arguments(CtArgs ++ EnvArgs),
io:format(user, "--- Merged args:~n~p~n", [FlagFilter(Merged)]),
- io:format(user, "----------------------------------------------------~n~n", []),
+ io:format(user, "-----------------------------------"
+ "-----------------~n~n", []),
Merged;
_ ->
merge_arguments(CtArgs)
@@ -127,50 +135,100 @@ script_start() ->
script_start(Args) ->
Tracing = start_trace(Args),
- Res =
- case ct_repeat:loop_test(script, Args) of
- false ->
- {ok,Cwd} = file:get_cwd(),
- CTVsn =
- case filename:basename(code:lib_dir(common_test)) of
- CTBase when is_list(CTBase) ->
- case string:tokens(CTBase, "-") of
- ["common_test",Vsn] -> " v"++Vsn;
- _ -> ""
- end
- end,
- io:format("~nCommon Test~s starting (cwd is ~s)~n~n",
- [CTVsn,Cwd]),
- Self = self(),
- Pid = spawn_link(fun() -> script_start1(Self, Args) end),
- receive
- {'EXIT',Pid,Reason} ->
- case Reason of
- {user_error,What} ->
- io:format("\nTest run failed!\n"
- "Reason: ~p\n\n", [What]),
- {error,What};
- _ ->
- io:format("Test run crashed! "
- "This could be an internal error "
- "- please report!\n\n"
- "~p\n\n", [Reason]),
- {error,Reason}
- end;
- {Pid,{error,Reason}} ->
- io:format("\nTest run failed! Reason:\n~p\n\n",
- [Reason]),
- {error,Reason};
- {Pid,Result} ->
- Result
- end;
- Result ->
- Result
- end,
+ case ct_repeat:loop_test(script, Args) of
+ false ->
+ {ok,Cwd} = file:get_cwd(),
+ CTVsn =
+ case filename:basename(code:lib_dir(common_test)) of
+ CTBase when is_list(CTBase) ->
+ case string:tokens(CTBase, "-") of
+ ["common_test",Vsn] -> " v"++Vsn;
+ _ -> ""
+ end
+ end,
+ io:format("~nCommon Test~s starting (cwd is ~s)~n~n",
+ [CTVsn,Cwd]),
+ Self = self(),
+ Pid = spawn_link(fun() -> script_start1(Self, Args) end),
+ receive
+ {'EXIT',Pid,Reason} ->
+ case Reason of
+ {user_error,What} ->
+ io:format("\nTest run failed!\nReason: ~p\n\n\n",
+ [What]),
+ finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args);
+ _ ->
+ io:format("Test run crashed! "
+ "This could be an internal error "
+ "- please report!\n\n"
+ "~p\n\n\n", [Reason]),
+ finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args)
+ end;
+ {Pid,{error,Reason}} ->
+ io:format("\nTest run failed! Reason:\n~p\n\n\n",[Reason]),
+ finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args);
+ {Pid,Result} ->
+ io:nl(),
+ finish(Tracing, analyze_test_result(Result, Args), Args)
+ end;
+ {error,_LoopReason} ->
+ finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args);
+ Result ->
+ io:nl(),
+ finish(Tracing, analyze_test_result(Result, Args), Args)
+ end.
+
+%% analyze the result of one test run, or many (in case of looped test)
+analyze_test_result(ok, _) ->
+ ?EXIT_STATUS_TEST_SUCCESSFUL;
+analyze_test_result({error,_Reason}, _) ->
+ ?EXIT_STATUS_TEST_RUN_FAILED;
+analyze_test_result({_Ok,Failed,{_UserSkipped,AutoSkipped}}, Args) ->
+ if Failed > 0 ->
+ ?EXIT_STATUS_TEST_CASE_FAILED;
+ true ->
+ case AutoSkipped of
+ 0 ->
+ ?EXIT_STATUS_TEST_SUCCESSFUL;
+ _ ->
+ case get_start_opt(exit_status,
+ fun([ExitOpt]) -> ExitOpt end,
+ Args) of
+ undefined ->
+ ?EXIT_STATUS_TEST_CASE_FAILED;
+ "ignore_config" ->
+ ?EXIT_STATUS_TEST_SUCCESSFUL
+ end
+ end
+ end;
+analyze_test_result([Result|Rs], Args) ->
+ case analyze_test_result(Result, Args) of
+ ?EXIT_STATUS_TEST_SUCCESSFUL ->
+ analyze_test_result(Rs, Args);
+ Other ->
+ Other
+ end;
+analyze_test_result([], _) ->
+ ?EXIT_STATUS_TEST_SUCCESSFUL;
+analyze_test_result(Unknown, _) ->
+ io:format("\nTest run failed! Reason:\n~p\n\n\n",[Unknown]),
+ ?EXIT_STATUS_TEST_RUN_FAILED.
+
+finish(Tracing, ExitStatus, Args) ->
stop_trace(Tracing),
timer:sleep(1000),
- io:nl(),
- Res.
+ %% it's possible to tell CT to finish execution with a call
+ %% to a different function than the normal halt/1 BIF
+ %% (meant to be used mainly for reading the CT exit status)
+ case get_start_opt(halt_with,
+ fun([HaltMod,HaltFunc]) -> {list_to_atom(HaltMod),
+ list_to_atom(HaltFunc)} end,
+ Args) of
+ undefined ->
+ halt(ExitStatus);
+ {M,F} ->
+ apply(M, F, [ExitStatus])
+ end.
script_start1(Parent, Args) ->
%% read general start flags
@@ -216,7 +274,7 @@ script_start1(Parent, Args) ->
end
end,
%% no_auto_compile + include
- IncludeDirs =
+ {AutoCompile,IncludeDirs} =
case proplists:get_value(no_auto_compile, Args) of
undefined ->
application:set_env(common_test, auto_compile, true),
@@ -232,16 +290,16 @@ script_start1(Parent, Args) ->
case os:getenv("CT_INCLUDE_PATH") of
false ->
application:set_env(common_test, include, InclDirs),
- InclDirs;
+ {undefined,InclDirs};
CtInclPath ->
AllInclDirs =
string:tokens(CtInclPath,[$:,$ ,$,]) ++ InclDirs,
application:set_env(common_test, include, AllInclDirs),
- AllInclDirs
+ {undefined,AllInclDirs}
end;
_ ->
application:set_env(common_test, auto_compile, false),
- []
+ {false,[]}
end,
%% silent connections
SilentConns =
@@ -253,20 +311,24 @@ script_start1(Parent, Args) ->
Stylesheet = get_start_opt(stylesheet,
fun([SS]) -> ?abs(SS) end, Args),
%% basic_html - used by ct_logs
- case proplists:get_value(basic_html, Args) of
- undefined ->
- application:set_env(common_test, basic_html, false);
- _ ->
- application:set_env(common_test, basic_html, true)
- end,
+ BasicHtml = case proplists:get_value(basic_html, Args) of
+ undefined ->
+ application:set_env(common_test, basic_html, false),
+ undefined;
+ _ ->
+ application:set_env(common_test, basic_html, true),
+ true
+ end,
StartOpts = #opts{label = Label, profile = Profile,
vts = Vts, shell = Shell, cover = Cover,
logdir = LogDir, logopts = LogOpts,
+ basic_html = BasicHtml,
verbosity = Verbosity,
event_handlers = EvHandlers,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
+ auto_compile = AutoCompile,
include = IncludeDirs,
silent_connections = SilentConns,
stylesheet = Stylesheet,
@@ -366,9 +428,36 @@ script_start2(StartOpts = #opts{vts = undefined,
StartOpts#opts.enable_builtin_hooks,
SpecStartOpts#opts.enable_builtin_hooks),
+ Stylesheet =
+ choose_val(StartOpts#opts.stylesheet,
+ SpecStartOpts#opts.stylesheet),
+
AllInclude = merge_vals([StartOpts#opts.include,
SpecStartOpts#opts.include]),
application:set_env(common_test, include, AllInclude),
+
+ AutoCompile =
+ case choose_val(StartOpts#opts.auto_compile,
+ SpecStartOpts#opts.auto_compile) of
+ undefined ->
+ true;
+ ACBool ->
+ application:set_env(common_test,
+ auto_compile,
+ ACBool),
+ ACBool
+ end,
+
+ BasicHtml =
+ case choose_val(StartOpts#opts.basic_html,
+ SpecStartOpts#opts.basic_html) of
+ undefined ->
+ false;
+ BHBool ->
+ application:set_env(common_test, basic_html,
+ BHBool),
+ BHBool
+ end,
{TS,StartOpts#opts{label = Label,
profile = Profile,
@@ -376,12 +465,15 @@ script_start2(StartOpts = #opts{vts = undefined,
cover = Cover,
logdir = LogDir,
logopts = AllLogOpts,
+ basic_html = BasicHtml,
verbosity = AllVerbosity,
config = SpecStartOpts#opts.config,
event_handlers = AllEvHs,
ct_hooks = AllCTHooks,
enable_builtin_hooks =
EnableBuiltinHooks,
+ stylesheet = Stylesheet,
+ auto_compile = AutoCompile,
include = AllInclude,
multiply_timetraps = MultTT,
scale_timetraps = ScaleTT,
@@ -570,7 +662,7 @@ script_start4(#opts{vts = true, cover = Cover}, _) ->
%% Add support later (maybe).
io:format("\nCan't run cover in vts mode.\n\n", [])
end,
- erlang:halt();
+ {error,no_cover_in_vts_mode};
script_start4(#opts{shell = true, cover = Cover}, _) ->
case Cover of
@@ -579,7 +671,8 @@ script_start4(#opts{shell = true, cover = Cover}, _) ->
_ ->
%% Add support later (maybe).
io:format("\nCan't run cover in interactive mode.\n\n", [])
- end;
+ end,
+ {error,no_cover_in_interactive_mode};
script_start4(Opts = #opts{tests = Tests}, Args) ->
do_run(Tests, [], Opts, Args).
@@ -722,7 +815,7 @@ run_test(StartOpts) when is_list(StartOpts) ->
Ref = monitor(process, CTPid),
receive
{'DOWN',Ref,process,CTPid,{user_error,Error}} ->
- Error;
+ {error,Error};
{'DOWN',Ref,process,CTPid,Other} ->
Other
end.
@@ -840,7 +933,7 @@ run_test2(StartOpts) ->
CreatePrivDir = get_start_opt(create_priv_dir, value, StartOpts),
%% auto compile & include files
- Include =
+ {AutoCompile,Include} =
case proplists:get_value(auto_compile, StartOpts) of
undefined ->
application:set_env(common_test, auto_compile, true),
@@ -856,16 +949,16 @@ run_test2(StartOpts) ->
case os:getenv("CT_INCLUDE_PATH") of
false ->
application:set_env(common_test, include, InclDirs),
- InclDirs;
+ {undefined,InclDirs};
CtInclPath ->
InclDirs1 = string:tokens(CtInclPath, [$:,$ ,$,]),
AllInclDirs = InclDirs1++InclDirs,
application:set_env(common_test, include, AllInclDirs),
- AllInclDirs
+ {undefined,AllInclDirs}
end;
ACBool ->
application:set_env(common_test, auto_compile, ACBool),
- []
+ {ACBool,[]}
end,
%% decrypt config file
@@ -879,11 +972,14 @@ run_test2(StartOpts) ->
end,
%% basic html - used by ct_logs
- case proplists:get_value(basic_html, StartOpts) of
- undefined ->
- application:set_env(common_test, basic_html, false);
- BasicHtmlBool ->
- application:set_env(common_test, basic_html, BasicHtmlBool)
+ BasicHtml =
+ case proplists:get_value(basic_html, StartOpts) of
+ undefined ->
+ application:set_env(common_test, basic_html, false),
+ undefined;
+ BasicHtmlBool ->
+ application:set_env(common_test, basic_html, BasicHtmlBool),
+ BasicHtmlBool
end,
%% stepped execution
@@ -891,11 +987,13 @@ run_test2(StartOpts) ->
Opts = #opts{label = Label, profile = Profile,
cover = Cover, step = Step, logdir = LogDir,
- logopts = LogOpts, config = CfgFiles,
+ logopts = LogOpts, basic_html = BasicHtml,
+ config = CfgFiles,
verbosity = Verbosity,
event_handlers = EvHandlers,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
+ auto_compile = AutoCompile,
include = Include,
silent_connections = SilentConns,
stylesheet = Stylesheet,
@@ -930,7 +1028,7 @@ run_spec_file(Relaxed,
log_ts_names(AbsSpecs),
case catch ct_testspec:collect_tests_from_file(AbsSpecs, Relaxed) of
{Error,CTReason} when Error == error ; Error == 'EXIT' ->
- exit(CTReason);
+ exit({error,CTReason});
TS ->
SpecOpts = get_data_for_node(TS, node()),
Label = choose_val(Opts#opts.label,
@@ -941,6 +1039,8 @@ run_spec_file(Relaxed,
SpecOpts#opts.logdir),
AllLogOpts = merge_vals([Opts#opts.logopts,
SpecOpts#opts.logopts]),
+ Stylesheet = choose_val(Opts#opts.stylesheet,
+ SpecOpts#opts.stylesheet),
AllVerbosity = merge_keyvals([Opts#opts.verbosity,
SpecOpts#opts.verbosity]),
AllConfig = merge_vals([CfgFiles, SpecOpts#opts.config]),
@@ -956,7 +1056,6 @@ run_spec_file(Relaxed,
SpecOpts#opts.event_handlers]),
AllInclude = merge_vals([Opts#opts.include,
SpecOpts#opts.include]),
-
AllCTHooks = merge_vals([Opts#opts.ct_hooks,
SpecOpts#opts.ct_hooks]),
EnableBuiltinHooks = choose_val(Opts#opts.enable_builtin_hooks,
@@ -964,14 +1063,37 @@ run_spec_file(Relaxed,
application:set_env(common_test, include, AllInclude),
+ AutoCompile = case choose_val(Opts#opts.auto_compile,
+ SpecOpts#opts.auto_compile) of
+ undefined ->
+ true;
+ ACBool ->
+ application:set_env(common_test, auto_compile,
+ ACBool),
+ ACBool
+ end,
+
+ BasicHtml = case choose_val(Opts#opts.basic_html,
+ SpecOpts#opts.basic_html) of
+ undefined ->
+ false;
+ BHBool ->
+ application:set_env(common_test, basic_html,
+ BHBool),
+ BHBool
+ end,
+
Opts1 = Opts#opts{label = Label,
profile = Profile,
cover = Cover,
logdir = which(logdir, LogDir),
logopts = AllLogOpts,
+ stylesheet = Stylesheet,
+ basic_html = BasicHtml,
verbosity = AllVerbosity,
config = AllConfig,
event_handlers = AllEvHs,
+ auto_compile = AutoCompile,
include = AllInclude,
testspecs = AbsSpecs,
multiply_timetraps = MultTT,
@@ -987,7 +1109,7 @@ run_spec_file(Relaxed,
{Run,Skip} = ct_testspec:prepare_tests(TS, node()),
reformat_result(catch do_run(Run, Skip, Opts1, StartOpts));
{error,GCFReason} ->
- exit(GCFReason)
+ exit({error,GCFReason})
end
end.
@@ -999,8 +1121,8 @@ run_prepared(Run, Skip, Opts = #opts{logdir = LogDir,
ok ->
reformat_result(catch do_run(Run, Skip, Opts#opts{logdir = LogDir1},
StartOpts));
- {error,Reason} ->
- exit(Reason)
+ {error,_Reason} = Error ->
+ exit(Error)
end.
check_config_file(Callback, File)->
@@ -1008,7 +1130,7 @@ check_config_file(Callback, File)->
false ->
case code:load_file(Callback) of
{module,_} -> ok;
- {error,Why} -> exit({cant_load_callback_module,Why})
+ {error,Why} -> exit({error,{cant_load_callback_module,Why}})
end;
_ ->
ok
@@ -1019,16 +1141,17 @@ check_config_file(Callback, File)->
{ok,{config,_}}->
File;
{error,{wrong_config,Message}}->
- exit({wrong_config,{Callback,Message}});
+ exit({error,{wrong_config,{Callback,Message}}});
{error,{nofile,File}}->
- exit({no_such_file,?abs(File)})
+ exit({error,{no_such_file,?abs(File)}})
end.
run_dir(Opts = #opts{logdir = LogDir,
config = CfgFiles,
event_handlers = EvHandlers,
ct_hooks = CTHook,
- enable_builtin_hooks = EnableBuiltinHooks }, StartOpts) ->
+ enable_builtin_hooks = EnableBuiltinHooks},
+ StartOpts) ->
LogDir1 = which(logdir, LogDir),
Opts1 = Opts#opts{logdir = LogDir1},
AbsCfgFiles =
@@ -1041,7 +1164,8 @@ run_dir(Opts = #opts{logdir = LogDir,
{module,Callback}->
ok;
{error,_}->
- exit({no_such_module,Callback})
+ exit({error,{no_such_module,
+ Callback}})
end
end,
{Callback,
@@ -1054,7 +1178,7 @@ run_dir(Opts = #opts{logdir = LogDir,
{ct_hooks, CTHook},
{enable_builtin_hooks,EnableBuiltinHooks}], LogDir1) of
ok -> ok;
- {error,IReason} -> exit(IReason)
+ {error,_IReason} = IError -> exit(IError)
end,
case {proplists:get_value(dir, StartOpts),
proplists:get_value(suite, StartOpts),
@@ -1096,7 +1220,7 @@ run_dir(Opts = #opts{logdir = LogDir,
[], Opts1, StartOpts));
{undefined,[Hd,_|_],_GsAndCs} when not is_integer(Hd) ->
- exit(multiple_suites_and_cases);
+ exit({error,multiple_suites_and_cases});
{undefined,Suite=[Hd|Tl],GsAndCs} when is_integer(Hd) ;
(is_list(Hd) and (Tl == [])) ;
@@ -1106,10 +1230,10 @@ run_dir(Opts = #opts{logdir = LogDir,
[], Opts1, StartOpts));
{[Hd,_|_],_Suites,[]} when is_list(Hd) ; not is_integer(Hd) ->
- exit(multiple_dirs_and_suites);
+ exit({error,multiple_dirs_and_suites});
{undefined,undefined,GsAndCs} when GsAndCs /= [] ->
- exit(incorrect_start_options);
+ exit({error,incorrect_start_options});
{Dir,Suite,GsAndCs} when is_integer(hd(Dir)) ;
(is_atom(Dir) and (Dir /= undefined)) ;
@@ -1118,7 +1242,7 @@ run_dir(Opts = #opts{logdir = LogDir,
Dir1 = if is_atom(Dir) -> atom_to_list(Dir);
true -> Dir end,
if Suite == undefined ->
- exit(incorrect_start_options);
+ exit({error,incorrect_start_options});
is_integer(hd(Suite)) ;
(is_atom(Suite) and (Suite /= undefined)) ;
@@ -1137,7 +1261,7 @@ run_dir(Opts = #opts{logdir = LogDir,
is_list(Suite) -> % multiple suites
case [suite_to_test(Dir1, S) || S <- Suite] of
[_,_|_] when GsAndCs /= [] ->
- exit(multiple_suites_and_cases);
+ exit({error,multiple_suites_and_cases});
[{Dir2,Mod}] when GsAndCs /= [] ->
reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs),
[], Opts1, StartOpts));
@@ -1148,10 +1272,10 @@ run_dir(Opts = #opts{logdir = LogDir,
end;
{undefined,undefined,[]} ->
- exit(no_test_specified);
+ exit({error,no_test_specified});
{Dir,Suite,GsAndCs} ->
- exit({incorrect_start_options,{Dir,Suite,GsAndCs}})
+ exit({error,{incorrect_start_options,{Dir,Suite,GsAndCs}}})
end.
%%%-----------------------------------------------------------------
@@ -1196,7 +1320,7 @@ run_testspec2(File) when is_list(File), is_integer(hd(File)) ->
run_testspec2(TestSpec) ->
case catch ct_testspec:collect_tests_from_list(TestSpec, false) of
{E,CTReason} when E == error ; E == 'EXIT' ->
- exit(CTReason);
+ exit({error,CTReason});
TS ->
Opts = get_data_for_node(TS, node()),
@@ -1218,8 +1342,8 @@ run_testspec2(TestSpec) ->
include = AllInclude},
{Run,Skip} = ct_testspec:prepare_tests(TS, node()),
reformat_result(catch do_run(Run, Skip, Opts1, []));
- {error,GCFReason} ->
- exit(GCFReason)
+ {error,_GCFReason} = GCFError ->
+ exit(GCFError)
end
end.
@@ -1227,6 +1351,8 @@ get_data_for_node(#testspec{label = Labels,
profile = Profiles,
logdir = LogDirs,
logopts = LogOptsList,
+ basic_html = BHs,
+ stylesheet = SSs,
verbosity = VLvls,
cover = CoverFs,
config = Cfgs,
@@ -1234,6 +1360,7 @@ get_data_for_node(#testspec{label = Labels,
event_handler = EvHs,
ct_hooks = CTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
+ auto_compile = ACs,
include = Incl,
multiply_timetraps = MTs,
scale_timetraps = STs,
@@ -1248,6 +1375,8 @@ get_data_for_node(#testspec{label = Labels,
undefined -> [];
LOs -> LOs
end,
+ BasicHtml = proplists:get_value(Node, BHs),
+ Stylesheet = proplists:get_value(Node, SSs),
Verbosity = case proplists:get_value(Node, VLvls) of
undefined -> [];
Lvls -> Lvls
@@ -1260,17 +1389,21 @@ get_data_for_node(#testspec{label = Labels,
[CBF || {N,CBF} <- UsrCfgs, N==Node],
EvHandlers = [{H,A} || {N,H,A} <- EvHs, N==Node],
FiltCTHooks = [Hook || {N,Hook} <- CTHooks, N==Node],
+ AutoCompile = proplists:get_value(Node, ACs),
Include = [I || {N,I} <- Incl, N==Node],
#opts{label = Label,
profile = Profile,
logdir = LogDir,
logopts = LogOpts,
+ basic_html = BasicHtml,
+ stylesheet = Stylesheet,
verbosity = Verbosity,
cover = Cover,
config = ConfigFiles,
event_handlers = EvHandlers,
ct_hooks = FiltCTHooks,
enable_builtin_hooks = EnableBuiltinHooks,
+ auto_compile = AutoCompile,
include = Include,
multiply_timetraps = MT,
scale_timetraps = ST,
@@ -1451,7 +1584,7 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) ->
case code:which(test_server) of
non_existing ->
- exit({error,no_path_to_test_server});
+ {error,no_path_to_test_server};
_ ->
Opts1 = if Cover == undefined ->
Opts;
@@ -1532,7 +1665,7 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) ->
exit(Error);
_ ->
ct_util:stop(normal),
- R
+ TestResult
end;
false ->
io:nl(),
@@ -1608,9 +1741,10 @@ verify_suites(TestSuites) ->
if Suite == all ->
{[DS|Found],NotFound};
true ->
- Beam =
- filename:join(TestDir,
- atom_to_list(Suite)++".beam"),
+ Beam = filename:join(TestDir,
+ atom_to_list(Suite)++
+ ".beam"),
+
case filelib:is_regular(Beam) of
true ->
{[DS|Found],NotFound};
@@ -1619,9 +1753,10 @@ verify_suites(TestSuites) ->
{file,SuiteFile} ->
%% test suite is already
%% loaded and since
- %% auto_compile == false, let's
- %% assume the user has loaded
- %% the beam file explicitly
+ %% auto_compile == false,
+ %% let's assume the user has
+ %% loaded the beam file
+ %% explicitly
ActualDir =
filename:dirname(SuiteFile),
{[{ActualDir,Suite}|Found],
@@ -1629,7 +1764,8 @@ verify_suites(TestSuites) ->
false ->
Name =
filename:join(TestDir,
- atom_to_list(Suite)),
+ atom_to_list(
+ Suite)),
io:format(user,
"Suite ~w not found"
"in directory ~s~n",
@@ -1686,7 +1822,7 @@ step(TestDir, Suite, Case) ->
%%% @equiv ct:step/4
step(TestDir, Suite, Case, Opts) when is_list(TestDir),
is_atom(Suite), is_atom(Case),
- Suite =/= all, Case =/= all ->
+ Suite =/= all, Case =/= all ->
do_run([{TestDir,Suite,Case}], [{step,Opts}]).
@@ -1887,8 +2023,8 @@ do_run_test(Tests, Skip, Opts) ->
incl_mods = CovIncl,
cross = CovCross,
src = _CovSrc}} ->
- ct_logs:log("COVER INFO","Using cover "
- "specification file: ~s~n"
+ ct_logs:log("COVER INFO",
+ "Using cover specification file: ~s~n"
"App: ~w~n"
"Cross cover: ~w~n"
"Including ~w modules~n"
@@ -1902,8 +2038,8 @@ do_run_test(Tests, Skip, Opts) ->
true ->
DelResult = file:delete(CovExport),
ct_logs:log("COVER INFO",
- "Warning! Export file ~s "
- "already exists. "
+ "Warning! "
+ "Export file ~s already exists. "
"Deleting with result: ~p",
[CovExport,DelResult]);
false ->
@@ -1984,7 +2120,13 @@ do_run_test(Tests, Skip, Opts) ->
maybe_cleanup_interpret(Suite, Opts#opts.step)
end, CleanUp),
[code:del_path(Dir) || Dir <- AddedToPath],
- ok;
+
+ case ct_util:get_testdata(stats) of
+ Stats = {_Ok,_Failed,{_UserSkipped,_AutoSkipped}} ->
+ Stats;
+ _ ->
+ {error,test_result_unknown}
+ end;
Error ->
Error
end.
@@ -2567,7 +2709,11 @@ make_abs1([], Path) ->
%% to ct_run start arguments (on the init arguments format) -
%% this is useful mainly for testing the ct_run start functions.
opts2args(EnvStartOpts) ->
- lists:flatmap(fun({config,CfgFiles}) ->
+ lists:flatmap(fun({exit_status,ExitStatusOpt}) when is_atom(ExitStatusOpt) ->
+ [{exit_status,[atom_to_list(ExitStatusOpt)]}];
+ ({halt_with,{HaltM,HaltF}}) ->
+ [{halt_with,[atom_to_list(HaltM),atom_to_list(HaltF)]}];
+ ({config,CfgFiles}) ->
[{ct_config,[CfgFiles]}];
({userconfig,{CBM,CfgStr=[X|_]}}) when is_integer(X) ->
[{userconfig,[atom_to_list(CBM),CfgStr]}];
@@ -2617,7 +2763,7 @@ opts2args(EnvStartOpts) ->
({decrypt,{file,File}}) ->
[{ct_decrypt_file,[File]}];
({basic_html,true}) ->
- ({basic_html,[]});
+ [{basic_html,[]}];
({basic_html,false}) ->
[];
({event_handler,EH}) when is_atom(EH) ->
diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl
index 6d7db7bb75..d66caef100 100644
--- a/lib/common_test/src/ct_testspec.erl
+++ b/lib/common_test/src/ct_testspec.erl
@@ -29,6 +29,8 @@
-include("ct_util.hrl").
+-define(testspec_fields, record_info(fields, testspec)).
+
%%%------------------------------------------------------------------
%%% NOTE:
%%% Multiple testspecs may be used as input with the result that
@@ -46,7 +48,8 @@
%%% Version 1 - extract and return all tests and skips for Node
%%% (incl all_nodes)
%%%-------------------------------------------------------------------
-prepare_tests(TestSpec,Node) when is_record(TestSpec,testspec), is_atom(Node) ->
+prepare_tests(TestSpec,Node) when is_record(TestSpec,testspec),
+ is_atom(Node) ->
case lists:keysearch(Node,1,prepare_tests(TestSpec)) of
{value,{Node,Run,Skip}} ->
{Run,Skip};
@@ -249,22 +252,23 @@ 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},
- Relaxed),
- collect_tests_from_file1(Specs,TestSpec1,Relaxed);
+ case collect_tests(Terms,
+ TestSpec#testspec{spec_dir=SpecDir},
+ Relaxed) of
+ TS = #testspec{tests=Tests, logdir=LogDirs} when Specs == [] ->
+ LogDirs1 = lists:delete(".",LogDirs) ++ ["."],
+ TS#testspec{tests=lists:flatten(Tests), logdir=LogDirs1};
+ TS = #testspec{alias = As, nodes = Ns} ->
+ TS1 = TS#testspec{alias = lists:reverse(As),
+ nodes = lists:reverse(Ns)},
+ collect_tests_from_file1(Specs,TS1,Relaxed)
+ end;
{error,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},_) ->
- TS#testspec{config=lists:reverse(Cfgs),
- event_handler=lists:reverse(EvHs),
- include=lists:reverse(Incl),
- tests=lists:flatten(Tests)}.
+ end.
collect_tests_from_list(Terms,Relaxed) ->
collect_tests_from_list(Terms,[node()],Relaxed).
@@ -278,30 +282,163 @@ collect_tests_from_list(Terms,Nodes,Relaxed) when is_list(Nodes) ->
E = {error,_} ->
E;
TS ->
- #testspec{config=Cfgs,event_handler=EvHs,include=Incl,tests=Tests} = TS,
- TS#testspec{config=lists:reverse(Cfgs),
- event_handler=lists:reverse(EvHs),
- include=lists:reverse(Incl),
- tests=lists:flatten(Tests)}
+ #testspec{tests=Tests, logdir=LogDirs} = TS,
+ LogDirs1 = lists:delete(".",LogDirs) ++ ["."],
+ TS#testspec{tests=lists:flatten(Tests), logdir=LogDirs1}
end.
collect_tests(Terms,TestSpec,Relaxed) ->
put(relaxed,Relaxed),
- TestSpec1 = get_global(Terms,TestSpec),
- TestSpec2 = get_all_nodes(Terms,TestSpec1),
- {Terms2, TestSpec3} = filter_init_terms(Terms, [], TestSpec2),
+ Terms1 = replace_names(Terms),
+ TestSpec1 = get_global(Terms1,TestSpec),
+ TestSpec2 = get_all_nodes(Terms1,TestSpec1),
+ {Terms2, TestSpec3} = filter_init_terms(Terms1, [], TestSpec2),
add_tests(Terms2,TestSpec3).
-get_global([{merge_tests, Bool} | Ts], Spec) ->
- get_global(Ts,Spec#testspec{ merge_tests = Bool });
+%% replace names (atoms) in the testspec matching those in 'define' terms by
+%% searching recursively through tuples and lists
+replace_names(Terms) ->
+ Defs =
+ lists:flatmap(fun(Def={define,Name,_Replacement}) ->
+ %% check that name follows convention
+ if not is_atom(Name) ->
+ throw({illegal_name_in_testspec,Name});
+ true ->
+ [First|_] = atom_to_list(Name),
+ if ((First == $?) or (First == $$)
+ or (First == $_)
+ or ((First >= $A)
+ and (First =< $Z))) ->
+ [Def];
+ true ->
+ throw({illegal_name_in_testspec,
+ Name})
+ end
+ end;
+ (_) -> []
+ end, Terms),
+ DefProps = replace_names_in_defs(Defs,[]),
+ replace_names(Terms,[],DefProps).
+
+replace_names_in_defs([Def|Left],ModDefs) ->
+ [{define,Name,Replacement}] = replace_names([Def],[],ModDefs),
+ replace_names_in_defs(Left,[{Name,Replacement}|ModDefs]);
+replace_names_in_defs([],ModDefs) ->
+ ModDefs.
+
+replace_names([Term|Ts],Modified,Defs) when is_tuple(Term) ->
+ [TypeTag|Data] = tuple_to_list(Term),
+ Term1 = list_to_tuple([TypeTag|replace_names_in_elems(Data,[],Defs)]),
+ replace_names(Ts,[Term1|Modified],Defs);
+replace_names([Term|Ts],Modified,Defs) when is_atom(Term) ->
+ case proplists:get_value(Term,Defs) of
+ undefined ->
+ replace_names(Ts,[Term|Modified],Defs);
+ Replacement ->
+ replace_names(Ts,[Replacement|Modified],Defs)
+ end;
+replace_names([Term=[Ch|_]|Ts],Modified,Defs) when is_integer(Ch) ->
+ %% Term *could* be a string, attempt to search through it
+ Term1 = replace_names_in_string(Term,Defs),
+ replace_names(Ts,[Term1|Modified],Defs);
+replace_names([Term|Ts],Modified,Defs) ->
+ replace_names(Ts,[Term|Modified],Defs);
+replace_names([],Modified,_Defs) ->
+ lists:reverse(Modified).
+
+replace_names_in_elems([Elem|Es],Modified,Defs) when is_tuple(Elem) ->
+ Elem1 = list_to_tuple(replace_names_in_elems(tuple_to_list(Elem),[],Defs)),
+ replace_names_in_elems(Es,[Elem1|Modified],Defs);
+replace_names_in_elems([Elem|Es],Modified,Defs) when is_atom(Elem) ->
+ case proplists:get_value(Elem,Defs) of
+ undefined ->
+ %% if Term is a node name, check it for replacements as well
+ Elem1 = replace_names_in_node(Elem,Defs),
+ replace_names_in_elems(Es,[Elem1|Modified],Defs);
+ Replacement ->
+ replace_names_in_elems(Es,[Replacement|Modified],Defs)
+ end;
+replace_names_in_elems([Elem=[Ch|_]|Es],Modified,Defs) when is_integer(Ch) ->
+ %% Term *could* be a string, attempt to search through it
+ case replace_names_in_string(Elem,Defs) of
+ Elem ->
+ List = replace_names_in_elems(Elem,[],Defs),
+ replace_names_in_elems(Es,[List|Modified],Defs);
+ Elem1 ->
+ replace_names_in_elems(Es,[Elem1|Modified],Defs)
+ end;
+replace_names_in_elems([Elem|Es],Modified,Defs) when is_list(Elem) ->
+ List = replace_names_in_elems(Elem,[],Defs),
+ replace_names_in_elems(Es,[List|Modified],Defs);
+replace_names_in_elems([Elem|Es],Modified,Defs) ->
+ replace_names_in_elems(Es,[Elem|Modified],Defs);
+replace_names_in_elems([],Modified,_Defs) ->
+ lists:reverse(Modified).
+
+replace_names_in_string(Term,Defs=[{Name,Replacement=[Ch|_]}|Ds])
+ when is_integer(Ch) ->
+ try re:replace(Term,[$'|atom_to_list(Name)]++"'",
+ Replacement,[{return,list}]) of
+ Term -> % no match, proceed
+ replace_names_in_string(Term,Ds);
+ Term1 ->
+ replace_names_in_string(Term1,Defs)
+ catch
+ _:_ -> Term % Term is not a string
+ end;
+replace_names_in_string(Term,[_|Ds]) ->
+ replace_names_in_string(Term,Ds);
+replace_names_in_string(Term,[]) ->
+ Term.
+
+replace_names_in_node(Node,Defs) ->
+ String = atom_to_list(Node),
+ case lists:member($@,String) of
+ true ->
+ list_to_atom(replace_names_in_node1(String,Defs));
+ false ->
+ Node
+ end.
+
+replace_names_in_node1(NodeStr,Defs=[{Name,Replacement}|Ds]) ->
+ ReplStr = case Replacement of
+ [Ch|_] when is_integer(Ch) -> Replacement;
+ _ when is_atom(Replacement) -> atom_to_list(Replacement);
+ _ -> false
+ end,
+ if ReplStr == false ->
+ replace_names_in_node1(NodeStr,Ds);
+ true ->
+ case re:replace(NodeStr,atom_to_list(Name),
+ ReplStr,[{return,list}]) of
+ NodeStr -> % no match, proceed
+ replace_names_in_node1(NodeStr,Ds);
+ NodeStr1 ->
+ replace_names_in_node1(NodeStr1,Defs)
+ end
+ end;
+replace_names_in_node1(NodeStr,[]) ->
+ NodeStr.
+
+
+%% global terms that will be used for analysing all other terms in the spec
+get_global([{merge_tests,Bool} | Ts], Spec) ->
+ get_global(Ts,Spec#testspec{merge_tests=Bool});
+
+%% the 'define' term replaces the 'alias' and 'node' terms, but we need to keep
+%% the latter two for backwards compatibility...
get_global([{alias,Ref,Dir}|Ts],Spec=#testspec{alias=Refs}) ->
get_global(Ts,Spec#testspec{alias=[{Ref,get_absdir(Dir,Spec)}|Refs]});
get_global([{node,Ref,Node}|Ts],Spec=#testspec{nodes=Refs}) ->
- get_global(Ts,Spec#testspec{nodes=[{Ref,Node}|lists:keydelete(Node,2,Refs)]});
-get_global([_|Ts],Spec) -> get_global(Ts,Spec);
-get_global([],Spec) -> Spec.
+ get_global(Ts,Spec#testspec{nodes=[{Ref,Node} |
+ lists:keydelete(Node,2,Refs)]});
-get_absfile(Callback, FullName,#testspec{spec_dir=SpecDir}) ->
+get_global([_|Ts],Spec) ->
+ get_global(Ts,Spec);
+get_global([],Spec=#testspec{nodes=Ns, alias=As}) ->
+ Spec#testspec{nodes=lists:reverse(Ns), alias=lists:reverse(As)}.
+
+get_absfile(Callback,FullName,#testspec{spec_dir=SpecDir}) ->
% we need to temporary switch to new cwd here, because
% otherwise config files cannot be found
{ok, OldWd} = file:get_cwd(),
@@ -329,29 +466,45 @@ get_absfile(FullName,#testspec{spec_dir=SpecDir}) ->
get_absdir(Dir,#testspec{spec_dir=SpecDir}) ->
get_absname(Dir,SpecDir).
-get_absname(TestDir,SpecDir) ->
- AbsName = filename:absname(TestDir,SpecDir),
- TestDirName = filename:basename(AbsName),
- Path = filename:dirname(AbsName),
- TopDir = filename:basename(Path),
- Path1 =
- case TopDir of
- "." ->
- [_|Rev] = lists:reverse(filename:split(Path)),
- filename:join(lists:reverse(Rev));
- ".." ->
- [_,_|Rev] = lists:reverse(filename:split(Path)),
- filename:join(lists:reverse(Rev));
- _ ->
- Path
- end,
- filename:join(Path1,TestDirName).
+get_absname(Dir,SpecDir) ->
+ AbsName = filename:absname(Dir,SpecDir),
+ shorten_path(AbsName,SpecDir).
+
+shorten_path(Path,SpecDir) ->
+ case shorten_split_path(filename:split(Path),[]) of
+ [] ->
+ [Root|_] = filename:split(SpecDir),
+ Root;
+ Short ->
+ filename:join(Short)
+ end.
+
+shorten_split_path([".."|Path],SoFar) ->
+ shorten_split_path(Path,tl(SoFar));
+shorten_split_path(["."|Path],SoFar) ->
+ shorten_split_path(Path,SoFar);
+shorten_split_path([Dir|Path],SoFar) ->
+ shorten_split_path(Path,[Dir|SoFar]);
+shorten_split_path([],SoFar) ->
+ lists:reverse(SoFar).
%% go through all tests and register all nodes found
get_all_nodes([{suites,Nodes,_,_}|Ts],Spec) when is_list(Nodes) ->
get_all_nodes(Ts,save_nodes(Nodes,Spec));
get_all_nodes([{suites,Node,_,_}|Ts],Spec) ->
get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([{groups,[Char|_],_,_,_}|Ts],Spec) when is_integer(Char) ->
+ get_all_nodes(Ts,Spec);
+get_all_nodes([{groups,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{groups,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{groups,_,_,_,{cases,_}}|Ts],Spec) ->
+ get_all_nodes(Ts,Spec);
+get_all_nodes([{groups,Node,_,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([{groups,Node,_,_,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
get_all_nodes([{cases,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) ->
get_all_nodes(Ts,save_nodes(Nodes,Spec));
get_all_nodes([{cases,Node,_,_,_}|Ts],Spec) ->
@@ -360,74 +513,93 @@ get_all_nodes([{skip_suites,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) ->
get_all_nodes(Ts,save_nodes(Nodes,Spec));
get_all_nodes([{skip_suites,Node,_,_,_}|Ts],Spec) ->
get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([{skip_groups,[Char|_],_,_,_,_}|Ts],Spec) when is_integer(Char) ->
+ get_all_nodes(Ts,Spec);
+get_all_nodes([{skip_groups,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{skip_groups,Node,_,_,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
+get_all_nodes([{skip_groups,Nodes,_,_,_,_,_}|Ts],Spec) when is_list(Nodes) ->
+ get_all_nodes(Ts,save_nodes(Nodes,Spec));
+get_all_nodes([{skip_groups,Node,_,_,_,_,_}|Ts],Spec) ->
+ get_all_nodes(Ts,save_nodes([Node],Spec));
get_all_nodes([{skip_cases,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) ->
get_all_nodes(Ts,save_nodes(Nodes,Spec));
get_all_nodes([{skip_cases,Node,_,_,_,_}|Ts],Spec) ->
get_all_nodes(Ts,save_nodes([Node],Spec));
-get_all_nodes([_|Ts],Spec) ->
+get_all_nodes([_Other|Ts],Spec) ->
get_all_nodes(Ts,Spec);
get_all_nodes([],Spec) ->
Spec.
-filter_init_terms([{init, InitOptions}|Ts], NewTerms, Spec)->
- filter_init_terms([{init, list_nodes(Spec), InitOptions}|Ts], NewTerms, Spec);
-filter_init_terms([{init, NodeRef, InitOptions}|Ts], NewTerms, Spec)
- when is_atom(NodeRef)->
- filter_init_terms([{init, [NodeRef], InitOptions}|Ts], NewTerms, Spec);
-filter_init_terms([{init, NodeRefs, InitOption}|Ts], NewTerms, Spec) when is_tuple(InitOption) ->
- filter_init_terms([{init, NodeRefs, [InitOption]}|Ts], NewTerms, Spec);
-filter_init_terms([{init, [NodeRef|NodeRefs], InitOptions}|Ts], NewTerms, Spec=#testspec{init=InitData})->
- NodeStartOptions = case lists:keyfind(node_start, 1, InitOptions) of
- {node_start, NSOptions}->
- case lists:keyfind(callback_module, 1, NSOptions) of
- {callback_module, _Callback}->
- NSOptions;
- false->
- [{callback_module, ct_slave}|NSOptions]
- end;
- false->
- []
- end,
- EvalTerms = case lists:keyfind(eval, 1, InitOptions) of
- {eval, MFA} when is_tuple(MFA)->
- [MFA];
- {eval, MFAs} when is_list(MFAs)->
- MFAs;
- false->
- []
- end,
+filter_init_terms([{init,InitOptions}|Ts],NewTerms,Spec) ->
+ filter_init_terms([{init,list_nodes(Spec),InitOptions}|Ts],
+ NewTerms,Spec);
+filter_init_terms([{init,all_nodes,InitOptions}|Ts],NewTerms,Spec) ->
+ filter_init_terms([{init,list_nodes(Spec),InitOptions}|Ts],
+ NewTerms,Spec);
+filter_init_terms([{init,NodeRef,InitOptions}|Ts],
+ NewTerms,Spec) when is_atom(NodeRef) ->
+ filter_init_terms([{init,[NodeRef],InitOptions}|Ts],NewTerms,Spec);
+filter_init_terms([{init,NodeRefs,InitOption}|Ts],
+ NewTerms,Spec) when is_tuple(InitOption) ->
+ filter_init_terms([{init,NodeRefs,[InitOption]}|Ts],NewTerms,Spec);
+filter_init_terms([{init,[NodeRef|NodeRefs],InitOptions}|Ts],
+ NewTerms,Spec=#testspec{init=InitData}) ->
+ NodeStartOptions =
+ case lists:keyfind(node_start,1,InitOptions) of
+ {node_start,NSOptions}->
+ case lists:keyfind(callback_module,1,NSOptions) of
+ {callback_module,_Callback}->
+ NSOptions;
+ false->
+ [{callback_module,ct_slave}|NSOptions]
+ end;
+ false->
+ []
+ end,
+ EvalTerms = case lists:keyfind(eval,1,InitOptions) of
+ {eval,MFA} when is_tuple(MFA) ->
+ [MFA];
+ {eval,MFAs} when is_list(MFAs) ->
+ MFAs;
+ false->
+ []
+ end,
Node = ref2node(NodeRef,Spec#testspec.nodes),
- InitData2 = add_option({node_start, NodeStartOptions}, Node, InitData, true),
- InitData3 = add_option({eval, EvalTerms}, Node, InitData2, false),
- filter_init_terms([{init, NodeRefs, InitOptions}|Ts], NewTerms, Spec#testspec{init=InitData3});
-filter_init_terms([{init, [], _}|Ts], NewTerms, Spec)->
- filter_init_terms(Ts, NewTerms, Spec);
-filter_init_terms([Term|Ts], NewTerms, Spec)->
- filter_init_terms(Ts, [Term|NewTerms], Spec);
-filter_init_terms([], NewTerms, Spec)->
- {lists:reverse(NewTerms), Spec}.
-
-add_option({Key, Value}, Node, List, WarnIfExists) when is_list(Value)->
- OldOptions = case lists:keyfind(Node, 1, List) of
- {Node, Options}->
+ InitData2 = add_option({node_start,NodeStartOptions},Node,InitData,true),
+ InitData3 = add_option({eval,EvalTerms},Node,InitData2,false),
+ filter_init_terms([{init,NodeRefs,InitOptions}|Ts],
+ NewTerms,Spec#testspec{init=InitData3});
+filter_init_terms([{init,[],_}|Ts],NewTerms,Spec) ->
+ filter_init_terms(Ts,NewTerms,Spec);
+filter_init_terms([Term|Ts],NewTerms,Spec) ->
+ filter_init_terms(Ts,[Term|NewTerms],Spec);
+filter_init_terms([],NewTerms,Spec) ->
+ {lists:reverse(NewTerms),Spec}.
+
+add_option({Key,Value},Node,List,WarnIfExists) when is_list(Value) ->
+ OldOptions = case lists:keyfind(Node,1,List) of
+ {Node,Options}->
Options;
false->
[]
end,
- NewOption = case lists:keyfind(Key, 1, OldOptions) of
- {Key, OldOption} when WarnIfExists, OldOption/=[]->
- io:format("There is an option ~w=~w already defined for node ~p, skipping new ~w~n",
- [Key, OldOption, Node, Value]),
+ NewOption = case lists:keyfind(Key,1,OldOptions) of
+ {Key,OldOption} when WarnIfExists,OldOption/=[]->
+ io:format("There is an option ~w=~w already "
+ "defined for node ~p, skipping new ~w~n",
+ [Key,OldOption,Node,Value]),
OldOption;
- {Key, OldOption}->
+ {Key,OldOption}->
OldOption ++ Value;
false->
Value
end,
- lists:keystore(Node, 1, List,
- {Node, lists:keystore(Key, 1, OldOptions, {Key, NewOption})});
-add_option({Key, Value}, Node, List, WarnIfExists)->
- add_option({Key, [Value]}, Node, List, WarnIfExists).
+ lists:keystore(Node,1,List,
+ {Node,lists:keystore(Key,1,OldOptions,{Key,NewOption})});
+add_option({Key,Value},Node,List,WarnIfExists) ->
+ add_option({Key,[Value]},Node,List,WarnIfExists).
save_nodes(Nodes,Spec=#testspec{nodes=NodeRefs}) ->
NodeRefs1 =
@@ -446,294 +618,18 @@ save_nodes(Nodes,Spec=#testspec{nodes=NodeRefs}) ->
end
end
end,NodeRefs,Nodes),
- Spec#testspec{nodes=NodeRefs1}.
+ Spec#testspec{nodes=NodeRefs1}.
list_nodes(#testspec{nodes=NodeRefs}) ->
lists:map(fun({_Ref,Node}) -> Node end, NodeRefs).
-
-%% ---------------------------------------------------------
-%% / \
-%% | When adding tests, remember to update valid_terms/0 also! |
-%% \ /
-%% ---------------------------------------------------------
-
-
-%% Associate a "global" logdir with all nodes
-%% except those with specific logdir, e.g:
-%% ["/tmp/logdir",{ct1@finwe,"/tmp/logdir2"}]
-%% means all nodes should write to /tmp/logdir
-%% except ct1@finwe that should use /tmp/logdir2.
-
-%% --- logdir ---
-add_tests([{logdir,all_nodes,Dir}|Ts],Spec) ->
- Dirs = Spec#testspec.logdir,
- Tests = [{logdir,N,get_absdir(Dir,Spec)} ||
- N <- list_nodes(Spec),
- lists:keymember(ref2node(N,Spec#testspec.nodes),
- 1,Dirs) == false],
- add_tests(Tests++Ts,Spec);
-add_tests([{logdir,Nodes,Dir}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,logdir,[Dir],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{logdir,Node,Dir}|Ts],Spec) ->
- Dirs = Spec#testspec.logdir,
- Dirs1 = [{ref2node(Node,Spec#testspec.nodes),get_absdir(Dir,Spec)} |
- lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,Dirs)],
- add_tests(Ts,Spec#testspec{logdir=Dirs1});
-add_tests([{logdir,Dir}|Ts],Spec) ->
- add_tests([{logdir,all_nodes,Dir}|Ts],Spec);
-
-%% --- 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);
-
-%% --- verbosity ---
-add_tests([{verbosity,all_nodes,VLvls}|Ts],Spec) ->
- VLvls0 = Spec#testspec.verbosity,
- Tests = [{verbosity,N,VLvls} ||
- N <- list_nodes(Spec),
- lists:keymember(ref2node(N,Spec#testspec.nodes),1,
- VLvls0) == false],
- add_tests(Tests++Ts,Spec);
-add_tests([{verbosity,Nodes,VLvls}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,verbosity,[VLvls],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{verbosity,Node,VLvls}|Ts],Spec) ->
- VLvls0 = Spec#testspec.verbosity,
- VLvls1 =
- if is_integer(VLvls) ->
- [{'$unspecified',VLvls}];
- is_list(VLvls) ->
- lists:map(fun(VLvl = {_Cat,_Lvl}) -> VLvl;
- (Lvl) -> {'$unspecified',Lvl} end, VLvls)
- end,
- Verbosity = [{ref2node(Node,Spec#testspec.nodes),VLvls1} |
- lists:keydelete(ref2node(Node,Spec#testspec.nodes),
- 1,VLvls0)],
- add_tests(Ts,Spec#testspec{verbosity=Verbosity});
-add_tests([{verbosity,VLvls}|Ts],Spec) ->
- add_tests([{verbosity,all_nodes,VLvls}|Ts],Spec);
-
-%% --- label ---
-add_tests([{label,all_nodes,Lbl}|Ts],Spec) ->
- Labels = Spec#testspec.label,
- Tests = [{label,N,Lbl} || N <- list_nodes(Spec),
- lists:keymember(ref2node(N,Spec#testspec.nodes),
- 1,Labels) == false],
- add_tests(Tests++Ts,Spec);
-add_tests([{label,Nodes,Lbl}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,label,[Lbl],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{label,Node,Lbl}|Ts],Spec) ->
- Labels = Spec#testspec.label,
- Labels1 = [{ref2node(Node,Spec#testspec.nodes),Lbl} |
- lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,Labels)],
- add_tests(Ts,Spec#testspec{label=Labels1});
-add_tests([{label,Lbl}|Ts],Spec) ->
- add_tests([{label,all_nodes,Lbl}|Ts],Spec);
-
-%% --- cover ---
-add_tests([{cover,all_nodes,File}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {cover,N,File} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{cover,Nodes,File}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,cover,[File],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{cover,Node,File}|Ts],Spec) ->
- CoverFs = Spec#testspec.cover,
- CoverFs1 = [{ref2node(Node,Spec#testspec.nodes),get_absfile(File,Spec)} |
- lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,CoverFs)],
- add_tests(Ts,Spec#testspec{cover=CoverFs1});
-add_tests([{cover,File}|Ts],Spec) ->
- add_tests([{cover,all_nodes,File}|Ts],Spec);
-
-%% --- multiply_timetraps ---
-add_tests([{multiply_timetraps,all_nodes,MT}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {multiply_timetraps,N,MT} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{multiply_timetraps,Nodes,MT}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,multiply_timetraps,[MT],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{multiply_timetraps,Node,MT}|Ts],Spec) ->
- MTs = Spec#testspec.multiply_timetraps,
- MTs1 = [{ref2node(Node,Spec#testspec.nodes),MT} |
- lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,MTs)],
- add_tests(Ts,Spec#testspec{multiply_timetraps=MTs1});
-add_tests([{multiply_timetraps,MT}|Ts],Spec) ->
- add_tests([{multiply_timetraps,all_nodes,MT}|Ts],Spec);
-
-%% --- scale_timetraps ---
-add_tests([{scale_timetraps,all_nodes,ST}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {scale_timetraps,N,ST} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{scale_timetraps,Nodes,ST}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,scale_timetraps,[ST],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{scale_timetraps,Node,ST}|Ts],Spec) ->
- STs = Spec#testspec.scale_timetraps,
- STs1 = [{ref2node(Node,Spec#testspec.nodes),ST} |
- lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,STs)],
- add_tests(Ts,Spec#testspec{scale_timetraps=STs1});
-add_tests([{scale_timetraps,ST}|Ts],Spec) ->
- add_tests([{scale_timetraps,all_nodes,ST}|Ts],Spec);
-
-%% --- create_priv_dir ---
-add_tests([{create_priv_dir,all_nodes,PD}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {create_priv_dir,N,PD} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{create_priv_dir,Nodes,PD}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,create_priv_dir,[PD],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{create_priv_dir,Node,PD}|Ts],Spec) ->
- PDs = Spec#testspec.create_priv_dir,
- PDs1 = [{ref2node(Node,Spec#testspec.nodes),PD} |
- lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,PDs)],
- add_tests(Ts,Spec#testspec{create_priv_dir=PDs1});
-add_tests([{create_priv_dir,PD}|Ts],Spec) ->
- add_tests([{create_priv_dir,all_nodes,PD}|Ts],Spec);
-
-%% --- config ---
-add_tests([{config,all_nodes,Files}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {config,N,Files} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{config,Nodes,Files}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,config,[Files],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{config,Node,[F|Fs]}|Ts],Spec) when is_list(F) ->
- Cfgs = Spec#testspec.config,
- Node1 = ref2node(Node,Spec#testspec.nodes),
- add_tests([{config,Node,Fs}|Ts],
- Spec#testspec{config=[{Node1,get_absfile(F,Spec)}|Cfgs]});
-add_tests([{config,_Node,[]}|Ts],Spec) ->
- add_tests(Ts,Spec);
-add_tests([{config,Node,F}|Ts],Spec) ->
- add_tests([{config,Node,[F]}|Ts],Spec);
-add_tests([{config,Files}|Ts],Spec) ->
- add_tests([{config,all_nodes,Files}|Ts],Spec);
-
-
-%% --- userconfig ---
-add_tests([{userconfig,all_nodes,CBF}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {userconfig,N,CBF} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{userconfig,Nodes,CBF}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,userconfig,[CBF],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{userconfig,Node,[{Callback, Config}|CBF]}|Ts],Spec) ->
- Cfgs = Spec#testspec.userconfig,
- Node1 = ref2node(Node,Spec#testspec.nodes),
- add_tests([{userconfig,Node,CBF}|Ts],
- Spec#testspec{userconfig=[{Node1,{Callback,
- get_absfile(Callback, Config ,Spec)}}|Cfgs]});
-add_tests([{userconfig,_Node,[]}|Ts],Spec) ->
- add_tests(Ts,Spec);
-add_tests([{userconfig,Node,CBF}|Ts],Spec) ->
- add_tests([{userconfig,Node,[CBF]}|Ts],Spec);
-add_tests([{userconfig,CBF}|Ts],Spec) ->
- add_tests([{userconfig,all_nodes,CBF}|Ts],Spec);
-
-%% --- event_handler ---
-add_tests([{event_handler,all_nodes,Hs}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {event_handler,N,Hs,[]} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{event_handler,all_nodes,Hs,Args}|Ts],Spec) when is_list(Args) ->
- Tests = lists:map(fun(N) -> {event_handler,N,Hs,Args} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{event_handler,Hs}|Ts],Spec) ->
- add_tests([{event_handler,all_nodes,Hs,[]}|Ts],Spec);
-add_tests([{event_handler,HsOrNodes,HsOrArgs}|Ts],Spec) ->
- case is_noderef(HsOrNodes,Spec#testspec.nodes) of
- true -> % HsOrNodes == Nodes, HsOrArgs == Hs
- case {HsOrNodes,HsOrArgs} of
- {Nodes,Hs} when is_list(Nodes) ->
- Ts1 = separate(Nodes,event_handler,[Hs,[]],Ts,
- Spec#testspec.nodes),
- add_tests(Ts1,Spec);
- {_Node,[]} ->
- add_tests(Ts,Spec);
- {Node,HOrHs} ->
- EvHs = Spec#testspec.event_handler,
- Node1 = ref2node(Node,Spec#testspec.nodes),
- case HOrHs of
- [H|Hs] when is_atom(H) ->
- add_tests([{event_handler,Node,Hs}|Ts],
- Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]});
- H when is_atom(H) ->
- add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]})
- end
- end;
- false -> % HsOrNodes == Hs, HsOrArgs == Args
- add_tests([{event_handler,all_nodes,HsOrNodes,HsOrArgs}|Ts],Spec)
- end;
-add_tests([{event_handler,Nodes,Hs,Args}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,event_handler,[Hs,Args],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{event_handler,Node,[H|Hs],Args}|Ts],Spec) when is_atom(H) ->
- EvHs = Spec#testspec.event_handler,
- Node1 = ref2node(Node,Spec#testspec.nodes),
- add_tests([{event_handler,Node,Hs,Args}|Ts],
- Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]});
-add_tests([{event_handler,_Node,[],_Args}|Ts],Spec) ->
- add_tests(Ts,Spec);
-add_tests([{event_handler,Node,H,Args}|Ts],Spec) when is_atom(H) ->
- EvHs = Spec#testspec.event_handler,
- Node1 = ref2node(Node,Spec#testspec.nodes),
- add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]});
-
-%% --- ct_hooks --
-add_tests([{ct_hooks, all_nodes, Hooks} | Ts], Spec) ->
- Tests = [{ct_hooks,N,Hooks} || N <- list_nodes(Spec)],
- add_tests(Tests ++ Ts, Spec);
-add_tests([{ct_hooks, Node, [Hook|Hooks]}|Ts], Spec) ->
- SuiteCbs = Spec#testspec.ct_hooks,
- Node1 = ref2node(Node,Spec#testspec.nodes),
- add_tests([{ct_hooks, Node, Hooks} | Ts],
- Spec#testspec{ct_hooks = [{Node1,Hook} | SuiteCbs]});
-add_tests([{ct_hooks, _Node, []}|Ts], Spec) ->
- add_tests(Ts, Spec);
-add_tests([{ct_hooks, Hooks}|Ts], Spec) ->
- add_tests([{ct_hooks, all_nodes, Hooks}|Ts], Spec);
-
-%% -- enable_builtin_hooks --
-add_tests([{enable_builtin_hooks,Bool}|Ts],Spec) ->
- add_tests(Ts, Spec#testspec{ enable_builtin_hooks = Bool });
-
-%% --- include ---
-add_tests([{include,all_nodes,InclDirs}|Ts],Spec) ->
- Tests = lists:map(fun(N) -> {include,N,InclDirs} end, list_nodes(Spec)),
- add_tests(Tests++Ts,Spec);
-add_tests([{include,Nodes,InclDirs}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,include,[InclDirs],Ts,Spec#testspec.nodes),
- add_tests(Ts1,Spec);
-add_tests([{include,Node,[D|Ds]}|Ts],Spec) when is_list(D) ->
- Dirs = Spec#testspec.include,
- Node1 = ref2node(Node,Spec#testspec.nodes),
- add_tests([{include,Node,Ds}|Ts],
- Spec#testspec{include=[{Node1,get_absdir(D,Spec)}|Dirs]});
-add_tests([{include,_Node,[]}|Ts],Spec) ->
- add_tests(Ts,Spec);
-add_tests([{include,Node,D}|Ts],Spec) ->
- add_tests([{include,Node,[D]}|Ts],Spec);
-add_tests([{include,InclDirs}|Ts],Spec) ->
- add_tests([{include,all_nodes,InclDirs}|Ts],Spec);
+%% -----------------------------------------------------
+%% / \
+%% | When adding test/config terms, remember to update |
+%% | valid_terms/0 also! |
+%% \ /
+%% -----------------------------------------------------
%% --- suites ---
add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec) ->
@@ -746,7 +642,7 @@ add_tests([{suites,Nodes,Dir,Ss}|Ts],Spec) when is_list(Nodes) ->
add_tests([{suites,Node,Dir,Ss}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = insert_suites(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Ss,Tests, Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
@@ -766,20 +662,22 @@ add_tests([{groups,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) ->
add_tests([{groups,Nodes,Dir,Suite,Gs}|Ts],Spec) when is_list(Nodes) ->
Ts1 = separate(Nodes,groups,[Dir,Suite,Gs],Ts,Spec#testspec.nodes),
add_tests(Ts1,Spec);
-add_tests([{groups,Nodes,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,groups,[Dir,Suite,Gs,{cases,TCs}],Ts,Spec#testspec.nodes),
+add_tests([{groups,Nodes,Dir,Suite,Gs,{cases,TCs}}|Ts],
+ Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,groups,[Dir,Suite,Gs,{cases,TCs}],Ts,
+ Spec#testspec.nodes),
add_tests(Ts1,Spec);
add_tests([{groups,Node,Dir,Suite,Gs}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = insert_groups(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Suite,Gs,all,Tests,
Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
add_tests([{groups,Node,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = insert_groups(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Suite,Gs,TCs,Tests,
Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
@@ -795,7 +693,7 @@ add_tests([{cases,Nodes,Dir,Suite,Cs}|Ts],Spec) when is_list(Nodes) ->
add_tests([{cases,Node,Dir,Suite,Cs}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = insert_cases(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Suite,Cs,Tests, Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
@@ -810,7 +708,7 @@ add_tests([{skip_suites,Nodes,Dir,Ss,Cmt}|Ts],Spec) when is_list(Nodes) ->
add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = skip_suites(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Ss,Cmt,Tests,
Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
@@ -819,7 +717,8 @@ add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) ->
add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,Cmt}|Ts],Spec) ->
add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,Cmt}|Ts],Spec);
add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) ->
- add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec);
+ add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],
+ Spec);
add_tests([{skip_groups,Dir,Suite,Gs,Cmt}|Ts],Spec) ->
add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,Cmt}|Ts],Spec);
add_tests([{skip_groups,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) ->
@@ -827,20 +726,22 @@ add_tests([{skip_groups,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) ->
add_tests([{skip_groups,Nodes,Dir,Suite,Gs,Cmt}|Ts],Spec) when is_list(Nodes) ->
Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,Cmt],Ts,Spec#testspec.nodes),
add_tests(Ts1,Spec);
-add_tests([{skip_groups,Nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) when is_list(Nodes) ->
- Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,{cases,TCs},Cmt],Ts,Spec#testspec.nodes),
+add_tests([{skip_groups,Nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],
+ Spec) when is_list(Nodes) ->
+ Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,{cases,TCs},Cmt],Ts,
+ Spec#testspec.nodes),
add_tests(Ts1,Spec);
add_tests([{skip_groups,Node,Dir,Suite,Gs,Cmt}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = skip_groups(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Suite,Gs,all,Cmt,Tests,
Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
add_tests([{skip_groups,Node,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = skip_groups(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Suite,Gs,TCs,Cmt,Tests,
Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
@@ -856,45 +757,101 @@ add_tests([{skip_cases,Nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) when is_list(Nodes) ->
add_tests([{skip_cases,Node,Dir,Suite,Cs,Cmt}|Ts],Spec) ->
Tests = Spec#testspec.tests,
Tests1 = skip_cases(ref2node(Node,Spec#testspec.nodes),
- ref2dir(Dir,Spec#testspec.alias),
+ ref2dir(Dir,Spec),
Suite,Cs,Cmt,Tests,Spec#testspec.merge_tests),
add_tests(Ts,Spec#testspec{tests=Tests1});
+%% --- various configuration terms ---
+add_tests([{config,Nodes,CfgDir,Files}|Ts],Spec) when is_list(Nodes);
+ Nodes == all_nodes ->
+ add_tests([{config,Nodes,{CfgDir,Files}}|Ts],Spec);
+add_tests([{config,Node,CfgDir,FileOrFiles}|Ts],Spec) ->
+ add_tests([{config,Node,{CfgDir,FileOrFiles}}|Ts],Spec);
+add_tests([{config,CfgDir=[Ch|_],Files}|Ts],Spec) when is_integer(Ch) ->
+ add_tests([{config,all_nodes,{CfgDir,Files}}|Ts],Spec);
+
+add_tests([{event_handler,Nodes,Hs,Args}|Ts],Spec) when is_list(Nodes);
+ Nodes == all_nodes ->
+ add_tests([{event_handler,Nodes,{Hs,Args}}|Ts],Spec);
+add_tests([{event_handler,Node,HOrHs,Args}|Ts],Spec) ->
+ add_tests([{event_handler,Node,{HOrHs,Args}}|Ts],Spec);
+
+add_tests([{enable_builtin_hooks,Bool}|Ts],Spec) ->
+ add_tests(Ts, Spec#testspec{enable_builtin_hooks = Bool});
+
+add_tests([{noinput,Bool}|Ts],Spec) ->
+ add_tests(Ts, Spec#testspec{noinput = Bool});
+
%% --- handled/errors ---
+add_tests([{define,_,_}|Ts],Spec) -> % handled
+ add_tests(Ts,Spec);
+
add_tests([{alias,_,_}|Ts],Spec) -> % handled
add_tests(Ts,Spec);
add_tests([{node,_,_}|Ts],Spec) -> % handled
add_tests(Ts,Spec);
-add_tests([{merge_tests, _} | Ts], Spec) -> % handled
+add_tests([{merge_tests, _} | Ts], Spec) -> % handled
add_tests(Ts,Spec);
-%% check if it's a CT term that has bad format or if the user seems to
-%% have added something of his/her own, which we'll let pass if relaxed
-%% mode is enabled.
-add_tests([Other|Ts],Spec) when is_tuple(Other) ->
- [Name|_] = tuple_to_list(Other),
- case lists:keymember(Name,1,valid_terms()) of
- true -> % halt
- throw({error,{bad_term_in_spec,Other}});
- false -> % ignore
- case get(relaxed) of
- true ->
- %% warn if name resembles a CT term
- case resembles_ct_term(Name,size(Other)) of
- true ->
- io:format("~nSuspicious term, please check:~n"
- "~p~n", [Other]);
- false ->
- ok
- end,
- add_tests(Ts,Spec);
- false ->
- throw({error,{undefined_term_in_spec,Other}})
- end
+%% --------------------------------------------------
+%% / \
+%% | General add_tests/2 clauses below will work for |
+%% | most test spec configuration terms |
+%% \ /
+%% --------------------------------------------------
+
+%% create one test entry per known node and reinsert
+add_tests([Term={Tag,all_nodes,Data}|Ts],Spec) ->
+ case check_term(Term) of
+ valid ->
+ Tests = [{Tag,Node,Data} || Node <- list_nodes(Spec),
+ should_be_added(Tag,Node,Data,Spec)],
+ add_tests(Tests++Ts,Spec);
+ invalid -> % ignore term
+ add_tests(Ts,Spec)
end;
-
+%% create one test entry per node in Nodes and reinsert
+add_tests([{Tag,[],Data}|Ts],Spec) ->
+ add_tests([{Tag,all_nodes,Data}|Ts],Spec);
+add_tests([{Tag,String=[Ch|_],Data}|Ts],Spec) when is_integer(Ch) ->
+ add_tests([{Tag,all_nodes,{String,Data}}|Ts],Spec);
+add_tests([{Tag,NodesOrOther,Data}|Ts],Spec) when is_list(NodesOrOther) ->
+ case lists:all(fun(Test) -> is_node(Test,Spec#testspec.nodes)
+ end, NodesOrOther) of
+ true ->
+ Ts1 = separate(NodesOrOther,Tag,[Data],Ts,Spec#testspec.nodes),
+ add_tests(Ts1,Spec);
+ false ->
+ add_tests([{Tag,all_nodes,{NodesOrOther,Data}}|Ts],Spec)
+ end;
+%% update data for testspec term of type Tag
+add_tests([Term={Tag,NodeOrOther,Data}|Ts],Spec) ->
+ case is_node(NodeOrOther,Spec#testspec.nodes) of
+ true ->
+ case check_term(Term) of
+ valid ->
+ Node = ref2node(NodeOrOther,Spec#testspec.nodes),
+ NodeIxData =
+ update_recorded(Tag,Node,Spec) ++
+ handle_data(Tag,Node,Data,Spec),
+ add_tests(Ts,mod_field(Spec,Tag,NodeIxData));
+ invalid -> % ignore term
+ add_tests(Ts,Spec)
+ end;
+ false ->
+ add_tests([{Tag,all_nodes,{NodeOrOther,Data}}|Ts],Spec)
+ end;
+%% this test should be added for all known nodes
+add_tests([Term={Tag,Data}|Ts],Spec) ->
+ case check_term(Term) of
+ valid ->
+ add_tests([{Tag,all_nodes,Data}|Ts],Spec);
+ invalid ->
+ add_tests(Ts,Spec)
+ end;
+%% some other data than a tuple
add_tests([Other|Ts],Spec) ->
case get(relaxed) of
true ->
@@ -906,6 +863,105 @@ add_tests([Other|Ts],Spec) ->
add_tests([],Spec) -> % done
Spec.
+%% check if it's a CT term that has bad format or if the user seems to
+%% have added something of his/her own, which we'll let pass if relaxed
+%% mode is enabled.
+check_term(Term) ->
+ Size = size(Term),
+ [Name|_] = tuple_to_list(Term),
+ Valid = valid_terms(),
+ case lists:member({Name,Size},Valid) of
+ true ->
+ valid;
+ false ->
+ case lists:keymember(Name,1,Valid) of
+ true -> % halt
+ throw({error,{bad_term_in_spec,Term}});
+ false -> % ignore
+ case get(relaxed) of
+ true ->
+ %% warn if name resembles a CT term
+ case resembles_ct_term(Name,size(Term)) of
+ true ->
+ io:format("~nSuspicious term, "
+ "please check:~n"
+ "~p~n", [Term]),
+ invalid;
+ false ->
+ invalid
+ end;
+ false ->
+ throw({error,{undefined_term_in_spec,Term}})
+ end
+ end
+ end.
+
+%% specific data handling before saving in testspec record, e.g.
+%% converting relative paths to absolute for directories and files
+%% (introduce a clause *only* if the data value needs processing)
+handle_data(logdir,Node,Dir,Spec) ->
+ [{Node,ref2dir(Dir,Spec)}];
+handle_data(cover,Node,File,Spec) ->
+ [{Node,get_absfile(File,Spec)}];
+handle_data(include,Node,Dirs=[D|_],Spec) when is_list(D) ->
+ [{Node,ref2dir(Dir,Spec)} || Dir <- Dirs];
+handle_data(include,Node,Dir=[Ch|_],Spec) when is_integer(Ch) ->
+ handle_data(include,Node,[Dir],Spec);
+handle_data(config,Node,File=[Ch|_],Spec) when is_integer(Ch) ->
+ handle_data(config,Node,[File],Spec);
+handle_data(config,Node,{CfgDir,File=[Ch|_]},Spec) when is_integer(Ch) ->
+ handle_data(config,Node,{CfgDir,[File]},Spec);
+handle_data(config,Node,Files=[F|_],Spec) when is_list(F) ->
+ [{Node,get_absfile(File,Spec)} || File <- Files];
+handle_data(config,Node,{CfgDir,Files=[F|_]},Spec) when is_list(F) ->
+ [{Node,filename:join(ref2dir(CfgDir,Spec),File)} || File <- Files];
+handle_data(userconfig,Node,CBs,Spec) when is_list(CBs) ->
+ [{Node,{Callback,get_absfile(Callback,Config,Spec)}} ||
+ {Callback,Config} <- CBs];
+handle_data(userconfig,Node,CB,Spec) when is_tuple(CB) ->
+ handle_data(userconfig,Node,[CB],Spec);
+handle_data(event_handler,Node,H,Spec) when is_atom(H) ->
+ handle_data(event_handler,Node,{[H],[]},Spec);
+handle_data(event_handler,Node,{H,Args},Spec) when is_atom(H) ->
+ handle_data(event_handler,Node,{[H],Args},Spec);
+handle_data(event_handler,Node,Hs,_Spec) when is_list(Hs) ->
+ [{Node,EvH,[]} || EvH <- Hs];
+handle_data(event_handler,Node,{Hs,Args},_Spec) when is_list(Hs) ->
+ [{Node,EvH,Args} || EvH <- Hs];
+handle_data(ct_hooks,Node,Hooks,_Spec) when is_list(Hooks) ->
+ [{Node,Hook} || Hook <- Hooks ];
+handle_data(ct_hooks,Node,Hook,_Spec) ->
+ [{Node,Hook}];
+handle_data(stylesheet,Node,CSSFile,Spec) ->
+ [{Node,get_absfile(CSSFile,Spec)}];
+handle_data(_Tag,Node,Data,_Spec) ->
+ [{Node,Data}].
+
+%% check if duplicates should be saved or not
+should_be_added(Tag,Node,_Data,Spec) ->
+ if
+ %% list terms *without* possible duplicates here
+ Tag == logdir; Tag == logopts;
+ Tag == basic_html; Tag == label;
+ Tag == auto_compile; Tag == stylesheet ->
+ lists:keymember(ref2node(Node,Spec#testspec.nodes),1,
+ read_field(Spec,Tag)) == false;
+ %% for terms *with* possible duplicates
+ true ->
+ true
+ end.
+
+%% check if previous elements for Node should be deleted
+update_recorded(Tag,Node,Spec) ->
+ if Tag == config; Tag == userconfig; Tag == event_handler;
+ Tag == ct_hooks; Tag == include ->
+ read_field(Spec,Tag);
+ true ->
+ %% delete previous value for Tag
+ lists:keydelete(Node,1,read_field(Spec,Tag))
+ end.
+
+%% create one test term per node
separate(Nodes,Tag,Data,Tests,Refs) ->
Separated = separate(Nodes,Tag,Data,Refs),
Separated ++ Tests.
@@ -913,7 +969,25 @@ separate([N|Ns],Tag,Data,Refs) ->
[list_to_tuple([Tag,ref2node(N,Refs)|Data])|separate(Ns,Tag,Data,Refs)];
separate([],_,_,_) ->
[].
-
+
+%% read the value for FieldName in record Rec#testspec
+read_field(Rec, FieldName) ->
+ catch lists:foldl(fun(F, Pos) when F == FieldName ->
+ throw(element(Pos, Rec));
+ (_,Pos) ->
+ Pos+1
+ end,2,?testspec_fields).
+
+%% modify the value for FieldName in record Rec#testspec
+mod_field(Rec, FieldName, NewVal) ->
+ [_testspec|RecList] = tuple_to_list(Rec),
+ RecList1 =
+ (catch lists:foldl(fun(F, {Prev,[_OldVal|Rest]}) when F == FieldName ->
+ throw(lists:reverse(Prev) ++ [NewVal|Rest]);
+ (_,{Prev,[Field|Rest]}) ->
+ {[Field|Prev],Rest}
+ end,{[],RecList},?testspec_fields)),
+ list_to_tuple([testspec|RecList1]).
%% Representation:
%% {{Node,Dir},[{Suite1,[GrOrCase11,GrOrCase12,...]},
@@ -1121,33 +1195,40 @@ ref2node(all_nodes,_Refs) ->
ref2node(master,_Refs) ->
master;
ref2node(RefOrNode,Refs) ->
- case string:chr(atom_to_list(RefOrNode),$@) of
- 0 -> % a ref
+ case lists:member($@,atom_to_list(RefOrNode)) of
+ false -> % a ref
case lists:keysearch(RefOrNode,1,Refs) of
{value,{RefOrNode,Node}} ->
Node;
false ->
throw({error,{noderef_missing,RefOrNode}})
end;
- _ -> % a node
+ true -> % a node
RefOrNode
end.
-ref2dir(Ref,Refs) when is_atom(Ref) ->
+ref2dir(Ref,Spec) ->
+ ref2dir(Ref,Spec#testspec.alias,Spec).
+
+ref2dir(Ref,Refs,Spec) when is_atom(Ref) ->
case lists:keysearch(Ref,1,Refs) of
{value,{Ref,Dir}} ->
- Dir;
+ get_absdir(Dir,Spec);
false ->
throw({error,{alias_missing,Ref}})
end;
-ref2dir(Dir,_) when is_list(Dir) ->
- Dir.
-
-is_noderef(What,Nodes) when is_atom(What) ->
- is_noderef([What],Nodes);
-is_noderef([master|_],_Nodes) ->
+ref2dir(Dir,_,Spec) when is_list(Dir) ->
+ get_absdir(Dir,Spec);
+ref2dir(What,_,_) ->
+ throw({error,{invalid_directory_name,What}}).
+
+is_node(What,Nodes) when is_atom(What) ->
+ is_node([What],Nodes);
+is_node([master|_],_Nodes) ->
true;
-is_noderef([What|_],Nodes) ->
+is_node(What={N,H},Nodes) when is_atom(N), is_atom(H) ->
+ is_node([What],Nodes);
+is_node([What|_],Nodes) ->
case lists:keymember(What,1,Nodes) or
lists:keymember(What,2,Nodes) of
true ->
@@ -1155,24 +1236,28 @@ is_noderef([What|_],Nodes) ->
false ->
false
end;
-is_noderef([],_) ->
+is_node([],_) ->
false.
valid_terms() ->
[
+ {define,3},
{node,3},
{cover,2},
{cover,3},
{config,2},
{config,3},
+ {config,4},
{userconfig,2},
{userconfig,3},
{alias,3},
- {merge_tests,1},
+ {merge_tests,2},
{logdir,2},
{logdir,3},
{logopts,2},
{logopts,3},
+ {basic_html,2},
+ {basic_html,3},
{verbosity,2},
{verbosity,3},
{label,2},
@@ -1182,13 +1267,18 @@ valid_terms() ->
{event_handler,4},
{ct_hooks,2},
{ct_hooks,3},
- {enable_builtin_hooks,1},
+ {enable_builtin_hooks,2},
+ {noinput,2},
{multiply_timetraps,2},
{multiply_timetraps,3},
{scale_timetraps,2},
{scale_timetraps,3},
{include,2},
{include,3},
+ {auto_compile,2},
+ {auto_compile,3},
+ {stylesheet,2},
+ {stylesheet,3},
{suites,3},
{suites,4},
{groups,4},
@@ -1203,7 +1293,8 @@ valid_terms() ->
{skip_groups,7},
{skip_cases,5},
{skip_cases,6},
- {create_priv_dir,2}
+ {create_priv_dir,2},
+ {create_priv_dir,3}
].
%% this function "guesses" if the user has misspelled a term name
diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl
index 4b19062697..990a8ae6ef 100644
--- a/lib/common_test/src/ct_util.erl
+++ b/lib/common_test/src/ct_util.erl
@@ -377,11 +377,23 @@ loop(Mode,TestData,StartDir) ->
{'EXIT',_Pid,normal} ->
loop(Mode,TestData,StartDir);
{'EXIT',Pid,Reason} ->
- %% Let process crash in case of error, this shouldn't happen!
- io:format("\n\nct_util_server got EXIT from ~p: ~p\n\n",
- [Pid,Reason]),
- file:set_cwd(StartDir),
- exit(Reason)
+ case ets:lookup(?conn_table,Pid) of
+ [#conn{address=A,callback=CB}] ->
+ %% A connection crashed - remove the connection but don't die
+ ct_logs:tc_log_async(ct_error_notify,
+ "Connection process died: "
+ "Pid: ~p, Address: ~p, Callback: ~p\n"
+ "Reason: ~p\n\n",
+ [Pid,A,CB,Reason]),
+ catch CB:close(Pid),
+ loop(Mode,TestData,StartDir);
+ _ ->
+ %% Let process crash in case of error, this shouldn't happen!
+ io:format("\n\nct_util_server got EXIT from ~p: ~p\n\n",
+ [Pid,Reason]),
+ file:set_cwd(StartDir),
+ exit(Reason)
+ end
end.
diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl
index 48e3027f0a..7015014441 100644
--- a/lib/common_test/src/ct_util.hrl
+++ b/lib/common_test/src/ct_util.hrl
@@ -34,6 +34,7 @@
profile=[],
logdir=["."],
logopts=[],
+ basic_html=[],
verbosity=[],
cover=[],
config=[],
@@ -41,7 +42,10 @@
event_handler=[],
ct_hooks=[],
enable_builtin_hooks=true,
+ noinput=false,
include=[],
+ auto_compile=[],
+ stylesheet=[],
multiply_timetraps=[],
scale_timetraps=[],
create_priv_dir=[],
@@ -65,3 +69,11 @@
-define(ct_config_txt, ct_config_plain).
-define(ct_profile_file, ".common_test").
+
+-define(css_default, "ct_default.css").
+-define(sortable_table_name, "SortableTable").
+-define(jquery_script, "jquery-latest.js").
+-define(tablesorter_script, "jquery.tablesorter.min.js").
+
+%% Logging information for error handler
+-record(conn_log, {client, name, address, action, module}).
diff --git a/lib/common_test/src/cth_conn_log.erl b/lib/common_test/src/cth_conn_log.erl
new file mode 100644
index 0000000000..3af89db3a5
--- /dev/null
+++ b/lib/common_test/src/cth_conn_log.erl
@@ -0,0 +1,124 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 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%
+%%
+%%----------------------------------------------------------------------
+%% CT hook for logging of connections.
+%%
+%% HookOptions can be hardcoded in the test suite:
+%%
+%% suite() ->
+%% [{ct_hooks, [{cth_conn_log,
+%% [{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}]}].
+%%
+%% or specified in a configuration file:
+%%
+%% {ct_conn_log,[{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}.
+%%
+%% The conn_mod() is the common test module implementing the protocol,
+%% e.g. ct_netconfc, ct_telnet, etc. This module must log by calling
+%%
+%% error_logger:info_report(ConnLogInfo,Data).
+%% ConnLogInfo = #conn_log{} | {ct_connection,Action,ConnName}
+%% Action = open | close | send | recv | term()
+%% ConnName = atom() - The 'KeyOrName' argument used when opening the connection
+%%
+%% ct_conn_log_h will print to html log or separate file (depending on
+%% log_type() option). conn_mod() must implement and export
+%%
+%% format_data(log_type(), Data).
+%%
+%% If logging to separate file, ct_conn_log_h will also log error
+%% reports which are witten like this:
+%%
+%% error_logger:error_report([{ct_connection,ConnName} | Report]).
+%%
+%%----------------------------------------------------------------------
+-module(cth_conn_log).
+
+-include_lib("common_test/include/ct.hrl").
+
+-export([init/2,
+ pre_init_per_testcase/3,
+ post_end_per_testcase/4]).
+
+-spec init(Id, HookOpts) -> Result when
+ Id :: term(),
+ HookOpts :: ct:hook_options(),
+ Result :: {ok,[{ct_netconfc:conn_mod(),
+ {ct_netconfc:log_type(),[ct_netconfc:key_or_name()]}}]}.
+init(_Id, HookOpts) ->
+ ConfOpts = ct:get_config(ct_conn_log,[]),
+ {ok,merge_log_info(ConfOpts,HookOpts)}.
+
+merge_log_info([{Mod,ConfOpts}|ConfList],HookList) ->
+ {Opts,HookList1} =
+ case lists:keytake(Mod,1,HookList) of
+ false ->
+ {ConfOpts,HookList};
+ {value,{_,HookOpts},HL1} ->
+ {ConfOpts ++ HookOpts, HL1} % ConfOpts overwrites HookOpts!
+ end,
+ [{Mod,get_log_opts(Opts)} | merge_log_info(ConfList,HookList1)];
+merge_log_info([],HookList) ->
+ [{Mod,get_log_opts(Opts)} || {Mod,Opts} <- HookList].
+
+get_log_opts(Opts) ->
+ LogType = proplists:get_value(log_type,Opts,html),
+ Hosts = proplists:get_value(hosts,Opts,[]),
+ {LogType,Hosts}.
+
+
+pre_init_per_testcase(TestCase,Config,CthState) ->
+ Logs =
+ lists:map(
+ fun({ConnMod,{LogType,Hosts}}) ->
+ case LogType of
+ LogType when LogType==raw; LogType==pretty ->
+ Dir = ?config(priv_dir,Config),
+ TCStr = atom_to_list(TestCase),
+ ConnModStr = atom_to_list(ConnMod),
+ DefLogName = TCStr ++ "-" ++ ConnModStr ++ ".txt",
+ DefLog = filename:join(Dir,DefLogName),
+ Ls = [{Host,
+ filename:join(Dir,TCStr ++ "-"++
+ atom_to_list(Host) ++ "-" ++
+ ConnModStr ++
+ ".txt")}
+ || Host <- Hosts]
+ ++[{default,DefLog}],
+ Str =
+ "<table borders=1>"
+ "<b>" ++ ConnModStr ++ " logs:</b>\n" ++
+ [io_lib:format(
+ "<tr><td>~p</td><td><a href=~p>~s</a></td></tr>",
+ [S,L,filename:basename(L)])
+ || {S,L} <- Ls] ++
+ "</table>",
+ io:format(Str,[]),
+ {ConnMod,{LogType,Ls}};
+ _ ->
+ {ConnMod,{LogType,[]}}
+ end
+ end,
+ CthState),
+ error_logger:add_report_handler(ct_conn_log_h,{group_leader(),Logs}),
+ {Config,CthState}.
+
+post_end_per_testcase(_TestCase,_Config,Return,CthState) ->
+ error_logger:delete_report_handler(ct_conn_log_h),
+ {Return,CthState}.
diff --git a/lib/common_test/src/cth_surefire.erl b/lib/common_test/src/cth_surefire.erl
index c42f956b3a..e7bd84e51b 100644
--- a/lib/common_test/src/cth_surefire.erl
+++ b/lib/common_test/src/cth_surefire.erl
@@ -49,9 +49,12 @@ init(Path, Opts) ->
properties = proplists:get_value(properties,Opts,[]),
timer = now() }.
-pre_init_per_suite(Suite,Config,State) ->
+pre_init_per_suite(Suite,Config,#state{ test_cases = [] } = State) ->
{Config, init_tc(State#state{ curr_suite = Suite, curr_suite_ts = now() },
- Config) }.
+ Config) };
+pre_init_per_suite(Suite,Config,State) ->
+ %% Have to close the previous suite
+ pre_init_per_suite(Suite,Config,close_suite(State)).
post_init_per_suite(_Suite,Config, Result, State) ->
{Result, end_tc(init_per_suite,Config,Result,State)}.
@@ -59,11 +62,7 @@ post_init_per_suite(_Suite,Config, Result, State) ->
pre_end_per_suite(_Suite,Config,State) -> {Config, init_tc(State, Config)}.
post_end_per_suite(_Suite,Config,Result,State) ->
- NewState = end_tc(end_per_suite,Config,Result,State),
- TCs = NewState#state.test_cases,
- Suite = get_suite(NewState, TCs),
- {Result, State#state{ test_cases = [],
- test_suites = [Suite | State#state.test_suites]}}.
+ {Result, end_tc(end_per_suite,Config,Result,State)}.
pre_init_per_group(Group,Config,State) ->
{Config, init_tc(State#state{ curr_group = [Group|State#state.curr_group]},
@@ -83,24 +82,36 @@ pre_init_per_testcase(_TC,Config,State) -> {Config, init_tc(State, Config)}.
post_end_per_testcase(TC,Config,Result,State) ->
{Result, end_tc(TC,Config, Result,State)}.
+on_tc_fail(_TC, _Res, State = #state{test_cases = []}) ->
+ State;
on_tc_fail(_TC, Res, State) ->
TCs = State#state.test_cases,
- TC = hd(State#state.test_cases),
- NewTC = TC#testcase{ failure =
- {fail,lists:flatten(io_lib:format("~p",[Res]))} },
+ TC = hd(TCs),
+ NewTC = TC#testcase{
+ failure =
+ {fail,lists:flatten(io_lib:format("~p",[Res]))} },
State#state{ test_cases = [NewTC | tl(TCs)]}.
+on_tc_skip(Tc,{Type,Reason} = Res, State) when Type == tc_auto_skip ->
+ do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(State,[])));
+on_tc_skip(_Tc, _Res, State = #state{test_cases = []}) ->
+ State;
on_tc_skip(_Tc, Res, State) ->
+ do_tc_skip(Res, State).
+
+do_tc_skip(Res, State) ->
TCs = State#state.test_cases,
- TC = hd(State#state.test_cases),
+ TC = hd(TCs),
NewTC = TC#testcase{
failure =
{skipped,lists:flatten(io_lib:format("~p",[Res]))} },
State#state{ test_cases = [NewTC | tl(TCs)]}.
+init_tc(State, Config) when is_list(Config) == false ->
+ State#state{ timer = now(), tc_log = "" };
init_tc(State, Config) ->
State#state{ timer = now(),
- tc_log = proplists:get_value(tc_logfile, Config)}.
+ tc_log = proplists:get_value(tc_logfile, Config, [])}.
end_tc(Func, Config, Res, State) when is_atom(Func) ->
end_tc(atom_to_list(Func), Config, Res, State);
@@ -118,26 +129,35 @@ end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite,
name = Name,
time = TimeTakes,
failure = passed }| State#state.test_cases]}.
-
-get_suite(State, TCs) ->
+close_suite(#state{ test_cases = [] } = State) ->
+ State;
+close_suite(#state{ test_cases = TCs } = State) ->
Total = length(TCs),
Succ = length(lists:filter(fun(#testcase{ failure = F }) ->
F == passed
end,TCs)),
Fail = Total - Succ,
TimeTaken = timer:now_diff(now(),State#state.curr_suite_ts) / 1000000,
- #testsuite{ name = atom_to_list(State#state.curr_suite),
- package = State#state.package,
- time = io_lib:format("~f",[TimeTaken]),
- timestamp = now_to_string(State#state.curr_suite_ts),
- errors = Fail, tests = Total, testcases = lists:reverse(TCs) }.
-
-terminate(State) ->
- {ok,D} = file:open(State#state.filepath,[write]),
+ Suite = #testsuite{ name = atom_to_list(State#state.curr_suite),
+ package = State#state.package,
+ time = io_lib:format("~f",[TimeTaken]),
+ timestamp = now_to_string(State#state.curr_suite_ts),
+ errors = Fail, tests = Total,
+ testcases = lists:reverse(TCs) },
+ State#state{ test_cases = [],
+ test_suites = [Suite | State#state.test_suites]}.
+
+terminate(State = #state{ test_cases = [] }) ->
+ {ok,D} = file:open(State#state.filepath,[write,{encoding,utf8}]),
io:format(D, "<?xml version=\"1.0\" encoding= \"UTF-8\" ?>", []),
io:format(D, to_xml(State), []),
catch file:sync(D),
- catch file:close(D).
+ catch file:close(D);
+terminate(State) ->
+ %% Have to close the last suite
+ terminate(close_suite(State)).
+
+
to_xml(#testcase{ group = Group, classname = CL, log = L, name = N, time = T, timestamp = TS, failure = F}) ->
["<testcase ",