%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2018-2018. 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%
%%
-module(socket_test_evaluator).
%% Evaluator control functions
-export([
start/3,
await_finish/1
]).
%% Functions used by evaluators to interact with eachother
-export([
%% Announce functions
%% (Send an announcement from one evaluator to another)
announce_start/1, announce_start/2,
announce_continue/2, announce_continue/3,
announce_ready/2, announce_ready/3,
announce_terminate/1,
%% Await functions
%% (Wait for an announcement from another evaluator)
await_start/0, await_start/1,
await_continue/3, await_continue/4,
await_ready/3, await_ready/4,
await_terminate/2, await_terminate/3,
await_termination/1, await_termination/2
]).
%% Utility functions
-export([
iprint/2, % Info printouts
eprint/2 % Error printouts
]).
-export_type([
ev/0,
initial_evaluator_state/0,
evaluator_state/0,
command_fun/0,
command/0
]).
-include("socket_test_evaluator.hrl").
-type ev() :: #ev{}.
-type initial_evaluator_state() :: map().
-type evaluator_state() :: term().
-type command_fun() ::
fun((State :: evaluator_state()) -> ok) |
fun((State :: evaluator_state()) -> {ok, evaluator_state()}) |
fun((State :: evaluator_state()) -> {error, term()}).
-type command() :: #{desc := string(),
cmd := command_fun()}.
%% ============================================================================
-define(LIB, socket_test_lib).
-define(LOGGER, socket_test_logger).
-define(EXTRA_NOTHING, '$nothing').
-define(ANNOUNCEMENT_START, '$start').
-define(ANNOUNCEMENT_READY, '$ready').
-define(ANNOUNCEMENT_CONTINUE, '$continue').
-define(ANNOUNCEMENT_TERMINATE, '$terminate').
-define(START_NAME_NONE, '$no-name').
-define(START_SLOGAN, ?ANNOUNCEMENT_START).
-define(TERMINATE_SLOGAN, ?ANNOUNCEMENT_TERMINATE).
%% ============================================================================
-spec start(Name, Seq, Init) -> ev() when
Name :: string(),
Seq :: [command()],
Init :: initial_evaluator_state().
start(Name, Seq, InitState)
when is_list(Name) andalso is_list(Seq) andalso (Seq =/= []) ->
%% Make sure 'parent' is not already used
case maps:find(parent, InitState) of
{ok, _} ->
erlang:error({already_used, parent});
error ->
InitState2 = InitState#{parent => self()},
{Pid, MRef} = erlang:spawn_monitor(
fun() -> init(Name, Seq, InitState2) end),
#ev{name = Name, pid = Pid, mref = MRef}
end.
init(Name, Seq, Init) ->
put(sname, Name),
loop(1, Seq, Init).
loop(_ID, [], FinalState) ->
exit(FinalState);
loop(ID, [#{desc := Desc,
cmd := Cmd}|Cmds], State) when is_function(Cmd, 1) ->
iprint("evaluate command ~2w: ~s", [ID, Desc]),
try Cmd(State) of
ok ->
loop(ID + 1, Cmds, State);
{ok, NewState} ->
loop(ID + 1, Cmds, NewState);
{skip, Reason} ->
exit({skip, Reason});
{error, Reason} ->
eprint("command ~w failed: "
"~n Reason: ~p", [ID, Reason]),
exit({command_failed, ID, Reason, State})
catch
throw:{skip, R} = E:_ ->
eprint("command ~w skip: "
"~n Skip Reason: ~p", [ID, R]),
exit(E);
C:E:S ->
eprint("command ~w crashed: "
"~n Class: ~p"
"~n Error: ~p"
"~n Call Stack: ~p", [ID, C, E, S]),
exit({command_crashed, ID, {C,E,S}, State})
end.
%% ============================================================================
-spec await_finish(Evs) -> term() when
Evs :: [ev()].
await_finish(Evs) ->
await_finish(Evs, []).
await_finish([], []) ->
ok;
await_finish([], Fails) ->
?SEV_EPRINT("Fails: "
"~n ~p", [Fails]),
Fails;
await_finish(Evs, Fails) ->
receive
%% Successfull termination of evaluator
{'DOWN', _MRef, process, Pid, normal} ->
case lists:keysearch(Pid, #ev.pid, Evs) of
{value, #ev{name = Name}} ->
iprint("evaluator '~s' (~p) success", [Name, Pid]),
NewEvs = lists:keydelete(Pid, #ev.pid, Evs),
await_finish(NewEvs, Fails);
false ->
iprint("unknown process ~p died (normal)", [Pid]),
await_finish(Evs, Fails)
end;
%% The evaluator can skip the teat case:
{'DOWN', _MRef, process, Pid, {skip, Reason}} ->
case lists:keysearch(Pid, #ev.pid, Evs) of
{value, #ev{name = Name}} ->
iprint("evaluator '~s' (~p) issued SKIP: "
"~n ~p", [Name, Pid, Reason]);
false ->
iprint("unknown process ~p issued SKIP: "
"~n ~p", [Pid, Reason])
end,
?LIB:skip(Reason);
%% Evaluator failed
{'DOWN', _MRef, process, Pid, Reason} ->
case lists:keysearch(Pid, #ev.pid, Evs) of
{value, #ev{name = Name}} ->
iprint("evaluator '~s' (~p) failed", [Name, Pid]),
NewEvs = lists:keydelete(Pid, #ev.pid, Evs),
await_finish(NewEvs, [{Pid, Reason}|Fails]);
false ->
iprint("unknown process ~p died: "
"~n ~p", [Pid, Reason]),
await_finish(Evs, Fails)
end
end.
%% ============================================================================
-spec announce_start(To) -> ok when
To :: pid().
announce_start(To) ->
announce(To, ?ANNOUNCEMENT_START, ?START_SLOGAN).
-spec announce_start(To, Extra) -> ok when
To :: pid(),
Extra :: term().
announce_start(To, Extra) ->
announce(To, ?ANNOUNCEMENT_START, ?START_SLOGAN, Extra).
%% ============================================================================
-spec announce_continue(To, Slogan) -> ok when
To :: pid(),
Slogan :: atom().
announce_continue(To, Slogan) ->
announce_continue(To, Slogan, ?EXTRA_NOTHING).
-spec announce_continue(To, Slogan, Extra) -> ok when
To :: pid(),
Slogan :: atom(),
Extra :: term().
announce_continue(To, Slogan, Extra) ->
announce(To, ?ANNOUNCEMENT_CONTINUE, Slogan, Extra).
%% ============================================================================
-spec announce_ready(To, Slogan) -> ok when
To :: pid(),
Slogan :: atom().
announce_ready(To, Slogan) ->
announce_ready(To, Slogan, ?EXTRA_NOTHING).
-spec announce_ready(To, Slogan, Extra) -> ok when
To :: pid(),
Slogan :: atom(),
Extra :: term().
announce_ready(To, Slogan, Extra) ->
announce(To, ?ANNOUNCEMENT_READY, Slogan, Extra).
%% ============================================================================
-spec announce_terminate(To) -> ok when
To :: pid().
announce_terminate(To) ->
announce(To, ?ANNOUNCEMENT_TERMINATE, ?TERMINATE_SLOGAN).
%% ============================================================================
-spec announce(To, Announcement, Slogan) -> ok when
To :: pid(),
Announcement :: atom(),
Slogan :: atom().
announce(To, Announcement, Slogan) ->
announce(To, Announcement, Slogan, ?EXTRA_NOTHING).
-spec announce(To, Announcement, Slogan, Extra) -> ok when
To :: pid(),
Announcement :: atom(),
Slogan :: atom(),
Extra :: term().
announce(To, Announcement, Slogan, Extra)
when is_pid(To) andalso
is_atom(Announcement) andalso
is_atom(Slogan) ->
%% iprint("announce -> entry with: "
%% "~n To: ~p"
%% "~n Announcement: ~p"
%% "~n Slogan: ~p"
%% "~n Extra: ~p",
%% [To, Announcement, Slogan, Extra]),
To ! {Announcement, self(), Slogan, Extra},
ok.
%% ============================================================================
-spec await_start() -> Pid | {Pid, Extra} when
Pid :: pid(),
Extra :: term().
await_start() ->
await_start(any).
-spec await_start(Pid) -> Pid | {Pid, Extra} when
Pid :: pid(),
Extra :: term().
await_start(P) when is_pid(P) orelse (P =:= any) ->
case await(P, ?START_NAME_NONE, ?ANNOUNCEMENT_START, ?START_SLOGAN, []) of
{ok, Any} when is_pid(P) ->
Any;
{ok, Pid} when is_pid(Pid) andalso (P =:= any) ->
Pid;
{ok, {Pid, _} = OK} when is_pid(Pid) andalso (P =:= any) ->
OK
end.
%% ============================================================================
-spec await_continue(From, Name, Slogan) -> ok | {ok, Extra} | {error, Reason} when
From :: pid(),
Name :: atom(),
Slogan :: atom(),
Extra :: term(),
Reason :: term().
await_continue(From, Name, Slogan) ->
await_continue(From, Name, Slogan, []).
-spec await_continue(From, Name, Slogan, OtherPids) ->
ok | {ok, Extra} | {error, Reason} when
From :: pid(),
Name :: atom(),
Slogan :: atom(),
OtherPids :: [{pid(), atom()}],
Extra :: term(),
Reason :: term().
await_continue(From, Name, Slogan, OtherPids)
when is_pid(From) andalso
is_atom(Name) andalso
is_atom(Slogan) andalso
is_list(OtherPids) ->
await(From, Name, ?ANNOUNCEMENT_CONTINUE, Slogan, OtherPids).
%% ============================================================================
-spec await_ready(From, Name, Slogan) -> ok | {ok, Extra} | {error, Reason} when
From :: pid(),
Name :: atom(),
Slogan :: atom(),
Extra :: term(),
Reason :: term().
await_ready(From, Name, Slogan) ->
await_ready(From, Name, Slogan, []).
-spec await_ready(From, Name, Slogan, OtherPids) ->
ok | {ok, Extra} | {error, Reason} when
From :: pid(),
Name :: atom(),
Slogan :: atom(),
OtherPids :: [{pid(), atom()}],
Extra :: term(),
Reason :: term().
await_ready(From, Name, Slogan, OtherPids)
when is_pid(From) andalso
is_atom(Name) andalso
is_atom(Slogan) andalso
is_list(OtherPids) ->
await(From, Name, ?ANNOUNCEMENT_READY, Slogan, OtherPids).
%% ============================================================================
-spec await_terminate(Pid, Name) -> ok | {error, Reason} when
Pid :: pid(),
Name :: atom(),
Reason :: term().
await_terminate(Pid, Name) when is_pid(Pid) andalso is_atom(Name) ->
await_terminate(Pid, Name, []).
-spec await_terminate(Pid, Name, OtherPids) -> ok | {error, Reason} when
Pid :: pid(),
Name :: atom(),
OtherPids :: [{pid(), atom()}],
Reason :: term().
await_terminate(Pid, Name, OtherPids) ->
await(Pid, Name, ?ANNOUNCEMENT_TERMINATE, ?TERMINATE_SLOGAN, OtherPids).
%% ============================================================================
-spec await_termination(Pid) -> ok | {error, Reason} when
Pid :: pid(),
Reason :: term().
await_termination(Pid) when is_pid(Pid) ->
await_termination(Pid, any).
-spec await_termination(Pid, ExpReason) -> ok | {error, Reason} when
Pid :: pid(),
ExpReason :: term(),
Reason :: term().
await_termination(Pid, ExpReason) ->
receive
{'DOWN', _, process, Pid, _} when (ExpReason =:= any) ->
ok;
{'DOWN', _, process, Pid, Reason} when (ExpReason =:= Reason) ->
ok;
{'DOWN', _, process, Pid, Reason} ->
{error, {unexpected_exit, ExpReason, Reason}}
end.
%% ============================================================================
%% We expect a message (announcement) from Pid, but we also watch for DOWN from
%% both Pid and OtherPids, in which case the test has failed!
-spec await(ExpPid, Name, Announcement, Slogan, OtherPids) ->
ok | {ok, Extra} | {error, Reason} when
ExpPid :: any | pid(),
Name :: atom(),
Announcement :: atom(),
Slogan :: atom(),
OtherPids :: [{pid(), atom()}],
Extra :: term(),
Reason :: term().
await(ExpPid, Name, Announcement, Slogan, OtherPids)
when (is_pid(ExpPid) orelse (ExpPid =:= any)) andalso
is_atom(Name) andalso
is_atom(Announcement) andalso
is_atom(Slogan) andalso
is_list(OtherPids) ->
receive
{Announcement, Pid, Slogan, ?EXTRA_NOTHING} when (ExpPid =:= any) ->
{ok, Pid};
{Announcement, Pid, Slogan, Extra} when (ExpPid =:= any) ->
{ok, {Pid, Extra}};
{Announcement, Pid, Slogan, ?EXTRA_NOTHING} when (Pid =:= ExpPid) ->
ok;
{Announcement, Pid, Slogan, Extra} when (Pid =:= ExpPid) ->
{ok, Extra};
{'DOWN', _, process, Pid, {skip, SkipReason}} when (Pid =:= ExpPid) ->
iprint("Unexpected SKIP from ~w (~p): "
"~n ~p", [Name, Pid, SkipReason]),
?LIB:skip({Name, SkipReason});
{'DOWN', _, process, Pid, Reason} when (Pid =:= ExpPid) ->
eprint("Unexpected DOWN from ~w (~p): "
"~n ~p", [Name, Pid, Reason]),
{error, {unexpected_exit, Name}};
{'DOWN', _, process, OtherPid, Reason} ->
case check_down(OtherPid, Reason, OtherPids) of
ok ->
iprint("DOWN from unknown process ~p: "
"~n ~p", [OtherPid, Reason]),
await(ExpPid, Name, Announcement, Slogan, OtherPids);
{error, _} = ERROR ->
ERROR
end
after infinity -> % For easy debugging, just change to some valid time (5000)
iprint("await -> timeout for msg from ~p (~w): "
"~n Announcement: ~p"
"~n Slogan: ~p"
"~nwhen"
"~n Messages: ~p",
[ExpPid, Name, Announcement, Slogan, pi(messages)]),
await(ExpPid, Name, Announcement, Slogan, OtherPids)
end.
pi(Item) ->
pi(self(), Item).
pi(Pid, Item) ->
{Item, Info} = process_info(Pid, Item),
Info.
check_down(Pid, DownReason, Pids) ->
case lists:keymember(Pid, 1, Pids) of
{value, {_, Name}} ->
eprint("Unexpected DOWN from ~w (~p): "
"~n ~p", [Name, Pid, DownReason]),
{error, {unexpected_exit, Name}};
false ->
ok
end.
%% ============================================================================
f(F, A) ->
lists:flatten(io_lib:format(F, A)).
iprint(F, A) ->
print("", F, A).
eprint(F, A) ->
print("<ERROR> ", F, A).
print(Prefix, F, A) ->
%% The two prints is to get the output both in the shell (for when
%% "personal" testing is going on) and in the logs.
IDStr =
case get(sname) of
undefined ->
%% This means its not an evaluator,
%% or a named process. Instead its
%% most likely the test case itself,
%% so skip the name and the pid.
"";
SName ->
f("[~s][~p]", [SName, self()])
end,
?LOGGER:format("[~s]~s ~s" ++ F,
[?LIB:formated_timestamp(), IDStr, Prefix | A]).