diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index dad972a8733..9d4ca44ef6c 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -259,7 +259,7 @@ $option['desc'] - + $color diff --git a/ChangeLog-9.0.md b/ChangeLog-9.0.md index 94f0cf13801..ed518c48605 100644 --- a/ChangeLog-9.0.md +++ b/ChangeLog-9.0.md @@ -11,6 +11,7 @@ All notable changes of the PHPUnit 9.0 release series are documented in this fil ### Changed * Implemented [#3746](https://github.com/sebastianbergmann/phpunit/issues/3746): Improve developer experience of global wrapper functions for assertions +* Implemented [#4024](https://github.com/sebastianbergmann/phpunit/issues/4024): Make `PHPUnit\TextUI\ResultPrinter` an interface ### Removed diff --git a/src/TextUI/Command.php b/src/TextUI/Command.php index 49da2068204..22e974f1ec0 100644 --- a/src/TextUI/Command.php +++ b/src/TextUI/Command.php @@ -15,7 +15,6 @@ use PharIo\Version\Version as PharIoVersion; use PHPUnit\Framework\Exception; use PHPUnit\Framework\Test; -use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\StandardTestSuiteLoader; use PHPUnit\Runner\TestSuiteLoader; @@ -285,7 +284,7 @@ protected function handleArguments(array $argv): void foreach ($this->options[0] as $option) { switch ($option[0]) { case '--colors': - $this->arguments['colors'] = $option[1] ?: ResultPrinter::COLOR_AUTO; + $this->arguments['colors'] = $option[1] ?: DefaultResultPrinter::COLOR_AUTO; break; @@ -1010,7 +1009,7 @@ protected function handleLoader(string $loaderClass, string $loaderFile = ''): ? protected function handlePrinter(string $printerClass, string $printerFile = '') { if (!\class_exists($printerClass, false)) { - if ($printerFile == '') { + if ($printerFile === '') { $printerFile = Filesystem::classNameToFilename( $printerClass ); @@ -1044,22 +1043,12 @@ protected function handlePrinter(string $printerClass, string $printerFile = '') // @codeCoverageIgnoreEnd } - if (!$class->implementsInterface(TestListener::class)) { + if (!$class->implementsInterface(ResultPrinter::class)) { $this->exitWithErrorMessage( \sprintf( 'Could not use "%s" as printer: class does not implement %s', $printerClass, - TestListener::class - ) - ); - } - - if (!$class->isSubclassOf(Printer::class)) { - $this->exitWithErrorMessage( - \sprintf( - 'Could not use "%s" as printer: class does not extend %s', - $printerClass, - Printer::class + ResultPrinter::class ) ); } diff --git a/src/TextUI/DefaultResultPrinter.php b/src/TextUI/DefaultResultPrinter.php new file mode 100644 index 00000000000..ba9a5bc9036 --- /dev/null +++ b/src/TextUI/DefaultResultPrinter.php @@ -0,0 +1,570 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TextUI; + +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Exception; +use PHPUnit\Framework\InvalidArgumentException; +use PHPUnit\Framework\Test; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestFailure; +use PHPUnit\Framework\TestResult; +use PHPUnit\Framework\TestSuite; +use PHPUnit\Framework\Warning; +use PHPUnit\Runner\PhptTestCase; +use PHPUnit\Util\Color; +use PHPUnit\Util\Printer; +use SebastianBergmann\Environment\Console; +use SebastianBergmann\Timer\Timer; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ +class DefaultResultPrinter extends Printer implements ResultPrinter +{ + public const EVENT_TEST_START = 0; + + public const EVENT_TEST_END = 1; + + public const EVENT_TESTSUITE_START = 2; + + public const EVENT_TESTSUITE_END = 3; + + public const COLOR_NEVER = 'never'; + + public const COLOR_AUTO = 'auto'; + + public const COLOR_ALWAYS = 'always'; + + public const COLOR_DEFAULT = self::COLOR_NEVER; + + private const AVAILABLE_COLORS = [self::COLOR_NEVER, self::COLOR_AUTO, self::COLOR_ALWAYS]; + + /** + * @var int + */ + protected $column = 0; + + /** + * @var int + */ + protected $maxColumn; + + /** + * @var bool + */ + protected $lastTestFailed = false; + + /** + * @var int + */ + protected $numAssertions = 0; + + /** + * @var int + */ + protected $numTests = -1; + + /** + * @var int + */ + protected $numTestsRun = 0; + + /** + * @var int + */ + protected $numTestsWidth; + + /** + * @var bool + */ + protected $colors = false; + + /** + * @var bool + */ + protected $debug = false; + + /** + * @var bool + */ + protected $verbose = false; + + /** + * @var int + */ + private $numberOfColumns; + + /** + * @var bool + */ + private $reverse; + + /** + * @var bool + */ + private $defectListPrinted = false; + + /** + * Constructor. + * + * @param null|resource|string $out + * @param int|string $numberOfColumns + * + * @throws Exception + */ + public function __construct($out = null, bool $verbose = false, string $colors = self::COLOR_DEFAULT, bool $debug = false, $numberOfColumns = 80, bool $reverse = false) + { + parent::__construct($out); + + if (!\in_array($colors, self::AVAILABLE_COLORS, true)) { + throw InvalidArgumentException::create( + 3, + \vsprintf('value from "%s", "%s" or "%s"', self::AVAILABLE_COLORS) + ); + } + + if (!\is_int($numberOfColumns) && $numberOfColumns !== 'max') { + throw InvalidArgumentException::create(5, 'integer or "max"'); + } + + $console = new Console; + $maxNumberOfColumns = $console->getNumberOfColumns(); + + if ($numberOfColumns === 'max' || ($numberOfColumns !== 80 && $numberOfColumns > $maxNumberOfColumns)) { + $numberOfColumns = $maxNumberOfColumns; + } + + $this->numberOfColumns = $numberOfColumns; + $this->verbose = $verbose; + $this->debug = $debug; + $this->reverse = $reverse; + + if ($colors === self::COLOR_AUTO && $console->hasColorSupport()) { + $this->colors = true; + } else { + $this->colors = (self::COLOR_ALWAYS === $colors); + } + } + + /** + * @throws \SebastianBergmann\Timer\RuntimeException + */ + public function printResult(TestResult $result): void + { + $this->printHeader(); + $this->printErrors($result); + $this->printWarnings($result); + $this->printFailures($result); + $this->printRisky($result); + + if ($this->verbose) { + $this->printIncompletes($result); + $this->printSkipped($result); + } + + $this->printFooter($result); + } + + /** + * An error occurred. + */ + public function addError(Test $test, \Throwable $t, float $time): void + { + $this->writeProgressWithColor('fg-red, bold', 'E'); + $this->lastTestFailed = true; + } + + /** + * A failure occurred. + */ + public function addFailure(Test $test, AssertionFailedError $e, float $time): void + { + $this->writeProgressWithColor('bg-red, fg-white', 'F'); + $this->lastTestFailed = true; + } + + /** + * A warning occurred. + */ + public function addWarning(Test $test, Warning $e, float $time): void + { + $this->writeProgressWithColor('fg-yellow, bold', 'W'); + $this->lastTestFailed = true; + } + + /** + * Incomplete test. + */ + public function addIncompleteTest(Test $test, \Throwable $t, float $time): void + { + $this->writeProgressWithColor('fg-yellow, bold', 'I'); + $this->lastTestFailed = true; + } + + /** + * Risky test. + */ + public function addRiskyTest(Test $test, \Throwable $t, float $time): void + { + $this->writeProgressWithColor('fg-yellow, bold', 'R'); + $this->lastTestFailed = true; + } + + /** + * Skipped test. + */ + public function addSkippedTest(Test $test, \Throwable $t, float $time): void + { + $this->writeProgressWithColor('fg-cyan, bold', 'S'); + $this->lastTestFailed = true; + } + + /** + * A testsuite started. + */ + public function startTestSuite(TestSuite $suite): void + { + if ($this->numTests == -1) { + $this->numTests = \count($suite); + $this->numTestsWidth = \strlen((string) $this->numTests); + $this->maxColumn = $this->numberOfColumns - \strlen(' / (XXX%)') - (2 * $this->numTestsWidth); + } + } + + /** + * A testsuite ended. + */ + public function endTestSuite(TestSuite $suite): void + { + } + + /** + * A test started. + */ + public function startTest(Test $test): void + { + if ($this->debug) { + $this->write( + \sprintf( + "Test '%s' started\n", + \PHPUnit\Util\Test::describeAsString($test) + ) + ); + } + } + + /** + * A test ended. + */ + public function endTest(Test $test, float $time): void + { + if ($this->debug) { + $this->write( + \sprintf( + "Test '%s' ended\n", + \PHPUnit\Util\Test::describeAsString($test) + ) + ); + } + + if (!$this->lastTestFailed) { + $this->writeProgress('.'); + } + + if ($test instanceof TestCase) { + $this->numAssertions += $test->getNumAssertions(); + } elseif ($test instanceof PhptTestCase) { + $this->numAssertions++; + } + + $this->lastTestFailed = false; + + if ($test instanceof TestCase && !$test->hasExpectationOnOutput()) { + $this->write($test->getActualOutput()); + } + } + + protected function printDefects(array $defects, string $type): void + { + $count = \count($defects); + + if ($count == 0) { + return; + } + + if ($this->defectListPrinted) { + $this->write("\n--\n\n"); + } + + $this->write( + \sprintf( + "There %s %d %s%s:\n", + ($count == 1) ? 'was' : 'were', + $count, + $type, + ($count == 1) ? '' : 's' + ) + ); + + $i = 1; + + if ($this->reverse) { + $defects = \array_reverse($defects); + } + + foreach ($defects as $defect) { + $this->printDefect($defect, $i++); + } + + $this->defectListPrinted = true; + } + + protected function printDefect(TestFailure $defect, int $count): void + { + $this->printDefectHeader($defect, $count); + $this->printDefectTrace($defect); + } + + protected function printDefectHeader(TestFailure $defect, int $count): void + { + $this->write( + \sprintf( + "\n%d) %s\n", + $count, + $defect->getTestName() + ) + ); + } + + protected function printDefectTrace(TestFailure $defect): void + { + $e = $defect->thrownException(); + $this->write((string) $e); + + while ($e = $e->getPrevious()) { + $this->write("\nCaused by\n" . $e); + } + } + + protected function printErrors(TestResult $result): void + { + $this->printDefects($result->errors(), 'error'); + } + + protected function printFailures(TestResult $result): void + { + $this->printDefects($result->failures(), 'failure'); + } + + protected function printWarnings(TestResult $result): void + { + $this->printDefects($result->warnings(), 'warning'); + } + + protected function printIncompletes(TestResult $result): void + { + $this->printDefects($result->notImplemented(), 'incomplete test'); + } + + protected function printRisky(TestResult $result): void + { + $this->printDefects($result->risky(), 'risky test'); + } + + protected function printSkipped(TestResult $result): void + { + $this->printDefects($result->skipped(), 'skipped test'); + } + + /** + * @throws \SebastianBergmann\Timer\RuntimeException + */ + protected function printHeader(): void + { + $this->write("\n\n" . Timer::resourceUsage() . "\n\n"); + } + + protected function printFooter(TestResult $result): void + { + if (\count($result) === 0) { + $this->writeWithColor( + 'fg-black, bg-yellow', + 'No tests executed!' + ); + + return; + } + + if ($result->wasSuccessful() && + $result->allHarmless() && + $result->allCompletelyImplemented() && + $result->noneSkipped()) { + $this->writeWithColor( + 'fg-black, bg-green', + \sprintf( + 'OK (%d test%s, %d assertion%s)', + \count($result), + (\count($result) === 1) ? '' : 's', + $this->numAssertions, + ($this->numAssertions === 1) ? '' : 's' + ) + ); + } else { + if ($result->wasSuccessful()) { + $color = 'fg-black, bg-yellow'; + + if ($this->verbose || !$result->allHarmless()) { + $this->write("\n"); + } + + $this->writeWithColor( + $color, + 'OK, but incomplete, skipped, or risky tests!' + ); + } else { + $this->write("\n"); + + if ($result->errorCount()) { + $color = 'fg-white, bg-red'; + + $this->writeWithColor( + $color, + 'ERRORS!' + ); + } elseif ($result->failureCount()) { + $color = 'fg-white, bg-red'; + + $this->writeWithColor( + $color, + 'FAILURES!' + ); + } elseif ($result->warningCount()) { + $color = 'fg-black, bg-yellow'; + + $this->writeWithColor( + $color, + 'WARNINGS!' + ); + } + } + + $this->writeCountString(\count($result), 'Tests', $color, true); + $this->writeCountString($this->numAssertions, 'Assertions', $color, true); + $this->writeCountString($result->errorCount(), 'Errors', $color); + $this->writeCountString($result->failureCount(), 'Failures', $color); + $this->writeCountString($result->warningCount(), 'Warnings', $color); + $this->writeCountString($result->skippedCount(), 'Skipped', $color); + $this->writeCountString($result->notImplementedCount(), 'Incomplete', $color); + $this->writeCountString($result->riskyCount(), 'Risky', $color); + $this->writeWithColor($color, '.'); + } + } + + protected function writeProgress(string $progress): void + { + if ($this->debug) { + return; + } + + $this->write($progress); + $this->column++; + $this->numTestsRun++; + + if ($this->column == $this->maxColumn || $this->numTestsRun == $this->numTests) { + if ($this->numTestsRun == $this->numTests) { + $this->write(\str_repeat(' ', $this->maxColumn - $this->column)); + } + + $this->write( + \sprintf( + ' %' . $this->numTestsWidth . 'd / %' . + $this->numTestsWidth . 'd (%3s%%)', + $this->numTestsRun, + $this->numTests, + \floor(($this->numTestsRun / $this->numTests) * 100) + ) + ); + + if ($this->column == $this->maxColumn) { + $this->writeNewLine(); + } + } + } + + protected function writeNewLine(): void + { + $this->column = 0; + $this->write("\n"); + } + + /** + * Formats a buffer with a specified ANSI color sequence if colors are + * enabled. + */ + protected function colorizeTextBox(string $color, string $buffer): string + { + if (!$this->colors) { + return $buffer; + } + + $lines = \preg_split('/\r\n|\r|\n/', $buffer); + $padding = \max(\array_map('\strlen', $lines)); + + $styledLines = []; + + foreach ($lines as $line) { + $styledLines[] = Color::colorize($color, \str_pad($line, $padding)); + } + + return \implode(\PHP_EOL, $styledLines); + } + + /** + * Writes a buffer out with a color sequence if colors are enabled. + */ + protected function writeWithColor(string $color, string $buffer, bool $lf = true): void + { + $this->write($this->colorizeTextBox($color, $buffer)); + + if ($lf) { + $this->write(\PHP_EOL); + } + } + + /** + * Writes progress with a color sequence if colors are enabled. + */ + protected function writeProgressWithColor(string $color, string $buffer): void + { + $buffer = $this->colorizeTextBox($color, $buffer); + $this->writeProgress($buffer); + } + + private function writeCountString(int $count, string $name, string $color, bool $always = false): void + { + static $first = true; + + if ($always || $count > 0) { + $this->writeWithColor( + $color, + \sprintf( + '%s%s: %d', + !$first ? ', ' : '', + $name, + $count + ), + false + ); + + $first = false; + } + } +} diff --git a/src/TextUI/ResultPrinter.php b/src/TextUI/ResultPrinter.php index d6a68a671cc..1d3aac9a632 100644 --- a/src/TextUI/ResultPrinter.php +++ b/src/TextUI/ResultPrinter.php @@ -9,563 +9,12 @@ */ namespace PHPUnit\TextUI; -use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Exception; -use PHPUnit\Framework\InvalidArgumentException; -use PHPUnit\Framework\Test; -use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestResult; -use PHPUnit\Framework\TestSuite; -use PHPUnit\Framework\Warning; -use PHPUnit\Runner\PhptTestCase; -use PHPUnit\Util\Color; -use PHPUnit\Util\Printer; -use SebastianBergmann\Environment\Console; -use SebastianBergmann\Timer\Timer; -/** - * @internal This class is not covered by the backward compatibility promise for PHPUnit - */ -class ResultPrinter extends Printer implements TestListener +interface ResultPrinter extends TestListener { - public const EVENT_TEST_START = 0; - - public const EVENT_TEST_END = 1; - - public const EVENT_TESTSUITE_START = 2; - - public const EVENT_TESTSUITE_END = 3; - - public const COLOR_NEVER = 'never'; - - public const COLOR_AUTO = 'auto'; - - public const COLOR_ALWAYS = 'always'; - - public const COLOR_DEFAULT = self::COLOR_NEVER; - - private const AVAILABLE_COLORS = [self::COLOR_NEVER, self::COLOR_AUTO, self::COLOR_ALWAYS]; - - /** - * @var int - */ - protected $column = 0; - - /** - * @var int - */ - protected $maxColumn; - - /** - * @var bool - */ - protected $lastTestFailed = false; - - /** - * @var int - */ - protected $numAssertions = 0; - - /** - * @var int - */ - protected $numTests = -1; - - /** - * @var int - */ - protected $numTestsRun = 0; - - /** - * @var int - */ - protected $numTestsWidth; - - /** - * @var bool - */ - protected $colors = false; - - /** - * @var bool - */ - protected $debug = false; - - /** - * @var bool - */ - protected $verbose = false; - - /** - * @var int - */ - private $numberOfColumns; - - /** - * @var bool - */ - private $reverse; - - /** - * @var bool - */ - private $defectListPrinted = false; - - /** - * Constructor. - * - * @param null|resource|string $out - * @param int|string $numberOfColumns - * - * @throws Exception - */ - public function __construct($out = null, bool $verbose = false, string $colors = self::COLOR_DEFAULT, bool $debug = false, $numberOfColumns = 80, bool $reverse = false) - { - parent::__construct($out); - - if (!\in_array($colors, self::AVAILABLE_COLORS, true)) { - throw InvalidArgumentException::create( - 3, - \vsprintf('value from "%s", "%s" or "%s"', self::AVAILABLE_COLORS) - ); - } - - if (!\is_int($numberOfColumns) && $numberOfColumns !== 'max') { - throw InvalidArgumentException::create(5, 'integer or "max"'); - } - - $console = new Console; - $maxNumberOfColumns = $console->getNumberOfColumns(); - - if ($numberOfColumns === 'max' || ($numberOfColumns !== 80 && $numberOfColumns > $maxNumberOfColumns)) { - $numberOfColumns = $maxNumberOfColumns; - } - - $this->numberOfColumns = $numberOfColumns; - $this->verbose = $verbose; - $this->debug = $debug; - $this->reverse = $reverse; - - if ($colors === self::COLOR_AUTO && $console->hasColorSupport()) { - $this->colors = true; - } else { - $this->colors = (self::COLOR_ALWAYS === $colors); - } - } - - /** - * @throws \SebastianBergmann\Timer\RuntimeException - */ - public function printResult(TestResult $result): void - { - $this->printHeader(); - $this->printErrors($result); - $this->printWarnings($result); - $this->printFailures($result); - $this->printRisky($result); - - if ($this->verbose) { - $this->printIncompletes($result); - $this->printSkipped($result); - } - - $this->printFooter($result); - } - - /** - * An error occurred. - */ - public function addError(Test $test, \Throwable $t, float $time): void - { - $this->writeProgressWithColor('fg-red, bold', 'E'); - $this->lastTestFailed = true; - } - - /** - * A failure occurred. - */ - public function addFailure(Test $test, AssertionFailedError $e, float $time): void - { - $this->writeProgressWithColor('bg-red, fg-white', 'F'); - $this->lastTestFailed = true; - } - - /** - * A warning occurred. - */ - public function addWarning(Test $test, Warning $e, float $time): void - { - $this->writeProgressWithColor('fg-yellow, bold', 'W'); - $this->lastTestFailed = true; - } - - /** - * Incomplete test. - */ - public function addIncompleteTest(Test $test, \Throwable $t, float $time): void - { - $this->writeProgressWithColor('fg-yellow, bold', 'I'); - $this->lastTestFailed = true; - } - - /** - * Risky test. - */ - public function addRiskyTest(Test $test, \Throwable $t, float $time): void - { - $this->writeProgressWithColor('fg-yellow, bold', 'R'); - $this->lastTestFailed = true; - } - - /** - * Skipped test. - */ - public function addSkippedTest(Test $test, \Throwable $t, float $time): void - { - $this->writeProgressWithColor('fg-cyan, bold', 'S'); - $this->lastTestFailed = true; - } - - /** - * A testsuite started. - */ - public function startTestSuite(TestSuite $suite): void - { - if ($this->numTests == -1) { - $this->numTests = \count($suite); - $this->numTestsWidth = \strlen((string) $this->numTests); - $this->maxColumn = $this->numberOfColumns - \strlen(' / (XXX%)') - (2 * $this->numTestsWidth); - } - } - - /** - * A testsuite ended. - */ - public function endTestSuite(TestSuite $suite): void - { - } - - /** - * A test started. - */ - public function startTest(Test $test): void - { - if ($this->debug) { - $this->write( - \sprintf( - "Test '%s' started\n", - \PHPUnit\Util\Test::describeAsString($test) - ) - ); - } - } - - /** - * A test ended. - */ - public function endTest(Test $test, float $time): void - { - if ($this->debug) { - $this->write( - \sprintf( - "Test '%s' ended\n", - \PHPUnit\Util\Test::describeAsString($test) - ) - ); - } - - if (!$this->lastTestFailed) { - $this->writeProgress('.'); - } - - if ($test instanceof TestCase) { - $this->numAssertions += $test->getNumAssertions(); - } elseif ($test instanceof PhptTestCase) { - $this->numAssertions++; - } - - $this->lastTestFailed = false; - - if ($test instanceof TestCase && !$test->hasExpectationOnOutput()) { - $this->write($test->getActualOutput()); - } - } - - protected function printDefects(array $defects, string $type): void - { - $count = \count($defects); - - if ($count == 0) { - return; - } - - if ($this->defectListPrinted) { - $this->write("\n--\n\n"); - } - - $this->write( - \sprintf( - "There %s %d %s%s:\n", - ($count == 1) ? 'was' : 'were', - $count, - $type, - ($count == 1) ? '' : 's' - ) - ); - - $i = 1; - - if ($this->reverse) { - $defects = \array_reverse($defects); - } - - foreach ($defects as $defect) { - $this->printDefect($defect, $i++); - } - - $this->defectListPrinted = true; - } - - protected function printDefect(TestFailure $defect, int $count): void - { - $this->printDefectHeader($defect, $count); - $this->printDefectTrace($defect); - } - - protected function printDefectHeader(TestFailure $defect, int $count): void - { - $this->write( - \sprintf( - "\n%d) %s\n", - $count, - $defect->getTestName() - ) - ); - } - - protected function printDefectTrace(TestFailure $defect): void - { - $e = $defect->thrownException(); - $this->write((string) $e); - - while ($e = $e->getPrevious()) { - $this->write("\nCaused by\n" . $e); - } - } - - protected function printErrors(TestResult $result): void - { - $this->printDefects($result->errors(), 'error'); - } - - protected function printFailures(TestResult $result): void - { - $this->printDefects($result->failures(), 'failure'); - } - - protected function printWarnings(TestResult $result): void - { - $this->printDefects($result->warnings(), 'warning'); - } - - protected function printIncompletes(TestResult $result): void - { - $this->printDefects($result->notImplemented(), 'incomplete test'); - } - - protected function printRisky(TestResult $result): void - { - $this->printDefects($result->risky(), 'risky test'); - } - - protected function printSkipped(TestResult $result): void - { - $this->printDefects($result->skipped(), 'skipped test'); - } - - /** - * @throws \SebastianBergmann\Timer\RuntimeException - */ - protected function printHeader(): void - { - $this->write("\n\n" . Timer::resourceUsage() . "\n\n"); - } - - protected function printFooter(TestResult $result): void - { - if (\count($result) === 0) { - $this->writeWithColor( - 'fg-black, bg-yellow', - 'No tests executed!' - ); - - return; - } - - if ($result->wasSuccessful() && - $result->allHarmless() && - $result->allCompletelyImplemented() && - $result->noneSkipped()) { - $this->writeWithColor( - 'fg-black, bg-green', - \sprintf( - 'OK (%d test%s, %d assertion%s)', - \count($result), - (\count($result) == 1) ? '' : 's', - $this->numAssertions, - ($this->numAssertions == 1) ? '' : 's' - ) - ); - } else { - if ($result->wasSuccessful()) { - $color = 'fg-black, bg-yellow'; - - if ($this->verbose || !$result->allHarmless()) { - $this->write("\n"); - } - - $this->writeWithColor( - $color, - 'OK, but incomplete, skipped, or risky tests!' - ); - } else { - $this->write("\n"); - - if ($result->errorCount()) { - $color = 'fg-white, bg-red'; - - $this->writeWithColor( - $color, - 'ERRORS!' - ); - } elseif ($result->failureCount()) { - $color = 'fg-white, bg-red'; - - $this->writeWithColor( - $color, - 'FAILURES!' - ); - } elseif ($result->warningCount()) { - $color = 'fg-black, bg-yellow'; - - $this->writeWithColor( - $color, - 'WARNINGS!' - ); - } - } - - $this->writeCountString(\count($result), 'Tests', $color, true); - $this->writeCountString($this->numAssertions, 'Assertions', $color, true); - $this->writeCountString($result->errorCount(), 'Errors', $color); - $this->writeCountString($result->failureCount(), 'Failures', $color); - $this->writeCountString($result->warningCount(), 'Warnings', $color); - $this->writeCountString($result->skippedCount(), 'Skipped', $color); - $this->writeCountString($result->notImplementedCount(), 'Incomplete', $color); - $this->writeCountString($result->riskyCount(), 'Risky', $color); - $this->writeWithColor($color, '.'); - } - } - - protected function writeProgress(string $progress): void - { - if ($this->debug) { - return; - } - - $this->write($progress); - $this->column++; - $this->numTestsRun++; - - if ($this->column == $this->maxColumn || $this->numTestsRun == $this->numTests) { - if ($this->numTestsRun == $this->numTests) { - $this->write(\str_repeat(' ', $this->maxColumn - $this->column)); - } - - $this->write( - \sprintf( - ' %' . $this->numTestsWidth . 'd / %' . - $this->numTestsWidth . 'd (%3s%%)', - $this->numTestsRun, - $this->numTests, - \floor(($this->numTestsRun / $this->numTests) * 100) - ) - ); - - if ($this->column == $this->maxColumn) { - $this->writeNewLine(); - } - } - } - - protected function writeNewLine(): void - { - $this->column = 0; - $this->write("\n"); - } - - /** - * Formats a buffer with a specified ANSI color sequence if colors are - * enabled. - */ - protected function colorizeTextBox(string $color, string $buffer): string - { - if (!$this->colors) { - return $buffer; - } - - $lines = \preg_split('/\r\n|\r|\n/', $buffer); - $padding = \max(\array_map('\strlen', $lines)); - - $styledLines = []; - - foreach ($lines as $line) { - $styledLines[] = Color::colorize($color, \str_pad($line, $padding)); - } - - return \implode(\PHP_EOL, $styledLines); - } - - /** - * Writes a buffer out with a color sequence if colors are enabled. - */ - protected function writeWithColor(string $color, string $buffer, bool $lf = true): void - { - $this->write($this->colorizeTextBox($color, $buffer)); - - if ($lf) { - $this->write(\PHP_EOL); - } - } - - /** - * Writes progress with a color sequence if colors are enabled. - */ - protected function writeProgressWithColor(string $color, string $buffer): void - { - $buffer = $this->colorizeTextBox($color, $buffer); - $this->writeProgress($buffer); - } - - private function writeCountString(int $count, string $name, string $color, bool $always = false): void - { - static $first = true; - - if ($always || $count > 0) { - $this->writeWithColor( - $color, - \sprintf( - '%s%s: %d', - !$first ? ', ' : '', - $name, - $count - ), - false - ); + public function printResult(TestResult $result): void; - $first = false; - } - } + public function write(string $buffer): void; } diff --git a/src/TextUI/TestRunner.php b/src/TextUI/TestRunner.php index 108bbcf054e..30b15bc6da0 100644 --- a/src/TextUI/TestRunner.php +++ b/src/TextUI/TestRunner.php @@ -83,7 +83,7 @@ final class TestRunner extends BaseTestRunner private $loader; /** - * @var Printer&TestListener + * @var ResultPrinter */ private $printer; @@ -269,11 +269,16 @@ public function doRun(Test $suite, array $arguments = [], bool $exit = true): Te if ($this->printer === null) { if (isset($arguments['printer'])) { - if ($arguments['printer'] instanceof Printer && $arguments['printer'] instanceof TestListener) { + if ($arguments['printer'] instanceof ResultPrinter) { $this->printer = $arguments['printer']; } elseif (\is_string($arguments['printer']) && \class_exists($arguments['printer'], false)) { try { - new \ReflectionClass($arguments['printer']); + $reflector = new \ReflectionClass($arguments['printer']); + + if ($reflector->implementsInterface(ResultPrinter::class)) { + $this->printer = $this->createPrinter($arguments['printer'], $arguments); + } + // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new Exception( @@ -283,13 +288,9 @@ public function doRun(Test $suite, array $arguments = [], bool $exit = true): Te ); } // @codeCoverageIgnoreEnd - - if (\is_subclass_of($arguments['printer'], ResultPrinter::class)) { - $this->printer = $this->createPrinter($arguments['printer'], $arguments); - } } } else { - $this->printer = $this->createPrinter(ResultPrinter::class, $arguments); + $this->printer = $this->createPrinter(DefaultResultPrinter::class, $arguments); } } @@ -630,10 +631,7 @@ public function doRun(Test $suite, array $arguments = [], bool $exit = true): Te } $result->flushListeners(); - - if ($this->printer instanceof ResultPrinter) { - $this->printer->printResult($result); - } + $this->printer->printResult($result); if (isset($codeCoverage)) { if (isset($arguments['coverageClover'])) { @@ -1160,7 +1158,7 @@ private function handleConfiguration(array &$arguments): void } if (isset($loggingConfiguration['plain'])) { - $arguments['listeners'][] = new ResultPrinter( + $arguments['listeners'][] = new DefaultResultPrinter( $loggingConfiguration['plain'], true ); @@ -1206,7 +1204,7 @@ private function handleConfiguration(array &$arguments): void $arguments['beStrictAboutResourceUsageDuringSmallTests'] = $arguments['beStrictAboutResourceUsageDuringSmallTests'] ?? false; $arguments['cacheResult'] = $arguments['cacheResult'] ?? true; $arguments['cacheTokens'] = $arguments['cacheTokens'] ?? false; - $arguments['colors'] = $arguments['colors'] ?? ResultPrinter::COLOR_DEFAULT; + $arguments['colors'] = $arguments['colors'] ?? DefaultResultPrinter::COLOR_DEFAULT; $arguments['columns'] = $arguments['columns'] ?? 80; $arguments['convertDeprecationsToExceptions'] = $arguments['convertDeprecationsToExceptions'] ?? true; $arguments['convertErrorsToExceptions'] = $arguments['convertErrorsToExceptions'] ?? true; @@ -1301,16 +1299,9 @@ private function writeMessage(string $type, string $message): void $this->messagePrinted = true; } - /** - * @template T as Printer - * - * @param class-string $class - * - * @return T - */ - private function createPrinter(string $class, array $arguments): Printer + private function createPrinter(string $class, array $arguments): ResultPrinter { - return new $class( + $object = new $class( (isset($arguments['stderr']) && $arguments['stderr'] === true) ? 'php://stderr' : null, $arguments['verbose'], $arguments['colors'], @@ -1318,6 +1309,10 @@ private function createPrinter(string $class, array $arguments): Printer $arguments['columns'], $arguments['reverseList'] ); + + \assert($object instanceof ResultPrinter); + + return $object; } private function codeCoverageGenerationStart(string $format): void diff --git a/src/Util/Configuration.php b/src/Util/Configuration.php index d2f555ed79a..f3777eb1add 100644 --- a/src/Util/Configuration.php +++ b/src/Util/Configuration.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\Exception; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\TestSuiteSorter; -use PHPUnit\TextUI\ResultPrinter; +use PHPUnit\TextUI\DefaultResultPrinter; use PHPUnit\Util\TestDox\CliTestDoxPrinter; use SebastianBergmann\FileIterator\Facade as FileIteratorFacade; @@ -473,9 +473,9 @@ public function getPHPUnitConfiguration(): array /* only allow boolean for compatibility with previous versions 'always' only allowed from command line */ if ($this->getBoolean($root->getAttribute('colors'), false)) { - $result['colors'] = ResultPrinter::COLOR_AUTO; + $result['colors'] = DefaultResultPrinter::COLOR_AUTO; } else { - $result['colors'] = ResultPrinter::COLOR_NEVER; + $result['colors'] = DefaultResultPrinter::COLOR_NEVER; } } diff --git a/src/Util/Log/TeamCity.php b/src/Util/Log/TeamCity.php index 74b7d4ee27c..45d74c2586e 100644 --- a/src/Util/Log/TeamCity.php +++ b/src/Util/Log/TeamCity.php @@ -18,7 +18,7 @@ use PHPUnit\Framework\TestResult; use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\Warning; -use PHPUnit\TextUI\ResultPrinter; +use PHPUnit\TextUI\DefaultResultPrinter; use PHPUnit\Util\Exception; use PHPUnit\Util\Filter; use SebastianBergmann\Comparator\ComparisonFailure; @@ -26,7 +26,7 @@ /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ -final class TeamCity extends ResultPrinter +final class TeamCity extends DefaultResultPrinter { /** * @var bool diff --git a/src/Util/Printer.php b/src/Util/Printer.php index 309fda3d978..b36d1656c3e 100644 --- a/src/Util/Printer.php +++ b/src/Util/Printer.php @@ -12,7 +12,7 @@ /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ -abstract class Printer +class Printer { /** * @var resource diff --git a/src/Util/TestDox/HtmlResultPrinter.php b/src/Util/TestDox/HtmlResultPrinter.php index 5dcef142978..d063b5b9106 100644 --- a/src/Util/TestDox/HtmlResultPrinter.php +++ b/src/Util/TestDox/HtmlResultPrinter.php @@ -9,6 +9,8 @@ */ namespace PHPUnit\Util\TestDox; +use PHPUnit\Framework\TestResult; + /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ @@ -76,6 +78,10 @@ final class HtmlResultPrinter extends ResultPrinter EOT; + public function printResult(TestResult $result): void + { + } + /** * Handler for 'start run' event. */ diff --git a/src/Util/TestDox/ResultPrinter.php b/src/Util/TestDox/ResultPrinter.php index c679912821e..d604b42d769 100644 --- a/src/Util/TestDox/ResultPrinter.php +++ b/src/Util/TestDox/ResultPrinter.php @@ -12,17 +12,17 @@ use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\Warning; use PHPUnit\Framework\WarningTestCase; use PHPUnit\Runner\BaseTestRunner; +use PHPUnit\TextUI\ResultPrinter as ResultPrinterInterface; use PHPUnit\Util\Printer; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ -abstract class ResultPrinter extends Printer implements TestListener +abstract class ResultPrinter extends Printer implements ResultPrinterInterface { /** * @var NamePrettifier diff --git a/src/Util/TestDox/TestDoxPrinter.php b/src/Util/TestDox/TestDoxPrinter.php index 7f6f376e77b..1d6855c0026 100644 --- a/src/Util/TestDox/TestDoxPrinter.php +++ b/src/Util/TestDox/TestDoxPrinter.php @@ -18,12 +18,12 @@ use PHPUnit\Runner\BaseTestRunner; use PHPUnit\Runner\PhptTestCase; use PHPUnit\Runner\TestSuiteSorter; -use PHPUnit\TextUI\ResultPrinter; +use PHPUnit\TextUI\DefaultResultPrinter; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ -class TestDoxPrinter extends ResultPrinter +class TestDoxPrinter extends DefaultResultPrinter { /** * @var NamePrettifier diff --git a/src/Util/TestDox/TextResultPrinter.php b/src/Util/TestDox/TextResultPrinter.php index 3732d8e97fe..8a1893e55ca 100644 --- a/src/Util/TestDox/TextResultPrinter.php +++ b/src/Util/TestDox/TextResultPrinter.php @@ -9,11 +9,17 @@ */ namespace PHPUnit\Util\TestDox; +use PHPUnit\Framework\TestResult; + /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TextResultPrinter extends ResultPrinter { + public function printResult(TestResult $result): void + { + } + /** * Handler for 'start class' event. */ diff --git a/tests/_files/CustomPrinter.php b/tests/_files/CustomPrinter.php index 3b89cb40a8e..041afae7c26 100644 --- a/tests/_files/CustomPrinter.php +++ b/tests/_files/CustomPrinter.php @@ -7,8 +7,9 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -use PHPUnit\TextUI\ResultPrinter; +use PHPUnit\TextUI\DefaultResultPrinter; -class CustomPrinter extends ResultPrinter +/** @todo */ +class CustomPrinter extends DefaultResultPrinter { } diff --git a/tests/end-to-end/_files/NullPrinter.php b/tests/end-to-end/_files/NullPrinter.php index 75aa7892a10..171cd71922a 100644 --- a/tests/end-to-end/_files/NullPrinter.php +++ b/tests/end-to-end/_files/NullPrinter.php @@ -11,9 +11,18 @@ use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestListenerDefaultImplementation; -use PHPUnit\Util\Printer; +use PHPUnit\Framework\TestResult; +use PHPUnit\TextUI\ResultPrinter; -final class NullPrinter extends Printer implements TestListener +final class NullPrinter implements ResultPrinter, TestListener { use TestListenerDefaultImplementation; + + public function printResult(TestResult $result): void + { + } + + public function write(string $buffer): void + { + } } diff --git a/tests/end-to-end/loggers/hooks.phpt b/tests/end-to-end/loggers/hooks.phpt index cffc6ebe2ef..95776197441 100644 --- a/tests/end-to-end/loggers/hooks.phpt +++ b/tests/end-to-end/loggers/hooks.phpt @@ -12,8 +12,6 @@ $arguments = [ require __DIR__ . '/../../bootstrap.php'; PHPUnit\TextUI\Command::main(); --EXPECTF-- -PHPUnit %s by Sebastian Bergmann and contributors. - PHPUnit\Test\Extension::tellAmountOfInjectedArguments: %d PHPUnit\Test\Extension::executeBeforeFirstTest PHPUnit\Test\Extension::executeBeforeTest: PHPUnit\Test\HookTest::testSuccess diff --git a/tests/unit/Util/ConfigurationTest.php b/tests/unit/Util/ConfigurationTest.php index b88faf8c797..4e3be6f5649 100644 --- a/tests/unit/Util/ConfigurationTest.php +++ b/tests/unit/Util/ConfigurationTest.php @@ -12,7 +12,7 @@ use PHPUnit\Framework\Exception; use PHPUnit\Framework\TestCase; use PHPUnit\Runner\TestSuiteSorter; -use PHPUnit\TextUI\ResultPrinter; +use PHPUnit\TextUI\DefaultResultPrinter; use PHPUnit\Util\TestDox\CliTestDoxPrinter; /** @@ -45,7 +45,7 @@ public function testShouldReadColorsWhenTrueInConfigurationFile(): void $configurationInstance = Configuration::getInstance($configurationFilename); $configurationValues = $configurationInstance->getPHPUnitConfiguration(); - $this->assertEquals(ResultPrinter::COLOR_AUTO, $configurationValues['colors']); + $this->assertEquals(DefaultResultPrinter::COLOR_AUTO, $configurationValues['colors']); } public function testShouldReadColorsWhenFalseInConfigurationFile(): void @@ -54,7 +54,7 @@ public function testShouldReadColorsWhenFalseInConfigurationFile(): void $configurationInstance = Configuration::getInstance($configurationFilename); $configurationValues = $configurationInstance->getPHPUnitConfiguration(); - $this->assertEquals(ResultPrinter::COLOR_NEVER, $configurationValues['colors']); + $this->assertEquals(DefaultResultPrinter::COLOR_NEVER, $configurationValues['colors']); } public function testShouldReadColorsWhenEmptyInConfigurationFile(): void @@ -63,7 +63,7 @@ public function testShouldReadColorsWhenEmptyInConfigurationFile(): void $configurationInstance = Configuration::getInstance($configurationFilename); $configurationValues = $configurationInstance->getPHPUnitConfiguration(); - $this->assertEquals(ResultPrinter::COLOR_NEVER, $configurationValues['colors']); + $this->assertEquals(DefaultResultPrinter::COLOR_NEVER, $configurationValues['colors']); } public function testShouldReadColorsWhenInvalidInConfigurationFile(): void @@ -72,7 +72,7 @@ public function testShouldReadColorsWhenInvalidInConfigurationFile(): void $configurationInstance = Configuration::getInstance($configurationFilename); $configurationValues = $configurationInstance->getPHPUnitConfiguration(); - $this->assertEquals(ResultPrinter::COLOR_NEVER, $configurationValues['colors']); + $this->assertEquals(DefaultResultPrinter::COLOR_NEVER, $configurationValues['colors']); } public function testInvalidConfigurationGeneratesValidationErrors(): void diff --git a/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php b/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php index a2b433bf5df..15eb17ec4c2 100644 --- a/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php +++ b/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; +use PHPUnit\TextUI\DefaultResultPrinter; use PHPUnit\Util\Color; /** @@ -26,12 +27,12 @@ final class CliTestDoxPrinterColorTest extends TestCase protected function setUp(): void { - $this->printer = new TestableCliTestDoxPrinter(null, true, \PHPUnit\TextUI\ResultPrinter::COLOR_ALWAYS); + $this->printer = new TestableCliTestDoxPrinter(null, true, DefaultResultPrinter::COLOR_ALWAYS); } protected function tearDown(): void { - $this->printer = null; + $this->printer = null; } public function testColorizesDiffInFailureMessage(): void