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
> consisting largely of a single macro. 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()}
"""
@doc """
Sets up Mneme configuration for this module and imports `auto_assert/1`.
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: [auto_assert: 1]
require Mneme.Options
Mneme.Options.register_attributes(unquote(opts))
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.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 """
Generate or run an assertion.
`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` as closely as
possible. In fact, it generates ExUnit assertions under the hood.
There are, however, a few small differences to note:
* Pattern-matching assertions use the `<-` operator instead of the
`=` match operator. Value-comparison assertions still use `==`
(for instance, when the expression returns `nil` or `false`).
* 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(body) do
ensure_in_test!(:auto_assert, __CALLER__)
code = {:auto_assert, Macro.Env.location(__CALLER__), [body]}
Mneme.Assertion.build(code, __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
end