%% 
%% %CopyrightBegin%
%% 
  Copyright Ericsson AB 2003 - 2020 . 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
%%
%%     -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(snmp_test_server).

-compile(export_all).

-export([
	 run/1, run/2,

	 error/3,
	 skip/3,
	 fatal_skip/3,

	 init_per_testcase/2,
	 end_per_testcase/2
	]).

-include("snmp_test_lib.hrl").

-define(GLOBAL_LOGGER, snmp_global_logger).
-define(TEST_CASE_SUP, snmp_test_case_supervisor).

-define(d(F,A),d(F,A,?LINE)).

-ifndef(snmp_priv_dir).
-define(snmp_priv_dir, "run-" ++ timestamp()).
-endif.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Evaluates a test case or test suite
%% Returns a list of failing test cases:
%% 
      { Mod , Fun , ExpectedRes , ActualRes }
%%----------------------------------------------------------------------

run([Mod, Fun]) when is_atom(Mod) andalso is_atom(Fun) ->
    Res = run({Mod, Fun}, default_config(Mod)),
    display_result(Res),
    Res;
run({Mod, _Fun} = Case) when is_atom(Mod) ->
    io:format("~n", []),
    Res = run(Case, default_config(Mod)),
    display_result(Res),
    Res;
run(Mod) when is_atom(Mod) ->
    io:format("~n", []),
    Res = run(Mod, default_config(Mod)),
    display_result(Res),
    Res;
run([Mod]) when is_atom(Mod) ->
    io:format("~n", []),
    Res = run(Mod, default_config(Mod)),
    display_result(Res),
    Res.
    

run({Mod, Fun}, Config) when is_atom(Mod) andalso 
			     is_atom(Fun) andalso 
			     is_list(Config) ->
    ?d("run(~p,~p) -> entry", [Mod, Fun]),
    case (catch apply(Mod, Fun, [suite])) of
	[] ->
	    io:format("~n~n*** Eval: ~p ***************~n", 
		      [{Mod, Fun}]),
	    case eval(Mod, Fun, Config) of
		{ok, _, _} ->
		    [];
		Other ->
		    [Other]
	    end;

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

        {conf, InitSuite, Cases, FinishSuite} when is_atom(InitSuite) andalso 
						   is_list(Cases) andalso 
						   is_atom(FinishSuite) ->
	    ?d("run -> conf: "
	       "~n   InitSuite:   ~p"
	       "~n   Cases:       ~p"
	       "~n   FinishSuite: ~p", [InitSuite, Cases, FinishSuite]),
	    do_suite(Mod, InitSuite, Cases, FinishSuite, Config);
                    
        {req, _, SubCases} when is_list(SubCases) ->
	    ?d("run -> req: "
	       "~n   SubCases: ~p", [SubCases]),
	    do_subcases(Mod, Fun, SubCases, Config, []);
                    
        {req, _, Conf} ->
	    ?d("run -> req: "
	       "~n   Conf: ~p", [Conf]),
	    do_subcases(Mod, Fun, [Conf], Config, []);
                    
        {'EXIT', {undef, _}} ->
            io:format("~n*** Undefined:   ~p~n", [{Mod, Fun}]),
            [{nyi, {Mod, Fun}, ok}];
                    
        Error ->
            io:format("~n*** Ignoring:   ~p: ~p~n", [{Mod, Fun}, Error]),
            [{failed, {Mod, Fun}, Error}]
    end;

run(Mod, Config) when is_atom(Mod) andalso is_list(Config) ->
    run({Mod, all}, Config);

run(Cases, Config) when is_list(Cases) andalso is_list(Config) ->
    Errors = [run(Case, Config) || Case <- Cases],
    lists:append(Errors);

run(Bad, _Config) ->
    [{badarg, Bad, ok}].


