mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 10:52:14 +02:00
wip
This commit is contained in:
96
src/Plugins/Tia/AutoloadEdges.php
Normal file
96
src/Plugins/Tia/AutoloadEdges.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
/**
|
||||
* Captures PHP files that were included while a test was running.
|
||||
*
|
||||
* Coverage drivers miss declaration-only files (classes, enums, interfaces,
|
||||
* traits) and files loaded before the coverage window opens. Diffing
|
||||
* `get_included_files()` gives TIA an explicit edge for those autoloaded files.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class AutoloadEdges
|
||||
{
|
||||
/**
|
||||
* @return array<string, true>
|
||||
*/
|
||||
public static function snapshot(): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
foreach (get_included_files() as $file) {
|
||||
if (is_string($file) && $file !== '') {
|
||||
$files[$file] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, true> $before
|
||||
* @param array<string, true> $after
|
||||
* @return list<string>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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`.
|
||||
|
||||
@ -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<string, list<string>>
|
||||
*/
|
||||
private array $testImportFileCache = [];
|
||||
|
||||
/**
|
||||
* Included-file snapshot captured at the start of the current test.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
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<int, string> $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<string>
|
||||
*/
|
||||
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<string>
|
||||
*/
|
||||
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
|
||||
|
||||
@ -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<int, string>
|
||||
*/
|
||||
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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string, array<int, string>> glob → list of project-relative test dirs
|
||||
* @var array<string, array<int, string>> 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<string, string> $patterns glob → project-relative test dir
|
||||
* @param array<string, string> $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<int, string> $changedFiles Project-relative paths.
|
||||
* @return array<int, string> Project-relative test directories.
|
||||
* @return array<int, string> 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<int, string> $directories Project-relative dirs.
|
||||
* @param array<int, string> $directories Project-relative dirs/files.
|
||||
* @param array<int, string> $allTestFiles Project-relative test files from graph.
|
||||
* @return array<int, string>
|
||||
*/
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user