<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\TestDox;
use const PHP_EOL;
use function array_map;
use function get_class;
use function implode;
use function method_exists;
use function preg_split;
use function trim;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Reorderable;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\Runner\BaseTestRunner;
use PHPUnit\Runner\PhptTestCase;
use PHPUnit\TextUI\DefaultResultPrinter;
use Throwable;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
class TestDoxPrinter extends DefaultResultPrinter
{
/**
* @var NamePrettifier
*/
protected $prettifier;
/**
* @var int The number of test results received from the TestRunner
*/
protected $testIndex = 0;
/**
* @var int The number of test results already sent to the output
*/
protected $testFlushIndex = 0;
/**
* @var array<int, array> Buffer for test results
*/
protected $testResults = [];
/**
* @var array<string, int> Lookup table for testname to testResults[index]
*/
protected $testNameResultIndex = [];
/**
* @var bool
*/
protected $enableOutputBuffer = false;
/**
* @var array array<string>
*/
protected $originalExecutionOrder = [];
/**
* @var int
*/
protected $spinState = 0;
/**
* @var bool
*/
protected $showProgress = true;
/**
* @param null|resource|string $out
* @param int|string $numberOfColumns
*
* @throws \PHPUnit\Framework\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, $verbose, $colors, $debug, $numberOfColumns, $reverse);
$this->prettifier = new NamePrettifier($this->colors);
}
public function setOriginalExecutionOrder(array $order): void
{
$this->originalExecutionOrder = $order;
$this->enableOutputBuffer = !empty($order);
}
public function setShowProgressAnimation(bool $showProgress): void
{
$this->showProgress = $showProgress;
}
public function printResult(TestResult $result): void
{
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function endTest(Test $test, float $time): void
{
if (!$test instanceof TestCase && !$test instanceof PhptTestCase && !$test instanceof TestSuite) {
return;
}
if ($this->testHasPassed()) {
$this->registerTestResult($test, null, BaseTestRunner::STATUS_PASSED, $time, false);
}
if ($test instanceof TestCase || $test instanceof PhptTestCase) {
$this->testIndex++;
}
parent::endTest($test, $time);
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function addError(Test $test, Throwable $t, float $time): void
{
$this->registerTestResult($test, $t, BaseTestRunner::STATUS_ERROR, $time, true);
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
$this->registerTestResult($test, $e, BaseTestRunner::STATUS_WARNING, $time, true);
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->registerTestResult($test, $e, BaseTestRunner::STATUS_FAILURE, $time, true);
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
{
$this->registerTestResult($test, $t, BaseTestRunner::STATUS_INCOMPLETE, $time, false);
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function addRiskyTest(Test $test, Throwable $t, float $time): void
{
$this->registerTestResult($test, $t, BaseTestRunner::STATUS_RISKY, $time, false);
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function addSkippedTest(Test $test, Throwable $t, float $time): void
{
$this->registerTestResult($test, $t, BaseTestRunner::STATUS_SKIPPED, $time, false);
}
public function writeProgress(string $progress): void
{
$this->flushOutputBuffer();
}
public function flush(): void
{
$this->flushOutputBuffer(true);
}
/**
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
protected function registerTestResult(Test $test, ?Throwable $t, int $status, float $time, bool $verbose): void
{
$testName = $test instanceof Reorderable ? $test->sortId() : $test->getName();
$result = [
'className' => $this->formatClassName($test),
'testName' => $testName,
'testMethod' => $this->formatTestName($test),
'message' => '',
'status' => $status,
'time' => $time,
'verbose' => $verbose,
];
if ($t !== null) {
$result['message'] = $this->formatTestResultMessage($t, $result);
}
$this->testResults[$this->testIndex] = $result;
$this->testNameResultIndex[$testName] = $this->testIndex;
}
protected function formatTestName(Test $test): string
{
return method_exists($test, 'getName') ? $test->getName() : '';
}
protected function formatClassName(Test $test): string
{
return get_class($test);
}
protected function testHasPassed(): bool
{
if (!isset($this->testResults[$this->testIndex]['status'])) {
return true;
}
if ($this->testResults[$this->testIndex]['status'] === BaseTestRunner::STATUS_PASSED) {
return true;
}
return false;
}
protected function flushOutputBuffer(bool $forceFlush = false): void
{
if ($this->testFlushIndex === $this->testIndex) {
return;
}
if ($this->testFlushIndex > 0) {
if ($this->enableOutputBuffer &&
isset($this->originalExecutionOrder[$this->testFlushIndex - 1])) {
$prevResult = $this->getTestResultByName($this->originalExecutionOrder[$this->testFlushIndex - 1]);
} else {
$prevResult = $this->testResults[$this->testFlushIndex - 1];
}
} else {
$prevResult = $this->getEmptyTestResult();
}
if (!$this->enableOutputBuffer) {
$this->writeTestResult($prevResult, $this->testResults[$this->testFlushIndex++]);
} else {
do {
$flushed = false;
if (!$forceFlush && isset($this->originalExecutionOrder[$this->testFlushIndex])) {
$result = $this->getTestResultByName($this->originalExecutionOrder[$this->testFlushIndex]);
} else {
// This test(name) cannot found in original execution order,
// flush result to output stream right away
$result = $this->testResults[$this->testFlushIndex];
}
if (!empty($result)) {
$this->hideSpinner();
$this->writeTestResult($prevResult, $result);
$this->testFlushIndex++;
$prevResult = $result;
$flushed = true;
} else {
$this->showSpinner();
}
} while ($flushed && $this->testFlushIndex < $this->testIndex);
}
}
protected function showSpinner(): void
{
if (!$this->showProgress) {
return;
}
if ($this->spinState) {
$this->undrawSpinner();
}
$this->spinState++;
$this->drawSpinner();
}
protected function hideSpinner(): void
{
if (!$this->showProgress) {
return;
}
if ($this->spinState) {
$this->undrawSpinner();
}
$this->spinState = 0;
}
protected function drawSpinner(): void
{
// optional for CLI printers: show the user a 'buffering output' spinner
}
protected function undrawSpinner(): void
{
// remove the spinner from the current line
}
protected function writeTestResult(array $prevResult, array $result): void
{
}
protected function getEmptyTestResult(): array
{
return [
'className' => '',
'testName' => '',
'message' => '',
'failed' => '',
'verbose' => '',
];
}
protected function getTestResultByName(?string $testName): array
{
if (isset($this->testNameResultIndex[$testName])) {
return $this->testResults[$this->testNameResultIndex[$testName]];
}
return [];
}
protected function formatThrowable(Throwable $t, ?int $status = null): string
{
$message = trim(\PHPUnit\Framework\TestFailure::exceptionToString($t));
if ($message) {
$message .= PHP_EOL . PHP_EOL . $this->formatStacktrace($t);
} else {
$message = $this->formatStacktrace($t);
}
return $message;
}
protected function formatStacktrace(Throwable $t): string
{
return \PHPUnit\Util\Filter::getFilteredStacktrace($t);
}
protected function formatTestResultMessage(Throwable $t, array $result, string $prefix = '│'): string
{
$message = $this->formatThrowable($t, $result['status']);
if ($message === '') {
return '';
}
if (!($this->verbose || $result['verbose'])) {
return '';
}
return $this->prefixLines($prefix, $message);
}
protected function prefixLines(string $prefix, string $message): string
{
$message = trim($message);
return implode(
PHP_EOL,
array_map(
static function (string $text) use ($prefix)
{
return ' ' . $prefix . ($text ? ' ' . $text : '');
},
preg_split('/\r\n|\r|\n/', $message)
)
);
}
}