Skip to content

Commit ef4c926

Browse files
committed
Provide exception blaming to linked and trapped exits in ExUnit
1 parent d1acad0 commit ef4c926

File tree

3 files changed

+218
-53
lines changed

3 files changed

+218
-53
lines changed

lib/ex_unit/examples/one_of_each.exs

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ExUnit.start [seed: 0]
1+
ExUnit.start(seed: 0)
22

33
defmodule TestOneOfEach do
44
@moduledoc """
@@ -10,8 +10,8 @@ defmodule TestOneOfEach do
1010
@one 1
1111
@two 2
1212

13-
@long_data_1 [field1: "one", field2: {:two1, :two2}, field3: 'three', field4: [1, 2, 3, 4]]
14-
@long_data_2 [field1: "one", field2: {:two1, :two3}, field3: 'three', field4: [1, 2, 3, 4]]
13+
@long_data_1 [field1: "one", field2: {:two1, :two2}, field3: 'three', field4: [1, 2, 3, 4]]
14+
@long_data_2 [field1: "one", field2: {:two1, :two3}, field3: 'three', field4: [1, 2, 3, 4]]
1515

1616
setup do
1717
{:ok, user_id: 1, post_id: 2, many_ids: Enum.to_list(1..50)}
@@ -62,10 +62,10 @@ defmodule TestOneOfEach do
6262
end
6363

6464
test "12. assert that a message is received within a timeout" do
65-
send self(), {:ok, 1}
66-
send self(), :message_in_my_inbox
67-
send self(), {:ok, 2}
68-
send self(), :another_message
65+
send(self(), {:ok, 1})
66+
send(self(), :message_in_my_inbox)
67+
send(self(), {:ok, 2})
68+
send(self(), :another_message)
6969
assert_receive :no_message_after_timeout
7070
end
7171

@@ -75,35 +75,35 @@ defmodule TestOneOfEach do
7575

7676
test "14. assert an exception with a given message is raised" do
7777
assert_raise(SomeException, "some message", fn ->
78-
raise "other exception"
79-
end)
78+
raise "other exception"
79+
end)
8080
end
8181

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

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

9292
test "17. assert two values are within some delta" do
93-
assert_in_delta 3.1415926, 22.0/7, 0.001
93+
assert_in_delta 3.1415926, 22.0 / 7, 0.001
9494
end
9595

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

100100
test "19. refute a message is received within a timeout" do
101-
send self(), {:hello, "Dave"}
101+
send(self(), {:hello, "Dave"})
102102
refute_receive {:hello, _}, 1000
103103
end
104104

105105
test "20. refute a message is ready to be received" do
106-
send self(), :hello_again
106+
send(self(), :hello_again)
107107
refute_received :hello_again
108108
end
109109

@@ -116,15 +116,16 @@ defmodule TestOneOfEach do
116116
end
117117

118118
test "23. flunk" do
119-
flunk "we failed. totally"
119+
flunk("we failed. totally")
120120
end
121121

122122
test "24. exception raised while running test" do
123123
assert blows_up()
124124
end
125125

126126
test "25. error due to exit" do
127-
spawn_link fn -> raise "oops" end
127+
spawn_link(fn -> raise "oops" end)
128+
128129
receive do
129130
end
130131
end
@@ -133,15 +134,17 @@ defmodule TestOneOfEach do
133134
error1 =
134135
try do
135136
assert [@one] = [@two]
136-
rescue e in ExUnit.AssertionError ->
137-
{:error, e, System.stacktrace}
137+
rescue
138+
e in ExUnit.AssertionError ->
139+
{:error, e, __STACKTRACE__}
138140
end
139141

140142
error2 =
141143
try do
142144
assert @one * 4 > @two * 3
143-
rescue e in ExUnit.AssertionError ->
144-
{:error, e, System.stacktrace}
145+
rescue
146+
e in ExUnit.AssertionError ->
147+
{:error, e, __STACKTRACE__}
145148
end
146149

147150
raise ExUnit.MultiError, errors: [error1, error2]
@@ -150,8 +153,8 @@ defmodule TestOneOfEach do
150153
@tag capture_log: true
151154
test "27. log capturing" do
152155
require Logger
153-
Logger.debug "this will be logged"
154-
flunk "oops"
156+
Logger.debug("this will be logged")
157+
flunk("oops")
155158
end
156159

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

168+
@tag :capture_log
169+
test "30. linked assertion error" do
170+
Task.async(fn -> assert 1 == 2 end) |> Task.await()
171+
end
172+
173+
@tag :capture_log
174+
test "31. linked function clause error" do
175+
Task.async(fn -> Access.fetch(:foo, :bar) end) |> Task.await()
176+
end
177+
178+
@tag :capture_log
179+
test "32. trapped assertion error" do
180+
Process.flag(:trap_exit, true)
181+
Task.async(fn -> assert 1 == 2 end) |> Task.await()
182+
end
183+
184+
@tag :capture_log
185+
test "33. trapped function clause error" do
186+
Process.flag(:trap_exit, true)
187+
Task.async(fn -> Access.fetch(:foo, :bar) end) |> Task.await()
188+
end
189+
165190
defp some_vars(_a, _b) do
166191
false
167192
end
@@ -171,6 +196,6 @@ defmodule TestOneOfEach do
171196
end
172197

173198
defp ignite(val) do
174-
1/val
199+
1 / val
175200
end
176201
end

lib/ex_unit/lib/ex_unit/formatter.ex

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ defmodule ExUnit.Formatter do
5454

5555
@counter_padding " "
5656
@mailbox_label_padding @counter_padding <> " "
57+
@formatter_exceptions [ExUnit.AssertionError, FunctionClauseError]
5758
@no_value ExUnit.AssertionError.no_value()
5859

5960
@doc """
@@ -136,10 +137,10 @@ defmodule ExUnit.Formatter do
136137

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

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

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

