Mutare.Project (mutare v0.1.2)

Copy Markdown View Source

Resolve the shape of the target into a copy-root and a mutate-scope.

Mutare copies one tree into the sandbox and mutates a set of source files in it. For a plain Mix project those are the same directory. For an umbrella they differ: the whole umbrella is copied (so in_umbrella sibling deps and the shared deps//config/ keep resolving), while only a chosen subset of apps/* gets metamutants.

resolve/2 turns the user-supplied target path plus the --app/--workspace flags into:

  • copy_root — the directory materialised into the sandbox (the umbrella root, or the project root for a single app). This is the root the rest of the pipeline (Mutare.Schema, Mutare.Sandbox, Mutare.Runner) treats as authoritative for every path operation; the struct duplicates it only so the entry points know what to pass.
  • mutate_scope — the apps whose sources are mutated, as %{app, dir} entries with dir relative to copy_root ("apps/foo", or "." for a single app).
  • apps — every app whose suite runs (all umbrella apps), which the per-app bootstrap injection and the dependency-graph scoping need.

Detection is static — Mutare runs outside the target's Mix, so the layout is read from the filesystem (an apps/ dir plus an apps_path: in the root mix.exs) rather than from Mix.Project.

Summary

Functions

Per mutate-scope app, the root-relative test/ dirs a broad (whole-suite) run may be narrowed to: the app itself plus every app that (transitively) depends on it. A mutant in app A can only be killed by a test that executes A's code, and a sibling executes A's code only through a declared dependency — so this set is a safe superset of A's possible killers; narrowing below it would risk a false survivor, so we never do.

Resolve target (+ scope flags) into a t/0.

Is dir (an absolute path) the root of an umbrella project?

Types

app()

@type app() :: %{app: atom() | nil, dir: String.t()}

t()

@type t() :: %Mutare.Project{
  apps: [app()],
  copy_root: Path.t(),
  mutate_scope: [app()],
  umbrella?: boolean()
}

Functions

app_test_scopes(project, sandbox, forward)

@spec app_test_scopes(t(), Path.t(), %{required(atom()) => [atom()]}) :: %{
  required(atom()) => [String.t()]
}

Per mutate-scope app, the root-relative test/ dirs a broad (whole-suite) run may be narrowed to: the app itself plus every app that (transitively) depends on it. A mutant in app A can only be killed by a test that executes A's code, and a sibling executes A's code only through a declared dependency — so this set is a safe superset of A's possible killers; narrowing below it would risk a false survivor, so we never do.

forward is the declared inter-app graph, %{app => [apps it depends on]}, as Mutare.Runner.AppGraph.read/2 reads it from Mix (nodes and deps outside the umbrella are ignored). Declared, not runtime: a runtime: false sibling dep is absent from the compiled .app's applications yet fully callable from the dependent's tests. Only dirs that actually exist under sandbox are returned. Returns %{} (⇒ no narrowing, run the whole umbrella) for a single app, or when forward lacks an umbrella app — a missing node would hide that app's dependents, so degrade safe rather than narrow on doubt.

resolve(target, opts \\ [])

@spec resolve(
  Path.t(),
  keyword()
) :: t()

Resolve target (+ scope flags) into a t/0.

opts:

  • :apps — app-name strings to mutate (from --app); nil/[] means "all".
  • :workspacetrue mutates every app (from --workspace).

A single (non-umbrella) project resolves to copy_root: target with a lone %{app: nil, dir: "."} scope, so the existing single-app pipeline is unchanged.

umbrella_root?(dir)

@spec umbrella_root?(Path.t()) :: boolean()

Is dir (an absolute path) the root of an umbrella project?