diff --git a/lib/ex_unit/examples/one_of_each.exs b/lib/ex_unit/examples/one_of_each.exs index b1abb44415..1b0e519f95 100644 --- a/lib/ex_unit/examples/one_of_each.exs +++ b/lib/ex_unit/examples/one_of_each.exs @@ -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 diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index 3caee0685f..ce3f1afc64 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -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, diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index 3dc0f6971e..3806cbde71 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -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} diff --git a/lib/ex_unit/lib/ex_unit/cli_formatter.ex b/lib/ex_unit/lib/ex_unit/cli_formatter.ex index 6509b46728..8a7095198f 100644 --- a/lib/ex_unit/lib/ex_unit/cli_formatter.ex +++ b/lib/ex_unit/lib/ex_unit/cli_formatter.ex @@ -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 diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index b7f9f79147..9250152b8a 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -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 diff --git a/lib/ex_unit/lib/ex_unit/on_exit_handler.ex b/lib/ex_unit/lib/ex_unit/on_exit_handler.ex index 773e702555..671b593b85 100644 --- a/lib/ex_unit/lib/ex_unit/on_exit_handler.ex +++ b/lib/ex_unit/lib/ex_unit/on_exit_handler.ex @@ -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 diff --git a/lib/ex_unit/lib/ex_unit/runner.ex b/lib/ex_unit/lib/ex_unit/runner.ex index acd2327538..6e16645416 100644 --- a/lib/ex_unit/lib/ex_unit/runner.ex +++ b/lib/ex_unit/lib/ex_unit/runner.ex @@ -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 diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index 183cf61461..e1df2ebc5a 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -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) =~ """ diff --git a/lib/ex_unit/test/ex_unit_test.exs b/lib/ex_unit/test/ex_unit_test.exs index 4ba6ceec39..719f0e9f7c 100644 --- a/lib/ex_unit/test/ex_unit_test.exs +++ b/lib/ex_unit/test/ex_unit_test.exs @@ -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]