Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wait for a number of calls feature (#81) #99

Merged
merged 3 commits into from
Aug 17, 2013
Merged
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
63 changes: 63 additions & 0 deletions src/meck.erl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
-export([reset/1]).
-export([capture/5]).
-export([capture/6]).
-export([wait/4]).
-export([wait/5]).
-export([wait/6]).

%% Syntactic sugar
-export([loop/1]).
Expand Down Expand Up @@ -435,6 +438,66 @@ num_calls(Mod, OptFun, OptArgsSpec) ->
num_calls(Mod, OptFun, OptArgsSpec, OptPid) ->
meck_history:num_calls(OptPid, Mod, OptFun, OptArgsSpec).

%% @doc Blocks until either function `Mod:Func' is called at least once with
%% 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
%% {@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.
%%
%% @equiv wait(1, Mod, OptFunc, OptArgsSpec, '_', Timeout)
-spec wait(Mod, OptFunc, OptArgsSpec, Timeout) -> ok when
Mod :: atom(),
OptFunc :: '_' | atom(),
OptArgsSpec :: '_' | args_spec(),
Timeout :: non_neg_integer().
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'.
%%
%% The number of calls is counted starting from the most resent 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(),
Mod :: atom(),
OptFunc :: '_' | atom(),
OptArgsSpec :: '_' | args_spec(),
Timeout :: non_neg_integer().
wait(Times, Mod, OptFunc, OptArgsSpec, Timeout) ->
wait(Times, 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'.
%%
%% The number of calls is counted starting from the most resent 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
%% function returns `ok' immediately.
Copy link
Owner

Choose a reason for hiding this comment

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

We should allow Times with a value of 0 which makes it easier to use this interface programatically.

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 is possible to specify timeout value of 0. A couple of tests indeed use 0 timeout.

Copy link
Owner

Choose a reason for hiding this comment

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

I meant Times.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, missed that. I will make sure that it works.

-spec wait(Times, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout) -> ok when
Times :: pos_integer(),
Mod :: atom(),
OptFunc :: '_' | atom(),
OptArgsSpec :: '_' | args_spec(),
OptCallerPid :: '_' | pid(),
Timeout :: non_neg_integer().
wait(0, _Mod, _OptFunc, _OptArgsSpec, _OptCallerPid, _Timeout) ->
ok;
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).

%% @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
9 changes: 5 additions & 4 deletions src/meck_history.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
-export([get_history/2]).
-export([num_calls/4]).
-export([capture/6]).
-export([new_filter/3]).

%%%============================================================================
%%% Types
Expand Down Expand Up @@ -93,10 +94,6 @@ capture(Occur, OptCallerPid, Mod, Func, OptArgsSpec, ArgNum) ->
lists:nth(ArgNum, Args)
end.

%%%============================================================================
%%% Internal functions
%%%============================================================================

-spec new_filter(opt_pid(), opt_func(), meck_args_matcher:args_matcher()) ->
fun((history_record()) -> boolean()).
new_filter(TheCallerPid, TheFunc, ArgsMatcher) ->
Expand All @@ -112,6 +109,10 @@ new_filter(TheCallerPid, TheFunc, ArgsMatcher) ->
false
end.

%%%============================================================================
%%% Internal functions
%%%============================================================================

-spec nth_record(Occur::pos_integer(), history()) -> history_record() |
not_found.
nth_record(Occur, History) ->
Expand Down
174 changes: 165 additions & 9 deletions src/meck_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
-export([set_expect/2]).
-export([delete_expect/3]).
-export([get_history/1]).
-export([wait/6]).
-export([reset/1]).
-export([validate/1]).
-export([stop/1]).
Expand Down Expand Up @@ -53,7 +54,21 @@
original :: term(),
was_sticky = false :: boolean(),
reload :: {Compiler::pid(), {From::pid(), Tag::any()}} |
undefined}).
undefined,
trackers = [] :: [tracker()]}).

-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()},
expire_at :: erlang:timestamp()}).

%%%============================================================================
%%% Types
%%%============================================================================

-type tracker() :: #tracker{}.

%%%============================================================================
%%% API
Expand Down Expand Up @@ -114,6 +129,36 @@ add_history(Mod, CallerPid, Func, Args, Result) ->
get_history(Mod) ->
gen_server(call, Mod, get_history).

-spec wait(Mod::atom(),
Times::non_neg_integer(),
OptFunc::'_' | atom(),
meck_args_matcher:args_matcher(),
OptCallerPid::'_' | pid(),
Timeout::non_neg_integer()) ->
ok.
wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout) ->
EffectiveTimeout = case Timeout of
Copy link
Owner

Choose a reason for hiding this comment

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

This case statement is not needed since there is a guard on meck:wait/6.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No It is needed because it is not to guard from wrong input, but to make sure that the remote process has chance to respond in case when 0 timeout it specified but the specified number of calls already happened. In that case wait should complete successfully. Previously the caller waited for response from meck_proc but now caller has its own timeout and that is why 0 timeout requires its own case. If you do not get what I am saying that is probably because I am typing an answer having a couple of bears in me :)

0 ->
infinity;
_Else ->
Timeout
end,
Name = meck_util:proc_name(Mod),
try gen_server:call(Name, {wait, Times, OptFunc, ArgsMatcher, OptCallerPid,
Timeout},
EffectiveTimeout)
of
ok ->
ok;
{error, timeout} ->
erlang:error(timeout)
catch
exit:{timeout, _Details} ->
erlang:error(timeout);
exit:_Reason ->
erlang:error({not_mocked, Mod})
end.

