Skip to content

ExUnit - Documented test process life cycle does not match implementation #15236

@camilleryr

Description

@camilleryr

Elixir and Erlang/OTP versions

Elixir 1.19.5 (compiled with Erlang/OTP 28)

Operating system

Mac OS 26.2 (25C56)

Current behavior

From my experience and reading through the code - I believe the documented ExUnit test process life cycle is not respected by the current implementation. The steps that seem inverted are

4. it stops all supervised processes
5. the test process exits with reason :shutdown

I believe these blocks of code show the current issue

defp spawn_test_monitor(
%{seed: seed, capture_log: capture_log, rand_algorithm: rand_algorithm},
test,
parent_pid,
context
) do
spawn_monitor(fn ->
Process.set_label({test.case, test.name})
ExUnit.OnExitHandler.register(self())
generate_test_seed(seed, test, rand_algorithm)
context = context |> Map.merge(test.tags) |> Map.put(:test_pid, self())
capture_log = Map.get(context, :capture_log, capture_log)
{time, test} =
:timer.tc(
maybe_capture_log(capture_log, test, fn ->
context = maybe_create_tmp_dir(context, test)
case exec_test_setup(test, context) do
{:ok, context} -> exec_test(test, context)
{:error, test} -> test
end
end)
)
send(parent_pid, {self(), :test_finished, %{test | time: time}})
exit(:shutdown)
end)
end

defp spawn_test(config, test, context) do
parent_pid = self()
timeout = get_timeout(config, test.tags)
{test_pid, test_ref} = spawn_test_monitor(config, test, parent_pid, context)
test = receive_test_reply(test, test_pid, test_ref, timeout)
exec_on_exit(test, test_pid, timeout)
end

defp exec_on_exit(test_or_case, pid, timeout) do
case ExUnit.OnExitHandler.run(pid, timeout) do
:ok ->
test_or_case
{kind, reason, stack} ->
state = test_or_case.state || failed(kind, reason, prune_stacktrace(stack))
%{test_or_case | state: state}
end
end

def run(pid, timeout) when is_pid(pid) do
[{^pid, sup, callbacks}] = :ets.take(@name, pid)
error = terminate_supervisor(sup, timeout)
exec_on_exit_callbacks(Enum.reverse(callbacks), timeout, error)
end

My understanding of this is that

1 - The Runner spawns a process to run the test
2 - On Complete the test processes sends a message back to the runner to alert it that has completed the test
ASYNC
3 A - The Runner receives the message from the test pid and shuts down the supervised processes
3 B - The test process exits

I have created a minimal reproduction of this problem that can be found in this repo

In my opinion it would be ideal to respect the currently documented test process lifecycle - but if that creates too large of a change to existing behavior it would be great if there was a way to opt into a more strict ordering. If the core team agrees that this should be addressed, I would be happy to work up a pr or in any other way provide whatever support I can

As always, appreciate everything the core team and community do!

Expected behavior

From the ExUnit docs

Here is a rundown of the life cycle of the test process:

the test process is spawned
it runs setup/2 callbacks
it runs the test itself
it stops all supervised processes
the test process exits with reason :shutdown
on_exit/2 callbacks are executed in a separate process

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions