From 510c6d763fba74f95ae8f894408c3658bcef4f83 Mon Sep 17 00:00:00 2001 From: Micah Anderson Date: Wed, 15 Jan 2014 18:13:16 +0000 Subject: embed dependencies that were previously pulled in by git during rebar build --- deps/meck/.scripts/tag_with_changelog.sh | 41 ++ deps/meck/.travis.yml | 8 + deps/meck/CHANGELOG | 1 + deps/meck/LICENSE | 178 +++++ deps/meck/Makefile | 14 + deps/meck/NOTICE | 5 + deps/meck/README.md | 210 ++++++ deps/meck/doc/overview.edoc | 25 + deps/meck/rebar.config | 6 + deps/meck/src/meck.app.src | 9 + deps/meck/src/meck.erl | 813 +++++++++++++++++++++ deps/meck/src/meck_abstract.hrl | 19 + deps/meck/src/meck_cover.erl | 110 +++ deps/meck/src/meck_mod.erl | 118 +++ deps/meck/test/cover_test_module.dontcompile | 21 + deps/meck/test/include/cover_test.hrl | 1 + deps/meck/test/meck_performance_test.erl | 65 ++ deps/meck/test/meck_test_module.erl | 8 + deps/meck/test/meck_test_parametrized_module.erl | 7 + deps/meck/test/meck_tests.erl | 890 +++++++++++++++++++++++ 20 files changed, 2549 insertions(+) create mode 100755 deps/meck/.scripts/tag_with_changelog.sh create mode 100644 deps/meck/.travis.yml create mode 100644 deps/meck/CHANGELOG create mode 100644 deps/meck/LICENSE create mode 100644 deps/meck/Makefile create mode 100644 deps/meck/NOTICE create mode 100644 deps/meck/README.md create mode 100644 deps/meck/doc/overview.edoc create mode 100644 deps/meck/rebar.config create mode 100644 deps/meck/src/meck.app.src create mode 100644 deps/meck/src/meck.erl create mode 100644 deps/meck/src/meck_abstract.hrl create mode 100644 deps/meck/src/meck_cover.erl create mode 100644 deps/meck/src/meck_mod.erl create mode 100644 deps/meck/test/cover_test_module.dontcompile create mode 100644 deps/meck/test/include/cover_test.hrl create mode 100644 deps/meck/test/meck_performance_test.erl create mode 100644 deps/meck/test/meck_test_module.erl create mode 100644 deps/meck/test/meck_test_parametrized_module.erl create mode 100644 deps/meck/test/meck_tests.erl (limited to 'deps/meck') diff --git a/deps/meck/.scripts/tag_with_changelog.sh b/deps/meck/.scripts/tag_with_changelog.sh new file mode 100755 index 00000000..e6a4d3d1 --- /dev/null +++ b/deps/meck/.scripts/tag_with_changelog.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Install for alias 'tag-cl' with: +# git config alias.tag-cl '!.scripts/tag_with_changelog.sh' + +set -e # Abort on first failure, so we don't mess something up + +if [ -z "$1" ]; then + # Missing tag name + echo "usage: git tag-cl " >&2 + exit 129 +fi +if [ ! -f CHANGELOG ]; then + # No changelog to be used + echo "fatal: CHANGELOG missing" >&2 + exit 128 +fi +if [ ! -z "$(git status --short)" ]; then + # Sanity check + echo "fatal: dirty repository" >&2 + exit 128 +fi + +CHANGELOG=$(cat CHANGELOG) + +# Clean up changelog +echo "" > CHANGELOG +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 +git add src/meck.app.src +git add doc/overview.edoc + +# Commit, tag and push +git commit -m "Version $1" +git tag -s $1 -m "Version $ + +$CHANGELOG" +git push && git push --tags diff --git a/deps/meck/.travis.yml b/deps/meck/.travis.yml new file mode 100644 index 00000000..6e520dbe --- /dev/null +++ b/deps/meck/.travis.yml @@ -0,0 +1,8 @@ +language: erlang +notifications: + disabled: true +otp_release: + - R15B + - R14B04 + - R14B03 + - R14B02 diff --git a/deps/meck/CHANGELOG b/deps/meck/CHANGELOG new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deps/meck/CHANGELOG @@ -0,0 +1 @@ + diff --git a/deps/meck/LICENSE b/deps/meck/LICENSE new file mode 100644 index 00000000..e454a525 --- /dev/null +++ b/deps/meck/LICENSE @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/deps/meck/Makefile b/deps/meck/Makefile new file mode 100644 index 00000000..5ac14ee2 --- /dev/null +++ b/deps/meck/Makefile @@ -0,0 +1,14 @@ +REBAR=`which rebar || ./rebar` + +all: compile + +compile: + @$(REBAR) compile + +test: force + @$(REBAR) eunit + +clean: + @$(REBAR) clean + +force: ; diff --git a/deps/meck/NOTICE b/deps/meck/NOTICE new file mode 100644 index 00000000..18ce5a73 --- /dev/null +++ b/deps/meck/NOTICE @@ -0,0 +1,5 @@ +Copyright 2011 Adam Lindberg + +Copyright 2011 Erlang Solutions +This product contains code developed at Erlang Solutions. +(http://www.erlang-solutions.com/) diff --git a/deps/meck/README.md b/deps/meck/README.md new file mode 100644 index 00000000..6613a088 --- /dev/null +++ b/deps/meck/README.md @@ -0,0 +1,210 @@ +[![Build Status](https://secure.travis-ci.org/eproxus/meck.png)](http://travis-ci.org/eproxus/meck) + + * [Introduction](#introduction) + * [Features](#features) + * [Examples](#examples) + * [Build](#build) + * [Install](#install) + * [Contribute](#contribute) + +meck +==== +A mocking library for Erlang. + + + + +Introduction +------------ + +With meck you can easily mock modules in Erlang. You can also perform +some basic validations on the mocked modules, such as making sure no +unexpected exceptions occurred or looking at the call history. + + + + +Features +-------- + + * Automatic renaming and restoration of original modules + * Automatic backup and restore of cover data + * Changing return values using sequences and loops of static values + * Pass through: use functions from the original module + * Mock is linked to the creating process (disable with `no_link`) + * Complete call history showing calls, results and exceptions + * Mocking of sticky modules (using the option `unstick`) + * Throwing of expected exceptions that keeps the module valid + + + + +Examples +-------- +Here's an example of using meck in the Erlang shell: + +```erl +Eshell V5.8.4 (abort with ^G) +1> meck:new(dog). +ok +2> meck:expect(dog, bark, fun() -> "Woof!" end). +ok +3> dog:bark(). +"Woof!" +4> meck:validate(dog). +true +5> meck:unload(dog). +ok +6> dog:bark(). +** exception error: undefined function dog:bark/0 +``` + +Exceptions can be anticipated by meck (resulting in validation still +passing). This is intended to be used to test code that can and should +handle certain exceptions indeed does take care of them: + +```erl +5> meck:expect(dog, meow, fun() -> meck:exception(error, not_a_cat) end). +ok +6> catch dog:meow(). +{'EXIT',{not_a_cat,[{meck,exception,2}, + {meck,exec,4}, + {dog,meow,[]}, + {erl_eval,do_apply,5}, + {erl_eval,expr,5}, + {shell,exprs,6}, + {shell,eval_exprs,6}, + {shell,eval_loop,3}]}} +7> meck:validate(dog). +true +``` + +Normal Erlang exceptions result in a failed validation. The following +example is just to demonstrate the behavior, in real test code the +exception would normally come from the code under test (which should, +if not expected, invalidate the mocked module): + +```erl +8> meck:expect(dog, jump, fun(Height) when Height > 3 -> + erlang:error(too_high); + (Height) -> + ok + end). +ok +9> dog:jump(2). +ok +10> catch dog:jump(5). +{'EXIT',{too_high,[{meck,exec,4}, + {dog,jump,[5]}, + {erl_eval,do_apply,5}, + {erl_eval,expr,5}, + {shell,exprs,6}, + {shell,eval_exprs,6}, + {shell,eval_loop,3}]}} +11> meck:validate(dog). +false +``` + +Here's an example of using meck inside an EUnit test case: + +```erlang +my_test() -> + meck:new(my_library_module), + meck:expect(my_library_module, fib, fun(8) -> 21 end), + ?assertEqual(21, code_under_test:run(fib, 8)), % Uses my_library_module + ?assert(meck:validate(my_library_module)), + meck:unload(my_library_module). +``` + +Pass-through is used when the original functionality of a module +should be kept. When the option `passthrough` is used when calling +`new/2` all functions in the original module will be kept in the +mock. These can later be overridden by calling `expect/3` or +`expect/4`. + +```erl +Eshell V5.8.4 (abort with ^G) +1> meck:new(string, [unstick, passthrough]). +ok +2> string:strip(" test "). +"test" +``` + +It's also possible to pass calls to the original function allowing us +to override only a certain behavior of a function (this usage is +compatible with the `passthrough` option). `passthrough/1` will always +call the original function with the same name as the expect is +defined in): + +```erl +Eshell V5.8.4 (abort with ^G) +1> meck:new(string, [unstick]). +ok +2> meck:expect(string, strip, fun(String) -> meck:passthrough([String]) end). +ok +3> string:strip(" test "). +"test" +4> meck:unload(string). +ok +5> string:strip(" test "). +"test" +``` + + + +Build +----- + +meck requires [rebar][1] to build. To build meck, go to the meck +directory and simply type: + +```sh +rebar compile +``` + +To make sure meck works on your platform, run the tests: + +```sh +rebar eunit +``` + +Two things might seem alarming when running the tests: + + 1. Warnings emitted by cover + 2. En 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. + + + + +Install +------- + +To install meck permanently, use of [Agner][2] is recommended: + +```sh +agner install meck +``` + +If you want to install your own built version of meck add the ebin +directory to your Erlang code path or move the meck folder into your +release folder and make sure that folder is in your `ERL_LIBS` +environment variable. + + + + +Contribute +---------- + +Patches are greatly appreciated! + +Should you find yourself using meck and have issues, comments or +feedback please [create an issue here on GitHub.] [3] + + [1]: https://github.com/basho/rebar "Rebar - A build tool for Erlang" + [2]: http://erlagner.org/ "Agner - Erlang Package Index & Package Manager" + [3]: http://github.com/eproxus/meck/issues "meck issues" diff --git a/deps/meck/doc/overview.edoc b/deps/meck/doc/overview.edoc new file mode 100644 index 00000000..b04686e6 --- /dev/null +++ b/deps/meck/doc/overview.edoc @@ -0,0 +1,25 @@ +%%============================================================================== +%% Copyright 2010 Erlang Solutions Ltd. +%% +%% 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. +%%============================================================================== + +@author Adam Lindberg +@copyright 2011, Adam Lindberg & Erlang Solutions Ltd +@version "0.7.2" +@title meck, a Mocking Library for Erlang + +@doc + +== About == +meck is a mocking library for Erlang. diff --git a/deps/meck/rebar.config b/deps/meck/rebar.config new file mode 100644 index 00000000..c589f4c7 --- /dev/null +++ b/deps/meck/rebar.config @@ -0,0 +1,6 @@ +{erl_opts, [warnings_as_errors, debug_info]}. +{xref_checks, [undefined_function_calls]}. +{dialyzer_opts, [{warnings, [unmatched_returns]}]}. + +{cover_enabled, true}. +{clean_files, [".eunit", "ebin/*.beam", "test/*.beam"]}. diff --git a/deps/meck/src/meck.app.src b/deps/meck/src/meck.app.src new file mode 100644 index 00000000..d9f57979 --- /dev/null +++ b/deps/meck/src/meck.app.src @@ -0,0 +1,9 @@ +%% -*- mode: erlang; -*- +{application, meck, + [{description, "A mocking framework for Erlang"}, + {vsn, "0.7.2"}, + {modules, []}, + {registered, []}, + {applications, [kernel, stdlib]}, + {build_dependencies, []}, + {env, []}]}. diff --git a/deps/meck/src/meck.erl b/deps/meck/src/meck.erl new file mode 100644 index 00000000..1669c32f --- /dev/null +++ b/deps/meck/src/meck.erl @@ -0,0 +1,813 @@ +%%============================================================================== +%% Copyright 2011 Adam Lindberg & Erlang Solutions Ltd. +%% +%% 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. +%%============================================================================== + +%% @author Adam Lindberg +%% @copyright 2011, Adam Lindberg & Erlang Solutions Ltd +%% @doc Module mocking library for Erlang. + +-module(meck). +-behaviour(gen_server). + +%% Interface exports +-export([new/1]). +-export([new/2]). +-export([expect/3]). +-export([expect/4]). +-export([sequence/4]). +-export([loop/4]). +-export([delete/3]). +-export([exception/2]). +-export([passthrough/1]). +-export([history/1]). +-export([history/2]). +-export([validate/1]). +-export([unload/0]). +-export([unload/1]). +-export([called/3]). +-export([called/4]). +-export([num_calls/3]). +-export([num_calls/4]). + +%% Callback exports +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([terminate/2]). +-export([code_change/3]). +-export([exec/5]). + +%% Types +%% @type meck_mfa() = {Mod::atom(), Func::atom(), Args::list(term())}. +%% Module, function and arguments that the mock module got called with. +-type meck_mfa() :: {Mod::atom(), Func::atom(), Args::[term()]}. + +%% @type history() = [{pid(), meck_mfa(), Result::term()} +%% | {pid(), meck_mfa(), Class:: exit | error | throw, +%% Reason::term(), Stacktrace::list(mfa())}]. +%% History is a list of either successful function calls with a returned +%% result or function calls that resulted in an exception with a type, +%% reason and a stack trace. Each tuple begins with the pid of the process +%% that made the call to the function. +-type history() :: [{pid(), meck_mfa(), Result::term()} + | {pid(), meck_mfa(), Class:: exit | error | throw, + Reason::term(), Stacktrace::[mfa()]}]. + +%% Records +-record(state, {mod :: atom(), + expects :: dict(), + valid = true :: boolean(), + history = [] :: history(), + original :: term(), + was_sticky :: boolean()}). + +%% Includes +-include("meck_abstract.hrl"). + +%%============================================================================== +%% Interface exports +%%============================================================================== + +%% @spec new(Mod:: atom() | list(atom())) -> ok +%% @equiv new(Mod, []) +-spec new(Mod:: atom() | [atom()]) -> ok. +new(Mod) when is_atom(Mod) -> new(Mod, []); +new(Mod) when is_list(Mod) -> lists:foreach(fun new/1, Mod), ok. + +%% @spec new(Mod:: atom() | list(atom()), Options::list(term())) -> ok +%% @doc Creates new mocked module(s). +%% +%% This replaces the current version (if any) of the modules in `Mod' +%% with an empty module. +%% +%% Since this library is intended to use from test code, this +%% function links a process for each mock to the calling process. +%% +%% The valid options are: +%%
+%%
`passthrough'
Retains the original functions, if not +%% mocked by meck.
+%%
`no_link'
Does not link the meck process to the caller +%% process (needed for using meck in rpc calls). +%%
+%%
`unstick'
Unstick the module to be mocked (e.g. needed +%% for using meck with kernel and stdlib modules). +%%
+%%
`no_passthrough_cover'
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. +%%
+%%
+-spec new(Mod:: atom() | [atom()], Options::[term()]) -> ok. +new(Mod, Options) when is_atom(Mod), is_list(Options) -> + case start(Mod, Options) of + {ok, _Pid} -> ok; + {error, Reason} -> erlang:error(Reason, [Mod, Options]) + end; +new(Mod, Options) when is_list(Mod) -> + lists:foreach(fun(M) -> new(M, Options) end, Mod), + ok. + +%% @spec expect(Mod:: atom() | list(atom()), Func::atom(), Expect::fun()) -> ok +%% @doc Add expectation for a function `Func' to the mocked modules `Mod'. +%% +%% An expectation is a fun that is executed whenever the function +%% `Func' is called. +%% +%% It affects the validation status of the mocked module(s). If an +%% expectation is called with the wrong number of arguments or invalid +%% arguments the mock module(s) is invalidated. It is also invalidated if +%% an unexpected exception occurs. +-spec expect(Mod:: atom() | [atom()], Func::atom(), Expect::fun()) -> ok. +expect(Mod, Func, Expect) + when is_atom(Mod), is_atom(Func), is_function(Expect) -> + call(Mod, {expect, Func, Expect}); +expect(Mod, Func, Expect) when is_list(Mod) -> + lists:foreach(fun(M) -> expect(M, Func, Expect) end, Mod), + ok. + +%% @spec expect(Mod:: atom() | list(atom()), Func::atom(), +%% Arity::pos_integer(), Result::term()) -> ok +%% @doc Adds an expectation with the supplied arity and return value. +%% +%% This creates an expectation which takes `Arity' number of functions +%% and always returns `Result'. +%% +%% @see expect/3. +-spec expect(Mod:: atom() | [atom()], Func::atom(), + Arity::pos_integer(), Result::term()) -> ok. +expect(Mod, Func, Arity, Result) + when is_atom(Mod), is_atom(Func), is_integer(Arity), Arity >= 0 -> + valid_expect(Mod, Func, Arity), + call(Mod, {expect, Func, Arity, Result}); +expect(Mod, Func, Arity, Result) when is_list(Mod) -> + lists:foreach(fun(M) -> expect(M, Func, Arity, Result) end, Mod), + ok. + +%% @spec sequence(Mod:: atom() | list(atom()), Func::atom(), +%% Arity::pos_integer(), Sequence::[term()]) -> ok +%% @doc Adds an expectation which returns a value from `Sequence' +%% until exhausted. +%% +%% This creates an expectation which takes `Arity' number of arguments +%% and returns one element from `Sequence' at a time. Thus, calls to +%% this expect will exhaust the list of return values in order until +%% the last value is reached. That value is then returned for all +%% subsequent calls. +-spec sequence(Mod:: atom() | [atom()], Func::atom(), + Arity::pos_integer(), Sequence::[term()]) -> ok. +sequence(Mod, Func, Arity, Sequence) + when is_atom(Mod), is_atom(Func), is_integer(Arity), Arity >= 0 -> + call(Mod, {sequence, Func, Arity, Sequence}); +sequence(Mod, Func, Arity, Sequence) when is_list(Mod) -> + lists:foreach(fun(M) -> sequence(M, Func, Arity, Sequence) end, Mod), + ok. + +%% @spec loop(Mod:: atom() | list(atom()), Func::atom(), +%% Arity::pos_integer(), Loop::[term()]) -> ok +%% @doc Adds an expectation which returns a value from `Loop' +%% infinitely. +%% +%% This creates an expectation which takes `Arity' number of arguments +%% and returns one element from `Loop' at a time. Thus, calls to this +%% expect will return one element at a time from the list and will +%% restart at the first element when the end is reached. +-spec loop(Mod:: atom() | [atom()], Func::atom(), + Arity::pos_integer(), Loop::[term()]) -> ok. +loop(Mod, Func, Arity, Loop) + when is_atom(Mod), is_atom(Func), is_integer(Arity), Arity >= 0 -> + call(Mod, {loop, Func, Arity, Loop}); +loop(Mod, Func, Arity, Loop) when is_list(Mod) -> + lists:foreach(fun(M) -> loop(M, Func, Arity, Loop) end, Mod), + ok. + +%% @spec delete(Mod:: atom() | list(atom()), Func::atom(), +%% Arity::pos_integer()) -> ok +%% @doc Deletes an expectation. +%% +%% Deletes the expectation for the function `Func' with the matching +%% arity `Arity'. +-spec delete(Mod:: atom() | [atom()], Func::atom(), Arity::pos_integer()) -> + ok. +delete(Mod, Func, Arity) + when is_atom(Mod), is_atom(Func), Arity >= 0 -> + call(Mod, {delete, Func, Arity}); +delete(Mod, Func, Arity) when is_list(Mod) -> + lists:foreach(fun(M) -> delete(M, Func, Arity) end, Mod), + ok. + +%% @spec exception(Class:: throw | error | exit, Reason::term()) -> no_return() +%% @doc Throws an expected exception inside an expect fun. +%% +%% This exception will get thrown without invalidating the mocked +%% module. That is, the code using the mocked module is expected to +%% handle this exception. +%% +%% Note: this code should only be used inside an expect fun. +-spec exception(Class:: throw | error | exit, Reason::term()) -> no_return(). +exception(Class, Reason) when Class == throw; Class == error; Class == exit -> + throw(mock_exception_fun(Class, Reason)). + +%% @spec passthrough(Args::list(term())) -> no_return() +%% @doc Calls the original function (if existing) inside an expectation fun. +%% +%% This call does not return, thus everything after this call inside +%% an expectation fun will be ignored. +%% +%% Note: this code should only be used inside an expect fun. +-spec passthrough(Args::[term()]) -> no_return(). +passthrough(Args) -> throw(passthrough_fun(Args)). + +%% @spec validate(Mod:: atom() | list(atom())) -> boolean() +%% @doc Validate the state of the mock module(s). +%% +%% The function returns `true' if the mocked module(s) has been used +%% according to its expectations. It returns `false' if a call has +%% failed in some way. Reasons for failure are wrong number of +%% arguments or non-existing function (undef), wrong arguments +%% (function clause) or unexpected exceptions. +%% +%% Use the {@link history/1} or {@link history/2} function to analyze errors. +-spec validate(Mod:: atom() | [atom()]) -> boolean(). +validate(Mod) when is_atom(Mod) -> + call(Mod, validate); +validate(Mod) when is_list(Mod) -> + not lists:member(false, [validate(M) || M <- Mod]). + +%% @spec history(Mod::atom()) -> history() +%% @doc Return the call history of the mocked module for all processes. +%% +%% @equiv history(Mod, '_') +-spec history(Mod::atom()) -> history(). +history(Mod) when is_atom(Mod) -> call(Mod, history). + +%% @spec history(Mod::atom(), Pid::pid()) -> history() +%% @doc Return the call history of the mocked module for the specified process. +%% +%% Returns a list of calls to the mocked module and their results for +%% the specified `Pid'. Results can be either normal Erlang terms or +%% exceptions that occurred. +%% +%% @see history/1 +%% @see called/3 +%% @see called/4 +%% @see num_calls/3 +%% @see num_calls/4 +-spec history(Mod::atom(), Pid:: pid() | '_') -> history(). +history(Mod, Pid) when is_atom(Mod), is_pid(Pid) orelse Pid == '_' -> + match_history(match_mfa('_', Pid), call(Mod, history)). + +%% @spec unload() -> list(atom()) +%% @doc Unloads all mocked modules from memory. +%% +%% The function returns the list of mocked modules that were unloaded +%% in the process. +-spec unload() -> [atom()]. +unload() -> lists:foldl(fun unload_if_mocked/2, [], registered()). + +%% @spec unload(Mod:: atom() | list(atom())) -> ok +%% @doc Unload a mocked module or a list of mocked modules. +%% +%% This will purge and delete the module(s) from the Erlang virtual +%% machine. If the mocked module(s) replaced an existing module, this +%% module will still be in the Erlang load path and can be loaded +%% manually or when called. +-spec unload(Mods:: atom() | [atom()]) -> ok. +unload(Mod) when is_atom(Mod) -> call(Mod, stop), wait_for_exit(Mod); +unload(Mods) when is_list(Mods) -> lists:foreach(fun unload/1, Mods), ok. + +%% @spec called(Mod:: atom(), Fun:: atom(), Args:: list(term())) -> boolean() +%% @doc Returns whether `Mod:Func' has been called with `Args'. +%% +%% @equiv called(Mod, Fun, Args, '_') +called(Mod, Fun, Args) -> + has_call({Mod, Fun, Args}, meck:history(Mod)). + +%% @spec called(Mod:: atom(), Fun:: atom(), Args:: list(term()), +%% Pid::pid()) -> boolean() +%% @doc Returns whether `Pid' has called `Mod:Func' with `Args'. +%% +%% This will check the history for the module, `Mod', to determine +%% whether process `Pid' call the function, `Fun', with arguments, `Args'. If +%% so, this function returns true, otherwise false. +%% +%% Wildcards can be used, at any level in any term, by using the underscore +%% atom: ``'_' '' +%% +%% @see called/3 +-spec called(Mod::atom(), Fun::atom(), Args::list(), Pid::pid()) -> boolean(). +called(Mod, Fun, Args, Pid) -> + has_call({Mod, Fun, Args}, meck:history(Mod, Pid)). + +%% @spec num_calls(Mod:: atom(), Fun:: atom(), Args:: list(term())) +%% -> non_neg_integer() +%% @doc Returns the number of times `Mod:Func' has been called with `Args'. +%% +%% @equiv num_calls(Mod, Fun, Args, '_') +num_calls(Mod, Fun, Args) -> + num_calls({Mod, Fun, Args}, meck:history(Mod)). + +%% @spec num_calls(Mod:: atom(), Fun:: atom(), Args:: list(term()), +%% Pid::pid()) -> non_neg_integer() +%% @doc Returns the number of times process `Pid' has called `Mod:Func' +%% with `Args'. +%% +%% This will check the history for the module, `Mod', to determine how +%% many times process `Pid' has called the function, `Fun', with +%% arguments, `Args' and returns the result. +%% +%% @see num_calls/3 +-spec num_calls(Mod::atom(), Fun::atom(), Args::list(), Pid::pid()) -> + non_neg_integer(). +num_calls(Mod, Fun, Args, Pid) -> + num_calls({Mod, Fun, Args}, meck:history(Mod, Pid)). + +%%============================================================================== +%% Callback functions +%%============================================================================== + +%% @hidden +init([Mod, Options]) -> + WasSticky = case proplists:is_defined(unstick, Options) of + true -> {module, Mod} = code:ensure_loaded(Mod), + unstick_original(Mod); + _ -> false + end, + NoPassCover = proplists:get_bool(no_passthrough_cover, Options), + Original = backup_original(Mod, NoPassCover), + process_flag(trap_exit, true), + Expects = init_expects(Mod, Options), + try + _Bin = meck_mod:compile_and_load_forms(to_forms(Mod, Expects)), + {ok, #state{mod = Mod, expects = Expects, original = Original, + was_sticky = WasSticky}} + catch + exit:{error_loading_module, Mod, sticky_directory} -> + {stop, module_is_sticky} + end. + +%% @hidden +handle_call({get_expect, Func, Arity}, _From, S) -> + {Expect, NewExpects} = get_expect(S#state.expects, Func, Arity), + {reply, Expect, S#state{expects = NewExpects}}; +handle_call({expect, Func, Expect}, _From, S) -> + NewExpects = store_expect(S#state.mod, Func, Expect, S#state.expects), + {reply, ok, S#state{expects = NewExpects}}; +handle_call({expect, Func, Arity, Result}, _From, S) -> + NewExpects = store_expect(S#state.mod, Func, {anon, Arity, Result}, + S#state.expects), + {reply, ok, S#state{expects = NewExpects}}; +handle_call({sequence, Func, Arity, Sequence}, _From, S) -> + NewExpects = store_expect(S#state.mod, Func, {sequence, Arity, Sequence}, + S#state.expects), + {reply, ok, S#state{expects = NewExpects}}; +handle_call({loop, Func, Arity, Loop}, _From, S) -> + NewExpects = store_expect(S#state.mod, Func, {loop, Arity, Loop, Loop}, + S#state.expects), + {reply, ok, S#state{expects = NewExpects}}; +handle_call({delete, Func, Arity}, _From, S) -> + NewExpects = delete_expect(S#state.mod, Func, Arity, S#state.expects), + {reply, ok, S#state{expects = NewExpects}}; +handle_call(history, _From, S) -> + {reply, lists:reverse(S#state.history), S}; +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({add_history, Item}, S) -> + {noreply, S#state{history = [Item| S#state.history]}}; +handle_cast(_Msg, S) -> + {noreply, S}. + +%% @hidden +handle_info(_Info, S) -> {noreply, S}. + +%% @hidden +terminate(_Reason, #state{mod = Mod, original = OriginalState, + was_sticky = WasSticky}) -> + export_original_cover(Mod, OriginalState), + cleanup(Mod), + restore_original(Mod, OriginalState, WasSticky), + ok. + +%% @hidden +code_change(_OldVsn, S, _Extra) -> {ok, S}. + +%% @hidden +exec(Pid, Mod, Func, Arity, Args) -> + Expect = call(Mod, {get_expect, Func, Arity}), + try Result = call_expect(Mod, Func, Expect, Args), + add_history(Pid, Mod, Func, Args, Result), + Result + catch + throw:Fun when is_function(Fun) -> + case is_mock_exception(Fun) of + true -> handle_mock_exception(Pid, Mod, Func, Fun, Args); + false -> invalidate_and_raise(Pid, Mod, Func, Args, throw, Fun) + end; + Class:Reason -> + invalidate_and_raise(Pid, Mod, Func, Args, Class, Reason) + end. + +%%============================================================================== +%% Internal functions +%%============================================================================== + +%% --- Process functions ------------------------------------------------------- + +start(Mod, Options) -> + case proplists:is_defined(no_link, Options) of + true -> start(start, Mod, Options); + false -> start(start_link, Mod, Options) + end. + +start(Func, Mod, Options) -> + gen_server:Func({local, proc_name(Mod)}, ?MODULE, [Mod, Options], []). + +cast(Mod, Msg) -> gen_server(cast, Mod, Msg). +call(Mod, Msg) -> gen_server(call, Mod, Msg). + +gen_server(Func, Mod, Msg) -> + Name = proc_name(Mod), + try gen_server:Func(Name, Msg) + catch exit:_Reason -> erlang:error({not_mocked, Mod}) end. + +proc_name(Name) -> list_to_atom(atom_to_list(Name) ++ "_meck"). + +original_name(Name) -> list_to_atom(atom_to_list(Name) ++ "_meck_original"). + +wait_for_exit(Mod) -> + MonitorRef = erlang:monitor(process, proc_name(Mod)), + receive {'DOWN', MonitorRef, _Type, _Object, _Info} -> ok end. + +unload_if_mocked(P, L) when is_atom(P) -> + unload_if_mocked(atom_to_list(P), L); +unload_if_mocked(P, L) when length(P) > 5 -> + case lists:split(length(P) - 5, P) of + {Name, "_meck"} -> + Mocked = list_to_existing_atom(Name), + try + unload(Mocked) + catch error:{not_mocked, Mocked} -> + ok + end, + [Mocked|L]; + _Else -> + L + end; +unload_if_mocked(_P, L) -> + L. + +%% --- Mock handling ----------------------------------------------------------- + +valid_expect(M, F, A) -> + case expect_type(M, F, A) of + autogenerated -> erlang:error({cannot_mock_autogenerated, {M, F, A}}); + builtin -> erlang:error({cannot_mock_builtin, {M, F, A}}); + normal -> ok + end. + +init_expects(Mod, Options) -> + case proplists:get_value(passthrough, Options, false) andalso exists(Mod) of + true -> dict:from_list([{FA, passthrough} || FA <- exports(Mod)]); + _ -> dict:new() + end. + + +get_expect(Expects, Func, Arity) -> + case e_fetch(Expects, Func, Arity) of + {sequence, Arity, [Result]} -> + {{sequence, Arity, Result}, Expects}; + {sequence, Arity, [Result|Rest]} -> + {{sequence, Arity, Result}, + e_store(Expects, Func, {sequence, Arity, Rest})}; + {loop, Arity, [Result], Loop} -> + {{loop, Arity, Result}, + e_store(Expects, Func, {loop, Arity, Loop, Loop})}; + {loop, Arity, [Result|Rest], Loop} -> + {{loop, Arity, Result}, + e_store(Expects, Func, {loop, Arity, Rest, Loop})}; + Other -> + {Other, Expects} + end. + +store_expect(Mod, Func, Expect, Expects) -> + change_expects(fun e_store/3, Mod, Func, Expect, Expects). + +delete_expect(Mod, Func, Arity, Expects) -> + change_expects(fun e_delete/3, Mod, Func, Arity, Expects). + +change_expects(Op, Mod, Func, Value, Expects) -> + NewExpects = Op(Expects, Func, Value), + _Bin = meck_mod:compile_and_load_forms(to_forms(Mod, NewExpects)), + NewExpects. + +e_store(Expects, Func, Expect) -> + dict:store({Func, arity(Expect)}, Expect, Expects). + +e_fetch(Expects, Func, Arity) -> + dict:fetch({Func, Arity}, Expects). + +e_delete(Expects, Func, Arity) -> + dict:erase({Func, Arity}, Expects). + +%% --- Code generation --------------------------------------------------------- + +func(Mod, {Func, Arity}, {anon, Arity, Result}) -> + case contains_opaque(Result) of + true -> + func_exec(Mod, Func, Arity); + false -> + func_native(Mod, Func, Arity, Result) + end; +func(Mod, {Func, Arity}, _Expect) -> + func_exec(Mod, Func, Arity). + +func_exec(Mod, Func, Arity) -> + Args = args(Arity), + ?function(Func, Arity, + [?clause(Args, + [?call(?MODULE, exec, + [?call(erlang, self, []), + ?atom(Mod), + ?atom(Func), + ?integer(Arity), + list(Args)])])]). + +func_native(Mod, Func, Arity, Result) -> + Args = args(Arity), + AbsResult = erl_parse:abstract(Result), + ?function( + Func, Arity, + [?clause( + Args, + [?call(gen_server, cast, + [?atom(proc_name(Mod)), + ?tuple([?atom(add_history), + ?tuple([?call(erlang, self, []), + ?tuple([?atom(Mod), ?atom(Func), + list(Args)]), + AbsResult])])]), + AbsResult])]). + +contains_opaque(Term) when is_pid(Term); is_port(Term); is_function(Term) -> + true; +contains_opaque(Term) when is_list(Term) -> + lists:any(fun contains_opaque/1, Term); +contains_opaque(Term) when is_tuple(Term) -> + lists:any(fun contains_opaque/1, tuple_to_list(Term)); +contains_opaque(_Term) -> + false. + + +to_forms(Mod, Expects) -> + {Exports, Functions} = functions(Mod, Expects), + [?attribute(module, Mod)] ++ Exports ++ Functions. + +functions(Mod, Expects) -> + dict:fold(fun(Export, Expect, {Exports, Functions}) -> + {[?attribute(export, [Export])|Exports], + [func(Mod, Export, Expect)|Functions]} + end, {[], []}, Expects). + +args(0) -> []; +args(Arity) -> [?var(var_name(N)) || N <- lists:seq(1, Arity)]. + +list([]) -> {nil, ?LINE}; +list([H|T]) -> {cons, ?LINE, H, list(T)}. + +var_name(A) -> list_to_atom("A"++integer_to_list(A)). + +arity({anon, Arity, _Result}) -> + Arity; +arity({sequence, Arity, _Sequence}) -> + Arity; +arity({loop, Arity, _Current, _Loop}) -> + Arity; +arity(Fun) when is_function(Fun) -> + {arity, Arity} = erlang:fun_info(Fun, arity), + Arity. + +%% --- Execution utilities ----------------------------------------------------- + +is_local_function(Fun) -> + {module, Module} = erlang:fun_info(Fun, module), + ?MODULE == Module. + +handle_mock_exception(Pid, Mod, Func, Fun, Args) -> + case Fun() of + {exception, Class, Reason} -> + % exception created with the mock:exception function, + % do not invalidate Mod + raise(Pid, Mod, Func, Args, Class, Reason); + {passthrough, PassthroughArgs} -> + % call_original(Args) called from mock function + Result = apply(original_name(Mod), Func, PassthroughArgs), + add_history(Pid, Mod, Func, PassthroughArgs, Result), + Result + end. + +-spec invalidate_and_raise(_, _, _, _, _, _) -> no_return(). +invalidate_and_raise(Pid, Mod, Func, Args, Class, Reason) -> + call(Mod, invalidate), + raise(Pid, Mod, Func, Args, Class, Reason). + +raise(Pid, Mod, Func, Args, Class, Reason) -> + Stacktrace = inject(Mod, Func, Args, erlang:get_stacktrace()), + add_history(Pid, Mod, Func, Args, Class, Reason, Stacktrace), + erlang:raise(Class, Reason, Stacktrace). + +mock_exception_fun(Class, Reason) -> fun() -> {exception, Class, Reason} end. + +passthrough_fun(Args) -> fun() -> {passthrough, Args} end. + +call_expect(_Mod, _Func, {_Type, Arity, Return}, VarList) + when Arity == length(VarList) -> + Return; +call_expect(Mod, Func, passthrough, VarList) -> + apply(original_name(Mod), Func, VarList); +call_expect(_Mod, _Func, Fun, VarList) when is_function(Fun) -> + apply(Fun, VarList). + +inject(_Mod, _Func, _Args, []) -> + []; +inject(Mod, Func, Args, [{meck, exec, _Arity} = Meck|Stack]) -> + [Meck, {Mod, Func, Args}|Stack]; +inject(Mod, Func, Args, [{meck, exec, _Arity, _Location} = Meck|Stack]) -> + [Meck, {Mod, Func, Args}|Stack]; +inject(Mod, Func, Args, [H|Stack]) -> + [H|inject(Mod, Func, Args, Stack)]. + +is_mock_exception(Fun) -> is_local_function(Fun). + +%% --- Original module handling ------------------------------------------------ + +backup_original(Module, NoPassCover) -> + Cover = get_cover_state(Module), + try + Forms = meck_mod:abstract_code(meck_mod:beam_file(Module)), + NewName = original_name(Module), + CompileOpts = meck_mod:compile_options(meck_mod:beam_file(Module)), + Renamed = meck_mod:rename_module(Forms, NewName), + Binary = meck_mod:compile_and_load_forms(Renamed, 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_passthrough_cover; + true -> + Binary2 = Binary, + meck_cover:compile_beam(NewName, Binary2) + end, + {Cover, Binary2} + catch + 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) -> + restick_original(Mod, WasSticky), + ok; +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 `_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(original_name(Mod)) ++ ".coverdata", + ok = cover:import(OriginalData), + ok = file:delete(OriginalData); +import_original_cover(_, _) -> + 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 = 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); +unstick_original(_,_) -> false. + +restick_original(Module, true) -> + code:stick_mod(Module), + {module, Module} = code:ensure_loaded(Module), + ok; +restick_original(_,_) -> ok. + +get_cover_state(Module) -> get_cover_state(Module, cover:is_compiled(Module)). + +get_cover_state(Module, {file, File}) -> + Data = atom_to_list(Module) ++ ".coverdata", + ok = cover:export(Data, Module), + CompileOptions = + try + meck_mod:compile_options(meck_mod:beam_file(Module)) + catch + throw:{object_code_not_found, _Module} -> [] + end, + {File, Data, CompileOptions}; +get_cover_state(_Module, _IsCompiled) -> + false. + +exists(Module) -> + code:which(Module) /= non_existing. + +exports(M) -> + [ FA || FA = {F, A} <- M:module_info(exports), + normal == expect_type(M, F, A)]. + +%% Functions we should not create expects for (auto-generated and BIFs) +expect_type(_, module_info, 0) -> autogenerated; +expect_type(_, module_info, 1) -> autogenerated; +expect_type(M, F, A) -> expect_type(erlang:is_builtin(M, F, A)). + +expect_type(true) -> builtin; +expect_type(false) -> normal. + +cleanup(Mod) -> + code:purge(Mod), + code:delete(Mod), + code:purge(original_name(Mod)), + code:delete(original_name(Mod)). + +%% --- History utilities ------------------------------------------------------- + +add_history(Pid, Mod, Func, Args, Result) -> + add_history(Mod, {Pid, {Mod, Func, Args}, Result}). +add_history(Pid, Mod, Func, Args, Class, Reason, Stacktrace) -> + add_history(Mod, {Pid, {Mod, Func, Args}, Class, Reason, Stacktrace}). + +add_history(Mod, Item) -> + cast(Mod, {add_history, Item}). + +has_call(MFA, History) -> + [] =/= match_history(match_mfa(MFA), History). + +num_calls(MFA, History) -> + length(match_history(match_mfa(MFA), History)). + +match_history(MatchSpec, History) -> + MS = ets:match_spec_compile(MatchSpec), + ets:match_spec_run(History, MS). + +match_mfa(MFA) -> match_mfa(MFA, '_'). + +match_mfa(MFA, Pid) -> + [{{Pid, MFA, '_'}, [], ['$_']}, + {{Pid, MFA, '_', '_', '_'}, [], ['$_']}]. diff --git a/deps/meck/src/meck_abstract.hrl b/deps/meck/src/meck_abstract.hrl new file mode 100644 index 00000000..8f3b9829 --- /dev/null +++ b/deps/meck/src/meck_abstract.hrl @@ -0,0 +1,19 @@ +-define(call(Module, Function, Arguments), + {call, ?LINE, + {remote, ?LINE, ?atom(Module), ?atom(Function)}, + Arguments}). + +-define(atom(Atom), {atom, ?LINE, Atom}). + +-define(integer(Integer), {integer, ?LINE, Integer}). + +-define(var(Name), {var, ?LINE, Name}). + +-define(attribute(Attribute, Args), {attribute, ?LINE, Attribute, Args}). + +-define(function(Name, Arity, Clauses), + {function, ?LINE, Name, Arity, Clauses}). + +-define(clause(Arguments, Body), {clause, ?LINE, Arguments, [], Body}). + +-define(tuple(Elements), {tuple, ?LINE, Elements}). diff --git a/deps/meck/src/meck_cover.erl b/deps/meck/src/meck_cover.erl new file mode 100644 index 00000000..da8888c8 --- /dev/null +++ b/deps/meck/src/meck_cover.erl @@ -0,0 +1,110 @@ +%%============================================================================== +%% 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 `_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), + _Bin = meck_mod:compile_and_load_forms(AbsCode2), + ok + 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, []), + ok = 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:foreach(write_term(Fd), Terms), + ok. + +write_term(Fd) -> + fun(Term) -> cover:write(Term, Fd) end. + diff --git a/deps/meck/src/meck_mod.erl b/deps/meck/src/meck_mod.erl new file mode 100644 index 00000000..22a237d8 --- /dev/null +++ b/deps/meck/src/meck_mod.erl @@ -0,0 +1,118 @@ +%%============================================================================== +%% Copyright 2011 Adam Lindberg & Erlang Solutions Ltd. +%% +%% 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. +%%============================================================================== + +%% @hidden +%% @author Adam Lindberg +%% @copyright 2011, Adam Lindberg & Erlang Solutions Ltd +%% @doc Module wrangling helper functions. + +-module(meck_mod). + +%% 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]). +-export([compile_options/1]). +-export([rename_module/2]). + +%% Types +-type erlang_form() :: term(). +-type compile_options() :: [term()]. +-type export() :: {atom(), byte()}. + +%%============================================================================== +%% Interface exports +%%============================================================================== + +-spec abstract_code(binary()) -> erlang_form(). +abstract_code(BeamFile) -> + case beam_lib:chunks(BeamFile, [abstract_code]) of + {ok, {_, [{abstract_code, {raw_abstract_v1, Forms}}]}} -> + Forms; + {ok, {_, [{abstract_code, no_abstract_code}]}} -> + 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 + case code:get_object_code(Module) of + {_, Binary, _Filename} -> Binary; + error -> throw({object_code_not_found, Module}) + end. + +-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()) -> binary(). +compile_and_load_forms(AbsCode, Opts) -> + case compile:forms(AbsCode, [return_errors|Opts]) of + {ok, ModName, Binary} -> + load_binary(ModName, Binary), + Binary; + {ok, ModName, Binary, _Warnings} -> + load_binary(ModName, Binary), + Binary; + Error -> + exit({compile_forms, Error}) + end. + +-spec compile_options(binary() | module()) -> compile_options(). +compile_options(BeamFile) when is_binary(BeamFile) -> + case beam_lib:chunks(BeamFile, [compile_info]) of + {ok, {_, [{compile_info, Info}]}} -> + filter_options(proplists:get_value(options, Info)); + _ -> + [] + end; +compile_options(Module) -> + filter_options(proplists:get_value(options, Module:module_info(compile))). + +-spec rename_module(erlang_form(), module()) -> erlang_form(). +rename_module([{attribute, Line, module, OldAttribute}|T], NewName) -> + case OldAttribute of + {_OldName, Variables} -> + [{attribute, Line, module, {NewName, Variables}}|T]; + _OldName -> + [{attribute, Line, module, NewName}|T] + end; +rename_module([H|T], NewName) -> + [H|rename_module(T, NewName)]. + +%%============================================================================== +%% Internal functions +%%============================================================================== + +load_binary(Name, Binary) -> + case code:load_binary(Name, "", Binary) of + {module, Name} -> ok; + {error, Reason} -> exit({error_loading_module, Name, Reason}) + end. + +% parse transforms have already been applied to the abstract code in the +% module, and often are not always available when compiling the forms, so +% filter them out of the options +filter_options (Options) -> + lists:filter(fun({parse_transform,_}) -> false; (_) -> true end, Options). + diff --git a/deps/meck/test/cover_test_module.dontcompile b/deps/meck/test/cover_test_module.dontcompile new file mode 100644 index 00000000..06e88967 --- /dev/null +++ b/deps/meck/test/cover_test_module.dontcompile @@ -0,0 +1,21 @@ +%% -*- mode: erlang -*- + +%% This file needs not to have the extension .erl, since otherwise +%% rebar will try to compile it, which won't work since it requires +%% special compilation options. See meck_tests:cover_test_. + +-module(cover_test_module). +-export([a/0, b/0, c/2]). + +%% a/0 is defined in include/cover_test.hrl. We don't put the full +%% path here, since it should be provided as a compiler option. +-include("cover_test.hrl"). + +%% Also, make that this module was compiled with -DTEST. +-ifdef(TEST). +b() -> + b. +-endif. + +c(A, B) -> + {A, B}. diff --git a/deps/meck/test/include/cover_test.hrl b/deps/meck/test/include/cover_test.hrl new file mode 100644 index 00000000..0e770862 --- /dev/null +++ b/deps/meck/test/include/cover_test.hrl @@ -0,0 +1 @@ +a() -> a. diff --git a/deps/meck/test/meck_performance_test.erl b/deps/meck/test/meck_performance_test.erl new file mode 100644 index 00000000..71af107d --- /dev/null +++ b/deps/meck/test/meck_performance_test.erl @@ -0,0 +1,65 @@ +%% @doc +-module(meck_performance_test). + +%% Interface exports +-export([run/1]). + +%%============================================================================== +%% Interface exports +%%============================================================================== + +run(N) -> + meck:new(test), + io:format("\t\tMin\tMax\tMed\tAvg~n"), + io:format("expect/3\t~p\t~p\t~p\t~p~n", + test_avg(meck, expect, [test, normal, fun() -> ok end], N)), + io:format("expect/3+args\t~p\t~p\t~p\t~p~n", + test_avg(meck, expect, [test, normal_args, + fun(_, _) -> ok end], N)), + io:format("expect/4\t~p\t~p\t~p\t~p~n", + test_avg(meck, expect, [test, shortcut, 0, ok], N)), + io:format("expect/4+args\t~p\t~p\t~p\t~p~n", + test_avg(meck, expect, [test, shortcut_args, 2, ok], N)), + + meck:expect(test, shortcut_opaque, 0, self()), + + io:format("~n\t\tMin\tMax\tMed\tAvg~n"), + io:format("normal\t\t~p\t~p\t~p\t~p~n", + test_avg(test, normal, [], N)), + io:format("normal_args\t~p\t~p\t~p\t~p~n", + test_avg(test, normal_args, [a, b], N)), + io:format("shortcut\t~p\t~p\t~p\t~p~n", + test_avg(test, shortcut, [], N)), + io:format("shortcut_args\t~p\t~p\t~p\t~p~n", + test_avg(test, shortcut_args, [a, b], N)), + io:format("shortcut_opaque\t~p\t~p\t~p\t~p~n", + test_avg(test, shortcut_opaque, [], N)), + meck:unload(test), + + meck:new(test), + meck:expect(test, func, 1, ok), + [test:func(I) || I <- lists:seq(1, 100)], + io:format("~n\t\tMin\tMax\tMed\tAvg~n"), + io:format("called\t\t~p\t~p\t~p\t~p~n", + test_avg(meck, called, [test, func, 50], N)), + meck:unload(test), + ok. + +%%============================================================================== +%% Internal functions +%%============================================================================== + +test_avg(M, F, A, N) when N > 0 -> + L = test_loop(M, F, A, N, []), + Length = length(L), + Min = lists:min(L), + Max = lists:max(L), + Med = lists:nth(round((Length / 2)), lists:sort(L)), + Avg = round(lists:foldl(fun(X, Sum) -> X + Sum end, 0, L) / Length), + [Min, Max, Med, Avg]. + +test_loop(_M, _F, _A, 0, List) -> + List; +test_loop(M, F, A, N, List) -> + {T, _Result} = timer:tc(M, F, A), + test_loop(M, F, A, N - 1, [T|List]). diff --git a/deps/meck/test/meck_test_module.erl b/deps/meck/test/meck_test_module.erl new file mode 100644 index 00000000..6dee52be --- /dev/null +++ b/deps/meck/test/meck_test_module.erl @@ -0,0 +1,8 @@ +-module(meck_test_module). +-export([a/0, b/0, c/2]). + +a() -> a. +b() -> b. + +c(A, B) -> + {A, B}. diff --git a/deps/meck/test/meck_test_parametrized_module.erl b/deps/meck/test/meck_test_parametrized_module.erl new file mode 100644 index 00000000..19126797 --- /dev/null +++ b/deps/meck/test/meck_test_parametrized_module.erl @@ -0,0 +1,7 @@ +-module(meck_test_parametrized_module, [Var1, Var2]). +-export([which/0, var1/0, var2/0]). + +which() -> original. + +var1() -> {original, Var1}. +var2() -> {original, Var2}. diff --git a/deps/meck/test/meck_tests.erl b/deps/meck/test/meck_tests.erl new file mode 100644 index 00000000..9e019521 --- /dev/null +++ b/deps/meck/test/meck_tests.erl @@ -0,0 +1,890 @@ +%%============================================================================== +%% Copyright 2010 Erlang Solutions Ltd. +%% +%% 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. +%%============================================================================== + +-module(meck_tests). + +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). + +meck_test_() -> + {foreach, fun setup/0, fun teardown/1, + [{with, [T]} || T <- [fun ?MODULE:new_/1, + fun ?MODULE:unload_/1, + fun ?MODULE:double_new_/1, + fun ?MODULE:validate_/1, + fun ?MODULE:expect_/1, + fun ?MODULE:exports_/1, + fun ?MODULE:call_return_value_/1, + fun ?MODULE:call_argument_/1, + fun ?MODULE:call_undef_/1, + fun ?MODULE:call_function_clause_/1, + fun ?MODULE:validate_unexpected_error_/1, + fun ?MODULE:validate_expected_error_/1, + fun ?MODULE:validate_chained_/1, + fun ?MODULE:stacktrace_/1, + fun ?MODULE:stacktrace_function_clause_/1, + fun ?MODULE:change_func_/1, + fun ?MODULE:caller_does_not_crash_on_reload_/1, + fun ?MODULE:call_original_undef_/1, + fun ?MODULE:history_empty_/1, + fun ?MODULE:history_call_/1, + fun ?MODULE:history_throw_/1, + fun ?MODULE:history_throw_fun_/1, + fun ?MODULE:history_exit_/1, + fun ?MODULE:history_error_/1, + fun ?MODULE:history_error_args_/1, + fun ?MODULE:history_meck_throw_/1, + fun ?MODULE:history_meck_throw_fun_/1, + fun ?MODULE:history_meck_exit_/1, + fun ?MODULE:history_meck_error_/1, + fun ?MODULE:history_by_pid_/1, + fun ?MODULE:shortcut_expect_/1, + fun ?MODULE:shortcut_expect_negative_arity_/1, + fun ?MODULE:shortcut_call_return_value_/1, + fun ?MODULE:shortcut_call_argument_/1, + fun ?MODULE:shortcut_re_add_/1, + fun ?MODULE:shortcut_opaque_/1, + fun ?MODULE:delete_/1, + fun ?MODULE:called_false_no_args_/1, + fun ?MODULE:called_true_no_args_/1, + fun ?MODULE:called_true_two_functions_/1, + fun ?MODULE:called_false_one_arg_/1, + fun ?MODULE:called_true_one_arg_/1, + fun ?MODULE:called_false_few_args_/1, + fun ?MODULE:called_true_few_args_/1, + fun ?MODULE:called_false_error_/1, + fun ?MODULE:called_true_error_/1, + fun ?MODULE:called_with_pid_no_args_/1, + fun ?MODULE:num_calls_/1, + fun ?MODULE:num_calls_error_/1, + fun ?MODULE:num_calls_with_pid_no_args_/1, + fun ?MODULE:called_wildcard_/1, + fun ?MODULE:sequence_/1, + fun ?MODULE:sequence_multi_/1, + fun ?MODULE:loop_/1, + fun ?MODULE:loop_multi_/1 + ]]}. + +setup() -> + % Uncomment to run tests with dbg: + % dbg:tracer(), + % dbg:p(all, call), + % dbg:tpl(meck, []), + ok = meck:new(mymod), + mymod. + +teardown(Module) -> + catch meck:unload(Module). + +%% --- Tests using setup and teardown ------------------------------------------ + +new_(Mod) -> + Info = Mod:module_info(), + ?assert(is_list(Info)). + +unload_(Mod) -> + ok = meck:unload(Mod), + ?assertEqual(false, code:is_loaded(Mod)). + +double_new_(Mod) -> + ?assertError({already_started, _}, meck:new(Mod)). + +validate_(Mod) -> + ?assertEqual(true, meck:validate(Mod)). + +expect_(Mod) -> + ok = meck:expect(Mod, test, fun() -> ok end), + ?assertEqual(true, meck:validate(Mod)). + +exports_(Mod) -> + ok = meck:expect(Mod, test1, fun() -> ok end), + ok = meck:expect(Mod, test2, fun(_) -> ok end), + ok = meck:expect(Mod, test3, fun(_, _) -> ok end), + ?assertEqual(0, proplists:get_value(test1, Mod:module_info(exports))), + ?assertEqual(1, proplists:get_value(test2, Mod:module_info(exports))), + ?assertEqual(2, proplists:get_value(test3, Mod:module_info(exports))), + ?assertEqual(true, meck:validate(Mod)). + +call_return_value_(Mod) -> + ok = meck:expect(Mod, test, fun() -> apa end), + ?assertEqual(apa, Mod:test()), + ?assertEqual(true, meck:validate(Mod)). + +call_argument_(Mod) -> + ok = meck:expect(Mod, test, fun(hest, 1) -> apa end), + ?assertEqual(apa, Mod:test(hest, 1)), + ?assertEqual(true, meck:validate(Mod)). + +call_function_clause_(Mod) -> + ok = meck:expect(Mod, test, fun(hest, 1) -> apa end), + ?assertError(function_clause, Mod:test(hest, 2)), + ?assertEqual(false, meck:validate(Mod)). + +validate_unexpected_error_(Mod) -> + ok = meck:expect(Mod, test, fun(hest, 1) -> erlang:error(timeout) end), + ?assertError(timeout, Mod:test(hest, 1)), + ?assertEqual(false, meck:validate(Mod)). + +validate_expected_error_(Mod) -> + ok = meck:expect(Mod, test, fun(hest, 1) -> + meck:exception(error, timeout) + end), + ?assertError(timeout, Mod:test(hest, 1)), + ?assertEqual(true, meck:validate(Mod)). + +validate_chained_(Mod) -> + ok = meck:new(mymod2), + ok = meck:expect(mymod2, test, fun() -> + meck:exception(error, test_error) + end), + ok = meck:expect(Mod, test, fun() -> + mymod2:test() + end), + ?assertError(test_error, Mod:test()), + ?assertEqual(false, meck:validate(Mod)), + ?assertEqual(true, meck:validate(mymod2)), + ok = meck:unload(mymod2). + +stacktrace_(Mod) -> + ok = meck:expect(Mod, test, fun() -> erlang:error(test_error) end), + try + Mod:test(), + throw(failed) + catch + error:test_error -> + ?assert(lists:any(fun({M, test, []}) when M == Mod -> true; + ({M, test, [],[]}) when M == Mod -> true; + (_) -> false + end, erlang:get_stacktrace())) + end. + +stacktrace_function_clause_(Mod) -> + ok = meck:expect(Mod, test, fun(1) -> ok end), + try + Mod:test(error), + throw(failed) + catch + error:function_clause -> + Stacktrace = erlang:get_stacktrace(), + ?assert(lists:any(fun({M, test, [error]}) when M == Mod -> true; + ({M, test, [error], []}) when M == Mod -> true; + (_) -> false + end, Stacktrace)) + end. + + +call_undef_(Mod) -> + ok = meck:expect(Mod, test, fun(hest, 1) -> apa end), + ?assertError(undef, Mod:test(hest)). + +caller_does_not_crash_on_reload_(Mod) -> + ok = meck:expect(Mod, test, fun() -> timer:sleep(infinity) end), + Pid = spawn(fun() -> Mod:test() end), + ok = meck:expect(Mod, new1, fun() -> ok end), + ok = meck:expect(Mod, new2, fun() -> ok end), + ok = meck:expect(Mod, new3, fun() -> ok end), + ?assertEqual(true, is_process_alive(Pid)). + +change_func_(Mod) -> + ok = meck:expect(Mod, test, fun() -> 1 end), + ?assertEqual(1, Mod:test()), + ok = meck:expect(Mod, test, fun() -> 2 end), + ?assertEqual(2, Mod:test()). + +call_original_undef_(Mod) -> + ok = meck:expect(Mod, test, fun() -> meck:passthrough([]) end), + ?assertError(undef, Mod:test()). + +history_empty_(Mod) -> + ?assertEqual([], meck:history(Mod)). + +history_call_(Mod) -> + ok = meck:expect(Mod, test, fun() -> ok end), + ok = meck:expect(Mod, test2, fun(_, _) -> result end), + ok = meck:expect(Mod, test3, 0, 3), + Mod:test(), + Mod:test2(a, b), + Mod:test3(), + ?assertEqual([{self(), {Mod, test, []}, ok}, + {self(), {Mod, test2, [a, b]}, result}, + {self(), {Mod, test3, []}, 3}], meck:history(Mod)). + +history_throw_(Mod) -> + ok = meck:expect(Mod, test, fun() -> throw(test_exception) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, throw, test_exception, _Stacktrace}], + meck:history(Mod)). + +history_throw_fun_(Mod) -> + Fun = fun() -> exception_fun end, + ok = meck:expect(Mod, test, fun() -> throw(Fun) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, throw, Fun, _Stacktrace}], + meck:history(Mod)). + +history_exit_(Mod) -> + ok = meck:expect(Mod, test, fun() -> exit(test_exit) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, exit, test_exit, _Stacktrace}], + meck:history(Mod)). + +history_error_(Mod) -> + ok = meck:expect(Mod, test, fun() -> erlang:error(test_error) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, error, test_error, _Stacktrace}], + meck:history(Mod)). + +history_error_args_(Mod) -> + ok = meck:expect(Mod, test, fun() -> erlang:error(test_error, [fake_args]) end), + catch Mod:test(), + History = meck:history(Mod), + ?assertMatch([{_Pid, {Mod, test, []}, error, test_error, _Stacktrace}], + meck:history(Mod)), + [{_Pid, _MFA, error, test_error, Stacktrace}] = History, + ?assert(lists:any(fun({_M, _F, [fake_args]}) -> true; + ({_M, _F, [fake_args], [{file,_},{line,_}]}) -> true; + (_) -> false end, Stacktrace)). + +history_meck_throw_(Mod) -> + ok = meck:expect(Mod, test, fun() -> meck:exception(throw, test_exception) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, throw, test_exception, _Stacktrace}], + meck:history(Mod)). + +history_meck_throw_fun_(Mod) -> + Fun = fun() -> exception_fun end, + ok = meck:expect(Mod, test, fun() -> meck:exception(throw, Fun) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, throw, Fun, _Stacktrace}], + meck:history(Mod)). + +history_meck_exit_(Mod) -> + ok = meck:expect(Mod, test, fun() -> meck:exception(exit, test_exit) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, exit, test_exit, _Stacktrace}], + meck:history(Mod)). + +history_meck_error_(Mod) -> + ok = meck:expect(Mod, test, fun() -> meck:exception(error, test_error) end), + catch Mod:test(), + ?assertMatch([{_Pid, {Mod, test, []}, error, test_error, _Stacktrace}], + meck:history(Mod)). + +history_by_pid_(Mod) -> + ok = meck:expect(Mod, test1, fun() -> ok end), + ok = meck:expect(Mod, test2, fun() -> ok end), + + TestPid = self(), + Fun = fun() -> + Mod:test1(), + TestPid ! {self(), done} + end, + Pid = spawn(Fun), + Mod:test1(), + Mod:test2(), + receive {Pid, done} -> ok end, + ?assertEqual([{Pid, {Mod, test1, []}, ok}], meck:history(Mod, Pid)), + ?assertEqual([{TestPid, {Mod, test1, []}, ok}, + {TestPid, {Mod, test2, []}, ok}], meck:history(Mod, TestPid)), + ?assertEqual(meck:history(Mod), meck:history(Mod, '_')). + +shortcut_expect_(Mod) -> + ok = meck:expect(Mod, test, 0, ok), + ?assertEqual(true, meck:validate(Mod)). + +shortcut_expect_negative_arity_(Mod) -> + ?assertError(function_clause, meck:expect(Mod, test, -1, ok)). + +shortcut_call_return_value_(Mod) -> + ok = meck:expect(Mod, test, 0, apa), + ?assertEqual(apa, Mod:test()), + ?assertEqual(true, meck:validate(Mod)). + +shortcut_call_argument_(Mod) -> + ok = meck:expect(Mod, test, 2, apa), + ?assertEqual(apa, Mod:test(hest, 1)), + ?assertEqual(true, meck:validate(Mod)). + +shortcut_re_add_(Mod) -> + ok = meck:expect(Mod, test, 2, apa), + ?assertEqual(apa, Mod:test(hest, 1)), + ok = meck:expect(Mod, test, 2, new), + ?assertEqual(new, Mod:test(hest, 1)), + ?assertEqual(true, meck:validate(Mod)). + +shortcut_opaque_(Mod) -> + ok = meck:expect(Mod, test, 0, {test, [a, self()]}), + ?assertMatch({test, [a, P]} when P == self(), Mod:test()). + +delete_(Mod) -> + ok = meck:expect(Mod, test, 2, ok), + ?assertEqual(ok, meck:delete(Mod, test, 2)), + ?assertError(undef, Mod:test(a, b)), + ?assert(meck:validate(Mod)). + +called_false_no_args_(Mod) -> + Args = [], + ok = meck:expect(Mod, test, length(Args), ok), + assert_called(Mod, test, Args, false), + ok. + +called_true_no_args_(Mod) -> + Args = [], + ok = meck:expect(Mod, test, length(Args), ok), + ok = apply(Mod, test, Args), + assert_called(Mod, test, Args, true), + ok. + +called_true_two_functions_(Mod) -> + Args = [], + ok = meck:expect(Mod, test1, length(Args), ok), + ok = meck:expect(Mod, test2, length(Args), ok), + ok = apply(Mod, test1, Args), + ok = apply(Mod, test2, Args), + assert_called(Mod, test2, Args, true), + ok. + +called_false_one_arg_(Mod) -> + Args = ["hello"], + ok = meck:expect(Mod, test, length(Args), ok), + assert_called(Mod, test, Args, false), + ok. + +called_true_one_arg_(Mod) -> + Args = ["hello"], + ok = meck:expect(Mod, test, length(Args), ok), + ok = apply(Mod, test, Args), + assert_called(Mod, test, Args, true), + ok. + +called_false_few_args_(Mod) -> + Args = [one, 2, {three, 3}, "four"], + ok = meck:expect(Mod, test, length(Args), ok), + assert_called(Mod, test, Args, false), + ok. + +called_true_few_args_(Mod) -> + Args = [one, 2, {three, 3}, "four"], + ok = meck:expect(Mod, test, length(Args), ok), + ok = apply(Mod, test, Args), + assert_called(Mod, test, Args, true), + ok. + +called_false_error_(Mod) -> + Args = [one, "two", {3, 3}], + TestFun = fun (_, _, _) -> meck:exception(error, my_error) end, + ok = meck:expect(Mod, test, TestFun), + assert_called(Mod, test, Args, false), + ok. + +called_true_error_(Mod) -> + Args = [one, "two", {3, 3}], + expect_catch_apply(Mod, test, Args), + assert_called(Mod, test, Args, true), + ok. + +called_with_pid_no_args_(Mod) -> + Args = [], + ok = meck:expect(Mod, test, length(Args), ok), + Pid = spawn_caller_and_sync(Mod, test, Args), + assert_called(Mod, test, Args, self(), false), + assert_called(Mod, test, Args, Pid, true), + ok = apply(Mod, test, Args), + assert_called(Mod, test, Args, self(), true), + ?assertEqual(meck:called(Mod, test, Args, '_'), + meck:called(Mod, test, Args)). + +spawn_caller_and_sync(Mod, Func, Args) -> + TestPid = self(), + Fun = fun() -> + catch apply(Mod, Func, Args), + TestPid ! {self(), done} + end, + Pid = spawn(Fun), + receive {Pid, done} -> ok end, % sync with the spawned process + Pid. + +num_calls_(Mod) -> + Args = [], + IncorrectArgs = [foo], + ok = meck:expect(Mod, test1, length(Args), ok), + ?assertEqual(0, meck:num_calls(Mod, test1, Args)), + ok = apply(Mod, test1, Args), + ?assertEqual(1, meck:num_calls(Mod, test1, Args)), + ?assertEqual(0, meck:num_calls(Mod, test1, IncorrectArgs)). + +num_calls_error_(Mod) -> + Args = [one, "two", {3, 3}], + expect_catch_apply(Mod, test, Args), + ?assertEqual(1, meck:num_calls(Mod, test, Args)). + +num_calls_with_pid_no_args_(Mod) -> + Args = [], + ok = meck:expect(Mod, test, length(Args), ok), + Pid = spawn_caller_and_sync(Mod, test, Args), + ?assertEqual(0, meck:num_calls(Mod, test, Args, self())), + ?assertEqual(1, meck:num_calls(Mod, test, Args, Pid)), + ok = apply(Mod, test, Args), + ?assertEqual(1, meck:num_calls(Mod, test, Args, self())), + ?assertEqual(meck:num_calls(Mod, test, Args, '_'), + meck:num_calls(Mod, test, Args)). + +expect_apply(Mod, Func, Args) -> + ok = meck:expect(Mod, Func, length(Args), ok), + ok = apply(Mod, Func, Args). + +expect_catch_apply(Mod, Func, Args) -> + TestFun = fun (_, _, _) -> meck:exception(error, my_error) end, + ok = meck:expect(Mod, Func, TestFun), + catch apply(Mod, Func, Args). + +called_wildcard_(Mod) -> + Args = [one, 2, {three, 3}, "four"], + ok = meck:expect(Mod, test, length(Args), ok), + ok = apply(Mod, test, Args), + assert_called(Mod, test, [one, '_', {three, '_'}, "four"], true), + ok. + +sequence_(Mod) -> + Sequence = [a, b, c, d, e], + ?assertEqual(ok, meck:sequence(Mod, s, 2, Sequence)), + ?assertEqual(Sequence, + [Mod:s(a, b) || _ <- lists:seq(1, length(Sequence))]), + ?assertEqual([e, e, e, e, e], + [Mod:s(a, b) || _ <- lists:seq(1, 5)]), + ?assert(meck:validate(Mod)). + +sequence_multi_(Mod) -> + meck:new(mymod2), + Mods = [Mod, mymod2], + Sequence = [a, b, c, d, e], + ?assertEqual(ok, meck:sequence(Mods, s, 2, Sequence)), + ?assertEqual(Sequence, + [Mod:s(a, b) || _ <- lists:seq(1, length(Sequence))]), + ?assertEqual([e, e, e, e, e], + [Mod:s(a, b) || _ <- lists:seq(1, 5)]), + ?assertEqual(Sequence, + [mymod2:s(a, b) || _ <- lists:seq(1, length(Sequence))]), + ?assertEqual([e, e, e, e, e], + [mymod2:s(a, b) || _ <- lists:seq(1, 5)]), + ?assert(meck:validate(Mods)). + +loop_(Mod) -> + Loop = [a, b, c, d, e], + ?assertEqual(ok, meck:loop(Mod, l, 2, Loop)), + [?assertEqual(V, Mod:l(a, b)) || _ <- lists:seq(1, length(Loop)), V <- Loop], + ?assert(meck:validate(Mod)). + +loop_multi_(Mod) -> + meck:new(mymod2), + Mods = [Mod, mymod2], + Loop = [a, b, c, d, e], + ?assertEqual(ok, meck:loop(Mods, l, 2, Loop)), + [[?assertEqual(V, M:l(a, b)) || _ <- lists:seq(1, length(Loop)), V <- Loop] + || M <- Mods], + ?assert(meck:validate(Mods)). + +%% --- Tests with own setup ---------------------------------------------------- + +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, [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), + ?assertEqual(c, meck_test_module:a()), + ?assertEqual(b, meck_test_module:b()), + ok = meck:unload(meck_test_module). + +unload_renamed_original_test() -> + ok = meck:new(meck_test_module), + ok = meck:unload(meck_test_module), + ?assertEqual(false, code:is_loaded(meck_test_module_meck_original)). + +unload_all_test() -> + Mods = [test_a, test_b, test_c, test_d, test_e], + ok = meck:new(Mods), + ?assertEqual(lists:sort(Mods), lists:sort(meck:unload())), + [?assertEqual(false, code:is_loaded(M)) || M <- Mods]. + +original_no_file_test() -> + {ok, Mod, Beam} = compile:forms([{attribute, 1, module, meck_not_on_disk}]), + {module, Mod} = code:load_binary(Mod, "", Beam), + ?assertEqual(ok, meck:new(meck_not_on_disk)), + ok = meck:unload(meck_not_on_disk). + +original_has_no_object_code_test() -> + {ok, Mod, Beam} = compile:forms([{attribute, 1, module, meck_on_disk}]), + ok = file:write_file("meck_on_disk.beam", Beam), + {module, Mod} = code:load_binary(Mod, "meck_on_disk.beam", Beam), + ?assertEqual(ok, meck:new(meck_on_disk)), + ok = file:delete("meck_on_disk.beam"), + ok = meck:unload(meck_on_disk). + +passthrough_nonexisting_module_test() -> + ok = meck:new(mymod, [passthrough]), + ok = meck:expect(mymod, test, fun() -> ok end), + ?assertEqual(ok, mymod:test()), + ok = meck:unload(mymod). + +passthrough_test() -> + 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()), + ?assertEqual({1, 2}, meck_test_module:c(1, 2)), + ok = meck:unload(meck_test_module). + +passthrough_different_arg_test() -> + ok = meck:new(meck_test_module), + ok = meck:expect(meck_test_module, c, + fun(_, _) -> meck:passthrough([x, y]) end), + ?assertEqual({x, y}, meck_test_module:c(1, 2)), + ok = meck:unload(meck_test_module). + +passthrough_bif_test() -> + ?assertEqual(ok, meck:new(file, [unstick, passthrough])), + ?assertEqual(ok, meck:unload(file)). + +cover_test() -> + {ok, _} = cover:compile("../test/meck_test_module.erl"), + a = meck_test_module:a(), + b = meck_test_module:b(), + {1, 2} = meck_test_module:c(1, 2), + {ok, {meck_test_module, {3,0}}} = cover:analyze(meck_test_module, module), + run_mock_no_cover_file(meck_test_module), + {ok, {meck_test_module, {3,0}}} = cover:analyze(meck_test_module, module). + +cover_options_test_() -> + {foreach, fun compile_options_setup/0, fun compile_options_teardown/1, + [{with, [T]} || T <- [fun ?MODULE:cover_options_/1, + fun ?MODULE:cover_options_fail_/1 + ]]}. + +compile_options_setup() -> + Module = cover_test_module, + % Our test module won't compile without compiler options that + % rebar won't give it, thus the rename dance. + Src = join("../test/", Module, ".erl"), + ok = file:rename(join("../test/", Module, ".dontcompile"), Src), + OldPath = code:get_path(), + code:add_path("../test"), + {OldPath, Src, Module}. + +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}) -> + % Test that compilation options (include paths and preprocessor + % definitions) are used when un-mecking previously cover compiled + % modules. + CompilerOptions = [{i, "../test/include"}, {d, 'TEST', true}], + % The option recover feature depends on having the BEAM file + % available. + {ok, _} = compile:file(Src, [{outdir, "../test"}|CompilerOptions]), + {ok, _} = cover:compile(Src, CompilerOptions), + a = Module:a(), + b = Module:b(), + {1, 2} = Module:c(1, 2), + % We get 2 instead of 3 as expected. Maybe because cover doesn't + % count include files? + ?assertEqual({ok, {Module, {2,0}}}, cover:analyze(Module, module)), + run_mock_no_cover_file(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) -> + ok = meck:new(Module), + ok = meck:expect(Module, a, fun () -> c end), + ?assertEqual(c, Module:a()), + ok = meck:unload(Module), + ?assert(not filelib:is_file(atom_to_list(Module) ++ ".coverdata")). + +%% @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([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), + ?assertMatch({file, _}, code:is_loaded(mymod)), + SaltedName = mymod_meck, + Pid = whereis(SaltedName), + ?assertEqual(true, is_pid(Pid)), + unlink(Pid), + exit(Pid, expected_test_exit), + timer:sleep(100), + ?assertEqual(undefined, whereis(SaltedName)), + ?assertEqual(false, code:is_loaded(mymod)). + +% @doc The mocked module is unloaded if the meck process crashes. +unlink_test() -> + ok = meck:new(mymod, [no_link]), + SaltedName = mymod_meck, + {links, Links} = process_info(whereis(SaltedName), links), + ?assert(not lists:member(self(), Links)), + ok = meck:unload(mymod). + +%% @doc Exception is thrown when you run expect on a non-existing (and not yet mocked) module. +expect_without_new_test() -> + ?assertError({not_mocked, othermod}, + meck:expect(othermod, test, fun() -> ok end)). + +history_passthrough_test() -> + ok = meck:new(meck_test_module, [passthrough]), + ok = meck:expect(meck_test_module, a, fun() -> c end), + c = meck_test_module:a(), + b = meck_test_module:b(), + ?assertEqual([{self(), {meck_test_module, a, []}, c}, + {self(), {meck_test_module, b, []}, b}], + meck:history(meck_test_module)), + ok = meck:unload(meck_test_module). + +multi_test() -> + Mods = [mod1, mod2, mod3], + ok = meck:new(Mods), + ok = meck:expect(Mods, test, fun() -> ok end), + ok = meck:expect(Mods, test2, 0, ok), + [?assertEqual(ok, M:test()) || M <- Mods], + ?assert(meck:validate(Mods)), + ok = meck:unload(Mods). + +multi_invalid_test() -> + Mods = [mod1, mod2, mod3], + ok = meck:new(Mods), + ok = meck:expect(Mods, test, fun(1) -> ok end), + ?assertError(function_clause, mod2:test(2)), + ?assert(not meck:validate(Mods)), + ok = meck:unload(Mods). + +multi_option_test() -> + Mods = [mod1, mod2, mod3], + ok = meck:new(Mods, [passthrough]), + ok = meck:expect(Mods, test, fun() -> ok end), + [?assertEqual(ok, M:test()) || M <- Mods], + ?assert(meck:validate(Mods)), + ok = meck:unload(Mods). + +multi_shortcut_test() -> + Mods = [mod1, mod2, mod3], + ok = meck:new(Mods), + ok = meck:expect(Mods, test, 0, ok), + [?assertEqual(ok, M:test()) || M <- Mods], + ?assert(meck:validate(Mods)), + ok = meck:unload(Mods). + +multi_delete_test() -> + Mods = [mod1, mod2, mod3], + ok = meck:new(Mods), + ok = meck:expect(Mods, test, 0, ok), + ?assertEqual(ok, meck:delete(Mods, test, 0)), + [?assertError(undef, M:test()) || M <- Mods], + ?assert(meck:validate(Mods)), + ok = meck:unload(Mods). + +handle_cast_unmodified_state_test() -> + S = dummy_state, + ?assertEqual({noreply, S}, meck:handle_cast(dummy_msg, S)). + +code_change_unmodified_state_test() -> + S = dummy_state, + ?assertEqual({ok, S}, meck:code_change(old_version, S, [])). + +remote_meck_test_() -> + {foreach, fun remote_setup/0, fun remote_teardown/1, + [{with, [T]} || T <- [fun remote_meck_/1, + fun remote_meck_cover_/1]]}. + +remote_setup() -> + [] = os:cmd("epmd -daemon"), + Hostname = "localhost", + Myself = list_to_atom("meck_eunit_test@" ++ Hostname), + net_kernel:start([Myself, shortnames]), + {ok, Node} = slave:start_link(list_to_atom(Hostname), meck_remote_test, + "-pa test"), + {Mod, Bin, File} = code:get_object_code(meck), + {module, Mod} = rpc:call(Node, code, load_binary, [Mod, File, Bin]), + {module, meck_test_module} = + rpc:call(Node, code, load_file, [meck_test_module]), + {Node, meck_test_module}. + +remote_teardown({Node, _Mod}) -> + ok = slave:stop(Node). + +remote_meck_({Node, Mod}) -> + ?assertEqual(ok, rpc:call(Node, meck, new, [Mod, [no_link]])), + ?assertEqual(ok, rpc:call(Node, meck, expect, [Mod, test, 0, true])), + ?assertEqual(true, rpc:call(Node, Mod, test, [])). + +remote_meck_cover_({Node, Mod}) -> + {ok, Mod} = cover:compile(Mod), + {ok, _Nodes} = cover:start([Node]), + ?assertEqual(ok, rpc:call(Node, meck, new, [Mod])). + +can_mock_sticky_modules_test() -> + code:stick_mod(meck_test_module), + meck:new(meck_test_module, [unstick]), + ?assertNot(code:is_sticky(meck_test_module)), + meck:unload(meck_test_module), + ?assert(code:is_sticky(meck_test_module)), + code:unstick_mod(meck_test_module). + + +sticky_directory_test_() -> + {foreach, fun sticky_setup/0, fun sticky_teardown/1, + [{with, [T]} + || T <- [fun ?MODULE:can_mock_sticky_module_not_yet_loaded_/1, + fun ?MODULE:cannot_mock_sticky_module_without_unstick_/1]]}. + +sticky_setup() -> + % Find out where the beam file is (purge because it is cover compiled) + Module = meck_test_module, + false = code:purge(Module), + {module, Module} = code:load_file(Module), + Beam = code:which(Module), + + % Unload module so it's not loaded when running meck + false = code:purge(Module), + true = code:delete(Module), + + % Create new sticky dir and copy beam file + Dir = "sticky_test", + ok = filelib:ensure_dir(filename:join(Dir, "dummy")), + Dest = filename:join(Dir, filename:basename(Beam)), + {ok, _BytesCopied} = file:copy(Beam, Dest), + true = code:add_patha(Dir), + ok = code:stick_dir(Dir), + code:load_file(Module), + + {Module, {Dir, Dest}}. + +sticky_teardown({Module, {Dir, Dest}}) -> + % Clean up + ok = code:unstick_dir(Dir), + false = code:purge(Module), + true = code:del_path(Dir), + ok = file:delete(Dest), + ok = file:del_dir(Dir). + +can_mock_sticky_module_not_yet_loaded_({Mod, _}) -> + ?assertEqual(ok, meck:new(Mod, [unstick])), + ?assertNot(code:is_sticky(Mod)), + ?assertEqual(ok, meck:unload(Mod)), + ?assert(code:is_sticky(Mod)). + +cannot_mock_sticky_module_without_unstick_({Mod, _}) -> + ?assertError(module_is_sticky, meck:new(Mod, [no_link])). + +can_mock_non_sticky_module_test() -> + ?assertNot(code:is_sticky(meck_test_module)), + ?assertEqual(ok, meck:new(meck_test_module, [unstick])), + ?assertNot(code:is_sticky(meck_test_module)), + ?assertEqual(ok, meck:unload(meck_test_module)), + ?assertNot(code:is_sticky(meck_test_module)). + +cannot_expect_bif_or_autogenerated_test() -> + ?assertEqual(ok, meck:new(unicode, [unstick, passthrough])), + ?assertError({cannot_mock_builtin, {unicode, characters_to_binary, 2}}, + meck:expect(unicode, characters_to_binary, 2, doh)), + ?assertError({cannot_mock_autogenerated, {unicode, module_info, 0}}, + meck:expect(unicode, module_info, 0, doh)), + ?assertEqual(ok, meck:unload(unicode)). + +meck_parametrized_module_test() -> + ?assertEqual(ok, meck:new(meck_test_parametrized_module)), + ?assertEqual(ok, meck:expect(meck_test_parametrized_module, new, + fun(V1, V2) -> + {meck_test_parametrized_module, V1, V2} + end)), + ?assertEqual(ok, meck:expect(meck_test_parametrized_module, which, 1, mecked)), + Object = meck_test_parametrized_module:new(var1, var2), + ?assertEqual(mecked, Object:which()), + ?assertEqual(ok, meck:unload(meck_test_parametrized_module)). + +meck_parametrized_module_passthrough_test() -> + ?assertEqual(ok, meck:new(meck_test_parametrized_module, [passthrough])), + ?assertEqual(ok, meck:expect(meck_test_parametrized_module, new, + fun(V1, V2) -> + {meck_test_parametrized_module, V1, V2} + end)), + ?assertEqual(ok, meck:expect(meck_test_parametrized_module, var2, + fun({_, _Var1, Var2} = _This) -> + {mecked, Var2} + end)), + Object = meck_test_parametrized_module:new(var1, var2), + ?assertEqual({original, var1}, Object:var1()), + ?assertEqual({mecked, var2}, Object:var2()), + ?assertEqual(ok, meck:unload(meck_test_parametrized_module)). + +%%============================================================================== +%% Internal Functions +%%============================================================================== + +assert_called(Mod, Function, Args, WasCalled) -> + ?assertEqual(WasCalled, meck:called(Mod, Function, Args)), + ?assert(meck:validate(Mod)). + +assert_called(Mod, Function, Args, Pid, WasCalled) -> + ?assertEqual(WasCalled, meck:called(Mod, Function, Args, Pid)), + ?assert(meck:validate(Mod)). -- cgit v1.2.3