do_suite(Mod, Init, Cases, Finish, Config0) ->
    ?d("do_suite -> entry with"
       "~n   Mod:     ~p"
       "~n   Init:    ~p"
       "~n   Cases:   ~p"
       "~n   Finish:  ~p"
       "~n   Config0: ~p", [Mod, Init, Cases, Finish, Config0]),
    case (catch apply(Mod, Init, [Config0])) of
	Config when is_list(Config) ->
	    io:format("~n*** Expand: ~p ...~n", [Mod]),
	    Map = fun(Case) when is_atom(Case) -> {Mod, Case};
		     (Case) -> Case
		  end,
	    Res = run(lists:map(Map, Cases), Config),
	    (catch apply(Mod, Finish, [Config])),
	    Res;

	{'EXIT', {skipped, Reason}} ->
	    io:format(" => skipping: ~p~n", [Reason]),
	    SkippedCases = 
		[{skipped, {Mod, Case}, suite_init_skipped} || Case <- Cases],
	    lists:flatten([[{skipped, {Mod, Init}, Reason}],
			   SkippedCases, 
			   [{skipped, {Mod, Finish}, suite_init_skipped}]]);

	Error ->
	    io:format(" => init (~p) failed: ~n~p~n", [Init, Error]),
	    InitResult = 
		[{failed, {Mod, Init}, Error}],
	    SkippedCases = 
		[{skipped, {Mod, Case}, suite_init_failed} || Case <- Cases],
	    FinResult = 
		case (catch apply(Mod, Finish, [Config0])) of
		    ok ->
			[];
		    FinConfig when is_list(FinConfig) ->
			[];
		    FinError ->
			[{failed, {Mod, Finish}, FinError}]
		end,
	    lists:flatten([InitResult, SkippedCases, FinResult])

    end.


do_subcases(_Mod, _Fun, [], _Config, Acc) ->
    lists:flatten(lists:reverse(Acc));
do_subcases(Mod, Fun, [{conf, Init, Cases, Finish}|SubCases], Config, Acc) 
  when is_atom(Init) andalso is_list(Cases) andalso is_atom(Finish) ->
    R = case (catch apply(Mod, Init, [Config])) of
	    Conf when is_list(Conf) ->
		io:format("~n*** Expand: ~p ...~n", [{Mod, Fun}]),
		Map = fun(Case) when is_atom(Case) -> {Mod, Case};
			 (Case) -> Case
		      end,
		Res = run(lists:map(Map, Cases), Conf),
		(catch apply(Mod, Finish, [Conf])),
		Res;
	    
	    {'EXIT', {skipped, Reason}} ->
		io:format(" => skipping: ~p~n", [Reason]),
		[{skipped, {Mod, Fun}, Reason}];
	    
	    Error ->
		io:format(" => init (~p) failed: ~n~p~n", [Init, Error]),
		(catch apply(Mod, Finish, [Config])),
		[{failed, {Mod, Fun}, Error}]
	end,
    do_subcases(Mod, Fun, SubCases, Config, [R|Acc]);
do_subcases(Mod, Fun, [SubCase|SubCases], Config, Acc) when is_atom(SubCase) ->
    R = do_case(Mod, SubCase, Config),
    do_subcases(Mod, Fun, SubCases,Config, [R|Acc]).


do_case(M, F, C) ->
    io:format("~n~n*** Eval: ~p ***************~n", [{M, F}]),
    case eval(M, F, C) of
	{ok, _, _} ->
	    [];
	Other ->
	    [Other]
    end.


eval(Mod, Fun, Config) ->
    Flag    = process_flag(trap_exit, true),
    global:register_name(?TEST_CASE_SUP, self()),
    Config2 = Mod:init_per_testcase(Fun, Config),
    Self    = self(), 
    Eval    = fun() -> do_eval(Self, Mod, Fun, Config2) end,
    Pid     = spawn_link(Eval),
    R       = wait_for_evaluator(Pid, Mod, Fun, Config2, []),
    Mod:end_per_testcase(Fun, Config2),
    global:unregister_name(?TEST_CASE_SUP),
    process_flag(trap_exit, Flag),
    R.

