# `Mutare.Sandbox.Command.Invocation`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.2.1/lib/mutare/sandbox/command/invocation.ex#L1)

Spawn `mix` against a materialised sandbox as a fresh OS process.

Every mutant is exercised by its own `mix test` process: the sources never
change between runs, so mix's incremental compiler finds nothing to rebuild
and the per-mutant cost is process boot plus the suite. `MIX_ENV=test` and
the variables selecting the mutant (`Mutare.Selector.environment/1`) are always
set; the rest of a run's environment comes from named **run options**
(`t:run_opts/0`) — an optional wall-clock `:cap` (a mutation can turn a
terminating loop infinite), the compile's own cap and compiler switches, the
coverage probe's capture vars, the heap cap, and the per-worker partition entry.

This module implements run invocation: the environment it
runs under (`environment/2`, the one builder every sandbox `mix` goes through;
`mix_env/0`; the self-hosting isolation vars), the raw spawn
(`mix/4`/`timed_mix/4`), and the **timeout enforcement mechanism** — the env var
used to pass the cap (`timeout_env/0`) and the dependency-free watcher AST
(`watcher_ast/0`) that `Mutare.Sandbox` renders into the target's test bootstrap.
The matching half — *decoding* what a run did from its exit code — is
`Mutare.Sandbox.Command`; the codes both halves share are the leaf
`Mutare.Sandbox.Command.Exit`, and `Mutare.Sandbox.Command.timed_test/4`
composes the two (run here, decode there).

## The reserved variable set is the builder's key set

Every variable Mutare sets on a sandbox `mix` is emitted by `environment/2` —
there is no raw env passthrough — so `reserved_env_names/0` is *derived* from it
(the builder run with every option armed), not maintained alongside it. A new run
option cannot be forgotten from the reserved set, because the set is whatever the
builder emits. The one key deliberately outside it is the `:partition` entry: its
name is the user's `:partition_env`, appended last, and the reserved set is what
`Mutare.Options` validates that name against.

## Self-halt watchers

The cap is not enforced by killing a process tree (which needs platform-specific
signals); instead the watcher reads `timeout_env/0` and, after the deadline,
`System.halt/1`s the run itself with `Mutare.Sandbox.Command.Exit.timeout/0`.

A second, structurally identical self-halt guards against the opposite failure:
the *owner* dying rather than the run overrunning. Every run's stdin is a pipe
whose write end only the spawning Mutare process holds, so if that process dies
— however abruptly — the pipe hits EOF. The owner-death watcher
(`owner_watch_ast/0`, gated by `owner_watch_env/0`, rendered by `Mutare.Sandbox`
into the sandbox's config and test bootstrap) blocks reading stdin and halts the
run with `Mutare.Sandbox.Command.Exit.owner_lost/0` as soon as EOF is read,
so no sandbox `mix` outlives the run that spawned it.

# `run_opts`

```elixir
@type run_opts() :: [
  cap: pos_integer() | nil,
  compile_cap: pos_integer() | nil,
  compile: boolean(),
  coverage: {Path.t(), Path.t()} | nil,
  max_heap_mb: pos_integer() | nil,
  partition: [{String.t(), String.t()}]
]
```

The named run options every sandbox `mix` is invoked with. Each one maps to the
environment entries `environment/2` emits for it:

  * `:cap` (ms, or `nil`) — the run's wall-clock cap, handed to the injected timeout
    watcher (`timeout_env/0`), which halts the run itself if it overruns.
  * `:compile_cap` (ms, or `nil`) — the **one metamutant compile**'s wall-clock cap
    (`compile_timeout_env/0`), armed only on that invocation.
  * `:compile` (boolean) — the compiler switches that speed the one compile
    (`Mutare.Sandbox.CompilerOptions.compiler_env/0`).
  * `:coverage` (`{dump_path, root}`, or `nil`) — the coverage probe's capture
    flag, dump path and path-normalisation root (`Mutare.Coverage.Recorder`).
  * `:max_heap_mb` (MB, or `nil`) — the per-process heap cap (`heap_cap_env/1`).
  * `:partition` (`[{name, id}]` or `[]`) — the user-named partition entry
    (`Mutare.Runner.Partitions.entry/2`), appended last.

Every option is optional. An absent `:cap`, `:compile_cap` or `:coverage` clears
its variables explicitly rather than emitting nothing, so a value inherited from
Mutare's own environment cannot enable a watcher or probe omitted from this invocation's
options. The remaining absent options emit nothing.

# `compile_timeout_env`

```elixir
@spec compile_timeout_env() :: String.t()
```

Env var the runner sets to give the **one metamutant compile** its wall-clock
cap (ms).

A sibling of `timeout_env/0` with its own name on purpose: the watcher that
reads it (`compile_watcher_ast/0`) lives in the sandbox `config/config.exs`
prefix, which mix evaluates on *every* sandbox boot — so it must be armed only
when `Mutare.Runner` sets this variable on the compile invocation, and stay
inert on the baseline, the coverage probe, and every per-mutant `mix test`
(whose cap is `timeout_env/0`, armed from the test bootstrap instead).

# `compile_watcher_ast`

```elixir
@spec compile_watcher_ast() :: Macro.t()
```

Dependency-free watcher that enforces the one metamutant compile's wall-clock cap.

The same self-halt watcher as `watcher_ast/0`, armed by `compile_timeout_env/0`
instead: `Mutare.Sandbox` renders it into the sandbox's `config/config.exs`
prefix (mix evaluates config before the compilers run, the same property the
owner-death watcher uses), and `Mutare.Runner` sets the variable only on the
compile invocation — so a thrashing compile halts itself with
`Mutare.Sandbox.Command.Exit.timeout/0` instead of blocking the run
indefinitely, and every other sandbox boot evaluates the watcher inert.

# `environment`

```elixir
@spec environment(Mutare.RuntimeId.t(), run_opts()) :: [
  {String.t(), String.t() | nil}
]
```

The full environment a sandbox `mix` for `mutant_id` runs under, given its run
options (`t:run_opts/0`) — the one builder `mix/4` uses and `reserved_env_names/0`
derives from.

Always present: `MIX_ENV`, the owner-death watcher's arming variable, the two
self-hosting isolation variables, and the selector variables for `mutant_id`. Then
one group per armed option, in `t:run_opts/0` order, and the `:partition` entry
last. Pure apart from reading Mutare's *own* environment where an entry merges
with an inherited value (`heap_cap_env/1`, `CompilerOptions.compiler_env/0`).

# `heap_cap_env`

```elixir
@spec heap_cap_env(pos_integer() | nil) :: [{String.t(), String.t()}]
```

The env entry that caps every BEAM process's heap in a sandbox run, or `[]`
when `mb` is `nil` (the `:max_heap_mb` default — no cap).

The cap rides in `ELIXIR_ERL_OPTIONS` as `+hmax <words>` (the emulator's
default per-process `max_heap_size`, which **kills the offending process**
when exceeded). This is the memory analogue of the wall-clock watcher, and
like it needs nothing platform-specific: no cgroups, no `ulimit`, no process
tree to hunt down. A mutation that makes code allocate without bound (the
motivating incident: a dropped guard turning a function unconditionally
self-recursive, ~25GB RSS in under a second, OOM-killed) then dies as an
ordinary, fast, attributable test failure inside the run — the growing heap
belongs to the test process exercising the mutant — instead of racing the
kernel's OOM killer for the whole host.

A pre-existing `ELIXIR_ERL_OPTIONS` in Mutare's own environment is preserved
and the cap appended after it (later emulator flags win), so a user's flags
survive with the cap applied on top.

One limitation: `max_heap_size` counts the process *heap* — lists, tuples,
maps, small binaries (the incident's growth shape, and the common one for
runaway recursion). Large (refc) binaries live off-heap and are not counted,
so a pure binary-append runaway is not contained by this cap.

Not applied to the one metamutant compile: the metamutant is ~25× the source
and the compiler's per-process memory is legitimately large — a cap sized for
the suite's runtime could sink the build. The runtime runs (baseline, coverage
probe, every per-mutant `mix test`) all get it — the baseline doubles as
validation that the suite itself fits under the cap, so a too-small value
surfaces as a red baseline up front rather than as false kills mid-run.

# `mix`

```elixir
@spec mix(Path.t(), [String.t()], Mutare.RuntimeId.t(), run_opts()) ::
  {String.t(), non_neg_integer()}
```

Run `mix <args>` in `sandbox` as a fresh OS process, returning
`{output, exit_status}`.

The environment is `environment/2` of `mutant_id` and `opts` (see `t:run_opts/0`).
`mutant_id` is a runtime identity (`Mutare.Selector.baseline/0` for a baseline
run); for a schema mutant, `Selector.environment/1` splits its `{file, local_id}`
into the namespace and integer environment variables, and integer calls clear any
inherited namespace.

# `mix_env`

```elixir
@spec mix_env() :: String.t()
```

The `MIX_ENV` every sandbox `mix` runs under (`"test"`). The single home for the value,
so `Mutare.Sandbox` (which builds `_build/<env>/lib` paths) and `Mutare.Transform.Uses`
share it rather than re-hardcoding the string.

# `owner_watch_ast`

```elixir
@spec owner_watch_ast() :: Macro.t()
```

Dependency-free watcher that halts a sandbox run whose owner died.

Reads `owner_watch_env/0`: unset (a manual run in a kept sandbox, CI with a
closed stdin) it is inert, otherwise it spawns a process that blocks reading
stdin and, on `:eof`, `System.halt/1`s the run with
`Mutare.Sandbox.Command.Exit.owner_lost/0`. Under `mix/4` the run's stdin is a
pipe whose write end only the owning Mutare process holds; when that process
dies — clean exit, crash, or SIGKILL (the kernel closes its descriptors) — the
pipe hits EOF and the run reaps itself, instead of surviving re-parented (a
compute-bound `mix` never touches stdout, so it would otherwise run on
untouched). `Mutare.Sandbox` renders this AST into the sandbox's
project prefix, `config/config.exs` (mix evaluates config before compiling, so the one-time
metamutant compile and every run's boot phase are covered) and into the test
bootstrap alongside `watcher_ast/0` (covering suites under a config layout the
config injection can't reach). Like the timeout watcher, it needs nothing
platform-specific and no dependency on Mutare — the same self-halt primitive
pointed at a second hazard. Only the first armed evaluation starts a watcher.

No data is sent on stdin under `mix/4` (Mutare writes nothing to the pipe),
so the watcher simply re-blocks on anything that isn't `:eof`; a target suite
that reads stdin receives the same input as without the watcher —
a silent, open pipe.

# `owner_watch_env`

```elixir
@spec owner_watch_env() :: String.t()
```

Env var that arms the owner-death watcher (`owner_watch_ast/0`).

Set by `mix/4` on every sandbox run, and only there: the watcher halts the run
the moment stdin hits EOF, which is the owner-died signal *only* when stdin is
the spawning process's pipe. A sandbox `mix` run by hand (or by CI with stdin
at `/dev/null`) must stay unaffected, so the watcher is inert unless this
variable is set.

# `reserved_env_names`

```elixir
@spec reserved_env_names() :: [String.t()]
```

The environment variable names Mutare itself sets on a sandbox `mix`: the keys of
`environment/2` with every run option armed (`:partition` excepted — its name is
the user's, and this set is what it is validated against).

Derived, not listed: a run option added to `environment/2` is reserved by
construction. `Mutare.Options` rejects a `:partition_env` that collides with one
of these, since the partition entry is appended to the environment and a
duplicate key's resolution is unspecified (it would silently clobber e.g.
`MIX_ENV`).

# `timed_mix`

```elixir
@spec timed_mix(Path.t(), [String.t()], Mutare.RuntimeId.t(), run_opts()) ::
  {non_neg_integer(), String.t(), non_neg_integer()}
```

Like `mix/4`, but wall-clock-timed: returns `{elapsed_ms, output, exit_status}`.
`opts` are the run options (`t:run_opts/0`), passed straight through.

# `timeout_env`

```elixir
@spec timeout_env() :: String.t()
```

Env var the runner sets to give a mutant run its wall-clock cap (ms).

# `watcher_ast`

```elixir
@spec watcher_ast() :: Macro.t()
```

Dependency-free watcher that enforces a mutant run's wall-clock cap.

Reads `timeout_env/0`: with no cap it is inert, otherwise it spawns a process
that sleeps for the cap and then `System.halt/1`s the run with
`Mutare.Sandbox.Command.Exit.timeout/0` — so the run halts *itself* and there is
no process tree to kill. `Mutare.Sandbox` renders this AST into the target
project prefix and test-bootstrap fallback, alongside `Mutare.Selector.bootstrap_ast/0`,
so the target needs nothing platform-specific and no dependency on Mutare.
The first armed evaluation starts the deadline; later prefixes cannot reset it.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
