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