This commit is contained in:
nuno maduro
2026-05-02 17:02:11 +01:00
parent 28305fcb7a
commit 5f37939fda
6 changed files with 127 additions and 123 deletions

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
use PHPUnit\TextUI\Configuration\Builder;
/**
* @internal
*/
final class BootPhpUnitConfiguration implements Bootstrapper
{
public function boot(): void
{
(new Builder)->build(['pest']);
}
}

View File

@ -41,6 +41,7 @@ final class Kernel
*/ */
private const array BOOTSTRAPPERS = [ private const array BOOTSTRAPPERS = [
Bootstrappers\BootOverrides::class, Bootstrappers\BootOverrides::class,
Bootstrappers\BootPhpUnitConfiguration::class,
Plugins\Tia\Bootstrapper::class, Plugins\Tia\Bootstrapper::class,
Bootstrappers\BootSubscribers::class, Bootstrappers\BootSubscribers::class,
Bootstrappers\BootFiles::class, Bootstrappers\BootFiles::class,

View File

@ -584,7 +584,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
if (! Fingerprint::structuralMatches($stored, $current)) { if (! Fingerprint::structuralMatches($stored, $current)) {
$drift = Fingerprint::structuralDrift($stored, $current); $drift = Fingerprint::structuralDrift($stored, $current);
$this->renderBadge('INFO', sprintf( $this->renderChild(sprintf(
'Graph structure outdated (%s).', 'Graph structure outdated (%s).',
$this->formatStructuralDrift($drift), $this->formatStructuralDrift($drift),
)); ));
@ -1446,7 +1446,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
]; ];
$projectRoot = TestSuite::getInstance()->rootPath; $projectRoot = TestSuite::getInstance()->rootPath;
$testPaths = SourceScope::testPaths($projectRoot); $testPaths = SourceScope::testPaths();
if ($testPaths === []) { if ($testPaths === []) {
return false; return false;

View File

@ -10,6 +10,7 @@ use Pest\Support\View;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\TextUI\Configuration\Registry;
/** /**
* @internal * @internal
@ -566,17 +567,58 @@ final class Graph
private function shouldRerun(int $status): bool private function shouldRerun(int $status): bool
{ {
$testStatus = TestStatus::from($status); $testStatus = TestStatus::from($status);
if ($testStatus->isFailure()) {
return true; if ($testStatus->isFailure() || $testStatus->isError()) {
}
if ($testStatus->isError()) {
return true;
}
if ($testStatus->isIncomplete()) {
return true; return true;
} }
return $testStatus->isRisky(); $configuration = Registry::get();
if ($testStatus->isRisky()) {
return $configuration->failOnRisky();
}
if ($testStatus->isWarning()) {
if ($configuration->failOnWarning()) {
return true;
}
return $configuration->displayDetailsOnTestsThatTriggerWarnings();
}
if ($testStatus->isNotice()) {
if ($configuration->failOnNotice()) {
return true;
}
return $configuration->displayDetailsOnTestsThatTriggerNotices();
}
if ($testStatus->isDeprecation()) {
if ($configuration->failOnDeprecation()) {
return true;
}
return $configuration->displayDetailsOnTestsThatTriggerDeprecations();
}
if ($testStatus->isIncomplete()) {
if ($configuration->failOnIncomplete()) {
return true;
}
return $configuration->displayDetailsOnIncompleteTests();
}
if ($testStatus->isSkipped()) {
if ($configuration->failOnSkipped()) {
return true;
}
return $configuration->displayDetailsOnSkippedTests();
}
return false;
} }
/** /**

View File

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace Pest\Plugins\Tia; namespace Pest\Plugins\Tia;
use PHPUnit\TextUI\Configuration\Registry;
use Throwable;
/** /**
* @internal * @internal
*/ */
@ -38,19 +41,21 @@ final readonly class SourceScope
public static function fromProjectRoot(string $projectRoot): self public static function fromProjectRoot(string $projectRoot): self
{ {
$configPath = self::configPath($projectRoot);
$phpunitIncludes = []; $phpunitIncludes = [];
$phpunitExcludes = []; $phpunitExcludes = [];
if ($configPath !== null) { try {
$xml = @simplexml_load_file($configPath); $source = Registry::get()->source();
if ($xml !== false) { foreach ($source->includeDirectories() as $dir) {
$configDir = dirname($configPath); $phpunitIncludes[] = self::normalise($dir->path());
$phpunitIncludes = self::extractDirectories($xml, 'source/include/directory', $configDir);
$phpunitExcludes = self::extractDirectories($xml, 'source/exclude/directory', $configDir);
} }
foreach ($source->excludeDirectories() as $dir) {
$phpunitExcludes[] = self::normalise($dir->path());
}
} catch (Throwable) {
// Registry not initialized — fall back to project-root scanning.
} }
$rootIncludes = self::topLevelProjectDirs($projectRoot); $rootIncludes = self::topLevelProjectDirs($projectRoot);
@ -71,26 +76,25 @@ final readonly class SourceScope
/** /**
* @return list<string> Absolute, normalised paths to testsuite directories and files declared in phpunit.xml. * @return list<string> Absolute, normalised paths to testsuite directories and files declared in phpunit.xml.
*/ */
public static function testPaths(string $projectRoot): array public static function testPaths(): array
{ {
$configPath = self::configPath($projectRoot); try {
$suites = Registry::get()->testSuite();
if ($configPath === null) { } catch (Throwable) {
return []; return [];
} }
$out = [];
foreach ($suites as $suite) {
foreach ($suite->directories() as $directory) {
$out[] = self::normalise($directory->path());
}
$xml = @simplexml_load_file($configPath); foreach ($suite->files() as $file) {
$out[] = self::normalise($file->path());
if ($xml === false) { }
return [];
} }
$configDir = dirname($configPath); return array_values(array_unique($out));
return array_values(array_unique([
...self::extractDirectories($xml, 'testsuites/testsuite/directory', $configDir),
...self::extractDirectories($xml, 'testsuites/testsuite/file', $configDir),
]));
} }
public function contains(string $absoluteFile): bool public function contains(string $absoluteFile): bool
@ -122,45 +126,6 @@ final readonly class SourceScope
return $this->includes; return $this->includes;
} }
private static function configPath(string $projectRoot): ?string
{
foreach (['phpunit.xml', 'phpunit.xml.dist'] as $name) {
$candidate = $projectRoot.DIRECTORY_SEPARATOR.$name;
if (is_file($candidate)) {
return $candidate;
}
}
return null;
}
/**
* @return list<string>
*/
private static function extractDirectories(\SimpleXMLElement $xml, string $xpath, string $configDir): array
{
$nodes = $xml->xpath($xpath);
if (! is_array($nodes)) {
return [];
}
$out = [];
foreach ($nodes as $node) {
$value = trim((string) $node);
if ($value === '') {
continue;
}
$out[] = self::resolveRelative($value, $configDir);
}
return array_values(array_unique($out));
}
/** /**
* @return list<string> * @return list<string>
*/ */
@ -216,22 +181,6 @@ final readonly class SourceScope
return $out; return $out;
} }
private static function resolveRelative(string $path, string $configDir): string
{
$isAbsolute = $path !== '' && ($path[0] === DIRECTORY_SEPARATOR || $path[0] === '/'
|| (strlen($path) >= 2 && $path[1] === ':'));
$combined = $isAbsolute ? $path : $configDir.DIRECTORY_SEPARATOR.$path;
$real = @realpath($combined);
if ($real === false) {
return self::normalise($combined);
}
return self::normalise($real);
}
private static function normalise(string $path): string private static function normalise(string $path): string
{ {
return rtrim($path, '/\\'); return rtrim($path, '/\\');

View File

@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Pest\Plugins\Tia; namespace Pest\Plugins\Tia;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\TextUI\Configuration\Registry;
use Throwable;
/** /**
* Resolves the set of project-relative paths that are considered test files, * Resolves the set of project-relative paths that are considered test files,
@ -28,39 +30,48 @@ final readonly class TestPaths
public static function fromProjectRoot(string $projectRoot): self public static function fromProjectRoot(string $projectRoot): self
{ {
$configPath = self::configPath($projectRoot);
$directories = []; $directories = [];
$files = []; $files = [];
$suffixes = ['.php']; $suffixes = [];
if ($configPath !== null) { try {
$xml = @simplexml_load_file($configPath); $configuration = Registry::get();
if ($xml !== false) { foreach ($configuration->testSuite() as $suite) {
$configDir = dirname($configPath); foreach ($suite->directories() as $directory) {
$rel = self::toRelative($directory->path(), $projectRoot);
foreach ($xml->xpath('testsuites/testsuite/directory') ?: [] as $node) {
$rel = self::toRelative((string) $node, $configDir, $projectRoot);
if ($rel !== null) { if ($rel !== null) {
$directories[] = $rel; $directories[] = $rel;
} }
$suffix = (string) ($node['suffix'] ?? ''); $suffix = $directory->suffix();
if ($suffix !== '' && ! in_array($suffix, $suffixes, true)) {
if ($suffix !== '') {
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix; $suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
} }
} }
foreach ($xml->xpath('testsuites/testsuite/file') ?: [] as $node) { foreach ($suite->files() as $file) {
$rel = self::toRelative((string) $node, $configDir, $projectRoot); $rel = self::toRelative($file->path(), $projectRoot);
if ($rel !== null) { if ($rel !== null) {
$files[] = $rel; $files[] = $rel;
} }
} }
} }
if ($suffixes === []) {
foreach ($configuration->testSuffixes() as $suffix) {
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
}
}
} catch (Throwable) {
// Registry not initialized — fall through to defaults.
}
if ($suffixes === []) {
$suffixes = ['.php'];
} }
if ($directories === [] && $files === []) { if ($directories === [] && $files === []) {
@ -109,20 +120,7 @@ final readonly class TestPaths
return false; return false;
} }
private static function configPath(string $projectRoot): ?string private static function toRelative(string $value, string $projectRoot): ?string
{
foreach (['phpunit.xml', 'phpunit.xml.dist'] as $name) {
$candidate = $projectRoot.DIRECTORY_SEPARATOR.$name;
if (is_file($candidate)) {
return $candidate;
}
}
return null;
}
private static function toRelative(string $value, string $configDir, string $projectRoot): ?string
{ {
$value = trim($value); $value = trim($value);
@ -130,13 +128,8 @@ final readonly class TestPaths
return null; return null;
} }
$isAbsolute = $value[0] === '/' || $value[0] === DIRECTORY_SEPARATOR $real = @realpath($value);
|| (strlen($value) >= 2 && $value[1] === ':'); $resolved = $real === false ? $value : $real;
$combined = $isAbsolute ? $value : $configDir.DIRECTORY_SEPARATOR.$value;
$real = @realpath($combined);
$resolved = $real === false ? $combined : $real;
$resolved = str_replace(DIRECTORY_SEPARATOR, '/', $resolved); $resolved = str_replace(DIRECTORY_SEPARATOR, '/', $resolved);
$root = str_replace(DIRECTORY_SEPARATOR, '/', rtrim($projectRoot, '/\\')).'/'; $root = str_replace(DIRECTORY_SEPARATOR, '/', rtrim($projectRoot, '/\\')).'/';
@ -152,7 +145,7 @@ final readonly class TestPaths
{ {
try { try {
$testPath = TestSuite::getInstance()->testPath; $testPath = TestSuite::getInstance()->testPath;
} catch (\Throwable) { } catch (Throwable) {
return null; return null;
} }