1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2008-2020. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%% Test support functions
22%%%
23%%% This is a support module for testing the Common Test Framework.
24%%%
25-module(ct_test_support).
26
27-include_lib("common_test/include/ct_event.hrl").
28-include_lib("common_test/include/ct.hrl").
29
30-export([init_per_suite/1, init_per_suite/2, end_per_suite/1,
31	 init_per_testcase/2, end_per_testcase/2,
32	 write_testspec/2, write_testspec/3,
33	 run/2, run/3, run/4, run_ct_run_test/2, run_ct_script_start/2,
34	 get_opts/1, wait_for_ct_stop/1]).
35
36-export([handle_event/2, start_event_receiver/1, get_events/2,
37	 verify_events/3, verify_events/4, reformat/2, log_events/4,
38	 join_abs_dirs/2]).
39
40-export([start_slave/3, slave_stop/1]).
41
42-export([ct_test_halt/1, ct_rpc/2]).
43
44-export([random_error/1]).
45
46-export([unique_timestamp/0]).
47
48-export([rm_dir/1]).
49
50-include_lib("kernel/include/file.hrl").
51
52%%%-----------------------------------------------------------------
53%%% init_per_suite/1
54
55init_per_suite(Config) ->
56    init_per_suite(Config, 50).
57
58init_per_suite(Config, Level) ->
59    ScaleFactor = test_server:timetrap_scale_factor(),
60    case os:type() of
61	{win32, _} ->
62	    %% Extend timeout to 1 hour for windows as starting node
63	    %% can take a long time there
64	    test_server:timetrap( 60*60*1000 * ScaleFactor );
65	_ ->
66	    ok
67    end,
68    case delete_old_logs(os:type(), Config) of
69	{'EXIT',DelLogsReason} ->
70	    test_server:format(0, "Failed to delete old log directories: ~tp~n",
71			       [DelLogsReason]);
72	_ ->
73	    ok
74    end,
75
76    {Mult,Scale} = test_server_ctrl:get_timetrap_parameters(),
77    test_server:format(Level, "Timetrap multiplier: ~w~n", [Mult]),
78    if Scale == true ->
79	    test_server:format(Level, "Timetrap scale factor: ~w~n",
80			       [ScaleFactor]);
81       true ->
82	    ok
83    end,
84
85    start_slave(Config, Level).
86
87start_slave(Config, Level) ->
88    start_slave(ct, Config, Level).
89
90start_slave(NodeName, Config, Level) ->
91    [_,Host] = string:lexemes(atom_to_list(node()), "@"),
92    test_server:format(0, "Trying to start ~s~n",
93		       [atom_to_list(NodeName)++"@"++Host]),
94    PR = proplists:get_value(printable_range,Config,io:printable_range()),
95    case slave:start(Host, NodeName, "+pc " ++ atom_to_list(PR)) of
96	{error,Reason} ->
97	    ct:fail(Reason);
98	{ok,CTNode} ->
99	    test_server:format(0, "Node ~p started~n", [CTNode]),
100	    IsCover = test_server:is_cover(),
101	    if IsCover ->
102		    cover:start(CTNode);
103	       true ->
104		    ok
105	    end,
106
107	    DataDir = proplists:get_value(data_dir, Config),
108	    PrivDir = proplists:get_value(priv_dir, Config),
109
110	    %% PrivDir as well as directory of Test Server suites
111	    %% have to be in code path on Common Test node.
112	    [_ | Parts] = lists:reverse(filename:split(DataDir)),
113	    TSDir = filename:join(lists:reverse(Parts)),
114	    AddPathDirs = case proplists:get_value(path_dirs, Config) of
115			      undefined -> [];
116			      Ds -> Ds
117			  end,
118	    TestSupDir = filename:dirname(code:which(?MODULE)),
119	    PathDirs = [PrivDir,TSDir,TestSupDir | AddPathDirs],
120	    [true = rpc:call(CTNode, code, add_patha, [D]) || D <- PathDirs],
121	    test_server:format(Level, "Dirs added to code path (on ~w):~n",
122			       [CTNode]),
123	    [io:format("~ts~n", [D]) || D <- PathDirs],
124
125	    case proplists:get_value(start_sasl, Config) of
126		true ->
127		    rpc:call(CTNode, application, start, [sasl]),
128		    test_server:format(Level, "SASL started on ~w~n", [CTNode]);
129		_ ->
130		    ok
131	    end,
132	    TraceFile = filename:join(DataDir, "ct.trace"),
133	    case file:read_file_info(TraceFile) of
134		{ok,_} ->
135		    [{trace_level,0},
136		     {ct_opts,[{ct_trace,TraceFile}]},
137		     {ct_node,CTNode} | Config];
138		_ ->
139		    [{trace_level,Level},
140		     {ct_opts,[]},
141		     {ct_node,CTNode} | Config]
142	    end
143    end.
144
145%%%-----------------------------------------------------------------
146%%% end_per_suite/1
147
148end_per_suite(Config) ->
149    CTNode = proplists:get_value(ct_node, Config),
150    PrivDir = proplists:get_value(priv_dir, Config),
151    true = rpc:call(CTNode, code, del_path, [filename:join(PrivDir,"")]),
152    slave_stop(CTNode),
153    ok.
154
155%%%-----------------------------------------------------------------
156%%% init_per_testcase/2
157
158init_per_testcase(_TestCase, Config) ->
159    Opts = get_opts(Config),
160    NetDir = proplists:get_value(net_dir, Opts),
161    LogDir = join_abs_dirs(NetDir, proplists:get_value(logdir, Opts)),
162    case lists:keysearch(master, 1, Config) of
163	false->
164	    test_server:format("See Common Test logs here:\n\n"
165			       "<a href=\"file://~ts/all_runs.html\">~ts/all_runs.html</a>\n"
166			       "<a href=\"file://~ts/index.html\">~ts/index.html</a>",
167			       [LogDir,LogDir,LogDir,LogDir]);
168	{value, _}->
169	    test_server:format("See CT Master Test logs here:\n\n"
170		       "<a href=\"file://~ts/master_runs.html\">~ts/master_runs.html</a>",
171		       [LogDir,LogDir])
172    end,
173    Config.
174
175%%%-----------------------------------------------------------------
176%%% end_per_testcase/2
177
178end_per_testcase(_TestCase, Config) ->
179    CTNode = proplists:get_value(ct_node, Config),
180    case wait_for_ct_stop(CTNode) of
181	%% Common test was not stopped to we restart node.
182	false ->
183	    slave_stop(CTNode),
184	    start_slave(Config,proplists:get_value(trace_level,Config)),
185	    {fail, "Could not stop common_test"};
186	true ->
187	    ok
188    end.
189
190%%%-----------------------------------------------------------------
191%%%
192write_testspec(TestSpec, Dir, Name) ->
193    write_testspec(TestSpec, filename:join(Dir, Name)).
194
195write_testspec(TestSpec, TSFile) ->
196    {ok,Dev} = file:open(TSFile, [write,{encoding,utf8}]),
197    [io:format(Dev, "~tp.~n", [Entry]) || Entry <- TestSpec],
198    file:close(Dev),
199    io:format("Test specification written to: ~tp~n", [TSFile]),
200    io:format(user, "Test specification written to: ~tp~n", [TSFile]),
201    TSFile.
202
203
204%%%-----------------------------------------------------------------
205%%%
206
207get_opts(Config) ->
208    PrivDir = proplists:get_value(priv_dir, Config),
209    TempDir = case os:getenv("TMP") of
210		  false ->
211		      case os:getenv("TEMP") of
212			  false ->
213			      undefined;
214			  Tmp ->
215			      create_tmp_logdir(Tmp)
216		      end;
217		  Tmp ->
218		      create_tmp_logdir(Tmp)
219	      end,
220    LogDir =
221	case os:getenv("CT_USE_TMP_DIR") of
222	    false -> PrivDir;
223            _ -> TempDir
224	end,
225
226    %% Copy test variables to app environment on new node
227    CtTestVars =
228	case init:get_argument(ct_test_vars) of
229	    {ok,[Vars]} ->
230		[begin {ok,Ts,_} = erl_scan:string(Str++"."),
231		       {ok,Expr} = erl_parse:parse_term(Ts),
232		       Expr
233		 end || Str <- Vars];
234	    _ ->
235		[]
236	end,
237    %% test_server:format("Test variables added to Config: ~p\n\n",
238    %%                    [CtTestVars]),
239    InitOpts =
240	case proplists:get_value(ct_opts, Config) of
241	    undefined -> [];
242	    CtOpts -> CtOpts
243	end,
244    [{logdir,LogDir} | InitOpts ++ CtTestVars].
245
246
247%%%-----------------------------------------------------------------
248%%%
249run(Opts0, Config) when is_list(Opts0) ->
250    Opts =
251	%% read (and override) opts from env variable, the form expected:
252	%% "[{some_key1,SomeVal2}, {some_key2,SomeVal2}]"
253	case os:getenv("CT_TEST_OPTS") of
254	    false -> Opts0;
255	    ""    -> Opts0;
256	    Terms ->
257		case erl_scan:string(Terms++".", 0) of
258		    {ok,Tokens,_} ->
259			case erl_parse:parse_term(Tokens) of
260			    {ok,OROpts} ->
261				Override =
262				    fun(O={Key,_}, Os) ->
263					    io:format(user, "ADDING START "
264						      "OPTION: ~tp~n", [O]),
265					    [O | lists:keydelete(Key, 1, Os)]
266				    end,
267				lists:foldl(Override, Opts0, OROpts);
268			    _ ->
269				Opts0
270			end;
271		    _ ->
272			Opts0
273		end
274	end,
275
276    %% use ct interface
277    CtRunTestResult=run_ct_run_test(Opts,Config),
278    %% use run_test interface (simulated)
279    ExitStatus=run_ct_script_start(Opts,Config),
280
281    check_result(CtRunTestResult,ExitStatus,Opts).
282
283run_ct_run_test(Opts,Config) ->
284    CTNode = proplists:get_value(ct_node, Config),
285    Level = proplists:get_value(trace_level, Config),
286    test_server:format(Level, "~n[RUN #1] Calling ct:run_test(~tp) on ~p~n",
287		       [Opts, CTNode]),
288
289    T0 = erlang:monotonic_time(),
290    CtRunTestResult = rpc:call(CTNode, ct, run_test, [Opts]),
291    T1 = erlang:monotonic_time(),
292    Elapsed = erlang:convert_time_unit(T1-T0, native, milli_seconds),
293    test_server:format(Level, "~n[RUN #1] Got return value ~tp after ~p ms~n",
294		       [CtRunTestResult,Elapsed]),
295    case rpc:call(CTNode, erlang, whereis, [ct_util_server]) of
296	undefined ->
297	    ok;
298	_ ->
299	    test_server:format(Level,
300			       "ct_util_server not stopped on ~p yet, waiting 5 s...~n",
301			       [CTNode]),
302	    timer:sleep(5000),
303	    undefined = rpc:call(CTNode, erlang, whereis, [ct_util_server])
304    end,
305    CtRunTestResult.
306
307run_ct_script_start(Opts, Config) ->
308    CTNode = proplists:get_value(ct_node, Config),
309    Level = proplists:get_value(trace_level, Config),
310    Opts1 = [{halt_with,{?MODULE,ct_test_halt}} | Opts],
311    test_server:format(Level, "Saving start opts on ~p: ~tp~n",
312		       [CTNode, Opts1]),
313    rpc:call(CTNode, application, set_env,
314	     [common_test, run_test_start_opts, Opts1]),
315    test_server:format(Level, "[RUN #2] Calling ct_run:script_start() on ~p~n",
316		       [CTNode]),
317    T0 = erlang:monotonic_time(),
318    ExitStatus = rpc:call(CTNode, ct_run, script_start, []),
319    T1 = erlang:monotonic_time(),
320    Elapsed = erlang:convert_time_unit(T1-T0, native, milli_seconds),
321    test_server:format(Level, "[RUN #2] Got exit status value ~tp after ~p ms~n",
322		       [ExitStatus,Elapsed]),
323    ExitStatus.
324
325check_result({_Ok,Failed,{_UserSkipped,_AutoSkipped}},1,_Opts)
326  when Failed > 0 ->
327    ok;
328check_result({_Ok,0,{_UserSkipped,AutoSkipped}},ExitStatus,Opts)
329  when AutoSkipped > 0 ->
330    case proplists:get_value(exit_status, Opts) of
331	ignore_config when ExitStatus == 1 ->
332	    {error,{wrong_exit_status,ExitStatus}};
333	_ ->
334	    ok
335    end;
336check_result({error,_}=Error,2,_Opts) ->
337    Error;
338check_result({error,_},ExitStatus,_Opts) ->
339    {error,{wrong_exit_status,ExitStatus}};
340check_result({_Ok,0,{_UserSkipped,_AutoSkipped}},0,_Opts) ->
341    ok;
342check_result(CtRunTestResult,ExitStatus,Opts)
343  when is_list(CtRunTestResult) -> % repeated testruns
344    try check_result(sum_testruns(CtRunTestResult,0,0,0,0),ExitStatus,Opts)
345    catch _:_ ->
346	    {error,{unexpected_return_value,{CtRunTestResult,ExitStatus}}}
347    end;
348check_result(done,0,_Opts) ->
349    %% refresh_logs return
350    ok;
351check_result(CtRunTestResult,ExitStatus,_Opts) ->
352    {error,{unexpected_return_value,{CtRunTestResult,ExitStatus}}}.
353
354sum_testruns([{O,F,{US,AS}}|T],Ok,Failed,UserSkipped,AutoSkipped) ->
355    sum_testruns(T,Ok+O,Failed+F,UserSkipped+US,AutoSkipped+AS);
356sum_testruns([],Ok,Failed,UserSkipped,AutoSkipped) ->
357    {Ok,Failed,{UserSkipped,AutoSkipped}}.
358
359run(M, F, A, Config) ->
360    run({M,F,A}, [], Config).
361
362run({M,F,A}, InitCalls, Config) ->
363    CTNode = proplists:get_value(ct_node, Config),
364    Level = proplists:get_value(trace_level, Config),
365    lists:foreach(
366      fun({IM,IF,IA}) ->
367	      test_server:format(Level, "~nInit call ~w:~tw(~tp) on ~p...~n",
368				 [IM, IF, IA, CTNode]),
369	      Result = rpc:call(CTNode, IM, IF, IA),
370	      test_server:format(Level, "~n...with result: ~tp~n", [Result])
371      end, InitCalls),
372    test_server:format(Level, "~nStarting test with ~w:~tw(~tp) on ~p~n",
373		       [M, F, A, CTNode]),
374    rpc:call(CTNode, M, F, A).
375
376%% this is the last function that ct_run:script_start() calls, so the
377%% return value here is what rpc:call/4 above returns
378ct_test_halt(ExitStatus) ->
379    ExitStatus.
380
381%%%-----------------------------------------------------------------
382%%% wait_for_ct_stop/1
383
384wait_for_ct_stop(CTNode) ->
385    %% Give CT at least 15 sec to stop (in case of bad make).
386    wait_for_ct_stop(5, CTNode).
387
388wait_for_ct_stop(0, CTNode) ->
389    test_server:format(0, "Giving up! Stopping ~p.", [CTNode]),
390    false;
391wait_for_ct_stop(Retries, CTNode) ->
392    case rpc:call(CTNode, erlang, whereis, [ct_util_server]) of
393	undefined ->
394	    true;
395	Pid ->
396	    Info = (catch process_info(Pid)),
397	    test_server:format(0, "Waiting for CT (~p) to finish (~p)...",
398			       [Pid,Retries]),
399	    test_server:format(0, "Process info for ~p:~n~tp", [Pid,Info]),
400	    timer:sleep(5000),
401	    wait_for_ct_stop(Retries-1, CTNode)
402    end.
403
404%%%-----------------------------------------------------------------
405%%% ct_rpc/1
406ct_rpc({M,F,A}, Config) ->
407    CTNode = proplists:get_value(ct_node, Config),
408    Level = proplists:get_value(trace_level, Config),
409    test_server:format(Level, "~nCalling ~w:~tw(~tp) on ~p...",
410		       [M,F,A, CTNode]),
411    rpc:call(CTNode, M, F, A).
412
413
414%%%-----------------------------------------------------------------
415%%% random_error/1
416random_error(Config) when is_list(Config) ->
417    rand:seed(exsplus),
418    Gen = fun(0,_) -> ok; (N,Fun) -> Fun(N-1, Fun) end,
419    Gen(rand:uniform(100), Gen),
420
421    ErrorTypes = ['BADMATCH','BADARG','CASE_CLAUSE','FUNCTION_CLAUSE',
422		  'EXIT','THROW','UNDEF'],
423    Type = lists:nth(rand:uniform(length(ErrorTypes)), ErrorTypes),
424    Where = case rand:uniform(2) of
425		1 ->
426		    io:format("ct_test_support *returning* error of type ~w",
427			      [Type]),
428		    tc;
429		2 ->
430		    io:format("ct_test_support *generating* error of type ~w",
431			      [Type]),
432		    lib
433	    end,
434    ErrorFun =
435	fun() ->
436		case Type of
437		    'BADMATCH' ->
438			ok = proplists:get_value(undefined, Config);
439		    'BADARG' ->
440			size(proplists:get_value(priv_dir, Config));
441		    'FUNCTION_CLAUSE' ->
442			random_error(x);
443		    'EXIT' ->
444			spawn_link(fun() ->
445					   undef_proc ! hello,
446					   ok
447				   end);
448		    'THROW' ->
449			PrivDir = proplists:get_value(priv_dir, Config),
450			if is_list(PrivDir) -> throw(generated_throw) end;
451		    'UNDEF' ->
452			apply(?MODULE, random_error, [])
453		end
454	end,
455    %% either call the fun here or return it to the caller (to be
456    %% executed in a test case instead)
457    case Where of
458	tc -> ErrorFun;
459	lib -> ErrorFun()
460    end.
461
462
463%%%-----------------------------------------------------------------
464%%% EVENT HANDLING
465
466handle_event(EH, Event) ->
467    event_receiver ! {self(),{event,EH,Event}},
468    receive {event_receiver,ok} -> ok end,
469    ok.
470
471start_event_receiver(Config) ->
472    CTNode = proplists:get_value(ct_node, Config),
473    Level = proplists:get_value(trace_level, Config),
474    ER = spawn_link(CTNode, fun() -> er() end),
475    test_server:format(Level, "~nEvent receiver ~w started!~n", [ER]),
476    ER.
477
478get_events(_, Config) ->
479    CTNode = proplists:get_value(ct_node, Config),
480    Level = proplists:get_value(trace_level, Config),
481    {event_receiver,CTNode} ! {self(),get_events},
482    Events = receive {event_receiver,Evs} -> Evs end,
483    test_server:format(Level, "Stopping event receiver!~n", []),
484    {event_receiver,CTNode} ! {self(),stop},
485    receive {event_receiver,stopped} -> ok end,
486    Events.
487
488er() ->
489    register(event_receiver, self()),
490    er_loop([]).
491
492er_loop(Evs) ->
493    receive
494	{From,{event,EH,Ev}} ->
495	    From ! {event_receiver,ok},
496	    er_loop([{EH,Ev} | Evs]);
497	{From,get_events} ->
498	    From ! {event_receiver,lists:reverse(Evs)},
499	    er_loop(Evs);
500	{From,stop} ->
501	    unregister(event_receiver),
502	    From ! {event_receiver,stopped},
503	    ok
504    end.
505
506verify_events(TEvs, Evs, Config) ->
507    Node = proplists:get_value(ct_node, Config),
508    case catch verify_events1(TEvs, Evs, Node, Config) of
509	{'EXIT',Reason} ->
510	    Reason;
511	_ ->
512	    ok
513    end.
514
515verify_events(TEvs, Evs, Node, Config) ->
516    case catch verify_events1(TEvs, Evs, Node, Config) of
517	{'EXIT',Reason} ->
518	    Reason;
519	_ ->
520	    ok
521    end.
522
523verify_events1([TestEv|_], [{TEH,#event{name=stop_logging,node=Node,data=_}}|_], Node, _)
524  when element(1,TestEv) == TEH, element(2,TestEv) =/= stop_logging ->
525    test_server:format("Failed to find ~tp in the list of events!~n", [TestEv]),
526    exit({event_not_found,TestEv});
527
528verify_events1(TEvs = [TestEv | TestEvs], Evs = [_|Events], Node, Config) ->
529    case catch locate(TestEv, Node, Evs, Config) of
530	nomatch ->
531	    verify_events1(TEvs, Events, Node, Config);
532	{'EXIT',Reason} ->
533	    test_server:format("Failed to find ~tp in ~tp~n"
534			       "Reason: ~tp~n", [TestEv,Evs,Reason]),
535	    exit(Reason);
536	{Config1,Events1} ->
537	    if is_list(TestEv) ->
538		    ok;
539	       element(1,TestEv) == parallel ; element(1,TestEv) == shuffle ->
540		    ok;
541	       true ->
542		    test_server:format("Found ~tp!", [TestEv])
543	    end,
544	    verify_events1(TestEvs, Events1, Node, Config1)
545    end;
546
547verify_events1([TestEv|_], [], _, _) ->
548    test_server:format("Failed to find ~tp in the list of events!~n", [TestEv]),
549    exit({event_not_found,TestEv});
550
551verify_events1([], Evs, _, Config) ->
552    {Config,Evs}.
553
554%%%----------------------------------------------------------------------------
555%%% locate({TEHandler,TEName,TEData}, TENode, Events, Config) -> {Config1,Evs1}
556%%%
557%%% A group is represented as either:
558%%% {parallel,ListOfCasesAndGroups},
559%%% {shuffle,ListOfCasesAndGroups}, or
560%%% ListOfCasesAndGroups.
561%%%
562%%% The two first and two last events in a group *may* be tc_start and tc_done
563%%% for init_per_group and end_per_group.
564
565%% group (not parallel or shuffle)
566locate(TEvs, Node, Evs, Config) when is_list(TEvs) ->
567    case TEvs of
568	[InitStart = {TEH,tc_start,{M,{init_per_group,GroupName,Props}}},
569	 InitDone  = {TEH,tc_done,{M,{init_per_group,GroupName,Props},R}} | TEvs1] ->
570	    case Evs of
571		[{TEH,#event{name=tc_start,
572			     node=Node,
573			     data={M,{init_per_group,GroupName,Props}}}},
574		 {TEH,#event{name=tc_done,
575			     node=Node,
576			     data={M,{init_per_group,GroupName,Props},Res}}} | Evs1] ->
577		    case result_match(R, Res) of
578			false ->
579			    nomatch;
580			true ->
581			    test_server:format("Found ~tp!", [InitStart]),
582			    test_server:format("Found ~tp!", [InitDone]),
583			    verify_events1(TEvs1, Evs1, Node, Config)
584		    end;
585		_ ->
586		    nomatch
587	    end;
588	_ ->
589	    verify_events1(TEvs, Evs, Node, Config)
590    end;
591
592%% Parallel events: Each test case in the group should be specified in a list
593%% with the tc_start, followed by the tc_done event. The order of the cases
594%% is irrelevant, but it must be checked that every test case exists and
595%% that tc_done comes after tc_start.
596locate({parallel,TEvs}, Node, Evs, Config) ->
597    Start =
598	case TEvs of
599	    [InitStart = {TEH,tc_start,{M,{init_per_group,GroupName,Props}}},
600	     InitDone  = {TEH,tc_done,{M,{init_per_group,GroupName,Props},R}} | TEs] ->
601		case Evs of
602		    [{TEH,#event{name=tc_start,
603				 node=Node,
604				 data={M,{init_per_group,
605					  GroupName,Props}}}}|Es] ->
606			%% Use dropwhile here as a tc_done from a
607			%% previous testcase might sneak in here
608			EvsG = lists:dropwhile(
609				fun({EH,#event{name=tc_done,
610						node=EvNode,
611						data={EvM,{init_per_group,
612							   EvGroupName,
613							   EvProps},EvR}}})
614				   when TEH == EH, EvNode == Node, EvM == M,
615					EvGroupName == GroupName,
616					EvProps == Props ->
617					case result_match(R, EvR) of
618					    true -> false;
619					    false -> true
620					end;
621				   ({EH,#event{name=stop_logging,
622						node=EvNode,data=_}})
623				   when EH == TEH, EvNode == Node ->
624					exit({group_init_done_not_found,
625					      GroupName,Props});
626				   (_) ->
627					true
628				end, Es),
629
630			test_server:format("Found ~tp!", [InitStart]),
631			test_server:format("Found ~tp!", [InitDone]),
632			{TEs,EvsG};
633		    _ ->
634			nomatch
635		end;
636	    _ ->
637		{TEvs,Evs}
638	end,
639    case Start of
640	nomatch ->
641	    nomatch;
642	{TEvs1,Evs1} ->
643	    {TcDoneEvs,RemainEvs,_} =
644		lists:foldl(
645		  %% tc_start event for a parallel test case
646		  fun(TEv={TEH,tc_start,{M,F}}, {Done,RemEvs,RemSize}) ->
647			  %% drop events until TEv is found
648			  Evs2 = lists:dropwhile(
649				   fun({EH,#event{name=tc_start,
650						  node=EvNode,
651						  data={Mod,Func}}}) when
652					     EH == TEH, EvNode == Node,
653					     Mod == M, Func == F ->
654					   false;
655				      ({EH,#event{name=stop_logging,
656						  node=EvNode,data=_}}) when
657					     EH == TEH, EvNode == Node ->
658					   exit({tc_start_not_found,TEv});
659				      (_) ->
660					   true
661				   end, Evs1),
662			  %% split the list at the tc_done event and record the smallest
663			  %% list of remaining events (Evs) as possible
664			  RemEvs1 =
665			      lists:dropwhile(
666				fun({EH,#event{name=tc_done,
667					       node=EvNode,
668					       data={Mod,Func,_}}}) when
669					  EH == TEH, EvNode == Node,
670					  Mod == M, Func == F ->
671					false;
672				   ({EH,#event{name=stop_logging,
673					       node=EvNode,data=_}}) when
674					  EH == TEH, EvNode == Node ->
675					exit({tc_done_not_found,TEv});
676				   (_) ->
677					true
678				end, Evs2),
679			  case RemEvs1 of
680			      [] when Evs2 == [] ->
681				  exit({unmatched,TEv});
682			      [] ->
683				  test_server:format("Found ~tp!", [TEv]),
684				  exit({tc_done_not_found,TEv});
685			      [TcDone|Evs3] ->
686				  test_server:format("Found ~tp!", [TEv]),
687				  RemSize1 = length(Evs3),
688				  if RemSize1 < RemSize ->
689					  {[TcDone|Done],Evs3,RemSize1};
690				     true ->
691					  {[TcDone|Done],RemEvs,RemSize}
692				  end
693			  end;
694		     %% tc_done event for a parallel test case
695		     (TEv={TEH,tc_done,{M,F,R}}, {Done,RemEvs,RemSize}) ->
696			  case [E || E={EH,#event{name=tc_done,
697						  node=EvNode,
698						  data={Mod,Func,Result}}} <- Done,
699				     EH == TEH, EvNode == Node, Mod == M,
700				     Func == F, result_match(R, Result)] of
701			      [TcDone|_] ->
702				  test_server:format("Found ~tp!", [TEv]),
703				  {lists:delete(TcDone, Done),RemEvs,RemSize};
704			      [] ->
705				  exit({unmatched,TEv})
706			  end;
707		     %% tc_start event for end_per_group
708		     (TEv={TEH,tc_start,{M,{end_per_group,GroupName,Props}}},
709		      {Done,RemEvs,_RemSize}) ->
710			  RemEvs1 =
711			      lists:dropwhile(
712				fun({EH,#event{name=tc_start,
713					       node=EvNode,
714					       data={Mod,{end_per_group,
715							  EvGName,EvProps}}}}) when
716					  EH == TEH, EvNode == Node, Mod == M,
717					  EvGName == GroupName, EvProps == Props ->
718					false;
719				   ({EH,#event{name=stop_logging,
720					       node=EvNode,data=_}}) when
721					  EH == TEH, EvNode == Node ->
722					exit({tc_start_not_found,TEv});
723				   (_) ->
724					true
725				end, RemEvs),
726			  case RemEvs1 of
727			      [] ->
728				  exit({end_per_group_not_found,TEv});
729			      [_ | RemEvs2] ->
730				  test_server:format("Found ~tp!", [TEv]),
731				  {Done,RemEvs2,length(RemEvs2)}
732			  end;
733		     %% tc_done event for end_per_group
734		     (TEv={TEH,tc_done,{M,{end_per_group,GroupName,Props},R}},
735		      {Done,RemEvs,_RemSize}) ->
736			  RemEvs1 =
737			      lists:dropwhile(
738				fun({EH,#event{name=tc_done,
739					       node=EvNode,
740					       data={Mod,{end_per_group,
741							  EvGName,EvProps},Res}}}) when
742					  EH == TEH, EvNode == Node, Mod == M,
743					  EvGName == GroupName, EvProps == Props ->
744					case result_match(R, Res) of
745					    true ->
746						false;
747					    false ->
748						true
749					end;
750				   ({EH,#event{name=stop_logging,
751					       node=EvNode,data=_}}) when
752					  EH == TEH, EvNode == Node ->
753					exit({tc_done_not_found,TEv});
754				   (_) ->
755					true
756				end, RemEvs),
757			  case RemEvs1 of
758			      [] ->
759				  exit({end_per_group_not_found,TEv});
760			      [_ | RemEvs2] ->
761				  test_server:format("Found ~tp!", [TEv]),
762				  {Done,RemEvs2,length(RemEvs2)}
763			  end;
764		     %% end_per_group auto- or user skipped
765		     (TEv={TEH,AutoOrUserSkip,{M,{end_per_group,G},R}}, {Done,RemEvs,_RemSize})
766			when AutoOrUserSkip == tc_auto_skip;
767			     AutoOrUserSkip == tc_user_skip ->
768			  RemEvs1 =
769			      lists:dropwhile(
770				fun({EH,#event{name=tc_auto_skip,
771					       node=EvNode,
772					       data={Mod,{end_per_group,EvGroupName},Reason}}}) when
773				   EH == TEH, EvNode == Node, Mod == M, EvGroupName == G ->
774					case match_data(R, Reason) of
775					    match -> false;
776					    _ -> true
777					end;
778				   ({EH,#event{name=tc_user_skip,
779					       node=EvNode,
780					       data={Mod,{end_per_group,EvGroupName},Reason}}}) when
781				   EH == TEH, EvNode == Node, Mod == M, EvGroupName == G ->
782					case match_data(R, Reason) of
783					    match -> false;
784					    _ -> true
785					end;
786				   ({EH,#event{name=stop_logging,
787					       node=EvNode,data=_}}) when
788					  EH == TEH, EvNode == Node ->
789					exit({tc_auto_or_user_skip_not_found,TEv});
790				   (_) ->
791					true
792				end, RemEvs),
793			  case RemEvs1 of
794			      [] ->
795				  exit({end_per_group_not_found,TEv});
796			      [_AutoSkip | RemEvs2] ->
797				  {Done,RemEvs2,length(RemEvs2)}
798			  end;
799		     (TEv={TEH,N,D}, Acc) ->
800			  case [E || E={EH,#event{name=Name,
801						  node=EvNode,
802						  data=Data}} <- Evs1,
803				     EH == TEH, EvNode == Node, Name == N,
804				     match == match_data(D,Data)] of
805			      [] ->
806				  exit({unmatched,TEv});
807			      _ ->
808				  test_server:format("Found ~tp!", [TEv]),
809				  Acc
810			  end;
811		     %% start of a sub-group
812		     (SubGroupTEvs, Acc) when is_list(SubGroupTEvs) ->
813			  verify_events1(SubGroupTEvs, Evs1, Node, Config),
814			  Acc;
815		     (TEv={Prop,_SubGroupTEvs}, Acc) when
816			    Prop == shuffle ; Prop == parallel ->
817			  verify_events1([TEv], Evs1, Node, Config),
818			  Acc
819		  end, {[],Evs1,length(Evs1)}, TEvs1),
820	    case TcDoneEvs of
821		[] ->
822		    test_server:format("Found all parallel events!", []),
823		    {Config,RemainEvs};
824		_ ->
825		    exit({unexpected_events,TcDoneEvs})
826	    end
827    end;
828
829%% Shuffled events: Each test case in the group should be specified in a list
830%% with the tc_start, followed by the tc_done event. The order of the cases
831%% is irrelevant, but it must be checked that every test case exists and
832%% that the tc_done event follows the tc_start.
833locate({shuffle,TEvs}, Node, Evs, Config) ->
834   Start =
835	case TEvs of
836	    [InitStart = {TEH,tc_start,{M,{init_per_group,GroupName,Props}}},
837	     InitDone  = {TEH,tc_done,{M,{init_per_group,GroupName,Props},R}} | TEs] ->
838		case Evs of
839		    [{TEH,#event{name=tc_start,
840				 node=Node,
841				 data={M,{init_per_group,GroupName,EvProps}}}},
842		     {TEH,#event{name=tc_done,
843				 node=Node,
844				 data={M,{init_per_group,GroupName,EvProps},Res}}} | Es] ->
845			case result_match(R, Res) of
846			    true ->
847				case proplists:get_value(shuffle, Props) of
848				    '_' ->
849					case proplists:get_value(shuffle, EvProps) of
850					    false ->
851						exit({no_shuffle_prop_found,
852						      {M,init_per_group,
853						       GroupName,EvProps}});
854					    _ ->
855						PropsCmp = proplists:delete(shuffle, EvProps),
856						PropsCmp = proplists:delete(shuffle, Props)
857					end;
858				    _ ->
859					Props = EvProps
860				end,
861				test_server:format("Found ~tp!", [InitStart]),
862				test_server:format("Found ~tp!", [InitDone]),
863				{TEs,Es};
864			    false ->
865				nomatch
866			end;
867		    _ ->
868			nomatch
869		end;
870	    _ ->
871		{TEvs,Evs}
872	end,
873    case Start of
874	nomatch ->
875	    nomatch;
876	{TEvs1,Evs1} ->
877	    {TcDoneEvs,RemainEvs,_} =
878		lists:foldl(
879		  %% tc_start event for a test case
880		  fun(TEv={TEH,tc_start,{M,F}}, {Done,RemEvs,RemSize}) ->
881			  %% drop events until TEv is found
882			  Evs2 = lists:dropwhile(
883				   fun({EH,#event{name=tc_start,
884						  node=EvNode,
885						  data={Mod,Func}}}) when
886					     EH == TEH, EvNode == Node,
887					     Mod == M, Func == F ->
888					   false;
889				      ({EH,#event{name=stop_logging,
890						  node=EvNode,data=_}}) when
891					     EH == TEH, EvNode == Node ->
892					   exit({tc_start_not_found,TEv});
893				      (_) ->
894					   true
895				   end, Evs1),
896			  %% verify the tc_done event comes next in Evs
897			  case Evs2 of
898			      [] ->
899				  exit({unmatched,TEv});
900			      [_TcStart, TcDone={TEH,#event{name=tc_done,
901							    node=Node,
902							    data={M,F,_}}} | Evs3] ->
903				  test_server:format("Found ~tp!", [TEv]),
904				  RemSize1 = length(Evs3),
905				  if RemSize1 < RemSize ->
906					  {[TcDone|Done],Evs3,RemSize1};
907				     true ->
908					  {[TcDone|Done],RemEvs,RemSize}
909				  end
910			  end;
911		     %% tc_done event for a test case
912		     (TEv={TEH,tc_done,{M,F,R}}, {Done,RemEvs,RemSize}) ->
913			  case [E || E={EH,#event{name=tc_done,
914						  node=EvNode,
915						  data={Mod,Func,Result}}} <- Done,
916				     EH == TEH, EvNode == Node, Mod == M,
917				     Func == F, result_match(R, Result)] of
918			      [TcDone|_] ->
919				  test_server:format("Found ~tp!", [TEv]),
920				  {lists:delete(TcDone, Done),RemEvs,RemSize};
921			      [] ->
922				  exit({unmatched,TEv})
923			  end;
924		     %% tc_start event for end_per_group
925		     (TEv={TEH,tc_start,{M,{end_per_group,GroupName,Props}}},
926		      {Done,RemEvs,_RemSize}) ->
927			  RemEvs1 =
928			      lists:dropwhile(
929				fun({EH,#event{name=tc_start,
930					       node=EvNode,
931					       data={Mod,{end_per_group,
932							  EvGName,_}}}}) when
933					  EH == TEH, EvNode == Node, Mod == M,
934					  EvGName == GroupName ->
935					false;
936				   ({EH,#event{name=stop_logging,
937					       node=EvNode,data=_}}) when
938					  EH == TEH, EvNode == Node ->
939					exit({tc_start_not_found,TEv});
940				   (_) ->
941					true
942				end, RemEvs),
943			  case RemEvs1 of
944			      [] ->
945				  exit({end_per_group_not_found,TEv});
946			      [{_,#event{data={_,{_,_,EvProps1}}}} | RemEvs2] ->
947				  case proplists:get_value(shuffle, Props) of
948				      '_' ->
949					  case proplists:get_value(shuffle, EvProps1) of
950					      false ->
951						  exit({no_shuffle_prop_found,
952							{M,end_per_group,GroupName,EvProps1}});
953					      _ ->
954						  PropsCmp1 = proplists:delete(shuffle, EvProps1),
955						  PropsCmp1 = proplists:delete(shuffle, Props)
956					  end;
957				      _ ->
958					  Props = EvProps1
959				  end,
960				  test_server:format("Found ~tp!", [TEv]),
961				  {Done,RemEvs2,length(RemEvs2)}
962			  end;
963		     %% tc_done event for end_per_group
964		     (TEv={TEH,tc_done,{M,{end_per_group,GroupName,Props},R}},
965		      {Done,RemEvs,_RemSize}) ->
966			  RemEvs1 =
967			      lists:dropwhile(
968				fun({EH,#event{name=tc_done,
969					       node=EvNode,
970					       data={Mod,{end_per_group,
971							  EvGName,_},Res}}}) when
972					  EH == TEH, EvNode == Node, Mod == M,
973					  EvGName == GroupName ->
974					case result_match(R, Res) of
975					    true ->
976						false;
977					    false ->
978						true
979					end;
980				   ({EH,#event{name=stop_logging,
981					       node=EvNode,data=_}}) when
982					  EH == TEH, EvNode == Node ->
983					exit({tc_done_not_found,TEv});
984				   (_) ->
985					true
986				end, RemEvs),
987			  case RemEvs1 of
988			      [] ->
989				  exit({end_per_group_not_found,TEv});
990			      [{_,#event{data={_,{_,_,EvProps1},_}}} | RemEvs2] ->
991				  case proplists:get_value(shuffle, Props) of
992				      '_' ->
993					  case proplists:get_value(shuffle, EvProps1) of
994					      false ->
995						  exit({no_shuffle_prop_found,
996							{M,end_per_group,GroupName,EvProps1}});
997					      _ ->
998						  PropsCmp1 = proplists:delete(shuffle, EvProps1),
999						  PropsCmp1 = proplists:delete(shuffle, Props)
1000					  end;
1001				      _ ->
1002					  Props = EvProps1
1003				  end,
1004				  test_server:format("Found ~tp!", [TEv]),
1005				  {Done,RemEvs2,length(RemEvs2)}
1006			  end;
1007		     %% end_per_group auto-or user skipped
1008		     (TEv={TEH,AutoOrUserSkip,{M,{end_per_group,G},R}}, {Done,RemEvs,_RemSize})
1009			when AutoOrUserSkip == tc_auto_skip;
1010			     AutoOrUserSkip == tc_user_skip ->
1011			  RemEvs1 =
1012			      lists:dropwhile(
1013				fun({EH,#event{name=tc_auto_skip,
1014					       node=EvNode,
1015					       data={Mod,{end_per_group,EvGroupName},Reason}}}) when
1016				   EH == TEH, EvNode == Node, Mod == M, EvGroupName == G, Reason == R ->
1017					false;
1018				   ({EH,#event{name=tc_user_skip,
1019					       node=EvNode,
1020					       data={Mod,{end_per_group,EvGroupName},Reason}}}) when
1021				   EH == TEH, EvNode == Node, Mod == M, EvGroupName == G, Reason == R ->
1022					false;
1023				   ({EH,#event{name=stop_logging,
1024					       node=EvNode,data=_}}) when
1025					  EH == TEH, EvNode == Node ->
1026					exit({tc_auto_skip_not_found,TEv});
1027				   (_) ->
1028					true
1029				end, RemEvs),
1030			  case RemEvs1 of
1031			      [] ->
1032				  exit({end_per_group_not_found,TEv});
1033			      [_AutoSkip | RemEvs2] ->
1034				  {Done,RemEvs2,length(RemEvs2)}
1035			  end;
1036		     %% match other event than test case
1037		     (TEv={TEH,N,D}, Acc) when D == '_' ->
1038			  case [E || E={EH,#event{name=Name,
1039						  node=EvNode,
1040						  data=_}} <- Evs1,
1041				     EH == TEH, EvNode == Node, Name == N] of
1042			      [] ->
1043				  exit({unmatched,TEv});
1044			      _ ->
1045				  test_server:format("Found ~tp!", [TEv]),
1046				  Acc
1047			  end;
1048		     (TEv={TEH,N,D}, Acc) ->
1049			  case [E || E={EH,#event{name=Name,
1050						  node=EvNode,
1051						  data=Data}} <- Evs1,
1052				     EH == TEH, EvNode == Node, Name == N, Data == D] of
1053			      [] ->
1054				  exit({unmatched,TEv});
1055			      _ ->
1056				  test_server:format("Found ~tp!", [TEv]),
1057				  Acc
1058			  end;
1059		     %% start of a sub-group
1060		     (SubGroupTEvs, Acc) when is_list(SubGroupTEvs) ->
1061			  verify_events1(SubGroupTEvs, Evs1, Node, Config),
1062			  Acc;
1063		     (TEv={Prop,_SubGroupTEvs}, Acc) when
1064			    Prop == shuffle ; Prop == parallel ->
1065			  verify_events1([TEv], Evs1, Node, Config),
1066			  Acc
1067		  end, {[],Evs1,length(Evs1)}, TEvs1),
1068	    case TcDoneEvs of
1069		[] ->
1070		    test_server:format("Found all shuffled events!", []),
1071		    {Config,RemainEvs};
1072		_ ->
1073		    exit({unexpected_events,TcDoneEvs})
1074	    end
1075    end;
1076
1077locate({TEH,Name,{'DEF','RUNDIR'}}, Node, [Ev|Evs], Config) ->
1078    case Ev of
1079	{TEH,#event{name=Name, node=Node, data=EvData}} ->
1080	    {_,{_,LogDir}} = lists:keysearch(logdir, 1, get_opts(Config)),
1081	    D = filename:join(LogDir, "ct_run." ++ atom_to_list(Node)),
1082	    case string:find(EvData, D) of
1083		nomatch -> exit({badmatch,EvData});
1084		_ -> ok
1085	    end,
1086	    {Config,Evs};
1087	_ ->
1088	    nomatch
1089    end;
1090
1091locate({TEH,Name,{'DEF',{'START_TIME','LOGDIR'}}}, Node, [Ev|Evs], Config) ->
1092    case Ev of
1093	{TEH,#event{name=Name, node=Node, data=EvData}} ->
1094	    case EvData of
1095		{DT={{_,_,_},{_,_,_}},Dir} when is_list(Dir) ->
1096		    {_,{_,LogDir}} = lists:keysearch(logdir, 1, get_opts(Config)),
1097		    D = filename:join(LogDir, "ct_run." ++ atom_to_list(Node)),
1098		    case string:find(Dir, D) of
1099			nomatch -> exit({badmatch,Dir});
1100			_ -> ok
1101		    end,
1102		    {[{start_time,DT}|Config],Evs};
1103		Data ->
1104		    exit({badmatch,Data})
1105	    end;
1106	_ ->
1107	    nomatch
1108    end;
1109
1110locate({TEH,Name,{'DEF','STOP_TIME'}}, Node, [Ev|Evs], Config) ->
1111    case Ev of
1112	{TEH,#event{name=Name, node=Node, data=EvData}} ->
1113	    case EvData of
1114		DT={{_,_,_},{_,_,_}} ->
1115		    {[{stop_time,DT}|Config],Evs};
1116		Data ->
1117		    exit({badmatch,Data})
1118	    end;
1119	_ ->
1120	    nomatch
1121    end;
1122
1123%% to match variable data as a result of an aborted test case
1124locate({TEH,tc_done,{undefined,undefined,{testcase_aborted,
1125					  {abort_current_testcase,Func},'_'}}},
1126       Node, [Ev|Evs], Config) ->
1127    case Ev of
1128	{TEH,#event{name=tc_done, node=Node,
1129		    data={undefined,undefined,
1130			  {testcase_aborted,{abort_current_testcase,Func},_}}}} ->
1131	    {Config,Evs};
1132	_ ->
1133	    nomatch
1134    end;
1135
1136%% to match variable data as a result of a failed test case
1137locate({TEH,tc_done,{Mod,Func,R={SkipOrFail,{_ErrInd,ErrInfo}}}},
1138       Node, [Ev|Evs], Config) when ((SkipOrFail == skipped) or
1139				     (SkipOrFail == failed)) and
1140				    ((size(ErrInfo) == 2) or
1141				     (size(ErrInfo) == 3)) ->
1142    case Ev of
1143	{TEH,#event{name=tc_done, node=Node,
1144		    data={Mod,Func,Result}}} ->
1145	    case result_match(R, Result) of
1146		true ->
1147		    {Config,Evs};
1148		false ->
1149		    nomatch
1150	    end;
1151	_ ->
1152	    nomatch
1153    end;
1154
1155%% Negative matching: Given two events, the first should not be present before
1156%% the other is matched.
1157locate({negative,NotMatch, Match} = Neg, Node, Evs, Config) ->
1158    case locate(NotMatch, Node, Evs, Config) of
1159	nomatch ->
1160	    locate(Match, Node, Evs, Config);
1161	_ ->
1162	    exit({found_negative_event,Neg})
1163    end;
1164
1165%% matches any event of type Name
1166locate({TEH,Name,Data}, Node, [{TEH,#event{name=Name,
1167					   data = EvData,
1168					   node = Node}}|Evs],
1169       Config) ->
1170    case match_data(Data, EvData) of
1171	match ->
1172	    {Config,Evs};
1173	_ ->
1174	    nomatch
1175    end;
1176
1177locate({_TEH,_Name,_Data}, _Node, [_|_Evs], _Config) ->
1178    nomatch.
1179
1180match_data(Data, EvData) ->
1181    try do_match_data(Data, EvData)
1182    catch _:_ ->
1183	    nomatch
1184    end.
1185
1186do_match_data(D,D) ->
1187    match;
1188do_match_data('_',_) ->
1189    match;
1190do_match_data(Fun,Data) when is_function(Fun) ->
1191    Fun(Data);
1192do_match_data('$proplist',Proplist) ->
1193    do_match_data(
1194      fun(List) ->
1195	      lists:foreach(fun({_,_}) -> ok end,List)
1196      end,Proplist);
1197do_match_data([H1|MatchT],[H2|ValT]) ->
1198    do_match_data(H1,H2),
1199    do_match_data(MatchT,ValT);
1200do_match_data(Tuple1,Tuple2) when is_tuple(Tuple1),is_tuple(Tuple2) ->
1201    do_match_data(tuple_to_list(Tuple1),tuple_to_list(Tuple2));
1202do_match_data([],[]) ->
1203    match.
1204
1205result_match({SkipOrFail,{ErrorInd,{Why,'_'}}},
1206	    {SkipOrFail,{ErrorInd,{Why,_Stack}}}) ->
1207    true;
1208result_match({SkipOrFail,{ErrorInd,{EMod,EFunc,{Why,'_'}}}},
1209	    {SkipOrFail,{ErrorInd,{EMod,EFunc,{Why,_Stack}}}}) ->
1210    true;
1211result_match({failed,{timetrap_timeout,{'$approx',Num}}},
1212	     {failed,{timetrap_timeout,Value}}) ->
1213    if Value >= trunc(Num-0.05*Num),
1214       Value =< trunc(Num+0.05*Num) -> true;
1215       true -> false
1216    end;
1217result_match({user_timetrap_error,{Why,'_'}},
1218	     {user_timetrap_error,{Why,_Stack}}) ->
1219    true;
1220result_match(Result, Result) ->
1221    true;
1222result_match(_, _) ->
1223    false.
1224
1225log_events(TC, Events, EvLogDir, Opts) ->
1226    LogFile = filename:join(EvLogDir, atom_to_list(TC)++".events"),
1227    {ok,Dev} = file:open(LogFile, [write,{encoding,utf8}]),
1228    io:format(Dev, "[~n", []),
1229    log_events1(Events, Dev, " "),
1230    file:close(Dev),
1231    FullLogFile = join_abs_dirs(proplists:get_value(net_dir, Opts),
1232				LogFile),
1233    ct:log("Events written to logfile: <a href=\"file://~ts\">~ts</a>~n",
1234	   [FullLogFile,FullLogFile],[no_css]),
1235    io:format(user, "Events written to logfile: ~tp~n", [LogFile]).
1236
1237log_events1(Evs, Dev, "") ->
1238    log_events1(Evs, Dev, " ");
1239log_events1([E={_EH,tc_start,{_M,{init_per_group,_GrName,Props}}} | Evs], Dev, Ind) ->
1240    case get_prop(Props) of
1241	undefined ->
1242	    io:format(Dev, "~s[~tp,~n", [Ind,E]),
1243	    log_events1(Evs, Dev, Ind++" ");
1244	Prop ->
1245	    io:format(Dev, "~s{~w,~n~s[~tp,~n", [Ind,Prop,Ind++" ",E]),
1246	    log_events1(Evs, Dev, Ind++"  ")
1247    end;
1248log_events1([E={_EH,tc_done,{_M,{init_per_group,_GrName,_Props},_R}} | Evs], Dev, Ind) ->
1249    io:format(Dev, "~s~tp,~n", [Ind,E]),
1250    log_events1(Evs, Dev, Ind++" ");
1251log_events1([E={_EH,tc_start,{_M,{end_per_group,_GrName,_Props}}} | Evs], Dev, Ind) ->
1252    Ind1 = Ind -- " ",
1253    io:format(Dev, "~s~tp,~n", [Ind1,E]),
1254    log_events1(Evs, Dev, Ind1);
1255log_events1([E={_EH,tc_done,{_M,{end_per_group,_GrName,Props},_R}} | Evs], Dev, Ind) ->
1256    case get_prop(Props) of
1257	undefined ->
1258	    io:format(Dev, "~s~tp],~n", [Ind,E]),
1259	    log_events1(Evs, Dev, Ind--" ");
1260	_Prop ->
1261	    io:format(Dev, "~s~tp]},~n", [Ind,E]),
1262	    log_events1(Evs, Dev, Ind--"  ")
1263    end;
1264log_events1([E={_EH,tc_auto_skip,{_M,{end_per_group,_GrName},_Reason}} | Evs], Dev, Ind) ->
1265    io:format(Dev, "~s~tp],~n", [Ind,E]),
1266    log_events1(Evs, Dev, Ind--" ");
1267log_events1([E={_EH,tc_user_skip,{_M,{end_per_group,_GrName},_Reason}} | Evs], Dev, Ind) ->
1268    io:format(Dev, "~s~tp],~n", [Ind,E]),
1269    log_events1(Evs, Dev, Ind--" ");
1270log_events1([E], Dev, Ind) ->
1271    io:format(Dev, "~s~tp~n].~n", [Ind,E]),
1272    ok;
1273log_events1([E | Evs], Dev, Ind) ->
1274    io:format(Dev, "~s~tp,~n", [Ind,E]),
1275    log_events1(Evs, Dev, Ind);
1276log_events1([], _Dev, _Ind) ->
1277    ok.
1278
1279get_prop(Props) ->
1280    case lists:member(parallel, Props) of
1281	true -> parallel;
1282	false -> case lists:member(shuffle, Props) of
1283		     true -> shuffle;
1284		     false -> case lists:keysearch(shuffle, 1, Props) of
1285				  {value,_} -> shuffle;
1286				  _ -> undefined
1287			      end
1288		 end
1289    end.
1290
1291reformat([{_EH,#event{name=start_write_file,data=_}} | Events], EH) ->
1292    reformat(Events, EH);
1293reformat([{_EH,#event{name=finished_write_file,data=_}} | Events], EH) ->
1294    reformat(Events, EH);
1295reformat([{_EH,#event{name=start_make,data=_}} | Events], EH) ->
1296    reformat(Events, EH);
1297reformat([{_EH,#event{name=finished_make,data=_}} | Events], EH) ->
1298    reformat(Events, EH);
1299reformat([{_EH,#event{name=start_logging,data=_}} | Events], EH) ->
1300    [{EH,start_logging,{'DEF','RUNDIR'}} | reformat(Events, EH)];
1301reformat([{_EH,#event{name=test_start,data=_}} | Events], EH) ->
1302    [{EH,test_start,{'DEF',{'START_TIME','LOGDIR'}}} | reformat(Events, EH)];
1303reformat([{_EH,#event{name=test_done,data=_}} | Events], EH) ->
1304    [{EH,test_done,{'DEF','STOP_TIME'}} | reformat(Events, EH)];
1305reformat([{_EH,#event{name=tc_logfile,data=_}} | Events], EH) ->
1306    reformat(Events, EH);
1307reformat([{_EH,#event{name=test_stats,data=Data}} | Events], EH) ->
1308    [{EH,test_stats,Data} | reformat(Events, EH)];
1309%% use this to only print the last test_stats event:
1310%%    case [N || {_,#event{name=N}} <- Events, N == test_stats] of
1311%%	[] ->					% last stats event
1312%%	    [{EH,test_stats,Data} | reformat(Events, EH)];
1313%%	_ ->
1314%%	    reformat(Events, EH)
1315%%    end;
1316reformat([{_EH,#event{name=Name,data=Data}} | Events], EH) ->
1317    [{EH,Name,Data} | reformat(Events, EH)];
1318reformat([], _EH) ->
1319    [].
1320
1321
1322%%%-----------------------------------------------------------------
1323%%% MISC HELP FUNCTIONS
1324
1325join_abs_dirs(undefined, Dir2) ->
1326    Dir2;
1327join_abs_dirs(Dir1, Dir2) ->
1328    case filename:pathtype(Dir2) of
1329	relative ->
1330	    filename:join(Dir1, Dir2);
1331	_ ->
1332	    [_Abs|Parts] = filename:split(Dir2),
1333	    filename:join(Dir1, filename:join(Parts))
1334    end.
1335
1336create_tmp_logdir(Tmp) ->
1337    LogDir = filename:join(Tmp,"ct"),
1338    file:make_dir(LogDir),
1339    LogDir.
1340
1341delete_old_logs({win32,_}, Config) ->
1342    case {proplists:get_value(priv_dir, Config),
1343	  proplists:get_value(logdir, get_opts(Config))} of
1344	{LogDir,LogDir} ->
1345	    ignore;
1346	{_,LogDir} ->				% using tmp for logs
1347	    catch delete_dirs(LogDir)
1348    end;
1349
1350delete_old_logs(_, Config) ->
1351    case os:getenv("CT_USE_TMP_DIR") of
1352	false ->
1353	    ignore;
1354	_ ->
1355	    catch delete_dirs(proplists:get_value(logdir,
1356						  get_opts(Config)))
1357    end.
1358
1359delete_dirs(LogDir) ->
1360    Now = calendar:datetime_to_gregorian_seconds(calendar:local_time()),
1361    SaveTime = list_to_integer(os:getenv("CT_SAVE_OLD_LOGS", "28800")),
1362    Deadline = Now - SaveTime,
1363    Dirs = filelib:wildcard(filename:join(LogDir,"ct_run*")),
1364    Dirs2Del =
1365	lists:foldl(fun(Dir, Del) ->
1366			    [S,Mi,H,D,Mo,Y|_] =
1367				lists:reverse(string:lexemes(Dir, [$.,$-,$_])),
1368			    S2I = fun(Str) -> list_to_integer(Str) end,
1369			    DT = {{S2I(Y),S2I(Mo),S2I(D)}, {S2I(H),S2I(Mi),S2I(S)}},
1370			    Then = calendar:datetime_to_gregorian_seconds(DT),
1371			    if Then > Deadline ->
1372				    Del;
1373			       true ->
1374				    [Dir | Del]
1375			    end
1376		    end, [], Dirs),
1377    case length(Dirs2Del) of
1378	0 ->
1379	    test_server:format(0, "No log directories older than ~w secs.", [SaveTime]);
1380        N ->
1381	    test_server:format(0, "Deleting ~w directories older than ~w secs.", [N,SaveTime])
1382    end,
1383    delete_dirs(LogDir, Dirs2Del).
1384
1385delete_dirs(_, []) ->
1386    ok;
1387delete_dirs(LogDir, [Dir | Dirs]) ->
1388    test_server:format(0, "Removing old log directory: ~ts", [Dir]),
1389    case catch rm_rec(Dir) of
1390	{_,Reason} ->
1391	    test_server:format(0, "Delete failed! (~tp)", [Reason]);
1392	ok ->
1393	    ok
1394    end,
1395    delete_dirs(LogDir, Dirs).
1396
1397rm_rec(Dir) ->
1398    %% ensure we're removing the ct_run directory
1399    case lists:reverse(filename:split(Dir)) of
1400	[[$c,$t,$_,$r,$u,$n,$.|_]|_] ->
1401	    rm_dir(filename:absname(Dir));
1402	_ ->
1403	    {error,{invalid_logdir,Dir}}
1404    end.
1405
1406rm_dir(Dir) ->
1407    case file:list_dir(Dir) of
1408	{error,Errno} ->
1409	    exit({ls_failed,Dir,Errno});
1410	{ok,Files} ->
1411	    rm_files([filename:join(Dir, F) || F <- Files]),
1412	    file:del_dir(Dir)
1413    end.
1414
1415rm_files([F | Fs]) ->
1416    Base = filename:basename(F),
1417    if Base == "." ; Base == ".." ->
1418	    rm_files(Fs);
1419       true ->
1420	    case file:read_file_info(F) of
1421		{ok,#file_info{type=directory}} ->
1422		    rm_dir(F),
1423		    rm_files(Fs);
1424		{ok,_Regular} ->
1425		    case file:delete(F) of
1426			ok ->
1427			    rm_files(Fs);
1428			{error,Errno} ->
1429			    exit({del_failed,F,Errno})
1430		    end
1431	    end
1432    end;
1433rm_files([]) ->
1434    ok.
1435
1436unique_timestamp() ->
1437    unique_timestamp(os:timestamp(), 100000).
1438
1439unique_timestamp(TS, 0) ->
1440    TS;
1441unique_timestamp(TS0, N) ->
1442    case os:timestamp() of
1443	TS0 ->
1444	    timer:sleep(1),
1445	    unique_timestamp(TS0, N-1);
1446	TS1 ->
1447	    TS1
1448    end.
1449
1450%%%-----------------------------------------------------------------
1451%%%
1452slave_stop(Node) ->
1453    Cover = test_server:is_cover(),
1454    if Cover-> cover:flush(Node);
1455       true -> ok
1456    end,
1457    erlang:monitor_node(Node, true),
1458    slave:stop(Node),
1459    receive
1460	{nodedown, Node} ->
1461	    if Cover -> cover:stop(Node);
1462	       true -> ok
1463	    end
1464    after 5000 ->
1465	    erlang:monitor_node(Node, false),
1466	    receive {nodedown, Node} -> ok after 0 -> ok end %flush
1467    end,
1468    ok.
1469