Repository navigation
Backport tupferl's mutation-harness improvements - #321
Merged
Merged
Conversation
…udget spellings, complete the spec path
The macOS leg went red with '6 discovered tests never ran'. A setUpClass that raises SkipTest reports once for the class, so the ids run_tests discovered never report back -- the accounting check working correctly, against a shape this branch introduced when it deferred the RLIMIT_AS probe out of import time. Invisible on Linux, where the limit is enforced and the classes run. tests/test_run_tests.py now refuses the shape for the whole suite.
_run handed the probe **os.environ, so WOSWOAR_MUTATE_TOTAL rode into every lane -- and _budget reads it before _visible_memory, so a nested harness sized itself for the outer machine and ignored the per-lane WOSWOAR_MUTATE_BUDGET beside it. _budget's own docstring already called that wrong; nothing stopped it. Two sweeps found it the expensive way: with the variable exported every probe answered _budget() with the whole machine, the suites that patch _visible_memory went red, and the baseline they broke voided all 394 rows. The first fix pinned the tests, which was treating the symptom.
The generated sweep reported the landed/finished callback as survivors on lines no test executes, and it was right: every sweep test stubbed mutate.run, so the callback that carries a row out to --json never ran. That is the mechanism this branch's headline fix turns on. Three tests now drive it for real, and a spec table proves all five mutations of it fail without them. The mid-run one earned its own lesson: written with the watcher's window equal to the held row's timeout, sweep's final write raced into the assertion and the deleted incremental write passed anyway -- SURVIVED narrowly, caught only under the whole suite, which is what a fixture that decides by timing looks like.
The sweep reported ahead_of's reachability any() as a survivor. Every fixture for it named a single module, where any and all agree -- the two-symmetric-inputs weakness, invisible in the test's own text.
martinus
marked this pull request as ready for review
August 25, 2026 17:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #320.
tupferl ported this repository's mutation harness, improved it, and measured the improvements; this brings them back. Everything below was measured here unless attributed; numbers measured in tupferl say so, in the code as well as in this description.
CI is green on all 12 checks. The mutation evidence is partial and the "Mutation sweep" section says exactly how — read that before merging.
What arrived
_run_spec). The dispatch wasrun(mutations)with every CLI flag silently dropped, survivors never confirmed,--jsonnever written. It now honours all of them — including--killers,--baseline-onlyand the--jsonmarker/pidfile lifecycle, which the issue's item 1 did not list and which the review found still inert after the first fix.tests/profiles.py), selected byWOSWOAR_HYPOTHESIS_PROFILE;tools/mutate.pypins a derandomised 20-examplemutationprofile for every probe; CI now setsciexplicitly. Measured here: the three heaviest property modules drop from ~7 s to 0.44 s undermutation, and a typo'd profile name fails loudly.EACH_TEST,--each-test): a hung test is interrupted, named, and filedBROKE— nevercaught.sweeps/killers.json, gitignored): the test that caught each mutation runs first next time, in front of the row's selection, never in place of it; rows with nothing remembered run a cheap greedy prefix. With items 3–4 this was tupferl's 2.07× (15m13s → 7m20s, 274 mutants).--baseline-onlyasks just the untouched-suite question.dedicated()/_confined()/WOSWOAR_MUTATE_TOTAL, and_why()prints which rule set the budget. On this 16 GiB/4-core container the unconditional halving gave 8037 MiB and 3 lanes where 7 fit.watch.py --pidfile-wait(its two deadline tests were the suite's slowest at 10.09 s + 10.08 s of a 249.7 s serial run; now sub-second), oneusable_cpusintools/cpus.py, andtests/test_verdict.py(30 direct tests for the module that grades everything else),tests/test_profiles.py,tests/test_cpus.py.Where the fix diverged from the issue (and why)
EACH_TESTis 150, not tupferl's 30. The issue's own constraint, confirmed by measurement: tests here bound the subprocesses they drive at 60 s and 120 s, and a hang those guards would catch (TimeoutExpiredis a test noticing) must reach the guard before the alarm files it asBROKE— tupferl lost seven mutants to the opposite ordering. The slowest honest test measured 10.09 s, so 150 is 25% above the tallest guard and half ofTIMEOUT.usable_cpuswent to a newtools/cpus.py, not intosandbox.pyas the issue suggested. The premise failed on inspection:sandbox.pyimportswoswoar.store, so pointing the test runner at it would make a brokenwoswoarpackage crash the runner instead of being reported per test.load_profilesets one process-wide default resolved when a test runs, so underdiscoverevery property module already ran at whichever profile the last import loaded. 400 is the highest of the old numbers, so nothing got weaker.woswoar/entry.py's clamp guard iscaughtbytests.test_entry, the one-character cap nudgesurvived.The review (rule 1, top row — the killers cache caches a fact about the world)
Full
/simplify, four angles, applied before the PR opened. The substantive findings:--jsonthrough the incremental writer,_recordeddrops the red flag, andsweepskips recorded files by name — so a resume treated void rows as final answers, feedingconfirmandkillers.learn.landednow stops firing the moment the baseline comes back red.setdefaultso the complete measurements win._baseline_is_greenre-implementedrun's shard list and lane sizing and had already diverged from its own "the same sizingrundoes" comment — both now come from shared_shards/_wanted.Killers.head/droppedwere dead state feeding dead re-announcement blocks (upstream too); deleted._whyand_budgetparsed_TOTALdifferently, so_TOTAL=0printed a reason that did not fire; one_told()reader now._confinedis the single cgroup reader.verdict.addErrorhad re-grown a copy of_answered's body. Stale--batchhelp andscopedocstring fixed.support.memory_caps_apply; the fifo class moved besidetest_verdict.py's probe driver; duplicated real-harness runs now run once per class;test_a_widened_row_carries_no_remembered_testcould not fail (it asserted on its own construction) and now drivesconfirm.Not applied, per rule 1's "say so rather than silently dropping": deduplicating a
firsttest out of the probe's combined suite (≤0.5 s per surviving row, but it adds filtering toverdict.collect, the one function whose failure modes this file exists to prevent), and full unification of_run_specwith the generated path's tail (the lifecycle and killers wiring are now shared; the control flow is not — a shared executor exceeds this PR's scope).Worth sending upstream to tupferl: the red-baseline resume hole, the timing-precedence flip, the dead
Killers.head/droppedpair, and the_baseline_is_greensizing divergence.What CI and the sweeps caught in this branch itself
Three defects, all mine, all found by the machinery rather than by reading:
6 discovered tests never ran. Deferring theRLIMIT_ASprobe out of import time (a review finding) had moved the gate intosetUpClass, and asetUpClassskip reports once for the class — so the idsrun_testsdiscovered never reported back. Invisible on Linux, where the limit is enforced and the classes run. Gating insetUpfixes it, andtests/test_run_tests.pynow refuses the shape for the whole suite withast._runhanded the probe**os.environ, soWOSWOAR_MUTATE_TOTALrode into every lane — and since_budgetreads it before_visible_memory, a nested harness sized itself for the outer machine and ignored the per-lane_BUDGETbeside it._budget's own docstring already called that wrong; nothing stopped it. My first fix pinned the tests, which was treating the symptom; the fix is one line in_run, with a test that drives a real probe and asserts both names.landed/finishedcallback as surviving on lines no test executes, and it was right: every sweep test stubbedmutate.run, so the callback never ran. Three tests now drive it for real. The mid-run one earned its own lesson — written with the watcher's window equal to the held row's timeout,sweep's final write raced into the assertion and the deleted incremental write passed anyway.Mutation sweep
WOSWOAR_MUTATE_TOTAL=… python -m tools.mutate --base main --limit 0 --batch --json sweeps/r.json, run after the review was applied. Verbatim tail:Report totals, from
sweeps/r.json:Crossed with coverage (
python -m tools.reached sweeps/r.json coverage.json):Three things make this evidence partial, and I would rather say so than round up.
widened: falseis honest: every survivor re-run timed out.verdict.collectruns aWHOLE_SUITEselection serially, the serial suite is 249.7 s,TIMEOUTis 300 s, and the re-runs are concurrent — so confirmation cannot fit here by construction. That is true onmaintoo; this branch only made it visible by producing 127 survivors instead of a handful. Filed as mutate: the whole-suite confirmation pass cannot fit in --timeout, so survivors silently become unanswered rows #322 with the arithmetic and three candidate fixes. Until it is fixed, a survivor list from this repo cannot be trusted to be free of narrow-selection false positives.run(16),main(15),sweep.finished(11),sweep(9),_baseline_is_green(9) — the functions that are hard to test because testing them means running the harness. I wrote tests for the load-bearing ones and proved them with a hand table, which is the part of the evidence that is against the current tree:One genuine weak fixture from the survivor list is also fixed:
ahead_of's reachabilityany()survived because every fixture named a single module, whereanyandallagree — the two-symmetric-inputs shape. A two-module selection tells them apart.The remainder are dominated by diagnostics (
printcalls and their guards), equivalent mutants (return ""vsNoneat call sites that only test falsiness;setitimer(…, 0)when the alarm is already off;err[0]/err[1]/err[-1]allNonein the same tuple), and lines only reachable through a subprocess that in-process coverage cannot see —reached.pyfolded 41 of those back in and says so. A full accounting wants a clean sweep once #322 is fixed, and I would not claim this branch has one.Notes
state.json, cache or day keys. The one change an existing checkout sees istests/profiles.py; no data moves.sweeps/joins.gitignore.CI=true WOSWOAR_HYPOTHESIS_PROFILE=ci python -m tools.run_tests), and with and without an ambientWOSWOAR_MUTATE_TOTAL.