From e5dc6f0ae2dfe468960c3a1913b7ac17f990a4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Sun, 30 Jul 2023 08:35:00 +0200 Subject: [PATCH 1/5] junit support --- src/Bootstrappers/BootSubscribers.php | 1 + src/Logging/JUnit/Converter.php | 228 ++++++++++ src/Logging/JUnit/JUnitLogger.php | 398 ++++++++++++++++++ src/Logging/JUnit/Subscriber/Subscriber.php | 28 ++ .../Subscriber/TestErroredSubscriber.php | 19 + .../JUnit/Subscriber/TestFailedSubscriber.php | 19 + .../Subscriber/TestFinishedSubscriber.php | 19 + .../TestMarkedIncompleteSubscriber.php | 19 + .../Subscriber/TestPreparedSubscriber.php | 19 + .../TestRunnerExecutionFinishedSubscriber.php | 19 + .../Subscriber/TestSkippedSubscriber.php | 19 + .../TestSuiteFinishedSubscriber.php | 19 + .../Subscriber/TestSuiteStartedSubscriber.php | 19 + src/Subscribers/EnsureJunitEnabled.php | 48 +++ 14 files changed, 874 insertions(+) create mode 100644 src/Logging/JUnit/Converter.php create mode 100644 src/Logging/JUnit/JUnitLogger.php create mode 100644 src/Logging/JUnit/Subscriber/Subscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestErroredSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestFailedSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestFinishedSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestMarkedIncompleteSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestPreparedSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestRunnerExecutionFinishedSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestSkippedSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestSuiteFinishedSubscriber.php create mode 100644 src/Logging/JUnit/Subscriber/TestSuiteStartedSubscriber.php create mode 100644 src/Subscribers/EnsureJunitEnabled.php diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index 248b9dde..ad4ab7da 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -25,6 +25,7 @@ final class BootSubscribers implements Bootstrapper Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureKernelDumpIsFlushed::class, Subscribers\EnsureTeamCityEnabled::class, + Subscribers\EnsureJunitEnabled::class, ]; /** diff --git a/src/Logging/JUnit/Converter.php b/src/Logging/JUnit/Converter.php new file mode 100644 index 00000000..f417a59e --- /dev/null +++ b/src/Logging/JUnit/Converter.php @@ -0,0 +1,228 @@ +stateGenerator = new StateGenerator(); + } + + /** + * Gets the test case method name. + */ + public function getTestCaseMethodName(Test $test): string + { + if (! $test instanceof TestMethod) { + throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); + } + + return $test->testDox()->prettifiedMethodName(); + } + + /** + * Gets the test case location. + */ + public function getTestCaseLocation(Test $test, bool $withDescription = false): string + { + if (! $test instanceof TestMethod) { + throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); + } + + $path = $test->testDox()->prettifiedClassName(); + $relativePath = $this->toRelativePath($path); + + // TODO: Get the description without the dataset. + $description = $test->testDox()->prettifiedMethodName(); + + if (! $withDescription) { + return $relativePath; + } + + return "$relativePath::$description"; + } + + /** + * Gets the exception message. + */ + public function getExceptionMessage(Throwable $throwable): string + { + if (is_a($throwable->className(), FrameworkException::class, true)) { + return $throwable->message(); + } + + $buffer = $throwable->className(); + $throwableMessage = $throwable->message(); + + if ($throwableMessage !== '') { + $buffer .= ": $throwableMessage"; + } + + return $buffer; + } + + /** + * Gets the exception details. + */ + public function getExceptionDetails(Throwable $throwable): string + { + $buffer = $this->getStackTrace($throwable); + + while ($throwable->hasPrevious()) { + $throwable = $throwable->previous(); + + $buffer .= sprintf( + "\nCaused by\n%s\n%s", + $throwable->description(), + $this->getStackTrace($throwable) + ); + } + + return $buffer; + } + + /** + * Gets the stack trace. + */ + public function getStackTrace(Throwable $throwable): string + { + $stackTrace = $throwable->stackTrace(); + + // Split stacktrace per frame. + $frames = explode("\n", $stackTrace); + + // Remove empty lines + $frames = array_filter($frames); + + // clean the paths of each frame. + $frames = array_map( + fn (string $frame): string => $this->toRelativePath($frame), + $frames + ); + + // Format stacktrace as `at ` + $frames = array_map( + fn (string $frame) => "at $frame", + $frames + ); + + return implode("\n", $frames); + } + + /** + * Gets the test suite name. + */ + public function getTestSuiteName(TestSuite $testSuite): string + { + $name = $testSuite->name(); + + if (str_starts_with($name, self::PREFIX)) { + return Str::after($name, self::PREFIX); + } + + return Str::after($name, $this->rootPath); + } + + /** + * Gets the test suite location. + */ + public function getTestSuiteLocation(TestSuite $testSuite): ?string + { + $tests = $testSuite->tests()->asArray(); + + // TODO: figure out how to get the file path without a test being there. + if ($tests === []) { + return null; + } + + $firstTest = $tests[0]; + if (! $firstTest instanceof TestMethod) { + throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); + } + + $path = $firstTest->testDox()->prettifiedClassName(); + + return $this->toRelativePath($path); + } + + /** + * Gets the test suite size. + */ + public function getTestSuiteSize(TestSuite $testSuite): int + { + return $testSuite->count(); + } + + /** + * Transforms the given path in relative path. + */ + private function toRelativePath(string $path): string + { + // Remove cwd from the path. + return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path); + } + + /** + * Get the test result. + */ + public function getStateFromResult(PhpUnitTestResult $result): State + { + $events = [ + ...$result->testErroredEvents(), + ...$result->testFailedEvents(), + ...$result->testSkippedEvents(), + ...array_merge(...array_values($result->testConsideredRiskyEvents())), + ...$result->testMarkedIncompleteEvents(), + ]; + + $numberOfNotPassedTests = count( + array_unique( + array_map( + function (BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string { + if ($event instanceof BeforeFirstTestMethodErrored) { + return $event->testClassName(); + } + + return $this->getTestCaseLocation($event->test()); + }, + $events + ) + ) + ); + + $numberOfPassedTests = $result->numberOfTestsRun() - $numberOfNotPassedTests; + + return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result); + } +} diff --git a/src/Logging/JUnit/JUnitLogger.php b/src/Logging/JUnit/JUnitLogger.php new file mode 100644 index 00000000..538d28cb --- /dev/null +++ b/src/Logging/JUnit/JUnitLogger.php @@ -0,0 +1,398 @@ + + */ + 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; + + /**✅ + * @throws EventFacadeIsSealedException + * @throws UnknownSubscriberTypeException + */ + public function __construct( + private readonly Printer $printer, + private readonly OutputInterface $output, + private readonly Converter $converter, + ) { + $this->registerSubscribers(); + $this->createDocument(); + } + + public function flush(): void + { + $this->printer->print($this->document->saveXML()); + + $this->printer->flush(); + + $this->output->writeln('Junit finished'); + } + + public function testSuiteStarted(Started $event): void + { + $testSuite = $this->document->createElement('testsuite'); + $testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); + + if ($event->testSuite()->isForTestClass()) { + $testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); + } + + 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 + * @throws NoDataSetFromDataProviderException + */ + public function testPrepared(Prepared $event): void + { + $this->createTestCase($event); + $this->prepared = true; + } + + /** + * @throws InvalidArgumentException + */ + public function testFinished(Finished $event): void + { + $this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed()); + } + + /** + * @throws InvalidArgumentException + * @throws NoDataSetFromDataProviderException + */ + public function testMarkedIncomplete(MarkedIncomplete $event): void + { + $this->handleIncompleteOrSkipped($event); + } + + /** + * @throws InvalidArgumentException + * @throws NoDataSetFromDataProviderException + */ + public function testSkipped(Skipped $event): void + { + $this->handleIncompleteOrSkipped($event); + } + + /** + * @throws InvalidArgumentException + * @throws NoDataSetFromDataProviderException + */ + public function testErrored(Errored $event): void + { + $this->handleFault($event, 'error'); + + $this->testSuiteErrors[$this->testSuiteLevel]++; + } + + /** + * @throws InvalidArgumentException + * @throws NoDataSetFromDataProviderException + */ + 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(): void + { + $subscribers = [ + new TestSuiteStartedSubscriber($this), + new TestSuiteFinishedSubscriber($this), + new TestPreparedSubscriber($this), + new TestFinishedSubscriber($this), + new TestErroredSubscriber($this), + new TestFailedSubscriber($this), + new TestMarkedIncompleteSubscriber($this), + new TestSkippedSubscriber($this), + new TestRunnerExecutionFinishedSubscriber($this), + ]; + + Facade::instance()->registerSubscribers(...$subscribers); + } + + private function createDocument(): void + { + $this->output->writeln('Start Junit'); + + $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 + * @throws NoDataSetFromDataProviderException + */ + private function handleFault(Errored|Failed $event, string $type): void + { + if (! $this->prepared) { + $this->createTestCase($event); + } + + assert($this->currentTestCase !== null); + + $testName = $this->converter->getTestCaseMethodName($event->test()); + // $message = $this->converter->getExceptionMessage($event->throwable()); + // $details = $this->converter->getExceptionDetails($event->throwable()); + + $buffer = $testName; + + $throwable = $event->throwable(); + $buffer .= trim( + $throwable->description().PHP_EOL. + $throwable->stackTrace(), + ); + + $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 + * @throws NoDataSetFromDataProviderException + */ + 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 + * @throws NoDataSetFromDataProviderException + * + * @psalm-assert !null $this->currentTestCase + */ + private function createTestCase(Errored|Failed|MarkedIncomplete|Prepared|Skipped $event): void + { + $testCase = $this->document->createElement('testcase'); + + $file = $this->converter->getTestCaseLocation($event->test()); + + $testCase->setAttribute('name', $this->converter->getTestCaseMethodName($event->test())); +// $testCase->setAttribute('name', $event->test()->name()); + $testCase->setAttribute('file', $file); +// $testCase->setAttribute('file', $event->test()->file()); + + if ($event->test()->isTestMethod()) { + assert($event->test() instanceof TestMethod); + + //dd(TestSuite::getInstance()->tests->get($file)); + // add classname, and line to this + + $testCase->setAttribute('line', (string) $event->test()->line()); //@todo figure out how to get line number in original pest file + $testCase->setAttribute('class', $event->test()->name()); + $testCase->setAttribute('classname', str_replace('\\', '.', $event->test()->name())); + } + + $this->currentTestCase = $testCase; + $this->time = $event->telemetryInfo()->time(); + } +} diff --git a/src/Logging/JUnit/Subscriber/Subscriber.php b/src/Logging/JUnit/Subscriber/Subscriber.php new file mode 100644 index 00000000..7ffa1f34 --- /dev/null +++ b/src/Logging/JUnit/Subscriber/Subscriber.php @@ -0,0 +1,28 @@ +logger; + } +} diff --git a/src/Logging/JUnit/Subscriber/TestErroredSubscriber.php b/src/Logging/JUnit/Subscriber/TestErroredSubscriber.php new file mode 100644 index 00000000..87172774 --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestErroredSubscriber.php @@ -0,0 +1,19 @@ +logger()->testErrored($event); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestFailedSubscriber.php b/src/Logging/JUnit/Subscriber/TestFailedSubscriber.php new file mode 100644 index 00000000..d0f6c1ea --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestFailedSubscriber.php @@ -0,0 +1,19 @@ +logger()->testFailed($event); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestFinishedSubscriber.php b/src/Logging/JUnit/Subscriber/TestFinishedSubscriber.php new file mode 100644 index 00000000..4c1e937d --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestFinishedSubscriber.php @@ -0,0 +1,19 @@ +logger()->testFinished($event); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestMarkedIncompleteSubscriber.php b/src/Logging/JUnit/Subscriber/TestMarkedIncompleteSubscriber.php new file mode 100644 index 00000000..4ed5d693 --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestMarkedIncompleteSubscriber.php @@ -0,0 +1,19 @@ +logger()->testMarkedIncomplete($event); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestPreparedSubscriber.php b/src/Logging/JUnit/Subscriber/TestPreparedSubscriber.php new file mode 100644 index 00000000..f9841361 --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestPreparedSubscriber.php @@ -0,0 +1,19 @@ +logger()->testPrepared($event); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestRunnerExecutionFinishedSubscriber.php b/src/Logging/JUnit/Subscriber/TestRunnerExecutionFinishedSubscriber.php new file mode 100644 index 00000000..8dcf762f --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestRunnerExecutionFinishedSubscriber.php @@ -0,0 +1,19 @@ +logger()->flush(); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestSkippedSubscriber.php b/src/Logging/JUnit/Subscriber/TestSkippedSubscriber.php new file mode 100644 index 00000000..afa764ad --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestSkippedSubscriber.php @@ -0,0 +1,19 @@ +logger()->testSkipped($event); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestSuiteFinishedSubscriber.php b/src/Logging/JUnit/Subscriber/TestSuiteFinishedSubscriber.php new file mode 100644 index 00000000..9ed15c15 --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestSuiteFinishedSubscriber.php @@ -0,0 +1,19 @@ +logger()->testSuiteFinished(); + } +} diff --git a/src/Logging/JUnit/Subscriber/TestSuiteStartedSubscriber.php b/src/Logging/JUnit/Subscriber/TestSuiteStartedSubscriber.php new file mode 100644 index 00000000..26f80239 --- /dev/null +++ b/src/Logging/JUnit/Subscriber/TestSuiteStartedSubscriber.php @@ -0,0 +1,19 @@ +logger()->testSuiteStarted($event); + } +} diff --git a/src/Subscribers/EnsureJunitEnabled.php b/src/Subscribers/EnsureJunitEnabled.php new file mode 100644 index 00000000..79c78eee --- /dev/null +++ b/src/Subscribers/EnsureJunitEnabled.php @@ -0,0 +1,48 @@ +input->hasParameterOption('--log-junit')) { + return; + } + + new JUnitLogger( + DefaultPrinter::from(Container::getInstance()->get(Configuration::class)->logfileJunit()), + $this->output, + new Converter($this->testSuite->rootPath), + ); + } +} From 117694f210f5500b0c42a1cc831801a85c1abde3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Sun, 20 Aug 2023 17:55:17 +0200 Subject: [PATCH 2/5] cleanup --- src/Logging/JUnit/Converter.php | 61 ++++--------------------------- src/Logging/JUnit/JUnitLogger.php | 37 ++++++++----------- 2 files changed, 24 insertions(+), 74 deletions(-) diff --git a/src/Logging/JUnit/Converter.php b/src/Logging/JUnit/Converter.php index f417a59e..e31fb1ff 100644 --- a/src/Logging/JUnit/Converter.php +++ b/src/Logging/JUnit/Converter.php @@ -4,22 +4,13 @@ declare(strict_types=1); namespace Pest\Logging\JUnit; -use NunoMaduro\Collision\Adapters\Phpunit\State; use Pest\Exceptions\ShouldNotHappen; -use Pest\Support\StateGenerator; use Pest\Support\Str; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\Throwable; -use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; -use PHPUnit\Event\Test\ConsideredRisky; -use PHPUnit\Event\Test\Errored; -use PHPUnit\Event\Test\Failed; -use PHPUnit\Event\Test\MarkedIncomplete; -use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\TestSuite\TestSuite; use PHPUnit\Framework\Exception as FrameworkException; -use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult; /** * @internal @@ -28,15 +19,12 @@ final class Converter { private const PREFIX = 'P\\'; - private readonly StateGenerator $stateGenerator; - /** * Creates a new instance of the Converter. */ public function __construct( private readonly string $rootPath, ) { - $this->stateGenerator = new StateGenerator(); } /** @@ -73,6 +61,14 @@ final class Converter return "$relativePath::$description"; } + /** + * Gets the trimmed test class name. + */ + public function getTrimmedTestClassName(TestMethod $test): string + { + return Str::after($test->className(), self::PREFIX); + } + /** * Gets the exception message. */ @@ -176,14 +172,6 @@ final class Converter return $this->toRelativePath($path); } - /** - * Gets the test suite size. - */ - public function getTestSuiteSize(TestSuite $testSuite): int - { - return $testSuite->count(); - } - /** * Transforms the given path in relative path. */ @@ -192,37 +180,4 @@ final class Converter // Remove cwd from the path. return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path); } - - /** - * Get the test result. - */ - public function getStateFromResult(PhpUnitTestResult $result): State - { - $events = [ - ...$result->testErroredEvents(), - ...$result->testFailedEvents(), - ...$result->testSkippedEvents(), - ...array_merge(...array_values($result->testConsideredRiskyEvents())), - ...$result->testMarkedIncompleteEvents(), - ]; - - $numberOfNotPassedTests = count( - array_unique( - array_map( - function (BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string { - if ($event instanceof BeforeFirstTestMethodErrored) { - return $event->testClassName(); - } - - return $this->getTestCaseLocation($event->test()); - }, - $events - ) - ) - ); - - $numberOfPassedTests = $result->numberOfTestsRun() - $numberOfNotPassedTests; - - return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result); - } } diff --git a/src/Logging/JUnit/JUnitLogger.php b/src/Logging/JUnit/JUnitLogger.php index 538d28cb..235899cc 100644 --- a/src/Logging/JUnit/JUnitLogger.php +++ b/src/Logging/JUnit/JUnitLogger.php @@ -15,7 +15,6 @@ use Pest\Logging\JUnit\Subscriber\TestRunnerExecutionFinishedSubscriber; use Pest\Logging\JUnit\Subscriber\TestSkippedSubscriber; use Pest\Logging\JUnit\Subscriber\TestSuiteFinishedSubscriber; use Pest\Logging\JUnit\Subscriber\TestSuiteStartedSubscriber; -use Pest\TestSuite; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; @@ -88,7 +87,7 @@ final class JUnitLogger private bool $prepared = false; - /**✅ + /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ @@ -271,7 +270,7 @@ final class JUnitLogger $this->prepared = false; } - /**✅ + /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ @@ -315,16 +314,16 @@ final class JUnitLogger assert($this->currentTestCase !== null); + $throwable = $event->throwable(); + $testName = $this->converter->getTestCaseMethodName($event->test()); - // $message = $this->converter->getExceptionMessage($event->throwable()); - // $details = $this->converter->getExceptionDetails($event->throwable()); + $message = $this->converter->getExceptionMessage($throwable); + $details = $this->converter->getExceptionDetails($throwable); $buffer = $testName; - - $throwable = $event->throwable(); $buffer .= trim( - $throwable->description().PHP_EOL. - $throwable->stackTrace(), + $message.PHP_EOL. + $details, ); $fault = $this->document->createElement( @@ -374,22 +373,18 @@ final class JUnitLogger { $testCase = $this->document->createElement('testcase'); - $file = $this->converter->getTestCaseLocation($event->test()); + $test = $event->test(); + $file = $this->converter->getTestCaseLocation($test); - $testCase->setAttribute('name', $this->converter->getTestCaseMethodName($event->test())); -// $testCase->setAttribute('name', $event->test()->name()); + $testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); $testCase->setAttribute('file', $file); -// $testCase->setAttribute('file', $event->test()->file()); - if ($event->test()->isTestMethod()) { - assert($event->test() instanceof TestMethod); + if ($test->isTestMethod()) { + assert($test instanceof TestMethod); - //dd(TestSuite::getInstance()->tests->get($file)); - // add classname, and line to this - - $testCase->setAttribute('line', (string) $event->test()->line()); //@todo figure out how to get line number in original pest file - $testCase->setAttribute('class', $event->test()->name()); - $testCase->setAttribute('classname', str_replace('\\', '.', $event->test()->name())); + $className = $this->converter->getTrimmedTestClassName($test); + $testCase->setAttribute('class', $className); + $testCase->setAttribute('classname', str_replace('\\', '.', $className)); } $this->currentTestCase = $testCase; From 8efd25ef65c363d5c284501a723d0a5ba0308ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Sun, 20 Aug 2023 18:03:32 +0200 Subject: [PATCH 3/5] remove debug output --- src/Logging/JUnit/JUnitLogger.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Logging/JUnit/JUnitLogger.php b/src/Logging/JUnit/JUnitLogger.php index 235899cc..68ce1756 100644 --- a/src/Logging/JUnit/JUnitLogger.php +++ b/src/Logging/JUnit/JUnitLogger.php @@ -105,8 +105,6 @@ final class JUnitLogger $this->printer->print($this->document->saveXML()); $this->printer->flush(); - - $this->output->writeln('Junit finished'); } public function testSuiteStarted(Started $event): void From 4550a344d36f7f0c674d1e936ebc71cc555543c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Sun, 20 Aug 2023 18:03:59 +0200 Subject: [PATCH 4/5] overwrite phpunit junit logging with noop --- overrides/Logging/JUnit/JunitXmlLogger.php | 16 ++++++++++++++++ src/Bootstrappers/BootOverrides.php | 1 + 2 files changed, 17 insertions(+) create mode 100644 overrides/Logging/JUnit/JunitXmlLogger.php diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php new file mode 100644 index 00000000..bc0e0953 --- /dev/null +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -0,0 +1,16 @@ + Date: Sun, 20 Aug 2023 18:11:54 +0200 Subject: [PATCH 5/5] unify converter --- src/Logging/{TeamCity => }/Converter.php | 10 +- src/Logging/JUnit/Converter.php | 183 ---------------------- src/Logging/JUnit/JUnitLogger.php | 1 + src/Logging/TeamCity/TeamCityLogger.php | 1 + src/Subscribers/EnsureJunitEnabled.php | 2 +- src/Subscribers/EnsureTeamCityEnabled.php | 2 +- 6 files changed, 13 insertions(+), 186 deletions(-) rename src/Logging/{TeamCity => }/Converter.php (96%) delete mode 100644 src/Logging/JUnit/Converter.php diff --git a/src/Logging/TeamCity/Converter.php b/src/Logging/Converter.php similarity index 96% rename from src/Logging/TeamCity/Converter.php rename to src/Logging/Converter.php index 2a2cce4f..7cbf8f12 100644 --- a/src/Logging/TeamCity/Converter.php +++ b/src/Logging/Converter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Pest\Logging\TeamCity; +namespace Pest\Logging; use NunoMaduro\Collision\Adapters\Phpunit\State; use Pest\Exceptions\ShouldNotHappen; @@ -150,6 +150,14 @@ final class Converter return Str::after($name, self::PREFIX); } + /** + * Gets the trimmed test class name. + */ + public function getTrimmedTestClassName(TestMethod $test): string + { + return Str::after($test->className(), self::PREFIX); + } + /** * Gets the test suite location. */ diff --git a/src/Logging/JUnit/Converter.php b/src/Logging/JUnit/Converter.php deleted file mode 100644 index e31fb1ff..00000000 --- a/src/Logging/JUnit/Converter.php +++ /dev/null @@ -1,183 +0,0 @@ -testDox()->prettifiedMethodName(); - } - - /** - * Gets the test case location. - */ - public function getTestCaseLocation(Test $test, bool $withDescription = false): string - { - if (! $test instanceof TestMethod) { - throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); - } - - $path = $test->testDox()->prettifiedClassName(); - $relativePath = $this->toRelativePath($path); - - // TODO: Get the description without the dataset. - $description = $test->testDox()->prettifiedMethodName(); - - if (! $withDescription) { - return $relativePath; - } - - return "$relativePath::$description"; - } - - /** - * Gets the trimmed test class name. - */ - public function getTrimmedTestClassName(TestMethod $test): string - { - return Str::after($test->className(), self::PREFIX); - } - - /** - * Gets the exception message. - */ - public function getExceptionMessage(Throwable $throwable): string - { - if (is_a($throwable->className(), FrameworkException::class, true)) { - return $throwable->message(); - } - - $buffer = $throwable->className(); - $throwableMessage = $throwable->message(); - - if ($throwableMessage !== '') { - $buffer .= ": $throwableMessage"; - } - - return $buffer; - } - - /** - * Gets the exception details. - */ - public function getExceptionDetails(Throwable $throwable): string - { - $buffer = $this->getStackTrace($throwable); - - while ($throwable->hasPrevious()) { - $throwable = $throwable->previous(); - - $buffer .= sprintf( - "\nCaused by\n%s\n%s", - $throwable->description(), - $this->getStackTrace($throwable) - ); - } - - return $buffer; - } - - /** - * Gets the stack trace. - */ - public function getStackTrace(Throwable $throwable): string - { - $stackTrace = $throwable->stackTrace(); - - // Split stacktrace per frame. - $frames = explode("\n", $stackTrace); - - // Remove empty lines - $frames = array_filter($frames); - - // clean the paths of each frame. - $frames = array_map( - fn (string $frame): string => $this->toRelativePath($frame), - $frames - ); - - // Format stacktrace as `at ` - $frames = array_map( - fn (string $frame) => "at $frame", - $frames - ); - - return implode("\n", $frames); - } - - /** - * Gets the test suite name. - */ - public function getTestSuiteName(TestSuite $testSuite): string - { - $name = $testSuite->name(); - - if (str_starts_with($name, self::PREFIX)) { - return Str::after($name, self::PREFIX); - } - - return Str::after($name, $this->rootPath); - } - - /** - * Gets the test suite location. - */ - public function getTestSuiteLocation(TestSuite $testSuite): ?string - { - $tests = $testSuite->tests()->asArray(); - - // TODO: figure out how to get the file path without a test being there. - if ($tests === []) { - return null; - } - - $firstTest = $tests[0]; - if (! $firstTest instanceof TestMethod) { - throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); - } - - $path = $firstTest->testDox()->prettifiedClassName(); - - return $this->toRelativePath($path); - } - - /** - * Transforms the given path in relative path. - */ - private function toRelativePath(string $path): string - { - // Remove cwd from the path. - return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path); - } -} diff --git a/src/Logging/JUnit/JUnitLogger.php b/src/Logging/JUnit/JUnitLogger.php index 68ce1756..497fe8ef 100644 --- a/src/Logging/JUnit/JUnitLogger.php +++ b/src/Logging/JUnit/JUnitLogger.php @@ -6,6 +6,7 @@ namespace Pest\Logging\JUnit; use DOMDocument; use DOMElement; +use Pest\Logging\Converter; use Pest\Logging\JUnit\Subscriber\TestErroredSubscriber; use Pest\Logging\JUnit\Subscriber\TestFailedSubscriber; use Pest\Logging\JUnit\Subscriber\TestFinishedSubscriber; diff --git a/src/Logging/TeamCity/TeamCityLogger.php b/src/Logging/TeamCity/TeamCityLogger.php index 92d35daf..072bd608 100644 --- a/src/Logging/TeamCity/TeamCityLogger.php +++ b/src/Logging/TeamCity/TeamCityLogger.php @@ -6,6 +6,7 @@ namespace Pest\Logging\TeamCity; use NunoMaduro\Collision\Adapters\Phpunit\Style; use Pest\Exceptions\ShouldNotHappen; +use Pest\Logging\Converter; use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber; use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber; use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber; diff --git a/src/Subscribers/EnsureJunitEnabled.php b/src/Subscribers/EnsureJunitEnabled.php index 79c78eee..b6f6a339 100644 --- a/src/Subscribers/EnsureJunitEnabled.php +++ b/src/Subscribers/EnsureJunitEnabled.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Pest\Subscribers; +use Pest\Logging\Converter; use Pest\Logging\JUnit\JUnitLogger; -use Pest\Logging\JUnit\Converter; use Pest\Support\Container; use Pest\TestSuite; use PHPUnit\Event\TestRunner\Configured; diff --git a/src/Subscribers/EnsureTeamCityEnabled.php b/src/Subscribers/EnsureTeamCityEnabled.php index bd6b1bf2..264800c0 100644 --- a/src/Subscribers/EnsureTeamCityEnabled.php +++ b/src/Subscribers/EnsureTeamCityEnabled.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Pest\Subscribers; -use Pest\Logging\TeamCity\Converter; +use Pest\Logging\Converter; use Pest\Logging\TeamCity\TeamCityLogger; use Pest\TestSuite; use PHPUnit\Event\TestRunner\Configured;