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
+%%%
+%%%
Internal logging of activities in Common Test Framework
+%%%
Compilation of test results into index pages on several levels
+%%%
+%%%
+
+-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.
".
+
+
+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 = ["
\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)