Skip to content

Integrate start_supervised supervisor with :$caller #13253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/ex_unit/lib/ex_unit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,7 @@ defmodule ExUnit do
def fetch_test_supervisor() do
case ExUnit.OnExitHandler.get_supervisor(self()) do
{:ok, nil} ->
opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
{:ok, sup} = Supervisor.start_link([], opts)
{:ok, sup} = ExUnit.OnExitHandler.Supervisor.start_link([])
ExUnit.OnExitHandler.put_supervisor(self(), sup)
{:ok, sup}

Expand Down
19 changes: 14 additions & 5 deletions lib/ex_unit/lib/ex_unit/callbacks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,13 @@ defmodule ExUnit.Callbacks do
See the `Supervisor` module for a discussion on child specifications
and the available specification keys.

The started process is not linked to the test process and a crash will
not necessarily fail the test. To start and link a process to guarantee
that any crash would also fail the test use `start_link_supervised!/2`.

This function returns `{:ok, pid}` in case of success, otherwise it
returns `{:error, reason}`.

The advantage of starting a process under the test supervisor is that
it is guaranteed to exit before the next test starts. Therefore, you
don't need to remove the process at the end of your tests via
Expand All @@ -528,12 +535,14 @@ defmodule ExUnit.Callbacks do
test, as simply shutting down the process would cause it to be restarted
according to its `:restart` value.

The started process is not linked to the test process and a crash will
not necessarily fail the test. To start and link a process to guarantee
that any crash would also fail the test use `start_link_supervised!/2`.
Another advantage is that the test process will act as both an ancestor
as well as a caller to supervised processes, which can be useful for certain
usecases, which need to relate some functionality to the test starting the
process. `$ancestors` is populated as expected given the supervisor is
started by the test process. Populating `$callers` doesn't happen by default
when starting a process as a child of a supervisor, but happens as part of
`start_supervised/2` out of convenience.

This function returns `{:ok, pid}` in case of success, otherwise it
returns `{:error, reason}`.
"""
@doc since: "1.5.0"
@spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) ::
Expand Down
25 changes: 25 additions & 0 deletions lib/ex_unit/lib/ex_unit/on_exit_handler/supervisor.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule ExUnit.OnExitHandler.Supervisor do
@moduledoc false
use Supervisor

def start_link(children) do
Supervisor.start_link(__MODULE__, {children, get_callers(self())})
end

@impl true
def init({children, callers}) do
put_callers(callers)
Supervisor.init(children, strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1)
end

defp get_callers(owner) do
case :erlang.get(:"$callers") do
[_ | _] = list -> [owner | list]
_ -> [owner]
end
end

defp put_callers(callers) do
:erlang.put(:"$callers", callers)
end
end
20 changes: 20 additions & 0 deletions lib/ex_unit/test/ex_unit/supervised_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ defmodule ExUnit.SupervisedTest do
end
end

test "starts a supervised process with correct :\"$callers\"" do
test_pid = self()
fun = fn -> send(test_pid, {:callers, Process.get(:"$callers")}) end
{:ok, _pid} = start_supervised({Task, fun})

assert_received {:callers, callers}

assert List.last(callers) == test_pid
end

test "starts a supervised process with correct :\"$ancestors\"" do
test_pid = self()
fun = fn -> send(test_pid, {:ancestors, Process.get(:"$ancestors")}) end
{:ok, _pid} = start_supervised({Task, fun})

assert_received {:ancestors, ancestors}

assert List.last(ancestors) == test_pid
end

test "stops a supervised process" do
{:ok, pid} = start_supervised({MyAgent, 0})
assert stop_supervised(MyAgent) == :ok
Expand Down