mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 02:52:12 +02:00
wip
This commit is contained in:
@ -79,7 +79,11 @@ final readonly class Fingerprint
|
|||||||
// composer.lock hash a behavioural subset — description,
|
// composer.lock hash a behavioural subset — description,
|
||||||
// keywords, scripts, authors, install timestamps, dist
|
// keywords, scripts, authors, install timestamps, dist
|
||||||
// URLs etc. no longer drift the structural fingerprint.
|
// URLs etc. no longer drift the structural fingerprint.
|
||||||
private const int SCHEMA_VERSION = 12;
|
// v13: Environment files (`.env`, `.env.testing`, local variants)
|
||||||
|
// are included in the environmental bucket. They are commonly
|
||||||
|
// git-ignored, so watch patterns alone cannot reliably notice
|
||||||
|
// edits; a drift drops cached results and re-executes the suite.
|
||||||
|
private const int SCHEMA_VERSION = 13;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
@ -128,6 +132,7 @@ final readonly class Fingerprint
|
|||||||
// the patch rarely changes anything test-visible.
|
// the patch rarely changes anything test-visible.
|
||||||
'php_minor' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION,
|
'php_minor' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION,
|
||||||
'extensions' => self::extensionsFingerprint($projectRoot),
|
'extensions' => self::extensionsFingerprint($projectRoot),
|
||||||
|
'env_files' => self::envFilesHash($projectRoot),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -289,6 +294,60 @@ final readonly class Fingerprint
|
|||||||
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hashes environment files that can globally alter app boot behaviour.
|
||||||
|
* These files are often git-ignored, so they cannot rely on changed-file
|
||||||
|
* detection. The environmental bucket keeps graph edges while forcing all
|
||||||
|
* cached results to refresh after an env edit.
|
||||||
|
*/
|
||||||
|
private static function envFilesHash(string $projectRoot): ?string
|
||||||
|
{
|
||||||
|
$paths = [
|
||||||
|
$projectRoot.'/.env',
|
||||||
|
$projectRoot.'/.env.testing',
|
||||||
|
$projectRoot.'/.env.local',
|
||||||
|
];
|
||||||
|
|
||||||
|
$localVariants = glob($projectRoot.'/.env.*.local');
|
||||||
|
|
||||||
|
if (is_array($localVariants)) {
|
||||||
|
foreach ($localVariants as $path) {
|
||||||
|
$paths[] = $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
if (isset($seen[$path])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$path] = true;
|
||||||
|
|
||||||
|
if (! is_file($path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = @file_get_contents($path);
|
||||||
|
|
||||||
|
if ($contents === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts[] = basename($path).':'.hash('xxh128', $contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parts === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sort($parts);
|
||||||
|
|
||||||
|
return hash('xxh128', implode("\n", $parts));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Behavioural subset of `composer.json`. Keeps the keys that
|
* Behavioural subset of `composer.json`. Keeps the keys that
|
||||||
* actually move test outcomes (autoload, require, extra,
|
* actually move test outcomes (autoload, require, extra,
|
||||||
|
|||||||
@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Plugins\Tia;
|
namespace Pest\Plugins\Tia;
|
||||||
|
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
use PHPUnit\Framework\TestStatus\TestStatus;
|
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
@ -119,6 +121,14 @@ final class Graph
|
|||||||
*/
|
*/
|
||||||
private readonly string $projectRoot;
|
private readonly string $projectRoot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached project-relative test files that contain at least one test in the
|
||||||
|
* `arch` group.
|
||||||
|
*
|
||||||
|
* @var array<string, true>|null
|
||||||
|
*/
|
||||||
|
private ?array $archTestFiles = null;
|
||||||
|
|
||||||
public function __construct(string $projectRoot)
|
public function __construct(string $projectRoot)
|
||||||
{
|
{
|
||||||
$real = @realpath($projectRoot);
|
$real = @realpath($projectRoot);
|
||||||
@ -420,7 +430,8 @@ final class Graph
|
|||||||
// Architecture tests inspect source structure by namespace / path rather
|
// Architecture tests inspect source structure by namespace / path rather
|
||||||
// than by executing the inspected files. A new enum/class can therefore
|
// than by executing the inspected files. A new enum/class can therefore
|
||||||
// fail an Arch expectation without ever producing a coverage edge. Keep
|
// fail an Arch expectation without ever producing a coverage edge. Keep
|
||||||
// this fallback narrow: only known Arch test files run, not the suite.
|
// this fallback narrow: only tests in Pest's `arch` group run, not the
|
||||||
|
// suite.
|
||||||
if ($sourcePhpChanged) {
|
if ($sourcePhpChanged) {
|
||||||
foreach (array_keys($this->edges) as $testFile) {
|
foreach (array_keys($this->edges) as $testFile) {
|
||||||
if ($this->isArchTestFile($testFile)) {
|
if ($this->isArchTestFile($testFile)) {
|
||||||
@ -910,6 +921,7 @@ final class Graph
|
|||||||
private function isProjectSourcePhp(string $rel): bool
|
private function isProjectSourcePhp(string $rel): bool
|
||||||
{
|
{
|
||||||
return str_ends_with($rel, '.php')
|
return str_ends_with($rel, '.php')
|
||||||
|
&& ! $this->isBladePath($rel)
|
||||||
&& ! str_starts_with($rel, 'tests/')
|
&& ! str_starts_with($rel, 'tests/')
|
||||||
&& ! str_starts_with($rel, 'vendor/')
|
&& ! str_starts_with($rel, 'vendor/')
|
||||||
&& ! str_starts_with($rel, 'storage/framework/')
|
&& ! str_starts_with($rel, 'storage/framework/')
|
||||||
@ -918,8 +930,97 @@ final class Graph
|
|||||||
|
|
||||||
private function isArchTestFile(string $rel): bool
|
private function isArchTestFile(string $rel): bool
|
||||||
{
|
{
|
||||||
return str_ends_with($rel, '.php')
|
return isset($this->archTestFiles()[$rel]);
|
||||||
&& (str_contains($rel, '/Arch/') || str_ends_with($rel, '/ArchTest.php'));
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, true>
|
||||||
|
*/
|
||||||
|
private function archTestFiles(): array
|
||||||
|
{
|
||||||
|
if ($this->archTestFiles !== null) {
|
||||||
|
return $this->archTestFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->archTestFiles = [];
|
||||||
|
$repo = TestSuite::getInstance()->tests;
|
||||||
|
|
||||||
|
foreach ($repo->getFilenames() as $filename) {
|
||||||
|
$factory = $repo->get($filename);
|
||||||
|
|
||||||
|
if ($factory === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($factory->methods as $method) {
|
||||||
|
if (! $this->methodHasGroup($method, 'arch')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rel = $this->relative($filename);
|
||||||
|
|
||||||
|
if ($rel !== null) {
|
||||||
|
$this->archTestFiles[$rel] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys($this->edges) as $testFile) {
|
||||||
|
if (isset($this->archTestFiles[$testFile])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->testSourceDeclaresArchGroup($testFile)) {
|
||||||
|
$this->archTestFiles[$testFile] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->archTestFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function methodHasGroup(object $method, string $group): bool
|
||||||
|
{
|
||||||
|
if (property_exists($method, 'groups') && is_array($method->groups) && in_array($group, $method->groups, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! property_exists($method, 'attributes') || ! is_array($method->attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($method->attributes as $attribute) {
|
||||||
|
if (! is_object($attribute)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! property_exists($attribute, 'name') || $attribute->name !== Group::class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! property_exists($attribute, 'arguments')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($attribute->arguments as $argument) {
|
||||||
|
if ($argument === $group) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function testSourceDeclaresArchGroup(string $rel): bool
|
||||||
|
{
|
||||||
|
$source = @file_get_contents($this->projectRoot.'/'.$rel);
|
||||||
|
|
||||||
|
if ($source === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_match('/\barch\s*\(/', $source) === 1
|
||||||
|
|| preg_match('/->\s*group\s*\(\s*[\'\"]arch[\'\"]/', $source) === 1
|
||||||
|
|| preg_match('/#\[\s*(?:\\\\)?(?:PHPUnit\\\\Framework\\\\Attributes\\\\)?Group\s*\(\s*[\'\"]arch[\'\"]/', $source) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isBladePath(string $rel): bool
|
private function isBladePath(string $rel): bool
|
||||||
|
|||||||
Reference in New Issue
Block a user