From e0f9d0bccf11e160e4ae74829e2ff602ae4a34bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Wed, 24 Jan 2024 21:33:40 +0100 Subject: [PATCH] just override the phpunit file --- overrides/Logging/JUnit/JunitXmlLogger.php | 441 ++++++++++++++++++++- 1 file changed, 436 insertions(+), 5 deletions(-) diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index bc0e0953..a2aaa42d 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -1,16 +1,447 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace PHPUnit\Logging\JUnit; +use function assert; +use function basename; +use function is_int; +use function sprintf; +use function str_replace; +use function trim; +use DOMDocument; +use DOMElement; +use PHPUnit\Event\Code\Test; +use PHPUnit\Event\Code\TestMethod; +use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; +use PHPUnit\Event\InvalidArgumentException; +use PHPUnit\Event\Telemetry\HRTime; +use PHPUnit\Event\Telemetry\Info; +use PHPUnit\Event\Test\Errored; +use PHPUnit\Event\Test\Failed; +use PHPUnit\Event\Test\Finished; +use PHPUnit\Event\Test\MarkedIncomplete; +use PHPUnit\Event\Test\PreparationStarted; +use PHPUnit\Event\Test\Prepared; +use PHPUnit\Event\Test\Skipped; +use PHPUnit\Event\TestSuite\Started; +use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\TextUI\Output\Printer; +use PHPUnit\Util\Xml; +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ final class JunitXmlLogger { + private readonly Printer $printer; + private readonly \Pest\Logging\Converter $converter; // pest-added + private DOMDocument $document; + private DOMElement $root; + + /** + * @var DOMElement[] + */ + private array $testSuites = []; + + /** + * @psalm-var array + */ + private array $testSuiteTests = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteAssertions = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteErrors = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteFailures = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteSkipped = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteTimes = [0]; + private int $testSuiteLevel = 0; + private ?DOMElement $currentTestCase = null; + private ?HRTime $time = null; + private bool $prepared = false; + private bool $preparationFailed = false; + + /** + * @throws EventFacadeIsSealedException + * @throws UnknownSubscriberTypeException + */ public function __construct(Printer $printer, Facade $facade) { - /** @see \Pest\Logging\JUnit\JUnitLogger */ + $this->printer = $printer; + $this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added + + $this->registerSubscribers($facade); + $this->createDocument(); + } + + public function flush(): void + { + $this->printer->print($this->document->saveXML()); + + $this->printer->flush(); + } + + public function testSuiteStarted(Started $event): void + { + $testSuite = $this->document->createElement('testsuite'); + $testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); // pest-changed + + if ($event->testSuite()->isForTestClass()) { + $testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); // pest-changed + } + + 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->testSuiteFailures[$this->testSuiteLevel] = 0; + $this->testSuiteSkipped[$this->testSuiteLevel] = 0; + $this->testSuiteTimes[$this->testSuiteLevel] = 0; + } + + public function testSuiteFinished(): 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( + '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->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--; + } + + /** + * @throws InvalidArgumentException + */ + public function testPreparationStarted(PreparationStarted $event): void + { + $this->createTestCase($event); + } + + /** + * @throws InvalidArgumentException + */ + public function testPreparationFailed(): void + { + $this->preparationFailed = true; + } + + /** + * @throws InvalidArgumentException + */ + public function testPrepared(): void + { + $this->prepared = true; + } + + /** + * @throws InvalidArgumentException + */ + public function testFinished(Finished $event): void + { + if ($this->preparationFailed) { + return; + } + + $this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed()); + } + + /** + * @throws InvalidArgumentException + */ + public function testMarkedIncomplete(MarkedIncomplete $event): void + { + $this->handleIncompleteOrSkipped($event); + } + + /** + * @throws InvalidArgumentException + */ + public function testSkipped(Skipped $event): void + { + $this->handleIncompleteOrSkipped($event); + } + + /** + * @throws InvalidArgumentException + */ + public function testErrored(Errored $event): void + { + $this->handleFault($event, 'error'); + + $this->testSuiteErrors[$this->testSuiteLevel]++; + } + + /** + * @throws InvalidArgumentException + */ + public function testFailed(Failed $event): void + { + $this->handleFault($event, 'failure'); + + $this->testSuiteFailures[$this->testSuiteLevel]++; + } + + /** + * @throws InvalidArgumentException + */ + private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void + { + assert($this->currentTestCase !== null); + assert($this->time !== null); + + $time = $telemetryInfo->time()->duration($this->time)->asFloat(); + + $this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed; + + $this->currentTestCase->setAttribute( + 'assertions', + (string) $numberOfAssertionsPerformed, + ); + + $this->currentTestCase->setAttribute( + 'time', + sprintf('%F', $time), + ); + + $this->testSuites[$this->testSuiteLevel]->appendChild( + $this->currentTestCase, + ); + + $this->testSuiteTests[$this->testSuiteLevel]++; + $this->testSuiteTimes[$this->testSuiteLevel] += $time; + + $this->currentTestCase = null; + $this->time = null; + $this->prepared = false; + } + + /** + * @throws EventFacadeIsSealedException + * @throws UnknownSubscriberTypeException + */ + private function registerSubscribers(Facade $facade): void + { + $facade->registerSubscribers( + new TestSuiteStartedSubscriber($this), + new TestSuiteFinishedSubscriber($this), + new TestPreparationStartedSubscriber($this), + new TestPreparationFailedSubscriber($this), + new TestPreparedSubscriber($this), + new TestFinishedSubscriber($this), + new TestErroredSubscriber($this), + new TestFailedSubscriber($this), + new TestMarkedIncompleteSubscriber($this), + new TestSkippedSubscriber($this), + new TestRunnerExecutionFinishedSubscriber($this), + ); + } + + private function createDocument(): void + { + $this->document = new DOMDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + + $this->root = $this->document->createElement('testsuites'); + $this->document->appendChild($this->root); + } + + /** + * @throws InvalidArgumentException + */ + private function handleFault(Errored|Failed $event, string $type): void + { + if (!$this->prepared) { + $this->createTestCase($event); + } + + assert($this->currentTestCase !== null); + + $buffer = $this->converter->getTestCaseMethodName($event->test()); // pest-changed + + $throwable = $event->throwable(); + $buffer .= trim( + $this->converter->getExceptionMessage($throwable) . PHP_EOL . // pest-changed + $this->converter->getExceptionDetails($throwable), // pest-changed + ); + + $fault = $this->document->createElement( + $type, + Xml::prepareString($buffer), + ); + + $fault->setAttribute('type', $throwable->className()); + + $this->currentTestCase->appendChild($fault); + + if (!$this->prepared) { + $this->handleFinish($event->telemetryInfo(), 0); + } + } + + /** + * @throws InvalidArgumentException + */ + private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void + { + if (!$this->prepared) { + $this->createTestCase($event); + } + + assert($this->currentTestCase !== null); + + $skipped = $this->document->createElement('skipped'); + + $this->currentTestCase->appendChild($skipped); + + $this->testSuiteSkipped[$this->testSuiteLevel]++; + + if (!$this->prepared) { + $this->handleFinish($event->telemetryInfo(), 0); + } + } + + /** + * @throws InvalidArgumentException + */ + private function testAsString(Test $test): string + { + if ($test->isPhpt()) { + return basename($test->file()); + } + + assert($test instanceof TestMethod); + + return sprintf( + '%s::%s%s', + $test->className(), + $this->name($test), + PHP_EOL, + ); + } + + /** + * @throws InvalidArgumentException + */ + private function name(Test $test): string + { + if ($test->isPhpt()) { + return basename($test->file()); + } + + assert($test instanceof TestMethod); + + if (!$test->testData()->hasDataFromDataProvider()) { + return $test->methodName(); + } + + $dataSetName = $test->testData()->dataFromDataProvider()->dataSetName(); + + if (is_int($dataSetName)) { + return sprintf( + '%s with data set #%d', + $test->methodName(), + $dataSetName, + ); + } + + return sprintf( + '%s with data set "%s"', + $test->methodName(), + $dataSetName, + ); + } + + /** + * @throws InvalidArgumentException + * + * @psalm-assert !null $this->currentTestCase + */ + private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void + { + $testCase = $this->document->createElement('testcase'); + + $test = $event->test(); + $file = $this->converter->getTestCaseLocation($test); // pest-added + + $testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); // pest-changed + $testCase->setAttribute('file', $file); // pest-changed + + if ($test->isTestMethod()) { + assert($test instanceof TestMethod); + + //$testCase->setAttribute('line', (string) $test->line()); // pest-removed + $className = $this->converter->getTrimmedTestClassName($test); // pest-added + $testCase->setAttribute('class', $className); // pest-changed + $testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed + } + + $this->currentTestCase = $testCase; + $this->time = $event->telemetryInfo()->time(); } }