refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

71
vendor/laravel/pao/src/Autoload.php vendored Normal file
View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/** @codeCoverageIgnoreStart */
namespace Laravel\Pao;
use Laravel\AgentDetector\AgentDetector;
/** @var array<int, string>|null $argv */
$argv = $_SERVER['argv'] ?? null;
if (! is_array($argv) || $argv === []) {
return;
}
if (isset($_SERVER['PAO_DISABLE'])) {
return;
}
$agent = AgentDetector::detect();
if (! $agent->isAgent) {
return;
}
if (array_intersect($argv, ['--version', '--help', '-h', 'worker'])) {
return;
}
unset($_SERVER['COLLISION_PRINTER']);
$_SERVER['PEST_PARALLEL_NO_OUTPUT'] = '1';
register_shutdown_function(function (): void {
if (! Execution::running()) {
return;
}
$execution = Execution::current();
$result = $execution->driver->parse() ?: [];
$captured = trim(UserFilters\CaptureFilter::output());
$execution->restoreStdout();
if ($captured !== '') {
$captured = OutputCleaner::clean($captured);
$lines = array_values(array_filter(
array_map(trim(...), explode("\n", $captured)),
fn (string $line): bool => $line !== ''
&& ! preg_match('/^[.st!]+$/', $line)
&& ! preg_match('/^(Tests:|Duration:|Parallel:|Time:|Generating code coverage)\s/', $line)
&& ! str_ends_with($line, 'by Sebastian Bergmann and contributors.'),
));
if ($lines !== []) {
$result['raw'] = $lines;
}
}
if ($result !== []) {
$result = ['tool' => $execution->driver->name()] + $result;
fwrite(STDOUT, json_encode($result, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR).PHP_EOL);
}
});
Execution::start($agent, $argv);

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Contracts;
/**
* @internal
*/
interface Driver
{
public function start(): void;
public function name(): string;
/**
* @return array<string, mixed>|null
*/
public function parse(): ?array;
}

View File

@@ -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;
}
}

View File

@@ -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];
}
}

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;
}
}

View File

@@ -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;
}
}

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;
}
}

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();
}
}
}

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;
}
}

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;
}
}

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;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Exceptions;
use RuntimeException;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class ShouldNotHappenException extends RuntimeException
{
public function __construct()
{
parent::__construct('This should not have happened. Please report this issue at [https://github.com/laravel/pao/issues/new].');
}
}

