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

Feature/capture #97

Merged
merged 5 commits into from
Mar 31, 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
129 changes: 96 additions & 33 deletions src/meck.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
-export([num_calls/3]).
-export([num_calls/4]).
-export([reset/1]).
-export([capture/5]).
-export([capture/6]).

%% Syntactic sugar
-export([loop/1]).
Expand Down Expand Up @@ -85,14 +87,26 @@
%% An instance of this type may be specified in any or even all positions of an
%% {@link arg_spec()}.

-type args_spec() :: [any() | '_' | matcher()].
%% It is used in {@link expect/3} and {@link expect/4} to define an expectation
%% by an argument pattern. The length of the list defines the arity of the
%% function an expectation is created for. Every list element corresponds to a
%% function argument at the respective position. '_' is a wildcard that matches
%% any value. Instead of exact values or '_' wildcards, you can also specify
%% a {@link matcher()} created by {@link is/1} from a predicate function or a
%% hamcrest matcher.
-type args_spec() :: [any() | '_' | matcher()] | non_neg_integer().
%% Argument specification is used to specify argument patterns throughout Meck.
%% In particular it is used in definition of expectation clauses by
%% {@link expect/3}, {@link expect/4}, and by history digging functions
%% {@link num_called/3}, {@link called/3} to specify what arguments of a
%% function call of interest should look like.
%%
%% An argument specification can be given as a argument pattern list or
%% as a non-negative integer that represents function clause/call arity.
%%
%% If an argument specification is given as an argument pattern, then every
%% pattern element corresponds to a function argument at the respective
%% position. '_' is a wildcard that matches any value. In fact you can specify
%% atom wildcard '_' at any level in the value structure.
%% (E.g.: {1, [blah, {'_', "bar", 2} | '_'], 3}). It is also possible to use a
%% {@link matcher()} created by {@link is/1} in-place of a value pattern.
%%
%% If an argument specification is given by an arity, then it is equivalent to
%% a pattern based argument specification that consists solely of wildcards,
%% and has the length of arity (e.g.: 3 is equivalent to ['_', '_', '_']).

-opaque ret_spec() :: meck_ret_spec:ret_spec().
%% Opaque data structure that specifies a value or a set of values to be returned
Expand Down Expand Up @@ -214,21 +228,20 @@ expect(Mod, Func, Expectation) when is_atom(Mod), is_atom(Func) ->