wait_for_evaluator(Pid, Mod, Fun, Config, Errors) ->
    Pre = lists:concat(["TEST CASE: ", Fun]),
    receive
	{'EXIT', _Watchdog, watchdog_timeout} ->
	    io:format("*** ~s WATCHDOG TIMEOUT~n", [Pre]), 
	    exit(Pid, kill),
	    {failed, {Mod, Fun}, watchdog_timeout};
	{done, Pid, ok} when Errors =:= [] ->
	    io:format("*** ~s OK~n", [Pre]),
	    {ok, {Mod, Fun}, Errors};
	{done, Pid, {ok, _}} when Errors =:= [] ->
	    io:format("*** ~s OK~n", [Pre]),
	    {ok, {Mod, Fun}, Errors};
	{done, Pid, Fail} ->
	    io:format("*** ~s FAILED~n~p~n", [Pre, Fail]),
	    {failed, {Mod, Fun}, Fail};
	{'EXIT', Pid, {skipped, Reason}} -> 
	    io:format("*** ~s SKIPPED~n~p~n", [Pre, Reason]),
	    {skipped, {Mod, Fun}, Errors};
	{'EXIT', Pid, Reason} -> 
	    io:format("*** ~s CRASHED~n~p~n", [Pre, Reason]),
	    {crashed, {Mod, Fun}, [{'EXIT', Reason} | Errors]};
	{fail, Pid, Reason} ->
	    io:format("*** ~s FAILED~n~p~n", [Pre, Reason]),
	    wait_for_evaluator(Pid, Mod, Fun, Config, Errors ++ [Reason])
   end.

do_eval(ReplyTo, Mod, Fun, Config) ->
    case (catch apply(Mod, Fun, [Config])) of
	{'EXIT', {skipped, Reason}} ->
	    ReplyTo ! {'EXIT', self(), {skipped, Reason}};
	Other ->
	    ReplyTo ! {done, self(), Other}
    end,
    unlink(ReplyTo),
    exit(shutdown).


display_result([]) ->    
    io:format("TEST OK~n", []);

display_result(Errors) when is_list(Errors) ->
    Nyi     = [MF || {nyi, MF, _} <- Errors],
    Skipped = [{MF, Reason} || {skipped, MF, Reason} <- Errors],
    Crashed = [{MF, Reason} || {crashed, MF, Reason} <- Errors],
    Failed  = [{MF, Reason} || {failed,  MF, Reason} <- Errors],
    display_skipped(Skipped),
    display_crashed(Crashed),
    display_failed(Failed),
    display_summery(Nyi, Skipped, Crashed, Failed).

display_summery(Nyi, Skipped, Crashed, Failed) ->
    io:format("~nTest case summery:~n", []),
    display_summery(Nyi, "not yet implemented"),
    display_summery(Skipped, "skipped"),
    display_summery(Crashed, "crashed"),
    display_summery(Failed, "failed"),
    io:format("~n", []).
   
display_summery([], _) ->
    ok;
display_summery(Res, Info) ->
    io:format("  ~w test cases ~s~n", [length(Res), Info]).
    
display_skipped([]) ->
    io:format("Skipped test cases: -~n", []);
display_skipped(Skipped) ->
    io:format("Skipped test cases:~n", []),
    [io:format("  ~p => ~p~n", [MF, Reason]) || {MF, Reason} <- Skipped],
    io:format("~n", []).
    
display_crashed([]) ->
    io:format("Crashed test cases: -~n", []);
display_crashed(Crashed) ->
    io:format("Crashed test cases:~n", []),
    [io:format("  ~p => ~p~n", [MF, Reason]) || {MF, Reason} <- Crashed],
    io:format("~n", []).
    
display_failed([]) ->
    io:format("Failed test cases: -~n", []);
display_failed(Failed) ->
    io:format("Failed test cases:~n", []),
    [io:format("  ~p => ~p~n", [MF, Reason]) || {MF, Reason} <- Failed],
    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(?GLOBAL_LOGGER, {failed, Mod, Line}),
    log("<ERROR> Bad result: ~p~n", [Actual], Mod, Line),
    case global:whereis_name(?TEST_CASE_SUP) of
	undefined -> 
	    ignore;
	Pid -> 
	    Pid ! {fail, self(), {Actual, Mod, Line}}
    end,
    Actual.

skip(Actual, Mod, Line) ->
    log("Skipping test case~n", [], Mod, Line),
    exit({skipped, {Actual, Mod, Line}}).

