The exit codes a sandbox mix run can end with, and what each one means.
A leaf on purpose: both halves of the exit-code contract read from here. The
producing half is Mutare.Sandbox.Command.Invocation — its watcher ASTs embed
timeout/0 and owner_lost/0 in the code the sandbox halts itself with — and the
decoding half is Mutare.Sandbox.Command (outcome/2), which refines
decode/1 with the run's output. Keeping the codes below both means neither
module needs the other to name a code.
The contract
0(success?/1) — every test passed: the mutation survived.failure/0— a test failed: the mutation was killed. Amix testexits with whatever--exit-statusit was given only on thefailures > 0path; every other failure (a compile error, a missing dependency, a brokentest_helper) exits with1(or a signal code). So forcing a distinctive--exit-statusis what separates a clean test failure from a harness error — they are no longer both "non-zero".timeout/0(timed_out?/1) — an injected watcher self-halted a run that overran its cap: a:timeoutfor a mutant (counted as a kill); for the one compile or the coverage probe, the overrun their callers name specially.owner_lost/0— the injected owner-death watcher self-halted a run whose spawning Mutare process died (Mutare.Sandbox.Command.Invocation.owner_watch_ast/0). Never decoded: by construction it is only ever exited with after the process that would read it is gone — it exists so the halt has a documented, recognisable code rather than an arbitrary one.sigkill/0(137=128 + 9) — the OS killed the run with SIGKILL. A:sigkilled: a harness error by verdict (the suite never reached one), but recognised by code because its dominant real-world cause — the kernel OOM killer reaping a mutant whose mutation made it allocate unboundedly — must not be retried back-to-back the way a transient harness error is (seeMutare.Runner).- anything else — the suite never returned a verdict: a
:harness_error, excluded from the score.
decode/1 is the single, total reading of the code alone. The two predicates exist
for the sandbox runs that are not mutant runs — the one compile, the baseline, the
coverage probe, the app-graph query — whose callers need only "did it succeed?" and
"was it the cap?", never the mutant verdict vocabulary; they read the same constants,
so the two readings cannot drift.
Summary
Functions
Decode a mutant-run exit status by its code alone — the single, total reading of
the contract in the moduledoc. Mutare.Sandbox.Command.outcome/2 refines the
:harness_error case with the run's output.
Exit code a clean ExUnit test failure is forced to (via mix test --exit-status), so a killed mutant is distinguishable from a harness error.
Exit code the owner-death watcher uses when a sandbox run halts itself because the Mutare process that spawned it died (its stdin pipe hit EOF).
Exit code of a run the OS killed with SIGKILL (137 = 128 + 9).
Whether status is the clean-success exit code (0).
Whether status is the code a self-halt watcher exits with past its cap.
Exit code the self-halt watchers use, signalling a run that overran its cap.
Types
@type decoded() :: :passed | :failed | :timeout | :sigkilled | :harness_error
Run outcomes classified by exit code alone (see decode/1).
Functions
@spec decode(non_neg_integer()) :: decoded()
Decode a mutant-run exit status by its code alone — the single, total reading of
the contract in the moduledoc. Mutare.Sandbox.Command.outcome/2 refines the
:harness_error case with the run's output.
@spec failure() :: non_neg_integer()
Exit code a clean ExUnit test failure is forced to (via mix test --exit-status), so a killed mutant is distinguishable from a harness error.
Chosen distinct from the codes a harness failure produces — 1 (a compile
error, a missing dep, a broken helper, a no-tests-matched), 2 (ExUnit's
default, were the flag ever dropped), timeout/0, and the 128 + signal
range — so only a genuine test failure ever lands on it.
@spec owner_lost() :: non_neg_integer()
Exit code the owner-death watcher uses when a sandbox run halts itself because the Mutare process that spawned it died (its stdin pipe hit EOF).
Nobody is left to decode it — the owner is gone — so unlike timeout/0 it has
no decode/1 branch; it is reserved here so the halt is documented and
distinguishable in e.g. a wrapper script's logs. Chosen outside the codes that
carry meaning elsewhere in the contract: 0, 1/2 (mix/ExUnit failures),
failure/0, timeout/0, and the 128 + signal range.
@spec sigkill() :: non_neg_integer()
Exit code of a run the OS killed with SIGKILL (137 = 128 + 9).
Unlike the other codes, nothing of Mutare's produces it — it is the kernel's,
and its signature real-world producer is the OOM killer reaping a mutant whose
mutation made it allocate without bound. Decoded to :sigkilled so the runner
can skip retries (repeating a deterministic allocation failure would exhaust memory again) and
point at the mitigation (:max_heap_mb).
@spec success?(non_neg_integer()) :: boolean()
Whether status is the clean-success exit code (0).
@spec timed_out?(non_neg_integer()) :: boolean()
Whether status is the code a self-halt watcher exits with past its cap.
@spec timeout() :: non_neg_integer()
Exit code the self-halt watchers use, signalling a run that overran its cap.