-spec reset(Mod::atom()) -> ok.
reset(Mod) ->
gen_server(call, Mod, reset).
Expand Down Expand Up @@ -190,6 +235,22 @@ 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,
S = #state{history = History, trackers = Trackers}) ->
case times_called(OptFunc, ArgsMatcher, OptCallerPid, History) of
CalledSoFar when CalledSoFar >= Times ->
{reply, ok, S};
_CalledSoFar when Timeout =:= 0 ->
{reply, {error, timeout}, S};
CalledSoFar ->
Tracker = #tracker{opt_func = OptFunc,
args_matcher = ArgsMatcher,
opt_caller_pid = OptCallerPid,
countdown = Times - CalledSoFar,
reply_to = From,
expire_at = timeout_to_timestamp(Timeout)},
{noreply, S#state{trackers = [Tracker | Trackers]}}
end;
handle_call(reset, _From, S) ->
{reply, ok, S#state{history = []}};
handle_call(invalidate, _From, S) ->
Expand All @@ -200,12 +261,18 @@ handle_call(stop, _From, S) ->
{stop, normal, ok, S}.

%% @hidden
handle_cast({add_history, _Item}, S = #state{history = undefined}) ->
{noreply, S};
handle_cast({add_history, Item}, S = #state{reload = Reload}) ->
handle_cast({add_history, HistoryRecord}, S = #state{history = undefined,
trackers = Trackers}) ->
UpdTracker = update_trackers(HistoryRecord, Trackers),
{noreply, S#state{trackers = UpdTracker}};
handle_cast({add_history, HistoryRecord}, S = #state{history = History,
trackers = Trackers,
reload = Reload}) ->
case Reload of
undefined ->
{noreply, S#state{history = [Item | S#state.history]}};
UpdTrackers = update_trackers(HistoryRecord, Trackers),
{noreply, S#state{history = [HistoryRecord | History],
trackers = UpdTrackers}};
_ ->
% Skip Item if the mocked module compiler is running.
{noreply, S}
Expand Down Expand Up @@ -424,10 +491,11 @@ compile_expects(Mod, Expects) ->
%% If the recompilation is made by the server that executes a module
%% no module that is called from meck_code:compile_and_load_forms/2
%% can be mocked by meck.
CompilerPid = spawn_link(fun() ->
Forms = meck_code_gen:to_forms(Mod, Expects),
meck_code:compile_and_load_forms(Forms)
end),
CompilerPid =
erlang:spawn_link(fun() ->
Forms = meck_code_gen:to_forms(Mod, Expects),
meck_code:compile_and_load_forms(Forms)
end),
{Expects, CompilerPid}.

restore_original(Mod, {false, _}, WasSticky) ->
Expand Down Expand Up @@ -483,3 +551,91 @@ cleanup(Mod) ->
code:delete(Mod),
code:purge(meck_util:original_name(Mod)),
code:delete(meck_util:original_name(Mod)).

-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) ->
update_trackers(HistoryRecord, Trackers, []).

-spec update_trackers(meck_history:history_record(),
Trackers::[tracker()],
CheckedSoFar::[tracker()]) ->
UpdTrackers::[tracker()].
update_trackers(_HistoryRecord, [], UpdatedSoFar) ->
UpdatedSoFar;
update_trackers(HistoryRecord, [Tracker | Rest], UpdatedSoFar) ->
CallerPid = erlang:element(1, HistoryRecord),
{_Mod, Func, Args} = erlang:element(2, HistoryRecord),
case update_tracker(Func, Args, CallerPid, Tracker) of
expired ->
update_trackers(HistoryRecord, Rest, UpdatedSoFar);
UpdTracker ->
update_trackers(HistoryRecord, Rest, [UpdTracker | UpdatedSoFar])
end.


-spec update_tracker(Func::atom(), Args::[any()], Caller::pid(), tracker()) ->
expired |
(UpdTracker::tracker()).
update_tracker(Func, Args, CallerPid,
#tracker{opt_func = OptFunc,
args_matcher = ArgsMatcher,
opt_caller_pid = OptCallerPid,
countdown = Countdown,
reply_to = ReplyTo,
expire_at = ExpireAt} = Tracker)
when (OptFunc =:= '_' orelse Func =:= OptFunc) andalso
(OptCallerPid =:= '_' orelse CallerPid =:= OptCallerPid) ->
case meck_args_matcher:match(Args, ArgsMatcher) of
false ->
Tracker;
true ->
case is_expired(ExpireAt) of
true ->
expired;
false when Countdown == 1 ->
gen_server:reply(ReplyTo, ok),
expired;
false ->
Tracker#tracker{countdown = Countdown - 1}
end
end;
update_tracker(_Func, _Args, _CallerPid, Tracker) ->
Tracker.

-spec timeout_to_timestamp(Timeout::non_neg_integer()) -> erlang:timestamp().
timeout_to_timestamp(Timeout) ->
{MacroSecs, Secs, MicroSecs} = os:timestamp(),
MicroSecs2 = MicroSecs + Timeout * 1000,
UpdMicroSecs = MicroSecs2 rem 1000000,
Secs2 = Secs + MicroSecs2 div 1000000,
UpdSecs = Secs2 rem 1000000,
UpdMacroSecs = MacroSecs + Secs2 div 1000000,
{UpdMacroSecs, UpdSecs, UpdMicroSecs}.

-spec is_expired(erlang:timestamp()) -> boolean().
is_expired({MacroSecs, Secs, MicroSecs}) ->
{NowMacroSecs, NowSecs, NowMicroSecs} = os:timestamp(),
((NowMacroSecs > MacroSecs) orelse
(NowMacroSecs == MacroSecs andalso NowSecs > Secs) orelse
(NowMacroSecs == MacroSecs andalso NowSecs == Secs andalso
NowMicroSecs > MicroSecs)).



Loading