This commit is contained in:
nuno maduro
2026-05-02 09:49:33 +01:00
parent 6407c4f78f
commit c38d32ae86
7 changed files with 155 additions and 32 deletions

View File

@ -25,6 +25,7 @@ final readonly class BootSubscribers implements Bootstrapper
Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
Subscribers\EnsureKernelDumpIsFlushed::class, Subscribers\EnsureKernelDumpIsFlushed::class,
Subscribers\EnsureTeamCityEnabled::class, Subscribers\EnsureTeamCityEnabled::class,
Subscribers\EnsureTiaIsRunningPestTestsOnly::class,
Subscribers\EnsureTiaCoverageIsRecorded::class, Subscribers\EnsureTiaCoverageIsRecorded::class,
Subscribers\EnsureTiaCoverageIsFlushed::class, Subscribers\EnsureTiaCoverageIsFlushed::class,
Subscribers\EnsureTiaResultsAreCollected::class, Subscribers\EnsureTiaResultsAreCollected::class,

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TiaRequiresPestTests extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct(private readonly string $className, private readonly string $file)
{
parent::__construct(sprintf(
'Tia mode requires Pest tests, but encountered PHPUnit class [%s] in [%s].',
$className,
$file,
));
}
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=red> ERROR </> Tia mode requires Pest tests.',
'',
sprintf(' Encountered PHPUnit class <fg=yellow>%s</>', $this->className),
sprintf(' in <fg=gray>%s</>.', $this->file),
'',
' Convert it to a Pest test, or run without Tia.',
'',
]);
}
public function exitCode(): int
{
return 1;
}
}

View File

@ -787,18 +787,18 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
} }
$affectedFromChanges = $changed === [] ? [] : $graph->affected($changed); $affectedFromChanges = $changed === [] ? [] : $graph->affected($changed);
$failedFromCache = []; $rerunFromCache = [];
if ($this->filteredMode) { if ($this->filteredMode) {
$failedFromCache = $graph->failedOrErroredTestFiles($this->branch); $rerunFromCache = $graph->testFilesToRerun($this->branch);
} }
$affected = array_values(array_unique([ $affected = array_values(array_unique([
...$affectedFromChanges, ...$affectedFromChanges,
...$failedFromCache, ...$rerunFromCache,
])); ]));
$this->reportAffectedSummary($changed, $affectedFromChanges, $failedFromCache, $affected); $this->reportAffectedSummary($changed, $affectedFromChanges, $rerunFromCache, $affected);
$affectedSet = array_fill_keys($affected, true); $affectedSet = array_fill_keys($affected, true);
$canRefreshReplayEdges = $affected !== [] && $coverageAvailable; $canRefreshReplayEdges = $affected !== [] && $coverageAvailable;
@ -852,10 +852,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
/** /**
* @param array<int, string> $changedFiles * @param array<int, string> $changedFiles
* @param array<int, string> $affectedFromChanges * @param array<int, string> $affectedFromChanges
* @param array<int, string> $failedFromCache * @param array<int, string> $rerunFromCache
* @param array<int, string> $affected * @param array<int, string> $affected
*/ */
private function reportAffectedSummary(array $changedFiles, array $affectedFromChanges, array $failedFromCache, array $affected): void private function reportAffectedSummary(array $changedFiles, array $affectedFromChanges, array $rerunFromCache, array $affected): void
{ {
$this->output->writeln(''); $this->output->writeln('');
@ -865,12 +865,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return; return;
} }
$newFailures = $failedFromCache === [] $newReruns = $rerunFromCache === []
? 0 ? 0
: count(array_diff($failedFromCache, $affectedFromChanges)); : count(array_diff($rerunFromCache, $affectedFromChanges));
$reasons = []; $reasons = [];
$singleReason = (int) ($affectedFromChanges !== []) + (int) ($newFailures > 0) === 1; $singleReason = (int) ($affectedFromChanges !== []) + (int) ($newReruns > 0) === 1;
if ($affectedFromChanges !== []) { if ($affectedFromChanges !== []) {
$reasons[] = $singleReason $reasons[] = $singleReason
@ -887,17 +887,17 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
); );
} }
if ($newFailures > 0) { if ($newReruns > 0) {
$reasons[] = $singleReason $reasons[] = $singleReason
? sprintf( ? sprintf(
'from %d previous failure%s', 'from %d previously unsuccessful test%s',
$newFailures, $newReruns,
$newFailures === 1 ? '' : 's', $newReruns === 1 ? '' : 's',
) )
: sprintf( : sprintf(
'%d from previous failure%s', '%d from previously unsuccessful test%s',
$newFailures, $newReruns,
$newFailures === 1 ? '' : 's', $newReruns === 1 ? '' : 's',
); );
} }

View File

