diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml
index 57b50809945..5cb472b87b1 100644
--- a/.psalm/baseline.xml
+++ b/.psalm/baseline.xml
@@ -676,6 +676,8 @@
)]]>
logfileJunit())]]>
logfileJunit())]]>
+ logfileXml())]]>
+ logfileXml())]]>
atLeastVersion
build
configurationFile
@@ -688,6 +690,7 @@
logEventsVerboseText
logfileJunit
logfileTeamcity
+ logfileXml
include_once $filename
diff --git a/phpunit.xsd b/phpunit.xsd
index bd22b2ca2a7..1ded398b4c3 100644
--- a/phpunit.xsd
+++ b/phpunit.xsd
@@ -287,6 +287,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 f4d0a8b7144..c4b58c0824f 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\Baseline\CannotLoadBaselineException;
use PHPUnit\Runner\Baseline\Generator as BaselineGenerator;
@@ -569,6 +570,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 034656bf7a6..3f8385f8aec 100644
--- a/src/TextUI/Configuration/Cli/Builder.php
+++ b/src/TextUI/Configuration/Cli/Builder.php
@@ -76,6 +76,7 @@ final class Builder
'list-tests-xml=',
'log-junit=',
'log-teamcity=',
+ 'log-xml=',
'migrate-configuration',
'no-configuration',
'no-coverage',
@@ -211,6 +212,7 @@ public function fromParameters(array $parameters): Configuration
$includePath = null;
$iniSettings = [];
$junitLogfile = null;
+ $xmlLogfile = null;
$listGroups = false;
$listSuites = false;
$listTests = false;
@@ -471,6 +473,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) {
@@ -883,6 +890,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 cca980eadaa..0afa22a7e44 100644
--- a/src/TextUI/Configuration/Cli/Configuration.php
+++ b/src/TextUI/Configuration/Cli/Configuration.php
@@ -80,6 +80,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;
@@ -125,7 +126,7 @@ final class Configuration
* @psalm-param list $arguments
* @psalm-param ?non-empty-list $testSuffixes
*/
- public function __construct(array $arguments, ?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, ?string $generateBaseline, ?string $useBaseline, bool $ignoreBaseline, 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(array $arguments, ?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, ?string $generateBaseline, ?string $useBaseline, bool $ignoreBaseline, 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->arguments = $arguments;
$this->atLeastVersion = $atLeastVersion;
@@ -188,6 +189,7 @@ public function __construct(array $arguments, ?string $atLeastVersion, ?bool $ba
$this->help = $help;
$this->includePath = $includePath;
$this->iniSettings = $iniSettings;
+ $this->xmlLogfile = $xmlLogfile;
$this->junitLogfile = $junitLogfile;
$this->listGroups = $listGroups;
$this->listSuites = $listSuites;
@@ -1352,6 +1354,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 72cea86e9e2..ca3fc694d25 100644
--- a/src/TextUI/Configuration/Configuration.php
+++ b/src/TextUI/Configuration/Configuration.php
@@ -114,6 +114,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;
@@ -145,7 +146,7 @@ final class Configuration
* @psalm-param non-empty-list $testSuffixes
* @psalm-param list}> $extensionBootstrappers
*/
- public function __construct(array $cliArguments, ?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, ?string $generateBaseline)
+ public function __construct(array $cliArguments, ?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, ?string $generateBaseline)
{
$this->cliArguments = $cliArguments;
$this->configurationFile = $configurationFile;
@@ -229,6 +230,7 @@ public function __construct(array $cliArguments, ?string $configurationFile, ?st
$this->logfileJunit = $logfileJunit;
$this->logfileTestdoxHtml = $logfileTestdoxHtml;
$this->logfileTestdoxText = $logfileTestdoxText;
+ $this->logfileXml = $logfileXml;
$this->logEventsText = $logEventsText;
$this->logEventsVerboseText = $logEventsVerboseText;
$this->teamCityOutput = $teamCityOutput;
@@ -1041,6 +1043,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 287b1916e48..ef71653a479 100644
--- a/src/TextUI/Configuration/Merger.php
+++ b/src/TextUI/Configuration/Merger.php
@@ -510,6 +510,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$logfileJunit = null;
$logfileTestdoxHtml = null;
$logfileTestdoxText = null;
+ $logfileXml = null;
$loggingFromXmlConfiguration = true;
if ($cliConfiguration->hasNoLogging() && $cliConfiguration->noLogging()) {
@@ -540,6 +541,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()) {
@@ -821,6 +828,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 5c652e95b32..a2c7e7ff9bf 100644
--- a/src/TextUI/Configuration/Xml/DefaultConfiguration.php
+++ b/src/TextUI/Configuration/Xml/DefaultConfiguration.php
@@ -80,6 +80,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 2c219292fb5..aaa37cbb624 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 23b4ab121ca..45c979ec9a5 100644
--- a/src/TextUI/Help.php
+++ b/src/TextUI/Help.php
@@ -129,6 +129,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 60af5ed4ae3..06ead65917e 100644
--- a/tests/end-to-end/_files/output-cli-help-color.txt
+++ b/tests/end-to-end/_files/output-cli-help-color.txt
@@ -120,6 +120,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 a3a03dd4de9..fbb56dae38e 100644
--- a/tests/end-to-end/_files/output-cli-usage.txt
+++ b/tests/end-to-end/_files/output-cli-usage.txt
@@ -93,6 +93,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 6878a49f67a..524a805c457 100644
--- a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
+++ b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
@@ -261,6 +261,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