diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index decf2616..08b5b4c2 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -9,7 +9,6 @@ use Pest\Exceptions\DatasetArgumentsMismatch; use Pest\Panic; use Pest\Plugins\Tia; use Pest\Plugins\Tia\Collectors; -use Pest\Plugins\Tia\Edges\AutoloadEdges; use Pest\Plugins\Tia\Recorder; use Pest\Plugins\Tia\Replay; use Pest\Preset; @@ -300,10 +299,6 @@ trait Testable $recorder->beginTest($this::class, $this->name(), self::$__filename); } - $autoloadBeforeSetUp = $recorder->isActive() - ? AutoloadEdges::snapshot() - : []; - parent::setUp(); Collectors::armAll($recorder); @@ -315,18 +310,6 @@ trait Testable } $this->__callClosure($beforeEach, $arguments); - - if ($recorder->isActive() && $autoloadBeforeSetUp !== []) { - $recorder->linkSourcesForTest( - self::$__filename, - AutoloadEdges::newProjectFiles( - $autoloadBeforeSetUp, - AutoloadEdges::snapshot(), - TestSuite::getInstance()->rootPath, - self::$__filename, - ), - ); - } } private function __shortCircuitCachedPass(): void diff --git a/src/Exceptions/TiaRequiresPestTests.php b/src/Exceptions/TiaRequiresPestTests.php index 61614dc1..71d7615a 100644 --- a/src/Exceptions/TiaRequiresPestTests.php +++ b/src/Exceptions/TiaRequiresPestTests.php @@ -16,7 +16,7 @@ use Symfony\Component\Console\Output\OutputInterface; */ final class TiaRequiresPestTests extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace { - public function __construct(private readonly string $className, private readonly string $filename) + public function __construct(private readonly string $className, string $filename) { parent::__construct(sprintf( 'Tia mode requires only functional based Pest tests, but encountered PHPUnit class [%s] in [%s].', diff --git a/src/Kernel.php b/src/Kernel.php index d96a3190..49693690 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -32,7 +32,7 @@ final class Kernel /** * Either the kernel is terminated or not. */ - private bool $terminated; + private bool $terminated = false; /** * The Kernel bootstrappers. @@ -64,12 +64,7 @@ final class Kernel /** * Creates a new Kernel instance. */ - public function __construct( - private Application $application, - private OutputInterface $output, - ) { - $this->terminated = false; - } + public function __construct(private readonly Application $application, private readonly OutputInterface $output) {} /** * Boots the Kernel. diff --git a/src/Plugins/Tia.php b/src/Plugins/Tia.php index 3319b9fe..50936c0d 100644 --- a/src/Plugins/Tia.php +++ b/src/Plugins/Tia.php @@ -631,6 +631,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable if (! $graph instanceof Graph && ! $forceRebuild && ! $this->baselineFetchAttemptedForDrift + && $this->watchPatterns->isBaselined() && $this->baselineSync->fetchIfAvailable($projectRoot, $this->forceRefetch)) { $this->baselineFetchAttemptedForDrift = true; $graph = $this->loadGraph($projectRoot); @@ -1415,10 +1416,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable } foreach ($arguments as $index => $arg) { - if ($arg === '' || str_starts_with($arg, '-')) { + if ($arg === '') { + continue; + } + if (str_starts_with($arg, '-')) { continue; } - if ($index > 0) { $previous = $arguments[$index - 1] ?? ''; if (in_array($previous, $valueTakingFlags, true)) { @@ -1507,6 +1510,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable $projectRoot = TestSuite::getInstance()->rootPath; $this->baselineFetchAttemptedForDrift = true; + if (! $this->watchPatterns->isBaselined()) { + return null; + } + if (! $this->baselineSync->fetchIfAvailable($projectRoot, $this->forceRefetch, hasAnchor: true)) { return null; } diff --git a/src/Plugins/Tia/BaselineSync.php b/src/Plugins/Tia/BaselineSync.php index 083687a5..0025c375 100644 --- a/src/Plugins/Tia/BaselineSync.php +++ b/src/Plugins/Tia/BaselineSync.php @@ -283,7 +283,7 @@ YAML; /** * @param-out string|null $failureKind * - * @return array{graph: string, coverage: ?string}|null + * @return array{graph: string, coverage: ?string, sizeOnDisk: int}|null */ private function download(string $repo, string $projectRoot, ?string &$failureKind = null, bool $hasAnchor = false): ?array { diff --git a/src/Plugins/Tia/Configuration.php b/src/Plugins/Tia/Configuration.php index 014a7d74..61789801 100644 --- a/src/Plugins/Tia/Configuration.php +++ b/src/Plugins/Tia/Configuration.php @@ -48,6 +48,18 @@ final class Configuration return $this; } + /** + * @return $this + */ + public function baselined(): self + { + /** @var WatchPatterns $watchPatterns */ + $watchPatterns = Container::getInstance()->get(WatchPatterns::class); + $watchPatterns->markBaselined(); + + return $this; + } + /** * @param array $patterns glob → project-relative test dir * @return $this diff --git a/src/Plugins/Tia/Edges/AutoloadEdges.php b/src/Plugins/Tia/Edges/AutoloadEdges.php deleted file mode 100644 index fcc14e6a..00000000 --- a/src/Plugins/Tia/Edges/AutoloadEdges.php +++ /dev/null @@ -1,90 +0,0 @@ - - */ - public static function snapshot(): array - { - $files = []; - - foreach (get_included_files() as $file) { - if ($file !== '') { - $files[$file] = true; - } - } - - return $files; - } - - /** - * @param array $before - * @param array $after - * @return list - */ - public static function newProjectFiles(array $before, array $after, string $projectRoot, ?string $testFile = null): array - { - $root = rtrim($projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; - $testReal = is_string($testFile) && $testFile !== '' ? @realpath($testFile) : false; - $out = []; - - foreach (array_keys($after) as $file) { - if (isset($before[$file])) { - continue; - } - - $real = @realpath($file); - if ($real === false) { - $real = $file; - } - - if ($testReal !== false && $real === $testReal) { - continue; - } - - if (! str_starts_with($real, $root)) { - continue; - } - - $relative = str_replace(DIRECTORY_SEPARATOR, '/', substr($real, strlen($root))); - - if (self::ignored($relative)) { - continue; - } - - if (! str_ends_with($relative, '.php')) { - continue; - } - - $out[$real] = true; - } - - return array_keys($out); - } - - private static function ignored(string $relative): bool - { - static $prefixes = [ - 'vendor/', - 'node_modules/', - 'storage/framework/', - 'bootstrap/cache/', - ]; - - foreach ($prefixes as $prefix) { - if (str_starts_with($relative, (string) $prefix)) { - return true; - } - } - - return false; - } -} diff --git a/src/Plugins/Tia/Graph.php b/src/Plugins/Tia/Graph.php index c42b2fc6..93274aa8 100644 --- a/src/Plugins/Tia/Graph.php +++ b/src/Plugins/Tia/Graph.php @@ -522,7 +522,7 @@ final class Graph $files = []; foreach ($baseline['results'] as $result) { - if (! self::shouldRerun($result['status'])) { + if (! $this->shouldRerun($result['status'])) { continue; } @@ -549,7 +549,7 @@ final class Graph $baseline = $this->baselineFor($branch, $fallbackBranch); foreach ($baseline['results'] as $result) { - if (! self::shouldRerun($result['status'])) { + if (! $this->shouldRerun($result['status'])) { continue; } @@ -563,14 +563,20 @@ final class Graph return false; } - private static function shouldRerun(int $status): bool + private function shouldRerun(int $status): bool { $testStatus = TestStatus::from($status); + if ($testStatus->isFailure()) { + return true; + } + if ($testStatus->isError()) { + return true; + } + if ($testStatus->isIncomplete()) { + return true; + } - return $testStatus->isFailure() - || $testStatus->isError() - || $testStatus->isIncomplete() - || $testStatus->isRisky(); + return $testStatus->isRisky(); } /** diff --git a/src/Plugins/Tia/Recorder.php b/src/Plugins/Tia/Recorder.php index dfd319a7..86f5db45 100644 --- a/src/Plugins/Tia/Recorder.php +++ b/src/Plugins/Tia/Recorder.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Pest\Plugins\Tia; -use Pest\Plugins\Tia\Edges\AutoloadEdges; use Pest\TestSuite; use ReflectionClass; @@ -33,21 +32,6 @@ final class Recorder /** @var array */ private array $classUsesDatabaseCache = []; - /** @var array> */ - private array $fileToClassNames = []; - - /** @var array */ - private array $indexedClassNames = []; - - /** @var array> */ - private array $classDependencyCache = []; - - /** @var array> */ - private array $testImportFileCache = []; - - /** @var array */ - private array $includedFilesAtTestStart = []; - private bool $active = false; private bool $driverChecked = false; @@ -89,13 +73,6 @@ final class Recorder return $this->driverAvailable; } - public function driver(): string - { - $this->driverAvailable(); - - return $this->driver; - } - public function beginTest(string $className, string $methodName, string $fallbackFile): void { if (! $this->active || ! $this->driverAvailable()) { @@ -113,15 +90,11 @@ final class Recorder } $this->currentTestFile = $file; - $this->includedFilesAtTestStart = AutoloadEdges::snapshot(); if ($this->classUsesDatabase($className)) { $this->perTestUsesDatabase[$file] = true; } - // $this->linkAncestorFiles($className); - // $this->linkImportedFiles($file); - if ($this->driver === 'pcov') { \pcov\clear(); \pcov\start(); @@ -166,19 +139,7 @@ final class Recorder $this->perTestFiles[$this->currentTestFile][$sourceFile] = true; } - foreach (AutoloadEdges::newProjectFiles( - $this->includedFilesAtTestStart, - AutoloadEdges::snapshot(), - TestSuite::getInstance()->rootPath, - $this->currentTestFile, - ) as $sourceFile) { - $this->perTestFiles[$this->currentTestFile][$sourceFile] = true; - } - - // $this->linkSourceDependencies($coveredFiles); - $this->currentTestFile = null; - $this->includedFilesAtTestStart = []; } public function linkSource(string $sourceFile): void @@ -198,295 +159,6 @@ final class Recorder $this->perTestFiles[$this->currentTestFile][$sourceFile] = true; } - /** @param iterable $sourceFiles */ - public function linkSourcesForTest(string $testFile, iterable $sourceFiles): void - { - if (! $this->active) { - return; - } - - if ($testFile === '') { - return; - } - - foreach ($sourceFiles as $sourceFile) { - if ($sourceFile === '') { - continue; - } - - $this->perTestFiles[$testFile][$sourceFile] = true; - } - } - - /** @param array $coveredFiles */ - private function linkSourceDependencies(array $coveredFiles): void - { - if ($this->currentTestFile === null) { - return; - } - - $this->refreshClassMap(); - - foreach ($coveredFiles as $coveredFile) { - if (! isset($this->fileToClassNames[$coveredFile])) { - continue; - } - - foreach ($this->fileToClassNames[$coveredFile] as $name) { - foreach ($this->classDependencies($name) as $depFile) { - $this->perTestFiles[$this->currentTestFile][$depFile] = true; - } - } - } - } - - private function refreshClassMap(): void - { - $names = array_merge( - get_declared_classes(), - get_declared_interfaces(), - get_declared_traits(), - ); - - foreach ($names as $name) { - if (isset($this->indexedClassNames[$name])) { - continue; - } - $this->indexedClassNames[$name] = true; - - if (! class_exists($name, false) - && ! interface_exists($name, false) - && ! trait_exists($name, false)) { - continue; - } - - $reflection = new ReflectionClass($name); - - if ($reflection->isInternal()) { - continue; - } - - $file = $reflection->getFileName(); - - if (! is_string($file)) { - continue; - } - - if (str_contains($file, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) { - continue; - } - - $this->fileToClassNames[$file][] = $name; - } - } - - /** @return list */ - private function classDependencies(string $className): array - { - if (isset($this->classDependencyCache[$className])) { - return $this->classDependencyCache[$className]; - } - - if (! class_exists($className, false) - && ! interface_exists($className, false) - && ! trait_exists($className, false)) { - return $this->classDependencyCache[$className] = []; - } - - $reflection = new ReflectionClass($className); - - $files = []; - - $linkSymbol = static function (string $name) use (&$files): void { - if (! class_exists($name, false) - && ! interface_exists($name, false) - && ! trait_exists($name, false)) { - return; - } - $r = new ReflectionClass($name); - if ($r->isInternal()) { - return; - } - $f = $r->getFileName(); - if (! is_string($f) || str_contains($f, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) { - return; - } - $files[$f] = true; - }; - - foreach ($reflection->getInterfaceNames() as $iname) { - $linkSymbol($iname); - } - - foreach ($reflection->getTraitNames() as $tname) { - $linkSymbol($tname); - } - - $parent = $reflection->getParentClass(); - while ($parent !== false && ! $parent->isInternal()) { - $f = $parent->getFileName(); - if (is_string($f) && ! str_contains($f, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) { - $files[$f] = true; - } - foreach ($parent->getTraitNames() as $tname) { - $linkSymbol($tname); - } - $parent = $parent->getParentClass(); - } - - return $this->classDependencyCache[$className] = array_keys($files); - } - - private function linkAncestorFiles(string $className): void - { - if (! class_exists($className, false)) { - return; - } - - $reflection = new ReflectionClass($className); - $parent = $reflection->getParentClass(); - - while ($parent !== false) { - if ($parent->isInternal()) { - break; - } - - $file = $parent->getFileName(); - - if (is_string($file) && ! str_contains($file, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) { - $this->perTestFiles[(string) $this->currentTestFile][$file] = true; - } - - $parent = $parent->getParentClass(); - } - } - - private function linkImportedFiles(string $testFile): void - { - if ($this->currentTestFile === null) { - return; - } - - foreach ($this->importedFilesFor($testFile) as $file) { - $this->perTestFiles[$this->currentTestFile][$file] = true; - } - } - - /** - * @return list - */ - private function importedFilesFor(string $testFile): array - { - if (array_key_exists($testFile, $this->testImportFileCache)) { - return $this->testImportFileCache[$testFile]; - } - - $source = @file_get_contents($testFile); - if ($source === false) { - return $this->testImportFileCache[$testFile] = []; - } - - $files = []; - - foreach ($this->importedClassNames($source) as $className) { - $file = $this->findAutoloadFile($className); - - if ($file !== null && ! str_contains($file, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) { - $files[$file] = true; - } - } - - return $this->testImportFileCache[$testFile] = array_keys($files); - } - - /** - * @return list - */ - private function importedClassNames(string $source): array - { - preg_match_all('/^use\s+(?!function\s|const\s)([^;]+);/mi', $source, $matches); - - $classes = []; - - foreach ($matches[1] as $import) { - $import = trim($import); - - if ($import === '') { - continue; - } - - $open = strpos($import, '{'); - $close = strrpos($import, '}'); - - if ($open !== false && $close !== false && $close > $open) { - $prefix = trim(trim(substr($import, 0, $open)), '\\'); - $items = explode(',', substr($import, $open + 1, $close - $open - 1)); - - foreach ($items as $item) { - $class = $this->normaliseImportedClass($prefix.'\\'.trim($item)); - - if ($class !== null) { - $classes[$class] = true; - } - } - - continue; - } - - $class = $this->normaliseImportedClass($import); - - if ($class !== null) { - $classes[$class] = true; - } - } - - return array_keys($classes); - } - - private function normaliseImportedClass(string $import): ?string - { - $import = trim(trim($import), '\\'); - - if ($import === '') { - return null; - } - - $parts = preg_split('/\s+as\s+/i', $import); - if ($parts === false) { - return null; - } - - $class = trim(trim($parts[0]), '\\'); - - return $class === '' ? null : $class; - } - - private function findAutoloadFile(string $className): ?string - { - foreach (spl_autoload_functions() as $loader) { - if (! is_array($loader)) { - continue; - } - if (! is_object($loader[0])) { - continue; - } - if (! method_exists($loader[0], 'findFile')) { - continue; - } - - /** @var mixed $file */ - $file = $loader[0]->findFile($className); - - if (is_string($file) && $file !== '') { - $real = @realpath($file); - - return $real === false ? $file : $real; - } - } - - return null; - } - private function classUsesDatabase(string $className): bool { if (array_key_exists($className, $this->classUsesDatabaseCache)) { @@ -691,9 +363,6 @@ final class Recorder $this->perTestUsesDatabase = []; $this->classFileCache = []; $this->classUsesDatabaseCache = []; - $this->fileToClassNames = []; - $this->indexedClassNames = []; - $this->classDependencyCache = []; $this->sourceScope = null; $this->active = false; } diff --git a/src/Plugins/Tia/WatchPatterns.php b/src/Plugins/Tia/WatchPatterns.php index 46c34778..78d9ce01 100644 --- a/src/Plugins/Tia/WatchPatterns.php +++ b/src/Plugins/Tia/WatchPatterns.php @@ -35,6 +35,8 @@ final class WatchPatterns private bool $filtered = false; + private bool $baselined = false; + public function useDefaults(string $projectRoot): void { $testPath = TestSuite::getInstance()->testPath; @@ -156,12 +158,23 @@ final class WatchPatterns return $this->filtered; } + public function markBaselined(): void + { + $this->baselined = true; + } + + public function isBaselined(): bool + { + return $this->baselined; + } + public function reset(): void { $this->patterns = []; $this->enabled = false; $this->locally = false; $this->filtered = false; + $this->baselined = false; } private function globMatches(string $pattern, string $file): bool diff --git a/src/Subscribers/EnsureTiaIsRunningPestTestsOnly.php b/src/Subscribers/EnsureTiaIsRunningPestTestsOnly.php index 591881e6..305490f6 100644 --- a/src/Subscribers/EnsureTiaIsRunningPestTestsOnly.php +++ b/src/Subscribers/EnsureTiaIsRunningPestTestsOnly.php @@ -45,6 +45,9 @@ final readonly class EnsureTiaIsRunningPestTestsOnly implements PreparedSubscrib Panic::with(new TiaRequiresPestTests($className, $test->file())); } + /** + * @param class-string $className + */ private function usesTestableTrait(string $className): bool { $reflection = new ReflectionClass($className); diff --git a/src/Support/Cpu.php b/src/Support/Cpu.php index 9ff5fda1..501cf88b 100644 --- a/src/Support/Cpu.php +++ b/src/Support/Cpu.php @@ -11,6 +11,9 @@ use Fidry\CpuCoreCounter\CpuCoreCounter; */ final class Cpu { + /** + * @param int<1, max> $fallback + */ public static function cores(int $fallback = 4): int { return (new CpuCoreCounter)->getCountWithFallback($fallback);