This commit is contained in:
nuno maduro
2026-05-02 18:47:26 +01:00
parent 4280233b40
commit 7bea819978
5 changed files with 86 additions and 127 deletions

View File

@ -166,7 +166,7 @@ final class TestCaseFactory
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode $traitsCode
private static \$__filename = '$filename'; public static \$__filename = '$filename';
$methodsCode $methodsCode
} }

View File

@ -1363,21 +1363,15 @@ private bool $piggybackCoverage = false;
return null; return null;
} }
$reflection = new \ReflectionClass($class); assert(property_exists($class, '__filename') && is_string($class::$__filename));
if ($reflection->hasProperty('__filename')) { $filename = $class::$__filename;
try {
$filename = $reflection->getStaticPropertyValue('__filename');
} catch (\ReflectionException) {
$filename = null;
}
if (is_string($filename) && $filename !== '' && ! str_contains($filename, "eval()'d")) { if ($filename !== '' && ! str_contains($filename, "eval()'d")) {
return $filename; return $filename;
}
} }
$current = $reflection; $current = new \ReflectionClass($class);
while ($current !== false) { while ($current !== false) {
$file = $current->getFileName(); $file = $current->getFileName();

View File

@ -32,6 +32,29 @@ final readonly class BaselineSync
private const int FETCH_COOLDOWN_SECONDS = 86400; private const int FETCH_COOLDOWN_SECONDS = 86400;
private const array DIAGNOSES = [
'network' => [
'pattern' => '/could not resolve host|connection refused|connection reset|temporary failure in name resolution|network is unreachable|no route to host|i\/o timeout|tls handshake|getaddrinfo/i',
'message' => 'network error (offline or DNS unreachable). Try again when connected.',
],
'gh-auth' => [
'pattern' => '/authentication failed|not logged in|requires authentication|bad credentials|401/i',
'message' => 'authentication failed — run `gh auth login` and retry.',
],
'rate-limit' => [
'pattern' => '/rate limit|too many requests|secondary rate limit/i',
'message' => 'GitHub API rate limit hit — try again later.',
],
'not-found' => [
'pattern' => '/404|not found|repository not found/i',
'message' => 'workflow or artifact not found in repo.',
],
'forbidden' => [
'pattern' => '/403|forbidden|access denied/i',
'message' => 'access denied — check that your `gh` token has repo + actions read scope.',
],
];
public function __construct( public function __construct(
private State $state, private State $state,
private OutputInterface $output, private OutputInterface $output,
@ -64,8 +87,9 @@ final readonly class BaselineSync
return false; return false;
} }
$failureKind = null; $result = $this->download($repo, $projectRoot, $hasAnchor);
$payload = $this->download($repo, $projectRoot, $failureKind, $hasAnchor); $payload = $result['payload'];
$failureKind = $result['failureKind'];
if ($payload === null) { if ($payload === null) {
if ($failureKind === 'no-runs' || $failureKind === null) { if ($failureKind === 'no-runs' || $failureKind === null) {
@ -162,7 +186,7 @@ final readonly class BaselineSync
$this->output->writeln(['', ...$indentedYaml, '']); $this->output->writeln(['', ...$indentedYaml, '']);
$this->renderChild(sprintf('Commit, push, then run once: gh workflow run tia-baseline.yml -R %s', $repo)); $this->renderChild(sprintf('Commit, push, then run once: gh workflow run tia-baseline.yml -R %s', $repo));
$this->renderChild('Details: https://pestphp.com/docs/tia/ci'); $this->renderChild('Details: https://pestphp.com/docs/tia');
} }
private function isCi(): bool private function isCi(): bool
@ -281,41 +305,27 @@ YAML;
} }
/** /**
* @param-out string|null $failureKind * @return array{payload: array{graph: string, coverage: ?string, sizeOnDisk: int}|null, failureKind: ?string}
*
* @return array{graph: string, coverage: ?string, sizeOnDisk: int}|null
*/ */
private function download(string $repo, string $projectRoot, ?string &$failureKind = null, bool $hasAnchor = false): ?array private function download(string $repo, string $projectRoot, bool $hasAnchor = false): array
{ {
$failureKind = null;
$this->validateGhDependencies($hasAnchor); $this->validateGhDependencies($hasAnchor);
[$runId, $listError] = $this->latestSuccessfulRunIdWithError($repo); [$runId, $listError] = $this->latestSuccessfulRunIdWithError($repo);
if ($listError !== null) { if ($listError !== null) {
$failureKind = $listError['kind']; $this->panicOnClassifiedError($listError, 'Failed to query baseline runs', $hasAnchor);
if (in_array($failureKind, ['forbidden', 'not-found'], true)) {
Panic::with(new BaselineFetchFailed(
sprintf('Failed to query baseline runs — %s', $listError['message']),
'Verify workflow tia-baseline.yml, artifact pest-tia-baseline, and gh token scope.',
$hasAnchor,
));
}
$this->renderBadge('WARN', sprintf( $this->renderBadge('WARN', sprintf(
'Failed to query baseline runs — %s', 'Failed to query baseline runs — %s',
$listError['message'], $listError['message'],
)); ));
return null; return ['payload' => null, 'failureKind' => $listError['kind']];
} }
if ($runId === null) { if ($runId === null) {
$failureKind = 'no-runs'; return ['payload' => null, 'failureKind' => 'no-runs'];
return null;
} }
$runCacheDir = $this->downloadCacheDir($projectRoot).DIRECTORY_SEPARATOR.$this->safeRunId($runId); $runCacheDir = $this->downloadCacheDir($projectRoot).DIRECTORY_SEPARATOR.$this->safeRunId($runId);
@ -329,22 +339,40 @@ YAML;
$runId, $runId,
)); ));
return $this->readArtifact($runCacheDir); return ['payload' => $this->readArtifact($runCacheDir), 'failureKind' => null];
} }
if (! @mkdir($runCacheDir, 0755, true) && ! is_dir($runCacheDir)) { if (! @mkdir($runCacheDir, 0755, true) && ! is_dir($runCacheDir)) {
return null; return ['payload' => null, 'failureKind' => null];
} }
if (! $this->downloadArtifact($repo, $runId, $runCacheDir, $hasAnchor, $failureKind)) { $download = $this->downloadArtifact($repo, $runId, $runCacheDir, $hasAnchor);
return null;
if (! $download['success']) {
return ['payload' => null, 'failureKind' => $download['failureKind']];
} }
$payload = $this->validateDownloadedArtifact($runCacheDir, $hasAnchor); $payload = $this->validateDownloadedArtifact($runCacheDir, $hasAnchor);
$this->trimDownloadCache($projectRoot); $this->trimDownloadCache($projectRoot);
return $payload; return ['payload' => $payload, 'failureKind' => null];
}
/**
* @param array{kind: string, message: string} $diagnosis
*/
private function panicOnClassifiedError(array $diagnosis, string $contextPrefix, bool $hasAnchor): void
{
if (! in_array($diagnosis['kind'], ['forbidden', 'not-found'], true)) {
return;
}
Panic::with(new BaselineFetchFailed(
sprintf('%s — %s', $contextPrefix, $diagnosis['message']),
'Verify workflow tia-baseline.yml, artifact pest-tia-baseline, and gh token scope.',
$hasAnchor,
));
} }
private function validateGhDependencies(bool $hasAnchor): void private function validateGhDependencies(bool $hasAnchor): void
@ -367,9 +395,9 @@ YAML;
} }
/** /**
* @param-out string|null $failureKind * @return array{success: bool, failureKind: ?string}
*/ */
private function downloadArtifact(string $repo, string $runId, string $runCacheDir, bool $hasAnchor, ?string &$failureKind): bool private function downloadArtifact(string $repo, string $runId, string $runCacheDir, bool $hasAnchor): array
{ {
$artifactSize = $this->artifactSize($repo, $runId); $artifactSize = $this->artifactSize($repo, $runId);
@ -404,28 +432,21 @@ YAML;
$this->clearProgressLine(); $this->clearProgressLine();
if ($process->isSuccessful()) { if ($process->isSuccessful()) {
return true; return ['success' => true, 'failureKind' => null];
} }
$this->cleanup($runCacheDir); $this->cleanup($runCacheDir);
$diagnosis = $this->classifyGhError($process->getErrorOutput().$process->getOutput()); $diagnosis = $this->classifyGhError($process->getErrorOutput().$process->getOutput());
$failureKind = $diagnosis['kind'];
if (in_array($failureKind, ['forbidden', 'not-found'], true)) { $this->panicOnClassifiedError($diagnosis, 'Baseline download failed', $hasAnchor);
Panic::with(new BaselineFetchFailed(
sprintf('Baseline download failed — %s', $diagnosis['message']),
'Verify workflow tia-baseline.yml, artifact pest-tia-baseline, and gh token scope.',
$hasAnchor,
));
}
$this->renderBadge('WARN', sprintf( $this->renderBadge('WARN', sprintf(
'Baseline download failed — %s', 'Baseline download failed — %s',
$diagnosis['message'], $diagnosis['message'],
)); ));
return false; return ['success' => false, 'failureKind' => $diagnosis['kind']];
} }
/** /**
@ -575,12 +596,10 @@ YAML;
$candidates = []; $candidates = [];
foreach ($entries as $entry) { foreach ($entries as $entry) {
if ($entry === '.') { if (in_array($entry, ['.', '..'], true)) {
continue;
}
if ($entry === '..') {
continue; continue;
} }
$path = $root.DIRECTORY_SEPARATOR.$entry; $path = $root.DIRECTORY_SEPARATOR.$entry;
if (! is_dir($path)) { if (! is_dir($path)) {
@ -651,30 +670,7 @@ YAML;
return ['kind' => 'unknown', 'message' => 'unknown error']; return ['kind' => 'unknown', 'message' => 'unknown error'];
} }
$diagnoses = [ foreach (self::DIAGNOSES as $kind => $diagnosis) {
'network' => [
'pattern' => '/could not resolve host|connection refused|connection reset|temporary failure in name resolution|network is unreachable|no route to host|i\/o timeout|tls handshake|getaddrinfo/i',
'message' => 'network error (offline or DNS unreachable). Try again when connected.',
],
'gh-auth' => [
'pattern' => '/authentication failed|not logged in|requires authentication|bad credentials|401/i',
'message' => 'authentication failed — run `gh auth login` and retry.',
],
'rate-limit' => [
'pattern' => '/rate limit|too many requests|secondary rate limit/i',
'message' => 'GitHub API rate limit hit — try again later.',
],
'not-found' => [
'pattern' => '/404|not found|repository not found/i',
'message' => 'workflow or artifact not found in repo.',
],
'forbidden' => [
'pattern' => '/403|forbidden|access denied/i',
'message' => 'access denied — check that your `gh` token has repo + actions read scope.',
],
];
foreach ($diagnoses as $kind => $diagnosis) {
if (preg_match($diagnosis['pattern'], $output) === 1) { if (preg_match($diagnosis['pattern'], $output) === 1) {
return ['kind' => $kind, 'message' => $diagnosis['message']]; return ['kind' => $kind, 'message' => $diagnosis['message']];
} }
@ -685,17 +681,10 @@ YAML;
private function commandExists(string $cmd): bool private function commandExists(string $cmd): bool
{ {
$probe = new Process(['command', '-v', $cmd]); $process = new Process(['which', $cmd]);
$probe->run(); $process->run();
if ($probe->isSuccessful()) { return $process->isSuccessful();
return true;
}
$which = new Process(['which', $cmd]);
$which->run();
return $which->isSuccessful();
} }
private function cleanup(string $dir): void private function cleanup(string $dir): void
@ -704,13 +693,17 @@ YAML;
return; return;
} }
$entries = glob($dir.DIRECTORY_SEPARATOR.'*'); $iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST,
);
if ($entries !== false) { /** @var \SplFileInfo $entry */
foreach ($entries as $entry) { foreach ($iterator as $entry) {
if (is_file($entry)) { if ($entry->isDir()) {
@unlink($entry); @rmdir($entry->getPathname());
} } else {
@unlink($entry->getPathname());
} }
} }

View File

@ -104,22 +104,8 @@ final class CoverageCollector
return null; return null;
} }
$reflection = new ReflectionClass($className); assert(property_exists($className, '__filename') && is_string($className::$__filename));
if ($reflection->hasProperty('__filename')) { return $className::$__filename;
$property = $reflection->getProperty('__filename');
if ($property->isStatic()) {
$value = $property->getValue();
if (is_string($value)) {
return $value;
}
}
}
$file = $reflection->getFileName();
return is_string($file) ? $file : null;
} }
} }

View File

@ -296,23 +296,9 @@ final class Recorder
return null; return null;
} }
$reflection = new ReflectionClass($className); assert(property_exists($className, '__filename') && is_string($className::$__filename));
if ($reflection->hasProperty('__filename')) { return $className::$__filename;
$property = $reflection->getProperty('__filename');
if ($property->isStatic()) {
$value = $property->getValue();
if (is_string($value)) {
return $value;
}
}
}
$file = $reflection->getFileName();
return is_string($file) ? $file : null;
} }
/** /**