Permalink
Browse files

Show arguments in ExUnit reports for non-operator matches

This mirrors the behaviour seen in operators, where both
the left and right side are shown, except it has been
generalized to arguments:

    foo = {1, 2, 3}
    assert is_list(foo)

will show in error reports as:

    1) test name (Module)
       Expected truthy, got false
       code: assert is_list(foo)
       arguments:

           # 1
           {1, 2, 3}

       stacktrace:
           test/example_test.exs

This behaviour is always enabled, except for macros and
special forms, where rewriting the arguments may change the
macro behaviour.

A function Macro.special_form?/2 has also been added to
support this functionality.
  • Loading branch information...
josevalim committed Jan 31, 2018
1 parent d58b315 commit eb5e809f10b4a24215d21cb109b506c2cf9f1761
View
@@ -1149,7 +1149,7 @@ defmodule Macro do
when is_atom(atom) and is_list(args) and is_list(meta) do
arity = length(args)
if :elixir_import.special_form(atom, arity) do
if special_form?(atom, arity) do
{original, false}
else
module = env.module
@@ -1202,6 +1202,15 @@ defmodule Macro do
# Anything else is just returned
defp do_expand_once(other, _env), do: {other, false}
@doc """
Returns true if the given name and arity is a special form.
"""
@since "1.7.0"
@spec special_form?(name :: atom(), arity()) :: boolean()
def special_form?(name, arity) when is_atom(name) and is_integer(arity) do
:elixir_import.special_form(name, arity)
end
@doc """
Receives an AST node and expands it until it can no longer
be expanded.
@@ -154,6 +154,14 @@ defmodule TestOneOfEach do
Access.fetch(:foo, :bar)
end
test "29. function call arguments" do
assert some_vars(1 + 2, 3 + 4)
end
defp some_vars(_a, _b) do
false
end
defp blows_up do
ignite(0) + 1
end
@@ -5,7 +5,11 @@ defmodule ExUnit.AssertionError do
@no_value :ex_unit_no_meaningful_value
defexception left: @no_value, right: @no_value, message: @no_value, expr: @no_value
defexception left: @no_value,
right: @no_value,
message: @no_value,
expr: @no_value,
args: @no_value
@doc """
Indicates no meaningful value for a field.
@@ -161,22 +165,23 @@ defmodule ExUnit.Assertions do
end
defmacro assert(assertion) do
case translate_assertion(:assert, assertion, __CALLER__) do
nil ->
quote do
value = unquote(assertion)
if translated = translate_assertion(:assert, assertion, __CALLER__) do
translated
else
{args, value} = extract_args(assertion, __CALLER__)
unless value do
raise ExUnit.AssertionError,
expr: unquote(escape_quoted(:assert, assertion)),
message: "Expected truthy, got #{inspect(value)}"
end
quote do
value = unquote(value)
value
unless value do
raise ExUnit.AssertionError,
args: unquote(args),
expr: unquote(escape_quoted(:assert, assertion)),
message: "Expected truthy, got #{inspect(value)}"
end
value ->
value
end
end
end
@@ -219,22 +224,23 @@ defmodule ExUnit.Assertions do
end
defmacro refute(assertion) do
case translate_assertion(:refute, assertion, __CALLER__) do
nil ->
quote do
value = unquote(assertion)
if translated = translate_assertion(:refute, assertion, __CALLER__) do
{:!, [], [translated]}
else
{args, value} = extract_args(assertion, __CALLER__)
if value do
raise ExUnit.AssertionError,
expr: unquote(escape_quoted(:refute, assertion)),
message: "Expected false or nil, got #{inspect(value)}"
end
quote do
value = unquote(value)
value
if value do
raise ExUnit.AssertionError,
args: unquote(args),
expr: unquote(escape_quoted(:refute, assertion)),
message: "Expected false or nil, got #{inspect(value)}"
end
value ->
{:!, [], [value]}
value
end
end
end
@@ -249,7 +255,7 @@ defmodule ExUnit.Assertions do
call = {operator, meta, [left, right]}
equality_check? = operator in [:<, :>, :!==, :!=]
message = "Assertion with #{operator} failed"
translate_assertion(:assert, expr, call, message, equality_check?, caller)
translate_operator(:assert, expr, call, message, equality_check?, caller)
end
defp translate_assertion(:refute, {operator, meta, [_, _]} = expr, caller)
@@ -259,14 +265,14 @@ defmodule ExUnit.Assertions do
call = {:not, meta, [{operator, meta, [left, right]}]}
equality_check? = operator in [:<=, :>=, :===, :==, :=~]
message = "Refute with #{operator} failed"
translate_assertion(:refute, expr, call, message, equality_check?, caller)
translate_operator(:refute, expr, call, message, equality_check?, caller)
end
defp translate_assertion(_kind, _expected, _caller) do
nil
end
defp translate_assertion(kind, {_, _, [left, right]} = expr, call, message, true, _caller) do
defp translate_operator(kind, {_, _, [left, right]} = expr, call, message, true, _caller) do
expr = escape_quoted(kind, expr)
quote do
@@ -288,7 +294,7 @@ defmodule ExUnit.Assertions do
end
end
defp translate_assertion(kind, {_, _, [left, right]} = expr, call, message, false, _caller) do
defp translate_operator(kind, {_, _, [left, right]} = expr, call, message, false, _caller) do
expr = escape_quoted(kind, expr)
quote do
@@ -312,6 +318,38 @@ defmodule ExUnit.Assertions do
Macro.escape({kind, [], [expr]})
end
defp extract_args({root, meta, [_ | _] = args} = expr, env) do
arity = length(args)
special_form? = is_atom(root) and Macro.special_form?(root, arity)
case Macro.expand_once(expr, env) do
^expr when not special_form? ->
vars = for i <- 1..arity, do: Macro.var(:"arg#{i}", __MODULE__)
assignments =
for {var, arg} <- Enum.zip(vars, args) do
quote do
unquote(var) = unquote(arg)
end
end
quoted =
quote do
unquote_splicing(assignments)
unquote({root, meta, vars})
end
{vars, quoted}
other ->
{ExUnit.AssertionError.no_value(), other}
end
end
defp extract_args(expr, _env) do
{ExUnit.AssertionError.no_value(), expr}
end
## END HELPERS
@doc """
@@ -140,6 +140,7 @@ defmodule ExUnit.Formatter do
note: if_value(struct.message, &format_message(&1, formatter)),
code: if_value(struct.expr, &code_multiline(&1, padding_size)),
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
arguments: if_value(struct.args, &format_args(&1, width)),
left: left,
right: right
]
@@ -154,12 +155,12 @@ defmodule ExUnit.Formatter do
end
@doc """
Receives a test case and formats its failure.
Receives a test module and formats its failure.
"""
def format_test_all_failure(test_module, failures, counter, width, formatter) do
name = test_module.name
test_case_info(with_counter(counter, "#{inspect(name)}: "), formatter) <>
test_module_info(with_counter(counter, "#{inspect(name)}: "), formatter) <>
Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, index} ->
{text, stack} = format_kind_reason(test_module, kind, reason, stack, width, formatter)
failure_header(failures, index) <> text <> format_stacktrace(stack, name, nil, formatter)
@@ -263,6 +264,19 @@ defmodule ExUnit.Formatter do
formatter.(:error_info, value)
end
defp format_args(args, width) do
entries =
for {arg, i} <- Enum.with_index(args, 1) do
"""
# #{i}
#{inspect_multiline(arg, 9, width)}
"""
end
"\n" <> IO.iodata_to_binary(entries)
end
defp code_multiline(expr, padding_size) when is_binary(expr) do
padding = String.duplicate(" ", padding_size)
String.replace(expr, "\n", "\n" <> padding)
@@ -377,8 +391,10 @@ defmodule ExUnit.Formatter do
"#{counter}) #{msg}"
end
defp test_case_info(msg, nil), do: msg <> "failure on setup_all callback, test invalidated\n"
defp test_case_info(msg, formatter), do: test_case_info(formatter.(:test_case_info, msg), nil)
defp test_module_info(msg, nil), do: msg <> "failure on setup_all callback, test invalidated\n"
defp test_module_info(msg, formatter),
do: test_module_info(formatter.(:test_module_info, msg), nil)
defp test_info(msg, nil), do: msg <> "\n"
defp test_info(msg, formatter), do: test_info(formatter.(:test_info, msg), nil)
@@ -612,6 +612,13 @@ defmodule ExUnit.AssertionsTest do
true = assert 1 + 2 < greater
end
test "assert special form" do
true =
assert (case :ok do
:ok -> true
end)
end
test "assert operator with custom message" do
"This should never be tested" = assert 1 > 2, "assertion"
rescue
@@ -222,6 +222,20 @@ defmodule ExUnit.FormatterTest do
"""
end
test "formats assertions with function call arguments" do
failure = [{:error, catch_assertion(assert is_list({1, 2, 3})), []}]
assert format_test_all_failure(test_module(), failure, 1, 80, &formatter/2) =~ """
1) Hello: failure on setup_all callback, test invalidated
Expected truthy, got false
code: assert is_list({1, 2, 3})
arguments:
# 1
{1, 2, 3}
"""
end
test "formats assertions with message with multiple lines" do
message = "Some meaningful error:\nuseful info\nanother useful info"
failure = [{:error, catch_assertion(assert(false, message)), []}]

2 comments on commit eb5e809

@michalmuskala

This comment has been minimized.

Member

michalmuskala replied Jan 31, 2018

This implementation has a bug, it expands to incorrect code. Normally foo(x = 1, x) is not a valid expression, but since here we change it to:

arg1 = x = 1
arg2 = x
foo(arg1, arg2)

it suddenly becomes valid.
It probably needs to do the same trick we do when expanding in -

{arg1, arg2} = {x = 1, x}
foo(arg1, arg2)

Then it will correctly evaluate the arguments in parallel and not compile this code.

@josevalim

This comment has been minimized.

Member

josevalim replied Jan 31, 2018

Good catch!

Please sign in to comment.