aboutsummaryrefslogblamecommitdiffstats
path: root/lib/megaco/test/megaco_test_lib.erl
blob: 3934a3a957f3042e4f081050b6daa3a62c0b4de3 (plain) (tree)
1
2
3
4
5


                   
                                                        
   










                                                                           







                                                                        
  


























































































                                                                         




                                     

                                                                      
                                                 


                        









































































                                                                              
                               

                                      
            
          

                                  












                                                    


                                                    

























                                                                  

                                      


                                                                    




                                                         















                                                                        
                              









                                                       



                                                
                                   













                                                              


                                                                   






                                                         

                                   


                                               

                                                                  
                    
















                                                                             
                              

                                                                      
                                                                 

                                                               
 

                





                                                         
        













                                                                    
                                       









                                                             







                                                   




































                                                                            




                                                          
                                            


                          

                                        

                                           

                                                   


                                               
                                                 

                                                 

                                              



                                                             








                                                                
 







































































































                                                                       
















                                                                         
                   



                                                

                             



                                           
                                            






                                                                       

                        











                                                      










                                                                             
                         
       


                                                                       



                              
                        
       


                                                                       

















































                                                                           
                                                 































































                                                                         
 



















                                                                             
                                  


                                     
                                                                        













































































                                                                         
                       
                                                      

                                                      








































































                                                                               

                                                          
%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1999-2016. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%%

%%
%%----------------------------------------------------------------------
%% Purpose: Lightweight test server
%%----------------------------------------------------------------------
%%

-module(megaco_test_lib).

-compile(export_all).

-include("megaco_test_lib.hrl").


%% ----------------------------------------------------------------
%% Time related function
%%

sleep(infinity) ->
    receive
    after infinity ->
            ok
    end;
sleep(MSecs) ->
    receive
    after trunc(MSecs) ->
            ok
    end,
    ok.


hours(N)   -> trunc(N * 1000 * 60 * 60).
minutes(N) -> trunc(N * 1000 * 60).
seconds(N) -> trunc(N * 1000).


%% ----------------------------------------------------------------
%% Conditional skip of testcases
%%

non_pc_tc_maybe_skip(Config, Condition, File, Line) 
  when is_list(Config) andalso is_function(Condition) ->
    %% Check if we shall skip the skip
    case os:getenv("TS_OS_BASED_SKIP") of
	"false" ->
	    ok;
	_ ->
	    case lists:keysearch(ts, 1, Config) of
		{value, {ts, megaco}} ->
		    %% Always run the testcase if we are using our own 
		    %% test-server...
		    ok;
		_ ->
		    case (catch Condition()) of
			true ->
			    skip(non_pc_testcase, File, Line);
			_ ->
			    ok
		    end
	    end
    end.


os_based_skip(any) ->
    true;
os_based_skip(Skippable) when is_list(Skippable) ->
    {OsFam, OsName} = 
	case os:type() of
	    {_Fam, _Name} = FamAndName ->
		FamAndName;
	    Fam ->
		{Fam, undefined}
	end,
    case lists:member(OsFam, Skippable) of
	true ->
	    true;
	false ->
	    case lists:keysearch(OsFam, 1, Skippable) of
		{value, {OsFam, OsName}} ->
		    true;
		{value, {OsFam, OsNames}} when is_list(OsNames) ->
		    lists:member(OsName, OsNames);
		_ ->
		    false
	    end
    end;
os_based_skip(_) ->
    false.
    
	    
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Evaluates a test case or test suite
%% Returns a list of failing test cases:
%% 
%%     {Mod, Fun, ExpectedRes, ActualRes}
%%----------------------------------------------------------------------

tickets([Mod]) ->
    tickets(Mod);
tickets(Mod) when is_atom(Mod) ->
    %% p("tickets -> entry with"
    %% 	      "~n   Mod: ~p", [Mod]),
    Res0 = t({Mod, {group, tickets}, Mod:groups()}, default_config()),
    Res  = lists:flatten(Res0),
    %% p("tickets(~w) -> Res: ~p~n", [Mod, Res]),
    display_result(Res),
    Res.


display_alloc_info() ->
    io:format("Allocator memory information:~n", []),
    AllocInfo = alloc_info(),
    display_alloc_info(AllocInfo).

display_alloc_info([]) ->
    ok;
display_alloc_info([{Alloc, Mem}|AllocInfo]) ->
    io:format("  ~15w: ~10w~n", [Alloc, Mem]),
    display_alloc_info(AllocInfo).
    
