The generated-code side of coverage capture: the contract the metamutant and the test bootstrap share to record coverage synchronously, in the test process, with no race.
Mutare.Coverage reads what this produces; this module owns how it is
produced. There are three pieces, all emitted into generated code:
record_ast/1— spliced byMutare.Transforminto every selector catch-all. At baseline, under a tracking flag, it records the site's mutant ids into shared ETS — in whatever process runs the line, including the test process. (Mutare.Selectorowns the selection contract the same way.)helper_source/0— a dependency-free helper module (Mutare.Sandboxwrites it into the sandbox) holding the ETS writes and the end-of-suite dump, so the per-site code stays a single call and the OTP-version-tolerant label read lives in one place.setup_ast/0andafter_suite_ast/0— injected around the target'stest_helper.exs; when the probe env var is set, setup creates the tables and flips the tracking flag before user helper code runs, while the after-suite hook is registered after the helper has started ExUnit.
Why this, not :cover
:cover's counters live in a single global table keyed {module, line} — there
is no per-process partition, so attributing coverage to a test in one run
needs a snapshot at each test boundary, and the only global per-test signal
ExUnit emits is an async cast to formatters that races test execution
(fast async: false modules' coverage is lost). The metamutant, by contrast,
is code we generate and that runs synchronously in the test process. So the
capture lives there, accumulate-only (never reset), keyed by mutant id directly.
The gate (why it is inert outside the probe)
The spliced expression is mutare_active == 0 and :persistent_term.get(:mutare_track, false) and <helper>.hit([<ids>]):
- per-mutant runs (
mutare_active != 0) short-circuit on the integer compare — ~zero hot-loop cost; - the baseline green run and Mutare's own unit tests (
:mutare_trackunset) short-circuit on the persistent-term read — the helper is never called; - only the probe run (
MUTARE_COVERAGEset →:mutare_tracktrue,mutare_active == 0) records.
The helper's hit/1 returns true so the and chain stays boolean (an :ets
call returns an int/true and would raise BadBooleanError mid-and).
Summary
Functions
After-suite AST appended after the target's test helper.
Catch-all clause pattern that binds the selector subject to var.
The file (relative to the sandbox) the end-of-suite dump is written to.
Env var the probe sets to the absolute path the dump is written to.
Env var the probe sets to turn coverage recording on for one run.
The module name Mutare's test/support/mutare_cov.ex stand-in defines itself as.
Env var a sandbox run sets to give the suite-under-test's stand-in a private module name.
The dependency-free helper module emitted into the sandbox.
Source for the dependency-free coverage helper written into the sandbox.
AST for the @compile {:no_warn_undefined, {<helper>, :hit, 1}} attribute.
AST that records coverage for this selector's mutant ids.
Return the dispatch variable read by a coverage record, or nil.
Env var the probe sets to the absolute root test-file paths are relative to.
Setup AST prepended before the target's test helper.
The private stand-in module name (fixture_override_env/0's value) used under dogfooding.
The :persistent_term flag the spliced record reads (false by default).
The canonical selector-subject variable name (:mutare_active); the default var.
Functions
@spec after_suite_ast() :: Macro.t()
After-suite AST appended after the target's test helper.
ExUnit.after_suite/1 requires ExUnit to be started, so the dump callback is
registered after the user's helper even though coverage tracking starts before
it.
Catch-all clause pattern that binds the selector subject to var.
Mutare.Transform uses this instead of _ so record_ast/2 can read the
active id without a second :persistent_term lookup. Pass the same per-file
variable to this function and to record_ast/2.
@spec dump_file() :: String.t()
The file (relative to the sandbox) the end-of-suite dump is written to.
@spec dump_path_env() :: String.t()
Env var the probe sets to the absolute path the dump is written to.
@spec env_var() :: String.t()
Env var the probe sets to turn coverage recording on for one run.
@spec fixture_module() :: module()
The module name Mutare's test/support/mutare_cov.ex stand-in defines itself as.
helper_module/0 (:mutare_cov) unless fixture_override_env/0 names another —
the one knob self-hosting needs so the stand-in does not collide with the real
helper the sandbox writes (see the constant's comment above).
@spec fixture_override_env() :: String.t()
Env var a sandbox run sets to give the suite-under-test's stand-in a private module name.
@spec helper_module() :: module()
The dependency-free helper module emitted into the sandbox.
@spec helper_source() :: String.t()
Source for the dependency-free coverage helper written into the sandbox.
The source comes from Mutare.Coverage.HelperTemplate, a normal
compile-checked module. Only its defmodule line is rewritten to
helper_module/0 (:mutare_cov).
The helper's hit/1 writes coverage to shared ETS tables. Its dump/1
callback, registered with ExUnit.after_suite/1, writes dump_file/0 and
maps each test module back to its source file.
@spec no_warn_attr_ast() :: Macro.t()
AST for the @compile {:no_warn_undefined, {<helper>, :hit, 1}} attribute.
Mutare.Transform prepends this to every metamutant module body. Selector
catch-alls call the generated coverage helper's hit/1; in an umbrella that
helper can live in a generated sibling app, so the mutated app may compile before
the helper and trigger a benign xref warning. The call resolves at runtime. This
attribute suppresses only that compile-time warning and is harmless when the
helper is compiled in the same app.
@spec record_ast([pos_integer()], atom()) :: Macro.t()
AST that records coverage for this selector's mutant ids.
Mutare.Transform prepends this expression to a selector catch-all body. It
records only when the selector is at baseline and the coverage probe has enabled
tracking; see the moduledoc for the full gate.
var must be the variable introduced by catch_all_pattern/1. The AST is
built directly so it can share that binding and splice ids as a literal list
of integers.
Literal arguments use clean metadata. In particular, the 0 must be wrapped as
{:__block__, [], [0]}; a bare integer can render badly when this expression is
emitted as a statement in a generated function body.
Return the dispatch variable read by a coverage record, or nil.
This is the inverse of record_ast/2. A coverage record has this shape:
<var> == 0 and :persistent_term.get(<track_key>, false) and <helper>.hit(<ids>)<var> is the file's dispatch variable, possibly salted. The internal
<track_key> read identifies a real coverage record, so Mutare.Manifest can
recover the dispatch name from rendered metamutant source without trusting a
source-level binding that only looks similar.
The helper call is not part of recognition; that keeps self-hosting helper-name
overrides from changing the result. Literal {:__block__, _, [literal]}
wrappers added during reparse are accepted.
@spec root_env() :: String.t()
Env var the probe sets to the absolute root test-file paths are relative to.
@spec setup_ast() :: Macro.t()
Setup AST prepended before the target's test helper.
It is inert unless env_var/0 is set. During the coverage probe it creates the
ETS tables and enables the tracking flag before user helper code runs, so app
startup and helper setup can be attributed. The tables are owned by the
test-helper process, which outlives the suite run.
@spec suite_fixture_module() :: String.t()
The private stand-in module name (fixture_override_env/0's value) used under dogfooding.
@spec track_key() :: atom()
The :persistent_term flag the spliced record reads (false by default).
@spec var_name() :: atom()
The canonical selector-subject variable name (:mutare_active); the default var.