@ -516,13 +516,13 @@ final class Graph
/** /**
* @return array<int, string> * @return array<int, string>
*/ */
public function failedOrErroredTestFiles(string $branch, string $fallbackBranch = 'main'): array public function testFilesToRerun(string $branch, string $fallbackBranch = 'main'): array
{ {
$baseline = $this->baselineFor($branch, $fallbackBranch); $baseline = $this->baselineFor($branch, $fallbackBranch);
$files = []; $files = [];
foreach ($baseline['results'] as $result) { foreach ($baseline['results'] as $result) {
if ($result['status'] !== 7 && $result['status'] !== 8) { if (! self::shouldRerun($result['status'])) {
continue; continue;
} }
@ -544,12 +544,12 @@ final class Graph
return array_keys($files); return array_keys($files);
} }
public function hasUnlocatedFailuresOrErrors(string $branch, string $fallbackBranch = 'main'): bool public function hasUnlocatedTestsToRerun(string $branch, string $fallbackBranch = 'main'): bool
{ {
$baseline = $this->baselineFor($branch, $fallbackBranch); $baseline = $this->baselineFor($branch, $fallbackBranch);
foreach ($baseline['results'] as $result) { foreach ($baseline['results'] as $result) {
if ($result['status'] !== 7 && $result['status'] !== 8) { if (! self::shouldRerun($result['status'])) {
continue; continue;
} }
@ -563,6 +563,16 @@ final class Graph
return false; return false;
} }
private static function shouldRerun(int $status): bool
{
$testStatus = TestStatus::from($status);
return $testStatus->isFailure()
|| $testStatus->isError()
|| $testStatus->isIncomplete()
|| $testStatus->isRisky();
}
/** /**
* @param array<string, string> $tree project-relative path → content hash * @param array<string, string> $tree project-relative path → content hash
*/ */

View File

@ -119,8 +119,8 @@ final class Recorder
$this->perTestUsesDatabase[$file] = true; $this->perTestUsesDatabase[$file] = true;
} }
$this->linkAncestorFiles($className); // $this->linkAncestorFiles($className);
$this->linkImportedFiles($file); // $this->linkImportedFiles($file);
if ($this->driver === 'pcov') { if ($this->driver === 'pcov') {
\pcov\clear(); \pcov\clear();
@ -175,7 +175,7 @@ final class Recorder
$this->perTestFiles[$this->currentTestFile][$sourceFile] = true; $this->perTestFiles[$this->currentTestFile][$sourceFile] = true;
} }
$this->linkSourceDependencies($coveredFiles); // $this->linkSourceDependencies($coveredFiles);
$this->currentTestFile = null; $this->currentTestFile = null;
$this->includedFilesAtTestStart = []; $this->includedFilesAtTestStart = [];

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Pest\Plugins\Tia; namespace Pest\Plugins\Tia;
use PHPUnit\Framework\TestStatus\TestStatus;
/** /**
* @internal * @internal
*/ */
@ -33,7 +35,7 @@ final class ResultCollector
return; return;
} }
$this->record(0, ''); $this->record(TestStatus::success());
} }
public function testFailed(string $message): void public function testFailed(string $message): void
@ -42,7 +44,7 @@ final class ResultCollector
return; return;
} }
$this->record(7, $message); $this->record(TestStatus::failure($message));
} }
public function testErrored(string $message): void public function testErrored(string $message): void
@ -51,7 +53,7 @@ final class ResultCollector
return; return;
} }
$this->record(8, $message); $this->record(TestStatus::error($message));
} }
public function testSkipped(string $message): void public function testSkipped(string $message): void
@ -60,7 +62,7 @@ final class ResultCollector
return; return;
} }
$this->record(1, $message); $this->record(TestStatus::skipped($message));
} }
public function testIncomplete(string $message): void public function testIncomplete(string $message): void
@ -69,7 +71,7 @@ final class ResultCollector
return; return;
} }
$this->record(2, $message); $this->record(TestStatus::incomplete($message));
} }
public function testRisky(string $message): void public function testRisky(string $message): void
@ -78,7 +80,7 @@ final class ResultCollector
return; return;
} }
$this->record(5, $message); $this->record(TestStatus::risky($message));
} }
/** /**
@ -121,7 +123,7 @@ final class ResultCollector
$this->startTime = null; $this->startTime = null;
} }
private function record(int $status, string $message): void private function record(TestStatus $status): void
{ {
if ($this->currentTestId === null) { if ($this->currentTestId === null) {
return; return;
@ -134,8 +136,8 @@ final class ResultCollector
$existing = $this->results[$this->currentTestId] ?? null; $existing = $this->results[$this->currentTestId] ?? null;
$this->results[$this->currentTestId] = [ $this->results[$this->currentTestId] = [
'status' => $status, 'status' => $status->asInt(),
'message' => $message, 'message' => $status->message(),
'time' => $time, 'time' => $time,
'assertions' => $existing['assertions'] ?? 0, 'assertions' => $existing['assertions'] ?? 0,
]; ];

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Pest\Subscribers;
use Pest\Concerns\Testable;
use Pest\Exceptions\TiaRequiresPestTests;
use Pest\Panic;
use Pest\Plugins\Tia\Recorder;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PreparedSubscriber;
use ReflectionClass;
/**
* @internal
*/
final readonly class EnsureTiaIsRunningPestTestsOnly implements PreparedSubscriber
{
public function __construct(private Recorder $recorder) {}
public function notify(Prepared $event): void
{
if (! $this->recorder->isActive()) {
return;
}
$test = $event->test();
if (! $test instanceof TestMethod) {
return;
}
$className = $test->className();
if (! class_exists($className, false)) {
return;
}
if ($this->usesTestableTrait($className)) {
return;
}
Panic::with(new TiaRequiresPestTests($className, $test->file()));
}
private function usesTestableTrait(string $className): bool
{
$reflection = new ReflectionClass($className);
do {
foreach ($reflection->getTraitNames() as $trait) {
if ($trait === Testable::class) {
return true;
}
}
$reflection = $reflection->getParentClass();
} while ($reflection !== false);
return false;
}
}