%%%-----------------------------------------------------------------------------
%%% @author Sven Heyll This mocking library works similar to Easymock. After a mock process is started by {@link new/0} it can be programmed to
%%% expect function calls and to react to them in two ways:
This is done
%%% with {@link strict/4}, {@link strict/5}, {@link stub/4}, {@link stub/5}
Before the code under test is executed, the mock must be told %%% that the programming phase is over by {@link replay/1}.
%%% %%%In the next phase the code under test is run, and might or might not call %%% the functions mocked. The mock process checks that all functions programmed %%% with {@link strict/4}, {@link strict/5} are called in the correct order, %%% with the expected arguments and reacts in the way defined during the %%% programming phase. If a mocked function is called although another function %%% was expected, or if an expected function was called with different %%% arguments, the mock process dies and prints a comprehensive error message %%% before failing the test.
%%% %%%To support mock invokations from multiple processes the strictness %%% requirement can be reduced to calls belonging to the same group. {@link %%% new_groups/2} creates a list of named groups, where calls belongig to %%% different groups may occur in any order. A group is passed as mock reference %%% (1st parameter) to {@link strict/5} or {@link strict/4}. Use {@link %%% await/1} with a list of groups to block the caller until all groups %%% are finished, i.e. the expectations assigned to each group via {@link %%% strict/5} were invoked. NOTE: It is prohibited to use the same expectations %%% with different return values among a list groups created together.
%%% %%%At the end of a unit test {@link await_expectations/1} is called to %%% await all invocations defined during the programming phase.
%%% %%%An alternative to {@link await_expectations/1} is {@link verify/1}. It is %%% called to check for missing invocations at the end of the programming phase, %%% if any expected invocations are missing at verify will throw an %%% exception.
%%% %%%When the mock process exits it tries hard to remove all modules, that %%% were dynamically created and loaded during the programming phase.
%%% %%% NOTE: This library works by purging the modules mocked and replacing them %%% with dynamically created and compiled code, so be careful what you mock, %%% i.e. it brings chaos to mock modules from kernel. This also implies, that %%% tests that mock the same modules must be run sequentially. %%% %%% Apart from that, it is very advisable to only mock owned modules %%% anyway. %%% %%% Also, mocking modules is an operation that mutates the whole erlang %%% virtual machine (more or less), therefore it is advisable to call %%% {@link lock/0} and {@link unlock/0} around a block %%% test code that relies on an intact set of modules, which are not %%% mocked. %%% %%% Note that all module loading by `em' is made sequential through a single %%% {@link em_module_loader} server. %%% %%% @end %%%----------------------------------------------------------------------------- %%% Copyright (c) 2011-2021 Sven Heyll %%% %%% Permission is hereby granted, free of charge, to any person obtaining a copy %%% of this software and associated documentation files (the "Software"), to %%% deal in the Software without restriction, including without limitation the %%% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or %%% sell copies of the Software, and to permit persons to whom the Software is %%% furnished to do so, subject to the following conditions: %%% %%% The above copyright notice and this permission notice shall be included in %%% all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS %%% IN THE SOFTWARE. %%% %%%----------------------------------------------------------------------------- -module(em). -behaviour(gen_statem). %% public API --- -export([lock/0, unlock/0, new/0, new_groups/2, nothing/2, strict/4, strict/5, any/0, zelf/0, stub/4, stub/5, replay/1, replay/2, await/2, await_expectations/1, verify/1, call_log/1]). %% gen_statem callbacks --- -export([programming/3, replaying/3, no_expectations/3, deranged/3, callback_mode/0, init/1, terminate/3, code_change/4 ]). %% !!!NEVER CALL THIS FUNCTION!!! --- -export([invoke/4]). -export_type([group/0, group_tag/0, timeout_millis/0]). -include("em.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% important types %% %%------------------------------------------------------------------------------ %% The type that defines the argument list passed to strict() or stub(). %% Each list element is either a value that will be matched to the actual value %% of the parameter at that position, or a predicate function which will be %% applied to the actual argument. %%------------------------------------------------------------------------------ -type args() :: [ fun((any()) -> true | false) | term()]. %%------------------------------------------------------------------------------ %% The type that defines the response to a mocked function call. A response is %% either that a value is returned, or the application of a function to the %% actual arguments. %%------------------------------------------------------------------------------ -type answer() :: {function, fun(([any()]) -> any())} | {return, any()} . %%------------------------------------------------------------------------------ %% A group is a pair with a tag for a group and a mock process. %%------------------------------------------------------------------------------ -type group_tag() :: {term(), reference()}. -type group() :: {group, pid(), group_tag()}. %%------------------------------------------------------------------------------ %% Timout for {@link replay/2} %%------------------------------------------------------------------------------ -type timeout_millis() :: non_neg_integer() | infinity. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% API %% %% When tests spawn processes, the chance arises that modules get mocked mocked %% while other processes use them. To prevent this, the test execution must be %% serialized with regard to module mocking. Use this function to take a global %% lock (also taken by {@link em:new/0}) to prevent any mocking activity until %% {@link em:unlock/0} is called. NOTE: calling {@link em:new/0} will %% automatically take the global lock, and this function doesn't need to be %% called, although precautions have been made to such that {@link em:new/0} %% will not take the lock when the calling process already owns it. The %% calling process is monitored and the lock is released when the process %% exists. @see unlock/0 @end %% ------------------------------------------------------------------------------ -spec lock() -> ok. lock() -> em_module_loader:start(), ok = em_module_loader:disable_module_loading(). %%------------------------------------------------------------------------------ %% @doc %% Release lock obtained by `lock()'. %% @end %% ------------------------------------------------------------------------------ -spec unlock() -> ok. unlock() -> em_module_loader:start(), ok = em_module_loader:enable_module_loading(). %%------------------------------------------------------------------------------ %% @doc %% Spawn a linked mock process and return the pid.This is usually the %% first thing to do in each unit test. The resulting pid is used in the other %% functions below.
NOTE: only a single mock proccess is required for a %% single unit test case. One mock process can mock an arbitrary number of %% different modules.
When the mock process dies, all uploaded modules %% are purged from the code server, and all cover compiled modules are %% restored.
When the process that started the mock exits, the mock %% automatically cleans up and exits.
After new() the mock is in %% 'programming' state.
%% @end %%------------------------------------------------------------------------------ -spec new() -> group(). new() -> {ok, M} = gen_statem:start_link(?MODULE, {erlang:self()}, []), RootTag = {root, make_ref()}, {group, M, RootTag}. %%------------------------------------------------------------------------------ %% @private %%------------------------------------------------------------------------------ callback_mode() -> state_functions. %%------------------------------------------------------------------------------ %% @doc %% Create a group handle to assign mock expectation to. The result can be passed %% to {@link strict/4} or {@link strict/5} and {@link await/2}. %% @end %%------------------------------------------------------------------------------ -spec new_groups(group(), [term()]) -> [group()]. new_groups({group, M, _}, GroupNames) -> GroupCluster = make_ref(), [{group, M, {GroupName, GroupCluster}} || GroupName <- GroupNames]. %%------------------------------------------------------------------------------ %% @doc %% Add an expectation during the programming phase for a specific function %% invokation. %% %%All expectations defined by 'strict' define an order in which the %% application must call the mocked functions, hence the name 'strict' as oposed %% to 'stub' (see below).
%% %%The parameters are: %%
M the mock pid, returned by {@link new/0}Mod the module of the function to mockFun the name of the function to mockArgs a list of expected arguments.
%% Each list element is either a value that will be matched to the actual value
%% of the parameter at that position, or a predicate function which will be
%% applied to the actual argument.This function returns a reference that identifies the expectation. This %% reference can be passed to {@link await/2} which blocks until the expected %% invokation happens.
%% %% The return value, that the application will get when calling the mocked
%% function in the replay phase is simply the atom ok. This
%% differentiates this function from {@link strict/5}, which allows the
%% definition of a custom response function or a custom return value.
new/0 and {@link
%% replay/1} - that is during the programming phase.
%%
%% @end
%%------------------------------------------------------------------------------
-spec strict(group(), atom(), atom(), args()) ->
reference().
strict(M, Mod, Fun, Args) ->
strict(M, Mod, Fun, Args, {return, ok}).
%%------------------------------------------------------------------------------
%% @doc
%% This function behaves like {@link strict/4}
%% and additionally accepts a return value or an answer function. That parameter
%% Answer may be:
%% {return, SomeValue} This causes the mocked function invocation to
%% return the specified value.{function, fun(([Arg1, ... , ArgN]) -> SomeValue)} This defines
%% a function to be called when the mocked invokation happens.
%% That function is applied to all captured actual arguments. For convenience these
%% are passed as a list, so the user can simply write fun(_) -> ...
%% when the actual values are not needed.
%% The function will be executed by the process that calls the mocked function, not
%% by the mock process. Hence the function may access self() and may
%% throw an exception, which will then correctly appear in the process under test,
%% allowing unit testing of exception handling.
%% Otherwise the value returned by the function is passed through as the value
%% returned from the invocation.
%% strict/4
%% @end
%%------------------------------------------------------------------------------
-spec stub(group(), atom(), atom(), args()) ->
ok.
stub(M, Mod, Fun, Args) ->
stub(M, Mod, Fun, Args, {return, ok}).
%%------------------------------------------------------------------------------
%% @doc
%% This is similar stub/4 except that it, like
%% strict/5 allows the definition of a return value
%% or an answer function.
%% @see stub/4. stub/4
%% @see strict/5. strict/5
%% @end
%%------------------------------------------------------------------------------
-spec stub(group(), atom(), atom(), args(), answer()) ->
ok.
stub({group, M, Group = {root, _}}, Mod, Fun, Args, Answer = {return, _})
when is_pid(M), is_atom(Mod), is_atom(Fun), is_list(Args) ->
ok = gen_statem:call(M, {stub, Group, Mod, Fun, Args, Answer}, infinity);
stub({group, M, Group = {root, _}}, Mod, Fun, Args, Answer = {function, _})
when is_pid(M), is_atom(Mod), is_atom(Fun), is_list(Args) ->
ok = gen_statem:call(M, {stub, Group, Mod, Fun, Args, Answer}, infinity).
%%------------------------------------------------------------------------------
%% @doc
%% This is used to express the expectation that no function of a certain module
%% is called. This will cause each function call on a module to throw an 'undef'
%% exception.
%% @end
%%------------------------------------------------------------------------------
-spec nothing(group(), atom()) ->
ok.
nothing({group, M, {root, _}}, Mod) when is_pid(M), is_atom(Mod) ->
ok = gen_statem:call(M, {nothing, Mod}, infinity).
%%------------------------------------------------------------------------------
%% @doc
%% Finishes the programming phase and switches to the replay phase where the
%% actual code under test may run and invoke the functions mocked. This may
%% be called only once, and only in the programming phase. This also loads
%% (or replaces) the modules of the functions mocked.
%% In the replay phase the code under test may call all mocked functions.
%% If the application calls a mocked function with invalid arguments, or
%% if the application calls a function not expected on a mocked module, the mock
%% process dies and - if used in a typical edoc test suite - fails the test.
%% @end
%%------------------------------------------------------------------------------
-spec replay(group()) -> ok.
replay(G) ->
replay(G, infinity).
%%------------------------------------------------------------------------------
%% @doc
%% Finishes the programming phase and switches to the replay phase, expecting
%% that invokations are recorded at least once every `InvokationTimeout' millis.
%% @see replay/1
%% @end
%%------------------------------------------------------------------------------
-spec replay(group(), timeout_millis()) -> ok.
replay({group, M, {root, _}}, InvokationTimeout) ->
ok = gen_statem:call(M, {replay, InvokationTimeout}, infinity).
%%------------------------------------------------------------------------------
%% @doc
%% Block until a specific invokation defined via {@link strict/4} during the
%% programming phase was made. The handle for the specific invokation is the %% value returned by {@link strict/4}.
The return value contains the %% parameters and the pid of the recorded invokation. This function maybe called %% anytime before or after the referenced invokation has actually %% happened.
If the handle is not valid, an error is returned.
%% @end %% ------------------------------------------------------------------------------ -spec await(group(), reference()) -> {success, InvPid :: pid(), Args :: [term()]} | {error, term()}. await({group, M, {root, _}}, Handle) -> gen_statem:call(M, {await, Handle}, infinity). %%------------------------------------------------------------------------------ %% @doc %% Retrieve a list of successfully mocked invokations, i.e. all calls that were %% accepted by the `em' process in the `replay' phase. Both strict and stub %% invokations are recorded. NOTE: The Answer might as well be a function, %% depending on the `return' argument passed to `strict' or `stub'. %% @end %%------------------------------------------------------------------------------ -spec call_log(group()) -> [{Mod :: atom(), Func :: atom(), Args :: [term()], Answer :: term()}]. call_log({group, M, {root, _}}) -> gen_statem:call(M, get_call_log, infinity). %%------------------------------------------------------------------------------ %% @doc %% Wait until all invokations defined during the programming phase were made. %% After this functions returns, the mock can be expected to exit and clean up %% all modules installed. %% @end %%------------------------------------------------------------------------------ -spec await_expectations(group()) -> ok. await_expectations({group, M, {root, _}}) -> dbgLog("Stopped/Await~n", []), case gen_statem:call(M, await_expectations, infinity) of ok -> ok; Error -> error_logger:error_msg("erlymock verification failed: ~p", [Error]), error(Error) end. %%------------------------------------------------------------------------------ %% @doc %% Finishes the replay phase. If the code under test did not cause all expected %% invokations defined by {@link strict/4} or {@link strict/5}, the %% call will fail withbadmatch with a comprehensive error message.
%% Otherwise the mock process exits normally, returning ok.
%% @end
%%------------------------------------------------------------------------------
-spec verify(group()) -> ok.
verify({group, M, {root, _}}) ->
dbgLog("Stopped/Verified~n", []),
case gen_statem:call(M, verify, infinity) of
ok ->
ok;
Error ->
error_logger:error_msg("erlymock verification failed: ~p",
[Error]),
error(Error)
end.
%%------------------------------------------------------------------------------
%% @doc
%% Utility function that can be used as a match function in an argument list
%% to match any value.
%% @end
%%------------------------------------------------------------------------------
-spec any() ->
fun((any()) ->
true).
any() ->
fun(_) ->
true
end.
%%------------------------------------------------------------------------------
%% @doc
%% Utility function that can be used as a match function in an
%% argument list to match self(), e.g. when it matches the pid of the
%% process, that calls the funtion during the replay phase.
%% @end
%%------------------------------------------------------------------------------
-spec zelf() ->
atom().
zelf() ->
'$$em zelf$$'.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%% internal state
%%
-record(expectation,
{id :: reference(),
g :: group_tag(),
m :: atom(),
f :: atom(),
a :: args(),
answer :: answer(),
listeners = [] :: [GenFsmFrom :: term()]}).
-record(strict_log,
{grpt :: group_tag(),
eref :: reference(),
ipid :: pid(),
args :: [term()]}).
-record(state,
{test_proc :: pid(),
inv_to :: timeout_millis() | infinity,
strict :: [#expectation{}],
strict_log :: [#strict_log{}],
stub :: [#expectation{}],
call_log :: [{Mod :: atom(),
Func :: atom(),
Args :: [term()],
Answer :: term()}],
blacklist :: [atom()],
on_finished :: term(), % GenFsmFrom
error = no_error :: no_error | term()
}).
-type statedata() :: #state{}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%% gen_statem callbacks
%%
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec init({TestProc :: term()}) ->
{ok, atom(), StateData :: statedata()}.
init({TestProc}) ->
ok = em_module_loader:start(),
process_flag(sensitive, true),
erlang:trace(self(), false, [all]),
{ok,
programming,
#state{
test_proc = TestProc,
inv_to = infinity,
strict = [],
strict_log = [],
stub = [],
call_log =[],
blacklist = []}}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec programming(gen_statem:event_type(), EventContent :: term(), statedata()) ->
gen_statem:event_handler_result(no_expectations|replaying).
programming({call, From},
{strict, Group, Mod, Fun, Args, Answer},
State = #state{strict = Strict}) ->
InvRef = make_ref(),
{keep_state,
State#state{
strict = [#expectation{id = InvRef,
g = Group,
m = Mod,
f = Fun,
a = Args,
answer = Answer,
listeners = []}
|Strict]},
{reply, From, InvRef}};
programming({call, From},
{stub, Group, Mod, Fun, Args, Answer},
State = #state{stub = Stub}) ->
InvRef = make_ref(),
{keep_state,
State#state{
stub = [#expectation{id = InvRef,
g = Group,
m = Mod,
f = Fun,
a = Args,
answer = Answer,
listeners = []}
|Stub]},
{reply, From, ok}};
programming({call, From},
{nothing, Mod},
State = #state{blacklist = BL}) ->
{keep_state,
State#state{
blacklist = [Mod | BL]},
{reply, From, ok}};
programming({call, From},
{replay, InvTo},
State) ->
NewState = prepare_strict_invocations(
set_invokation_timeout(InvTo, State)),
load_mock_modules(NewState),
NextStateName = case NewState#state.strict of
[] -> no_expectations;
_ -> replaying
end,
{next_state,
NextStateName,
NewState,
[{reply, From, ok}
|[start_invokation_timer(NewState)||NextStateName == replaying]]};
programming({call, From}, get_call_log, State) ->
{keep_state_and_data,
{reply, From, lists:reverse(State#state.call_log)}};
programming({call, From}, Event, _State) ->
{keep_state_and_data,
{reply, From, {error, {bad_request, programming, Event}}}}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec replaying(gen_statem:event_type(), EventContent :: term(), statedata()) ->
gen_statem:event_handler_result(no_expectations|deranged).
replaying({timeout, invokation_timeout},
invokation_timeout,
State = #state{ strict = Expectations}) ->
{stop, {invokation_timeout, {missing_invokations, Expectations}}, State};
replaying({call, From},
Inv = {invokation, _M, _F, _A, _IPid},
St) ->
case
find_matching_expectation(
Inv,
[],
get_next_expectations(St))
of
{ok, E = #expectation{}} ->
stop_or_continue_replay(
answer_invokation(Inv, E, From),
remove_expectation(
E,
log_invokation(Inv, E, St)));
{error, Error} ->
enter_deranged([{reply, From, {'$em_error', Error}}], Error, St)
end;
replaying({call, From}, verify, State) ->
Reason = {invokations_missing, State#state.strict},
dbgLog("Stopping : ~w~n", [Reason]),
{stop_and_reply, normal, {reply, From, Reason}, State};
replaying({call, From}, {await, H}, State) ->
{NewState, ReplyActions} = add_invokation_listener(From, H, State),
{keep_state, NewState, ReplyActions};
replaying({call, From},
await_expectations,
State = #state{on_finished = undefined}) ->
{keep_state, State#state{ on_finished = From }};
replaying({call, From}, get_call_log, State) ->
{keep_state_and_data,
{reply, From, lists:reverse(State#state.call_log)}};
replaying({call, From}, Event, _) ->
{keep_state_and_data, {reply, From, {error, {bad_request, replaying, Event}}}}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec no_expectations(gen_statem:event_type(),
EventData :: term(),
statedata()) ->
gen_statem:event_handler_result(deranged).
no_expectations({call, From},
{invokation, Mod, Fun, Args, IPid},
State = #state{call_log = CallLog}) ->
Stubs = State#state.stub,
MatchingStubs = [Stub
|| Stub = #expectation {m = M, f = F, a = A} <- Stubs,
M == Mod,
F == Fun,
length(Args) == length(A),
check_args(Args, A, IPid)],
case MatchingStubs of
[#expectation{answer = Answer}|_] ->
{keep_state,
State#state{call_log = [{Mod, Fun, Args, Answer}|CallLog]},
[{reply, From, Answer}]};
_ ->
Error = {unexpected_invokation, {invokation, Mod, Fun, Args, IPid}},
enter_deranged([{reply, From, {'$em_error', Error}}], Error, State)
end;
no_expectations({call, From}, verify, State) ->
dbgLog("[~w] Stopping : ~w~n", [self(), [no_expectations, verify]]),
Res = em_module_loader:restore_modules(),
{stop_and_reply, normal, {reply, From, Res}, State};
no_expectations({call, From}, await_expectations, State) ->
dbgLog("[~w] Stopping : ~w~n", [self(), [no_expectations, await_expectations]]),
Res = em_module_loader:restore_modules(),
{stop_and_reply, normal, {reply, From, Res}, State};
no_expectations({call, From}, {await, H}, State) ->
{NewState, ReplyActions} = add_invokation_listener(From, H, State),
{keep_state, NewState, ReplyActions};
no_expectations({call, From}, get_call_log, State) ->
{keep_state_and_data,
{reply, From, lists:reverse(State#state.call_log)}};
no_expectations({call, From}, Event, _) ->
{keep_state_and_data,
{reply, From, {error, {bad_request, no_expectations, Event}}}}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec deranged(gen_statem:event_type(),
EventData :: term(),
statedata()) ->
gen_statem:state_callback_result(gen_statem:reply_action()).
deranged({call, From}, verify, State = #state{ error = Error }) ->
dbgLog("Stopping : ~w~n", [[deranged, verify]]),
{stop_and_reply, normal, {reply, From, Error}, State};
deranged({call, From}, await_expectations, State = #state{ error = Error }) ->
dbgLog("Stopping : ~w~n", [[deranged, await_expectations]]),
{stop_and_reply, normal, {reply, From, Error}, State};
deranged({call, From}, {await, _}, _) ->
{keep_state_and_data, {reply, From, {error, mock_deranged}}};
deranged({call, From}, {invokation, _M, _F, _A, _IPid}, _) ->
{keep_state_and_data, {reply, From, {'$em_error', mock_deranged}}};
deranged({call, From}, get_call_log, State) ->
{keep_state_and_data,
{reply, From, lists:reverse(State#state.call_log)}};
deranged({call, From}, Event, _) ->
{keep_state_and_data,
{reply, From, {error, {bad_request, deranged, Event}}}}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec terminate(Reason :: term(), StateName :: atom(),
StateData :: statedata()) -> no_return().
terminate(_Reason, _StateName, _State) -> void.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec code_change(OldVsn :: term(), StateName :: atom(), State :: statedata(),
Extra :: term()) ->
{ok, NextState :: atom(), NewStateData :: statedata()}.
code_change(_OldVsn, StateName, State, _Extra) ->
{ok, StateName, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%% api for generated mock code
%%
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec invoke(M :: term(), Mod :: term(), Fun :: fun(), Args :: list()) ->
{Value :: term()}.
invoke(M, Mod, Fun, Args) ->
{current_stacktrace, Trace} = erlang:process_info(self(), current_stacktrace),
try io:format("~nEM: ~w:~w ~p", [Mod, Fun, Args]) catch _:_ -> ok end,
Rv = case gen_statem:call(M, {invokation, Mod, Fun, Args, self()}, infinity) of
{return, Value} ->
Value;
{'$em_error' , WTF} ->
(catch io:format(" *ERROR* -> ~p~nAT: ~p~n~n", [WTF, Trace])),
exit({mock_error, WTF});
{function, F} ->
F(Args)
end,
try io:format(" -> ~p~n",[Rv]) catch _:_ -> ok end,
Rv.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%% internal functions
%%
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
load_mock_modules(#state{ strict = ExpectationsStrict,
stub = ExpectationsStub}) ->
Expectations = ExpectationsStub ++ ExpectationsStrict,
ExpectationModules =
lists:usort([M || #expectation{m = M} <- Expectations]),
MockModules =
[begin
ModExpectations =
[E || E=#expectation{m = Me} <- Expectations,
M =:= Me],
code:ensure_loaded(M),
case code:which(M) of
preloaded ->
throw({'refusing to mock preloaded module', M});
non_existing -> ok;
_FNameOrCoverCompiled ->
[assert_mocked_function_exists(E) || E <- ModExpectations]
end,
compile_mock_module(M, ModExpectations)
end || M <- ExpectationModules],
case em_module_loader:load_modules(MockModules, infinity) of
ok ->
ok;
Error ->
throw({'failed to load modules', Error})
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
compile_mock_module(Mod, Expectations) ->
ModHeaderSyn = [erl_syntax:attribute(erl_syntax:atom(module),
[erl_syntax:atom(Mod)]),
erl_syntax:attribute(erl_syntax:atom(?ERLYMOCK_COMPILED),
[erl_syntax:atom(true)]),
erl_syntax:attribute(erl_syntax:atom(compile),
[erl_syntax:list(
[erl_syntax:atom(export_all)])])],
Funs = lists:usort(
[{F, length(A)} ||
#expectation{ m = M, f = F, a = A } <- Expectations,
M == Mod]),
FunFormsSyn = [mock_fun_syn(Mod, F, A) || {F, A} <- Funs],
{ok, Mod, Code} =
compile:forms([erl_syntax:revert(F)
|| F <- ModHeaderSyn ++ FunFormsSyn]),
FName = lists:flatten(io_lib:format("em_magic_~w.beam", [Mod])),
{Mod, FName, Code}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
mock_fun_syn(Mod, F, Args) ->
ArgsSyn = var_list_syn(Args),
FunSyn = erl_syntax:atom(F),
erl_syntax:function(
FunSyn,
[erl_syntax:clause(ArgsSyn,
none,
body_syn(Mod, FunSyn, ArgsSyn))]).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
var_list_syn(Args) ->
[erl_syntax:variable(list_to_atom("Arg_" ++ integer_to_list(I)))
|| I <- lists:seq(0, Args - 1)].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
body_syn(Mod, FunSyn, ArgsSyn) ->
SelfStr = pid_to_list(erlang:self()),
SelfSyn = erl_syntax:application(
erl_syntax:atom(erlang),
erl_syntax:atom(list_to_pid),
[erl_syntax:string(SelfStr)]),
[erl_syntax:application(
erl_syntax:atom(?MODULE),
erl_syntax:atom(invoke),
[SelfSyn,
erl_syntax:atom(Mod),
FunSyn,
erl_syntax:list(ArgsSyn)])].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
prepare_strict_invocations(S = #state{ strict = Strict }) ->
S#state{ strict = lists:reverse(Strict) }.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
check_args(Args, ArgSpecs, InvokationPid) ->
try
[begin
if
is_function(E) ->
case E(A) of
true ->
ok;
_ ->
throw({error, I, E, A})
end;
true ->
case E of
'$$em zelf$$' ->
if A =/= InvokationPid ->
throw({error, I, E, A});
true ->
ok
end;
A ->
ok;
_Otherwise ->
throw({error, I, E, A})
end
end
end
|| {I, A, E} <- lists:zip3(lists:seq(1, length(Args)),
Args,
ArgSpecs)]
of
_ -> true
catch
E -> E
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec add_invokation_listener(gen_statem:from(), Ref :: term(), statedata()) ->
{statedata(), [gen_statem:reply_action()]}.
add_invokation_listener(From, Ref, State = #state{strict = Strict,
strict_log = StrictSucc}) ->
%% if the invokation does not exist, check the strict_history
case lists:keyfind(Ref, #expectation.id, Strict) of
false ->
case lists:keyfind(Ref, #strict_log.eref, StrictSucc) of
false ->
{State, [{reply, From, {error, invalid_handle}}]};
#strict_log{ args = Args,
ipid = IPid
} ->
{State, [{reply, From, {success, IPid, Args}}]}
end;
E = #expectation{listeners = Ls} ->
NewE = E#expectation{listeners = [From|Ls]},
NewStrict = lists:keyreplace(Ref, 2, Strict, NewE),
{State#state{strict = NewStrict}, []}
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
assert_mocked_function_exists(#expectation{m = Mod, f = Fun, a = Args}) ->
case erlang:function_exported(Mod, module_info, 1) of
true ->
Attrs = Mod:module_info(attributes),
case lists:keyfind(?ERLYMOCK_COMPILED, 1 , Attrs) of
false ->
case erlang:function_exported(Mod, Fun, length(Args)) of
false ->
throw({'_______________em_invalid_mock_program_______________',
lists:flatten(
io_lib:format(
"erly_mock: mocked function not exported: ~w:~w/~w",
[Mod, Fun, length(Args)])),
Args});
true ->
ok
end;
_ ->
ok
end;
false ->
ok
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
get_next_expectations(#state{strict = Es, stub = StubEs}) ->
heads_by_group_tag(Es) ++ StubEs.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
heads_by_group_tag(Es) ->
lists:foldl(fun
(#expectation{g = EG}, Acc = [#expectation{g = AG}|_])
when EG =:= AG ->
Acc;
(E, Acc) ->
[E|Acc]
end,
[],
lists:keysort(#expectation.g, Es)).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
find_matching_expectation(I, Hints, []) ->
{error, {unexpected_invokation, I, Hints}};
find_matching_expectation(I = {invokation, Mod, Fun, Args, IPid},
Hints,
[E|RestEs]) ->
case E of
#expectation{m = Mod,
f = Fun,
a = EArgs}
when
length(EArgs) == length(Args) ->
case check_args(Args, EArgs, IPid) of
true ->
{ok, E};
{error, Index, Expected, Actual} ->
Hint = {parameter_mismatch,
{parameter, Index},
{expected, Expected},
{actual, Actual},
E},
find_matching_expectation(I, [Hint|Hints], RestEs)
end;
_ ->
Hint = {mfa_mismatch, E},
find_matching_expectation(I, [Hint|Hints], RestEs)
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec answer_invokation(Invokation :: term(),
#expectation{},
gen_statem:from()) ->
[gen_statem:reply_action()].
answer_invokation({invokation, _Mod, _Fun, Args, IPid},
#expectation{answer = Answer,
listeners = Listeners},
From) ->
[{reply, From, Answer}|
[{reply, Listener, {success, IPid, Args}} || Listener <- Listeners]].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
remove_expectation(#expectation{id = EId},
St = #state{strict = Stricts}) ->
St#state {
strict = lists:keydelete(
EId,
#expectation.id,
Stricts)
}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
log_invokation({invokation, Mod, Fun, Args, IPid},
#expectation {
id = EId,
g = GroupTag,
answer = Answer
},
St = #state{ strict_log = Log,
call_log = CallLog }) ->
St#state {
strict_log = [#strict_log {
eref = EId,
grpt = GroupTag,
ipid = IPid,
args = Args
} | Log],
call_log = [{Mod, Fun, Args, Answer} | CallLog]}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec stop_or_continue_replay([gen_statem:reply_action()], statedata()) ->
gen_statem:event_handler_result(no_expectations).
stop_or_continue_replay(
ReplyActions,
St = #state{
strict = Expectations,
on_finished = OnFinished
}) ->
case {Expectations, OnFinished} of
{[], undefined} ->
{next_state, no_expectations, St,
[reset_invokation_timer()|ReplyActions]};
{[_|_], _} ->
{keep_state, St, [start_invokation_timer(St)|ReplyActions]};
{[], OnFinished} ->
{stop_and_reply, normal, ReplyActions++[{reply, OnFinished, ok}],
St}
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
set_invokation_timeout(InvTimeout, S = #state{}) ->
S#state{ inv_to = InvTimeout }.
-define(set_invokation_timeout_action(T),
{{timeout, invokation_timeout}, T, invokation_timeout}).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec start_invokation_timer(statedata()) -> gen_statem:enter_action().
start_invokation_timer(#state{ inv_to = InvTo }) ->
?set_invokation_timeout_action(InvTo).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec reset_invokation_timer() -> gen_statem:enter_action().
reset_invokation_timer() -> ?set_invokation_timeout_action(infinity).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec enter_deranged([gen_statem:reply_action()], Error :: term(), #state{}) ->
gen_statem:event_handler_result(deranged).
enter_deranged(ReplyActions, What, State = #state{ error = no_error,
strict = Strict}) ->
{next_state,
deranged,
State#state{ error = What },
[reset_invokation_timer()
|
[{reply, L, {error, mock_deranged}} ||
#expectation{ listeners = Ls } <- Strict,
L <- Ls]
++ ReplyActions]}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-ifdef(EM_DEBUG).
dbgLog(Fmt,Args) ->
io:format(standard_error, "+++++++++++ EM +++++++++++ " ++ Fmt, Args).
-else.
dbgLog(_Fmt,_Args) -> ok.
-endif.