Mutare.Sandbox (mutare v0.3.1)

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 combines MUTARE_MUTANT_NAMESPACE (the active mutant's file) and MUTARE_ACTIVE_MUTANT (its id within that file) into one :persistent_term entry 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. Sandbox mix.exs files also wrap project/0 to disable signature inference in the effective compiler options (Mutare.Sandbox.CompilerOptions), including every umbrella child. Mutare.Sandbox.ProjectEvaluation marks escaping project failures so the decoder retains startup-kill attribution even without project stack frames. The target's own project files remain untouched.

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 by Mutare.Sandbox.Mirror: a file is rewritten only when its desired content differs (unchanged files keep their mtime, so mix's incremental compiler reuses _build), and files no longer mirrored, generated, or explicitly managed 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.

This module materialises the workspace. Sandbox mix invocations and per-mutant timeouts are implemented in Mutare.Sandbox.Command.Invocation.

Summary

Types

Results of prepare/3 for --verbose output, which Mutare.Runner.Compile relays on the :on_phase hook: the app-build seed's outcome (t:Mutare.Sandbox.Seed.summary/0), and each mix.exs whose type-signature-inference override was not applied, paired with the reason, in path order. Such a project compiles with inference on, which can stretch the one compile from seconds to hours, so a long compile should not go unexplained.

Functions

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

Prepare a sandbox for schema taken from root. Returns the sandbox path and a materialized/0 summary of what happened.

Re-render schema's metamutants into an already-prepared sandbox, in place.

Types

materialized()

@type materialized() :: %{
  seed: Mutare.Sandbox.Seed.summary(),
  declined: [{Path.t(), String.t()}]
}

Results of prepare/3 for --verbose output, which Mutare.Runner.Compile relays on the :on_phase hook: the app-build seed's outcome (t:Mutare.Sandbox.Seed.summary/0), and each mix.exs whose type-signature-inference override was not applied, paired with the reason, in path order. Such a project compiles with inference on, which can stretch the one compile from seconds to hours, so a long compile should not go unexplained.

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 and a materialized/0 summary of what happened.

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; its hooks are not consulted — the runner reports the summary.

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 rejects it as a stale leftover.

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.