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.
425 lines
11 KiB
425 lines
11 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\Util\Log;
|
|
|
|
use function class_exists;
|
|
use function get_class;
|
|
use function method_exists;
|
|
use function sprintf;
|
|
use function str_replace;
|
|
use function trim;
|
|
use DOMDocument;
|
|
use DOMElement;
|
|
use PHPUnit\Framework\AssertionFailedError;
|
|
use PHPUnit\Framework\ExceptionWrapper;
|
|
use PHPUnit\Framework\SelfDescribing;
|
|
use PHPUnit\Framework\Test;
|
|
use PHPUnit\Framework\TestFailure;
|
|
use PHPUnit\Framework\TestListener;
|
|
use PHPUnit\Framework\TestSuite;
|
|
use PHPUnit\Framework\Warning;
|
|
use PHPUnit\Util\Exception;
|
|
use PHPUnit\Util\Filter;
|
|
use PHPUnit\Util\Printer;
|
|
use PHPUnit\Util\Xml;
|
|
use ReflectionClass;
|
|
use ReflectionException;
|
|
use Throwable;
|
|
|
|
/**
|
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
|
*/
|
|
final class JUnit extends Printer implements TestListener
|
|
{
|
|
/**
|
|
* @var DOMDocument
|
|
*/
|
|
private $document;
|
|
|
|
/**
|
|
* @var DOMElement
|
|
*/
|
|
private $root;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private $reportRiskyTests = false;
|
|
|
|
/**
|
|
* @var DOMElement[]
|
|
*/
|
|
private $testSuites = [];
|
|
|
|
/**
|
|
* @var int[]
|
|
*/
|
|
private $testSuiteTests = [0];
|
|
|
|
/**
|
|
* @var int[]
|
|
*/
|
|
private $testSuiteAssertions = [0];
|
|
|
|
/**
|
|
* @var int[]
|
|
*/
|
|
private $testSuiteErrors = [0];
|
|
|
|
/**
|
|
* @var int[]
|
|
*/
|
|
private $testSuiteWarnings = [0];
|
|
|
|
/**
|
|
* @var int[]
|
|
*/
|
|
private $testSuiteFailures = [0];
|
|
|
|
/**
|
|
* @var int[]
|
|
*/
|
|
private $testSuiteSkipped = [0];
|
|
|
|
/**
|
|
* @var int[]
|
|
*/
|
|
private $testSuiteTimes = [0];
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
private $testSuiteLevel = 0;
|
|
|
|
/**
|
|
* @var DOMElement
|
|
*/
|
|
private $currentTestCase;
|
|
|
|
/**
|
|
* @param null|mixed $out
|
|
*/
|
|
public function __construct($out = null, bool $reportRiskyTests = false)
|
|
{
|
|
$this->document = new DOMDocument('1.0', 'UTF-8');
|
|
$this->document->formatOutput = true;
|
|
|
|
$this->root = $this->document->createElement('testsuites');
|
|
$this->document->appendChild($this->root);
|
|
|
|
parent::__construct($out);
|
|
|
|
$this->reportRiskyTests = $reportRiskyTests;
|
|
}
|
|
|
|
/**
|
|
* Flush buffer and close output.
|
|
*/
|
|
public function flush(): void
|
|
{
|
|
$this->write($this->getXML());
|
|
|
|
parent::flush();
|
|
}
|
|
|
|
/**
|
|
* An error occurred.
|
|
*/
|
|
public function addError(Test $test, Throwable $t, float $time): void
|
|
{
|
|
$this->doAddFault($test, $t, 'error');
|
|
$this->testSuiteErrors[$this->testSuiteLevel]++;
|
|
}
|
|
|
|
/**
|
|
* A warning occurred.
|
|
*/
|
|
public function addWarning(Test $test, Warning $e, float $time): void
|
|
{
|
|
$this->doAddFault($test, $e, 'warning');
|
|
$this->testSuiteWarnings[$this->testSuiteLevel]++;
|
|
}
|
|
|
|
/**
|
|
* A failure occurred.
|
|
*/
|
|
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
|
{
|
|
$this->doAddFault($test, $e, 'failure');
|
|
$this->testSuiteFailures[$this->testSuiteLevel]++;
|
|
}
|
|
|
|
/**
|
|
* Incomplete test.
|
|
*/
|
|
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
|
|
{
|
|
$this->doAddSkipped();
|
|
}
|
|
|
|
/**
|
|
* Risky test.
|
|
*/
|
|
public function addRiskyTest(Test $test, Throwable $t, float $time): void
|
|
{
|
|
if (!$this->reportRiskyTests) {
|
|
return;
|
|
}
|
|
|
|
$this->doAddFault($test, $t, 'error');
|
|
$this->testSuiteErrors[$this->testSuiteLevel]++;
|
|
}
|
|
|
|
/**
|
|
* Skipped test.
|
|
*/
|
|
public function addSkippedTest(Test $test, Throwable $t, float $time): void
|
|
{
|
|
$this->doAddSkipped();
|
|
}
|
|
|
|
/**
|
|
* A testsuite started.
|
|
*/
|
|
public function startTestSuite(TestSuite $suite): void
|
|
{
|
|
$testSuite = $this->document->createElement('testsuite');
|
|
$testSuite->setAttribute('name', $suite->getName());
|
|
|
|
if (class_exists($suite->getName(), false)) {
|
|
try {
|
|
$class = new ReflectionClass($suite->getName());
|
|
|
|
$testSuite->setAttribute('file', $class->getFileName());
|
|
} catch (ReflectionException $e) {
|
|
}
|
|
}
|
|
|
|
if ($this->testSuiteLevel > 0) {
|
|
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
|
|
} else {
|
|
$this->root->appendChild($testSuite);
|
|
}
|
|
|
|
$this->testSuiteLevel++;
|
|
$this->testSuites[$this->testSuiteLevel] = $testSuite;
|
|
$this->testSuiteTests[$this->testSuiteLevel] = 0;
|
|
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
|
|
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
|
|
$this->testSuiteWarnings[$this->testSuiteLevel] = 0;
|
|
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
|
|
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
|
|
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
|
|
}
|
|
|
|
/**
|
|
* A testsuite ended.
|
|
*/
|
|
public function endTestSuite(TestSuite $suite): void
|
|
{
|
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
'tests',
|
|
(string) $this->testSuiteTests[$this->testSuiteLevel],
|
|
);
|
|
|
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
'assertions',
|
|
(string) $this->testSuiteAssertions[$this->testSuiteLevel],
|
|
);
|
|
|
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
'errors',
|
|
(string) $this->testSuiteErrors[$this->testSuiteLevel],
|
|
);
|
|
|
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
'warnings',
|
|
(string) $this->testSuiteWarnings[$this->testSuiteLevel],
|
|
);
|
|
|
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
'failures',
|
|
(string) $this->testSuiteFailures[$this->testSuiteLevel],
|
|
);
|
|
|
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
'skipped',
|
|
(string) $this->testSuiteSkipped[$this->testSuiteLevel],
|
|
);
|
|
|
|
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
|
'time',
|
|
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]),
|
|
);
|
|
|
|
if ($this->testSuiteLevel > 1) {
|
|
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
|
|
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
|
|
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
|
|
$this->testSuiteWarnings[$this->testSuiteLevel - 1] += $this->testSuiteWarnings[$this->testSuiteLevel];
|
|
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
|
|
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
|
|
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
|
|
}
|
|
|
|
$this->testSuiteLevel--;
|
|
}
|
|
|
|
/**
|
|
* A test started.
|
|
*/
|
|
public function startTest(Test $test): void
|
|
{
|
|
$usesDataprovider = false;
|
|
|
|
if (method_exists($test, 'usesDataProvider')) {
|
|
$usesDataprovider = $test->usesDataProvider();
|
|
}
|
|
|
|
$testCase = $this->document->createElement('testcase');
|
|
$testCase->setAttribute('name', $test->getName());
|
|
|
|
try {
|
|
$class = new ReflectionClass($test);
|
|
// @codeCoverageIgnoreStart
|
|
} catch (ReflectionException $e) {
|
|
throw new Exception(
|
|
$e->getMessage(),
|
|
$e->getCode(),
|
|
$e,
|
|
);
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
$methodName = $test->getName(!$usesDataprovider);
|
|
|
|
if ($class->hasMethod($methodName)) {
|
|
try {
|
|
$method = $class->getMethod($methodName);
|
|
// @codeCoverageIgnoreStart
|
|
} catch (ReflectionException $e) {
|
|
throw new Exception(
|
|
$e->getMessage(),
|
|
$e->getCode(),
|
|
$e,
|
|
);
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
$testCase->setAttribute('class', $class->getName());
|
|
$testCase->setAttribute('classname', str_replace('\\', '.', $class->getName()));
|
|
$testCase->setAttribute('file', $class->getFileName());
|
|
$testCase->setAttribute('line', (string) $method->getStartLine());
|
|
}
|
|
|
|
$this->currentTestCase = $testCase;
|
|
}
|
|
|
|
/**
|
|
* A test ended.
|
|
*/
|
|
public function endTest(Test $test, float $time): void
|
|
{
|
|
$numAssertions = 0;
|
|
|
|
if (method_exists($test, 'getNumAssertions')) {
|
|
$numAssertions = $test->getNumAssertions();
|
|
}
|
|
|
|
$this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions;
|
|
|
|
$this->currentTestCase->setAttribute(
|
|
'assertions',
|
|
(string) $numAssertions,
|
|
);
|
|
|
|
$this->currentTestCase->setAttribute(
|
|
'time',
|
|
sprintf('%F', $time),
|
|
);
|
|
|
|
$this->testSuites[$this->testSuiteLevel]->appendChild(
|
|
$this->currentTestCase,
|
|
);
|
|
|
|
$this->testSuiteTests[$this->testSuiteLevel]++;
|
|
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
|
|
|
|
$testOutput = '';
|
|
|
|
if (method_exists($test, 'hasOutput') && method_exists($test, 'getActualOutput')) {
|
|
$testOutput = $test->hasOutput() ? $test->getActualOutput() : '';
|
|
}
|
|
|
|
if (!empty($testOutput)) {
|
|
$systemOut = $this->document->createElement(
|
|
'system-out',
|
|
Xml::prepareString($testOutput),
|
|
);
|
|
|
|
$this->currentTestCase->appendChild($systemOut);
|
|
}
|
|
|
|
$this->currentTestCase = null;
|
|
}
|
|
|
|
/**
|
|
* Returns the XML as a string.
|
|
*/
|
|
public function getXML(): string
|
|
{
|
|
return $this->document->saveXML();
|
|
}
|
|
|
|
private function doAddFault(Test $test, Throwable $t, string $type): void
|
|
{
|
|
if ($this->currentTestCase === null) {
|
|
return;
|
|
}
|
|
|
|
if ($test instanceof SelfDescribing) {
|
|
$buffer = $test->toString() . "\n";
|
|
} else {
|
|
$buffer = '';
|
|
}
|
|
|
|
$buffer .= trim(
|
|
TestFailure::exceptionToString($t) . "\n" .
|
|
Filter::getFilteredStacktrace($t),
|
|
);
|
|
|
|
$fault = $this->document->createElement(
|
|
$type,
|
|
Xml::prepareString($buffer),
|
|
);
|
|
|
|
if ($t instanceof ExceptionWrapper) {
|
|
$fault->setAttribute('type', $t->getClassName());
|
|
} else {
|
|
$fault->setAttribute('type', get_class($t));
|
|
}
|
|
|
|
$this->currentTestCase->appendChild($fault);
|
|
}
|
|
|
|
private function doAddSkipped(): void
|
|
{
|
|
if ($this->currentTestCase === null) {
|
|
return;
|
|
}
|
|
|
|
$skipped = $this->document->createElement('skipped');
|
|
|
|
$this->currentTestCase->appendChild($skipped);
|
|
|
|
$this->testSuiteSkipped[$this->testSuiteLevel]++;
|
|
}
|
|
}
|