fix(codecov): sweep stale XML files via sentinel-file guard (#125)#260
fix(codecov): sweep stale XML files via sentinel-file guard (#125)#260rossaddison wants to merge 3 commits into
Conversation
…o#125) Per-file XMLs from a previous run were never cleaned when source files were renamed or deleted, leaving Infection (and any other consumer) with ghost entries in the output directory. Add a `.testo-coverage` sentinel file to mark the directory as ours. On every run after the first, the sentinel's presence triggers a recursive sweep of all `*.xml` files before the new set is written. If the sentinel is absent (directory not previously written by this reporter), no sweeping occurs — guarding against accidental data loss. - `generate()`: check/touch sentinel, call `sweepStaleXml()` when present - `sweepStaleXml()`: `RecursiveIteratorIterator` removes `*.xml` files only - Class-level PHPDoc documents the stale-file guard behaviour - Three new tests: re-run same set, re-run after file removed, first-run on non-empty directory without sentinel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
- Concat + ConcatOperandRemoval on sentinelPath: both mutations write the sentinel alongside outputDir rather than inside it (e.g. /tmp/abc.testo- coverage vs /tmp/abc/.testo-coverage). New test sentinelIsWrittenInsideOutputDir asserts file_exists($dir . '/.testo-coverage') and !is_file($dir . '.testo-coverage'), failing when either concat mutation is applied. - LogicalAnd (isFile() && getExtension() === 'xml'): escaped because RecursiveIteratorIterator::LEAVES_ONLY only yields files, making isFile() always true and && vs || equivalent. Removed the redundant isFile() check; added a comment explaining the invariant. No logical change; Infection can no longer apply LogicalAnd where there is no boolean operator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…is mirrored tests/Application/Stub/EmptyRun/ is intentionally empty — it is the test fixture for EmptyRunTest, which asserts that a Testo run over an empty directory yields Status::Risky with zero tests collected. Git does not track empty directories, and bin/build-phpunit.php only copies *.php files when populating the tests/PhpUnit/ mirror, so the mirror never contained tests/PhpUnit/Application/Stub/EmptyRun/. The mirrored EmptyRunTest resolved __DIR__ . '/../../Stub/EmptyRun' to that missing path and threw InvalidArgumentException: File or directory not found — aborting Infection's initial PHPUnit test run on every CI push to 1.x. Add .placeholder.php (no namespace, no classes, no tests) to the source directory. The build script copies it verbatim into the mirror, which creates the required directory. Testo's FinderConfig still discovers zero tests there, so Status::Risky is reported and the assertion holds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR SummaryThree commits, two problems fixed. Issue #125 — stale XML files in
|
| Mutant | Root cause | Fix |
|---|---|---|
Concat / ConcatOperandRemoval on $sentinelPath |
Both mutations place the sentinel alongside outputDir (e.g. /tmp/abc.testo-coverage) rather than inside it (/tmp/abc/.testo-coverage), but find it consistently on re-runs so the sweep still triggers |
sentinelIsWrittenInsideOutputDir asserts file_exists($dir . '/.testo-coverage') and !is_file($dir . '.testo-coverage') |
LogicalAnd → || in sweepStaleXml() |
RecursiveIteratorIterator::LEAVES_ONLY only yields files, so isFile() is always true and && vs || are equivalent |
Removed the redundant isFile() check; added a comment explaining the invariant |
Pre-existing CI fix — EmptyRunTest in PHPUnit mirror
tests/Application/Stub/EmptyRun/ is an intentionally empty directory (only .gitkeep). The PHPUnit mirror build (bin/build-phpunit.php) copies only *.php files, so tests/PhpUnit/Application/Stub/EmptyRun/ was never created in CI. The mirrored EmptyRunTest resolves __DIR__ . '/../../Stub/EmptyRun' to that missing path and threw InvalidArgumentException, aborting Infection's initial PHPUnit run on every push to 1.x since 2026-07-03.
Fix: added .placeholder.php (no namespace, no classes, no tests) to the source directory. The build script copies it into the mirror, creating the required directory. Testo's FinderConfig still discovers zero tests there, so Status::Risky is reported and the existing assertion holds.
Problem
PhpUnitXmlReport::generate()writes per-file XMLs into<outputDir>but never cleans old files between runs. When a source file is renamed or deleted, its corresponding XML remains, leaving Infection (and any other consumer) with ghost entries.Approach — sentinel-file guard
On the first successful run a file
.testo-coverageis created inside<outputDir>. On every subsequent run, if the sentinel is present the directory is known to belong to this reporter, so all*.xmlfiles are removed before the new set is written.If the sentinel is absent (e.g.
<outputDir>is pointed at an unrelated directory), no sweeping takes place — protecting against accidental data loss.This is the approach suggested by the maintainer in issue #125.
Changes
PhpUnitXmlReport::generate()— check/touch sentinel, callsweepStaleXml()when presentPhpUnitXmlReport::sweepStaleXml()—RecursiveIteratorIteratordeletes*.xmlfiles only (non-XML files untouched)private const SENTINEL = '.testo-coverage'— name is a constant for easy discoverabilityreRunSameSourceSetProducesNoStaleFiles— idempotent re-runs don't accumulate filesreRunAfterFileRemovedCleansStaleXml— deleted source file's XML is removed on next runfirstRunOnNonEmptyDirectoryWithoutSentinelLeavesExistingFilesAlone— no sweep without sentinelTest results
All pre-existing tests pass unchanged; no
index.xmlcontent or<file href>semantics were altered.Fixes #125