%% @doc Adds an expectation with the supplied arity and return value.
%%
%% This creates an expectation which takes `Arity' number of functions
%% and always returns `Result'.
%% This creates an expectation that has the only clause {`ArgsSpec', `RetSpec'}.
%%
%% @see expect/3.
-spec expect(Mods, Func, AriOrArgs, RetSpec) -> ok when
%% @equiv expect(Mod, Func, [{ArgsSpec, RetSpec}])
-spec expect(Mods, Func, ArgsSpec, RetSpec) -> ok when
Mods :: Mod | [Mod],
Mod :: atom(),
Func :: atom(),
AriOrArgs :: byte() | args_spec(),
ArgsSpec :: args_spec(),
RetSpec :: ret_spec().
expect(Mod, Func, AriOrArgs, RetSpec) when is_list(Mod) ->
lists:foreach(fun(M) -> expect(M, Func, AriOrArgs, RetSpec) end, Mod),
expect(Mod, Func, ArgsSpec, RetSpec) when is_list(Mod) ->
lists:foreach(fun(M) -> expect(M, Func, ArgsSpec, RetSpec) end, Mod),
ok;
expect(Mod, Func, AriOrArgs, RetSpec) when is_atom(Mod), is_atom(Func) ->
Expect = meck_expect:new(Func, AriOrArgs, RetSpec),
expect(Mod, Func, ArgsSpec, RetSpec) when is_atom(Mod), is_atom(Func) ->
Expect = meck_expect:new(Func, ArgsSpec, RetSpec),
check_expect_result(meck_proc:set_expect(Mod, Expect)).

%% @equiv expect(Mod, Func, Ari, seq(Sequence))
Expand Down Expand Up @@ -373,12 +386,12 @@ unload(Mods) when is_list(Mods) ->
%% @doc Returns whether `Mod:Func' has been called with `Args'.
%%
%% @equiv called(Mod, Fun, Args, '_')
-spec called(Mod, OptFun, OptArgs) -> boolean() when
-spec called(Mod, OptFun, OptArgsSpec) -> boolean() when
Mod :: atom(),
OptFun :: '_' | atom(),
OptArgs :: '_' | args_spec().
called(Mod, OptFun, OptArgs) ->
meck_history:num_calls('_', Mod, OptFun, OptArgs) > 0.
OptArgsSpec :: '_' | args_spec().
called(Mod, OptFun, OptArgsSpec) ->
meck_history:num_calls('_', Mod, OptFun, OptArgsSpec) > 0.

%% @doc Returns whether `Pid' has called `Mod:Func' with `Args'.
%%
Expand All @@ -390,23 +403,23 @@ called(Mod, OptFun, OptArgs) ->
%% atom: ``'_' ''
%%
%% @see called/3
-spec called(Mod, OptFun, OptArgs, OptCallerPid) -> boolean() when
-spec called(Mod, OptFun, OptArgsSpec, OptCallerPid) -> boolean() when
Mod :: atom(),
OptFun :: '_' | atom(),
OptArgs :: '_' | args_spec(),
OptArgsSpec :: '_' | args_spec(),
OptCallerPid :: '_' | pid().
called(Mod, OptFun, OptArgs, OptPid) ->
meck_history:num_calls(OptPid, Mod, OptFun, OptArgs) > 0.
called(Mod, OptFun, OptArgsSpec, OptPid) ->
meck_history:num_calls(OptPid, Mod, OptFun, OptArgsSpec) > 0.

%% @doc Returns the number of times `Mod:Func' has been called with `Args'.
%%
%% @equiv num_calls(Mod, Fun, Args, '_')
-spec num_calls(Mod, OptFun, OptArgs) -> non_neg_integer() when
-spec num_calls(Mod, OptFun, OptArgsSpec) -> non_neg_integer() when
Mod :: atom(),
OptFun :: '_' | atom(),
OptArgs :: '_' | args_spec().
num_calls(Mod, OptFun, OptArgs) ->
meck_history:num_calls('_', Mod, OptFun, OptArgs).
OptArgsSpec :: '_' | args_spec().
num_calls(Mod, OptFun, OptArgsSpec) ->
meck_history:num_calls('_', Mod, OptFun, OptArgsSpec).

%% @doc Returns the number of times process `Pid' has called `Mod:Func'
%% with `Args'.
Expand All @@ -416,13 +429,14 @@ num_calls(Mod, OptFun, OptArgs) ->
%% arguments, `Args' and returns the result.
%%
%% @see num_calls/3
-spec num_calls(Mod, OptFun, OptArgs, OptCallerPid) -> non_neg_integer() when
-spec num_calls(Mod, OptFun, OptArgsSpec, OptCallerPid) ->
non_neg_integer() when
Mod :: atom(),
OptFun :: '_' | atom(),
OptArgs :: '_' | args_spec(),
OptArgsSpec :: '_' | args_spec(),
OptCallerPid :: '_' | pid().
num_calls(Mod, OptFun, OptArgs, OptPid) ->
meck_history:num_calls(OptPid, Mod, OptFun, OptArgs).
num_calls(Mod, OptFun, OptArgsSpec, OptPid) ->
meck_history:num_calls(OptPid, Mod, OptFun, OptArgsSpec).

%% @doc Erases the call history for a mocked module or a list of mocked modules.
%%
Expand Down Expand Up @@ -506,6 +520,55 @@ exec(Fun) -> meck_ret_spec:exec(Fun).
is(MatcherImpl) ->
meck_matcher:new(MatcherImpl).

%% @doc Returns the value of an argument as it was passed to a particular
%% function call made by a particular process. It fails with `not_found' error
%% if a function call of interest has never been made.
%%
%% It retrieves the value of argument at `ArgNum' position as it was passed
%% to function call `Mod:Func' with arguments that match `OptArgsSpec' made by
%% process `CallerPid' that occurred `Occur''th according to the call history.
%%
%% Atoms `first' and `last' can be used in place of the occurrence number to
%% retrieve the argument value passed when the function was called the first
%% or the last time respectively.
%%
%% If an occurrence of a function call irrespective of the calling process needs
%% to be captured then `_' might be passed as `OptCallerPid', but it is better
%% to use {@link capture/3} instead.
-spec capture(Occur, Mod, Func, OptArgsSpec, ArgNum, OptCallerPid) -> ArgValue when
Occur :: first | last | pos_integer(),
Mod :: atom(),
Func :: atom(),
OptArgsSpec :: '_' | args_spec(),
ArgNum :: pos_integer(),
OptCallerPid :: '_' | pid(),
ArgValue :: any().
capture(Occur, Mod, Func, OptArgsSpec, ArgNum, OptCallerPid) ->
meck_history:capture(Occur, OptCallerPid, Mod, Func, OptArgsSpec, ArgNum).

%% @doc Returns the value of an argument as it was passed to a particular
%% function call, It fails with `not_found' error if a function call of
%% interest has never been made.
%%
%% It retrieves the value of argument at `ArgNum' position as it was passed
%% to function call `Mod:Func' with arguments that match `OptArgsSpec' that
%% occurred `Occur''th according to the call history.
%%
%% Atoms `first' and `last' can be used in place of the occurrence number to
%% retrieve the argument value passed when the function was called the first
%% or the last time respectively.
%%
%% @equiv capture(Occur, '_', Mod, Func, OptArgsSpec, ArgNum)
-spec capture(Occur, Mod, Func, OptArgsSpec, ArgNum) -> ArgValue when
Occur :: first | last | pos_integer(),
Mod::atom(),
Func::atom(),
OptArgsSpec :: args_spec(),
ArgNum :: pos_integer(),
ArgValue :: any().
capture(Occur, Mod, Func, OptArgsSpec, ArgNum) ->
meck_history:capture(Occur, '_', Mod, Func, OptArgsSpec, ArgNum).

%%%============================================================================
%%% Internal functions
%%%============================================================================
Expand Down
66 changes: 39 additions & 27 deletions src/meck_args_matcher.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,60 +21,72 @@

%% API
-export([new/1,
arity/1,
match/2]).

%%%============================================================================
%%% Definitions
%%%============================================================================

-record(args_matcher, {args_spec :: opt_args_spec(),
-record(args_matcher, {opt_args_pattern :: opt_args_pattern(),
comp_match_spec :: ets:comp_match_spec(),
has_matchers = false :: boolean()}).
has_matchers :: boolean()}).

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

-type args_spec() :: [any()].
-type opt_args_spec() :: args_spec() | '_'.
-type args_spec() :: args_pattern() | non_neg_integer().
-type opt_args_pattern() :: args_pattern() | '_'.
-type args_pattern() :: [any() | '_' | meck_matcher:matcher()].

-opaque args_matcher() :: #args_matcher{}.

%%%============================================================================
%%% API
%%%============================================================================

-spec new(byte() | opt_args_spec()) -> args_matcher().
new(ArgsSpec = '_') ->
MatchSpecItem = meck_util:match_spec_item({ArgsSpec}),
-spec new(opt_args_spec()) -> args_matcher().
new('_') ->
MatchSpecItem = meck_util:match_spec_item({'_'}),
CompMatchSpec = ets:match_spec_compile([MatchSpecItem]),
#args_matcher{args_spec = ArgsSpec, comp_match_spec = CompMatchSpec};
new(Ari) when is_number(Ari) ->
ArgsSpec = lists:duplicate(Ari, '_'),
MatchSpecItem = meck_util:match_spec_item({ArgsSpec}),
#args_matcher{opt_args_pattern = '_',
comp_match_spec = CompMatchSpec,
has_matchers = false};
new(Arity) when is_number(Arity) ->
ArgsPattern = lists:duplicate(Arity, '_'),
MatchSpecItem = meck_util:match_spec_item({ArgsPattern}),
CompMatchSpec = ets:match_spec_compile([MatchSpecItem]),
#args_matcher{args_spec = ArgsSpec, comp_match_spec = CompMatchSpec};
new(ArgsSpec) when is_list(ArgsSpec) ->
{HasMatchers, Pattern} = case strip_off_matchers(ArgsSpec) of
#args_matcher{opt_args_pattern = ArgsPattern,
comp_match_spec = CompMatchSpec,
has_matchers = false};
new(ArgsPattern) when is_list(ArgsPattern) ->
{HasMatchers, Pattern} = case strip_off_matchers(ArgsPattern) of
unchanged ->
{false, ArgsSpec};
{false, ArgsPattern};
StrippedArgsSpec ->
{true, StrippedArgsSpec}
end,
MatchSpecItem = meck_util:match_spec_item({Pattern}),
CompMatchSpec = ets:match_spec_compile([MatchSpecItem]),
#args_matcher{args_spec = ArgsSpec,
#args_matcher{opt_args_pattern = ArgsPattern,
comp_match_spec = CompMatchSpec,
has_matchers = HasMatchers}.

