├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── patch.ex └── patch │ ├── access.ex │ ├── apply.ex │ ├── assertions.ex │ ├── case.ex │ ├── importer.ex │ ├── listener.ex │ ├── listener │ └── supervisor.ex │ ├── macro.ex │ ├── mock.ex │ ├── mock │ ├── code.ex │ ├── code │ │ ├── freezer.ex │ │ ├── generate.ex │ │ ├── generators │ │ │ ├── delegate.ex │ │ │ ├── facade.ex │ │ │ ├── frozen.ex │ │ │ └── original.ex │ │ ├── queries │ │ │ ├── exports.ex │ │ │ └── functions.ex │ │ ├── query.ex │ │ ├── transform.ex │ │ ├── transforms │ │ │ ├── clean.ex │ │ │ ├── export.ex │ │ │ ├── filter.ex │ │ │ ├── remote.ex │ │ │ ├── rename.ex │ │ │ └── reroute.ex │ │ └── unit.ex │ ├── history.ex │ ├── history │ │ └── tagged.ex │ ├── naming.ex │ ├── server.ex │ ├── supervisor.ex │ ├── value.ex │ └── values │ │ ├── callable.ex │ │ ├── callable_stack.ex │ │ ├── cycle.ex │ │ ├── raises.ex │ │ ├── scalar.ex │ │ ├── sequence.ex │ │ └── throws.ex │ ├── reflection.ex │ └── supervisor.ex ├── mix.exs ├── mix.lock ├── pages ├── cheatsheet.cheatmd ├── guide-book │ ├── 01-introduction.md │ ├── 02-patching.md │ ├── 03-mock-values.md │ ├── 04-spies-and-fakes.md │ └── 05-processes.md └── super-powers.md └── test ├── support ├── unit │ ├── access.ex │ └── mock.ex └── user │ ├── assert_any_call.ex │ ├── assert_called.ex │ ├── assert_called_once.ex │ ├── expose.ex │ ├── fake │ ├── fake.ex │ └── real.ex │ ├── history.ex │ ├── inject │ ├── caller.ex │ ├── target.ex │ └── targetless.ex │ ├── listener │ └── counter.ex │ ├── patch │ ├── arity.ex │ ├── callable.ex │ ├── callable_stack.ex │ ├── cycle.ex │ ├── erlang_unsticky.erl │ ├── execution_context.ex │ ├── freezer.ex │ ├── gen_server_example.ex │ ├── local_call.ex │ ├── raises.ex │ ├── scalar.ex │ ├── sequence.ex │ ├── throws.ex │ └── unknown_function.ex │ ├── private.ex │ ├── reflection │ ├── elixir_target.ex │ └── erlang_target.erl │ ├── refute_any_call.ex │ ├── refute_called.ex │ ├── refute_called_once.ex │ ├── replace.ex │ ├── restore.ex │ └── spy.ex ├── test_helper.exs ├── unit └── patch │ ├── access_test.exs │ ├── macro_test.exs │ └── mock │ └── code │ ├── queries │ └── exports_test.exs │ └── transforms │ └── clean_test.exs └── user ├── assert_any_call_test.exs ├── assert_called_once_test.exs ├── assert_called_test.exs ├── expose_test.exs ├── fake_test.exs ├── history_test.exs ├── import_test.exs ├── inject_test.exs ├── listen_test.exs ├── patch ├── arity_test.exs ├── callable_stack_test.exs ├── callable_test.exs ├── cycle_test.exs ├── erlang_test.exs ├── execution_context_test.exs ├── freezer_test.exs ├── gen_server_test.exs ├── local_call_test.exs ├── raises_test.exs ├── scalar_test.exs ├── sequence_test.exs ├── throws_test.exs └── unknown_function_test.exs ├── private_test.exs ├── readme.md ├── reflection_test.exs ├── refute_any_call_test.exs ├── refute_called_once_test.exs ├── refute_called_test.exs ├── replace_test.exs ├── restore_test.exs └── spy_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [ 5 | assert_any_call: 1, 6 | assert_any_call: 2, 7 | assert_called: 1, 8 | assert_called: 2, 9 | assert_called_once: 1, 10 | refute_any_call: 1, 11 | refute_any_call: 2, 12 | refute_called: 1, 13 | refute_called: 2, 14 | refute_called_once: 1 15 | ], 16 | export: [ 17 | locals_without_parens: [ 18 | assert_any_call: 1, 19 | assert_any_call: 2, 20 | assert_called: 1, 21 | assert_called: 2, 22 | assert_called_once: 1, 23 | refute_any_call: 1, 24 | refute_any_call: 2, 25 | refute_called: 1, 26 | refute_called: 2, 27 | refute_called_once: 1 28 | ] 29 | ] 30 | ] 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | include: 16 | - elixir-version: 1.11.4 17 | otp-version: 24.3 18 | - elixir-version: 1.12.3 19 | otp-version: 24.3 20 | - elixir-version: 1.13.4 21 | otp-version: 24.3 22 | - elixir-version: 1.13.4 23 | otp-version: 25.3 24 | - elixir-version: 1.14.5 25 | otp-version: 24.3 26 | - elixir-version: 1.14.5 27 | otp-version: 25.3 28 | - elixir-version: 1.14.5 29 | otp-version: 26.2 30 | - elixir-version: 1.15.7 31 | otp-version: 24.3 32 | - elixir-version: 1.15.7 33 | otp-version: 25.3 34 | - elixir-version: 1.15.7 35 | otp-version: 26.2 36 | - elixir-version: 1.16.3 37 | otp-version: 24.3 38 | - elixir-version: 1.16.3 39 | otp-version: 25.3 40 | - elixir-version: 1.16.3 41 | otp-version: 26.2 42 | - elixir-version: 1.17.3 43 | otp-version: 25.3 44 | - elixir-version: 1.17.3 45 | otp-version: 26.2 46 | - elixir-version: 1.17.3 47 | otp-version: 27.3 48 | - elixir-version: 1.18.4 49 | otp-version: 26.2 50 | - elixir-version: 1.18.4 51 | otp-version: 27.3 52 | steps: 53 | - uses: actions/checkout@v3 54 | - name: Set up Elixir 55 | uses: erlef/setup-beam@v1 56 | with: 57 | elixir-version: ${{ matrix.elixir-version }} 58 | otp-version: ${{ matrix.otp-version }} 59 | - name: Restore dependencies cache 60 | uses: actions/cache@v3 61 | with: 62 | path: deps 63 | key: ${{ runner.os }}-${{ matrix.elixir-version }}-${{ matrix.otp-version}}-mix-${{ hashFiles('**/mix.lock') }} 64 | restore-keys: ${{ runner.os }}-${{ matrix.elixir-version}}-${{ matrix.otp-version }}-mix- 65 | - name: Start EPMD 66 | run: epmd -daemon 67 | - name: Install dependencies 68 | run: mix deps.get 69 | - name: Run tests 70 | run: mix test 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | patch-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matt Nowack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | import_config("#{Mix.env()}.exs") 4 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /lib/patch/access.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Access do 2 | 3 | def get(target, keys, default \\ nil) 4 | 5 | def get(%{} = target, [key], default) do 6 | Map.get(target, key, default) 7 | end 8 | 9 | def get(%{} = target, [key | rest], default) do 10 | case Map.fetch(target, key) do 11 | {:ok, value} -> 12 | get(value, rest, default) 13 | 14 | :error -> 15 | default 16 | end 17 | end 18 | 19 | def fetch(%{} = target, [key]) do 20 | Map.fetch(target, key) 21 | end 22 | 23 | def fetch(%{} = target, [key | rest]) do 24 | with {:ok, value} <- Map.fetch(target, key) do 25 | fetch(value, rest) 26 | end 27 | end 28 | 29 | def put(%{} = target, [key], value) do 30 | Map.put(target, key, value) 31 | end 32 | 33 | def put(%{} = target, [key | rest], value) do 34 | inner = get(target, [key]) 35 | updated = put(inner, rest, value) 36 | Map.put(target, key, updated) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/patch/apply.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Apply do 2 | @moduledoc """ 3 | Utility module to assist with applying functions. 4 | """ 5 | 6 | @doc """ 7 | Safe application of an anonymous function. 8 | 9 | This is just like `apply/2` but if the the anonymous function is unable to 10 | handle the call (raising either `BadArityError` or `FunctionClauseError`) then 11 | the `:error` atom is returned. 12 | 13 | If the function evaluates successfully its reply is returned wrapped in an 14 | `{:ok, result}` tuple. 15 | """ 16 | @spec safe(function :: function(), arguments :: [term()]) :: {:ok, term()} | :error 17 | def safe(function, arguments) do 18 | try do 19 | result = apply(function, arguments) 20 | 21 | {:ok, result} 22 | rescue 23 | e in [BadArityError, FunctionClauseError] -> 24 | if direct_exception?(function, e) do 25 | :error 26 | else 27 | reraise e, __STACKTRACE__ 28 | end 29 | end 30 | end 31 | 32 | ## Private 33 | 34 | @spec direct_exception?(function :: function(), error :: Exception.t()) :: boolean() 35 | defp direct_exception?(function, %BadArityError{} = error) do 36 | function == error.function 37 | end 38 | 39 | defp direct_exception?(function, %FunctionClauseError{} = error) do 40 | info = Function.info(function) 41 | info[:arity] == error.arity and info[:name] == error.function 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/patch/case.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Case do 2 | defmacro __using__(_) do 3 | quote do 4 | setup do 5 | debug = Application.fetch_env(:patch, :debug) 6 | start_supervised!(Patch.Supervisor) 7 | 8 | on_exit(fn -> 9 | Patch.Mock.Code.Freezer.empty() 10 | 11 | case debug do 12 | {:ok, value} -> 13 | Application.put_env(:patch, :debug, value) 14 | 15 | :error -> 16 | Application.delete_env(:patch, :debug) 17 | end 18 | end) 19 | 20 | :ok 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/patch/listener.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Listener do 2 | use GenServer 3 | 4 | @default_capture_replies true 5 | @default_timeout 5000 6 | 7 | @typedoc """ 8 | Listeners are started with a tag so the listening process can differentiate 9 | between multiple listeners. 10 | """ 11 | @type tag :: atom() 12 | 13 | @typedoc """ 14 | Listeners listen to a target. 15 | """ 16 | @type target :: GenServer.server() | nil 17 | 18 | @typedoc """ 19 | Option to control whether or not to capture GenServer.call replies. 20 | 21 | Defaults to #{@default_capture_replies} 22 | """ 23 | @type capture_replies_option :: {:capture_replies, boolean()} 24 | 25 | @typedoc """ 26 | Option to control how long the listener should wait for GenServer.call 27 | 28 | Value is either the number of milliseconds to wait or the `:infinity` atom. 29 | 30 | If `capture_replies` is set to false this setting has no effect. 31 | 32 | Defaults to #{@default_timeout} 33 | """ 34 | @type timeout_option :: {:timeout, timeout()} 35 | 36 | @typedoc """ 37 | Sum-type of all valid options 38 | """ 39 | @type option :: capture_replies_option() | timeout_option() 40 | 41 | @type t :: %__MODULE__{ 42 | capture_replies: boolean(), 43 | recipient: pid(), 44 | tag: atom(), 45 | target: pid(), 46 | timeout: timeout() 47 | } 48 | defstruct [:capture_replies, :recipient, :tag, :target, :timeout] 49 | 50 | ## Client 51 | 52 | def child_spec(args) do 53 | recipient = Keyword.fetch!(args, :recipient) 54 | tag = Keyword.fetch!(args, :tag) 55 | target = Keyword.fetch!(args, :target) 56 | options = Keyword.get(args, :options, []) 57 | 58 | %{ 59 | id: __MODULE__, 60 | start: {__MODULE__, :start_link, [recipient, tag, target, options]}, 61 | restart: :temporary 62 | } 63 | end 64 | 65 | @spec start_link(recipient :: atom(), tag :: tag(), target :: target(), [option()]) :: 66 | {:ok, pid()} | {:error, :not_found} 67 | def start_link(recipient, tag, target, options \\ []) 68 | 69 | def start_link(recipient, tag, target, options) when is_pid(target) or is_nil(target) do 70 | capture_replies = Keyword.get(options, :capture_replies, @default_capture_replies) 71 | timeout = Keyword.get(options, :timeout, @default_timeout) 72 | 73 | state = %__MODULE__{ 74 | capture_replies: capture_replies, 75 | recipient: recipient, 76 | tag: tag, 77 | target: target, 78 | timeout: timeout 79 | } 80 | 81 | GenServer.start_link(__MODULE__, state) 82 | end 83 | 84 | def start_link(recipient, tag, target, options) when is_atom(target) do 85 | case Process.whereis(target) do 86 | nil -> 87 | {:error, :not_found} 88 | 89 | pid -> 90 | true = Process.unregister(target) 91 | {:ok, listener} = start_link(recipient, tag, pid, options) 92 | Process.register(listener, target) 93 | 94 | {:ok, listener} 95 | end 96 | end 97 | 98 | def target(listener) do 99 | GenServer.call(listener, {__MODULE__, :target}) 100 | end 101 | 102 | ## Server 103 | 104 | @spec init(t()) :: {:ok, t()} 105 | def init(%__MODULE__{} = state) do 106 | if is_pid(state.target) do 107 | Process.monitor(state.target) 108 | end 109 | 110 | {:ok, state} 111 | end 112 | 113 | def handle_call({__MODULE__, :target}, _from, state) do 114 | {:reply, state.target, state} 115 | end 116 | 117 | def handle_call(message, from, %__MODULE__{target: nil} = state) do 118 | report(state, {GenServer, :call, message, from}) 119 | report(state, {:EXIT, :no_listener_target}) 120 | Process.exit(self(), :no_listener_target) 121 | end 122 | 123 | def handle_call(message, from, %__MODULE__{capture_replies: false} = state) do 124 | report(state, {GenServer, :call, message, from}) 125 | forward(state, {:"$gen_call", from, message}) 126 | {:noreply, state} 127 | end 128 | 129 | def handle_call(message, from, state) do 130 | report(state, {GenServer, :call, message, from}) 131 | 132 | try do 133 | response = GenServer.call(state.target, message, state.timeout) 134 | report(state, {GenServer, :reply, response, from}) 135 | {:reply, response, state} 136 | catch 137 | :exit, {reason, _call} -> 138 | report(state, {:EXIT, reason}) 139 | Process.exit(self(), reason) 140 | end 141 | end 142 | 143 | def handle_cast(message, state) do 144 | report(state, {GenServer, :cast, message}) 145 | GenServer.cast(state.target, message) 146 | {:noreply, state} 147 | end 148 | 149 | def handle_info({:DOWN, _, :process, pid, reason}, %__MODULE__{target: pid} = state) do 150 | report(state, {:DOWN, reason}) 151 | {:stop, {:shutdown, {:DOWN, reason}}, state} 152 | end 153 | 154 | def handle_info(message, state) do 155 | report(state, message) 156 | forward(state, message) 157 | {:noreply, state} 158 | end 159 | 160 | ## Private 161 | 162 | @compile {:inline, report: 2} 163 | defp report(%__MODULE__{} = state, message) do 164 | send(state.recipient, {state.tag, message}) 165 | end 166 | 167 | @compile {:inline, forward: 2} 168 | defp forward(%__MODULE__{target: nil}, _) do 169 | :ok 170 | end 171 | 172 | defp forward(%__MODULE__{} = state, message) do 173 | send(state.target, message) 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/patch/listener/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Listener.Supervisor do 2 | use DynamicSupervisor 3 | 4 | def start_link(_ \\ []) do 5 | DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) 6 | end 7 | 8 | def start_child(recipient, tag, target, options) do 9 | DynamicSupervisor.start_child( 10 | __MODULE__, 11 | {Patch.Listener, recipient: recipient, tag: tag, target: target, options: options} 12 | ) 13 | end 14 | 15 | def init(:ok) do 16 | DynamicSupervisor.init(strategy: :one_for_one) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/patch/macro.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Macro do 2 | @doc """ 3 | Utility function that acts like `inspect/1` but prints out the Macro as code. 4 | """ 5 | @spec debug(ast :: Macro.t()) :: Macro.t() 6 | def debug(ast) do 7 | ast 8 | |> Macro.to_string() 9 | |> IO.puts() 10 | 11 | ast 12 | end 13 | 14 | @doc """ 15 | Performs an non-hygienic match. 16 | 17 | If the match succeeds true is returned, otherwise a MatchError is raised. 18 | 19 | Since the match is non-hygienic pins can be used from the user-scope and binds will effect 20 | user-scope. 21 | """ 22 | @spec match(pattern :: Macro.t(), expression :: Macro.t()) :: Macro.t() 23 | defmacro match(pattern, expression) do 24 | user_pattern = user_variables(pattern) 25 | pattern_expression = pattern_expression(pattern) 26 | variables = variables(pattern) 27 | 28 | quote generated: true do 29 | unquote(pattern_expression) = 30 | case unquote(expression) do 31 | unquote(user_pattern) -> 32 | _ = unquote(variables) 33 | unquote(expression) 34 | 35 | _ -> 36 | raise MatchError, term: unquote(expression) 37 | end 38 | 39 | _ = unquote(variables) 40 | true 41 | end 42 | end 43 | 44 | @doc """ 45 | Performs a match, return true if match matches, false otherwise. 46 | """ 47 | @spec match?(pattern :: Macro.t(), expression :: Macro.t()) :: Macro.t() 48 | defmacro match?(pattern, expression) do 49 | quote generated: true do 50 | try do 51 | Patch.Macro.match(unquote(pattern), unquote(expression)) 52 | true 53 | rescue 54 | MatchError -> 55 | false 56 | end 57 | end 58 | end 59 | 60 | ## Private 61 | 62 | defp pattern_expression(pattern) do 63 | Macro.prewalk(pattern, fn 64 | {:^, _, [{name, meta, _}]} -> 65 | {name, meta, nil} 66 | 67 | {:_, _, _} -> 68 | unique_variable() 69 | 70 | node -> 71 | node 72 | end) 73 | end 74 | 75 | defp unique_variable do 76 | {:"_ignore#{:erlang.unique_integer([:positive])}", [generated: true], nil} 77 | end 78 | 79 | defp user_variables(pattern) do 80 | Macro.prewalk(pattern, fn 81 | {name, meta, context} when is_atom(name) and is_atom(context) -> 82 | {name, meta, nil} 83 | 84 | node -> 85 | node 86 | end) 87 | end 88 | 89 | defp variables(pattern) do 90 | pattern 91 | |> Macro.prewalk([], fn 92 | {:_, _, _} = node, acc -> 93 | {node, acc} 94 | 95 | {:@, anno, _}, acc -> 96 | # Replace module attribute with wildcard so we don't convert into a variable 97 | {{:_, anno, nil}, acc} 98 | 99 | {name, meta, context} = node, acc when is_atom(name) and is_atom(context) -> 100 | ignored? = 101 | name 102 | |> Atom.to_string() 103 | |> String.starts_with?("_") 104 | 105 | if ignored? do 106 | {node, acc} 107 | else 108 | {node, [{name, meta, nil} | acc]} 109 | end 110 | 111 | node, acc -> 112 | {node, acc} 113 | end) 114 | |> elem(1) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/patch/mock.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock do 2 | alias Patch.Mock 3 | alias Patch.Mock.Code 4 | alias Patch.Mock.Code.Freezer 5 | 6 | @typedoc """ 7 | What exposures should be made in a module. 8 | 9 | - `:public` will only expose the public functions 10 | - `:all` will expose both public and private functions 11 | - A list of exports can be provided, they will be added to the `:public` functions. 12 | """ 13 | @type exposes :: :all | :public | Code.exports() 14 | 15 | @typedoc """ 16 | The exposes option controls if any private functions should be exposed. 17 | 18 | The default is `:public`. 19 | """ 20 | @type exposes_option :: {:exposes, exposes()} 21 | 22 | @typedoc """ 23 | This history_limit option controls how large of a history a mock should store 24 | 25 | It defaults to `:infinity` which will store an unlimited history. 26 | """ 27 | @type history_limit_option :: {:history_limit, non_neg_integer() | :infinity} 28 | 29 | @typedoc """ 30 | Sum-type of all valid options 31 | """ 32 | @type option :: exposes_option() | history_limit_option() 33 | 34 | 35 | @doc """ 36 | Expose private functions in a module. 37 | 38 | If the module is not already mocked, calling this function will mock it. 39 | """ 40 | @spec expose(module :: module, exposes :: exposes()) :: :ok | {:error, term()} 41 | def expose(module, exposes) do 42 | with {:ok, _} <- module(module, exposes: exposes) do 43 | Mock.Server.expose(module, exposes) 44 | end 45 | end 46 | 47 | @doc """ 48 | Gets the call history for a module. 49 | 50 | If the module is not already mocked, this function returns an empty new history. 51 | """ 52 | @spec history(module :: module()) :: Mock.History.t() 53 | def history(module) do 54 | Mock.Server.history(module) 55 | end 56 | 57 | @doc """ 58 | Mocks the given module. 59 | 60 | Mocking a module accepts two options, see the `t:option/0` type in this module for details. 61 | """ 62 | @spec module(module :: module(), options :: [option()]) :: 63 | {:ok, pid()} | {:error, term()} 64 | def module(module, options \\ []) do 65 | :ok = Freezer.put(module) 66 | 67 | case Mock.Supervisor.start_child(module, options) do 68 | {:ok, pid} -> 69 | {:ok, pid} 70 | 71 | {:error, {:already_started, pid}} -> 72 | {:ok, pid} 73 | 74 | {:error, _} = error -> 75 | error 76 | end 77 | end 78 | 79 | @doc """ 80 | Registers a mock value for a function. 81 | 82 | If the module is not already mocked, this function will mock it with no private functions 83 | exposed. 84 | """ 85 | @spec register(module :: module(), name :: atom(), value :: Mock.Value.t()) :: :ok 86 | def register(module, name, value) do 87 | with {:ok, _} <- module(module) do 88 | Mock.Server.register(module, name, value) 89 | end 90 | end 91 | 92 | @doc """ 93 | Restores a module to pre-patch functionality. 94 | 95 | If the module is not already mocked, this function no-ops. 96 | """ 97 | @spec restore(module :: module()) :: :ok 98 | def restore(module) do 99 | Mock.Server.restore(module) 100 | end 101 | 102 | @doc """ 103 | Restores a function in a module to pre-patch functionality. 104 | 105 | If the module or function are not already mocked, this function no-ops. 106 | """ 107 | @spec restore(mdoule :: module(), name :: atom()) :: :ok 108 | def restore(module, name) do 109 | Mock.Server.restore(module, name) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/patch/mock/code/freezer.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Freezer do 2 | @moduledoc """ 3 | The Code Freezer is a registry that can be used to register and use modules 4 | that might be frozen. 5 | 6 | Modules that Patch relies on must be freezable so that the end-user can Patch 7 | them and the frozen versions are still available for internal use. 8 | """ 9 | 10 | alias Patch.Mock.Code 11 | alias Patch.Mock.Naming 12 | 13 | @freezable [GenServer] 14 | 15 | @doc """ 16 | Destroy all frozen modules 17 | """ 18 | @spec empty() :: :ok 19 | def empty() do 20 | __MODULE__ 21 | |> Application.get_all_env() 22 | |> Enum.each(fn {key, frozen} -> 23 | Code.purge(frozen) 24 | Application.delete_env(__MODULE__, key) 25 | end) 26 | end 27 | 28 | @doc """ 29 | Get the possibly-frozen module to use for a module. 30 | 31 | If the module is frozen then the frozen name will be returned. 32 | 33 | If the module is not frozen then the module is returned.s 34 | """ 35 | @spec get(module :: module()) :: module() 36 | def get(module) do 37 | Application.get_env(__MODULE__, module, module) 38 | end 39 | 40 | @doc """ 41 | Puts a module into the freezer. 42 | 43 | The module must be freezable. Repeated calls for frozen modules are no-ops. 44 | """ 45 | @spec put(module :: module()) :: :ok 46 | def put(module) when module in @freezable do 47 | case Application.fetch_env(__MODULE__, module) do 48 | {:ok, _} -> 49 | :ok 50 | 51 | :error -> 52 | :ok = Code.freeze(module) 53 | Application.put_env(__MODULE__, module, Naming.frozen(module)) 54 | end 55 | end 56 | 57 | def put(_) do 58 | :ok 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/patch/mock/code/generate.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Generate do 2 | @moduledoc """ 3 | This module provides the ability to generate derivative modules based on a target module. 4 | 5 | This module delegates code generation to purpose specific Generator modules, see those modules 6 | for additional details on how specific generation works. 7 | 8 | - `Patch.Mock.Code.Generators.Delegate` for how `delegate` modules are generated 9 | - `Patch.Mock.Code.Generators.Facade` for how `facade` modules are generated 10 | - `Patch.Mock.Code.Generators.Frozen` for how `frozen` modules are generated 11 | - `Patch.Mock.Code.Generators.Original` for how `original` modules are generated 12 | """ 13 | 14 | alias Patch.Mock.Code.Generators 15 | 16 | defdelegate delegate(abstract_forms, module, exports), to: Generators.Delegate, as: :generate 17 | defdelegate facade(abstract_forms, module, exports), to: Generators.Facade, as: :generate 18 | defdelegate frozen(abstract_forms, module), to: Generators.Frozen, as: :generate 19 | defdelegate original(abstract_forms, module, exports), to: Generators.Original, as: :generate 20 | end 21 | -------------------------------------------------------------------------------- /lib/patch/mock/code/generators/delegate.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Generators.Delegate do 2 | @moduledoc """ 3 | Generator for `delegate` modules. 4 | 5 | `delegate` modules are generated by taking the `target` module and creating a stub function for 6 | each function in the module that calls the `Patch.Mock.Server`'s `delegate/3` function. 7 | 8 | The `delegate` module will also expose every function in the module regardless of the original 9 | visibility. 10 | """ 11 | 12 | @generated [generated: true] 13 | 14 | alias Patch.Mock.Code 15 | alias Patch.Mock.Code.Transform 16 | alias Patch.Mock.Naming 17 | 18 | @doc """ 19 | Generates a new delegate module based on the forms of a provided module. 20 | """ 21 | @spec generate(abstract_forms :: [Code.form()], module :: module(), exports :: Code.exports()) :: [Code.form()] 22 | def generate(abstract_forms, module, exports) do 23 | delegate_name = Naming.delegate(module) 24 | 25 | abstract_forms 26 | |> Transform.clean() 27 | |> Transform.export(exports) 28 | |> Transform.filter(exports) 29 | |> Transform.rename(delegate_name) 30 | |> Enum.map(fn 31 | {:function, _, name, arity, _} -> 32 | function(module, name, arity) 33 | 34 | other -> 35 | other 36 | end) 37 | end 38 | 39 | ## Private 40 | 41 | defp arguments(0) do 42 | cons([]) 43 | end 44 | 45 | defp arguments(arity) do 46 | 1..arity 47 | |> Enum.to_list() 48 | |> cons() 49 | end 50 | 51 | defp cons([]), do: {nil, @generated} 52 | 53 | defp cons([head | tail]) do 54 | {:cons, @generated, {:var, @generated, :"_arg#{head}"}, cons(tail)} 55 | end 56 | 57 | defp body(module, name, arity) do 58 | [ 59 | {:call, @generated, 60 | {:remote, @generated, {:atom, @generated, Patch.Mock.Server}, 61 | {:atom, @generated, :delegate}}, 62 | [ 63 | {:atom, @generated, module}, 64 | {:atom, @generated, name}, 65 | arguments(arity) 66 | ]} 67 | ] 68 | end 69 | 70 | defp function(module, name, arity) do 71 | clause = { 72 | :clause, 73 | @generated, 74 | patterns(arity), 75 | [], 76 | body(module, name, arity) 77 | } 78 | 79 | {:function, @generated, name, arity, [clause]} 80 | end 81 | 82 | defp patterns(0) do 83 | [] 84 | end 85 | 86 | defp patterns(arity) do 87 | Enum.map(1..arity, fn position -> 88 | {:var, @generated, :"_arg#{position}"} 89 | end) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/patch/mock/code/generators/facade.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Generators.Facade do 2 | @moduledoc """ 3 | Generator for `facade` modules. 4 | 5 | `facade` modules are generated by taking the `target` module and creating a stub function for 6 | each function in the module that calls the `delegate` modules corresponding function. 7 | 8 | The `facade` module can optionally expose private functions as public. 9 | """ 10 | 11 | alias Patch.Mock.Naming 12 | alias Patch.Mock.Code 13 | alias Patch.Mock.Code.Transform 14 | 15 | @generated [generated: true] 16 | 17 | @doc """ 18 | Generates a new facade module based on the forms of the provided module. 19 | """ 20 | @spec generate( 21 | abstract_forms :: [Code.form()], 22 | module :: module(), 23 | exports :: Code.exports() 24 | ) :: [Code.form()] 25 | def generate(abstract_forms, module, exports) do 26 | abstract_forms 27 | |> Transform.clean() 28 | |> Transform.export(exports) 29 | |> Transform.filter(exports) 30 | |> module(module) 31 | end 32 | 33 | ## Private 34 | 35 | defp arguments(0) do 36 | [] 37 | end 38 | 39 | defp arguments(arity) do 40 | Enum.map(1..arity, &{:var, @generated, :"_arg#{&1}"}) 41 | end 42 | 43 | defp body(module, name, arity) do 44 | delegate = Naming.delegate(module) 45 | 46 | [ 47 | { 48 | :call, 49 | @generated, 50 | {:remote, @generated, {:atom, @generated, delegate}, {:atom, @generated, name}}, 51 | arguments(arity) 52 | } 53 | ] 54 | end 55 | 56 | defp function(module, name, arity) do 57 | clause = { 58 | :clause, 59 | @generated, 60 | patterns(arity), 61 | [], 62 | body(module, name, arity) 63 | } 64 | 65 | {:function, @generated, name, arity, [clause]} 66 | end 67 | 68 | @spec module(abstract_forms :: [Code.form()], module :: module()) :: 69 | [Code.form()] 70 | defp module(abstract_forms, module) do 71 | Enum.map(abstract_forms, fn 72 | {:function, _, name, arity, _} -> 73 | function(module, name, arity) 74 | 75 | other -> 76 | other 77 | end) 78 | end 79 | 80 | defp patterns(0) do 81 | [] 82 | end 83 | 84 | defp patterns(arity) do 85 | Enum.map(1..arity, fn position -> 86 | {:var, @generated, :"_arg#{position}"} 87 | end) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/patch/mock/code/generators/frozen.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Generators.Frozen do 2 | @moduledoc """ 3 | Generator for `frozen` modules. 4 | 5 | `frozen` modules are generated by taking the `target` module and renaming it and ensuring 6 | that any call in the module to itself is sent to the `frozen` module and not the provided 7 | module. 8 | 9 | The `frozen` module simple acts as a way to call the unaltered original code. It is only 10 | used internally to preserve the GenServer module since the internal implementation requires that 11 | GenServer work as expected. 12 | """ 13 | alias Patch.Mock.Code 14 | alias Patch.Mock.Code.Transform 15 | alias Patch.Mock.Naming 16 | 17 | @doc """ 18 | Generates a new frozen module based on the forms of a provided module. 19 | """ 20 | @spec generate(abstract_forms :: [Code.form()], module :: module()) :: [Code.form()] 21 | def generate(abstract_forms, module) do 22 | frozen_module = Naming.frozen(module) 23 | 24 | abstract_forms 25 | |> Transform.clean() 26 | |> Transform.reroute(module, frozen_module) 27 | |> Transform.rename(frozen_module) 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/patch/mock/code/generators/original.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Generators.Original do 2 | alias Patch.Mock.Code 3 | alias Patch.Mock.Code.Transform 4 | alias Patch.Mock.Naming 5 | 6 | @doc """ 7 | Generates a new original module based on the forms of the provided module. 8 | """ 9 | @spec generate(abstract_forms :: [Code.form()], module :: module(), exports :: Code.exports()) :: [Code.form()] 10 | def generate(abstract_forms, module, exports) do 11 | delegate_module = Naming.delegate(module) 12 | original_module = Naming.original(module) 13 | 14 | abstract_forms 15 | |> Transform.export(exports) 16 | |> Transform.filter(exports) 17 | |> Transform.remote(delegate_module) 18 | |> Transform.rename(original_module) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/patch/mock/code/queries/exports.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Queries.Exports do 2 | alias Patch.Mock.Code 3 | 4 | @doc """ 5 | Queries the provided forms for the exported functions. 6 | """ 7 | @spec query(abstract_forms :: [Code.form()]) :: Code.exports() 8 | def query(abstract_forms) do 9 | abstract_forms 10 | |> Enum.filter(&match?({:attribute, _, :export, _}, &1)) 11 | |> Enum.reduce([], fn {_, _, _, exports}, acc -> 12 | [exports | acc] 13 | end) 14 | |> List.flatten() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/patch/mock/code/queries/functions.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Queries.Functions do 2 | alias Patch.Mock.Code 3 | 4 | @doc """ 5 | Queries the provided forms for all defined functions. 6 | """ 7 | @spec query(abstract_forms :: [Code.form()]) :: Code.exports() 8 | def query(abstract_forms) do 9 | abstract_forms 10 | |> Enum.filter(&match?({:function, _, _, _, _}, &1)) 11 | |> Enum.reduce([], fn {:function, _, name, arity, _}, acc -> 12 | [{name, arity} | acc] 13 | end) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/patch/mock/code/query.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Query do 2 | @moduledoc """ 3 | This module provides the ability to query abstract forms. 4 | 5 | This module delegates queries to purpose specific Queries modules, see those modules for 6 | additional details on how specific queries work. 7 | 8 | - `Patch.Mock.Code.Queries.Exports` for how exports are queried 9 | - `Patch.Mock.Code.Queries.Functions` for how functions are queried 10 | """ 11 | 12 | alias Patch.Mock.Code.Queries 13 | 14 | defdelegate exports(abstract_forms), to: Queries.Exports, as: :query 15 | defdelegate functions(abstract_forms), to: Queries.Functions, as: :query 16 | end 17 | -------------------------------------------------------------------------------- /lib/patch/mock/code/transform.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Transform do 2 | @moduledoc """ 3 | This module provides the ability to transform abstract forms. 4 | 5 | This module delegates transformation to purpose specific Transforms modules, see those modules 6 | for additional details on how specific transformations work. 7 | 8 | - `Patch.Mock.Code.Transforms.Clean` for how a module is prepared for generation. 9 | - `Patch.Mock.Code.Transforms.Export` for how exports are rewritten 10 | - `Patch.Mock.Code.Transforms.Filter` for how functions are filtered 11 | - `Patch.Mock.Code.Transforms.Remote` for how local calls are made remote 12 | - `Patch.Mock.Code.Transforms.Rename` for how a module is renamed 13 | - `Patch.Mock.Code.Transforms.Reroute` for how remote calls are rerouted 14 | """ 15 | 16 | alias Patch.Mock.Code.Transforms 17 | 18 | defdelegate clean(abstract_forms), to: Transforms.Clean, as: :transform 19 | defdelegate export(abstract_forms, exports), to: Transforms.Export, as: :transform 20 | defdelegate filter(abstract_forms, exports), to: Transforms.Filter, as: :transform 21 | defdelegate remote(abstract_forms, module), to: Transforms.Remote, as: :transform 22 | defdelegate rename(abstract_forms, module), to: Transforms.Rename, as: :transform 23 | defdelegate reroute(abstract_forms, source, destination), to: Transforms.Reroute, as: :transform 24 | end 25 | -------------------------------------------------------------------------------- /lib/patch/mock/code/transforms/clean.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Transforms.Clean do 2 | @moduledoc """ 3 | Cleans abstract form to prepare it for subsequent generation. 4 | 5 | The following forms are retained: 6 | - module attribute 7 | - exports 8 | - no_auto_import compiler options (This is needed to prevent R14 shadow import warnings) 9 | - functions 10 | """ 11 | 12 | alias Patch.Mock.Code 13 | 14 | @spec transform(abstract_forms :: [Code.form()]) :: [Code.form()] 15 | def transform(abstract_forms) do 16 | abstract_forms 17 | |> Enum.reduce([], fn 18 | {:attribute, _, :module, _} = form, acc -> 19 | [form | acc] 20 | 21 | {:attribute, _, :export, _} = form, acc -> 22 | [form | acc] 23 | 24 | {:attribute, anno, :compile, options}, acc -> 25 | case filter_compile_options(options) do 26 | [] -> 27 | acc 28 | 29 | options -> 30 | form = {:attribute, anno, :compile, options} 31 | [form | acc] 32 | end 33 | 34 | {:function, _, _, _, _} = form, acc -> 35 | [form | acc] 36 | 37 | _, acc -> 38 | acc 39 | end) 40 | |> Enum.reverse() 41 | end 42 | 43 | ## Private Functions 44 | 45 | @spec filter_compile_options(option :: term()) :: term() 46 | defp filter_compile_options(:no_auto_import) do 47 | :no_auto_import 48 | end 49 | 50 | defp filter_compile_options({:no_auto_import, _} = option) do 51 | option 52 | end 53 | 54 | @spec filter_compile_options(options :: [term()]) :: [term()] 55 | defp filter_compile_options(options) when is_list(options) do 56 | Enum.filter(options, fn 57 | :no_auto_import -> 58 | true 59 | 60 | {:no_auto_import, _} -> 61 | true 62 | 63 | _ -> 64 | false 65 | end) 66 | end 67 | 68 | defp filter_compile_options(_) do 69 | [] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/patch/mock/code/transforms/export.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Transforms.Export do 2 | alias Patch.Mock.Code 3 | 4 | @generated [generated: true] 5 | 6 | @doc """ 7 | Transforms the provided forms to export the given list of functions. 8 | """ 9 | @spec transform( 10 | abstract_forms :: [Code.form()], 11 | exports :: Code.exports() 12 | ) :: [Code.form()] 13 | def transform(abstract_forms, exports) do 14 | abstract_forms 15 | |> Enum.reduce({[], false}, fn 16 | {:attribute, _, :export, _}, {acc, false} -> 17 | {[{:attribute, @generated, :export, exports} | acc], true} 18 | 19 | {:attribute, _, :export, _}, {acc, true} -> 20 | {acc, true} 21 | 22 | other, {acc, exported?} -> 23 | {[other | acc], exported?} 24 | end) 25 | |> elem(0) 26 | |> Enum.reverse() 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/patch/mock/code/transforms/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Transforms.Filter do 2 | @moduledoc """ 3 | Filters the functions and specifications in a module to the exports provided. 4 | """ 5 | 6 | alias Patch.Mock.Code 7 | 8 | @spec transform(abstract_forms :: [Code.form()], exports :: Code.exports()) :: [Code.form()] 9 | def transform(abstract_forms, exports) do 10 | Enum.filter(abstract_forms, fn 11 | {:attribute, _, :spec, {name_arity, _}} -> 12 | name_arity in exports 13 | 14 | {:function, _, name, arity, _} -> 15 | {name, arity} in exports 16 | 17 | _ -> 18 | true 19 | end) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/patch/mock/code/transforms/remote.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Transforms.Remote do 2 | alias Patch.Mock.Code 3 | 4 | @generated [generated: true] 5 | 6 | @doc """ 7 | Transforms the provided forms to rewrite any local call into a remote call to 8 | the provided `module`. 9 | """ 10 | @spec transform(abstract_forms :: [Code.form()], module :: module()) :: [Code.form()] 11 | def transform(abstract_forms, module) do 12 | exports = Code.Query.exports(abstract_forms) 13 | 14 | Enum.map(abstract_forms, fn 15 | {:function, anno, name, arity, clauses} -> 16 | {:function, anno, name, arity, clauses(clauses, module, exports)} 17 | 18 | other -> 19 | other 20 | end) 21 | end 22 | 23 | ## Private 24 | 25 | @spec clauses(abstract_forms :: [Code.form()], module :: module(), exports :: Code.exports()) :: 26 | [Code.form()] 27 | defp clauses(abstract_forms, module, exports) do 28 | Enum.map(abstract_forms, fn 29 | {:clause, anno, patterns, guards, body} -> 30 | {:clause, anno, patterns, guards, expressions(body, module, exports)} 31 | end) 32 | end 33 | 34 | @spec expression(abstract_form :: Code.form(), module :: module(), exports :: Code.exports()) :: 35 | Code.form() 36 | defp expression({:call, anno, {:remote, _, _, _} = remote, arguments}, module, exports) do 37 | {:call, anno, remote, expressions(arguments, module, exports)} 38 | end 39 | 40 | defp expression({:call, anno, local, arguments}, module, exports) do 41 | arity = Enum.count(arguments) 42 | 43 | case local do 44 | {:atom, _, name} -> 45 | if {name, arity} in exports do 46 | {:call, anno, {:remote, @generated, {:atom, @generated, module}, local}, 47 | expressions(arguments, module, exports)} 48 | else 49 | {:call, anno, local, expressions(arguments, module, exports)} 50 | end 51 | 52 | _ -> 53 | {:call, anno, local, expressions(arguments, module, exports)} 54 | end 55 | end 56 | 57 | defp expression({:block, anno, body}, module, exports) do 58 | {:block, anno, expressions(body, module, exports)} 59 | end 60 | 61 | defp expression({:case, anno, expression, clauses}, module, exports) do 62 | {:case, anno, expression(expression, module, exports), clauses(clauses, module, exports)} 63 | end 64 | 65 | defp expression({:catch, anno, expression}, module, exports) do 66 | {:catch, anno, expression(expression, module, exports)} 67 | end 68 | 69 | defp expression({:cons, anno, head, tail}, module, exports) do 70 | {:cons, anno, expression(head, module, exports), expression(tail, module, exports)} 71 | end 72 | 73 | defp expression({:fun, anno, {:function, name, arity}}, module, _) do 74 | {:fun, anno, 75 | {:function, {:atom, @generated, module}, {:atom, @generated, name}, 76 | {:integer, @generated, arity}}} 77 | end 78 | 79 | defp expression({:fun, anno, {:clauses, clauses}}, module, exports) do 80 | {:fun, anno, {:clauses, clauses(clauses, module, exports)}} 81 | end 82 | 83 | defp expression({:named_fun, anno, name, clauses}, module, exports) do 84 | {:named_fun, anno, name, clauses(clauses, module, exports)} 85 | end 86 | 87 | defp expression({:if, anno, clauses}, module, exports) do 88 | {:if, anno, clauses(clauses, module, exports)} 89 | end 90 | 91 | defp expression({:lc, anno, expression, qualifiers}, module, exports) do 92 | {:lc, anno, expression(expression, module, exports), expressions(qualifiers, module, exports)} 93 | end 94 | 95 | defp expression({:map, anno, associations}, module, exports) do 96 | {:map, anno, expressions(associations, module, exports)} 97 | end 98 | 99 | defp expression({:map, anno, expression, associations}, module, exports) do 100 | {:map, anno, expression(expression, module, exports), 101 | expressions(associations, module, exports)} 102 | end 103 | 104 | defp expression({:map_field_assoc, anno, key, value}, module, exports) do 105 | {:map_field_assoc, anno, expression(key, module, exports), expression(value, module, exports)} 106 | end 107 | 108 | defp expression({:map_field_exact, anno, key, value}, module, exports) do 109 | {:map_field_exact, anno, expression(key, module, exports), expression(value, module, exports)} 110 | end 111 | 112 | defp expression({:match, anno, pattern, expression}, module, exports) do 113 | {:match, anno, pattern, expression(expression, module, exports)} 114 | end 115 | 116 | defp expression({:op, anno, operation, operand_expression}, module, exports) do 117 | {:op, anno, operation, expression(operand_expression, module, exports)} 118 | end 119 | 120 | defp expression({:op, anno, operation, lhs_expression, rhs_expression}, module, exports) do 121 | {:op, anno, operation, expression(lhs_expression, module, exports), 122 | expression(rhs_expression, module, exports)} 123 | end 124 | 125 | defp expression({:receive, anno, clauses}, module, exports) do 126 | {:receive, anno, clauses(clauses, module, exports)} 127 | end 128 | 129 | defp expression({:receive, anno, clauses, timeout_expression, body}, module, exports) do 130 | {:receive, anno, clauses(clauses, module, exports), 131 | expression(timeout_expression, module, exports), expressions(body, module, exports)} 132 | end 133 | 134 | defp expression({:record, anno, name, fields}, module, exports) do 135 | {:record, anno, name, expressions(fields, module, exports)} 136 | end 137 | 138 | defp expression({:record, anno, expression, name, fields}, module, exports) do 139 | {:record, anno, expression(expression, module, exports), name, 140 | expressions(fields, module, exports)} 141 | end 142 | 143 | defp expression({:record_field, anno, field, expression}, module, exports) do 144 | {:record_field, anno, field, expression(expression, module, exports)} 145 | end 146 | 147 | defp expression({:record_field, anno, expression, name, field}, module, exports) do 148 | {:record_field, anno, expression(expression, module, exports), name, field} 149 | end 150 | 151 | defp expression({:tuple, anno, expressions}, module, exports) do 152 | {:tuple, anno, expressions(expressions, module, exports)} 153 | end 154 | 155 | defp expression({:try, anno, body, case_clauses, catch_clauses}, module, exports) do 156 | { 157 | :try, 158 | anno, 159 | expressions(body, module, exports), 160 | clauses(case_clauses, module, exports), 161 | clauses(catch_clauses, module, exports) 162 | } 163 | end 164 | 165 | defp expression({:try, anno, body, case_clauses, catch_clauses, after_body}, module, exports) do 166 | { 167 | :try, 168 | anno, 169 | expressions(body, module, exports), 170 | clauses(case_clauses, module, exports), 171 | clauses(catch_clauses, module, exports), 172 | expressions(after_body, module, exports) 173 | } 174 | end 175 | 176 | defp expression(other, _, _) do 177 | other 178 | end 179 | 180 | @spec expressions( 181 | abstract_forms :: [Code.form()], 182 | module :: module(), 183 | exports :: Code.exports() 184 | ) :: [Code.form()] 185 | defp expressions(abstract_forms, module, exports) do 186 | Enum.map(abstract_forms, &expression(&1, module, exports)) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/patch/mock/code/transforms/rename.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Transforms.Rename do 2 | alias Patch.Mock.Code 3 | 4 | @generated [generated: true] 5 | 6 | @doc """ 7 | Transforms the provided forms to rename the module to the provided module 8 | name. 9 | """ 10 | @spec transform(abstract_forms :: [Code.form()], module :: module) :: [Code.form()] 11 | def transform(abstract_forms, module) do 12 | Enum.map(abstract_forms, fn 13 | {:attribute, _, :module, _} -> 14 | {:attribute, @generated, :module, module} 15 | 16 | other -> 17 | other 18 | end) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/patch/mock/code/transforms/reroute.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Transforms.Reroute do 2 | alias Patch.Mock.Code 3 | 4 | @generated [generated: true] 5 | 6 | @doc """ 7 | Transforms the provided forms to rewrite any remote calls to the `source` module into remote 8 | calls to the `destination` module. 9 | """ 10 | @spec transform(abstract_forms :: [Code.form()], source :: module(), destination :: module()) :: [Code.form()] 11 | def transform(abstract_forms, source, destination) do 12 | Enum.map(abstract_forms, fn 13 | {:function, anno, name, arity, clauses} -> 14 | {:function, anno, name, arity, clauses(clauses, source, destination)} 15 | 16 | other -> 17 | other 18 | end) 19 | end 20 | 21 | ## Private 22 | 23 | @spec clauses(abstract_forms :: [Code.form()], source :: module(), destination :: module()) :: 24 | [Code.form()] 25 | defp clauses(abstract_forms, source, destination) do 26 | Enum.map(abstract_forms, fn 27 | {:clause, anno, patterns, guards, body} -> 28 | {:clause, anno, patterns, guards, expressions(body, source, destination)} 29 | end) 30 | end 31 | 32 | @spec expression(abstract_form :: Code.form(), source :: module(), destination :: module()) :: 33 | Code.form() 34 | defp expression({:call, _, {:remote, _, {:atom, _, source}, name}, arguments}, source, destination) do 35 | { 36 | :call, 37 | @generated, 38 | { 39 | :remote, 40 | @generated, 41 | {:atom, @generated, destination}, 42 | expression(name, source, destination) 43 | }, 44 | expressions(arguments, source, destination) 45 | } 46 | end 47 | 48 | defp expression({:call, call_anno, {:remote, remote_anno, module, name}, arguments}, source, destination) do 49 | { 50 | :call, 51 | call_anno, 52 | { 53 | :remote, 54 | remote_anno, 55 | expression(module, source, destination), 56 | expression(name, source, destination), 57 | }, 58 | expressions(arguments, source, destination) 59 | } 60 | end 61 | 62 | defp expression({:call, anno, local, arguments}, source, destination) do 63 | { 64 | :call, 65 | anno, 66 | expression(local, source, destination), 67 | expressions(arguments, source, destination) 68 | } 69 | end 70 | 71 | defp expression({:block, anno, body}, source, destination) do 72 | {:block, anno, expressions(body, source, destination)} 73 | end 74 | 75 | defp expression({:case, anno, expression, clauses}, source, destination) do 76 | {:case, anno, expression(expression, source, destination), clauses(clauses, source, destination)} 77 | end 78 | 79 | defp expression({:catch, anno, expression}, source, destination) do 80 | {:catch, anno, expression(expression, source, destination)} 81 | end 82 | 83 | defp expression({:cons, anno, head, tail}, source, destination) do 84 | {:cons, anno, expression(head, source, destination), expression(tail, source, destination)} 85 | end 86 | 87 | defp expression({:fun, _, {:function, {:atom, _, source}, name, arity}}, source, destination) do 88 | { 89 | :fun, 90 | @generated, 91 | { 92 | :function, 93 | {:atom, @generated, destination}, 94 | expression(name, source, destination), 95 | expression(arity, source, destination) 96 | } 97 | } 98 | end 99 | 100 | defp expression({:fun, anno, {:function, module, name, arity}}, source, destination) do 101 | { 102 | :fun, 103 | anno, 104 | { 105 | :function, 106 | expression(module, source, destination), 107 | expression(name, source, destination), 108 | expression(arity, source, destination) 109 | } 110 | } 111 | end 112 | 113 | defp expression({:fun, anno, {:clauses, clauses}}, source, destination) do 114 | {:fun, anno, {:clauses, clauses(clauses, source, destination)}} 115 | end 116 | 117 | defp expression({:named_fun, anno, name, clauses}, source, destination) do 118 | {:named_fun, anno, name, clauses(clauses, source, destination)} 119 | end 120 | 121 | defp expression({:if, anno, clauses}, source, destination) do 122 | {:if, anno, clauses(clauses, source, destination)} 123 | end 124 | 125 | defp expression({:lc, anno, expression, qualifiers}, source, destination) do 126 | { 127 | :lc, 128 | anno, 129 | expression(expression, source, destination), 130 | expressions(qualifiers, source, destination) 131 | } 132 | end 133 | 134 | defp expression({:map, anno, associations}, source, destination) do 135 | {:map, anno, expressions(associations, source, destination)} 136 | end 137 | 138 | defp expression({:map, anno, expression, associations}, source, destination) do 139 | { 140 | :map, 141 | anno, 142 | expression(expression, source, destination), 143 | expressions(associations, source, destination) 144 | } 145 | end 146 | 147 | defp expression({:map_field_assoc, anno, key, value}, source, destination) do 148 | { 149 | :map_field_assoc, 150 | anno, 151 | expression(key, source, destination), 152 | expression(value, source, destination) 153 | } 154 | end 155 | 156 | defp expression({:map_field_exact, anno, key, value}, source, destination) do 157 | { 158 | :map_field_exact, 159 | anno, 160 | expression(key, source, destination), 161 | expression(value, source, destination) 162 | } 163 | end 164 | 165 | defp expression({:match, anno, pattern, expression}, source, destination) do 166 | {:match, anno, pattern, expression(expression, source, destination)} 167 | end 168 | 169 | defp expression({:op, anno, operation, operand_expression}, source, destination) do 170 | {:op, anno, operation, expression(operand_expression, source, destination)} 171 | end 172 | 173 | defp expression({:op, anno, operation, lhs_expression, rhs_expression}, source, destination) do 174 | { 175 | :op, 176 | anno, 177 | operation, 178 | expression(lhs_expression, source, destination), 179 | expression(rhs_expression, source, destination) 180 | } 181 | end 182 | 183 | defp expression({:receive, anno, clauses}, source, destination) do 184 | {:receive, anno, clauses(clauses, source, destination)} 185 | end 186 | 187 | defp expression({:receive, anno, clauses, timeout_expression, body}, source, destination) do 188 | { 189 | :receive, 190 | anno, 191 | clauses(clauses, source, destination), 192 | expression(timeout_expression, source, destination), 193 | expressions(body, source, destination) 194 | } 195 | end 196 | 197 | defp expression({:record, anno, name, fields}, source, destination) do 198 | {:record, anno, name, expressions(fields, source, destination)} 199 | end 200 | 201 | defp expression({:record, anno, expression, name, fields}, source, destination) do 202 | { 203 | :record, 204 | anno, 205 | expression(expression, source, destination), 206 | name, 207 | expressions(fields, source, destination) 208 | } 209 | end 210 | 211 | defp expression({:record_field, anno, field, expression}, source, destination) do 212 | {:record_field, anno, field, expression(expression, source, destination)} 213 | end 214 | 215 | defp expression({:record_field, anno, expression, name, field}, source, destination) do 216 | {:record_field, anno, expression(expression, source, destination), name, field} 217 | end 218 | 219 | defp expression({:tuple, anno, expressions}, source, destination) do 220 | {:tuple, anno, expressions(expressions, source, destination)} 221 | end 222 | 223 | defp expression({:try, anno, body, case_clauses, catch_clauses}, source, destination) do 224 | { 225 | :try, 226 | anno, 227 | expressions(body, source, destination), 228 | clauses(case_clauses, source, destination), 229 | clauses(catch_clauses, source, destination) 230 | } 231 | end 232 | 233 | defp expression({:try, anno, body, case_clauses, catch_clauses, after_body}, source, destination) do 234 | { 235 | :try, 236 | anno, 237 | expressions(body, source, destination), 238 | clauses(case_clauses, source, destination), 239 | clauses(catch_clauses, source, destination), 240 | expressions(after_body, source, destination) 241 | } 242 | end 243 | 244 | defp expression(other, _, _) do 245 | other 246 | end 247 | 248 | @spec expressions( 249 | abstract_forms :: [Code.form()], 250 | source :: module(), 251 | destination :: module() 252 | ) :: [Code.form()] 253 | defp expressions(abstract_forms, source, destination) do 254 | Enum.map(abstract_forms, &expression(&1, source, destination)) 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /lib/patch/mock/code/unit.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Code.Unit do 2 | alias Patch.Mock.Code 3 | alias Patch.Mock.Naming 4 | 5 | @type t :: %__MODULE__{ 6 | abstract_forms: [Code.form()], 7 | compiler_options: [Code.compiler_option()], 8 | module: module(), 9 | sticky?: boolean() 10 | } 11 | defstruct [:abstract_forms, :compiler_options, :module, :sticky?] 12 | 13 | 14 | @spec purge(unit :: t()) :: :ok 15 | def purge(%__MODULE__{} = unit) do 16 | [ 17 | &Naming.delegate/1, 18 | &Naming.facade/1, 19 | &Naming.original/1 20 | ] 21 | |> Enum.each(fn factory -> 22 | unit.module 23 | |> factory.() 24 | |> Code.purge() 25 | end) 26 | end 27 | 28 | @spec restore(unit :: t()) :: :ok 29 | def restore(%__MODULE__{} = unit) do 30 | :ok = purge(unit) 31 | :ok = Code.compile(unit.abstract_forms, unit.compiler_options) 32 | 33 | if unit.sticky? do 34 | Code.stick_module(unit.module) 35 | end 36 | 37 | :ok 38 | end 39 | 40 | defimpl Inspect do 41 | import Inspect.Algebra 42 | 43 | def inspect(%Patch.Mock.Code.Unit{} = unit, opts) do 44 | concat([ 45 | "#Patch.Mock.Code.Unit<", 46 | to_doc(unit.module, opts), 47 | ">" 48 | ]) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/patch/mock/history.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.History do 2 | @type name :: atom() 3 | @type argument :: term() 4 | @type entry :: {name(), [argument()]} 5 | @type limit :: non_neg_integer() | :infinity 6 | @type sorting :: :asc | :desc 7 | 8 | @type t :: %__MODULE__{ 9 | count: non_neg_integer(), 10 | entries: [entry()], 11 | limit: limit() 12 | } 13 | defstruct count: 0, 14 | entries: [], 15 | limit: :infinity 16 | 17 | @spec new(limit :: limit()) :: t() 18 | def new(limit \\ :infinity) do 19 | %__MODULE__{limit: limit} 20 | end 21 | 22 | @spec entries(history :: t(), sorting :: sorting()) :: [entry()] 23 | def entries(history, sorting \\ :asc) 24 | 25 | def entries(%__MODULE__{} = history, :asc) do 26 | Enum.reverse(history.entries) 27 | end 28 | 29 | def entries(%__MODULE__{} = history, :desc) do 30 | history.entries 31 | end 32 | 33 | @spec put(history :: t(), name :: name(), arguments :: [argument()]) :: t() 34 | def put(%__MODULE__{limit: 0} = history, _name, _arguments) do 35 | # When the limit is 0, no-op. 36 | history 37 | end 38 | 39 | def put(%__MODULE__{limit: 1} = history, name, arguments) do 40 | # When the limit is 1 just set the entries directly 41 | %__MODULE__{history | count: 1, entries: [{name, arguments}]} 42 | end 43 | 44 | def put(%__MODULE__{limit: :infinity} = history, name, arguments) do 45 | # When the history is infinite just keep appending. 46 | %__MODULE__{ 47 | history 48 | | count: history.count + 1, 49 | entries: [{name, arguments} | history.entries] 50 | } 51 | end 52 | 53 | def put(%__MODULE__{limit: limit, count: count} = history, name, arguments) 54 | when count < limit do 55 | # When there is still slack capacity in the history, do a simple append. 56 | %__MODULE__{ 57 | history 58 | | count: history.count + 1, 59 | entries: [{name, arguments} | history.entries] 60 | } 61 | end 62 | 63 | def put(%__MODULE__{} = history, name, arguments) do 64 | # Buffer is bounded and out of slack capacity. 65 | entries = Enum.take([{name, arguments} | history.entries], history.limit) 66 | %__MODULE__{history | entries: entries} 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/patch/mock/history/tagged.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.History.Tagged do 2 | alias Patch.Mock.History 3 | 4 | @type tag :: boolean() 5 | @type entry :: {tag(), History.entry()} 6 | @type t :: [entry()] 7 | 8 | @doc """ 9 | Determine if any of the entries have been tagged in the affirmative 10 | """ 11 | @spec any?(tagged :: t()) :: boolean() 12 | def any?(tagged) do 13 | Enum.any?(tagged, &tag/1) 14 | end 15 | 16 | @doc """ 17 | Calculates the count of entries that have been tagged in the affirmative 18 | """ 19 | @spec count(tagged :: t()) :: non_neg_integer() 20 | def count(tagged) do 21 | tagged 22 | |> Enum.filter(&tag/1) 23 | |> Enum.count() 24 | end 25 | 26 | @doc """ 27 | Returns the first entry that was tagged in the affirmative 28 | """ 29 | @spec first(tagged :: t()) :: {:ok, History.entry()} | false 30 | def first(tagged) do 31 | Enum.find_value(tagged, fn 32 | {true, call} -> 33 | {:ok, call} 34 | 35 | _ -> 36 | false 37 | end) 38 | end 39 | 40 | def format(entries, module) do 41 | entries 42 | |> Enum.reverse() 43 | |> Enum.with_index(1) 44 | |> Enum.map(fn {{tag, {function, arguments}}, i} -> 45 | marker = 46 | if tag do 47 | "* " 48 | else 49 | " " 50 | end 51 | 52 | "#{marker}#{i}. #{inspect(module)}.#{function}(#{format_arguments(arguments)})" 53 | end) 54 | |> case do 55 | [] -> 56 | " [No Calls Received]" 57 | 58 | calls -> 59 | Enum.join(calls, "\n") 60 | end 61 | end 62 | 63 | @doc """ 64 | Construct a new Tagged History from a History and a Call. 65 | 66 | Every entry in the History will be tagged with either `true` if the entry 67 | matches the provided call or `false` if the entry does not match. 68 | """ 69 | @spec for_call(history :: Macro.t(), call :: Macro.t()) :: Macro.t() 70 | defmacro for_call(history, call) do 71 | {_, function, pattern} = Macro.decompose_call(call) 72 | 73 | quote do 74 | unquote(history) 75 | |> Patch.Mock.History.entries(:desc) 76 | |> Enum.map(fn 77 | {unquote(function), arguments} = call -> 78 | {Patch.Macro.match?(unquote(pattern), arguments), call} 79 | 80 | call -> 81 | {false, call} 82 | end) 83 | end 84 | end 85 | 86 | @doc """ 87 | Construct a new Tagged History from a History and a Function Name. 88 | 89 | Every entry in the History will be tagged with either `true` if the entry 90 | matches the provided Function Name or `false` if the entry does not match. 91 | """ 92 | @spec for_function(history :: History.t(), name :: History.name()) :: t() 93 | def for_function(%History{} = history, name) do 94 | history 95 | |> History.entries(:desc) 96 | |> Enum.map(fn 97 | {^name, _} = call -> 98 | {true, call} 99 | 100 | call -> 101 | {false, call} 102 | end) 103 | end 104 | 105 | @doc """ 106 | Returns the tag for the given tagged entry 107 | """ 108 | @spec tag(entry :: entry()) :: tag() 109 | def tag({tag, _}) do 110 | tag 111 | end 112 | 113 | ## Private 114 | 115 | @spec format_arguments(arguments :: [term()]) :: String.t() 116 | defp format_arguments(arguments) do 117 | arguments 118 | |> Enum.map(&Kernel.inspect/1) 119 | |> Enum.join(", ") 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/patch/mock/naming.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Naming do 2 | @doc """ 3 | Canonical name for the delegate module for a provided module. 4 | """ 5 | @spec delegate(module :: module()) :: module() 6 | def delegate(module) do 7 | Module.concat(Patch.Mock.Delegate.For, module) 8 | end 9 | 10 | @doc """ 11 | Canonical name for the facade module for a provided module. 12 | 13 | The facade module simply takes on the name of the provided module. 14 | """ 15 | @spec facade(module :: module()) :: module() 16 | def facade(module) do 17 | module 18 | end 19 | 20 | @doc """ 21 | Canonical name for the frozen module for a provided module. 22 | """ 23 | @spec frozen(module :: module()) :: module() 24 | def frozen(module) do 25 | Module.concat(Patch.Mock.Frozen.For, module) 26 | end 27 | 28 | @doc """ 29 | Canonical name for the original module for a provided module. 30 | """ 31 | @spec original(module :: module()) :: module() 32 | def original(module) do 33 | Module.concat(Patch.Mock.Original.For, module) 34 | end 35 | 36 | @doc """ 37 | Canonical name for the server process for a provided module. 38 | """ 39 | @spec server(module :: module()) :: GenServer.name() 40 | def server(module) do 41 | Module.concat(Patch.Mock.Server.For, module) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/patch/mock/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Supervisor do 2 | use DynamicSupervisor 3 | 4 | def start_link(_ \\ []) do 5 | DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) 6 | end 7 | 8 | def start_child(module, options) do 9 | DynamicSupervisor.start_child( 10 | __MODULE__, 11 | {Patch.Mock.Server, module: module, options: options} 12 | ) 13 | end 14 | 15 | def init(:ok) do 16 | DynamicSupervisor.init(strategy: :one_for_one) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/patch/mock/values/callable.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Values.Callable do 2 | alias Patch.Apply 3 | 4 | @type dispatch_mode :: :apply | :list 5 | @type evaluate_mode :: :passthrough | :strict 6 | 7 | @type dispatch_option :: {:dispatch, dispatch_mode()} 8 | @type evaluate_option :: {:evaluate, evaluate_mode()} 9 | 10 | @type option :: dispatch_option() | evaluate_option() 11 | 12 | @type t :: %__MODULE__{ 13 | dispatch: dispatch_mode(), 14 | evaluate: evaluate_mode(), 15 | target: function() 16 | } 17 | defstruct [:dispatch, :evaluate, :target] 18 | 19 | @spec advance(callable :: t()) :: t() 20 | def advance(callable) do 21 | callable 22 | end 23 | 24 | @spec new(target :: function(), options :: [option()]) :: t() 25 | def new(target, options \\ []) do 26 | options = validate_options!(options) 27 | 28 | %__MODULE__{ 29 | dispatch: options[:dispatch], 30 | evaluate: options[:evaluate], 31 | target: target 32 | } 33 | end 34 | 35 | @spec next(callable :: t(), arguments :: [term()]) :: {:ok, t(), term()} | :error 36 | def next(%__MODULE__{} = callable, arguments) do 37 | arguments 38 | |> dispatch(callable) 39 | |> evaluate(callable) 40 | end 41 | 42 | ## Private 43 | 44 | @spec dispatch(arguments :: [term()], callable :: t()) :: [term()] 45 | defp dispatch(arguments, %__MODULE__{dispatch: :apply}) do 46 | arguments 47 | end 48 | 49 | defp dispatch(arguments, %__MODULE__{dispatch: :list}) do 50 | [arguments] 51 | end 52 | 53 | @spec evaluate(arguments :: [term()], callable :: t()) :: {:ok, t(), term()} | :error 54 | defp evaluate(arguments, %__MODULE__{evaluate: :passthrough} = callable) do 55 | with {:ok, result} <- Apply.safe(callable.target, arguments) do 56 | {:ok, callable, result} 57 | end 58 | end 59 | 60 | defp evaluate(arguments, %__MODULE__{evaluate: :strict} = callable) do 61 | {:ok, callable, apply(callable.target, arguments)} 62 | end 63 | 64 | 65 | 66 | @spec validate_options!(options :: [option()]) :: [option()] 67 | defp validate_options!(options) do 68 | {dispatch, options} = Keyword.pop(options, :dispatch, :apply) 69 | {evaluate, options} = Keyword.pop(options, :evaluate, :passthrough) 70 | 71 | unless Enum.empty?(options) do 72 | unexpected_options = 73 | options 74 | |> Keyword.keys() 75 | |> Enum.map(&inspect/1) 76 | |> Enum.join(" \n - ") 77 | 78 | 79 | message = """ 80 | \n 81 | Callable contains unexpected options: 82 | 83 | #{unexpected_options} 84 | """ 85 | 86 | raise Patch.ConfigurationError, message: message 87 | end 88 | 89 | unless dispatch in [:apply, :list] do 90 | message = "Invalid :dispatch option #{inspect(dispatch)}. Must be one of [:apply, :list]" 91 | raise Patch.ConfigurationError, message: message 92 | end 93 | 94 | unless evaluate in [:passthrough, :strict] do 95 | message = 96 | "Invalid :evaluate option #{inspect(evaluate)}. Must be one of [:passthrough, :strict]" 97 | raise Patch.ConfigurationError, message: message 98 | end 99 | 100 | [dispatch: dispatch, evaluate: evaluate] 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/patch/mock/values/callable_stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Values.CallableStack do 2 | alias Patch.Mock.Values.Callable 3 | 4 | @type t :: %__MODULE__{ 5 | stack: [Callable.t()] 6 | } 7 | defstruct [:stack] 8 | 9 | @spec advance(stack :: t()) :: t() 10 | def advance(stack) do 11 | stack 12 | end 13 | 14 | @spec new(stack :: [Callable.t()]) :: t() 15 | def new(stack) do 16 | %__MODULE__{stack: stack} 17 | end 18 | 19 | @spec next(stack :: t(), arguments :: [term()]) :: {:ok, t(), term()} | :error 20 | def next(%__MODULE__{} = stack, arguments) do 21 | Enum.reduce_while(stack.stack, :error, fn callable, acc -> 22 | case Callable.next(callable, arguments) do 23 | {:ok, _callable, result} -> 24 | {:halt, {:ok, stack, result}} 25 | 26 | :error -> 27 | {:cont, acc} 28 | end 29 | end) 30 | end 31 | 32 | @spec push(stack :: t(), callable :: Callable.t()) :: t() 33 | def push(%__MODULE__{} = stack, %Callable{} = callable) do 34 | %__MODULE__{stack | stack: [callable | stack.stack]} 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/patch/mock/values/cycle.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Values.Cycle do 2 | @type t :: %__MODULE__{ 3 | values: [term()] 4 | } 5 | defstruct [:values] 6 | 7 | def advance(%__MODULE__{values: []} = cycle) do 8 | cycle 9 | end 10 | 11 | def advance(%__MODULE__{values: [_]} = cycle) do 12 | cycle 13 | end 14 | 15 | def advance(%__MODULE__{values: [head | rest]} = cycle) do 16 | %__MODULE__{cycle | values: rest ++ [head]} 17 | end 18 | 19 | @spec new(values :: [term()]) :: t() 20 | def new(values) do 21 | %__MODULE__{values: values} 22 | end 23 | 24 | @spec next(t(), arguments :: [term()]) :: {:ok, t(), term()} | :error 25 | def next(%__MODULE__{values: []} = cycle, _arguments) do 26 | {:ok, cycle, nil} 27 | end 28 | 29 | def next(%__MODULE__{values: [value]} = cycle, _arguments) do 30 | {:ok, cycle, value} 31 | end 32 | 33 | def next(%__MODULE__{values: [head | rest]} = cycle, _arguments) do 34 | cycle = %__MODULE__{cycle | values: rest ++ [head]} 35 | {:ok, cycle, head} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/patch/mock/values/raises.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Values.Raises do 2 | @type t :: %__MODULE__{ 3 | exception: module(), 4 | attributes: Keyword.t() 5 | } 6 | defstruct [:exception, :attributes] 7 | 8 | @spec advance(raises :: t()) :: t() 9 | def advance(%__MODULE__{} = raises) do 10 | raises 11 | end 12 | 13 | @spec new(message :: String.t()) :: t() 14 | def new(message) do 15 | new(RuntimeError, message: message) 16 | end 17 | 18 | @spec new(exception :: module(), attributes :: Keyword.t()) :: t() 19 | def new(exception, attributes) do 20 | %__MODULE__{exception: exception, attributes: attributes} 21 | end 22 | 23 | @spec next(raises :: t(), arguments :: [term()]) :: none() 24 | def next(%__MODULE__{} = raises, _arguments) do 25 | raise raises.exception, raises.attributes 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/patch/mock/values/scalar.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Values.Scalar do 2 | @type t :: %__MODULE__{ 3 | value: term() 4 | } 5 | defstruct [:value] 6 | 7 | @spec advance(scalar :: t()) :: t() 8 | def advance(%__MODULE__{} = scalar) do 9 | scalar 10 | end 11 | 12 | @spec new(scalar :: term()) :: t() 13 | def new(scalar) do 14 | %__MODULE__{value: scalar} 15 | end 16 | 17 | @spec next(scalar :: t(), arguments :: [term()]) :: {:ok, t(), term()} | :error 18 | def next(%__MODULE__{} = scalar, _arguments) do 19 | {:ok, scalar, scalar.value} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/patch/mock/values/sequence.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Values.Sequence do 2 | @type t :: %__MODULE__{ 3 | values: [term()] 4 | } 5 | defstruct [:values] 6 | 7 | @spec advance(sequence :: t()) :: t() 8 | def advance(%__MODULE__{values: []} = sequence) do 9 | sequence 10 | end 11 | 12 | def advance(%__MODULE__{values: [_]} = sequence) do 13 | sequence 14 | end 15 | 16 | def advance(%__MODULE__{values: [_ | rest]} = sequence) do 17 | %__MODULE__{sequence | values: rest} 18 | end 19 | 20 | @spec new(values :: [term()]) :: t() 21 | def new(values) do 22 | %__MODULE__{values: values} 23 | end 24 | 25 | @spec next(sequence :: t(), arguments :: [term()]) :: {:ok, t(), term()} | :error 26 | def next(%__MODULE__{values: []} = sequence, _arguments) do 27 | {:ok, sequence, nil} 28 | end 29 | 30 | def next(%__MODULE__{values: [last]} = sequence, _arguments) do 31 | {:ok, sequence, last} 32 | end 33 | 34 | def next(%__MODULE__{values: [head | rest]} = sequence, _arguments) do 35 | sequence = %__MODULE__{sequence | values: rest} 36 | {:ok, sequence, head} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/patch/mock/values/throws.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Mock.Values.Throws do 2 | @type t :: %__MODULE__{ 3 | value: term() 4 | } 5 | defstruct [:value] 6 | 7 | @spec advance(throws :: t()) :: t() 8 | def advance(%__MODULE__{} = throws) do 9 | throws 10 | end 11 | 12 | @spec new(throws :: term()) :: t() 13 | def new(throws) do 14 | %__MODULE__{value: throws} 15 | end 16 | 17 | @spec next(throws :: t(), arguments :: [term()]) :: none() 18 | def next(%__MODULE__{} = throws, _arguments) do 19 | throw throws.value 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/patch/reflection.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Reflection do 2 | @spec find_functions(module :: module()) :: Keyword.t(arity()) 3 | def find_functions(module) do 4 | Code.ensure_loaded(module) 5 | 6 | cond do 7 | function_exported?(module, :__info__, 1) -> 8 | module.__info__(:functions) 9 | 10 | function_exported?(module, :module_info, 1) -> 11 | module.module_info(:exports) 12 | 13 | true -> 14 | [] 15 | end 16 | end 17 | 18 | @spec find_arities(module :: module(), function :: function()) :: [arity()] 19 | def find_arities(module, function) do 20 | module 21 | |> find_functions() 22 | |> Keyword.get_values(function) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/patch/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(_ \\ []) do 5 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) 6 | end 7 | 8 | def init(:ok) do 9 | children = [ 10 | Patch.Listener.Supervisor, 11 | Patch.Mock.Supervisor 12 | ] 13 | 14 | Supervisor.init(children, strategy: :one_for_one) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :patch, 7 | version: "0.16.0", 8 | elixir: "~> 1.9", 9 | erlc_paths: erlc_paths(Mix.env()), 10 | elixirc_paths: elixirc_paths(Mix.env()), 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | docs: docs(), 14 | package: package() 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:ex_doc, "~> 0.34.2", only: :dev, runtime: false}, 29 | ] 30 | end 31 | 32 | # Specifies which erlang paths to compile per environment 33 | defp erlc_paths(:test), do: ["test/support"] 34 | defp erlc_paths(_), do: [] 35 | 36 | # Specifies which elixir paths to compile per environment. 37 | defp elixirc_paths(:test), do: ["lib", "test/support"] 38 | defp elixirc_paths(_), do: ["lib"] 39 | 40 | defp docs do 41 | [ 42 | name: "Patch", 43 | extras: [ 44 | "README.md", 45 | "pages/cheatsheet.cheatmd", 46 | "pages/super-powers.md", 47 | "pages/guide-book/01-introduction.md", 48 | "pages/guide-book/02-patching.md", 49 | "pages/guide-book/03-mock-values.md", 50 | "pages/guide-book/04-spies-and-fakes.md", 51 | "pages/guide-book/05-processes.md", 52 | "CHANGELOG.md" 53 | ], 54 | groups_for_extras: [ 55 | "Guide Book": [ 56 | "pages/guide-book/01-introduction.md", 57 | "pages/guide-book/02-patching.md", 58 | "pages/guide-book/03-mock-values.md", 59 | "pages/guide-book/04-spies-and-fakes.md", 60 | "pages/guide-book/05-processes.md" 61 | ] 62 | ], 63 | groups_for_modules: [ 64 | "Developer Interface": [ 65 | Patch 66 | ], 67 | Listener: [ 68 | Patch.Listener, 69 | Patch.Listener.Supervisor 70 | ], 71 | Mock: [ 72 | Patch.Mock, 73 | Patch.Mock.History, 74 | Patch.Mock.History.Tagged, 75 | Patch.Mock.Naming, 76 | Patch.Mock.Server, 77 | Patch.Mock.Supervisor 78 | ], 79 | "Mock Code": [ 80 | Patch.Mock.Code, 81 | Patch.Mock.Code.Freezer, 82 | Patch.Mock.Code.Generate, 83 | Patch.Mock.Code.Query, 84 | Patch.Mock.Code.Transform, 85 | Patch.Mock.Code.Unit 86 | ], 87 | "Mock Code Generators": [ 88 | Patch.Mock.Code.Generators.Delegate, 89 | Patch.Mock.Code.Generators.Facade, 90 | Patch.Mock.Code.Generators.Frozen, 91 | Patch.Mock.Code.Generators.Original 92 | ], 93 | "Mock Code Queries": [ 94 | Patch.Mock.Code.Queries.Exports, 95 | Patch.Mock.Code.Queries.Functions 96 | ], 97 | "Mock Code Transforms": [ 98 | Patch.Mock.Code.Transforms.Clean, 99 | Patch.Mock.Code.Transforms.Export, 100 | Patch.Mock.Code.Transforms.Filter, 101 | Patch.Mock.Code.Transforms.Remote, 102 | Patch.Mock.Code.Transforms.Rename, 103 | Patch.Mock.Code.Transforms.Reroute 104 | ], 105 | "Mock Values": [ 106 | Patch.Mock.Value, 107 | Patch.Mock.Values.Callable, 108 | Patch.Mock.Values.CallableStack, 109 | Patch.Mock.Values.Cycle, 110 | Patch.Mock.Values.Raises, 111 | Patch.Mock.Values.Scalar, 112 | Patch.Mock.Values.Sequence, 113 | Patch.Mock.Values.Throws 114 | ], 115 | Utilities: [ 116 | Patch.Access, 117 | Patch.Apply, 118 | Patch.Assertions, 119 | Patch.Macro, 120 | Patch.Reflection, 121 | Patch.Supervisor 122 | ] 123 | ], 124 | main: "readme", 125 | source_ref: "master", 126 | source_url: "https://github.com/ihumanable/patch" 127 | ] 128 | end 129 | 130 | defp package() do 131 | [ 132 | description: "Ergonomic Mocking for Elixir Unit Testing", 133 | licenses: ["MIT"], 134 | maintainers: ["Matt Nowack"], 135 | links: %{ 136 | "GitHub" => "https://github.com/ihumanable/patch" 137 | } 138 | ] 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 3 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 4 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 8 | } 9 | -------------------------------------------------------------------------------- /pages/cheatsheet.cheatmd: -------------------------------------------------------------------------------- 1 | # Cheatsheet 2 | 3 | This cheatsheet provides simple examples for how to use Patch, for more details see the linked documentation. 4 | 5 | ## Installation 6 | {: .col-2} 7 | 8 | ### Add Patch to your Dependencies 9 | 10 | In the `deps/0` function in the mix.exs file add a line for Patch. 11 | 12 | #### mix.exs 13 | ```elixir 14 | def deps do 15 | [ 16 | {:patch, "~> 0.15.0", only: [:test]} 17 | ] 18 | end 19 | ``` 20 | 21 | ### Optionally Including / Excluding Imports 22 | 23 | `:only` will cause only a subset of symbols to be imported 24 | 25 | #### test/example_only_test.exs 26 | ```elixir 27 | defmodule ExampleOnlyTest do 28 | use ExUnit.Case 29 | use Patch, only: [:expose, :patch, :private] 30 | 31 | # ... snip the rest of the module ... 32 | end 33 | ``` 34 | 35 | `:except` will import all symbols except the ones specified 36 | 37 | #### test/example_except_test.exs 38 | ```elixir 39 | defmodule ExampleExceptTest do 40 | use ExUnit.Case 41 | use Patch, except: [:fake, :history] 42 | 43 | # ... snip the rest of the module ... 44 | end 45 | ``` 46 | 47 | ### Use Patch in your Test Case 48 | 49 | In any ExUnit.Case based Test Case add a line to use Patch. 50 | 51 | #### test/example_test.exs 52 | ```elixir 53 | defmodule ExampleTest do 54 | use ExUnit.Case 55 | use Patch 56 | 57 | # ... snip the rest of the module ... 58 | end 59 | ``` 60 | 61 | ### Aliasing Imports 62 | 63 | `:alias` allows the test author to import a symbol while renaming it. 64 | 65 | #### test/example_alias_test.exs 66 | ```elixir 67 | defmodule ExampleAliasTest do 68 | use ExUnit.Case 69 | use Patch, alias: [patch: :mock] 70 | 71 | # ... snip the rest of the module ... 72 | end 73 | ``` 74 | 75 | ## Patching 76 | {: .col-2} 77 | 78 | ### Scalars 79 | 80 | ```elixir 81 | test "can patch with scalar values" do 82 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 83 | 84 | patch(String, :upcase, "PATCHED") 85 | 86 | assert String.upcase("Post-Patched") == "PATCHED" 87 | end 88 | ``` 89 | 90 | ### Callables 91 | 92 | ```elixir 93 | test "can patch with a callable" do 94 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 95 | 96 | patch(String, :upcase, fn s -> String.length(s) end) 97 | 98 | assert String.upcase("Post-Patched") == 12 99 | end 100 | ``` 101 | 102 | ### Cycles 103 | 104 | ```elixir 105 | test "can patch with a cycle" do 106 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 107 | 108 | patch(String, :upcase, cycle([1, 2, 3])) 109 | 110 | assert String.upcase("Post-Patched") == 1 111 | assert String.upcase("Post-Patched") == 2 112 | assert String.upcase("Post-Patched") == 3 113 | assert String.upcase("Post-Patched") == 1 114 | assert String.upcase("Post-Patched") == 2 115 | end 116 | ``` 117 | 118 | ### Sequences 119 | 120 | ```elixir 121 | test "can patch with a sequence" do 122 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 123 | 124 | patch(String, :upcase, sequence([1, 2, 3])) 125 | 126 | assert String.upcase("Post-Patched") == 1 127 | assert String.upcase("Post-Patched") == 2 128 | assert String.upcase("Post-Patched") == 3 129 | assert String.upcase("Post-Patched") == 3 130 | assert String.upcase("Post-Patched") == 3 131 | end 132 | ``` 133 | 134 | ### Raises 135 | 136 | ```elixir 137 | test "can patch to raise a RuntimeError" do 138 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 139 | 140 | patch(String, :upcase, raises("patched")) 141 | 142 | assert_raise RuntimeError, "patched", fn -> 143 | String.upcase("Post-Patched") 144 | end 145 | end 146 | 147 | test "can patch to raise any exception" do 148 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 149 | 150 | patch(String, :upcase, raises(ArgumentError, message: "patched")) 151 | 152 | assert_raise ArgumentError, "patched", fn -> 153 | String.upcase("Post-Patched") 154 | end 155 | end 156 | ``` 157 | 158 | ### Throws 159 | 160 | ```elixir 161 | test "can patch to throw a value" do 162 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 163 | 164 | patch(String, :upcase, throws(:patched)) 165 | 166 | assert catch_throw(String.upcase("Post-Patched")) == :patched 167 | end 168 | ``` 169 | 170 | ## Assertions 171 | {: .col-2} 172 | 173 | ### assert_called 174 | 175 | ```elixir 176 | test "can assert calls on patched functions" do 177 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 178 | 179 | patch(String, :upcase, "PATCHED") 180 | 181 | assert String.upcase("Post-Patched") == "PATCHED" 182 | assert_called String.upcase("Post-Patched") 183 | 184 | ## Arguments can be bound or pattern-matched 185 | assert_called String.upcase(argument) 186 | assert argument == "Post-Patched" 187 | 188 | ## The number of calls can be specified 189 | assert_called String.upcase("Post-Patched"), 1 190 | end 191 | ``` 192 | 193 | ### refute_called 194 | 195 | ```elixir 196 | test "can refute calls on patched functions" do 197 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 198 | 199 | patch(String, :upcase, "PATCHED") 200 | 201 | assert String.upcase("Post-Patched") == "PATCHED" 202 | refute_called String.upcase("Other") 203 | end 204 | ``` 205 | 206 | ### assert_any_call 207 | 208 | ```elixir 209 | test "can assert that a patched function was called with any arity" do 210 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 211 | 212 | patch(String, :upcase, "PATCHED") 213 | 214 | assert String.upcase("Post-Patched") == "PATCHED" 215 | assert_any_call String, :upcase 216 | end 217 | ``` 218 | 219 | ### refute_any_call 220 | 221 | ```elixir 222 | test "can refute that a patched function was called with any arity" do 223 | assert String.upcase("Pre-Patched") == "PRE-PATCHED" 224 | 225 | patch(String, :upcase, "PATCHED") 226 | 227 | refute_any_call String, :upcase 228 | end 229 | ``` 230 | 231 | ### spy 232 | 233 | ```elixir 234 | test "can assert / refute calls on spied modules without changing behavior" do 235 | spy(String) 236 | 237 | assert String.upcase("Example") == "EXAMPLE" 238 | 239 | assert_called String.upcase("Example") 240 | refute_called String.upcase("Other") 241 | end 242 | ``` 243 | 244 | ### history 245 | 246 | ```elixir 247 | test "can retrieve the list of all calls to a patched module" do 248 | spy(String) 249 | 250 | assert String.upcase("Example") == "EXAMPLE" 251 | assert String.downcase("Example") == "example" 252 | 253 | assert history(String) == [{:upcase, ["Example"]}, {:downcase, ["Example"]}] 254 | assert history(String, :asc) == [{:upcase, ["Example"]}, {:downcase, ["Example"]}] 255 | assert history(String, :desc) == [{:downcase, ["Example"]}, {:upcase, ["Example"]}] 256 | end 257 | ``` 258 | 259 | ## Private Functions 260 | {: .col-2} 261 | 262 | ### expose 263 | 264 | ```elixir 265 | test "can expose private functions for testing" do 266 | expose(Example, private_function: 1) 267 | 268 | assert Example.private_function(:argument) == {:ok, :argument} 269 | end 270 | ``` 271 | 272 | ### private 273 | 274 | ```elixir 275 | test "can suppress warnings about calling private functions" do 276 | expose(Example, private_function: 1) 277 | 278 | assert private(Example.private_function(:argument)) == {:ok, :argument} 279 | end 280 | ``` 281 | 282 | ## Processes 283 | {: .col-2} 284 | 285 | ### listen 286 | 287 | ```elixir 288 | test "can listen to the messages sent to a named process" do 289 | listen(:tag, ExampleNamedProcess) 290 | 291 | send(ExampleNamedProcess, :hello) 292 | 293 | assert_receive {:tag, :hello} 294 | end 295 | ``` 296 | 297 | ```elixir 298 | test "can listen to the messages sent to a pid" do 299 | pid = Example.start_link() 300 | 301 | listen(:tag, pid) 302 | 303 | send(pid, :hello) 304 | 305 | assert_receive {:tag, :hello} 306 | end 307 | ``` 308 | 309 | ```elixir 310 | test "can listen to GenServer messages" do 311 | Counter.start_link(0, name: Counter) 312 | 313 | listen(:tag, Counter) 314 | 315 | assert Counter.increment() == 1 316 | 317 | assert_receive {:tag, {GenServer, :call, :increment, from}} # Bind `from` 318 | assert_receive {:tag, {GenServer, :reply, 1, ^from}} # Match the pinned `from` 319 | end 320 | ``` 321 | 322 | ### inject 323 | 324 | ```elixir 325 | test "listeners can be injected into another processes state" do 326 | {:ok, parent_pid} = Parent.start_link() 327 | 328 | inject(:tag, parent_pid, [:child_pid]) 329 | 330 | assert Parent.ask_child() == :ok 331 | 332 | assert_recieve {:tag, :ask} 333 | end 334 | ``` 335 | 336 | ### replace 337 | 338 | ```elixir 339 | test "process state can be replaced by key" do 340 | {:ok, pid} = Example.start_link() 341 | 342 | assert :sys.get_state(pid).field == :original 343 | 344 | replace(pid, [:field], :updated) 345 | 346 | assert :sys.get_state(pid).field == :updated 347 | end 348 | ``` -------------------------------------------------------------------------------- /pages/guide-book/01-introduction.md: -------------------------------------------------------------------------------- 1 | # Chapter 1: Introduction 2 | 3 | This guide book will walk you through using all the features and functionality of `Patch` to make great unit tests. 4 | 5 | ## One Big Idea 6 | 7 | Patch is founded on One Big Idea 8 | 9 | > Patched functions should **always** return the mock value they are given. 10 | 11 | This is how patched functions behave in `Patch`. Every function is a valid target for patching and once patched the function will always return the mock value given. If you ever feel lost, remember this One Big Idea. 12 | 13 | ## Terminology 14 | 15 | Throughout this guide book we are going to use some common terminology, let's define them. 16 | 17 | ### Patch 18 | 19 | Patch is used as both a verb and a noun. To "patch" a function is to replace it with an alternative implementation. The alternative implementation is the noun form of "patch" sometimes called the "patched function." 20 | 21 | ```elixir 22 | patch(Example, :example, :patched) 23 | ``` 24 | 25 | > `Example.example` has been "patched" with a "patch" that always returns the value `:patched` 26 | 27 | ### Mock Value 28 | 29 | The value returned by a "patch" is referred to as the "mock value". There are a number of types of "mock values" that will be covered in detail in this guide book. 30 | 31 | ### Observed Call 32 | 33 | After a module has been patched, the calls to the module can be observed by `Patch`. `Patch` comes with utilities to assert or refute that certain calls have been observed. 34 | 35 | Calls that happen before the module has been patched are unobserved, the test author can not assert or refute anything about the calls to a module before it has been patched. 36 | -------------------------------------------------------------------------------- /pages/guide-book/03-mock-values.md: -------------------------------------------------------------------------------- 1 | # Chapter 3: Mock Values 2 | 3 | In [Chapter 2: Patching](02-patching.html) we covered two kinds of mock values, Callables and Scalars. 4 | 5 | There are 5 other kinds of mock values available for use in a test. 6 | 7 | ## Cycle Values 8 | 9 | Cycle Values will endlessly cycle through a list of return values. 10 | 11 | When a patched function has a `Values.Cycle` as its mock value, it will provide the first value in the cycle and then move the first value to the end of the cycle on every invocation. 12 | 13 | Consider a function patched with `cycle([1, 2, 3])` via the following code 14 | 15 | ```elixir 16 | patch(Example, :example, cycle([1, 2, 3])) 17 | ``` 18 | 19 | | Invocation | Cycle Before Call | Return Value | Cycle After Call | 20 | |------------|-------------------|--------------|------------------| 21 | | 1 | [1, 2, 3] | 1 | [2, 3, 1] | 22 | | 2 | [2, 3, 1] | 2 | [3, 1, 2] | 23 | | 3 | [3, 1, 2] | 3 | [1, 2, 3] | 24 | | 4 | [1, 2, 3] | 1 | [2, 3, 1] | 25 | | 5 | [2, 3, 1] | 2 | [3, 1, 2] | 26 | | 6 | [3, 1, 2] | 3 | [1, 2, 3] | 27 | | 7 | [1, 2, 3] | 1 | [2, 3, 1] | 28 | 29 | We could continue the above table forever since the cycle will repeat endlessly. Cycles can contain `callable/1,2`, `raise/1,2` and `throw/1` mock values. 30 | 31 | We could create a patch that raises a RuntimeError every other call. 32 | 33 | ```elixir 34 | patch(Example, :example, cycle([:ok, raises("broken")])) 35 | ``` 36 | 37 | This can be helpful for testing retry and backoff constructs, a cycle like this is a good simulation of an unreliable network or dependency. 38 | 39 | ## Sequence Values 40 | 41 | Sequence values are similar to cycles, but instead of cycling the list is consumed until only one element is remaining. Once the sequence has only a single element remaining, that element will be returned on all subsequent calls. 42 | 43 | Consider a function patched with `sequence([1, 2, 3])` via the following code 44 | 45 | ```elixir 46 | patch(Example, :example, sequence([1, 2, 3])) 47 | ``` 48 | 49 | | Invocation | Sequence Before Call | Return Value | Sequence After Call | 50 | |------------|----------------------|--------------|---------------------| 51 | | 1 | [1, 2, 3] | 1 | [2, 3] | 52 | | 2 | [2, 3] | 2 | [3] | 53 | | 3 | [3] | 3 | [3] | 54 | | 4 | [3] | 3 | [3] | 55 | | 5 | [3] | 3 | [3] | 56 | 57 | We could continue the above table forever since the sequence will continue to return the last value endlessly. Sequences can contain `callable/1,2`, `raise/1,2` and `throw/1` mock values. 58 | 59 | There is one special behavior of sequence, and that's an empty sequence, which always returns the value `nil` on every invocation. 60 | 61 | If the test author would like to simulate an exhaustable sequence, one that returns a set number of items and then responds to every other call with `nil`, they can simply add a `nil` as the last element in the sequence 62 | 63 | ```elixir 64 | patch(Example, :example, sequence([1, 2, 3, nil]) 65 | ``` 66 | 67 | | Invocation | Sequence Before Call | Return Value | Sequence After Call | 68 | |------------|----------------------|--------------|---------------------| 69 | | 1 | [1, 2, 3, nil] | 1 | [2, 3, nil] | 70 | | 2 | [2, 3, nil] | 2 | [3, nil] | 71 | | 3 | [3, nil] | 3 | [nil] | 72 | | 4 | [nil] | nil | [nil] | 73 | | 5 | [nil] | nil | [nil] | 74 | 75 | ## Raises Value 76 | 77 | When a function can fail by raising an exception we can use `raises/1,2` to have the patched function raise. 78 | 79 | `raise/1` creates a special `Values.Callable` to be used as a mock value. 80 | 81 | This callable ignores the arguments passed in and unconditionally raises a `RuntimeError` with the 82 | given message. 83 | 84 | ```elixir 85 | patch(Example, :example, raises("patched")) 86 | 87 | assert_raise RuntimeError, "patched", fn -> 88 | Example.example() 89 | end 90 | ``` 91 | 92 | `raise/2` creates a special `Values.Callable` to be used as a mock value. 93 | 94 | This callable ignores the arguments passed in and unconditionally raises the specified exception with the given attributes. 95 | 96 | ```elixir 97 | patch(Example, :example, raises(ArgumentError, message: "patched")) 98 | 99 | assert_raise ArgumentError, "patched", fn -> 100 | Example.example() 101 | end 102 | ``` 103 | 104 | ## Throws Value 105 | 106 | When a function can fail by raising an exception we can use `throws/1` to have the patched function throw. 107 | 108 | `throws/1` creates a special `Values.Callable` to be used as a mock value. 109 | 110 | This callable ignores the arguments passed in and unconditionally throws the given value. 111 | 112 | ```elixir 113 | patch(Example, :example, throws(:patched)) 114 | 115 | assert catch_throw(Example.example()) == :patched 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /pages/guide-book/04-spies-and-fakes.md: -------------------------------------------------------------------------------- 1 | # Chapter 4: Spies and Fakes 2 | 3 | In [Chapter 2: Patching](02-patching.html) and [Chapter 3: Mock Values](03-mock-values.html) we saw how we can patch functions to return particular mock values. 4 | 5 | There are two common cases for patching that have special helpers. 6 | 7 | ## Spies 8 | 9 | If a test wishes to assert / refute calls that happen to a module without actually changing the behavior of the module it can simply `spy/1` the module. Spies behave identically to the original module but all calls are recorded so `assert_called/1`, `refute_called/1`, `assert_any_called/2`, and `refute_any_called/2` work as expected. 10 | 11 | ```elixir 12 | defmodule PatchExample do 13 | use ExUnit.Case 14 | use Patch 15 | 16 | def example(value) do 17 | String.upcase(value) 18 | end 19 | 20 | test "spies can see what calls happen without changing functionality" do 21 | spy(String) 22 | 23 | assert "HELLO" == example("hello") 24 | 25 | assert_called String.upcase("hello") 26 | end 27 | end 28 | ``` 29 | 30 | ## Fakes 31 | 32 | Sometimes we want to replace one module with another for testing, for example we might want to replace a module that connects to a real datastore with a fake that stores data in memory while providing the same API. 33 | 34 | The `fake/2,3` functions can be used to replace one module with another. The replacement module can be completely stand alone or can utilize the functionality of the replaced module, it will be made available through use of the `real/1` function. 35 | 36 | ```elixir 37 | defmodule HighLatencyDatabase do 38 | @latency System.convert_time_unit(20, :second, :microsecond) 39 | 40 | def get(id) do 41 | {elapsed, response} = :timer.tc(fn -> Patch.real(Database).get(id) end) 42 | induce_latency(elapsed) 43 | response 44 | end 45 | 46 | defp induce_latency(elapsed) when elapsed < @latency do 47 | time_to_sleep = System.convert_time_unit(@latency - elapsed, :microsecond, :millisecond) 48 | Process.sleep(time_to_sleep) 49 | end 50 | 51 | defp induce_latency(_), do: :ok 52 | end 53 | ``` 54 | 55 | This fake module uses the real module to actually get the record from the database and then makes sure that a minimum amount of latency, in this case 20 seconds, is introduced before returning the result. 56 | 57 | To swap out our real Database with our fake HighLatencyDatabase in a test we can now do the following 58 | 59 | ```elixir 60 | defmodule PatchExample do 61 | use ExUnit.Case 62 | use Patch 63 | 64 | def example(value) do 65 | String.upcase(value) 66 | end 67 | 68 | test "API raises TimeoutError when database is experiencing high latency" do 69 | fake(Database, HighLatencyDatabase) 70 | 71 | assert_raises TimeoutError, fn -> 72 | API.get(:user, 1) 73 | end 74 | end 75 | end 76 | ``` -------------------------------------------------------------------------------- /pages/super-powers.md: -------------------------------------------------------------------------------- 1 | # Super Powers 2 | 3 | Patch provides unique features that no other mocking library for Elixir offers. See the [Mockompare](https://github.com/ihumanable/mockompare) suite for a comparison of Elixir / Erlang mocking libraries. If there is a way to accomplish the following with another library, please open an issue so this section and the comparisons can be updated. 4 | 5 | So what are these super powers? 6 | 7 | 1. Patch mocks are effective for both local and remote calls. This means a patched function **always** resolves to the patch. 8 | 2. Patch can patch private functions without changing their visibility. 9 | 3. Patch makes it possible to test your private functions without changing their visibility via the `expose/2` functionality. 10 | 11 | ## Local and Remote Calls 12 | 13 | In Elixir we have two different ways to call a function, local call vs remote call. 14 | 15 | In a local call, the module is not specified. 16 | 17 | ```elixir 18 | defmodule Example do 19 | def example do 20 | collaborator() # No Module, this is a local call 21 | end 22 | 23 | def collaborator do 24 | :original 25 | end 26 | end 27 | ``` 28 | 29 | In a remote call the module is specified, this is most common when calling from one module to another, but can be done within a module if desired. 30 | 31 | ```elixir 32 | defmodule Example do 33 | def example do 34 | __MODULE__.collaborator() # Module specified, this is a remote call 35 | end 36 | 37 | def collaborator do 38 | :original 39 | end 40 | end 41 | ``` 42 | 43 | It is exceedingly common to use local calls when writing a module. The problem comes when mocking a collaborator function. First, why might we want to mock out a collaborator? Here's an example where we might want to skip some functionality. 44 | 45 | ```elixir 46 | defmodule Example do 47 | def save(thing) do 48 | if valid?(thing) do 49 | do_save(thing) 50 | else 51 | {:error, :invalid} 52 | end 53 | end 54 | 55 | def valid?(thing) do 56 | [ 57 | &Example.Validation.name_not_blank?/1, 58 | &Example.Validation.token_hash_valid?/1, 59 | &Example.Validation.flux_capacitor_within_limits?/1 60 | ] 61 | |> Enum.all?(fn validator -> validator.(thing) end) 62 | end 63 | 64 | defp do_save(thing) do 65 | DB.insert(thing) 66 | end 67 | end 68 | ``` 69 | 70 | In the unit tests for `Example.save/1` we want to test the high level logic of "valid gets saved, invalid gets an error." 71 | 72 | This is complicated though because `Example.valid?/1` has real validations baked in. A common approach is to create a fixture that can pass the validation rules and one that can't. This is a brittle solution though, it introduces a high degree of coupling between the `Example.save/1` tests and the implementation of `Example.valid?/1`. 73 | 74 | A more robust approach is simply to patch out the call to `Example.valid?/1`. When we want to test that a valid thing gets saved, we don't have to jump through hoops to get `Example.valid?/1` to return true, we can just patch it and tell it to return true. When someone comes along and changes the validation rules in `Example.valid?/1` it won't break our `Example.save/1` tests, it might break the tests for `Example.valid?/1` but that's a much better outcome because the test breaking is directly related to the code being changed. 75 | 76 | Additionally, in a unit test we would want to isolate the unit from the database. Our `Example.do_save/1` method wants to actually write to a database, but this is an implementation detail as far as `Example.save/1` is concerned. A common approach in unit testing is to replace external dependencies, like APIs and Databases, with Fakes. 77 | 78 | A Fake DB could be as complex another copy of the schema actually running on the real database software that's isolated for test data or as simple as an in memory map. In this style of testing, the test author can let the code read and write from the fake datastore and then query to make sure the datastore is in the appropriate state. This approach "over tests" the datastore, which is likely already well tested. A simpler approach is to simply patch out `Example.do_save/1` since we only care that it gets called and it's correct functioning should be guaranteed by tests that directly test that function. 79 | 80 | With Patch, we **can** mock these functions and have the mocks be effective even though the module is using the common local call pattern. 81 | 82 | ```elixir 83 | defmodule ExampleTest do 84 | use ExUnit.Case 85 | use Patch 86 | 87 | describe "save/1" do 88 | test "valid things will be saved" do 89 | # Make everything valid 90 | patch(Example, :valid?, true) 91 | 92 | # Patch out do_save so we don't try to hit the database 93 | patch(Example, :do_save, :ok) 94 | 95 | assert :ok == Example.save(:thing) 96 | assert_called Example.do_save(:thing) 97 | end 98 | 99 | test "invalid things will not be saved" do 100 | # We want to refute the call to do_save/1, so let's spy the entire module 101 | spy(Example) 102 | 103 | # Make everything invalid 104 | patch(Example, :valid?, false) 105 | 106 | assert {:error, :invalid} == Example.save(:thing) 107 | refute_called Example.do_save(_) 108 | end 109 | end 110 | end 111 | ``` 112 | 113 | ## Patching Private Functions 114 | 115 | Patch allows the test author to patch private functions without doing anything special. 116 | 117 | Given the following module 118 | 119 | ```elixir 120 | defmodule Example do 121 | def public_function(a) do 122 | {:ok, private_function(a)} 123 | end 124 | 125 | defp private_function(a) do 126 | {:private, a} 127 | end 128 | end 129 | ``` 130 | 131 | We can write the following test. 132 | 133 | ```elixir 134 | defmodule ExampleTest do 135 | use ExUnit.Case 136 | use Patch 137 | 138 | test "public_function/1 wraps private_function/1" do 139 | patch(Example, :private_function, :patched) 140 | 141 | assert Example.public_function(:test_argument) == {:ok, :patched} 142 | end 143 | end 144 | ``` 145 | 146 | Since Patch guarantees that all calls to a patched function return the mock value, this works as expected. 147 | 148 | Private functions can be patched just like a public function, but unless they are exposed via `expose/2` they don't become public. This means that any other code in the project that might have mistakenly made a call to `Example.private_function/1` will fail under test because the visibility is still private. 149 | 150 | To make a private function public, read on to the next section. 151 | 152 | ## Testing Private Functions 153 | 154 | Private functions frequently go untested because they are difficult to test. Developers are faced with a few options when they have a private function. 155 | 156 | 1. Don't test the private function. 157 | 2. Test the private function circuitously by calling some public functions. 158 | 3. Make a public wrapper for the private function and test that. 159 | 4. Change the visibility to public and put a comment with some form of, "This is public just for testing, this function should be treated as though it's private." 160 | 161 | Patch provides a new mechanism for testing private functions, `expose/2`. 162 | 163 | With `expose/2` the test author can expose any private function to the tests as though it's public. Here's an example. 164 | 165 | ```elixir 166 | defmodule Example do 167 | def public_function(arg) do 168 | value = private_function(arg) 169 | 170 | if value < 100 do 171 | :small 172 | else 173 | :large 174 | end 175 | end 176 | 177 | ## Private 178 | 179 | defp private_function(arg) when arg < 20 do 180 | arg * 1000 181 | end 182 | 183 | defp private_function(arg) when arg < 80 do 184 | arg - 3 185 | end 186 | 187 | defp private_function(arg) do 188 | Integer.floor_div(arg, 2) 189 | end 190 | end 191 | ``` 192 | 193 | Testing `public_function/1` gives us a very coarse measure of if the `private_function/1` logic is working correctly. 194 | 195 | It would be great if we could expose `private_function/1` to be able to more directly test this unit. 196 | 197 | Here's how we can expose this function for testing via `expose/2`. Calling an exposed functions will be flagged by the 198 | Elixir Compiler as a warning, since the exposure happens at runtime not compile-time. To suppress these warnings, the 199 | `private/1` macro is provided, just wrap the call to the exposed function with `private/1`. 200 | 201 | ```elixir 202 | defmodule ExampleTest do 203 | use ExUnit.Case 204 | use Patch 205 | 206 | describe "private_function/1" do 207 | test "values less than 20 get magnified by 1000" do 208 | expose(Example, private_function: 1) 209 | 210 | assert private(Example.private_function(10)) == 10_000 211 | end 212 | 213 | test "values between 20 and 80 are reduced by 3" do 214 | expose(Example, private_function: 1) 215 | 216 | assert private(Example.private_function(50)) == 47 217 | end 218 | 219 | test "values greater than or equal to 80 are halved" do 220 | expose(Example, private_function: 1) 221 | 222 | assert private(Example.private_function(120)) == 60 223 | end 224 | end 225 | end 226 | ``` 227 | 228 | ## How does this all work? 229 | 230 | Check out the documentation for `Patch.Mock.Code` for more details on how this is accomplished. 231 | -------------------------------------------------------------------------------- /test/support/unit/access.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.Unit.Access do 2 | 3 | defmodule Inner do 4 | defstruct [:value] 5 | end 6 | 7 | defstruct [:value] 8 | end 9 | -------------------------------------------------------------------------------- /test/support/unit/mock.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.Unit.Mock do 2 | def example(a) do 3 | {:original, a} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/assert_any_call.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.AssertAnyCall do 2 | def function_with_multiple_arities(a) do 3 | {:original, a} 4 | end 5 | 6 | def function_with_multiple_arities(a, b) do 7 | {:original, {a, b}} 8 | end 9 | 10 | def function_with_multiple_arities(a, b, c) do 11 | {:original, {a, b, c}} 12 | end 13 | 14 | def other_function(a) do 15 | {:other, a} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/support/user/assert_called.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.AssertCalled do 2 | def example(a, b) do 3 | {:original, a, b} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/assert_called_once.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.AssertCalledOnce do 2 | def example(a, b) do 3 | {:original, a, b} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/expose.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Expose do 2 | def public_function do 3 | {private_function_a(), private_function_b()} 4 | end 5 | 6 | defp private_function_a do 7 | :private_a 8 | end 9 | 10 | defp private_function_b do 11 | :private_b 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/support/user/fake/fake.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Fake.Fake do 2 | alias Patch.Test.Support.User.Fake.Real 3 | 4 | def example(a) do 5 | {:fake, {:example, a}} 6 | end 7 | 8 | def delegate(a) do 9 | real = Patch.real(Real).delegate(a) 10 | {:fake, real} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/support/user/fake/real.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Fake.Real do 2 | def example(a) do 3 | {:real, {:example, a}} 4 | end 5 | 6 | def delegate(a) do 7 | {:real, {:delegate, a}} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/support/user/history.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.History do 2 | def public_caller(a) do 3 | {:original, public_function(a)} 4 | end 5 | 6 | def public_function(a) do 7 | {:public, a} 8 | end 9 | 10 | def private_caller(a) do 11 | {:original, private_function(a)} 12 | end 13 | 14 | ## Private 15 | 16 | defp private_function(a) do 17 | {:private, a} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/support/user/inject/caller.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Inject.Caller do 2 | use GenServer 3 | 4 | alias Patch.Test.Support.User.Inject.Target 5 | 6 | defstruct [:bonus, :target_pid] 7 | 8 | ## Client 9 | 10 | def start_link(bonus, multiplier) do 11 | GenServer.start_link(__MODULE__, {bonus, multiplier}) 12 | end 13 | 14 | def calculate(pid, argument) do 15 | GenServer.call(pid, {:calculate, argument}) 16 | end 17 | 18 | ## Server 19 | 20 | def init({bonus, multiplier}) do 21 | {:ok, target_pid} = Target.start_link(multiplier) 22 | 23 | {:ok, %__MODULE__{bonus: bonus, target_pid: target_pid}} 24 | end 25 | 26 | def handle_call({:calculate, argument}, _from, %__MODULE__{} = state) do 27 | multiplied = Target.work(state.target_pid, argument) 28 | {:reply, multiplied + state.bonus, state} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/support/user/inject/target.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Inject.Target do 2 | use GenServer 3 | 4 | ## Client 5 | 6 | def start_link(multiplier) do 7 | GenServer.start_link(__MODULE__, multiplier) 8 | end 9 | 10 | def work(pid, argument) do 11 | GenServer.call(pid, {:work, argument}) 12 | end 13 | 14 | ## Server 15 | 16 | def init(multiplier) do 17 | {:ok, multiplier} 18 | end 19 | 20 | def handle_call({:work, argument}, _from, multiplier) do 21 | {:reply, argument * multiplier, multiplier} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/support/user/inject/targetless.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Inject.Targetless do 2 | @moduledoc """ 3 | In the scenarios this module is used in, target_pid is meant to be a pid that 4 | is not defined at init but is expected to be loaded after initialization. 5 | 6 | This module is used to test inject on part of the state that is nil 7 | """ 8 | 9 | use GenServer 10 | 11 | defstruct [:target_pid] 12 | 13 | ## Client 14 | 15 | def start_link() do 16 | GenServer.start_link(__MODULE__, :ok) 17 | end 18 | 19 | def hello(pid) do 20 | GenServer.call(pid, :hello) 21 | end 22 | 23 | ## Server 24 | 25 | def init(:ok) do 26 | {:ok, %__MODULE__{}} 27 | end 28 | 29 | def handle_call(:hello, _from, %__MODULE__{} = state) do 30 | send(state.target_pid, :greetings) 31 | {:reply, :ok, state} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/support/user/listener/counter.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Listener.Counter do 2 | use GenServer 3 | 4 | def start_link(opts) do 5 | GenServer.start_link(__MODULE__, :ok, opts) 6 | end 7 | 8 | def init(:ok) do 9 | {:ok, 0} 10 | end 11 | 12 | def handle_call({:sleep, millis}, _from, state) do 13 | Process.sleep(millis) 14 | {:reply, :ok, state} 15 | end 16 | 17 | def handle_call(:crash, _from, state) do 18 | {:stop, {:shutdown, :crash}, state} 19 | end 20 | 21 | def handle_call(:exit, _from, state) do 22 | {:stop, :normal, state} 23 | end 24 | 25 | def handle_call(:increment, _from, state) do 26 | {:reply, state + 1, state + 1} 27 | end 28 | 29 | def handle_call(:decrement, _from, state) do 30 | {:reply, state - 1, state - 1} 31 | end 32 | 33 | def handle_call(:value, _from, state) do 34 | {:reply, state, state} 35 | end 36 | 37 | def handle_call(:deferred_value, from, state) do 38 | send(self(), {:deferred_value, from}) 39 | {:noreply, state} 40 | end 41 | 42 | def handle_cast(:increment, state) do 43 | {:noreply, state + 1} 44 | end 45 | 46 | def handle_cast(:decrement, state) do 47 | {:noreply, state - 1} 48 | end 49 | 50 | def handle_info(:increment, state) do 51 | {:noreply, state + 1} 52 | end 53 | 54 | def handle_info(:decrement, state) do 55 | {:noreply, state - 1} 56 | end 57 | 58 | def handle_info({:deferred_value, from}, state) do 59 | GenServer.reply(from, state) 60 | {:noreply, state} 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/support/user/patch/arity.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Arity do 2 | def function_of_arity_0 do 3 | :original 4 | end 5 | 6 | def function_of_arity_20( 7 | a, 8 | b, 9 | c, 10 | d, 11 | e, 12 | f, 13 | g, 14 | h, 15 | i, 16 | j, 17 | k, 18 | l, 19 | m, 20 | n, 21 | o, 22 | p, 23 | q, 24 | r, 25 | s, 26 | t 27 | ) do 28 | {:original, {a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t}} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/support/user/patch/callable.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Callable do 2 | def example(argument) do 3 | {:original, argument} 4 | end 5 | 6 | def example(a, b, c) do 7 | {:original, a, b, c} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/support/user/patch/callable_stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.CallableStack do 2 | def example(argument) do 3 | {:original, argument} 4 | end 5 | 6 | def example(a, b, c) do 7 | {:original, a, b, c} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/support/user/patch/cycle.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Cycle do 2 | def example do 3 | :original 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/patch/erlang_unsticky.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_unsticky). 2 | -export([example_function/0]). 3 | 4 | example_function() -> 5 | real_value. -------------------------------------------------------------------------------- /test/support/user/patch/execution_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.ExecutionContext do 2 | def example() do 3 | :original 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/patch/freezer.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Freezer do 2 | use GenServer 3 | 4 | def start_link do 5 | GenServer.start_link(__MODULE__, :ok) 6 | end 7 | 8 | def work(pid) do 9 | GenServer.call(pid, :work) 10 | end 11 | 12 | def init(:ok) do 13 | {:ok, nil} 14 | end 15 | 16 | def handle_call(:work, _from, state) do 17 | {:reply, :ok, state} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/support/user/patch/gen_server_example.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.GenServerExample do 2 | use GenServer 3 | 4 | ## Client 5 | 6 | def start_link(options \\ []) do 7 | options = Keyword.put_new(options, :name, __MODULE__) 8 | GenServer.start_link(__MODULE__, :ok, options) 9 | end 10 | 11 | def a(server \\ __MODULE__, argument) do 12 | GenServer.call(server, {:a, argument}) 13 | end 14 | 15 | def b(server \\ __MODULE__, argument) do 16 | GenServer.call(server, {:b, argument}) 17 | end 18 | 19 | def c(server \\ __MODULE__, argument) do 20 | GenServer.call(server, {:c, argument}) 21 | end 22 | 23 | ## Server 24 | 25 | def init(:ok) do 26 | {:ok, []} 27 | end 28 | 29 | def handle_call({:a, argument}, _from, state) do 30 | {:reply, {:original, argument}, [{:a, argument} | state]} 31 | end 32 | 33 | def handle_call({:b, argument}, _from, state) do 34 | {:reply, {:original, argument}, [{:b, argument} | state]} 35 | end 36 | 37 | def handle_call({:c, argument}, _from, state) do 38 | {:reply, {:original, argument}, [{:c, argument} | state]} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/user/patch/local_call.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.LocalCall do 2 | def public_caller(a) do 3 | {:original, public_function(a)} 4 | end 5 | 6 | def public_function(a) do 7 | {:public, a} 8 | end 9 | 10 | def public_argument_consumer do 11 | Enum.map(public_arguments(), &(&1 * 2)) 12 | end 13 | 14 | def public_arguments do 15 | [1, 2, 3] 16 | end 17 | 18 | def private_caller(a) do 19 | {:original, private_function(a)} 20 | end 21 | 22 | ## Private 23 | 24 | defp private_function(a) do 25 | {:private, a} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/support/user/patch/raises.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Raises do 2 | def example do 3 | :original 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/patch/scalar.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Scalar do 2 | def example do 3 | :original 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/patch/sequence.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Sequence do 2 | def example do 3 | :original 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/patch/throws.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.Throws do 2 | def example do 3 | :original 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/patch/unknown_function.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Patch.UnknownFunction do 2 | end 3 | -------------------------------------------------------------------------------- /test/support/user/private.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Private do 2 | def public_function(a) do 3 | # This function has to exist so the compiler won't optimize away private_function/1 4 | private_function(a) 5 | end 6 | 7 | defp private_function(a) do 8 | {:private, a} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/support/user/reflection/elixir_target.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Reflection.ElixirTarget do 2 | def public_function(a) do 3 | private_function(a) 4 | end 5 | 6 | def public_function(a, b) do 7 | {:ok, a, b} 8 | end 9 | 10 | def other_public_function do 11 | :ok 12 | end 13 | 14 | ## Private 15 | 16 | defp private_function(a) do 17 | {:ok, a} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/support/user/reflection/erlang_target.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_target). 2 | -export([public_function/1, public_function/2, other_public_function/0]). 3 | 4 | public_function(A) -> 5 | private_function(A). 6 | 7 | public_function(A, B) -> 8 | {ok, A, B}. 9 | 10 | other_public_function() -> 11 | ok. 12 | 13 | private_function(A) -> 14 | {ok, A}. -------------------------------------------------------------------------------- /test/support/user/refute_any_call.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.RefuteAnyCall do 2 | def function_with_multiple_arities(a) do 3 | {:original, a} 4 | end 5 | 6 | def function_with_multiple_arities(a, b) do 7 | {:original, {a, b}} 8 | end 9 | 10 | def function_with_multiple_arities(a, b, c) do 11 | {:original, {a, b, c}} 12 | end 13 | 14 | def other_function(a) do 15 | {:other, a} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/support/user/refute_called.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.RefuteCalled do 2 | def example(a, b) do 3 | {:original, a, b} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/refute_called_once.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.RefuteCalledOnce do 2 | def example(a, b) do 3 | {:original, a, b} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/support/user/replace.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Replace do 2 | use GenServer 3 | 4 | defmodule Inner do 5 | @type t :: %__MODULE__{ 6 | value: term() 7 | } 8 | defstruct [:value] 9 | end 10 | 11 | @type t :: %__MODULE__{ 12 | value: term(), 13 | inner: Inner.t() 14 | } 15 | defstruct [:value, :inner] 16 | 17 | ## Client 18 | 19 | def start_link(value, options \\ []) do 20 | GenServer.start_link(__MODULE__, value, options) 21 | end 22 | 23 | ## Server 24 | 25 | def init(value) do 26 | state = %__MODULE__{value: value, inner: %Inner{value: value}} 27 | 28 | {:ok, state} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/support/user/restore.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Restore do 2 | def example do 3 | :original 4 | end 5 | 6 | def other do 7 | :original 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/support/user/spy.ex: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Support.User.Spy do 2 | def example(argument) do 3 | {:original, argument} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/unit/patch/access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Unit.Patch.AccessTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.Unit.Access, as: Target 6 | 7 | describe "fetch/2" do 8 | test "works with a struct" do 9 | target = %Target{value: :test} 10 | 11 | assert Patch.Access.fetch(target, [:value]) == {:ok, :test} 12 | end 13 | 14 | test "works with nested structs" do 15 | target = %Target{value: %Target.Inner{value: :test}} 16 | 17 | assert Patch.Access.fetch(target, [:value, :value]) == {:ok, :test} 18 | end 19 | 20 | test "works with maps" do 21 | target = %{key: :test} 22 | 23 | assert Patch.Access.fetch(target, [:key]) == {:ok, :test} 24 | end 25 | 26 | test "works with nested maps" do 27 | target = %{key: %{key: :test}} 28 | 29 | assert Patch.Access.fetch(target, [:key, :key]) == {:ok, :test} 30 | end 31 | 32 | test "works with mix of maps and structs" do 33 | target = %{key: %Target{value: %{key: %Target.Inner{value: :test}}}} 34 | 35 | assert Patch.Access.fetch(target, [:key, :value, :key, :value]) == {:ok, :test} 36 | end 37 | 38 | test "works on non-terminal keys" do 39 | target_value = %{key: %Target.Inner{value: :test}} 40 | 41 | target = %{key: %Target{value: target_value}} 42 | 43 | assert Patch.Access.fetch(target, [:key, :value]) == {:ok, target_value} 44 | end 45 | 46 | test "keys not found return :error" do 47 | target = %{outer: %{inner: :test}} 48 | 49 | assert Patch.Access.fetch(target, [:missing]) == :error 50 | assert Patch.Access.fetch(target, [:outer, :missing]) == :error 51 | end 52 | end 53 | 54 | describe "get/2,3" do 55 | test "works with a struct" do 56 | target = %Target{value: :test} 57 | 58 | assert Patch.Access.get(target, [:value]) == :test 59 | end 60 | 61 | test "works with nested structs" do 62 | target = %Target{value: %Target.Inner{value: :test}} 63 | 64 | assert Patch.Access.get(target, [:value, :value]) == :test 65 | end 66 | 67 | test "works with maps" do 68 | target = %{key: :test} 69 | 70 | assert Patch.Access.get(target, [:key]) == :test 71 | end 72 | 73 | test "works with nested maps" do 74 | target = %{key: %{key: :test}} 75 | 76 | assert Patch.Access.get(target, [:key, :key]) == :test 77 | end 78 | 79 | test "works with mix of maps and structs" do 80 | target = %{key: %Target{value: %{key: %Target.Inner{value: :test}}}} 81 | 82 | assert Patch.Access.get(target, [:key, :value, :key, :value]) == :test 83 | end 84 | 85 | test "works on non-terminal keys" do 86 | target_value = %{key: %Target.Inner{value: :test}} 87 | 88 | target = %{key: %Target{value: target_value}} 89 | 90 | assert Patch.Access.get(target, [:key, :value]) == target_value 91 | end 92 | 93 | test "keys not found returns default" do 94 | target = %{outer: %{inner: :test}} 95 | 96 | refute Patch.Access.get(target, [:missing]) 97 | refute Patch.Access.get(target, [:outer, :missing]) 98 | end 99 | 100 | test "default can be customized" do 101 | target = %{outer: %{inner: :test}} 102 | 103 | assert Patch.Access.get(target, [:missing], :custom_default) == :custom_default 104 | assert Patch.Access.get(target, [:outer, :missing], :custom_default) == :custom_default 105 | end 106 | end 107 | 108 | describe "put/2" do 109 | test "works with a struct" do 110 | target = %Target{value: :test} 111 | expected = %Target{value: :updated} 112 | 113 | assert Patch.Access.put(target, [:value], :updated) == expected 114 | end 115 | 116 | test "works with nested structs" do 117 | target = %Target{value: %Target.Inner{value: :test}} 118 | expected = %Target{value: %Target.Inner{value: :updated}} 119 | 120 | assert Patch.Access.put(target, [:value, :value], :updated) == expected 121 | end 122 | 123 | test "works with maps" do 124 | target = %{key: :test} 125 | expected = %{key: :updated} 126 | 127 | assert Patch.Access.put(target, [:key], :updated) == expected 128 | end 129 | 130 | test "works with nested maps" do 131 | target = %{key: %{key: :test}} 132 | expected = %{key: %{key: :updated}} 133 | 134 | assert Patch.Access.put(target, [:key, :key], :updated) == expected 135 | end 136 | 137 | test "works with mix of maps and structs" do 138 | target = %{key: %Target{value: %{key: %Target.Inner{value: :test}}}} 139 | expected = %{key: %Target{value: %{key: %Target.Inner{value: :updated}}}} 140 | 141 | assert Patch.Access.put(target, [:key, :value, :key, :value], :updated) == expected 142 | end 143 | 144 | test "works on non-terminal keys" do 145 | target = %{key: %Target{value: %{key: %Target.Inner{value: :test}}}} 146 | expected = %{key: %Target{value: :updated}} 147 | 148 | assert Patch.Access.put(target, [:key, :value], :updated) == expected 149 | end 150 | end 151 | 152 | end 153 | -------------------------------------------------------------------------------- /test/unit/patch/macro_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Unit.Patch.MacroTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | require Patch.Macro 6 | 7 | @module_attribute :module_attribute 8 | @other_attribute :other_attribute 9 | 10 | describe "match?/2" do 11 | test "can match literal expressions" do 12 | assert Patch.Macro.match?(1, 1) 13 | end 14 | 15 | test "can mismatch literal expressions" do 16 | refute Patch.Macro.match?(1, 2) 17 | end 18 | 19 | test "can match wildcards" do 20 | assert Patch.Macro.match?(_, 1) 21 | end 22 | 23 | test "can match multiple wildcards" do 24 | assert Patch.Macro.match?({_, _}, {1, 2}) 25 | end 26 | 27 | test "can match module attributes" do 28 | assert Patch.Macro.match?(@module_attribute, @module_attribute) 29 | assert Patch.Macro.match?(:module_attribute, @module_attribute) 30 | assert Patch.Macro.match?(@module_attribute, :module_attribute) 31 | end 32 | 33 | test "can mismatch module attributes" do 34 | refute Patch.Macro.match?(@module_attribute, @other_attribute) 35 | refute Patch.Macro.match?(@module_attribute, :other_attribute) 36 | refute Patch.Macro.match?(:module_attribute, @other_attribute) 37 | end 38 | 39 | test "can match pins" do 40 | x = 1 41 | assert Patch.Macro.match?(^x, 1) 42 | end 43 | 44 | test "can mismatch pins" do 45 | x = 1 46 | refute Patch.Macro.match?(^x, 2) 47 | end 48 | 49 | test "can match variables, but does not bind them" do 50 | x = :unbound 51 | assert Patch.Macro.match?(x, 1) 52 | assert x == :unbound 53 | end 54 | 55 | test "can match unused variables" do 56 | assert Patch.Macro.match?(_x, 1) 57 | end 58 | 59 | test "can match a mix of used and unused variabled, but does not bind them" do 60 | x = :unbound 61 | assert Patch.Macro.match?({x, _y}, {1, 2}) 62 | assert x == :unbound 63 | end 64 | 65 | test "functionality works in arbitrarily complex expressions" do 66 | x = 1 67 | assert Patch.Macro.match?([^x, y, _, %{a: 1}], [1, 2, 3, %{a: 1, b: 2}]) 68 | end 69 | end 70 | 71 | describe "match/2" do 72 | test "can match literal expressions" do 73 | assert Patch.Macro.match(1, 1) 74 | end 75 | 76 | test "raise MatchError on literal expressions mismatch" do 77 | assert_raise MatchError, "no match of right hand side value: 2", fn -> 78 | Patch.Macro.match(1, 2) 79 | end 80 | end 81 | 82 | test "can match wildcards" do 83 | assert Patch.Macro.match(_, 1) 84 | end 85 | 86 | test "can match multiple wildcards" do 87 | assert Patch.Macro.match({_, _}, {1, 2}) 88 | end 89 | 90 | test "can match module attributes" do 91 | assert Patch.Macro.match(@module_attribute, @module_attribute) 92 | assert Patch.Macro.match(:module_attribute, @module_attribute) 93 | assert Patch.Macro.match(@module_attribute, :module_attribute) 94 | end 95 | 96 | test "can mismatch module attributes" do 97 | assert_raise MatchError, "no match of right hand side value: :other_attribute", fn -> 98 | Patch.Macro.match(@module_attribute, @other_attribute) 99 | end 100 | 101 | assert_raise MatchError, "no match of right hand side value: :other_attribute", fn -> 102 | Patch.Macro.match(:module_attribute, @other_attribute) 103 | end 104 | 105 | assert_raise MatchError, "no match of right hand side value: :other_attribute", fn -> 106 | Patch.Macro.match(@module_attribute, :other_attribute) 107 | end 108 | end 109 | 110 | test "can match pins" do 111 | x = 1 112 | assert Patch.Macro.match(^x, 1) 113 | end 114 | 115 | test "raise MatchError on mismatch pins" do 116 | x = 1 117 | 118 | assert_raise MatchError, "no match of right hand side value: 2", fn -> 119 | Patch.Macro.match(^x, 2) 120 | end 121 | end 122 | 123 | test "can match variables and binds them" do 124 | assert Patch.Macro.match(x, 1) 125 | assert x == 1 126 | end 127 | 128 | test "can match unused variables" do 129 | assert Patch.Macro.match(_x, 1) 130 | end 131 | 132 | test "can match a mix of used and unused variabled" do 133 | assert Patch.Macro.match({x, _y}, {1, 2}) 134 | assert x == 1 135 | end 136 | 137 | test "functionality works in arbitrarily complex expressions" do 138 | x = 1 139 | assert Patch.Macro.match([^x, y, _, %{a: 1}], [1, 2, 3, %{a: 1, b: 2}]) 140 | assert y == 2 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /test/unit/patch/mock/code/queries/exports_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Unit.Patch.Mock.Code.Queries.ExportsTest do 2 | use ExUnit.Case 3 | 4 | alias Patch.Mock.Code.Queries.Exports 5 | 6 | describe "query/1" do 7 | test "handles functions with multiple arities declared across multiple exports" do 8 | forms = [ 9 | {:attribute, 1, :export, [a: 1]}, 10 | {:attribute, 1, :export, [a: 2]} 11 | ] 12 | 13 | expected = Enum.sort([a: 1, a: 2]) 14 | 15 | actual = 16 | forms 17 | |> Exports.query() 18 | |> Enum.sort() 19 | 20 | assert expected == actual 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/unit/patch/mock/code/transforms/clean_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.Unit.Patch.Mock.Code.Transforms.CleanTest do 2 | use ExUnit.Case 3 | 4 | alias Patch.Mock.Code.Transforms.Clean 5 | 6 | describe "transform/1 compile attribute handling" do 7 | test "retains wildcard no_auto_import when expressed as scalar" do 8 | forms = [ 9 | {:attribute, 1, :compile, :no_auto_import} 10 | ] 11 | 12 | cleaned = Clean.transform(forms) 13 | 14 | assert cleaned == forms 15 | end 16 | 17 | test "retains wildcard no_auto_import when expressed as list" do 18 | forms = [ 19 | {:attribute, 1, :compile, [:no_auto_import]} 20 | ] 21 | 22 | cleaned = Clean.transform(forms) 23 | 24 | assert cleaned == forms 25 | end 26 | 27 | test "retains specific no_auto_import when expressed as scalar" do 28 | forms = [ 29 | {:attribute, 1, :compile, {:no_auto_import, [example: 1]}} 30 | ] 31 | 32 | cleaned = Clean.transform(forms) 33 | 34 | assert cleaned == forms 35 | end 36 | 37 | test "retains specific no_auto_import when expressed as list" do 38 | forms = [ 39 | {:attribute, 1, :compile, [no_auto_import: [example: 1]]} 40 | ] 41 | 42 | cleaned = Clean.transform(forms) 43 | 44 | assert cleaned == forms 45 | end 46 | 47 | test "strips other options" do 48 | forms = [ 49 | {:attribute, 1, :compile, [:no_auto_import, {:inline, [example: 1]}]} 50 | ] 51 | 52 | cleaned = Clean.transform(forms) 53 | 54 | assert cleaned == [ 55 | {:attribute, 1, :compile, [:no_auto_import]} 56 | ] 57 | end 58 | 59 | test "removes attribute entirely if no valid options expressed as scalar" do 60 | forms = [ 61 | {:attribute, 1, :compile, {:inline, [example: 1]}} 62 | ] 63 | 64 | cleaned = Clean.transform(forms) 65 | 66 | assert cleaned == [] 67 | end 68 | 69 | test "removes attribute entirely if no valid options expressed as list" do 70 | forms = [ 71 | {:attribute, 1, :compile, [{:inline, [example: 1]}]} 72 | ] 73 | 74 | cleaned = Clean.transform(forms) 75 | 76 | assert cleaned == [] 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/user/assert_any_call_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.AssertAnyCallTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.AssertAnyCall 6 | 7 | describe "assert_any_call/1" do 8 | test "does not raise if a patched function has a call of any arity (/1)" do 9 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 10 | 11 | assert :patched == AssertAnyCall.function_with_multiple_arities(1) 12 | 13 | assert_any_call AssertAnyCall.function_with_multiple_arities 14 | end 15 | 16 | test "does not raise if a patched function has a call of any arity (/2)" do 17 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 18 | 19 | assert :patched == AssertAnyCall.function_with_multiple_arities(1, 2) 20 | 21 | assert_any_call AssertAnyCall.function_with_multiple_arities 22 | end 23 | 24 | test "does not raise if a patched function has a call of any arity (/3)" do 25 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 26 | 27 | assert :patched == AssertAnyCall.function_with_multiple_arities(1, 2, 3) 28 | 29 | assert_any_call AssertAnyCall.function_with_multiple_arities 30 | end 31 | 32 | test "does not raise if a spied module has a call of any arity (/1)" do 33 | spy(AssertAnyCall) 34 | 35 | assert {:original, 1} == AssertAnyCall.function_with_multiple_arities(1) 36 | 37 | assert_any_call AssertAnyCall.function_with_multiple_arities 38 | end 39 | 40 | test "does not raise if a spied module has a call of any arity (/2)" do 41 | spy(AssertAnyCall) 42 | 43 | assert {:original, {1, 2}} == AssertAnyCall.function_with_multiple_arities(1, 2) 44 | 45 | assert_any_call AssertAnyCall.function_with_multiple_arities 46 | end 47 | 48 | test "does not raise if a spied module has a call of any arity (/3)" do 49 | spy(AssertAnyCall) 50 | 51 | assert {:original, {1, 2, 3}} == AssertAnyCall.function_with_multiple_arities(1, 2, 3) 52 | 53 | assert_any_call AssertAnyCall.function_with_multiple_arities 54 | end 55 | 56 | test "raises if a patched function has no calls" do 57 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 58 | 59 | assert_raise Patch.MissingCall, fn -> 60 | assert_any_call AssertAnyCall.function_with_multiple_arities 61 | end 62 | end 63 | 64 | test "raises if a spied module function has no calls" do 65 | spy(AssertAnyCall) 66 | 67 | assert_raise Patch.MissingCall, fn -> 68 | assert_any_call AssertAnyCall.function_with_multiple_arities 69 | end 70 | end 71 | 72 | test "raises in the presence of calls to other functions" do 73 | patch(AssertAnyCall, :other_function, :patched) 74 | 75 | assert AssertAnyCall.other_function(1) == :patched 76 | 77 | assert_raise Patch.MissingCall, fn -> 78 | assert_any_call AssertAnyCall.function_with_multiple_arities 79 | end 80 | end 81 | 82 | test "exception formatting" do 83 | patch(AssertAnyCall, :other_function, :patched) 84 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 85 | 86 | assert AssertAnyCall.other_function(1) == :patched 87 | 88 | expected_message = """ 89 | \n 90 | Expected any call to the following function: 91 | 92 | Patch.Test.Support.User.AssertAnyCall.function_with_multiple_arities 93 | 94 | Calls which were received (matching calls are marked with *): 95 | 96 | 1. Patch.Test.Support.User.AssertAnyCall.other_function(1) 97 | """ 98 | 99 | assert_raise Patch.MissingCall, expected_message, fn -> 100 | assert_any_call AssertAnyCall.function_with_multiple_arities 101 | end 102 | end 103 | 104 | end 105 | 106 | describe "assert_any_call/2" do 107 | test "does not raise if a patched function has a call of any arity (/1)" do 108 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 109 | 110 | assert :patched == AssertAnyCall.function_with_multiple_arities(1) 111 | 112 | assert_any_call AssertAnyCall, :function_with_multiple_arities 113 | end 114 | 115 | test "does not raise if a patched function has a call of any arity (/2)" do 116 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 117 | 118 | assert :patched == AssertAnyCall.function_with_multiple_arities(1, 2) 119 | 120 | assert_any_call AssertAnyCall, :function_with_multiple_arities 121 | end 122 | 123 | test "does not raise if a patched function has a call of any arity (/3)" do 124 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 125 | 126 | assert :patched == AssertAnyCall.function_with_multiple_arities(1, 2, 3) 127 | 128 | assert_any_call AssertAnyCall, :function_with_multiple_arities 129 | end 130 | 131 | test "does not raise if a spied module has a call of any arity (/1)" do 132 | spy(AssertAnyCall) 133 | 134 | assert {:original, 1} == AssertAnyCall.function_with_multiple_arities(1) 135 | 136 | assert_any_call AssertAnyCall, :function_with_multiple_arities 137 | end 138 | 139 | test "does not raise if a spied module has a call of any arity (/2)" do 140 | spy(AssertAnyCall) 141 | 142 | assert {:original, {1, 2}} == AssertAnyCall.function_with_multiple_arities(1, 2) 143 | 144 | assert_any_call AssertAnyCall, :function_with_multiple_arities 145 | end 146 | 147 | test "does not raise if a spied module has a call of any arity (/3)" do 148 | spy(AssertAnyCall) 149 | 150 | assert {:original, {1, 2, 3}} == AssertAnyCall.function_with_multiple_arities(1, 2, 3) 151 | 152 | assert_any_call AssertAnyCall, :function_with_multiple_arities 153 | end 154 | 155 | test "raises if a patched function has no calls" do 156 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 157 | 158 | assert_raise Patch.MissingCall, fn -> 159 | assert_any_call AssertAnyCall, :function_with_multiple_arities 160 | end 161 | end 162 | 163 | test "raises if a spied module function has no calls" do 164 | spy(AssertAnyCall) 165 | 166 | assert_raise Patch.MissingCall, fn -> 167 | assert_any_call AssertAnyCall, :function_with_multiple_arities 168 | end 169 | end 170 | 171 | test "raises in the presence of calls to other functions" do 172 | patch(AssertAnyCall, :other_function, :patched) 173 | 174 | assert AssertAnyCall.other_function(1) == :patched 175 | 176 | assert_raise Patch.MissingCall, fn -> 177 | assert_any_call AssertAnyCall, :function_with_multiple_arities 178 | end 179 | end 180 | 181 | test "exception formatting" do 182 | patch(AssertAnyCall, :other_function, :patched) 183 | patch(AssertAnyCall, :function_with_multiple_arities, :patched) 184 | 185 | assert AssertAnyCall.other_function(1) == :patched 186 | 187 | expected_message = """ 188 | \n 189 | Expected any call to the following function: 190 | 191 | Patch.Test.Support.User.AssertAnyCall.function_with_multiple_arities 192 | 193 | Calls which were received (matching calls are marked with *): 194 | 195 | 1. Patch.Test.Support.User.AssertAnyCall.other_function(1) 196 | """ 197 | 198 | assert_raise Patch.MissingCall, expected_message, fn -> 199 | assert_any_call AssertAnyCall, :function_with_multiple_arities 200 | end 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /test/user/assert_called_once_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.AssertCalledOnecTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.AssertCalledOnce 6 | 7 | describe "assert_called_once/1" do 8 | test "exact call can be asserted" do 9 | patch(AssertCalledOnce, :example, :patched) 10 | 11 | assert AssertCalledOnce.example(1, 2) == :patched 12 | 13 | assert_called_once AssertCalledOnce.example(1, 2) 14 | end 15 | 16 | test "exact call mismatch raises MissingCall" do 17 | patch(AssertCalledOnce, :example, :patched) 18 | 19 | assert AssertCalledOnce.example(1, 2) == :patched 20 | 21 | assert_raise Patch.MissingCall, fn -> 22 | assert_called_once AssertCalledOnce.example(3, 4) 23 | end 24 | end 25 | 26 | test "exact call after multiple calls raises UnexpectedCall" do 27 | patch(AssertCalledOnce, :example, :patched) 28 | 29 | assert AssertCalledOnce.example(1, 2) == :patched 30 | assert AssertCalledOnce.example(1, 2) == :patched 31 | 32 | assert_raise Patch.UnexpectedCall, fn -> 33 | assert_called_once AssertCalledOnce.example(1, 2) 34 | end 35 | end 36 | 37 | test "partial call can be asserted" do 38 | patch(AssertCalledOnce, :example, :patched) 39 | 40 | assert AssertCalledOnce.example(1, 2) == :patched 41 | 42 | assert_called_once AssertCalledOnce.example(1, _) 43 | assert_called_once AssertCalledOnce.example(_, 2) 44 | end 45 | 46 | test "partial call mismatch raises MissingCall" do 47 | patch(AssertCalledOnce, :example, :patched) 48 | 49 | assert AssertCalledOnce.example(1, 2) == :patched 50 | 51 | assert_raise Patch.MissingCall, fn -> 52 | assert_called_once AssertCalledOnce.example(3, _) 53 | end 54 | 55 | assert_raise Patch.MissingCall, fn -> 56 | assert_called_once AssertCalledOnce.example(_, 4) 57 | end 58 | end 59 | 60 | test "partial call after multiple calls raises UnexpectedCall" do 61 | patch(AssertCalledOnce, :example, :patched) 62 | 63 | assert AssertCalledOnce.example(1, 2) == :patched 64 | assert AssertCalledOnce.example(1, 3) == :patched 65 | assert AssertCalledOnce.example(3, 2) == :patched 66 | 67 | 68 | assert_raise Patch.UnexpectedCall, fn -> 69 | assert_called_once AssertCalledOnce.example(1, _) 70 | end 71 | 72 | assert_raise Patch.UnexpectedCall, fn -> 73 | assert_called_once AssertCalledOnce.example(_, 2) 74 | end 75 | end 76 | 77 | test "wildcard call can be asserted" do 78 | patch(AssertCalledOnce, :example, :patched) 79 | 80 | assert AssertCalledOnce.example(1, 2) == :patched 81 | 82 | assert_called_once AssertCalledOnce.example(_, _) 83 | end 84 | 85 | test "wildcard call raises MissingCall when no calls present" do 86 | patch(AssertCalledOnce, :example, :patched) 87 | 88 | assert_raise Patch.MissingCall, fn -> 89 | assert_called_once AssertCalledOnce.example(_, _) 90 | end 91 | end 92 | 93 | test "wildcard call raises UnexpectedCall when multiple calls present" do 94 | patch(AssertCalledOnce, :example, :patched) 95 | 96 | assert AssertCalledOnce.example(1, 2) == :patched 97 | assert AssertCalledOnce.example(3, 4) == :patched 98 | 99 | assert_raise Patch.UnexpectedCall, fn -> 100 | assert_called_once AssertCalledOnce.example(_, _) 101 | end 102 | end 103 | 104 | test "exception formatting with no calls" do 105 | patch(AssertCalledOnce, :example, :patched) 106 | 107 | expected_message = """ 108 | \n 109 | Expected the following call to occur exactly once, but call occurred 0 times: 110 | 111 | Patch.Test.Support.User.AssertCalledOnce.example(1, 2) 112 | 113 | Calls which were received (matching calls are marked with *): 114 | 115 | [No Calls Received] 116 | """ 117 | 118 | assert_raise Patch.MissingCall, expected_message, fn -> 119 | assert_called_once AssertCalledOnce.example(1, 2) 120 | end 121 | end 122 | 123 | test "exception formatting with non-matching calls" do 124 | patch(AssertCalledOnce, :example, :patched) 125 | 126 | assert AssertCalledOnce.example(1, 2) == :patched 127 | 128 | expected_message = """ 129 | \n 130 | Expected the following call to occur exactly once, but call occurred 0 times: 131 | 132 | Patch.Test.Support.User.AssertCalledOnce.example(3, 4) 133 | 134 | Calls which were received (matching calls are marked with *): 135 | 136 | 1. Patch.Test.Support.User.AssertCalledOnce.example(1, 2) 137 | """ 138 | 139 | assert_raise Patch.MissingCall, expected_message, fn -> 140 | assert_called_once AssertCalledOnce.example(3, 4) 141 | end 142 | end 143 | 144 | test "exception formatting with wrong number of matched calls" do 145 | patch(AssertCalledOnce, :example, :patched) 146 | 147 | assert AssertCalledOnce.example(1, 2) == :patched 148 | assert AssertCalledOnce.example(3, 4) == :patched 149 | assert AssertCalledOnce.example(1, 2) == :patched 150 | 151 | expected_message = """ 152 | \n 153 | Expected the following call to occur exactly once, but call occurred 2 times: 154 | 155 | Patch.Test.Support.User.AssertCalledOnce.example(1, 2) 156 | 157 | Calls which were received (matching calls are marked with *): 158 | 159 | * 1. Patch.Test.Support.User.AssertCalledOnce.example(1, 2) 160 | 2. Patch.Test.Support.User.AssertCalledOnce.example(3, 4) 161 | * 3. Patch.Test.Support.User.AssertCalledOnce.example(1, 2) 162 | """ 163 | 164 | assert_raise Patch.UnexpectedCall, expected_message, fn -> 165 | assert_called_once AssertCalledOnce.example(1, 2) 166 | end 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /test/user/expose_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.ExposeTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Expose 6 | 7 | describe "expose/2" do 8 | test "no change in visibility when :public" do 9 | assert Expose.public_function() == {:private_a, :private_b} 10 | 11 | assert_raise UndefinedFunctionError, fn -> 12 | private(Expose.private_function_a()) 13 | end 14 | 15 | assert_raise UndefinedFunctionError, fn -> 16 | private(Expose.private_function_b()) 17 | end 18 | 19 | expose(Expose, :public) 20 | 21 | assert Expose.public_function() == {:private_a, :private_b} 22 | 23 | assert_raise UndefinedFunctionError, fn -> 24 | private(Expose.private_function_a()) 25 | end 26 | 27 | assert_raise UndefinedFunctionError, fn -> 28 | private(Expose.private_function_b()) 29 | end 30 | end 31 | 32 | test "all private functions available when :all" do 33 | assert Expose.public_function() == {:private_a, :private_b} 34 | 35 | assert_raise UndefinedFunctionError, fn -> 36 | private(Expose.private_function_a()) 37 | end 38 | 39 | assert_raise UndefinedFunctionError, fn -> 40 | private(Expose.private_function_b()) 41 | end 42 | 43 | expose(Expose, :all) 44 | 45 | assert Expose.public_function() == {:private_a, :private_b} 46 | 47 | assert private(Expose.private_function_a()) == :private_a 48 | 49 | assert private(Expose.private_function_b()) == :private_b 50 | end 51 | 52 | test "a subset of functions can be exposed" do 53 | assert Expose.public_function() == {:private_a, :private_b} 54 | 55 | assert_raise UndefinedFunctionError, fn -> 56 | private(Expose.private_function_a()) 57 | end 58 | 59 | assert_raise UndefinedFunctionError, fn -> 60 | private(Expose.private_function_b()) 61 | end 62 | 63 | expose(Expose, private_function_a: 0) 64 | 65 | assert Expose.public_function() == {:private_a, :private_b} 66 | 67 | assert private(Expose.private_function_a()) == :private_a 68 | 69 | assert_raise UndefinedFunctionError, fn -> 70 | private(Expose.private_function_b()) 71 | end 72 | end 73 | 74 | test "expose can be changed after patching without losing previous patches or history" do 75 | patch(Expose, :public_function, :patched) 76 | 77 | assert Expose.public_function() == :patched 78 | 79 | assert_raise UndefinedFunctionError, fn -> 80 | private(Expose.private_function_a()) 81 | end 82 | 83 | assert_raise UndefinedFunctionError, fn -> 84 | private(Expose.private_function_b()) 85 | end 86 | 87 | expose(Expose, :all) 88 | 89 | assert Expose.public_function() == :patched 90 | 91 | assert private(Expose.private_function_a()) == :private_a 92 | 93 | assert private(Expose.private_function_b()) == :private_b 94 | 95 | assert history(Expose) == [ 96 | {:public_function, []}, 97 | {:public_function, []}, 98 | {:private_function_a, []}, 99 | {:private_function_b, []} 100 | ] 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/user/fake_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.FakeTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Fake.Fake 6 | alias Patch.Test.Support.User.Fake.Real 7 | 8 | describe "fake/2" do 9 | test "replaces the real module with a fake module" do 10 | assert {:real, {:example, :a}} == Real.example(:a) 11 | 12 | fake(Real, Fake) 13 | 14 | assert {:fake, {:example, :a}} == Real.example(:a) 15 | end 16 | 17 | test "real is still available with through the real/1 function" do 18 | assert {:real, {:example, :a}} == Real.example(:a) 19 | 20 | fake(Real, Fake) 21 | 22 | assert {:real, {:example, :a}} == real(Real).example(:a) 23 | end 24 | 25 | test "fake module can delegate to the real module" do 26 | assert {:real, {:delegate, :a}} == Real.delegate(:a) 27 | 28 | fake(Real, Fake) 29 | 30 | assert {:fake, {:real, {:delegate, :a}}} == Real.delegate(:a) 31 | end 32 | 33 | test "attempting to use the real module before faking raises UndefinedFunctionError" do 34 | assert_raise UndefinedFunctionError, fn -> 35 | real(Real).example(:a) 36 | end 37 | end 38 | 39 | test "assert_called when call is present" do 40 | fake(Real, Fake) 41 | 42 | assert {:fake, {:example, :present}} == Real.example(:present) 43 | assert_called Real.example(:present) 44 | end 45 | 46 | test "assert_called raises when call is absent" do 47 | fake(Real, Fake) 48 | 49 | assert {:fake, {:example, :present}} == Real.example(:present) 50 | 51 | assert_raise Patch.MissingCall, fn -> 52 | assert_called Real.example(:absent) 53 | end 54 | end 55 | 56 | test "refute_called when call is present" do 57 | fake(Real, Fake) 58 | 59 | assert {:fake, {:example, :present}} == Real.example(:present) 60 | 61 | assert_raise Patch.UnexpectedCall, fn -> 62 | refute_called Real.example(:present) 63 | end 64 | end 65 | 66 | test "refute_called when call is absent" do 67 | fake(Real, Fake) 68 | 69 | assert {:fake, {:example, :present}} == Real.example(:present) 70 | refute_called Real.example(:absent) 71 | end 72 | 73 | test "assert_any_called when no calls have happened" do 74 | fake(Real, Fake) 75 | 76 | assert_raise Patch.MissingCall, fn -> 77 | assert_any_call Real, :example 78 | end 79 | end 80 | 81 | test "assert_any_called when calls have happened" do 82 | fake(Real, Fake) 83 | 84 | assert {:fake, {:example, :a}} == Real.example(:a) 85 | assert_any_call Real, :example 86 | end 87 | 88 | test "refute_any_called when no calls have happened" do 89 | fake(Real, Fake) 90 | 91 | refute_any_call Real, :example 92 | end 93 | 94 | test "refute_any_called when calls have happened" do 95 | fake(Real, Fake) 96 | 97 | assert {:fake, {:example, :a}} == Real.example(:a) 98 | 99 | assert_raise Patch.UnexpectedCall, fn -> 100 | refute_any_call Real, :example 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/user/history_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.HistoryTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.History 6 | 7 | describe "history/2" do 8 | test "records the call to a function" do 9 | spy(History) 10 | 11 | assert History.public_function(:test_argument) == {:public, :test_argument} 12 | 13 | assert history(History) == [{:public_function, [:test_argument]}] 14 | end 15 | 16 | test "records public collaborator calls" do 17 | spy(History) 18 | 19 | assert History.public_caller(:test_argument) == {:original, {:public, :test_argument}} 20 | 21 | assert history(History) == [ 22 | {:public_caller, [:test_argument]}, 23 | {:public_function, [:test_argument]} 24 | ] 25 | end 26 | 27 | test "records private collaborator calls" do 28 | spy(History) 29 | 30 | assert History.private_caller(:test_argument) == {:original, {:private, :test_argument}} 31 | 32 | assert history(History) == [ 33 | {:private_caller, [:test_argument]}, 34 | {:private_function, [:test_argument]} 35 | ] 36 | end 37 | 38 | test "records calls to exposed private functions" do 39 | expose(History, :all) 40 | 41 | assert private(History.private_function(:test_argument)) == {:private, :test_argument} 42 | 43 | assert history(History) == [ 44 | {:private_function, [:test_argument]} 45 | ] 46 | end 47 | 48 | test "does not record calls before patching" do 49 | assert History.public_function(:before_patch) == {:public, :before_patch} 50 | 51 | assert history(History) == [] 52 | 53 | patch(History, :public_function, :patched) 54 | 55 | assert History.public_function(:after_patch) == :patched 56 | 57 | assert history(History) == [{:public_function, [:after_patch]}] 58 | end 59 | 60 | test "changing expose does not discard history" do 61 | patch(History, :public_function, :patched) 62 | 63 | assert History.public_function(:test_argument) == :patched 64 | 65 | assert history(History) == [{:public_function, [:test_argument]}] 66 | 67 | expose(History, :all) 68 | 69 | assert private(History.private_function(:test_argument)) == {:private, :test_argument} 70 | 71 | assert history(History) == [ 72 | {:public_function, [:test_argument]}, 73 | {:private_function, [:test_argument]} 74 | ] 75 | end 76 | 77 | test "history can be returned oldest-first (ascending) or newest-first (descending)" do 78 | spy(History) 79 | 80 | History.public_function(1) 81 | History.public_function(2) 82 | History.public_function(3) 83 | 84 | assert history(History) == [ 85 | {:public_function, [1]}, 86 | {:public_function, [2]}, 87 | {:public_function, [3]} 88 | ] 89 | 90 | assert history(History, :asc) == [ 91 | {:public_function, [1]}, 92 | {:public_function, [2]}, 93 | {:public_function, [3]} 94 | ] 95 | 96 | assert history(History, :desc) == [ 97 | {:public_function, [3]}, 98 | {:public_function, [2]}, 99 | {:public_function, [1]} 100 | ] 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/user/inject_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.InjectTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Inject.Caller 6 | alias Patch.Test.Support.User.Inject.Targetless 7 | 8 | describe "inject/4" do 9 | test "Target listener can be injected into the Caller Process" do 10 | bonus = 5 11 | multiplier = 10 12 | 13 | {:ok, caller_pid} = Caller.start_link(bonus, multiplier) 14 | 15 | inject(:target, caller_pid, [:target_pid]) 16 | 17 | assert Caller.calculate(caller_pid, 7) == 75 # (7 * 10) + 5 18 | 19 | assert_receive {:target, {GenServer, :call, {:work, 7}, from}} 20 | assert_receive {:target, {GenServer, :reply, 70, ^from}} 21 | end 22 | 23 | test "Targetless listener can be injected into the Caller Process" do 24 | {:ok, targetless_pid} = Targetless.start_link() 25 | 26 | inject(:target, targetless_pid, [:target_pid]) 27 | 28 | assert Targetless.hello(targetless_pid) == :ok 29 | 30 | assert_receive {:target, :greetings} 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/user/patch/arity_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.ArityTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Arity 6 | 7 | describe "patch has no arity restrictions" do 8 | test "can patch a function of arity 0" do 9 | assert Arity.function_of_arity_0() == :original 10 | 11 | patch(Arity, :function_of_arity_0, :patched) 12 | 13 | assert Arity.function_of_arity_0() == :patched 14 | end 15 | 16 | test "can patch a function of arity 20" do 17 | assert {:original, _} = Arity.function_of_arity_20(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t) 18 | 19 | patch(Arity, :function_of_arity_20, :patched) 20 | 21 | assert Arity.function_of_arity_20(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t) == :patched 22 | end 23 | end 24 | 25 | 26 | end 27 | -------------------------------------------------------------------------------- /test/user/patch/callable_stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.CallableStackTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.CallableStack 6 | 7 | describe "patch/3 with Stacked Callables" do 8 | test "callable can replace non-callable without stacking" do 9 | assert CallableStack.example(:a) == {:original, :a} 10 | 11 | patch(CallableStack, :example, :patched) 12 | assert CallableStack.example(:a) == :patched 13 | 14 | patch(CallableStack, :example, fn a -> {:patched, a} end) 15 | assert CallableStack.example(:a) == {:patched, :a} 16 | end 17 | 18 | test "without matching, last assignment wins" do 19 | assert CallableStack.example(:a) == {:original, :a} 20 | 21 | patch(CallableStack, :example, fn a -> {:first_patch, a} end) 22 | assert CallableStack.example(:a) == {:first_patch, :a} 23 | 24 | patch(CallableStack, :example, fn a -> {:second_patch, a} end) 25 | assert CallableStack.example(:a) == {:second_patch, :a} 26 | end 27 | 28 | test "with matching and passthrough evaluation, latest matching wins with original called for no match" do 29 | assert CallableStack.example(:a) == {:original, :a} 30 | 31 | patch(CallableStack, :example, fn :a -> :first_patch end) 32 | patch(CallableStack, :example, fn :b -> :second_patch end) 33 | 34 | assert CallableStack.example(:a) == :first_patch 35 | assert CallableStack.example(:b) == :second_patch 36 | assert CallableStack.example(:c) == {:original, :c} 37 | end 38 | 39 | test "with matching and strict evaluation, latest matching wins with FunctionClauseError for no match" do 40 | assert CallableStack.example(:a) == {:original, :a} 41 | 42 | patch(CallableStack, :example, callable(fn :a -> :first_patch end, evaluate: :strict)) 43 | patch(CallableStack, :example, fn :b -> :second_patch end) 44 | 45 | assert CallableStack.example(:a) == :first_patch 46 | assert CallableStack.example(:b) == :second_patch 47 | 48 | assert_raise FunctionClauseError, fn -> 49 | CallableStack.example(:c) 50 | end 51 | end 52 | 53 | test "stacking can be used on multiple arities" do 54 | assert CallableStack.example(:a) == {:original, :a} 55 | assert CallableStack.example(:a, :b, :c) == {:original, :a, :b, :c} 56 | 57 | patch(CallableStack, :example, fn a -> {:patched, a} end) 58 | patch(CallableStack, :example, fn a, b, c -> {:patched, a, b, c} end) 59 | 60 | assert CallableStack.example(:a) == {:patched, :a} 61 | assert CallableStack.example(:a, :b, :c) == {:patched, :a, :b, :c} 62 | end 63 | 64 | test "stacking can be used with multiple arities and pattern matching" do 65 | assert CallableStack.example(:a) == {:original, :a} 66 | assert CallableStack.example(:a, :b, :c) == {:original, :a, :b, :c} 67 | 68 | patch(CallableStack, :example, fn 1 -> :first_patch end) 69 | patch(CallableStack, :example, fn 1, b, c -> {:second_patch, 1, b, c} end) 70 | 71 | patch(CallableStack, :example, fn 2 -> :third_patch end) 72 | patch(CallableStack, :example, fn 2, b, c -> {:fourth_patch, 2, b, c} end) 73 | 74 | assert CallableStack.example(1) == :first_patch 75 | assert CallableStack.example(1, 2, 3) == {:second_patch, 1, 2, 3} 76 | 77 | assert CallableStack.example(2) == :third_patch 78 | assert CallableStack.example(2, 3, 4) == {:fourth_patch, 2, 3, 4} 79 | 80 | assert CallableStack.example(:a) == {:original, :a} 81 | assert CallableStack.example(:a, :b, :c) == {:original, :a, :b, :c} 82 | end 83 | 84 | test "mock functions can raise FunctionClauseError" do 85 | patch(CallableStack, :example, fn _ -> 86 | raise FunctionClauseError 87 | end) 88 | 89 | assert_raise FunctionClauseError, fn -> 90 | CallableStack.example(:a) 91 | end 92 | end 93 | 94 | test "mock functions can raise BadArityError" do 95 | patch(CallableStack, :example, fn _ -> 96 | raise BadArityError, function: &String.trim/1, args: [] 97 | end) 98 | 99 | assert_raise BadArityError, fn -> 100 | CallableStack.example(:a) 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/user/patch/callable_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.CallableTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Callable 6 | 7 | describe "patch/3 with callable using default configuration" do 8 | test "uses default values for dispatch and evaluation" do 9 | callable = patch(Callable, :example, callable(fn _ -> :patched end)) 10 | 11 | assert callable.dispatch == :apply 12 | assert callable.evaluate == :passthrough 13 | end 14 | end 15 | 16 | describe "patch/3 with callable using apply dispatch and passthrough evaluation" do 17 | test "calling the patched function with an existing but unpatched arity passes through to the original module" do 18 | assert Callable.example(:a) == {:original, :a} 19 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 20 | 21 | patch(Callable, :example, fn a, b, c -> {:patched, a, b, c} end) 22 | 23 | assert Callable.example(:a) == {:original, :a} 24 | assert Callable.example(:a, :b, :c) == {:patched, :a, :b, :c} 25 | end 26 | 27 | test "calling the patched function with arguments that do not match calls the original function" do 28 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 29 | 30 | patch(Callable, :example, fn 1, b, c -> {:patched, 1, b, c} end) 31 | 32 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 33 | assert Callable.example(1, :b, :c) == {:patched, 1, :b, :c} 34 | end 35 | end 36 | 37 | describe "patch/3 with callable using apply dispatch and strict evaluation" do 38 | test "calling the patched function with an existing but unpatched arity raises BadArityError" do 39 | assert Callable.example(:a) == {:original, :a} 40 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 41 | 42 | patch(Callable, :example, callable(fn a, b, c -> {:patched, a, b, c} end, evaluate: :strict)) 43 | 44 | assert_raise BadArityError, fn -> 45 | Callable.example(:a) 46 | end 47 | 48 | assert Callable.example(:a, :b, :c) == {:patched, :a, :b, :c} 49 | end 50 | 51 | test "calling the patched function with arguments that do not match raises FunctionClauseError" do 52 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 53 | 54 | patch(Callable, :example, callable(fn 1, b, c -> {:patched, 1, b, c} end, evaluate: :strict)) 55 | 56 | assert_raise FunctionClauseError, fn -> 57 | Callable.example(:a, :b, :c) 58 | end 59 | 60 | assert Callable.example(1, :b, :c) == {:patched, 1, :b, :c} 61 | end 62 | end 63 | 64 | describe "patch/3 with callable using list dispatch and passthrough evaluation" do 65 | test "provides all arguments in a list using the legacy convention" do 66 | assert Callable.example(:a) == {:original, :a} 67 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 68 | 69 | callable = callable(fn arguments -> {:patched, arguments} end, :list) 70 | 71 | patch(Callable, :example, callable) 72 | 73 | assert Callable.example(:a) == {:patched, [:a]} 74 | assert Callable.example(:a, :b, :c) == {:patched, [:a, :b, :c]} 75 | end 76 | 77 | test "provides all arguments in a list using the options convention" do 78 | assert Callable.example(:a) == {:original, :a} 79 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 80 | 81 | callable = callable(fn arguments -> {:patched, arguments} end, dispatch: :list) 82 | 83 | patch(Callable, :example, callable) 84 | 85 | assert Callable.example(:a) == {:patched, [:a]} 86 | assert Callable.example(:a, :b, :c) == {:patched, [:a, :b, :c]} 87 | end 88 | 89 | test "callable can cover multiple arities using list dispatch" do 90 | assert Callable.example(:a) == {:original, :a} 91 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 92 | 93 | callable = callable(fn 94 | [a] -> 95 | {:patched, a} 96 | 97 | [a, b, c] -> 98 | {:patched, a, b, c} 99 | end, dispatch: :list) 100 | 101 | patch(Callable, :example, callable) 102 | 103 | assert Callable.example(:a) == {:patched, :a} 104 | assert Callable.example(:a, :b, :c) == {:patched, :a, :b, :c} 105 | end 106 | 107 | test "calling the patched function with an existing but unpatched arity passes through to the original module" do 108 | assert Callable.example(:a) == {:original, :a} 109 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 110 | 111 | callable = callable(fn 112 | [a] -> 113 | {:patched, a} 114 | end, dispatch: :list) 115 | 116 | patch(Callable, :example, callable) 117 | 118 | assert Callable.example(:a) == {:patched, :a} 119 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 120 | end 121 | 122 | test "calling the patched function with arguments that do not match calls the original function" do 123 | assert Callable.example(:a) == {:original, :a} 124 | 125 | callable = callable(fn 126 | [1] -> 127 | {:patched, 1} 128 | end, dispatch: :list) 129 | 130 | patch(Callable, :example, callable) 131 | 132 | assert Callable.example(1) == {:patched, 1} 133 | assert Callable.example(:a) == {:original, :a} 134 | end 135 | end 136 | 137 | describe "patch/3 with callable using list dispach and strict evaluation" do 138 | test "provides all arguments in a list using the options convention" do 139 | assert Callable.example(:a) == {:original, :a} 140 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 141 | 142 | callable = callable(fn arguments -> 143 | {:patched, arguments} 144 | end, dispatch: :list, evaluate: :strict) 145 | 146 | patch(Callable, :example, callable) 147 | 148 | assert Callable.example(:a) == {:patched, [:a]} 149 | assert Callable.example(:a, :b, :c) == {:patched, [:a, :b, :c]} 150 | end 151 | 152 | test "callable can cover multiple arities using list dispatch" do 153 | assert Callable.example(:a) == {:original, :a} 154 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 155 | 156 | callable = callable(fn 157 | [a] -> 158 | {:patched, a} 159 | 160 | [a, b, c] -> 161 | {:patched, a, b, c} 162 | end, dispatch: :list, evaluate: :strict) 163 | 164 | patch(Callable, :example, callable) 165 | 166 | assert Callable.example(:a) == {:patched, :a} 167 | assert Callable.example(:a, :b, :c) == {:patched, :a, :b, :c} 168 | end 169 | 170 | test "calling the patched function with an existing but unpatched arity raises FunctionClauseError" do 171 | assert Callable.example(:a) == {:original, :a} 172 | assert Callable.example(:a, :b, :c) == {:original, :a, :b, :c} 173 | 174 | callable = callable(fn 175 | [a] -> 176 | {:patched, a} 177 | end, dispatch: :list, evaluate: :strict) 178 | 179 | patch(Callable, :example, callable) 180 | 181 | assert Callable.example(:a) == {:patched, :a} 182 | 183 | assert_raise FunctionClauseError, fn -> 184 | Callable.example(:a, :b, :c) 185 | end 186 | end 187 | 188 | test "calling the patched function with arguments that do not match raises FunctionClauseError" do 189 | assert Callable.example(:a) == {:original, :a} 190 | 191 | callable = callable(fn 192 | [1] -> 193 | {:patched, 1} 194 | end, dispatch: :list, evaluate: :strict) 195 | 196 | patch(Callable, :example, callable) 197 | 198 | assert Callable.example(1) == {:patched, 1} 199 | 200 | assert_raise FunctionClauseError, fn -> 201 | Callable.example(:a) 202 | end 203 | end 204 | end 205 | 206 | describe "callable configuration" do 207 | test "unknown keys in the configuration raises a ConfigurationError" do 208 | assert_raise Patch.ConfigurationError, fn -> 209 | callable(fn -> :ok end, unknown: :option) 210 | end 211 | end 212 | 213 | test "invalid dispatch modes raises a ConfigurationError" do 214 | assert_raise Patch.ConfigurationError, fn -> 215 | callable(fn -> :ok end, dispatch: :invalid) 216 | end 217 | end 218 | 219 | test "invalid evaluate modes raises a ConfigurationError" do 220 | assert_raise Patch.ConfigurationError, fn -> 221 | callable(fn -> :ok end, evaluate: :invalid) 222 | end 223 | end 224 | end 225 | 226 | describe "callable embedding" do 227 | test "can be embedded in a cycle" do 228 | assert Callable.example(:a) == {:original, :a} 229 | 230 | patch(Callable, :example, cycle([1, fn arg -> {2, arg} end, 3])) 231 | 232 | assert Callable.example(:a) == 1 233 | assert Callable.example(:a) == {2, :a} 234 | assert Callable.example(:a) == 3 235 | assert Callable.example(:b) == 1 236 | assert Callable.example(:b) == {2, :b} 237 | assert Callable.example(:b) == 3 238 | end 239 | 240 | test "can be embedded in a sequence" do 241 | assert Callable.example(:a) == {:original, :a} 242 | 243 | patch(Callable, :example, sequence([1, fn arg -> {2, arg} end, fn arg -> {3, arg} end])) 244 | 245 | assert Callable.example(:a) == 1 246 | assert Callable.example(:a) == {2, :a} 247 | assert Callable.example(:a) == {3, :a} 248 | assert Callable.example(:b) == {3, :b} 249 | 250 | end 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /test/user/patch/cycle_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.CycleTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Cycle 6 | 7 | describe "patch/3 with cycles" do 8 | test "cycles the values on each call" do 9 | assert Cycle.example() == :original 10 | 11 | patch(Cycle, :example, cycle([1, 2, 3])) 12 | 13 | assert Cycle.example() == 1 14 | assert Cycle.example() == 2 15 | assert Cycle.example() == 3 16 | assert Cycle.example() == 1 17 | assert Cycle.example() == 2 18 | assert Cycle.example() == 3 19 | assert Cycle.example() == 1 20 | end 21 | 22 | 23 | 24 | test "cycles can contain functions as scalars" do 25 | assert Cycle.example() == :original 26 | 27 | callable = fn -> 2 end 28 | scalar_callable = scalar(callable) 29 | 30 | patch(Cycle, :example, cycle([1, scalar_callable, 3])) 31 | 32 | assert Cycle.example() == 1 33 | assert Cycle.example() == callable 34 | assert Cycle.example() == 3 35 | assert Cycle.example() == 1 36 | assert Cycle.example() == callable 37 | assert Cycle.example() == 3 38 | assert Cycle.example() == 1 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/user/patch/erlang_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.ErlangTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | describe "patching erlang modules" do 6 | test "non-sticky erlang modules can be patched" do 7 | patch(:erlang_unsticky, :example_function, :test_value) 8 | 9 | assert :erlang_unsticky.example_function() == :test_value 10 | end 11 | 12 | 13 | test "sticky erlang modules can be patched" do 14 | patch(:array, :new, :test_value) 15 | 16 | assert :array.new() == :test_value 17 | end 18 | 19 | test "sticky erlang modules with erlang builtin functions can be patched" do 20 | patch(:string, :is_empty, :test_value) 21 | 22 | assert :string.is_empty("test") == :test_value 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/user/patch/execution_context_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.ExecutionContextTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.ExecutionContext 6 | 7 | describe "execution context" do 8 | test "patch executes the mock function in the caller's execution context" do 9 | assert ExecutionContext.example() == :original 10 | 11 | patch(ExecutionContext, :example, fn -> self() end) 12 | 13 | assert ExecutionContext.example() == self() 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/user/patch/freezer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.FreezerTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Freezer 6 | 7 | describe "Internal Dependencies" do 8 | test "patching GenServer does not break Patch" do 9 | {:ok, pid} = Freezer.start_link() 10 | 11 | assert :ok == Freezer.work(pid) 12 | 13 | patch(GenServer, :call, :patched) 14 | 15 | assert :patched == Freezer.work(pid) 16 | 17 | assert_called GenServer.call(^pid, :work) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/user/patch/gen_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.GenServerTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.GenServerExample, as: Example 6 | 7 | describe "Patch GenServer Support" do 8 | test "can patch callbacks" do 9 | start_supervised(Example) 10 | 11 | assert Example.a(:a) == {:original, :a} 12 | assert Example.b(:b) == {:original, :b} 13 | assert Example.c(:c) == {:original, :c} 14 | 15 | patch(Example, :handle_call, fn {_message, argument}, _from, state -> {:reply, {:patched, argument}, state} end) 16 | 17 | assert Example.a(:a) == {:patched, :a} 18 | assert Example.b(:b) == {:patched, :b} 19 | assert Example.c(:c) == {:patched, :c} 20 | end 21 | 22 | test "can patch subset of callbacks with a single patch call" do 23 | start_supervised(Example) 24 | 25 | assert Example.a(:a) == {:original, :a} 26 | assert Example.b(:b) == {:original, :b} 27 | assert Example.c(:c) == {:original, :c} 28 | 29 | patch(Example, :handle_call, fn 30 | {:a, argument}, _from, state -> 31 | {:reply, {:patched, argument}, state} 32 | 33 | {:b, argument}, _from, state -> 34 | {:reply, {:patched, argument}, state} 35 | end) 36 | 37 | assert Example.a(:a) == {:patched, :a} 38 | assert Example.b(:b) == {:patched, :b} 39 | assert Example.c(:c) == {:original, :c} 40 | end 41 | 42 | test "can patch subset of callbacks with multiple patch calls" do 43 | start_supervised(Example) 44 | 45 | assert Example.a(:a) == {:original, :a} 46 | assert Example.b(:b) == {:original, :b} 47 | assert Example.c(:c) == {:original, :c} 48 | 49 | patch(Example, :handle_call, fn 50 | {:a, argument}, _from, state -> 51 | {:reply, {:patched, argument}, state} 52 | end) 53 | 54 | patch(Example, :handle_call, fn 55 | {:b, argument}, _from, state -> 56 | {:reply, {:patched, argument}, state} 57 | end) 58 | 59 | assert Example.a(:a) == {:patched, :a} 60 | assert Example.b(:b) == {:patched, :b} 61 | assert Example.c(:c) == {:original, :c} 62 | end 63 | 64 | test "can change how a callback handles state" do 65 | {:ok, pid} = start_supervised(Example) 66 | 67 | assert Example.a(:a) == {:original, :a} 68 | assert :sys.get_state(pid) == [{:a, :a}] 69 | 70 | patch(Example, :handle_call, fn 71 | {:a, argument}, _from, state -> 72 | {:reply, {:patched, argument}, [:patched | state]} 73 | end) 74 | 75 | assert Example.a(:a) == {:patched, :a} 76 | assert :sys.get_state(pid) == [:patched, {:a, :a}] 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/user/patch/local_call_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.LocalCallTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.LocalCall 6 | 7 | describe "local calls to public functions" do 8 | test "can be patched" do 9 | assert LocalCall.public_caller(:test_argument) == {:original, {:public, :test_argument}} 10 | 11 | patch(LocalCall, :public_function, :patched) 12 | 13 | assert LocalCall.public_caller(:test_argument) == {:original, :patched} 14 | end 15 | 16 | test "can be assert_called" do 17 | assert LocalCall.public_caller(:test_argument) == {:original, {:public, :test_argument}} 18 | 19 | patch(LocalCall, :public_function, :patched) 20 | 21 | assert LocalCall.public_caller(:test_argument) == {:original, :patched} 22 | 23 | assert_called LocalCall.public_function(:test_argument) 24 | end 25 | 26 | test "can be refute_called" do 27 | assert LocalCall.public_caller(:test_argument) == {:original, {:public, :test_argument}} 28 | 29 | patch(LocalCall, :public_function, :patched) 30 | 31 | refute_called LocalCall.public_function(_) 32 | end 33 | end 34 | 35 | describe "local calls to private functions" do 36 | test "can be patched" do 37 | assert LocalCall.private_caller(:test_argument) == {:original, {:private, :test_argument}} 38 | 39 | patch(LocalCall, :private_function, :patched) 40 | 41 | assert LocalCall.private_caller(:test_argument) == {:original, :patched} 42 | end 43 | 44 | test "can be assert_called" do 45 | assert LocalCall.private_caller(:test_argument) == {:original, {:private, :test_argument}} 46 | 47 | patch(LocalCall, :private_function, :patched) 48 | 49 | assert LocalCall.private_caller(:test_argument) == {:original, :patched} 50 | 51 | assert_called LocalCall.private_function(:test_argument) 52 | end 53 | 54 | test "can be refute_called" do 55 | assert LocalCall.private_caller(:test_argument) == {:original, {:private, :test_argument}} 56 | 57 | patch(LocalCall, :private_function, :patched) 58 | 59 | refute_called LocalCall.private_function(_) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/user/patch/raises_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.RaisesTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Raises 6 | 7 | describe "patch/3 with raises" do 8 | test "can be used to raise a runtime exception" do 9 | assert Raises.example() == :original 10 | 11 | patch(Raises, :example, raises("patched")) 12 | 13 | assert_raise RuntimeError, "patched", fn -> 14 | Raises.example() 15 | end 16 | end 17 | 18 | test "can be used to raise a specific exception" do 19 | assert Raises.example() == :original 20 | 21 | patch(Raises, :example, raises(ArgumentError, message: "patched")) 22 | 23 | assert_raise ArgumentError, "patched", fn -> 24 | Raises.example() 25 | end 26 | end 27 | 28 | test "can be embedded in a cycle" do 29 | assert Raises.example() == :original 30 | 31 | patch(Raises, :example, cycle([1, raises("patched"), 2])) 32 | 33 | assert Raises.example() == 1 34 | assert_raise RuntimeError, "patched", fn -> 35 | Raises.example() 36 | end 37 | assert Raises.example() == 2 38 | 39 | assert Raises.example() == 1 40 | assert_raise RuntimeError, "patched", fn -> 41 | Raises.example() 42 | end 43 | assert Raises.example() == 2 44 | end 45 | 46 | test "can be embedded in a sequence" do 47 | assert Raises.example() == :original 48 | 49 | patch(Raises, :example, sequence([1, raises("patched"), 2])) 50 | 51 | assert Raises.example() == 1 52 | assert_raise RuntimeError, "patched", fn -> 53 | Raises.example() 54 | end 55 | assert Raises.example() == 2 56 | assert Raises.example() == 2 57 | assert Raises.example() == 2 58 | end 59 | 60 | test "can raise BadArityError" do 61 | assert Raises.example() == :original 62 | 63 | patch(Raises, :example, raises(BadArityError, function: fn -> :ok end, args: [])) 64 | 65 | assert_raise BadArityError, fn -> 66 | Raises.example() 67 | end 68 | end 69 | 70 | test "can raise FunctionClauseError" do 71 | assert Raises.example() == :original 72 | 73 | patch(Raises, :example, raises(FunctionClauseError, function: nil)) 74 | 75 | assert_raise FunctionClauseError, fn -> 76 | Raises.example() 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/user/patch/scalar_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.ScalarTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Scalar 6 | 7 | describe "patch/3 with scalars" do 8 | test "returns the implicit scalar" do 9 | assert Scalar.example() == :original 10 | 11 | patch(Scalar, :example, :test_implicit_scalar) 12 | 13 | assert Scalar.example() == :test_implicit_scalar 14 | end 15 | 16 | test "returns the explicit scalar" do 17 | assert Scalar.example() == :original 18 | 19 | patch(Scalar, :example, scalar(:test_explicit_scalar)) 20 | 21 | assert Scalar.example() == :test_explicit_scalar 22 | end 23 | 24 | test "functions can be wrapped with scalar/1" do 25 | assert Scalar.example() == :original 26 | 27 | function = fn -> 28 | :ok 29 | end 30 | 31 | patch(Scalar, :example, scalar(function)) 32 | 33 | assert Scalar.example() == function 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/user/patch/sequence_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.SequenceTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Sequence 6 | 7 | describe "patch/3 with sequence" do 8 | test "empty sequence always returns nil" do 9 | assert Sequence.example() == :original 10 | 11 | patch(Sequence, :example, sequence([])) 12 | 13 | assert is_nil(Sequence.example()) 14 | assert is_nil(Sequence.example()) 15 | end 16 | 17 | test "sequence of 1 always returns the only element" do 18 | assert Sequence.example() == :original 19 | 20 | patch(Sequence, :example, sequence([1])) 21 | 22 | assert Sequence.example() == 1 23 | assert Sequence.example() == 1 24 | end 25 | 26 | test "sequence of N returns the values in order" do 27 | assert Sequence.example() == :original 28 | 29 | patch(Sequence, :example, sequence([1, 2, 3])) 30 | 31 | assert Sequence.example() == 1 32 | assert Sequence.example() == 2 33 | assert Sequence.example() == 3 34 | end 35 | 36 | test "exhausted sequences continue to return the last element repeatedly" do 37 | assert Sequence.example() == :original 38 | 39 | patch(Sequence, :example, sequence([1, 2, 3])) 40 | 41 | assert Sequence.example() == 1 42 | assert Sequence.example() == 2 43 | assert Sequence.example() == 3 44 | assert Sequence.example() == 3 45 | assert Sequence.example() == 3 46 | end 47 | 48 | test "sequence can contain functions as scalars" do 49 | assert Sequence.example() == :original 50 | 51 | callable = fn -> 2 end 52 | scalar_callable = scalar(callable) 53 | 54 | patch(Sequence, :example, sequence([1, scalar_callable, 3])) 55 | 56 | assert Sequence.example() == 1 57 | assert Sequence.example() == callable 58 | assert Sequence.example() == 3 59 | assert Sequence.example() == 3 60 | assert Sequence.example() == 3 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/user/patch/throws_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.ThrowsTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.Throws 6 | 7 | describe "patch/3 with throws" do 8 | test "can be used to throw a value" do 9 | assert Throws.example() == :original 10 | 11 | patch(Throws, :example, throws(:patched)) 12 | 13 | assert catch_throw(Throws.example()) == :patched 14 | end 15 | 16 | test "can be embedded in a cycle" do 17 | assert Throws.example() == :original 18 | 19 | patch(Throws, :example, cycle([1, throws(:patched), 2])) 20 | 21 | assert Throws.example() == 1 22 | assert catch_throw(Throws.example()) == :patched 23 | assert Throws.example() == 2 24 | 25 | assert Throws.example() == 1 26 | assert catch_throw(Throws.example()) == :patched 27 | assert Throws.example() == 2 28 | end 29 | 30 | test "can be embedded in a sequence" do 31 | assert Throws.example() == :original 32 | 33 | patch(Throws, :example, sequence([1, throws(:patched), 2])) 34 | 35 | assert Throws.example() == 1 36 | assert catch_throw(Throws.example()) == :patched 37 | assert Throws.example() == 2 38 | assert Throws.example() == 2 39 | assert Throws.example() == 2 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/user/patch/unknown_function_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.Patch.UnknownFunctionTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Patch.UnknownFunction 6 | 7 | describe "patching unknown functions" do 8 | test "is a no-op" do 9 | patch(UnknownFunction, :function_that_does_not_exist, :patched) 10 | 11 | assert_raise UndefinedFunctionError, fn -> 12 | apply(UnknownFunction, :function_that_does_not_exist, []) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/user/private_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.PrivateTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Private 6 | 7 | describe "private/1" do 8 | test "can call a private function without raising a compiler warning" do 9 | assert_raise UndefinedFunctionError, fn -> 10 | private(Private.private_function(:test_argument)) 11 | end 12 | end 13 | 14 | test "can call an exposed private function without raising a compiler warning" do 15 | expose(Private, :all) 16 | 17 | patch(Private, :private_function, :patched) 18 | 19 | assert private(Private.private_function(:test_argument)) == :patched 20 | end 21 | end 22 | 23 | describe "private/2" do 24 | test "can pipeline into a private call" do 25 | expose(Private, :all) 26 | 27 | patch(Private, :private_function, fn argument -> {:patched, argument} end) 28 | 29 | result = 30 | :test 31 | |> private(Private.private_function()) 32 | 33 | assert result == {:patched, :test} 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/user/readme.md: -------------------------------------------------------------------------------- 1 | # User Tests 2 | 3 | User tests provide a set of tests that test Patch from the point of view of the user. 4 | 5 | These are high level integration tests that do not interrogate the internals of how patch works but instead just attempt to use the library as it is intended to be used. 6 | 7 | ## Structure 8 | 9 | Each user test should have a minimal support module. Tests should not share support modules to prevent any support module from becoming too complex. 10 | 11 | ## Regressions / Bugs 12 | 13 | These tests are key to preventing regressions and validating and fixing bugs. If you believe you have found a bug in Patch, writing a failing user test is a good way to communicate what is expected to happen and provide a reproduction case. 14 | 15 | ## Documentation Augmentation 16 | 17 | The tests here show simplified but varied use of the library interface. These test compliment the documentation by providing many approachable examples of all the ways the library affordances can be used that are guaranteed to work correctly since they are tests. 18 | 19 | -------------------------------------------------------------------------------- /test/user/reflection_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.ReflectionTest do 2 | use ExUnit.Case 3 | 4 | alias Patch.Reflection 5 | alias Patch.Test.Support.User.Reflection.ElixirTarget 6 | 7 | describe "find_functions/1" do 8 | test "returns the public functions of an Elixir Module" do 9 | functions = Reflection.find_functions(ElixirTarget) 10 | 11 | assert {:public_function, 1} in functions 12 | assert {:public_function, 2} in functions 13 | assert {:other_public_function, 0} in functions 14 | end 15 | 16 | test "does not return the private functions of an Elixir Module" do 17 | functions = Reflection.find_functions(ElixirTarget) 18 | 19 | refute {:private_function, 1} in functions 20 | end 21 | 22 | test "returns the public functions of an Erlang Module" do 23 | functions = Reflection.find_functions(:erlang_target) 24 | 25 | assert {:public_function, 1} in functions 26 | assert {:public_function, 2} in functions 27 | assert {:other_public_function, 0} in functions 28 | end 29 | 30 | test "does not return the private functions of an Erlang Module" do 31 | functions = Reflection.find_functions(:erlang_target) 32 | 33 | refute {:private_function, 1} in functions 34 | end 35 | 36 | test "empty list for unknown modules" do 37 | assert [] == Reflection.find_functions(UnknownModule) 38 | end 39 | end 40 | 41 | describe "find_arities/2" do 42 | test "returns the arities of the public function of an Elixir Module" do 43 | arities = Reflection.find_arities(ElixirTarget, :public_function) 44 | 45 | assert Enum.sort(arities) == [1, 2] 46 | end 47 | 48 | test "empty list for private functions of an Elixir Module" do 49 | assert [] == Reflection.find_arities(ElixirTarget, :private_function) 50 | end 51 | 52 | test "empty list for unknown functions of an Elixir Module" do 53 | assert [] == Reflection.find_arities(ElixirTarget, :unkonwn_function) 54 | end 55 | 56 | test "returns the arities of the public function of an Erlang Module" do 57 | arities = Reflection.find_arities(:erlang_target, :public_function) 58 | 59 | assert Enum.sort(arities) == [1, 2] 60 | end 61 | 62 | test "empty list for private functions of an Erlang Module" do 63 | assert [] == Reflection.find_arities(:erlang_target, :private_function) 64 | end 65 | 66 | test "empty list for unknown functions of an Erlang Module" do 67 | assert [] == Reflection.find_arities(:erlang_target, :unkonwn_function) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/user/refute_any_call_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.RefuteAnyCallTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.RefuteAnyCall 6 | 7 | describe "refute_any_call/1" do 8 | test "raises if a patched function has a call of any arity (/1)" do 9 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 10 | 11 | assert :patched == RefuteAnyCall.function_with_multiple_arities(1) 12 | 13 | assert_raise Patch.UnexpectedCall, fn -> 14 | refute_any_call RefuteAnyCall.function_with_multiple_arities 15 | end 16 | end 17 | 18 | test "raises if a patched function has a call of any arity (/2)" do 19 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 20 | 21 | assert :patched == RefuteAnyCall.function_with_multiple_arities(1, 2) 22 | 23 | assert_raise Patch.UnexpectedCall, fn -> 24 | refute_any_call RefuteAnyCall.function_with_multiple_arities 25 | end 26 | end 27 | 28 | test "raises if a patched function has a call of any arity (/3)" do 29 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 30 | 31 | assert :patched == RefuteAnyCall.function_with_multiple_arities(1, 2, 3) 32 | 33 | assert_raise Patch.UnexpectedCall, fn -> 34 | refute_any_call RefuteAnyCall.function_with_multiple_arities 35 | end 36 | end 37 | 38 | test "raises if a spied module has a call of any arity (/1)" do 39 | spy(RefuteAnyCall) 40 | 41 | assert {:original, 1} == RefuteAnyCall.function_with_multiple_arities(1) 42 | 43 | assert_raise Patch.UnexpectedCall, fn -> 44 | refute_any_call RefuteAnyCall.function_with_multiple_arities 45 | end 46 | end 47 | 48 | test "raises if a spied module has a call of any arity (/2)" do 49 | spy(RefuteAnyCall) 50 | 51 | assert {:original, {1, 2}} == RefuteAnyCall.function_with_multiple_arities(1, 2) 52 | 53 | assert_raise Patch.UnexpectedCall, fn -> 54 | refute_any_call RefuteAnyCall.function_with_multiple_arities 55 | end 56 | end 57 | 58 | test "raises if a spied module has a call of any arity (/3)" do 59 | spy(RefuteAnyCall) 60 | 61 | assert {:original, {1, 2, 3}} == RefuteAnyCall.function_with_multiple_arities(1, 2, 3) 62 | 63 | assert_raise Patch.UnexpectedCall, fn -> 64 | refute_any_call RefuteAnyCall.function_with_multiple_arities 65 | end 66 | end 67 | 68 | test "does not raise if a patched function has no calls" do 69 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 70 | refute_any_call RefuteAnyCall.function_with_multiple_arities 71 | end 72 | 73 | test "does not raise if a spied module function has no calls" do 74 | spy(RefuteAnyCall) 75 | refute_any_call RefuteAnyCall.function_with_multiple_arities 76 | end 77 | 78 | test "does not raise if another function is called" do 79 | patch(RefuteAnyCall, :other_function, :patched) 80 | 81 | assert RefuteAnyCall.other_function(1) == :patched 82 | 83 | refute_any_call RefuteAnyCall.function_with_multiple_arities 84 | end 85 | 86 | test "exception formatting" do 87 | patch(RefuteAnyCall, :other_function, :patched) 88 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 89 | 90 | assert RefuteAnyCall.function_with_multiple_arities(1) == :patched 91 | assert RefuteAnyCall.other_function(1) == :patched 92 | 93 | expected_message = """ 94 | \n 95 | Unexpected call received, expected no calls: 96 | 97 | Patch.Test.Support.User.RefuteAnyCall.function_with_multiple_arities 98 | 99 | Calls which were received (matching calls are marked with *): 100 | 101 | * 1. Patch.Test.Support.User.RefuteAnyCall.function_with_multiple_arities(1) 102 | 2. Patch.Test.Support.User.RefuteAnyCall.other_function(1) 103 | """ 104 | 105 | assert_raise Patch.UnexpectedCall, expected_message, fn -> 106 | refute_any_call RefuteAnyCall.function_with_multiple_arities 107 | end 108 | end 109 | end 110 | 111 | 112 | describe "refute_any_call/2" do 113 | test "raises if a patched function has a call of any arity (/1)" do 114 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 115 | 116 | assert :patched == RefuteAnyCall.function_with_multiple_arities(1) 117 | 118 | assert_raise Patch.UnexpectedCall, fn -> 119 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 120 | end 121 | end 122 | 123 | test "raises if a patched function has a call of any arity (/2)" do 124 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 125 | 126 | assert :patched == RefuteAnyCall.function_with_multiple_arities(1, 2) 127 | 128 | assert_raise Patch.UnexpectedCall, fn -> 129 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 130 | end 131 | end 132 | 133 | test "raises if a patched function has a call of any arity (/3)" do 134 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 135 | 136 | assert :patched == RefuteAnyCall.function_with_multiple_arities(1, 2, 3) 137 | 138 | assert_raise Patch.UnexpectedCall, fn -> 139 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 140 | end 141 | end 142 | 143 | test "raises if a spied module has a call of any arity (/1)" do 144 | spy(RefuteAnyCall) 145 | 146 | assert {:original, 1} == RefuteAnyCall.function_with_multiple_arities(1) 147 | 148 | assert_raise Patch.UnexpectedCall, fn -> 149 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 150 | end 151 | end 152 | 153 | test "raises if a spied module has a call of any arity (/2)" do 154 | spy(RefuteAnyCall) 155 | 156 | assert {:original, {1, 2}} == RefuteAnyCall.function_with_multiple_arities(1, 2) 157 | 158 | assert_raise Patch.UnexpectedCall, fn -> 159 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 160 | end 161 | end 162 | 163 | test "raises if a spied module has a call of any arity (/3)" do 164 | spy(RefuteAnyCall) 165 | 166 | assert {:original, {1, 2, 3}} == RefuteAnyCall.function_with_multiple_arities(1, 2, 3) 167 | 168 | assert_raise Patch.UnexpectedCall, fn -> 169 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 170 | end 171 | end 172 | 173 | test "does not raise if a patched function has no calls" do 174 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 175 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 176 | end 177 | 178 | test "does not raise if a spied module function has no calls" do 179 | spy(RefuteAnyCall) 180 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 181 | end 182 | 183 | test "does not raise if another function is called" do 184 | patch(RefuteAnyCall, :other_function, :patched) 185 | 186 | assert RefuteAnyCall.other_function(1) == :patched 187 | 188 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 189 | end 190 | 191 | test "exception formatting" do 192 | patch(RefuteAnyCall, :other_function, :patched) 193 | patch(RefuteAnyCall, :function_with_multiple_arities, :patched) 194 | 195 | assert RefuteAnyCall.function_with_multiple_arities(1) == :patched 196 | assert RefuteAnyCall.other_function(1) == :patched 197 | 198 | expected_message = """ 199 | \n 200 | Unexpected call received, expected no calls: 201 | 202 | Patch.Test.Support.User.RefuteAnyCall.function_with_multiple_arities 203 | 204 | Calls which were received (matching calls are marked with *): 205 | 206 | * 1. Patch.Test.Support.User.RefuteAnyCall.function_with_multiple_arities(1) 207 | 2. Patch.Test.Support.User.RefuteAnyCall.other_function(1) 208 | """ 209 | 210 | assert_raise Patch.UnexpectedCall, expected_message, fn -> 211 | refute_any_call RefuteAnyCall, :function_with_multiple_arities 212 | end 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /test/user/refute_called_once_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.RefuteCalledOnecTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.RefuteCalledOnce 6 | 7 | describe "refute_called_once/1" do 8 | test "exact call can be refuted" do 9 | patch(RefuteCalledOnce, :example, :patched) 10 | 11 | assert RefuteCalledOnce.example(1, 2) == :patched 12 | 13 | refute_called_once RefuteCalledOnce.example(3, 4) 14 | end 15 | 16 | test "exact call that have happened raise UnexpectedCall" do 17 | patch(RefuteCalledOnce, :example, :patched) 18 | 19 | assert RefuteCalledOnce.example(1, 2) == :patched 20 | 21 | assert_raise Patch.UnexpectedCall, fn -> 22 | refute_called_once RefuteCalledOnce.example(1, 2) 23 | end 24 | end 25 | 26 | test "exact call after multiple calls can be refuted" do 27 | patch(RefuteCalledOnce, :example, :patched) 28 | 29 | assert RefuteCalledOnce.example(1, 2) == :patched 30 | assert RefuteCalledOnce.example(1, 2) == :patched 31 | 32 | refute_called_once RefuteCalledOnce.example(1, 2) 33 | end 34 | 35 | test "partial call can be refuted" do 36 | patch(RefuteCalledOnce, :example, :patched) 37 | 38 | assert RefuteCalledOnce.example(1, 2) == :patched 39 | 40 | refute_called_once RefuteCalledOnce.example(3, _) 41 | refute_called_once RefuteCalledOnce.example(_, 4) 42 | end 43 | 44 | test "partial call that match raises UnexpectedCall" do 45 | patch(RefuteCalledOnce, :example, :patched) 46 | 47 | assert RefuteCalledOnce.example(1, 2) == :patched 48 | 49 | assert_raise Patch.UnexpectedCall, fn -> 50 | refute_called_once RefuteCalledOnce.example(1, _) 51 | end 52 | 53 | assert_raise Patch.UnexpectedCall, fn -> 54 | refute_called_once RefuteCalledOnce.example(_, 2) 55 | end 56 | end 57 | 58 | test "partial call after multiple calls can be refuted" do 59 | patch(RefuteCalledOnce, :example, :patched) 60 | 61 | assert RefuteCalledOnce.example(1, 2) == :patched 62 | assert RefuteCalledOnce.example(1, 3) == :patched 63 | assert RefuteCalledOnce.example(3, 2) == :patched 64 | 65 | refute_called_once RefuteCalledOnce.example(1, _) 66 | refute_called_once RefuteCalledOnce.example(_, 2) 67 | end 68 | 69 | test "an uncalled function can be wildcard refuted" do 70 | patch(RefuteCalledOnce, :example, :patched) 71 | 72 | refute_called_once RefuteCalledOnce.example(_, _) 73 | end 74 | 75 | test "any call causes a wildcard to raise UnexpectedCall" do 76 | patch(RefuteCalledOnce, :example, :patched) 77 | 78 | assert RefuteCalledOnce.example(1, 2) == :patched 79 | 80 | assert_raise Patch.UnexpectedCall, fn -> 81 | refute_called_once RefuteCalledOnce.example(_, _) 82 | end 83 | end 84 | 85 | test "wildcard call with multiple calls can be refuted" do 86 | patch(RefuteCalledOnce, :example, :patched) 87 | 88 | assert RefuteCalledOnce.example(1, 2) == :patched 89 | assert RefuteCalledOnce.example(3, 4) == :patched 90 | 91 | refute_called_once RefuteCalledOnce.example(_, _) 92 | end 93 | 94 | test "exception formatting" do 95 | patch(RefuteCalledOnce, :example, :patched) 96 | 97 | assert RefuteCalledOnce.example(1, 2) == :patched 98 | assert RefuteCalledOnce.example(3, 4) == :patched 99 | 100 | expected_message = """ 101 | \n 102 | Expected the following call to occur any number of times but once, but it occurred once: 103 | 104 | Patch.Test.Support.User.RefuteCalledOnce.example(1, 2) 105 | 106 | Calls which were received (matching calls are marked with *): 107 | 108 | * 1. Patch.Test.Support.User.RefuteCalledOnce.example(1, 2) 109 | 2. Patch.Test.Support.User.RefuteCalledOnce.example(3, 4) 110 | """ 111 | 112 | assert_raise Patch.UnexpectedCall, expected_message, fn -> 113 | refute_called_once RefuteCalledOnce.example(1, 2) 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/user/refute_called_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.RefuteCalledTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.RefuteCalled 6 | 7 | describe "refute_called/1" do 8 | test "exact call can be refuted" do 9 | patch(RefuteCalled, :example, :patched) 10 | 11 | assert RefuteCalled.example(1, 2) == :patched 12 | 13 | refute_called RefuteCalled.example(3, 4) 14 | end 15 | 16 | test "exact calls that have happened raise UnexpectedCall" do 17 | patch(RefuteCalled, :example, :patched) 18 | 19 | assert RefuteCalled.example(1, 2) == :patched 20 | 21 | assert_raise Patch.UnexpectedCall, fn -> 22 | refute_called RefuteCalled.example(1, 2) 23 | end 24 | end 25 | 26 | test "partial call can be refuted" do 27 | patch(RefuteCalled, :example, :patched) 28 | 29 | assert RefuteCalled.example(1, 2) == :patched 30 | 31 | refute_called RefuteCalled.example(3, _) 32 | refute_called RefuteCalled.example(_, 4) 33 | end 34 | 35 | test "partial calls that match raises UnexpectedCall" do 36 | patch(RefuteCalled, :example, :patched) 37 | 38 | assert RefuteCalled.example(1, 2) == :patched 39 | 40 | assert_raise Patch.UnexpectedCall, fn -> 41 | refute_called RefuteCalled.example(1, _) 42 | end 43 | 44 | assert_raise Patch.UnexpectedCall, fn -> 45 | refute_called RefuteCalled.example(_, 2) 46 | end 47 | end 48 | 49 | test "an uncalled function can be wildcard refuted" do 50 | patch(RefuteCalled, :example, :patched) 51 | 52 | refute_called RefuteCalled.example(_, _) 53 | end 54 | 55 | test "any call causes a wildcard refute to raise UnexpectedCall" do 56 | patch(RefuteCalled, :example, :patched) 57 | 58 | assert RefuteCalled.example(1, 2) == :patched 59 | 60 | assert_raise Patch.UnexpectedCall, fn -> 61 | refute_called RefuteCalled.example(_, _) 62 | end 63 | end 64 | 65 | test "exception formatting" do 66 | patch(RefuteCalled, :example, :patched) 67 | 68 | assert RefuteCalled.example(1, 2) == :patched 69 | assert RefuteCalled.example(3, 4) == :patched 70 | assert RefuteCalled.example(1, 2) == :patched 71 | 72 | expected_message = """ 73 | \n 74 | Unexpected call received: 75 | 76 | Patch.Test.Support.User.RefuteCalled.example(1, 2) 77 | 78 | Calls which were received (matching calls are marked with *): 79 | 80 | * 1. Patch.Test.Support.User.RefuteCalled.example(1, 2) 81 | 2. Patch.Test.Support.User.RefuteCalled.example(3, 4) 82 | * 3. Patch.Test.Support.User.RefuteCalled.example(1, 2) 83 | """ 84 | 85 | assert_raise Patch.UnexpectedCall, expected_message, fn -> 86 | refute_called RefuteCalled.example(1, 2) 87 | end 88 | end 89 | end 90 | 91 | describe "refute_called/2" do 92 | test "exact call can be refuted with literal count" do 93 | patch(RefuteCalled, :example, :patched) 94 | 95 | assert RefuteCalled.example(1, 2) == :patched 96 | 97 | refute_called RefuteCalled.example(1, 2), 2 98 | 99 | assert RefuteCalled.example(1, 2) == :patched 100 | 101 | refute_called RefuteCalled.example(1, 2), 1 102 | end 103 | 104 | test "exact call can be refuted with expression count" do 105 | patch(RefuteCalled, :example, :patched) 106 | 107 | unexpected_count = 2 108 | 109 | assert RefuteCalled.example(1, 2) == :patched 110 | 111 | refute_called RefuteCalled.example(1, 2), unexpected_count 112 | 113 | assert RefuteCalled.example(1, 2) == :patched 114 | 115 | refute_called RefuteCalled.example(1, 2), unexpected_count - 1 116 | end 117 | 118 | test "exact calls that have happened count times raise UnexpectedCall" do 119 | patch(RefuteCalled, :example, :patched) 120 | 121 | assert RefuteCalled.example(1, 2) == :patched 122 | 123 | assert_raise Patch.UnexpectedCall, fn -> 124 | refute_called RefuteCalled.example(1, 2), 1 125 | end 126 | end 127 | 128 | test "partial call can be refuted with literal count" do 129 | patch(RefuteCalled, :example, :patched) 130 | 131 | assert RefuteCalled.example(1, 2) == :patched 132 | 133 | refute_called RefuteCalled.example(1, _), 2 134 | refute_called RefuteCalled.example(_, 2), 2 135 | end 136 | 137 | test "partial call can be refuted with expression count" do 138 | patch(RefuteCalled, :example, :patched) 139 | 140 | assert RefuteCalled.example(1, 2) == :patched 141 | 142 | unexpected_count = 2 143 | 144 | refute_called RefuteCalled.example(1, _), unexpected_count 145 | refute_called RefuteCalled.example(_, 2), unexpected_count + 1 146 | end 147 | 148 | test "partial calls that match raises UnexpectedCall" do 149 | patch(RefuteCalled, :example, :patched) 150 | 151 | assert RefuteCalled.example(1, 2) == :patched 152 | 153 | assert_raise Patch.UnexpectedCall, fn -> 154 | refute_called RefuteCalled.example(1, _), 1 155 | end 156 | 157 | assert_raise Patch.UnexpectedCall, fn -> 158 | refute_called RefuteCalled.example(_, 2), 1 159 | end 160 | end 161 | 162 | test "an uncalled function can be wildcard refuted" do 163 | patch(RefuteCalled, :example, :patched) 164 | 165 | refute_called RefuteCalled.example(_, _), 1 166 | end 167 | 168 | test "any call causes a wildcard refute to raise UnexpectedCall" do 169 | patch(RefuteCalled, :example, :patched) 170 | 171 | assert RefuteCalled.example(1, 2) == :patched 172 | 173 | assert_raise Patch.UnexpectedCall, fn -> 174 | refute_called RefuteCalled.example(_, _), 1 175 | end 176 | end 177 | 178 | test "exception formatting" do 179 | patch(RefuteCalled, :example, :patched) 180 | 181 | assert RefuteCalled.example(1, 2) == :patched 182 | assert RefuteCalled.example(3, 4) == :patched 183 | assert RefuteCalled.example(1, 2) == :patched 184 | 185 | expected_message = """ 186 | \n 187 | Expected any count except 2 of the following calls, but found 2: 188 | 189 | Patch.Test.Support.User.RefuteCalled.example(1, 2) 190 | 191 | Calls which were received (matching calls are marked with *): 192 | 193 | * 1. Patch.Test.Support.User.RefuteCalled.example(1, 2) 194 | 2. Patch.Test.Support.User.RefuteCalled.example(3, 4) 195 | * 3. Patch.Test.Support.User.RefuteCalled.example(1, 2) 196 | """ 197 | 198 | assert_raise Patch.UnexpectedCall, expected_message, fn -> 199 | refute_called RefuteCalled.example(1, 2), 2 200 | end 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /test/user/replace_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.ReplaceTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Replace 6 | alias Patch.Test.Support.User.Replace.Inner 7 | 8 | 9 | describe "replace/3 with an anonymous process" do 10 | test "top-level fields can be updated" do 11 | {:ok, pid} = Replace.start_link(:initial_value) 12 | 13 | replace(pid, [:value], :replaced_value) 14 | 15 | assert :sys.get_state(pid) == %Replace{ 16 | value: :replaced_value, 17 | inner: %Inner{ 18 | value: :initial_value 19 | } 20 | } 21 | end 22 | 23 | test "nested fields can be updated" do 24 | {:ok, pid} = Replace.start_link(:initial_value) 25 | 26 | replace(pid, [:inner, :value], :replaced_value) 27 | 28 | assert :sys.get_state(pid) == %Replace{ 29 | value: :initial_value, 30 | inner: %Inner{ 31 | value: :replaced_value 32 | } 33 | } 34 | end 35 | 36 | test "replaces key's value wholesale" do 37 | {:ok, pid} = Replace.start_link(:initial_value) 38 | 39 | replace(pid, [:inner], :replaced_value) 40 | 41 | assert :sys.get_state(pid) == %Replace{ 42 | value: :initial_value, 43 | inner: :replaced_value 44 | } 45 | end 46 | end 47 | 48 | 49 | describe "replace/3 with a named process" do 50 | test "top-level fields can be updated" do 51 | {:ok, pid} = Replace.start_link(:initial_value, name: Replace) 52 | 53 | replace(Replace, [:value], :replaced_value) 54 | 55 | assert :sys.get_state(pid) == %Replace{ 56 | value: :replaced_value, 57 | inner: %Inner{ 58 | value: :initial_value 59 | } 60 | } 61 | end 62 | 63 | test "nested fields can be updated" do 64 | {:ok, pid} = Replace.start_link(:initial_value, name: Replace) 65 | 66 | replace(Replace, [:inner, :value], :replaced_value) 67 | 68 | assert :sys.get_state(pid) == %Replace{ 69 | value: :initial_value, 70 | inner: %Inner{ 71 | value: :replaced_value 72 | } 73 | } 74 | end 75 | 76 | test "replaces key's value wholesale" do 77 | {:ok, pid} = Replace.start_link(:initial_value, name: Replace) 78 | 79 | replace(Replace, [:inner], :replaced_value) 80 | 81 | assert :sys.get_state(pid) == %Replace{ 82 | value: :initial_value, 83 | inner: :replaced_value 84 | } 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/user/restore_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.RestoreTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Restore 6 | alias Patch.Test.Support.User.Fake.Fake 7 | alias Patch.Test.Support.User.Fake.Real 8 | 9 | describe "restore/1" do 10 | test "patches can be restored to original functionality" do 11 | assert Restore.example() == :original 12 | 13 | patch(Restore, :example, :patched) 14 | assert Restore.example() == :patched 15 | 16 | restore(Restore) 17 | assert Restore.example() == :original 18 | end 19 | 20 | test "fakes can be restored to original functionality" do 21 | assert Real.example(:a) == {:real, {:example, :a}} 22 | 23 | fake(Real, Fake) 24 | assert Real.example(:a) == {:fake, {:example, :a}} 25 | 26 | restore(Real) 27 | assert Real.example(:a) == {:real, {:example, :a}} 28 | end 29 | 30 | test "restoring an unpatched module is a no-op" do 31 | assert Restore.example() == :original 32 | 33 | restore(Restore) 34 | assert Restore.example() == :original 35 | end 36 | end 37 | 38 | describe "restore/2" do 39 | test "functions can be restored" do 40 | assert Restore.example() == :original 41 | 42 | patch(Restore, :example, :patched) 43 | assert Restore.example() == :patched 44 | 45 | restore(Restore, :example) 46 | assert Restore.example() == :original 47 | end 48 | 49 | test "function restoration is isolated" do 50 | assert Restore.example() == :original 51 | assert Restore.other() == :original 52 | 53 | patch(Restore, :example, :patched) 54 | patch(Restore, :other, :patched) 55 | assert Restore.example() == :patched 56 | assert Restore.other() == :patched 57 | 58 | restore(Restore, :example) 59 | assert Restore.example() == :original 60 | assert Restore.other() == :patched 61 | end 62 | 63 | test "restoring works on stacked callables" do 64 | assert Restore.example() == :original 65 | 66 | patch(Restore, :example, :first) 67 | assert Restore.example() == :first 68 | 69 | patch(Restore, :example, :second) 70 | assert Restore.example() == :second 71 | 72 | restore(Restore, :example) 73 | assert Restore.example() == :original 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/user/spy_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Patch.Test.User.SpyTest do 2 | use ExUnit.Case 3 | use Patch 4 | 5 | alias Patch.Test.Support.User.Spy 6 | 7 | describe "spy/1" do 8 | test "are transparent to the caller" do 9 | assert Spy.example(:test_argument_1) == {:original, :test_argument_1} 10 | 11 | spy(Spy) 12 | 13 | assert Spy.example(:test_argument_2) == {:original, :test_argument_2} 14 | end 15 | 16 | test "allows for assert_called" do 17 | assert Spy.example(:test_argument) == {:original, :test_argument} 18 | 19 | spy(Spy) 20 | 21 | assert Spy.example(:test_argument) == {:original, :test_argument} 22 | 23 | assert_called Spy.example(:test_argument) 24 | end 25 | 26 | test "raises MissingCall for assert_called that have not occurred" do 27 | assert Spy.example(:test_argument_1) == {:original, :test_argument_1} 28 | 29 | spy(Spy) 30 | 31 | assert Spy.example(:test_argument_2) == {:original, :test_argument_2} 32 | 33 | assert_raise Patch.MissingCall, fn -> 34 | # This call happened before the spy was applied 35 | assert_called Spy.example(:test_argument_1) 36 | end 37 | end 38 | 39 | test "allows for exact assert_called" do 40 | assert Spy.example(:test_argument_1) == {:original, :test_argument_1} 41 | 42 | spy(Spy) 43 | 44 | assert Spy.example(:test_argument_2) == {:original, :test_argument_2} 45 | 46 | assert_called Spy.example(:test_argument_2) 47 | end 48 | 49 | test "allows for assert_called with wildcards" do 50 | assert Spy.example(:test_argument_1) == {:original, :test_argument_1} 51 | 52 | spy(Spy) 53 | 54 | assert Spy.example(:test_argument_2) == {:original, :test_argument_2} 55 | 56 | assert_called Spy.example(_) 57 | end 58 | 59 | test "raises MissingCall if no calls have occurred on assert_called with wildcard" do 60 | assert Spy.example(:test_argument) == {:original, :test_argument} 61 | 62 | spy(Spy) 63 | 64 | assert_raise Patch.MissingCall, fn -> 65 | assert_called Spy.example(_) 66 | end 67 | end 68 | 69 | test "allows for refute_called" do 70 | assert Spy.example(:test_argument_1) == {:original, :test_argument_1} 71 | 72 | spy(Spy) 73 | 74 | assert Spy.example(:test_argument_2) == {:original, :test_argument_2} 75 | 76 | # This call happened before the spy was applied 77 | refute_called Spy.example(:test_argument_1) 78 | end 79 | 80 | test "raises UnexpectedCall for refute_called that have occurred" do 81 | assert Spy.example(:test_argument_1) == {:original, :test_argument_1} 82 | 83 | spy(Spy) 84 | 85 | assert Spy.example(:test_argument_2) == {:original, :test_argument_2} 86 | 87 | assert_raise Patch.UnexpectedCall, fn -> 88 | refute_called Spy.example(:test_argument_2) 89 | end 90 | end 91 | 92 | test "allows for refute_called with wildcards" do 93 | assert Spy.example(:test_argument) == {:original, :test_argument} 94 | 95 | spy(Spy) 96 | 97 | refute_called Spy.example(_) 98 | end 99 | 100 | test "raises UnexpectedCall for refute_called with wildcards if any call to the spy has occurred" do 101 | assert Spy.example(:test_argument_1) == {:original, :test_argument_1} 102 | 103 | spy(Spy) 104 | 105 | assert Spy.example(:test_argument_2) == {:original, :test_argument_2} 106 | 107 | assert_raise Patch.UnexpectedCall, fn -> 108 | refute_called Spy.example(_) 109 | end 110 | end 111 | end 112 | end 113 | --------------------------------------------------------------------------------