defmodule Mneme do
@moduledoc """
Augments `ExUnit.Assertions` with a set of assertions that know how to
update themselves.
> #### `use Mneme` {: .info}
>
> When you `use Mneme` in a test module, [assertions](#assertions) are
> imported and module attributes are made available for
> [configuration](#configuration).
## Configuration
Mneme supports a variety of flexible configuration options that can be
applied at multiple levels of granularity, from your entire test suite
down to an individual test. While the default behavior will work well
for the majority of cases, it's worth knowing which levers and knobs
you have available to tweak Mneme to fit your individual workflow.
### Options
#{Mneme.Options.docs()}
### Configuring Mneme
There are four ways you can apply configuration options; Each is more
specific than the last and will override any conflicting options that
were set prior.
* When calling `Mneme.start/1`, which will apply to the entire test
run;
* When calling `use Mneme`, which will apply to all tests in that
module;
* In a `@mneme_describe` module attribute, which will apply to all
tests that follow in the given `ExUnit.Case.describe/2` block;
* In a `@mneme` module attribute, which will apply only to the next
test that follows.
For instance, when an auto-assertion has multiple possible patterns
available, Mneme will try to infer the best one to show you first.
If you always want the last (and usually most complex) generated
pattern, you could call `Mneme.start/1` like this:
# test/test_helper.exs
ExUnit.start()
Mneme.start(default_pattern: :last)
As mentioned above, this can be overriden at the module-level, in a
`describe` block, or for an individual test:
defmodule MyTest do
use ExUnit.Case
use Mneme, default_pattern: :infer
test "the default pattern will exclude :baz when this runs" do
map = %{foo: :one, baz: :three}
auto_assert %{foo: 1, bar: 2} <- Map.put(map, bar: :two)
end
describe "..." do
@mneme_describe action: :reject
test "fails without prompting" do
auto_assert :wrong <- MyModule.some_fun()
end
@mneme action: :prompt
test "prompts to update" do
auto_assert :wrong <- MyModule.another_fun()
end
end
end
Breaking up with Mneme? While the official stance of the library is
that this is Not Recommended™, Mneme can even convert all of your
existing auto-assertions to regular assertions:
Mneme.start(
target: :ex_unit,
force_update: true,
action: :accept
)
Probably don't do this, but if you do, make sure all your tests are
committed first in case you want to get back together.
"""
@ex_unit_default_receive_timeout 100
@doc false
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()
"""
@doc section: :assertion
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
"""
@doc section: :assertion
@doc since: "0.3.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`.
"""
@doc section: :assertion
@doc since: "0.3.0"
defmacro auto_assert_raise(exception, function) do
__build_assertion__(:auto_assert_raise, [exception, function], __CALLER__)
end
@doc """
See `auto_assert_raise/3`.
"""
@doc section: :assertion
@doc since: "0.3.0"
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
"""
@doc section: :assertion
@doc since: "0.3.0"
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`.
"""
@doc section: :assertion
@doc since: "0.3.0"
defmacro auto_assert_receive(pattern) do
__build_assertion__(:auto_assert_receive, [pattern], __CALLER__)
end
@doc """
See `auto_assert_receive/2`.
"""
@doc section: :assertion
@doc since: "0.3.0"
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}
"""
@doc section: :assertion
@doc since: "0.3.0"
defmacro auto_assert_received(pattern) do
__build_assertion__(:auto_assert_received, [pattern], __CALLER__)
end
@doc """
See `auto_assert_received/1`.
"""
@doc section: :assertion
@doc since: "0.3.0"
defmacro auto_assert_received do
__build_assertion__(:auto_assert_received, [], __CALLER__)
end
@doc false
def __build_assertion__(kind, args, caller) do
opts = fetch_opts!(kind, caller)
Mneme.Assertion.build(kind, args, caller, opts)
end
defp fetch_opts!(kind, caller) do
with {fun_name, 1} <- caller.function,
%ExUnit.Test{name: ^fun_name, tags: tags} <- get_last_registered_test(caller.module) do
Mneme.Options.options(tags)
else
_ -> raise Mneme.CompileError, message: "#{kind} can only be used inside of a test"
end
end
# Elixir v1.15.0 introduced `ExUnit.Case.get_last_registered_test/1`,
# which can be called at compile-time to get the `%ExUnit.Test{}`
# struct containing test tags. For prior versions of Elixir, we access
# the private module attribute that ExUnit uses to store test structs
# as they are registered.
if Version.match?(System.version(), ">= 1.15.0") do
defp get_last_registered_test(module) do
ExUnit.Case.get_last_registered_test(module)
end
else
defp get_last_registered_test(module) do
module
|> Module.get_attribute(:ex_unit_tests)
|> List.first()
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`.
* Any option defined in the [Configuration](#module-configuration)
section of the module docs.
"""
@doc section: :setup
def start(opts \\ []) do
{restart?, opts} = Keyword.pop(opts, :restart, false)
supervisor = Process.whereis(Mneme.Supervisor)
cond do
!supervisor ->
configure!(opts)
start_server!()
supervisor && restart? ->
configure!(opts)
restart_server!()
true ->
raise RuntimeError,
"Mneme has already started. Ensure that `:restart` is used if rerunning tests without restarting the application: `Mneme.start(restart: true)`"
end
:ok
end
defp start_server! do
children = [
Mneme.Server
]
opts = [
name: Mneme.Supervisor,
strategy: :one_for_one
]
{:ok, _pid} = Supervisor.start_link(children, opts)
end
defp restart_server! do
_ = Supervisor.terminate_child(Mneme.Supervisor, Mneme.Server)
{:ok, _pid} = Supervisor.restart_child(Mneme.Supervisor, Mneme.Server)
end
defp configure!(opts) do
ExUnit.configure(
formatters: [Mneme.Server.ExUnitFormatter],
default_formatter: ExUnit.CLIFormatter,
timeout: :infinity
)
Mneme.Options.configure(opts)
end
@doc false
def __receive_timeout__, do: @ex_unit_default_receive_timeout
end