alloc_info() ->
    case erlang:system_info(allocator) of
	{_Allocator, _Version, Features, _Settings} ->
	    alloc_info(Features);
	_ ->
	    []
    end.

alloc_info(Allocators) ->
    Allocs = [temp_alloc, sl_alloc, std_alloc, ll_alloc, eheap_alloc, 
	      ets_alloc, binary_alloc, driver_alloc], 
    alloc_info(Allocators, Allocs, []).

alloc_info([], _, Acc) ->
    lists:reverse(Acc);
alloc_info([Allocator | Allocators], Allocs, Acc) ->
    case lists:member(Allocator, Allocs) of
	true ->
	    Instances0 = erlang:system_info({allocator, Allocator}),
	    Instances = 
		if 
		    is_list(Instances0) ->
			[Instance || Instance <- Instances0, 
				     element(1, Instance) =:= instance];
		    true ->
			[]
		end,
	    AllocatorMem = alloc_mem_info(Instances),
	    alloc_info(Allocators, Allocs, [{Allocator, AllocatorMem} | Acc]);

	false ->
	    alloc_info(Allocators, Allocs, Acc)
    end.
    
alloc_mem_info(Instances) ->
    alloc_mem_info(Instances, []).

alloc_mem_info([], Acc) ->
    lists:sum([Mem || {instance, _, Mem} <- Acc]);
alloc_mem_info([{instance, N, Info}|Instances], Acc) ->
    InstanceMemInfo = alloc_instance_mem_info(Info),
    alloc_mem_info(Instances, [{instance, N, InstanceMemInfo} | Acc]).

alloc_instance_mem_info(InstanceInfo) ->
    MBCS = alloc_instance_mem_info(mbcs, InstanceInfo),
    SBCS = alloc_instance_mem_info(sbcs, InstanceInfo),
    MBCS + SBCS.

alloc_instance_mem_info(Key, InstanceInfo) ->
    case lists:keysearch(Key, 1, InstanceInfo) of
	{value, {Key, Info}} ->
	    case lists:keysearch(blocks_size, 1, Info) of
		{value, {blocks_size, Mem, _, _}} ->
		    Mem;
		_ ->
		    0
	    end;
	_ ->
	    0
    end.
		      
    
t([Case]) when is_atom(Case) ->
    %% p("t -> entry with"
    %% 	 "~n   [Case]: [~p]", [Case]),
    t(Case);
t(Case) ->
    %% p("t -> entry with"
    %% 	 "~n   Case: ~p", [Case]),
    process_flag(trap_exit, true),
    MEM = fun() -> case (catch erlang:memory()) of
			{'EXIT', _} ->
			    [];
			Res ->
			    Res
		    end
	  end,
    Alloc1 = alloc_info(),
    Mem1 = MEM(),
    Res  = lists:flatten(t(Case, default_config())),
    Alloc2 = alloc_info(),
    Mem2 = MEM(),
    display_result(Res, Alloc1, Mem1, Alloc2, Mem2),
    Res.


groups(Mod) when is_atom(Mod) ->
    try Mod:groups() of
	Groups when is_list(Groups) ->
	    Groups;
	BadGroups ->
	    exit({bad_groups, Mod, BadGroups})
    catch 
	_:_ ->
	    []
    end.

init_suite(Mod, Config) ->
    Mod:init_per_suite(Config).

end_suite(Mod, Config) ->
    Mod:end_per_suite(Config).

init_group(Mod, Group, Config) ->
    Mod:init_per_group(Group, Config).

end_group(Mod, Group, Config) ->
    Mod:init_per_group(Group, Config).

%% This is for sub-SUITEs
t({_Mod, {NewMod, all}, _Groups}, _Config) when is_atom(NewMod) ->
    %% p("t(all) -> entry with"
    %%   "~n   NewMod: ~p", [NewMod]),
    t(NewMod);
