Skip to content

Commit d3b6f80

Browse files
authored
Add start_supervised/2 and stop_supervised/1 to ExUnit (#6193)
1 parent e2b48ef commit d3b6f80

File tree

7 files changed

+285
-65
lines changed

7 files changed

+285
-65
lines changed

lib/elixir/test/elixir/registry_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ defmodule RegistryTest do
99
partitions = config[:partitions] || 1
1010
listeners = List.wrap(config[:listener])
1111
opts = [keys: keys, name: config.test, partitions: partitions, listeners: listeners]
12-
{:ok, sup} = Supervisor.start_link([{Registry, opts}], strategy: :one_for_one)
13-
{:ok, %{registry: config.test, partitions: partitions, sup: sup}}
12+
{:ok, _} = start_supervised({Registry, opts})
13+
{:ok, %{registry: config.test, partitions: partitions}}
1414
end
1515

1616
for {describe, partitions} <- ["with 1 partition": 1, "with 8 partitions": 8] do

lib/ex_unit/lib/ex_unit/callbacks.ex

Lines changed: 116 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,43 @@ defmodule ExUnit.Callbacks do
33
Defines ExUnit callbacks.
44
55
This module defines both `setup_all` and `setup` callbacks, as well as
6-
the `on_exit/2` function.
6+
the `on_exit/2`, `start_supervised/2` and `stop_supervised/1` functions.
77
88
The setup callbacks are defined via macros and each one can optionally
99
receive a map with metadata, usually referred to as `context`. The
1010
callback may optionally put extra data into the `context` to be used in
1111
the tests.
1212
13-
The `setup_all` callbacks are invoked only once to setup the test case before any
14-
test is run and all `setup` callbacks are run before each test. No callback
13+
The `setup_all` callbacks are invoked only once per module, before any
14+
test runs. All `setup` callbacks are run before each test. No callback
1515
runs if the test case has no tests or all tests have been filtered out.
1616
17+
`start_supervised/2` is used to start processes under a supervisor. The
18+
supervisor is linked to the current test process. The supervisor as well
19+
as all child processes are guaranteed to terminate before any `on_exit/2`
20+
callback runs.
21+
1722
`on_exit/2` callbacks are registered on demand, usually to undo an action
1823
performed by a setup callback. `on_exit/2` may also take a reference,
1924
allowing callback to be overridden in the future. A registered `on_exit/2`
2025
callback always runs, while failures in `setup` and `setup_all` will stop
2126
all remaining setup callbacks from executing.
2227
23-
Finally, `setup_all` callbacks run in the test case process, while all
24-
`setup` callbacks run in the same process as the test itself. `on_exit/2`
25-
callbacks always run in a separate process than the test case or the
26-
test itself. Since the test process exits with reason `:shutdown`, most
27-
of times `on_exit/2` can be avoided as processes are going to clean
28-
up on their own.
28+
Finally, `setup_all` callbacks run in a separate process per module, while
29+
all `setup` callbacks run in the same process as the test itself. `on_exit/2`
30+
callbacks always run in a separate process, as implied by their name. The
31+
test process always exits with reason `:shutdown`, which means any process
32+
linked to the test process will also exit, although asynchronously. Therefore
33+
it is preferred to use `start_supervised/2` to guarantee synchronous termination.
34+
35+
Here is a run down of the life-cycle of the test process:
36+
37+
1. the test process is spawned
38+
2. it runs `setup/2` callbacks
39+
3. it runs the test itself
40+
4. it stops all supervised processes
41+
5. the test process exits with reason `:shutdown`
42+
6. `on_exit/2` callbacks are executed in a separate process
2943
3044
## Context
3145
@@ -49,41 +63,40 @@ defmodule ExUnit.Callbacks do
4963
defmodule AssertionTest do
5064
use ExUnit.Case, async: true
5165
52-
# "setup_all" is called once to setup the case before any test is run
66+
# "setup_all" is called once per module before any test runs
5367
setup_all do
5468
IO.puts "Starting AssertionTest"
5569
5670
# No context is returned here
5771
:ok
5872
end
5973
60-
# "setup" is called before each test is run
74+
# "setup" is called before each test
6175
setup do
62-
IO.puts "This is a setup callback"
76+
IO.puts "This is a setup callback for #{inspect self()}"
6377
6478
on_exit fn ->
65-
IO.puts "This is invoked once the test is done"
79+
IO.puts "This is invoked once the test is done. Process: #{inspect self()}"
6680
end
6781
6882
# Returns extra metadata to be merged into context
6983
[hello: "world"]
7084
end
7185
72-
# Same as "setup", but receives the context
73-
# for the current test
86+
# Same as above, but receives the context as argument
7487
setup context do
75-
IO.puts "Setting up: #{context[:test]}"
88+
IO.puts "Setting up: #{context.test}"
7689
:ok
7790
end
7891
79-
# Setups can also invoke a local or imported function that can return a context
92+
# Setups can also invoke a local or imported function that returns a context
8093
setup :invoke_local_or_imported_function
8194
8295
test "always pass" do
8396
assert true
8497
end
8598
86-
test "another one", context do
99+
test "uses metadata from setup", context do
87100
assert context[:hello] == "world"
88101
end
89102
@@ -198,15 +211,16 @@ defmodule ExUnit.Callbacks do
198211
end
199212

200213
@doc """
201-
Defines a callback that runs on the test (or test case) exit.
214+
Defines a callback that runs once the test exits.
202215
203216
`callback` is a function that receives no arguments and
204217
runs in a separate process than the caller.
205218
206-
`on_exit/2` is usually called from `setup` and `setup_all` callbacks,
207-
often to undo the action performed during `setup`. However, `on_exit/2`
208-
may also be called dynamically, where a reference can be used to
209-
guarantee the callback will be invoked only once.
219+
`on_exit/2` is usually called from `setup` and `setup_all`
220+
callbacks, often to undo the action performed during `setup`.
221+
However, `on_exit/2` may also be called dynamically, where a
222+
reference can be used to guarantee the callback will be invoked
223+
only once.
210224
"""
211225
@spec on_exit(term, (() -> term)) :: :ok | no_return
212226
def on_exit(name_or_ref \\ make_ref(), callback) when is_function(callback, 0) do
@@ -217,6 +231,85 @@ defmodule ExUnit.Callbacks do
217231
end
218232
end
219233

234+
@supervisor_opts [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
235+
236+
@doc """
237+
Starts a child process under the test supervisor.
238+
239+
It expects a child specification or a module, similar to the ones
240+
given to `Supervisor.start_link/2`. For example, if your application
241+
starts a supervision tree by running:
242+
243+
Supervisor.start_link([MyServer, {OtherSupervisor, ...}], ...)
244+
245+
You can start those processes under test in isolation by running:
246+
247+
start_supervised(MyServer)
248+
start_supervised({OtherSupervisor, :initial_value})
249+
250+
A keyword list can also be given if there is a need to change
251+
the child specification for the given child process:
252+
253+
start_supervised({MyServer, :initial_value}, restart: :temporary)
254+
255+
See the `Supervisor` module for a discussion on child specifications
256+
and the available specification keys.
257+
258+
The advantage of starting a process under the test supervisor is that
259+
it is guaranteed to exit before the next test starts. Furthermore,
260+
because the child process is supervised, it will be restarted in case
261+
of crashes according to the `:restart` strategy in the child
262+
specification, even if stopped manually. Therefore, to guarantee a
263+
process started with `start_supervised/2` terminates without restarts,
264+
see `stop_supervised/1`.
265+
266+
This function returns `{:ok, pid}` in case of success, otherwise it
267+
returns `{:error, reason}`.
268+
"""
269+
@spec start_supervised(Supervisor.child_spec | module | {module, term}, keyword) ::
270+
Supervisor.on_start_child
271+
def start_supervised(child_spec_or_module, opts \\ []) do
272+
sup =
273+
case ExUnit.OnExitHandler.get_supervisor(self()) do
274+
{:ok, nil} ->
275+
{:ok, sup} = Supervisor.start_link([], @supervisor_opts)
276+
ExUnit.OnExitHandler.put_supervisor(self(), sup)
277+
sup
278+
{:ok, sup} ->
279+
sup
280+
:error ->
281+
raise ArgumentError, "start_supervised/2 can only be invoked from the test process"
282+
end
283+
284+
Supervisor.start_child(sup, Supervisor.child_spec(child_spec_or_module, opts))
285+
end
286+
287+
@doc """
288+
Stops a child process started via `start_supervised/2`.
289+
290+
This function expects the `id` in the child specification.
291+
For example:
292+
293+
{:ok, _} = start_supervised(MyServer)
294+
:ok = stop_supervised(MyServer)
295+
296+
It returns `:ok` if there is a supervised process with such
297+
`id`, `{:error, :not_found}` otherwise.
298+
"""
299+
@spec stop_supervised(id :: term()) :: :ok | {:error, :not_found}
300+
def stop_supervised(id) do
301+
case ExUnit.OnExitHandler.get_supervisor(self()) do
302+
{:ok, nil} ->
303+
{:error, :not_found}
304+
{:ok, sup} ->
305+
with :ok <- Supervisor.terminate_child(sup, id),
306+
:ok <- Supervisor.delete_child(sup, id),
307+
do: :ok
308+
:error ->
309+
raise ArgumentError, "stop_supervised/1 can only be invoked from the test process"
310+
end
311+
end
312+
220313
## Helpers
221314

222315
@reserved [:case, :file, :line, :test, :async, :registered, :describe]
@@ -238,7 +331,6 @@ defmodule ExUnit.Callbacks do
238331
merge(mod, context, value, value)
239332
end
240333

241-
@doc false
242334
defp merge(_mod, context, :ok, _original_value) do
243335
context
244336
end

lib/ex_unit/lib/ex_unit/case.ex

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ end
44

55
defmodule ExUnit.Case do
66
@moduledoc """
7-
Sets up an ExUnit test case.
7+
Helpers for defining test cases.
88
99
This module must be used in other modules as a way to configure
1010
and prepare them for testing.
@@ -16,8 +16,10 @@ defmodule ExUnit.Case do
1616
does not change any global state. Defaults to `false`.
1717
1818
This module automatically includes all callbacks defined in
19-
`ExUnit.Callbacks`. See that module's documentation for more
20-
information.
19+
`ExUnit.Callbacks`. See that module for more information on `setup`,
20+
`start_supervised`, `on_exit` and the test process lifecycle.
21+
22+
For grouping tests together, see `describe/2` in this module.
2123
2224
## Examples
2325
@@ -41,7 +43,7 @@ defmodule ExUnit.Case do
4143
4244
setup do
4345
{:ok, pid} = KV.start_link
44-
{:ok, [pid: pid]}
46+
{:ok, pid: pid}
4547
end
4648
4749
test "stores key-value pairs", context do
@@ -183,20 +185,21 @@ defmodule ExUnit.Case do
183185
184186
## Log Capture
185187
186-
ExUnit can optionally suppress printing of log messages that are generated during a test. Log
187-
messages generated while running a test are captured and only if the test fails are they printed
188-
to aid with debugging.
188+
ExUnit can optionally suppress printing of log messages that are generated
189+
during a test. Log messages generated while running a test are captured and
190+
only if the test fails are they printed to aid with debugging.
189191
190-
You can opt into this behaviour for individual tests by tagging them with `:capture_log` or enable
191-
log capture for all tests in the ExUnit configuration:
192+
You can opt into this behaviour for individual tests by tagging them with
193+
`:capture_log` or enable log capture for all tests in the ExUnit configuration:
192194
193195
ExUnit.start(capture_log: true)
194196
195-
This default can be overridden by `@tag capture_log: false` or `@moduletag capture_log: false`.
197+
This default can be overridden by `@tag capture_log: false` or
198+
`@moduletag capture_log: false`.
196199
197-
Since `setup_all` blocks don't belong to a specific test, log messages generated in them (or
198-
between tests) are never captured. If you want to suppress these messages as well, remove the
199-
console backend globally:
200+
Since `setup_all` blocks don't belong to a specific test, log messages generated
201+
in them (or between tests) are never captured. If you want to suppress these
202+
messages as well, remove the console backend globally:
200203
201204
config :logger, backends: []
202205
"""

0 commit comments

Comments
 (0)