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 @@ -[![Release](http://img.shields.io/github/release/eproxus/meck.svg)](https://github.com/eproxus/meck/releases/latest) -[![Build Status](http://img.shields.io/travis/eproxus/meck.svg)](http://travis-ci.org/eproxus/meck) -[![Code Climate](http://img.shields.io/badge/code_climate-17.0-brightgreen.svg)](https://travis-ci.org/eproxus/meck) +[![Release](http://img.shields.io/github/release/eproxus/meck.svg?style=flat-square)](https://github.com/eproxus/meck/releases/latest) +[![Build Status](http://img.shields.io/travis/eproxus/meck.svg?style=flat-square)](http://travis-ci.org/eproxus/meck) +[![Code Climate](http://img.shields.io/badge/code_climate-Erlang_17.4-brightgreen.svg?style=flat-square)](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.