diff --git a/bin/worker.php b/bin/worker.php index f8afa19c..6b2660d4 100644 --- a/bin/worker.php +++ b/bin/worker.php @@ -31,6 +31,7 @@ $bootPest = (static function (): void { $getopt = getopt('', [ 'status-file:', 'progress-file:', + 'unexpected-output-file:', 'testresult-file:', 'teamcity-file:', 'testdox-file:', @@ -58,6 +59,7 @@ $bootPest = (static function (): void { assert(is_resource($statusFile)); assert(isset($getopt['progress-file']) && is_string($getopt['progress-file'])); + assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file'])); assert(isset($getopt['testresult-file']) && is_string($getopt['testresult-file'])); assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file'])); assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file'])); @@ -73,6 +75,7 @@ $bootPest = (static function (): void { $application = new ApplicationForWrapperWorker( $phpunitArgv, $getopt['progress-file'], + $getopt['unexpected-output-file'], $getopt['testresult-file'], $getopt['teamcity-file'] ?? null, $getopt['testdox-file'] ?? null, @@ -88,10 +91,10 @@ $bootPest = (static function (): void { $testPath = fgets(STDIN); if ($testPath === false || $testPath === WrapperWorker::COMMAND_EXIT) { $application->end(); - exit; } + // It must be a 1 byte string to ensure filesize() is equal to the number of tests executed $exitCode = $application->runTest(realpath(trim($testPath))); fwrite($statusFile, (string) $exitCode); diff --git a/composer.json b/composer.json index bc742354..32f9022c 100644 --- a/composer.json +++ b/composer.json @@ -18,16 +18,16 @@ ], "require": { "php": "^8.1.0", - "brianium/paratest": "^7.1.4", + "brianium/paratest": "^7.2.0", "nunomaduro/collision": "^7.5.2", "nunomaduro/termwind": "^1.15.1", "pestphp/pest-plugin": "^2.0.1", "pestphp/pest-plugin-arch": "^2.2.0", - "phpunit/phpunit": "^10.2.1" + "phpunit/phpunit": "^10.2.2" }, "conflict": { "webmozart/assert": "<1.11.0", - "phpunit/phpunit": ">10.2.1" + "phpunit/phpunit": ">10.2.2" }, "autoload": { "psr-4": { diff --git a/src/Plugins/Parallel/Paratest/ResultPrinter.php b/src/Plugins/Parallel/Paratest/ResultPrinter.php index 62297e1c..4602ee31 100644 --- a/src/Plugins/Parallel/Paratest/ResultPrinter.php +++ b/src/Plugins/Parallel/Paratest/ResultPrinter.php @@ -12,20 +12,20 @@ use function fread; use function fseek; use function ftell; use function fwrite; -use NunoMaduro\Collision\Adapters\Phpunit\State; use ParaTest\Options; use Pest\Plugins\Parallel\Support\CompactPrinter; use Pest\Support\StateGenerator; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\Output\Printer; -use function preg_replace; use SebastianBergmann\Timer\Duration; use SplFileInfo; use function strlen; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Output\OutputInterface; -/** @internal */ +/** + * @internal + */ final class ResultPrinter { /** @@ -51,7 +51,7 @@ final class ResultPrinter /** @var resource|null */ private $teamcityLogFileHandle; - /** @var array */ + /** @var array */ private array $tailPositions; public function __construct( @@ -74,6 +74,7 @@ final class ResultPrinter if (str_starts_with($buffer, 'done [')) { return; } + $this->output->write(OutputFormatter::escape($buffer)); } @@ -93,9 +94,12 @@ final class ResultPrinter $this->teamcityLogFileHandle = $teamcityLogFileHandle; } - /** @param array $teamcityFiles */ - public function printFeedback(SplFileInfo $progressFile, array $teamcityFiles): void - { + /** @param list $teamcityFiles */ + public function printFeedback( + SplFileInfo $progressFile, + SplFileInfo $outputFile, + array $teamcityFiles + ): void { if ($this->options->needsTeamcity) { $teamcityProgress = $this->tailMultiple($teamcityFiles); @@ -115,6 +119,16 @@ final class ResultPrinter return; } + $unexpectedOutput = $this->tail($outputFile); + if ($unexpectedOutput !== '') { + // if unexpected output only contains the letter "T", like "T", or "TT", or "TTT", etc, then ignore it. + if (preg_match('/^T+$/', $unexpectedOutput) !== false) { + return; + } + + $this->output->write($unexpectedOutput); + } + $feedbackItems = $this->tail($progressFile); if ($feedbackItems === '') { return; @@ -129,8 +143,8 @@ final class ResultPrinter } /** - * @param array $teamcityFiles - * @param array $testdoxFiles + * @param list $teamcityFiles + * @param list $testdoxFiles */ public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles, Duration $duration): void { @@ -183,7 +197,7 @@ final class ResultPrinter $this->compactPrinter->descriptionItem($item); } - /** @param array $files */ + /** @param list $files */ private function tailMultiple(array $files): string { $content = ''; @@ -201,6 +215,7 @@ final class ResultPrinter private function tail(SplFileInfo $file): string { $path = $file->getPathname(); + assert($path !== ''); $handle = fopen($path, 'r'); assert($handle !== false); $fseek = fseek($handle, $this->tailPositions[$path] ?? 0); diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index bec0dd5a..c45d6a5a 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -48,36 +48,39 @@ final class WrapperRunner implements RunnerInterface private readonly Timer $timer; - /** @var array */ + /** @var list */ private array $pending = []; - private int $exitCode = -1; + private int $exitcode = -1; - /** @var array */ + /** @var array */ private array $workers = []; /** @var array */ private array $batches = []; - /** @var array */ + /** @var list */ + private array $unexpectedOutputFiles = []; + + /** @var list */ private array $testresultFiles = []; - /** @var array */ + /** @var list */ private array $coverageFiles = []; - /** @var array */ + /** @var list */ private array $junitFiles = []; - /** @var array */ + /** @var list */ private array $teamcityFiles = []; - /** @var array */ + /** @var list */ private array $testdoxFiles = []; - /** @var array */ + /** @var non-empty-string[] */ private readonly array $parameters; - private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry; + private CodeCoverageFilterRegistry $codeCoverageFilterRegistry; public function __construct( private readonly Options $options, @@ -86,11 +89,10 @@ final class WrapperRunner implements RunnerInterface $this->printer = new ResultPrinter($output, $options); $this->timer = new Timer(); - $worker = realpath( + $wrapper = realpath( dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'worker.php', ); - - assert($worker !== false); + assert($wrapper !== false); $phpFinder = new PhpExecutableFinder(); $phpBin = $phpFinder->find(false); assert($phpBin !== false); @@ -101,7 +103,7 @@ final class WrapperRunner implements RunnerInterface $parameters = array_merge($parameters, $options->passthruPhp); } - $parameters[] = $worker; + $parameters[] = $wrapper; $this->parameters = $parameters; $this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry(); @@ -110,14 +112,15 @@ final class WrapperRunner implements RunnerInterface public function run(): int { $directory = dirname(__DIR__); - assert(strlen($directory) > 0); + assert($directory !== ''); ExcludeList::addDirectory($directory); - TestResultFacade::init(); - EventFacade::instance()->seal(); - - $suiteLoader = new SuiteLoader($this->options, $this->output, $this->codeCoverageFilterRegistry); + $suiteLoader = new SuiteLoader( + $this->options, + $this->output, + $this->codeCoverageFilterRegistry, + ); $this->pending = $this->getTestFiles($suiteLoader); $result = TestResultFacade::result(); @@ -142,7 +145,7 @@ final class WrapperRunner implements RunnerInterface { $batchSize = $this->options->maxBatchSize; - while ($this->pending !== [] && $this->workers !== []) { + while (count($this->pending) > 0 && count($this->workers) > 0) { foreach ($this->workers as $token => $worker) { if (! $worker->isRunning()) { throw $worker->getWorkerCrashedException(); @@ -160,13 +163,11 @@ final class WrapperRunner implements RunnerInterface } if ( - $this->exitCode > 0 + $this->exitcode > 0 && $this->options->configuration->stopOnFailure() ) { $this->pending = []; } elseif (($pending = array_shift($this->pending)) !== null) { - $this->debug(sprintf('Assigning %s to worker %d', $pending, $token)); - $worker->assign($pending); $this->batches[$token]++; } @@ -178,9 +179,10 @@ final class WrapperRunner implements RunnerInterface private function flushWorker(WrapperWorker $worker): void { - $this->exitCode = max($this->exitCode, $worker->getExitCode()); + $this->exitcode = max($this->exitcode, $worker->getExitCode()); $this->printer->printFeedback( $worker->progressFile, + $worker->unexpectedOutputFile, $this->teamcityFiles, ); $worker->reset(); @@ -189,10 +191,10 @@ final class WrapperRunner implements RunnerInterface private function waitForAllToFinish(): void { $stopped = []; - while ($this->workers !== []) { + while (count($this->workers) > 0) { foreach ($this->workers as $index => $worker) { if ($worker->isRunning()) { - if (! array_key_exists($index, $stopped) && $worker->isFree()) { + if (! isset($stopped[$index]) && $worker->isFree()) { $worker->stop(); $stopped[$index] = true; } @@ -212,22 +214,19 @@ final class WrapperRunner implements RunnerInterface } } + /** @param positive-int $token */ private function startWorker(int $token): WrapperWorker { - /** @var array $parameters */ - $parameters = $this->parameters; - $worker = new WrapperWorker( $this->output, $this->options, - $parameters, + $this->parameters, $token, ); - $worker->start(); - $this->batches[$token] = 0; + $this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile; $this->testresultFiles[] = $worker->testresultFile; if (isset($worker->junitFile)) { @@ -330,15 +329,16 @@ final class WrapperRunner implements RunnerInterface $this->generateCodeCoverageReports(); $this->generateLogs(); - $exitCode = Result::exitCode($this->options->configuration, $testResultSum); + $exitcode = Result::exitCode($this->options->configuration, $testResultSum); + $this->clearFiles($this->unexpectedOutputFiles); $this->clearFiles($this->testresultFiles); $this->clearFiles($this->coverageFiles); $this->clearFiles($this->junitFiles); $this->clearFiles($this->teamcityFiles); $this->clearFiles($this->testdoxFiles); - return $exitCode; + return $exitcode; } private function generateCodeCoverageReports(): void @@ -348,10 +348,11 @@ final class WrapperRunner implements RunnerInterface } $coverageManager = new CodeCoverage(); - - // @phpstan-ignore-next-line - is_bool(true) && $coverageManager->init($this->options->configuration, $this->codeCoverageFilterRegistry, true); - + $coverageManager->init( + $this->options->configuration, + $this->codeCoverageFilterRegistry, + false, + ); $coverageMerger = new CoverageMerger($coverageManager->codeCoverage()); foreach ($this->coverageFiles as $coverageFile) { $coverageMerger->addCoverageFromFile($coverageFile); @@ -376,7 +377,7 @@ final class WrapperRunner implements RunnerInterface ); } - /** @param array $files */ + /** @param list $files */ private function clearFiles(array $files): void { foreach ($files as $file) { @@ -391,31 +392,19 @@ final class WrapperRunner implements RunnerInterface /** * Returns the test files to be executed. * - * @return array + * @return array */ private function getTestFiles(SuiteLoader $suiteLoader): array { - $this->debug(sprintf('Found %d test file%s', count($suiteLoader->files), count($suiteLoader->files) === 1 ? '' : 's')); - - /** @var array $files */ - $files = $suiteLoader->files; - - return [ + /** @var array $files */ + $files = [ ...array_values(array_filter( - $files, + $suiteLoader->tests, fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code") )), ...TestSuite::getInstance()->tests->getFilenames(), ]; - } - /** - * Prints a debug message. - */ - private function debug(string $message): void - { - if ($this->options->verbose) { - $this->output->writeln(" {$message} "); - } + return $files; // @phpstan-ignore-line } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 401a436b..4a22473a 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -932,6 +932,9 @@ ✓ it ensures the given closures reports the correct class name ✓ it ensures the given closures reports the correct class name and suggests the [uses()] function + PASS Tests\Unit\Support\HigherOrderMessage + ✓ undefined method exceptions + PASS Tests\Unit\Support\Reflection ✓ it gets file name from closure ✓ it gets property values @@ -1035,4 +1038,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 2 deprecated, 3 warnings, 4 incomplete, 1 notice, 8 todos, 17 skipped, 715 passed (1729 assertions) \ No newline at end of file + Tests: 2 deprecated, 3 warnings, 4 incomplete, 1 notice, 8 todos, 17 skipped, 716 passed (1733 assertions) \ No newline at end of file diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 3abd01a9..a67437b2 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -18,7 +18,7 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 1 deprecated, 3 warnings, 4 incomplete, 1 notice, 8 todos, 14 skipped, 704 passed (1714 assertions)') + ->toContain('Tests: 1 deprecated, 3 warnings, 4 incomplete, 1 notice, 8 todos, 14 skipped, 705 passed (1718 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows();