Skip to content

ditto v0.12.0

Latest

Choose a tag to compare

@Disble Disble released this 20 Sep 03:13

A score that no longer mixes two questions

ditto staged scoped 43 mutants to a staged change spanning five packages and judged them with one command naming one of them. 17 of the 20 survivors lived in a package that command never compiles, so nothing it ran could have killed them: the run's ceiling was 0.605, and the number printed as a verdict was 0.53 against a bar of 0.80. The score was measuring the tests and the command's scope together, and nothing in the output told the two apart.

A release now reads which packages the test command executes — from the command's own go test -json stream, which the baseline run already produces, resolved through go list -deps -test so that a mutant in a dependency of a named package is correctly judged rather than accused. The mutants it cannot compile:

  • leave the numerator and the denominator, exactly as a mutant that never compiled does (S = D/(M − E)), so the printed score is a real measurement of what could be judged;
  • are not run, because a guaranteed survivor bought with a full suite run is not evidence about anybody's tests;
  • are named with the package that holds them, and the run exits 3.
┃ • Total:       26                    ┃
┃ • Killed:      23                    ┃
┃ • Survived:     3                    ┃
┃ • Unmeasured:       17               ┃
┃ ⨯ Score:     0.88 (minimum: 0.80)    ┃
┃ 17 of the 43 mutants in this scope are never compiled by this test command, so
┃ nothing it runs can kill them and they are out of the score entirely:
┃   internal/desktop (17)
┃ Name every package the scope mutates in --test-command, or narrow the scope.

Exit 3 is the other half: a scope ditto cannot measure and a score below the bar were both 1, and a gate has to be able to tell them apart, because no test answers the first — only naming every package the scope mutates, or narrowing the scope, does.

--include-prefix

The mirror of --exclude-prefix, on staged and changed. The fix above is what makes it necessary: a run refuses a scope its command cannot compile, and the honest answer is usually to narrow the run to the package the command names — one flag, where one exclusion per other package the change happens to touch does the same.

PlanStaged, RunStaged, PlanChanged and RunChanged take a ditto.Prefixes where they took one []string, so what a run is about and what it leaves out are one argument instead of two that can drift.

Verification

Checked on the tagged commit ac3c2ddce0310122a3d7d76862478ba5331c9351:

  • CI, both matrix legs (Go 1.27 and latest): pass
  • CodeQL, OSV-Scanner, SonarCloud: pass
  • chained mutation gate over the change: 118 mutants, 94 killed, 24 survived, score 0.80 against a 0.50 minimum
  • local gate: lint 0 issues, the full suite green with a cold cache (go test -count=1 ./...), and the exact counters passed with mutantsPerReleaseOnThisRepository at 948

The behaviour was read off the shipped binary, not off the suite: a throwaway git repository with three packages — one the command names, one its tests import, and one nothing compiles — run under a command that names only the first. It printed the block above, with internal/untested named and exit 3; the same fixture under ./... reported nothing unmeasured and exited 0. That fixture and its golden are now in the repository, with the control beside it, because the middle package is the case that decides whether the check is usable at all: a mutant in a package the command does not name is killable by that named package's tests.

What this release does not claim

  • The check only speaks when the command reports itself. It reads the packages the command executes out of its own go test -json stream, so a --test-command that is make, gotestsum, a wrapper script or go test without -json gets no check at all, and keeps the behaviour it always had.
  • It works per package, not per file. A file behind a build tag for another operating system is inside a package the command does compile, so its mutants stay ordinary survivors — which they are.
  • A gated run is not checked, because Gated() replaces the plan of the exact module-scope command, whose packages the gated path already resolves for itself; asking the ordinary laboratory would buy a second suite run to learn what it already knows.
  • Per-package test commands are not shipped. Grouping a scope's mutants by owning package and deriving one command per group is the version of this that would make the check unnecessary rather than enforceable, and it is a cost-model change: docs/backlog.md entry 29 records the measurement that would have to argue for it.
  • One line of output moved deliberately: the baseline announcement prints before the first file's announcement, because asking the laboratory which packages the command compiles is what pays for the baseline. Every verdict and every mutant address is identical, and the golden says so where someone will look.

The whole release is one report's defect, committed with it: docs/reports/ditto-mutation-scope.md.