diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml
index 1eab89355fd..486d65aee9b 100644
--- a/.psalm/baseline.xml
+++ b/.psalm/baseline.xml
@@ -576,6 +576,8 @@
)]]>
logfileJunit())]]>
logfileJunit())]]>
+ logfileXml())]]>
+ logfileXml())]]>
atLeastVersion
build
configurationFile
@@ -588,6 +590,7 @@
logEventsVerboseText
logfileJunit
logfileTeamcity
+ logfileXml
include_once $filename
diff --git a/phpunit.xsd b/phpunit.xsd
index c50b17ba02f..0922ca3c7fa 100644
--- a/phpunit.xsd
+++ b/phpunit.xsd
@@ -284,6 +284,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 7489e7412f7..90021539e81 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;
@@ -540,6 +541,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 97239fc9edc..6a22d029df9 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 1afff6a14aa..133e91c5e5b 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;
@@ -1301,6 +1303,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 fb1253d5e37..75937118a4b 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;
@@ -132,7 +133,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)
+ 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)
{
$this->cliArgument = $cliArgument;
$this->configurationFile = $configurationFile;
@@ -216,6 +217,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;
@@ -1001,6 +1003,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 5c0705dbb5b..3315c07039a 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 da1f8978f32..6fbd05aca0c 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 594697e6958..20b6cc8b80d 100644
--- a/src/TextUI/Configuration/Xml/Loader.php
+++ b/src/TextUI/Configuration/Xml/Loader.php
@@ -68,6 +68,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 @@
[33mLogging:[0m
+ [32m--log-xml [36m[0m [0m Write test results in PHPUnit XML format to
+ file
[32m--log-junit [36m[0m [0m Write test results in JUnit XML format to
file
[32m--log-teamcity [36m[0m [0m 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 293409f0358..68068206a36 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