├── doc ├── edoc-info ├── README.md └── mockgyver.md ├── .edts ├── .gitignore ├── src ├── mockgyver.app.src ├── mockgyver_sup.erl ├── mockgyver_app.erl ├── mockgyver_xform.erl └── mockgyver.erl ├── rebar.lock ├── .github └── workflows │ └── erlang.yml ├── rebar.config ├── LICENSE ├── test ├── mockgyver_dummy.erl └── mockgyver_tests.erl ├── include └── mockgyver.hrl └── README.md /doc/edoc-info: -------------------------------------------------------------------------------- 1 | %% encoding: UTF-8 2 | {application,mockgyver}. 3 | {modules,[mockgyver]}. 4 | -------------------------------------------------------------------------------- /.edts: -------------------------------------------------------------------------------- 1 | :name "mockgyver" 2 | :lib-dirs '("deps" "test") 3 | :start-command "erl -sname mockgyver -pa test" 4 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # The mockgyver application # 4 | 5 | 6 | ## Modules ## 7 | 8 | 9 | 10 |
mockgyver
11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | # Rebar3 builds in this directory 3 | /_build 4 | # Compiled code 5 | /ebin/*.beam 6 | /test/*.beam 7 | # rebar3 edoc artifacts (but keep the markdown files for viewing in github) 8 | /doc/*.html 9 | /doc/erlang.png 10 | /doc/stylesheet.css 11 | # The .app file is automatically generated by rebar 12 | /ebin/*.app 13 | # Created by test cases 14 | /test/mockgyver_dummy*.erl 15 | -------------------------------------------------------------------------------- /src/mockgyver.app.src: -------------------------------------------------------------------------------- 1 | %% Tell emacs to use -*- erlang -*- mode for this file 2 | {application, 3 | mockgyver, 4 | [{description,"Mocking library"}, 5 | {vsn,"1.8.0"}, 6 | {registered,[]}, 7 | {mod, {mockgyver_app, []}}, 8 | {applications,[kernel, stdlib]}, 9 | {env,[]}, 10 | {licenses, ["BSD"]}, 11 | {maintainers, ["Klas Johansson"]}, 12 | {links, [{"Github", "https://github.com/klajo/mockgyver"}]}]}. 13 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"eunit_addons">>,{pkg,<<"eunit_addons">>,<<"1.2.0">>},0}, 3 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.1">>},0}]}. 4 | [ 5 | {pkg_hash,[ 6 | {<<"eunit_addons">>, <<"A9987E7CC3BC372809A2B8F90807BD863A84DBF1550672E8F2919FBE81C2227B">>}, 7 | {<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>}]}, 8 | {pkg_hash_ext,[ 9 | {<<"eunit_addons">>, <<"C28C8C72F4BE61D45F59801E1CAC7D810AB1A8F8C6F3B96648E8C422C1ADBF2A">>}, 10 | {<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>}]} 11 | ]. 12 | -------------------------------------------------------------------------------- /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | otpvsn: [23, 24, 25, 26] 16 | 17 | container: 18 | image: erlang:${{ matrix.otpvsn }} 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Compile 23 | run: rebar3 compile 24 | - name: Dialyze 25 | run: rebar3 dialyzer 26 | - name: Run tests 27 | run: rebar3 eunit 28 | - name: Generate docs 29 | run: rebar3 edoc 30 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% Tell emacs to use -*- erlang -*- mode for this file 2 | {deps, 3 | [{parse_trans, "3.4.1"}, 4 | {eunit_addons, "1.2.0"} 5 | ]}. 6 | 7 | {project_plugins, [rebar3_ex_doc]}. 8 | {hex, [{doc, ex_doc}]}. 9 | {ex_doc, 10 | [{extras, ["README.md", "LICENSE"]}, 11 | {main, "README.md"}, 12 | {source_url, "https://github.com/klajo/mockgyver"} 13 | ]}. 14 | 15 | {edoc_opts, 16 | [{preprocess, true}]}. 17 | 18 | {dialyzer, [{warnings, [unknown]}, 19 | {plt_apps, all_deps}, 20 | {plt_extra_apps, [syntax_tools, compiler, 21 | parse_trans]}]}. 22 | {profiles, 23 | %% Add the edown dependency (for generating markdown docs) when 24 | %% running with the edown profile ==> make it an optional dependency. 25 | [{edown, [{deps, [edown]}, 26 | {edoc_opts, [{doclet, edown_doclet}, 27 | {preprocess, true}]}]}]}. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Klas Johansson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in 13 | the documentation and/or other materials provided with the 14 | distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 21 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 23 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 26 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /test/mockgyver_dummy.erl: -------------------------------------------------------------------------------- 1 | %%%=================================================================== 2 | %%% Copyright (c) 2011, Klas Johansson 3 | %%% All rights reserved. 4 | %%% 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are 7 | %%% met: 8 | %%% 9 | %%% * Redistributions of source code must retain the above copyright 10 | %%% notice, this list of conditions and the following disclaimer. 11 | %%% 12 | %%% * Redistributions in binary form must reproduce the above copyright 13 | %%% notice, this list of conditions and the following disclaimer in 14 | %%% the documentation and/or other materials provided with the 15 | %%% distribution. 16 | %%% 17 | %%% * Neither the name of the copyright holder nor the names of its 18 | %%% contributors may be used to endorse or promote products derived 19 | %%% from this software without specific prior written permission. 20 | %%% 21 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | %%% IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | %%% TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | %%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | %%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | %%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 27 | %%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | %%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | %%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | %%%=================================================================== 33 | 34 | %%%------------------------------------------------------------------- 35 | %%% @doc 36 | %%% Test {@link mockgyver} 37 | %%% @end 38 | %%%------------------------------------------------------------------- 39 | -module(mockgyver_dummy). 40 | 41 | -export([return_arg/1, return_arg/2, return_arg2/1]). 42 | 43 | return_arg(N) -> 44 | N. 45 | 46 | return_arg(M, N) -> 47 | {M, N}. 48 | 49 | return_arg2(N) -> 50 | N. 51 | -------------------------------------------------------------------------------- /include/mockgyver.hrl: -------------------------------------------------------------------------------- 1 | -ifndef(MOCKGYVER_HRL). 2 | -define(MOCKGYVER_HRL, true). 3 | 4 | -include_lib("eunit_addons/include/eunit_addons.hrl"). 5 | 6 | -compile({parse_transform, mockgyver_xform}). 7 | 8 | %% run tests with a mock 9 | -define(WITH_MOCKED_SETUP(SetupFun, CleanupFun, ForAllTimeout, PerTcTimeout, 10 | Tests), 11 | {timeout, ForAllTimeout, 12 | {setup, 13 | fun() -> mockgyver:start_session(?MOCK_SESSION_PARAMS) end, 14 | fun(_) -> mockgyver:end_session() end, 15 | [{timeout, PerTcTimeout, % timeout for each test 16 | {spawn, 17 | {atom_to_list(__Test), % label per test 18 | fun() -> 19 | try 20 | case mockgyver:start_session_element() of 21 | ok -> 22 | Env = SetupFun(), 23 | try 24 | apply(?MODULE, __Test, [Env]) 25 | after 26 | CleanupFun(Env) 27 | end; 28 | {error, Reason} -> 29 | error({mockgyver_session_elem_fail, Reason}) 30 | end 31 | after 32 | mockgyver:end_session_element() 33 | end 34 | end}}} 35 | || __Test <- Tests]}}). 36 | 37 | -define(WITH_MOCKED_SETUP(SetupFun, CleanupFun, ForAllTimeout, PerTcTimeout), 38 | ?WITH_MOCKED_SETUP(SetupFun, CleanupFun, ForAllTimeout, PerTcTimeout, 39 | eunit_addons:get_tests_with_setup(?MODULE))). 40 | 41 | -define(WITH_MOCKED_SETUP(SetupFun, CleanupFun), 42 | ?WITH_MOCKED_SETUP(SetupFun, CleanupFun, 43 | ?FOR_ALL_TIMEOUT, ?PER_TC_TIMEOUT)). 44 | 45 | -define(WRAP(Type), 46 | {'$mock', Type, {?FILE, ?LINE}}). 47 | 48 | -define(WRAP(Type, Expr), 49 | {'$mock', Type, Expr, {?FILE, ?LINE}}). 50 | 51 | -define(MOCK_SESSION_PARAMS, ?WRAP(m_session_params)). 52 | 53 | -define(MOCK(Expr), mockgyver:exec(?MOCK_SESSION_PARAMS, (Expr))). 54 | 55 | -define(WHEN(Expr), ?WRAP(m_when, case x of Expr end)). 56 | 57 | -define(VERIFY(Expr, Args), 58 | ?WRAP(m_verify, {case x of Expr -> ok end, Args})). 59 | 60 | -define(WAS_CALLED(Expr), 61 | ?WAS_CALLED(Expr, once)). 62 | -define(WAS_CALLED(Expr, Criteria), 63 | ?VERIFY(Expr, {was_called, Criteria})). 64 | 65 | -define(WAIT_CALLED(Expr), 66 | ?WAIT_CALLED(Expr, once)). 67 | -define(WAIT_CALLED(Expr, Criteria), 68 | ?VERIFY(Expr, {wait_called, Criteria})). 69 | 70 | -define(NUM_CALLS(Expr), 71 | ?VERIFY(Expr, num_calls)). 72 | 73 | -define(GET_CALLS(Expr), 74 | ?VERIFY(Expr, get_calls)). 75 | 76 | -define(FORGET_WHEN(Expr), 77 | ?VERIFY(Expr, forget_when)). 78 | 79 | -define(FORGET_CALLS(Expr), 80 | ?VERIFY(Expr, forget_calls)). 81 | 82 | -define(FORGET_CALLS(), 83 | mockgyver:forget_all_calls()). 84 | 85 | -endif. 86 | -------------------------------------------------------------------------------- /src/mockgyver_sup.erl: -------------------------------------------------------------------------------- 1 | %%%=================================================================== 2 | %%% Copyright (c) 2011, Klas Johansson 3 | %%% All rights reserved. 4 | %%% 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are 7 | %%% met: 8 | %%% 9 | %%% * Redistributions of source code must retain the above copyright 10 | %%% notice, this list of conditions and the following disclaimer. 11 | %%% 12 | %%% * Redistributions in binary form must reproduce the above copyright 13 | %%% notice, this list of conditions and the following disclaimer in 14 | %%% the documentation and/or other materials provided with the 15 | %%% distribution. 16 | %%% 17 | %%% * Neither the name of the copyright holder nor the names of its 18 | %%% contributors may be used to endorse or promote products derived 19 | %%% from this software without specific prior written permission. 20 | %%% 21 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | %%% IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | %%% TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | %%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | %%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | %%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 27 | %%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | %%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | %%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | %%%=================================================================== 33 | 34 | %%%------------------------------------------------------------------- 35 | %%% @author Klas Johansson 36 | %%% @copyright 2011, Klas Johansson 37 | %%% @private 38 | %%% @doc Supervise the app. 39 | %%% @end 40 | %%%------------------------------------------------------------------- 41 | -module(mockgyver_sup). 42 | 43 | -behaviour(supervisor). 44 | 45 | %% API 46 | -export([start_link/0]). 47 | 48 | %% Supervisor callbacks 49 | -export([init/1]). 50 | 51 | -define(SERVER, ?MODULE). 52 | 53 | %%%=================================================================== 54 | %%% API functions 55 | %%%=================================================================== 56 | 57 | %%-------------------------------------------------------------------- 58 | %% @doc 59 | %% Starts the supervisor 60 | %% @end 61 | %%-------------------------------------------------------------------- 62 | -spec start_link() -> supervisor:startlink_ret(). 63 | start_link() -> 64 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 65 | 66 | %%%=================================================================== 67 | %%% Supervisor callbacks 68 | %%%=================================================================== 69 | 70 | %%-------------------------------------------------------------------- 71 | %% @private 72 | %% @doc 73 | %% Whenever a supervisor is started using supervisor:start_link/[2,3], 74 | %% this function is called by the new process to find out about 75 | %% restart strategy, maximum restart frequency and child 76 | %% specifications. 77 | %% @end 78 | %%-------------------------------------------------------------------- 79 | -spec init(Args :: term()) -> {ok, {supervisor:sup_flags(), 80 | [supervisor:child_spec()]}} | 81 | ignore. 82 | init([]) -> 83 | Child = {mockgyver, {mockgyver, start_link, []}, 84 | permanent, 10000, worker, [mockgyver]}, 85 | {ok, {{one_for_one, 100, 60}, [Child]}}. 86 | 87 | %%%=================================================================== 88 | %%% Internal functions 89 | %%%=================================================================== 90 | -------------------------------------------------------------------------------- /src/mockgyver_app.erl: -------------------------------------------------------------------------------- 1 | %%%=================================================================== 2 | %%% Copyright (c) 2011, Klas Johansson 3 | %%% All rights reserved. 4 | %%% 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are 7 | %%% met: 8 | %%% 9 | %%% * Redistributions of source code must retain the above copyright 10 | %%% notice, this list of conditions and the following disclaimer. 11 | %%% 12 | %%% * Redistributions in binary form must reproduce the above copyright 13 | %%% notice, this list of conditions and the following disclaimer in 14 | %%% the documentation and/or other materials provided with the 15 | %%% distribution. 16 | %%% 17 | %%% * Neither the name of the copyright holder nor the names of its 18 | %%% contributors may be used to endorse or promote products derived 19 | %%% from this software without specific prior written permission. 20 | %%% 21 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | %%% IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | %%% TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | %%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | %%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | %%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 27 | %%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | %%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | %%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | %%%=================================================================== 33 | 34 | %%%------------------------------------------------------------------- 35 | %%% @author Klas Johansson 36 | %%% @copyright 2011, Klas Johansson 37 | %%% @private 38 | %%% @doc Start the application. 39 | %%% @end 40 | %%%------------------------------------------------------------------- 41 | -module(mockgyver_app). 42 | 43 | -behaviour(application). 44 | 45 | %% Application callbacks 46 | -export([start/2, stop/1]). 47 | 48 | %%%=================================================================== 49 | %%% Application callbacks 50 | %%%=================================================================== 51 | 52 | %%-------------------------------------------------------------------- 53 | %% @private 54 | %% @doc 55 | %% This function is called whenever an application is started using 56 | %% application:start/[1,2], and should start the processes of the 57 | %% application. If the application is structured according to the OTP 58 | %% design principles as a supervision tree, this means starting the 59 | %% top supervisor of the tree. 60 | %% @end 61 | %%-------------------------------------------------------------------- 62 | -spec start(StartType :: application:start_type(), 63 | StartArgs :: term()) -> 64 | {ok, pid()} | 65 | {ok, pid(), State :: term()} | 66 | {error, Reason :: term()}. 67 | start(_StartType, _StartArgs) -> 68 | case mockgyver_sup:start_link() of 69 | {ok, _Pid} = OkRes -> 70 | OkRes; 71 | Error -> 72 | Error 73 | end. 74 | 75 | %%-------------------------------------------------------------------- 76 | %% @private 77 | %% @doc 78 | %% This function is called whenever an application has stopped. It 79 | %% is intended to be the opposite of Module:start/2 and should do 80 | %% any necessary cleaning up. The return value is ignored. 81 | %% @end 82 | %%-------------------------------------------------------------------- 83 | -spec stop(State :: term()) -> ok. 84 | stop(_State) -> 85 | ok. 86 | 87 | %%%=================================================================== 88 | %%% Internal functions 89 | %%%=================================================================== 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mockgyver 2 | ========= 3 | 4 | [![Hex pm](https://img.shields.io/hexpm/v/mockgyver.svg?style=flat)](https://hex.pm/packages/mockgyver) 5 | [![Build Status](https://github.com/klajo/mockgyver/workflows/Erlang%20CI/badge.svg)](https://github.com/klajo/mockgyver/actions?query=workflow%3A%22Erlang+CI%22) 6 | [![Erlang Versions](https://img.shields.io/badge/Supported%20Erlang%2FOTP%20releases-23%20to%2026-blue)](http://www.erlang.org) 7 | 8 | mockgyver is an Erlang tool which will make it easier 9 | to write EUnit tests which need to replace or alter 10 | (stub/mock) the behaviour of other modules. 11 | 12 | mockgyver aims to make that process as easy as possible 13 | with a readable and concise syntax. 14 | 15 | mockgyver is built around two main constructs: 16 | **?WHEN** which makes it possible to alter the 17 | behaviour of a function and another set of macros (like 18 | **?WAS\_CALLED**) which check that a function was called 19 | with a chosen set of arguments. 20 | 21 | Read more about constructs and syntax in the 22 | documentation for the mockgyver module. 23 | 24 | The documentation is generated using the [edown][4] 25 | extension which generates documentation which is 26 | immediately readable on github. Remove the edown lines 27 | from `rebar.config` to generate regular edoc. 28 | 29 | A quick tutorial 30 | ---------------- 31 | 32 | Let's assume we want to make sure a fictional program 33 | sets up an ssh connection correctly (in order to test 34 | the part of our program which calls ssh:connect/3) 35 | without having to start an ssh server. Then we can use 36 | the ?WHEN macro to replace the original ssh module and 37 | let connect/3 return a bogus ssh\_connection\_ref(): 38 | 39 | ```erlang 40 | ?WHEN(ssh:connect(_Host, _Port, _Opts) -> {ok, ssh_ref}), 41 | ``` 42 | 43 | Also, let's mock close/1 while we're at it to make sure 44 | it won't crash on the bogus ssh\_ref: 45 | 46 | ```erlang 47 | ?WHEN(ssh:close(_ConnRef) -> ok), 48 | ``` 49 | 50 | When testing our program, we want to make sure it calls 51 | the ssh module with the correct arguments so we'll add 52 | these lines: 53 | 54 | ```erlang 55 | ?WAS_CALLED(ssh:connect({127,0,0,1}, 2022, [])), 56 | ?WAS_CALLED(ssh:close(ssh_ref)), 57 | ``` 58 | 59 | For all of this to work, the test needs to be 60 | encapsulated within either the ?MOCK macro or the 61 | ?WITH\_MOCKED\_SETUP (recommended for eunit). Assume the 62 | test case above is within a function called 63 | sets\_up\_and\_tears\_down\_ssh\_connection: 64 | 65 | ```erlang 66 | sets_up_and_tears_down_ssh_connection_test() -> 67 | ?MOCK(fun sets_up_and_tears_down_ssh_connection/0). 68 | ``` 69 | 70 | Or, if you prefer ?WITH\_MOCKED\_SETUP: 71 | 72 | ```erlang 73 | ssh_test_() -> 74 | ?WITH_MOCKED_SETUP(fun setup/0, fun cleanup/1). 75 | 76 | sets_up_and_tears_down_ssh_connection_test(_) -> 77 | ... 78 | ``` 79 | 80 | Sometimes a test requires a process to be started 81 | before a test, and stopped after a test. In that case, 82 | the latter is better (it'll automatically export and 83 | call all ...test/1 functions). 84 | 85 | The final test case could look something like this: 86 | 87 | ```erlang 88 | -include_lib("mockgyver/include/mockgyver.hrl"). 89 | 90 | ssh_test_() -> 91 | ?WITH_MOCKED_SETUP(fun setup/0, fun cleanup/1). 92 | 93 | setup() -> 94 | ok. 95 | 96 | cleanup(_) -> 97 | ok. 98 | 99 | sets_up_and_tears_down_ssh_connection_test(_) -> 100 | ?WHEN(ssh:connect(_Host, _Port, _Opts) -> {ok, ssh_ref}), 101 | ?WHEN(ssh:close(_ConnRef) -> ok), 102 | ...start the program and trigger the ssh connection to open... 103 | ?WAS_CALLED(ssh:connect({127,0,0,1}, 2022, [])), 104 | ...trigger the ssh connection to close... 105 | ?WAS_CALLED(ssh:close(ssh_ref)), 106 | ``` 107 | 108 | API documentation 109 | ----------------- 110 | 111 | See [`mockgyver`](http://github.com/klajo/mockgyver/blob/master/doc/mockgyver.md) 112 | for details. 113 | 114 | 115 | Caveats 116 | ------- 117 | 118 | There are some pitfalls in using mockgyver that you 119 | might want to know about. 120 | 121 | * It's not possible to mock local functions. 122 | 123 | This has to do with the way mockgyver works through 124 | unloading and loading of modules. 125 | 126 | * Take care when mocking modules which are used by 127 | other parts of the system. 128 | 129 | Examples include those in stdlib and kernel. A common 130 | pitfall is mocking io. Since mockgyver is 131 | potentially unloading and reloading the original 132 | module many times during a test suite, processes 133 | which are running that module may get killed as part 134 | of the code loading mechanism within Erlang. A common 135 | situation when mocking io is that eunit will stop 136 | printing the progress and you will wonder what has 137 | happened. 138 | 139 | * NIFs cannot be mocked and mockgyver will try to 140 | inform you if that is the case. 141 | 142 | * mockgyver does currently not play well with cover and 143 | cover will complain that a module has not been cover 144 | compiled. This is probably solvable. 145 | 146 | History 147 | ------- 148 | 149 | It all started when a friend of mine (Tomas 150 | Abrahamsson) and I (Klas Johansson) wrote a tool we 151 | called the stubber. Using it looked something like this: 152 | 153 | ```erlang 154 | stubber:run_with_replaced_modules( 155 | [{math, pi, fun() -> 4 end}, 156 | {some_module, some_function, fun(X, Y) -> ... end}, 157 | ...], 158 | fun() -> 159 | code which should be run with replacements above 160 | end), 161 | ``` 162 | 163 | Time went by and we had test cases which needed a more 164 | intricate behaviour, the stubs grew more and more 165 | complicated and that's when I thought: it must be 166 | possible to come up with something better and that's 167 | when I wrote mockgyver. 168 | 169 | Using mockgyver with your own application 170 | ----------------------------------------- 171 | 172 | mockgyver is available as a [hex package][1]. Just add it as a 173 | dependency to your rebar.config: 174 | 175 | ```sh 176 | {deps, [mockgyver]}. 177 | ``` 178 | 179 | ... or with a specific version: 180 | 181 | ```sh 182 | {deps, [{mockgyver, "some.good.version"}]}. 183 | ``` 184 | 185 | Building 186 | -------- 187 | 188 | Build mockgyver using [rebar][2]. Also, 189 | [parse\_trans][3] (for the special syntax), [edown][4] 190 | (for docs on github) and [eunit\_addons][5] (for ?WITH\_MOCKED\_SETUP) 191 | are required, but rebar takes care of that. 192 | 193 | ```sh 194 | $ git clone git://github.com/klajo/mockgyver.git 195 | $ rebar3 compile 196 | ``` 197 | 198 | Build docs using the edown library (for markdown format): 199 | ```sh 200 | $ rebar3 as edown edoc 201 | ``` 202 | 203 | Build regular docs: 204 | ```sh 205 | $ rebar3 edoc 206 | ``` 207 | 208 | [1]: https://hex.pm/packages/mockgyver 209 | [2]: https://www.rebar3.org 210 | [3]: https://hex.pm/packages/parse_trans 211 | [4]: https://hex.pm/packages/edown 212 | [5]: https://hex.pm/packages/eunit_addons 213 | -------------------------------------------------------------------------------- /doc/mockgyver.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module mockgyver # 4 | * [Description](#description) 5 | 6 | Mock functions and modules. 7 | 8 | Copyright (c) 2011, Klas Johansson 9 | 10 | __Behaviours:__ [`gen_statem`](gen_statem.md). 11 | 12 | __Authors:__ Klas Johansson. 13 | 14 | 15 | 16 | ## Description ## 17 | 18 | 19 | #### Initiating mock #### 20 | 21 | In order to use the various macros below, mocking must be 22 | initiated using the `?MOCK` macro or `?WITH_MOCKED_SETUP` 23 | (recommended from eunit tests). 24 | 25 |
?MOCK syntax
26 | 27 | 28 | ```erlang 29 | 30 | ?MOCK(Expr) 31 | ``` 32 | 33 | where `Expr` in a single expression, like a fun. The rest of the 34 | macros in this module can be used within this fun or in a function 35 | called by the fun. 36 | 37 |
?WITH_MOCKED_SETUP syntax
38 | 39 | 40 | ```erlang 41 | 42 | ?WITH_MOCKED_SETUP(SetupFun, CleanupFun), 43 | ?WITH_MOCKED_SETUP(SetupFun, CleanupFun, ForAllTimeout, PerTcTimeout), 44 | ?WITH_MOCKED_SETUP(SetupFun, CleanupFun, ForAllTimeout, PerTcTimeout, 45 | Tests), 46 | ``` 47 | 48 | This is an easy way of using mocks from within eunit tests and is 49 | mock-specific version of the `?WITH_SETUP` macro. See the docs 50 | for the `?WITH_SETUP` macro in the `eunit_addons` project for more 51 | information on parameters and settings. 52 | 53 | 54 | #### Mocking a function #### 55 | 56 |
Introduction
57 | 58 | By mocking a function, its original side-effects and return value 59 | (or throw/exit/error) are overridden and replaced. This can be used to: 60 | 61 | * replace existing functions in existing modules 62 | 63 | * add new functions to existing modules 64 | 65 | * add new modules 66 | 67 | 68 | BIFs (built-in functions) cannot be mocked. 69 | 70 | The original module will be renamed (a "^" will be appended to the 71 | original module name, i.e. `foo` will be renamed to `'foo^'`). 72 | A mock can then call the original function just by performing a regular 73 | function call. 74 | 75 | Since WHEN is a macro, and macros don't support argument lists 76 | (something like "Arg..."), multi-expression mocks must be 77 | surrounded by `begin ... end` to be treated as one argument by the 78 | preprocessor. 79 | 80 | A mock that was introduced using the ?WHEN macro can be forgotten, 81 | i.e. returned to the behaviour of the original module, using the 82 | `?FORGET_WHEN` macro. 83 | 84 |
?WHEN syntax
85 | 86 | 87 | ```erlang 88 | 89 | ?WHEN(module:function(Arg1, Arg2, ...) -> Expr), 90 | ``` 91 | 92 | where `Expr` is a single expression (like a term) or a series of 93 | expressions surrounded by `begin` and `end`. 94 | 95 |
?FORGET_WHEN syntax
96 | 97 | 98 | ```erlang 99 | 100 | ?FORGET_WHEN(module:function(_, _, ...)), 101 | ``` 102 | 103 | The only things of interest are the name of the module, the name 104 | of the function and the arity. The arguments of the function are 105 | ignored and it can be a wise idea to set these to the "don't care" 106 | variable: underscore. 107 | 108 |
Examples
109 | 110 | Note: Apparently the Erlang/OTP team doesn't want us to redefine 111 | PI to 4 anymore :-), since starting at R15B, math:pi/0 is marked as 112 | pure which means that the compiler is allowed to replace the 113 | math:pi() function call by a constant: 3.14... This means that 114 | even though mockgyver can mock the pi/0 function, a test case will 115 | never call math:pi/0 since it will be inlined. See commit 116 | 5adf009cb09295893e6bb01b4666a569590e0f19 (compiler: Turn calls to 117 | math:pi/0 into constant values) in the otp sources. 118 | 119 | Redefine pi to 4: 120 | 121 | ```erlang 122 | 123 | ?WHEN(math:pi() -> 4), 124 | ``` 125 | 126 | Implement a mock with multiple clauses: 127 | 128 | ```erlang 129 | 130 | ?WHEN(my_module:classify_number(N) when N >= 0 -> positive; 131 | my_module:classify_number(_N) -> negative), 132 | ``` 133 | 134 | Call original module: 135 | 136 | ```erlang 137 | 138 | ?WHEN(math:pi() -> 'math^':pi() * 2), 139 | ``` 140 | 141 | Use a variable bound outside the mock: 142 | 143 | ```erlang 144 | 145 | Answer = 42, 146 | ?WHEN(math:pi() -> Answer), 147 | ``` 148 | 149 | Redefine the mock: 150 | 151 | ```erlang 152 | 153 | ?WHEN(math:pi() -> 4), 154 | 4 = math:pi(), 155 | ?WHEN(math:pi() -> 5), 156 | 5 = math:pi(), 157 | ``` 158 | 159 | Let the mock exit with an error: 160 | 161 | ```erlang 162 | 163 | ?WHEN(math:pi() -> erlang:error(some_error)), 164 | ``` 165 | 166 | Make a new module: 167 | 168 | ```erlang 169 | 170 | ?WHEN(my_math:pi() -> 4), 171 | ?WHEN(my_math:e() -> 3), 172 | ``` 173 | 174 | Put multiple clauses in a function's body: 175 | 176 | ```erlang 177 | 178 | ?WHEN(math:pi() -> 179 | begin 180 | do_something1(), 181 | do_something2() 182 | end), 183 | ``` 184 | 185 | Revert the pi function to its default behaviour (return value from 186 | the original module), any other mocks in the same module, or any 187 | other module are left untouched: 188 | 189 | ```erlang 190 | 191 | ?WHEN(math:pi() -> 4), 192 | 4 = math:pi(), 193 | ?FORGET_WHEN(math:pi()), 194 | 3.1415... = math:pi(), 195 | ``` 196 | 197 | 198 | #### Validating calls #### 199 | 200 |
Introduction
201 | 202 | There are a number of ways to check that a certain function has 203 | been called and that works for both mocks and non-mocks. 204 | 205 | * `?WAS_CALLED`: Check that a function was called with 206 | certain set of parameters a chosen number of times. 207 | The validation is done at the place of the macro, consider 208 | this when verifying asynchronous procedures 209 | (see also `?WAIT_CALLED`). Return a list of argument lists, 210 | one argument list for each call to the function. An 211 | argument list contains the arguments of a specific call. 212 | Will crash with an error if the criteria isn't fulfilled. 213 | 214 | * `?WAIT_CALLED`: Same as `?WAS_CALLED`, with a twist: waits for 215 | the criteria to be fulfilled which can be useful for 216 | asynchronous procedures. 217 | 218 | * `?GET_CALLS`: Return a list of argument lists (just like 219 | `?WAS_CALLED` or `?WAIT_CALLED`) without checking any criteria. 220 | 221 | * `?NUM_CALLS`: Return the number of calls to a function. 222 | 223 | * `?FORGET_CALLS`: Forget the calls that have been logged. 224 | This exists in two versions: 225 | 226 | * One which forgets calls to a certain function. 227 | Takes arguments and guards into account, i.e. only 228 | the calls which match the module name, function 229 | name and all arguments as well as any guards will 230 | be forgotten, while the rest of the calls remain. 231 | 232 | * One which forgets all calls to any function. 233 | 234 | 235 | 236 | 237 |
?WAS_CALLED syntax
238 | 239 | 240 | ```erlang 241 | 242 | ?WAS_CALLED(module:function(Arg1, Arg2, ...)), 243 | equivalent to ?WAS_CALLED(module:function(Arg1, Arg2, ...), once) 244 | ?WAS_CALLED(module:function(Arg1, Arg2, ...), Criteria), 245 | Criteria = once | never | {times, N} | {at_least, N} | {at_most, N} 246 | N = integer() 247 | Result: [CallArgs] 248 | CallArgs = [CallArg] 249 | CallArg = term() 250 | ``` 251 | 252 | 253 |
?WAIT_CALLED syntax
254 | 255 | See syntax for `?WAS_CALLED`. 256 | 257 |
?GET_CALLS syntax
258 | 259 | 260 | ```erlang 261 | 262 | ?GET_CALLS(module:function(Arg1, Arg2, ...)), 263 | Result: [CallArgs] 264 | CallArgs = [CallArg] 265 | CallArg = term() 266 | ``` 267 | 268 |
?NUM_CALLS syntax
269 | 270 | 271 | ```erlang 272 | 273 | ?NUM_CALLS(module:function(Arg1, Arg2, ...)), 274 | Result: integer() 275 | ``` 276 | 277 | 278 |
?FORGET_CALLS syntax
279 | 280 | 281 | ```erlang 282 | 283 | ?FORGET_CALLS(module:function(Arg1, Arg2, ...)), 284 | ?FORGET_CALLS(), 285 | ``` 286 | 287 | 288 |
Examples
289 | 290 | Check that a function has been called once (the two alternatives 291 | are equivalent): 292 | 293 | ```erlang 294 | 295 | ?WAS_CALLED(math:pi()), 296 | ?WAS_CALLED(math:pi(), once), 297 | ``` 298 | 299 | Check that a function has never been called: 300 | 301 | ```erlang 302 | 303 | ?WAS_CALLED(math:pi(), never), 304 | ``` 305 | 306 | Check that a function has been called twice: 307 | 308 | ```erlang 309 | 310 | ?WAS_CALLED(math:pi(), {times, 2}), 311 | ``` 312 | 313 | Check that a function has been called at least twice: 314 | 315 | ```erlang 316 | 317 | ?WAS_CALLED(math:pi(), {at_least, 2}), 318 | ``` 319 | 320 | Check that a function has been called at most twice: 321 | 322 | ```erlang 323 | 324 | ?WAS_CALLED(math:pi(), {at_most, 2}), 325 | ``` 326 | 327 | Use pattern matching to check that a function was called with 328 | certain arguments: 329 | 330 | ```erlang 331 | 332 | ?WAS_CALLED(lists:reverse([a, b, c])), 333 | ``` 334 | 335 | Pattern matching can even use bound variables: 336 | 337 | ```erlang 338 | 339 | L = [a, b, c], 340 | ?WAS_CALLED(lists:reverse(L)), 341 | ``` 342 | 343 | Use a guard to validate the parameters in a call: 344 | 345 | ```erlang 346 | 347 | ?WAS_CALLED(lists:reverse(L) when is_list(L)), 348 | ``` 349 | 350 | Retrieve the arguments in a call while verifying the number of calls: 351 | 352 | ```erlang 353 | 354 | a = lists:nth(1, [a, b]), 355 | d = lists:nth(2, [c, d]), 356 | [[1, [a, b]], [2, [c, d]]] = ?WAS_CALLED(lists:nth(_, _), {times, 2}), 357 | ``` 358 | 359 | Retrieve the arguments in a call without verifying the number of calls: 360 | 361 | ```erlang 362 | 363 | a = lists:nth(1, [a, b]), 364 | d = lists:nth(2, [c, d]), 365 | [[1, [a, b]], [2, [c, d]]] = ?GET_CALLS(lists:nth(_, _)), 366 | ``` 367 | 368 | Retrieve the number of calls: 369 | 370 | ```erlang 371 | 372 | a = lists:nth(1, [a, b]), 373 | d = lists:nth(2, [c, d]), 374 | 2 = ?NUM_CALLS(lists:nth(_, _)), 375 | ``` 376 | 377 | Forget calls to functions: 378 | 379 | ```erlang 380 | 381 | a = lists:nth(1, [a, b, c]), 382 | e = lists:nth(2, [d, e, f]), 383 | i = lists:nth(3, [g, h, i]), 384 | ?WAS_CALLED(lists:nth(1, [a, b, c]), once), 385 | ?WAS_CALLED(lists:nth(2, [d, e, f]), once), 386 | ?WAS_CALLED(lists:nth(3, [g, h, i]), once), 387 | ?FORGET_CALLS(lists:nth(2, [d, e, f])), 388 | ?WAS_CALLED(lists:nth(1, [a, b, c]), once), 389 | ?WAS_CALLED(lists:nth(2, [d, e, f]), never), 390 | ?WAS_CALLED(lists:nth(3, [g, h, i]), once), 391 | ?FORGET_CALLS(lists:nth(_, _)), 392 | ?WAS_CALLED(lists:nth(1, [a, b, c]), never), 393 | ?WAS_CALLED(lists:nth(2, [d, e, f]), never), 394 | ?WAS_CALLED(lists:nth(3, [g, h, i]), never), 395 | ``` 396 | 397 | Forget calls to all functions: 398 | 399 | ```erlang 400 | 401 | a = lists:nth(1, [a, b, c]), 402 | e = lists:nth(2, [d, e, f]), 403 | i = lists:nth(3, [g, h, i]), 404 | ?WAS_CALLED(lists:nth(1, [a, b, c]), once), 405 | ?WAS_CALLED(lists:nth(2, [d, e, f]), once), 406 | ?WAS_CALLED(lists:nth(3, [g, h, i]), once), 407 | ?FORGET_CALLS(), 408 | ?WAS_CALLED(lists:nth(1, [a, b, c]), never), 409 | ?WAS_CALLED(lists:nth(2, [d, e, f]), never), 410 | ?WAS_CALLED(lists:nth(3, [g, h, i]), never), 411 | ``` 412 | -------------------------------------------------------------------------------- /src/mockgyver_xform.erl: -------------------------------------------------------------------------------- 1 | %%%=================================================================== 2 | %%% Copyright (c) 2011, Klas Johansson 3 | %%% All rights reserved. 4 | %%% 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are 7 | %%% met: 8 | %%% 9 | %%% * Redistributions of source code must retain the above copyright 10 | %%% notice, this list of conditions and the following disclaimer. 11 | %%% 12 | %%% * Redistributions in binary form must reproduce the above copyright 13 | %%% notice, this list of conditions and the following disclaimer in 14 | %%% the documentation and/or other materials provided with the 15 | %%% distribution. 16 | %%% 17 | %%% * Neither the name of the copyright holder nor the names of its 18 | %%% contributors may be used to endorse or promote products derived 19 | %%% from this software without specific prior written permission. 20 | %%% 21 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | %%% IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | %%% TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | %%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | %%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | %%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 27 | %%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | %%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | %%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | %%%=================================================================== 33 | 34 | %%%------------------------------------------------------------------- 35 | %%% @author Klas Johansson 36 | %%% @copyright 2011, Klas Johansson 37 | %%% @private 38 | %%% @doc Transform mock syntax into real function calls. 39 | %%% @end 40 | %%%------------------------------------------------------------------- 41 | -module(mockgyver_xform). 42 | 43 | %% This might be confusing, but this module (which is a parse 44 | %% transform) is actually itself parse transformed by a 3pp library 45 | %% (http://github.com/esl/parse_trans). This transform makes it 46 | %% easier for this module to generate code within the modules it 47 | %% transforms. Simple, eh? :-) 48 | -compile({parse_transform, parse_trans_codegen}). 49 | 50 | %% API 51 | -export([parse_transform/2]). 52 | 53 | %% Records 54 | -record(m_session_params, {loc}). 55 | -record(m_when, {m, f, a, action, loc}). 56 | -record(m_verify, {m, f, a, g, crit, loc}). 57 | 58 | -record(env, {mock_mfas, trace_mfas}). 59 | 60 | %%%=================================================================== 61 | %%% API 62 | %%%=================================================================== 63 | 64 | -spec parse_transform(Forms :: [erl_parse:abstract_form() | 65 | erl_parse:form_info()], 66 | Opts :: [compile:option()]) -> 67 | [erl_parse:abstract_form() | 68 | erl_parse:form_info()]. 69 | parse_transform(Forms, Opts) -> 70 | parse_trans:top(fun parse_transform_2/2, Forms, Opts). 71 | 72 | parse_transform_2(Forms0, Ctxt) -> 73 | Env = #env{mock_mfas = find_mfas_to_mock(Forms0, Ctxt), 74 | trace_mfas = find_mfas_to_trace(Forms0, Ctxt)}, 75 | {Forms1, _} = rewrite_session_params_stmts(Forms0, Ctxt, Env), 76 | {Forms2, _} = rewrite_when_stmts(Forms1, Ctxt), 77 | {Forms, _} = rewrite_verify_stmts(Forms2, Ctxt), 78 | parse_trans:revert(Forms). 79 | 80 | %%------------------------------------------------------------ 81 | %% session_params statements 82 | %%------------------------------------------------------------ 83 | rewrite_session_params_stmts(Forms, Ctxt, Env) -> 84 | parse_trans:do_transform(fun rewrite_session_params_stmts_2/4, 85 | Env, Forms, Ctxt). 86 | 87 | rewrite_session_params_stmts_2(Type, Form0, _Ctxt, Env) -> 88 | case is_mock_expr(Type, Form0) of 89 | {true, #m_session_params{}} -> 90 | Befores = [], 91 | MockMfas = Env#env.mock_mfas, 92 | TraceMfas = Env#env.trace_mfas, 93 | [Form] = codegen:exprs( 94 | fun() -> 95 | {{'$var', MockMfas}, {'$var', TraceMfas}} 96 | end), 97 | Afters = [], 98 | {Befores, Form, Afters, false, Env}; 99 | _ -> 100 | {Form0, true, Env} 101 | end. 102 | 103 | %%------------------------------------------------------------ 104 | %% when statements 105 | %%------------------------------------------------------------ 106 | rewrite_when_stmts(Forms, Ctxt) -> 107 | parse_trans:do_transform(fun rewrite_when_stmts_2/4, x, Forms, Ctxt). 108 | 109 | rewrite_when_stmts_2(Type, Form0, _Ctxt, Acc) -> 110 | case is_mock_expr(Type, Form0) of 111 | {true, #m_when{m=M, f=F, action=ActionFun, loc=Location}} -> 112 | Befores = [], 113 | [Form] = codegen:exprs( 114 | fun() -> 115 | mockgyver:set_action( 116 | {{'$var',M}, {'$var',F}, {'$form',ActionFun}}, 117 | [{location, {'$var',Location}}]) 118 | end), 119 | Afters = [], 120 | {Befores, Form, Afters, false, Acc}; 121 | _ -> 122 | {Form0, true, Acc} 123 | end. 124 | 125 | find_mfas_to_mock(Forms, Ctxt) -> 126 | lists:usort( 127 | parse_trans:do_inspect(fun find_mfas_to_mock_f/4, [], Forms, Ctxt)). 128 | 129 | find_mfas_to_mock_f(Type, Form, _Ctxt, Acc) -> 130 | case is_mock_expr(Type, Form) of 131 | {true, #m_when{} = W} -> {false, [when_to_mfa(W) | Acc]}; 132 | {true, _} -> {true, Acc}; 133 | false -> {true, Acc} 134 | end. 135 | 136 | when_to_mfa(#m_when{m=M, f=F, a=A}) -> 137 | {M, F, A}. 138 | 139 | %%------------------------------------------------------------ 140 | %% was called statements 141 | %%------------------------------------------------------------ 142 | rewrite_verify_stmts(Forms, Ctxt) -> 143 | parse_trans:do_transform(fun rewrite_verify_stmts_2/4, x, Forms, Ctxt). 144 | 145 | rewrite_verify_stmts_2(Type, Form0, _Ctxt, Acc) -> 146 | case is_mock_expr(Type, Form0) of 147 | {true, #m_verify{m=M, f=F, a=A, g=G, crit=C, loc=Location}} -> 148 | Fun = mk_verify_checker_fun(A, G), 149 | Befores = [], 150 | [Form] = codegen:exprs( 151 | fun() -> 152 | mockgyver:verify( 153 | {{'$var', M}, {'$var', F}, {'$form', Fun}}, 154 | {'$form', C}, 155 | [{location, {'$var', Location}}]) 156 | end), 157 | Afters = [], 158 | {Befores, Form, Afters, false, Acc}; 159 | _ -> 160 | {Form0, true, Acc} 161 | end. 162 | 163 | find_mfas_to_trace(Forms, Ctxt) -> 164 | lists:usort( 165 | parse_trans:do_inspect(fun find_mfas_to_trace_f/4, [], Forms, Ctxt)). 166 | 167 | find_mfas_to_trace_f(Type, Form, _Ctxt, Acc) -> 168 | case is_mock_expr(Type, Form) of 169 | {true, #m_verify{} = WC} -> {false, [verify_to_tpat(WC) | Acc]}; 170 | {true, _} -> {true, Acc}; 171 | false -> {true, Acc} 172 | end. 173 | 174 | verify_to_tpat(#m_verify{m=M, f=F, a=A}) -> 175 | {M, F, length(A)}. % a trace pattern we can pass to erlang:trace_pattern 176 | 177 | %%------------------------------------------------------------ 178 | %% mock expression analysis 179 | %%------------------------------------------------------------ 180 | is_mock_expr(tuple, Form) -> 181 | case erl_syntax:tuple_elements(Form) of 182 | [H | T] -> 183 | case erl_syntax:is_atom(H, '$mock') of 184 | true -> 185 | [TypeTree | Rest] = T, 186 | Type = erl_syntax:atom_value(TypeTree), 187 | MockInfo = {Type, lists:droplast(Rest)}, 188 | Location = erl_syntax:concrete(lists:last(Rest)), 189 | {true, analyze_mock_form(MockInfo, Location)}; 190 | false -> 191 | false 192 | end; 193 | _Other -> 194 | false 195 | end; 196 | is_mock_expr(_Type, _Form) -> 197 | false. 198 | 199 | analyze_mock_form(Info, Location) -> 200 | case Info of 201 | {m_session_params, []} -> analyze_session_params_expr(Location); 202 | {m_when, [Expr]} -> analyze_when_expr(Expr, Location); 203 | {m_verify, [Expr]} -> analyze_verify_expr(Expr, Location) 204 | end. 205 | 206 | analyze_session_params_expr(Location) -> 207 | #m_session_params{loc=Location}. 208 | 209 | analyze_when_expr(Expr, Location) -> 210 | %% The sole purpose of the entire if expression is to let us write 211 | %% ?WHEN(m:f(_) -> some_result). 212 | %% or 213 | %% ?WHEN(m:f(1) -> some_result;). 214 | %% ?WHEN(m:f(2) -> some_other_result). 215 | Clauses0 = erl_syntax:case_expr_clauses(Expr), 216 | {M, F, A} = ensure_all_clauses_implement_the_same_function(Clauses0), 217 | Clauses = lists:map(fun(Clause) -> 218 | [Call | _] = erl_syntax:clause_patterns(Clause), 219 | {_M, _F, Args} = analyze_application(Call), 220 | Guard = erl_syntax:clause_guard(Clause), 221 | Body = erl_syntax:clause_body(Clause), 222 | erl_syntax:clause(Args, Guard, Body) 223 | end, 224 | Clauses0), 225 | ActionFun = erl_syntax:fun_expr(Clauses), 226 | #m_when{m=M, f=F, a=A, action=ActionFun, loc=Location}. 227 | 228 | ensure_all_clauses_implement_the_same_function(Clauses) -> 229 | lists:foldl(fun(Clause, undefined) -> 230 | get_when_call_sig(Clause); 231 | (Clause, {M, F, A}=MFA) -> 232 | case get_when_call_sig(Clause) of 233 | {M, F, A} -> 234 | MFA; 235 | OtherMFA -> 236 | erlang:error({when_expr_function_mismatch, 237 | {expected, MFA}, 238 | {got, OtherMFA}}) 239 | end 240 | end, 241 | undefined, 242 | Clauses). 243 | 244 | get_when_call_sig(Clause) -> 245 | [Call | _] = erl_syntax:clause_patterns(Clause), 246 | {M, F, A} = analyze_application(Call), 247 | {M, F, length(A)}. 248 | 249 | analyze_verify_expr(Form, Location) -> 250 | [Case, Criteria] = erl_syntax:tuple_elements(Form), 251 | [Clause | _] = erl_syntax:case_expr_clauses(Case), 252 | [Call | _] = erl_syntax:clause_patterns(Clause), 253 | G = erl_syntax:clause_guard(Clause), 254 | {M, F, A} = analyze_application(Call), 255 | #m_verify{m=M, f=F, a=A, g=G, crit=erl_syntax:revert(Criteria), 256 | loc=Location}. 257 | 258 | mk_verify_checker_fun(Args0, Guard0) -> 259 | %% Let's say there's a statement like this: 260 | %% N = 42, 261 | %% ... 262 | %% ?WAS_CALLED(x:y(N), once), 263 | %% 264 | %% How do we rewrite this to something that can be used to check 265 | %% whether it matches or not? 266 | %% 267 | %% * we want N to match only 42, but "fun(N) -> ... end" would 268 | %% match anything 269 | %% 270 | %% * we could write a guard, but we'd need to take care of guards 271 | %% the user has written and make sure our guards work with theirs 272 | %% 273 | %% * a match would take care of this 274 | %% 275 | %% Hence, convert the statement to a fun: 276 | %% fun([____N]) -> 277 | %% N = ____N, 278 | %% [N] 279 | %% end 280 | 281 | %% Rename all variables in the args list 282 | {Args1, NameMap0} = rename_vars(erl_syntax:list(Args0), fun(_) -> true end), 283 | Args = erl_syntax:list_elements(Args1), 284 | %% Rename only variables in guards which are also present in the args list 285 | RenameVars = [N0 || {N0, _N1} <- NameMap0], 286 | {Guard, _NameMap1} = 287 | rename_vars(Guard0, fun(V) -> lists:member(V, RenameVars) end), 288 | Body = 289 | %% This first statement generates one match expression per 290 | %% variable. The idea is that a badmatch implies that the fun 291 | %% didn't match the call. 292 | [erl_syntax:match_expr(erl_syntax:variable(N0), 293 | erl_syntax:variable(N1)) 294 | || {N0, N1} <- NameMap0] 295 | %% This section ensures that all variables are used and we 296 | %% avoid the "unused variable" warning 297 | ++ [erl_syntax:list([erl_syntax:variable(N0) || 298 | {N0, _N1} <- NameMap0])], 299 | Clause = erl_syntax:clause(Args, Guard, Body), 300 | Clauses = parse_trans:revert([Clause]), 301 | erl_syntax:revert(erl_syntax:fun_expr(Clauses)). 302 | 303 | rename_vars(none, _RenameP) -> 304 | {none, []}; 305 | rename_vars(Forms0, RenameP) -> 306 | {Forms, {NameMap, RenameP}} = 307 | erl_syntax_lib:mapfold(fun rename_vars_2/2, {[], RenameP}, Forms0), 308 | {Forms, lists:usort(NameMap)}. 309 | 310 | rename_vars_2({var, L, VarName0}=Form, {NameMap0, RenameP}) -> 311 | case RenameP(VarName0) of 312 | true -> 313 | case atom_to_list(VarName0) of 314 | "_"++_ -> 315 | {Form, {NameMap0, RenameP}}; 316 | VarNameStr0 -> 317 | VarName1 = list_to_atom(VarNameStr0 ++ "@mockgyver"), 318 | NameMap = [{VarName0, VarName1} | NameMap0], 319 | {{var, L, VarName1}, {NameMap, RenameP}} 320 | end; 321 | false -> 322 | {Form, {NameMap0, RenameP}} 323 | end; 324 | rename_vars_2(Form, {_NameMap, _RenameP}=Acc) -> 325 | {Form, Acc}. 326 | 327 | analyze_application(Form) -> 328 | MF = erl_syntax:application_operator(Form), 329 | M = erl_syntax:concrete(erl_syntax:module_qualifier_argument(MF)), 330 | F = erl_syntax:concrete(erl_syntax:module_qualifier_body(MF)), 331 | A = erl_syntax:application_arguments(Form), 332 | {M, F, A}. 333 | -------------------------------------------------------------------------------- /test/mockgyver_tests.erl: -------------------------------------------------------------------------------- 1 | %%%=================================================================== 2 | %%% Copyright (c) 2011, Klas Johansson 3 | %%% All rights reserved. 4 | %%% 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are 7 | %%% met: 8 | %%% 9 | %%% * Redistributions of source code must retain the above copyright 10 | %%% notice, this list of conditions and the following disclaimer. 11 | %%% 12 | %%% * Redistributions in binary form must reproduce the above copyright 13 | %%% notice, this list of conditions and the following disclaimer in 14 | %%% the documentation and/or other materials provided with the 15 | %%% distribution. 16 | %%% 17 | %%% * Neither the name of the copyright holder nor the names of its 18 | %%% contributors may be used to endorse or promote products derived 19 | %%% from this software without specific prior written permission. 20 | %%% 21 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | %%% IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | %%% TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | %%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | %%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | %%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 27 | %%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | %%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | %%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | %%%=================================================================== 33 | 34 | -module(mockgyver_tests). 35 | 36 | -include_lib("eunit/include/eunit.hrl"). 37 | -include_lib("stdlib/include/ms_transform.hrl"). 38 | -include("../include/mockgyver.hrl"). 39 | 40 | -record('DOWN', {mref, type, obj, info}). 41 | 42 | -define(recv(PatternAction), ?recv(PatternAction, 4000)). 43 | -define(recv(PatternAction, Timeout), 44 | fun() -> 45 | receive PatternAction 46 | after Timeout -> 47 | error({failed_to_receive, ??PatternAction, 48 | process_info(self(), messages)}) 49 | end 50 | end()). 51 | 52 | 53 | mock_test_() -> 54 | code:add_patha(test_dir()), 55 | ?WITH_MOCKED_SETUP(fun setup/0, fun cleanup/1). 56 | 57 | setup() -> 58 | ok. 59 | 60 | cleanup(_) -> 61 | ok. 62 | 63 | only_allows_one_mock_at_a_time_test_() -> 64 | {timeout, ?PER_TC_TIMEOUT, fun only_allows_one_mock_at_a_time_test_aux/0}. 65 | 66 | only_allows_one_mock_at_a_time_test_aux() -> 67 | NumMockers = 10, 68 | Parent = self(), 69 | Mocker = fun() -> 70 | ?MOCK(fun() -> 71 | Parent ! {mock_start, self()}, 72 | timer:sleep(5), 73 | Parent ! {mock_end, self()} 74 | end) 75 | end, 76 | Mockers = [proc_lib:spawn(Mocker) || _ <- lists:seq(1, NumMockers)], 77 | wait_until_mockers_terminate(Mockers), 78 | Msgs = fetch_n_msgs(NumMockers*2), 79 | check_no_simultaneous_mockers_outside(Msgs). 80 | 81 | wait_until_mockers_terminate([Pid | Pids]) -> 82 | MRef = erlang:monitor(process, Pid), 83 | receive 84 | #'DOWN'{mref=MRef, info=Info} when Info==normal; Info==noproc -> 85 | wait_until_mockers_terminate(Pids); 86 | #'DOWN'{mref=MRef, info=Info} -> 87 | erlang:error({mocker_terminated_abnormally, Pid, Info}) 88 | end; 89 | wait_until_mockers_terminate([]) -> 90 | ok. 91 | 92 | fetch_n_msgs(0) -> []; 93 | fetch_n_msgs(N) -> [receive M -> M end | fetch_n_msgs(N-1)]. 94 | 95 | check_no_simultaneous_mockers_outside([{mock_start, Pid} | Msgs]) -> 96 | check_no_simultaneous_mockers_inside(Msgs, Pid); 97 | check_no_simultaneous_mockers_outside([]) -> 98 | ok. 99 | 100 | check_no_simultaneous_mockers_inside([{mock_end, Pid} | Msgs], Pid) -> 101 | check_no_simultaneous_mockers_outside(Msgs). 102 | 103 | can_test_again_after_mocking_dies_test_() -> 104 | {timeout, ?PER_TC_TIMEOUT, fun can_test_again_after_mocking_dies_aux/0}. 105 | 106 | can_test_again_after_mocking_dies_aux() -> 107 | P1 = proc_lib:spawn(fun() -> ?MOCK(fun() -> crash_me_via_process_link() end) 108 | end), 109 | M1 = monitor(process, P1), 110 | ?recv(#'DOWN'{mref=M1} -> ok), 111 | ?MOCK(fun() -> ok end), 112 | ok. 113 | 114 | can_test_again_after_session_dies_test_() -> 115 | {timeout, ?PER_TC_TIMEOUT, fun can_test_again_after_session_dies_aux/0}. 116 | 117 | can_test_again_after_session_dies_aux() -> 118 | P1 = proc_lib:spawn( 119 | fun() -> 120 | ok = mockgyver:start_session({[], []}), 121 | crash_me_via_process_link() 122 | end), 123 | M1 = monitor(process, P1), 124 | ?recv(#'DOWN'{mref=M1, info=Reason} -> 125 | if Reason == normal -> ok; 126 | Reason == shutdown -> ok; 127 | true -> error({helper_crashed, Reason}) 128 | end), 129 | ?MOCK(fun() -> ok end), 130 | ok. 131 | 132 | can_test_again_after_session_elem_dies_test_() -> 133 | {timeout, ?PER_TC_TIMEOUT, 134 | fun can_test_again_after_session_elem_dies_aux/0}. 135 | 136 | can_test_again_after_session_elem_dies_aux() -> 137 | P1 = proc_lib:spawn( 138 | fun() -> 139 | ok = mockgyver:start_session({[], []}), 140 | P2 = proc_lib:spawn( 141 | fun() -> 142 | ok = mockgyver:start_session_element(), 143 | crash_me_via_process_link() 144 | end), 145 | M2 = monitor(process, P2), 146 | ?recv(#'DOWN'{mref=M2} -> ok) 147 | end), 148 | M1 = monitor(process, P1), 149 | ?recv(#'DOWN'{mref=M1, info=Reason} -> 150 | if Reason == normal -> ok; 151 | true -> error({helper_crashed, Reason}) 152 | end), 153 | ?MOCK(fun() -> ok end), 154 | ok. 155 | 156 | crash_me_via_process_link() -> 157 | spawn_link(fun() -> exit(shutdown) end), 158 | timer:sleep(infinity). 159 | 160 | traces_single_arg_test(_) -> 161 | 1 = mockgyver_dummy:return_arg(1), 162 | 2 = mockgyver_dummy:return_arg(2), 163 | 2 = mockgyver_dummy:return_arg(2), 164 | ?WAS_CALLED(mockgyver_dummy:return_arg(1), once), 165 | ?WAS_CALLED(mockgyver_dummy:return_arg(2), {times, 2}), 166 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), {times, 3}). 167 | 168 | traces_multi_args_test(_) -> 169 | {a, 1} = mockgyver_dummy:return_arg(a, 1), 170 | {a, 2} = mockgyver_dummy:return_arg(a, 2), 171 | {b, 2} = mockgyver_dummy:return_arg(b, 2), 172 | ?WAS_CALLED(mockgyver_dummy:return_arg(a, 1), once), 173 | ?WAS_CALLED(mockgyver_dummy:return_arg(a, 2), once), 174 | ?WAS_CALLED(mockgyver_dummy:return_arg(b, 2), once), 175 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, 1), {times, 1}), 176 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, 2), {times, 2}). 177 | 178 | traces_in_separate_process_test(_) -> 179 | Pid = proc_lib:spawn_link(fun() -> mockgyver_dummy:return_arg(1) end), 180 | MRef = erlang:monitor(process, Pid), 181 | receive {'DOWN',MRef,_,_,_} -> ok end, 182 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), once). 183 | 184 | was_called_defaults_to_once_test(_) -> 185 | mockgyver_dummy:return_arg(1), 186 | ?WAS_CALLED(mockgyver_dummy:return_arg(_)), 187 | mockgyver_dummy:return_arg(1), 188 | ?assertError(_, ?WAS_CALLED(mockgyver_dummy:return_arg(_))). 189 | 190 | matches_called_arguments_test(_) -> 191 | N = 42, 192 | O = 1, 193 | P = 4711, 194 | mockgyver_dummy:return_arg(1), 195 | mockgyver_dummy:return_arg({N, O}), 196 | mockgyver_dummy:return_arg(P, P), 197 | ?WAS_CALLED(mockgyver_dummy:return_arg(N), never), 198 | ?WAS_CALLED(mockgyver_dummy:return_arg(O), once), 199 | ?WAS_CALLED(mockgyver_dummy:return_arg({_, N}), never), 200 | ?WAS_CALLED(mockgyver_dummy:return_arg({N, O}), once), 201 | ?WAS_CALLED(mockgyver_dummy:return_arg(P, P), once). 202 | 203 | allows_was_called_guards_test(_) -> 204 | mockgyver_dummy:return_arg(1), 205 | ?WAS_CALLED(mockgyver_dummy:return_arg(N) when N == 1, once), 206 | ?WAS_CALLED(mockgyver_dummy:return_arg(N) when N == 2, never). 207 | 208 | allows_was_called_guards_with_variables_not_used_in_args_list_test(_) -> 209 | W = 2, 210 | mockgyver_dummy:return_arg(1), 211 | ?WAS_CALLED(mockgyver_dummy:return_arg(_) when W == 2, once). 212 | 213 | returns_called_arguments_test(_) -> 214 | mockgyver_dummy:return_arg(1), 215 | mockgyver_dummy:return_arg(2), 216 | [[1], [2]] = ?WAS_CALLED(mockgyver_dummy:return_arg(N), {times, 2}). 217 | 218 | allows_variables_in_criteria_test(_) -> 219 | C = {times, 0}, 220 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), C). 221 | 222 | returns_error_on_invalid_criteria_test(_) -> 223 | lists:foreach( 224 | fun(C) -> 225 | ?assertError({{reason, {invalid_criteria, C}}, 226 | {location, _}}, 227 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), C)) 228 | end, 229 | [0, 230 | x, 231 | {at_least, x}, 232 | {at_most, x}, 233 | {times, x}]). 234 | 235 | checks_that_two_different_functions_are_called_test(_) -> 236 | mockgyver_dummy:return_arg(1), 237 | mockgyver_dummy:return_arg2(1), 238 | ?WAS_CALLED(mockgyver_dummy:return_arg(1), once), 239 | ?WAS_CALLED(mockgyver_dummy:return_arg2(1), once). 240 | 241 | knows_the_difference_in_arities_test(_) -> 242 | mockgyver_dummy:return_arg(1), 243 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), once), 244 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, _), never). 245 | 246 | can_verify_both_mocked_and_non_mocked_modules_test(_) -> 247 | ?WHEN(mockgyver_dummy:return_arg(_) -> [3, 2, 1]), 248 | [3, 2, 1] = lists:reverse([1, 2, 3]), 249 | [3, 2, 1] = mockgyver_dummy:return_arg(1), 250 | ?WAS_CALLED(lists:reverse(_)), 251 | ?WAS_CALLED(mockgyver_dummy:return_arg(_)). 252 | 253 | handles_all_criterias_test(_) -> 254 | %% never 255 | {error, {fewer_calls_than_expected, _, _}} = 256 | mockgyver:check_criteria(never, -1), 257 | ok = mockgyver:check_criteria(never, 0), 258 | {error, {more_calls_than_expected, _, _}} = 259 | mockgyver:check_criteria(never, 1), 260 | %% once 261 | {error, {fewer_calls_than_expected, _, _}} = 262 | mockgyver:check_criteria(once, 0), 263 | ok = mockgyver:check_criteria(once, 1), 264 | {error, {more_calls_than_expected, _, _}} = 265 | mockgyver:check_criteria(once, 2), 266 | %% at_least 267 | {error, {fewer_calls_than_expected, _, _}} = 268 | mockgyver:check_criteria({at_least, 0}, -1), 269 | ok = mockgyver:check_criteria({at_least, 0}, 0), 270 | ok = mockgyver:check_criteria({at_least, 0}, 1), 271 | %% at_most 272 | ok = mockgyver:check_criteria({at_most, 0}, -1), 273 | ok = mockgyver:check_criteria({at_most, 0}, 0), 274 | {error, {more_calls_than_expected, _, _}} = 275 | mockgyver:check_criteria({at_most, 0}, 1), 276 | %% times 277 | {error, {fewer_calls_than_expected, _, _}} = 278 | mockgyver:check_criteria({times, 0}, -1), 279 | ok = mockgyver:check_criteria({times, 0}, 0), 280 | {error, {more_calls_than_expected, _, _}} = 281 | mockgyver:check_criteria({times, 0}, 1), 282 | ok. 283 | 284 | returns_immediately_if_waiters_criteria_already_fulfilled_test(_) -> 285 | mockgyver_dummy:return_arg(1), 286 | ?WAIT_CALLED(mockgyver_dummy:return_arg(N), once). 287 | 288 | waits_until_waiters_criteria_fulfilled_test(_) -> 289 | spawn(fun() -> timer:sleep(50), mockgyver_dummy:return_arg(1) end), 290 | ?WAIT_CALLED(mockgyver_dummy:return_arg(N), once). 291 | 292 | fails_directly_if_waiters_criteria_already_surpassed_test(_) -> 293 | mockgyver_dummy:return_arg(1), 294 | mockgyver_dummy:return_arg(1), 295 | ?assertError(_, ?WAIT_CALLED(mockgyver_dummy:return_arg(N), once)). 296 | 297 | returns_other_value_test(_) -> 298 | 1 = mockgyver_dummy:return_arg(1), 299 | 2 = mockgyver_dummy:return_arg(2), 300 | ?WHEN(mockgyver_dummy:return_arg(1) -> 42), 301 | 42 = mockgyver_dummy:return_arg(1), 302 | ?assertError(function_clause, mockgyver_dummy:return_arg(2)). 303 | 304 | can_change_return_value_test(_) -> 305 | 1 = mockgyver_dummy:return_arg(1), 306 | ?WHEN(mockgyver_dummy:return_arg(1) -> 42), 307 | 42 = mockgyver_dummy:return_arg(1), 308 | ?assertError(function_clause, mockgyver_dummy:return_arg(2)), 309 | ?WHEN(mockgyver_dummy:return_arg(_) -> 43), 310 | 43 = mockgyver_dummy:return_arg(1), 311 | 43 = mockgyver_dummy:return_arg(2). 312 | 313 | inherits_variables_from_outer_scope_test(_) -> 314 | NewVal = 42, 315 | ?WHEN(mockgyver_dummy:return_arg(_) -> NewVal), 316 | 42 = mockgyver_dummy:return_arg(1). 317 | 318 | can_use_params_test(_) -> 319 | ?WHEN(mockgyver_dummy:return_arg(N) -> N+1), 320 | 2 = mockgyver_dummy:return_arg(1). 321 | 322 | can_use_multi_clause_functions_test(_) -> 323 | ?WHEN(mockgyver_dummy:return_arg(N) when N >= 0 -> positive; 324 | mockgyver_dummy:return_arg(_N) -> negative), 325 | positive = mockgyver_dummy:return_arg(1), 326 | negative = mockgyver_dummy:return_arg(-1). 327 | 328 | fails_gracefully_when_mocking_a_bif_test(_) -> 329 | %% previously the test checked that pi/0 could be successfully 330 | %% mocked here -- it's a non-bif function -- but starting at 331 | %% Erlang/OTP R15B this is marked as a pure function which means that 332 | %% the compiler will inline it into the testcase so we cannot mock it. 333 | %% 334 | %% cos/1 should work (uses the bif) 335 | 1.0 = math:cos(0), 336 | %% mocking the bif should fail gracefully 337 | ?assertError({cannot_mock_bif, {math, cos, 1}}, ?WHEN(math:cos(_) -> 0)). 338 | 339 | can_call_renamed_module_test(_) -> 340 | ?WHEN(mockgyver_dummy:return_arg(N) -> 2*'mockgyver_dummy^':return_arg(N)), 341 | 6 = mockgyver_dummy:return_arg(3). 342 | 343 | can_make_new_module_test(_) -> 344 | ?WHEN(mockgyver_extra_dummy:return_arg(N) -> N), 345 | ?WHEN(mockgyver_extra_dummy:return_arg(M, N) -> {M, N}), 346 | 1 = mockgyver_extra_dummy:return_arg(1), 347 | {1, 2} = mockgyver_extra_dummy:return_arg(1, 2). 348 | 349 | can_make_new_function_in_existing_module_test(_) -> 350 | ?WHEN(mockgyver_dummy:inc_arg_by_two(N) -> 2*N), 351 | 1 = mockgyver_dummy:return_arg(1), 352 | 2 = mockgyver_dummy:inc_arg_by_two(1). 353 | 354 | counts_calls_test(_) -> 355 | mockgyver_dummy:return_arg(1), 356 | mockgyver_dummy:return_arg(2), 357 | mockgyver_dummy:return_arg(2), 358 | 0 = ?NUM_CALLS(mockgyver_dummy:return_arg(_, _)), 359 | 1 = ?NUM_CALLS(mockgyver_dummy:return_arg(1)), 360 | 2 = ?NUM_CALLS(mockgyver_dummy:return_arg(2)), 361 | 3 = ?NUM_CALLS(mockgyver_dummy:return_arg(_)). 362 | 363 | returns_calls_test(_) -> 364 | mockgyver_dummy:return_arg(1), 365 | mockgyver_dummy:return_arg(2), 366 | mockgyver_dummy:return_arg(2), 367 | [] = ?GET_CALLS(mockgyver_dummy:return_arg(_, _)), 368 | [[1]] = 369 | ?GET_CALLS(mockgyver_dummy:return_arg(1)), 370 | [[2], [2]] = 371 | ?GET_CALLS(mockgyver_dummy:return_arg(2)), 372 | [[1], [2], [2]] = 373 | ?GET_CALLS(mockgyver_dummy:return_arg(_)). 374 | 375 | forgets_when_to_default_test(_) -> 376 | 1 = mockgyver_dummy:return_arg(1), 377 | {1, 2} = mockgyver_dummy:return_arg(1, 2), 378 | ?WHEN(mockgyver_dummy:return_arg(_) -> foo), 379 | ?WHEN(mockgyver_dummy:return_arg(_, _) -> foo), 380 | foo = mockgyver_dummy:return_arg(1), 381 | foo = mockgyver_dummy:return_arg(1, 2), 382 | ?FORGET_WHEN(mockgyver_dummy:return_arg(_)), 383 | 1 = mockgyver_dummy:return_arg(1), 384 | foo = mockgyver_dummy:return_arg(1, 2). 385 | 386 | forgets_registered_calls_test(_) -> 387 | 1 = mockgyver_dummy:return_arg(1), 388 | 2 = mockgyver_dummy:return_arg(2), 389 | 3 = mockgyver_dummy:return_arg(3), 390 | {1, 2} = mockgyver_dummy:return_arg(1, 2), 391 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), {times, 3}), 392 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, _), once), 393 | ?FORGET_CALLS(mockgyver_dummy:return_arg(1)), 394 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), {times, 2}), 395 | ?WAS_CALLED(mockgyver_dummy:return_arg(1), never), 396 | ?WAS_CALLED(mockgyver_dummy:return_arg(2), once), 397 | ?WAS_CALLED(mockgyver_dummy:return_arg(2), once), 398 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, _), once), 399 | ?FORGET_CALLS(mockgyver_dummy:return_arg(_)), 400 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), never), 401 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, _), once), 402 | ?FORGET_CALLS(mockgyver_dummy:return_arg(_, _)), 403 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), never), 404 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, _), never). 405 | 406 | forgets_all_calls_test(_) -> 407 | 1 = mockgyver_dummy:return_arg(1), 408 | 2 = mockgyver_dummy:return_arg(2), 409 | 3 = mockgyver_dummy:return_arg(3), 410 | {1, 2} = mockgyver_dummy:return_arg(1, 2), 411 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), {times, 3}), 412 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, _), once), 413 | ?FORGET_CALLS(), 414 | ?WAS_CALLED(mockgyver_dummy:return_arg(_), never), 415 | ?WAS_CALLED(mockgyver_dummy:return_arg(_, _), never). 416 | 417 | returns_error_on_trying_to_mock_or_check_criteria_when_not_mocking_test() -> 418 | %% Note: This function has intentionally no parameter, to make 419 | %% eunit pick it up and avoid starting the mocking 420 | %% framework. Check that this returns a nice and 421 | %% understandable error message. 422 | %% 423 | %% These are calls that will be made from a test case. 424 | ?assertError(mocking_not_started, 425 | ?WHEN(mockgyver_dummy:return_arg(_) -> foo)), 426 | ?assertError(mocking_not_started, 427 | ?WAS_CALLED(mockgyver_dummy:return_arg(_))), 428 | ?assertError(mocking_not_started, 429 | ?WAIT_CALLED(mockgyver_dummy:return_arg(_))), 430 | ?assertError(mocking_not_started, 431 | ?NUM_CALLS(mockgyver_dummy:return_arg(_))), 432 | ?assertError(mocking_not_started, 433 | ?GET_CALLS(mockgyver_dummy:return_arg(_))), 434 | ?assertError(mocking_not_started, 435 | ?FORGET_WHEN(mockgyver_dummy:return_arg(_))), 436 | ?assertError(mocking_not_started, 437 | ?FORGET_CALLS(mockgyver_dummy:return_arg(_))), 438 | ?assertError(mocking_not_started, 439 | ?FORGET_CALLS()), 440 | ok. 441 | 442 | returns_error_on_trying_to_get_mock_info_when_not_mocking_test() -> 443 | %% Note: This function has intentionally no parameter, to make 444 | %% eunit pick it up and avoid starting the mocking 445 | %% framework. Check that this returns a nice and 446 | %% understandable error message. 447 | %% 448 | %% These are calls that will be made from within a mock, 449 | %% i.e. calls are mackgyver-internal and will not be seen in 450 | %% test cases. 451 | ?assertError(mocking_not_started, 452 | mockgyver:get_action({mockgyver_dummy, return_arg, [1]})), 453 | ?assertError(mocking_not_started, 454 | mockgyver:reg_call_and_get_action({mockgyver_dummy, return_arg, [1]})). 455 | 456 | includes_mocked_mfa_in_stacktrace_on_error_test(_) -> 457 | ?WHEN(mockgyver_dummy:return_arg(_) -> error(bar)), 458 | try 459 | mockgyver_dummy:return_arg(foo) 460 | catch 461 | error:bar:StackTrace -> 462 | [{mockgyver_dummy, return_arg, 1, _}, 463 | {?MODULE, ?FUNCTION_NAME, 1, _} | _] = StackTrace 464 | end. 465 | 466 | includes_mocked_mfa_in_stacktrace_on_throw_test(_) -> 467 | ?WHEN(mockgyver_dummy:return_arg(_) -> throw(bar)), 468 | try 469 | mockgyver_dummy:return_arg(foo) 470 | catch 471 | throw:bar:StackTrace -> 472 | [{mockgyver_dummy, return_arg, 1, _}, 473 | {?MODULE, ?FUNCTION_NAME, 1, _} | _] = StackTrace 474 | end. 475 | 476 | includes_mocked_mfa_in_stacktrace_on_exit_test(_) -> 477 | ?WHEN(mockgyver_dummy:return_arg(_) -> exit(bar)), 478 | try 479 | mockgyver_dummy:return_arg(foo) 480 | catch 481 | exit:bar:StackTrace -> 482 | [{mockgyver_dummy, return_arg, 1, _}, 483 | {?MODULE, ?FUNCTION_NAME, 1, _} | _] = StackTrace 484 | end. 485 | 486 | includes_mocked_mfa_in_stacktrace_on_function_clause_test(_) -> 487 | ?WHEN(mockgyver_dummy:return_arg(1) -> a), 488 | try 489 | mockgyver_dummy:return_arg(-1) 490 | catch 491 | error:function_clause:StackTrace -> 492 | [{mockgyver_dummy, return_arg, [-1], _}, 493 | {?MODULE, ?FUNCTION_NAME, 1, _} | _] = StackTrace 494 | end. 495 | 496 | keeps_3_tuple_stacktrace_elems_on_error_test(_) -> 497 | ok = compile_load_txt_forms( 498 | mockgyver_dummy7, 499 | ["-module(mockgyver_dummy7).\n", 500 | "-export([have_fun/0]).\n", 501 | "have_fun() -> fun() -> error(foo) end.\n"]), 502 | Fun = mockgyver_dummy7:have_fun(), 503 | code:purge(mockgyver_dummy7), 504 | true = code:delete(mockgyver_dummy7), 505 | code:purge(mockgyver_dummy7), 506 | false = code:is_loaded(mockgyver_dummy7), 507 | ?WHEN(mockgyver_dummy:return_arg(_) -> Fun()), 508 | try 509 | mockgyver_dummy:return_arg(foo) 510 | catch 511 | error:undef:StackTrace -> 512 | [{Fun, [], _}, 513 | {?MODULE, ?FUNCTION_NAME, 1, _} | _] = StackTrace 514 | end. 515 | 516 | %% Ensure that trace on lists:reverse has been enabled at least once 517 | %% so we can test that it has been removed in 518 | %% 'removes_trace_pattern_test'. 519 | activates_trace_on_lists_reverse_test(_) -> 520 | [] = lists:reverse([]), 521 | ?WAS_CALLED(lists:reverse(_)). 522 | 523 | %% Trace is removed after the mockgyver session has finished, so we 524 | %% cannot test it in a mockgyver test case (arity 1). 525 | removes_trace_pattern_test_() -> 526 | {timeout, ?PER_TC_TIMEOUT, fun removes_trace_pattern_test_aux/0}. 527 | 528 | removes_trace_pattern_test_aux() -> 529 | {flags, Flags} = erlang:trace_info(new, flags), 530 | erlang:trace(all, false, Flags), 531 | erlang:trace(all, true, [call, {tracer, self()}]), 532 | 533 | Master = self(), 534 | Ref = make_ref(), 535 | %% spawn as trace is not activated on the tracing process 536 | spawn(fun() -> 537 | lists:reverse([]), 538 | Master ! {Ref, done} 539 | end), 540 | 541 | receive {Ref, done} -> ok end, 542 | TRef = erlang:trace_delivered(self()), 543 | receive {trace_delivered, _, TRef} -> ok end, 544 | 545 | ?assertEqual([], flush()). 546 | 547 | %% A cached mocking module shall not be used if the original module 548 | %% has been changed. 549 | changed_module_is_used_instead_of_cached_test_() -> 550 | {timeout, ?PER_TC_TIMEOUT, 551 | fun changed_module_is_used_instead_of_cached_test_aux/0}. 552 | 553 | changed_module_is_used_instead_of_cached_test_aux() -> 554 | create_dummy(mockgyver_dummy2, a), 555 | ?MOCK(fun() -> 556 | %% fool mockgyver into mocking the module by mocking 557 | %% a function that we will _not_ use within that 558 | %% module 559 | ?WHEN(mockgyver_dummy2:c() -> ok), 560 | 1 = mockgyver_dummy2:a(1), 561 | ?assertError(undef, mockgyver_dummy2:b(1)) 562 | end), 563 | create_dummy(mockgyver_dummy2, b), 564 | ?MOCK(fun() -> 565 | %% fool mockgyver into mocking the module by mocking 566 | %% a function that we will _not_ use within that 567 | %% module 568 | ?WHEN(mockgyver_dummy2:c() -> ok), 569 | ?assertError(undef, mockgyver_dummy2:a(1)), 570 | 1 = mockgyver_dummy2:b(1) 571 | end). 572 | 573 | can_mock_a_dynamically_generated_module_test_() -> 574 | %% Check that it is possible to mock a module that is loaded into 575 | %% the vm, but does not exist on disk, such as a dynamically 576 | %% generated module. 577 | RText = lists:flatten(io_lib:format("~p", [make_ref()])), 578 | ok = compile_load_txt_forms( 579 | mockgyver_dyn_ref_text, 580 | ["-module(mockgyver_dyn_ref_text).\n", 581 | "-export([ref_text/0]).\n", 582 | "ref_text() -> \"" ++ RText ++ "\".\n"]), 583 | RText = mockgyver_dyn_ref_text:ref_text(), 584 | {timeout, ?PER_TC_TIMEOUT, 585 | fun() -> can_mock_a_dynamically_generated_module_test_aux(RText) end}. 586 | 587 | can_mock_a_dynamically_generated_module_test_aux(_OrigRText) -> 588 | ?MOCK(fun() -> 589 | ?WHEN(mockgyver_dyn_ref_text:ref_text() -> "a"), 590 | ?assertEqual("a", mockgyver_dyn_ref_text:ref_text()) 591 | end). 592 | 593 | can_mock_a_module_with_on_load_test_() -> 594 | {timeout, ?PER_TC_TIMEOUT, 595 | fun can_mock_a_module_with_on_load_aux/0}. 596 | 597 | can_mock_a_module_with_on_load_aux() -> 598 | %% The code:atomic_load/1 does not allow modules with -on_load(). 599 | %% The mockgyver falls back to loading such modules individually. 600 | %% Test that. 601 | Dir = test_dir(), 602 | Mod = mockgyver_dummy3, 603 | Filename = filename:join(Dir, atom_to_list(Mod) ++ ".erl"), 604 | ok = file:write_file(Filename, 605 | io_lib:format("%% Generated by ~p:~p~n" 606 | "-module(~p).~n" 607 | "-export([z/1]).~n" 608 | "-on_load(do_nothing/0).~n" 609 | "z(Z) -> Z.~n" 610 | "do_nothing() -> ok.~n", 611 | [?MODULE, ?FUNCTION_NAME, Mod])), 612 | {ok, Mod} = compile:file(Filename, [{outdir, Dir}]), 613 | ?MOCK(fun() -> 614 | ?WHEN(mockgyver_dummy3:x() -> mocked), 615 | ?WHEN(mockgyver_dummy3:y() -> also_mocked), 616 | mocked = mockgyver_dummy3:x(), 617 | abc123 = mockgyver_dummy3:z(abc123) 618 | end). 619 | 620 | two_sessions_with_elements_test_() -> 621 | {timeout, ?PER_TC_TIMEOUT, fun two_sessions_with_elements_aux/0}. 622 | 623 | two_sessions_with_elements_aux() -> 624 | %% This imitates two ?WITH_MOCKED_SETUP after each other. 625 | create_dummy(mockgyver_dummy5, a), 626 | create_dummy(mockgyver_dummy6, a), 627 | UnmockedChecksum5 = mockgyver_dummy5:module_info(md5), 628 | ok = mockgyver:start_session(?MOCK_SESSION_PARAMS), 629 | 630 | ok = mockgyver:start_session_element(), 631 | ?WHEN(mockgyver_dummy5:a(_) -> mocked_1_1), 632 | mocked_1_1 = mockgyver_dummy5:a(1), 633 | ok = mockgyver:end_session_element(), 634 | 635 | %% It should stay mocked across session elements in a session, 636 | %% that's the optimizaion. Can't call it to test it though---that 637 | %% would require a session element---so test it in another way. 638 | ?assertNotEqual(UnmockedChecksum5, mockgyver_dummy5:module_info(md5)), 639 | 640 | ok = mockgyver:start_session_element(), 641 | %% The ?WHEN from previous ?MOCK in the mock sueqence 642 | %% should not initially be active: 643 | 1 = mockgyver_dummy5:a(1), 644 | %% But it should still be possible to mock it again: 645 | ?WHEN(mockgyver_dummy5:a(_) -> mocked_1_2), 646 | mocked_1_2 = mockgyver_dummy5:a(1), 647 | ok = mockgyver:end_session_element(), 648 | 649 | ok = mockgyver:end_session(), 650 | 651 | %% It should get restored after the last session in the sequence 652 | ?assertEqual(UnmockedChecksum5, mockgyver_dummy5:module_info(md5)), 653 | 1 = mockgyver_dummy5:a(1), 654 | 655 | %% Next session 656 | ok = mockgyver:start_session(?MOCK_SESSION_PARAMS), 657 | 658 | ok = mockgyver:start_session_element(), 659 | ?WHEN(mockgyver_dummy6:a(_) -> mocked_2_1), 660 | not_mocked = mockgyver_dummy5:a(not_mocked), 661 | mocked_2_1 = mockgyver_dummy6:a(1), 662 | ok = mockgyver:end_session_element(), 663 | 664 | ok = mockgyver:start_session_element(), 665 | ?WHEN(mockgyver_dummy6:a(_) -> mocked_2_2), 666 | not_mocked = mockgyver_dummy5:a(not_mocked), 667 | mocked_2_2 = mockgyver_dummy6:a(1), 668 | ok = mockgyver:end_session_element(), 669 | 670 | ok = mockgyver:end_session(), 671 | ok. 672 | 673 | renamed_gets_called_when_mocked_mod_called_between_session_elems_test_() -> 674 | {timeout, ?PER_TC_TIMEOUT, 675 | fun renamed_gets_called_when_mocked_mod_called_between_sn_elems_aux/0}. 676 | 677 | renamed_gets_called_when_mocked_mod_called_between_sn_elems_aux() -> 678 | with_tmp_app_env( 679 | mock_sequence_timeout, ?PER_TC_TIMEOUT * 1000, 680 | fun renamed_gets_called_when_mocked_mod_called_between_sn_elems_aux2/0). 681 | 682 | renamed_gets_called_when_mocked_mod_called_between_sn_elems_aux2() -> 683 | create_dummy(mockgyver_dummyb, a), 684 | 685 | ok = mockgyver:start_session(?MOCK_SESSION_PARAMS), 686 | ok = mockgyver:start_session_element(), 687 | ?WHEN(mockgyver_dummyb:a(_) -> 11), 688 | 11 = mockgyver_dummyb:a(1), 689 | ok = mockgyver:end_session_element(), 690 | 691 | %% Calls to functions in mocked modules should go to the renamed module^ 692 | %% between sessions 693 | 1 = mockgyver_dummyb:a(1), 694 | 695 | ok = mockgyver:start_session_element(), 696 | ?WHEN(mockgyver_dummyb:a(_) -> 12), 697 | 12 = mockgyver_dummyb:a(1), 698 | ok = mockgyver:end_session_element(), 699 | 700 | ok = mockgyver:end_session(), 701 | ok. 702 | 703 | backwards_compat_test_() -> 704 | %% If a build system would not detect that test beams needs 705 | %% to be recompiled, they would call exec/3 like this. 706 | ?WITH_FUN(fun(_) -> 707 | mockgyver:exec([], [], fun backwards_compat_aux/0) 708 | end, 709 | ?PER_TC_TIMEOUT, 710 | ?PER_TC_TIMEOUT, 711 | [x]). 712 | 713 | backwards_compat_aux() -> 714 | ok. 715 | 716 | with_tmp_app_env(Var, Val, F) -> 717 | Orig = application:get_env(mockgyver, Var), 718 | application:set_env(mockgyver, Var, Val), 719 | try F() 720 | after 721 | %% Make sure the temporary timeout is restored: 722 | case Orig of 723 | undefined -> application:unset_env(mockgyver, Var); 724 | {ok, OrigVal} -> application:set_env(mockgyver, Var, OrigVal) 725 | end 726 | end. 727 | 728 | create_dummy(Mod, Func) -> 729 | Dir = test_dir(), 730 | Filename = filename:join(Dir, atom_to_list(Mod) ++ ".erl"), 731 | file:write_file(Filename, 732 | io_lib:format("%% Generated by ~p:~p~n" 733 | "-module(~p).~n" 734 | "-export([~p/1]).~n" 735 | "~p(X) -> X.", 736 | [?MODULE, ?FUNCTION_NAME, Mod, 737 | Func, Func])), 738 | {ok, Mod} = compile:file(Filename, [{outdir, Dir}]). 739 | 740 | compile_load_txt_forms(Mod, TextForms) -> 741 | Forms = [begin 742 | {ok, Tokens, _} = erl_scan:string(Text), 743 | {ok, F} = erl_parse:parse_form(Tokens), 744 | F 745 | end 746 | || Text <- TextForms], 747 | {ok, Mod, Bin, []} = compile:noenv_forms(Forms, [binary, return]), 748 | {module, Mod} = code:load_binary(Mod, "dyn", Bin), 749 | ok. 750 | 751 | flush() -> 752 | receive Msg -> 753 | [Msg | flush()] 754 | after 0 -> 755 | [] 756 | end. 757 | 758 | test_dir() -> 759 | filename:dirname(code:which(?MODULE)). 760 | -------------------------------------------------------------------------------- /src/mockgyver.erl: -------------------------------------------------------------------------------- 1 | %%%=================================================================== 2 | %%% Copyright (c) 2011, Klas Johansson 3 | %%% All rights reserved. 4 | %%% 5 | %%% Redistribution and use in source and binary forms, with or without 6 | %%% modification, are permitted provided that the following conditions are 7 | %%% met: 8 | %%% 9 | %%% * Redistributions of source code must retain the above copyright 10 | %%% notice, this list of conditions and the following disclaimer. 11 | %%% 12 | %%% * Redistributions in binary form must reproduce the above copyright 13 | %%% notice, this list of conditions and the following disclaimer in 14 | %%% the documentation and/or other materials provided with the 15 | %%% distribution. 16 | %%% 17 | %%% * Neither the name of the copyright holder nor the names of its 18 | %%% contributors may be used to endorse or promote products derived 19 | %%% from this software without specific prior written permission. 20 | %%% 21 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | %%% IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | %%% TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | %%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | %%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | %%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 27 | %%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | %%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | %%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | %%%=================================================================== 33 | 34 | %%%------------------------------------------------------------------- 35 | %%% @author Klas Johansson 36 | %%% @copyright 2011, Klas Johansson 37 | %%% @doc 38 | %%% Mock functions and modules 39 | %%% 40 | %%% === Initiating mock === 41 | %%% 42 | %%% In order to use the various macros below, mocking must be 43 | %%% initiated using the `?MOCK' macro or `?WITH_MOCKED_SETUP' 44 | %%% (recommended from eunit tests). 45 | %%% 46 | %%% ==== ?MOCK syntax ==== 47 | %%%
  48 | %%%     ?MOCK(Expr)
  49 | %%% 
50 | %%% where `Expr' in a single expression, like a fun. The rest of the 51 | %%% macros in this module can be used within this fun or in a function 52 | %%% called by the fun. 53 | %%% 54 | %%% ==== ?WITH_MOCKED_SETUP syntax ==== 55 | %%%
  56 | %%%     ?WITH_MOCKED_SETUP(SetupFun, CleanupFun),
  57 | %%%     ?WITH_MOCKED_SETUP(SetupFun, CleanupFun, ForAllTimeout, PerTcTimeout),
  58 | %%%     ?WITH_MOCKED_SETUP(SetupFun, CleanupFun, ForAllTimeout, PerTcTimeout,
  59 | %%%                        Tests),
  60 | %%% 
61 | %%% This is an easy way of using mocks from within eunit tests and is 62 | %%% mock-specific version of the `?WITH_SETUP' macro. See the docs 63 | %%% for the `?WITH_SETUP' macro in the `eunit_addons' project for more 64 | %%% information on parameters and settings. 65 | %%% 66 | %%% === Mocking a function === 67 | %%% 68 | %%% ==== Introduction ==== 69 | %%% By mocking a function, its original side-effects and return value 70 | %%% (or throw/exit/error) are overridden and replaced. This can be used to: 71 | %%% 72 | %%% 77 | %%% 78 | %%% BIFs (built-in functions) cannot be mocked. 79 | %%% 80 | %%% The original module will be renamed (a "^" will be appended to the 81 | %%% original module name, i.e. ``foo'' will be renamed to `` 'foo^' ''). 82 | %%% A mock can then call the original function just by performing a regular 83 | %%% function call. 84 | %%% 85 | %%% Since WHEN is a macro, and macros don't support argument lists 86 | %%% (something like "Arg..."), multi-expression mocks must be 87 | %%% surrounded by `begin ... end' to be treated as one argument by the 88 | %%% preprocessor. 89 | %%% 90 | %%% A mock that was introduced using the ?WHEN macro can be forgotten, 91 | %%% i.e. returned to the behaviour of the original module, using the 92 | %%% `?FORGET_WHEN' macro. 93 | %%% 94 | %%% ==== ?WHEN syntax ==== 95 | %%%
  96 | %%%     ?WHEN(module:function(Arg1, Arg2, ...) -> Expr),
  97 | %%% 
98 | %%% 99 | %%% where `Expr' is a single expression (like a term) or a series of 100 | %%% expressions surrounded by `begin' and `end'. 101 | %%% 102 | %%% ==== ?FORGET_WHEN syntax ==== 103 | %%%
 104 | %%%     ?FORGET_WHEN(module:function(_, _, ...)),
 105 | %%% 
106 | %%% 107 | %%% The only things of interest are the name of the module, the name 108 | %%% of the function and the arity. The arguments of the function are 109 | %%% ignored and it can be a wise idea to set these to the "don't care" 110 | %%% variable: underscore. 111 | %%% 112 | %%% ==== Examples ==== 113 | %%% Note: Apparently the Erlang/OTP team doesn't want us to redefine 114 | %%% PI to 4 anymore :-), since starting at R15B, math:pi/0 is marked as 115 | %%% pure which means that the compiler is allowed to replace the 116 | %%% math:pi() function call by a constant: 3.14... This means that 117 | %%% even though mockgyver can mock the pi/0 function, a test case will 118 | %%% never call math:pi/0 since it will be inlined. See commit 119 | %%% 5adf009cb09295893e6bb01b4666a569590e0f19 (compiler: Turn calls to 120 | %%% math:pi/0 into constant values) in the otp sources. 121 | %%% 122 | %%% Redefine pi to 4: 123 | %%%
 124 | %%%     ?WHEN(math:pi() -> 4),
 125 | %%% 
126 | %%% Implement a mock with multiple clauses: 127 | %%%
 128 | %%%     ?WHEN(my_module:classify_number(N) when N >= 0 -> positive;
 129 | %%%           my_module:classify_number(_N)            -> negative),
 130 | %%% 
131 | %%% Call original module: 132 | %%%
 133 | %%%     ?WHEN(math:pi() -> 'math^':pi() * 2),
 134 | %%% 
135 | %%% Use a variable bound outside the mock: 136 | %%%
 137 | %%%     Answer = 42,
 138 | %%%     ?WHEN(math:pi() -> Answer),
 139 | %%% 
140 | %%% Redefine the mock: 141 | %%%
 142 | %%%     ?WHEN(math:pi() -> 4),
 143 | %%%     4 = math:pi(),
 144 | %%%     ?WHEN(math:pi() -> 5),
 145 | %%%     5 = math:pi(),
 146 | %%% 
147 | %%% Let the mock exit with an error: 148 | %%%
 149 | %%%     ?WHEN(math:pi() -> erlang:error(some_error)),
 150 | %%% 
151 | %%% Make a new module: 152 | %%%
 153 | %%%     ?WHEN(my_math:pi() -> 4),
 154 | %%%     ?WHEN(my_math:e() -> 3),
 155 | %%% 
156 | %%% Put multiple clauses in a function's body: 157 | %%%
 158 | %%%     ?WHEN(math:pi() ->
 159 | %%%               begin
 160 | %%%                   do_something1(),
 161 | %%%                   do_something2()
 162 | %%%               end),
 163 | %%% 
164 | %%% Revert the pi function to its default behaviour (return value from 165 | %%% the original module), any other mocks in the same module, or any 166 | %%% other module are left untouched: 167 | %%%
 168 | %%%     ?WHEN(math:pi() -> 4),
 169 | %%%     4 = math:pi(),
 170 | %%%     ?FORGET_WHEN(math:pi()),
 171 | %%%     3.1415... = math:pi(),
 172 | %%% 
173 | %%% 174 | %%% === Validating calls === 175 | %%% 176 | %%% ==== Introduction ==== 177 | %%% 178 | %%% There are a number of ways to check that a certain function has 179 | %%% been called and that works for both mocks and non-mocks. 180 | %%% 181 | %%% 208 | %%% 209 | %%% ==== ?WAS_CALLED syntax ==== 210 | %%%
 211 | %%%     ?WAS_CALLED(module:function(Arg1, Arg2, ...)),
 212 | %%%         equivalent to ?WAS_CALLED(module:function(Arg1, Arg2, ...), once)
 213 | %%%     ?WAS_CALLED(module:function(Arg1, Arg2, ...), Criteria),
 214 | %%%         Criteria = once | never | {times, N} | {at_least, N} | {at_most, N}
 215 | %%%         N = integer()
 216 | %%%
 217 | %%%         Result: [CallArgs]
 218 | %%%                 CallArgs = [CallArg]
 219 | %%%                 CallArg = term()
 220 | %%%
 221 | %%% 
222 | %%% ==== ?WAIT_CALLED syntax ==== 223 | %%% 224 | %%% See syntax for `?WAS_CALLED'. 225 | %%% 226 | %%% ==== ?GET_CALLS syntax ==== 227 | %%%
 228 | %%%     ?GET_CALLS(module:function(Arg1, Arg2, ...)),
 229 | %%%
 230 | %%%         Result: [CallArgs]
 231 | %%%                 CallArgs = [CallArg]
 232 | %%%                 CallArg = term()
 233 | %%% 
234 | %%% 235 | %%% ==== ?NUM_CALLS syntax ==== 236 | %%%
 237 | %%%     ?NUM_CALLS(module:function(Arg1, Arg2, ...)),
 238 | %%%
 239 | %%%         Result: integer()
 240 | %%% 
241 | %%% ==== ?FORGET_CALLS syntax ==== 242 | %%%
 243 | %%%     ?FORGET_CALLS(module:function(Arg1, Arg2, ...)),
 244 | %%%     ?FORGET_CALLS(),
 245 | %%% 
246 | %%% ==== Examples ==== 247 | %%% Check that a function has been called once (the two alternatives 248 | %%% are equivalent): 249 | %%%
 250 | %%%     ?WAS_CALLED(math:pi()),
 251 | %%%     ?WAS_CALLED(math:pi(), once),
 252 | %%% 
253 | %%% Check that a function has never been called: 254 | %%%
 255 | %%%     ?WAS_CALLED(math:pi(), never),
 256 | %%% 
257 | %%% Check that a function has been called twice: 258 | %%%
 259 | %%%     ?WAS_CALLED(math:pi(), {times, 2}),
 260 | %%% 
261 | %%% Check that a function has been called at least twice: 262 | %%%
 263 | %%%     ?WAS_CALLED(math:pi(), {at_least, 2}),
 264 | %%% 
265 | %%% Check that a function has been called at most twice: 266 | %%%
 267 | %%%     ?WAS_CALLED(math:pi(), {at_most, 2}),
 268 | %%% 
269 | %%% Use pattern matching to check that a function was called with 270 | %%% certain arguments: 271 | %%%
 272 | %%%     ?WAS_CALLED(lists:reverse([a, b, c])),
 273 | %%% 
274 | %%% Pattern matching can even use bound variables: 275 | %%%
 276 | %%%     L = [a, b, c],
 277 | %%%     ?WAS_CALLED(lists:reverse(L)),
 278 | %%% 
279 | %%% Use a guard to validate the parameters in a call: 280 | %%%
 281 | %%%     ?WAS_CALLED(lists:reverse(L) when is_list(L)),
 282 | %%% 
283 | %%% Retrieve the arguments in a call while verifying the number of calls: 284 | %%%
 285 | %%%     a = lists:nth(1, [a, b]),
 286 | %%%     d = lists:nth(2, [c, d]),
 287 | %%%     [[1, [a, b]], [2, [c, d]]] = ?WAS_CALLED(lists:nth(_, _), {times, 2}),
 288 | %%% 
289 | %%% Retrieve the arguments in a call without verifying the number of calls: 290 | %%%
 291 | %%%     a = lists:nth(1, [a, b]),
 292 | %%%     d = lists:nth(2, [c, d]),
 293 | %%%     [[1, [a, b]], [2, [c, d]]] = ?GET_CALLS(lists:nth(_, _)),
 294 | %%% 
295 | %%% Retrieve the number of calls: 296 | %%%
 297 | %%%     a = lists:nth(1, [a, b]),
 298 | %%%     d = lists:nth(2, [c, d]),
 299 | %%%     2 = ?NUM_CALLS(lists:nth(_, _)),
 300 | %%% 
301 | %%% Forget calls to functions: 302 | %%%
 303 | %%%     a = lists:nth(1, [a, b, c]),
 304 | %%%     e = lists:nth(2, [d, e, f]),
 305 | %%%     i = lists:nth(3, [g, h, i]),
 306 | %%%     ?WAS_CALLED(lists:nth(1, [a, b, c]), once),
 307 | %%%     ?WAS_CALLED(lists:nth(2, [d, e, f]), once),
 308 | %%%     ?WAS_CALLED(lists:nth(3, [g, h, i]), once),
 309 | %%%     ?FORGET_CALLS(lists:nth(2, [d, e, f])),
 310 | %%%     ?WAS_CALLED(lists:nth(1, [a, b, c]), once),
 311 | %%%     ?WAS_CALLED(lists:nth(2, [d, e, f]), never),
 312 | %%%     ?WAS_CALLED(lists:nth(3, [g, h, i]), once),
 313 | %%%     ?FORGET_CALLS(lists:nth(_, _)),
 314 | %%%     ?WAS_CALLED(lists:nth(1, [a, b, c]), never),
 315 | %%%     ?WAS_CALLED(lists:nth(2, [d, e, f]), never),
 316 | %%%     ?WAS_CALLED(lists:nth(3, [g, h, i]), never),
 317 | %%% 
318 | %%% Forget calls to all functions: 319 | %%%
 320 | %%%     a = lists:nth(1, [a, b, c]),
 321 | %%%     e = lists:nth(2, [d, e, f]),
 322 | %%%     i = lists:nth(3, [g, h, i]),
 323 | %%%     ?WAS_CALLED(lists:nth(1, [a, b, c]), once),
 324 | %%%     ?WAS_CALLED(lists:nth(2, [d, e, f]), once),
 325 | %%%     ?WAS_CALLED(lists:nth(3, [g, h, i]), once),
 326 | %%%     ?FORGET_CALLS(),
 327 | %%%     ?WAS_CALLED(lists:nth(1, [a, b, c]), never),
 328 | %%%     ?WAS_CALLED(lists:nth(2, [d, e, f]), never),
 329 | %%%     ?WAS_CALLED(lists:nth(3, [g, h, i]), never),
 330 | %%% 
331 | %%% 332 | %%% %%% ==== ?MOCK_SESSION_PARAMS ==== 333 | %%% 334 | %%% This is expands to a term that describes MFAs that are (to be) 335 | %%% mocked (with ?WHEN) and to be watched or traced (with ?WAS_CALLED 336 | %%% and similar). It can be used with the start_session or exec 337 | %%% functions. 338 | %%% 339 | %%% @end 340 | %%%------------------------------------------------------------------- 341 | -module(mockgyver). 342 | 343 | -behaviour(gen_statem). 344 | 345 | %% This transform makes it easier for this module to generate code. 346 | %% Depends on a 3pp library (http://github.com/esl/parse_trans). 347 | -compile({parse_transform, parse_trans_codegen}). 348 | 349 | %% API 350 | -export([exec/2, 351 | exec/3]). 352 | 353 | -export([start_link/0]). 354 | -export([stop/0]). 355 | 356 | -export([reg_call_and_get_action/1, get_action/1, set_action/1, set_action/2]). 357 | -export([verify/2, verify/3]). 358 | -export([forget_all_calls/0]). 359 | 360 | %% Low-level session handling, intended mostly for use from mockgyver.hrl 361 | -export([start_session/1]). 362 | -export([end_session/0]). 363 | -export([start_session_element/0]). 364 | -export([end_session_element/0]). 365 | 366 | %% For test 367 | -export([check_criteria/2]). 368 | 369 | %% state functions 370 | -export([no_session/3, 371 | session/3, 372 | session_element/3]). 373 | %% gen_statem callbacks 374 | -export([init/1, 375 | callback_mode/0, 376 | terminate/3, 377 | code_change/4]). 378 | 379 | -define(SERVER, ?MODULE). 380 | -define(CACHE_TAB, list_to_atom(?MODULE_STRING ++ "_module_cache")). 381 | 382 | -define(beam_num_bytes_alignment, 4). %% according to spec below 383 | 384 | -define(cand_resem_threshold, 5). %% threshold for similarity (0 = identical) 385 | 386 | -record(call, 387 | %% holds information on a called MFA (used in eg. error messages) 388 | {m :: module(), 389 | f :: atom(), 390 | a :: args()}). 391 | 392 | -record(action, 393 | %% holds information on what will happen when an MFA is called (?WHEN) 394 | {%% MFA that needs to be called for the func to be run 395 | mfa :: mfa(), 396 | %% Fun to run when the MFA is called 397 | func :: function()}). 398 | 399 | -record(call_waiter, 400 | %% holds information when waiting on a call to an MFA (?WAIT_CALLED) 401 | {%% A reference to the waiter 402 | from :: gen_statem:from(), 403 | %% MFA for which the waiter is waiting 404 | mfa :: mf_args_expectation(), 405 | %% Criteria that shall be fulfilled for the wait to be complete 406 | crit :: criteria(), 407 | %% A pointer to the location which is waiting (for error messages) 408 | loc :: {FIle::string(), Line::integer()}}). 409 | 410 | -record(state, 411 | {%% Storage of the mocks 412 | actions=[] :: [#action{}], 413 | %% Storage of calls to the mocks 414 | calls :: [#call{}] | undefined, 415 | %% Process which started the session 416 | session_mref :: reference() | undefined, 417 | %% A queue of session starters who have to wait for their turn 418 | session_waiters=queue:new() :: queue:queue(), 419 | %% Monitor for process which started the session element 420 | session_element_mref :: reference() | undefined, 421 | %% Storage of waiters 422 | call_waiters=[] :: [#call_waiter{}], 423 | %% MFAs being mocked 424 | mock_mfas=[] :: [mfa()], 425 | %% MFAs being traced 426 | watch_mfas=[] :: [mfa()], 427 | %% For restoring loaded modules during session_end 428 | init_modinfos=[]}). 429 | 430 | %-record(trace, {msg}). 431 | -record('DOWN', 432 | {mref :: reference(), 433 | type :: atom(), 434 | obj :: pid() | port(), 435 | info :: term()}). 436 | 437 | -define(mocking_key(Mod, Hash), {mocking_mod, Mod, Hash}). 438 | -record(mocking_mod, 439 | {key :: ?mocking_key(module(), binary()), 440 | code :: binary()}). 441 | 442 | -define(modinfo_key(Mod), {modinfo, Mod}). 443 | -record(modinfo, 444 | %% This record is for caching modules to load and mock, 445 | %% to minimize disk searches and exported functions and arities. 446 | {key :: ?modinfo_key(module()), 447 | exported_fas :: [{Fn::atom(), Arity::non_neg_integer()}], 448 | code :: binary(), 449 | filename :: string(), 450 | checksum :: checksum()}). 451 | -record(nomodinfo, 452 | %% For modules to mock when we have no cached info. 453 | {key :: ?modinfo_key(module())}). 454 | 455 | -type checksum() :: term(). 456 | 457 | -type criteria() :: once | {at_least, integer()} | {at_most, integer()} | {times, integer()} | never. 458 | 459 | -type verify_op() :: {was_called, criteria()} | 460 | {wait_called, criteria()} | 461 | num_calls | 462 | get_calls | 463 | forget_when | 464 | forget_calls. 465 | 466 | -type verify_opt() :: {location, {File :: string(), Line :: integer()}}. 467 | 468 | -type args() :: [term()]. 469 | 470 | -type mf_args_expectation() :: {module(), atom(), args_expectation()}. 471 | 472 | -type args_expectation() :: function(). % called with actual args to check match 473 | 474 | -type state_name() :: no_session | session. 475 | 476 | -type session_params() :: {Mocked::[mfa()], Watched::[mfa()]}. 477 | % see ?MOCK_SESSION_PARAMS 478 | 479 | -ifdef(OTP_RELEASE). 480 | %% The stack trace syntax introduced in Erlang 21 coincided 481 | %% with the introduction of the predefined macro OTP_RELEASE. 482 | -define(with_stacktrace(Class, Reason, Stack), 483 | Class:Reason:Stack ->). 484 | -else. % OTP_RELEASE 485 | -define(with_stacktrace(Class, Reason, Stack), 486 | Class:Reason -> 487 | Stack = erlang:get_stacktrace(),). 488 | -endif. % OTP_RELEASE. 489 | 490 | %%%=================================================================== 491 | %%% API 492 | %%%=================================================================== 493 | 494 | %% @private 495 | %% For backwards compatibility 496 | -spec exec([mfa()], [mfa()], fun(() -> Ret)) -> Ret. 497 | exec(MockMFAs, WatchMFAs, Fun) -> 498 | exec({MockMFAs, WatchMFAs}, Fun). 499 | 500 | %% @private 501 | -spec exec(session_params(), fun(() -> Ret)) -> Ret. 502 | exec(SessionParams, Fun) -> 503 | ok = ensure_application_started(), 504 | try 505 | case start_session(SessionParams) of 506 | ok -> 507 | exec_session_element(Fun); 508 | {error, _} = Error -> 509 | erlang:error(Error) 510 | end 511 | after 512 | end_session() 513 | end. 514 | 515 | %% @private 516 | -spec exec_session_element(fun(() -> Ret)) -> Ret. 517 | exec_session_element(Fun) -> 518 | try 519 | case start_session_element() of 520 | ok -> 521 | Fun(); 522 | {error, Error} -> 523 | erlang:error({session_element, Error}) 524 | end 525 | after 526 | end_session_element() 527 | end. 528 | 529 | %% @private 530 | -spec reg_call_and_get_action(MFA :: mfa()) -> function(). 531 | reg_call_and_get_action(MFA) -> 532 | chk(sync_send_event({reg_call_and_get_action, MFA})). 533 | 534 | %% @private 535 | -spec get_action(MFA :: mfa()) -> function(). 536 | get_action(MFA) -> 537 | chk(sync_send_event({get_action, MFA})). 538 | 539 | %% @private 540 | -spec set_action(MFA :: mfa()) -> ok. 541 | set_action(MFA) -> 542 | set_action(MFA, _Opts=[]). 543 | 544 | %% @private 545 | -spec set_action(MFA :: mfa(), Opts :: []) -> ok. 546 | set_action(MFA, Opts) -> 547 | chk(sync_send_event({set_action, MFA, Opts})). 548 | 549 | %% @private 550 | -spec start_link() -> gen_statem:start_ret(). 551 | start_link() -> 552 | gen_statem:start_link({local, ?SERVER}, ?MODULE, {}, []). 553 | 554 | %% @private 555 | -spec stop() -> ok. 556 | stop() -> 557 | sync_send_event(stop). 558 | 559 | -spec ensure_application_started() -> ok | {error, Reason :: term()}. 560 | ensure_application_started() -> 561 | case application:start(?MODULE) of 562 | ok -> ok; 563 | {error, {already_started, _}} -> ok; 564 | {error, _} = Error -> Error 565 | end. 566 | 567 | -spec start_session(session_params()) -> ok | {error, Reason::term()}. 568 | start_session({MockMFAs, WatchMFAs}) -> 569 | sync_send_event({start_session, MockMFAs, WatchMFAs, self()}). 570 | 571 | -spec end_session() -> ok. 572 | end_session() -> 573 | sync_send_event(end_session). 574 | 575 | start_session_element() -> 576 | sync_send_event({start_session_element, self()}). 577 | 578 | end_session_element() -> 579 | sync_send_event(end_session_element). 580 | 581 | %% @private 582 | -spec verify(MFA :: mfa(), Op :: verify_op()) -> [list()]. 583 | verify({M, F, A}, Op) -> 584 | verify({M, F, A}, Op, _Opts=[]). 585 | 586 | %% @private 587 | -spec verify(MFA :: mfa(), Op :: verify_op(), Opts :: [verify_opt()]) -> 588 | [list()]. 589 | verify({M, F, A}, Op, Opts) -> 590 | wait_until_trace_delivered(), 591 | chk(sync_send_event({verify, {M, F, A}, Op, Opts})). 592 | 593 | %% @private 594 | -spec forget_all_calls() -> ok. 595 | forget_all_calls() -> 596 | chk(sync_send_event(forget_all_calls)). 597 | 598 | %%%=================================================================== 599 | %%% gen_statem callbacks 600 | %%%=================================================================== 601 | 602 | %% The state machine and its transitions works like this: 603 | %% 604 | %% +-------------+ A +---------+ C +-----------------+ 605 | %% init | |---->| |---->| | 606 | %% ----->| no_session | | session | | session_element | 607 | %% | |<----| |<----| | 608 | %% +-------------+ B +---------+ D +-----------------+ 609 | %% 610 | %% On A: Setup module mockings 611 | %% On B: Restore mocked module 612 | %% On C: Setup call tracing and recording of called modules 613 | %% On D: Clear call tracing and clear any mockings set with ?WHEN() 614 | %% 615 | %% Each eunit tests is intended to execute in a session element of its own, 616 | %% and a ?WITH_MOCKED_SETUP() starts and ends a session. 617 | 618 | %% @private 619 | %% @doc state_functions means StateName/3 620 | -spec callback_mode() -> gen_statem:callback_mode_result(). 621 | callback_mode() -> 622 | state_functions. 623 | 624 | %%-------------------------------------------------------------------- 625 | %% @private 626 | %% @doc Initialize the state machine 627 | %% @end 628 | %%-------------------------------------------------------------------- 629 | -spec init({}) -> gen_statem:init_result(state_name()). 630 | init({}) -> 631 | create_mod_cache(), 632 | {ok, no_session, #state{}}. 633 | 634 | %%-------------------------------------------------------------------- 635 | %% @private 636 | %% @doc State for when no session is yet started 637 | %% @end 638 | %%-------------------------------------------------------------------- 639 | -spec no_session(EventType :: gen_statem:event_type(), 640 | EventContent :: term(), 641 | State :: #state{}) -> 642 | gen_statem:event_handler_result(state_name()). 643 | no_session({call, From}, {start_session, MockMFAs, WatchMFAs, Pid}, State0) -> 644 | {Reply, State} = i_start_session(MockMFAs, WatchMFAs, Pid, State0), 645 | {next_state, session, State, {reply, From, Reply}}; 646 | no_session(EventType, Event, State) -> 647 | handle_other(EventType, Event, ?FUNCTION_NAME, State). 648 | 649 | %%-------------------------------------------------------------------- 650 | %% @private 651 | %% @doc State for when a session has been started 652 | %% @end 653 | %%-------------------------------------------------------------------- 654 | -spec session(EventType :: gen_statem:event_type(), 655 | EventContent :: term(), 656 | State :: #state{}) -> 657 | gen_statem:event_handler_result(state_name()). 658 | session({call, From}, {start_session, MockMFAs, WatchMFAs, Pid}, State0) -> 659 | State = enqueue_session({From, MockMFAs, WatchMFAs, Pid}, State0), 660 | {keep_state, State}; 661 | session({call, From}, end_session, State0) -> 662 | {NextStateName, State1} = i_end_session_and_possibly_dequeue(State0), 663 | {next_state, NextStateName, State1, {reply, From, ok}}; 664 | session({call, From}, {start_session_element, Pid}, State0) -> 665 | {Reply, State} = i_start_session_element(Pid, State0), 666 | {next_state, session_element, State, {reply, From, Reply}}; 667 | session({call, From}, {reg_call_and_get_action, _MFA}, _State) -> 668 | {keep_state_and_data, {reply, From, {ok, undefined}}}; 669 | session({call, From}, {get_action, _MFA}, _State) -> 670 | {keep_state_and_data, {reply, From, {ok, undefined}}}; 671 | session(EventType, Event, State) -> 672 | handle_other(EventType, Event, ?FUNCTION_NAME, State). 673 | 674 | %%-------------------------------------------------------------------- 675 | %% @private 676 | %% @doc State for an element of a session 677 | %% @end 678 | %%-------------------------------------------------------------------- 679 | 680 | session_element({call, From}, end_session_element, State0) -> 681 | State1 = i_end_session_element(State0), 682 | {next_state, session, State1, {reply, From, ok}}; 683 | session_element({call, From}, {reg_call_and_get_action, MFA}, State0) -> 684 | State = register_call(MFA, State0), 685 | ActionFun = i_get_action(MFA, State), 686 | {keep_state, State, {reply, From, {ok, ActionFun}}}; 687 | session_element({call, From}, {get_action, MFA}, State) -> 688 | ActionFun = i_get_action(MFA, State), 689 | {keep_state_and_data, {reply, From, {ok, ActionFun}}}; 690 | session_element({call, From}, {set_action, MFA, Opts}, State0) -> 691 | {Reply, State} = i_set_action(MFA, Opts, State0), 692 | {keep_state, State, {reply, From, Reply}}; 693 | session_element({call, From}, 694 | {verify, MFA, {was_called, Criteria}, Opts}, 695 | State) -> 696 | Reply = get_and_check_matches(MFA, Criteria, State), 697 | {keep_state_and_data, {reply, From, possibly_add_location(Reply, Opts)}}; 698 | session_element({call, From}, 699 | {verify, MFA, {wait_called, Criteria}, Opts}, 700 | State) -> 701 | case get_and_check_matches(MFA, Criteria, State) of 702 | {ok, _} = Reply -> 703 | {keep_state_and_data, {reply, From, Reply}}; 704 | {error, {fewer_calls_than_expected, _, _}} -> 705 | %% It only makes sense to enqueue waiters if their 706 | %% criteria is not yet fulfilled - at least there's a 707 | %% chance it might actually happen. 708 | Waiters = State#state.call_waiters, 709 | Waiter = #call_waiter{from=From, mfa=MFA, crit=Criteria, 710 | loc=proplists:get_value(location, Opts)}, 711 | {keep_state, State#state{call_waiters = [Waiter|Waiters]}}; 712 | {error, _} = Error -> 713 | %% Fail directly if the waiter's criteria can never be 714 | %% fulfilled, if the criteria syntax was bad, etc. 715 | Reply = possibly_add_location(Error, Opts), 716 | {keep_state_and_data, {reply, From, Reply}} 717 | end; 718 | session_element({call, From}, {verify, MFA, num_calls, _Opts}, State) -> 719 | Matches = get_matches(MFA, State), 720 | {keep_state_and_data, {reply, From, {ok, length(Matches)}}}; 721 | session_element({call, From}, {verify, MFA, get_calls, _Opts}, State) -> 722 | Matches = get_matches(MFA, State), 723 | {keep_state_and_data, {reply, From, {ok, Matches}}}; 724 | session_element({call, From}, {verify, MFA, forget_when, _Opts}, State0) -> 725 | State = i_forget_action(MFA, State0), 726 | {keep_state, State, {reply, From, ok}}; 727 | session_element({call, From}, {verify, MFA, forget_calls, _Opts}, State0) -> 728 | State = remove_matching_calls(MFA, State0), 729 | {keep_state, State, {reply, From, ok}}; 730 | session_element({call, From}, forget_all_calls, State) -> 731 | {keep_state, State#state{calls=[]}, {reply, From, ok}}; 732 | session_element(EventType, Event, State) -> 733 | handle_other(EventType, Event, ?FUNCTION_NAME, State). 734 | 735 | %%-------------------------------------------------------------------- 736 | 737 | handle_other({call, From}, stop, _StateName, _State) -> 738 | {stop_and_reply, normal, {reply, From, ok}}; 739 | handle_other({call, From}, _Other, no_session, _State) -> 740 | {keep_state_and_data, {reply, From, {error, mocking_not_started}}}; 741 | handle_other({call, From}, _Other, session, _State) -> 742 | {keep_state_and_data, {reply, From, {error, mocking_not_started}}}; 743 | handle_other({call, From}, Req, _StateName, _State) -> 744 | {keep_state_and_data, {reply, From, {error, {invalid_request, Req}}}}; 745 | handle_other(info, #'DOWN'{mref=MRef}, StateName, 746 | #state{session_mref=SnMRef, 747 | session_element_mref=ElemMRef, 748 | call_waiters=Waiters, 749 | calls=Calls}=State0) when MRef == SnMRef; 750 | MRef == ElemMRef -> 751 | %% The test died before it got a chance to clean up after itself. 752 | %% Check whether there are any pending waiters. If so, just print 753 | %% the calls we've logged so far. Hopefully that helps in 754 | %% debugging. This is probably the best we can accomplish -- being 755 | %% able to fail the eunit test would be nice. Another day perhaps. 756 | {NextStateName, State} = 757 | if MRef == ElemMRef, StateName == session_element -> 758 | possibly_print_call_waiters(Waiters, Calls), 759 | {session, i_end_session_element(State0)}; 760 | MRef == SnMRef, StateName == session -> 761 | i_end_session_and_possibly_dequeue(State0); 762 | MRef == SnMRef, StateName == session_element -> 763 | possibly_print_call_waiters(Waiters, Calls), 764 | State1 = i_end_session_element(State0), 765 | i_end_session_and_possibly_dequeue(State1) 766 | end, 767 | {next_state, NextStateName, State}; 768 | handle_other(info, {trace, _, call, MFA}, _StateName, State0) -> 769 | State = register_call(MFA, State0), 770 | {keep_state, State}; 771 | handle_other(info, Info, _StateName, _State) -> 772 | io:format(user, "~p got message: ~p~n", [?MODULE, Info]), 773 | keep_state_and_data; 774 | handle_other(_EventType, _Event, _StateName, _State) -> 775 | keep_state_and_data. 776 | 777 | is_within_session(#state{session_mref=MRef}) -> MRef =/= undefined. 778 | 779 | enqueue_session(Session, #state{session_waiters=Waiters}=State) -> 780 | State#state{session_waiters=queue:in(Session, Waiters)}. 781 | 782 | possibly_dequeue_session(#state{session_waiters=Waiters0}=State0) -> 783 | case queue:out(Waiters0) of 784 | {{value, {From, MockMFAs, WatchMFAs, Pid}}, Waiters} -> 785 | {Reply, State} = i_start_session(MockMFAs, WatchMFAs, Pid, State0), 786 | gen_statem:reply(From, Reply), 787 | State#state{session_waiters=Waiters}; 788 | {empty, _} -> 789 | State0 790 | end. 791 | 792 | possibly_print_call_waiters([], _Calls) -> 793 | ok; 794 | possibly_print_call_waiters(Waiters, Calls) -> 795 | io:format(user, 796 | "Test died while waiting for a call.~n~n" 797 | "~s~n", 798 | [[fmt_waiter_calls(Waiter, Calls) || Waiter <- Waiters]]). 799 | 800 | fmt_waiter_calls(#call_waiter{mfa={WaitM,WaitF,WaitA0}, loc={File,Line}}=Waiter, 801 | Calls) -> 802 | {arity, WaitA} = erlang:fun_info(WaitA0, arity), 803 | CandMFAs = get_sorted_candidate_mfas(Waiter), 804 | CallMFAs = get_sorted_calls_similar_to_waiter(Waiter, Calls), 805 | lists:flatten( 806 | [f("~s:~p:~n Waiter: ~p:~p/~p~n~n", [File, Line, WaitM, WaitF, WaitA]), 807 | case CandMFAs of 808 | [] -> 809 | f(" Unfortunately there are no similar functions~n", []); 810 | [{WaitM, WaitF, WaitA}] -> 811 | ""; 812 | _ -> 813 | f(" Did you intend to verify one of these functions?~n" 814 | "~s~n", 815 | [fmt_candidate_mfas(CandMFAs, 8)]) 816 | end, 817 | case CallMFAs of 818 | [] -> f(" Unfortunately there are no registered calls~n", []); 819 | _ -> f(" Registered calls in order of decreasing similarity:~n" 820 | "~s~n", 821 | [fmt_calls(CallMFAs, 8)]) 822 | end, 823 | f("~n", [])]). 824 | 825 | fmt_calls(Calls, Indent) -> 826 | string:join([fmt_call(Call, Indent) || Call <- Calls], ",\n"). 827 | 828 | fmt_call(#call{m=M, f=F, a=As}, Indent) -> 829 | %% This is a crude way of pretty printing the MFA, in a way that 830 | %% both literals and non-literals in As are printed. Example: 831 | %% 832 | %% Input: 833 | %% 834 | %% #call{m = mockgyver_dummy, 835 | %% f = return_arg, 836 | %% a = [fun() -> ok end, 1, "abc", #{f=>100}, lists:seq(1,100)] 837 | %% 838 | %% Output: 839 | %% 840 | %% mockgyver_dummy:return_arg([#Fun,1,"abc", 841 | %% #{f => 100}, 842 | %% [1,2,3,4,5,6,7,8,9,10,11,12,13|...]]) 843 | %% ^^^^^^^^--- this is the indent 844 | IndentStr = string:chars($\s, Indent), 845 | %% This is all the text up to, but not including, the first "(" 846 | Preamble = io_lib:format("~s~p:~p", [IndentStr, M, F]), 847 | PreambleLen = string:length(Preamble), 848 | %% This is all the arguments pretty-printed. Since they're in a 849 | %% list and that will also be included in the output, strip the 850 | %% leading "[" and trailing "]" from the output. 851 | FmtStr = f("~~~p.~pP", [_LineLength=80, _ArgIdent=PreambleLen + 1]), 852 | AsStr0 = f(FmtStr, [As, _Depth=20]), 853 | AsStr = string:sub_string(AsStr0, 2, string:length(AsStr0)-1), 854 | %% Crudeness is done 855 | f("~s(~s)", [Preamble, AsStr]). 856 | 857 | get_sorted_calls_similar_to_waiter(#call_waiter{}=Waiter, Calls) -> 858 | ResemCalls0 = calc_resemblance_for_calls(Waiter, Calls), 859 | ResemCalls1 = [ResemCall || {Resem, #call{}}=ResemCall <- ResemCalls0, 860 | Resem =< ?cand_resem_threshold], 861 | ResemCalls = lists:sort(fun({Resem1, #call{}}, {Resem2, #call{}}) -> 862 | Resem1 =< Resem2 863 | end, 864 | ResemCalls1), 865 | [Call || {_Resem, #call{}=Call} <- ResemCalls]. 866 | 867 | calc_resemblance_for_calls(#call_waiter{mfa={WaitM,WaitF,WaitA0}}, Calls) -> 868 | {arity, WaitA} = erlang:fun_info(WaitA0, arity), 869 | [{calc_mfa_resemblance({WaitM,WaitF,WaitA}, {CallM,CallF,length(CallA)}), 870 | Call}|| 871 | #call{m=CallM, f=CallF, a=CallA}=Call <- Calls]. 872 | 873 | fmt_candidate_mfas(CandMFAs, Indent) -> 874 | [string:chars($\s, Indent) ++ f("~p:~p/~p~n", [CandM, CandF, CandA]) || 875 | {CandM, CandF, CandA} <- CandMFAs]. 876 | 877 | get_sorted_candidate_mfas(#call_waiter{mfa={WaitM,WaitF,WaitA0}}=Waiter) -> 878 | {arity, WaitA} = erlang:fun_info(WaitA0, arity), 879 | WaitMFA = {WaitM, WaitF, WaitA}, 880 | CandMFAs = lists:sort(fun({Resem1, _CandMFA1}, {Resem2, _CandMFA2}) -> 881 | Resem1 =< Resem2 882 | end, 883 | get_candidate_mfas_aux(get_candidate_modules(Waiter), 884 | WaitMFA)), 885 | [CandMFA || {_Resem, CandMFA} <- CandMFAs]. 886 | 887 | get_candidate_mfas_aux([CandM | CandMs], WaitMFA) -> 888 | get_candidate_mfas_by_module(CandM, WaitMFA) 889 | ++ get_candidate_mfas_aux(CandMs, WaitMFA); 890 | get_candidate_mfas_aux([], _WaitMFA) -> 891 | []. 892 | 893 | get_candidate_mfas_by_module(CandM, WaitMFA) -> 894 | CandFAs = CandM:module_info(exports), 895 | lists:foldl( 896 | fun(CandMFA, CandMFAs) -> 897 | %% Only include similar MFAs 898 | case calc_mfa_resemblance(WaitMFA, CandMFA) of 899 | Resem when Resem =< ?cand_resem_threshold -> 900 | [{Resem, CandMFA} | CandMFAs]; 901 | _Resem -> 902 | CandMFAs 903 | end 904 | end, 905 | [], 906 | [{CandM, CandF, CandA} || {CandF, CandA} <- CandFAs]). 907 | 908 | %% Return a list of all loaded modules which are similar 909 | get_candidate_modules(#call_waiter{mfa={WaitM, _WaitF, _WaitA}}) -> 910 | [CandM || {CandM, _Loaded} <- code:all_loaded(), 911 | calc_atom_resemblance(WaitM, CandM) =< ?cand_resem_threshold, 912 | not is_renamed_module(CandM)]. 913 | 914 | is_renamed_module(M) -> 915 | lists:suffix("^", atom_to_list(M)). 916 | 917 | renamed_module_name(Mod) -> 918 | list_to_atom(atom_to_list(Mod)++"^"). 919 | 920 | %% Calculate a positive integer which corresponds to the similarity 921 | %% between two MFAs. Returns 0 when they are equal. 922 | calc_mfa_resemblance({M1, F1, A1}, {M2, F2, A2}) -> 923 | calc_atom_resemblance(M1, M2) + calc_atom_resemblance(F1, F2) + abs(A1-A2). 924 | 925 | calc_atom_resemblance(A1, A2) -> 926 | calc_levenshtein_dist(atom_to_list(A1), 927 | atom_to_list(A2)). 928 | 929 | %%-------------------------------------------------------------------- 930 | %% @private 931 | %% @doc 932 | %% This function is called by a gen_statem when it is about to 933 | %% terminate. It should be the opposite of Module:init/1 and do any 934 | %% necessary cleaning up. When it returns, the gen_statem terminates with 935 | %% Reason. The return value is ignored. 936 | %% @end 937 | %%-------------------------------------------------------------------- 938 | -spec terminate(Reason :: term(), 939 | StateName :: state_name(), 940 | State :: #state{}) -> term(). 941 | terminate(_Reason, _StateName, State) -> 942 | i_end_session(State), % ensure mock modules are unloaded when terminating 943 | destroy_mod_cache(), 944 | ok. 945 | 946 | %%-------------------------------------------------------------------- 947 | %% @private 948 | %% @doc 949 | %% Convert process state when code is changed 950 | %% @end 951 | %%-------------------------------------------------------------------- 952 | -spec code_change(OldVsn :: term(), 953 | StateName :: state_name(), 954 | State :: #state{}, 955 | Extra :: term()) -> 956 | {ok, 957 | NewStateName :: state_name(), 958 | NewState :: #state{}} | 959 | term(). 960 | code_change(_OldVsn, StateName, State, _Extra) -> 961 | {ok, StateName, State}. 962 | 963 | %%%=================================================================== 964 | %%% Internal functions 965 | %%%=================================================================== 966 | 967 | sync_send_event(Msg) -> 968 | ensure_server_started(), 969 | gen_statem:call(?SERVER, Msg). 970 | 971 | ensure_server_started() -> 972 | case whereis(?SERVER) of 973 | undefined -> 974 | ok = ensure_application_started(); 975 | P when is_pid(P) -> 976 | ok 977 | end. 978 | 979 | i_start_session(MockMFAs, WatchMFAs, Pid, State0) -> 980 | State1 = State0#state{mock_mfas=MockMFAs, watch_mfas=WatchMFAs}, 981 | Modinfos = mock_and_load_mods(MockMFAs), 982 | State = State1#state{init_modinfos=Modinfos}, 983 | MRef = erlang:monitor(process, Pid), 984 | {ok, State#state{session_mref=MRef}}. 985 | 986 | i_start_session_element(Pid, 987 | #state{mock_mfas=MockMFAs, 988 | watch_mfas=WatchMFAs}=State) -> 989 | possibly_shutdown_old_tracer(), 990 | erlang:trace(all, true, [call, {tracer, self()}]), 991 | %% We mustn't trace non-mocked modules, since we'll register 992 | %% calls for those as part of reg_call_and_get_action. If we 993 | %% did, we'd get double the amount of calls. 994 | MockMods = get_unique_mods_by_mfas(MockMFAs), 995 | TraceMFAs = get_trace_mfas(WatchMFAs, MockMods), 996 | case setup_trace_on_all_mfas(TraceMFAs) of 997 | ok -> 998 | MRef = erlang:monitor(process, Pid), 999 | {ok, State#state{calls=[], session_element_mref=MRef}}; 1000 | {error, _}=Error -> 1001 | {Error, i_end_session_element(State)} 1002 | end. 1003 | 1004 | possibly_shutdown_old_tracer() -> 1005 | %% The problem here is that a process may only be traced by one 1006 | %% and only one other process. We need the traces to record what 1007 | %% happens for the validation afterwards. One could perhaps 1008 | %% design a complicated trace relay, but at least for the time 1009 | %% being we stop the current tracer (if any) and add ourselves as 1010 | %% the sole tracer. 1011 | case get_orig_tracer_info() of 1012 | {_Tracer, Flags} -> 1013 | %% One could warn the user about this happening, but 1014 | %% what's a good way of doing that? 1015 | %% 1016 | %% * error_logger:info_msg/warning_msg is always shown 1017 | %% ==> clutters eunit results in the shell and there's 1018 | %% no way of turning that off 1019 | %% 1020 | %% * io:format(Format, Args) is only shown if an eunit 1021 | %% test case fails (I think), increasing the verbosity 1022 | %% doesn't help 1023 | %% ==> bad, since one would like to see the warning at 1024 | %% least in verbose mode 1025 | %% 1026 | %% * io:format(user, Format, Args) is always shown 1027 | %% ==> see error_logger bullet above 1028 | %% 1029 | %% Just silently steal the trace. 1030 | erlang:trace(all, false, Flags); 1031 | undefined -> 1032 | ok 1033 | end. 1034 | 1035 | get_orig_tracer_info() -> 1036 | case erlang:trace_info(new, tracer) of 1037 | {tracer, []} -> 1038 | undefined; 1039 | {tracer, Tracer} -> 1040 | {flags, Flags} = erlang:trace_info(new, flags), 1041 | {Tracer, Flags} 1042 | end. 1043 | 1044 | get_trace_mfas(WatchMFAs, MockMods) -> 1045 | [{M,F,A} || {M,F,A} <- WatchMFAs, not lists:member(M, MockMods)]. 1046 | 1047 | setup_trace_on_all_mfas(MFAs) -> 1048 | lists:foldl(fun({M,_F,_A} = MFA, ok) -> 1049 | %% Ensure the module is loaded, otherwise 1050 | %% the trace_pattern won't match anything 1051 | %% and we won't get any traces. 1052 | case code:ensure_loaded(M) of 1053 | {module, _} -> 1054 | case erlang:trace_pattern(MFA, true, [local]) of 1055 | 0 -> 1056 | {error, {undef, MFA}}; 1057 | _ -> 1058 | ok 1059 | end; 1060 | {error, Reason} -> 1061 | {error, {failed_to_load_module, M, Reason}} 1062 | end; 1063 | (_MFA, {error, _} = Error) -> 1064 | Error 1065 | end, 1066 | ok, 1067 | MFAs). 1068 | 1069 | remove_trace_on_all_mfas(MFAs) -> 1070 | [erlang:trace_pattern(MFA, false, [local]) || MFA <- MFAs]. 1071 | 1072 | i_end_session(#state{session_mref=MRef, init_modinfos=Modinfos} = State) -> 1073 | restore_mods(Modinfos), 1074 | erlang:trace(all, false, [call, {tracer, self()}]), 1075 | if MRef =/= undefined -> erlang:demonitor(MRef, [flush]); 1076 | true -> ok 1077 | end, 1078 | State#state{session_mref=undefined, 1079 | mock_mfas=[], watch_mfas=[], init_modinfos=[]}. 1080 | 1081 | i_end_session_element(#state{mock_mfas=MockMFAs, watch_mfas=WatchMFAs, 1082 | session_element_mref=ElemMRef} = State) -> 1083 | MockMods = get_unique_mods_by_mfas(MockMFAs), 1084 | TraceMFAs = get_trace_mfas(WatchMFAs, MockMods), 1085 | remove_trace_on_all_mfas(TraceMFAs), 1086 | if ElemMRef =/= undefined -> erlang:demonitor(ElemMRef, [flush]); 1087 | true -> ok 1088 | end, 1089 | State#state{actions=[], calls=[], call_waiters=[], 1090 | session_element_mref=undefined}. 1091 | 1092 | i_end_session_and_possibly_dequeue(State0) -> 1093 | State1 = i_end_session(State0), 1094 | State = possibly_dequeue_session(State1), 1095 | case is_within_session(State) of 1096 | true -> {session, State}; 1097 | false -> {no_session, State} 1098 | end. 1099 | 1100 | register_call(MFA, State0) -> 1101 | State1 = store_call(MFA, State0), 1102 | possibly_notify_waiters(State1). 1103 | 1104 | store_call({M, F, A}, #state{calls=Calls} = State) -> 1105 | State#state{calls=[#call{m=M, f=F, a=A} | Calls]}. 1106 | 1107 | possibly_notify_waiters(#state{call_waiters=Waiters0} = State) -> 1108 | Waiters = 1109 | lists:filter(fun(#call_waiter{from=From, mfa=MFA, crit=Criteria}) -> 1110 | case get_and_check_matches(MFA, Criteria, State) of 1111 | {ok, _} = Reply -> 1112 | gen_statem:reply(From, Reply), 1113 | false; % remove from waiting list 1114 | {error, _} -> 1115 | true % keep in waiting list 1116 | end 1117 | end, 1118 | Waiters0), 1119 | State#state{call_waiters=Waiters}. 1120 | 1121 | get_and_check_matches(ExpectMFA, Criteria, State) -> 1122 | Matches = get_matches(ExpectMFA, State), 1123 | case check_criteria(Criteria, length(Matches)) of 1124 | ok -> 1125 | {ok, Matches}; 1126 | {error, _} = Error -> 1127 | Error 1128 | end. 1129 | 1130 | get_matches({_M, _F, _A}=ExpectMFA, #state{calls=Calls}) -> 1131 | lists:foldl(fun(#call{m=M0, f=F0, a=A0}, Matches) -> 1132 | case is_match({M0,F0,A0}, ExpectMFA) of 1133 | true -> [A0 | Matches]; 1134 | false -> Matches 1135 | end 1136 | end, 1137 | [], 1138 | Calls). 1139 | 1140 | remove_matching_calls({_M, _F, _A} = ExpectMFA, #state{calls=Calls0}=State) -> 1141 | Calls = lists:filter(fun(#call{m=M0, f=F0, a=A0}) -> 1142 | not is_match({M0,F0,A0}, ExpectMFA) 1143 | end, 1144 | Calls0), 1145 | State#state{calls=Calls}. 1146 | 1147 | is_match({CallM,CallF,CallA}, {ExpectM,ExpectF,ExpectA}) when CallM==ExpectM, 1148 | CallF==ExpectF -> 1149 | try 1150 | apply(ExpectA, CallA), 1151 | true 1152 | catch 1153 | error:function_clause -> % when guards don't match 1154 | false; 1155 | error:{badarity, _} -> % when arity doesn't match 1156 | false; 1157 | error:{badmatch, _} -> % when previously bound vars don't match 1158 | false 1159 | end; 1160 | is_match(_CallMFA, _ExpectMFA) -> 1161 | false. 1162 | 1163 | %% @private 1164 | check_criteria(Criteria, N) -> 1165 | case check_criteria_syntax(Criteria) of 1166 | ok -> check_criteria_value(Criteria, N); 1167 | {error, _}=Error -> Error 1168 | end. 1169 | 1170 | check_criteria_syntax(once) -> ok; 1171 | check_criteria_syntax({at_least, N}) when is_integer(N) -> ok; 1172 | check_criteria_syntax({at_most, N}) when is_integer(N) -> ok; 1173 | check_criteria_syntax({times, N}) when is_integer(N) -> ok; 1174 | check_criteria_syntax(never) -> ok; 1175 | check_criteria_syntax(Criteria) -> 1176 | {error, {invalid_criteria, Criteria}}. 1177 | 1178 | check_criteria_value(once, 1) -> ok; 1179 | check_criteria_value({at_least, N}, X) when X >= N -> ok; 1180 | check_criteria_value({at_most, N}, X) when X =< N -> ok; 1181 | check_criteria_value({times, N}, N) -> ok; 1182 | check_criteria_value(never, 0) -> ok; 1183 | check_criteria_value(Criteria, N) -> 1184 | Reason = case classify_relation_to_target_value(Criteria, N) of 1185 | fewer -> fewer_calls_than_expected; 1186 | more -> more_calls_than_expected 1187 | end, 1188 | {error, {Reason, {expected, Criteria}, {actual, N}}}. 1189 | 1190 | classify_relation_to_target_value(once, X) when X < 1 -> fewer; 1191 | classify_relation_to_target_value(once, X) when X > 1 -> more; 1192 | classify_relation_to_target_value({at_least, N}, X) when X < N -> fewer; 1193 | classify_relation_to_target_value({at_most, N}, X) when X > N -> more; 1194 | classify_relation_to_target_value({times, N}, X) when X < N -> fewer; 1195 | classify_relation_to_target_value({times, N}, X) when X > N -> more; 1196 | classify_relation_to_target_value(never, X) when X < 0 -> fewer; 1197 | classify_relation_to_target_value(never, X) when X > 0 -> more. 1198 | 1199 | i_get_action({M,F,Args}, #state{actions=Actions}) -> 1200 | A = length(Args), 1201 | case lists:keysearch({M,F,A}, #action.mfa, Actions) of 1202 | {value, #action{func=ActionFun}} -> ActionFun; 1203 | false -> undefined 1204 | end. 1205 | 1206 | i_set_action({M, F, ActionFun}, _Opts, #state{actions=Actions0} = State) -> 1207 | {arity, A} = erlang:fun_info(ActionFun, arity), 1208 | MFA = {M, F, A}, 1209 | case erlang:is_builtin(M, F, A) of 1210 | true -> 1211 | {{error, {cannot_mock_bif, MFA}}, State}; 1212 | false -> 1213 | Actions = lists:keystore(MFA, #action.mfa, Actions0, 1214 | #action{mfa=MFA, func=ActionFun}), 1215 | {ok, State#state{actions=Actions}} 1216 | end. 1217 | 1218 | i_forget_action({M, F, ActionFun}, #state{actions=Actions0} = State) -> 1219 | {arity, A} = erlang:fun_info(ActionFun, arity), 1220 | MFA = {M, F, A}, 1221 | Actions = lists:keydelete(MFA, #action.mfa, Actions0), 1222 | State#state{actions=Actions}. 1223 | 1224 | wait_until_trace_delivered() -> 1225 | Ref = erlang:trace_delivered(all), 1226 | receive {trace_delivered, _, Ref} -> ok end. 1227 | 1228 | chk(ok) -> ok; 1229 | chk({ok, Value}) -> Value; 1230 | chk({error, Reason}) -> erlang:error(Reason); 1231 | chk({error, Reason, Location}) -> erlang:error({{reason, Reason}, 1232 | {location, Location}}). 1233 | 1234 | possibly_add_location({error, Reason}, Opts) -> 1235 | case proplists:get_value(location, Opts) of 1236 | undefined -> {error, Reason}; 1237 | Location -> {error, Reason, Location} 1238 | end; 1239 | possibly_add_location({ok, _}=OkRes, _Opts) -> 1240 | OkRes. 1241 | 1242 | 1243 | mock_and_load_mods(MFAs) -> 1244 | %% General strategy: 1245 | %% 1246 | %% Do as much in over lists of modules as possible, 1247 | %% using such functions in the code module, since this is somewhat 1248 | %% faster on average. 1249 | %% 1250 | %% Unloading a module can take time due to gc of literal data, 1251 | %% so do as few such operations as possibly needed. 1252 | %% Avoid looking for modules in the code path, cache such things, 1253 | %% to speed things up when the code path is long. 1254 | 1255 | ModsFAs = group_fas_by_mod(MFAs), 1256 | {Mods, ModFAs} = lists:unzip(ModsFAs), 1257 | %% We will have to try to load any missing modules in order 1258 | %% to be able to mock them. So we might as well try to load 1259 | %% all modules we will need, under the assumption that including 1260 | %% an already loaded module is cheap. 1261 | %% Assume loading of some may potentially fail. 1262 | code:ensure_modules_loaded(Mods), 1263 | [ok = possibly_unstick_mod(Mod) || Mod <- Mods], 1264 | ModinfosWithCacheModDeltas = par_map(fun collect_init_modinfo/1, Mods), 1265 | MockMods = lists:append( 1266 | par_map(fun({FAs, {Modinfo, CacheModDelta}}) -> 1267 | mock_mod(FAs, Modinfo, CacheModDelta) 1268 | end, 1269 | lists:zip(ModFAs, ModinfosWithCacheModDeltas))), 1270 | ok = load_mods([{Mod, "mock", Code} || {Mod, Code} <- MockMods]), 1271 | {Modinfos, _CacheModDeltas} = lists:unzip(ModinfosWithCacheModDeltas), 1272 | Modinfos. 1273 | 1274 | -spec collect_init_modinfo(module()) -> {Modinfo, CacheModDelta} when 1275 | Modinfo :: #modinfo{} | #nomodinfo{}, 1276 | CacheModDelta :: cache_up_to_date | cache_invalidated | cache_updated. 1277 | collect_init_modinfo(Mod) -> 1278 | %% At this point it is assumed that Mod is loaded, if it existed on disk. 1279 | %% 1280 | %% #modinfo{} records get cached into the ?CACHE_TAB. #nomodinfo{} do not. 1281 | case ets:lookup(?CACHE_TAB, ?modinfo_key(Mod)) of 1282 | [#modinfo{key=Key, checksum=CachedCSum, filename=Filename}=Modinfo] -> 1283 | %% Check if the modinfo known to be up-to-date, 1284 | %% otherwise invalidate the entry. 1285 | %% 1286 | %% Reading the checksum from file is faster than loading the 1287 | %% module to ask it, even though that implies parsing some chunks. 1288 | case erlang:module_loaded(Mod) of 1289 | true -> 1290 | LoadedModChecksumMatchesCached = 1291 | get_module_checksum(Mod) =:= CachedCSum, 1292 | BeamChecksumOnDiskMatchesCached = 1293 | get_file_checksum(Filename) =:= CachedCSum, 1294 | if LoadedModChecksumMatchesCached, 1295 | BeamChecksumOnDiskMatchesCached -> 1296 | {Modinfo, cache_up_to_date}; 1297 | true -> 1298 | update_modinfo_cache_from_disk(Modinfo) 1299 | end; 1300 | false -> 1301 | ets:delete(?CACHE_TAB, Key), 1302 | {#nomodinfo{key=Key}, cache_invalidated} 1303 | end; 1304 | [] -> 1305 | case erlang:module_loaded(Mod) of 1306 | true -> 1307 | update_modinfo_cache_from_loaded_mod(Mod); 1308 | false -> 1309 | {#nomodinfo{key=?modinfo_key(Mod)}, cache_up_to_date} 1310 | end 1311 | end. 1312 | 1313 | update_modinfo_cache_from_loaded_mod(Mod) -> 1314 | {ok, FAs} = get_exported_fas(Mod), 1315 | case get_code(Mod) of 1316 | {ok, {Code, Filename}} -> 1317 | Checksum = get_module_checksum(Mod), 1318 | Modinfo = #modinfo{key=?modinfo_key(Mod), 1319 | exported_fas=FAs, 1320 | code=Code, 1321 | filename=Filename, 1322 | checksum=Checksum}, 1323 | ets:insert(?CACHE_TAB, Modinfo), 1324 | {Modinfo, cache_updated}; 1325 | error -> 1326 | {#nomodinfo{key=?modinfo_key(Mod)}, cache_up_to_date} 1327 | end. 1328 | 1329 | update_modinfo_cache_from_disk(#modinfo{key=?modinfo_key(Mod)=Key, 1330 | filename=Filename}=Modinfo0) -> 1331 | case file:read_file(Filename) of 1332 | {ok, Code} -> 1333 | {ok, {Mod, [{exports, FAs}]}} = 1334 | beam_lib:chunks(Code, [exports]), 1335 | Checksum = beam_lib:md5(Code), 1336 | Modinfo1 = Modinfo0#modinfo{key=?modinfo_key(Mod), 1337 | exported_fas=filter_fas(FAs), 1338 | code=Code, 1339 | checksum=Checksum}, 1340 | ets:insert(?CACHE_TAB, Modinfo1), 1341 | {Modinfo1, cache_updated}; 1342 | {error, _} -> 1343 | ets:delete(?CACHE_TAB, Key), 1344 | {#nomodinfo{key=?modinfo_key(Mod)}, cache_invalidated} 1345 | end. 1346 | 1347 | get_code(Mod) -> 1348 | %% It should be loaded already, if it exists on disk, so ask. 1349 | %% But if code paths have changed, it might not be available any more. 1350 | case code:is_loaded(Mod) of 1351 | false -> 1352 | error; 1353 | {file, preloaded} -> 1354 | error; 1355 | {file, cover_compiled} -> 1356 | error; 1357 | {file, Filename} -> 1358 | case file:read_file(Filename) of 1359 | {ok, Bin} -> 1360 | {ok, {Bin, Filename}}; 1361 | {error, _} -> 1362 | error 1363 | end 1364 | end. 1365 | 1366 | mock_mod(UserAddedFAs, 1367 | #modinfo{key=?modinfo_key(Mod), exported_fas=ExportedFAs, 1368 | checksum=Checksum}=Modinfo, 1369 | CacheModDelta) -> 1370 | RenamedMod = renamed_module_name(Mod), % module -> module^ 1371 | Renamed = ensure_renamed_mod_to_load(RenamedMod, Modinfo, CacheModDelta), 1372 | FAs = get_non_bif_fas(Mod, lists:usort(ExportedFAs++UserAddedFAs)), 1373 | case retrieve_mocking_mod(Mod, Checksum) of 1374 | {ok, MockingMod} -> 1375 | [MockingMod] ++ Renamed; 1376 | undefined -> 1377 | MockingMod = mk_mocking_mod(Mod, RenamedMod, FAs), 1378 | store_mocking_mod(MockingMod, Checksum), 1379 | [MockingMod] ++ Renamed 1380 | end; 1381 | mock_mod(UserAddedFAs, #nomodinfo{key=?modinfo_key(Mod)}, _CacheDeltaInfo) -> 1382 | [mk_new_mod(Mod, UserAddedFAs)]. 1383 | 1384 | load_mods(Modules) -> 1385 | [code:purge(Mod) || {Mod, _Filename, _Code} <- Modules], 1386 | load_mods_aux(Modules). 1387 | 1388 | load_mods_aux(Modules) -> 1389 | case code:atomic_load(Modules) of 1390 | ok -> 1391 | ok; 1392 | {error, ModReasons} -> 1393 | %% possible reasons could be on_load_not_allowed, load those 1394 | %% individually 1395 | {NoErrorMods, OnLoadMods} = 1396 | lists:partition( 1397 | fun({Mod, _, _}) -> 1398 | case lists:keyfind(Mod, 1, ModReasons) of 1399 | false -> 1400 | true; 1401 | {Mod, on_load_not_allowed} -> 1402 | false; 1403 | {Mod, Other} -> 1404 | error({unexpected_atomic_load_fail, 1405 | Mod, Other}) 1406 | end 1407 | end, 1408 | Modules), 1409 | ok = load_mods_aux(NoErrorMods), 1410 | [{module, Mod} = code:load_binary(Mod, Filename, Code) 1411 | || {Mod, Filename, Code} <- OnLoadMods], 1412 | ok 1413 | end. 1414 | 1415 | get_module_checksum(Mod) -> 1416 | try 1417 | %% This macro was introduced in Erlang/OTP 18.0. 1418 | Mod:module_info(md5) 1419 | catch 1420 | error:badarg -> 1421 | %% This is a workaround for older releases. 1422 | {ok, {_Mod, Md5}} = beam_lib:md5(code:which(Mod)), 1423 | Md5 1424 | end. 1425 | 1426 | get_file_checksum(Filename) -> 1427 | case beam_lib:md5(Filename) of 1428 | {ok, {_Mod, Checksum}} -> 1429 | Checksum; 1430 | {error, beam_lib, Reason} -> 1431 | {error, Reason} 1432 | end. 1433 | 1434 | create_mod_cache() -> 1435 | ets:new(?CACHE_TAB, [named_table, {keypos,2}, public]). 1436 | 1437 | store_mocking_mod({Mod, Bin}, Hash) -> 1438 | true = ets:insert_new(?CACHE_TAB, 1439 | #mocking_mod{key=?mocking_key(Mod, Hash), 1440 | code=Bin}). 1441 | 1442 | retrieve_mocking_mod(Mod, Hash) -> 1443 | case ets:lookup(?CACHE_TAB, ?mocking_key(Mod, Hash)) of 1444 | [] -> 1445 | undefined; 1446 | [#mocking_mod{code=Bin}] -> 1447 | {ok, {Mod, Bin}} 1448 | end. 1449 | 1450 | destroy_mod_cache() -> 1451 | ets:delete(?CACHE_TAB). 1452 | 1453 | possibly_unstick_mod(Mod) -> 1454 | case code:is_sticky(Mod) of 1455 | true -> 1456 | case code:which(Mod) of 1457 | Filename when is_list(Filename) -> 1458 | case code:unstick_dir(filename:dirname(Filename)) of 1459 | ok -> 1460 | ok; 1461 | error -> 1462 | erlang:error({failed_to_unstick_module, Mod}) 1463 | end; 1464 | Other -> 1465 | erlang:error({failed_to_unstick_module, Mod, 1466 | {code_which_output, Other}}) 1467 | end; 1468 | false -> 1469 | ok 1470 | end. 1471 | 1472 | ensure_renamed_mod_to_load(RenamedMod, Modinfo, CacheModDelta) -> 1473 | case CacheModDelta of 1474 | cache_up_to_date -> 1475 | %% Will normally not be needed unless the renamed mod^ was 1476 | %% unloaded by someone else with between or during tests. 1477 | %% It is cheap when nothing needs to be done, though. 1478 | %% Assume nobody modifies it in between though. 1479 | ensure_renamed_mod_to_load_aux(RenamedMod, Modinfo); 1480 | cache_updated -> 1481 | unload_mod(RenamedMod), 1482 | ensure_renamed_mod_to_load_aux(RenamedMod, Modinfo) 1483 | end. 1484 | 1485 | ensure_renamed_mod_to_load_aux(RenamedMod, #modinfo{code=Code}) -> 1486 | case erlang:module_loaded(RenamedMod) of 1487 | true -> 1488 | []; 1489 | false -> 1490 | RenamedCode = rename(Code, RenamedMod), 1491 | [{RenamedMod, RenamedCode}] 1492 | end. 1493 | 1494 | mk_mocking_mod(Mod, RenamedMod, ExportedFAs) -> 1495 | FmtNoAction = 1496 | fun(FnName, Args) -> 1497 | f("apply(~p, ~s, ~s)", [RenamedMod, FnName, Args]) 1498 | end, 1499 | mk_mod(Mod, mk_mock_impl_functions(Mod, ExportedFAs, FmtNoAction)). 1500 | 1501 | mk_new_mod(Mod, ExportedFAs) -> 1502 | FmtNoAction = 1503 | fun(FnName, Args) -> 1504 | f("error_handler:raise_undef_exception(~p, ~s, ~s)", 1505 | [Mod, FnName, Args]) 1506 | end, 1507 | mk_mod(Mod, mk_mock_impl_functions(Mod, ExportedFAs, FmtNoAction)). 1508 | 1509 | mk_mock_impl_functions(Mod, ExportedFAs, FmtNoAction) -> 1510 | [mk_handle_undefined_function(Mod, ExportedFAs, FmtNoAction), 1511 | mk_externalize_stack_trace_function(Mod), 1512 | mk_fun_to_inlined_function(), 1513 | mk_fun_to_inlined_r_function(), 1514 | mk_filter_st_function(Mod), 1515 | mk_map_st_function()]. 1516 | 1517 | mk_handle_undefined_function(Mod, ExportedFAs, FmtNoAction) -> 1518 | %% Parsing the string is approx 20% slower than constructing 1519 | %% the syntax tree using erl_syntax calls. 1520 | %% The string version is easier to understand though. 1521 | %% 1522 | %% It is many times faster than constructing a number of functions, 1523 | %% each containing the inner case expression, though. 1524 | func_from_str_fmt( 1525 | "'$handle_undefined_function'(FnName, Args) -> 1526 | Arity = length(Args), 1527 | case lists:member({FnName, Arity}, ~p) of 1528 | true -> 1529 | case mockgyver:reg_call_and_get_action({~p,FnName,Args}) of 1530 | undefined -> 1531 | ~s; 1532 | ActionFun -> 1533 | try 1534 | apply(ActionFun, Args) 1535 | catch 1536 | Class:Reason:St0 -> 1537 | St = '$mockgyver_externalize_stacktrace'( 1538 | ActionFun, FnName, Arity, St0), 1539 | erlang:raise(Class, Reason, St) 1540 | end 1541 | end; 1542 | false -> 1543 | error_handler:raise_undef_exception(~p, FnName, Args) 1544 | end.", 1545 | [ExportedFAs, Mod, FmtNoAction("FnName", "Args"), Mod]). 1546 | 1547 | mk_externalize_stack_trace_function(Mod) -> 1548 | %% In case of an error, fixup the stacktrace 1549 | %% so that it looks like something the user can relate to. 1550 | %% 1551 | %% - Remove the internal '$handle_undefined_function' wrapper level. 1552 | %% 1553 | %% - Make the stacktrace refer to the mocked functions instead of 1554 | %% the anonymous fun expression that the parse transform introduces. 1555 | %% 1556 | %% In Erlang 25+, the '-x/1-fun-0-' is sometimes '-x/1-inlined-0-' in 1557 | %% stacktraces, so handle both. 1558 | func_from_str_fmt( 1559 | "'$mockgyver_externalize_stacktrace'(ActionFun, FnName, Arity, St0) -> 1560 | {module, FromM} = erlang:fun_info(ActionFun, module), 1561 | {name, FromF} = erlang:fun_info(ActionFun, name), 1562 | FromMF1 = {FromM, FromF}, 1563 | FromMF2 = {FromM, '$fun_to_inlined'(FromF)}, 1564 | ToMF = {~p, FnName}, 1565 | St1 = '$mockgyver_filter_st'(St0), 1566 | St = '$mockgyver_map_st'(FromMF1, ToMF, Arity, St1), 1567 | '$mockgyver_map_st'(FromMF2, ToMF, Arity, St).", 1568 | [Mod]). 1569 | 1570 | mk_fun_to_inlined_function() -> 1571 | func_from_str_fmt( 1572 | %% Process reversed strings, to make sure we only substitute 1573 | %% the last occurrence. In case it would be lexically nested. 1574 | "'$fun_to_inlined'(FnName) -> 1575 | list_to_atom( 1576 | lists:reverse( 1577 | '$fun_to_inlined_r'( 1578 | lists:reverse( 1579 | atom_to_list(FnName))))).", 1580 | []). 1581 | mk_fun_to_inlined_r_function() -> 1582 | func_from_str_fmt( 1583 | %% Processing reversed string: 1584 | %% fun inlined 1585 | "'$fun_to_inlined_r'(\"-nuf-\" ++ T) -> \"-denilni-\" ++ T; 1586 | '$fun_to_inlined_r'([C | T]) -> [C | '$fun_to_inlined_r'(T)]; 1587 | '$fun_to_inlined_r'(\"\") -> \"\".", 1588 | []). 1589 | 1590 | mk_filter_st_function(Mod) -> 1591 | func_from_str_fmt( 1592 | "'$mockgyver_filter_st'(Stacktrace) -> 1593 | lists:filter( 1594 | fun({~p, '$handle_undefined_function', _Arity, _Extra}) -> 1595 | false; 1596 | (_) -> 1597 | true 1598 | end, 1599 | Stacktrace).", 1600 | [Mod]). 1601 | 1602 | %% Ensure a stacktrace like the following contains the mocked module 1603 | %% and function, instead of an internal fun that the user was not 1604 | %% involved in creating. 1605 | %% 1606 | %% Test code: 1607 | %% 1608 | %% ?WHEN(mockgyver_dummy:return_arg(N) -> error(foo)), 1609 | %% mockgyver_dummy:return_arg(1), 1610 | %% 1611 | %% Before: 1612 | %% 1613 | %% Failure/Error: {error,function_clause, 1614 | %% [{mockgyver_tests, 1615 | %% '-some_test/1-fun-0-', 1616 | %% [-1], 1617 | %% [...]}, 1618 | %% ... 1619 | %% 1620 | %% After: 1621 | %% 1622 | %% Failure/Error: {error,function_clause, 1623 | %% [{mockgyver_dummy, 1624 | %% return_arg, 1625 | %% [-1], 1626 | %% [...]}, 1627 | %% ... 1628 | mk_map_st_function() -> 1629 | func_from_str_fmt( 1630 | "'$mockgyver_map_st'({FromM, FromF}, {ToM, ToF}, Arity, Stacktrace) -> 1631 | lists:map(fun({M, F, A, Extra}) 1632 | when M == FromM andalso 1633 | F == FromF andalso 1634 | (A == Arity orelse (length(A) == Arity)) -> 1635 | {ToM, ToF, A, Extra}; 1636 | (StElem) -> 1637 | StElem 1638 | end, 1639 | Stacktrace).", 1640 | []). 1641 | 1642 | func_from_str_fmt(FmtStr, Args) -> 1643 | S = lists:flatten(io_lib:format(FmtStr ++ "\n", Args)), 1644 | {ok, Tokens, _} = erl_scan:string(S), 1645 | {ok, Form} = erl_parse:parse_form(Tokens), 1646 | Form. 1647 | 1648 | mk_mod(Mod, FuncForms) -> 1649 | Forms0 = ([erl_syntax:attribute(erl_syntax:abstract(module), 1650 | [erl_syntax:abstract(Mod)])] 1651 | ++ FuncForms), 1652 | Forms = [erl_syntax:revert(Form) || Form <- Forms0], 1653 | %%io:format("--------------------------------------------------~n" 1654 | %% "~s~n", 1655 | %% [[erl_pp:form(Form) || Form <- Forms]]), 1656 | {ok, Mod, Bin} = compile:forms(Forms, [report, export_all]), 1657 | {Mod, Bin}. 1658 | 1659 | restore_mods(Modinfos) -> 1660 | %% To speed things up for next session (commonly next eunit test), 1661 | %% reload the original module instead of unloading, if possible. 1662 | load_mods([{Mod, Filename, Code} 1663 | || #modinfo{key=?modinfo_key(Mod), 1664 | code=Code, 1665 | filename=Filename} <- Modinfos]), 1666 | [unload_mod(Mod) || #nomodinfo{key=?modinfo_key(Mod)} <- Modinfos], 1667 | ok. 1668 | 1669 | unload_mod(Mod) -> 1670 | case code:is_loaded(Mod) of 1671 | {file, _} -> 1672 | code:purge(Mod), 1673 | true = code:delete(Mod); 1674 | false -> 1675 | ok 1676 | end. 1677 | 1678 | get_unique_mods_by_mfas(MFAs) -> 1679 | lists:usort([M || {M,_F,_A} <- MFAs]). 1680 | 1681 | group_fas_by_mod(MFAs) -> 1682 | ModFAs = lists:foldl(fun({M, F, A}, AccModFAs) -> 1683 | dict:append(M, {F, A}, AccModFAs) 1684 | end, 1685 | dict:new(), 1686 | MFAs), 1687 | dict:to_list(ModFAs). 1688 | 1689 | get_exported_fas(Mod) -> 1690 | try 1691 | {ok, filter_fas(Mod:module_info(exports))} 1692 | catch 1693 | error:undef -> 1694 | {error, {no_such_module, Mod}} 1695 | end. 1696 | 1697 | filter_fas(FAs) -> 1698 | [{F, A} || {F, A} <- FAs, 1699 | {F, A} =/= {module_info, 0}, 1700 | {F, A} =/= {module_info, 1}]. 1701 | 1702 | get_non_bif_fas(Mod, FAs) -> 1703 | [{F, A} || {F, A} <- FAs, not erlang:is_builtin(Mod, F, A)]. 1704 | 1705 | %% Calculate the Levenshtein distance between two strings. 1706 | %% http://en.wikipedia.org/wiki/Levenshtein_distance 1707 | %% 1708 | %% Returns 0 when the strings are identical. Returns at most a value 1709 | %% which is equal to to the length of the longest string. 1710 | %% 1711 | %% Insertions, deletions and substitutions have the same weight. 1712 | calc_levenshtein_dist(S, T) -> 1713 | calc_levenshtein_dist_t(S, T, lists:seq(0, length(S)), 0). 1714 | 1715 | %% Loop over the target string and calculate rows in the tables you'll 1716 | %% find on web pages which describe the algorithm. S is the source 1717 | %% string, T the target string, Ds0 is the list of distances for the 1718 | %% previous row and J is the base for the leftmost column. 1719 | calc_levenshtein_dist_t(S, [_|TT]=T, Ds0, J) -> 1720 | Ds = calc_levenshtein_dist_s(S, T, Ds0, [J+1], J), 1721 | calc_levenshtein_dist_t(S, TT, Ds, J+1); 1722 | calc_levenshtein_dist_t(_S, [], Ds, _J) -> 1723 | hd(lists:reverse(Ds)). 1724 | 1725 | %% Loop over the source string and calculate the columns for a 1726 | %% specific row in the tables you'll find on web pages which describe 1727 | %% the algorithm. 1728 | calc_levenshtein_dist_s([SH|ST], [TH|_]=T, [DH|DT], AccDs, PrevD) -> 1729 | NextD = if SH==TH -> DH; 1730 | true -> lists:min([PrevD+1, % deletion 1731 | hd(DT)+1, % insertion 1732 | DH+1]) % substitution 1733 | end, 1734 | calc_levenshtein_dist_s(ST, T, DT, [NextD|AccDs], NextD); 1735 | calc_levenshtein_dist_s([], _T, _Ds, AccDs, _PrevD) -> 1736 | lists:reverse(AccDs). 1737 | 1738 | 1739 | f(Format, Args) -> 1740 | lists:flatten(io_lib:format(Format, Args)). 1741 | 1742 | %%------------------------------------------------------------------- 1743 | %% Rename a module which is already compiled. 1744 | %%------------------------------------------------------------------- 1745 | 1746 | %% The idea behind `beam_renamer` is to be able to load an erlang module 1747 | %% (which is already compiled) under a different name. Normally, there's 1748 | %% an error message if one does that: 1749 | %% 1750 | %% 1> {x, Bin, _} = code:get_object_code(x). 1751 | %% {x,<<...>>,...} 1752 | %% 2> code:load_binary(y, "y.beam", Bin). 1753 | %% {error,badfile} 1754 | %% 1755 | %% =ERROR REPORT==== 8-Nov-2009::22:01:24 === 1756 | %% Loading of y.beam failed: badfile 1757 | %% 1758 | %% =ERROR REPORT==== 8-Nov-2009::22:01:24 === 1759 | %% beam/beam_load.c(1022): Error loading module y: 1760 | %% module name in object code is x 1761 | %% 1762 | %% This is where `beam_renamer` comes in handy. It'll rename the module 1763 | %% by replacing the module name *within* the beam file. 1764 | %% 1765 | %% 1> {x, Bin0, _} = code:get_object_code(x). 1766 | %% {x,<<...>>,...} 1767 | %% 2> Bin = beam_renamer:rename(Bin0, y). 1768 | %% <<...>> 1769 | %% 2> code:load_binary(y, "y.beam", Bin). 1770 | %% {module,y} 1771 | 1772 | %% In order to load a module under a different name, the module name 1773 | %% has to be changed within the beam file itself. The following code 1774 | %% snippet does just that. It's based on a specification of the beam 1775 | %% format (a fairly old one, from March 1 2000, but it seems there are 1776 | %% not changes changes which affect the code below): 1777 | %% 1778 | %% http://www.erlang.se/~bjorn/beam_file_format.html 1779 | %% 1780 | %% BEWARE of modules which refer to themselves! This is where things 1781 | %% start to become interesting... If ?MODULE is used in a function 1782 | %% call, things should be ok (the module name is replaced in the 1783 | %% function call). The same goes for a ?MODULE which stands on its 1784 | %% own in a statement (like the sole return value). But if it's 1785 | %% embedded for example within a tuple or list with only constant 1786 | %% values, it's added to the constant pool which is a separate chunk 1787 | %% within the beam file. The current code doesn't replace occurrences 1788 | %% within the constant pool. Although possible, I'll leave that for 1789 | %% later. :-) 1790 | %% 1791 | %% The rename function does two things: It replaces the first atom of 1792 | %% the atom table (since apparently that's where the module name is). 1793 | %% Since the new name may be shorter or longer than the old name, one 1794 | %% might have to adjust the length of the atom table chunk 1795 | %% accordingly. Finally it updates the top-level form size, since the 1796 | %% atom table chunk might have grown or shrunk. 1797 | %% 1798 | %% From the above beam format specification: 1799 | %% 1800 | %% This file format is based on EA IFF 85 - Standard for 1801 | %% Interchange Format Files. This "standard" is not widely used; 1802 | %% the only uses I know of is the IFF graphic file format for the 1803 | %% Amiga and Blorb (a resource file format for Interactive Fiction 1804 | %% games). Despite of this, I decided to use IFF instead of 1805 | %% inventing my of own format, because IFF is almost right. 1806 | %% 1807 | %% The only thing that is not right is the even alignment of 1808 | %% chunks. I use four-byte alignment instead. Because of this 1809 | %% change, Beam files starts with 'FOR1' instead of 'FORM' to 1810 | %% allow reader programs to distinguish "classic" IFF from "beam" 1811 | %% IFF. The name 'FOR1' is included in the IFF document as a 1812 | %% future way to extend IFF. 1813 | %% 1814 | %% In the description of the chunks that follow, the word 1815 | %% mandatory means that the module cannot be loaded without it. 1816 | %% 1817 | %% 1818 | %% FORM HEADER 1819 | %% 1820 | %% 4 bytes 'FOR1' Magic number indicating an IFF form. This is an 1821 | %% extension to IFF indicating that all chunks are 1822 | %% four-byte aligned. 1823 | %% 4 bytes n Form length (file length - 8) 1824 | %% 4 bytes 'BEAM' Form type 1825 | %% n-8 bytes ... The chunks, concatenated. 1826 | %% 1827 | %% 1828 | %% ATOM TABLE CHUNK 1829 | %% 1830 | %% The atom table chunk is mandatory. The first atom in the table must 1831 | %% be the module name. 1832 | %% 1833 | %% 4 bytes 'Atom' 1834 | %% or 'AtU8' chunk ID 1835 | %% 4 bytes size total chunk length 1836 | %% 4 bytes n number of atoms 1837 | %% xx bytes ... Atoms. Each atom is a string preceded 1838 | %% by the length in a byte, encoded 1839 | %% in latin1 (if chunk ID == 'Atom') or 1840 | %% or UTF-8 (if chunk ID == 'AtU8') 1841 | %% 1842 | %% The following section about the constant pool (literal table) was 1843 | %% reverse engineered from the source (beam_lib etc), since it wasn't 1844 | %% included in the beam format specification referred above. 1845 | %% 1846 | %% CONSTANT POOL/LITERAL TABLE CHUNK 1847 | %% 1848 | %% The literal table chunk is optional. 1849 | %% 1850 | %% 4 bytes 'LitT' chunk ID 1851 | %% 4 bytes size total chunk length 1852 | %% 4 bytes size size of uncompressed constants 1853 | %% xx bytes ... zlib compressed constants 1854 | %% 1855 | %% Once uncompressed, the format of the constants are as follows: 1856 | %% 1857 | %% 4 bytes size unknown 1858 | %% 4 bytes size size of first literal 1859 | %% xx bytes ... term_to_binary encoded literal 1860 | %% 4 bytes size size of next literal 1861 | %% ... 1862 | 1863 | %%-------------------------------------------------------------------- 1864 | %% @doc Rename a module. `BeamBin0' is a binary containing the 1865 | %% contents of the beam file. 1866 | %% @end 1867 | %%-------------------------------------------------------------------- 1868 | -spec rename(BeamBin0 :: binary(), Name :: atom()) -> BeamBin :: binary(). 1869 | rename(BeamBin0, Name) -> 1870 | BeamBin = replace_in_atab(BeamBin0, Name), 1871 | update_form_size(BeamBin). 1872 | 1873 | %% Replace the first atom of the atom table with the new name 1874 | replace_in_atab(<<"Atom", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> 1875 | replace_first_atom(<<"Atom">>, Cnk, CnkSz0, Rest, latin1, Name); 1876 | replace_in_atab(<<"AtU8", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> 1877 | replace_first_atom(<<"AtU8">>, Cnk, CnkSz0, Rest, unicode, Name); 1878 | replace_in_atab(<>, Name) -> 1879 | <>. 1880 | 1881 | replace_first_atom(CnkName, Cnk, CnkSz0, Rest, Encoding, Name) -> 1882 | <> = Cnk, 1883 | NumPad0 = num_pad_bytes(CnkSz0), 1884 | <<_:NumPad0/unit:8, NextCnks/binary>> = Rest, 1885 | NameBin = atom_to_binary(Name, Encoding), 1886 | NameSz = byte_size(NameBin), 1887 | CnkSz = CnkSz0 + NameSz - NameSz0, 1888 | NumPad = num_pad_bytes(CnkSz), 1889 | <>. 1891 | 1892 | 1893 | %% Calculate the number of padding bytes that have to be added for the 1894 | %% BinSize to be an even multiple of ?beam_num_bytes_alignment. 1895 | num_pad_bytes(BinSize) -> 1896 | case ?beam_num_bytes_alignment - (BinSize rem ?beam_num_bytes_alignment) of 1897 | 4 -> 0; 1898 | N -> N 1899 | end. 1900 | 1901 | %% Update the size within the top-level form 1902 | update_form_size(<<"FOR1", _OldSz:32, Rest/binary>> = Bin) -> 1903 | Sz = size(Bin) - 8, 1904 | <<"FOR1", Sz:32, Rest/binary>>. 1905 | 1906 | par_map(F, List) -> 1907 | PMs = [spawn_monitor(wrap_call(F, Elem)) || Elem <- List], 1908 | [receive {'DOWN', MRef, _, _, Res} -> unwrap(Res) end 1909 | || {_Pid, MRef} <- PMs]. 1910 | 1911 | wrap_call(F, Elem) -> 1912 | fun() -> 1913 | exit(try {ok, F(Elem)} 1914 | catch ?with_stacktrace(Class, Reason, Stack) 1915 | {error, Class, Reason, Stack} 1916 | end) 1917 | end. 1918 | 1919 | unwrap({ok, Res}) -> Res; 1920 | unwrap({error, Class, Reason, InnerStack}) -> 1921 | try error(just_to_get_a_stack) 1922 | catch ?with_stacktrace(error, just_to_get_a_stack,OuterStack) 1923 | erlang:raise(Class, Reason, InnerStack ++ OuterStack) 1924 | end. 1925 | 1926 | --------------------------------------------------------------------------------