%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2003-2010. 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, 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(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, [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.

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({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_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 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), {NewResult,Succ,Fail,UserSkip,AutoSkip,NotBuilt}; error -> error end. make_one_index_entry1(SuiteName, Link, Label, 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, {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=["CT Log\n", "",OldRunsLink,"\n"], {L,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", Lbl, 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) -> {Label,TimestampCell,AllInfo} = case All of true -> {" \n", " \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", Label, 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(Label, StartTime) -> Head = case Label of undefined -> header("Test Results", format_time(StartTime)); _ -> header("Test Results for \"" ++ Label ++ "\"", format_time(StartTime)) end, [Head | ["
\n", "

Common Test Framework Log

", "\n" "\n", "\n" "\n", "\n" "\n" "\n"]]. all_suites_index_header() -> {ok,Cwd} = file:get_cwd(), LogDir = filename:basename(Cwd), AllRuns = "All test runs in \"" ++ LogDir ++ "\"", [header("Test Results") | ["
\n", "",AllRuns,"\n", "

\n", "
Test Name_Ok" "_FailedSkipped
(User/Auto)
Missing
Suites
\n" "\n", "\n", "\n", "\n" "\n", "\n" "\n" "\n", "\n", "\n", "\n"]]. all_runs_header() -> {ok,Cwd} = file:get_cwd(), LogDir = filename:basename(Cwd), Title = "All test runs in \"" ++ LogDir ++ "\"", [header(Title) | ["
Test NameLabelTest Run Started_Ok" "_FailedSkipped
(User/Auto)
Missing
Suites
NodeCT LogOld Runs
\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n"]]. header(Title) -> header1(Title, ""). header(Title, SubTitle) -> header1(Title, SubTitle). header1(Title, SubTitle) -> SubTitleHTML = if SubTitle =/= "" -> ["
\n", "

" ++ SubTitle ++ "

\n", "
\n
\n"]; true -> "
\n" end, ["\n" "\n" "\n", "\n", "" ++ Title ++ " " ++ SubTitle ++ "\n", "\n", "\n", body_tag(), "\n", "
\n", "

" ++ Title ++ "

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

\n" "

\n" "
\n" "

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

\n" "\n"]. body_tag() -> case basic_html() of true -> "\n"; false -> CTPath = code:lib_dir(common_test), TileFile = filename:join(filename:join(CTPath,"priv"),"tile1.jpg"), "\n" end. 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,Label,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", "",Label,"\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,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}}). 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, Labels) -> [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(SuiteName, 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, make_all_suites_index3(Rest, [Result|Result1], TotSucc+Succ, TotFail+Fail, UserSkip+USkip, AutoSkip1, TotNotBuilt+NotBuilt,Labels1); error -> make_all_suites_index3(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt,Labels1) 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.