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 37s 2s 2s 39s 37s
Eval 39s 53s 53s 1m33s 0s
Build 1h38m37s 52s 23s 1h39m00s 1h37m04s
Suite 37s 1m48s 1h38m23s 1h39m00s 1h37m41s

Timeline

0s40s50s1m1m10s1m20s1m30s1h38m1h38m1h39m