Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rz passthrough cover #56

Merged
merged 1 commit into from
Feb 28, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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