lib/patch.ex

defmodule Patch do
  @moduledoc """
  Patch - Ergonomic Mocking for Elixir

  Patch makes it easy to mock one or more functions in a module returning a value or executing
  custom logic.  Patches and Spies allow tests to assert or refute that function calls have been
  made.

  Using Patch is as easy as adding a single line to your test case.

  ```elixir
  use Patch
  ```

  After this all the patch functions will be available, see the function documentation for
  details.
  """
  alias Patch.Mock
  alias Patch.Mock.Naming
  alias Patch.Mock.Value
  import Value
  require Value

  ## Exceptions

  defmodule ConfigurationError do
    defexception [:message]
  end

  defmodule InvalidAnyCall do
    defexception [:message]
  end

  defmodule MissingCall do
    defexception [:message]
  end

  defmodule UnexpectedCall do
    defexception [:message]
  end

  defmacro __using__(_) do
    quote do
      require unquote(__MODULE__)
      import unquote(__MODULE__)
      import Patch.Mock.Value, except: [advance: 1, next: 2]

      require Patch.Macro
      require Patch.Mock
      require Patch.Assertions


      setup do
        start_supervised!(Patch.Supervisor)

        on_exit(fn ->
          Patch.Mock.Code.Freezer.empty()
        end)

        :ok
      end
    end
  end

  @doc """
  Asserts that the given module and function has been called with any arity.

  ```elixir
  patch(Example, :function, :patch)

  assert_any_call Example.function   # fails

  Example.function(1, 2, 3)

  assert_any_call Example.function   # passes
  ```
  """
  @spec assert_any_call(call :: Macro.t()) :: Macro.t()
  defmacro assert_any_call(call) do
    {module, function, arguments} = Macro.decompose_call(call)

    unless Enum.empty?(arguments) do
      raise InvalidAnyCall, message: "assert_any_call/1 does not support arguments"
    end

    quote do
      Patch.Assertions.assert_any_call(unquote(module), unquote(function))
    end
  end

  @doc """
  Asserts that the given module and function has been called with any arity.

  ```elixir
  patch(Example, :function, :patch)

  assert_any_call Example, :function   # fails

  Example.function(1, 2, 3)

  assert_any_call Example, :function   # passes
  ```

  This function exists for advanced use cases where the module or function are not literals in the
  test code.  If they are literals then `assert_any_call/1` should be preferred.
  """
  @spec assert_any_call(module :: module(), function :: atom()) :: nil
  defdelegate assert_any_call(module, function), to: Patch.Assertions

  @doc """
  Given a call will assert that a matching call was observed by the patched function.

  This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
  `assert_receive/3` and `assert_received/2`.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  assert_called Example.function(1, 2, 3)   # passes
  assert_called Example.function(1, _, 3)   # passes
  assert_called Example.function(4, 5, 6)   # fails
  assert_called Example.function(4, _, 6)   # fails
  ```
  """
  @spec assert_called(Macro.t()) :: Macro.t()
  defmacro assert_called(call) do
    quote do
      Patch.Assertions.assert_called(unquote(call))
    end
  end

  @doc """
  Given a call will assert that a matching call was observed exactly the number of times provided
  by the patched function.

  This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
  `assert_receive/3` and `assert_received/2`.  Any binds will bind to the latest matching call
  values.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  assert_called Example.function(1, 2, 3), 1   # passes
  assert_called Example.function(1, _, 3), 1   # passes

  Example.function(1, 2, 3)

  assert_called Example.function(1, 2, 3), 2   # passes
  assert_called Example.function(1, _, 3), 2   # passes
  ```
  """
  @spec assert_called(call :: Macro.t(), count :: Macro.t()) :: Macro.t()
  defmacro assert_called(call, count) do
    quote do
      Patch.Assertions.assert_called(unquote(call), unquote(count))
    end
  end

  @doc """
  Given a call will assert that a matching call was observed exactly once by the patched function.

  This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
  `assert_receive/3` and `assert_received/2`.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  assert_called_once Example.function(1, 2, 3)   # passes
  assert_called_once Example.function(1, _, 3)   # passes

  Example.function(1, 2, 3)

  assert_called_once Example.function(1, 2, 3)   # fails
  assert_called_once Example.function(1, _, 3)   # fails
  ```
  """
  @spec assert_called_once(call :: Macro.t()) :: Macro.t()
  defmacro assert_called_once(call) do
    quote do
      Patch.Assertions.assert_called_once(unquote(call))
    end
  end


  @doc """
  Expose can be used to turn private functions into public functions for the
  purpose of testing them.

  To expose every private function as a public function, pass the sentinel value `:all`.

  ```elixir
  expose(Example, :all)
  ```

  Otherwise pass a `Keyword.t(arity)` of the functions to expose.

  For example, if one wanted to expose `private_function/1` and `private_function/2`.

  ```elixir
  expose(Example, [private_function: 1, private_function: 2])
  ```

  After exposing a function, attempting to call the exposed function will cause the Elixir
  Compiler to flag calls to exposed functions as a warning.  There is a companion macro
  `private/1` that test authors can wrap their calls with to prevent warnings.
  """
  @spec expose(module :: module, exposes :: Patch.Mock.exposes()) :: :ok | {:error, term()}
  def expose(module, exposes) do
    Mock.expose(module, exposes)
  end

  @doc """
  Fakes out a module with an alternative implementation.

  The real module can still be accessed with `real/1`.

  For example, if your project has the module `Example.Datastore` and there's a fake available in the testing
  environment named `Example.Test.InMemoryDatastore`  the following table describes which calls are executed by which
  code before and after faking with the following call.

  ```elixir
  fake(Example.Datastore, Example.Test.InMemoryDatastore)
  ```

  | Calling Code                         | Responding Module before fake/2      | Responding Module after fake/2       |
  |--------------------------------------|--------------------------------------|--------------------------------------|
  | Example.Datastore.get/1              | Example.Datastore.get/1              | Example.Test.InMemoryDatastore.get/1 |
  | Example.Test.InMemoryDatastore.get/1 | Example.Test.InMemoryDatastore.get/1 | Example.Test.InMemoryDatastore.get/1 |
  | real(Example.Datastore).get/1        | (UndefinedFunctionError)             | Example.Datastore.get/1              |

  The fake module can use the renamed module to access the original implementation.
  """
  @spec fake(real_module :: module(), fake_module :: module()) :: :ok
  def fake(real_module, fake_module) do
    {:ok, _} = Mock.module(real_module)

    real_functions = Patch.Reflection.find_functions(real_module)
    fake_functions = Patch.Reflection.find_functions(fake_module)

    Enum.each(fake_functions, fn {name, arity} ->
      is_real_function? = Enum.any?(real_functions, &match?({^name, ^arity}, &1))

      if is_real_function? do
        patch(
          real_module,
          name,
          callable(fn args ->
            apply(fake_module, name, args)
          end, :list)
        )
      end
    end)
  end

  @spec inject(
          tag :: Patch.Listener.tag(),
          target :: Patch.Listener.target(),
          keys :: [term(), ...],
          options :: [Patch.Listener.option()]
        ) :: {:ok, pid()} | {:error, :not_found} | {:error, :invalid_keys}
  def inject(tag, target, keys, options \\ []) do
    state = :sys.get_state(target)

    case Patch.Access.fetch(state, keys) do
      {:ok, subject} ->
        with {:ok, listener} <- listen(tag, subject, options) do
          replace(target, keys, listener)
          {:ok, listener}
        end

      :error ->
        {:error, :invalid_keys}
    end
  end

  @doc """
  Get all the observed calls to a module.  These calls are expressed as a `{name, argument}` tuple
  and can either be provided in ascending (oldest first) or descending (newest first) order by
  providing a sorting of `:asc` or `:desc`, respectively.

  ```elixir
  Example.example(1, 2, 3)
  Example.function(:a)

  assert history(Example) == [{:example, [1, 2, 3]}, {:function, [:a]}]
  assert history(Example, :desc) == [{:function, [:a]}, {:example, [1, 2, 3]}]
  ```

  For asserting or refuting that a call happened the `assert_called/1`, `assert_any_call/2`,
  `refute_called/1`, and `refute_any_call/2` functions provide a more convenient API.
  """
  @spec history(module :: module(), sorting :: :asc | :desc) :: [Mock.History.entry()]
  def history(module, sorting \\ :asc) do
    module
    |> Mock.history()
    |> Mock.History.entries(sorting)
  end

  @doc """
  Starts a listener process.

  Each listener should provide a unique `tag` that will be used when forwarding messages to the
  test process.

  When used on a named process, this is sufficient to begin intercepting all messages to the named
  process.

  ```elixir
  listen(:listener, Example)
  ```

  When used on an unnamed process, the process that is spawned will forward any messages to the
  caller and target process but any processes holding a reference to the old pid will need to be
  updated.

  `inject/3` can be used to inject a listener into a running process.

  ```elixir
  {:ok, listener} = listen(:listener, original)
  inject(target, :original, listener)
  ```
  """
  @spec listen(
          tag :: Patch.Listener.tag(),
          target :: Patch.Listener.target(),
          options :: [Patch.Listener.option()]
        ) :: {:ok, pid()} | {:error, :not_found}
  def listen(tag, target, options \\ []) do
    Patch.Listener.Supervisor.start_child(self(), tag, target, options)
  end

  @doc """
  Patches a function in a module

  When called with a function the function will be called instead of the original function and its
  results returned.

  ```elixir
  patch(Example, :function, fn arg -> {:mock, arg} end)

  assert Example.function(:test) == {:mock, :test}
  ```

  To handle multiple arities create a `callable/2` with the `:list` option and the arguments will
  be wrapped to the function in a list.

  ```elixir
  patch(Example, :function, callable(fn
    [] ->
      :zero

    [a] ->
      {:one, a}

    [a, b] ->
      {:two, a, b}
  end, :list))

  assert Example.function() == :zero
  assert Example.function(1) == {:one, 1}
  assert Example.function(1, 2) == {:two, 1, 2}
  ```

  To provide a function as a literal value to be returned, use the `scalar/1` function.

  ```elixir
  patch(Example, :function, scalar(fn arg -> {:mock, arg} end))

  callable = Example.function()
  assert callable.(:test) == {:mock, :test}
  ```

  The function `cycle/1` can be given a list which will be infinitely cycled when the function is
  called.

  ```elixir
  patch(Example, :function, cycle([1, 2, 3]))

  assert Example.function() == 1
  assert Example.function() == 2
  assert Example.function() == 3
  assert Example.function() == 1
  assert Example.function() == 2
  assert Example.function() == 3
  assert Example.function() == 1
  ```

  The function `raises/1` can be used to `raise/1` a `RuntimeError` when the function is called.

  ```elixir
  patch(Example, :function, raises("patched"))

  assert_raise RuntimeError, "patched", fn ->
    Example.function()
  end
  ```

  The function `raises/2` can be used to `raise/2` any exception with any attributes when the function
  is called.

  ```elixir
  patch(Example, :function, raises(ArgumentError, message: "patched"))

  assert_raise ArgumentError, "patched", fn ->
    Example.function()
  end
  ```

  The function `sequence/1` can be given a list which will be used until a singal value is
  remaining, the remaining value will be returned on all subsequent calls.

  ```elixir
  patch(Example, :function, sequence([1, 2, 3]))

  assert Example.function() == 1
  assert Example.function() == 2
  assert Example.function() == 3
  assert Example.function() == 3
  assert Example.function() == 3
  assert Example.function() == 3
  assert Example.function() == 3
  ```

  The function `throws/1` can be given a value to `throw/1` when the function is called.

  ```elixir
  patch(Example, :function, throws(:patched))

  assert catch_throw(Example.function()) == :patched
  ```

  Any other value will be returned as a literal scalar value when the function is called.

  ```elixir
  patch(Example, :function, :patched)

  assert Example.function() == :patched
  ```
  """
  @spec patch(module :: module(), function :: atom(), value :: Value.t()) :: Value.t()
  def patch(module, function, %value_module{} = value) when is_value(value_module) do
    {:ok, _} = Patch.Mock.module(module)
    :ok = Patch.Mock.register(module, function, value)
    value
  end

  @spec patch(module :: module(), function :: atom(), callable) :: callable when callable: function()
  def patch(module, function, callable) when is_function(callable) do
    patch(module, function, callable(callable))
    callable
  end

  @spec patch(module :: module(), function :: atom(), return_value) :: return_value
        when return_value: term()
  def patch(module, function, return_value) do
    patch(module, function, scalar(return_value))
    return_value
  end

  @doc """
  Suppress warnings for using exposed private functions in tests.

  Patch allows you to make a private function public via the `expose/2` function.  Exposure
  happens dynamically at test time. The Elixir Compiler will flag calls to exposed functions as a
  warning.

  One way around this is to change the normal function call into an `apply/3` but this is
  cumbersome and makes tests harder to read.

  This macro just rewrites a normal looking call into an `apply/3` so the compiler won't complain
  about calling an exposed function.

  ```elixir
  expose(Example, :all)

  patch(Example, :private_function, :patched)

  assert Example.private_function() == :patched   # Compiler will warn about call to undefined function
  assert apply(Example, :private_function, []) == :patched   # Compiler will not warn
  assert private(Example.private_function()) == :patched     # Same as previous line, but looks nicer.
  ```
  """
  @spec private(Macro.t()) :: Macro.t()
  defmacro private(call) do
    {module, function, arguments} = Macro.decompose_call(call)

    quote do
      apply(unquote(module), unquote(function), unquote(arguments))
    end
  end

  @doc """
  Gets the real module name for a fake.

  This is useful for Fakes that want to defer some part of the functionality back to the real
  module.

  ```elixir
  def Example do
    def calculate(a) do
      # ...snip some complex calculations...
      result
    end
  end

  def Example.Fake do
    import Patch, only: [real: 1]

    def calculate(a) do
      real_result = real(Example).calculate(a)

      {:fake, real_result}
    end
  end
  """
  @spec real(module :: module()) :: module()
  def real(module) do
    Naming.original(module)
  end

  @doc """
  Refutes that the given module and function has been called with any arity.

  ```elixir
  patch(Example, :function, :patch)

  refute_any_call Example.function   # passes

  Example.function(1, 2, 3)

  refute_any_call Example.function   # fails
  ```
  """
  @spec refute_any_call(call :: Macro.t()) :: Macro.t()
  defmacro refute_any_call(call) do
    {module, function, arguments} = Macro.decompose_call(call)

    unless Enum.empty?(arguments) do
      raise InvalidAnyCall, message: "refute_any_call/1 does not support arguments"
    end

    quote do
      Patch.Assertions.refute_any_call(unquote(module), unquote(function))
    end
  end

  @doc """
  Refutes that the given module and function has been called with any arity.

  ```elixir
  patch(Example, :function, :patch)

  refute_any_call Example, :function   # passes

  Example.function(1, 2, 3)

  refute_any_call Example, :function   # fails
  ```

  This function exists for advanced use cases where the module or function are not literals in the
  test code.  If they are literals then `refute_any_call/1` should be preferred.
  """
  @spec refute_any_call(module :: module(), function :: atom()) :: nil
  defdelegate refute_any_call(module, function), to: Patch.Assertions

  @doc """
  Given a call will refute that a matching call was observed by the patched function.

  This macro fully supports patterns.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  refute_called Example.function(4, 5, 6)   # passes
  refute_called Example.function(4, _, 6)   # passes
  refute_called Example.function(1, 2, 3)   # fails
  refute_called Example.function(1, _, 3)   # fails
  ```
  """
  @spec refute_called(call :: Macro.t()) :: Macro.t()
  defmacro refute_called(call) do
    quote do
      Patch.Assertions.refute_called(unquote(call))
    end
  end

  @doc """
  Given a call will refute that a matching call was observed exactly the number of times provided
  by the patched function.

  This macro fully supports patterns.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  refute_called Example.function(1, 2, 3), 2   # passes
  refute_called Example.function(1, _, 3), 2   # passes

  Example.function(1, 2, 3)

  refute_called Example.function(1, 2, 3), 1   # passes
  refute_called Example.function(1, _, 3), 1   # passes
  ```
  """
  @spec refute_called(call :: Macro.t(), count :: Macro.t()) :: Macro.t()
  defmacro refute_called(call, count) do
    quote do
      Patch.Assertions.refute_called(unquote(call), unquote(count))
    end
  end

  @doc """
  Given a call will refute that a matching call was observed exactly once by the patched function.

  This macro fully supports patterns.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  refute_called_once Example.function(1, 2, 3)   # fails
  refute_called_once Example.function(1, _, 3)   # fails

  Example.function(1, 2, 3)

  refute_called_once Example.function(1, 2, 3)   # passes
  refute_called_once Example.function(1, _, 3)   # passes
  ```
  """
  @spec refute_called_once(call :: Macro.t()) :: Macro.t()
  defmacro refute_called_once(call) do
    quote do
      Patch.Assertions.refute_called_once(unquote(call))
    end
  end

  @doc """
  Convenience function for replacing part of the state of a running process.

  Uses the `Access` module to traverse the state structure according to the given `keys`.

  Structs have special handling so that they can be updated without having to implement the
  `Access` behavior.

  For example to replace the key `:key` in the map found under the key `:map` with the value
  `:replaced`

  ```elixir
  replace(target, [:map, :key], :replaced)
  ```
  """
  @spec replace(target :: GenServer.server(), keys :: [term(), ...], value :: term()) :: term()
  def replace(target, keys, value) do
    :sys.replace_state(target, &Patch.Access.put(&1, keys, value))
  end

  @doc """
  Remove any mocks or spies from the given module

  ```elixir
  original = Example.example()

  patch(Example, :example, :patched)
  assert Example.example() == :patched

  restore(Example)
  assert Example.example() == original
  ```
  """
  @spec restore(module :: module()) :: :ok | {:error, term()}
  def restore(module) do
    Mock.restore(module)
  end

  @doc """
  Remove any patches associated with a function in a module.

  ```elixir
  original = Example.example()

  patch(Example, :example, :example_patch)
  patch(Example, :other, :other_patch)

  assert Example.example() == :example_patch
  assert Example.other() == :other_patch

  restore(Example, :example)

  assert Example.example() == original
  assert Example.other() == :other_patch
  """
  @spec restore(module :: module(), name :: atom()) :: :ok | {:error, term()}
  def restore(module, name) do
    Mock.restore(module, name)
  end

  @doc """
  Spies on the provided module

  Once a module has been spied on the calls to that module can be asserted / refuted without
  changing the behavior of the module.

  ```elixir
  spy(Example)

  Example.example(1, 2, 3)

  assert_called Example.example(1, 2, 3)   # passes
  """
  @spec spy(module :: module()) :: :ok
  def spy(module) do
    {:ok, _} = Mock.module(module)
    :ok
  end
end