Support raising and formatting of errors in ExUnit

This commit is contained in:
José Valim
2015-11-27 18:03:54 +01:00
parent 715bdccf2c
commit 3ba93e780f
9 changed files with 160 additions and 58 deletions
+22 -5
View File
@@ -5,14 +5,13 @@ defmodule TestOneOfEach do
This module contains one of each type of failing test.
It is used simply to document the style of each.
"""
use ExUnit.Case, async: false
use ExUnit.Case
@one 1
@two 2
@long_data_1 [ field1: "one", field2: {:two1, :two2}, field3: 'three', field4: [1, 2, 3, 4]]
@long_data_2 [ field1: "one", field2: {:two1, :two3}, field3: 'three', field4: [1, 2, 3, 4]]
@long_data_1 [field1: "one", field2: {:two1, :two2}, field3: 'three', field4: [1, 2, 3, 4]]
@long_data_2 [field1: "one", field2: {:two1, :two3}, field3: 'three', field4: [1, 2, 3, 4]]
test "1. assert with a match" do
assert [@one] = [@two]
@@ -51,7 +50,7 @@ defmodule TestOneOfEach do
end
test "10. assert with explicit expected and actual values" do
assert @one > @two, @one, @two, "one should be greater than two"
assert @one > @two, left: @one, right: @two, message: "one should be greater than two"
end
test "11. assert that a message is ready to be received" do
@@ -122,6 +121,24 @@ defmodule TestOneOfEach do
end
end
test "26. multi error" do
error1 =
try do
assert [@one] = [@two]
rescue e in ExUnit.AssertionError ->
e
end
error2 =
try do
assert @one * 4 > @two *3
rescue e in ExUnit.AssertionError ->
e
end
raise ExUnit.MultiError, errors: [error1, error2]
end
defp blows_up do
ignite(0) + 1
end
+5 -8
View File
@@ -57,7 +57,7 @@ defmodule ExUnit do
files. See `Mix.Tasks.Test` for more information.
"""
@typedoc "The state returned by ExUnit.Test and ExUnit.TestCase"
@typedoc "The error state returned by ExUnit.Test and ExUnit.TestCase"
@type state :: nil | {:failed, failed} | {:skip, binary} | {:invalid, module}
@type failed :: {Exception.kind, reason :: term, stacktrace :: [tuple]}
@@ -69,14 +69,13 @@ defmodule ExUnit do
* `:name` - the test name
* `:case` - the test case
* `:state` - the test state (see ExUnit.state)
* `:state` - the test error state (see ExUnit.state)
* `:time` - the time to run the test
* `:tags` - the test tags
* `:logs` - the captured logs
"""
defstruct [:name, :case, :state,
time: 0, tags: %{}, logs: ""]
defstruct [:name, :case, :state, time: 0, tags: %{}, logs: ""]
@type t :: %__MODULE__{
name: atom,
@@ -93,13 +92,11 @@ defmodule ExUnit do
It is received by formatters and contains the following fields:
* `:name` - the test case name
* `:state` - the test state (see ExUnit.state)
* `:state` - the test error state (see ExUnit.state)
* `:tests` - all tests for this case
"""
defstruct name: nil,
state: nil,
tests: []
defstruct [:name, :state, tests: []]
@type t :: %__MODULE__{
name: module,
+18 -1
View File
@@ -1,4 +1,8 @@
defmodule ExUnit.AssertionError do
@moduledoc """
Raised to signal an assertion error.
"""
@no_value :ex_unit_no_meaningful_value
defexception left: @no_value,
@@ -14,6 +18,19 @@ defmodule ExUnit.AssertionError do
end
end
defmodule ExUnit.MultiError do
@moduledoc """
Raised to signal multiple errors happened in a test case.
"""
defexception [errors: []]
def message(exception) do
"got the following errors:\n\n * " <>
Enum.map_join(exception, "\n * ", &Exception.message/1)
end
end
defmodule ExUnit.Assertions do
@moduledoc """
This module contains a set of assertion functions that are
@@ -388,7 +405,7 @@ defmodule ExUnit.Assertions do
Macro.prewalk(expr, [], fn
{:::, _, [left, _]}, acc ->
{[left], acc}
{:^, _, [_]}, acc ->
{skip, _, [_]}, acc when skip in [:^, :@] ->
{:ok, acc}
{:_, _, context}, acc when is_atom(context) ->
{:ok, acc}
+4 -4
View File
@@ -23,6 +23,10 @@ defmodule ExUnit.CLIFormatter do
{:ok, config}
end
def handle_event({:suite_started, _opts}, config) do
{:ok, config}
end
def handle_event({:suite_finished, run_us, load_us}, config) do
print_suite(config, run_us, load_us)
:remove_handler
@@ -92,10 +96,6 @@ defmodule ExUnit.CLIFormatter do
{:ok, %{config | failures_counter: config.failures_counter + 1}}
end
def handle_event(_, config) do
{:ok, config}
end
## Tracing
defp trace_test_name(%ExUnit.Test{name: name}) do
+17 -9
View File
@@ -105,24 +105,29 @@ defmodule ExUnit.Formatter do
@doc """
Receives a test and formats its failure.
"""
def format_test_failure(test, failure, counter, width, formatter)
def format_test_failure(test, {kind, reason, stack}, counter, width, formatter) do
def format_test_failure(test, failures, counter, width, formatter) do
%ExUnit.Test{name: name, case: case, tags: tags} = test
test_info(with_counter(counter, "#{name} (#{inspect case})"), formatter)
<> test_location(with_location(tags), formatter)
<> format_kind_reason(kind, reason, width, formatter)
<> format_stacktrace(stack, case, name, formatter)
<> test_location(with_location(tags), formatter)
<> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, i} ->
failure_header(failures, i)
<> format_kind_reason(kind, reason, width, formatter)
<> format_stacktrace(stack, case, name, formatter)
end)
end
@doc """
Receives a test case and formats its failure.
"""
def format_test_case_failure(test_case, failure, counter, width, formatter)
def format_test_case_failure(test_case, {kind, reason, stacktrace}, counter, width, formatter) do
def format_test_case_failure(test_case, failures, counter, width, formatter) do
%ExUnit.TestCase{name: name} = test_case
test_case_info(with_counter(counter, "#{inspect name}: "), formatter)
<> format_kind_reason(kind, reason, width, formatter)
<> format_stacktrace(stacktrace, name, nil, formatter)
<> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, i} ->
failure_header(failures, i)
<> format_kind_reason(kind, reason, width, formatter)
<> format_stacktrace(stack, name, nil, formatter)
end)
end
defp format_kind_reason(:error, %ExUnit.AssertionError{} = struct, width, formatter) do
@@ -218,6 +223,9 @@ defmodule ExUnit.Formatter do
"#{Path.relative_to_cwd(tags[:file])}:#{tags[:line]}"
end
defp failure_header([_], _), do: ""
defp failure_header(_, i), do: "\n#{@counter_padding}Failure ##{i+1}\n"
defp with_counter(counter, msg) when counter < 10 do " #{counter}) #{msg}" end
defp with_counter(counter, msg) when counter < 100 do " #{counter}) #{msg}" end
defp with_counter(counter, msg) do "#{counter}) #{msg}" end
+1 -1
View File
@@ -83,6 +83,6 @@ defmodule ExUnit.OnExitHandler do
nil
catch
kind, error ->
{kind, Exception.normalize(kind, error), System.stacktrace}
{kind, error, System.stacktrace}
end
end
+17 -11
View File
@@ -176,7 +176,7 @@ defmodule ExUnit.Runner do
end
{test_case, tests}
{:DOWN, ^case_ref, :process, ^case_pid, error} ->
test_case = %{test_case | state: {:failed, {{:EXIT, case_pid}, error, []}}}
test_case = %{test_case | state: failed({:EXIT, case_pid}, error, [])}
{test_case, []}
end
@@ -188,7 +188,7 @@ defmodule ExUnit.Runner do
{:ok, test_case, context}
catch
kind, error ->
failed = {:failed, {kind, Exception.normalize(kind, error), pruned_stacktrace}}
failed = failed(kind, error, pruned_stacktrace())
{:error, %{test_case | state: failed}}
end
@@ -211,7 +211,7 @@ defmodule ExUnit.Runner do
message =
"could not run test, it uses @tag :capture_log" <>
" but the :logger application is not running"
%{test | state: {:failed, {:error, RuntimeError.exception(message), []}}}
%{test | state: failed(:error, RuntimeError.exception(message), [])}
else
logged ->
receive do
@@ -262,7 +262,7 @@ defmodule ExUnit.Runner do
end
test
{:DOWN, ^test_ref, :process, ^test_pid, error} ->
%{test | state: {:failed, {{:EXIT, test_pid}, error, []}}}
%{test | state: failed({:EXIT, test_pid}, error, [])}
after
timeout ->
stacktrace =
@@ -275,7 +275,7 @@ defmodule ExUnit.Runner do
end
Process.exit(test_pid, :kill)
Process.demonitor(test_ref, [:flush])
%{test | state: {:failed, {:error, %ExUnit.TimeoutError{timeout: timeout}, stacktrace}}}
%{test | state: failed(:error, %ExUnit.TimeoutError{timeout: timeout}, stacktrace)}
end
exec_on_exit(test, test_pid)
@@ -285,9 +285,8 @@ defmodule ExUnit.Runner do
{:ok, context} = case.__ex_unit__(:setup, context)
{:ok, test, context}
catch
kind2, error2 ->
failed = {:failed, {kind2, Exception.normalize(kind2, error2), pruned_stacktrace()}}
{:error, %{test | state: failed}}
kind, error ->
{:error, %{test | state: failed(kind, error, pruned_stacktrace())}}
end
defp exec_test(%ExUnit.Test{case: case, name: name} = test, context) do
@@ -295,8 +294,7 @@ defmodule ExUnit.Runner do
test
catch
kind, error ->
failed = {:failed, {kind, Exception.normalize(kind, error), pruned_stacktrace()}}
%{test | state: failed}
%{test | state: failed(kind, error, pruned_stacktrace())}
end
defp exec_on_exit(test_or_case, pid) do
@@ -304,7 +302,7 @@ defmodule ExUnit.Runner do
:ok ->
test_or_case
{kind, reason, stack} ->
state = test_or_case.state || {:failed, {kind, reason, prune_stacktrace(stack)}}
state = test_or_case.state || failed(kind, reason, prune_stacktrace(stack))
%{test_or_case | state: state}
end
end
@@ -344,6 +342,14 @@ defmodule ExUnit.Runner do
end
end
defp failed(:error, %ExUnit.MultiError{errors: errors}, stack) do
{:failed, Enum.map(errors, &{:error, Exception.normalize(:error, &1), stack})}
end
defp failed(kind, reason, stack) do
{:failed, [{kind, Exception.normalize(kind, reason), stack}]}
end
defp pruned_stacktrace, do: prune_stacktrace(System.stacktrace)
# Assertions can pop-up in the middle of the stack
+32 -13
View File
@@ -34,7 +34,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats test errors" do
failure = {:error, catch_error(raise "oops"), []}
failure = [{:error, catch_error(raise "oops"), []}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
@@ -43,7 +43,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats test exits" do
failure = {:exit, 1, []}
failure = [{:exit, 1, []}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
@@ -52,7 +52,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats test exits with mfa" do
failure = {:exit, {:bye, {:m, :f, []}}, []}
failure = [{:exit, {:bye, {:m, :f, []}}, []}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
@@ -62,7 +62,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats test throws" do
failure = {:throw, 1, []}
failure = [{:throw, 1, []}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
@@ -71,7 +71,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats test EXITs" do
failure = {{:EXIT, self}, 1, []}
failure = [{{:EXIT, self}, 1, []}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) == """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
@@ -80,7 +80,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats stacktraces" do
failure = {:error, catch_error(raise "oops"), [{Oops, :wrong, 1, [file: "formatter_test.exs", line: 1]}]}
failure = [{:error, catch_error(raise "oops"), [{Oops, :wrong, 1, [file: "formatter_test.exs", line: 1]}]}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
@@ -91,7 +91,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats assertions" do
failure = {:error, catch_assertion(assert ExUnit.FormatterTest.falsy), []}
failure = [{:error, catch_assertion(assert ExUnit.FormatterTest.falsy), []}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
@@ -100,8 +100,27 @@ defmodule ExUnit.FormatterTest do
"""
end
test "formats multiple assertions" do
failure = [{:error, catch_assertion(assert ExUnit.FormatterTest.falsy), []},
{:error, catch_assertion(assert 1 == 2), []}]
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
1) world (Hello)
test/ex_unit/formatter_test.exs:1
Failure #1
Expected truthy, got false
code: ExUnit.FormatterTest.falsy()
Failure #2
Assertion with == failed
code: 1 == 2
lhs: 1
rhs: 2
"""
end
test "formats test case errors" do
failure = {:error, catch_error(raise "oops"), []}
failure = [{:error, catch_error(raise "oops"), []}]
assert format_test_case_failure(case(), failure, 1, 80, &formatter/2) =~ """
1) Hello: failure on setup_all callback, tests invalidated
** (RuntimeError) oops
@@ -109,7 +128,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats assertions with operators with no limit" do
failure = {:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}
failure = [{:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}]
assert format_test_case_failure(case(), failure, 1, :infinity, &formatter/2) =~ """
1) Hello: failure on setup_all callback, tests invalidated
Assertion with == failed
@@ -120,7 +139,7 @@ defmodule ExUnit.FormatterTest do
end
test "formats assertions with operators with column limit" do
failure = {:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}
failure = [{:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}]
assert format_test_case_failure(case(), failure, 1, 15, &formatter/2) =~ """
1) Hello: failure on setup_all callback, tests invalidated
Assertion with == failed
@@ -136,7 +155,7 @@ defmodule ExUnit.FormatterTest do
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)), []}
failure = [{:error, catch_assertion(assert(false, message)), []}]
assert format_test_case_failure(case(), failure, 1, :infinity, &formatter/2) =~ """
1) Hello: failure on setup_all callback, tests invalidated
Some meaningful error:
@@ -156,7 +175,7 @@ defmodule ExUnit.FormatterTest do
end
test "inspect failure" do
failure = {:error, catch_assertion(assert :will_fail == %BadInspect{}), []}
failure = [{:error, catch_assertion(assert :will_fail == %BadInspect{}), []}]
message = "got FunctionClauseError with message \"no function clause matching " <>
"in Inspect.ExUnit.FormatterTest.BadInspect.inspect/2\" while inspecting " <>
@@ -181,7 +200,7 @@ defmodule ExUnit.FormatterTest do
end
test "message failure" do
failure = {:error, catch_error(raise BadMessage), []}
failure = [{:error, catch_error(raise BadMessage), []}]
message = "got RuntimeError with message \"oops\" while retrieving Exception.message/1 " <>
"for %ExUnit.FormatterTest.BadMessage{key: 0}"
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
+44 -6
View File
@@ -7,7 +7,7 @@ defmodule ExUnitTest do
test "it supports many runs" do
defmodule SampleTest do
use ExUnit.Case, async: false
use ExUnit.Case
test "true" do
assert false
@@ -25,7 +25,7 @@ defmodule ExUnitTest do
test "it doesn't hang on exists" do
defmodule EventServerTest do
use ExUnit.Case, async: false
use ExUnit.Case
test "spawn and crash" do
spawn_link(fn ->
@@ -67,7 +67,6 @@ defmodule ExUnitTest do
ExUnit.configure(timeout: 5)
output = capture_io(fn -> ExUnit.run end)
assert output =~ "** (ExUnit.TimeoutError) test timed out after 5ms"
after
ExUnit.configure(timeout: 60_000)
end
@@ -153,10 +152,49 @@ defmodule ExUnitTest do
refute output =~ "[debug] four"
end
test "it supports multi errors" do
capture_io :stderr, fn ->
defmodule MultiTest do
use ExUnit.Case
test "multi" do
error1 =
try do
assert 1 = 2
rescue e in ExUnit.AssertionError ->
e
end
error2 =
try do
assert 4 > 3
rescue e in ExUnit.AssertionError ->
e
end
raise ExUnit.MultiError, errors: [error1, error2]
end
end
end
output = capture_io(fn ->
assert ExUnit.run == %{failures: 1, skipped: 0, total: 1}
end)
assert output =~ "1 test, 1 failure"
assert output =~ """
1) test multi (ExUnitTest.MultiTest)
test/ex_unit_test.exs:160
Failure #1
match (=) failed
"""
end
test "it registers only the first test with any given name" do
capture_io :stderr, fn ->
defmodule TestWithSameNames do
use ExUnit.Case, async: false
use ExUnit.Case
test "same name, different outcome" do
assert 1 == 1
@@ -175,7 +213,7 @@ defmodule ExUnitTest do
test "it produces error on not implemented tests" do
defmodule TestNotImplemented do
use ExUnit.Case, async: false
use ExUnit.Case
setup context do
assert context[:not_implemented]
@@ -195,7 +233,7 @@ defmodule ExUnitTest do
test "it skips tagged test with skip" do
defmodule TestSkipped do
use ExUnit.Case, async: false
use ExUnit.Case
setup context do
assert context[:not_implemented]