From 84adefa331c4159d432d22840663c38f155cd4c1 Mon Sep 17 00:00:00 2001 From: Erlang/OTP Date: Fri, 20 Nov 2009 14:54:40 +0000 Subject: The R13B03 release. --- lib/common_test/src/ct_logs.erl | 1606 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1606 insertions(+) create mode 100644 lib/common_test/src/ct_logs.erl (limited to 'lib/common_test/src/ct_logs.erl') diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl new file mode 100644 index 0000000000..bd1a89ae1f --- /dev/null +++ b/lib/common_test/src/ct_logs.erl @@ -0,0 +1,1606 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2003-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%%% @doc Logging functionality for Common Test Framework. +%%% +%%%

This module implements +%%%

+%%%

+ +-module(ct_logs). + +-export([init/1,close/1,init_tc/0,end_tc/1]). +-export([get_log_dir/0,log/3,start_log/1,cont_log/2,end_log/0]). +-export([set_stylesheet/2,clear_stylesheet/1]). +-export([add_external_logs/1,add_link/3]). +-export([make_last_run_index/0]). +-export([make_all_suites_index/1,make_all_runs_index/1]). + +%% Logging stuff directly from testcase +-export([tc_log/3,tc_print/3,tc_pal/3]). + +%% Simulate logger process for use without ct environment running +-export([simulate/0]). + +-include("ct_event.hrl"). +-include("ct_util.hrl"). +-include_lib("kernel/include/file.hrl"). + +-define(suitelog_name,"suite.log"). +-define(run_summary, "suite.summary"). +-define(logdir_ext, ".logs"). +-define(ct_log_name, "ctlog.html"). +-define(all_runs_name, "all_runs.html"). +-define(index_name, "index.html"). +-define(totals_name, "totals.info"). + +-define(table_color1,"#ADD8E6"). +-define(table_color2,"#E4F0FE"). +-define(table_color3,"#F0F8FF"). + +-define(testname_width, 70). + +-define(abs(Name), filename:absname(Name)). + +%%%----------------------------------------------------------------- +%%% @spec init(Mode) -> Result +%%% Mode = normal | interactive +%%% Result = {StartTime,LogDir} +%%% StartTime = term() +%%% LogDir = string() +%%% +%%% @doc Initiate the logging mechanism (tool-internal use only). +%%% +%%%

This function is called by ct_util.erl when testing is +%%% started. A new directory named ct_run.<timestamp> is created +%%% and all logs are stored under this directory.