-spec arity(args_matcher()) -> Arity::non_neg_integer().
arity(#args_matcher{opt_args_pattern = ArgsPattern}) ->
erlang:length(ArgsPattern).

-spec match(Args::any(), args_matcher()) -> boolean().
match(Args, #args_matcher{args_spec = ArgsSpec,
match(Args, #args_matcher{opt_args_pattern = OptArgsPattern,
comp_match_spec = CompMatchSpec,
has_matchers = HasMatchers}) ->
case ets:match_spec_run([{Args}], CompMatchSpec) of
[] ->
false;
_Matches when HasMatchers ->
check_by_matchers(Args, ArgsSpec);
_Matches when HasMatchers andalso erlang:is_list(OptArgsPattern) ->
check_by_matchers(Args, OptArgsPattern);
_Matches ->
true
end.
Expand All @@ -83,19 +95,19 @@ match(Args, #args_matcher{args_spec = ArgsSpec,
%%% Internal functions
%%%============================================================================

-spec strip_off_matchers(args_spec()) ->
NewArgsSpec::args_spec() | unchanged.
strip_off_matchers(ArgsSpec) ->
strip_off_matchers(ArgsSpec, [], false).
-spec strip_off_matchers(args_pattern()) ->
NewArgsPattern::args_pattern() | unchanged.
strip_off_matchers(ArgsPattern) ->
strip_off_matchers(ArgsPattern, [], false).

-spec strip_off_matchers(args_spec(), Stripped::[any() | '_'], boolean()) ->
NewArgsSpec::args_spec() | unchanged.
strip_off_matchers([ArgSpec | Rest], Stripped, HasMatchers) ->
case meck_matcher:is_matcher(ArgSpec) of
-spec strip_off_matchers(args_pattern(), Stripped::[any() | '_'], boolean()) ->
NewArgsPattern::args_pattern() | unchanged.
strip_off_matchers([ArgPattern | Rest], Stripped, HasMatchers) ->
case meck_matcher:is_matcher(ArgPattern) of
true ->
strip_off_matchers(Rest, ['_' | Stripped], true);
_ ->
strip_off_matchers(Rest, [ArgSpec | Stripped], HasMatchers)
strip_off_matchers(Rest, [ArgPattern | Stripped], HasMatchers)
end;
strip_off_matchers([], Stripped, true) ->
lists:reverse(Stripped);
Expand Down
13 changes: 5 additions & 8 deletions src/meck_expect.erl
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,10 @@ new(Func, ClauseSpecs) when is_list(ClauseSpecs) ->
{{Func, Arity}, Clauses}.

-spec new(Func::atom(),
byte() | meck_args_matcher:args_spec(),
meck_args_matcher:args_spec(),
meck_ret_spec:ret_spec()) ->
expect().
new(Func, Ari, RetSpec) when is_integer(Ari), Ari >= 0 ->
Clause = {meck_args_matcher:new(Ari), RetSpec},
{{Func, Ari}, [Clause]};
new(Func, ArgsSpec, RetSpec) when is_list(ArgsSpec) ->
new(Func, ArgsSpec, RetSpec) ->
{Ari, Clause} = parse_clause_spec({ArgsSpec, RetSpec}),
{{Func, Ari}, [Clause]}.

Expand Down Expand Up @@ -104,7 +101,7 @@ parse_clause_specs([ClauseSpec | Rest]) ->
{Ari, Clause} = parse_clause_spec(ClauseSpec),
parse_clause_specs(Rest, Ari, [Clause]).

-spec parse_clause_specs([meck:func_clause_spec()],
-spec parse_clause_specs([func_clause_spec()],
FirstClauseAri::byte(),
Clauses::[func_clause()]) ->
{Ari::byte(), [func_clause()]}.
Expand All @@ -121,11 +118,11 @@ parse_clause_specs([ClauseSpec | Rest], FirstClauseAri, Clauses) ->
parse_clause_specs([], FirstClauseAri, Clauses) ->
{FirstClauseAri, lists:reverse(Clauses)}.

-spec parse_clause_spec(meck:func_clause_spec()) ->
-spec parse_clause_spec(func_clause_spec()) ->
{Ari::byte(), func_clause()}.
parse_clause_spec({ArgsSpec, RetSpec}) ->
Ari = length(ArgsSpec),
ArgsMatcher = meck_args_matcher:new(ArgsSpec),
Ari = meck_args_matcher:arity(ArgsMatcher),
Clause = {ArgsMatcher, RetSpec},
{Ari, Clause}.

Expand Down
Loading