%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2018-2019. 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 = erlang:spawn_link( fun() -> init(Name, Seq, InitState2) end), %% MRef = erlang:monitor(process, Pid), #ev{name = Name, pid = Pid}%, mref = MRef} end. init(Name, Seq, Init) -> put(sname, Name), process_flag(trap_exit, true), 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} -> ?SEV_IPRINT("command ~w skip: " "~n ~p", [ID, Reason]), exit({skip, Reason}); {error, Reason} -> ?SEV_EPRINT("command ~w failed: " "~n ~p", [ID, Reason]), exit({command_failed, ID, Reason, State}) catch C:{skip, command} = E:_ when ((C =:= throw) orelse (C =:= exit)) -> %% Secondary skip exit(E); C:{skip, R} = E:_ when ((C =:= throw) orelse (C =:= exit)) -> ?SEV_IPRINT("command ~w skip catched(~w): " "~n Reason: ~p", [ID, C, R]), exit(E); C:E:S -> ?SEV_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([], _OK, Fails) -> ?SEV_EPRINT("Fails: " "~n ~p", [Fails]), Fails; await_finish(Evs, OK, Fails) -> receive %% Successfull termination of evaluator {'DOWN', _MRef, process, Pid, normal} -> {Evs2, OK2, Fails2} = await_finish_normal(Pid, Evs, OK, Fails), await_finish(Evs2, OK2, Fails2); {'EXIT', Pid, normal} -> {Evs2, OK2, Fails2} = await_finish_normal(Pid, Evs, OK, Fails), await_finish(Evs2, OK2, Fails2); %% The evaluator can skip the test case: {'DOWN', _MRef, process, Pid, {skip, Reason}} -> %% ?SEV_IPRINT("await_finish -> skip (down) received: " %% "~n Pid: ~p" %% "~n Reason: ~p", [Pid, Reason]), await_finish_skip(Pid, Reason, Evs, OK); {'EXIT', Pid, {skip, Reason}} -> %% ?SEV_IPRINT("await_finish -> skip (exit) received: " %% "~n Pid: ~p" %% "~n Reason: ~p", [Pid, Reason]), await_finish_skip(Pid, Reason, Evs, OK); %% Evaluator failed {'DOWN', _MRef, process, Pid, Reason} -> %% ?SEV_IPRINT("await_finish -> fail (down) received: " %% "~n Pid: ~p" %% "~n Reason: ~p", [Pid, Reason]), {Evs2, OK2, Fails2} = await_finish_fail(Pid, Reason, Evs, OK, Fails), await_finish(Evs2, OK2, Fails2); {'EXIT', Pid, Reason} -> %% ?SEV_IPRINT("await_finish -> fail (exit) received: " %% "~n Pid: ~p" %% "~n Reason: ~p", [Pid, Reason]), {Evs2, OK2, Fails2} = await_finish_fail(Pid, Reason, Evs, OK, Fails), await_finish(Evs2, OK2, Fails2) end. await_finish_normal(Pid, Evs, OK, Fails) -> 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), {NewEvs, [Pid|OK], Fails}; false -> case lists:member(Pid, OK) of true -> ok; false -> iprint("unknown process ~p died (normal)", [Pid]), ok end, {Evs, OK, Fails} end. await_finish_skip(Pid, Reason, Evs, OK) -> Evs2 = case lists:keysearch(Pid, #ev.pid, Evs) of {value, #ev{name = Name}} -> ?SEV_IPRINT("evaluator '~s' (~p) issued SKIP: " "~n ~p", [Name, Pid, Reason]), lists:keydelete(Pid, #ev.pid, Evs); false -> case lists:member(Pid, OK) of true -> ?SEV_IPRINT("already terminated (ok) process ~p skip" "~n ~p", [Pid]), ok; false -> ?SEV_IPRINT("unknown process ~p issued SKIP: " "~n ~p", [Pid, Reason]), iprint("unknown process ~p issued SKIP: " "~n ~p", [Pid, Reason]) end, Evs end, await_evs_terminated(Evs2), ?LIB:skip(Reason). await_evs_terminated(Evs) -> Instructions = [ %% Just wait for the evaluators to die on their own {fun() -> ?SEV_IPRINT("await (no action) evs termination") end, fun(_) -> ok end}, %% Send them a skip message, causing the evaluators to %% die with a skip reason. {fun() -> ?SEV_IPRINT("await (send skip message) evs termination") end, fun(#ev{pid = Pid}) -> Pid ! skip end}, %% And if nothing else works, try to kill the remaining evaluators {fun() -> ?SEV_IPRINT("await (issue exit kill) evs termination") end, fun(#ev{pid = Pid}) -> exit(Pid, kill) end}], await_evs_terminated(Evs, Instructions). await_evs_terminated([], _) -> ok; await_evs_terminated(Evs, []) -> {error, {failed_terminated, [P||#ev{pid=P} <- Evs]}}; await_evs_terminated(Evs, [{Inform, Command}|Instructions]) -> Inform(), lists:foreach(Command, Evs), RemEvs = await_evs_termination(Evs), await_evs_terminated(RemEvs, Instructions). await_evs_termination(Evs) -> await_evs_termination(Evs, 2000). await_evs_termination([], _Timeout) -> []; await_evs_termination(Evs, Timeout) -> T = t(), receive {'DOWN', _MRef, process, Pid, _Reason} -> %% ?SEV_IPRINT("await_evs_termination -> DOWN: " %% "~n Pid: ~p" %% "~n Reason: ~p", [Pid, Reason]), Evs2 = lists:keydelete(Pid, #ev.pid, Evs), await_evs_termination(Evs2, tdiff(T, t())); {'EXIT', Pid, _Reason} -> %% ?SEV_IPRINT("await_evs_termination -> EXIT: " %% "~n Pid: ~p" %% "~n Reason: ~p", [Pid, Reason]), Evs2 = lists:keydelete(Pid, #ev.pid, Evs), await_evs_termination(Evs2, tdiff(T, t())) after Timeout -> Evs end. await_finish_fail(Pid, Reason, Evs, OK, Fails) -> 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), {NewEvs, OK, [{Pid, Reason}|Fails]}; false -> case lists:member(Pid, OK) of true -> ok; false -> iprint("unknown process ~p died: " "~n ~p", [Pid, Reason]) end, {Evs, OK, Fails} 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_reason, 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 skip -> %% This means that another evaluator has issued a skip, %% and we have been instructed to terminate as a result. ?LIB:skip(command); {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, Reason}}; {'DOWN', _, process, OtherPid, Reason} -> case check_down(OtherPid, Reason, OtherPids) of ok -> iprint("DOWN from unknown process ~p: " "~n ~p" "~n when" "~n OtherPids: " "~n ~p", [OtherPid, Reason, OtherPids]), 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, DownReason}}; false -> ok end. %% ============================================================================ f(F, A) -> lists:flatten(io_lib:format(F, A)). iprint(F, A) -> print("", F, A). eprint(F, A) -> print(" ", 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]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% t() -> os:timestamp(). tdiff({A1, B1, C1} = _T1x, {A2, B2, C2} = _T2x) -> T1 = A1*1000000000+B1*1000+(C1 div 1000), T2 = A2*1000000000+B2*1000+(C2 div 1000), T2 - T1.