Read back the coverage the metamutant recorded during the probe run.
The metamutant self-records coverage synchronously, in the test process, as
the suite runs (Mutare.Coverage.Recorder owns the generated side); at suite end
an ExUnit.after_suite/1 hook dumps it to a file. This module reads that dump.
The dump is three things, all keyed by mutant id (no metamutant↔original line mapping — the original line is only for the report):
:aggregate— the set of mutant ids whose selector ran at all, in any process (test body,setup,setup_all, a spawned task). This is the no-coverage signal: an id not in it can never be killed, so skip it and keep it out of the score's denominator.:by_file—%{test_file => MapSet(mutant ids)}: which mutant ids each test file covered. A line running in the test process is labeled directly; one running in aTaskit spawned is attributed via the caller chain; one running in a module'ssetup_allis attributed via the__ex_unit__/2stacktrace frame (module-granular, the file selection needs). This drives per-file test selection.:unlabeled— the set of mutant ids whose selector ran in a process with no recoverable test label at all — a bare spawn, asetup-registeredon_exitclosure, or the raresetup_allwhose work happened off-stack in aTaskit spawned (anon_exitregistered in a test body is recovered, via its closure frame in ExUnit's per-test runner process). An id here was covered, but which test owns it is unknown, so the caller runs the whole suite for it — even if:by_filealso attributes it to some file, since that partial attribution would otherwise mask the unlabeled coverage and manufacture a false survivor.
Two more keys carry the finer test-case granularity :tests selection uses
(both keyed by mutant id, both empty in a dump written before this contract, so an
old dump degrades :tests to :coverage):
:by_test—%{mutant id => MapSet(runnable test names)}: the individual ExUnit tests (test/doctest/propertynames) that covered each id, so a mutant can runmix test <file> --only test:<name>instead of the whole file.:wholefile— the set of ids with a labeled but non-narrowable attribution (asetup_allor anon_exit, which cover through a module-scoped context, not a single runnable test).:testsmust run the whole file for such an id — narrowing to named tests would drop the covering context and manufacture a false survivor.
Mutare.Runner.CoverageProbe reconciles them all.
Why not :cover: its counters live in a single global table keyed
{module, line} with no per-process partition, so attributing coverage to a
test in one run needs a per-test snapshot, 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). Self-recording in the metamutant
sidesteps that entirely — and is process-agnostic for the aggregate, so it is a
better no-coverage detector than :cover ever was.
Summary
Functions
Read and decode the probe's coverage dump at path.
Types
@type t() :: %{ aggregate: MapSet.t(pos_integer()), by_file: %{required(String.t()) => MapSet.t(pos_integer())}, unlabeled: MapSet.t(pos_integer()), by_test: %{required(pos_integer()) => MapSet.t(String.t())}, wholefile: MapSet.t(pos_integer()) }
The decoded dump. Keyed by mutant id throughout:
aggregate— the process-agnostic hit set (no-coverage detection).by_file— per-test-file attribution (:coverageselection, and the:testsfallback).unlabeled— the whole-suite hit set.by_test— per-test-case attribution:id => runnable test namesthat covered it (:testsnarrowing).wholefile— ids with a labeled but non-narrowable attribution (setup_all/on_exit), which:testsmust not narrow to named tests.
by_test/wholefile are absent from a dump written before this contract; they
default to empty so an old dump still reads (degrading :tests to :coverage).
Functions
Read and decode the probe's coverage dump at path.
Returns {:error, _} on anything unusable (missing file, truncated/garbled
payload, unexpected shape — including a well-formed outer map whose nested keys,
ids or collections are the wrong type) — the caller degrades such uncertainty to
running the whole suite, never to a false :no_coverage. It raises for no input.