100
vendor/laravel/pao/src/Execution.php vendored Normal file
View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao;
use Laravel\AgentDetector\AgentResult;
use Laravel\Pao\Contracts\Driver;
use Laravel\Pao\Exceptions\ShouldNotHappenException;
use Laravel\Pao\UserFilters\CaptureFilter;
/**
* @internal
*
* @codeCoverageIgnore
*
* @phpstan-type TestDetail array{test: string, file: string, line: int, message: string}
* @phpstan-type ProfileEntry array{test: string, file: string, duration_ms: int}
* @phpstan-type Result array{result: 'passed'|'failed', tests: int, passed: int, duration_ms: int, failed?: int, failures?: list<TestDetail>, errors?: int, error_details?: list<TestDetail>, skipped?: int, profile?: list<ProfileEntry>, raw?: list<string>}
*/
final class Execution
{
private static ?self $instance = null;
/**
* @param resource|null $stdout
* @param resource|null $filter
*/
private function __construct(
public readonly AgentResult $agent,
public readonly Driver $driver,
public mixed $stdout = null,
public mixed $filter = null,
) {
//
}
/**
* @param array<int, string> $argv
*/
public static function start(AgentResult $agent, array $argv): void
{
if (self::running()) {
throw new ShouldNotHappenException;
}
$binary = basename($argv[0] ?? '');
$starter = match ($binary) {
'paratest' => new Drivers\Paratest\Starter,
'pest' => new Drivers\Pest\Starter,
'phpstan', 'phpstan.phar' => new Drivers\Phpstan\Starter,
'phpunit' => new Drivers\Phpunit\Starter,
default => null,
};
if ($starter instanceof Driver) {
self::$instance = new self(
$agent,
$starter,
);
$starter->start();
}
}
public static function running(): bool
{
return self::$instance instanceof Execution;
}
public static function current(): self
{
return self::$instance ?? throw new ShouldNotHappenException;
}
public function restoreStdout(): void
{
if (is_resource($this->filter)) {
stream_filter_remove($this->filter);
$this->filter = null;
}
}
public function flushStdout(): void
{
if (! is_resource($this->filter)) {
return;
}
$captured = CaptureFilter::output();
$this->restoreStdout();
if ($captured !== '') {
fwrite(STDOUT, $captured);
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Laravel;
use Illuminate\Console\OutputStyle;
use Laravel\Pao\OutputCleaner;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class PaoOutputStyle extends OutputStyle
{
private static ?OutputFormatter $formatter = null;
public function __construct(InputInterface $input, OutputInterface $output)
{
$output->setDecorated(false);
parent::__construct($input, $output);
}
/**
* @param string|iterable<string> $messages
*/
#[\Override]
public function write(string|iterable $messages, bool $newline = false, int $options = 0): void
{
parent::write($this->clean($messages), $newline, $options);
}
/**
* @param string|iterable<string> $messages
*/
#[\Override]
public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void
{
parent::writeln($this->clean($messages), $type);
}
/**
* @param string|iterable<string> $messages
* @return string|list<string>
*/
private function clean(string|iterable $messages): string|array
{
$formatter = self::$formatter ??= new OutputFormatter(false);
$strip = fn (string $m): string => OutputCleaner::clean((string) $formatter->format($m));
if (is_string($messages)) {
return $strip($messages);
}
return array_values(array_map($strip, [...$messages]));
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Laravel;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Laravel\AgentDetector\AgentDetector;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class ServiceProvider extends LaravelServiceProvider
{
public function boot(): void
{
if (isset($_SERVER['PAO_DISABLE'])) {
return;
}
if (! $this->app->runningInConsole()) {
return;
}
if ($this->app->runningUnitTests()) {
return;
}
if (! AgentDetector::detect()->isAgent) {
return;
}
$this->app->bind(OutputStyle::class, PaoOutputStyle::class);
/** @var Dispatcher $events */
$events = $this->app->make(Dispatcher::class);
$events->listen(CommandStarting::class, function (CommandStarting $event): void {
$event->output->setDecorated(false);
});
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class OutputCleaner
{
public static function clean(string $output): string
{
$output = (string) preg_replace('/\e\[[0-9;]*[A-Za-z]/', '', $output);
$output = (string) preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $output);
$output = (string) preg_replace('/\x{FFFD}/u', '', $output);
$output = (string) preg_replace('/[─━│┌┐└┘├┤┬┴┼▓░▒═║╔╗╚╝╠╣╦╩╬➜▶►⚠✖✔●◆■▪→←↑↓▕⨯✕]+/u', '', $output);
$output = (string) preg_replace('/\.{3,}/', '..', $output);
$output = (string) preg_replace('/[ \t]+/', ' ', $output);
return (string) preg_replace('/\n\s*\n/', "\n", $output);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\UserFilters;
use php_user_filter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class CaptureFilter extends php_user_filter
{
private static string $captured = '';
/**
* @param resource $in
* @param resource $out
* @param int $consumed
*/
public function filter($in, $out, &$consumed, bool $closing): int // @pest-ignore-type
{
while ($bucket = stream_bucket_make_writeable($in)) {
/** @var int $datalen */
$datalen = $bucket->datalen;
$consumed += $datalen;
/** @var string $data */
$data = $bucket->data;
self::$captured .= $data;
$bucket->data = '';
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
public static function output(): string
{
return self::$captured;
}
public static function reset(): void
{
self::$captured = '';
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\UserFilters;
use php_user_filter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class NullFilter extends php_user_filter
{
public function filter($in, $out, &$consumed, bool $closing): int // @pest-ignore-type
{
while ($bucket = stream_bucket_make_writeable($in)) {
/** @var int $datalen */
$datalen = $bucket->datalen;
$consumed += $datalen;
$bucket->data = '';
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}