From d7c7f850d18a4efeb6e0f5054818b5b2bc6de263 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Mon, 15 May 2023 08:18:58 +0200 Subject: [PATCH] Initial work on #4321 --- .psalm/baseline.xml | 3 ++ phpunit.xsd | 1 + src/Logging/Xml/Subscriber/Subscriber.php | 28 ++++++++++ .../TestRunnerExecutionFinishedSubscriber.php | 24 +++++++++ src/Logging/Xml/XmlLogger.php | 52 +++++++++++++++++++ src/TextUI/Application.php | 8 +++ src/TextUI/Configuration/Cli/Builder.php | 8 +++ .../Configuration/Cli/Configuration.php | 24 ++++++++- src/TextUI/Configuration/Configuration.php | 24 ++++++++- src/TextUI/Configuration/Merger.php | 8 +++ .../Xml/DefaultConfiguration.php | 1 + src/TextUI/Configuration/Xml/Loader.php | 16 ++++++ .../Configuration/Xml/Logging/Logging.php | 21 +++++++- src/TextUI/Configuration/Xml/Logging/Xml.php | 32 ++++++++++++ src/TextUI/Help.php | 1 + tests/_files/configuration_logging.xml | 1 + .../_files/output-cli-help-color.txt | 2 + tests/end-to-end/_files/output-cli-usage.txt | 1 + tests/end-to-end/logging/log-xml-to-file.phpt | 27 ++++++++++ .../end-to-end/logging/log-xml-to-stdout.phpt | 16 ++++++ .../TextUI/Configuration/Xml/LoaderTest.php | 3 ++ 21 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 src/Logging/Xml/Subscriber/Subscriber.php create mode 100644 src/Logging/Xml/Subscriber/TestRunnerExecutionFinishedSubscriber.php create mode 100644 src/Logging/Xml/XmlLogger.php create mode 100644 src/TextUI/Configuration/Xml/Logging/Xml.php create mode 100644 tests/end-to-end/logging/log-xml-to-file.phpt create mode 100644 tests/end-to-end/logging/log-xml-to-stdout.phpt diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index cd6cf903cb6..8bb77a60bc0 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -634,6 +634,8 @@ )]]> logfileJunit())]]> logfileJunit())]]> + logfileXml())]]> + logfileXml())]]> atLeastVersion build configurationFile @@ -646,6 +648,7 @@ logEventsVerboseText logfileJunit logfileTeamcity + logfileXml include_once $filename diff --git a/phpunit.xsd b/phpunit.xsd index c4c961d6db5..930b3141eea 100644 --- a/phpunit.xsd +++ b/phpunit.xsd @@ -286,6 +286,7 @@ + diff --git a/src/Logging/Xml/Subscriber/Subscriber.php b/src/Logging/Xml/Subscriber/Subscriber.php new file mode 100644 index 00000000000..43e68930031 --- /dev/null +++ b/src/Logging/Xml/Subscriber/Subscriber.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Logging\Xml; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ +abstract class Subscriber +{ + private readonly XmlLogger $logger; + + public function __construct(XmlLogger $logger) + { + $this->logger = $logger; + } + + protected function logger(): XmlLogger + { + return $this->logger; + } +} diff --git a/src/Logging/Xml/Subscriber/TestRunnerExecutionFinishedSubscriber.php b/src/Logging/Xml/Subscriber/TestRunnerExecutionFinishedSubscriber.php new file mode 100644 index 00000000000..35cdd087215 --- /dev/null +++ b/src/Logging/Xml/Subscriber/TestRunnerExecutionFinishedSubscriber.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Logging\Xml; + +use PHPUnit\Event\TestRunner\ExecutionFinished; +use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ +final class TestRunnerExecutionFinishedSubscriber extends Subscriber implements ExecutionFinishedSubscriber +{ + public function notify(ExecutionFinished $event): void + { + $this->logger()->flush(); + } +} diff --git a/src/Logging/Xml/XmlLogger.php b/src/Logging/Xml/XmlLogger.php new file mode 100644 index 00000000000..ae629f163ff --- /dev/null +++ b/src/Logging/Xml/XmlLogger.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Logging\Xml; + +use PHPUnit\Event\EventFacadeIsSealedException; +use PHPUnit\Event\Facade; +use PHPUnit\Event\UnknownSubscriberTypeException; +use PHPUnit\TextUI\Output\Printer; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ +final class XmlLogger +{ + private readonly Printer $printer; + + /** + * @throws EventFacadeIsSealedException + * @throws UnknownSubscriberTypeException + */ + public function __construct(Printer $printer, Facade $facade) + { + $this->printer = $printer; + + $this->registerSubscribers($facade); + } + + public function flush(): void + { + $this->printer->print('todo'); + + $this->printer->flush(); + } + + /** + * @throws EventFacadeIsSealedException + * @throws UnknownSubscriberTypeException + */ + private function registerSubscribers(Facade $facade): void + { + $facade->registerSubscribers( + new TestRunnerExecutionFinishedSubscriber($this), + ); + } +} diff --git a/src/TextUI/Application.php b/src/TextUI/Application.php index 9009191947f..06efa1c1700 100644 --- a/src/TextUI/Application.php +++ b/src/TextUI/Application.php @@ -27,6 +27,7 @@ use PHPUnit\Logging\TestDox\HtmlRenderer as TestDoxHtmlRenderer; use PHPUnit\Logging\TestDox\PlainTextRenderer as TestDoxTextRenderer; use PHPUnit\Logging\TestDox\TestResultCollector as TestDoxResultCollector; +use PHPUnit\Logging\Xml\XmlLogger; use PHPUnit\Metadata\Api\CodeCoverage as CodeCoverageMetadataApi; use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\Extension\ExtensionBootstrapper; @@ -548,6 +549,13 @@ private function registerLogfileWriters(Configuration $configuration): void ); } + if ($configuration->hasLogfileXml()) { + new XmlLogger( + OutputFacade::printerFor($configuration->logfileXml()), + EventFacade::instance(), + ); + } + if ($configuration->hasLogfileJunit()) { new JunitXmlLogger( OutputFacade::printerFor($configuration->logfileJunit()), diff --git a/src/TextUI/Configuration/Cli/Builder.php b/src/TextUI/Configuration/Cli/Builder.php index b1cbda8ef45..dcb2b8e6168 100644 --- a/src/TextUI/Configuration/Cli/Builder.php +++ b/src/TextUI/Configuration/Cli/Builder.php @@ -70,6 +70,7 @@ final class Builder 'list-tests-xml=', 'log-junit=', 'log-teamcity=', + 'log-xml=', 'migrate-configuration', 'no-configuration', 'no-coverage', @@ -203,6 +204,7 @@ public function fromParameters(array $parameters): Configuration $includePath = null; $iniSettings = []; $junitLogfile = null; + $xmlLogfile = null; $listGroups = false; $listSuites = false; $listTests = false; @@ -444,6 +446,11 @@ public function fromParameters(array $parameters): Configuration break; + case '--log-xml': + $xmlLogfile = $option[1]; + + break; + case '--order-by': foreach (explode(',', $option[1]) as $order) { switch ($order) { @@ -853,6 +860,7 @@ public function fromParameters(array $parameters): Configuration $help, $includePath, $iniSettings, + $xmlLogfile, $junitLogfile, $listGroups, $listSuites, diff --git a/src/TextUI/Configuration/Cli/Configuration.php b/src/TextUI/Configuration/Cli/Configuration.php index 2e0901e872c..afddc4b847b 100644 --- a/src/TextUI/Configuration/Cli/Configuration.php +++ b/src/TextUI/Configuration/Cli/Configuration.php @@ -74,6 +74,7 @@ final class Configuration private readonly bool $help; private readonly ?string $includePath; private readonly ?array $iniSettings; + private readonly ?string $xmlLogfile; private readonly ?string $junitLogfile; private readonly bool $listGroups; private readonly bool $listSuites; @@ -118,7 +119,7 @@ final class Configuration /** * @psalm-param ?non-empty-list $testSuffixes */ - public function __construct(?string $argument, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticProperties, ?bool $beStrictAboutChangesToGlobalState, ?string $bootstrap, ?string $cacheDirectory, ?bool $cacheResult, ?string $cacheResultFile, bool $checkVersion, ?string $colors, null|int|string $columns, ?string $configurationFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, ?string $coverageCacheDirectory, bool $warmCoverageCache, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?bool $failOnDeprecation, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnNotice, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?bool $stopOnDefect, ?bool $stopOnDeprecation, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnNotice, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $filter, bool $generateConfiguration, bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, bool $listGroups, bool $listSuites, bool $listTests, ?string $listTestsXml, ?bool $noCoverage, ?bool $noExtensions, ?bool $noOutput, ?bool $noProgress, ?bool $noResults, ?bool $noLogging, ?bool $processIsolation, ?int $randomOrderSeed, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?string $teamcityLogfile, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?array $testSuffixes, ?string $testSuite, ?string $excludeTestSuite, bool $useDefaultConfiguration, ?bool $displayDetailsOnIncompleteTests, ?bool $displayDetailsOnSkippedTests, ?bool $displayDetailsOnTestsThatTriggerDeprecations, ?bool $displayDetailsOnTestsThatTriggerErrors, ?bool $displayDetailsOnTestsThatTriggerNotices, ?bool $displayDetailsOnTestsThatTriggerWarnings, bool $version, ?array $coverageFilter, ?string $logEventsText, ?string $logEventsVerboseText, ?bool $printerTeamCity, ?bool $printerTestDox) + public function __construct(?string $argument, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticProperties, ?bool $beStrictAboutChangesToGlobalState, ?string $bootstrap, ?string $cacheDirectory, ?bool $cacheResult, ?string $cacheResultFile, bool $checkVersion, ?string $colors, null|int|string $columns, ?string $configurationFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, ?string $coverageCacheDirectory, bool $warmCoverageCache, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?bool $failOnDeprecation, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnNotice, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?bool $stopOnDefect, ?bool $stopOnDeprecation, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnNotice, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $filter, bool $generateConfiguration, bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, bool $help, ?string $includePath, ?array $iniSettings, ?string $xmlLogfile, ?string $junitLogfile, bool $listGroups, bool $listSuites, bool $listTests, ?string $listTestsXml, ?bool $noCoverage, ?bool $noExtensions, ?bool $noOutput, ?bool $noProgress, ?bool $noResults, ?bool $noLogging, ?bool $processIsolation, ?int $randomOrderSeed, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?string $teamcityLogfile, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?array $testSuffixes, ?string $testSuite, ?string $excludeTestSuite, bool $useDefaultConfiguration, ?bool $displayDetailsOnIncompleteTests, ?bool $displayDetailsOnSkippedTests, ?bool $displayDetailsOnTestsThatTriggerDeprecations, ?bool $displayDetailsOnTestsThatTriggerErrors, ?bool $displayDetailsOnTestsThatTriggerNotices, ?bool $displayDetailsOnTestsThatTriggerWarnings, bool $version, ?array $coverageFilter, ?string $logEventsText, ?string $logEventsVerboseText, ?bool $printerTeamCity, ?bool $printerTestDox) { $this->argument = $argument; $this->atLeastVersion = $atLeastVersion; @@ -178,6 +179,7 @@ public function __construct(?string $argument, ?string $atLeastVersion, ?bool $b $this->help = $help; $this->includePath = $includePath; $this->iniSettings = $iniSettings; + $this->xmlLogfile = $xmlLogfile; $this->junitLogfile = $junitLogfile; $this->listGroups = $listGroups; $this->listSuites = $listSuites; @@ -1309,6 +1311,26 @@ public function iniSettings(): array return $this->iniSettings; } + /** + * @psalm-assert-if-true !null $this->xmlLogfile + */ + public function hasXmlLogfile(): bool + { + return $this->xmlLogfile !== null; + } + + /** + * @throws Exception + */ + public function xmlLogfile(): string + { + if (!$this->hasXmlLogfile()) { + throw new Exception; + } + + return $this->xmlLogfile; + } + /** * @psalm-assert-if-true !null $this->junitLogfile */ diff --git a/src/TextUI/Configuration/Configuration.php b/src/TextUI/Configuration/Configuration.php index ba08e568425..09b5f0eb3ba 100644 --- a/src/TextUI/Configuration/Configuration.php +++ b/src/TextUI/Configuration/Configuration.php @@ -106,6 +106,7 @@ final class Configuration private readonly ?string $logfileJunit; private readonly ?string $logfileTestdoxHtml; private readonly ?string $logfileTestdoxText; + private readonly ?string $logfileXml; private readonly ?string $logEventsText; private readonly ?string $logEventsVerboseText; private readonly ?array $testsCovering; @@ -134,7 +135,7 @@ final class Configuration * @psalm-param non-empty-list $testSuffixes * @psalm-param list}> $extensionBootstrappers */ - public function __construct(?string $cliArgument, ?string $configurationFile, ?string $bootstrap, bool $cacheResult, ?string $cacheDirectory, ?string $coverageCacheDirectory, Source $source, string $testResultCacheFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4j, int $coverageCrap4jThreshold, ?string $coverageHtml, int $coverageHtmlLowUpperBound, int $coverageHtmlHighLowerBound, string $coverageHtmlColorSuccessLow, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorWarning, string $coverageHtmlColorDanger, ?string $coverageHtmlCustomCssFile, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $pathCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $failOnDeprecation, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, bool $outputToStandardErrorStream, int|string $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $disallowTestOutput, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $registerMockObjectsFromTestArgumentsRecursively, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $teamCityOutput, bool $testDoxOutput, ?array $testsCovering, ?array $testsUsing, ?string $filter, ?array $groups, ?array $excludeGroups, int $randomOrderSeed, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection) + public function __construct(?string $cliArgument, ?string $configurationFile, ?string $bootstrap, bool $cacheResult, ?string $cacheDirectory, ?string $coverageCacheDirectory, Source $source, string $testResultCacheFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4j, int $coverageCrap4jThreshold, ?string $coverageHtml, int $coverageHtmlLowUpperBound, int $coverageHtmlHighLowerBound, string $coverageHtmlColorSuccessLow, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorWarning, string $coverageHtmlColorDanger, ?string $coverageHtmlCustomCssFile, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $pathCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $failOnDeprecation, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, bool $outputToStandardErrorStream, int|string $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $disallowTestOutput, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $registerMockObjectsFromTestArgumentsRecursively, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logfileXml, ?string $logEventsText, ?string $logEventsVerboseText, bool $teamCityOutput, bool $testDoxOutput, ?array $testsCovering, ?array $testsUsing, ?string $filter, ?array $groups, ?array $excludeGroups, int $randomOrderSeed, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection) { $this->cliArgument = $cliArgument; $this->configurationFile = $configurationFile; @@ -218,6 +219,7 @@ public function __construct(?string $cliArgument, ?string $configurationFile, ?s $this->logfileJunit = $logfileJunit; $this->logfileTestdoxHtml = $logfileTestdoxHtml; $this->logfileTestdoxText = $logfileTestdoxText; + $this->logfileXml = $logfileXml; $this->logEventsText = $logEventsText; $this->logEventsVerboseText = $logEventsVerboseText; $this->teamCityOutput = $teamCityOutput; @@ -1005,6 +1007,26 @@ public function logfileTestdoxText(): string return $this->logfileTestdoxText; } + /** + * @psalm-assert-if-true !null $this->logfileXml + */ + public function hasLogfileXml(): bool + { + return $this->logfileXml !== null; + } + + /** + * @throws LoggingNotConfiguredException + */ + public function logfileXml(): string + { + if (!$this->hasLogfileXml()) { + throw new LoggingNotConfiguredException; + } + + return $this->logfileXml; + } + /** * @psalm-assert-if-true !null $this->logEventsText */ diff --git a/src/TextUI/Configuration/Merger.php b/src/TextUI/Configuration/Merger.php index 72a494e6988..f90d43236d9 100644 --- a/src/TextUI/Configuration/Merger.php +++ b/src/TextUI/Configuration/Merger.php @@ -516,6 +516,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC $logfileJunit = null; $logfileTestdoxHtml = null; $logfileTestdoxText = null; + $logfileXml = null; $loggingFromXmlConfiguration = true; if ($cliConfiguration->hasNoLogging() && $cliConfiguration->noLogging()) { @@ -546,6 +547,12 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC $logfileTestdoxText = $xmlConfiguration->logging()->testDoxText()->target()->path(); } + if ($cliConfiguration->hasXmlLogfile()) { + $logfileXml = $cliConfiguration->xmlLogfile(); + } elseif ($loggingFromXmlConfiguration && $xmlConfiguration->logging()->hasXml()) { + $logfileXml = $xmlConfiguration->logging()->xml()->target()->path(); + } + $logEventsText = null; if ($cliConfiguration->hasLogEventsText()) { @@ -809,6 +816,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC $logfileJunit, $logfileTestdoxHtml, $logfileTestdoxText, + $logfileXml, $logEventsText, $logEventsVerboseText, $teamCityOutput, diff --git a/src/TextUI/Configuration/Xml/DefaultConfiguration.php b/src/TextUI/Configuration/Xml/DefaultConfiguration.php index 00f73810d20..414f28427b4 100644 --- a/src/TextUI/Configuration/Xml/DefaultConfiguration.php +++ b/src/TextUI/Configuration/Xml/DefaultConfiguration.php @@ -78,6 +78,7 @@ public static function create(): self null, null, null, + null, ), new Php( DirectoryCollection::fromArray([]), diff --git a/src/TextUI/Configuration/Xml/Loader.php b/src/TextUI/Configuration/Xml/Loader.php index d78701a69df..cdaaf824d92 100644 --- a/src/TextUI/Configuration/Xml/Loader.php +++ b/src/TextUI/Configuration/Xml/Loader.php @@ -66,6 +66,7 @@ use PHPUnit\TextUI\XmlConfiguration\Logging\TeamCity; use PHPUnit\TextUI\XmlConfiguration\Logging\TestDox\Html as TestDoxHtml; use PHPUnit\TextUI\XmlConfiguration\Logging\TestDox\Text as TestDoxText; +use PHPUnit\TextUI\XmlConfiguration\Logging\Xml; use PHPUnit\Util\VersionComparisonOperator; use PHPUnit\Util\Xml\Loader as XmlLoader; use PHPUnit\Util\Xml\XmlException; @@ -178,11 +179,26 @@ private function logging(string $filename, DOMXPath $xpath): Logging ); } + $xml = null; + $element = $this->element($xpath, 'logging/xml'); + + if ($element) { + $xml = new Xml( + new File( + $this->toAbsolutePath( + $filename, + (string) $this->getStringAttribute($element, 'outputFile'), + ), + ), + ); + } + return new Logging( $junit, $teamCity, $testDoxHtml, $testDoxText, + $xml, ); } diff --git a/src/TextUI/Configuration/Xml/Logging/Logging.php b/src/TextUI/Configuration/Xml/Logging/Logging.php index 587f727f82e..4aca98e5ef8 100644 --- a/src/TextUI/Configuration/Xml/Logging/Logging.php +++ b/src/TextUI/Configuration/Xml/Logging/Logging.php @@ -24,13 +24,15 @@ final class Logging private readonly ?TeamCity $teamCity; private readonly ?TestDoxHtml $testDoxHtml; private readonly ?TestDoxText $testDoxText; + private readonly ?Xml $xml; - public function __construct(?Junit $junit, ?TeamCity $teamCity, ?TestDoxHtml $testDoxHtml, ?TestDoxText $testDoxText) + public function __construct(?Junit $junit, ?TeamCity $teamCity, ?TestDoxHtml $testDoxHtml, ?TestDoxText $testDoxText, ?Xml $xml) { $this->junit = $junit; $this->teamCity = $teamCity; $this->testDoxHtml = $testDoxHtml; $this->testDoxText = $testDoxText; + $this->xml = $xml; } public function hasJunit(): bool @@ -100,4 +102,21 @@ public function testDoxText(): TestDoxText return $this->testDoxText; } + + public function hasXml(): bool + { + return $this->xml !== null; + } + + /** + * @throws Exception + */ + public function xml(): Xml + { + if ($this->xml === null) { + throw new Exception('Logger "XML" is not configured'); + } + + return $this->xml; + } } diff --git a/src/TextUI/Configuration/Xml/Logging/Xml.php b/src/TextUI/Configuration/Xml/Logging/Xml.php new file mode 100644 index 00000000000..7fce2ab2db7 --- /dev/null +++ b/src/TextUI/Configuration/Xml/Logging/Xml.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TextUI\XmlConfiguration\Logging; + +use PHPUnit\TextUI\Configuration\File; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + * + * @psalm-immutable + */ +final class Xml +{ + private readonly File $target; + + public function __construct(File $target) + { + $this->target = $target; + } + + public function target(): File + { + return $this->target; + } +} diff --git a/src/TextUI/Help.php b/src/TextUI/Help.php index e232de08628..5afa6160909 100644 --- a/src/TextUI/Help.php +++ b/src/TextUI/Help.php @@ -127,6 +127,7 @@ final class Help ], 'Logging' => [ + ['arg' => '--log-xml ', 'desc' => 'Write test results in PHPUnit XML format to file'], ['arg' => '--log-junit ', 'desc' => 'Write test results in JUnit XML format to file'], ['arg' => '--log-teamcity ', 'desc' => 'Write test results in TeamCity format to file'], ['arg' => '--testdox-html ', 'desc' => 'Write test results in TestDox format (HTML) to file'], diff --git a/tests/_files/configuration_logging.xml b/tests/_files/configuration_logging.xml index d3df475da6c..747ff85823a 100644 --- a/tests/_files/configuration_logging.xml +++ b/tests/_files/configuration_logging.xml @@ -5,5 +5,6 @@ + diff --git a/tests/end-to-end/_files/output-cli-help-color.txt b/tests/end-to-end/_files/output-cli-help-color.txt index 641a2ed8485..eea4bf781dc 100644 --- a/tests/end-to-end/_files/output-cli-help-color.txt +++ b/tests/end-to-end/_files/output-cli-help-color.txt @@ -118,6 +118,8 @@ Logging: + --log-xml   Write test results in PHPUnit XML format to + file --log-junit   Write test results in JUnit XML format to file --log-teamcity   Write test results in TeamCity format to diff --git a/tests/end-to-end/_files/output-cli-usage.txt b/tests/end-to-end/_files/output-cli-usage.txt index 03a38c7b8f6..0ec21e6dc82 100644 --- a/tests/end-to-end/_files/output-cli-usage.txt +++ b/tests/end-to-end/_files/output-cli-usage.txt @@ -91,6 +91,7 @@ Reporting: Logging: + --log-xml Write test results in PHPUnit XML format to file --log-junit Write test results in JUnit XML format to file --log-teamcity Write test results in TeamCity format to file --testdox-html Write test results in TestDox format (HTML) to file diff --git a/tests/end-to-end/logging/log-xml-to-file.phpt b/tests/end-to-end/logging/log-xml-to-file.phpt new file mode 100644 index 00000000000..93adda75a04 --- /dev/null +++ b/tests/end-to-end/logging/log-xml-to-file.phpt @@ -0,0 +1,27 @@ +--TEST-- +phpunit --log-xml logfile.xml _files/StatusTest.php +--SKIPIF-- +run($_SERVER['argv']); + +print file_get_contents($logfile); + +unlink($logfile); +--EXPECTF-- +todo diff --git a/tests/end-to-end/logging/log-xml-to-stdout.phpt b/tests/end-to-end/logging/log-xml-to-stdout.phpt new file mode 100644 index 00000000000..5a6cc1f6019 --- /dev/null +++ b/tests/end-to-end/logging/log-xml-to-stdout.phpt @@ -0,0 +1,16 @@ +--TEST-- +phpunit --log-xml php://stdout _files/StatusTest.php +--FILE-- +run($_SERVER['argv']); +--EXPECTF-- +todo diff --git a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php index a29b3d6678c..c522d9d0183 100644 --- a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php +++ b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php @@ -257,6 +257,9 @@ public function testLoggingConfigurationIsReadCorrectly(): void $this->assertTrue($logging->hasTestDoxText()); $this->assertSame(TEST_FILES_PATH . 'testdox.txt', $logging->testDoxText()->target()->path()); + + $this->assertTrue($logging->hasXml()); + $this->assertSame(TEST_FILES_PATH . 'phpunit.xml', $logging->xml()->target()->path()); } public function testPHPConfigurationIsReadCorrectly(): void