61123a12

By: Tom Sydney Kerckhove <syd@cs-syd.eu>

sydtest: control-group (no-op) mutations

Add control-group mutations: deliberate no-ops that are expected to survive.
The plugin wraps an expression as `(\ctrl -> ifMutation cmid ctrl ctrl) e` (the
same expression on both branches, so a genuine non-diff), one after every 12
real mutations. Both branches share the subtree through the lambda binder, so
the no-op does not duplicate the instrumented expression. The control rides the
existing operator field under the reserved name "Control"; no new record field.

A control changes no behaviour, so killing it is impossible unless the mutation
testing is unsound. Tests are already retried, so a killed control is a real
defect (a flaky/nondeterministic suite or a harness bug), not transient noise,
and it fails the run like a survivor: assert-score fails on it, the driver's
fail-fast exit trips on it, and isMutationFailure is True for it. A passing
control never trips anything and is excluded from the killed/survived score.

MutationRunReport is split into three parts to match: a MutationTally (the
normal killed/survived/timed-out/uncovered/skipped score), a ControlTally
(passed/failed), and the per-mutation groups; the JSON nests to match. Record
types used as formal arguments use RecordWildCards.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 2s 2s 2s 5s 2s
Eval 5s 37s 37s 42s 0s
Build 55m41s 9s 6s 55m48s 54m59s
Suite 2s 49s 55m45s 55m48s 55m02s

Timeline

0s10s20s30s40s