t({Mod, {group, Name} = Group, Groups}, Config) 
  when is_atom(Mod) andalso is_atom(Name) andalso is_list(Groups) ->
    %% p("t(group) -> entry with"
    %%   "~n   Mod:    ~p"
    %%   "~n   Name:   ~p"
    %%   "~n   Groups: ~p"
    %%   "~n   Config: ~p", [Mod, Name, Groups, Config]),
    case lists:keysearch(Name, 1, Groups) of
	{value, {Name, _Props, GroupsAndCases}} ->
	    try init_group(Mod, Name, Config) of
		Config2 when is_list(Config2) ->
		    Res = [t({Mod, Case, Groups}, Config2) || 
			      Case <- GroupsAndCases],
		    (catch end_group(Mod, Name, Config2)), 
		    Res;
		Error ->
		    io:format(" => group (~w) init failed: ~p~n", 
			      [Name, Error]),
		    [{failed, {Mod, Group}, Error}]
	    catch
		exit:{skipped, SkipReason} ->
		    io:format(" => skipping group: ~p~n", [SkipReason]),
		    [{skipped, {Mod, Group}, SkipReason, 0}];
		error:undef ->
		    [t({Mod, Case, Groups}, Config) || 
			      Case <- GroupsAndCases];
		T:E ->
		    [{failed, {Mod, Group}, {T,E}, 0}]
	    end;
	false ->
	    exit({unknown_group, Mod, Name, Groups})
    end;
t({Mod, Fun, _}, Config) 
  when is_atom(Mod) andalso is_atom(Fun) ->
    %% p("t -> entry with"
    %%   "~n   Mod:    ~p"
    %%   "~n   Fun:    ~p"
    %%   "~n   Config: ~p", [Mod, Fun, Config]),
    try apply(Mod, Fun, [suite]) of
	[] ->
	    io:format("Eval:   ~p:", [{Mod, Fun}]),
	    Res = eval(Mod, Fun, Config),
	    {R, _, _, _} = Res,
	    io:format(" ~p~n", [R]),
	    Res;

	Cases when is_list(Cases) ->
	    io:format("Expand: ~p ...~n", [{Mod, Fun}]),
	    Map = fun(Case) when is_atom(Case) -> {Mod, Case};
		     (Case) -> Case
		  end,
	    t(lists:map(Map, Cases), Config);

        Error ->
	    io:format("Ignoring:   ~p: ~p~n", [{Mod, Fun}, Error]),
	    [{failed, {Mod, Fun}, Error, 0}]

    catch
        error:undef ->
	    io:format("Undefined:   ~p~n", [{Mod, Fun}]),
	    [{nyi, {Mod, Fun}, ok, 0}]

	
    end;
t(Mod, Config) when is_atom(Mod) ->
    %% p("t -> entry with"
    %%   "~n   Mod:    ~p"
    %%   "~n   Config: ~p", [Mod, Config]),    
    %% This is assumed to be a test suite, so we start by calling 
    %% the top test suite function(s) (all/0 and groups/0).
    try Mod:all() of
	Cases when is_list(Cases) -> 
	    %% The list may contain atoms (actual test cases) and
	    %% group-tuples (a tuple naming a group of test cases).
	    %% A group is defined by the (optional) groups/0 function.
	    Groups = groups(Mod),
	    try init_suite(Mod, Config) of
		Config2 when is_list(Config2) ->
		    Res = [t({Mod, Case, Groups}, Config2) || Case <- Cases],
		    (catch end_suite(Mod, Config2)),
		    Res;
		Error ->
		    io:format(" => suite init failed: ~p~n", [Error]),
		    [{failed, {Mod, init_per_suite}, Error}]
	    catch 
		exit:{skipped, SkipReason} ->
		    io:format(" => skipping suite: ~p~n", [SkipReason]),
		    [{skipped, {Mod, init_per_suite}, SkipReason, 0}];
		error:undef ->
		    [t({Mod, Case, Groups}, Config) || Case <- Cases];
		T:E ->
		    io:format(" => failed suite: ~p~n", [{T,E}]),
		    [{failed, {Mod, init_per_suite}, {T,E}, 0}]
	    end;

	Crap ->
	    Crap

    catch
        error:undef ->
	    io:format("Undefined:   ~p~n", [{Mod, all}]),
	    [{nyi, {Mod, all}, ok, 0}]
		    
    end;
t(Bad, _Config) ->
    [{badarg, Bad, ok, 0}].

