Skip to content

Commit

Permalink
Merge pull request eproxus#56 from rzezeski/rz-passthrough-cover
Browse files Browse the repository at this point in the history
Record cover data on passtrhough calls
  • Loading branch information
eproxus committed Feb 28, 2012
2 parents 850fbff + 43571a4 commit 2252aa8
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 2252aa8

Please sign in to comment.