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
5 changes: 5 additions & 0 deletions src/meck.erl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
-export([wait/4]).
-export([wait/5]).
-export([wait/6]).
-export([wait_for/6]).
-export([mocked/0]).

%% Syntactic sugar
Expand Down Expand Up @@ -601,6 +602,10 @@ wait(Times, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout)
ArgsMatcher = meck_args_matcher:new(OptArgsSpec),
meck_proc:wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout).

wait_for({Cond, CondState}, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout) when is_function(Cond, 2) ->
ArgsMatcher = meck_args_matcher:new(OptArgsSpec),
meck_proc:wait_for(Mod, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout).

%% @doc Erases the call history for a mocked module or a list of mocked modules.
%%
%% This function will erase all calls made heretofore from the history of the
Expand Down
89 changes: 53 additions & 36 deletions src/meck_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
-export([list_expects/2]).
-export([get_history/1]).
-export([wait/6]).
-export([wait_for/6]).
-export([reset/1]).
-export([validate/1]).
-export([stop/1]).
Expand Down Expand Up @@ -64,10 +65,13 @@
trackers = [] :: [tracker()],
restore = false :: boolean()}).

-type cond_state() :: term().
-type cond_fun() :: fun((Args :: [term()], cond_state()) -> cond_state()).

-record(tracker, {opt_func :: '_' | atom(),
args_matcher :: meck_args_matcher:args_matcher(),
opt_caller_pid :: '_' | pid(),
countdown :: non_neg_integer(),
awaiting :: {cond_fun(), cond_state()},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether 'awaiting' here should be 'condition'.

reply_to :: {Caller::pid(), Tag::any()},
expire_at :: erlang:timestamp()}).

Expand Down Expand Up @@ -152,14 +156,24 @@ get_history(Mod) ->
Timeout::non_neg_integer()) ->
ok.
wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout) ->
EffectiveTimeout = case Timeout of
0 ->
infinity;
_Else ->
Timeout
end,
Cond = fun
(_, T) when T =:= 0 ->
{halt, ok};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

halt and cont are taken from Elixir's Enum.reduce_while. The ability to return {halt, Reply} is maybe overkill, but it means that you can return something other than ok from meck:wait_for.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think those are fine. It is also nice to be able to return a different return value with the state, I like that 👍

Could this code be inlined above where wait/7 is defined, then we would need one less jump?

(_, T) ->
{cont, T - 1}
end,
wait_for(Mod, {Cond, Times - 1}, OptFunc, ArgsMatcher, OptCallerPid, Timeout).

wait_for(Mod, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout) ->
EffectiveTimeout =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that this got reformatted, making the diff bigger. I can revert just this bit, if that's wanted.

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_for, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid,
Timeout},
EffectiveTimeout)
of
Expand Down Expand Up @@ -297,18 +311,34 @@ 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_for, {Cond, CondState1}, 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, CondState} = Acc) ->
case Filter(HistoryRec) of
true ->
{_Pid, {_M, _F, Args}, _Result} = HistoryRec,
Cond(Args, CondState);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure whether just 'Args' was enough here, or whether also passing 'Result' would be useful. I couldn't pass the whole HistoryRec, because we don't see the whole thing when updating the tracker (unless I'm misreading that code).

false ->
Acc
end;
(_HistoryRec, {halt, _Reply} = Acc) ->
Acc
end,
{cont, CondState1},
History
),
case Result of
{halt, _Reply} ->
{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,
awaiting = {Cond, CondState2},
reply_to = From,
expire_at = timeout_to_timestamp(Timeout)},
{noreply, S#state{trackers = [Tracker | Trackers]}}
Expand Down Expand Up @@ -668,22 +698,6 @@ cleanup(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 @@ -713,7 +727,7 @@ update_tracker(Func, Args, CallerPid,
#tracker{opt_func = OptFunc,
args_matcher = ArgsMatcher,
opt_caller_pid = OptCallerPid,
countdown = Countdown,
awaiting = {Cond, CondState},
reply_to = ReplyTo,
expire_at = ExpireAt} = Tracker)
when (OptFunc =:= '_' orelse Func =:= OptFunc) andalso
Expand All @@ -725,17 +739,20 @@ 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 Cond(Args, CondState) of
{halt, Result} ->
gen_server:reply(ReplyTo, Result),
expired;
{cont, CondState2} ->
Tracker#tracker{awaiting = {Cond, CondState2}}
end
end
end;
update_tracker(_Func, _Args, _CallerPid, Tracker) ->
Tracker.

-spec timeout_to_timestamp(Timeout::non_neg_integer()) -> erlang:timestamp().
-spec timeout_to_timestamp(Timeout :: non_neg_integer()) -> erlang:timestamp().
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, more reformatting noise. Can revert.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do 🙂

timeout_to_timestamp(Timeout) ->
{MacroSecs, Secs, MicroSecs} = os:timestamp(),
MicroSecs2 = MicroSecs + Timeout * 1000,
Expand Down
57 changes: 57 additions & 0 deletions test/meck_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,63 @@ wait_purge_expired_tracker_test() ->
%% Clean
meck:unload().

wait_for_test() ->
%% Given
meck:new(test, [non_strict]),
meck:expect(test, foo, 2, ok),
%% When
Pid = erlang:spawn(fun() ->
test:foo(1, 1),
test:foo(1, 2)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs that these tests don't actually address the "any order / grouping" motivation. I can add more tests if the general approach looks sensible.

end),
%% Then
Cond = fun([_A, B], Expected) ->
case Expected -- [B] of
[] -> {halt, ok};
Remaining -> {cont, Remaining}
end
end,
meck:wait_for(
{Cond, [1, 2]},
test,
foo,
['_', '_'],
Pid,
1
),
%% Clean
meck:unload().

wait_for_fails_test() ->
%% Given
meck:new(test, [non_strict]),
meck:expect(test, foo, 2, ok),
%% When
Pid = erlang:spawn(fun() ->
test:foo(1, 1),
test:foo(1, 2)
end),
%% Then
Cond = fun([_A, B], Expected) ->
case Expected -- [B] of
[] -> {halt, ok};
Remaining -> {cont, Remaining}
end
end,
?assertError(
timeout,
meck:wait_for(
{Cond, [1, 2, 3]},
test,
foo,
['_', '_'],
Pid,
1
)
),
%% Clean
meck:unload().

mocked_test() ->
%% At start, no modules should be mocked:
[] = meck:mocked(),
Expand Down