terminal = terminal(); $this->output = $output; $this->compactSymbolsPerLine = $this->terminal->width() - 4; } /** * Prints the content similar too:. * * ``` * WARN Your XML configuration validates against a deprecated schema... * ``` */ public function writeWarning(string $message): void { $this->output->writeln(['', ' WARN '.$message]); } /** * Prints the content similar too:. * * ``` * WARN Your XML configuration validates against a deprecated schema... * ``` */ public function writeThrowable(\Throwable $throwable): void { $this->output->writeln(['', ' ERROR '.$throwable->getMessage()]); } /** * Prints the content similar too:. * * ``` * PASS Unit\ExampleTest * ✓ basic test * ``` */ public function writeCurrentTestCaseSummary(State $state): void { if ($state->testCaseTestsCount() === 0 || is_null($state->testCaseName)) { return; } if (! $state->headerPrinted && ! DefaultPrinter::compact()) { $this->output->writeln($this->titleLineFrom( $state->getTestCaseFontColor(), $state->getTestCaseTitleColor(), $state->getTestCaseTitle(), $state->testCaseName, $state->todosCount(), )); $state->headerPrinted = true; } $state->eachTestCaseTests(function (TestResult $testResult): void { if ($testResult->description !== '') { if (DefaultPrinter::compact()) { $this->writeCompactDescriptionLine($testResult); } else { $this->writeDescriptionLine($testResult); } } }); } /** * Prints the content similar too:. * * ``` * PASS Unit\ExampleTest * ✓ basic test * ``` */ public function writeErrorsSummary(State $state): void { $configuration = Registry::get(); $failTypes = [ TestResult::FAIL, ]; if ($configuration->displayDetailsOnTestsThatTriggerNotices()) { $failTypes[] = TestResult::NOTICE; } if ($configuration->displayDetailsOnTestsThatTriggerDeprecations()) { $failTypes[] = TestResult::DEPRECATED; } if ($configuration->failOnWarning() || $configuration->displayDetailsOnTestsThatTriggerWarnings()) { $failTypes[] = TestResult::WARN; } if ($configuration->failOnRisky()) { $failTypes[] = TestResult::RISKY; } if ($configuration->failOnIncomplete() || $configuration->displayDetailsOnIncompleteTests()) { $failTypes[] = TestResult::INCOMPLETE; } if ($configuration->failOnSkipped() || $configuration->displayDetailsOnSkippedTests()) { $failTypes[] = TestResult::SKIPPED; } $failTypes = array_unique($failTypes); $errors = array_values(array_filter($state->suiteTests, fn (TestResult $testResult) => in_array( $testResult->type, $failTypes, true ))); array_map(function (TestResult $testResult): void { if (! $testResult->throwable instanceof 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, SkippedWithMessageException::class, TestOutcome::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 === 'yellow' ? 'yellow-400' : $testResult->color, $testResult->color === 'yellow' ? 'text-black' : '', $testResult->type, $testCaseName, $description, $throwableClassName)); $this->writeError($testResult->throwable); }, $errors); } /** * Writes the final recap. */ public function writeRecap(State $state, Info $telemetry, PHPUnitTestResult $result): void { $tests = []; foreach (self::TYPES as $type) { if (($countTests = $state->countTestsInTestSuiteBy($type)) !== 0) { $color = TestResult::makeColor($type); if ($type === TestResult::WARN && $countTests < 2) { $type = 'warning'; } if ($type === TestResult::NOTICE && $countTests > 1) { $type = 'notices'; } if ($type === TestResult::TODO && $countTests > 1) { $type = 'todos'; } $tests[] = "$countTests $type"; } } $pending = $result->numberOfTests() - $result->numberOfTestsRun(); if ($pending > 0) { $tests[] = "\e[2m$pending pending\e[22m"; } $timeElapsed = number_format($telemetry->durationSinceStart()->asFloat(), 2, '.', ''); $this->output->writeln(['']); if (! empty($tests)) { $this->output->writeln([ sprintf( ' Tests: %s (%s assertions)', implode(', ', $tests), $result->numberOfAssertions() ), ]); } $this->output->writeln([ sprintf( ' Duration: %ss', $timeElapsed ), ]); $this->output->writeln(''); } /** * @param array $slowTests */ public function writeSlowTests(array $slowTests, Info $telemetry): void { $this->output->writeln(' Top 10 slowest tests:'); $timeElapsed = $telemetry->durationSinceStart()->asFloat(); foreach ($slowTests as $testResult) { $seconds = number_format($testResult->duration / 1000, 2, '.', ''); // If duration is more than 25% of the total time elapsed, set the color as red // If duration is more than 10% of the total time elapsed, set the color as yellow // Otherwise, set the color as default $color = ($testResult->duration / 1000) > $timeElapsed * 0.25 ? 'red' : ($testResult->duration > $timeElapsed * 0.1 ? 'yellow' : 'gray'); renderUsing($this->output); render(sprintf(<<<'HTML'
%s>%s %ss
HTML, $testResult->testCaseName, $testResult->description, $color, $seconds)); } $timeElapsedInSlowTests = array_sum(array_map(fn (TestResult $testResult) => $testResult->duration / 1000, $slowTests)); $timeElapsedAsString = number_format($timeElapsed, 2, '.', ''); $percentageInSlowTestsAsString = number_format($timeElapsedInSlowTests * 100 / $timeElapsed, 2, '.', ''); $timeElapsedInSlowTestsAsString = number_format($timeElapsedInSlowTests, 2, '.', ''); renderUsing($this->output); render(sprintf(<<<'HTML'

(%s%% of %ss) %ss
HTML, $percentageInSlowTestsAsString, $timeElapsedAsString, $timeElapsedInSlowTestsAsString)); } /** * Displays the error using Collision's writer and terminates with exit code === 1. */ public function writeError(Throwable $throwable): void { $writer = (new Writer())->setOutput($this->output); $throwable = new TestException($throwable, $this->output->isVerbose()); $writer->showTitle(false); $writer->ignoreFilesIn([ '/vendor\/nunomaduro\/collision/', '/vendor\/bin\/pest/', '/bin\/pest/', '/vendor\/pestphp\/pest/', '/vendor\/pestphp\/pest-plugin-arch/', '/vendor\/phpspec\/prophecy-phpunit/', '/vendor\/phpspec\/prophecy/', '/vendor\/phpunit\/phpunit\/src/', '/vendor\/mockery\/mockery/', '/vendor\/laravel\/dusk/', '/vendor\/laravel\/framework\/src\/Illuminate\/Testing/', '/vendor\/laravel\/framework\/src\/Illuminate\/Foundation\/Testing/', '/vendor\/symfony\/framework-bundle\/Test/', '/vendor\/symfony\/phpunit-bridge/', '/vendor\/symfony\/dom-crawler/', '/vendor\/symfony\/browser-kit/', '/vendor\/symfony\/css-selector/', '/vendor\/bin\/.phpunit/', '/bin\/.phpunit/', '/vendor\/bin\/simple-phpunit/', '/bin\/phpunit/', '/vendor\/coduo\/php-matcher\/src\/PHPUnit/', '/vendor\/sulu\/sulu\/src\/Sulu\/Bundle\/TestBundle\/Testing/', '/vendor\/webmozart\/assert/', $this->ignorePestPipes(...), $this->ignorePestExtends(...), $this->ignorePestInterceptors(...), ]); /** @var \Throwable $throwable */ $inspector = new Inspector($throwable); $writer->write($inspector); } /** * Returns the title contents. */ private function titleLineFrom(string $fg, string $bg, string $title, string $testCaseName, int $todos): string { return sprintf( "\n %s %s%s", $fg, $bg, $title, $testCaseName, $todos > 0 ? sprintf(' - %s todo%s', $todos, $todos > 1 ? 's' : '') : '', ); } /** * Writes a description line. */ private function writeCompactDescriptionLine(TestResult $result): void { $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', $result->compactColor, $result->compactIcon)); $this->compactProcessed++; } /** * Writes a description line. */ private function writeDescriptionLine(TestResult $result): void { if (! empty($warning = $result->warning)) { if (! str_contains($warning, "\n")) { $warning = sprintf( ' → %s', $warning ); } else { $warningLines = explode("\n", $warning); $warning = ''; foreach ($warningLines as $w) { $warning .= sprintf( "\n ⇂ %s", trim($w) ); } } } $seconds = ''; if (($result->duration / 1000) > 0.0) { $seconds = number_format($result->duration / 1000, 2, '.', ''); $seconds = $seconds !== '0.00' ? sprintf('%ss', $seconds) : ''; } // Pest specific if (isset($_SERVER['REBUILD_SNAPSHOTS']) || (isset($_SERVER['COLLISION_IGNORE_DURATION']) && $_SERVER['COLLISION_IGNORE_DURATION'] === 'true')) { $seconds = ''; } $truncateClasses = $this->output->isVerbose() ? '' : 'flex-1 truncate'; if ($warning !== '') { $warning = sprintf('%s', $warning); } $description = preg_replace('/`([^`]+)`/', '$1', $result->description); renderUsing($this->output); render(sprintf(<<<'HTML'
%s%s%s %s
HTML, $seconds === '' ? '' : 'flex space-x-1 justify-between', $truncateClasses, $result->color, $result->icon, $description, $warning, $seconds)); } /** * @param Frame $frame */ private function ignorePestPipes($frame): bool { if (class_exists(Expectation::class)) { $reflection = new ReflectionClass(Expectation::class); /** @var array> $expectationPipes */ $expectationPipes = $reflection->getStaticPropertyValue('pipes', []); foreach ($expectationPipes as $pipes) { foreach ($pipes as $pipeClosure) { if ($this->isFrameInClosure($frame, $pipeClosure)) { return true; } } } } return false; } /** * @param Frame $frame */ private function ignorePestExtends($frame): bool { if (class_exists(Expectation::class)) { $reflection = new ReflectionClass(Expectation::class); /** @var array $extends */ $extends = $reflection->getStaticPropertyValue('extends', []); foreach ($extends as $extendClosure) { if ($this->isFrameInClosure($frame, $extendClosure)) { return true; } } } return false; } /** * @param Frame $frame */ private function ignorePestInterceptors($frame): bool { if (class_exists(Expectation::class)) { $reflection = new ReflectionClass(Expectation::class); /** @var array> $expectationInterceptors */ $expectationInterceptors = $reflection->getStaticPropertyValue('interceptors', []); foreach ($expectationInterceptors as $pipes) { foreach ($pipes as $pipeClosure) { if ($this->isFrameInClosure($frame, $pipeClosure)) { return true; } } } } return false; } /** * @param Frame $frame */ private function isFrameInClosure($frame, Closure $closure): bool { $reflection = new ReflectionFunction($closure); $sanitizedPath = (string) str_replace('\\', '/', (string) $frame->getFile()); /** @phpstan-ignore-next-line */ $sanitizedClosurePath = (string) str_replace('\\', '/', $reflection->getFileName()); if ($sanitizedPath === $sanitizedClosurePath) { if ($reflection->getStartLine() <= $frame->getLine() && $frame->getLine() <= $reflection->getEndLine()) { return true; } } return false; } }