lib/mneme.ex

defmodule Mneme do
  @external_resource "mix.exs"
  @external_resource "README.md"
  @mdoc "README.md"
        |> File.read!()
        |> String.split("<!-- MDOC !-->")
        |> Enum.fetch!(1)

  @moduledoc """
  /ni:mi:/ - Snapshot testing for Elixir ExUnit

  > #### Early days {: .info}
  >
  > Mneme is in its infancy and has an intentionally minimal API. Please
  > feel free to submit any feedback, bugs, or suggestions as
  > [issues on Github](https://github.com/zachallaun/mneme). Thanks!

  #{@mdoc}

  ## Configuration

  Certain behavior can be configured globally using application config
  or locally in test modules either at the module, describe-block, or
  test level.

  To configure Mneme globally, you can set `:defaults` for the `:mneme`
  application:

      config :mneme,
        defaults: [
          diff: :semantic
        ]

  These defaults can be overriden in test modules at various levels
  either as options to `use Mneme` or as module attributes.

      defmodule MyTest do
        use ExUnit.Case

        # reject all changes to auto-assertions by default
        use Mneme, action: :reject

        test "this test will fail" do
          auto_assert 1 + 1
        end

        describe "some describe block" do
          # accept all changes to auto-assertions in this describe block
          @mneme_describe action: :accept

          test "this will update without prompting" do
            auto_assert 2 + 2
          end

          # prompt for any changes in this test
          @mneme action: :prompt
          test "this will prompt before updating" do
            auto_assert 3 + 3
          end
        end
      end

  Configuration that is "closer to the test" will override more general
  configuration:

      @mneme > @mneme_describe > opts to use Mneme > :mneme app config

  The exception to this is the `CI` environment variable, which causes
  all updates to be rejected. See the "Continuous Integration" section
  for more info.

  ### Options

  #{Mneme.Options.docs()}
  """

  @ex_unit_default_receive_timeout 100

  @doc """
  Sets up Mneme configuration for this module and imports auto-assertion
  macros.

  This macro accepts all options described in the "Configuration"
  section above.

  ## Example

      defmodule MyTest do
        use ExUnit.Case
        use Mneme # <- add this

        test "..." do
          auto_assert ...
        end
      end
  """
  defmacro __using__(opts) do
    quote do
      import Mneme, only: :macros
      require Mneme.Options
      Mneme.Options.register_attributes(unquote(opts))
    end
  end

  @doc """
  Pattern-generating variant of `ExUnit.Assertions.assert/1`.

  ## Examples

  `auto_assert` generates assertions when tests run, issuing a terminal
  prompt before making any changes (unless configured otherwise).

      auto_assert [1, 2] ++ [3, 4]

      # after running the test and accepting the change
      auto_assert [1, 2, 3, 4] <- [1, 2] ++ [3, 4]

  If the match no longer succeeds, a warning and new prompt will be
  issued to update it to the new value.

      auto_assert [1, 2, 3, 4] <- [1, 2] ++ [:a, :b]

      # after running the test and accepting the change
      auto_assert [1, 2, :a, :b] <- [1, 2] ++ [:a, :b]

  Prompts are only issued if the pattern doesn't match the value, so
  that pattern can also be changed manually.

      # this assertion succeeds, so no prompt is issued
      auto_assert [1, 2, | _] <- [1, 2] ++ [:a, :b]

  ## Differences from ExUnit `assert`

  The `auto_assert` macro is meant to match `assert` very closely, but
  there are a few differences to note:

    * Pattern-matching assertions use the `<-` operator instead of the
      `=` match operator.

    * Unlike ExUnit's `assert`, `auto_assert` can match falsy values.
      The following are equivalent:

          falsy = nil
          auto_assert nil <- falsy
          assert falsy == nil

    * Guards can be added with a `when` clause, while `assert` would
      require a second assertion. For example:

          auto_assert pid when is_pid(pid) <- self()

          assert pid = self()
          assert is_pid(pid)

    * Bindings in an `auto_assert` are not available outside of that
      assertion. For example:

          auto_assert pid when is_pid(pid) <- self()
          pid # ERROR: pid is not bound

      If you need to use the result of the assertion, it will evaluate
      to the expression's value.

          pid = auto_assert pid when is_pid(pid) <- self()
          pid # pid is the result of self()

  """
  defmacro auto_assert(expression) do
    build_assertion(:auto_assert, [expression], __CALLER__)
  end

  @doc """
  Pattern-generating variant of `ExUnit.Assertions.assert_raise/3`.

  If the given function does not raise, the assertion will fail.

  Like `auto_assert/1`, you will be prompted to automatically update
  the assertion if the raised raised exception changes.

  ## Examples

  You can pass an anonymous function that takes no arguments and is
  expected to raise an exception.

      auto_assert_raise fn ->
        some_call_expected_to_raise()
      end

      # after running the test and accepting changes
      auto_assert_raise Some.Exception, fn ->
        some_call_expected_to_raise()
      end

      # optionally include the message
      auto_assert_raise Some.Exception, "perhaps with a message", fn ->
        some_call_expected_to_raise()
      end

  A captured function of arity zero can also be used.

      auto_assert_raise &some_call_expected_to_raise/0

      # after running the test and accepting changes
      auto_assert_raise Some.Exception, &some_call_expected_to_raise/0

  """
  defmacro auto_assert_raise(exception, message, function) do
    build_assertion(:auto_assert_raise, [exception, message, function], __CALLER__)
  end

  @doc """
  See `auto_assert_raise/3`.
  """
  defmacro auto_assert_raise(exception, function) do
    build_assertion(:auto_assert_raise, [exception, function], __CALLER__)
  end

  @doc """
  See `auto_assert_raise/3`.
  """
  defmacro auto_assert_raise(function) do
    build_assertion(:auto_assert_raise, [function], __CALLER__)
  end

  @doc """
  Pattern-generating variant of `ExUnit.Assertions.assert_receive/3`.

  `timeout` is in milliseconds and defaults to `#{@ex_unit_default_receive_timeout}`.

  ## Examples

      Process.send_after(self(), {:some, :message}, 50)

      auto_assert_receive()

      # after running the test, messages appearing within 100ms
      # will be available as options
      auto_assert_receive {:some, :message}

  A custom timeout can be specified as a second argument.

      Process.send_after(self(), {:some, :message}, 150)

      auto_assert_receive nil, 300

      # messages appearing within 300ms will now appear as options
      auto_assert_receive {:some, :message}, 300

  """
  defmacro auto_assert_receive(pattern, timeout) when is_integer(timeout) and timeout >= 0 do
    build_assertion(:auto_assert_receive, [pattern, timeout], __CALLER__)
  end

  @doc """
  See `auto_assert_receive/2`.
  """
  defmacro auto_assert_receive(pattern) do
    build_assertion(:auto_assert_receive, [pattern], __CALLER__)
  end

  @doc """
  See `auto_assert_receive/2`.
  """
  defmacro auto_assert_receive do
    build_assertion(:auto_assert_receive, [], __CALLER__)
  end

  @doc """
  Pattern-generating variant of `ExUnit.Assertions.assert_received/2`.

  Similar to `auto_assert_receive/2`, except that the timeout is set to
  0, so the expected message must already be in the current process'
  mailbox.

  ## Examples

      send(self(), {:some, :message})

      auto_assert_received()

      # after running the test, messages in the current process
      # inbox will be available as options
      auto_assert_receive {:some, :message}

  """
  defmacro auto_assert_received(pattern) do
    build_assertion(:auto_assert_received, [pattern], __CALLER__)
  end

  @doc """
  See `auto_assert_received/1`.
  """
  defmacro auto_assert_received do
    build_assertion(:auto_assert_received, [], __CALLER__)
  end

  defp build_assertion(call, args, caller) do
    ensure_in_test!(call, caller)
    Mneme.Assertion.build(call, args, caller)
  end

  defp ensure_in_test!(call, caller) do
    with {fun_name, 1} <- caller.function,
         "test " <> _ <- to_string(fun_name) do
      :ok
    else
      _ -> raise Mneme.CompileError, message: "#{call} can only be used inside of a test"
    end
  end

  @doc """
  Starts Mneme to run auto-assertions as they appear in your tests.

  This will almost always be added to your `test/test_helper.exs`, just
  below the call to `ExUnit.start()`:

      # test/test_helper.exs
      ExUnit.start()
      Mneme.start()

  ## Options

    * `:restart` (boolean) - Restarts Mneme if it has previously been
      started. This option enables certain IEx-based testing workflows
      that allow tests to be run without a startup penalty. Defaults to
      `false`.

  """
  def start(opts \\ []) do
    ExUnit.configure(
      formatters: [Mneme.Server.ExUnitFormatter],
      default_formatter: ExUnit.CLIFormatter,
      timeout: :infinity
    )

    Mneme.Options.configure()

    if opts[:restart] && Process.whereis(Mneme.Supervisor) do
      _ = Supervisor.terminate_child(Mneme.Supervisor, Mneme.Server)
      {:ok, _pid} = Supervisor.restart_child(Mneme.Supervisor, Mneme.Server)
    else
      children = [
        Mneme.Server
      ]

      opts = [
        name: Mneme.Supervisor,
        strategy: :one_for_one
      ]

      Supervisor.start_link(children, opts)
    end

    :ok
  end

  @doc false
  def __receive_timeout__, do: @ex_unit_default_receive_timeout
end