diff --git a/.gitignore b/.gitignore
index 0a68ef29..2564eedd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ tags
*.sublime-project
deps/*
.rebar
+.deps.plt
diff --git a/.scripts/tag_with_changelog.sh b/.scripts/tag_with_changelog.sh
index 173d964e..883fd606 100755
--- a/.scripts/tag_with_changelog.sh
+++ b/.scripts/tag_with_changelog.sh
@@ -30,8 +30,10 @@ git add CHANGELOG
# Update version in .app file
sed -i "" -e "s/{vsn, .*}/{vsn, \"$1\"}/g" src/meck.app.src
sed -i "" -e "s/@version .*/@version $1/g" doc/overview.edoc
+sed -i "" -e "s/version: \".*\"/version: \"$1\"/g" package.exs
git add src/meck.app.src
git add doc/overview.edoc
+git add package.exs
# Commit, tag and push
git commit -m "Version $1"
@@ -39,3 +41,5 @@ git tag -s $1 -m "Version $
$CHANGELOG"
git push && git push --tags
+MIX_EXS=package.exs mix hex.publish
+
diff --git a/.travis.yml b/.travis.yml
index 944335e7..17d668c9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,8 +3,9 @@ notifications:
email:
- hello@alind.io
otp_release:
+ - 17.4
- R16B03-1
- - R16B
- R15B03
before_script: "make get-deps"
-script: "make all"
+script: "make test"
+
diff --git a/CHANGELOG b/CHANGELOG
index 91d5f91c..8b137891 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,2 +1 @@
-- The return value from passthrough/1 can now be used in expect
- functions before returning
+
diff --git a/Makefile b/Makefile
index 18115e34..8ec58373 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
PLTFILE=$(CURDIR)/.deps.plt
-APP_DEPS=kernel stdlib eunit tools compiler
+APP_DEPS=kernel stdlib eunit tools compiler erts
ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/deps/*/ebin
REBAR="./rebar"
@@ -18,7 +18,7 @@ DIALYZER_INC=$(shell test -d include && echo '-I include') $(shell test -d deps
.PHONY: all rebuild compile doc clean test dialyzer typer get-deps clean-deps \
shell clean-plt clean-doc distclean
-all: get-deps compile test doc
+all: get-deps compile
rebuild: distclean get-deps all
diff --git a/README.md b/README.md
index 7f6f1a67..4d4fd9ea 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-[](https://github.com/eproxus/meck/releases/latest)
-[](http://travis-ci.org/eproxus/meck)
-[](https://travis-ci.org/eproxus/meck)
+[](https://github.com/eproxus/meck/releases/latest)
+[](http://travis-ci.org/eproxus/meck)
+[](https://travis-ci.org/eproxus/meck)
Meck
====
@@ -147,22 +147,33 @@ ok
Build
-----
-Meck requires `make` and [rebar][1] to build. To build Meck and run tests, go to the Meck
-directory and simply type:
+Meck requires `make` and [rebar][1] to build. To build Meck go to the Meck directory
+and simply type:
```sh
make
```
+In order to run all tests for Meck type the following command from the same directory:
+
+```sh
+make test
+```
+
Two things might seem alarming when running the tests:
1. Warnings emitted by cover
- 2. En exception printed by SASL
+ 2. An exception printed by SASL
Both are expected due to the way Erlang currently prints errors. The
important line you should look for is `All XX tests passed`, if that
appears all is correct.
+Documentation can be generated through the use of the following command:
+
+```sh
+make doc
+```
@@ -175,7 +186,7 @@ your `rebar.config` in your project root:
```erlang
{deps, [
{meck, ".*",
- {git, "https://github.com/eproxus/meck.git", {tag, "0.8"}}}
+ {git, "https://github.com/eproxus/meck.git", {tag, "0.8.3"}}}
]}.
```
@@ -187,6 +198,53 @@ environment variable.
+Caveats
+-------
+
+Meck will have trouble mocking certain modules since Meck works by
+recompiling and reloading modules. Since Erlang have a flat module
+namespace, replacing a module has to be done globally in the
+Erlang VM. This means certain modules cannot be mocked. The
+following is a non-exhaustive list of modules that can either be
+problematic to mock or not possible at all:
+
+* `erlang`
+* `os`
+* `crypto`
+* `compile`
+* `global`
+* `timer` (possible to mock, but used by some test frameworks, like Elixir's ExUnit)
+
+Also, a meck expectation set up for a function _f_ does not apply to the module-local invocation of _f_ within the mocked module.
+Consider the following module:
+```
+-module(test).
+-export([a/0, b/0, c/0]).
+
+a() ->
+ c().
+
+b() ->
+ ?MODULE:c().
+
+c() ->
+ original.
+```
+Note how the module-local call to `c/0` in `a/0` stays unchanged even though the expectation changes the externally visible behaviour of `c/0`:
+
+```
+3> meck:new(test, [passthrough]).
+ok
+4> meck:expect(test,c,0,changed).
+ok
+5> test:a().
+original
+6> test:b().
+changed
+6> test:c().
+changed
+```
+
Contribute
----------
@@ -198,33 +256,16 @@ when developing new features or fixes for meck.
Should you find yourself using Meck and have issues, comments or
feedback please [create an issue here on GitHub][4].
-Contributors:
-
-- Maxim Vladimirsky (@horkhe)
-- Ryan Zezeski (@rzezeski)
-- David Haglund (@daha)
-- Magnus Henoch (@legoscia)
-- Susan Potter (@mbbx6spp)
-- Andreas Amsenius (@adbl)
-- Anthony Molinaro (@djnym)
-- Matt Campbell (@xenolinguist)
-- Martynas Pumputis (@brb)
-- Shunichi Shinohara (@shino)
-- Miƫtek Bak
-- Henry Nystrom
-- Ward Bekker (@wardbekker)
-- Damon Richardson
-- Christopher Meiklejohn
-- Joseph Wayne Norton (@norton)
-- Erkan Yilmaz (@Erkan-Yilmaz)
-- Joe Williams (@joewilliams)
-- Russell Brown
-- Michael Klishin (@michaelklishin)
-- Magnus Klaar
+Meck has been greatly improved by [many contributors]
+(https://github.com/eproxus/meck/graphs/contributors)!
+### Donations
+
+If you or your company use Meck and find it useful, Bitcoin donations to the address `1M7pLbBpjkwxffT7kZPKhxiPGKf4eHDqtz` are greatly appreciated!
[1]: https://github.com/eproxus/meck/wiki/0.8-Release-Notes
"0.8 Release Notes"
[2]: https://github.com/hyperthunk/hamcrest-erlang "Hamcrest for Erlang"
[3]: https://github.com/basho/rebar "Rebar - A build tool for Erlang"
[4]: https://github.com/eproxus/meck/issues "Meck issues"
+
diff --git a/doc/overview.edoc b/doc/overview.edoc
index b04686e6..00c5dc49 100644
--- a/doc/overview.edoc
+++ b/doc/overview.edoc
@@ -16,7 +16,7 @@
@author Adam Lindberg
@copyright 2011, Adam Lindberg & Erlang Solutions Ltd
-@version "0.7.2"
+@version 0.8.3
@title meck, a Mocking Library for Erlang
@doc
diff --git a/package.exs b/package.exs
new file mode 100644
index 00000000..f9a02035
--- /dev/null
+++ b/package.exs
@@ -0,0 +1,45 @@
+defmodule Meck.Mixfile do
+ use Mix.Project
+
+ def project do
+ [
+ app: :meck,
+ version: "0.8.3",
+ description: description,
+ package: package,
+ deps: [],
+ ]
+ end
+
+ defp description do
+ """
+ A mocking framework for Erlang
+ """
+ end
+
+ defp package do
+ [
+ files: [
+ "Makefile",
+ "rebar.config",
+ "test.config",
+ "src",
+ "test/*.erl",
+ "test/cover_test_module.dontcompile",
+ "test/include",
+ "README.md",
+ "LICENSE",
+ "CHANGELOG",
+ ],
+ contributors: [
+ "Adam Lindberg",
+ ],
+ licenses: [
+ "Apache 2.0",
+ ],
+ links: %{
+ "GitHub" => "https://github.com/eproxus/meck",
+ },
+ ]
+ end
+end
diff --git a/rebar b/rebar
index 1b23ad86..e1bddea7 100755
Binary files a/rebar and b/rebar differ
diff --git a/rebar.config b/rebar.config
index e7e33fff..bd09e126 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,7 +1,7 @@
%% Compiler Options ===========================================================
{erl_opts, [
%% Erlang releases after 17 don't put R in front of their name, and also require dict() to be written like dict:dict()
- {platform_define, "^[0-9]+", namespaced_dicts},
+ {platform_define, "^[0-9]+", namespaced_types},
warn_export_all,
warn_export_vars,
warn_shadow_vars,
diff --git a/src/meck.erl b/src/meck.erl
index d0ab62aa..2d70315d 100644
--- a/src/meck.erl
+++ b/src/meck.erl
@@ -173,7 +173,7 @@ new(Mod) when is_list(Mod) -> lists:foreach(fun new/1, Mod), ok.
%%
%% `non_strict'
%% A mock created with this option will allow setting expectations on
-%% functions that are not exported from the mocked module. With this
+%% functions that does not exist in the mocked module. With this
%% option on it is even possible to mock non existing modules.
%%
%% `{stub_all, '{@link ret_spec()}`}'
@@ -182,6 +182,13 @@ new(Mod) when is_list(Mod) -> lists:foreach(fun new/1, Mod), ok.
%% passed in. It is possible to specify this option as just `stub_all'
%% then stubs will return atom `ok'. If used along with `passthrough'
%% then `stub_all' is ignored.
+%%
+%% `merge_expects'
+%% The expectations for the function/arity signature are merged with
+%% existing ones instead of replacing all of them each time an
+%% expectation is added. Expectations are added to the end of the
+%% function clause list, meaning that pattern matching will be performed
+%% in the order the expectations were added.
%%
-spec new(Mods, Options) -> ok when
Mods :: Mod | [Mod],
diff --git a/src/meck_code.erl b/src/meck_code.erl
index 895ded04..806a7217 100644
--- a/src/meck_code.erl
+++ b/src/meck_code.erl
@@ -28,6 +28,7 @@
-export([compile_and_load_forms/1]).
-export([compile_and_load_forms/2]).
-export([compile_options/1]).
+-export([enable_on_load/2]).
-export([rename_module/2]).
%% Types
@@ -89,6 +90,14 @@ compile_options(BeamFile) when is_binary(BeamFile) ->
compile_options(Module) ->
filter_options(proplists:get_value(options, Module:module_info(compile))).
+enable_on_load(Forms, false) ->
+ Map = fun({attribute,L,on_load,{F,A}}) -> {attribute,L,export,[{F,A}]};
+ (Other) -> Other
+ end,
+ lists:map(Map, Forms);
+enable_on_load(Forms, _) ->
+ Forms.
+
-spec rename_module(erlang_form(), module()) -> erlang_form().
rename_module([{attribute, Line, module, OldAttribute}|T], NewName) ->
case OldAttribute of
diff --git a/src/meck_code_gen.erl b/src/meck_code_gen.erl
index 6010aefb..e7e67365 100644
--- a/src/meck_code_gen.erl
+++ b/src/meck_code_gen.erl
@@ -63,7 +63,7 @@ attributes(Mod) ->
try
[?attribute(Key, Val) || {Key, Val} <-
proplists:get_value(attributes, Mod:module_info(), []),
- Key =/= vsn, Key =/= deprecated]
+ Key =/= vsn, Key =/= deprecated, Key =/= optional_callbacks]
catch
error:undef -> []
end.
@@ -144,7 +144,7 @@ var_name(A) -> list_to_atom("A"++integer_to_list(A)).
-spec exec(CallerPid::pid(), Mod::atom(), Func::atom(), Args::[any()]) ->
Result::any().
exec(Pid, Mod, Func, Args) ->
- case meck_proc:get_result_spec(Mod, Func, Args) of
+ try meck_proc:get_result_spec(Mod, Func, Args) of
undefined ->
meck_proc:invalidate(Mod),
raise(Pid, Mod, Func, Args, error, function_clause);
@@ -160,6 +160,9 @@ exec(Pid, Mod, Func, Args) ->
after
erase(?CURRENT_CALL)
end
+ catch
+ error:{not_mocked, Mod} ->
+ apply(Mod, Func, Args)
end.
-spec handle_exception(CallerPid::pid(), Mod::atom(), Func::atom(),
diff --git a/src/meck_cover.erl b/src/meck_cover.erl
index 7ef2963b..fccdfcc4 100644
--- a/src/meck_cover.erl
+++ b/src/meck_cover.erl
@@ -20,6 +20,7 @@
%% Interface exports
-export([compile_beam/2]).
-export([rename_module/2]).
+-export([dump_coverdata/1]).
%%=============================================================================
%% Interface exports
@@ -27,8 +28,8 @@
%% @doc Enabled cover on `_meck_original'.
compile_beam(OriginalMod, Bin) ->
- alter_cover(),
- {ok, _} = cover:compile_beam(OriginalMod, Bin).
+ CompileBeams = alter_cover(),
+ [{ok, _}] = CompileBeams([{OriginalMod, Bin}]).
%% @doc Given a cover file `File' exported by `cover:export' overwrite
%% the module name with `Name'.
@@ -37,6 +38,14 @@ rename_module(File, Name) ->
write_terms(File, NewTerms),
ok.
+%% @doc Dump cover data for `Mod' into a .coverdata file in the current
+%% directory. Return the absolute file name.
+dump_coverdata(Mod) ->
+ {ok, CWD} = file:get_cwd(),
+ File = filename:join(CWD, atom_to_list(Mod) ++ ".coverdata"),
+ ok = cover:export(File, Mod),
+ File.
+
%%=============================================================================
%% Internal functions
%%=============================================================================
@@ -52,19 +61,38 @@ rename_module(File, Name) ->
%%
%% 2. In order to avoid creating temporary files meck needs direct
%% access to `compile_beam/2' which allows passing a binary.
+%% In OTP 18.0 the internal API of cover changed a bit and
+%% compile_beam/2 was replaced by compile_beams/1.
alter_cover() ->
- case lists:member({compile_beam,2}, cover:module_info(exports)) of
- true ->
- ok;
- false ->
+ CoverExports = cover:module_info(exports),
+ case {lists:member({compile_beams,1}, CoverExports),
+ lists:member({compile_beam,2}, CoverExports)} of
+ {true, _} ->
+ fun cover:compile_beams/1;
+ {_, true} ->
+ fun compile_beam_wrapper/1;
+ {false, false} ->
Beam = meck_code:beam_file(cover),
AbsCode = meck_code:abstract_code(Beam),
- Exports = [{compile_beam, 2}, {get_term, 1}, {write, 2}],
+ {Exports, CompileBeams} =
+ case lists:member({analyse,0}, CoverExports) of
+ true ->
+ %% new API from OTP 18.0 on
+ {[{compile_beams, 1}, {get_term, 1}, {write, 2}],
+ fun cover:compile_beams/1};
+ false ->
+ {[{compile_beam, 2}, {get_term, 1}, {write, 2}],
+ fun compile_beam_wrapper/1}
+ end,
AbsCode2 = meck_code:add_exports(Exports, AbsCode),
_Bin = meck_code:compile_and_load_forms(AbsCode2),
- ok
+ CompileBeams
end.
+%% wrap cover's pre-18.0 internal API to simulate the new API
+compile_beam_wrapper(ModFiles) ->
+ [cover:compile_beam(Mod, Bin)||{Mod, Bin} <- ModFiles].
+
change_cover_mod_name(CoverTerms, Name) ->
{_, Terms} = lists:foldl(fun change_name_in_term/2, {Name,[]}, CoverTerms),
Terms.
diff --git a/src/meck_history.erl b/src/meck_history.erl
index 526186a3..e8c22d27 100644
--- a/src/meck_history.erl
+++ b/src/meck_history.erl
@@ -24,6 +24,7 @@
-export_type([meck_mfa/0]).
-export_type([successfull_call/0]).
-export_type([faulty_call/0]).
+-export_type([history_record/0]).
-export_type([history/0]).
-export([get_history/2]).
diff --git a/src/meck_proc.erl b/src/meck_proc.erl
index 523c6f99..db01b067 100644
--- a/src/meck_proc.erl
+++ b/src/meck_proc.erl
@@ -47,7 +47,7 @@
%%% Definitions
%%%============================================================================
--ifdef(namespaced_dicts).
+-ifdef(namespaced_types).
-type meck_dict() :: dict:dict().
-else.
-type meck_dict() :: dict().
@@ -60,6 +60,7 @@
history = [] :: meck_history:history() | undefined,
original :: term(),
was_sticky = false :: boolean(),
+ merge_expects = false :: boolean(),
reload :: {Compiler::pid(), {From::pid(), Tag::any()}} |
undefined,
trackers = [] :: [tracker()]}).
@@ -81,9 +82,7 @@
%%% API
%%%============================================================================
--spec start(Mod::atom(), Options::[proplists:property()]) ->
- {ok, MockProcPid::pid()} |
- {error, Reason::any()}.
+-spec start(Mod::atom(), Options::[proplists:property()]) -> ok | no_return().
start(Mod, Options) ->
StartFunc = case proplists:is_defined(no_link, Options) of
true -> start;
@@ -181,7 +180,7 @@ validate(Mod) ->
-spec invalidate(Mod::atom()) -> ok.
invalidate(Mod) ->
- gen_server(call, Mod, invalidate).
+ gen_server(cast, Mod, invalidate).
-spec stop(Mod::atom()) -> ok.
stop(Mod) ->
@@ -200,7 +199,9 @@ init([Mod, Options]) ->
_ -> false
end,
NoPassCover = proplists:get_bool(no_passthrough_cover, Options),
- Original = backup_original(Mod, NoPassCover),
+ MergeExpects = proplists:get_bool(merge_expects, Options),
+ EnableOnLoad = proplists:get_bool(enable_on_load, Options),
+ Original = backup_original(Mod, NoPassCover, EnableOnLoad),
NoHistory = proplists:get_bool(no_history, Options),
History = if NoHistory -> undefined; true -> [] end,
CanExpect = resolve_can_expect(Mod, Exports, Options),
@@ -214,6 +215,7 @@ init([Mod, Options]) ->
expects = Expects,
original = Original,
was_sticky = WasSticky,
+ merge_expects = MergeExpects,
history = History}}
catch
exit:{error_loading_module, Mod, sticky_directory} ->
@@ -225,13 +227,13 @@ handle_call({get_result_spec, Func, Args}, _From, S) ->
{ResultSpec, NewExpects} = do_get_result_spec(S#state.expects, Func, Args),
{reply, ResultSpec, S#state{expects = NewExpects}};
handle_call({set_expect, Expect}, From,
- S = #state{mod = Mod, expects = Expects}) ->
+ S = #state{mod = Mod, expects = Expects, merge_expects = MergeExpects}) ->
check_if_being_reloaded(S),
FuncAri = {Func, Ari} = meck_expect:func_ari(Expect),
case validate_expect(Mod, Func, Ari, S#state.can_expect) of
ok ->
{NewExpects, CompilerPid} = store_expect(Mod, FuncAri, Expect,
- Expects),
+ Expects, MergeExpects),
{noreply, S#state{expects = NewExpects,
reload = {CompilerPid, From}}};
{error, Reason} ->
@@ -265,14 +267,14 @@ handle_call({wait, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout}, From,
end;
handle_call(reset, _From, S) ->
{reply, ok, S#state{history = []}};
-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(invalidate, S) ->
+ {noreply, S#state{valid = false}};
handle_cast({add_history, HistoryRecord}, S = #state{history = undefined,
trackers = Trackers}) ->
UpdTracker = update_trackers(HistoryRecord, Trackers),
@@ -307,9 +309,9 @@ handle_info(_Info, S) ->
%% @hidden
terminate(_Reason, #state{mod = Mod, original = OriginalState,
was_sticky = WasSticky}) ->
- export_original_cover(Mod, OriginalState),
+ BackupCover = export_original_cover(Mod, OriginalState),
cleanup(Mod),
- restore_original(Mod, OriginalState, WasSticky),
+ restore_original(Mod, OriginalState, WasSticky, BackupCover),
ok.
%% @hidden
@@ -338,16 +340,17 @@ expect_type(Mod, Func, Ari) ->
false -> normal
end.
--spec backup_original(Mod::atom(), NoPassCover::boolean()) ->
+-spec backup_original(Mod::atom(), NoPassCover::boolean(), EnableOnLoad::boolean()) ->
{Cover:: false |
{File::string(), Data::string(), CompiledOptions::[any()]},
Binary:: no_binary |
no_passthrough_cover |
binary()}.
-backup_original(Mod, NoPassCover) ->
+backup_original(Mod, NoPassCover, EnableOnLoad) ->
Cover = get_cover_state(Mod),
try
- Forms = meck_code:abstract_code(meck_code:beam_file(Mod)),
+ Forms0 = meck_code:abstract_code(meck_code:beam_file(Mod)),
+ Forms = meck_code:enable_on_load(Forms0, EnableOnLoad),
NewName = meck_util:original_name(Mod),
CompileOpts = meck_code:compile_options(meck_code:beam_file(Mod)),
Renamed = meck_code:rename_module(Forms, NewName),
@@ -391,15 +394,14 @@ backup_original(Mod, NoPassCover) ->
get_cover_state(Mod) ->
case cover:is_compiled(Mod) of
{file, File} ->
- Data = atom_to_list(Mod) ++ ".coverdata",
- ok = cover:export(Data, Mod),
+ OriginalCover = meck_cover:dump_coverdata(Mod),
CompileOptions =
try
meck_code:compile_options(meck_code:beam_file(Mod))
catch
throw:{object_code_not_found, _Module} -> []
end,
- {File, Data, CompileOptions};
+ {File, OriginalCover, CompileOptions};
_ ->
false
end.
@@ -486,10 +488,19 @@ validate_expect(Mod, Func, Ari, CanExpect) ->
end.
-spec store_expect(Mod::atom(), meck_expect:func_ari(),
- meck_expect:expect(), Expects::meck_dict()) ->
+ meck_expect:expect(), Expects::meck_dict(), boolean()) ->
{NewExpects::meck_dict(), CompilerPid::pid()}.
-store_expect(Mod, FuncAri, Expect, Expects) ->
- NewExpects = dict:store(FuncAri, Expect, Expects),
+store_expect(Mod, FuncAri, Expect, Expects, true) ->
+ NewExpects = case dict:is_key(FuncAri, Expects) of
+ true ->
+ {FuncAri, ExistingClauses} = dict:fetch(FuncAri, Expects),
+ {FuncAri, NewClauses} = Expect,
+ dict:store(FuncAri, {FuncAri, ExistingClauses ++ NewClauses}, Expects);
+ false -> dict:store(FuncAri, Expect, Expects)
+ end,
+ compile_expects(Mod, NewExpects);
+store_expect(Mod, FuncAri, Expect, Expects, false) ->
+ NewExpects = dict:store(FuncAri, Expect, Expects),
compile_expects(Mod, NewExpects).
-spec do_delete_expect(Mod::atom(), meck_expect:func_ari(), Expects::meck_dict()) ->
@@ -511,10 +522,10 @@ compile_expects(Mod, Expects) ->
end),
{Expects, CompilerPid}.
-restore_original(Mod, {false, _}, WasSticky) ->
+restore_original(Mod, {false, _Bin}, WasSticky, _BackupCover) ->
restick_original(Mod, WasSticky),
ok;
-restore_original(Mod, OriginalState={{File, Data, Options},_}, WasSticky) ->
+restore_original(Mod, {{File, OriginalCover, Options}, _Bin}, WasSticky, BackupCover) ->
case filename:extension(File) of
".erl" ->
{ok, Mod} = cover:compile_module(File, Options);
@@ -522,30 +533,26 @@ restore_original(Mod, OriginalState={{File, Data, Options},_}, WasSticky) ->
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(meck_util:original_name(Mod)) ++ ".coverdata",
- ok = cover:import(OriginalData),
- ok = file:delete(OriginalData);
-import_original_cover(_, _) ->
+ if BackupCover =/= undefined ->
+ %% Import the cover data for `_meck_original' but since it was
+ %% modified by `export_original_cover' it will count towards `'.
+ ok = cover:import(BackupCover),
+ ok = file:delete(BackupCover);
+ true -> ok
+ end,
+ ok = cover:import(OriginalCover),
+ ok = file:delete(OriginalCover),
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 = meck_util:original_name(Mod),
- File = atom_to_list(OriginalMod) ++ ".coverdata",
- ok = cover:export(File, OriginalMod),
- ok = meck_cover:rename_module(File, Mod);
+ BackupCover = meck_cover:dump_coverdata(OriginalMod),
+ ok = meck_cover:rename_module(BackupCover, Mod),
+ BackupCover;
export_original_cover(_, _) ->
- ok.
+ undefined.
unstick_original(Module) -> unstick_original(Module, code:is_sticky(Module)).
diff --git a/test.config b/test.config
index f2159d79..74bf4448 100644
--- a/test.config
+++ b/test.config
@@ -4,11 +4,11 @@
{branch, "master"}}}]}.
%% Compiler Options ===========================================================
-% FIXME: Add warnings_as_errors once Hamcrest is fixed
{erl_opts, [
%% Erlang releases after 17 don't put R in front of their name, and also require dict() to be written like dict:dict()
- {platform_define, "^[0-9]+", namespaced_dicts},
+ {platform_define, "^[0-9]+", namespaced_types},
{platform_define, "^R(?!16B03)", cover_empty_compile_opts},
+ warnings_as_errors,
debug_info
]}.
diff --git a/test/meck_on_load_module.erl b/test/meck_on_load_module.erl
new file mode 100644
index 00000000..3519dde9
--- /dev/null
+++ b/test/meck_on_load_module.erl
@@ -0,0 +1,10 @@
+-module(meck_on_load_module).
+-on_load(on_load/0).
+-export([ping/0]).
+
+on_load() ->
+ % Assumes that there's an on_load_listener.
+ catch (on_load_listener ! on_load_called),
+ ok.
+
+ping() -> pong.
diff --git a/test/meck_on_load_tests.erl b/test/meck_on_load_tests.erl
new file mode 100644
index 00000000..0e52411a
--- /dev/null
+++ b/test/meck_on_load_tests.erl
@@ -0,0 +1,40 @@
+-module(meck_on_load_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+on_load_test_() ->
+ {foreach, fun setup/0, fun teardown/1,
+ [fun no_enable_on_load/0,
+ fun enable_on_load/0]}.
+
+setup() ->
+ ok.
+
+teardown(_) ->
+ meck:unload().
+
+no_enable_on_load() ->
+ % We _don't_ want on_load to be called. Listen out for it.
+ register(on_load_listener, self()),
+ meck:new(meck_on_load_module, [passthrough]),
+ ?assertEqual(pong, meck_on_load_module:ping()),
+ receive
+ on_load_called -> erlang:error(unexpected_call_to_on_load)
+ after 100 ->
+ % Use a relatively short timeout, because the happy path goes
+ % through here.
+ ok
+ end.
+
+enable_on_load() ->
+ % We _do_ want on_load to be called.
+ register(on_load_listener, self()),
+ meck:new(meck_on_load_module, [passthrough, enable_on_load]),
+ ?assertEqual(pong, meck_on_load_module:ping()),
+ receive
+ on_load_called -> ok
+ after 200 ->
+ % Use a longer timeout, because testing for not-called is harder,
+ % and this is the sad path.
+ erlang:error(expected_call_to_on_load)
+ end.
diff --git a/test/meck_tests.erl b/test/meck_tests.erl
index 547aa500..bbf2d750 100644
--- a/test/meck_tests.erl
+++ b/test/meck_tests.erl
@@ -803,6 +803,42 @@ expect_ret_specs_(Mod) ->
%% --- Tests with own setup ----------------------------------------------------
+merge_expects_module_test() ->
+ Mod = merge_mod,
+ meck:new(Mod, [non_strict, merge_expects]),
+ %% Given
+ meck:expect(Mod, f, [2001], meck:raise(error, a)),
+ meck:expect(Mod, f, [2002], meck:raise(throw, b)),
+ meck:expect(Mod, f, [2003], meck:raise(exit, c)),
+ meck:expect(Mod, f, [2004], meck:val(d)),
+ %% When/Then
+ ?assertException(error, a, Mod:f(2001)),
+ ?assertException(throw, b, Mod:f(2002)),
+ ?assertException(exit, c, Mod:f(2003)),
+ ?assertMatch(d, Mod:f(2004)),
+ meck:unload(Mod).
+
+merge_expects_ret_specs_test() ->
+ Mod = merge_mod,
+ meck:new(Mod, [non_strict, merge_expects]),
+ %% When
+ meck:expect(Mod, f, [1, 1], meck:seq([a, b, c])),
+ meck:expect(Mod, f, [1, '_'], meck:loop([d, e])),
+ meck:expect(Mod, f, ['_', '_'], meck:val(f)),
+ %% Then
+ ?assertEqual(d, Mod:f(1, 2)),
+ ?assertEqual(f, Mod:f(2, 2)),
+ ?assertEqual(e, Mod:f(1, 2)),
+ ?assertEqual(a, Mod:f(1, 1)),
+ ?assertEqual(d, Mod:f(1, 2)),
+ ?assertEqual(b, Mod:f(1, 1)),
+ ?assertEqual(c, Mod:f(1, 1)),
+ ?assertEqual(f, Mod:f(2, 2)),
+ ?assertEqual(c, Mod:f(1, 1)),
+ ?assertEqual(e, Mod:f(1, 2)),
+ ?assertEqual(c, Mod:f(1, 1)),
+ meck:unload(Mod).
+
undefined_module_test() ->
%% When/Then
?assertError({{undefined_module, blah}, _}, meck:new(blah, [no_link])).
@@ -1042,6 +1078,25 @@ cover_passthrough_test() ->
?assertEqual({ok, {meck_test_module, {2,1}}},
cover:analyze(meck_test_module, module)).
+cover_path_test() ->
+ {ok, _} = cover:compile("../test/meck_test_module.erl"),
+ ?assertEqual({ok, {meck_test_module, {0,3}}},
+ cover:analyze(meck_test_module, module)),
+ ok = meck:new(meck_test_module, [passthrough]),
+ ok = meck:expect(meck_test_module, a, fun() -> c end),
+ ?assertEqual(c, meck_test_module:a()),
+ ?assertEqual(b, meck_test_module:b()),
+ ?assertEqual({1, 2}, meck_test_module:c(1, 2)),
+ {ok, CWD} = file:get_cwd(),
+ try
+ ok = file:set_cwd("/tmp"),
+ ok = meck:unload(meck_test_module),
+ ?assertEqual({ok, {meck_test_module, {2,1}}},
+ cover:analyze(meck_test_module, module))
+ after
+ ok = file:set_cwd(CWD)
+ end.
+
% @doc The mocked module is unloaded if the meck process crashes.
unload_when_crashed_test() ->
ok = meck:new(mymod, [non_strict]),
@@ -1063,6 +1118,30 @@ unlink_test() ->
?assert(not lists:member(self(), Links)),
ok = meck:unload(mymod).
+%% @doc A concurrent process calling into the mocked module while it's
+%% being unloaded gets either the mocked response or the original
+%% response, but won't crash.
+atomic_unload_test() ->
+ ok = meck:new(meck_test_module),
+ ok = meck:expect(meck_test_module, a, fun () -> c end),
+
+ %% Suspend the meck_proc in order to ensure all messages are in
+ %% its inbox in the correct order before it would process them
+ Proc = meck_util:proc_name(meck_test_module),
+ sys:suspend(Proc),
+ StopReq = concurrent_req(
+ Proc,
+ fun () -> ?assertEqual(ok, meck:unload(meck_test_module)) end),
+ SpecReq = concurrent_req(
+ Proc,
+ fun () -> ?assertMatch(V when V =:= a orelse V =:= c,
+ meck_test_module:a())
+ end),
+ sys:resume(Proc),
+
+ ?assertEqual(normal, wait_concurrent_req(StopReq)),
+ ?assertEqual(normal, wait_concurrent_req(SpecReq)).
+
%% @doc Exception is thrown when you run expect on a non-existing (and not yet
%% mocked) module.
expect_without_new_test() ->
@@ -1156,9 +1235,9 @@ remote_meck_test_() ->
remote_setup() ->
[] = os:cmd("epmd -daemon"),
- case node() of
- 'nonode@nohost' -> Hostname = "localhost";
- _ -> Hostname = test_server_sup:hoststr()
+ Hostname = case node() of
+ 'nonode@nohost' -> "localhost";
+ _ -> test_server_sup:hoststr()
end,
Myself = list_to_atom("meck_eunit_test@" ++ Hostname),
net_kernel:start([Myself, shortnames]),
@@ -1420,3 +1499,49 @@ assert_called(Mod, Function, Args, WasCalled) ->
assert_called(Mod, Function, Args, Pid, WasCalled) ->
?assertEqual(WasCalled, meck:called(Mod, Function, Args, Pid)),
?assert(meck:validate(Mod)).
+
+%% @doc Spawn a new process to concurrently call `Fun'. `Fun' is
+%% expected to send a request to the specified process, and this
+%% function will wait for this message to arrive. (Therefore the
+%% process should be suspended and not consuming its message queue.)
+%%
+%% The returned request handle can be used later in in {@link
+%% wait_concurrent_req/1} to wait for the concurrent process to
+%% terminate.
+concurrent_req(Name, Fun) when is_atom(Name) ->
+ case whereis(Name) of
+ Pid when is_pid(Pid) ->
+ concurrent_req(Pid, Fun);
+ undefined ->
+ exit(noproc)
+ end;
+concurrent_req(Pid, Fun) when is_pid(Pid) ->
+ {message_queue_len, Msgs} = process_info(Pid, message_queue_len),
+ Req = spawn_monitor(Fun),
+ wait_message(Pid, Msgs + 1, 100),
+ Req.
+
+%% @doc Wait for a concurrent request started with {@link
+%% concurrent_req/2} to terminate. The return value is the exit reason
+%% of the process.
+wait_concurrent_req(Req = {Pid, Monitor}) ->
+ receive
+ {'DOWN', Monitor, process, Pid, Reason} ->
+ Reason
+ after
+ 1000 ->
+ exit(Pid, kill),
+ wait_concurrent_req(Req)
+ end.
+
+wait_message(Pid, _ExpMsgs, Retries) when Retries < 0 ->
+ exit(Pid, kill),
+ exit(wait_message_timeout);
+wait_message(Pid, ExpMsgs, Retries) ->
+ {message_queue_len, Msgs} = process_info(Pid, message_queue_len),
+ if Msgs >= ExpMsgs ->
+ ok;
+ true ->
+ timer:sleep(1),
+ wait_message(Pid, ExpMsgs, Retries - 1)
+ end.