Mutare.Sandbox (mutare v0.1.0)

Copy Markdown View Source

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 :sandbox it lives at a stable per-project temp dir; CI pins :sandbox at a cached directory instead. See NOTES.md for 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

bootstrap()

@spec bootstrap() :: String.t()

The bootstrap snippet prepended to the sandbox's test helper.

prepare(root, schema, opts \\ [])

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.

rematerialize(sandbox, schema)

@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.