feat(tia): continues to work on poc

This commit is contained in:
nuno maduro
2026-04-16 10:59:06 -07:00
parent e91634ff05
commit 41f11c0ef3
11 changed files with 668 additions and 20 deletions

View File

@ -27,6 +27,7 @@ final readonly class BootSubscribers implements Bootstrapper
Subscribers\EnsureTeamCityEnabled::class,
Subscribers\EnsureTiaCoverageIsRecorded::class,
Subscribers\EnsureTiaCoverageIsFlushed::class,
Subscribers\EnsureTiaResultsAreCollected::class,
];
/**

View File

@ -5,8 +5,11 @@ 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\Preset;
use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace;
@ -75,6 +78,12 @@ trait Testable
*/
public bool $__ran = false;
/**
* Set when a `BeforeEachable` plugin returns a cached success result.
* Checked in `__runTest` and `tearDown` to skip body + cleanup.
*/
private bool $__cachedPass = false;
/**
* The test's test closure.
*/
@ -227,6 +236,31 @@ trait Testable
{
TestSuite::getInstance()->test = $this;
$this->__cachedPass = false;
/** @var BeforeEachable $plugin */
foreach (Loader::getPlugins(BeforeEachable::class) as $plugin) {
$cached = $plugin->beforeEach(self::$__filename, $this::class.'::'.$this->name());
if ($cached instanceof CachedTestResult) {
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'),
};
}
}
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$description = $method->description;
@ -302,6 +336,12 @@ trait Testable
*/
protected function tearDown(...$arguments): void
{
if ($this->__cachedPass) {
TestSuite::getInstance()->test = null;
return;
}
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
if ($this->__afterEach instanceof Closure) {
@ -327,6 +367,12 @@ trait Testable
*/
private function __runTest(Closure $closure, ...$args): mixed
{
if ($this->__cachedPass) {
$this->addToAssertionCount(1);
return null;
}
$arguments = $this->__resolveTestArguments($args);
$this->__ensureDatasetArgumentNameAndNumberMatches($arguments);

View File

@ -0,0 +1,25 @@
<?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;
}

View File

@ -67,7 +67,8 @@ final readonly class Kernel
->add(OutputInterface::class, $output)
->add(Container::class, $container)
->add(Tia\Recorder::class, new Tia\Recorder)
->add(Tia\WatchPatterns::class, new Tia\WatchPatterns);
->add(Tia\WatchPatterns::class, new Tia\WatchPatterns)
->add(Tia\ResultCollector::class, new Tia\ResultCollector);
$kernel = new self(
new Application,

View File

@ -5,12 +5,15 @@ 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 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;
@ -61,7 +64,7 @@ use Throwable;
*
* @internal
*/
final class Tia implements AddsOutput, HandlesArguments, Terminable
final class Tia implements AddsOutput, BeforeEachable, HandlesArguments, Terminable
{
use Concerns\HandleArguments;
@ -98,6 +101,28 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private bool $graphWritten = false;
private bool $replayRan = false;
/**
* Holds the graph during replay so `beforeEach` can look up cached
* results without re-loading from disk on every test.
*/
private ?Graph $replayGraph = null;
/**
* Current git branch (or `HEAD` SHA when detached). Resolved once per
* run so all graph accesses use the same branch key.
*/
private string $branch = 'main';
/**
* Test files that are affected (should re-execute). Keyed by
* project-relative path. Set during `enterReplayMode`.
*
* @var array<string, true>
*/
private array $affectedFiles = [];
private static function tempDir(): string
{
$dir = (string) realpath(self::TEMP_DIR);
@ -137,6 +162,34 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private readonly WatchPatterns $watchPatterns,
) {}
public function beforeEach(string $filename, string $testId): ?CachedTestResult
{
if ($this->replayGraph === null) {
return null;
}
// Resolve file to project-relative path.
$projectRoot = TestSuite::getInstance()->rootPath;
$real = @realpath($filename);
$rel = $real !== false
? str_replace(DIRECTORY_SEPARATOR, '/', substr($real, strlen(rtrim($projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR)))
: null;
// Affected files must re-execute.
if ($rel !== null && isset($this->affectedFiles[$rel])) {
return null;
}
// Unknown files (not in graph) must execute — they're new.
if ($rel === null || ! $this->replayGraph->knowsTest($rel)) {
return null;
}
// 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);
}
/**
* {@inheritDoc}
*/
@ -211,7 +264,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$graph = Graph::load($projectRoot, $cachePath) ?? new Graph($projectRoot);
$graph->setFingerprint(Fingerprint::compute($projectRoot));
$graph->setRecordedAtSha((new ChangedFiles($projectRoot))->currentSha());
$graph->setRecordedAtSha($this->branch, (new ChangedFiles($projectRoot))->currentSha());
$graph->replaceEdges($perTest);
$graph->pruneMissingTests();
@ -242,6 +295,20 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return $exitCode;
}
// After a successful replay run, advance the recorded SHA to HEAD
// so the next run only diffs against what changed since NOW, not
// since the original recording. Without this, re-running `--tia`
// twice in a row would re-execute the same affected tests both
// times even though nothing new changed.
if ($this->replayRan) {
$this->bumpRecordedSha();
}
// Snapshot per-test results (status + message) from PHPUnit's result
// cache into our graph so future replay runs can faithfully reproduce
// pass/fail/skip/todo/incomplete for unaffected tests.
$this->snapshotTestResults();
if ((string) Parallel::getGlobal(self::RECORDING_GLOBAL) !== '1') {
return $exitCode;
}
@ -257,7 +324,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$graph = Graph::load($projectRoot, $cachePath) ?? new Graph($projectRoot);
$graph->setFingerprint(Fingerprint::compute($projectRoot));
$graph->setRecordedAtSha((new ChangedFiles($projectRoot))->currentSha());
$graph->setRecordedAtSha($this->branch, (new ChangedFiles($projectRoot))->currentSha());
$merged = [];
@ -317,6 +384,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
// this point).
$this->watchPatterns->useDefaults($projectRoot);
// Resolve current branch once per run so every baseline lookup uses
// the same key. Detached HEAD (or no git) falls back to `main` as
// the implicit branch identity.
$this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
$cachePath = self::cachePath();
$fingerprint = Fingerprint::compute($projectRoot);
@ -331,10 +403,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
if ($graph instanceof Graph) {
$changedFiles = new ChangedFiles($projectRoot);
$branchSha = $graph->recordedAtSha($this->branch);
if ($changedFiles->gitAvailable()
&& $graph->recordedAtSha() !== null
&& $changedFiles->since($graph->recordedAtSha()) === null) {
&& $branchSha !== null
&& $changedFiles->since($branchSha) === null) {
$this->output->writeln(
' <fg=yellow>TIA</> recorded commit is no longer reachable — graph will be rebuilt.',
);
@ -355,6 +428,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
*/
private function handleWorker(array $arguments, string $projectRoot, bool $recordingGlobal, bool $replayingGlobal): array
{
$this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
if ($replayingGlobal) {
// Replay in a worker: load the graph and the affected set that
// the parent persisted, then install the per-file filter so
@ -435,7 +510,15 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return $arguments;
}
$changed = $changedFiles->since($graph->recordedAtSha()) ?? [];
$changed = $changedFiles->since($graph->recordedAtSha($this->branch)) ?? [];
// Drop files whose content hash matches the last-run snapshot. This
// is the "dirty but identical" filter: if a file is uncommitted but
// its content hasn't moved since the last `--tia` invocation, its
// dependents already re-ran last time and don't need re-running
// again.
$changed = $changedFiles->filterUnchangedSinceLastRun($changed, $graph->lastRunTree($this->branch));
$affected = $changed === [] ? [] : $graph->affected($changed);
$totalKnown = count($graph->allTestFiles());
@ -445,13 +528,13 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$testSuite = TestSuite::getInstance();
$affectedSet = array_fill_keys($affected, true);
if (! Parallel::isEnabled()) {
$testSuite->tests->addTestCaseFilter(
new TiaTestCaseFilter($projectRoot, $graph, $affectedSet),
);
$this->replayRan = true;
$this->replayGraph = $graph;
$this->affectedFiles = $affectedSet;
if (! Parallel::isEnabled()) {
$this->output->writeln(sprintf(
' <fg=green>TIA</> %d changed file(s) → %d affected, %d cached.',
' <fg=green>TIA</> %d changed file(s) → %d affected, %d replayed.',
count($changed),
$affectedCount,
$cachedCount,
@ -659,6 +742,80 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return $out;
}
/**
* After a successful replay, bump the graph's `recorded_at_sha` to the
* current HEAD. This way the next `--tia` run diffs only against what
* changed since THIS run, not since the original recording.
*
* The graph edges themselves are untouched — only the SHA marker moves.
*/
/**
* After a successful replay, advance the baseline: bump `recorded_at_sha`
* to the current HEAD (handles committed changes) and snapshot the
* working tree's content hashes (handles uncommitted changes). Next run
* compares against this baseline so identical files are skipped even if
* git still reports them as modified.
*/
private function bumpRecordedSha(): void
{
$projectRoot = TestSuite::getInstance()->rootPath;
$cachePath = self::cachePath();
$graph = Graph::load($projectRoot, $cachePath);
if (! $graph instanceof Graph) {
return;
}
$changedFiles = new ChangedFiles($projectRoot);
$currentSha = $changedFiles->currentSha();
if ($currentSha !== null) {
$graph->setRecordedAtSha($this->branch, $currentSha);
}
// Snapshot the working tree: hash every currently-modified file.
// On next run, files still appearing as modified but whose hash
// matches this snapshot are treated as unchanged.
$workingTreeFiles = $changedFiles->since($currentSha) ?? [];
$graph->setLastRunTree($this->branch, $changedFiles->snapshotTree($workingTreeFiles));
$graph->save($cachePath);
}
/**
* Merges per-test status + message from the `ResultCollector` into the
* TIA graph. Runs after every `--tia` invocation so the graph always has
* fresh results for faithful replay (pass, fail, skip, todo, etc.).
*/
private function snapshotTestResults(): void
{
/** @var ResultCollector $collector */
$collector = Container::getInstance()->get(ResultCollector::class);
$results = $collector->all();
if ($results === []) {
return;
}
$cachePath = self::cachePath();
$projectRoot = TestSuite::getInstance()->rootPath;
$graph = Graph::load($projectRoot, $cachePath);
if (! $graph instanceof Graph) {
return;
}
foreach ($results as $testId => $result) {
$graph->setResult($this->branch, $testId, $result['status'], $result['message'], $result['time']);
}
$graph->save($cachePath);
$collector->reset();
}
private function coverageReportActive(): bool
{
try {

View File

@ -0,0 +1,33 @@
<?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;
}
}

View File

@ -32,6 +32,84 @@ final readonly class ChangedFiles
* from HEAD (rebase / force-push) — in
* that case the graph should be rebuilt.
*/
/**
* Removes files whose current content hash matches the snapshot from the
* last `--tia` run. Used to ignore "dirty but unchanged" files — a file
* that git still reports as modified but whose content is bit-identical
* to the previous TIA invocation.
*
* @param array<int, string> $files project-relative paths.
* @param array<string, string> $lastRunTree path → content hash from last run.
* @return array<int, string>
*/
public function filterUnchangedSinceLastRun(array $files, array $lastRunTree): array
{
if ($lastRunTree === []) {
return $files;
}
$remaining = [];
foreach ($files as $file) {
if (! isset($lastRunTree[$file])) {
$remaining[] = $file;
continue;
}
$absolute = $this->projectRoot.DIRECTORY_SEPARATOR.$file;
if (! is_file($absolute)) {
// File deleted since last run — definitely changed.
$remaining[] = $file;
continue;
}
$hash = @hash_file('xxh128', $absolute);
if ($hash === false || $hash !== $lastRunTree[$file]) {
$remaining[] = $file;
}
}
return $remaining;
}
/**
* Computes content hashes for the given project-relative files. Used to
* snapshot the working tree after a successful run so the next run can
* detect which files are actually different.
*
* @param array<int, string> $files
* @return array<string, string> path → xxh128 content hash
*/
public function snapshotTree(array $files): array
{
$out = [];
foreach ($files as $file) {
$absolute = $this->projectRoot.DIRECTORY_SEPARATOR.$file;
if (! is_file($absolute)) {
continue;
}
$hash = @hash_file('xxh128', $absolute);
if ($hash !== false) {
$out[$file] = $hash;
}
}
return $out;
}
/**
* @return array<int, string>|null `null` when git is unavailable, or when
* the recorded SHA is no longer reachable
* from HEAD (rebase / force-push).
*/
public function since(?string $sha): ?array
{
if (! $this->gitAvailable()) {
@ -87,6 +165,24 @@ final readonly class ChangedFiles
return false;
}
public function currentBranch(): ?string
{
if (! $this->gitAvailable()) {
return null;
}
$process = new Process(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], $this->projectRoot);
$process->run();
if (! $process->isSuccessful()) {
return null;
}
$branch = trim($process->getOutput());
return $branch === '' || $branch === 'HEAD' ? null : $branch;
}
public function gitAvailable(): bool
{
$process = new Process(['git', 'rev-parse', '--git-dir'], $this->projectRoot);

View File

@ -47,9 +47,22 @@ final class Graph
private array $fingerprint = [];
/**
* Commit SHA the graph was recorded against (if in a git repo).
* Per-branch baselines. Each branch independently tracks:
* - `sha` — last HEAD at which `--tia` ran on this branch
* - `tree` — content hashes of modified files at that point
* - `results` — per-test status + message + time
*
* Graph edges (test → source) stay shared across branches because
* structure doesn't change per branch. Only run-state is per-branch so
* a failing test on one branch doesn't poison another branch's replay.
*
* @var array<string, array{
* sha: ?string,
* tree: array<string, string>,
* results: array<string, array{status: int, message: string, time: float}>
* }>
*/
private ?string $recordedAtSha = null;
private array $baselines = [];
/**
* Canonicalised project root. Resolved through `realpath()` so paths
@ -224,14 +237,84 @@ final class Graph
return $this->fingerprint;
}
public function setRecordedAtSha(?string $sha): void
/**
* Returns the SHA the given branch last ran against, or falls back to
* `$fallbackBranch` (typically `main`) when this branch has no baseline
* yet. That way a freshly-created feature branch inherits main's
* baseline on its first run.
*/
public function recordedAtSha(string $branch, string $fallbackBranch = 'main'): ?string
{
$this->recordedAtSha = $sha;
$baseline = $this->baselineFor($branch, $fallbackBranch);
return $baseline['sha'];
}
public function recordedAtSha(): ?string
public function setRecordedAtSha(string $branch, ?string $sha): void
{
return $this->recordedAtSha;
$this->ensureBaseline($branch);
$this->baselines[$branch]['sha'] = $sha;
}
public function setResult(string $branch, string $testId, int $status, string $message, float $time): void
{
$this->ensureBaseline($branch);
$this->baselines[$branch]['results'][$testId] = [
'status' => $status, 'message' => $message, 'time' => $time,
];
}
public function getResult(string $branch, string $testId, string $fallbackBranch = 'main'): ?CachedTestResult
{
$baseline = $this->baselineFor($branch, $fallbackBranch);
if (! isset($baseline['results'][$testId])) {
return null;
}
$r = $baseline['results'][$testId];
return new CachedTestResult($r['status'], $r['message'], $r['time']);
}
/**
* @param array<string, string> $tree project-relative path → content hash
*/
public function setLastRunTree(string $branch, array $tree): void
{
$this->ensureBaseline($branch);
$this->baselines[$branch]['tree'] = $tree;
}
/**
* @return array<string, string>
*/
public function lastRunTree(string $branch, string $fallbackBranch = 'main'): array
{
return $this->baselineFor($branch, $fallbackBranch)['tree'];
}
/**
* @return array{sha: ?string, tree: array<string, string>, results: array<string, array{status: int, message: string, time: float}>}
*/
private function baselineFor(string $branch, string $fallbackBranch): array
{
if (isset($this->baselines[$branch])) {
return $this->baselines[$branch];
}
if ($branch !== $fallbackBranch && isset($this->baselines[$fallbackBranch])) {
return $this->baselines[$fallbackBranch];
}
return ['sha' => null, 'tree' => [], 'results' => []];
}
private function ensureBaseline(string $branch): void
{
if (! isset($this->baselines[$branch])) {
$this->baselines[$branch] = ['sha' => null, 'tree' => [], 'results' => []];
}
}
/**
@ -296,10 +379,10 @@ final class Graph
$graph = new self($projectRoot);
$graph->fingerprint = is_array($data['fingerprint'] ?? null) ? $data['fingerprint'] : [];
$graph->recordedAtSha = is_string($data['recorded_at_sha'] ?? null) ? $data['recorded_at_sha'] : null;
$graph->files = is_array($data['files'] ?? null) ? array_values($data['files']) : [];
$graph->fileIds = array_flip($graph->files);
$graph->edges = is_array($data['edges'] ?? null) ? $data['edges'] : [];
$graph->baselines = is_array($data['baselines'] ?? null) ? $data['baselines'] : [];
return $graph;
}
@ -315,9 +398,9 @@ final class Graph
$payload = [
'schema' => 1,
'fingerprint' => $this->fingerprint,
'recorded_at_sha' => $this->recordedAtSha,
'files' => $this->files,
'edges' => $this->edges,
'baselines' => $this->baselines,
];
$tmp = $path.'.'.bin2hex(random_bytes(4)).'.tmp';

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Tia;
/**
* Collects per-test status + message during the run so the graph can persist
* them for faithful replay. PHPUnit's own result cache discards messages
* during serialisation — this collector retains them.
*
* @internal
*/
final class ResultCollector
{
/**
* @var array<string, array{status: int, message: string, time: float}>
*/
private array $results = [];
private ?string $currentTestId = null;
private ?float $startTime = null;
public function testPrepared(string $testId): void
{
$this->currentTestId = $testId;
$this->startTime = microtime(true);
}
public function testPassed(): void
{
if ($this->currentTestId === null) {
return;
}
$this->record(0, '');
}
public function testFailed(string $message): void
{
if ($this->currentTestId === null) {
return;
}
$this->record(7, $message);
}
public function testErrored(string $message): void
{
if ($this->currentTestId === null) {
return;
}
$this->record(8, $message);
}
public function testSkipped(string $message): void
{
if ($this->currentTestId === null) {
return;
}
$this->record(1, $message);
}
public function testIncomplete(string $message): void
{
if ($this->currentTestId === null) {
return;
}
$this->record(2, $message);
}
public function testRisky(string $message): void
{
if ($this->currentTestId === null) {
return;
}
$this->record(5, $message);
}
/**
* @return array<string, array{status: int, message: string, time: float}>
*/
public function all(): array
{
return $this->results;
}
public function reset(): void
{
$this->results = [];
$this->currentTestId = null;
$this->startTime = null;
}
private function record(int $status, string $message): void
{
if ($this->currentTestId === null) {
return;
}
$time = $this->startTime !== null
? round(microtime(true) - $this->startTime, 3)
: 0.0;
$this->results[$this->currentTestId] = [
'status' => $status,
'message' => $message,
'time' => $time,
];
$this->currentTestId = null;
$this->startTime = null;
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
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.
*
* @internal
*/
final class EnsureTiaResultsAreCollected implements
ConsideredRiskySubscriber,
ErroredSubscriber,
FailedSubscriber,
MarkedIncompleteSubscriber,
PassedSubscriber,
PreparedSubscriber,
SkippedSubscriber
{
public function __construct(private readonly ResultCollector $collector) {}
public function notify(Prepared|Passed|Failed|Errored|Skipped|MarkedIncomplete|ConsideredRisky $event): void
{
if ($event instanceof Prepared) {
$test = $event->test();
if ($test instanceof TestMethod) {
$this->collector->testPrepared($test->className().'::'.$test->methodName());
}
return;
}
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
}
}

View File

@ -37,6 +37,7 @@ arch('contracts')
->toOnlyUse([
'NunoMaduro\Collision\Contracts',
'Pest\Factories\TestCaseMethodFactory',
'Pest\Plugins\Tia\CachedTestResult',
'Symfony\Component\Console',
'Pest\Arch\Contracts',
'Pest\PendingCalls',