defmodule FlowAssertions.Define.BodyParts do import ExUnit.Assertions alias ExUnit.AssertionError import FlowAssertions.Define.Defchain alias FlowAssertions.Messages @moduledoc """ Functions helpful in the construction of a new assertion. Mostly, they give you more control over what's shown in a failing test by letting you set `ExUnit.AssertionError` values like `:left` and `:right`. All such functions take a string first argument. That's shorthand for setting the `:message` field. """ @doc """ Like `ExUnit.Assertions.flunk/1` but the second argument is used to set `AssertionError` keys. ``` elaborate_flunk("the value is wrong", left: value_to_check) ``` Warning: as far as I know, the structure of `ExUnit.AssertionError` is not guaranteed to be stable. See also `elaborate_assert/3`. """ def elaborate_flunk(message, opts) do try do flunk message rescue ex in AssertionError -> annotated = Enum.reduce(opts, ex, fn {k, v}, acc -> Map.put(acc, k, v) end) reraise annotated, __STACKTRACE__ end end @doc """ Like `ExUnit.Assertions.assert/2` but the third argument is used to set `AssertionError` keys. ``` elaborate_assert( left =~ right, "Regular expression didn't match", left: left, right: right) ``` Warning: as far as I know, the structure of `ExUnit.AssertionError` is not guaranteed to be stable. See also `elaborate_assert_equal/4`. """ defchain elaborate_assert(value, message, opts) do if !value, do: elaborate_flunk(message, opts) end @doc """ `elaborate_assert/3`, except the value is expected to be falsy. """ defchain elaborate_refute(value, message, opts), do: elaborate_assert(!value, message, opts) @doc """ This replicates the diagnostic output from `assert a == b`, except for the code snippet that's reported. The user will see a failing test containing: Assertion with == failed code: assert_same_map(new, old, ignoring: [:stable]) left: ... right: ... ... instead of the assertion that actually failed, something like this: Assertion with == failed code: assert Map.drop(new, fields_to_ignore) == Map.drop(old, fields_to_ignore) left: ... right: ... """ defchain elaborate_assert_equal(left, right) do elaborate_assert(left == right, Messages.stock_equality, left: left, right: right, expr: AssertionError.no_value) end # ---------------------------------------------------------------------------- @doc """ Flunk test if it checks structure fields that don't exist. It doesn't make sense to write an assertion that checks a field that a structure can't contain. If a user tries, this function will object with a message like: ``` Test error: there is no key `:b` in a `MyApp.Struct` ``` Notes: * It's safe to call on non-struct values. * It returns its first argument. """ defchain struct_must_have_key!(struct, key) when is_struct(struct) do elaborate_assert( Map.has_key?(struct, key), Messages.required_key_missing(key, struct), left: struct |> Map.from_struct |> Map.keys) end def struct_must_have_key!(x, _), do: x @doc""" Same as `struct_must_have_key!/2` but checks multiple keys. """ defchain struct_must_have_keys!(struct, keys) when is_struct(struct) do for key <- keys, do: struct_must_have_key!(struct, key) end def struct_must_have_keys!(x, _), do: x # ---------------------------------------------------------------------------- @doc ~S""" Run a function, perhaps generating an assertion error. If so, use the keyword arguments to replace or update values in the error. **Replacement:** adjust_assertion_error(fn -> MiscA.assert_good_enough(Map.get(kvs, key), expected) end, message: "Field `#{inspect key}` has the wrong value", expr: AssertionError.no_value) Setting the `expr` field to `AssertionError.no_value` has the handy effect of making the reporting machinery report the code of the assertion the user called, rather than the nested assertion that generated the error. **Update:** adjust_assertion_error(fn -> MiscA.assert_good_enough(Map.get(kvs, key), expected) end, expr: fn expr -> [expr, "..."] end) # indicate something missing. See also `adjust_assertion_message/2` """ def adjust_assertion_error(f, replacements) do try do f.() rescue ex in AssertionError -> Enum.reduce(replacements, ex, fn {key, value}, acc -> if is_function(value), do: Map.update!(acc, key, value), else: Map.put(acc, key, value) end) |> reraise(__STACKTRACE__) end end @doc ~S""" Run a function, perhaps generating an assertion error. If so, call the second function, passing the current assertion message as its argument. The result is installed as the new assertion message. adjust_assertion_message( fn -> flunk "message" end, fn message -> "#{message} and #{message}" end) See also `adjust_assertion_error/2`" """ def adjust_assertion_message(asserter, adjuster) do try do asserter.() rescue ex in AssertionError -> Map.put(ex, :message, adjuster.(ex.message)) |> reraise(__STACKTRACE__) end end end