Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 65 additions & 18 deletions src/meck.erl
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
%% It is used in {@link expect/3} and {@link expect/4} to define a function
%% clause of complex multi-clause expectations.

-type condition_state() :: term().
-type condition_fun() :: fun((Args :: [term()], condition_state()) -> condition_state()).
-type condition() :: {condition_fun(), condition_state()}.
%% It is used in {@link wait/5} and {@link wait/6} to define a condition.

%%%============================================================================
%%% Interface exports
%%%============================================================================
Expand Down Expand Up @@ -545,7 +550,7 @@ num_calls(Mod, OptFun, OptArgsSpec, OptPid) ->
%% arguments matching `OptArgsSpec', or `Timeout' has elapsed. In the latter
%% case the call fails with `error:timeout'.
%%
%% The number of calls is counted starting from the most resent call to
%% The number of calls is counted starting from the most recent call to
%% {@link reset/1} on the mock or from the mock creation, whichever occurred
%% latter. If a matching call has already occurred, then the function returns
%% `ok' immediately.
Expand All @@ -559,35 +564,66 @@ num_calls(Mod, OptFun, OptArgsSpec, OptPid) ->
wait(Mod, OptFunc, OptArgsSpec, Timeout) ->
wait(1, Mod, OptFunc, OptArgsSpec, '_', Timeout).

%% @doc Blocks until either function `Mod:Func' is called at least `Times' with
%% arguments matching `OptArgsSpec', or `Timeout' has elapsed. In the latter
%% case the call fails with `error:timeout'.
%% @doc Blocks until function `Mod:Func' is called with arguments matching
%% `OptArgsSpec' and matching the `Condition`.
%%
%% Times out if instead `Timeout' has elapsed. In this case the call fails with
%% `error:timeout'.
%%
%% `Condition` is a tuple: `{CondFunc, CondState}`.
%%
%% On each call to `Mod:Func`, `CondFunc(Args, CondState)` is called.
%%
%% The condition function must return either `{halt, ok}` or
%% `{cont, NextCondState}`.
%%
%% In the former case, the condition is considered satisfied, and `wait`
%% returns. In the latter case, `wait` waits for further calls to `Mod:Func`.
%%
%% For backwards-compatibility, the condition can be specified as an integer.
%% This counts the number of calls to the function.
%%
%% The number of calls is counted starting from the most resent call to
%% The number of calls is counted starting from the most recent call to
%% {@link reset/1} on the mock or from the mock creation, whichever occurred
%% latter. If `Times' number of matching calls has already occurred, then the
%% function returns `ok' immediately.
%%
%% @equiv wait(Times, Mod, OptFunc, OptArgsSpec, '_', Timeout)
-spec wait(Times, Mod, OptFunc, OptArgsSpec, Timeout) -> ok when
Times :: pos_integer(),
%% @equiv wait(Condition, Mod, OptFunc, OptArgsSpec, '_', Timeout)
-spec wait(Condition, Mod, OptFunc, OptArgsSpec, Timeout) -> ok when
Condition :: pos_integer() | condition(),
Mod :: atom(),
OptFunc :: '_' | atom(),
OptArgsSpec :: '_' | args_spec(),
Timeout :: non_neg_integer().
wait(Times, Mod, OptFunc, OptArgsSpec, Timeout) ->
wait(Times, Mod, OptFunc, OptArgsSpec, '_', Timeout).
wait(Condition, Mod, OptFunc, OptArgsSpec, Timeout) ->
wait(Condition, Mod, OptFunc, OptArgsSpec, '_', Timeout).

%% @doc Blocks until either function `Mod:Func' is called at least `Times' with
%% arguments matching `OptArgsSpec' by process `OptCallerPid', or `Timeout' has
%% elapsed. In the latter case the call fails with `error:timeout'.

%% @doc Blocks until function `Mod:Func' is called with arguments matching
%% `OptArgsSpec' and matching the `Condition`.
%%
%% Times out if instead `Timeout' has elapsed. In this case the call fails with
%% `error:timeout'.
%%
%% `Condition` is a tuple: `{CondFunc, CondState}`.
%%
%% On each call to `Mod:Func`, `CondFunc(Args, CondState)` is called.
%%
%% The condition function must return either `{halt, ok}` or
%% `{cont, NextCondState}`.
%%
%% The number of calls is counted starting from the most resent call to
%% In the former case, the condition is considered satisfied, and `wait`
%% returns. In the latter case, `wait` waits for further calls to `Mod:Func`.
%%
%% For backwards-compatibility, the condition can be specified as an integer.
%% This counts the number of calls to the function.
%%
%% The number of calls is counted starting from the most recent call to
%% {@link reset/1} on the mock or from the mock creation, whichever occurred
%% latter. If `Times' number of matching call has already occurred, then the
%% latter. If `Times' number of matching calls has already occurred, then the
%% function returns `ok' immediately.
-spec wait(Times, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout) -> ok when
Times :: pos_integer(),
-spec wait(Condition, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout) -> ok when
Condition :: pos_integer() | condition(),
Mod :: atom(),
OptFunc :: '_' | atom(),
OptArgsSpec :: '_' | args_spec(),
Expand All @@ -599,7 +635,18 @@ wait(Times, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout)
when is_integer(Times) andalso Times > 0 andalso
is_integer(Timeout) andalso Timeout >= 0 ->
ArgsMatcher = meck_args_matcher:new(OptArgsSpec),
meck_proc:wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout).
Condition = {
fun
(_, T) when T =:= 0 -> {halt, ok};
(_, T) -> {cont, T - 1}
end,
Times - 1
},
meck_proc:wait(Mod, Condition, OptFunc, ArgsMatcher, OptCallerPid, Timeout);
wait({CondFun, _} = Condition, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout)
when is_function(CondFun, 2) ->
ArgsMatcher = meck_args_matcher:new(OptArgsSpec),
meck_proc:wait(Mod, Condition, OptFunc, ArgsMatcher, OptCallerPid, Timeout).

