mirror of
https://github.com/pestphp/pest.git
synced 2026-04-21 14:37:29 +02:00
feat(tia): continues to work on poc
This commit is contained in:
@ -119,6 +119,14 @@ final readonly class Configuration
|
|||||||
return new Browser\Configuration;
|
return new Browser\Configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the TIA (Test Impact Analysis) configuration.
|
||||||
|
*/
|
||||||
|
public function tia(): Plugins\Tia\Configuration
|
||||||
|
{
|
||||||
|
return new Plugins\Tia\Configuration;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proxies calls to the uses method.
|
* Proxies calls to the uses method.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -284,6 +284,11 @@ final class Tia implements AddsOutput, AfterEachable, BeforeEachable, HandlesArg
|
|||||||
*/
|
*/
|
||||||
private function handleParent(array $arguments, string $projectRoot, bool $forceRebuild): array
|
private function handleParent(array $arguments, string $projectRoot, bool $forceRebuild): array
|
||||||
{
|
{
|
||||||
|
// Initialise watch patterns (defaults + any user additions from
|
||||||
|
// tests/Pest.php which has already been loaded by BootFiles at
|
||||||
|
// this point).
|
||||||
|
WatchPatterns::instance()->useDefaults($projectRoot);
|
||||||
|
|
||||||
$cachePath = $projectRoot.DIRECTORY_SEPARATOR.self::CACHE_PATH;
|
$cachePath = $projectRoot.DIRECTORY_SEPARATOR.self::CACHE_PATH;
|
||||||
$fingerprint = Fingerprint::compute($projectRoot);
|
$fingerprint = Fingerprint::compute($projectRoot);
|
||||||
|
|
||||||
|
|||||||
38
src/Plugins/Tia/Configuration.php
Normal file
38
src/Plugins/Tia/Configuration.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-facing TIA configuration, returned by `pest()->tia()`.
|
||||||
|
*
|
||||||
|
* Usage in `tests/Pest.php`:
|
||||||
|
*
|
||||||
|
* pest()->tia()->watch([
|
||||||
|
* 'resources/js/**\/*.tsx' => 'tests/Browser',
|
||||||
|
* 'public/build/**\/*' => 'tests/Browser',
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
* Patterns are merged with the built-in defaults (config, routes, views,
|
||||||
|
* frontend assets, migrations). Duplicate glob keys overwrite the default
|
||||||
|
* mapping so users can redirect a pattern to a narrower directory.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Configuration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds watch-pattern → test-directory mappings that supplement (or
|
||||||
|
* override) the built-in defaults.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $patterns glob → project-relative test dir
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function watch(array $patterns): self
|
||||||
|
{
|
||||||
|
WatchPatterns::instance()->add($patterns);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -88,11 +88,18 @@ final class Graph
|
|||||||
/**
|
/**
|
||||||
* Returns the set of test files whose dependencies intersect $changedFiles.
|
* Returns the set of test files whose dependencies intersect $changedFiles.
|
||||||
*
|
*
|
||||||
|
* Two resolution paths:
|
||||||
|
* 1. **Coverage edges** — test depends on a PHP source file that changed.
|
||||||
|
* 2. **Watch patterns** — a non-PHP file (JS, CSS, config, …) matches a
|
||||||
|
* glob that maps to a test directory; every test under that directory
|
||||||
|
* is affected.
|
||||||
|
*
|
||||||
* @param array<int, string> $changedFiles Absolute or relative paths.
|
* @param array<int, string> $changedFiles Absolute or relative paths.
|
||||||
* @return array<int, string> Relative test file paths.
|
* @return array<int, string> Relative test file paths.
|
||||||
*/
|
*/
|
||||||
public function affected(array $changedFiles): array
|
public function affected(array $changedFiles): array
|
||||||
{
|
{
|
||||||
|
// 1. Coverage-edge lookup (PHP → PHP).
|
||||||
$changedIds = [];
|
$changedIds = [];
|
||||||
|
|
||||||
foreach ($changedFiles as $file) {
|
foreach ($changedFiles as $file) {
|
||||||
@ -107,19 +114,38 @@ final class Graph
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$affected = [];
|
$affectedSet = [];
|
||||||
|
|
||||||
foreach ($this->edges as $testFile => $ids) {
|
foreach ($this->edges as $testFile => $ids) {
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
if (isset($changedIds[$id])) {
|
if (isset($changedIds[$id])) {
|
||||||
$affected[] = $testFile;
|
$affectedSet[$testFile] = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $affected;
|
// 2. Watch-pattern lookup (non-PHP assets → test directories).
|
||||||
|
$watchPatterns = WatchPatterns::instance();
|
||||||
|
$normalised = [];
|
||||||
|
|
||||||
|
foreach ($changedFiles as $file) {
|
||||||
|
$rel = $this->relative($file);
|
||||||
|
|
||||||
|
if ($rel !== null) {
|
||||||
|
$normalised[] = $rel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dirs = $watchPatterns->matchedDirectories($this->projectRoot, $normalised);
|
||||||
|
$allTestFiles = array_keys($this->edges);
|
||||||
|
|
||||||
|
foreach ($watchPatterns->testsUnderDirectories($dirs, $allTestFiles) as $testFile) {
|
||||||
|
$affectedSet[$testFile] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($affectedSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
118
src/Plugins/Tia/WatchDefaults/Browser.php
Normal file
118
src/Plugins/Tia/WatchDefaults/Browser.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||||
|
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
use Pest\Factories\TestCaseFactory;
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Browser implements WatchDefault
|
||||||
|
{
|
||||||
|
public function applicable(): bool
|
||||||
|
{
|
||||||
|
// Browser tests can exist in any PHP project. We only activate when
|
||||||
|
// there is an actual `tests/Browser` directory OR pest-plugin-browser
|
||||||
|
// is installed.
|
||||||
|
return class_exists(InstalledVersions::class)
|
||||||
|
&& InstalledVersions::isInstalled('pestphp/pest-plugin-browser');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaults(string $projectRoot, string $testPath): array
|
||||||
|
{
|
||||||
|
$browserDirs = $this->detectBrowserTestDirs($projectRoot, $testPath);
|
||||||
|
|
||||||
|
$globs = [
|
||||||
|
'resources/js/**/*.js',
|
||||||
|
'resources/js/**/*.ts',
|
||||||
|
'resources/js/**/*.tsx',
|
||||||
|
'resources/js/**/*.jsx',
|
||||||
|
'resources/js/**/*.vue',
|
||||||
|
'resources/js/**/*.svelte',
|
||||||
|
'resources/css/**/*.css',
|
||||||
|
'resources/css/**/*.scss',
|
||||||
|
'resources/css/**/*.less',
|
||||||
|
// Vite / Webpack build output that browser tests may consume.
|
||||||
|
'public/build/**/*.js',
|
||||||
|
'public/build/**/*.css',
|
||||||
|
];
|
||||||
|
|
||||||
|
$patterns = [];
|
||||||
|
|
||||||
|
foreach ($globs as $glob) {
|
||||||
|
$patterns[$glob] = $browserDirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function detectBrowserTestDirs(string $projectRoot, string $testPath): array
|
||||||
|
{
|
||||||
|
$dirs = [];
|
||||||
|
|
||||||
|
$candidate = $testPath.'/Browser';
|
||||||
|
|
||||||
|
if (is_dir($projectRoot.DIRECTORY_SEPARATOR.$candidate)) {
|
||||||
|
$dirs[] = $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan TestRepository via BrowserTestIdentifier if pest-plugin-browser
|
||||||
|
// is installed to find tests using `visit()` outside the conventional
|
||||||
|
// Browser/ folder.
|
||||||
|
if (class_exists(\Pest\Browser\Support\BrowserTestIdentifier::class)) {
|
||||||
|
$repo = TestSuite::getInstance()->tests;
|
||||||
|
|
||||||
|
foreach ($repo->getFilenames() as $filename) {
|
||||||
|
$factory = $repo->get($filename);
|
||||||
|
|
||||||
|
if (! $factory instanceof TestCaseFactory) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($factory->methods as $method) {
|
||||||
|
if (\Pest\Browser\Support\BrowserTestIdentifier::isBrowserTest($method)) {
|
||||||
|
$rel = $this->fileRelative($projectRoot, $filename);
|
||||||
|
|
||||||
|
if ($rel !== null) {
|
||||||
|
$dirs[] = dirname($rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($dirs === [] ? [$testPath] : $dirs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fileRelative(string $projectRoot, string $path): ?string
|
||||||
|
{
|
||||||
|
$real = @realpath($path);
|
||||||
|
|
||||||
|
if ($real === false) {
|
||||||
|
$real = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = rtrim($projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
if (! str_starts_with($real, $root)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace(DIRECTORY_SEPARATOR, '/', substr($real, strlen($root)));
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Plugins/Tia/WatchDefaults/Inertia.php
Normal file
53
src/Plugins/Tia/WatchDefaults/Inertia.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||||
|
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch patterns for Inertia.js projects (Laravel or otherwise).
|
||||||
|
*
|
||||||
|
* Inertia bridges PHP controllers with JS/TS page components. A change to
|
||||||
|
* a React / Vue / Svelte page can break assertions in browser tests or
|
||||||
|
* Inertia-specific feature tests.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Inertia implements WatchDefault
|
||||||
|
{
|
||||||
|
public function applicable(): bool
|
||||||
|
{
|
||||||
|
return class_exists(InstalledVersions::class)
|
||||||
|
&& (InstalledVersions::isInstalled('inertiajs/inertia-laravel')
|
||||||
|
|| InstalledVersions::isInstalled('rompetomp/inertia-bundle'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaults(string $projectRoot, string $testPath): array
|
||||||
|
{
|
||||||
|
$browserDir = is_dir($projectRoot.DIRECTORY_SEPARATOR.$testPath.'/Browser')
|
||||||
|
? $testPath.'/Browser'
|
||||||
|
: $testPath;
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Inertia page components (React / Vue / Svelte).
|
||||||
|
'resources/js/Pages/**/*.vue' => [$testPath, $browserDir],
|
||||||
|
'resources/js/Pages/**/*.tsx' => [$testPath, $browserDir],
|
||||||
|
'resources/js/Pages/**/*.jsx' => [$testPath, $browserDir],
|
||||||
|
'resources/js/Pages/**/*.svelte' => [$testPath, $browserDir],
|
||||||
|
|
||||||
|
// Shared layouts / components consumed by pages.
|
||||||
|
'resources/js/Layouts/**/*.vue' => [$browserDir],
|
||||||
|
'resources/js/Layouts/**/*.tsx' => [$browserDir],
|
||||||
|
'resources/js/Components/**/*.vue' => [$browserDir],
|
||||||
|
'resources/js/Components/**/*.tsx' => [$browserDir],
|
||||||
|
|
||||||
|
// SSR entry point.
|
||||||
|
'resources/js/ssr.js' => [$browserDir],
|
||||||
|
'resources/js/ssr.ts' => [$browserDir],
|
||||||
|
'resources/js/app.js' => [$browserDir],
|
||||||
|
'resources/js/app.ts' => [$browserDir],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/Plugins/Tia/WatchDefaults/Laravel.php
Normal file
81
src/Plugins/Tia/WatchDefaults/Laravel.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||||
|
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch patterns for Laravel projects.
|
||||||
|
*
|
||||||
|
* Laravel boots the entire application inside `setUp()` (before PHPUnit's
|
||||||
|
* `Prepared` event where TIA's coverage window opens). That means PHP files
|
||||||
|
* loaded during boot — config, routes, service providers, migrations — are
|
||||||
|
* invisible to the coverage driver. Watch patterns are the only way to
|
||||||
|
* track them.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Laravel implements WatchDefault
|
||||||
|
{
|
||||||
|
public function applicable(): bool
|
||||||
|
{
|
||||||
|
return class_exists(InstalledVersions::class)
|
||||||
|
&& InstalledVersions::isInstalled('laravel/framework');
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// and seeds DB based on config values.
|
||||||
|
'config/*.php' => [$testPath],
|
||||||
|
'config/**/*.php' => [$testPath],
|
||||||
|
|
||||||
|
// Routes — loaded during boot. HTTP/Feature tests depend on them.
|
||||||
|
'routes/*.php' => [$featurePath],
|
||||||
|
'routes/**/*.php' => [$featurePath],
|
||||||
|
|
||||||
|
// Service providers / bootstrap — loaded during boot, affect
|
||||||
|
// bindings, middleware, event listeners, scheduled tasks.
|
||||||
|
'bootstrap/app.php' => [$testPath],
|
||||||
|
'bootstrap/providers.php' => [$testPath],
|
||||||
|
|
||||||
|
// Migrations — run via RefreshDatabase/FastRefreshDatabase in
|
||||||
|
// setUp. Schema changes can break any test that touches DB.
|
||||||
|
'database/migrations/**/*.php' => [$testPath],
|
||||||
|
|
||||||
|
// Seeders — often run globally via Pest.php beforeEach.
|
||||||
|
'database/seeders/**/*.php' => [$testPath],
|
||||||
|
|
||||||
|
// Factories — loaded lazily but still PHP that coverage may miss
|
||||||
|
// if the factory file was already autoloaded before Prepared.
|
||||||
|
'database/factories/**/*.php' => [$testPath],
|
||||||
|
|
||||||
|
// Blade templates — compiled to cache, source file not executed.
|
||||||
|
'resources/views/**/*.blade.php' => [$featurePath],
|
||||||
|
|
||||||
|
// 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],
|
||||||
|
|
||||||
|
// 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],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/Plugins/Tia/WatchDefaults/Livewire.php
Normal file
38
src/Plugins/Tia/WatchDefaults/Livewire.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||||
|
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch patterns for projects using Livewire.
|
||||||
|
*
|
||||||
|
* Livewire components pair a PHP class with a Blade view. A view change can
|
||||||
|
* break rendering or assertions in feature / browser tests even though the
|
||||||
|
* PHP side is untouched.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Livewire implements WatchDefault
|
||||||
|
{
|
||||||
|
public function applicable(): bool
|
||||||
|
{
|
||||||
|
return class_exists(InstalledVersions::class)
|
||||||
|
&& InstalledVersions::isInstalled('livewire/livewire');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaults(string $projectRoot, string $testPath): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Livewire views live alongside Blade views or in a dedicated dir.
|
||||||
|
'resources/views/livewire/**/*.blade.php' => [$testPath],
|
||||||
|
'resources/views/components/**/*.blade.php' => [$testPath],
|
||||||
|
|
||||||
|
// Livewire JS interop / Alpine plugins.
|
||||||
|
'resources/js/**/*.js' => [$testPath],
|
||||||
|
'resources/js/**/*.ts' => [$testPath],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Plugins/Tia/WatchDefaults/Php.php
Normal file
53
src/Plugins/Tia/WatchDefaults/Php.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baseline watch patterns for any PHP project.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Php implements WatchDefault
|
||||||
|
{
|
||||||
|
public function applicable(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaults(string $projectRoot, string $testPath): array
|
||||||
|
{
|
||||||
|
// NOTE: composer.json / composer.lock changes are caught by the
|
||||||
|
// fingerprint (which hashes composer.lock). PHP files are tracked by
|
||||||
|
// the coverage driver. Only non-PHP, non-fingerprinted files that
|
||||||
|
// can silently alter test behaviour belong here.
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Environment files — can change DB drivers, feature flags,
|
||||||
|
// queue connections, etc. Not PHP, not fingerprinted.
|
||||||
|
'.env' => [$testPath],
|
||||||
|
'.env.testing' => [$testPath],
|
||||||
|
|
||||||
|
// Docker / CI — can affect integration test infrastructure.
|
||||||
|
'docker-compose.yml' => [$testPath],
|
||||||
|
'docker-compose.yaml' => [$testPath],
|
||||||
|
|
||||||
|
// PHPUnit / Pest config (XML) — phpunit.xml IS fingerprinted, but
|
||||||
|
// phpunit.xml.dist and other XML overrides are not individually
|
||||||
|
// tracked by the coverage driver.
|
||||||
|
'phpunit.xml.dist' => [$testPath],
|
||||||
|
|
||||||
|
// Test fixtures — JSON, CSV, XML, TXT data files consumed by
|
||||||
|
// assertions. A fixture change can flip a test result.
|
||||||
|
$testPath.'/Fixtures/**/*.json' => [$testPath],
|
||||||
|
$testPath.'/Fixtures/**/*.csv' => [$testPath],
|
||||||
|
$testPath.'/Fixtures/**/*.xml' => [$testPath],
|
||||||
|
$testPath.'/Fixtures/**/*.txt' => [$testPath],
|
||||||
|
|
||||||
|
// Pest snapshots — external edits to snapshot files invalidate
|
||||||
|
// snapshot assertions.
|
||||||
|
$testPath.'/.pest/snapshots/**/*.snap' => [$testPath],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/Plugins/Tia/WatchDefaults/Symfony.php
Normal file
75
src/Plugins/Tia/WatchDefaults/Symfony.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||||
|
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch patterns for Symfony projects.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final readonly class Symfony implements WatchDefault
|
||||||
|
{
|
||||||
|
public function applicable(): bool
|
||||||
|
{
|
||||||
|
return class_exists(InstalledVersions::class)
|
||||||
|
&& InstalledVersions::isInstalled('symfony/framework-bundle');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaults(string $projectRoot, string $testPath): array
|
||||||
|
{
|
||||||
|
// Symfony boots the kernel in setUp() (before the coverage window).
|
||||||
|
// PHP config, routes, kernel, and migrations are loaded during boot
|
||||||
|
// and invisible to the coverage driver. Same reasoning as Laravel.
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Config — YAML, XML, and PHP. All loaded during kernel boot.
|
||||||
|
'config/*.yaml' => [$testPath],
|
||||||
|
'config/*.yml' => [$testPath],
|
||||||
|
'config/*.php' => [$testPath],
|
||||||
|
'config/*.xml' => [$testPath],
|
||||||
|
'config/**/*.yaml' => [$testPath],
|
||||||
|
'config/**/*.yml' => [$testPath],
|
||||||
|
'config/**/*.php' => [$testPath],
|
||||||
|
'config/**/*.xml' => [$testPath],
|
||||||
|
|
||||||
|
// Routes — loaded during boot.
|
||||||
|
'config/routes/*.yaml' => [$testPath],
|
||||||
|
'config/routes/*.php' => [$testPath],
|
||||||
|
'config/routes/*.xml' => [$testPath],
|
||||||
|
'config/routes/**/*.yaml' => [$testPath],
|
||||||
|
|
||||||
|
// Kernel / bootstrap — loaded during boot.
|
||||||
|
'src/Kernel.php' => [$testPath],
|
||||||
|
|
||||||
|
// Migrations — run during setUp (before coverage window).
|
||||||
|
'migrations/**/*.php' => [$testPath],
|
||||||
|
|
||||||
|
// Twig templates — compiled, source not PHP-executed.
|
||||||
|
'templates/**/*.html.twig' => [$testPath],
|
||||||
|
'templates/**/*.twig' => [$testPath],
|
||||||
|
|
||||||
|
// Translations (YAML / XLF / XLIFF).
|
||||||
|
'translations/**/*.yaml' => [$testPath],
|
||||||
|
'translations/**/*.yml' => [$testPath],
|
||||||
|
'translations/**/*.xlf' => [$testPath],
|
||||||
|
'translations/**/*.xliff' => [$testPath],
|
||||||
|
|
||||||
|
// Doctrine XML/YAML mappings.
|
||||||
|
'config/doctrine/**/*.xml' => [$testPath],
|
||||||
|
'config/doctrine/**/*.yaml' => [$testPath],
|
||||||
|
|
||||||
|
// Webpack Encore / asset-mapper config + frontend sources.
|
||||||
|
'webpack.config.js' => [$testPath],
|
||||||
|
'importmap.php' => [$testPath],
|
||||||
|
'assets/**/*.js' => [$testPath],
|
||||||
|
'assets/**/*.ts' => [$testPath],
|
||||||
|
'assets/**/*.vue' => [$testPath],
|
||||||
|
'assets/**/*.css' => [$testPath],
|
||||||
|
'assets/**/*.scss' => [$testPath],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Plugins/Tia/WatchDefaults/WatchDefault.php
Normal file
28
src/Plugins/Tia/WatchDefaults/WatchDefault.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of file-watch patterns that apply when a particular framework,
|
||||||
|
* library or project layout is detected.
|
||||||
|
*
|
||||||
|
* Each implementation probes for the presence of the tool it covers
|
||||||
|
* (`applicable`) and returns glob → test-directory mappings (`defaults`)
|
||||||
|
* that are merged into `WatchPatterns`.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface WatchDefault
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Whether this default set applies to the current project.
|
||||||
|
*/
|
||||||
|
public function applicable(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<int, string>> glob → list of project-relative test dirs
|
||||||
|
*/
|
||||||
|
public function defaults(string $projectRoot, string $testPath): array;
|
||||||
|
}
|
||||||
194
src/Plugins/Tia/WatchPatterns.php
Normal file
194
src/Plugins/Tia/WatchPatterns.php
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Tia;
|
||||||
|
|
||||||
|
use Pest\Plugins\Tia\WatchDefaults\WatchDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps non-PHP file globs to the test directories 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.
|
||||||
|
*
|
||||||
|
* Defaults are assembled dynamically from the `WatchDefaults/` registry —
|
||||||
|
* each implementation probes the current project and contributes patterns
|
||||||
|
* when applicable. Users extend via `pest()->tia()->watch(…)`.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class WatchPatterns
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* All known default providers, in evaluation order.
|
||||||
|
*
|
||||||
|
* @var array<int, class-string<WatchDefault>>
|
||||||
|
*/
|
||||||
|
private const array DEFAULTS = [
|
||||||
|
WatchDefaults\Php::class,
|
||||||
|
WatchDefaults\Laravel::class,
|
||||||
|
WatchDefaults\Symfony::class,
|
||||||
|
WatchDefaults\Livewire::class,
|
||||||
|
WatchDefaults\Inertia::class,
|
||||||
|
WatchDefaults\Browser::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array<int, string>> glob → list of project-relative test dirs
|
||||||
|
*/
|
||||||
|
private array $patterns = [];
|
||||||
|
|
||||||
|
private static ?self $instance = null;
|
||||||
|
|
||||||
|
public static function instance(): self
|
||||||
|
{
|
||||||
|
return self::$instance ??= new self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probes every registered `WatchDefault` and merges the patterns of
|
||||||
|
* those that apply. Called once during Tia plugin boot, after BootFiles
|
||||||
|
* has loaded `tests/Pest.php` (so user-added `pest()->tia()->watch()`
|
||||||
|
* calls are already in `$this->patterns`).
|
||||||
|
*/
|
||||||
|
public function useDefaults(string $projectRoot): void
|
||||||
|
{
|
||||||
|
$testPath = \Pest\TestSuite::getInstance()->testPath;
|
||||||
|
|
||||||
|
foreach (self::DEFAULTS as $class) {
|
||||||
|
$default = new $class;
|
||||||
|
|
||||||
|
if (! $default->applicable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($default->defaults($projectRoot, $testPath) as $glob => $dirs) {
|
||||||
|
$this->patterns[$glob] = array_values(array_unique(
|
||||||
|
array_merge($this->patterns[$glob] ?? [], $dirs),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
public function add(array $patterns): void
|
||||||
|
{
|
||||||
|
foreach ($patterns as $glob => $dir) {
|
||||||
|
$this->patterns[$glob] = array_values(array_unique(
|
||||||
|
array_merge($this->patterns[$glob] ?? [], [$dir]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all test directories 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.
|
||||||
|
*/
|
||||||
|
public function matchedDirectories(string $projectRoot, array $changedFiles): array
|
||||||
|
{
|
||||||
|
if ($this->patterns === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$matched = [];
|
||||||
|
|
||||||
|
foreach ($changedFiles as $file) {
|
||||||
|
foreach ($this->patterns as $glob => $dirs) {
|
||||||
|
if ($this->globMatches($glob, $file)) {
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
$matched[$dir] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the affected directories, returns every test file in the graph
|
||||||
|
* that lives under one of those directories.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $directories Project-relative dirs.
|
||||||
|
* @param array<int, string> $allTestFiles Project-relative test files from graph.
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function testsUnderDirectories(array $directories, array $allTestFiles): array
|
||||||
|
{
|
||||||
|
if ($directories === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$affected = [];
|
||||||
|
|
||||||
|
foreach ($allTestFiles as $testFile) {
|
||||||
|
foreach ($directories as $dir) {
|
||||||
|
$prefix = rtrim($dir, '/').'/';
|
||||||
|
|
||||||
|
if (str_starts_with($testFile, $prefix)) {
|
||||||
|
$affected[] = $testFile;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $affected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(): void
|
||||||
|
{
|
||||||
|
$this->patterns = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a project-relative file against a glob pattern.
|
||||||
|
*
|
||||||
|
* Supports `*` (single segment), `**` (any depth) and `?`.
|
||||||
|
*/
|
||||||
|
private function globMatches(string $pattern, string $file): bool
|
||||||
|
{
|
||||||
|
$pattern = str_replace('\\', '/', $pattern);
|
||||||
|
$file = str_replace('\\', '/', $file);
|
||||||
|
|
||||||
|
$regex = '';
|
||||||
|
$len = strlen($pattern);
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
while ($i < $len) {
|
||||||
|
$c = $pattern[$i];
|
||||||
|
|
||||||
|
if ($c === '*' && isset($pattern[$i + 1]) && $pattern[$i + 1] === '*') {
|
||||||
|
$regex .= '.*';
|
||||||
|
$i += 2;
|
||||||
|
|
||||||
|
if (isset($pattern[$i]) && $pattern[$i] === '/') {
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
} elseif ($c === '*') {
|
||||||
|
$regex .= '[^/]*';
|
||||||
|
$i++;
|
||||||
|
} elseif ($c === '?') {
|
||||||
|
$regex .= '[^/]';
|
||||||
|
$i++;
|
||||||
|
} else {
|
||||||
|
$regex .= preg_quote($c, '#');
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) preg_match('#^'.$regex.'$#', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user