fatal_skip(Actual, Mod, Line) ->
    error(Actual, Mod, Line),
    exit(shutdown).


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


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Test server callbacks

init_per_testcase(_Case, Config) ->
    global:register_name(?GLOBAL_LOGGER, group_leader()),
    Config.

end_per_testcase(_Case, _Config) ->
    global:unregister_name(?GLOBAL_LOGGER),
    ok.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Internal utility functions

default_config(Mod) ->
    PrivDir0 = ?snmp_priv_dir,
    case filename:pathtype(PrivDir0) of
	absolute ->
	    ok;
	_ ->
	    case file:make_dir(Mod) of
		ok ->
		    ok;
		{error, eexist} ->
		    ok
	    end,
	    PrivDir = filename:join(Mod, PrivDir0), 
	    case file:make_dir(PrivDir) of
		ok ->
		    ok;
		{error, eexist} ->
		    ok;
		Error ->
		    ?FAIL({failed_creating_subsuite_top_dir, Error})
	    end,
	    [{priv_dir, PrivDir}]
    end.


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

d(F, A, L) ->
    d(true, F, A, L).
  d(get(dbg ) , F , A , L ) .

d(true, F, A, L) ->
    io:format("STS[~w] ~p " ++ F ++ "~n", [L,self()|A]);
d(_, _, _, _) ->
    ok.

timestamp() ->
    {Date, Time}     = calendar:now_to_datetime( erlang:timestamp() ),
    {YYYY, MM, DD}   = Date,
    {Hour, Min, Sec} = Time,
    FormatDate =
        io_lib:format("~.4w-~.2.0w-~.2.0w_~.2.0w.~.2.0w.~.2.0w",
                      [YYYY,MM,DD,Hour,Min,Sec]),
    lists:flatten(FormatDate).
    

 
 %CopyrightBegin%
 
 
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     -2.0

 Unless required by applicable law or agreed to in writing, software
 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
----------------------------------------------------------------------

 Evaluates a test case or test suite
 Returns a list of failing test cases:
 
----------------------------------------------------------------------

 Returns the actual result
 Stores the result in the process dictionary if mismatch

 Test server callbacks

 Internal utility functions

  Copyright Ericsson AB 2003 - 2020 . All Rights Reserved .
  Licensed under the Apache License , Version 2.0 ( the " License " ) ;
  distributed under the License is distributed on an " AS IS " BASIS ,


-module(snmp_test_server).

-compile(export_all).

-export([
	 run/1, run/2,

	 error/3,
	 skip/3,
	 fatal_skip/3,

	 init_per_testcase/2,
	 end_per_testcase/2
	]).

-include("snmp_test_lib.hrl").

-define(GLOBAL_LOGGER, snmp_global_logger).
-define(TEST_CASE_SUP, snmp_test_case_supervisor).

-define(d(F,A),d(F,A,?LINE)).

-ifndef(snmp_priv_dir).
-define(snmp_priv_dir, "run-" ++ timestamp()).
-endif.


      { Mod , Fun , ExpectedRes , ActualRes }

run([Mod, Fun]) when is_atom(Mod) andalso is_atom(Fun) ->
    Res = run({Mod, Fun}, default_config(Mod)),
    display_result(Res),
    Res;
run({Mod, _Fun} = Case) when is_atom(Mod) ->
    io:format("~n", []),
    Res = run(Case, default_config(Mod)),
    display_result(Res),
    Res;
run(Mod) when is_atom(Mod) ->
    io:format("~n", []),
    Res = run(Mod, default_config(Mod)),
    display_result(Res),
    Res;
run([Mod]) when is_atom(Mod) ->
    io:format("~n", []),
    Res = run(Mod, default_config(Mod)),
    display_result(Res),
    Res.
    

