%%============================================================================== %% Copyright 2011 Adam Lindberg & Erlang Solutions Ltd. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%============================================================================== %% @author Adam Lindberg %% @copyright 2011, Adam Lindberg & Erlang Solutions Ltd %% @doc Module mocking library for Erlang. -module(meck). -behaviour(gen_server). %% Interface exports -export([new/1]). -export([new/2]). -export([expect/3]). -export([expect/4]). -export([sequence/4]). -export([loop/4]). -export([delete/3]). -export([exception/2]). -export([passthrough/1]). -export([history/1]). -export([history/2]). -export([validate/1]). -export([unload/0]). -export([unload/1]). -export([called/3]). -export([called/4]). -export([num_calls/3]). -export([num_calls/4]). %% Callback exports -export([init/1]). -export([handle_call/3]). -export([handle_cast/2]). -export([handle_info/2]). -export([terminate/2]). -export([code_change/3]). -export([exec/5]). %% Types %% @type meck_mfa() = {Mod::atom(), Func::atom(), Args::list(term())}. %% Module, function and arguments that the mock module got called with. -type meck_mfa() :: {Mod::atom(), Func::atom(), Args::[term()]}. %% @type history() = [{pid(), meck_mfa(), Result::term()} %% | {pid(), meck_mfa(), Class:: exit | error | throw, %% Reason::term(), Stacktrace::list(mfa())}]. %% History is a list of either successful function calls with a returned %% result or function calls that resulted in an exception with a type, %% reason and a stack trace. Each tuple begins with the pid of the process %% that made the call to the function. -type history() :: [{pid(), meck_mfa(), Result::term()} | {pid(), meck_mfa(), Class:: exit | error | throw, Reason::term(), Stacktrace::[mfa()]}]. %% Records -record(state, {mod :: atom(), expects :: dict(), valid = true :: boolean(), history = [] :: history(), original :: term(), was_sticky :: boolean()}). %% Includes -include("meck_abstract.hrl"). %%============================================================================== %% Interface exports %%============================================================================== %% @spec new(Mod:: atom() | list(atom())) -> ok %% @equiv new(Mod, []) -spec new(Mod:: atom() | [atom()]) -> ok. new(Mod) when is_atom(Mod) -> new(Mod, []); new(Mod) when is_list(Mod) -> lists:foreach(fun new/1, Mod), ok. %% @spec new(Mod:: atom() | list(atom()), Options::list(term())) -> ok %% @doc Creates new mocked module(s). %% %% This replaces the current version (if any) of the modules in `Mod' %% with an empty module. %% %% Since this library is intended to use from test code, this %% function links a process for each mock to the calling process. %% %% The valid options are: %%
%%
`passthrough'
Retains the original functions, if not %% mocked by meck.
%%
`no_link'
Does not link the meck process to the caller %% process (needed for using meck in rpc calls). %%
%%
`unstick'
Unstick the module to be mocked (e.g. needed %% for using meck with kernel and stdlib modules). %%
%%
`no_passthrough_cover'
If cover is enabled on the module to be %% mocked then meck will continue to %% capture coverage on passthrough calls. %% This option allows you to disable that %% feature if it causes problems. %%
%%
-spec new(Mod:: atom() | [atom()], Options::[term()]) -> ok. new(Mod, Options) when is_atom(Mod), is_list(Options) -> case start(Mod, Options) of {ok, _Pid} -> ok; {error, Reason} -> erlang:error(Reason, [Mod, Options]) end; new(Mod, Options) when is_list(Mod) -> lists:foreach(fun(M) -> new(M, Options) end, Mod), ok. %% @spec expect(Mod:: atom() | list(atom()), Func::atom(), Expect::fun()) -> ok %% @doc Add expectation for a function `Func' to the mocked modules `Mod'. %% %% An expectation is a fun that is executed whenever the function %% `Func' is called. %% %% It affects the validation status of the mocked module(s). If an %% expectation is called with the wrong number of arguments or invalid %% arguments the mock module(s) is invalidated. It is also invalidated if %% an unexpected exception occurs. -spec expect(Mod:: atom() | [atom()], Func::atom(), Expect::fun()) -> ok. expect(Mod, Func, Expect) when is_atom(Mod), is_atom(Func), is_function(Expect) -> call(Mod, {expect, Func, Expect}); expect(Mod, Func, Expect) when is_list(Mod) -> lists:foreach(fun(M) -> expect(M, Func, Expect) end, Mod), ok. %% @spec expect(Mod:: atom() | list(atom()), Func::atom(), %% Arity::pos_integer(), Result::term()) -> ok %% @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'. %% %% @see expect/3. -spec expect(Mod:: atom() | [atom()], Func::atom(), Arity::pos_integer(), Result::term()) -> ok. expect(Mod, Func, Arity, Result) when is_atom(Mod), is_atom(Func), is_integer(Arity), Arity >= 0 -> valid_expect(Mod, Func, Arity), call(Mod, {expect, Func, Arity, Result}); expect(Mod, Func, Arity, Result) when is_list(Mod) -> lists:foreach(fun(M) -> expect(M, Func, Arity, Result) end, Mod), ok. %% @spec sequence(Mod:: atom() | list(atom()), Func::atom(), %% Arity::pos_integer(), Sequence::[term()]) -> ok %% @doc Adds an expectation which returns a value from `Sequence' %% until exhausted. %% %% This creates an expectation which takes `Arity' number of arguments %% and returns one element from `Sequence' at a time. Thus, calls to %% this expect will exhaust the list of return values in order until %% the last value is reached. That value is then returned for all %% subsequent calls. -spec sequence(Mod:: atom() | [atom()], Func::atom(), Arity::pos_integer(), Sequence::[term()]) -> ok. sequence(Mod, Func, Arity, Sequence) when is_atom(Mod), is_atom(Func), is_integer(Arity), Arity >= 0 -> call(Mod, {sequence, Func, Arity, Sequence}); sequence(Mod, Func, Arity, Sequence) when is_list(Mod) -> lists:foreach(fun(M) -> sequence(M, Func, Arity, Sequence) end, Mod), ok. %% @spec loop(Mod:: atom() | list(atom()), Func::atom(), %% Arity::pos_integer(), Loop::[term()]) -> ok %% @doc Adds an expectation which returns a value from `Loop' %% infinitely. %% %% This creates an expectation which takes `Arity' number of arguments %% and returns one element from `Loop' at a time. Thus, calls to this %% expect will return one element at a time from the list and will %% restart at the first element when the end is reached. -spec loop(Mod:: atom() | [atom()], Func::atom(), Arity::pos_integer(), Loop::[term()]) -> ok. loop(Mod, Func, Arity, Loop) when is_atom(Mod), is_atom(Func), is_integer(Arity), Arity >= 0 -> call(Mod, {loop, Func, Arity, Loop}); loop(Mod, Func, Arity, Loop) when is_list(Mod) -> lists:foreach(fun(M) -> loop(M, Func, Arity, Loop) end, Mod), ok. %% @spec delete(Mod:: atom() | list(atom()), Func::atom(), %% Arity::pos_integer()) -> ok %% @doc Deletes an expectation. %% %% Deletes the expectation for the function `Func' with the matching %% arity `Arity'. -spec delete(Mod:: atom() | [atom()], Func::atom(), Arity::pos_integer()) -> ok. delete(Mod, Func, Arity) when is_atom(Mod), is_atom(Func), Arity >= 0 -> call(Mod, {delete, Func, Arity}); delete(Mod, Func, Arity) when is_list(Mod) -> lists:foreach(fun(M) -> delete(M, Func, Arity) end, Mod), ok. %% @spec exception(Class:: throw | error | exit, Reason::term()) -> no_return() %% @doc Throws an expected exception inside an expect fun. %% %% This exception will get thrown without invalidating the mocked %% module. That is, the code using the mocked module is expected to %% handle this exception. %% %% Note: this code should only be used inside an expect fun. -spec exception(Class:: throw | error | exit, Reason::term()) -> no_return(). exception(Class, Reason) when Class == throw; Class == error; Class == exit -> throw(mock_exception_fun(Class, Reason)). %% @spec passthrough(Args::list(term())) -> no_return() %% @doc Calls the original function (if existing) inside an expectation fun. %% %% This call does not return, thus everything after this call inside %% an expectation fun will be ignored. %% %% Note: this code should only be used inside an expect fun. -spec passthrough(Args::[term()]) -> no_return(). passthrough(Args) -> throw(passthrough_fun(Args)). %% @spec validate(Mod:: atom() | list(atom())) -> boolean() %% @doc Validate the state of the mock module(s). %% %% The function returns `true' if the mocked module(s) has been used %% according to its expectations. It returns `false' if a call has %% failed in some way. Reasons for failure are wrong number of %% arguments or non-existing function (undef), wrong arguments %% (function clause) or unexpected exceptions. %% %% Use the {@link history/1} or {@link history/2} function to analyze errors. -spec validate(Mod:: atom() | [atom()]) -> boolean(). validate(Mod) when is_atom(Mod) -> call(Mod, validate); validate(Mod) when is_list(Mod) -> not lists:member(false, [validate(M) || M <- Mod]). %% @spec history(Mod::atom()) -> history() %% @doc Return the call history of the mocked module for all processes. %% %% @equiv history(Mod, '_') -spec history(Mod::atom()) -> history(). history(Mod) when is_atom(Mod) -> call(Mod, history). %% @spec history(Mod::atom(), Pid::pid()) -> history() %% @doc Return the call history of the mocked module for the specified process. %% %% Returns a list of calls to the mocked module and their results for %% the specified `Pid'. Results can be either normal Erlang terms or %% exceptions that occurred. %% %% @see history/1 %% @see called/3 %% @see called/4 %% @see num_calls/3 %% @see num_calls/4 -spec history(Mod::atom(), Pid:: pid() | '_') -> history(). history(Mod, Pid) when is_atom(Mod), is_pid(Pid) orelse Pid == '_' -> match_history(match_mfa('_', Pid), call(Mod, history)). %% @spec unload() -> list(atom()) %% @doc Unloads all mocked modules from memory. %% %% The function returns the list of mocked modules that were unloaded %% in the process. -spec unload() -> [atom()]. unload() -> lists:foldl(fun unload_if_mocked/2, [], registered()). %% @spec unload(Mod:: atom() | list(atom())) -> ok %% @doc Unload a mocked module or a list of mocked modules. %% %% This will purge and delete the module(s) from the Erlang virtual %% machine. If the mocked module(s) replaced an existing module, this %% module will still be in the Erlang load path and can be loaded %% manually or when called. -spec unload(Mods:: atom() | [atom()]) -> ok. unload(Mod) when is_atom(Mod) -> call(Mod, stop), wait_for_exit(Mod); unload(Mods) when is_list(Mods) -> lists:foreach(fun unload/1, Mods), ok. %% @spec called(Mod:: atom(), Fun:: atom(), Args:: list(term())) -> boolean() %% @doc Returns whether `Mod:Func' has been called with `Args'. %% %% @equiv called(Mod, Fun, Args, '_') called(Mod, Fun, Args) -> has_call({Mod, Fun, Args}, meck:history(Mod)). %% @spec called(Mod:: atom(), Fun:: atom(), Args:: list(term()), %% Pid::pid()) -> boolean() %% @doc Returns whether `Pid' has called `Mod:Func' with `Args'. %% %% This will check the history for the module, `Mod', to determine %% whether process `Pid' call the function, `Fun', with arguments, `Args'. If %% so, this function returns true, otherwise false. %% %% Wildcards can be used, at any level in any term, by using the underscore %% atom: ``'_' '' %% %% @see called/3 -spec called(Mod::atom(), Fun::atom(), Args::list(), Pid::pid()) -> boolean(). called(Mod, Fun, Args, Pid) -> has_call({Mod, Fun, Args}, meck:history(Mod, Pid)). %% @spec num_calls(Mod:: atom(), Fun:: atom(), Args:: list(term())) %% -> non_neg_integer() %% @doc Returns the number of times `Mod:Func' has been called with `Args'. %% %% @equiv num_calls(Mod, Fun, Args, '_') num_calls(Mod, Fun, Args) -> num_calls({Mod, Fun, Args}, meck:history(Mod)). %% @spec num_calls(Mod:: atom(), Fun:: atom(), Args:: list(term()), %% Pid::pid()) -> non_neg_integer() %% @doc Returns the number of times process `Pid' has called `Mod:Func' %% with `Args'. %% %% This will check the history for the module, `Mod', to determine how %% many times process `Pid' has called the function, `Fun', with %% arguments, `Args' and returns the result. %% %% @see num_calls/3 -spec num_calls(Mod::atom(), Fun::atom(), Args::list(), Pid::pid()) -> non_neg_integer(). num_calls(Mod, Fun, Args, Pid) -> num_calls({Mod, Fun, Args}, meck:history(Mod, Pid)). %%============================================================================== %% Callback functions %%============================================================================== %% @hidden init([Mod, Options]) -> WasSticky = case proplists:is_defined(unstick, Options) of true -> {module, Mod} = code:ensure_loaded(Mod), unstick_original(Mod); _ -> false end, NoPassCover = proplists:get_bool(no_passthrough_cover, Options), Original = backup_original(Mod, NoPassCover), process_flag(trap_exit, true), Expects = init_expects(Mod, Options), try _Bin = meck_mod:compile_and_load_forms(to_forms(Mod, Expects)), {ok, #state{mod = Mod, expects = Expects, original = Original, was_sticky = WasSticky}} catch exit:{error_loading_module, Mod, sticky_directory} -> {stop, module_is_sticky} end. %% @hidden handle_call({get_expect, Func, Arity}, _From, S) -> {Expect, NewExpects} = get_expect(S#state.expects, Func, Arity), {reply, Expect, S#state{expects = NewExpects}}; handle_call({expect, Func, Expect}, _From, S) -> NewExpects = store_expect(S#state.mod, Func, Expect, S#state.expects), {reply, ok, S#state{expects = NewExpects}}; handle_call({expect, Func, Arity, Result}, _From, S) -> NewExpects = store_expect(S#state.mod, Func, {anon, Arity, Result}, S#state.expects), {reply, ok, S#state{expects = NewExpects}}; handle_call({sequence, Func, Arity, Sequence}, _From, S) -> NewExpects = store_expect(S#state.mod, Func, {sequence, Arity, Sequence}, S#state.expects), {reply, ok, S#state{expects = NewExpects}}; handle_call({loop, Func, Arity, Loop}, _From, S) -> NewExpects = store_expect(S#state.mod, Func, {loop, Arity, Loop, Loop}, S#state.expects), {reply, ok, S#state{expects = NewExpects}}; handle_call({delete, Func, Arity}, _From, S) -> NewExpects = delete_expect(S#state.mod, Func, Arity, S#state.expects), {reply, ok, S#state{expects = NewExpects}}; handle_call(history, _From, S) -> {reply, lists:reverse(S#state.history), S}; handle_call(invalidate, _From, S) -> {reply, ok, S#state{valid = false}}; handle_call(validate, _From, S) -> {reply, S#state.valid, S}; handle_call(stop, _From, S) -> {stop, normal, ok, S}. %% @hidden handle_cast({add_history, Item}, S) -> {noreply, S#state{history = [Item| S#state.history]}}; handle_cast(_Msg, S) -> {noreply, S}. %% @hidden handle_info(_Info, S) -> {noreply, S}. %% @hidden terminate(_Reason, #state{mod = Mod, original = OriginalState, was_sticky = WasSticky}) -> export_original_cover(Mod, OriginalState), cleanup(Mod), restore_original(Mod, OriginalState, WasSticky), ok. %% @hidden code_change(_OldVsn, S, _Extra) -> {ok, S}. %% @hidden exec(Pid, Mod, Func, Arity, Args) -> Expect = call(Mod, {get_expect, Func, Arity}), try Result = call_expect(Mod, Func, Expect, Args), add_history(Pid, Mod, Func, Args, Result), Result catch throw:Fun when is_function(Fun) -> case is_mock_exception(Fun) of true -> handle_mock_exception(Pid, Mod, Func, Fun, Args); false -> invalidate_and_raise(Pid, Mod, Func, Args, throw, Fun) end; Class:Reason -> invalidate_and_raise(Pid, Mod, Func, Args, Class, Reason) end. %%============================================================================== %% Internal functions %%============================================================================== %% --- Process functions ------------------------------------------------------- start(Mod, Options) -> case proplists:is_defined(no_link, Options) of true -> start(start, Mod, Options); false -> start(start_link, Mod, Options) end. start(Func, Mod, Options) -> gen_server:Func({local, proc_name(Mod)}, ?MODULE, [Mod, Options], []). cast(Mod, Msg) -> gen_server(cast, Mod, Msg). call(Mod, Msg) -> gen_server(call, Mod, Msg). gen_server(Func, Mod, Msg) -> Name = proc_name(Mod), try gen_server:Func(Name, Msg) catch exit:_Reason -> erlang:error({not_mocked, Mod}) end. proc_name(Name) -> list_to_atom(atom_to_list(Name) ++ "_meck"). original_name(Name) -> list_to_atom(atom_to_list(Name) ++ "_meck_original"). wait_for_exit(Mod) -> MonitorRef = erlang:monitor(process, proc_name(Mod)), receive {'DOWN', MonitorRef, _Type, _Object, _Info} -> ok end. unload_if_mocked(P, L) when is_atom(P) -> unload_if_mocked(atom_to_list(P), L); unload_if_mocked(P, L) when length(P) > 5 -> case lists:split(length(P) - 5, P) of {Name, "_meck"} -> Mocked = list_to_existing_atom(Name), try unload(Mocked) catch error:{not_mocked, Mocked} -> ok end, [Mocked|L]; _Else -> L end; unload_if_mocked(_P, L) -> L. %% --- Mock handling ----------------------------------------------------------- valid_expect(M, F, A) -> case expect_type(M, F, A) of autogenerated -> erlang:error({cannot_mock_autogenerated, {M, F, A}}); builtin -> erlang:error({cannot_mock_builtin, {M, F, A}}); normal -> ok end. init_expects(Mod, Options) -> case proplists:get_value(passthrough, Options, false) andalso exists(Mod) of true -> dict:from_list([{FA, passthrough} || FA <- exports(Mod)]); _ -> dict:new() end. get_expect(Expects, Func, Arity) -> case e_fetch(Expects, Func, Arity) of {sequence, Arity, [Result]} -> {{sequence, Arity, Result}, Expects}; {sequence, Arity, [Result|Rest]} -> {{sequence, Arity, Result}, e_store(Expects, Func, {sequence, Arity, Rest})}; {loop, Arity, [Result], Loop} -> {{loop, Arity, Result}, e_store(Expects, Func, {loop, Arity, Loop, Loop})}; {loop, Arity, [Result|Rest], Loop} -> {{loop, Arity, Result}, e_store(Expects, Func, {loop, Arity, Rest, Loop})}; Other -> {Other, Expects} end. store_expect(Mod, Func, Expect, Expects) -> change_expects(fun e_store/3, Mod, Func, Expect, Expects). delete_expect(Mod, Func, Arity, Expects) -> change_expects(fun e_delete/3, Mod, Func, Arity, Expects). change_expects(Op, Mod, Func, Value, Expects) -> NewExpects = Op(Expects, Func, Value), _Bin = meck_mod:compile_and_load_forms(to_forms(Mod, NewExpects)), NewExpects. e_store(Expects, Func, Expect) -> dict:store({Func, arity(Expect)}, Expect, Expects). e_fetch(Expects, Func, Arity) -> dict:fetch({Func, Arity}, Expects). e_delete(Expects, Func, Arity) -> dict:erase({Func, Arity}, Expects). %% --- Code generation --------------------------------------------------------- func(Mod, {Func, Arity}, {anon, Arity, Result}) -> case contains_opaque(Result) of true -> func_exec(Mod, Func, Arity); false -> func_native(Mod, Func, Arity, Result) end; func(Mod, {Func, Arity}, _Expect) -> func_exec(Mod, Func, Arity). func_exec(Mod, Func, Arity) -> Args = args(Arity), ?function(Func, Arity, [?clause(Args, [?call(?MODULE, exec, [?call(erlang, self, []), ?atom(Mod), ?atom(Func), ?integer(Arity), list(Args)])])]). func_native(Mod, Func, Arity, Result) -> Args = args(Arity), AbsResult = erl_parse:abstract(Result), ?function( Func, Arity, [?clause( Args, [?call(gen_server, cast, [?atom(proc_name(Mod)), ?tuple([?atom(add_history), ?tuple([?call(erlang, self, []), ?tuple([?atom(Mod), ?atom(Func), list(Args)]), AbsResult])])]), AbsResult])]). contains_opaque(Term) when is_pid(Term); is_port(Term); is_function(Term) -> true; contains_opaque(Term) when is_list(Term) -> lists:any(fun contains_opaque/1, Term); contains_opaque(Term) when is_tuple(Term) -> lists:any(fun contains_opaque/1, tuple_to_list(Term)); contains_opaque(_Term) -> false. to_forms(Mod, Expects) -> {Exports, Functions} = functions(Mod, Expects), [?attribute(module, Mod)] ++ Exports ++ Functions. functions(Mod, Expects) -> dict:fold(fun(Export, Expect, {Exports, Functions}) -> {[?attribute(export, [Export])|Exports], [func(Mod, Export, Expect)|Functions]} end, {[], []}, Expects). args(0) -> []; args(Arity) -> [?var(var_name(N)) || N <- lists:seq(1, Arity)]. list([]) -> {nil, ?LINE}; list([H|T]) -> {cons, ?LINE, H, list(T)}. var_name(A) -> list_to_atom("A"++integer_to_list(A)). arity({anon, Arity, _Result}) -> Arity; arity({sequence, Arity, _Sequence}) -> Arity; arity({loop, Arity, _Current, _Loop}) -> Arity; arity(Fun) when is_function(Fun) -> {arity, Arity} = erlang:fun_info(Fun, arity), Arity. %% --- Execution utilities ----------------------------------------------------- is_local_function(Fun) -> {module, Module} = erlang:fun_info(Fun, module), ?MODULE == Module. handle_mock_exception(Pid, Mod, Func, Fun, Args) -> case Fun() of {exception, Class, Reason} -> % exception created with the mock:exception function, % do not invalidate Mod raise(Pid, Mod, Func, Args, Class, Reason); {passthrough, PassthroughArgs} -> % call_original(Args) called from mock function Result = apply(original_name(Mod), Func, PassthroughArgs), add_history(Pid, Mod, Func, PassthroughArgs, Result), Result end. -spec invalidate_and_raise(_, _, _, _, _, _) -> no_return(). invalidate_and_raise(Pid, Mod, Func, Args, Class, Reason) -> call(Mod, invalidate), raise(Pid, Mod, Func, Args, Class, Reason). raise(Pid, Mod, Func, Args, Class, Reason) -> Stacktrace = inject(Mod, Func, Args, erlang:get_stacktrace()), add_history(Pid, Mod, Func, Args, Class, Reason, Stacktrace), erlang:raise(Class, Reason, Stacktrace). mock_exception_fun(Class, Reason) -> fun() -> {exception, Class, Reason} end. passthrough_fun(Args) -> fun() -> {passthrough, Args} end. call_expect(_Mod, _Func, {_Type, Arity, Return}, VarList) when Arity == length(VarList) -> Return; call_expect(Mod, Func, passthrough, VarList) -> apply(original_name(Mod), Func, VarList); call_expect(_Mod, _Func, Fun, VarList) when is_function(Fun) -> apply(Fun, VarList). inject(_Mod, _Func, _Args, []) -> []; inject(Mod, Func, Args, [{meck, exec, _Arity} = Meck|Stack]) -> [Meck, {Mod, Func, Args}|Stack]; inject(Mod, Func, Args, [{meck, exec, _Arity, _Location} = Meck|Stack]) -> [Meck, {Mod, Func, Args}|Stack]; inject(Mod, Func, Args, [H|Stack]) -> [H|inject(Mod, Func, Args, Stack)]. is_mock_exception(Fun) -> is_local_function(Fun). %% --- Original module handling ------------------------------------------------ backup_original(Module, NoPassCover) -> Cover = get_cover_state(Module), try Forms = meck_mod:abstract_code(meck_mod:beam_file(Module)), NewName = original_name(Module), CompileOpts = meck_mod:compile_options(meck_mod:beam_file(Module)), Renamed = meck_mod:rename_module(Forms, NewName), Binary = meck_mod:compile_and_load_forms(Renamed, CompileOpts), %% At this point we care about `Binary' if and only if we want %% to recompile it to enable cover on the original module code %% so that we can still collect cover stats on functions that %% have not been mocked. Below are the different values %% passed back along with `Cover'. %% %% `no_passthrough_cover' - there is no coverage on the %% original module OR passthrough coverage has been disabled %% via the `no_passthrough_cover' option %% %% `no_binary' - something went wrong while trying to compile %% the original module in `backup_original' %% %% Binary - a `binary()' of the compiled code for the original %% module that is being mocked, this needs to be passed around %% so that it can be passed to Cover later. There is no way %% to use the code server to access this binary without first %% saving it to disk. Instead, it's passed around as state. if (Cover == false) orelse NoPassCover -> Binary2 = no_passthrough_cover; true -> Binary2 = Binary, meck_cover:compile_beam(NewName, Binary2) end, {Cover, Binary2} catch throw:{object_code_not_found, _Module} -> {Cover, no_binary}; % TODO: What to do here? throw:no_abstract_code -> {Cover, no_binary} % TODO: What to do here? end. restore_original(Mod, {false, _}, WasSticky) -> restick_original(Mod, WasSticky), ok; restore_original(Mod, OriginalState={{File, Data, Options},_}, WasSticky) -> case filename:extension(File) of ".erl" -> {ok, Mod} = cover:compile_module(File, Options); ".beam" -> cover:compile_beam(File) end, restick_original(Mod, WasSticky), import_original_cover(Mod, OriginalState), ok = cover:import(Data), ok = file:delete(Data), ok. %% @doc Import the cover data for `_meck_original' but since it %% was modified by `export_original_cover' it will count towards %% `'. import_original_cover(Mod, {_,Bin}) when is_binary(Bin) -> OriginalData = atom_to_list(original_name(Mod)) ++ ".coverdata", ok = cover:import(OriginalData), ok = file:delete(OriginalData); import_original_cover(_, _) -> ok. %% @doc Export the cover data for `_meck_original' and modify %% the data so it can be imported under `'. export_original_cover(Mod, {_, Bin}) when is_binary(Bin) -> OriginalMod = original_name(Mod), File = atom_to_list(OriginalMod) ++ ".coverdata", ok = cover:export(File, OriginalMod), ok = meck_cover:rename_module(File, Mod); export_original_cover(_, _) -> ok. unstick_original(Module) -> unstick_original(Module, code:is_sticky(Module)). unstick_original(Module, true) -> code:unstick_mod(Module); unstick_original(_,_) -> false. restick_original(Module, true) -> code:stick_mod(Module), {module, Module} = code:ensure_loaded(Module), ok; restick_original(_,_) -> ok. get_cover_state(Module) -> get_cover_state(Module, cover:is_compiled(Module)). get_cover_state(Module, {file, File}) -> Data = atom_to_list(Module) ++ ".coverdata", ok = cover:export(Data, Module), CompileOptions = try meck_mod:compile_options(meck_mod:beam_file(Module)) catch throw:{object_code_not_found, _Module} -> [] end, {File, Data, CompileOptions}; get_cover_state(_Module, _IsCompiled) -> false. exists(Module) -> code:which(Module) /= non_existing. exports(M) -> [ FA || FA = {F, A} <- M:module_info(exports), normal == expect_type(M, F, A)]. %% Functions we should not create expects for (auto-generated and BIFs) expect_type(_, module_info, 0) -> autogenerated; expect_type(_, module_info, 1) -> autogenerated; expect_type(M, F, A) -> expect_type(erlang:is_builtin(M, F, A)). expect_type(true) -> builtin; expect_type(false) -> normal. cleanup(Mod) -> code:purge(Mod), code:delete(Mod), code:purge(original_name(Mod)), code:delete(original_name(Mod)). %% --- History utilities ------------------------------------------------------- add_history(Pid, Mod, Func, Args, Result) -> add_history(Mod, {Pid, {Mod, Func, Args}, Result}). add_history(Pid, Mod, Func, Args, Class, Reason, Stacktrace) -> add_history(Mod, {Pid, {Mod, Func, Args}, Class, Reason, Stacktrace}). add_history(Mod, Item) -> cast(Mod, {add_history, Item}). has_call(MFA, History) -> [] =/= match_history(match_mfa(MFA), History). num_calls(MFA, History) -> length(match_history(match_mfa(MFA), History)). match_history(MatchSpec, History) -> MS = ets:match_spec_compile(MatchSpec), ets:match_spec_run(History, MS). match_mfa(MFA) -> match_mfa(MFA, '_'). match_mfa(MFA, Pid) -> [{{Pid, MFA, '_'}, [], ['$_']}, {{Pid, MFA, '_', '_', '_'}, [], ['$_']}].