├── 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 |
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 | [](https://hex.pm/packages/mockgyver)
5 | [](https://github.com/klajo/mockgyver/actions?query=workflow%3A%22Erlang+CI%22)
6 | [](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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
254 |
255 | See syntax for `?WAS_CALLED`.
256 |
257 |
258 |
259 |
260 | ```erlang
261 |
262 | ?GET_CALLS(module:function(Arg1, Arg2, ...)),
263 | Result: [CallArgs]
264 | CallArgs = [CallArg]
265 | CallArg = term()
266 | ```
267 |
268 |
269 |
270 |
271 | ```erlang
272 |
273 | ?NUM_CALLS(module:function(Arg1, Arg2, ...)),
274 | Result: integer()
275 | ```
276 |
277 |
278 |
279 |
280 |
281 | ```erlang
282 |
283 | ?FORGET_CALLS(module:function(Arg1, Arg2, ...)),
284 | ?FORGET_CALLS(),
285 | ```
286 |
287 |
288 |
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 | %%%
73 | %%% - replace existing functions in existing modules
74 | %%% - add new functions to existing modules
75 | %%% - add new modules
76 | %%%
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 | %%%
182 | %%% - `?WAS_CALLED': Check that a function was called with
183 | %%% certain set of parameters a chosen number of times.
184 | %%% The validation is done at the place of the macro, consider
185 | %%% this when verifying asynchronous procedures
186 | %%% (see also `?WAIT_CALLED'). Return a list of argument lists,
187 | %%% one argument list for each call to the function. An
188 | %%% argument list contains the arguments of a specific call.
189 | %%% Will crash with an error if the criteria isn't fulfilled.
190 | %%% - `?WAIT_CALLED': Same as `?WAS_CALLED', with a twist: waits for
191 | %%% the criteria to be fulfilled which can be useful for
192 | %%% asynchronous procedures.
193 | %%% - `?GET_CALLS': Return a list of argument lists (just like
194 | %%% `?WAS_CALLED' or `?WAIT_CALLED') without checking any criteria.
195 | %%% - `?NUM_CALLS': Return the number of calls to a function.
196 | %%% - `?FORGET_CALLS': Forget the calls that have been logged.
197 | %%% This exists in two versions:
198 | %%%
199 | %%% - One which forgets calls to a certain function.
200 | %%% Takes arguments and guards into account, i.e. only
201 | %%% the calls which match the module name, function
202 | %%% name and all arguments as well as any guards will
203 | %%% be forgotten, while the rest of the calls remain.
204 | %%% - One which forgets all calls to any function.
205 | %%%
206 | %%%
207 | %%%
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 |
--------------------------------------------------------------------------------