From 405d8d440646819bb56ac58cd305fe79f5867dd7 Mon Sep 17 00:00:00 2001 From: nuno maduro Date: Tue, 28 Apr 2026 21:28:46 +0100 Subject: [PATCH] wip --- src/Concerns/Testable.php | 24 ++- src/Plugins/Tia/AutoloadEdges.php | 96 +++++++++ src/Plugins/Tia/Graph.php | 67 ++++++- src/Plugins/Tia/Recorder.php | 185 ++++++++++++++++++ src/Plugins/Tia/WatchDefaults/Browser.php | 27 ++- src/Plugins/Tia/WatchDefaults/Inertia.php | 18 +- src/Plugins/Tia/WatchDefaults/Laravel.php | 34 ++-- src/Plugins/Tia/WatchPatterns.php | 29 +-- .../EnsureTiaCoverageIsRecorded.php | 5 +- 9 files changed, 421 insertions(+), 64 deletions(-) create mode 100644 src/Plugins/Tia/AutoloadEdges.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 5657c4d8..15a7bc67 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -8,6 +8,7 @@ use Closure; use Pest\Exceptions\DatasetArgumentsMismatch; use Pest\Panic; use Pest\Plugins\Tia; +use Pest\Plugins\Tia\AutoloadEdges; use Pest\Plugins\Tia\BladeEdges; use Pest\Plugins\Tia\InertiaEdges; use Pest\Plugins\Tia\Recorder; @@ -317,6 +318,16 @@ trait Testable throw new AssertionFailedError($cached->message() ?: 'Cached failure'); } + $recorder = Container::getInstance()->get(Recorder::class); + + if ($recorder instanceof Recorder && $recorder->isActive()) { + $recorder->beginTest($this::class, $this->name(), self::$__filename); + } + + $autoloadBeforeSetUp = $recorder instanceof Recorder && $recorder->isActive() + ? AutoloadEdges::snapshot() + : []; + parent::setUp(); // TIA blade-edge + table-edge recording (Laravel-only). Runs @@ -325,7 +336,6 @@ trait Testable // idempotent against the current app instance so the 774-test // suite doesn't stack 774 composers / listeners when Laravel // keeps the same app across tests. - $recorder = Container::getInstance()->get(Recorder::class); if ($recorder instanceof Recorder) { BladeEdges::arm($recorder); TableTracker::arm($recorder); @@ -339,6 +349,18 @@ trait Testable } $this->__callClosure($beforeEach, $arguments); + + if ($recorder instanceof Recorder && $recorder->isActive() && $autoloadBeforeSetUp !== []) { + $recorder->linkSourcesForTest( + self::$__filename, + AutoloadEdges::newProjectFiles( + $autoloadBeforeSetUp, + AutoloadEdges::snapshot(), + TestSuite::getInstance()->rootPath, + self::$__filename, + ), + ); + } } /** diff --git a/src/Plugins/Tia/AutoloadEdges.php b/src/Plugins/Tia/AutoloadEdges.php new file mode 100644 index 00000000..0ecf9719 --- /dev/null +++ b/src/Plugins/Tia/AutoloadEdges.php @@ -0,0 +1,96 @@ + + */ + public static function snapshot(): array + { + $files = []; + + foreach (get_included_files() as $file) { + if (is_string($file) && $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, $prefix)) { + return true; + } + } + + return false; + } +} diff --git a/src/Plugins/Tia/Graph.php b/src/Plugins/Tia/Graph.php index 47f81783..c49a9501 100644 --- a/src/Plugins/Tia/Graph.php +++ b/src/Plugins/Tia/Graph.php @@ -387,11 +387,28 @@ final class Graph foreach ($nonMigrationPaths as $rel) { if (isset($this->fileIds[$rel])) { $changedIds[$this->fileIds[$rel]] = true; - } elseif (str_ends_with($rel, '.php') && ! str_starts_with($rel, 'tests/')) { + + continue; + } + + if (str_ends_with($rel, '.php') && ! str_starts_with($rel, 'tests/')) { + $absolute = $this->projectRoot.'/'.$rel; + + if (! is_file($absolute)) { + // Deleted source file unknown to the graph — can't affect + // any test because no edge ever pointed to it. + continue; + } + // Source PHP file unknown to the graph — might be a new file // that only exists on this branch (graph inherited from main). - // Track its directory for the sibling heuristic (step 3). - $unknownSourceDirs[dirname($rel)] = true; + // Only use the sibling heuristic for files that commonly + // participate in framework discovery / bootstrap. Ordinary new + // classes, enums, DTOs, services, etc. should not re-run sibling + // tests just because they live in the same directory. + if ($this->usesSiblingHeuristicForUnknownPhp($rel)) { + $unknownSourceDirs[dirname($rel)] = true; + } } } @@ -434,6 +451,12 @@ final class Graph continue; } if (! isset($this->fileIds[$rel])) { + if (! is_file($this->projectRoot.'/'.$rel)) { + // Deleted file unknown to the graph — no edge ever + // pointed to it, so it can't affect any test. + continue; + } + $unknownToGraph[] = $rel; } } @@ -455,10 +478,11 @@ final class Graph // whose graph was inherited from another branch (e.g. main). In the // latter case the graph simply never saw the file. // - // To avoid silent misses: find tests that already cover ANY file in - // the same directory. If `app/Models/OrderItem.php` is unknown but - // `app/Models/Order.php` is covered by `OrderTest`, run `OrderTest` - // — it likely exercises sibling files in the same module. + // To avoid silent misses for framework-discovered files: find tests + // that already cover ANY file in the same directory. If + // `app/Listeners/SendWelcomeEmail.php` is unknown but neighbouring + // listeners are covered by a mail-flow test, run that test — it likely + // exercises the same discovery surface. // // This over-runs slightly (sibling may be unrelated) but never // under-runs. And once the test executes, its coverage captures the @@ -801,6 +825,35 @@ final class Graph return str_starts_with($rel, 'database/migrations/') && str_ends_with($rel, '.php'); } + /** + * Unknown PHP files have no historical edge yet. Keep sibling fan-out only + * for framework-discovered / boot-loaded conventions where adding a file can + * change behaviour without another source file changing too. + */ + private function usesSiblingHeuristicForUnknownPhp(string $rel): bool + { + static $prefixes = [ + 'app/Providers/', + 'app/Listeners/', + 'app/Events/', + 'app/Observers/', + 'app/Policies/', + 'app/Console/Commands/', + 'app/Mail/', + 'app/Notifications/', + 'database/factories/', + 'database/seeders/', + ]; + + foreach ($prefixes as $prefix) { + if (str_starts_with($rel, $prefix)) { + return true; + } + } + + return false; + } + /** * Reads `$rel` relative to the project root and extracts the * tables it declares via `Schema::create/table/drop/rename`. diff --git a/src/Plugins/Tia/Recorder.php b/src/Plugins/Tia/Recorder.php index 724510f1..15f804a7 100644 --- a/src/Plugins/Tia/Recorder.php +++ b/src/Plugins/Tia/Recorder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Plugins\Tia; +use Pest\TestSuite; use ReflectionClass; /** @@ -108,6 +109,20 @@ final class Recorder */ private array $classDependencyCache = []; + /** + * Cached test-file import resolution. + * + * @var array> + */ + private array $testImportFileCache = []; + + /** + * Included-file snapshot captured at the start of the current test. + * + * @var array + */ + private array $includedFilesAtTestStart = []; + private bool $active = false; private bool $driverChecked = false; @@ -169,6 +184,10 @@ final class Recorder return; } + if ($this->currentTestFile !== null) { + return; + } + $file = $this->resolveTestFile($className, $fallbackFile); if ($file === null) { @@ -176,6 +195,7 @@ final class Recorder } $this->currentTestFile = $file; + $this->includedFilesAtTestStart = AutoloadEdges::snapshot(); if ($this->classUsesDatabase($className)) { $this->perTestUsesDatabase[$file] = true; @@ -193,6 +213,7 @@ final class Recorder // the explicit walk for ancestors whose own bodies might be // empty. $this->linkAncestorFiles($className); + $this->linkImportedFiles($file); if ($this->driver === 'pcov') { \pcov\clear(); @@ -228,6 +249,15 @@ 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; + } + // Walk each covered class's interfaces / traits / parent chain // and link those files explicitly. Interface declarations have // no executable bytecode, so coverage drivers never emit lines @@ -239,6 +269,7 @@ final class Recorder $this->linkSourceDependencies(array_keys($data)); $this->currentTestFile = null; + $this->includedFilesAtTestStart = []; } /** @@ -267,6 +298,31 @@ final class Recorder $this->perTestFiles[$this->currentTestFile][$sourceFile] = true; } + /** + * Records source dependencies for a specific test file. Used for edges + * captured before `Prepared` has opened the normal per-test recorder window. + * + * @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; + } + } + /** * For each project-local source file the coverage driver * captured for this test, finds the classes / interfaces / traits @@ -468,6 +524,135 @@ final class Recorder } } + /** + * Links project-local classes imported by the test file. This catches + * declaration-only support classes / enums / interfaces that may never emit + * executable coverage lines, and avoids relying on global autoload timing. + */ + 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 || $parts === []) { + 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) || ! isset($loader[0]) || ! 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; + } + /** * True when `$className` (or any of its ancestors) uses one of * Laravel's database-resetting traits. Walking up `getTraits()` is diff --git a/src/Plugins/Tia/WatchDefaults/Browser.php b/src/Plugins/Tia/WatchDefaults/Browser.php index d4f18a67..a6efd5d0 100644 --- a/src/Plugins/Tia/WatchDefaults/Browser.php +++ b/src/Plugins/Tia/WatchDefaults/Browser.php @@ -12,9 +12,8 @@ use Pest\TestSuite; /** * Watch patterns for frontend assets that affect browser tests. * - * Uses `BrowserTestIdentifier` from pest-plugin-browser (if installed) to - * auto-discover directories containing browser tests. Falls back to the - * `tests/Browser` convention when the plugin is absent. + * Uses `BrowserTestIdentifier` from pest-plugin-browser to auto-discover tests + * using `visit()`. Also keeps the `tests/Browser` convention when present. * * @internal */ @@ -31,7 +30,7 @@ final readonly class Browser implements WatchDefault public function defaults(string $projectRoot, string $testPath): array { - $browserDirs = $this->detectBrowserTestDirs($projectRoot, $testPath); + $browserTargets = self::detectBrowserTestTargets($projectRoot, $testPath); $globs = [ 'resources/js/**/*.js', @@ -51,7 +50,7 @@ final readonly class Browser implements WatchDefault $patterns = []; foreach ($globs as $glob) { - $patterns[$glob] = $browserDirs; + $patterns[$glob] = $browserTargets; } return $patterns; @@ -60,19 +59,19 @@ final readonly class Browser implements WatchDefault /** * @return array */ - private function detectBrowserTestDirs(string $projectRoot, string $testPath): array + public static function detectBrowserTestTargets(string $projectRoot, string $testPath): array { - $dirs = []; + $targets = []; $candidate = $testPath.'/Browser'; if (is_dir($projectRoot.DIRECTORY_SEPARATOR.$candidate)) { - $dirs[] = $candidate; + $targets[] = $candidate; } // Scan TestRepository via BrowserTestIdentifier if pest-plugin-browser - // is installed to find tests using `visit()` outside the conventional - // Browser/ folder. + // is installed to find exact tests using `visit()` outside the + // conventional Browser/ folder. if (class_exists(BrowserTestIdentifier::class)) { $repo = TestSuite::getInstance()->tests; @@ -85,10 +84,10 @@ final readonly class Browser implements WatchDefault foreach ($factory->methods as $method) { if (BrowserTestIdentifier::isBrowserTest($method)) { - $rel = $this->fileRelative($projectRoot, $filename); + $rel = self::fileRelative($projectRoot, $filename); if ($rel !== null) { - $dirs[] = dirname($rel); + $targets[] = $rel; } break; @@ -97,10 +96,10 @@ final readonly class Browser implements WatchDefault } } - return array_values(array_unique($dirs === [] ? [$testPath] : $dirs)); + return array_values(array_unique($targets)); } - private function fileRelative(string $projectRoot, string $path): ?string + private static function fileRelative(string $projectRoot, string $path): ?string { $real = @realpath($path); diff --git a/src/Plugins/Tia/WatchDefaults/Inertia.php b/src/Plugins/Tia/WatchDefaults/Inertia.php index 54a9d55a..14042206 100644 --- a/src/Plugins/Tia/WatchDefaults/Inertia.php +++ b/src/Plugins/Tia/WatchDefaults/Inertia.php @@ -26,12 +26,10 @@ final readonly class Inertia implements WatchDefault public function defaults(string $projectRoot, string $testPath): array { - $browserDir = is_dir($projectRoot.DIRECTORY_SEPARATOR.$testPath.'/Browser') - ? $testPath.'/Browser' - : $testPath; + $browserTargets = Browser::detectBrowserTestTargets($projectRoot, $testPath); // Inertia page components (React / Vue / Svelte). Scoped to - // `$browserDir` only — a Vue/React edit cannot change the + // browser tests only — a Vue/React edit cannot change the // output of a server-side Inertia test (those assert on the // component *name* returned by `Inertia::render()`, not its // client-side implementation). Broad invalidation is only @@ -47,21 +45,21 @@ final readonly class Inertia implements WatchDefault foreach (['Pages', 'pages'] as $pages) { foreach (['vue', 'tsx', 'jsx', 'svelte', 'ts', 'js'] as $ext) { - $patterns["resources/js/{$pages}/**/*.{$ext}"] = [$browserDir]; + $patterns["resources/js/{$pages}/**/*.{$ext}"] = $browserTargets; } } foreach (['Layouts', 'layouts', 'Components', 'components'] as $shared) { foreach (['vue', 'tsx', 'ts', 'js'] as $ext) { - $patterns["resources/js/{$shared}/**/*.{$ext}"] = [$browserDir]; + $patterns["resources/js/{$shared}/**/*.{$ext}"] = $browserTargets; } } // SSR entry point. - $patterns['resources/js/ssr.js'] = [$browserDir]; - $patterns['resources/js/ssr.ts'] = [$browserDir]; - $patterns['resources/js/app.js'] = [$browserDir]; - $patterns['resources/js/app.ts'] = [$browserDir]; + $patterns['resources/js/ssr.js'] = $browserTargets; + $patterns['resources/js/ssr.ts'] = $browserTargets; + $patterns['resources/js/app.js'] = $browserTargets; + $patterns['resources/js/app.ts'] = $browserTargets; return $patterns; } diff --git a/src/Plugins/Tia/WatchDefaults/Laravel.php b/src/Plugins/Tia/WatchDefaults/Laravel.php index b2e8445e..ace946a6 100644 --- a/src/Plugins/Tia/WatchDefaults/Laravel.php +++ b/src/Plugins/Tia/WatchDefaults/Laravel.php @@ -27,10 +27,6 @@ final readonly class Laravel implements WatchDefault public function defaults(string $projectRoot, string $testPath): array { - $featurePath = is_dir($projectRoot.DIRECTORY_SEPARATOR.$testPath.'/Feature') - ? $testPath.'/Feature' - : $testPath; - return [ // Config — loaded during app boot (setUp), invisible to coverage. // Affects both Feature and Unit: Pest.php commonly binds fakes @@ -39,8 +35,8 @@ final readonly class Laravel implements WatchDefault 'config/**/*.php' => [$testPath], // Routes — loaded during boot. HTTP/Feature tests depend on them. - 'routes/*.php' => [$featurePath], - 'routes/**/*.php' => [$featurePath], + 'routes/*.php' => [$testPath], + 'routes/**/*.php' => [$testPath], // Service providers / bootstrap — loaded during boot, affect // bindings, middleware, event listeners, scheduled tasks. @@ -59,27 +55,27 @@ final readonly class Laravel implements WatchDefault 'database/factories/**/*.php' => [$testPath], // Blade templates — compiled to cache, source file not executed. - 'resources/views/**/*.blade.php' => [$featurePath], + 'resources/views/**/*.blade.php' => [$testPath], // Email templates are nested under views/email or views/emails // by convention and power mailable tests that render markup. - 'resources/views/email/**/*.blade.php' => [$featurePath], - 'resources/views/emails/**/*.blade.php' => [$featurePath], + 'resources/views/email/**/*.blade.php' => [$testPath], + 'resources/views/emails/**/*.blade.php' => [$testPath], // Translations — JSON translations read via file_get_contents, // PHP translations loaded via include (but during boot). - 'lang/**/*.php' => [$featurePath], - 'lang/**/*.json' => [$featurePath], - 'resources/lang/**/*.php' => [$featurePath], - 'resources/lang/**/*.json' => [$featurePath], + 'lang/**/*.php' => [$testPath], + 'lang/**/*.json' => [$testPath], + 'resources/lang/**/*.php' => [$testPath], + 'resources/lang/**/*.json' => [$testPath], // Build tool config — affects compiled assets consumed by // browser and Inertia tests. - 'vite.config.js' => [$featurePath], - 'vite.config.ts' => [$featurePath], - 'webpack.mix.js' => [$featurePath], - 'tailwind.config.js' => [$featurePath], - 'tailwind.config.ts' => [$featurePath], - 'postcss.config.js' => [$featurePath], + 'vite.config.js' => [$testPath], + 'vite.config.ts' => [$testPath], + 'webpack.mix.js' => [$testPath], + 'tailwind.config.js' => [$testPath], + 'tailwind.config.ts' => [$testPath], + 'postcss.config.js' => [$testPath], ]; } } diff --git a/src/Plugins/Tia/WatchPatterns.php b/src/Plugins/Tia/WatchPatterns.php index 71591bbf..0d03e243 100644 --- a/src/Plugins/Tia/WatchPatterns.php +++ b/src/Plugins/Tia/WatchPatterns.php @@ -8,12 +8,13 @@ use Pest\Plugins\Tia\WatchDefaults\WatchDefault; use Pest\TestSuite; /** - * Maps non-PHP file globs to the test directories they should invalidate. + * Maps non-PHP file globs to the tests they should invalidate. * * Coverage drivers only see `.php` files. Frontend assets, config files, * Blade templates, routes and environment files are invisible to the graph. * Watch patterns bridge the gap: when a changed file matches a glob, every - * test under the associated directory is marked as affected. + * test under the associated directory (or the exact associated test file) is + * marked as affected. * * Defaults are assembled dynamically from the `WatchDefaults/` registry — * each implementation probes the current project and contributes patterns @@ -38,7 +39,7 @@ final class WatchPatterns ]; /** - * @var array> glob → list of project-relative test dirs + * @var array> glob → list of project-relative test dirs/files */ private array $patterns = []; @@ -71,7 +72,7 @@ final class WatchPatterns * Adds user-defined patterns. Merges with existing entries so a single * glob can map to multiple directories. * - * @param array $patterns glob → project-relative test dir + * @param array $patterns glob → project-relative test dir/file */ public function add(array $patterns): void { @@ -83,12 +84,12 @@ final class WatchPatterns } /** - * Returns all test directories whose watch patterns match at least one of + * Returns all test targets whose watch patterns match at least one of * the given changed files. * * @param string $projectRoot Absolute path. * @param array $changedFiles Project-relative paths. - * @return array Project-relative test directories. + * @return array Project-relative test dirs/files. */ public function matchedDirectories(string $projectRoot, array $changedFiles): array { @@ -112,10 +113,10 @@ final class WatchPatterns } /** - * Given the affected directories, returns every test file in the graph - * that lives under one of those directories. + * Given the affected targets, returns every test file in the graph that + * either matches an exact file target or lives under a directory target. * - * @param array $directories Project-relative dirs. + * @param array $directories Project-relative dirs/files. * @param array $allTestFiles Project-relative test files from graph. * @return array */ @@ -128,8 +129,14 @@ final class WatchPatterns $affected = []; foreach ($allTestFiles as $testFile) { - foreach ($directories as $dir) { - $prefix = rtrim($dir, '/').'/'; + foreach ($directories as $target) { + if ($testFile === $target) { + $affected[] = $testFile; + + break; + } + + $prefix = rtrim($target, '/').'/'; if (str_starts_with($testFile, $prefix)) { $affected[] = $testFile; diff --git a/src/Subscribers/EnsureTiaCoverageIsRecorded.php b/src/Subscribers/EnsureTiaCoverageIsRecorded.php index f7ef815f..09cd2de8 100644 --- a/src/Subscribers/EnsureTiaCoverageIsRecorded.php +++ b/src/Subscribers/EnsureTiaCoverageIsRecorded.php @@ -10,8 +10,9 @@ use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; /** - * Starts PCOV collection before each test. No-op unless the TIA recorder was - * activated by the `--tia` plugin. + * Starts PCOV collection before each test. Pest tests start from + * `Testable::setUp()` so Laravel boot is covered; this subscriber remains the + * fallback for PHPUnit-style tests and is idempotent for Pest tests. * * @internal */