Skip to content

Instantly share code, notes, and snippets.

@moonpolysoft
Created April 9, 2009 22:35
Show Gist options
  • Save moonpolysoft/92788 to your computer and use it in GitHub Desktop.
Save moonpolysoft/92788 to your computer and use it in GitHub Desktop.
%%%-------------------------------------------------------------------
%%% File: mock_fun.erl
%%% @author Cliff Moon <> []
%%% @copyright 2009 Cliff Moon
%%% @doc
%%%
%%% @end
%%%
%%% @since 2009-04-03 by Cliff Moon
%%%-------------------------------------------------------------------
-module(mock_fun).
-author('cliff@powerset.com').
-behaviour(gen_server).
-include_lib("eunit/include/eunit.hrl").
%% API
-export([mock/2, expects/3, expects/4, verify/1, verify_and_stop/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {expectations=[],name}).
-include("common.hrl").
%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% @spec start_link() -> {ok,Pid} | ignore | {error,Error}
%% @doc Starts the server
%% @end
%%--------------------------------------------------------------------
mock(Name, Arity) ->
case gen_server:start_link({local, Name}, ?MODULE, [Name], []) of
{ok, Pid} ->
{ok, generate_function(Name, Arity)};
Err -> Err
end.
expects(Name, Args, Ret) ->
gen_server:call(Name, {expects, Args, Ret, 1}).
expects(Name, Args, Ret, Times) ->
gen_server:call(Name, {expects, Args, Ret, Times}).
verify(Name) ->
?assertEqual(ok, gen_server:call(Name, verify)).
stop(Name) ->
gen_server:cast(Name, stop),
timer:sleep(100).
verify_and_stop(Name) ->
verify(Name),
stop(Name).
%%====================================================================
%% gen_server callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @doc Initiates the server
%% @end
%%--------------------------------------------------------------------
init([Name]) ->
{ok, #state{name=Name}}.
%%--------------------------------------------------------------------
%% @spec
%% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% @doc Handling call messages
%% @end
%%--------------------------------------------------------------------
handle_call({expects, Args, Ret, Times}, _From, State = #state{expectations=Expects}) ->
{reply, ok, State#state{expectations=add_expectation(Args, Ret, Times, Expects)}};
handle_call({proxy_call, Args}, _From, State = #state{expectations=Expects,name=Name}) ->
case match_expectation(Args, Expects) of
{matched, ReturnTerm, NewExpects} -> {reply, ReturnTerm, State#state{expectations=NewExpects}};
unmatched -> {stop, ?fmt("Got unexpected call to ~p", [Name])}
end;
handle_call(verify, _From, State = #state{expectations=Expects,name=Name}) ->
?debugFmt("verifying ~p", [Name]),
if
length(Expects) > 0 -> {reply, {mismatch, format_missing_expectations(Expects, Name)}, State};
true -> {reply, ok, State}
end.
%%--------------------------------------------------------------------
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @doc Handling cast messages
%% @end
%%--------------------------------------------------------------------
handle_cast(stop, State) ->
{stop, shutdown, State}.
%%--------------------------------------------------------------------
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @doc Handling all non call/cast messages
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @spec terminate(Reason, State) -> void()
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @doc Convert process state when code is changed
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
generate_function(Arity, Name) ->
VarList = argument_forms(Arity),
Forms = [{'fun',1,
{clauses,
[{clause,1,
VarList,
[],
[{call,1,
{remote,1,{atom,1,gen_server},{atom,1,call}},
[{atom,1,name},
{tuple,1,
[{atom,1,proxy_fun},{tuple,1,VarList}]}]}]}]}}],
erl_eval:exprs(Forms, erl_eval:new_bindings()).
argument_forms(Arity) ->
lists:map(fun(N) ->
{var,1,list_to_atom(lists:concat(['Var', N]))}
end, lists:seq(1, Arity)).
format_missing_expectations(Expects, Name) ->
format_missing_expectations(Expects, Name, []).
format_missing_expectations([], _, Msgs) ->
lists:reverse(Msgs);
format_missing_expectations([{_, _, Times, Called}|Expects], Name, Msgs) ->
Msgs1 = [?fmt("expected ~p to be called ~p times but was called ~p", [Name,Times,Called])|Msgs],
format_missing_expectations(Expects, Name, Msgs1).
add_expectation(Args, Ret, Times, Expects) ->
Expects ++ [{Args, Ret, Times, 0}].
match_expectation(Args, Expectations) ->
match_expectation(Args, Expectations, []).
match_expectation(_, [], _) ->
unmatched;
match_expectation(Args, [Expectation = {Matcher, Ret, MaxTimes, Invoked}|Expects], Rest) ->
case Matcher(Args) of
true ->
ReturnTerm = prepare_return(Args, Ret, Invoked+1),
if
Invoked + 1 >= MaxTimes -> {matched, ReturnTerm, lists:reverse(Rest) ++ Expects};
true -> {matched, ReturnTerm, lists:reverse(Rest) ++ [{Matcher, Ret, MaxTimes, Invoked+1}] ++ Expects}
end;
false ->
match_expectation(Args, Expects, [Expectation|Rest])
end.
prepare_return(Args, Ret, Invoked) when is_function(Ret) ->
Ret(Args, Invoked);
prepare_return(Args, Ret, Invoked) ->
Ret.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment