Materialise a schema as a runnable copy of the target project.
We copy the project to a dedicated directory (excluding build output), write
each metamutant over its original, and inject a tiny bootstrap into
test/test_helper.exs that reads MUTARE_ACTIVE_MUTANT into :persistent_term
before the suite starts. A second injection prefixes config/config.exs with
the owner-death watcher (Mutare.Sandbox.Command.Invocation.owner_watch_ast/0),
so every sandbox mix — including the one-time compile, which runs before any
test bootstrap — halts itself if the Mutare process that spawned it dies,
instead of surviving as an orphan. Everything injected is plain Erlang/Elixir
with no dependency on Mutare, so the sandbox needs nothing added to its deps.
One copy serves every concurrent mutant run: they share this sandbox and its
_build, and Mix's build lock serialises only their --no-compile boot check —
a small, fixed fraction of a run (measured in NOTES.md,
"Per-worker MIX_BUILD_PATH vs the shared sandbox build") — so there is no
per-worker isolation to configure.
Two materialisation modes, chosen by :keep_sandbox:
- kept (default,
keep_sandbox: true) — the sandbox (and its compiled_build) is preserved between runs and re-materialised in place: a file is rewritten only when its desired content differs (unchanged files keep their mtime, so mix's incremental compiler reuses_build), and files Mutare no longer owns are pruned. The mirror carries each source's permission mode and recreates its symlinks (never following them), matching what the fresh copy preserves. Without an explicit:sandboxit lives at a stable per-project temp dir; CI pins:sandboxat a cached directory instead. SeeNOTES.mdfor the cache pattern. - fresh (
keep_sandbox: false) — a throwaway dir is wiped and re-copied every run, so the metamutant recompiles cold, and the runner removes it afterwards. Always correct, no caching — the reset for a kept sandbox that has gone bad.
Materialising the workspace lives here; running mix against it (and the
per-mutant timeout cap the bootstrap honours) lives in
Mutare.Sandbox.Command.Invocation.
Summary
Functions
The bootstrap snippet prepended to the sandbox's test helper.
Prepare a sandbox for schema taken from root. Returns the sandbox path.
Re-render schema's metamutants into an already-prepared sandbox, in place.
Functions
@spec bootstrap() :: String.t()
The bootstrap snippet prepended to the sandbox's test helper.
@spec prepare( Path.t(), Mutare.Schema.t(), Mutare.Run.Context.t() | Mutare.Options.t() | keyword() ) :: Path.t()
Prepare a sandbox for schema taken from root. Returns the sandbox path.
opts may be a Mutare.Run.Context, a Mutare.Options struct, or a keyword
list. :sandbox selects the target directory; without it Mutare uses a stable
per-project temp directory (kept mode, the default) or a fresh one (keep_sandbox: false). The context's project scope controls which umbrella apps are
materialized.
The sandbox must be separate from the project tree: it may not be the project
root, contain the project, or live inside it. This check is done here because it
depends on root.
Mutare will only use a target path that is absent, empty, or already marked as a
Mutare-owned sandbox. Any other existing path is refused without modification.
An owned directory is reused only for an explicit :sandbox path or when
:keep_sandbox is enabled. If a generated fresh-path sandbox already exists,
Mutare treats it as a stale leftover and refuses it.
@spec rematerialize(Path.t(), Mutare.Schema.t()) :: Path.t()
Re-render schema's metamutants into an already-prepared sandbox, in place.
This is the poison-recovery path. After a failed compile removes the implicated mutant ids and rebuilds the schema, the project copy, bootstrap, coverage helper, and seeded builds are still valid. Only the metamutant source files may have changed.
The rewrite is byte-aware, so unchanged files keep their timestamps and Mix recompiles as little as possible. The same sandbox path is returned.