Skip to content

Commit

Permalink
Provide exception blaming to linked and trapped exits in ExUnit
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed May 22, 2020
1 parent 44061c8 commit 10fdb8e
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 53 deletions.
71 changes: 48 additions & 23 deletions lib/ex_unit/examples/one_of_each.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ExUnit.start [seed: 0]
ExUnit.start(seed: 0)

defmodule TestOneOfEach do
@moduledoc """
Expand All @@ -10,8 +10,8 @@ defmodule TestOneOfEach do
@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]]

setup do
{:ok, user_id: 1, post_id: 2, many_ids: Enum.to_list(1..50)}
Expand Down Expand Up @@ -62,10 +62,10 @@ defmodule TestOneOfEach do
end

test "12. assert that a message is received within a timeout" do
send self(), {:ok, 1}
send self(), :message_in_my_inbox
send self(), {:ok, 2}
send self(), :another_message
send(self(), {:ok, 1})
send(self(), :message_in_my_inbox)
send(self(), {:ok, 2})
send(self(), :another_message)
assert_receive :no_message_after_timeout
end

Expand All @@ -75,35 +75,35 @@ defmodule TestOneOfEach do

test "14. assert an exception with a given message is raised" do
assert_raise(SomeException, "some message", fn ->
raise "other exception"
end)
raise "other exception"
end)
end

test "15. assert an exception with a given message is raised, but the message is wrong" do
assert_raise(RuntimeError, "some message", fn ->
raise "other error"
end)
raise "other error"
end)
end

test "16. assert an exception is raised" do
assert_raise(SomeException, fn -> nil end)
end

test "17. assert two values are within some delta" do
assert_in_delta 3.1415926, 22.0/7, 0.001
assert_in_delta 3.1415926, 22.0 / 7, 0.001
end

test "18. refute a value with a message" do
refute @one != @two, "one should equal two"
end

test "19. refute a message is received within a timeout" do
send self(), {:hello, "Dave"}
send(self(), {:hello, "Dave"})
refute_receive {:hello, _}, 1000
end

test "20. refute a message is ready to be received" do
send self(), :hello_again
send(self(), :hello_again)
refute_received :hello_again
end

Expand All @@ -116,15 +116,16 @@ defmodule TestOneOfEach do
end

test "23. flunk" do
flunk "we failed. totally"
flunk("we failed. totally")
end

test "24. exception raised while running test" do
assert blows_up()
end

test "25. error due to exit" do
spawn_link fn -> raise "oops" end
spawn_link(fn -> raise "oops" end)

receive do
end
end
Expand All @@ -133,15 +134,17 @@ defmodule TestOneOfEach do
error1 =
try do
assert [@one] = [@two]
rescue e in ExUnit.AssertionError ->
{:error, e, System.stacktrace}
rescue
e in ExUnit.AssertionError ->
{:error, e, __STACKTRACE__}
end

error2 =
try do
assert @one * 4 > @two * 3
rescue e in ExUnit.AssertionError ->
{:error, e, System.stacktrace}
rescue
e in ExUnit.AssertionError ->
{:error, e, __STACKTRACE__}
end

raise ExUnit.MultiError, errors: [error1, error2]
Expand All @@ -150,8 +153,8 @@ defmodule TestOneOfEach do
@tag capture_log: true
test "27. log capturing" do
require Logger
Logger.debug "this will be logged"
flunk "oops"
Logger.debug("this will be logged")
flunk("oops")
end

test "28. function clause error" do
Expand All @@ -162,6 +165,28 @@ defmodule TestOneOfEach do
assert some_vars(1 + 2, 3 + 4)
end

@tag :capture_log
test "30. linked assertion error" do
Task.async(fn -> assert 1 == 2 end) |> Task.await()
end

@tag :capture_log
test "31. linked function clause error" do
Task.async(fn -> Access.fetch(:foo, :bar) end) |> Task.await()
end

@tag :capture_log
test "32. trapped assertion error" do
Process.flag(:trap_exit, true)
Task.async(fn -> assert 1 == 2 end) |> Task.await()
end

@tag :capture_log
test "33. trapped function clause error" do
Process.flag(:trap_exit, true)
Task.async(fn -> Access.fetch(:foo, :bar) end) |> Task.await()
end

defp some_vars(_a, _b) do
false
end
Expand All @@ -171,6 +196,6 @@ defmodule TestOneOfEach do
end

defp ignite(val) do
1/val
1 / val
end
end
90 changes: 60 additions & 30 deletions lib/ex_unit/lib/ex_unit/formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ defmodule ExUnit.Formatter do

@counter_padding " "
@mailbox_label_padding @counter_padding <> " "
@formatter_exceptions [ExUnit.AssertionError, FunctionClauseError]
@no_value ExUnit.AssertionError.no_value()

@doc """
Expand Down Expand Up @@ -136,10 +137,10 @@ defmodule ExUnit.Formatter do

