Skip to content

Commit b08eea7

Browse files
authored
Prevent exits from leaking in ExUnit.CaptureServer (#14348)
When a process that traps exits used ExUnit's log capture functionality, it would receive stray exits from the Tasks started inside it that could lead to issues if not handled.
1 parent b601b1c commit b08eea7

File tree

2 files changed

+43
-21
lines changed

2 files changed

+43
-21
lines changed

lib/ex_unit/lib/ex_unit/capture_server.ex

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -231,29 +231,41 @@ defmodule ExUnit.CaptureServer do
231231
## :logger handler callback.
232232

233233
def log(event, _config) do
234-
:ets.tab2list(@ets)
235-
|> Enum.filter(fn {_ref, _string_io, level, _formatter_mod, _formatter_config} ->
236-
:logger.compare_levels(event.level, level) in [:gt, :eq]
237-
end)
238-
|> Enum.group_by(
239-
fn {_ref, _string_io, _level, formatter_mod, formatter_config} ->
240-
{formatter_mod, formatter_config}
241-
end,
242-
fn {_ref, string_io, _level, _formatter_mod, _formatter_config} ->
243-
string_io
244-
end
245-
)
246-
|> Enum.map(fn {{formatter_mod, formatter_config}, string_ios} ->
247-
Task.async(fn ->
248-
chardata = formatter_mod.format(event, formatter_config)
249-
250-
# Simply send, do not wait for reply
251-
for string_io <- string_ios do
252-
send(string_io, {:io_request, self(), make_ref(), {:put_chars, :unicode, chardata}})
234+
{:trap_exit, trapping_exits?} = Process.info(self(), :trap_exit)
235+
236+
tasks =
237+
:ets.tab2list(@ets)
238+
|> Enum.filter(fn {_ref, _string_io, level, _formatter_mod, _formatter_config} ->
239+
:logger.compare_levels(event.level, level) in [:gt, :eq]
240+
end)
241+
|> Enum.group_by(
242+
fn {_ref, _string_io, _level, formatter_mod, formatter_config} ->
243+
{formatter_mod, formatter_config}
244+
end,
245+
fn {_ref, string_io, _level, _formatter_mod, _formatter_config} ->
246+
string_io
253247
end
248+
)
249+
|> Enum.map(fn {{formatter_mod, formatter_config}, string_ios} ->
250+
Task.async(fn ->
251+
chardata = formatter_mod.format(event, formatter_config)
252+
253+
# Simply send, do not wait for reply
254+
for string_io <- string_ios do
255+
send(string_io, {:io_request, self(), make_ref(), {:put_chars, :unicode, chardata}})
256+
end
257+
end)
254258
end)
255-
end)
256-
|> Task.await_many(:infinity)
259+
260+
Task.await_many(tasks)
261+
262+
if trapping_exits? do
263+
for %{pid: pid} <- tasks do
264+
receive do
265+
{:EXIT, ^pid, _} -> :ok
266+
end
267+
end
268+
end
257269

258270
:ok
259271
end

lib/ex_unit/test/ex_unit/capture_log_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ defmodule ExUnit.CaptureLogTest do
9595
end)
9696
end
9797

98+
test "exits don't leak" do
99+
Process.flag(:trap_exit, true)
100+
101+
capture_log(fn ->
102+
Logger.error("oh no!")
103+
end)
104+
105+
refute_receive {:EXIT, _, _}
106+
end
107+
98108
describe "with_log/2" do
99109
test "returns the result and the log" do
100110
{result, log} =

0 commit comments

Comments
 (0)