run({Mod, Fun}, Config) when is_atom(Mod) andalso 
			     is_atom(Fun) andalso 
			     is_list(Config) ->
    ?d("run(~p,~p) -> entry", [Mod, Fun]),
    case (catch apply(Mod, Fun, [suite])) of
	[] ->
	    io:format("~n~n*** Eval: ~p ***************~n", 
		      [{Mod, Fun}]),
	    case eval(Mod, Fun, Config) of
		{ok, _, _} ->
		    [];
		Other ->
		    [Other]
	    end;

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

        {conf, InitSuite, Cases, FinishSuite} when is_atom(InitSuite) andalso 
						   is_list(Cases) andalso 
						   is_atom(FinishSuite) ->
	    ?d("run -> conf: "
	       "~n   InitSuite:   ~p"
	       "~n   Cases:       ~p"
	       "~n   FinishSuite: ~p", [InitSuite, Cases, FinishSuite]),
	    do_suite(Mod, InitSuite, Cases, FinishSuite, Config);
                    
        {req, _, SubCases} when is_list(SubCases) ->
	    ?d("run -> req: "
	       "~n   SubCases: ~p", [SubCases]),
	    do_subcases(Mod, Fun, SubCases, Config, []);
                    
        {req, _, Conf} ->
	    ?d("run -> req: "
	       "~n   Conf: ~p", [Conf]),
	    do_subcases(Mod, Fun, [Conf], Config, []);
                    
        {'EXIT', {undef, _}} ->
            io:format("~n*** Undefined:   ~p~n", [{Mod, Fun}]),
            [{nyi, {Mod, Fun}, ok}];
                    
        Error ->
            io:format("~n*** Ignoring:   ~p: ~p~n", [{Mod, Fun}, Error]),
            [{failed, {Mod, Fun}, Error}]
    end;

run(Mod, Config) when is_atom(Mod) andalso is_list(Config) ->
    run({Mod, all}, Config);

run(Cases, Config) when is_list(Cases) andalso is_list(Config) ->
    Errors = [run(Case, Config) || Case <- Cases],
    lists:append(Errors);

run(Bad, _Config) ->
    [{badarg, Bad, ok}].


do_suite(Mod, Init, Cases, Finish, Config0) ->
    ?d("do_suite -> entry with"
       "~n   Mod:     ~p"
       "~n   Init:    ~p"
       "~n   Cases:   ~p"
       "~n   Finish:  ~p"
       "~n   Config0: ~p", [Mod, Init, Cases, Finish, Config0]),
    case (catch apply(Mod, Init, [Config0])) of
	Config when is_list(Config) ->
	    io:format("~n*** Expand: ~p ...~n", [Mod]),
	    Map = fun(Case) when is_atom(Case) -> {Mod, Case};
		     (Case) -> Case
		  end,
	    Res = run(lists:map(Map, Cases), Config),
	    (catch apply(Mod, Finish, [Config])),
	    Res;

	{'EXIT', {skipped, Reason}} ->
	    io:format(" => skipping: ~p~n", [Reason]),
	    SkippedCases = 
		[{skipped, {Mod, Case}, suite_init_skipped} || Case <- Cases],
	    lists:flatten([[{skipped, {Mod, Init}, Reason}],
			   SkippedCases, 
			   [{skipped, {Mod, Finish}, suite_init_skipped}]]);

	Error ->
	    io:format(" => init (~p) failed: ~n~p~n", [Init, Error]),
	    InitResult = 
		[{failed, {Mod, Init}, Error}],
	    SkippedCases = 
		[{skipped, {Mod, Case}, suite_init_failed} || Case <- Cases],
	    FinResult = 
		case (catch apply(Mod, Finish, [Config0])) of
		    ok ->
			[];
		    FinConfig when is_list(FinConfig) ->
			[];
		    FinError ->
			[{failed, {Mod, Finish}, FinError}]
		end,
	    lists:flatten([InitResult, SkippedCases, FinResult])

    end.


do_subcases(_Mod, _Fun, [], _Config, Acc) ->
    lists:flatten(lists:reverse(Acc));
