Skip to content
Browse files

Merge pull request #56 from rzezeski/rz-passthrough-cover

Record cover data on passtrhough calls
  • Loading branch information...
2 parents 850fbff + 43571a4 commit 2252aa855025174b7d4f3b3095ea67f619d125e3 Adam Lindberg committed Feb 28, 2012
Showing with 242 additions and 20 deletions.
  1. +70 −11 src/meck.erl
  2. +109 −0 src/meck_cover.erl
  3. +14 −4 src/meck_mod.erl
  4. +49 −5 test/meck_tests.erl
View
81 src/meck.erl
@@ -106,6 +106,12 @@ new(Mod) when is_list(Mod) -> lists:foreach(fun new/1, Mod), ok.
%% <dt>`unstick'</dt> <dd>Unstick the module to be mocked (e.g. needed
%% for using meck with kernel and stdlib modules).
%% </dd>
+%% <dt>`no_passthrough_cover'</dt><dd>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.
+%% </dd>
%% </dl>
-spec new(Mod:: atom() | [atom()], Options::[term()]) -> ok.
new(Mod, Options) when is_atom(Mod), is_list(Options) ->
@@ -342,7 +348,8 @@ init([Mod, Options]) ->
unstick_original(Mod);
_ -> false
end,
- Original = backup_original(Mod),
+ NoPassCover = proplists:get_bool(no_passthrough_cover, Options),
+ Original = backup_original(Mod, NoPassCover),
process_flag(trap_exit, true),
Expects = init_expects(Mod, Options),
try
@@ -395,7 +402,9 @@ handle_cast(_Msg, S) ->
handle_info(_Info, S) -> {noreply, S}.
%% @hidden
-terminate(_Reason, #state{mod = Mod, original = OriginalState, was_sticky = WasSticky}) ->
+terminate(_Reason, #state{mod = Mod, original = OriginalState,
+ was_sticky = WasSticky}) ->
+ export_original_cover(Mod, OriginalState),
cleanup(Mod),
restore_original(Mod, OriginalState, WasSticky),
ok.
@@ -652,34 +661,84 @@ is_mock_exception(Fun) -> is_local_function(Fun).
%% --- Original module handling ------------------------------------------------
-backup_original(Module) ->
+backup_original(Module, NoPassCover) ->
Cover = get_cover_state(Module),
try
Forms = meck_mod:abstract_code(meck_mod:beam_file(Module)),
NewName = original_name(Module),
- meck_mod:compile_and_load_forms(meck_mod:rename_module(Forms, NewName),
- meck_mod:compile_options(Module))
+ CompileOpts = meck_mod:compile_options(meck_mod:beam_file(Module)),
+ Binary = meck_mod:compile_and_load_forms(meck_mod:rename_module(Forms, NewName),
+ 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_passtrhough_cover;
+ true ->
+ Binary2 = Binary,
+ meck_cover:compile_beam(NewName, Binary2)
+ end,
+ {Cover, Binary2}
catch
- throw:{object_code_not_found, _Module} -> ok; % TODO: What to do here?
- throw:no_abstract_code -> ok % TODO: What to do here?
- end,
- Cover.
+ 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) ->
+restore_original(Mod, {false, _}, WasSticky) ->
restick_original(Mod, WasSticky),
ok;
-restore_original(Mod, {File, Data, Options}, WasSticky) ->
+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 `<name>_meck_original' but since it
+%% was modified by `export_original_cover' it will count towards
+%% `<name>'.
+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 `<name>_meck_original' and modify
+%% the data so it can be imported under `<name>'.
+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);
View
109 src/meck_cover.erl
@@ -0,0 +1,109 @@
+%%==============================================================================
+%% 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.
+%%==============================================================================
+
+%% @doc Module containing functions needed by meck to integrate with cover.
+
+-module(meck_cover).
+
+%% Interface exports
+-export([compile_beam/2]).
+-export([rename_module/2]).
+
+%%==============================================================================
+%% Interface exports
+%%==============================================================================
+
+%% @doc Enabled cover on `<name>_meck_original'.
+compile_beam(OriginalMod, Bin) ->
+ alter_cover(),
+ {ok, _} = cover:compile_beam(OriginalMod, Bin).
+
+%% @doc Given a cover file `File' exported by `cover:export' overwrite
+%% the module name with `Name'.
+rename_module(File, Name) ->
+ NewTerms = change_cover_mod_name(read_cover_file(File), Name),
+ write_terms(File, NewTerms),
+ ok.
+
+%%==============================================================================
+%% Internal functions
+%%==============================================================================
+
+%% @private
+%%
+%% @doc Alter the cover BEAM module to export some of it's private
+%% functions. This is done for two reasons:
+%%
+%% 1. Meck needs to alter the export analysis data on disk and
+%% therefore needs to understand this format. This is why `get_term'
+%% and `write' are exposed.
+%%
+%% 2. In order to avoid creating temporary files meck needs direct
+%% access to `compile_beam/2' which allows passing a binary.
+alter_cover() ->
+ case lists:member({compile_beam,2}, cover:module_info(exports)) of
+ true ->
+ ok;
+ false ->
+ Beam = meck_mod:beam_file(cover),
+ AbsCode = meck_mod:abstract_code(Beam),
+ Exports = [{compile_beam, 2}, {get_term, 1}, {write, 2}],
+ AbsCode2 = meck_mod:add_exports(Exports, AbsCode),
+ meck_mod:compile_and_load_forms(AbsCode2)
+ end.
+
+change_cover_mod_name(CoverTerms, Name) ->
+ {_, Terms} = lists:foldl(fun change_name_in_term/2, {Name,[]}, CoverTerms),
+ Terms.
+
+change_name_in_term({file, Mod, File}, {Name, Terms}) ->
+ Term2 = {file, Name, replace_string(File, Mod, Name)},
+ {Name, [Term2|Terms]};
+change_name_in_term({Bump={bump,_,_,_,_,_},_}=Term, {Name, Terms}) ->
+ Bump2 = setelement(2, Bump, Name),
+ Term2 = setelement(1, Term, Bump2),
+ {Name, [Term2|Terms]};
+change_name_in_term({_Mod,Clauses}, {Name, Terms}) ->
+ Clauses2 = lists:foldl(fun change_name_in_clause/2, {Name, []}, Clauses),
+ Term2 = {Name, Clauses2},
+ {Name, [Term2|Terms]}.
+
+change_name_in_clause(Clause, {Name, NewClauses}) ->
+ {Name, [setelement(1, Clause, Name)|NewClauses]}.
+
+replace_string(File, Old, New) ->
+ Old2 = atom_to_list(Old),
+ New2 = atom_to_list(New),
+ re:replace(File, Old2, New2, [{return, list}]).
+
+read_cover_file(File) ->
+ {ok, Fd} = file:open(File, [read, binary, raw]),
+ Terms = get_terms(Fd, []),
+ file:close(Fd),
+ Terms.
+
+get_terms(Fd, Terms) ->
+ case cover:get_term(Fd) of
+ eof -> Terms;
+ Term -> get_terms(Fd, [Term|Terms])
+ end.
+
+write_terms(File, Terms) ->
+ {ok, Fd} = file:open(File, [write, binary, raw]),
+ lists:map(write_term(Fd), Terms),
+ ok.
+
+write_term(Fd) ->
+ fun(Term) -> cover:write(Term, Fd) end.
+
View
18 src/meck_mod.erl
@@ -23,6 +23,7 @@
%% Interface exports
-export([abstract_code/1]).
+-export([add_exports/2]).
-export([beam_file/1]).
-export([compile_and_load_forms/1]).
-export([compile_and_load_forms/2]).
@@ -32,6 +33,7 @@
%% Types
-type erlang_form() :: term().
-type compile_options() :: [term()].
+-type export() :: {atom(), byte()}.
%%==============================================================================
%% Interface exports
@@ -46,6 +48,12 @@ abstract_code(BeamFile) ->
throw(no_abstract_code)
end.
+-spec add_exports([export()], erlang_form()) -> erlang_form().
+add_exports(Exports, AbsCode) ->
+ {attribute, Line, export, OrigExports} = lists:keyfind(export, 3, AbsCode),
+ Attr = {attribute, Line, export, OrigExports ++ Exports},
+ lists:keyreplace(export, 3, AbsCode, Attr).
+
-spec beam_file(module()) -> binary().
beam_file(Module) ->
% code:which/1 cannot be used for cover_compiled modules
@@ -54,16 +62,18 @@ beam_file(Module) ->
error -> throw({object_code_not_found, Module})
end.
--spec compile_and_load_forms(erlang_form()) -> ok.
+-spec compile_and_load_forms(erlang_form()) -> binary().
compile_and_load_forms(AbsCode) -> compile_and_load_forms(AbsCode, []).
--spec compile_and_load_forms(erlang_form(), compile_options()) -> ok.
+-spec compile_and_load_forms(erlang_form(), compile_options()) -> binary().
compile_and_load_forms(AbsCode, Opts) ->
case compile:forms(AbsCode, Opts) of
{ok, ModName, Binary} ->
- load_binary(ModName, Binary);
+ load_binary(ModName, Binary),
+ Binary;
{ok, ModName, Binary, _Warnings} ->
- load_binary(ModName, Binary);
+ load_binary(ModName, Binary),
+ Binary;
Error ->
exit({compile_forms, Error})
end.
View
54 test/meck_tests.erl
@@ -503,7 +503,7 @@ loop_multi_(Mod) ->
call_original_test() ->
false = code:purge(meck_test_module),
?assertEqual({module, meck_test_module}, code:load_file(meck_test_module)),
- ok = meck:new(meck_test_module),
+ ok = meck:new(meck_test_module, [no_passthrough_cover]),
?assertEqual({file, ""}, code:is_loaded(meck_test_module_meck_original)),
ok = meck:expect(meck_test_module, a, fun() -> c end),
ok = meck:expect(meck_test_module, b, fun() -> meck:passthrough([]) end),
@@ -543,7 +543,10 @@ passthrough_nonexisting_module_test() ->
ok = meck:unload(mymod).
passthrough_test() ->
- ok = meck:new(meck_test_module, [passthrough]),
+ passthrough_test([]).
+
+passthrough_test(Opts) ->
+ ok = meck:new(meck_test_module, [passthrough|Opts]),
ok = meck:expect(meck_test_module, a, fun() -> c end),
?assertEqual(c, meck_test_module:a()),
?assertEqual(b, meck_test_module:b()),
@@ -572,7 +575,9 @@ cover_test() ->
cover_options_test_() ->
{foreach, fun compile_options_setup/0, fun compile_options_teardown/1,
- [{with, [T]} || T <- [fun ?MODULE:cover_options_/1]]}.
+ [{with, [T]} || T <- [fun ?MODULE:cover_options_/1,
+ fun ?MODULE:cover_options_fail_/1
+ ]]}.
compile_options_setup() ->
Module = cover_test_module,
@@ -586,6 +591,8 @@ compile_options_setup() ->
compile_options_teardown({OldPath, Src, Module}) ->
file:rename(Src, join("../test/", Module, ".dontcompile")),
+ code:purge(Module),
+ code:delete(Module),
code:set_path(OldPath).
cover_options_({_OldPath, Src, Module}) ->
@@ -607,6 +614,31 @@ cover_options_({_OldPath, Src, Module}) ->
% 2 instead of 3, as above
?assertEqual({ok, {Module, {2,0}}}, cover:analyze(Module, module)).
+cover_options_fail_({_OldPath, Src, Module}) ->
+ %% This may look like the test above but there is a subtle
+ %% difference. When `cover:compile_beam' is called it squashes
+ %% compile options. This test verifies that function `b/0', which
+ %% relies on the `TEST' directive being set can still be called
+ %% after the module is meck'ed.
+ CompilerOptions = [{i, "../test/include"}, {d, 'TEST', true},
+ {outdir, "../test"}, debug_info],
+ {ok, _} = compile:file(Src, CompilerOptions),
+ ?assertEqual(CompilerOptions, meck_mod:compile_options(Module)),
+ {ok, _} = cover:compile_beam(Module),
+ ?assertEqual([], meck_mod:compile_options(Module)),
+ a = Module:a(),
+ b = Module:b(),
+ {1, 2} = Module:c(1, 2),
+ ?assertEqual({ok, {Module, {2,0}}}, cover:analyze(Module, module)),
+ ok = meck:new(Module, [passthrough]),
+ ok = meck:expect(Module, a, fun () -> c end),
+ ?assertEqual(c, Module:a()),
+ ?assertEqual(b, Module:b()),
+ ?assertEqual({1, 2}, Module:c(1, 2)),
+ ok = meck:unload(Module),
+ %% Verify passthru calls went to cover
+ ?assertEqual({ok, {Module, 4}}, cover:analyze(Module, calls, module)).
+
join(Path, Module, Ext) -> filename:join(Path, atom_to_list(Module) ++ Ext).
run_mock_no_cover_file(Module) ->
@@ -616,12 +648,23 @@ run_mock_no_cover_file(Module) ->
ok = meck:unload(Module),
?assert(not filelib:is_file(atom_to_list(Module) ++ ".coverdata")).
-cover_passthrough_test() ->
+%% @doc Verify that passthrough calls _don't_ appear in cover
+%% analysis.
+no_cover_passthrough_test() ->
{ok, _} = cover:compile("../test/meck_test_module.erl"),
{ok, {meck_test_module, {0,3}}} = cover:analyze(meck_test_module, module),
- passthrough_test(),
+ passthrough_test([no_passthrough_cover]),
{ok, {meck_test_module, {0,3}}} = cover:analyze(meck_test_module, module).
+%% @doc Verify that passthrough calls appear in cover analysis.
+cover_passthrough_test() ->
+ {ok, _} = cover:compile("../test/meck_test_module.erl"),
+ ?assertEqual({ok, {meck_test_module, {0,3}}},
+ cover:analyze(meck_test_module, module)),
+ passthrough_test([]),
+ ?assertEqual({ok, {meck_test_module, {2,1}}},
+ cover:analyze(meck_test_module, module)).
+
% @doc The mocked module is unloaded if the meck process crashes.
unload_when_crashed_test() ->
ok = meck:new(mymod),
@@ -772,6 +815,7 @@ sticky_setup() ->
{ok, _BytesCopied} = file:copy(Beam, Dest),
true = code:add_patha(Dir),
ok = code:stick_dir(Dir),
+ code:load_file(Module),
{Module, {Dir, Dest}}.

0 comments on commit 2252aa8

Please sign in to comment.
Something went wrong with that request. Please try again.