+%%% +init(Mode) -> + Self = self(), + Pid = spawn_link(fun() -> logger(Self,Mode) end), + MRef = erlang:monitor(process,Pid), + receive + {started,Pid,Result} -> + erlang:demonitor(MRef), + Result; + {'DOWN',MRef,process,_,Reason} -> + exit({could_not_start_process,?MODULE,Reason}) + end. + +make_dirname({{YY,MM,DD},{H,M,S}}) -> + io_lib:format(logdir_node_prefix()++".~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w", + [YY,MM,DD,H,M,S]). + +logdir_prefix() -> + "ct_run". +logdir_node_prefix() -> + logdir_prefix()++"."++atom_to_list(node()). + +%%%----------------------------------------------------------------- +%%% @spec close(How) -> ok +%%% +%%% @doc Create index pages with test results and close the CT Log +%%% (tool-internal use only). +close(How) -> + make_last_run_index(), + + ct_event:notify(#event{name=stop_logging,node=node(),data=[]}), + + case whereis(?MODULE) of + Pid when is_pid(Pid) -> + MRef = erlang:monitor(process,Pid), + ?MODULE ! stop, + receive + {'DOWN',MRef,process,_,_} -> + ok + end; + undefined -> + ok + end, + + if How == clean -> + case cleanup() of + ok -> + ok; + Error -> + io:format("Warning! Cleanup failed: ~p~n", [Error]) + end; + true -> + file:set_cwd("..") + end, + + make_all_suites_index(stop), + make_all_runs_index(stop), + + ok. + +%%%----------------------------------------------------------------- +%%% @spec set_stylesheet(TC,SSFile) -> ok +set_stylesheet(TC, SSFile) -> + cast({set_stylesheet,TC,SSFile}). + +%%%----------------------------------------------------------------- +%%% @spec clear_stylesheet(TC) -> ok +clear_stylesheet(TC) -> + cast({clear_stylesheet,TC}). + +%%%----------------------------------------------------------------- +%%% @spec get_log_dir() -> {ok,Dir} | {error,Reason} +get_log_dir() -> + call(get_log_dir). + +%%%----------------------------------------------------------------- +%%% make_last_run_index() -> ok +make_last_run_index() -> + call(make_last_run_index). + +call(Msg) -> + case whereis(?MODULE) of + undefined -> + {error,does_not_exist}; + Pid -> + MRef = erlang:monitor(process,Pid), + Ref = make_ref(), + ?MODULE ! {Msg,{self(),Ref}}, + receive + {Ref, Result} -> + erlang:demonitor(MRef), + Result; + {'DOWN',MRef,process,_,Reason} -> + {error,{process_down,?MODULE,Reason}} + end + end. + +return({To,Ref},Result) -> + To ! {Ref, Result}. + +cast(Msg) -> + case whereis(?MODULE) of + undefined -> + {error,does_not_exist}; + _Pid -> + ?MODULE ! Msg + end. + + +%%%----------------------------------------------------------------- +%%% @spec init_tc() -> ok +%%% +%%% @doc Test case initiation (tool-internal use only). +%%% +%%%

This function is called by ct_framework:init_tc/3

+init_tc() -> + call({init_tc,self(),group_leader()}), + ok. + +%%%----------------------------------------------------------------- +%%% @spec end_tc(TCPid) -> ok | {error,Reason} +%%% +%%% @doc Test case clean up (tool-internal use only). +%%% +%%%

This function is called by ct_framework:end_tc/3

+end_tc(TCPid) -> + %% use call here so that the TC process will wait and receive + %% possible exit signals from ct_logs before end_tc returns ok + call({end_tc,TCPid}). + +%%%----------------------------------------------------------------- +%%% @spec log(Heading,Format,Args) -> ok +%%% +%%% @doc Log internal activity (tool-internal use only). +%%% +%%%

This function writes an entry to the currently active log, +%%% i.e. either the CT log or a test case log.

+%%% +%%%

Heading is a short string indicating what type of +%%% activity it is. Format and Args is the +%%% data to log (as in io:format(Format,Args)).

+log(Heading,Format,Args) -> + cast({log,self(),group_leader(), + [{int_header(),[log_timestamp(now()),Heading]}, + {Format,Args}, + {int_footer(),[]}]}), + ok. + +%%%----------------------------------------------------------------- +%%% @spec start_log(Heading) -> ok +%%% +%%% @doc Starts the logging of an activity (tool-internal use only). +%%% +%%%

This function must be used in combination with +%%% cont_log/2 and end_log/0. The intention +%%% is to call start_log once, then cont_log +%%% any number of times and finally end_log once.

+%%% +%%%

For information about the parameters, see log/3.

+%%% +%%% @see log/3 +%%% @see cont_log/2 +%%% @see end_log/0 +start_log(Heading) -> + cast({log,self(),group_leader(), + [{int_header(),[log_timestamp(now()),Heading]}]}), + ok. + +%%%----------------------------------------------------------------- +%%% @spec cont_log(Format,Args) -> ok +%%% +%%% @doc Adds information about an activity (tool-internal use only). +%%% +%%% @see start_log/1 +%%% @see end_log/0 +cont_log([],[]) -> + ok; +cont_log(Format,Args) -> + maybe_log_timestamp(), + cast({log,self(),group_leader(),[{Format,Args}]}), + ok. + +%%%----------------------------------------------------------------- +%%% @spec end_log() -> ok +%%% +%%% @doc Ends the logging of an activity (tool-internal use only). +%%% +%%% @see start_log/1 +%%% @see cont_log/2 +end_log() -> + cast({log,self(),group_leader(),[{int_footer(), []}]}), + ok. + + +%%%----------------------------------------------------------------- +%%% @spec add_external_logs(Logs) -> ok +%%% Logs = [Log] +%%% Log = string() +%%% +%%% @doc Print a link to each given Log in the test case +%%% log. +%%% +%%%

The given Logs must exist in the priv dir of the +%%% calling test suite.

+add_external_logs(Logs) -> + start_log("External Logs"), + [cont_log("~s\n", + [filename:join("log_private",Log),Log]) || Log <- Logs], + end_log(). + +%%%----------------------------------------------------------------- +%%% @spec add_link(Heading,File,Type) -> ok +%%% Heading = string() +%%% File = string() +%%% Type = string() +%%% +%%% @doc Print a link to a given file stored in the priv_dir of the +%%% calling test suite. +add_link(Heading,File,Type) -> + log(Heading,"~s\n", + [filename:join("log_private",File),Type,File]). + + + +%%%----------------------------------------------------------------- +%%% @spec tc_log(Category,Format,Args) -> ok +%%% Category = atom() +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Printout from a testcase. +%%% +%%%

This function is called by ct when logging +%%% stuff directly from a testcase (i.e. not from within the CT +%%% framework).

+tc_log(Category,Format,Args) -> + cast({log,self(),group_leader(),[{div_header(Category),[]}, + {Format,Args}, + {div_footer(),[]}]}), + ok. + +%%%----------------------------------------------------------------- +%%% @spec tc_print(Category,Format,Args) -> ok +%%% Category = atom() +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Console printout from a testcase. +%%% +%%%

This function is called by ct when printing +%%% stuff a testcase on the user console.

+tc_print(Category,Format,Args) -> + print_heading(Category), + io:format(user,Format,Args), + io:format(user,"\n\n",[]), + ok. + +print_heading(default) -> + io:format(user, + "----------------------------------------------------\n~s\n", + [log_timestamp(now())]); +print_heading(Category) -> + io:format(user, + "----------------------------------------------------\n~s ~w\n", + [log_timestamp(now()),Category]). + + +%%%----------------------------------------------------------------- +%%% @spec tc_pal(Category,Format,Args) -> ok +%%% Category = atom() +%%% Format = string() +%%% Args = list() +%%% +%%% @doc Print and log from a testcase. +%%% +%%%

This function is called by ct when logging +%%% stuff directly from a testcase. The info is written both in the +%%% log and on the console.

+tc_pal(Category,Format,Args) -> + tc_print(Category,Format,Args), + cast({log,self(),group_leader(),[{div_header(Category),[]}, + {Format,Args}, + {div_footer(),[]}]}), + ok. + + +%%%================================================================= +%%% Internal functions +int_header() -> + "
*** CT ~s *** ~s". +int_footer() -> + "
". + +div_header(Class) -> + "
*** User " ++ + log_timestamp(now()) ++ " ***". +div_footer() -> + "
". + + +maybe_log_timestamp() -> + {MS,S,US} = now(), + case get(log_timestamp) of + {MS,S,_} -> + ok; + _ -> + cast({log,self(),group_leader(), + [{"~s",[log_timestamp({MS,S,US})]}]}) + end. + +log_timestamp(Now) -> + put(log_timestamp,Now), + {_,{H,M,S}} = calendar:now_to_local_time(Now), + lists:flatten(io_lib:format("~2.2.0w:~2.2.0w:~2.2.0w", + [H,M,S])). + +%%%----------------------------------------------------------------- +%%% The logger server +-record(logger_state,{parent, + log_dir, + start_time, + orig_GL, + ct_log_fd, + tc_groupleaders, + stylesheet}). + +logger(Parent,Mode) -> + register(?MODULE,self()), + + %%! Below is a temporary workaround for the limitation of + %%! max one test run per second. + %%! ---> + Time0 = calendar:local_time(), + Dir0 = make_dirname(Time0), + {Time,Dir} = + case filelib:is_dir(Dir0) of + true -> + timer:sleep(1000), + Time1 = calendar:local_time(), + Dir1 = make_dirname(Time1), + + {Time1,Dir1}; + false -> + {Time0,Dir0} + end, + %%! <--- + + file:make_dir(Dir), + ct_event:notify(#event{name=start_logging,node=node(), + data=?abs(Dir)}), + make_all_suites_index(start), + make_all_runs_index(start), + case Mode of + interactive -> interactive_link(); + _ -> ok + end, + file:set_cwd(Dir), + make_last_run_index(Time), + CtLogFd = open_ctlog(), + io:format(CtLogFd,int_header()++int_footer(), + [log_timestamp(now()),"Common Test Logger started"]), + Parent ! {started,self(),{Time,filename:absname("")}}, + set_evmgr_gl(CtLogFd), + logger_loop(#logger_state{parent=Parent, + log_dir=Dir, + start_time=Time, + orig_GL=group_leader(), + ct_log_fd=CtLogFd, + tc_groupleaders=[]}). + +logger_loop(State) -> + receive + {log,Pid,GL,List} -> + case get_groupleader(Pid,GL,State) of + {tc_log,TCGL,TCGLs} -> + case erlang:is_process_alive(TCGL) of + true -> + %% we have to build one io-list of all strings + %% before printing, or other io printouts (made in + %% parallel) may get printed between this header + %% and footer + Fun = + fun({Str,Args},IoList) -> + case catch io_lib:format(Str,Args) of + {'EXIT',_Reason} -> + Fd = State#logger_state.ct_log_fd, + io:format(Fd, + "Logging fails! Str: ~p, Args: ~p~n", + [Str,Args]), + %% stop the testcase, we need to see the fault + exit(Pid,logging_failed), + ok; + IoStr when IoList == [] -> + [IoStr]; + IoStr -> + [IoList,"\n",IoStr] + end + end, + io:format(TCGL,"~s",[lists:foldl(Fun,[],List)]), + logger_loop(State#logger_state{tc_groupleaders=TCGLs}); + false -> + %% Group leader is dead, so write to the CtLog instead + Fd = State#logger_state.ct_log_fd, + [begin io:format(Fd,Str,Args),io:nl(Fd) end || + {Str,Args} <- List], + logger_loop(State) + end; + {ct_log,Fd,TCGLs} -> + [begin io:format(Fd,Str,Args),io:nl(Fd) end || {Str,Args} <- List], + logger_loop(State#logger_state{tc_groupleaders=TCGLs}) + end; + {{init_tc,TCPid,GL},From} -> + print_style(GL, State#logger_state.stylesheet), + set_evmgr_gl(GL), + TCGLs = add_tc_gl(TCPid,GL,State), + return(From,ok), + logger_loop(State#logger_state{tc_groupleaders=TCGLs}); + {{end_tc,TCPid},From} -> + set_evmgr_gl(State#logger_state.ct_log_fd), + return(From,ok), + logger_loop(State#logger_state{tc_groupleaders=rm_tc_gl(TCPid,State)}); + {get_log_dir,From} -> + return(From,{ok,State#logger_state.log_dir}), + logger_loop(State); + {make_last_run_index,From} -> + make_last_run_index(State#logger_state.start_time), + return(From,State#logger_state.log_dir), + logger_loop(State); + {set_stylesheet,_,SSFile} when State#logger_state.stylesheet == SSFile -> + logger_loop(State); + {set_stylesheet,TC,SSFile} -> + Fd = State#logger_state.ct_log_fd, + io:format(Fd, "~p uses external style sheet: ~s~n", [TC,SSFile]), + logger_loop(State#logger_state{stylesheet=SSFile}); + {clear_stylesheet,_} when State#logger_state.stylesheet == undefined -> + logger_loop(State); + {clear_stylesheet,_} -> + logger_loop(State#logger_state{stylesheet=undefined}); + stop -> + io:format(State#logger_state.ct_log_fd, + int_header()++int_footer(), + [log_timestamp(now()),"Common Test Logger finished"]), + close_ctlog(State#logger_state.ct_log_fd), + ok + end. + +%% #logger_state.tc_groupleaders == [{Pid,{Type,GLPid}},...] +%% Type = tc | io +%% +%% Pid can either be a test case process (tc), an IO process (io) +%% spawned by a test case process, or a common test process (never +%% registered by an init_tc msg). An IO process gets registered the +%% first time it sends data and will be stored in the list until the +%% last TC process associated with the same group leader gets +%% unregistered. +%% +%% If a process that has not been spawned by a test case process +%% sends a log request, the data will be printed to a test case +%% log file *if* there exists one registered process only in the +%% tc_groupleaders list. If multiple test case processes are +%% running, the data gets printed to the CT framework log instead. +%% +%% Note that an external process must not be registered as an IO +%% process since it could then accidentally be associated with +%% the first test case process that starts in a group of parallel +%% cases (if the log request would come in between the registration +%% of the first and second test case process). + +get_groupleader(Pid,GL,State) -> + TCGLs = State#logger_state.tc_groupleaders, + %% check if Pid is registered either as a TC or IO process + case proplists:get_value(Pid,TCGLs) of + undefined -> + %% this could be a process spawned by the test case process, + %% if so they have the same registered group leader + case lists:keysearch({tc,GL},2,TCGLs) of + {value,_} -> + %% register the io process + {tc_log,GL,[{Pid,{io,GL}}|TCGLs]}; + false -> + %% check if only one test case is executing, + %% if so return the group leader for it + case [TCGL || {_,{Type,TCGL}} <- TCGLs, Type == tc] of + [TCGL] -> + %% an external process sending the log + %% request, don't register + {tc_log,TCGL,TCGLs}; + _ -> + {ct_log,State#logger_state.ct_log_fd,TCGLs} + end + end; + {_,GL} -> + {tc_log,GL,TCGLs}; + _ -> + %% special case where a test case io process has changed + %% its group leader to an non-registered GL process + TCGLs1 = proplists:delete(Pid,TCGLs), + case [TCGL || {_,{Type,TCGL}} <- TCGLs1, Type == tc] of + [TCGL] -> + {tc_log,TCGL,TCGLs1}; + _ -> + {ct_log,State#logger_state.ct_log_fd,TCGLs1} + end + end. + +add_tc_gl(TCPid,GL,State) -> + TCGLs = State#logger_state.tc_groupleaders, + [{TCPid,{tc,GL}} | lists:keydelete(TCPid,1,TCGLs)]. + +rm_tc_gl(TCPid,State) -> + TCGLs = State#logger_state.tc_groupleaders, + case proplists:get_value(TCPid,TCGLs) of + {tc,GL} -> + TCGLs1 = lists:keydelete(TCPid,1,TCGLs), + case lists:keysearch({tc,GL},2,TCGLs1) of + {value,_} -> + %% test cases using GL remain, keep associated IO processes + TCGLs1; + false -> + %% last test case using GL, delete all associated IO processes + lists:filter(fun({_,{io,GLPid}}) when GL == GLPid -> false; + (_) -> true + end, TCGLs1) + end; + _ -> + %% add_tc_gl has not been called for this Pid, ignore + TCGLs + end. + +set_evmgr_gl(GL) -> + case whereis(?CT_EVMGR_REF) of + undefined -> ok; + EvMgrPid -> group_leader(GL,EvMgrPid) + end. + +open_ctlog() -> + {ok,Fd} = file:open(?ct_log_name,[write]), + io:format(Fd,header("Common Test Framework"),[]), + case file:consult(ct_run:variables_file_name("../")) of + {ok,Vars} -> + io:format(Fd, config_table(Vars), []); + {error,Reason} -> + {ok,Cwd} = file:get_cwd(), + Dir = filename:dirname(Cwd), + Variables = ct_run:variables_file_name(Dir), + io:format(Fd, + "Can not read the file \'~s\' Reason: ~w\n" + "No configuration found for test!!\n", + [Variables,Reason]) + end, + print_style(Fd,undefined), + io:format(Fd, + "

Progress Log

\n" + "
\n",[]),
+    Fd.
+
+print_style(Fd,undefined) ->
+    io:format(Fd,
+	      "\n",
+	      []);
+
+print_style(Fd,StyleSheet) ->
+    case file:read_file(StyleSheet) of
+	{ok,Bin} ->
+	    Str = binary_to_list(Bin),
+	    Pos0 = case string:str(Str,"") of
+			       0 -> string:str(Str,"");
+			       N1 -> N1
+			   end,
+		    case Pos1 of
+			0 -> 
+			    print_style_error(Fd,StyleSheet,missing_style_end_tag);
+			_ -> 
+			    Style = string:sub_string(Str,Pos0,Pos1+7),
+			    io:format(Fd,"~s\n",[Style])
+		    end
+	    end;
+	{error,Reason} ->
+	    print_style_error(Fd,StyleSheet,Reason)  
+    end.
+
+%% Simple link version, doesn't work with all browsers unfortunately. :-(
+%% print_style(Fd, StyleSheet) ->
+%%    io:format(Fd,
+%%	      "",
+%%	      [StyleSheet]).
+
+print_style_error(Fd,StyleSheet,Reason) ->
+    io:format(Fd,"\n\n",
+	      [StyleSheet,Reason]),
+    print_style(Fd,undefined).    
+
+close_ctlog(Fd) ->
+    io:format(Fd,"
",[]), + io:format(Fd,footer(),[]), + file:close(Fd). + + + +%%%----------------------------------------------------------------- +%%% Make an index page for the last run +make_last_run_index(StartTime) -> + IndexName = ?index_name, + AbsIndexName = ?abs(IndexName), + case catch make_last_run_index1(StartTime,IndexName) of + {'EXIT', Reason} -> + io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), + io:format("~p~n", [Reason]), + {error, Reason}; + {error, Reason} -> + io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), + io:format("~p~n", [Reason]), + {error, Reason}; + ok -> +% io:put_chars("done\n"), + ok; + Err -> + io:format("Unknown internal error while updating ~s. " + "Please report.\n(Err: ~p, ID: 1)", + [AbsIndexName,Err]), + {error, Err} + end. + +make_last_run_index1(StartTime,IndexName) -> + %% this manoeuvre is to ensure the tests get logged + %% in correct order of time (the 1 sec resolution + %% of the dirnames may be too big) + Logs1 = + case filelib:wildcard([$*|?logdir_ext]) of + [Log] -> % first test + [Log]; + Logs -> + case read_totals_file(?totals_name) of + {_Node,Logs0,_Totals} -> + insert_dirs(Logs,Logs0); + _ -> + %% someone deleted the totals file!? + Logs + end + end, + Missing = + case file:read_file(?missing_suites_info) of + {ok,Bin} -> binary_to_term(Bin); + _ -> [] + end, + {ok,Index0,Totals} = make_last_run_index(Logs1, index_header(StartTime), + 0, 0, 0, 0, 0, Missing), + %% write current Totals to file, later to be used in all_runs log + write_totals_file(?totals_name,Logs1,Totals), + Index = [Index0|index_footer()], + case force_write_file(IndexName, Index) of + ok -> + ok; + {error, Reason} -> + {error,{index_write_error, Reason}} + end. + +insert_dirs([NewDir|NewDirs],Dirs) -> + Dirs1 = insert_dir(NewDir,Dirs), + insert_dirs(NewDirs,Dirs1); +insert_dirs([],Dirs) -> + Dirs. +insert_dir(D,Dirs=[D|_]) -> + Dirs; +insert_dir(D,[D1|Ds]) -> + [D1|insert_dir(D,Ds)]; +insert_dir(D,[]) -> + [D]. + +make_last_run_index([Name|Rest], Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt, Missing) -> + case last_test(Name) of + false -> + %% Silently skip. + make_last_run_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt, Missing); + LastLogDir -> + SuiteName = filename:rootname(filename:basename(Name)), + case make_one_index_entry(SuiteName, LastLogDir, false, Missing) of + {Result1,Succ,Fail,USkip,ASkip,NotBuilt} -> + %% for backwards compatibility + AutoSkip1 = case catch AutoSkip+ASkip of + {'EXIT',_} -> undefined; + Res -> Res + end, + make_last_run_index(Rest, [Result|Result1], TotSucc+Succ, + TotFail+Fail, UserSkip+USkip, AutoSkip1, + TotNotBuilt+NotBuilt, Missing); + error -> + make_last_run_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt, Missing) + end + end; +make_last_run_index([], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, _) -> + {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, false)], + {TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}}. + +make_one_index_entry(SuiteName, LogDir, All, Missing) -> + case count_cases(LogDir) of + {Succ,Fail,UserSkip,AutoSkip} -> + NotBuilt = not_built(SuiteName, LogDir, All, Missing), + NewResult = make_one_index_entry1(SuiteName, LogDir, Succ, Fail, + UserSkip, AutoSkip, NotBuilt, All), + {NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt}; + error -> + error + end. + +make_one_index_entry1(SuiteName, Link, Success, Fail, UserSkip, AutoSkip, + NotBuilt, All) -> + LogFile = filename:join(Link, ?suitelog_name ++ ".html"), + CrashDumpName = SuiteName ++ "_erl_crash.dump", + CrashDumpLink = + case filelib:is_file(CrashDumpName) of + true -> + [" (CrashDump)"]; + false -> + "" + end, + {Timestamp,Node,AllInfo} = + case All of + {true,OldRuns} -> + [_Prefix,NodeOrDate|_] = string:tokens(Link,"."), + Node1 = case string:chr(NodeOrDate,$@) of + 0 -> "-"; + _ -> NodeOrDate + end, + N = ["",Node1,"\n"], + CtRunDir = filename:dirname(filename:dirname(Link)), + T = ["",timestamp(CtRunDir),"\n"], + CtLogFile = filename:join(CtRunDir,?ct_log_name), + OldRunsLink = + case OldRuns of + [] -> "none"; + _ -> "Old Runs" + end, + A=["CT Log\n", + "",OldRunsLink,"\n"], + {T,N,A}; + false -> + {"","",""} + end, + NotBuiltStr = + if NotBuilt == 0 -> + ["",integer_to_list(NotBuilt),"\n"]; + true -> + ["", + integer_to_list(NotBuilt),"\n"] + end, + FailStr = + if Fail > 0 -> + ["", + integer_to_list(Fail),""]; + true -> + integer_to_list(Fail) + end, + {AllSkip,UserSkipStr,AutoSkipStr} = + if AutoSkip == undefined -> {UserSkip,"?","?"}; + true -> + ASStr = if AutoSkip > 0 -> + ["", + integer_to_list(AutoSkip),""]; + true -> integer_to_list(AutoSkip) + end, + {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr} + end, + ["\n", + "",SuiteName,"",CrashDumpLink,"\n", + Timestamp, + "",integer_to_list(Success),"\n", + "",FailStr,"\n", + "",integer_to_list(AllSkip), + " (",UserSkipStr,"/",AutoSkipStr,")\n", + NotBuiltStr, + Node, + AllInfo, + "\n"]. +total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) -> + {TimestampCell,AllInfo} = + case All of + true -> + {" \n"," \n \n"}; + false -> + {"",""} + end, + + {AllSkip,UserSkipStr,AutoSkipStr} = + if AutoSkip == undefined -> {UserSkip,"?","?"}; + true -> {UserSkip+AutoSkip, + integer_to_list(UserSkip),integer_to_list(AutoSkip)} + end, + ["\n", + "Total", + TimestampCell, + "",integer_to_list(Success),"\n", + "",integer_to_list(Fail),"\n", + "",integer_to_list(AllSkip), + " (",UserSkipStr,"/",AutoSkipStr,")\n", + "",integer_to_list(NotBuilt),"\n", + AllInfo, + "\n"]. + +not_built(_BaseName,_LogDir,_All,[]) -> + 0; +not_built(BaseName,_LogDir,_All,Missing) -> + %% find out how many suites didn't compile + %% BaseName = + %% Top.ObjDir | Top.ObjDir.suites | Top.ObjDir.Suite | + %% Top.ObjDir.Suite.cases | Top.ObjDir.Suite.Case + Failed = + case string:tokens(BaseName,".") of + [T,O] when is_list(T) -> % all under Top.ObjDir + locate_info({T,O},all,Missing); + [T,O,"suites"] -> + locate_info({T,O},suites,Missing); + [T,O,S] -> + locate_info({T,O},list_to_atom(S),Missing); + [T,O,S,_] -> + locate_info({T,O},list_to_atom(S),Missing); + _ -> % old format - don't crash + [] + end, + length(Failed). + +locate_info(Path={Top,Obj},AllOrSuite,[{{Dir,Suite},Failed}|Errors]) -> + case lists:reverse(filename:split(Dir)) of + ["test",Obj,Top|_] -> + get_missing_suites(AllOrSuite,{Suite,Failed}) ++ + locate_info(Path,AllOrSuite,Errors); + [Obj,Top|_] -> + get_missing_suites(AllOrSuite,{Suite,Failed}) ++ + locate_info(Path,AllOrSuite,Errors); + _ -> + locate_info(Path,AllOrSuite,Errors) + end; +locate_info(_,_,[]) -> + []. + +get_missing_suites(all,{"all",Failed}) -> + Failed; +get_missing_suites(suites,{_Suite,Failed}) -> + Failed; +get_missing_suites(Suite,{Suite,Failed}) -> + Failed; +get_missing_suites(_,_) -> + []. + +term_to_text(Term) -> + lists:flatten(io_lib:format("~p.\n", [Term])). + + +%%% Headers and footers. + +index_header(StartTime) -> + [header("Test Results " ++ format_time(StartTime)) | + ["
\n", + "

Common Test Framework Log

", + "\n" + "\n", + "\n" + "\n", + "\n" + "\n" + "\n"]]. + +all_suites_index_header() -> + [header("Test Results") | + ["
\n", + "All Test Runs in this directory\n", + "

\n", + "
Name_Ok" + "_FailedSkipped
(User/Auto)
Missing
Suites
\n" + "\n", + "\n", + "\n" + "\n", + "\n" + "\n" + "\n", + "\n", + "\n", + "\n"]]. + +all_runs_header() -> + [header("All test runs in current directory") | + ["
NameTest Run Started_Ok" + "_FailedSkipped
(User/Auto)
Missing
Suites
NodeCT LogOld Runs
\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n"]]. + +header(Title) -> + ["\n" + "\n" + "\n", + "\n", + + "" ++ Title ++ "\n", + "\n", + + "\n", + + body_tag(), + + "\n", + + "
\n", + "

" ++ Title ++ "

\n", + "
\n", + + "\n"]. + +index_footer() -> + ["
HistoryNodeTestsNamesTotal_Ok" + "_FailedSkipped
(User/Auto)
Missing
Suites
\n" + "
\n" | footer()]. + +footer() -> + ["

\n" + "
\n" + "

\n" + "Copyright © ", year(), + " Open Telecom Platform
\n" + "Updated: ", current_time(), "
\n" + "
\n" + "

\n" + "\n"]. + + +body_tag() -> + "\n". + +current_time() -> + format_time(calendar:local_time()). + +format_time({{Y, Mon, D}, {H, Min, S}}) -> + Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)), + lists:flatten(io_lib:format("~s ~s ~p ~w ~2.2.0w:~2.2.0w:~2.2.0w", + [Weekday, month(Mon), D, Y, H, Min, S])). + +weekday(1) -> "Mon"; +weekday(2) -> "Tue"; +weekday(3) -> "Wed"; +weekday(4) -> "Thu"; +weekday(5) -> "Fri"; +weekday(6) -> "Sat"; +weekday(7) -> "Sun". + +month(1) -> "Jan"; +month(2) -> "Feb"; +month(3) -> "Mar"; +month(4) -> "Apr"; +month(5) -> "May"; +month(6) -> "Jun"; +month(7) -> "Jul"; +month(8) -> "Aug"; +month(9) -> "Sep"; +month(10) -> "Oct"; +month(11) -> "Nov"; +month(12) -> "Dec". + +year() -> + {Y, _, _} = date(), + integer_to_list(Y). + + +%% Count test cases in the given directory (a directory of the type +%% run.1997-08-04_09.58.52). + +count_cases(Dir) -> + SumFile = filename:join(Dir, ?run_summary), + case read_summary(SumFile, [summary]) of + {ok, [{Succ,Fail,Skip}]} -> + {Succ,Fail,Skip,undefined}; + {ok, [Summary]} -> + Summary; + {error, _} -> + LogFile = filename:join(Dir, ?suitelog_name), + case file:read_file(LogFile) of + {ok, Bin} -> + case count_cases1(binary_to_list(Bin), + {undefined,undefined,undefined,undefined}) of + {error,not_complete} -> + %% The test is not complete - dont write summary + %% file yet. + {0,0,0,0}; + Summary -> + write_summary(SumFile, Summary), + Summary + end; + {error, _Reason} -> + io:format("\nFailed to read ~p (skipped)\n", [LogFile]), + error + end + end. + +write_summary(Name, Summary) -> + File = [term_to_text({summary, Summary})], + force_write_file(Name, File). + +read_summary(Name, Keys) -> + case file:consult(Name) of + {ok, []} -> + {error, "Empty summary file"}; + {ok, Terms} -> + {ok, lists:map(fun(Key) -> {value, {_, Value}} = + lists:keysearch(Key, 1, Terms), + Value end, + Keys)}; + {error, Reason} -> + {error, Reason} + end. + +count_cases1("=failed" ++ Rest, {Success, _Fail, UserSkip,AutoSkip}) -> + {NextLine, Count} = get_number(Rest), + count_cases1(NextLine, {Success, Count, UserSkip,AutoSkip}); +count_cases1("=successful" ++ Rest, {_Success, Fail, UserSkip,AutoSkip}) -> + {NextLine, Count} = get_number(Rest), + count_cases1(NextLine, {Count, Fail, UserSkip,AutoSkip}); +count_cases1("=skipped" ++ Rest, {Success, Fail, _UserSkip,_AutoSkip}) -> + {NextLine, Count} = get_number(Rest), + count_cases1(NextLine, {Success, Fail, Count,undefined}); +count_cases1("=user_skipped" ++ Rest, {Success, Fail, _UserSkip,AutoSkip}) -> + {NextLine, Count} = get_number(Rest), + count_cases1(NextLine, {Success, Fail, Count,AutoSkip}); +count_cases1("=auto_skipped" ++ Rest, {Success, Fail, UserSkip,_AutoSkip}) -> + {NextLine, Count} = get_number(Rest), + count_cases1(NextLine, {Success, Fail, UserSkip,Count}); +count_cases1([], {Su,F,USk,_ASk}) when Su==undefined;F==undefined; + USk==undefined -> + {error,not_complete}; +count_cases1([], Counters) -> + Counters; +count_cases1(Other, Counters) -> + count_cases1(skip_to_nl(Other), Counters). + +get_number([$\s|Rest]) -> + get_number(Rest); +get_number([Digit|Rest]) when $0 =< Digit, Digit =< $9 -> + get_number(Rest, Digit-$0). + +get_number([Digit|Rest], Acc) when $0 =< Digit, Digit =< $9 -> + get_number(Rest, Acc*10+Digit-$0); +get_number([$\n|Rest], Acc) -> + {Rest, Acc}; +get_number([_|Rest], Acc) -> + get_number(Rest, Acc). + +skip_to_nl([$\n|Rest]) -> + Rest; +skip_to_nl([_|Rest]) -> + skip_to_nl(Rest); +skip_to_nl([]) -> + []. + + +config_table(Vars) -> + [config_table_header()|config_table1(Vars)]. + +config_table_header() -> + ["

Configuration

\n", + "\n"]. + +config_table1([{Key,Value}|Vars]) -> + ["\n", + "\n" | + config_table1(Vars)]; +config_table1([]) -> + ["
KeyValue
", atom_to_list(Key), "
",io_lib:format("~p",[Value]),"
\n"]. + + +make_all_runs_index(When) -> + AbsName = ?abs(?all_runs_name), + notify_and_lock_file(AbsName), + if When == start -> ok; + true -> io:put_chars("Updating " ++ AbsName ++ "... ") + end, + Dirs = filelib:wildcard(logdir_prefix()++"*.*"), + DirsSorted = (catch sort_all_runs(Dirs)), + Header = all_runs_header(), + BasicHtml = basic_html(), + Index = [runentry(Dir, BasicHtml) || Dir <- DirsSorted], + Result = file:write_file(AbsName,Header++Index++index_footer()), + if When == start -> ok; + true -> io:put_chars("done\n") + end, + notify_and_unlock_file(AbsName), + Result. + +sort_all_runs(Dirs) -> + %% sort on time string, always last and on the format: + %% "YYYY-MM-DD_HH.MM.SS" + KeyList = + lists:map(fun(Dir) -> + case lists:reverse(string:tokens(Dir,[$.,$_])) of + [SS,MM,HH,Date|_] -> + {{Date,HH,MM,SS},Dir}; + _Other -> + throw(Dirs) + end + end,Dirs), + lists:reverse(lists:map(fun({_,Dir}) -> + Dir + end,lists:keysort(1,KeyList))). + + +interactive_link() -> + [Dir|_] = lists:reverse(filelib:wildcard(logdir_prefix()++"*.*")), + CtLog = filename:join(Dir,"ctlog.html"), + Body = ["Log from last interactive run: ", + timestamp(Dir),""], + file:write_file("last_interactive.html",Body), + io:format("~n~nUpdated ~s\n" + "Any CT activities will be logged here\n", + [?abs("last_interactive.html")]). + +runentry(Dir, BasicHtml) -> + TotalsFile = filename:join(Dir,?totals_name), + TotalsStr = + case read_totals_file(TotalsFile) of + {Node,Logs,{TotSucc,TotFail,UserSkip,AutoSkip,NotBuilt}} -> + TotFailStr = + if TotFail > 0 -> + ["", + integer_to_list(TotFail),""]; + true -> + integer_to_list(TotFail) + end, + {AllSkip,UserSkipStr,AutoSkipStr} = + if AutoSkip == undefined -> {UserSkip,"?","?"}; + true -> + ASStr = if AutoSkip > 0 -> + ["", + integer_to_list(AutoSkip),""]; + true -> integer_to_list(AutoSkip) + end, + {UserSkip+AutoSkip,integer_to_list(UserSkip),ASStr} + end, + NoOfTests = case length(Logs) of + 0 -> "-"; + N -> integer_to_list(N) + end, + StripExt = + fun(File) -> + string:sub_string(File,1, + length(File)- + length(?logdir_ext)) ++ ", " + end, + Polish = fun(S) -> case lists:reverse(S) of + [32,$,|Rev] -> lists:reverse(Rev); + [$,|Rev] -> lists:reverse(Rev); + _ -> S + end + end, + TestNames = Polish(lists:flatten(lists:map(StripExt,Logs))), + TestNamesTrunc = + if TestNames=="" -> + ""; + length(TestNames) < ?testname_width -> + TestNames; + true -> + Trunc = Polish(string:substr(TestNames,1,?testname_width-3)), + lists:flatten(io_lib:format("~s...",[Trunc])) + end, + Total = TotSucc+TotFail+AllSkip, + A = ["",Node,"\n", + "",NoOfTests,"\n"], + B = if BasicHtml -> + ["",TestNamesTrunc,"\n"]; + true -> + [" ", + TestNamesTrunc,"\n"] + end, + C = ["",integer_to_list(Total),"\n", + "",integer_to_list(TotSucc),"\n", + "",TotFailStr,"\n", + "",integer_to_list(AllSkip), + " (",UserSkipStr,"/",AutoSkipStr,")\n", + "",integer_to_list(NotBuilt),"\n"], + A++B++C; + _ -> + ["", + "Test data missing or corrupt","\n"] + end, + Index = filename:join(Dir,?index_name), + ["\n" + "",timestamp(Dir),"",TotalsStr,"\n" + "\n"]. + +write_totals_file(Name,Logs,Totals) -> + AbsName = ?abs(Name), + notify_and_lock_file(AbsName), + force_write_file(AbsName, + term_to_binary({atom_to_list(node()), + Logs,Totals})), + notify_and_unlock_file(AbsName). + +read_totals_file(Name) -> + AbsName = ?abs(Name), + notify_and_lock_file(AbsName), + Result = + case file:read_file(AbsName) of + {ok,Bin} -> + case catch binary_to_term(Bin) of + {'EXIT',_Reason} -> % corrupt file + {"-",[],undefined}; + R = {Node,Ls,Tot} -> + case Tot of + {_,_,_,_,_} -> % latest format + R; + {TotSucc,TotFail,AllSkip,NotBuilt} -> + {Node,Ls,{TotSucc,TotFail,AllSkip,undefined,NotBuilt}} + end; + %% for backwards compatibility + {Ls,Tot} -> {"-",Ls,Tot}; + Tot -> {"-",[],Tot} + end; + Error -> + Error + end, + notify_and_unlock_file(AbsName), + Result. + +force_write_file(Name,Contents) -> + force_delete(Name), + file:write_file(Name,Contents). + +force_delete(Name) -> + case file:delete(Name) of + {error,eacces} -> + force_rename(Name,Name++".old.",0); + Other -> + Other + end. + +force_rename(From,To,Number) -> + Dest = [To|integer_to_list(Number)], + case file:read_file_info(Dest) of + {ok,_} -> + force_rename(From,To,Number+1); + {error,_} -> + file:rename(From,Dest) + end. + + +timestamp(Dir) -> + TsR = lists:reverse(string:tokens(Dir,".-_")), + [S,Min,H,D,M,Y] = [list_to_integer(N) || N <- lists:sublist(TsR,6)], + format_time({{Y,M,D},{H,Min,S}}). + +make_all_suites_index(When) -> + AbsIndexName = ?abs(?index_name), + notify_and_lock_file(AbsIndexName), + LogDirs = filelib:wildcard(logdir_prefix()++".*/*"++?logdir_ext), + Sorted = sort_logdirs(LogDirs,[]), + Result = make_all_suites_index1(When,Sorted), + notify_and_unlock_file(AbsIndexName), + Result. + +sort_logdirs([Dir|Dirs],Groups) -> + TestName = filename:rootname(filename:basename(Dir)), + case filelib:wildcard(filename:join(Dir,"run.*")) of + [RunDir] -> + Groups1 = insert_test(TestName,{filename:basename(RunDir),RunDir},Groups), + sort_logdirs(Dirs,Groups1); + _ -> % ignore missing run directory + sort_logdirs(Dirs,Groups) + end; +sort_logdirs([],Groups) -> + lists:keysort(1,sort_each_group(Groups)). + +insert_test(Test,IxDir,[{Test,IxDirs}|Groups]) -> + [{Test,[IxDir|IxDirs]}|Groups]; +insert_test(Test,IxDir,[]) -> + [{Test,[IxDir]}]; +insert_test(Test,IxDir,[TestDir|Groups]) -> + [TestDir|insert_test(Test,IxDir,Groups)]. + +sort_each_group([{Test,IxDirs}|Groups]) -> + Sorted = lists:reverse([Dir || {_,Dir} <- lists:keysort(1,IxDirs)]), + [{Test,Sorted}| sort_each_group(Groups)]; +sort_each_group([]) -> + []. + +make_all_suites_index1(When,AllSuitesLogDirs) -> + IndexName = ?index_name, + AbsIndexName = ?abs(IndexName), + if When == start -> ok; + true -> io:put_chars("Updating " ++ AbsIndexName ++ "... ") + end, + case catch make_all_suites_index2(IndexName,AllSuitesLogDirs) of + {'EXIT', Reason} -> + io:put_chars("CRASHED while updating " ++ AbsIndexName ++ "!\n"), + io:format("~p~n", [Reason]), + {error, Reason}; + {error, Reason} -> + io:put_chars("FAILED while updating " ++ AbsIndexName ++ "\n"), + io:format("~p~n", [Reason]), + {error, Reason}; + ok -> + if When == start -> ok; + true -> io:put_chars("done\n") + end, + ok; + Err -> + io:format("Unknown internal error while updating ~s. " + "Please report.\n(Err: ~p, ID: 1)", + [AbsIndexName,Err]), + {error, Err} + end. + +make_all_suites_index2(IndexName,AllSuitesLogDirs) -> + {ok,Index0,_Totals} = make_all_suites_index3(AllSuitesLogDirs, + all_suites_index_header(), + 0, 0, 0, 0, 0), + Index = [Index0|index_footer()], + case force_write_file(IndexName, Index) of + ok -> + ok; + {error, Reason} -> + {error,{index_write_error, Reason}} + end. + +make_all_suites_index3([{SuiteName,[LastLogDir|OldDirs]}|Rest], + Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt) -> + [EntryDir|_] = filename:split(LastLogDir), + Missing = + case file:read_file(filename:join(EntryDir,?missing_suites_info)) of + {ok,Bin} -> binary_to_term(Bin); + _ -> [] + end, + case make_one_index_entry(SuiteName, LastLogDir, {true,OldDirs}, Missing) of + {Result1,Succ,Fail,USkip,ASkip,NotBuilt} -> + %% for backwards compatibility + AutoSkip1 = case catch AutoSkip+ASkip of + {'EXIT',_} -> undefined; + Res -> Res + end, + make_all_suites_index3(Rest, [Result|Result1], TotSucc+Succ, + TotFail+Fail, UserSkip+USkip, AutoSkip1, + TotNotBuilt+NotBuilt); + error -> + make_all_suites_index3(Rest, Result, TotSucc, TotFail, + UserSkip, AutoSkip, TotNotBuilt) + end; +make_all_suites_index3([], Result, TotSucc, TotFail, UserSkip, AutoSkip, + TotNotBuilt) -> + {ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt,true)], + {TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}}. + + +%%----------------------------------------------------------------- +%% Remove log files. +%% Cwd should always be set to the root logdir when finished. +cleanup() -> + {ok,Cwd} = file:get_cwd(), + ok = file:set_cwd("../"), + {ok,Top} = file:get_cwd(), + Result = + case catch try_cleanup(Cwd) of + ok -> + ok; + {'EXIT',Reason} -> + {error,Reason}; + Error -> + {error,Error} + end, + ok = file:set_cwd(Top), + Result. + +try_cleanup(CTRunDir) -> + %% ensure we're removing the ct_run directory + case lists:reverse(filename:split(CTRunDir)) of + [[$c,$t,$_,$r,$u,$n,$.|_]|_] -> + case filelib:wildcard(filename:join(CTRunDir,"ct_run.*")) of + [] -> % "double check" + rm_dir(CTRunDir); + _ -> + unknown_logdir + end; + _ -> + unknown_logdir + end. + +rm_dir(Dir) -> + case file:list_dir(Dir) of + {error,Errno} -> + exit({ls_failed,Dir,Errno}); + {ok,Files} -> + rm_files([filename:join(Dir, F) || F <- Files]), + case file:del_dir(Dir) of + {error,Errno} -> + exit({rmdir_failed,Errno}); + ok -> + ok + end + end. + +rm_files([F | Fs]) -> + Base = filename:basename(F), + if Base == "." ; Base == ".." -> + rm_files(Fs); + true -> + case file:read_file_info(F) of + {ok,#file_info{type=directory}} -> + rm_dir(F), + rm_files(Fs); + {ok,_Regular} -> + case file:delete(F) of + ok -> + rm_files(Fs); + {error,Errno} -> + exit({del_failed,F,Errno}) + end + end + end; +rm_files([]) -> + ok. + +%%%----------------------------------------------------------------- +%%% @spec simulate() -> pid() +%%% +%%% @doc Simulate the logger process. +%%% +%%%

Simulate the logger process - for use when testing code using +%%% ct_logs logging mechanism without using the ct +%%% environment. (E.g. when testing code with ts)

+simulate() -> + cast(stop), + S = self(), + Pid = spawn(fun() -> + register(?MODULE,self()), + S ! {self(),started}, + simulate_logger_loop() + end), + receive {Pid,started} -> Pid end. + + +simulate_logger_loop() -> + receive + {log,_,_,List} -> + S = [[io_lib:format(Str,Args),io_lib:nl()] || {Str,Args} <- List], + io:format("~s",[S]), + simulate_logger_loop(); + stop -> + ok + end. + +%%%----------------------------------------------------------------- +%%% @spec notify_and_lock_file(Files) -> ok +%%% +%%% @doc +%%% +notify_and_lock_file(File) -> + case ct_event:is_alive() of + true -> + ct_event:sync_notify(#event{name=start_write_file, + node=node(), + data=File}); + false -> + ok + end. + +%%%----------------------------------------------------------------- +%%% @spec notify_and_unlock_file(Files) -> ok +%%% +%%% @doc +%%% +notify_and_unlock_file(File) -> + case ct_event:is_alive() of + true -> + ct_event:sync_notify(#event{name=finished_write_file, + node=node(), + data=File}); + false -> + ok + end. + +%%%----------------------------------------------------------------- +%%% @spec last_test(Dir) -> string() | false +%%% +%%% @doc +%%% +last_test(Dir) -> + last_test(filelib:wildcard(filename:join(Dir, "run.[1-2]*")), false). + +last_test([Run|Rest], false) -> + last_test(Rest, Run); +last_test([Run|Rest], Latest) when Run > Latest -> + last_test(Rest, Run); +last_test([_|Rest], Latest) -> + last_test(Rest, Latest); +last_test([], Latest) -> + Latest. + +%%%----------------------------------------------------------------- +%%% @spec basic_html() -> true | false +%%% +%%% @doc +%%% +basic_html() -> + case application:get_env(common_test, basic_html) of + {ok,true} -> + true; + _ -> + false + end. -- cgit v1.2.3