Skip to content

Commit 40dff7e

Browse files
committed
Support on_exit for registering callbacks in ExUnit.
1 parent d44d71c commit 40dff7e

File tree

3 files changed

+160
-1
lines changed

3 files changed

+160
-1
lines changed

lib/ex_unit/lib/ex_unit/callbacks.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ defmodule ExUnit.Callbacks do
143143
end
144144
end
145145

146+
@spec on_exit(term, (() -> term)) :: :ok
147+
def on_exit(ref \\ make_ref, callback) do
148+
send(Process.get(ExUnit.Runner.Pid), {self(), :register_on_exit_callback, ref, callback})
149+
receive do
150+
{:registered_on_exit_callback, ^ref} -> :ok
151+
end
152+
end
153+
146154
## Helpers
147155

148156
@doc false

lib/ex_unit/lib/ex_unit/runner.ex

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ defmodule ExUnit.Runner do
195195

196196
{test_pid, test_ref} =
197197
spawn_monitor(fn ->
198+
Process.put(ExUnit.Runner.Pid, self_pid)
198199
{us, test} =
199200
:timer.tc(fn ->
200201
case exec_test_setup(test, context) do
@@ -211,10 +212,24 @@ defmodule ExUnit.Runner do
211212
send self_pid, {self, :test_finished, %{test | time: us}}
212213
end)
213214

215+
loop_test(test, test_pid, test_ref, HashDict.new)
216+
end
217+
218+
defp loop_test(test, test_pid, test_ref, on_exit_callbacks) do
214219
receive do
220+
{^test_pid, :register_on_exit_callback, ref, callback} ->
221+
on_exit_callbacks = Dict.put(on_exit_callbacks, ref, callback)
222+
send(test_pid, {:registered_on_exit_callback, ref})
223+
loop_test(test, test_pid, test_ref, on_exit_callbacks)
215224
{^test_pid, :test_finished, test} ->
216-
test
225+
on_exit_state = exec_on_exit_callbacks(on_exit_callbacks)
226+
if nil?(test.state) do
227+
%{test | state: on_exit_state}
228+
else
229+
test
230+
end
217231
{:DOWN, ^test_ref, :process, ^test_pid, error} ->
232+
exec_on_exit_callbacks(on_exit_callbacks)
218233
%{test | state: {:failed, {{:EXIT, test_pid}, error, []}}}
219234
end
220235
end
@@ -249,6 +264,59 @@ defmodule ExUnit.Runner do
249264
end
250265
end
251266

267+
defp exec_on_exit_callbacks(callbacks) do
268+
{runner_pid, runner_monitor, callback_state} = Enum.reduce Dict.values(callbacks), {nil, nil, nil}, fn(callback, {runner_pid, runner_monitor, callback_state}) ->
269+
{runner_pid, runner_monitor} = ensure_alive_callback_runner(runner_pid, runner_monitor)
270+
send(runner_pid, {:run, self(), callback})
271+
receive do
272+
{^runner_pid, nil} -> {runner_pid, runner_monitor, callback_state}
273+
{^runner_pid, error} ->
274+
if nil?(callback_state) do
275+
{runner_pid, runner_monitor, error}
276+
else
277+
{runner_pid, runner_monitor, callback_state}
278+
end
279+
{:DOWN, ^runner_monitor, :process, ^runner_pid, error} ->
280+
if nil?(callback_state) do
281+
{nil, nil, {:failed, {{:EXIT, runner_pid}, error, []}}}
282+
else
283+
{nil, nil, callback_state}
284+
end
285+
end
286+
end
287+
288+
if is_pid(runner_pid) and Process.alive?(runner_pid) do
289+
send(runner_pid, :shutdown)
290+
receive do
291+
{:DOWN, ^runner_monitor, :process, ^runner_pid, _error} -> :ok
292+
end
293+
end
294+
295+
callback_state
296+
end
297+
298+
defp ensure_alive_callback_runner(nil, nil), do: spawn_monitor(__MODULE__, :on_exit_runner_loop, [])
299+
defp ensure_alive_callback_runner(runner_pid, runner_monitor), do: {runner_pid, runner_monitor}
300+
301+
def on_exit_runner_loop do
302+
receive do
303+
{:run, from, fun} ->
304+
result = exec_on_exit_callback(fun)
305+
send(from, {self(), result})
306+
on_exit_runner_loop()
307+
:shutdown -> :ok
308+
end
309+
end
310+
311+
defp exec_on_exit_callback(callback) do
312+
callback.()
313+
nil
314+
catch
315+
kind, error ->
316+
{:failed, {kind, Exception.normalize(kind, error), pruned_stacktrace}}
317+
end
318+
319+
252320
## Helpers
253321

254322
defp shuffle(%{seed: 0}, list) do

lib/ex_unit/test/ex_unit/callbacks_test.exs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,89 @@ defmodule ExUnit.CallbacksTest do
132132
assert capture_io(fn -> ExUnit.run end) =~
133133
"** (MatchError) no match of right hand side value: :error"
134134
end
135+
136+
test "doesn't choke on on_exit errors" do
137+
defmodule OnExitErrorTest do
138+
use ExUnit.Case, async: false
139+
140+
test "ok" do
141+
on_exit fn ->
142+
:ok = error
143+
end
144+
145+
:ok
146+
end
147+
148+
defp error, do: :error
149+
end
150+
151+
ExUnit.configure(formatters: [ExUnit.CLIFormatter])
152+
153+
assert capture_io(fn -> ExUnit.run end) =~
154+
"** (MatchError) no match of right hand side value: :error"
155+
end
156+
157+
test "doesn't choke when on_exit exits" do
158+
defmodule OnExitExitTest do
159+
use ExUnit.Case, async: false
160+
161+
test "ok" do
162+
on_exit fn ->
163+
Process.exit(self(), :kill)
164+
end
165+
166+
:ok
167+
end
168+
end
169+
170+
ExUnit.configure(formatters: [ExUnit.CLIFormatter])
171+
assert capture_io(fn -> ExUnit.run end) =~
172+
">) killed"
173+
end
174+
175+
test "runs multiple on_exit exits and overrides by ref" do
176+
defmodule OnExitOverrideTest do
177+
use ExUnit.Case, async: false
178+
179+
setup do
180+
on_exit fn ->
181+
IO.puts "on_exit setup run"
182+
end
183+
184+
on_exit {:overridden, 1}, fn ->
185+
IO.puts "on_exit 1 overridden -> not run"
186+
end
187+
end
188+
189+
test "ok" do
190+
on_exit fn ->
191+
IO.puts "simple on_exit run"
192+
end
193+
194+
on_exit {:overridden, 2}, fn ->
195+
IO.puts "on_exit 2 overridden -> not run"
196+
end
197+
198+
on_exit {:overridden, 2}, fn ->
199+
IO.puts "on_exit 2 overrides -> run"
200+
end
201+
202+
on_exit {:overridden, 1}, fn ->
203+
IO.puts "on_exit 1 overrides -> run"
204+
end
205+
206+
:ok
207+
end
208+
end
209+
210+
ExUnit.configure(formatters: [ExUnit.CLIFormatter])
211+
captured_id = capture_io(fn -> ExUnit.run end)
212+
assert captured_id =~ "on_exit setup run"
213+
assert captured_id =~ "simple on_exit run"
214+
refute captured_id =~ "not run"
215+
assert captured_id =~ "on_exit 1 overrides -> run"
216+
assert captured_id =~ "on_exit 2 overrides -> run"
217+
end
135218
end
136219

137220
defmodule ExUnit.CallbacksNoTests do

0 commit comments

Comments
 (0)