Skip to content

Commit 8e0fc67

Browse files
test(mutmut): item D closure - wl_csv.py 547 survivors + harness cache-leak fix
v1.1 item D from the 2026-05-18 pre-release sweep. First real survivor data for bin/wl_csv.py with the correct test selector, and a fix for a cache-leak harness bug discovered during the run. ## wl_csv.py fresh-run result Mutmut run with the corrected selector (tests/unit/test_csv.py + tests/unit/test_diff_fuzz.py) on a fresh :ro-mount + tmpfs container. 723 mutations applied, 547 survived, 176 killed. Effective mutation score: 24%. Why the score is low: bin/wl_csv.py is 1244 lines. Only ~600 of those (the diff engine + hash-registry plumbing) have unit-test coverage in test_csv.py / test_diff_fuzz.py. The other ~600 lines (save_csv_pipeline + create_csv_pipeline) are integration-tested in tests/integration/, NOT exercised by the unit selector mutmut uses. This shows up in the survivor ID ranges: 191-472 have a mix of killed and survived (~50% each); 494-913 are almost all survivors (integration paths). Triage on 10 sampled survivors shows the killable subset: - logger / docstring strings: skip (low value) - constant strings → paths: killable, low priority - value→None crash mutations: killable, trivial tests - hidden-column filter strings: killable, security boundary - tuple default-value sentinels: mostly equivalent - response-dict key names: killable, highest value (contract pin) - JSON formatting params: equivalent (indent=2 vs 3) - integration-path mutations (~400 of them): killable but expensive (needs integration test coverage) Genuine killable survivors in the unit-tested core: ~100-150. Closing them = ~100-150 new unit tests = 1-2 days of work. Deferred to v1.1 item G (test coverage push 32.4% → 80%), which naturally covers the same code paths. Doing the work twice would be wasteful. ## Harness cache-leak fix Discovered during this run: a leftover .mutmut-cache (~192 KB) from a prior :rw-mount session was sitting on the host repo. populate_scratch()'s 'cp -a /repo/. /scratch/' copied this stale cache into the fresh tmpfs, polluting 'mutmut results' with phantom entries from prior misconfigured runs (37 wl_audit + 10- 12 wl_validation entries that survived in old caches but were killed in newer runs). Fix: populate_scratch() now wipes /scratch/.mutmut-cache and C:/Program Files/Git/scratch/mutants after the cp. Two-line addition. The wl_csv.py survivor count for THIS run is unaffected by the leak (mutmut's progress counter showed 723/723 actually tested — the leak only polluted the cumulative results display, not the current-run results). Host-side leftover at wl_manager/.mutmut-cache was also deleted. .gitignore already excludes it from tracking. ## Doc updates in MUTATION_TESTING.md - New section: "2026-05-19 wl_csv.py fresh-run results (item D)" with triage categories table + harness-fix note - "Open" list: items 3 (wl_validation re-run, item C) and 4 (wl_csv run, item D) marked CLOSED. Only wl_audit re-run remains in the open queue. doc-drift: OK (33 docs against build 660).
1 parent 9655ba4 commit 8e0fc67

2 files changed

Lines changed: 105 additions & 13 deletions

File tree

docs/MUTATION_TESTING.md

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ test selector. Notable findings:
189189
The 45 "survivors" reported for `bin/wl_csv.py` are NOT actionable.
190190
The mutmut command in the prior session was:
191191

