You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
586 lines
15 KiB
586 lines
15 KiB
<?php declare(strict_types=1);
|
|
/*
|
|
* This file is part of PHPUnit.
|
|
*
|
|
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
namespace PHPUnit\TextUI;
|
|
|
|
use const PHP_EOL;
|
|
use function array_map;
|
|
use function array_reverse;
|
|
use function count;
|
|
use function floor;
|
|
use function implode;
|
|
use function in_array;
|
|
use function is_int;
|
|
use function max;
|
|
use function preg_split;
|
|
use function sprintf;
|
|
use function str_pad;
|
|
use function str_repeat;
|
|
use function strlen;
|
|
use function trim;
|
|
use function vsprintf;
|
|
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\ResourceUsageFormatter;
|
|
use SebastianBergmann\Timer\Timer;
|
|
use Throwable;
|
|
|
|
/**
|
|
* @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;
|
|
|
|
/**
|
|
* @var Timer
|
|
*/
|
|
private $timer;
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
$this->timer = new Timer;
|
|
|
|
$this->timer->start();
|
|
}
|
|
|
|
public function printResult(TestResult $result): void
|
|
{
|
|
$this->printHeader($result);
|
|
$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" . trim((string) $e) . "\n");
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
protected function printHeader(TestResult $result): void
|
|
{
|
|
if (count($result) > 0) {
|
|
$this->write(PHP_EOL . PHP_EOL . (new ResourceUsageFormatter)->resourceUsage($this->timer->stop()) . PHP_EOL . PHP_EOL);
|
|
}
|
|
}
|
|
|
|
protected function printFooter(TestResult $result): void
|
|
{
|
|
if (count($result) === 0) {
|
|
$this->writeWithColor(
|
|
'fg-black, bg-yellow',
|
|
'No tests executed!',
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
if ($result->wasSuccessfulAndNoTestIsRiskyOrSkippedOrIncomplete()) {
|
|
$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',
|
|
),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$color = 'fg-black, bg-yellow';
|
|
|
|
if ($result->wasSuccessful()) {
|
|
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;
|
|
}
|
|
}
|
|
}
|