From d03302db7b93a09ebda7b73461442401e96e187f Mon Sep 17 00:00:00 2001 From: Luke Downing Date: Mon, 6 Feb 2023 23:06:13 +0000 Subject: [PATCH] WIP --- src/Plugins/Parallel/Handlers/Laravel.php | 18 ++ .../Parallel/Paratest/ResultPrinter.php | 106 +++------- .../Parallel/Paratest/WrapperRunner.php | 6 + .../Parallel/Support/CompactPrinter.php | 181 ++++++++++++++++++ 4 files changed, 231 insertions(+), 80 deletions(-) create mode 100644 src/Plugins/Parallel/Support/CompactPrinter.php diff --git a/src/Plugins/Parallel/Handlers/Laravel.php b/src/Plugins/Parallel/Handlers/Laravel.php index 07fcb062..50f34950 100644 --- a/src/Plugins/Parallel/Handlers/Laravel.php +++ b/src/Plugins/Parallel/Handlers/Laravel.php @@ -4,7 +4,12 @@ declare(strict_types=1); namespace Pest\Plugins\Parallel\Handlers; +use Illuminate\Testing\ParallelRunner; +use ParaTest\Options; +use ParaTest\RunnerInterface; use Pest\Plugins\Concerns\HandleArguments; +use Pest\Plugins\Parallel\Paratest\WrapperRunner; +use Symfony\Component\Console\Output\OutputInterface; /** * @internal @@ -19,6 +24,8 @@ final class Laravel return $args; } + $this->setLaravelParallelRunner(); + foreach ($args as $value) { if (str_starts_with($value, '--runner')) { $args = $this->popArgument($value, $args); @@ -28,6 +35,17 @@ final class Laravel return $this->pushArgument('--runner=\Illuminate\Testing\ParallelRunner', $args); } + private function setLaravelParallelRunner(): void + { + if (!method_exists(ParallelRunner::class, 'resolveRunnerUsing')) { + exit('Using parallel with Pest requires Laravel v8.55.0 or higher.'); + } + + ParallelRunner::resolveRunnerUsing(function (Options $options, OutputInterface $output): RunnerInterface { + return new WrapperRunner($options, $output); + }); + } + private static function isALaravelApplication(): bool { return class_exists(\Illuminate\Foundation\Application::class) diff --git a/src/Plugins/Parallel/Paratest/ResultPrinter.php b/src/Plugins/Parallel/Paratest/ResultPrinter.php index 5fd0cdb2..752b1787 100644 --- a/src/Plugins/Parallel/Paratest/ResultPrinter.php +++ b/src/Plugins/Parallel/Paratest/ResultPrinter.php @@ -4,7 +4,10 @@ declare(strict_types=1); namespace Pest\Plugins\Parallel\Paratest; +use NunoMaduro\Collision\Adapters\Phpunit\TestResult as CollisionTestResult; +use NunoMaduro\Collision\Exceptions\TestException; use ParaTest\Options; +use Pest\Plugins\Parallel\Support\CompactPrinter; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\Output\Default\ResultPrinter as DefaultResultPrinter; @@ -13,11 +16,13 @@ use PHPUnit\TextUI\Output\SummaryPrinter; use PHPUnit\Util\Color; use SebastianBergmann\CodeCoverage\Driver\Selector; use SebastianBergmann\CodeCoverage\Filter; +use SebastianBergmann\Timer\Duration; use SebastianBergmann\Timer\ResourceUsageFormatter; use SplFileInfo; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Output\OutputInterface; +use Termwind\Terminal; use function assert; use function fclose; use function feof; @@ -32,6 +37,7 @@ use function sprintf; use function str_repeat; use function strlen; +use function Termwind\terminal; use const DIRECTORY_SEPARATOR; use const PHP_EOL; use const PHP_VERSION; @@ -40,6 +46,7 @@ use const PHP_VERSION; final class ResultPrinter { public readonly Printer $printer; + private readonly CompactPrinter $compactPrinter; private int $numTestsWidth = 0; private int $maxColumn = 0; @@ -72,6 +79,8 @@ final class ResultPrinter } }; + $this->compactPrinter = new CompactPrinter(); + if (! $this->options->configuration->hasLogfileTeamcity()) { return; } @@ -88,19 +97,13 @@ final class ResultPrinter public function start(): void { - $this->numTestsWidth = strlen((string) $this->totalCases); - $this->maxColumn = $this->numberOfColumns - + (DIRECTORY_SEPARATOR === '\\' ? -1 : 0) // fix windows blank lines - - strlen($this->getProgress()); - - // @see \PHPUnit\TextUI\TestRunner::writeMessage() - $output = $this->output; - $write = static function (string $type, string $message) use ($output): void { - $output->write(sprintf("%-15s%s\n", $type . ':', $message)); - }; - - // @see \PHPUnit\TextUI\Application::writeRuntimeInformation() - $write('Processes', (string) $this->options->processes); + $this->compactPrinter->line(sprintf( + 'Running %d test file%s using %d process%s', + $this->totalCases, + $this->totalCases === 1 ? '' : 's', + $this->options->processes, + $this->options->processes === 1 ? '' : 'es') + ); } /** @param list $teamcityFiles */ @@ -142,7 +145,7 @@ final class ResultPrinter * @param list $teamcityFiles * @param list $testdoxFiles */ - public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles): void + public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles, Duration $duration): void { if ($this->options->needsTeamcity) { $teamcityProgress = $this->tailMultiple($teamcityFiles); @@ -168,78 +171,21 @@ final class ResultPrinter return; } - $resultPrinter = new DefaultResultPrinter( - $this->printer, - $this->options->configuration->displayDetailsOnIncompleteTests(), - $this->options->configuration->displayDetailsOnSkippedTests(), - $this->options->configuration->displayDetailsOnTestsThatTriggerDeprecations(), - $this->options->configuration->displayDetailsOnTestsThatTriggerErrors(), - $this->options->configuration->displayDetailsOnTestsThatTriggerNotices(), - $this->options->configuration->displayDetailsOnTestsThatTriggerWarnings(), - false, - ); - $summaryPrinter = new SummaryPrinter( - $this->printer, - $this->options->configuration->colors(), - ); + $this->compactPrinter->newLine(); - $this->printer->print(PHP_EOL . (new ResourceUsageFormatter())->resourceUsageSinceStartOfRequest() . PHP_EOL . PHP_EOL); + $issues = array_map(fn($event) => CollisionTestResult::fromTestCase( + $event->test(), + CollisionTestResult::FAIL, + $event->throwable(), + ), [...$testResult->testFailedEvents(), ...$testResult->testErroredEvents()]); - $resultPrinter->print($testResult); - $summaryPrinter->print($testResult); + $this->compactPrinter->errors($issues); + $this->compactPrinter->recap($testResult, $duration); } private function printFeedbackItem(string $item): void { - $this->printFeedbackItemColor($item); - ++$this->column; - ++$this->casesProcessed; - if ($this->column !== $this->maxColumn && $this->casesProcessed < $this->totalCases) { - return; - } - - if ( - $this->casesProcessed > 0 - && $this->casesProcessed === $this->totalCases - && ($pad = $this->maxColumn - $this->column) > 0 - ) { - $this->output->write(str_repeat(' ', $pad)); - } - - $this->output->write($this->getProgress() . "\n"); - $this->column = 0; - } - - private function printFeedbackItemColor(string $item): void - { - $buffer = match ($item) { - 'E' => $this->colorizeTextBox('fg-red, bold', $item), - 'F' => $this->colorizeTextBox('bg-red, fg-white', $item), - 'I', 'N', 'D', 'R', 'W' => $this->colorizeTextBox('fg-yellow, bold', $item), - 'S' => $this->colorizeTextBox('fg-cyan, bold', $item), - default => $item, - }; - - $this->output->write($buffer); - } - - private function getProgress(): string - { - return sprintf( - ' %' . $this->numTestsWidth . 'd / %' . $this->numTestsWidth . 'd (%3s%%)', - $this->casesProcessed, - $this->totalCases, - floor(($this->totalCases > 0 ? $this->casesProcessed / $this->totalCases : 0) * 100), - ); - } - - private function colorizeTextBox(string $color, string $buffer): string - { - if (! $this->options->configuration->colors()) { - return $buffer; - } - - return Color::colorizeTextBox($color, $buffer); + $this->compactPrinter->descriptionItem($item); } /** @param list $files */ diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 4ea37787..76a77fde 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -18,6 +18,7 @@ use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\ShellExitCodeCalculator; use PHPUnit\Util\ExcludeList; +use SebastianBergmann\Timer\Timer; use SplFileInfo; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\PhpExecutableFinder; @@ -42,6 +43,7 @@ final class WrapperRunner implements RunnerInterface { private const CYCLE_SLEEP = 10000; private readonly ResultPrinter $printer; + private Timer $timer; /** @var non-empty-string[] */ private array $pending = []; @@ -70,6 +72,7 @@ final class WrapperRunner implements RunnerInterface private readonly OutputInterface $output ) { $this->printer = new ResultPrinter($output, $options); + $this->timer = new Timer(); $wrapper = realpath( dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'pest-wrapper.php', @@ -93,6 +96,8 @@ final class WrapperRunner implements RunnerInterface public function run(): int { + $this->timer->start(); + ExcludeList::addDirectory(dirname(__DIR__)); TestResultFacade::init(); EventFacade::seal(); @@ -277,6 +282,7 @@ final class WrapperRunner implements RunnerInterface $testResultSum, $this->teamcityFiles, $this->testdoxFiles, + $this->timer->stop(), ); $this->generateCodeCoverageReports(); $this->generateLogs(); diff --git a/src/Plugins/Parallel/Support/CompactPrinter.php b/src/Plugins/Parallel/Support/CompactPrinter.php new file mode 100644 index 00000000..1e852e5f --- /dev/null +++ b/src/Plugins/Parallel/Support/CompactPrinter.php @@ -0,0 +1,181 @@ +terminal = terminal(); + $this->output = new ConsoleOutput(decorated: true); + $this->style = new Style($this->output); + + $this->compactSymbolsPerLine = $this->terminal->width() - 4; + } + + public function newLine(): void + { + render('
'); + } + + public function line(string $message): void + { + render("{$message}"); + } + + public function descriptionItem(string $item): void + { + // TODO: Support todos + + $icon = match (strtolower($item)) { + 'f', 'e' => '⨯', // FAILED + 's' => 's', // SKIPPED + 'w', 'r' => '!', // WARN, RISKY + 'i' => '…', // INCOMPLETE + '.' => '.', // PASSED + default => $item, + }; + + $color = match (strtolower($item)) { + 'f', 'e' => 'red', + 'd', 's', 'i', 'r', 'w' => 'yellow', + default => 'gray', + }; + + $symbolsOnCurrentLine = $this->compactProcessed % $this->compactSymbolsPerLine; + + if ($symbolsOnCurrentLine >= $this->terminal->width() - 4) { + $symbolsOnCurrentLine = 0; + } + + if ($symbolsOnCurrentLine === 0) { + $this->output->writeln(''); + $this->output->write(' '); + } + + $this->output->write(sprintf('%s', $color, $icon)); + + $this->compactProcessed++; + + //switch ($item) { + // case self::TODO: + // return '↓'; + // case self::RUNS: + // return '•'; + // default: + // return '✓'; + //} + } + + public function errors(array $errors): void + { + array_map(function (TestResult $testResult): void { + if (! $testResult->throwable instanceof \PHPUnit\Event\Code\Throwable) { + throw new ShouldNotHappen(); + } + + renderUsing($this->output); + render(<<<'HTML' +
+
+
+ HTML); + + $testCaseName = $testResult->testCaseName; + $description = $testResult->description; + + /** @var class-string $throwableClassName */ + $throwableClassName = $testResult->throwable->className(); + + $throwableClassName = ! in_array($throwableClassName, [ + ExpectationFailedException::class, + IncompleteTestError::class, + ], true) ? sprintf('%s', (new ReflectionClass($throwableClassName))->getShortName()) + : ''; + + $truncateClasses = $this->output->isVerbose() ? '' : 'flex-1 truncate'; + + renderUsing($this->output); + render(sprintf(<<<'HTML' +
+ + %s %s>%s + + + %s + +
+ HTML, $truncateClasses, $testResult->color, $testResult->type, $testCaseName, $description, $throwableClassName)); + + $this->style->writeError($testResult->throwable); + }, $errors); + } + + public function recap(\PHPUnit\TestRunner\TestResult\TestResult $testResult, Duration $duration): void + { + $testCounts = [ + 'passed' => ['green', $testResult->numberOfTestsRun()], + 'failed' => ['red', $testResult->numberOfTestFailedEvents()], + 'errored' => ['red', $testResult->numberOfTestErroredEvents()], + 'skipped' => ['yellow', $testResult->numberOfTestSkippedEvents()], + 'incomplete' => ['yellow', $testResult->numberOfTestMarkedIncompleteEvents()], + 'risky' => ['yellow', $testResult->numberOfTestsWithTestConsideredRiskyEvents()], + 'warnings' => ['yellow', $testResult->numberOfTestsWithTestTriggeredWarningEvents()], + ]; + + $tests = []; + + foreach ($testCounts as $type => [$color, $count]) { + if ($count === 0) { + continue; + } + + $tests[] = "$count $type"; + } + + $this->output->writeln(['']); + + if (! empty($tests)) { + $this->output->writeln([ + sprintf( + ' Tests: %s (%s assertions)', + implode(', ', $tests), + $testResult->numberOfAssertions() + ), + ]); + } + + $this->output->writeln([ + sprintf( + ' Duration: %ss', + number_format($duration->asSeconds(), 2, '.', '') + ), + ]); + + $this->output->writeln(''); + } +}