eval(Mod, Fun, Config) ->
    TestCase = {?MODULE, Mod, Fun},
    Label = lists:concat(["TEST CASE: ", Fun]),
    megaco:report_event(40, ?MODULE, Mod, Label ++ " started",
			[TestCase, Config]),
    global:register_name(megaco_test_case_sup, self()),
    Flag = process_flag(trap_exit, true),
    put(megaco_test_server, true),
    Config2 = Mod:init_per_testcase(Fun, Config),
    Pid = spawn_link(?MODULE, do_eval, [self(), Mod, Fun, Config2]),
    R = wait_for_evaluator(Pid, Mod, Fun, Config2, []),
    Mod:end_per_testcase(Fun, Config2),
    erase(megaco_test_server),    
    global:unregister_name(megaco_test_case_sup),
    process_flag(trap_exit, Flag),
    R.

-record('REASON', {mod, line, desc}).

wait_for_evaluator(Pid, Mod, Fun, Config, Errors) ->
    wait_for_evaluator(Pid, Mod, Fun, Config, Errors, 0).
wait_for_evaluator(Pid, Mod, Fun, Config, Errors, AccTime) ->
    %% p("wait_for_evaluator -> "
    %%   "~n   Pid:     ~p"
    %%   "~n   Mod:     ~p"
    %%   "~n   Fun:     ~p"
    %%   "~n   Config:  ~p"
    %%   "~n   Errors:  ~p"
    %%   "~n   AccTime: ~p", 
    %%   [Pid, Mod, Fun, Config, Errors, AccTime]),
    TestCase = {?MODULE, Mod, Fun},
    Label = lists:concat(["TEST CASE: ", Fun]),
    receive
	{done, Pid, ok, Time} when Errors =:= [] ->
	    megaco:report_event(40, Mod, ?MODULE, Label ++ " ok",
				[TestCase, Config]),
	    {ok, {Mod, Fun}, Errors, Time};
	{done, Pid, ok, Time} ->
	    megaco:report_event(40, Mod, ?MODULE, Label ++ " failed",
				[TestCase, Config]),
	    {failed, {Mod, Fun}, Errors, Time};
	{done, Pid, {ok, _}, Time} when Errors =:= [] ->
	    megaco:report_event(40, Mod, ?MODULE, Label ++ " ok",
				[TestCase, Config]),
	    {ok, {Mod, Fun}, Errors, Time};
	{done, Pid, {ok, _}, Time} ->
	    megaco:report_event(40, Mod, ?MODULE, Label ++ " failed",
				[TestCase, Config]),
	    {failed, {Mod, Fun}, Errors, Time};
	{done, Pid, Fail, Time} ->
	    megaco:report_event(20, Mod, ?MODULE, Label ++ " failed",
				[TestCase, Config, {return, Fail}, Errors]),
	    {failed, {Mod,Fun}, Fail, Time};
	{'EXIT', Pid, {skipped, Reason}, Time} -> 
	    megaco:report_event(20, Mod, ?MODULE, Label ++ " skipped",
				[TestCase, Config, {skipped, Reason}]),
	    {skipped, {Mod, Fun}, Errors, Time};
	{'EXIT', Pid, Reason, Time} -> 
	    megaco:report_event(20, Mod, ?MODULE, Label ++ " crashed",
				[TestCase, Config, {'EXIT', Reason}]),
	    {crashed, {Mod, Fun}, [{'EXIT', Reason} | Errors], Time};
	{fail, Pid, Reason, Time} ->
	    wait_for_evaluator(Pid, Mod, Fun, Config, 
			       Errors ++ [Reason], AccTime + Time)
    end.

do_eval(ReplyTo, Mod, Fun, Config) ->
    %% p("do_eval -> "
    %%   "~n   ReplyTo: ~p"
    %%   "~n   Mod:     ~p"
    %%   "~n   Fun:     ~p"
    %%   "~n   Config:  ~p", [ReplyTo, Mod, Fun, Config]),
    display_system_info("before", Mod, Fun),
    T1 = os:timestamp(), 
    try Mod:Fun(Config) of
	Res ->
	    %% p("do_eval -> done"
	    %%   "~n   Res: ~p", [Res]),
	    T2   = os:timestamp(), 
	    Time = timer:now_diff(T2, T1), 
	    display_tc_time(Time),
	    display_system_info("after", Mod, Fun),
	    ReplyTo ! {done, self(), Res, Time}
    catch
	error:undef ->
	    %% p("do_eval -> error - undef", []),
	    ReplyTo ! {'EXIT', self(), undef, 0};
	exit:{skipped, Reason} ->
	    %% p("do_eval -> exit - skipped"
	    %%   "~n   Reason: ~p", [Reason]),
	    T2   = os:timestamp(), 
	    Time = timer:now_diff(T2, T1), 
	    display_tc_time(Time),
	    display_system_info("after (skipped)", Mod, Fun),
	    ReplyTo ! {'EXIT', self(), {skipped, Reason}, Time};
	exit:{suite_failed, Reason} ->
	    %% p("do_eval -> exit - suite-failed"
	    %%   "~n   Reason: ~p", [Reason]),
	    T2   = os:timestamp(), 
	    Time = timer:now_diff(T2, T1), 
	    display_tc_time(Time),
	    display_system_info("after (failed)", Mod, Fun),
	    ReplyTo ! {done, self(), Reason, Time}

    end,
    unlink(ReplyTo),
    exit(shutdown).


