glastree_on_gitea

This commit is contained in:
2026-05-26 08:14:29 +02:00
commit 0bed099d05
9556 changed files with 1186307 additions and 0 deletions
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Concerns;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Test\Finished;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class ProfileCollector
{
private static bool $executionStarted = false;
private static ?HRTime $startTime = null;
private static float $preparedAt = 0.0;
/** @var list<array{test: string, file: string, duration_ms: int}> */
private static array $entries = [];
public static function executionStarted(): void
{
self::$executionStarted = true;
}
public static function hasExecutionStarted(): bool
{
return self::$executionStarted;
}
public static function startTimer(HRTime $time): void
{
self::$startTime = $time;
}
public static function startTimerFromNanoseconds(float $nanoseconds): void
{
$seconds = (int) ($nanoseconds / 1_000_000_000);
$nanos = (int) ($nanoseconds - ($seconds * 1_000_000_000));
self::$startTime = HRTime::fromSecondsAndNanoseconds($seconds, $nanos);
}
public static function durationMs(): int
{
if (! self::$startTime instanceof HRTime) {
return 0;
}
$startNs = (self::$startTime->seconds() * 1_000_000_000) + self::$startTime->nanoseconds();
return (int) round((hrtime(true) - $startNs) / 1_000_000);
}
public static function prepared(): void
{
self::$preparedAt = hrtime(true);
}
public static function finished(Finished $event): void
{
$test = $event->test();
$file = $test->file();
$doubleColonPos = strpos($file, '::');
if ($doubleColonPos !== false) {
$file = substr($file, 0, $doubleColonPos);
}
self::$entries[] = [
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
'file' => $file,
'duration_ms' => self::$preparedAt > 0
? (int) round((hrtime(true) - self::$preparedAt) / 1_000_000)
: (int) round($event->telemetryInfo()->durationSincePrevious()->asFloat() * 1000),
];
self::$preparedAt = 0.0;
}
/**
* @return list<array{test: string, file: string, duration_ms: int}>
*/
public static function entries(): array
{
return self::$entries;
}
}
@@ -0,0 +1,288 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Concerns;
use Pest\Plugins\Parallel\Paratest\WrapperRunner;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PreparedSubscriber;
use PHPUnit\Event\TestRunner\ExecutionStarted;
use PHPUnit\Event\TestRunner\ExecutionStartedSubscriber;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\Issues\Issue;
use PHPUnit\TestRunner\TestResult\TestResult;
/**
* @internal
*
* @codeCoverageIgnore
*/
trait TestResultParsable
{
public ?TestResult $testResult = null;
protected function startTimer(): void
{
try {
EventFacade::instance()->registerSubscriber(
new class implements ExecutionStartedSubscriber
{
public function notify(ExecutionStarted $event): void
{
ProfileCollector::executionStarted();
ProfileCollector::startTimer($event->telemetryInfo()->time());
}
},
);
} catch (\Throwable) {
//
}
}
protected function registerProfileSubscriber(): void
{
/** @var list<string> $argv */
$argv = $_SERVER['argv'] ?? [];
if (! in_array('--profile', $argv, true)) {
return;
}
EventFacade::instance()->registerSubscribers(
new class implements PreparedSubscriber
{
public function notify(Prepared $event): void
{
ProfileCollector::prepared();
}
},
new class implements FinishedSubscriber
{
public function notify(Finished $event): void
{
ProfileCollector::finished($event);
}
},
);
}
/**
* @return array<string, mixed>|null
*/
public function parse(): ?array
{
$testResult = $this->resolveTestResult();
if (! $testResult instanceof TestResult) {
return null;
}
if ($testResult->numberOfTestsRun() > 0 || ProfileCollector::hasExecutionStarted()) {
return $this->parseTestResult($testResult);
}
return null;
}
private function resolveTestResult(): ?TestResult
{
if ($this->testResult instanceof TestResult) {
return $this->testResult;
}
if (class_exists(WrapperRunner::class, false)
&& WrapperRunner::$result instanceof TestResult) {
return WrapperRunner::$result;
}
try {
return TestResultFacade::result();
} catch (\Throwable) {
return null;
}
}
/**
* @return array<string, mixed>
*/
private function parseTestResult(TestResult $testResult): array
{
$failedCount = $testResult->numberOfTestFailedEvents();
$erroredCount = $testResult->numberOfTestErroredEvents();
$skipped = $testResult->numberOfTestSkippedEvents() + $testResult->numberOfTestSkippedByTestSuiteSkippedEvents();
$incomplete = $testResult->numberOfTestMarkedIncompleteEvents();
$tests = $testResult->numberOfTestsRun();
$assertions = $testResult->numberOfAssertions();
$deprecations = $testResult->numberOfPhpOrUserDeprecations();
$warnings = $testResult->numberOfWarnings();
$notices = $testResult->numberOfNotices();
$risky = $testResult->numberOfTestsWithTestConsideredRiskyEvents();
$ignoredByBaseline = $testResult->numberOfIssuesIgnoredByBaseline();
$durationMs = ProfileCollector::durationMs();
/** @var list<array{test: string, file: string, line: int, message: string}> $failureDetails */
$failureDetails = [];
foreach ($testResult->testFailedEvents() as $event) {
$test = $event->test();
$throwable = $event->throwable();
$message = trim($throwable->description());
$file = $test->file();
$line = $test instanceof TestMethod ? $test->line() : 0;
[$file, $line] = $this->resolveTestLocation($file, $line, $throwable);
$failureDetails[] = [
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
'file' => $file,
'line' => $line,
'message' => $message,
];
}
/** @var list<array{test: string, file: string, line: int, message: string}> $errorDetails */
$errorDetails = [];
foreach ($testResult->testErroredEvents() as $event) {
if ($event instanceof Errored) {
$test = $event->test();
$throwable = $event->throwable();
$message = trim($throwable->message());
$file = $test->file();
$line = $test instanceof TestMethod ? $test->line() : 0;
[$file, $line] = $this->resolveTestLocation($file, $line, $throwable);
$errorDetails[] = [
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
'file' => $file,
'line' => $line,
'message' => $message,
];
}
}
/** @var array<string, mixed> $result */
$result = [
'result' => $testResult->wasSuccessful() ? 'passed' : 'failed',
'tests' => $tests,
'passed' => $tests - $failedCount - $erroredCount - $skipped,
'assertions' => $assertions,
'duration_ms' => $durationMs,
];
if ($failedCount > 0) {
$result['failed'] = $failedCount;
$result['failures'] = $failureDetails;
}
if ($erroredCount > 0) {
$result['errors'] = $erroredCount;
$result['error_details'] = $errorDetails;
}
if ($skipped > 0) {
$result['skipped'] = $skipped;
}
if ($incomplete > 0) {
$result['incomplete'] = $incomplete;
}
if ($deprecations > 0) {
$result['deprecations'] = $deprecations;
$result['deprecation_details'] = $this->extractIssueDetails(
[...$testResult->deprecations(), ...$testResult->phpDeprecations()],
);
}
if ($warnings > 0) {
$result['warnings'] = $warnings;
$result['warning_details'] = $this->extractIssueDetails(
[...$testResult->warnings(), ...$testResult->phpWarnings()],
);
}
if ($notices > 0) {
$result['notices'] = $notices;
$result['notice_details'] = $this->extractIssueDetails(
[...$testResult->notices(), ...$testResult->phpNotices()],
);
}
$phpErrors = $testResult->errors();
if ($phpErrors !== []) {
$result['php_errors'] = count($phpErrors);
$result['php_error_details'] = $this->extractIssueDetails($phpErrors);
}
if ($risky > 0) {
$result['risky'] = $risky;
}
if ($ignoredByBaseline > 0) {
$result['ignored_by_baseline'] = $ignoredByBaseline;
}
$profileEntries = ProfileCollector::entries();
if ($profileEntries !== []) {
usort($profileEntries, fn (array $a, array $b): int => $b['duration_ms'] <=> $a['duration_ms']);
$result['profile'] = array_slice($profileEntries, 0, 10);
}
return $result;
}
/**
* @param list<Issue> $issues
* @return list<array{file: string, line: int, message: string}>
*/
private function extractIssueDetails(array $issues): array
{
$details = [];
foreach ($issues as $issue) {
$details[] = [
'file' => $issue->file(),
'line' => $issue->line(),
'message' => $issue->description(),
];
}
return $details;
}
/**
* @return array{string, int}
*/
private function resolveTestLocation(string $file, int $line, Throwable $throwable): array
{
$isReal = $line > 0 && ! str_contains($file, "eval()'d code");
if ($isReal) {
return [$file, $line];
}
$text = $throwable->description()."\n".$throwable->stackTrace();
if (preg_match('/\bat\s+(.+\.php):(\d+)/', $text, $matches) === 1) {
return [$matches[1], (int) $matches[2]];
}
if (preg_match('#([\w/\\\\._-]+\.php):(\d+)#', $throwable->stackTrace(), $matches) === 1) {
return [$matches[1], (int) $matches[2]];
}
return [$file, $line];
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Paratest;
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
use Laravel\Pao\Drivers\Starter as BaseStarter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
use TestResultParsable;
public function name(): string
{
return 'paratest';
}
public function start(): void
{
$this->registerNullFilter();
$this->startTimer();
$this->silenceStdout();
/** @var list<string> $serverArgv */
$serverArgv = $_SERVER['argv'];
$argv = $serverArgv;
$argv[] = '--runner';
$argv[] = WrapperRunner::class;
$_SERVER['argv'] = $argv;
}
}
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Paratest;
use Laravel\Pao\Drivers\Concerns\ProfileCollector;
use Laravel\Pao\Execution;
use ParaTest\Options;
use ParaTest\RunnerInterface;
use ParaTest\WrapperRunner\ResultPrinter;
use ParaTest\WrapperRunner\SuiteLoader;
use ParaTest\WrapperRunner\WrapperRunner as ParatestWrapperRunner;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\Util\ExcludeList;
use ReflectionObject;
use SplFileInfo;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*
* @codeCoverageIgnore
*/
final readonly class WrapperRunner implements RunnerInterface
{
private ParatestWrapperRunner $runner;
public function __construct(
Options $options,
) {
$this->runner = new ParatestWrapperRunner($options, new NullOutput);
}
public function run(): int
{
$runner = $this->runner;
$r = new ReflectionObject($runner);
/** @var non-empty-string $directory */
$directory = dirname((string) $r->getFileName(), 2);
ExcludeList::addDirectory($directory);
/** @var Options $options */
$options = $r->getProperty('options')->getValue($runner);
/** @var OutputInterface $output */
$output = $r->getProperty('output')->getValue($runner);
/** @var CodeCoverageFilterRegistry $filterRegistry */
$filterRegistry = $r->getProperty('codeCoverageFilterRegistry')->getValue($runner);
$suiteLoader = new SuiteLoader($options, $output, $filterRegistry);
$result = TestResultFacade::result();
$r->getProperty('pending')->setValue($runner, $suiteLoader->tests);
/** @var ResultPrinter $printer */
$printer = $r->getProperty('printer')->getValue($runner);
$printer->setTestCount($suiteLoader->testCount);
$printer->start();
$startTime = hrtime(true);
$r->getMethod('startWorkers')->invoke($runner);
$r->getMethod('assignAllPendingTests')->invoke($runner);
$r->getMethod('waitForAllToFinish')->invoke($runner);
ProfileCollector::startTimerFromNanoseconds($startTime);
/** @var list<SplFileInfo> $testResultFiles */
$testResultFiles = $r->getProperty('testResultFiles')->getValue($runner);
$mergedResult = $this->mergeTestResults($result, $testResultFiles);
if (Execution::running()) {
$driver = Execution::current()->driver;
if ($driver instanceof Starter) {
$driver->testResult = $mergedResult;
}
}
/** @var int $exitCode */
$exitCode = $r->getMethod('complete')->invoke($runner, $result);
return $exitCode;
}
/**
* @param list<SplFileInfo> $testResultFiles
*/
private function mergeTestResults(TestResult $sum, array $testResultFiles): TestResult
{
foreach ($testResultFiles as $testResultFile) {
if (! $testResultFile->isFile()) {
continue;
}
$contents = file_get_contents($testResultFile->getPathname());
if ($contents === false) {
continue;
}
$testResult = unserialize($contents);
if (! $testResult instanceof TestResult) {
continue;
}
$sum = new TestResult(
(int) $sum->hasTests() + (int) $testResult->hasTests(),
$sum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
$sum->numberOfAssertions() + $testResult->numberOfAssertions(),
[...$sum->testErroredEvents(), ...$testResult->testErroredEvents()],
[...$sum->testFailedEvents(), ...$testResult->testFailedEvents()],
array_merge_recursive($sum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()), // @phpstan-ignore argument.type
[...$sum->testSuiteSkippedEvents(), ...$testResult->testSuiteSkippedEvents()],
[...$sum->testSkippedEvents(), ...$testResult->testSkippedEvents()],
[...$sum->testMarkedIncompleteEvents(), ...$testResult->testMarkedIncompleteEvents()],
array_merge_recursive($sum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), // @phpstan-ignore argument.type
array_merge_recursive($sum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), // @phpstan-ignore argument.type
array_merge_recursive($sum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()), // @phpstan-ignore argument.type
array_merge_recursive($sum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), // @phpstan-ignore argument.type
[...$sum->testRunnerTriggeredDeprecationEvents(), ...$testResult->testRunnerTriggeredDeprecationEvents()],
[...$sum->testRunnerTriggeredNoticeEvents(), ...$testResult->testRunnerTriggeredNoticeEvents()],
[...$sum->testRunnerTriggeredWarningEvents(), ...$testResult->testRunnerTriggeredWarningEvents()],
[...$sum->errors(), ...$testResult->errors()],
[...$sum->deprecations(), ...$testResult->deprecations()],
[...$sum->notices(), ...$testResult->notices()],
[...$sum->warnings(), ...$testResult->warnings()],
[...$sum->phpDeprecations(), ...$testResult->phpDeprecations()],
[...$sum->phpNotices(), ...$testResult->phpNotices()],
[...$sum->phpWarnings(), ...$testResult->phpWarnings()],
$sum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
);
}
return $sum;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Pest;
use Laravel\Pao\Execution;
use Pest\Contracts\Plugins\HandlesArguments;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Plugin implements HandlesArguments
{
public function __construct()
{
//
}
/**
* @param array<int, string> $arguments
* @return array<int, string>
*/
public function handleArguments(array $arguments): array
{
if (! Execution::running()) {
return $arguments;
}
$arguments[] = '--no-output';
$arguments[] = '--no-progress';
return $arguments;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Pest;
use Laravel\Pao\Drivers\Concerns\ProfileCollector;
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
use Laravel\Pao\Drivers\Starter as BaseStarter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
use TestResultParsable;
public function name(): string
{
return 'pest';
}
public function start(): void
{
$this->registerNullFilter();
$this->startTimer();
$this->saveStdout();
$this->silenceStdout();
/** @var list<string> $argv */
$argv = $_SERVER['argv'] ?? [];
if (in_array('--parallel', $argv, true)) {
ProfileCollector::startTimerFromNanoseconds(hrtime(true));
} else {
$this->registerProfileSubscriber();
}
}
}
+208
View File
@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Phpstan;
use Laravel\Pao\Drivers\Starter as BaseStarter;
use Laravel\Pao\UserFilters\CaptureFilter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
public function name(): string
{
return 'phpstan';
}
public function start(): void
{
$this->registerNullFilter();
$this->silenceStderr();
/** @var array<int, string> $argv */
$argv = $_SERVER['argv'];
$argv = $this->ensureErrorFormatJson($argv);
$argv = $this->ensureNoProgress($argv);
$_SERVER['argv'] = $argv;
$this->silenceStdout();
}
/**
* @return array<string, mixed>|null
*/
public function parse(): ?array
{
$captured = trim(CaptureFilter::output());
CaptureFilter::reset();
if ($captured === '') {
return null;
}
$start = strpos($captured, '{');
if ($start !== false && $start > 0) {
$captured = substr($captured, $start);
}
/** @var array<string, mixed>|null $data */
$data = json_decode($captured, associative: true);
if (! is_array($data) || ! isset($data['totals'])) {
return null;
}
/** @var array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>> $errorDetails */
$errorDetails = [];
$totalFileErrors = 0;
/** @var array<string, array{errors: int, messages: list<array{message: string, line: int, identifier?: string, ignorable?: bool, tip?: string}>}> $files */
$files = is_array($data['files'] ?? null) ? $data['files'] : [];
foreach ($files as $file => $fileData) {
foreach ($fileData['messages'] as $message) {
$totalFileErrors++;
$detail = [
'line' => $message['line'],
'message' => $message['message'],
'identifier' => $message['identifier'] ?? 'unknown',
];
if (isset($message['ignorable']) && $message['ignorable'] === false) {
$detail['ignorable'] = false;
}
if (isset($message['tip']) && $message['tip'] !== '') {
$detail['tip'] = $message['tip'];
}
$errorDetails[$file][] = $detail;
}
}
/** @var list<string> $errors */
$errors = is_array($data['errors'] ?? null) ? $data['errors'] : [];
/** @var list<string> $generalErrors */
$generalErrors = array_values(array_filter($errors, static fn (string $error): bool => $error !== ''));
$totalErrors = $totalFileErrors + count($generalErrors);
/** @var array<string, mixed> $result */
$result = [
'result' => $totalErrors > 0 ? 'failed' : 'passed',
'errors' => $totalErrors,
];
if ($errorDetails !== []) {
$verbose = $this->isVerbose();
$limit = 30;
if (! $verbose && $totalFileErrors > $limit) {
$result['error_details'] = $this->truncateGrouped($errorDetails, $limit);
$result['truncated'] = true;
$result['hint'] = 'Pass -v to see all errors.';
} else {
$result['error_details'] = $errorDetails;
}
}
if ($generalErrors !== []) {
$result['general_errors'] = $generalErrors;
}
return $result;
}
/**
* @param array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>> $grouped
* @return array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>>
*/
private function truncateGrouped(array $grouped, int $limit): array
{
$result = [];
$count = 0;
foreach ($grouped as $file => $errors) {
foreach ($errors as $error) {
if ($count >= $limit) {
break 2;
}
$result[$file][] = $error;
$count++;
}
}
return $result;
}
private function isVerbose(): bool
{
/** @var array<int, string> $argv */
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $arg) {
if (in_array($arg, ['-v', '-vv', '-vvv', '--verbose'], true)) {
return true;
}
}
return false;
}
/**
* @param array<int, string> $argv
* @return array<int, string>
*/
private function ensureErrorFormatJson(array $argv): array
{
$filtered = [];
$skipNext = false;
foreach ($argv as $arg) {
if ($skipNext) {
$skipNext = false;
continue;
}
if (str_starts_with($arg, '--error-format=')) {
continue;
}
if ($arg === '--error-format') {
$skipNext = true;
continue;
}
$filtered[] = $arg;
}
$filtered[] = '--error-format=json';
return $filtered;
}
/**
* @param array<int, string> $argv
* @return array<int, string>
*/
private function ensureNoProgress(array $argv): array
{
if (! in_array('--no-progress', $argv, true)) {
$argv[] = '--no-progress';
}
return $argv;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Phpunit;
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
use Laravel\Pao\Drivers\Starter as BaseStarter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
use TestResultParsable;
public function name(): string
{
return 'phpunit';
}
public function start(): void
{
$this->registerNullFilter();
$this->startTimer();
$this->registerProfileSubscriber();
/** @var list<string> $serverArgv */
$serverArgv = $_SERVER['argv'];
$argv = $serverArgv;
if (! in_array('--no-output', $argv, true)) {
$argv[] = '--no-output';
}
$_SERVER['argv'] = $argv;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers;
use Laravel\Pao\Contracts\Driver;
use Laravel\Pao\Execution;
use Laravel\Pao\UserFilters\CaptureFilter;
use Laravel\Pao\UserFilters\NullFilter;
/**
* @internal
*
* @codeCoverageIgnore
*/
abstract class Starter implements Driver
{
protected function registerNullFilter(): void
{
if (! in_array('agent_output_null', stream_get_filters(), true)) {
stream_filter_register('agent_output_null', NullFilter::class);
}
}
protected function silenceStdout(): void
{
if (! in_array('agent_output_capture', stream_get_filters(), true)) {
stream_filter_register('agent_output_capture', CaptureFilter::class);
}
CaptureFilter::reset();
$execution = Execution::current();
$execution->filter = stream_filter_append(STDOUT, 'agent_output_capture', STREAM_FILTER_WRITE) ?: null;
}
protected function silenceStderr(): void
{
stream_filter_append(STDERR, 'agent_output_null', STREAM_FILTER_WRITE);
}
protected function saveStdout(): void
{
$execution = Execution::current();
$execution->stdout = fopen('php://stdout', 'w') ?: STDOUT;
}
}