Skip to content

Commit

Permalink
Record cover data on passtrhough calls
Browse files Browse the repository at this point in the history
If a module with cover instrumentation is mocked then make sure to
instrument any passtrhough calls.  This way coverage analysis is still
available for passtrhough calls.

This functionality can be disabled via the `no_passthrough_cover`
option.

Implementation Details
----------------------

This is coded for the specific use case of running eunit tests from
rebar with coverage analysis enabled.  Previously, if just some of the
functions in a src module were mocked then all coverage analyis on
said module, while it was mocked, was lost.

1. check if module is instrumented for coverage

2. compile `<name>_meck_original` (thus known as `OriginalMod`) and
   then cover compile it

3. let meck do it's thing

4. during unload/termination of mocked module make sure to first
   export the cover data collected on `OriginalMod`, after exporting
   modify the data to use the real modules name
   (e.g. `foo_meck_original` -> `foo`), this way the data can be
   imported and counted against `foo`

5. let meck do it's usual cleanup

6. during restore import the `OriginalMod` coverage data

Tricks are played with cover's BEAM code so that private functions can
be called.  This was done to avoid creating temporary files and
copy/pasting code from cover.
  • Loading branch information
rzezeski committed Feb 27, 2012
1 parent 850fbff commit 43571a4
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 20 deletions.
81 changes: 70 additions & 11 deletions src/meck.erl
Expand Up @@ -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) ->
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
109 changes: 109 additions & 0 deletions 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.

18 changes: 14 additions & 4 deletions src/meck_mod.erl
Expand Up @@ -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]).
Expand All @@ -32,6 +33,7 @@
%% Types
-type erlang_form() :: term().
-type compile_options() :: [term()].
-type export() :: {atom(), byte()}.

%%==============================================================================
%% Interface exports
Expand All @@ -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
Expand All @@ -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.
Expand Down
54 changes: 49 additions & 5 deletions test/meck_tests.erl
Expand Up @@ -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),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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,
Expand All @@ -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}) ->
Expand All @@ -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) ->
Expand All @@ -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),
Expand Down Expand Up @@ -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}}.

Expand Down

0 comments on commit 43571a4

Please sign in to comment.