mirror of
https://github.com/pestphp/pest.git
synced 2026-04-21 06:27:28 +02:00
wip
This commit is contained in:
@ -67,6 +67,7 @@ final readonly class Kernel
|
||||
->add(OutputInterface::class, $output)
|
||||
->add(Container::class, $container)
|
||||
->add(Tia\Recorder::class, new Tia\Recorder)
|
||||
->add(Tia\CoverageCollector::class, new Tia\CoverageCollector)
|
||||
->add(Tia\WatchPatterns::class, new Tia\WatchPatterns)
|
||||
->add(Tia\ResultCollector::class, new Tia\ResultCollector);
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Contracts\Plugins\Terminable;
|
||||
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||
use Pest\Plugins\Tia\ChangedFiles;
|
||||
use Pest\Plugins\Tia\CoverageCollector;
|
||||
use Pest\Plugins\Tia\Fingerprint;
|
||||
use Pest\Plugins\Tia\Graph;
|
||||
use Pest\Plugins\Tia\Recorder;
|
||||
@ -99,6 +100,15 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
*/
|
||||
private const string REPLAYING_GLOBAL = 'TIA_REPLAYING';
|
||||
|
||||
/**
|
||||
* Global flag that tells workers to piggyback on PHPUnit's coverage
|
||||
* driver (set by the parent whenever `--tia --coverage` is used). Workers
|
||||
* can't infer this from their own argv because paratest forwards only
|
||||
* `--coverage-php=<path>` — not the `--coverage` flag Pest's Coverage
|
||||
* plugin inspects.
|
||||
*/
|
||||
private const string PIGGYBACK_COVERAGE_GLOBAL = 'TIA_PIGGYBACK_COVERAGE';
|
||||
|
||||
private bool $graphWritten = false;
|
||||
|
||||
private bool $replayRan = false;
|
||||
@ -186,9 +196,26 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
return self::tempDir().DIRECTORY_SEPARATOR.self::WORKER_RESULTS_PREFIX.'*.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* True when TIA is piggybacking on PHPUnit's own coverage driver. Toggled
|
||||
* in `handleArguments` whenever `--tia` runs alongside `--coverage` so
|
||||
* both the parent and workers read edges from the shared `CodeCoverage`
|
||||
* instance instead of starting a second PCOV / Xdebug session.
|
||||
*/
|
||||
private bool $piggybackCoverage = false;
|
||||
|
||||
/**
|
||||
* True once we have committed to recording in this process — either by
|
||||
* activating our own `Recorder` or by delegating to PHPUnit's coverage
|
||||
* driver via `CoverageCollector`. `terminate()` only flushes when this
|
||||
* is set, so runs that never entered record mode don't poke the graph.
|
||||
*/
|
||||
private bool $recordingActive = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
private readonly Recorder $recorder,
|
||||
private readonly CoverageCollector $coverageCollector,
|
||||
private readonly WatchPatterns $watchPatterns,
|
||||
) {}
|
||||
|
||||
@ -249,16 +276,17 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
$arguments = $this->popArgument(self::OPTION, $arguments);
|
||||
$arguments = $this->popArgument(self::REBUILD_OPTION, $arguments);
|
||||
|
||||
if ($this->coverageReportActive()) {
|
||||
if (! $isWorker) {
|
||||
$this->output->writeln(
|
||||
' <fg=yellow>TIA</> `--coverage` is active — TIA disabled to avoid '.
|
||||
'conflicting with PHPUnit\'s own coverage collection.',
|
||||
);
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
// When `--coverage` is active, piggyback on PHPUnit's CodeCoverage
|
||||
// instead of starting our own PCOV / Xdebug session. Running two
|
||||
// collectors against the same driver corrupts both — so we let
|
||||
// PHPUnit drive, and read per-test edges from the shared instance
|
||||
// at the end of the run via `CoverageCollector`. Workers can't
|
||||
// detect `--coverage` from their own argv (paratest strips it,
|
||||
// keeping only `--coverage-php=<path>`) so the parent broadcasts
|
||||
// via a global.
|
||||
$this->piggybackCoverage = $isWorker
|
||||
? (string) Parallel::getGlobal(self::PIGGYBACK_COVERAGE_GLOBAL) === '1'
|
||||
: $this->coverageReportActive();
|
||||
|
||||
$projectRoot = TestSuite::getInstance()->rootPath;
|
||||
|
||||
@ -285,17 +313,20 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
|
||||
$recorder = $this->recorder;
|
||||
|
||||
if (! $recorder->isActive()) {
|
||||
if (! $this->recordingActive && ! $recorder->isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->graphWritten = true;
|
||||
|
||||
$projectRoot = TestSuite::getInstance()->rootPath;
|
||||
$perTest = $recorder->perTestFiles();
|
||||
$perTest = $this->piggybackCoverage
|
||||
? $this->coverageCollector->perTestFiles()
|
||||
: $recorder->perTestFiles();
|
||||
|
||||
if ($perTest === []) {
|
||||
$recorder->reset();
|
||||
$this->coverageCollector->reset();
|
||||
|
||||
return;
|
||||
}
|
||||
@ -303,6 +334,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
if (Parallel::isWorker()) {
|
||||
$this->flushWorkerPartial($projectRoot, $perTest);
|
||||
$recorder->reset();
|
||||
$this->coverageCollector->reset();
|
||||
|
||||
return;
|
||||
}
|
||||
@ -330,6 +362,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
));
|
||||
|
||||
$recorder->reset();
|
||||
$this->coverageCollector->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -472,10 +505,22 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
}
|
||||
|
||||
if ($graph instanceof Graph) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
if ($graph instanceof Graph && $this->piggybackCoverage) {
|
||||
$this->output->writeln(
|
||||
' <fg=cyan>TIA</> `--coverage` active — running full suite and refreshing graph.',
|
||||
);
|
||||
}
|
||||
|
||||
return $this->enterRecordMode($projectRoot, $arguments);
|
||||
}
|
||||
|
||||
@ -501,6 +546,15 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
// Piggyback: PHPUnit starts its coverage driver, `CoverageCollector`
|
||||
// harvests the per-test edges in `terminate()`. The Recorder stays
|
||||
// idle — starting our own driver would corrupt PHPUnit's data.
|
||||
if ($this->piggybackCoverage) {
|
||||
$this->recordingActive = true;
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
$recorder = $this->recorder;
|
||||
|
||||
if (! $recorder->driverAvailable()) {
|
||||
@ -511,6 +565,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
$recorder->activate();
|
||||
$this->recordingActive = true;
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
@ -656,7 +711,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
{
|
||||
$recorder = $this->recorder;
|
||||
|
||||
if (! $recorder->driverAvailable()) {
|
||||
// Piggyback: PHPUnit's coverage driver is already running under
|
||||
// `--coverage`. We don't need our own driver — `CoverageCollector`
|
||||
// harvests the per-test edges from PHPUnit's shared `CodeCoverage`
|
||||
// at terminate time. Skip the driver check entirely in this mode.
|
||||
if (! $this->piggybackCoverage && ! $recorder->driverAvailable()) {
|
||||
// Both series and parallel record require the coverage driver.
|
||||
// Parallel also requires it because workers inherit the parent's
|
||||
// PHP config — if the parent lacks the driver, workers will too
|
||||
@ -677,8 +736,24 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
|
||||
Parallel::setGlobal(self::RECORDING_GLOBAL, '1');
|
||||
|
||||
if ($this->piggybackCoverage) {
|
||||
Parallel::setGlobal(self::PIGGYBACK_COVERAGE_GLOBAL, '1');
|
||||
}
|
||||
|
||||
$this->output->writeln($this->piggybackCoverage
|
||||
? ' <fg=cyan>TIA</> recording dependency graph in parallel via `--coverage` (first run) — '.
|
||||
'subsequent `--tia` runs will only re-execute affected tests.'
|
||||
: ' <fg=cyan>TIA</> recording dependency graph in parallel (first run) — '.
|
||||
'subsequent `--tia` runs will only re-execute affected tests.');
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
if ($this->piggybackCoverage) {
|
||||
$this->recordingActive = true;
|
||||
|
||||
$this->output->writeln(
|
||||
' <fg=cyan>TIA</> recording dependency graph in parallel (first run) — '.
|
||||
' <fg=cyan>TIA</> recording dependency graph via `--coverage` (first run) — '.
|
||||
'subsequent `--tia` runs will only re-execute affected tests.',
|
||||
);
|
||||
|
||||
@ -686,6 +761,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
}
|
||||
|
||||
$recorder->activate();
|
||||
$this->recordingActive = true;
|
||||
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=cyan>TIA</> recording dependency graph via %s (first run) — '.
|
||||
|
||||
152
src/Plugins/Tia/CoverageCollector.php
Normal file
152
src/Plugins/Tia/CoverageCollector.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use PHPUnit\Runner\CodeCoverage as PhpUnitCodeCoverage;
|
||||
use ReflectionClass;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Extracts per-test file coverage from PHPUnit's shared `CodeCoverage`
|
||||
* instance. Used when TIA piggybacks on `--coverage` instead of starting
|
||||
* its own driver session — both share the same PCOV / Xdebug state, so
|
||||
* running two recorders in parallel would corrupt each other's data.
|
||||
*
|
||||
* PHPUnit tags every coverage sample with the current test's id
|
||||
* (`$test->valueObjectForEvents()->id()`, e.g. `Foo\BarTest::baz`). The
|
||||
* per-file / per-line coverage map therefore already carries everything
|
||||
* we need to rebuild TIA edges at the end of the run.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CoverageCollector
|
||||
{
|
||||
/**
|
||||
* Cached `className → test file` lookups. Class reflection is cheap
|
||||
* individually but the record run can visit tens of thousands of
|
||||
* samples, so the cache matters.
|
||||
*
|
||||
* @var array<string, string|null>
|
||||
*/
|
||||
private array $classFileCache = [];
|
||||
|
||||
/**
|
||||
* Rebuilds the same `absolute test file → list<absolute source file>`
|
||||
* shape that `Recorder::perTestFiles()` exposes, so callers can treat
|
||||
* the two collectors interchangeably when feeding the graph.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public function perTestFiles(): array
|
||||
{
|
||||
if (! PhpUnitCodeCoverage::instance()->isActive()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$lineCoverage = PhpUnitCodeCoverage::instance()
|
||||
->codeCoverage()
|
||||
->getData()
|
||||
->lineCoverage();
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, true>> $edges */
|
||||
$edges = [];
|
||||
|
||||
foreach ($lineCoverage as $sourceFile => $lines) {
|
||||
// Collect the set of tests that hit any line in this file once,
|
||||
// then emit one edge per (testFile, sourceFile) pair. Walking
|
||||
// the lines per test would re-resolve the test file repeatedly.
|
||||
$testIds = [];
|
||||
|
||||
foreach ($lines as $hits) {
|
||||
if ($hits === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($hits as $id) {
|
||||
$testIds[$id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($testIds) as $testId) {
|
||||
$testFile = $this->testIdToFile($testId);
|
||||
|
||||
if ($testFile === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$edges[$testFile][$sourceFile] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$out = [];
|
||||
|
||||
foreach ($edges as $testFile => $sources) {
|
||||
$out[$testFile] = array_keys($sources);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->classFileCache = [];
|
||||
}
|
||||
|
||||
private function testIdToFile(string $testId): ?string
|
||||
{
|
||||
// PHPUnit's test id is `ClassName::methodName` with an optional
|
||||
// `#dataSetName` suffix for data-provider runs. Strip the dataset
|
||||
// part — we only need the class.
|
||||
$hash = strpos($testId, '#');
|
||||
$identifier = $hash === false ? $testId : substr($testId, 0, $hash);
|
||||
|
||||
if (! str_contains($identifier, '::')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$className] = explode('::', $identifier, 2);
|
||||
|
||||
if (array_key_exists($className, $this->classFileCache)) {
|
||||
return $this->classFileCache[$className];
|
||||
}
|
||||
|
||||
$file = $this->resolveClassFile($className);
|
||||
$this->classFileCache[$className] = $file;
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
private function resolveClassFile(string $className): ?string
|
||||
{
|
||||
if (! class_exists($className, false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
// Pest's eval'd test classes expose the original `.php` path on a
|
||||
// static `$__filename`. The eval'd class itself has no file of its
|
||||
// own, so prefer this property when present.
|
||||
if ($reflection->hasProperty('__filename')) {
|
||||
$property = $reflection->getProperty('__filename');
|
||||
|
||||
if ($property->isStatic()) {
|
||||
$value = $property->getValue();
|
||||
|
||||
if (is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$file = $reflection->getFileName();
|
||||
|
||||
return is_string($file) ? $file : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user