Skip to content

Commit 10e14ea

Browse files
authored
Use Erlang logger handlers in ExUnit.CaptureLog (#12408)
1 parent fca7d2a commit 10e14ea

File tree

3 files changed

+71
-77
lines changed

3 files changed

+71
-77
lines changed

lib/ex_unit/lib/ex_unit/capture_log.ex

Lines changed: 5 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ defmodule ExUnit.CaptureLog do
3737
3838
"""
3939

40-
alias Logger.Backends.Console
41-
42-
@compile {:no_warn_undefined, [Logger, Logger.Backends.Internal]}
40+
@compile {:no_warn_undefined, Logger}
4341

4442
@doc """
4543
Captures Logger messages generated when evaluating `fun`.
@@ -93,6 +91,7 @@ defmodule ExUnit.CaptureLog do
9391
9492
assert result == 4
9593
assert log =~ "log msg"
94+
9695
"""
9796
@doc since: "1.13.0"
9897
@spec with_log(keyword, (-> result)) :: {result, String.t()} when result: any
@@ -101,74 +100,22 @@ defmodule ExUnit.CaptureLog do
101100
{:ok, string_io} = StringIO.open("")
102101

103102
try do
104-
:ok = add_capture(string_io, opts)
105-
ref = ExUnit.CaptureServer.log_capture_on(self())
103+
ref = ExUnit.CaptureServer.log_capture_on(self(), string_io, opts)
106104

107105
try do
108106
fun.()
109107
after
110108
:ok = Logger.flush()
111109
:ok = ExUnit.CaptureServer.log_capture_off(ref)
112-
:ok = remove_capture(string_io)
113110
end
114111
catch
115112
kind, reason ->
116113
_ = StringIO.close(string_io)
117114
:erlang.raise(kind, reason, __STACKTRACE__)
118115
else
119116
result ->
120-
{:ok, content} = StringIO.close(string_io)
121-
{result, elem(content, 1)}
122-
end
123-
end
124-
125-
defp add_capture(pid, opts) do
126-
case :proc_lib.start(__MODULE__, :init_proxy, [pid, opts, self()]) do
127-
:ok ->
128-
:ok
129-
130-
:noproc ->
131-
raise "cannot capture_log/2 because the :logger application was not started"
132-
133-
{:error, reason} ->
134-
mfa = {ExUnit.CaptureLog, :add_capture, [pid, opts]}
135-
exit({reason, mfa})
136-
end
137-
end
138-
139-
@doc false
140-
def init_proxy(pid, opts, parent) do
141-
Logger.Backends.Internal.configure([])
142-
catch
143-
_, _ -> :proc_lib.init_ack(:noproc)
144-
else
145-
_ ->
146-
case :gen_event.add_sup_handler(Logger, {Console, pid}, {Console, [device: pid] ++ opts}) do
147-
:ok ->
148-
ref = Process.monitor(parent)
149-
:proc_lib.init_ack(:ok)
150-
151-
receive do
152-
{:DOWN, ^ref, :process, ^parent, _reason} -> :ok
153-
{:gen_event_EXIT, {Console, ^pid}, _reason} -> :ok
154-
end
155-
156-
{:EXIT, reason} ->
157-
:proc_lib.init_ack({:error, reason})
158-
159-
{:error, reason} ->
160-
:proc_lib.init_ack({:error, reason})
161-
end
162-
end
163-
164-
defp remove_capture(pid) do
165-
case :gen_event.delete_handler(Logger, {Console, pid}, :ok) do
166-
:ok ->
167-
:ok
168-
169-
{:error, :module_not_found} = error ->
170-
mfa = {ExUnit.CaptureLog, :remove_capture, [pid]}
171-
exit({error, mfa})
117+
{:ok, {_input, output}} = StringIO.close(string_io)
118+
{result, output}
172119
end
173120
end
174121
end

lib/ex_unit/lib/ex_unit/capture_server.ex

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule ExUnit.CaptureServer do
33
@compile {:no_warn_undefined, Logger}
44
@timeout :infinity
55
@name __MODULE__
6+
@ets __MODULE__
67

78
use GenServer
89

@@ -22,8 +23,8 @@ defmodule ExUnit.CaptureServer do
2223
GenServer.call(@name, {:device_capture_off, ref}, @timeout)
2324
end
2425

25-
def log_capture_on(pid) do
26-
GenServer.call(@name, {:log_capture_on, pid}, @timeout)
26+
def log_capture_on(pid, string_io, opts) do
27+
GenServer.call(@name, {:log_capture_on, pid, string_io, opts}, @timeout)
2728
end
2829

2930
def log_capture_off(ref) do
@@ -32,7 +33,10 @@ defmodule ExUnit.CaptureServer do
3233

3334
## Callbacks
3435

36+
@impl true
3537
def init(:ok) do
38+
:ets.new(@name, [:named_table, :public, :set])
39+
3640
state = %{
3741
devices: %{},
3842
log_captures: %{},
@@ -42,6 +46,9 @@ defmodule ExUnit.CaptureServer do
4246
{:ok, state}
4347
end
4448

49+
@impl true
50+
def handle_call(call, from, state)
51+
4552
def handle_call({:device_capture_on, name, encoding, input}, {caller, _}, config) do
4653
capture_device(name, encoding, input, config, caller)
4754
end
@@ -59,11 +66,17 @@ defmodule ExUnit.CaptureServer do
5966
{:reply, :ok, release_device(ref, config)}
6067
end
6168

62-
def handle_call({:log_capture_on, pid}, _from, config) do
69+
def handle_call({:log_capture_on, pid, string_io, opts}, _from, config) do
6370
ref = Process.monitor(pid)
6471
refs = Map.put(config.log_captures, ref, true)
6572

73+
{level, opts} = Keyword.pop(opts, :level)
74+
true = :ets.insert(@ets, {ref, string_io, level || :all})
75+
6676
if map_size(refs) == 1 do
77+
formatter = Logger.default_formatter(opts)
78+
:ok = :logger.add_handler(@name, __MODULE__, %{formatter: formatter})
79+
6780
status =
6881
with {:ok, config} <- :logger.get_handler_config(:default),
6982
:ok <- :logger.remove_handler(:default) do
@@ -84,6 +97,7 @@ defmodule ExUnit.CaptureServer do
8497
{:reply, :ok, config}
8598
end
8699

100+
@impl true
87101
def handle_info({:DOWN, ref, _, _, _}, config) do
88102
config = remove_log_capture(ref, config)
89103
config = release_device(ref, config)
@@ -186,21 +200,39 @@ defmodule ExUnit.CaptureServer do
186200
end
187201

188202
defp remove_log_capture(ref, %{log_captures: refs} = config) do
203+
true = :ets.delete(@ets, ref)
204+
189205
case Map.pop(refs, ref, false) do
190206
{true, refs} ->
191-
maybe_add_console(refs, config.log_status)
207+
maybe_revert_to_default_handler(refs, config.log_status)
192208
%{config | log_captures: refs}
193209

194210
{false, _refs} ->
195211
config
196212
end
197213
end
198214

199-
defp maybe_add_console(refs, {:ok, %{module: module} = config}) when map_size(refs) == 0 do
200-
:logger.add_handler(:default, module, config)
215+
defp maybe_revert_to_default_handler(refs, status) when map_size(refs) == 0 do
216+
:logger.remove_handler(@name)
217+
218+
with {:ok, %{module: module} = config} <- status do
219+
:logger.add_handler(:default, module, config)
220+
end
201221
end
202222

203-
defp maybe_add_console(_refs, _config) do
223+
defp maybe_revert_to_default_handler(_refs, _config) do
204224
:ok
205225
end
226+
227+
## :logger handler callback.
228+
229+
def log(event, %{} = config) do
230+
%{formatter: {formatter_mod, formatter_config}} = config
231+
chardata = formatter_mod.format(event, formatter_config)
232+
233+
for [string_io, level] <- :ets.match(@ets, {:_, :"$1", :"$2"}),
234+
:logger.compare_levels(event.level, level) in [:gt, :eq] do
235+
:ok = IO.write(string_io, chardata)
236+
end
237+
end
206238
end

lib/ex_unit/test/ex_unit/capture_log_test.exs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,39 @@ defmodule ExUnit.CaptureLogTest do
7878
end
7979
end
8080

81-
test "with_log" do
82-
{4, log} =
83-
with_log(fn ->
84-
Logger.error("calculating...")
85-
2 + 2
86-
end)
81+
describe "with_log/2" do
82+
test "returns the result and the log" do
83+
{result, log} =
84+
with_log(fn ->
85+
Logger.error("calculating...")
86+
2 + 2
87+
end)
88+
89+
assert result == 4
90+
assert log =~ "calculating..."
91+
end
92+
93+
test "respects the :format, :metadata, and :colors options" do
94+
options = [format: "$metadata| $message", metadata: [:id], colors: [enabled: false]]
95+
96+
assert {4, log} =
97+
with_log(options, fn ->
98+
Logger.info("hello", id: 123)
99+
2 + 2
100+
end)
87101

88-
assert log =~ "calculating..."
102+
assert log == "id=123 | hello"
103+
end
89104
end
90105

91106
defp wait_capture_removal() do
92-
case :gen_event.which_handlers(Logger) do
93-
[Logger.Backends.Config] ->
94-
:ok
95-
96-
_otherwise ->
107+
case Enum.map(:logger.get_handler_config(), & &1.id) do
108+
[ExUnit.CaptureServer] ->
97109
Process.sleep(20)
98110
wait_capture_removal()
111+
112+
[:default] ->
113+
:ok
99114
end
100115
end
101116
end

0 commit comments

Comments
 (0)