151-
[
152-
note: if_value(struct.message, &format_message(&1, formatter)),
153-
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
154-
code: if_value(struct.expr, code_multiline),
155-
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
156-
arguments: if_value(struct.args, &format_args(&1, width))
157-
]
158-
|> Kernel.++(format_context(struct, formatter, padding_size, width))
159-
|> format_meta(formatter, counter_padding, label_padding_size)
160-
|> IO.iodata_to_binary()
152+
formatted =
153+
[
154+
note: if_value(struct.message, &format_message(&1, formatter)),
155+
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
156+
code: if_value(struct.expr, code_multiline),
157+
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
158+
arguments: if_value(struct.args, &format_args(&1, width))
159+
]
160+
|> Kernel.++(format_context(struct, formatter, padding_size, width))
161+
|> format_meta(formatter, pad, label_padding_size)
162+
|> IO.iodata_to_binary()
163+
164+
{formatted, stack}
165+
end
166+
167+
defp format_exception(test, %FunctionClauseError{} = struct, stack, _width, formatter, _pad) do
168+
{blamed, stack} = Exception.blame(:error, struct, stack)
169+
banner = Exception.format_banner(:error, struct)
170+
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, &2, formatter))
171+
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
172+
{message <> format_code(test, stack, formatter), stack}
161173
end
162174

163175
@doc false
@@ -179,30 +191,48 @@ defmodule ExUnit.Formatter do
179191
end)
180192
end
181193

182-
defp format_kind_reason(
183-
test,
184-
:error,
185-
%ExUnit.AssertionError{} = struct,
186-
stack,
187-
width,
188-
formatter
189-
) do
190-
{format_assertion_error(test, struct, stack, width, formatter, @counter_padding), stack}
194+
defp format_kind_reason(test, :error, %mod{} = struct, stack, width, formatter)
195+
when mod in @formatter_exceptions do
196+
format_exception(test, struct, stack, width, formatter, @counter_padding)
191197
end
192198

193-
defp format_kind_reason(test, :error, %FunctionClauseError{} = struct, stack, _width, formatter) do
194-
{blamed, stack} = Exception.blame(:error, struct, stack)
195-
banner = Exception.format_banner(:error, struct)
196-
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, &2, formatter))
197-
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
198-
{message <> format_code(test, stack, formatter), stack}
199+
defp format_kind_reason(test, kind, reason, stack, width, formatter) do
200+
case linked_or_trapped_exit(kind, reason) do
201+
{header, wrapped_reason, wrapped_stack} ->
202+
struct = Exception.normalize(:error, wrapped_reason, wrapped_stack)
203+
204+
{formatted_reason, _} =
205+
format_exception(test, struct, wrapped_stack, width, formatter, @counter_padding)
206+
207+
formatted_stack = format_stacktrace(wrapped_stack, test.module, test.name, formatter)
208+
{error_info(header, formatter) <> pad(formatted_reason <> formatted_stack), stack}
209+
210+
:error ->
211+
{reason, stack} = Exception.blame(kind, reason, stack)
212+
message = error_info(Exception.format_banner(kind, reason), formatter)
213+
{message <> format_code(test, stack, formatter), stack}
214+
end
199215
end
200216

201-
defp format_kind_reason(test, kind, reason, stack, _width, formatter) do
202-
message = error_info(Exception.format_banner(kind, reason), formatter)
203-
{message <> format_code(test, stack, formatter), stack}
217+
defp linked_or_trapped_exit({:EXIT, pid}, {reason, [_ | _] = stack})
218+
when :erlang.map_get(:__struct__, reason) in @formatter_exceptions
219+
when reason == :function_clause do
220+
{"** (EXIT from #{inspect(pid)}) an exception was raised:\n", reason, stack}
204221
end
205222

223+
defp linked_or_trapped_exit(:exit, {{reason, [_ | _] = stack}, {mod, fun, args}})
224+
when is_atom(mod) and is_atom(fun) and is_list(args) and
225+
:erlang.map_get(:__struct__, reason) in @formatter_exceptions
226+
when is_atom(mod) and is_atom(fun) and is_list(args) and reason == :function_clause do
227+
{
228+
"** (exit) exited in: #{Exception.format_mfa(mod, fun, args)}\n ** (EXIT) an exception was raised:",
229+
reason,
230+
stack
231+
}
232+
end
233+
234+
defp linked_or_trapped_exit(_kind, _reason), do: :error
235+
206236
defp format_code(test, stack, formatter) do
207237
if snippet = get_code(test, stack) do
208238
" " <> formatter.(:extra_info, "code: ") <> snippet <> "\n"

0 commit comments

Comments
 (0)