summaryrefslogtreecommitdiff
path: root/test/mock.erl
diff options
context:
space:
mode:
Diffstat (limited to 'test/mock.erl')
-rw-r--r--test/mock.erl322
1 files changed, 322 insertions, 0 deletions
diff --git a/test/mock.erl b/test/mock.erl
new file mode 100644
index 00000000..2ecbf4f7
--- /dev/null
+++ b/test/mock.erl
@@ -0,0 +1,322 @@
+%%% -*- erlang-indent-level:2 -*-
+%%%-------------------------------------------------------------------
+%%% File: mock.erl
+%%% @author Cliff Moon <> []
+%%% @copyright 2009 Cliff Moon
+%%% @doc
+%%%
+%%% @end
+%%%
+%%% @since 2009-01-04 by Cliff Moon
+%%%-------------------------------------------------------------------
+-module(mock).
+-author('cliff@powerset.com').
+
+%% API
+-export([mock/1, proxy_call/2, proxy_call/3, expects/4, expects/5,
+ verify_and_stop/1, verify/1, stub_proxy_call/3, stop/1]).
+
+-include_lib("eunit/include/eunit.hrl").
+-include("../include/common.hrl").
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(mockstate, {old_code, module, expectations=[]}).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% @spec mock(Module::atom()) -> {ok,Mock::record()} | ignore | {error,Error}
+%% @doc Starts the server
+%% @end
+%%--------------------------------------------------------------------
+mock(Module) ->
+ case gen_server:start_link({local, mod_to_name(Module)}, mock, Module, []) of
+ {ok, Pid} -> {ok, Pid};
+ {error, Reason} -> {error, Reason}
+ end.
+
+%% @spec proxy_call(Module::atom(), Function::atom()) -> term()
+%% @doc Proxies a call to the mock server for Module without arguments
+%% @end
+proxy_call(Module, Function) ->
+ gen_server:call(mod_to_name(Module), {proxy_call, Function, {}}).
+
+%% @spec proxy_call(Module::atom(), Function::atom(), Args::tuple()) -> term()
+%% @doc Proxies a call to the mock server for Module with arguments
+%% @end
+proxy_call(Module, Function, Args) ->
+ gen_server:call(mod_to_name(Module), {proxy_call, Function, Args}).
+
+stub_proxy_call(Module, Function, Args) ->
+ RegName = list_to_atom(lists:concat([Module, "_", Function, "_stub"])),
+ Ref = make_ref(),
+ RegName ! {Ref, self(), Args},
+ ?debugFmt("sending {~p,~p,~p}", [Ref, self(), Args]),
+ receive
+ {Ref, Answer} -> Answer
+ end.
+
+%% @spec expects(Module::atom(),
+%% Function::atom(),
+%% Args::function(),
+%% Ret::function() | term() ) -> term()
+
+%% Times:: {at_least, integer()} | never | {no_more_than, integer()} | integer()) -> term()
+
+%% @doc Sets the expectation that Function of Module will be called during a
+%% test with Args. Args should be a fun predicate that will return true or
+%% false whether or not the argument list matches. The argument list of the
+%% function is passed in as a tuple. Ret is either a value to return or a fun
+%% of arity 2 to be evaluated in response to a proxied call. The first argument
+%% is the actual args from the call, the second is the call count starting
+%% with 1.
+expects(Module, Function, Args, Ret) ->
+ gen_server:call(mod_to_name(Module), {expects, Function, Args, Ret, 1}).
+
+expects(Module, Function, Args, Ret, Times) ->
+ gen_server:call(mod_to_name(Module), {expects, Function, Args, Ret, Times}).
+
+%% stub(Module, Function, Args, Ret) ->
+%% gen_server:call(mod_to_name(Module), {stub, Function, Args, Ret}).
+
+verify_and_stop(Module) ->
+ verify(Module),
+ stop(Module).
+
+verify(Module) ->
+ ?assertEqual(ok, gen_server:call(mod_to_name(Module), verify)).
+
+stop(Module) ->
+ gen_server:cast(mod_to_name(Module), stop),
+ timer:sleep(10).
+
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% @spec init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% @doc Initiates the server
+%% @end
+%%--------------------------------------------------------------------
+init(Module) ->
+ case code:get_object_code(Module) of
+ {Module, Bin, Filename} ->
+ case replace_code(Module) of
+ ok -> {ok, #mockstate{module=Module,old_code={Module, Bin, Filename}}};
+ {error, Reason} -> {stop, Reason}
+ end;
+ error -> {stop, ?fmt("Could not get object code for module ~p", [Module])}
+ end.
+
+%%--------------------------------------------------------------------
+%% @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({proxy_call, Function, Args}, _From,
+ State = #mockstate{module=Mod,expectations=Expects}) ->
+ case match_expectation(Function, Args, Expects) of
+ {matched, ReturnTerm, NewExpects} ->
+ {reply, ReturnTerm, State#mockstate{expectations=NewExpects}};
+ unmatched ->
+ {stop, ?fmt("got unexpected call to ~p:~p", [Mod,Function])}
+ end;
+
+handle_call({expects, Function, Args, Ret, Times}, _From,
+ State = #mockstate{expectations=Expects}) ->
+ {reply, ok, State#mockstate{
+ expectations=add_expectation(Function, Args, Ret, Times, Expects)}};
+
+handle_call(verify, _From, State = #mockstate{expectations=Expects,module=Mod}) ->
+ ?infoFmt("verifying ~p~n", [Mod]),
+ if
+ length(Expects) > 0 ->
+ {reply, {mismatch, format_missing_expectations(Expects, Mod)}, 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) ->
+ timer:sleep(10),
+ {stop, normal, 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, #mockstate{old_code={Module, Binary, Filename}}) ->
+ code:purge(Module),
+ code:delete(Module),
+ code:load_binary(Module, Filename, Binary),
+ timer:sleep(10).
+
+%%--------------------------------------------------------------------
+%% @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
+%%--------------------------------------------------------------------
+format_missing_expectations(Expects, Mod) ->
+ format_missing_expectations(Expects, Mod, []).
+
+format_missing_expectations([], _, Msgs) ->
+ lists:reverse(Msgs);
+
+format_missing_expectations([{Function, _Args, _Ret, Times, Called}|Expects], Mod, Msgs) ->
+ Msgs1 = [?fmt("expected ~p:~p to be called ~p times but was called ~p", [Mod,Function,Times,Called])|Msgs],
+ format_missing_expectations(Expects, Mod, Msgs1).
+
+add_expectation(Function, Args, Ret, Times, Expects) ->
+ Expects ++ [{Function, Args, Ret, Times, 0}].
+
+match_expectation(Function, Args, Expectations) ->
+ match_expectation(Function, Args, Expectations, []).
+
+match_expectation(_Function, _Args, [], _Rest) ->
+ unmatched;
+
+match_expectation(Function, Args, [{Function, 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) ++ [{Function, Matcher, Ret, MaxTimes, Invoked+1}] ++ Expects}
+ end;
+ false -> match_expectation(Function, Args, Expects, [{Function,Matcher,Ret,MaxTimes,Invoked}|Rest])
+ end;
+
+match_expectation(Function, Args, [Expect|Expects], Rest) ->
+ match_expectation(Function, Args, Expects, [Expect|Rest]).
+
+prepare_return(Args, Ret, Invoked) when is_function(Ret) ->
+ Ret(Args, Invoked);
+
+prepare_return(_Args, Ret, _Invoked) ->
+ Ret.
+
+replace_code(Module) ->
+ Info = Module:module_info(),
+ Exports = get_exports(Info),
+ unload_code(Module),
+ NewFunctions = generate_functions(Module, Exports),
+ Forms = [
+ {attribute,1,module,Module},
+ {attribute,2,export,Exports}
+ ] ++ NewFunctions,
+ case compile:forms(Forms, [binary]) of
+ {ok, Module, Binary} -> case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Binary) of
+ {module, Module} -> ok;
+ {error, Reason} -> {error, Reason}
+ end;
+ error -> {error, "An undefined error happened when compiling."};
+ {error, Errors, Warnings} -> {error, Errors ++ Warnings}
+ end.
+
+unload_code(Module) ->
+ code:purge(Module),
+ code:delete(Module).
+
+get_exports(Info) ->
+ get_exports(Info, []).
+
+get_exports(Info, Acc) ->
+ case lists:keytake(exports, 1, Info) of
+ {value, {exports, Exports}, ModInfo} ->
+ get_exports(ModInfo, Acc ++ lists:filter(fun({module_info, _}) -> false; (_) -> true end, Exports));
+ _ -> Acc
+ end.
+
+%% stub_function_loop(Fun) ->
+%% receive
+%% {Ref, Pid, Args} ->
+%% ?debugFmt("received {~p,~p,~p}", [Ref, Pid, Args]),
+%% Ret = (catch Fun(Args) ),
+%% ?debugFmt("sending {~p,~p}", [Ref,Ret]),
+%% Pid ! {Ref, Ret},
+%% stub_function_loop(Fun)
+%% end.
+
+% Function -> {function, Lineno, Name, Arity, [Clauses]}
+% Clause -> {clause, Lineno, [Variables], [Guards], [Expressions]}
+% Variable -> {var, Line, Name}
+%
+generate_functions(Module, Exports) ->
+ generate_functions(Module, Exports, []).
+
+generate_functions(_Module, [], FunctionForms) ->
+ lists:reverse(FunctionForms);
+
+generate_functions(Module, [{Name,Arity}|Exports], FunctionForms) ->
+ generate_functions(Module, Exports, [generate_function(Module, Name, Arity)|FunctionForms]).
+
+generate_function(Module, Name, Arity) ->
+ {function, 1, Name, Arity, [{clause, 1, generate_variables(Arity), [], generate_expression(mock, proxy_call, Module, Name, Arity)}]}.
+
+generate_variables(0) -> [];
+generate_variables(Arity) ->
+ lists:map(fun(N) ->
+ {var, 1, list_to_atom(lists:concat(['Arg', N]))}
+ end, lists:seq(1, Arity)).
+
+generate_expression(M, F, Module, Name, 0) ->
+ [{call,1,{remote,1,{atom,1,M},{atom,1,F}}, [{atom,1,Module}, {atom,1,Name}]}];
+generate_expression(M, F, Module, Name, Arity) ->
+ [{call,1,{remote,1,{atom,1,M},{atom,1,F}}, [{atom,1,Module}, {atom,1,Name}, {tuple,1,lists:map(fun(N) ->
+ {var, 1, list_to_atom(lists:concat(['Arg', N]))}
+ end, lists:seq(1, Arity))}]}].
+
+mod_to_name(Module) ->
+ list_to_atom(lists:concat([mock_, Module])).
+
+%% replace_function(FF, Forms) ->
+%% replace_function(FF, Forms, []).
+
+%% replace_function(FF, [], Ret) ->
+%% [FF|lists:reverse(Ret)];
+
+%% replace_function({function,_,Name,Arity,Clauses}, [{function,Line,Name,Arity,_}|Forms], Ret) ->
+%% lists:reverse(Ret) ++ [{function,Line,Name,Arity,Clauses}|Forms];
+
+%% replace_function(FF, [FD|Forms], Ret) ->
+%% replace_function(FF, Forms, [FD|Ret]).