glastree_on_gitea
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user