do_subcases(Mod, Fun, [{conf, Init, Cases, Finish}|SubCases], Config, Acc) 
  when is_atom(Init) andalso is_list(Cases) andalso is_atom(Finish) ->
    R = case (catch apply(Mod, Init, [Config])) of
	    Conf when is_list(Conf) ->
		io:format("~n*** Expand: ~p ...~n", [{Mod, Fun}]),
		Map = fun(Case) when is_atom(Case) -> {Mod, Case};
			 (Case) -> Case
		      end,
		Res = run(lists:map(Map, Cases), Conf),
		(catch apply(Mod, Finish, [Conf])),
		Res;
	    
	    {'EXIT', {skipped, Reason}} ->
		io:format(" => skipping: ~p~n", [Reason]),
		[{skipped, {Mod, Fun}, Reason}];
	    
	    Error ->
		io:format(" => init (~p) failed: ~n~p~n", [Init, Error]),
		(catch apply(Mod, Finish, [Config])),
		[{failed, {Mod, Fun}, Error}]
	end,
    do_subcases(Mod, Fun, SubCases, Config, [R|Acc]);
do_subcases(Mod, Fun, [SubCase|SubCases], Config, Acc) when is_atom(SubCase) ->
    R = do_case(Mod, SubCase, Config),
    do_subcases(Mod, Fun, SubCases,Config, [R|Acc]).


do_case(M, F, C) ->
    io:format("~n~n*** Eval: ~p ***************~n", [{M, F}]),
    case eval(M, F, C) of
	{ok, _, _} ->
	    [];
	Other ->
	    [Other]
    end.


eval(Mod, Fun, Config) ->
    Flag    = process_flag(trap_exit, true),
    global:register_name(?TEST_CASE_SUP, self()),
    Config2 = Mod:init_per_testcase(Fun, Config),
    Self    = self(), 
    Eval    = fun() -> do_eval(Self, Mod, Fun, Config2) end,
    Pid     = spawn_link(Eval),
    R       = wait_for_evaluator(Pid, Mod, Fun, Config2, []),
    Mod:end_per_testcase(Fun, Config2),
    global:unregister_name(?TEST_CASE_SUP),
    process_flag(trap_exit, Flag),
    R.

wait_for_evaluator(Pid, Mod, Fun, Config, Errors) ->
    Pre = lists:concat(["TEST CASE: ", Fun]),
    receive
	{'EXIT', _Watchdog, watchdog_timeout} ->
	    io:format("*** ~s WATCHDOG TIMEOUT~n", [Pre]), 
	    exit(Pid, kill),
	    {failed, {Mod, Fun}, watchdog_timeout};
	{done, Pid, ok} when Errors =:= [] ->
	    io:format("*** ~s OK~n", [Pre]),
	    {ok, {Mod, Fun}, Errors};
	{done, Pid, {ok, _}} when Errors =:= [] ->
	    io:format("*** ~s OK~n", [Pre]),
	    {ok, {Mod, Fun}, Errors};
	{done, Pid, Fail} ->
	    io:format("*** ~s FAILED~n~p~n", [Pre, Fail]),
	    {failed, {Mod, Fun}, Fail};
	{'EXIT', Pid, {skipped, Reason}} -> 
	    io:format("*** ~s SKIPPED~n~p~n", [Pre, Reason]),
	    {skipped, {Mod, Fun}, Errors};
	{'EXIT', Pid, Reason} -> 
	    io:format("*** ~s CRASHED~n~p~n", [Pre, Reason]),
	    {crashed, {Mod, Fun}, [{'EXIT', Reason} | Errors]};
	{fail, Pid, Reason} ->
	    io:format("*** ~s FAILED~n~p~n", [Pre, Reason]),
	    wait_for_evaluator(Pid, Mod, Fun, Config, Errors ++ [Reason])
   end.

do_eval(ReplyTo, Mod, Fun, Config) ->
    case (catch apply(Mod, Fun, [Config])) of
	{'EXIT', {skipped, Reason}} ->
	    ReplyTo ! {'EXIT', self(), {skipped, Reason}};
	Other ->
	    ReplyTo ! {done, self(), Other}
    end,
    unlink(ReplyTo),
    exit(shutdown).


display_result([]) ->    
    io:format("TEST OK~n", []);

