mirror of
https://github.com/pestphp/pest.git
synced 2026-04-20 22:20:17 +02:00
wip
This commit is contained in:
@ -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(
|
||||
' <fg=cyan>TIA</> `--coverage` active — running full suite and refreshing graph.',
|
||||
);
|
||||
if ($graph instanceof Graph) {
|
||||
return $this->enterReplayMode($graph, $projectRoot, $arguments);
|
||||
}
|
||||
|
||||
return $this->enterRecordMode($projectRoot, $arguments);
|
||||
|
||||
149
src/Plugins/Tia/CoverageMerger.php
Normal file
149
src/Plugins/Tia/CoverageMerger.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Plugins\Tia;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Merges the current run's PHPUnit coverage into a cached full-suite
|
||||
* snapshot so `--tia --coverage` can produce a complete report after
|
||||
* executing only the affected tests.
|
||||
*
|
||||
* Invoked from `Pest\Support\Coverage::report()` right before the coverage
|
||||
* file is consumed. A marker file dropped by the `Tia` plugin gates the
|
||||
* behaviour — plain `--coverage` runs (no `--tia`) leave the marker absent
|
||||
* and therefore keep their existing semantics.
|
||||
*
|
||||
* Algorithm
|
||||
* ---------
|
||||
* The PHPUnit coverage PHP file unserialises to a `CodeCoverage` object.
|
||||
* Its `ProcessedCodeCoverageData` stores, per source file, per line, the
|
||||
* list of test IDs that covered that line. We:
|
||||
*
|
||||
* 1. Load the cached snapshot (from a previous `--tia --coverage` run).
|
||||
* 2. Strip every test id that re-ran this time from the cached map —
|
||||
* the tests that ran now are the ones whose attribution is fresh.
|
||||
* 3. Merge the current run into the stripped cached snapshot via
|
||||
* `CodeCoverage::merge()`.
|
||||
* 4. Write the merged result back to the report path (so Pest's report
|
||||
* generator sees the full suite) and to the cache path (for the
|
||||
* next invocation).
|
||||
* 5. Remove the marker so subsequent plain `--coverage` runs are
|
||||
* untouched.
|
||||
*
|
||||
* If no cache exists yet (first `--tia --coverage` run on this machine)
|
||||
* we simply save the current file as the cache — nothing to merge yet.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CoverageMerger
|
||||
{
|
||||
public static function applyIfMarked(string $reportPath): void
|
||||
{
|
||||
$markerPath = Tia::coverageMarkerPath();
|
||||
|
||||
if (! is_file($markerPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@unlink($markerPath);
|
||||
|
||||
$cachePath = Tia::coverageCachePath();
|
||||
|
||||
if (! is_file($cachePath)) {
|
||||
// First `--tia --coverage` run: nothing cached yet, the current
|
||||
// report is the full suite itself. Save it verbatim so the next
|
||||
// run has a snapshot to merge against.
|
||||
@copy($reportPath, $cachePath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var CodeCoverage $cached */
|
||||
$cached = require $cachePath;
|
||||
|
||||
/** @var CodeCoverage $current */
|
||||
$current = require $reportPath;
|
||||
} catch (Throwable) {
|
||||
// Corrupt cache or unreadable report — fall back to the plain
|
||||
// PHPUnit behaviour (the existing `require $reportPath` in the
|
||||
// caller still runs against the untouched file).
|
||||
return;
|
||||
}
|
||||
|
||||
self::stripCurrentTestsFromCached($cached, $current);
|
||||
|
||||
$cached->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 = "<?php return unserialize(".var_export(serialize($cached), true).");\n";
|
||||
|
||||
@file_put_contents($reportPath, $serialised);
|
||||
@file_put_contents($cachePath, $serialised);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes from `$cached`'s per-line test attribution any test id that
|
||||
* appears in `$current`. Those tests just ran, so the fresh slice is
|
||||
* authoritative — keeping stale attribution in the cache would claim
|
||||
* a test still covers a line it no longer touches.
|
||||
*/
|
||||
private static function stripCurrentTestsFromCached(CodeCoverage $cached, CodeCoverage $current): void
|
||||
{
|
||||
$currentIds = self::collectTestIds($current);
|
||||
|
||||
if ($currentIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cachedData = $cached->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<int, string>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user