192-
```
192+
```text
193193
mutmut run --paths-to-mutate=bin/wl_csv.py --tests-dir=tests/unit \
194194
--runner='python -m pytest -x -q --tb=no \
195195
tests/unit/test_validation.py tests/unit/test_ascii_validation.py'
@@ -208,9 +208,80 @@ TEST_RUNNER_FILES="tests/unit/test_csv.py" \
208208
scripts/mutmut.sh run
209209
```
210210

211-
This run was NOT executed during the 2026-05-18 session — the time
212-
budget was exhausted by the discovery + safety work above. It is
213-
queued as v1.1 maintenance work.
211+
Re-ran 2026-05-19 — see next section for results.
212+
213+
---
214+
215+
## 2026-05-19 wl_csv.py fresh-run results (item D)
216+
217+
Mutmut run with the corrected selector
218+
(`tests/unit/test_csv.py tests/unit/test_diff_fuzz.py`) on a fresh
219+
`:ro`-mount + tmpfs container.
220+
221+
**Result: 723 mutations applied, 547 survived, 176 killed.
222+
Effective mutation score: 24%.**
223+
224+
### Why the score is low
225+
226+
`bin/wl_csv.py` is 1244 lines. Only ~600 of those — the diff engine
227+
(`compute_diff`, `compute_added`, `compute_removed`, `compute_edited`)
228+
and the hash-registry plumbing (`_csv_file_hash`,
229+
`update_csv_expected_hash`, `bootstrap_csv_expected_hashes`,
230+
`remove_csv_expected_hash`) — have unit-test coverage in
231+
`test_csv.py` and `test_diff_fuzz.py`. The remaining ~600 lines
232+
(`save_csv_pipeline` ~lines 747–1148 and `create_csv_pipeline`
233+
~lines 1149–1244) are tested via the live Splunk container in
234+
`tests/integration/`, NOT by the unit suite mutmut is exercising.
235+
236+
This shows up in the survivor ID ranges: IDs 191–472 (the unit-
237+
testable core) have a mix of killed and survived mutations (~50%
238+
each in spot checks), while IDs 494–913 (the integration-only code)
239+
are almost entirely survivors.
240+
241+
### Triage categories
242+
243+
Spot-checked 10 representative survivors across the range; the
244+
pattern divides cleanly:
245+
246+
| Class | Example mutants | Killable? | Action |
247+
|---|---|---|---|
248+
| Logger / docstring strings | 191 (logger name) | Low value | Skip |
249+
| Constant strings flowing into paths | 200 (`CSV_EXPECTED_HASHES_FILE`) | Killable | Worth a test pin |
250+
| Value→None crash mutations | 207 (`parent = None`) | Killable | Trivial unit test |
251+
| Hidden-column filter strings | 264, 388 (`startswith("_")`) | Killable | Security boundary, worth pinning |
252+
| Tuple default-value sentinels | 313, 332 (`row.get(h, "")`) | Mostly equivalent | Skip |
253+
| Response-dict key names | 431, 600 (`"text_diff"`, `"added_row_count"`) | Killable | Contract pinning — highest value |
254+
| JSON formatting params | 494 (`indent=2 → 3`) | Equivalent | Skip |
255+
| Integration-path mutations | 600+ (most of 494–913) | Killable but expensive | Defer to integration-test coverage |
256+
257+
Rough estimate of GENUINE killable survivors in the unit-tested
258+
core (IDs 191–472): ~100–150. Closing them would require ~100–150
259+
new unit tests, ~5–10 lines each. Total work: 1–2 days.
260+
261+
### Not addressed in this commit
262+
263+
Closing the 100–150 genuine survivors is in scope of v1.1 item G
264+
(test coverage push from 32.4% → 80%). Writing those tests would
265+
naturally exercise the same code paths and kill the same mutants.
266+
Doing the work twice (once for mutmut-survivors, once for coverage)
267+
would be wasteful.
268+
269+
### Harness bug discovered + fixed
270+
271+
During this run, a leftover `.mutmut-cache` (~192 KB) from a prior
272+
`:rw`-mount session was found on the host repo. The
273+
`populate_scratch()` helper's `cp -a /repo/. /scratch/` copied this
274+
stale cache into the fresh tmpfs, leaking prior-run results (37
275+
phantom wl_audit survivors + 10–12 wl_validation entries) into the
276+
cumulative results display. This didn't affect the wl_csv survivor
277+
count for the new run (the mutmut progress counter showed
278+
`723/723` mutations actually tested), but it polluted the
279+
`mutmut results` aggregate view.
280+
281+
Fix: `populate_scratch()` now explicitly wipes
282+
`/scratch/.mutmut-cache` and `/scratch/mutants` after the cp. The
283+
host-side leftover at `wl_manager/.mutmut-cache` was also removed.
284+
`.gitignore` already excluded it from tracking.
214285

215286
---
216287

@@ -238,17 +309,25 @@ queued as v1.1 maintenance work.
238309
Source-refresh signal: `scripts/mutmut.sh kill` then re-run (the
239310
tmpfs is repopulated from `/repo` only on container creation).
240311

241-
### Open (queued for v1.1 release prep)
242-
243-
1. **Re-run wl_validation with the correct selector** to get a
244-
fresh survivor count after the 2 new tests above. Expected
245-
result: ≤8 survivors (the equivalent mutants), down from 12.
312+
3. ~~**Re-run wl_validation with the correct selector**~~ — DONE.
313+
Result: 10 surviving mutants, all equivalent. See "2026-05-19
314+
fresh-run confirmation" section above. The estimate of "≤8
315+
survivors" was off by 2 — the original triage incorrectly listed
316+
mutants 138 and 146 as "killed by existing tests" when they're
317+
actually equivalent. Doc corrected.
318+
319+
4. ~~**Run wl_csv.py with the correct selector**~~ — DONE.
320+
Result: 547 survivors of 723 mutations (24% kill rate). The
321+
estimate of "5-20 genuine gaps" was wildly low — the actual
322+
genuine-gap count is ~100-150 in the unit-tested core, plus
323+
~400 survivors in integration-only code that won't be killed
324+
by unit tests at all. See "2026-05-19 wl_csv.py fresh-run
325+
results (item D)" section above. Closing the ~100-150 genuine
326+
survivors is deferred to v1.1 item G (test coverage push).
246327

247-
2. **Run wl_csv.py with the correct selector**. Expected: real
248-
survivor count, plausibly 5-20 genuine gaps in CSV diff /
249-
hash-registry logic.
328+
### Open (queued for v1.1 release prep)
250329

251-
3. **Add `bin/wl_audit.py` mutation pass.** The 37 reported survivors
330+
1. **Add `bin/wl_audit.py` mutation pass.** The 37 reported survivors
252331
on `wl_audit.py` from a prior session need the same re-validation
253332
under the correct selector before deciding what to do with them.
254333

scripts/mutmut.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,25 @@ populate_scratch() {
249249
# Running as -u 0 (root) inside the container because /scratch is
250250
# owned by root after tmpfs mount; the default container user
251251
# cannot write to a fresh root-owned mount.
252+
#
253+
# Post-cp: explicitly wipe any stale mutmut artifacts that came
254+
# along from /repo. The 2026-05-19 item D run discovered that a
255+
# prior :rw-mount session had left a `.mutmut-cache` (~190 KB)
256+
# on the host repo. cp -a happily copied it into /scratch, and
257+
# mutmut then aggregated those stale entries into the cumulative
258+
# results table — confusing the wl_csv (correct selector) result
259+
# display with leftover wl_audit / wl_validation entries from
260+
# misconfigured prior runs. The wipe forces every container
261+
# creation to start with a guaranteed-empty mutmut cache.
252262
echo "→ populating /scratch from /repo (tmpfs is fresh)..."
253263
if ! docker exec -u 0 "$CONTAINER" sh -c 'cp -a /repo/. /scratch/'; then
254264
echo "✖ scratch population failed — removing container." >&2
255265
docker rm -f "$CONTAINER" >/dev/null
256266
exit 1
257267
fi
268+
# Wipe any prior mutmut artifacts that may have ridden along from
269+
# the host. Safe to run even if neither exists.
270+
docker exec -u 0 "$CONTAINER" sh -c 'rm -rf /scratch/.mutmut-cache /scratch/mutants' >/dev/null 2>&1 || true
258271
}
259272

260273
scratch_is_empty() {

0 commit comments

Comments
 (0)