mirror of
https://github.com/pestphp/pest.git
synced 2026-04-20 22:20:17 +02:00
Compare commits
2 Commits
41f11c0ef3
...
c89493dd9b
| Author | SHA1 | Date | |
|---|---|---|---|
| c89493dd9b | |||
| 15035d37ef |
@ -28,6 +28,13 @@ final readonly class BootSubscribers implements Bootstrapper
|
||||
Subscribers\EnsureTiaCoverageIsRecorded::class,
|
||||
Subscribers\EnsureTiaCoverageIsFlushed::class,
|
||||
Subscribers\EnsureTiaResultsAreCollected::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnPassed::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnFailed::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnErrored::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnSkipped::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnIncomplete::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnRisky::class,
|
||||
Subscribers\EnsureTiaAssertionsAreRecordedOnFinished::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -5,17 +5,17 @@ declare(strict_types=1);
|
||||
namespace Pest\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Pest\Contracts\Plugins\BeforeEachable;
|
||||
use Pest\Exceptions\DatasetArgumentsMismatch;
|
||||
use Pest\Panic;
|
||||
use Pest\Plugin\Loader;
|
||||
use Pest\Plugins\Tia\CachedTestResult;
|
||||
use Pest\Plugins\Tia;
|
||||
use Pest\Preset;
|
||||
use Pest\Support\Container;
|
||||
use Pest\Support\ChainableClosure;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Support\Shell;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\Attributes\PostCondition;
|
||||
use PHPUnit\Framework\IncompleteTest;
|
||||
use PHPUnit\Framework\SkippedTest;
|
||||
@ -238,27 +238,30 @@ trait Testable
|
||||
|
||||
$this->__cachedPass = false;
|
||||
|
||||
/** @var BeforeEachable $plugin */
|
||||
foreach (Loader::getPlugins(BeforeEachable::class) as $plugin) {
|
||||
$cached = $plugin->beforeEach(self::$__filename, $this::class.'::'.$this->name());
|
||||
/** @var Tia $tia */
|
||||
$tia = Container::getInstance()->get(Tia::class);
|
||||
$cached = $tia->getCachedResult(self::$__filename, $this::class.'::'.$this->name());
|
||||
|
||||
if ($cached instanceof CachedTestResult) {
|
||||
if ($cached->isSuccess()) {
|
||||
$this->__cachedPass = true;
|
||||
if ($cached !== null) {
|
||||
if ($cached->isSuccess()) {
|
||||
$this->__cachedPass = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-success: throw appropriate exception. PHPUnit catches
|
||||
// it in runBare() and marks the test with the correct status.
|
||||
// This makes skips, failures, incompletes, todos appear in
|
||||
// output exactly as if the test ran.
|
||||
match ($cached->status) {
|
||||
1 => $this->markTestSkipped($cached->message), // skip / todo
|
||||
2 => $this->markTestIncomplete($cached->message), // incomplete
|
||||
default => throw new \PHPUnit\Framework\AssertionFailedError($cached->message ?: 'Cached failure'),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-success: throw the matching PHPUnit exception. Runner
|
||||
// catches it and marks the test with the correct status so
|
||||
// skips, failures, incompletes and todos appear in output
|
||||
// exactly as they did in the cached run.
|
||||
if ($cached->isSkipped()) {
|
||||
$this->markTestSkipped($cached->message());
|
||||
}
|
||||
|
||||
if ($cached->isIncomplete()) {
|
||||
$this->markTestIncomplete($cached->message());
|
||||
}
|
||||
|
||||
throw new AssertionFailedError($cached->message() ?: 'Cached failure');
|
||||
}
|
||||
|
||||
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Contracts\Plugins;
|
||||
|
||||
use Pest\Plugins\Tia\CachedTestResult;
|
||||
|
||||
/**
|
||||
* Plugins implementing this interface are consulted before each test's
|
||||
* `setUp()`. The return value controls what happens:
|
||||
*
|
||||
* - `null` → test proceeds normally.
|
||||
* - `CachedTestResult` → test replays the cached status. For non-success
|
||||
* statuses the appropriate exception is thrown
|
||||
* from `setUp` (PHPUnit handles it natively). For
|
||||
* success, a synthetic assertion is registered and
|
||||
* the body + tearDown are skipped via a flag.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface BeforeEachable
|
||||
{
|
||||
public function beforeEach(string $filename, string $testId): ?CachedTestResult;
|
||||
}
|
||||
@ -5,16 +5,14 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins;
|
||||
|
||||
use Pest\Contracts\Plugins\AddsOutput;
|
||||
use Pest\Contracts\Plugins\BeforeEachable;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Contracts\Plugins\Terminable;
|
||||
use Pest\Plugins\Tia\CachedTestResult;
|
||||
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||
use Pest\Plugins\Tia\ChangedFiles;
|
||||
use Pest\Plugins\Tia\Fingerprint;
|
||||
use Pest\Plugins\Tia\Graph;
|
||||
use Pest\Plugins\Tia\Recorder;
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use Pest\TestCaseFilters\TiaTestCaseFilter;
|
||||
use Pest\Plugins\Tia\WatchPatterns;
|
||||
use Pest\Support\Container;
|
||||
use Pest\TestSuite;
|
||||
@ -64,7 +62,7 @@ use Throwable;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Terminable
|
||||
final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
{
|
||||
use Concerns\HandleArguments;
|
||||
|
||||
@ -103,6 +101,26 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
|
||||
private bool $replayRan = false;
|
||||
|
||||
/**
|
||||
* Counts cache hits during a replay run. Incremented each time
|
||||
* `getCachedResult()` returns a non-null status so the end-of-run
|
||||
* summary reflects what actually happened, not a graph-level estimate.
|
||||
*/
|
||||
private int $replayedCount = 0;
|
||||
|
||||
/**
|
||||
* Captured at replay setup so the end-of-run summary can report the
|
||||
* scope of the changes that drove the run.
|
||||
*/
|
||||
private int $changedFileCount = 0;
|
||||
|
||||
/**
|
||||
* Captured at replay setup — number of tests the graph flagged as
|
||||
* affected (i.e. should re-execute). May overshoot the actually-
|
||||
* executed count when the user narrows with a path filter.
|
||||
*/
|
||||
private int $affectedTestCount = 0;
|
||||
|
||||
/**
|
||||
* Holds the graph during replay so `beforeEach` can look up cached
|
||||
* results without re-loading from disk on every test.
|
||||
@ -162,7 +180,11 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
private readonly WatchPatterns $watchPatterns,
|
||||
) {}
|
||||
|
||||
public function beforeEach(string $filename, string $testId): ?CachedTestResult
|
||||
/**
|
||||
* Returns the cached result for the given test, or `null` if the test
|
||||
* must run (affected, unknown, or no replay mode active).
|
||||
*/
|
||||
public function getCachedResult(string $filename, string $testId): ?TestStatus
|
||||
{
|
||||
if ($this->replayGraph === null) {
|
||||
return null;
|
||||
@ -187,7 +209,13 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
|
||||
// Known + unaffected: return cached result if we have one for this
|
||||
// branch (falls back to main if branch is fresh).
|
||||
return $this->replayGraph->getResult($this->branch, $testId);
|
||||
$result = $this->replayGraph->getResult($this->branch, $testId);
|
||||
|
||||
if ($result !== null) {
|
||||
$this->replayedCount++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -302,6 +330,7 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
// times even though nothing new changed.
|
||||
if ($this->replayRan) {
|
||||
$this->bumpRecordedSha();
|
||||
$this->emitReplaySummary();
|
||||
}
|
||||
|
||||
// Snapshot per-test results (status + message) from PHPUnit's result
|
||||
@ -435,7 +464,7 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
// the parent persisted, then install the per-file filter so
|
||||
// whichever tests paratest happens to hand this worker are
|
||||
// accepted / rejected consistently with the series path.
|
||||
$this->installWorkerReplayFilter($projectRoot);
|
||||
$this->installWorkerReplay($projectRoot);
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
@ -458,7 +487,14 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
private function installWorkerReplayFilter(string $projectRoot): void
|
||||
/**
|
||||
* Wires worker-side replay. Mirrors the series path: sets `replayGraph`
|
||||
* + `affectedFiles` so the `BeforeEachable` hook in `beforeEach()` can
|
||||
* answer per-test. Unaffected tests replay their cached status (pass,
|
||||
* fail, skip, todo, incomplete) so the user sees the full suite report
|
||||
* in parallel runs exactly like in series.
|
||||
*/
|
||||
private function installWorkerReplay(string $projectRoot): void
|
||||
{
|
||||
$cachePath = self::cachePath();
|
||||
$affectedPath = self::affectedPath();
|
||||
@ -489,9 +525,8 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
}
|
||||
}
|
||||
|
||||
TestSuite::getInstance()->tests->addTestCaseFilter(
|
||||
new TiaTestCaseFilter($projectRoot, $graph, $affectedSet),
|
||||
);
|
||||
$this->replayGraph = $graph;
|
||||
$this->affectedFiles = $affectedSet;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -521,11 +556,9 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
|
||||
$affected = $changed === [] ? [] : $graph->affected($changed);
|
||||
|
||||
$totalKnown = count($graph->allTestFiles());
|
||||
$affectedCount = count($affected);
|
||||
$cachedCount = $totalKnown - $affectedCount;
|
||||
$this->changedFileCount = count($changed);
|
||||
$this->affectedTestCount = count($affected);
|
||||
|
||||
$testSuite = TestSuite::getInstance();
|
||||
$affectedSet = array_fill_keys($affected, true);
|
||||
|
||||
$this->replayRan = true;
|
||||
@ -533,13 +566,6 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
$this->affectedFiles = $affectedSet;
|
||||
|
||||
if (! Parallel::isEnabled()) {
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=green>TIA</> %d changed file(s) → %d affected, %d replayed.',
|
||||
count($changed),
|
||||
$affectedCount,
|
||||
$cachedCount,
|
||||
));
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
@ -554,13 +580,6 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
|
||||
Parallel::setGlobal(self::REPLAYING_GLOBAL, '1');
|
||||
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=green>TIA</> %d changed file(s) → %d affected, %d cached (parallel).',
|
||||
count($changed),
|
||||
$affectedCount,
|
||||
$cachedCount,
|
||||
));
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
@ -756,6 +775,22 @@ final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Termina
|
||||
* compares against this baseline so identical files are skipped even if
|
||||
* git still reports them as modified.
|
||||
*/
|
||||
/**
|
||||
* Prints the post-run TIA summary. Runs after the test report so the
|
||||
* replayed count reflects what actually happened (cache hits counted
|
||||
* inside `getCachedResult`) rather than a graph-level estimate that
|
||||
* ignores any CLI path filter the user passed in.
|
||||
*/
|
||||
private function emitReplaySummary(): void
|
||||
{
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=green>TIA</> %d changed file(s) → %d affected, %d replayed.',
|
||||
$this->changedFileCount,
|
||||
$this->affectedTestCount,
|
||||
$this->replayedCount,
|
||||
));
|
||||
}
|
||||
|
||||
private function bumpRecordedSha(): void
|
||||
{
|
||||
$projectRoot = TestSuite::getInstance()->rootPath;
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
/**
|
||||
* Immutable snapshot of a previous test run's outcome. Stored in the TIA
|
||||
* graph and returned by `BeforeEachable::beforeEach` so `Testable` can
|
||||
* faithfully replay the exact status — pass, fail, skip, todo, incomplete,
|
||||
* risky, etc. — without executing the test body.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class CachedTestResult
|
||||
{
|
||||
/**
|
||||
* PHPUnit TestStatus int constants:
|
||||
* 0 = success, 1 = skipped, 2 = incomplete,
|
||||
* 3 = notice, 4 = deprecation, 5 = risky,
|
||||
* 6 = warning, 7 = failure, 8 = error.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $status,
|
||||
public string $message = '',
|
||||
public float $time = 0.0,
|
||||
) {}
|
||||
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->status === 0;
|
||||
}
|
||||
}
|
||||
@ -60,8 +60,13 @@ final readonly class ChangedFiles
|
||||
$absolute = $this->projectRoot.DIRECTORY_SEPARATOR.$file;
|
||||
|
||||
if (! is_file($absolute)) {
|
||||
// File deleted since last run — definitely changed.
|
||||
$remaining[] = $file;
|
||||
// File is absent now. If the snapshot recorded it as absent
|
||||
// too (sentinel ''), state is identical to last run — treat
|
||||
// as unchanged. Otherwise it was present last run and got
|
||||
// deleted since — that's a real change.
|
||||
if ($lastRunTree[$file] !== '') {
|
||||
$remaining[] = $file;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
@ -92,6 +97,11 @@ final readonly class ChangedFiles
|
||||
$absolute = $this->projectRoot.DIRECTORY_SEPARATOR.$file;
|
||||
|
||||
if (! is_file($absolute)) {
|
||||
// Record the deletion with an empty-string sentinel so the
|
||||
// next run recognises "still deleted" as unchanged rather
|
||||
// than re-flagging the file as a fresh change.
|
||||
$out[$file] = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Support\Container;
|
||||
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||
|
||||
/**
|
||||
* File-level Test Impact Analysis graph.
|
||||
@ -264,7 +265,7 @@ final class Graph
|
||||
];
|
||||
}
|
||||
|
||||
public function getResult(string $branch, string $testId, string $fallbackBranch = 'main'): ?CachedTestResult
|
||||
public function getResult(string $branch, string $testId, string $fallbackBranch = 'main'): ?TestStatus
|
||||
{
|
||||
$baseline = $this->baselineFor($branch, $fallbackBranch);
|
||||
|
||||
@ -274,7 +275,21 @@ final class Graph
|
||||
|
||||
$r = $baseline['results'][$testId];
|
||||
|
||||
return new CachedTestResult($r['status'], $r['message'], $r['time']);
|
||||
// PHPUnit's `TestStatus::from(int)` ignores messages, so reconstruct
|
||||
// each variant via its specific factory. Keeps the stored message
|
||||
// intact (important for skips/failures shown to the user).
|
||||
return match ($r['status']) {
|
||||
0 => TestStatus::success(),
|
||||
1 => TestStatus::skipped($r['message']),
|
||||
2 => TestStatus::incomplete($r['message']),
|
||||
3 => TestStatus::notice($r['message']),
|
||||
4 => TestStatus::deprecation($r['message']),
|
||||
5 => TestStatus::risky($r['message']),
|
||||
6 => TestStatus::warning($r['message']),
|
||||
7 => TestStatus::failure($r['message']),
|
||||
8 => TestStatus::error($r['message']),
|
||||
default => TestStatus::unknown(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -14,7 +14,7 @@ namespace Pest\Plugins\Tia;
|
||||
final class ResultCollector
|
||||
{
|
||||
/**
|
||||
* @var array<string, array{status: int, message: string, time: float}>
|
||||
* @var array<string, array{status: int, message: string, time: float, assertions: int}>
|
||||
*/
|
||||
private array $results = [];
|
||||
|
||||
@ -83,13 +83,20 @@ final class ResultCollector
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{status: int, message: string, time: float}>
|
||||
* @return array<string, array{status: int, message: string, time: float, assertions: int}>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
public function recordAssertions(string $testId, int $assertions): void
|
||||
{
|
||||
if (isset($this->results[$testId])) {
|
||||
$this->results[$testId]['assertions'] = $assertions;
|
||||
}
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->results = [];
|
||||
@ -111,6 +118,7 @@ final class ResultCollector
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'time' => $time,
|
||||
'assertions' => 0,
|
||||
];
|
||||
|
||||
$this->currentTestId = null;
|
||||
|
||||
34
src/Subscribers/EnsureTiaAssertionsAreRecordedOnFinished.php
Normal file
34
src/Subscribers/EnsureTiaAssertionsAreRecordedOnFinished.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Code\TestMethod;
|
||||
use PHPUnit\Event\Test\Finished;
|
||||
use PHPUnit\Event\Test\FinishedSubscriber;
|
||||
|
||||
/**
|
||||
* Fires last for each test, after the outcome subscribers. Records the exact
|
||||
* assertion count so replay can emit the same `addToAssertionCount()` instead
|
||||
* of a hardcoded value.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaAssertionsAreRecordedOnFinished implements FinishedSubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(Finished $event): void
|
||||
{
|
||||
$test = $event->test();
|
||||
|
||||
if ($test instanceof TestMethod) {
|
||||
$this->collector->recordAssertions(
|
||||
$test->className().'::'.$test->methodName(),
|
||||
$event->numberOfAssertionsPerformed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Subscribers/EnsureTiaResultIsRecordedOnErrored.php
Normal file
22
src/Subscribers/EnsureTiaResultIsRecordedOnErrored.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Test\Errored;
|
||||
use PHPUnit\Event\Test\ErroredSubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaResultIsRecordedOnErrored implements ErroredSubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(Errored $event): void
|
||||
{
|
||||
$this->collector->testErrored($event->throwable()->message());
|
||||
}
|
||||
}
|
||||
22
src/Subscribers/EnsureTiaResultIsRecordedOnFailed.php
Normal file
22
src/Subscribers/EnsureTiaResultIsRecordedOnFailed.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Test\Failed;
|
||||
use PHPUnit\Event\Test\FailedSubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaResultIsRecordedOnFailed implements FailedSubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(Failed $event): void
|
||||
{
|
||||
$this->collector->testFailed($event->throwable()->message());
|
||||
}
|
||||
}
|
||||
22
src/Subscribers/EnsureTiaResultIsRecordedOnIncomplete.php
Normal file
22
src/Subscribers/EnsureTiaResultIsRecordedOnIncomplete.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
||||
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaResultIsRecordedOnIncomplete implements MarkedIncompleteSubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(MarkedIncomplete $event): void
|
||||
{
|
||||
$this->collector->testIncomplete($event->throwable()->message());
|
||||
}
|
||||
}
|
||||
22
src/Subscribers/EnsureTiaResultIsRecordedOnPassed.php
Normal file
22
src/Subscribers/EnsureTiaResultIsRecordedOnPassed.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Test\Passed;
|
||||
use PHPUnit\Event\Test\PassedSubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaResultIsRecordedOnPassed implements PassedSubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(Passed $event): void
|
||||
{
|
||||
$this->collector->testPassed();
|
||||
}
|
||||
}
|
||||
22
src/Subscribers/EnsureTiaResultIsRecordedOnRisky.php
Normal file
22
src/Subscribers/EnsureTiaResultIsRecordedOnRisky.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Test\ConsideredRisky;
|
||||
use PHPUnit\Event\Test\ConsideredRiskySubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaResultIsRecordedOnRisky implements ConsideredRiskySubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(ConsideredRisky $event): void
|
||||
{
|
||||
$this->collector->testRisky($event->message());
|
||||
}
|
||||
}
|
||||
22
src/Subscribers/EnsureTiaResultIsRecordedOnSkipped.php
Normal file
22
src/Subscribers/EnsureTiaResultIsRecordedOnSkipped.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Test\Skipped;
|
||||
use PHPUnit\Event\Test\SkippedSubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaResultIsRecordedOnSkipped implements SkippedSubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(Skipped $event): void
|
||||
{
|
||||
$this->collector->testSkipped($event->message());
|
||||
}
|
||||
}
|
||||
@ -6,81 +6,30 @@ namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Plugins\Tia\ResultCollector;
|
||||
use PHPUnit\Event\Code\TestMethod;
|
||||
use PHPUnit\Event\Test\ConsideredRisky;
|
||||
use PHPUnit\Event\Test\ConsideredRiskySubscriber;
|
||||
use PHPUnit\Event\Test\Errored;
|
||||
use PHPUnit\Event\Test\ErroredSubscriber;
|
||||
use PHPUnit\Event\Test\Failed;
|
||||
use PHPUnit\Event\Test\FailedSubscriber;
|
||||
use PHPUnit\Event\Test\MarkedIncomplete;
|
||||
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
|
||||
use PHPUnit\Event\Test\Passed;
|
||||
use PHPUnit\Event\Test\PassedSubscriber;
|
||||
use PHPUnit\Event\Test\Prepared;
|
||||
use PHPUnit\Event\Test\PreparedSubscriber;
|
||||
use PHPUnit\Event\Test\Skipped;
|
||||
use PHPUnit\Event\Test\SkippedSubscriber;
|
||||
|
||||
/**
|
||||
* Feeds per-test outcomes (status + message + time) into the TIA
|
||||
* `ResultCollector` so the graph can persist them for faithful replay.
|
||||
* Starts a per-test recording window on Prepared. Sibling subscribers
|
||||
* (`EnsureTia*`) close it with the outcome and the assertion count so the
|
||||
* graph can persist everything needed for faithful replay.
|
||||
*
|
||||
* Why one subscriber per event: PHPUnit's `TypeMap::map()` picks only the
|
||||
* first subscriber interface it finds on a class, so one class cannot fan
|
||||
* out to multiple events — each event needs its own subscriber class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureTiaResultsAreCollected implements
|
||||
ConsideredRiskySubscriber,
|
||||
ErroredSubscriber,
|
||||
FailedSubscriber,
|
||||
MarkedIncompleteSubscriber,
|
||||
PassedSubscriber,
|
||||
PreparedSubscriber,
|
||||
SkippedSubscriber
|
||||
final class EnsureTiaResultsAreCollected implements PreparedSubscriber
|
||||
{
|
||||
public function __construct(private readonly ResultCollector $collector) {}
|
||||
|
||||
public function notify(Prepared|Passed|Failed|Errored|Skipped|MarkedIncomplete|ConsideredRisky $event): void
|
||||
public function notify(Prepared $event): void
|
||||
{
|
||||
if ($event instanceof Prepared) {
|
||||
$test = $event->test();
|
||||
$test = $event->test();
|
||||
|
||||
if ($test instanceof TestMethod) {
|
||||
$this->collector->testPrepared($test->className().'::'.$test->methodName());
|
||||
}
|
||||
|
||||
return;
|
||||
if ($test instanceof TestMethod) {
|
||||
$this->collector->testPrepared($test->className().'::'.$test->methodName());
|
||||
}
|
||||
|
||||
if ($event instanceof Passed) {
|
||||
$this->collector->testPassed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof Failed) {
|
||||
$this->collector->testFailed($event->throwable()->message());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof Errored) {
|
||||
$this->collector->testErrored($event->throwable()->message());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof Skipped) {
|
||||
$this->collector->testSkipped($event->message());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof MarkedIncomplete) {
|
||||
$this->collector->testIncomplete($event->throwable()->message());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Last possible type: ConsideredRisky (all others returned above).
|
||||
$this->collector->testRisky($event->message()); // @phpstan-ignore method.notFound
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\TestCaseFilters;
|
||||
|
||||
use Pest\Contracts\TestCaseFilter;
|
||||
use Pest\Plugins\Tia\Graph;
|
||||
|
||||
/**
|
||||
* Accepts a test file in one of three cases:
|
||||
*
|
||||
* 1. The file falls outside the project root (we cannot reason about it, so
|
||||
* stay safe and run it).
|
||||
* 2. The graph has no record of the file — this is a new test that was
|
||||
* never part of a recording run, so we accept it by default. Skipping
|
||||
* unknown tests would be a correctness hazard (developers add tests and
|
||||
* TIA would silently not run them).
|
||||
* 3. The graph knows the file AND it is in the affected set.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class TiaTestCaseFilter implements TestCaseFilter
|
||||
{
|
||||
/**
|
||||
* @param array<string, true> $affectedTestFiles Keys are project-relative test file paths.
|
||||
*/
|
||||
public function __construct(
|
||||
private string $projectRoot,
|
||||
private Graph $graph,
|
||||
private array $affectedTestFiles,
|
||||
) {}
|
||||
|
||||
public function accept(string $testCaseFilename): bool
|
||||
{
|
||||
$rel = $this->relative($testCaseFilename);
|
||||
|
||||
if ($rel === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->graph->knowsTest($rel)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isset($this->affectedTestFiles[$rel]);
|
||||
}
|
||||
|
||||
private function relative(string $path): ?string
|
||||
{
|
||||
$real = @realpath($path);
|
||||
|
||||
if ($real === false) {
|
||||
$real = $path;
|
||||
}
|
||||
|
||||
$root = rtrim($this->projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
|
||||
if (! str_starts_with($real, $root)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return str_replace(DIRECTORY_SEPARATOR, '/', substr($real, strlen($root)));
|
||||
}
|
||||
}
|
||||
@ -37,7 +37,6 @@ arch('contracts')
|
||||
->toOnlyUse([
|
||||
'NunoMaduro\Collision\Contracts',
|
||||
'Pest\Factories\TestCaseMethodFactory',
|
||||
'Pest\Plugins\Tia\CachedTestResult',
|
||||
'Symfony\Component\Console',
|
||||
'Pest\Arch\Contracts',
|
||||
'Pest\PendingCalls',
|
||||
|
||||
Reference in New Issue
Block a user