@doc false
def format_assertion_error(%ExUnit.AssertionError{} = struct) do
format_assertion_error(%{}, struct, [], :infinity, fn _, msg -> msg end, "")
format_exception(%{}, struct, [], :infinity, fn _, msg -> msg end, "") |> elem(0)
end

defp format_assertion_error(test, struct, stack, width, formatter, counter_padding) do
defp format_exception(test, %ExUnit.AssertionError{} = struct, stack, width, formatter, pad) do
label_padding_size = if has_value?(struct.right), do: 7, else: 6
padding_size = label_padding_size + byte_size(@counter_padding)

Expand All @@ -148,16 +149,27 @@ defmodule ExUnit.Formatter do
do: &pad_multiline(&1, padding_size),
else: &code_multiline(&1, padding_size)

[
note: if_value(struct.message, &format_message(&1, formatter)),
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
code: if_value(struct.expr, code_multiline),
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
arguments: if_value(struct.args, &format_args(&1, width))
]
|> Kernel.++(format_context(struct, formatter, padding_size, width))
|> format_meta(formatter, counter_padding, label_padding_size)
|> IO.iodata_to_binary()
formatted =
[
note: if_value(struct.message, &format_message(&1, formatter)),
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
code: if_value(struct.expr, code_multiline),
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
arguments: if_value(struct.args, &format_args(&1, width))
]
|> Kernel.++(format_context(struct, formatter, padding_size, width))
|> format_meta(formatter, pad, label_padding_size)
|> IO.iodata_to_binary()

{formatted, stack}
end

defp format_exception(test, %FunctionClauseError{} = struct, stack, _width, formatter, _pad) do
{blamed, stack} = Exception.blame(:error, struct, stack)
banner = Exception.format_banner(:error, struct)
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, &2, formatter))
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
{message <> format_code(test, stack, formatter), stack}
end

@doc false
Expand All @@ -179,30 +191,48 @@ defmodule ExUnit.Formatter do
end)
end

defp format_kind_reason(
test,
:error,
%ExUnit.AssertionError{} = struct,
stack,
width,
formatter
) do
{format_assertion_error(test, struct, stack, width, formatter, @counter_padding), stack}
defp format_kind_reason(test, :error, %mod{} = struct, stack, width, formatter)
when mod in @formatter_exceptions do
format_exception(test, struct, stack, width, formatter, @counter_padding)
end

defp format_kind_reason(test, :error, %FunctionClauseError{} = struct, stack, _width, formatter) do
{blamed, stack} = Exception.blame(:error, struct, stack)
banner = Exception.format_banner(:error, struct)
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, &2, formatter))
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
{message <> format_code(test, stack, formatter), stack}
defp format_kind_reason(test, kind, reason, stack, width, formatter) do
case linked_or_trapped_exit(kind, reason) do
{header, wrapped_reason, wrapped_stack} ->
struct = Exception.normalize(:error, wrapped_reason, wrapped_stack)

{formatted_reason, _} =
format_exception(test, struct, wrapped_stack, width, formatter, @counter_padding)

formatted_stack = format_stacktrace(wrapped_stack, test.module, test.name, formatter)
{error_info(header, formatter) <> pad(formatted_reason <> formatted_stack), stack}

:error ->
{reason, stack} = Exception.blame(kind, reason, stack)
message = error_info(Exception.format_banner(kind, reason), formatter)
{message <> format_code(test, stack, formatter), stack}
end
end

defp format_kind_reason(test, kind, reason, stack, _width, formatter) do
message = error_info(Exception.format_banner(kind, reason), formatter)
{message <> format_code(test, stack, formatter), stack}
defp linked_or_trapped_exit({:EXIT, pid}, {reason, [_ | _] = stack})
when reason.__struct__ in @formatter_exceptions
when reason == :function_clause do
{"** (EXIT from #{inspect(pid)}) an exception was raised:\n", reason, stack}
end

defp linked_or_trapped_exit(:exit, {{reason, [_ | _] = stack}, {mod, fun, args}})
when is_atom(mod) and is_atom(fun) and is_list(args) and
reason.__struct__ in @formatter_exceptions
when is_atom(mod) and is_atom(fun) and is_list(args) and reason == :function_clause do
{
"** (exit) exited in: #{Exception.format_mfa(mod, fun, args)}\n ** (EXIT) an exception was raised:",
reason,
stack
}
end

defp linked_or_trapped_exit(_kind, _reason), do: :error

defp format_code(test, stack, formatter) do
if snippet = get_code(test, stack) do
" " <> formatter.(:extra_info, "code: ") <> snippet <> "\n"
Expand Down
Loading

0 comments on commit 10fdb8e

Please sign in to comment.