display_result(Errors) when is_list(Errors) ->
    Nyi     = [MF || {nyi, MF, _} <- Errors],
    Skipped = [{MF, Reason} || {skipped, MF, Reason} <- Errors],
    Crashed = [{MF, Reason} || {crashed, MF, Reason} <- Errors],
    Failed  = [{MF, Reason} || {failed,  MF, Reason} <- Errors],
    display_skipped(Skipped),
    display_crashed(Crashed),
    display_failed(Failed),
    display_summery(Nyi, Skipped, Crashed, Failed).

display_summery(Nyi, Skipped, Crashed, Failed) ->
    io:format("~nTest case summery:~n", []),
    display_summery(Nyi, "not yet implemented"),
    display_summery(Skipped, "skipped"),
    display_summery(Crashed, "crashed"),
    display_summery(Failed, "failed"),
    io:format("~n", []).
   
display_summery([], _) ->
    ok;
display_summery(Res, Info) ->
    io:format("  ~w test cases ~s~n", [length(Res), Info]).
    
display_skipped([]) ->
    io:format("Skipped test cases: -~n", []);
display_skipped(Skipped) ->
    io:format("Skipped test cases:~n", []),
    [io:format("  ~p => ~p~n", [MF, Reason]) || {MF, Reason} <- Skipped],
    io:format("~n", []).
    
display_crashed([]) ->
    io:format("Crashed test cases: -~n", []);
display_crashed(Crashed) ->
    io:format("Crashed test cases:~n", []),
    [io:format("  ~p => ~p~n", [MF, Reason]) || {MF, Reason} <- Crashed],
    io:format("~n", []).
    
display_failed([]) ->
    io:format("Failed test cases: -~n", []);
display_failed(Failed) ->
    io:format("Failed test cases:~n", []),
    [io:format("  ~p => ~p~n", [MF, Reason]) || {MF, Reason} <- Failed],
    io:format("~n", []).
        
    
  Verify that the actual result of a test case matches the exected one

error(Actual, Mod, Line) ->
    global:send(?GLOBAL_LOGGER, {failed, Mod, Line}),
    log("<ERROR> Bad result: ~p~n", [Actual], Mod, Line),
    case global:whereis_name(?TEST_CASE_SUP) of
	undefined -> 
	    ignore;
	Pid -> 
	    Pid ! {fail, self(), {Actual, Mod, Line}}
    end,
    Actual.

skip(Actual, Mod, Line) ->
    log("Skipping test case~n", [], Mod, Line),
    exit({skipped, {Actual, Mod, Line}}).

fatal_skip(Actual, Mod, Line) ->
    error(Actual, Mod, Line),
    exit(shutdown).


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



init_per_testcase(_Case, Config) ->
    global:register_name(?GLOBAL_LOGGER, group_leader()),
    Config.

end_per_testcase(_Case, _Config) ->
    global:unregister_name(?GLOBAL_LOGGER),
    ok.



default_config(Mod) ->
    PrivDir0 = ?snmp_priv_dir,
    case filename:pathtype(PrivDir0) of
	absolute ->
	    ok;
	_ ->
	    case file:make_dir(Mod) of
		ok ->
		    ok;
		{error, eexist} ->
		    ok
	    end,
	    PrivDir = filename:join(Mod, PrivDir0), 
	    case file:make_dir(PrivDir) of
		ok ->
		    ok;
		{error, eexist} ->
		    ok;
		Error ->
		    ?FAIL({failed_creating_subsuite_top_dir, Error})
	    end,
	    [{priv_dir, PrivDir}]
    end.



d(F, A, L) ->
    d(true, F, A, L).
  d(get(dbg ) , F , A , L ) .

d(true, F, A, L) ->
    io:format("STS[~w] ~p " ++ F ++ "~n", [L,self()|A]);
d(_, _, _, _) ->
    ok.

timestamp() ->
    {Date, Time}     = calendar:now_to_datetime( erlang:timestamp() ),
    {YYYY, MM, DD}   = Date,
    {Hour, Min, Sec} = Time,
    FormatDate =
        io_lib:format("~.4w-~.2.0w-~.2.0w_~.2.0w.~.2.0w.~.2.0w",
                      [YYYY,MM,DD,Hour,Min,Sec]),
    lists:flatten(FormatDate).
    
