%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2003-2011. 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/2,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,
basic_html/0]).
%% 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, 60).
-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, [flush]),
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(Info, StartDir) -> ok
%%%
%%% @doc Create index pages with test results and close the CT Log
%%% (tool-internal use only).
close(Info, StartDir) ->
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 Info == clean ->
case cleanup() of
ok ->
ok;
Error ->
io:format("Warning! Cleanup failed: ~p~n", [Error])
end,
make_all_suites_index(stop),
make_all_runs_index(stop);
true ->
file:set_cwd(".."),
make_all_suites_index(stop),
make_all_runs_index(stop),
case ct_util:get_profile_data(browser, StartDir) of
undefined ->
ok;
BrowserData ->
case {proplists:get_value(prog, BrowserData),
proplists:get_value(args, BrowserData),
proplists:get_value(page, BrowserData)} of
{Prog,Args,Page} when is_list(Args),
is_list(Page) ->
URL = "\"file://" ++ ?abs(Page) ++ "\"",
ct_util:open_url(Prog, Args, URL);
_ ->
ok
end
end
end,
ok.
%%%-----------------------------------------------------------------
%%% @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, [flush]),
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({MS,S,US}) ->
put(log_timestamp, {MS,S,US}),
{{Year,Month,Day}, {Hour,Min,Sec}} =
calendar:now_to_local_time({MS,S,US}),
MilliSec = trunc(US/1000),
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B "
"~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0B",
[Year,Month,Day,Hour,Min,Sec,MilliSec])).
%%%-----------------------------------------------------------------
%%% 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_runs_index(start),
make_all_suites_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 loading 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,_Lbl,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,
Label = case application:get_env(common_test, test_label) of
{ok,Lbl} -> Lbl;
_ -> undefined
end,
{ok,Index0,Totals} = make_last_run_index(Logs1,
index_header(Label,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,Label,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, Label, 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, Label, Succ, Fail,
UserSkip, AutoSkip, NotBuilt, All,
normal),
{NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt};
error ->
error
end.
make_one_index_entry1(SuiteName, Link, Label, Success, Fail, UserSkip, AutoSkip,
NotBuilt, All, Mode) ->
LogFile = filename:join(Link, ?suitelog_name ++ ".html"),
CrashDumpLink = case Mode of
cached ->
"";
normal ->
CrashDumpName = SuiteName ++ "_erl_crash.dump",
case filelib:is_file(CrashDumpName) of
true ->
[" (CrashDump)"];
false ->
""
end
end,
{Lbl,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)),
L = ["
",Label,"
\n"],
T = ["
",timestamp(CtRunDir),"
\n"],
CtLogFile = filename:join(CtRunDir,?ct_log_name),
OldRunsLink =
case OldRuns of
[] -> "none";
_ -> "Old Runs"
end,
A=["
\n"].
write_totals_file(Name,Label,Logs,Totals) ->
AbsName = ?abs(Name),
notify_and_lock_file(AbsName),
force_write_file(AbsName,
term_to_binary({atom_to_list(node()),
Label,Logs,Totals})),
notify_and_unlock_file(AbsName).
%% this function needs to convert from old formats to new so that old
%% test results (prev ct versions) can be listed together with new
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};
{Node,Label,Ls,Tot} -> % all info available
Label1 = case Label of
undefined -> "-";
_ -> Label
end,
case Tot of
{_Ok,_Fail,_USkip,_ASkip,_NoBuild} -> % latest format
{Node,Label1,Ls,Tot};
{TotSucc,TotFail,AllSkip,NotBuilt} ->
{Node,Label1,Ls,{TotSucc,TotFail,AllSkip,undefined,NotBuilt}}
end;
{Node,Ls,Tot} -> % no label found
case Tot of
{_Ok,_Fail,_USkip,_ASkip,_NoBuild} -> % latest format
{Node,"-",Ls,Tot};
{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}}).
%% ----------------------------- NOTE --------------------------------------
%% The top level index file is generated based on the file contents under
%% logdir. This takes place initially when the test run starts (When = start)
%% and an update takes place at the end of the test run, or when the user
%% requests an explicit refresh (When = refresh).
%% The index file needs to be updated also at the start of each individual
%% test (in order for the user to be able to track test progress by refreshing
%% the browser). Since it would be too expensive to generate a new file from
%% scratch every time (by reading the data from disk), a copy of the dir tree
%% is cached as a result of the first index file creation. This copy is then
%% used for all top level index page updates that occur during the test run.
%% This means that any changes to the dir tree under logdir during the test
%% run will not show until after the final refresh.
%% -------------------------------------------------------------------------
%% Creates the top level index file. When == start | refresh.
%% A copy of the dir tree under logdir is cached as a result.
make_all_suites_index(When) when is_atom(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, AbsIndexName, Sorted),
notify_and_unlock_file(AbsIndexName),
Result;
%% This updates the top level index file using cached data from
%% the initial index file creation.
make_all_suites_index(NewTestData = {_TestName,DirName}) ->
%% AllLogDirs = [{TestName,Label,Missing,{LastLogDir,Summary},OldDirs}|...]
{AbsIndexName,LogDirData} = ct_util:get_testdata(test_index),
CtRunDirPos = length(filename:split(AbsIndexName)),
CtRunDir = filename:join(lists:sublist(filename:split(DirName),
CtRunDirPos)),
Label = case read_totals_file(filename:join(CtRunDir, ?totals_name)) of
{_,"-",_,_} -> "...";
{_,Lbl,_,_} -> Lbl;
_ -> "..."
end,
notify_and_lock_file(AbsIndexName),
Result =
case catch make_all_suites_ix_cached(AbsIndexName,
NewTestData,
Label,
LogDirData) 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 ->
ok;
Err ->
io:format("Unknown internal error while updating ~s. "
"Please report.\n(Err: ~p, ID: 1)",
[AbsIndexName,Err]),
{error, Err}
end,
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, AbsIndexName, AllLogDirs) ->
IndexName = ?index_name,
if When == start -> ok;
true -> io:put_chars("Updating " ++ AbsIndexName ++ "... ")
end,
case catch make_all_suites_index2(IndexName, AllLogDirs) 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,CacheData} ->
case When of
start ->
ct_util:set_testdata_async({test_index,{AbsIndexName,
CacheData}}),
ok;
_ ->
io:put_chars("done\n"),
ok
end;
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, AllTestLogDirs) ->
{ok,Index0,_Totals,CacheData} =
make_all_suites_index3(AllTestLogDirs,
all_suites_index_header(),
0, 0, 0, 0, 0, [], []),
Index = [Index0|index_footer()],
case force_write_file(IndexName, Index) of
ok ->
{ok,CacheData};
{error, Reason} ->
{error,{index_write_error, Reason}}
end.
make_all_suites_index3([{TestName,[LastLogDir|OldDirs]}|Rest],
Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt,
Labels, CacheData) ->
[EntryDir|_] = filename:split(LastLogDir),
Missing =
case file:read_file(filename:join(EntryDir, ?missing_suites_info)) of
{ok,Bin} -> binary_to_term(Bin);
_ -> []
end,
{Label,Labels1} =
case proplists:get_value(EntryDir, Labels) of
undefined ->
case read_totals_file(filename:join(EntryDir, ?totals_name)) of
{_,Lbl,_,_} -> {Lbl,[{EntryDir,Lbl}|Labels]};
_ -> {"-",[{EntryDir,"-"}|Labels]}
end;
Lbl ->
{Lbl,Labels}
end,
case make_one_index_entry(TestName, LastLogDir, Label, {true,OldDirs}, Missing) of
{Result1,Succ,Fail,USkip,ASkip,NotBuilt} ->
%% for backwards compatibility
AutoSkip1 = case catch AutoSkip+ASkip of
{'EXIT',_} -> undefined;
Res -> Res
end,
IxEntry = {TestName,Label,Missing,
{LastLogDir,{Succ,Fail,USkip,ASkip}},OldDirs},
make_all_suites_index3(Rest, [Result|Result1], TotSucc+Succ,
TotFail+Fail, UserSkip+USkip, AutoSkip1,
TotNotBuilt+NotBuilt, Labels1,
[IxEntry|CacheData]);
error ->
IxEntry = {TestName,Label,Missing,{LastLogDir,error},OldDirs},
make_all_suites_index3(Rest, Result, TotSucc, TotFail,
UserSkip, AutoSkip, TotNotBuilt, Labels1,
[IxEntry|CacheData])
end;
make_all_suites_index3([], Result, TotSucc, TotFail, UserSkip, AutoSkip,
TotNotBuilt, _, CacheData) ->
{ok, [Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt,true)],
{TotSucc,TotFail,UserSkip,AutoSkip,TotNotBuilt}, lists:reverse(CacheData)}.
make_all_suites_ix_cached(AbsIndexName, NewTestData, Label, AllTestLogDirs) ->
AllTestLogDirs1 = insert_new_test_data(NewTestData, Label, AllTestLogDirs),
IndexDir = filename:dirname(AbsIndexName),
Index0 = make_all_suites_ix_cached1(AllTestLogDirs1,
all_suites_index_header(IndexDir),
0, 0, 0, 0, 0),
Index = [Index0|index_footer()],
case force_write_file(AbsIndexName, Index) of
ok ->
ok;
{error, Reason} ->
{error,{index_write_error, Reason}}
end.
insert_new_test_data({NewTestName,NewTestDir}, NewLabel, AllTestLogDirs) ->
AllTestLogDirs1 =
case lists:keysearch(NewTestName, 1, AllTestLogDirs) of
{value,{_,_,_,{LastLogDir,_},OldDirs}} ->
[{NewTestName,NewLabel,[],{NewTestDir,{0,0,0,0}},
[LastLogDir|OldDirs]} |
lists:keydelete(NewTestName, 1, AllTestLogDirs)];
false ->
[{NewTestName,NewLabel,[],{NewTestDir,{0,0,0,0}},[]} |
AllTestLogDirs]
end,
lists:keysort(1, AllTestLogDirs1).
make_all_suites_ix_cached1([{TestName,Label,Missing,LastLogDirData,OldDirs}|Rest],
Result, TotSucc, TotFail, UserSkip, AutoSkip,
TotNotBuilt) ->
case make_one_ix_entry_cached(TestName, LastLogDirData,
Label, {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_ix_cached1(Rest, [Result|Result1], TotSucc+Succ,
TotFail+Fail, UserSkip+USkip, AutoSkip1,
TotNotBuilt+NotBuilt);
error ->
make_all_suites_ix_cached1(Rest, Result, TotSucc, TotFail,
UserSkip, AutoSkip, TotNotBuilt)
end;
make_all_suites_ix_cached1([], Result, TotSucc, TotFail, UserSkip, AutoSkip,
TotNotBuilt) ->
[Result|total_row(TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, true)].
make_one_ix_entry_cached(TestName, {LogDir,Summary}, Label, All, Missing) ->
case Summary of
{Succ,Fail,UserSkip,AutoSkip} ->
NotBuilt = not_built(TestName, LogDir, All, Missing),
NewResult = make_one_index_entry1(TestName, LogDir, Label,
Succ, Fail, UserSkip, AutoSkip,
NotBuilt, All, cached),
{NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt};
error ->
error
end.
%%-----------------------------------------------------------------
%% 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.