mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 10:52:14 +02:00
wip
This commit is contained in:
@ -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
|
||||
{
|
||||
|
||||
@ -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<string, string> $patterns glob → project-relative test dir
|
||||
* @return $this
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia\Edges;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final readonly class AutoloadEdges
|
||||
{
|
||||
/**
|
||||
* @return array<string, true>
|
||||
*/
|
||||
public static function snapshot(): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
foreach (get_included_files() as $file) {
|
||||
if ($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, (string) $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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<string, bool> */
|
||||
private array $classUsesDatabaseCache = [];
|
||||
|
||||
/** @var array<string, list<string>> */
|
||||
private array $fileToClassNames = [];
|
||||
|
||||
/** @var array<string, true> */
|
||||
private array $indexedClassNames = [];
|
||||
|
||||
/** @var array<string, list<string>> */
|
||||
private array $classDependencyCache = [];
|
||||
|
||||
/** @var array<string, list<string>> */
|
||||
private array $testImportFileCache = [];
|
||||
|
||||
/** @var array<string, true> */
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, string> $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<string> */
|
||||
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<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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user