diff options
Diffstat (limited to 'test/mock.erl')
-rw-r--r-- | test/mock.erl | 322 |
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]). |