%% @doc Erases the call history for a mocked module or a list of mocked modules.
%%
Expand Down
74 changes: 41 additions & 33 deletions src/meck_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
%%% Definitions
%%%============================================================================

-type condition_state() :: term().
-type condition_fun() :: fun((Args :: [term()], condition_state()) -> condition_state()).
-type condition() :: {condition_fun(), condition_state()}.

-type meck_dict() :: dict:dict().

-record(state, {mod :: atom(),
Expand All @@ -67,8 +71,8 @@
-record(tracker, {opt_func :: '_' | atom(),
args_matcher :: meck_args_matcher:args_matcher(),
opt_caller_pid :: '_' | pid(),
countdown :: non_neg_integer(),
reply_to :: {Caller::pid(), Tag::any()},
condition :: condition(),
reply_to :: gen_server:from(),
expire_at :: erlang:timestamp()}).

%%%============================================================================
Expand Down Expand Up @@ -151,21 +155,21 @@ get_history(Mod) ->
gen_server(call, Mod, get_history).

-spec wait(Mod::atom(),
Times::non_neg_integer(),
Condition::condition(),
OptFunc::'_' | atom(),
meck_args_matcher:args_matcher(),
OptCallerPid::'_' | pid(),
Timeout::non_neg_integer()) ->
ok.
wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout) ->
wait(Mod, {_CondFun, _CondSt} = Condition, OptFunc, ArgsMatcher, OptCallerPid, Timeout) ->
EffectiveTimeout = case Timeout of
0 ->
infinity;
_Else ->
Timeout
end,
Name = meck_util:proc_name(Mod),
try gen_server:call(Name, {wait, Times, OptFunc, ArgsMatcher, OptCallerPid,
try gen_server:call(Name, {wait, Condition, OptFunc, ArgsMatcher, OptCallerPid,
Timeout},
EffectiveTimeout)
of
Expand Down Expand Up @@ -304,18 +308,35 @@ handle_call(get_history, _From, S = #state{history = undefined}) ->
{reply, [], S};
handle_call(get_history, _From, S) ->
{reply, lists:reverse(S#state.history), S};
handle_call({wait, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout}, From,
handle_call({wait, {CondFun, CondState} = _Condition, OptFunc, ArgsMatcher, OptCallerPid, Timeout}, From,
S = #state{history = History, trackers = Trackers}) ->
case times_called(OptFunc, ArgsMatcher, OptCallerPid, History) of
CalledSoFar when CalledSoFar >= Times ->
Filter = meck_history:new_filter(OptCallerPid, OptFunc, ArgsMatcher),
Result = lists:foldl(
fun
(HistoryRec, {cont, CondSt} = Acc) ->
case Filter(HistoryRec) of
true ->
{_Pid, {_M, _F, Args}, _Result} = HistoryRec,
CondFun(Args, CondSt);
false ->
Acc
end;
(_HistoryRec, {halt, _} = Acc) ->
Acc
end,
{cont, CondState},
History
),
case Result of
{halt, ok} ->
{reply, ok, S};
_CalledSoFar when Timeout =:= 0 ->
{cont, _} when Timeout =:= 0 ->
{reply, {error, timeout}, S};
CalledSoFar ->
{cont, CondState2} ->
Tracker = #tracker{opt_func = OptFunc,
args_matcher = ArgsMatcher,
opt_caller_pid = OptCallerPid,
countdown = Times - CalledSoFar,
condition = {CondFun, CondState2},
reply_to = From,
expire_at = timeout_to_timestamp(Timeout)},
{noreply, S#state{trackers = [Tracker | Trackers]}}
Expand Down Expand Up @@ -687,28 +708,12 @@ cleanup(Mod) ->
code:purge(meck_util:original_name(Mod)),
Res = code:delete(meck_util:original_name(Mod)),

% `cover:export` might still export the meck generated module,
% `cover:export` might still export the meck generated module,
% make sure that does not happen.
_ = cover:reset(meck_util:original_name(Mod)),

Res.

-spec times_called(OptFunc::'_' | atom(),
meck_args_matcher:args_matcher(),
OptCallerPid::'_' | pid(),
meck_history:history()) ->
non_neg_integer().
times_called(OptFunc, ArgsMatcher, OptCallerPid, History) ->
Filter = meck_history:new_filter(OptCallerPid, OptFunc, ArgsMatcher),
lists:foldl(fun(HistoryRec, Acc) ->
case Filter(HistoryRec) of
true ->
Acc + 1;
_Else ->
Acc
end
end, 0, History).

-spec update_trackers(meck_history:history_record(), [tracker()]) ->
UpdTracker::[tracker()].
update_trackers(HistoryRecord, Trackers) ->
Expand Down Expand Up @@ -738,7 +743,7 @@ update_tracker(Func, Args, CallerPid,
#tracker{opt_func = OptFunc,
args_matcher = ArgsMatcher,
opt_caller_pid = OptCallerPid,
countdown = Countdown,
condition = {CondFun, CondState},
reply_to = ReplyTo,
expire_at = ExpireAt} = Tracker)
when (OptFunc =:= '_' orelse Func =:= OptFunc) andalso
Expand All @@ -750,11 +755,14 @@ update_tracker(Func, Args, CallerPid,
case is_expired(ExpireAt) of
true ->
expired;
false when Countdown == 1 ->
gen_server:reply(ReplyTo, ok),
expired;
false ->
Tracker#tracker{countdown = Countdown - 1}
case CondFun(Args, CondState) of
{halt, ok} ->
gen_server:reply(ReplyTo, ok),
expired;
{cont, CondState2} ->
Tracker#tracker{condition = {CondFun, CondState2}}
end
end
end;
update_tracker(_Func, _Args, _CallerPid, Tracker) ->
Expand Down
Loading