display_tc_time(Time) ->
    io:format("~n"
	      "~n*********************************************"
	      "~n"
	      "~nTest case completion time: ~.3f sec (~w)"
	      "~n", [(Time / 1000000), Time]),
    ok.

display_system_info(WhenStr) ->
    display_system_info(WhenStr, undefined, undefined).

display_system_info(WhenStr, undefined, undefined) ->
    display_system_info(WhenStr, "");
display_system_info(WhenStr, Mod, Func) ->
    ModFuncStr = lists:flatten(io_lib:format(" ~w:~w", [Mod, Func])),
    display_system_info(WhenStr, ModFuncStr).

display_system_info(WhenStr, ModFuncStr) ->
    Fun = fun(F) -> case (catch F()) of
			{'EXIT', _} ->
			    undefined;
			Res ->
			    Res
		    end
	  end,
    ProcCount    = Fun(fun() -> erlang:system_info(process_count) end),
    ProcLimit    = Fun(fun() -> erlang:system_info(process_limit) end),
    ProcMemAlloc = Fun(fun() -> erlang:memory(processes) end),
    ProcMemUsed  = Fun(fun() -> erlang:memory(processes_used) end),
    ProcMemBin   = Fun(fun() -> erlang:memory(binary) end),
    ProcMemTot   = Fun(fun() -> erlang:memory(total) end),
    %% error_logger:info_msg(
    io:format("~n"
	      "~n*********************************************"
	      "~n"
	      "System info ~s~s => "
	      "~n   Process count:        ~w"
              "~n   Process limit:        ~w"
              "~n   Process memory alloc: ~w"
              "~n   Process memory used:  ~w"
              "~n   Memory for binaries:  ~w"
              "~n   Memory total:         ~w"
	      "~n"
	      "~n*********************************************"
	      "~n"
	      "~n", [WhenStr, ModFuncStr, 
		     ProcCount, ProcLimit, ProcMemAlloc, ProcMemUsed, 
		     ProcMemBin, ProcMemTot]),
    ok.

display_result(Res, Alloc1, Mem1, Alloc2, Mem2) ->
    io:format("~nAllocator info: ~n", []),
    display_alloc(Alloc1, Alloc2),
    io:format("~nMemory info: ~n", []),
    display_memory(Mem1, Mem2),
    display_result(Res).

display_alloc([], []) ->
    io:format("-~n", []),
    ok;
display_alloc(A1, A2) ->
    do_display_alloc(A1, A2).

do_display_alloc([], _) ->
    ok;
do_display_alloc([{Alloc, Mem1}|AllocInfo1], AllocInfo2) ->
    Mem2 = 
	case lists:keysearch(Alloc, 1, AllocInfo2) of
	    {value, {_, Val}} ->
		Val;
	    false ->
		undefined
	end,
    io:format("~15w: ~10w -> ~w~n", [Alloc, Mem1, Mem2]),
    do_display_alloc(AllocInfo1, AllocInfo2).

display_memory([], []) ->
    io:format("-~n", []),
    ok;
display_memory(Mem1, Mem2) ->
    do_display_memory(Mem1, Mem2).


do_display_memory([], _) ->
    ok;
do_display_memory([{Key, Mem1}|MemInfo1], MemInfo2) ->
    Mem2 = 
	case lists:keysearch(Key, 1, MemInfo2) of
	    {value, {_, Val}} ->
		Val;
	    false ->
		undefined
	end,
    io:format("~15w: ~10w -> ~w~n", [Key, Mem1, Mem2]),
    do_display_memory(MemInfo1, MemInfo2).

display_result([]) ->    
    io:format("OK~n", []);
display_result(Res) when is_list(Res) ->
    Ok           = [{MF, Time} || {ok,  MF, _, Time}  <- Res],
    Nyi          = [MF || {nyi, MF, _, _Time} <- Res],
    SkippedGrps  = [{{M,G}, Reason} || 
		       {skipped, {M, {group, G}}, Reason, _Time} <- Res],
    SkippedCases = [{MF, Reason} || 
		       {skipped, {_M, F} = MF, Reason, _Time} <- Res, 
		       is_atom(F)],
    FailedGrps   = [{{M,G}, Reason} || 
		       {failed,  {M, {group, G}}, Reason, _Time} <- Res],
    FailedCases  = [{MF, Reason} || 
		       {failed,  {_M, F} = MF, Reason, _Time} <- Res, 
		       is_atom(F)],
    Crashed      = [{MF, Reason} || {crashed, MF, Reason, _Time} <- Res],
    display_summery(Ok, Nyi, 
		    SkippedGrps, SkippedCases, 
		    FailedGrps,  FailedCases, 
		    Crashed),
    display_ok(Ok),
    display_skipped("groups",     SkippedGrps),
    display_skipped("test cases", SkippedCases),
    display_failed("groups",      FailedGrps),
    display_failed("test cases",  FailedCases),
    display_crashed(Crashed).

display_summery(Ok, Nyi, 
		SkippedGrps, SkippedCases, 
		FailedGrps, FailedCases, 
		Crashed) ->
    io:format("~nTest case summery:~n", []),
    display_summery(Ok,           "test case",  "successfull"),
    display_summery(Nyi,          "test case",  "not yet implemented"),
    display_summery(SkippedGrps,  "group",      "skipped"),
    display_summery(SkippedCases, "test case",  "skipped"),
    display_summery(FailedGrps,   "group",      "failed"),
    display_summery(FailedCases,  "test case",  "failed"),
    display_summery(Crashed,      "test case",  "crashed"),
    io:format("~n", []).
   

display_summery(Res, Kind, Info) ->
    Len = length(Res),
    if 
	Len =:= 1 ->
	    display_summery(Len, Kind ++ " " ++ Info);
	true ->
	    display_summery(Len, Kind ++ "s " ++ Info)
    end.
    
display_summery(Len, Info) ->
    io:format("  ~w ~s~n", [Len, Info]).
    
display_ok([]) ->
    ok;
display_ok(Ok) ->
    io:format("Ok test cases:~n", []),
    F = fun({{M, F}, Time}) -> 
		io:format("  ~w : ~w => ~.2f sec~n", [M, F, Time / 1000000]) 
	end,
    lists:foreach(F, Ok),
    io:format("~n", []).

display_skipped(_, []) ->
    ok;
display_skipped(Pre, Skipped) ->
    io:format("Skipped ~s:~n", [Pre]),
    F = fun({X, Reason}) -> io:format("  ~p => ~p~n", [X, Reason]) end,
    lists:foreach(F, Skipped),
    io:format("~n", []).


display_failed(_, []) ->
    ok;
display_failed(Pre, Failed) ->
    io:format("Failed ~s:~n", [Pre]),
    F = fun({X, Reason}) -> io:format("  ~p => ~p~n", [X, Reason]) end,
    lists:foreach(F, Failed),
    io:format("~n", []).

display_crashed([]) ->
    ok;
display_crashed(Crashed) ->
    io:format("Crashed test cases:~n", []),
    F = fun({MF, Reason}) -> io:format("  ~p => ~p~n", [MF, Reason]) end,
    lists:foreach(F, Crashed),
    io:format("~n", []).
        
    
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Verify that the actual result of a test case matches the exected one
%% Returns the actual result
%% Stores the result in the process dictionary if mismatch

error(Actual, Mod, Line) ->
    global:send(megaco_global_logger, {failed, Mod, Line}),
    log("<ERROR> Bad result: ~p~n", [Actual], Mod, Line),
    Label = lists:concat([Mod, "(", Line, ") unexpected result"]),
    megaco:report_event(60, Mod, Mod, Label,
			[{line, Mod, Line}, {error, Actual}]),
    case global:whereis_name(megaco_test_case_sup) of
	undefined -> 
	    ignore;
	Pid -> 
	    Fail = #'REASON'{mod = Mod, line = Line, desc = Actual},
	    Pid ! {fail, self(), Fail}
    end,
    Actual.

log(Format, Args, Mod, Line) ->
    case global:whereis_name(megaco_global_logger) of
	undefined ->
	    io:format(user, "~p~p(~p): " ++ Format, 
		      [self(), Mod, Line] ++ Args);
	Pid ->
	    io:format(Pid, "~p~p(~p): " ++ Format, 
		      [self(), Mod, Line] ++ Args)
    end.

skip(Actual, File, Line) ->
    log("Skipping test case~n", [], File, Line),
    String = lists:flatten(io_lib:format("Skipping test case ~p(~p): ~p~n",
					 [File, Line, Actual])),
    exit({skipped, String}).

fatal_skip(Actual, File, Line) ->
    error(Actual, File, Line),
    exit({skipped, {fatal, Actual, File, Line}}).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Flush the message queue and return its messages
flush() ->
    receive
	Msg ->
	    [Msg | flush()]
    after 1000 ->
	    []
    end.
	    
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Check if process is alive and kicking
still_alive(Pid) ->   
    case catch erlang:is_process_alive(Pid) of % New BIF in Erlang/OTP R5
	true -> 
	    true;
	false -> 
	    false;
	{'EXIT', _} -> % Pre R5 backward compatibility 
	    case process_info(Pid, message_queue_len) of
		undefined -> false;
		_ -> true
	    end 
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% The proxy process

proxy_start(ProxyId) ->
    spawn_link(?MODULE, proxy_init, [ProxyId, self()]).

proxy_start(Node, ProxyId) ->
    spawn_link(Node, ?MODULE, proxy_init, [ProxyId, self()]).

proxy_init(ProxyId, Controller) ->
    process_flag(trap_exit, true),
    ?LOG("[~p] proxy started by ~p~n",[ProxyId, Controller]),
    proxy_loop(ProxyId, Controller).

proxy_loop(OwnId, Controller) ->
    receive
	{'EXIT', Controller, Reason} ->
	    p("proxy_loop -> received exit from controller"
	      "~n   Reason: ~p"
	      "~n", [Reason]),
	    exit(Reason);
	{apply, Fun} ->
	    p("proxy_loop -> received apply request~n", []),
	    Res = Fun(),
	    p("proxy_loop -> apply result: "
	      "~n   ~p"
	      "~n", [Res]),
	    Controller ! {res, OwnId, Res},
	    proxy_loop(OwnId, Controller);
	OtherMsg ->
	    p("proxy_loop -> received unknown message: "
	      "~n  OtherMsg: ~p"
	      "~n", [OtherMsg]),
	    Controller ! {msg, OwnId, OtherMsg},
	    proxy_loop(OwnId, Controller)
    end.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Test server callbacks
init_per_testcase(_Case, Config) ->
    Pid = group_leader(),
    Name = megaco_global_logger,
    case global:whereis_name(Name) of
	undefined ->
	    global:register_name(megaco_global_logger, Pid);
	Pid ->
	    io:format("~w:init_per_testcase -> "
		      "already registered to ~p~n", [?MODULE, Pid]),
	    ok;
	OtherPid when is_pid(OtherPid) ->
	    io:format("~w:init_per_testcase -> "
		      "already registered to other ~p (~p)~n", 
		      [?MODULE, OtherPid, Pid]),
	    exit({already_registered, {megaco_global_logger, OtherPid, Pid}})
    end,
    set_kill_timer(Config).

end_per_testcase(_Case, Config) ->
    Name = megaco_global_logger,
    case global:whereis_name(Name) of
	undefined ->
	    io:format("~w:end_per_testcase -> already un-registered~n", 
		      [?MODULE]),
	    ok;
	Pid when is_pid(Pid) ->
	    global:unregister_name(megaco_global_logger),
	    ok
    end,
    reset_kill_timer(Config).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Set kill timer

set_kill_timer(Config) ->
    case init:get_argument(megaco_test_timeout) of
	{ok, _} -> 
	    Config;
	_ ->
	    Time = 
		case lookup_config(tc_timeout, Config) of
		    [] ->
			timer:minutes(5);
		    ConfigTime when is_integer(ConfigTime) ->
			ConfigTime
		end,
	    Dog = 
		case get(megaco_test_server) of
		    true ->
			spawn_link(?MODULE, watchdog, [self(), Time]);
		    _ ->
			test_server:timetrap(Time)
		end,
	    [{kill_timer, Dog}|Config]
		    
	    
    end.

reset_kill_timer(Config) ->
    DogKiller = 
	case get(megaco_test_server) of
	    true ->
		fun(P) when is_pid(P) -> P ! stop;
		   (_) -> ok 
		end;
	    _ ->
		fun(Ref) -> test_server:timetrap_cancel(Ref) end
	end,
    case lists:keysearch(kill_timer, 1, Config) of
	{value, {kill_timer, Dog}} ->
	    DogKiller(Dog), 
	    lists:keydelete(kill_timer, 1, Config);
	_ ->
	    Config
    end.

watchdog(Pid, Time) ->
    erlang:now(),
    receive
	stop ->
	    ok
    after Time ->
	    case (catch process_info(Pid)) of
		undefined ->
		    ok;
		Info ->
		    ?LOG("<ERROR> Watchdog in test case timed out "
			"for ~p after ~p min"
			 "~n~p"
			 "~n",
		    [Pid, Time div (1000*60), Info]),
		    exit(Pid, kill)
	    end
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

prepare_test_case(Actions, N, Config, File, Line) ->
    OrigNodes = lookup_config(nodes, Config),
    TestNodes = lookup_config(nodenames, Config), %% For testserver
    This      = node(),
    SomeNodes = OrigNodes ++ (TestNodes -- OrigNodes),
    AllNodes  = [This | (SomeNodes -- [This])],
    Nodes     = pick_n_nodes(N, AllNodes, File, Line),
    start_nodes(Nodes, File, Line),
    do_prepare_test_case(Actions, Nodes, Config, File, Line).

do_prepare_test_case([init | Actions], Nodes, Config, File, Line) ->
    process_flag(trap_exit, true),
    megaco_test_lib:flush(),
    do_prepare_test_case(Actions, Nodes, Config, File, Line);
do_prepare_test_case([{stop_app, App} | Actions], Nodes, Config, File, Line) ->
    _Res = rpc:multicall(Nodes, application, stop, [App]),
    do_prepare_test_case(Actions, Nodes, Config, File, Line);
do_prepare_test_case([], Nodes, _Config, _File, _Line) ->
    Nodes.

pick_n_nodes(all, AllNodes, _File, _Line) ->
    AllNodes;
pick_n_nodes(N, AllNodes, _File, _Line) 
  when is_integer(N) andalso (length(AllNodes) >= N) ->
    AllNodes -- lists:nthtail(N, AllNodes);
pick_n_nodes(N, AllNodes, File, Line) ->
    fatal_skip({too_few_nodes, N, AllNodes}, File, Line).
   
lookup_config(Key,Config) ->
    case lists:keysearch(Key, 1, Config) of
	{value,{Key,Val}} ->
	    Val;
	_ ->
	    []
    end.

default_config() ->
    [{nodes, default_nodes()}, {ts, megaco}].

default_nodes() ->    
    mk_nodes(2, []).

mk_nodes(0, Nodes) ->
    Nodes;
mk_nodes(N, []) ->
    mk_nodes(N - 1, [node()]);
mk_nodes(N, Nodes) when N > 0 ->
    Head = hd(Nodes),
    [Name, Host] = node_to_name_and_host(Head),
    Nodes ++ [mk_node(I, Name, Host) || I <- lists:seq(1, N)].

mk_node(N, Name, Host) ->
    list_to_atom(lists:concat([Name ++ integer_to_list(N) ++ "@" ++ Host])).
    
%% Returns [Name, Host]    
node_to_name_and_host(Node) ->
    string:tokens(atom_to_list(Node), [$@]).

start_nodes([Node | Nodes], File, Line) ->
    case net_adm:ping(Node) of
	pong ->
	    start_nodes(Nodes, File, Line);
	pang ->
	    [Name, Host] = node_to_name_and_host(Node),
	    case slave:start_link(Host, Name) of
		{ok, NewNode} when NewNode =:= Node ->
		    Path = code:get_path(),
		    {ok, Cwd} = file:get_cwd(),
		    true = rpc:call(Node, code, set_path, [Path]),
		    ok = rpc:call(Node, file, set_cwd, [Cwd]),
		    true = rpc:call(Node, code, set_path, [Path]),
		    {_, []} = rpc:multicall(global, sync, []),
		    start_nodes(Nodes, File, Line);
		Other ->
		    fatal_skip({cannot_start_node, Node, Other}, File, Line)
	    end
    end;
start_nodes([], _File, _Line) ->
    ok.

p(F, A) ->
    io:format("~p~w:" ++ F ++ "~n", [self(), ?MODULE |A]).