Renders mutation results for the human report.
Diffs are patched against the original source via Sourceror.patch_string at the site's recorded range, so untouched source stays byte-identical and a survivor reads as a precise source-range change. A replacement is parenthesized where its position would otherwise reread it (!(a == b) → (a == b) beside a |>, a -1 for the 0.75 of -0.75), on the assumption that binary operators are spaced as mix format writes them: in unformatted a-0, a -1 replacement is patched as a--1.
The mutation score and tallies in the report come from Mutare.Score, which also implements the CI gates; this module only renders.
Summary
Functions
A -/+ diff of the lines touched by the mutation.
One line for a harness-errored mutant, including a compact diagnostic.
Header line for a surviving mutant, e.g.
lib/x.ex:42 [relational, in-place] SURVIVED.
One line for an ignored mutant, e.g.
lib/x.ex:42 [arithmetic] IGNORED — off-by-one is intentional.
The lines holding mutants no test ran, one line per file, with runs of
consecutive lines collapsed into a range. Files keep the order of results.
Returns "" when every mutant was covered.
Apply a single mutation to the original source string.
Render the whole report from results and a %{file => original_source} map.
Renders the human report with the same arity as machine reporters.
One-line tally, e.g. mutation score: 66.7% (2 killed, 1 survived, 3 total).
Full diff block (header + diff) for one survivor.
Functions
@spec diff(Mutare.Site.t(), String.t()) :: String.t()
A -/+ diff of the lines touched by the mutation.
The diff is line-based within the site's source range. Changed lines are shown as deletions and insertions; unchanged lines inside a multi-line fragment are shown as context. This keeps multi-line replacements aligned when a mutation adds or removes a line in the middle of the fragment.
@spec harness_error(Mutare.Result.t()) :: String.t()
One line for a harness-errored mutant, including a compact diagnostic.
@spec header(Mutare.Site.t()) :: String.t()
Header line for a surviving mutant, e.g.
lib/x.ex:42 [relational, in-place] SURVIVED.
A mutation with a note appends it as a trailing — note, matching the ignored
mutant reason format.
@spec ignored(Mutare.Site.t()) :: String.t()
One line for an ignored mutant, e.g.
lib/x.ex:42 [arithmetic] IGNORED — off-by-one is intentional.
The trailing — reason is present only when the directive carried one, so a
bare # mutare:ignore reads as … IGNORED with nothing after it.
@spec no_coverage([Mutare.Result.t()]) :: String.t()
The lines holding mutants no test ran, one line per file, with runs of
consecutive lines collapsed into a range. Files keep the order of results.
Returns "" when every mutant was covered.
Listing each uncovered mutant would flood a large run, since a line can hold many; collapsing to lines keeps the section proportional to the untested regions. Only strictly consecutive lines merge: a line with no uncovered mutant between two that have one may be covered or outside the run's scope, so a range never spans it.
iex> results = [
...> %Mutare.Result{status: :no_coverage, site: %Mutare.Site{file: "lib/a.ex", line: 13}},
...> %Mutare.Result{status: :no_coverage, site: %Mutare.Site{file: "lib/a.ex", line: 12}},
...> %Mutare.Result{status: :killed, site: %Mutare.Site{file: "lib/a.ex", line: 20}},
...> %Mutare.Result{status: :no_coverage, site: %Mutare.Site{file: "lib/a.ex", line: 30}},
...> %Mutare.Result{status: :no_coverage, site: %Mutare.Site{file: "lib/b.ex", line: 7}}
...> ]
iex> Mutare.Report.no_coverage(results)
"no test ran the mutants on:\n lib/a.ex:12-13, 30\n lib/b.ex:7"
@spec patch(Mutare.Site.t(), String.t()) :: String.t()
Apply a single mutation to the original source string.
@spec render([Mutare.Result.t()], %{optional(String.t()) => String.t()}) :: String.t()
Render the whole report from results and a %{file => original_source} map.
The report lists each survivor as a diff, then the lines holding mutants no
test ran (no_coverage/1), then ignored mutants and harness errors, one per
line, and ends with the summary/1 tally.
@spec render([Mutare.Result.t()], %{optional(String.t()) => String.t()}, keyword()) :: String.t()
Renders the human report with the same arity as machine reporters.
opts is ignored; score gating is handled by the caller.
@spec summary([Mutare.Result.t()]) :: String.t()
One-line tally, e.g. mutation score: 66.7% (2 killed, 1 survived, 3 total).
iex> Mutare.Report.summary([
...> %Mutare.Result{status: :killed},
...> %Mutare.Result{status: :survived}
...> ])
"mutation score: 50.0% (1 killed, 1 survived, 2 total)"
@spec survivor(Mutare.Site.t(), String.t()) :: String.t()
Full diff block (header + diff) for one survivor.