From 0d99c33c4edbb712dbd0a04c244b6f483b93c197 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Mon, 20 Apr 2026 13:16:59 -0700 Subject: [PATCH] wip --- src/Plugins/Tia.php | 44 ++++++--- src/Plugins/Tia/CoverageMerger.php | 149 +++++++++++++++++++++++++++++ src/Support/Coverage.php | 6 ++ 3 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 src/Plugins/Tia/CoverageMerger.php diff --git a/src/Plugins/Tia.php b/src/Plugins/Tia.php index a2d6ea5b..348fa1e1 100644 --- a/src/Plugins/Tia.php +++ b/src/Plugins/Tia.php @@ -85,6 +85,21 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable private const string AFFECTED_FILE = 'tia-affected.json'; + /** + * Cache file holding PHPUnit's `CodeCoverage` object from the last + * `--tia --coverage` run. When the next run replays most tests from + * the TIA graph, only the affected tests produce fresh coverage; the + * rest is merged in from this cache so the report stays complete. + */ + private const string COVERAGE_CACHE_FILE = 'tia-coverage.php'; + + /** + * Marker file dropped by `Tia` to tell `Support\Coverage` to apply the + * merge. Absent on plain `--coverage` runs so non-TIA usage keeps its + * current (narrow) behaviour. + */ + private const string COVERAGE_MARKER_FILE = 'tia-coverage.marker'; + private const string WORKER_PREFIX = 'tia-worker-'; private const string WORKER_RESULTS_PREFIX = 'tia-worker-results-'; @@ -196,6 +211,16 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable return self::tempDir().DIRECTORY_SEPARATOR.self::WORKER_RESULTS_PREFIX.'*.json'; } + public static function coverageCachePath(): string + { + return self::tempDir().DIRECTORY_SEPARATOR.self::COVERAGE_CACHE_FILE; + } + + public static function coverageMarkerPath(): string + { + return self::tempDir().DIRECTORY_SEPARATOR.self::COVERAGE_MARKER_FILE; + } + /** * True when TIA is piggybacking on PHPUnit's own coverage driver. Toggled * in `handleArguments` whenever `--tia` runs alongside `--coverage` so @@ -505,20 +530,15 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable } } - // Force record mode whenever `--coverage` is active. Replay short- - // circuits tests via cached results, which would make their code - // paths invisible to PHPUnit's coverage driver and tank the report. - // A `--tia --coverage` run is the one the user wants FULL coverage - // from — we just harvest graph edges alongside, to feed future - // `--tia` (no `--coverage`) runs. - if ($graph instanceof Graph && ! $this->piggybackCoverage) { - return $this->enterReplayMode($graph, $projectRoot, $arguments); + // Drop the marker so `Support\Coverage::report()` knows to merge the + // current (narrow) coverage with the cached full-run snapshot. Plain + // `--coverage` runs don't drop it, so their behaviour is untouched. + if ($this->piggybackCoverage) { + @file_put_contents(self::coverageMarkerPath(), ''); } - if ($graph instanceof Graph && $this->piggybackCoverage) { - $this->output->writeln( - ' TIA `--coverage` active — running full suite and refreshing graph.', - ); + if ($graph instanceof Graph) { + return $this->enterReplayMode($graph, $projectRoot, $arguments); } return $this->enterRecordMode($projectRoot, $arguments); diff --git a/src/Plugins/Tia/CoverageMerger.php b/src/Plugins/Tia/CoverageMerger.php new file mode 100644 index 00000000..3094c8bc --- /dev/null +++ b/src/Plugins/Tia/CoverageMerger.php @@ -0,0 +1,149 @@ +merge($current); + + // Serialise the merged object back using PHPUnit's own "return + // expression" PHP format. Using `var_export` on the serialised + // payload keeps the file self-contained and independent of + // PHPUnit's internal exporter — the reader only needs to + // `require` it back. + $serialised = "getData(); + $lineCoverage = $cachedData->lineCoverage(); + + foreach ($lineCoverage as $file => $lines) { + foreach ($lines as $line => $ids) { + if ($ids === null || $ids === []) { + continue; + } + + $filtered = array_values(array_diff($ids, $currentIds)); + + if ($filtered !== $ids) { + $lineCoverage[$file][$line] = $filtered; + } + } + } + + $cachedData->setLineCoverage($lineCoverage); + } + + /** + * @return array + */ + private static function collectTestIds(CodeCoverage $coverage): array + { + $ids = []; + + foreach ($coverage->getData()->lineCoverage() as $lines) { + foreach ($lines as $hits) { + if ($hits === null) { + continue; + } + + foreach ($hits as $id) { + $ids[$id] = true; + } + } + } + + return array_keys($ids); + } +} diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index a36dd8ed..6845ad4e 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -88,6 +88,12 @@ final class Coverage throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath)); } + // If TIA's marker is present, this run executed only the affected + // tests. Merge their fresh coverage slice into the cached full-run + // snapshot (stored by the previous `--tia --coverage` pass) so the + // report reflects the entire suite, not just what re-ran. + \Pest\Plugins\Tia\CoverageMerger::applyIfMarked($reportPath); + /** @var CodeCoverage $codeCoverage */ $codeCoverage = require $reportPath; unlink($reportPath);