diff --git a/phpunit.xsd b/phpunit.xsd
index ec7af3048bf..07b01d6fc3e 100644
--- a/phpunit.xsd
+++ b/phpunit.xsd
@@ -74,6 +74,7 @@
+
diff --git a/src/Runner/CodeCoverage.php b/src/Runner/CodeCoverage.php
index e0f5a402172..9a0ad32db1b 100644
--- a/src/Runner/CodeCoverage.php
+++ b/src/Runner/CodeCoverage.php
@@ -10,7 +10,9 @@
namespace PHPUnit\Runner;
use function assert;
+use function class_exists;
use function implode;
+use function is_subclass_of;
use function sprintf;
use function sys_get_temp_dir;
use DateTimeImmutable;
@@ -20,6 +22,7 @@
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Output\Printer;
use PHPUnit\Util\Filesystem;
+use ReflectionClass;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\Driver\Granularity;
use SebastianBergmann\CodeCoverage\Driver\Selector;
@@ -82,10 +85,17 @@ public function init(Configuration $configuration, CodeCoverageFilterRegistry $c
return CodeCoverageInitializationStatus::NOT_REQUESTED;
}
+ $coverageDriver = null;
+
+ if ($configuration->hasCoverageDriver()) {
+ $coverageDriver = $configuration->coverageDriver();
+ }
+
$this->activate(
$codeCoverageFilterRegistry->get(),
$configuration->branchCoverage(),
$configuration->pathCoverage(),
+ $coverageDriver,
);
if (!$this->isActive()) {
@@ -512,7 +522,7 @@ public function warnIfFilterIsNotConfigured(CodeCoverageFilterRegistry $codeCove
$this->deactivate();
}
- private function activate(Filter $filter, bool $branchCoverage, bool $pathCoverage): void
+ private function activate(Filter $filter, bool $branchCoverage, bool $pathCoverage, ?string $driverClass = null): void
{
try {
$granularity = Granularity::Line;
@@ -522,19 +532,15 @@ private function activate(Filter $filter, bool $branchCoverage, bool $pathCovera
}
if ($pathCoverage) {
- $granularity = Granularity::LineBranchAndPath;
- }
-
- /**
- * @todo This needs to be removed once code coverage drivers are supported that can collect branch coverage without path coverage
- */
- if ($branchCoverage || $pathCoverage) {
$branchCoverage = true;
- $pathCoverage = true;
$granularity = Granularity::LineBranchAndPath;
}
- $this->driver = (new Selector)->select($filter, $granularity);
+ if ($driverClass !== null) {
+ $this->driver = $this->instantiateDriver($driverClass, $filter, $granularity);
+ } else {
+ $this->driver = (new Selector)->select($filter, $granularity);
+ }
$this->codeCoverage = new \SebastianBergmann\CodeCoverage\CodeCoverage(
$this->driver,
@@ -554,6 +560,60 @@ private function activate(Filter $filter, bool $branchCoverage, bool $pathCovera
}
}
+ /**
+ * @phpstan-ignore return.internalClass
+ */
+ private function instantiateDriver(string $driverClass, Filter $filter, Granularity $granularity): Driver
+ {
+ if (!class_exists($driverClass)) {
+ throw new CodeCoverageDriverException(
+ sprintf(
+ 'Configured code coverage driver class "%s" does not exist',
+ $driverClass,
+ ),
+ );
+ }
+
+ /** @phpstan-ignore classConstant.internalClass */
+ if (!is_subclass_of($driverClass, Driver::class)) {
+ throw new CodeCoverageDriverException(
+ sprintf(
+ 'Configured code coverage driver class "%s" does not extend %s',
+ $driverClass,
+ /** @phpstan-ignore classConstant.internalClass */
+ Driver::class,
+ ),
+ );
+ }
+
+ $reflection = new ReflectionClass($driverClass);
+
+ if (!$reflection->isInstantiable()) {
+ throw new CodeCoverageDriverException(
+ sprintf(
+ 'Configured code coverage driver class "%s" is not instantiable',
+ $driverClass,
+ ),
+ );
+ }
+
+ $constructor = $reflection->getConstructor();
+
+ if ($constructor !== null && $constructor->getNumberOfRequiredParameters() > 0) {
+ $driver = $reflection->newInstance($filter);
+ } else {
+ $driver = $reflection->newInstance();
+ }
+
+ /** @phpstan-ignore instanceof.internalClass */
+ assert($driver instanceof Driver);
+
+ /** @phpstan-ignore method.internalClass */
+ $driver->setGranularity($granularity);
+
+ return $driver;
+ }
+
private function codeCoverageGenerationStart(Printer $printer, string $format): void
{
$printer->print(
diff --git a/src/Runner/Exception/CodeCoverageDriverException.php b/src/Runner/Exception/CodeCoverageDriverException.php
new file mode 100644
index 00000000000..cc85ced1afb
--- /dev/null
+++ b/src/Runner/Exception/CodeCoverageDriverException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner;
+
+use RuntimeException;
+
+/**
+ * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
+ *
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class CodeCoverageDriverException extends RuntimeException implements Exception
+{
+}
diff --git a/src/TextUI/Configuration/Configuration.php b/src/TextUI/Configuration/Configuration.php
index 7bed93137af..74e39129bce 100644
--- a/src/TextUI/Configuration/Configuration.php
+++ b/src/TextUI/Configuration/Configuration.php
@@ -63,6 +63,7 @@
private Source $source;
private bool $pathCoverage;
private bool $branchCoverage;
+ private ?string $coverageDriver;
/**
* @var ?non-empty-string
@@ -553,7 +554,7 @@
* @param null|non-empty-string $generateBaseline
* @param non-negative-int $shortenArraysForExportThreshold
*/
- public function __construct(array $cliArguments, ?string $testFilesFile, ?string $configurationFile, ?string $bootstrap, array $bootstrapForTestSuite, 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 $coverageHtmlColorSuccessLowDark, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessMediumDark, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorSuccessHighDark, string $coverageHtmlColorSuccessBar, string $coverageHtmlColorSuccessBarDark, string $coverageHtmlColorWarning, string $coverageHtmlColorWarningDark, string $coverageHtmlColorWarningBar, string $coverageHtmlColorWarningBarDark, string $coverageHtmlColorDanger, string $coverageHtmlColorDangerDark, string $coverageHtmlColorDangerBar, string $coverageHtmlColorDangerBarDark, string $coverageHtmlColorBreadcrumbs, string $coverageHtmlColorBreadcrumbsDark, ?string $coverageHtmlCustomCssFile, ?string $coverageOpenClover, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $coverageXmlIncludeSource, bool $pathCoverage, bool $branchCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $disableCoverageTargeting, bool $failOnAllIssues, bool $failOnDeprecation, bool $failOnPhpunitDeprecation, bool $failOnPhpunitNotice, bool $failOnPhpunitWarning, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $doNotFailOnDeprecation, bool $doNotFailOnPhpunitDeprecation, bool $doNotFailOnPhpunitNotice, bool $doNotFailOnPhpunitWarning, bool $doNotFailOnEmptyTestSuite, bool $doNotFailOnIncomplete, bool $doNotFailOnNotice, bool $doNotFailOnRisky, bool $doNotFailOnSkipped, bool $doNotFailOnWarning, int $stopOnDefect, int $stopOnDeprecation, ?string $specificDeprecationToStopOn, int $stopOnError, int $stopOnFailure, int $stopOnIncomplete, int $stopOnNotice, int $stopOnRisky, int $stopOnSkipped, int $stopOnWarning, bool $outputToStandardErrorStream, int $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $diffContext, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $requireCoverageContribution, bool $disallowTestOutput, bool $displayDetailsOnAllIssues, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnPhpunitDeprecations, bool $displayDetailsOnPhpunitNotices, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $requireSealedMockObjects, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileOtr, bool $includeGitInformation, bool $includeGitInformationInOtrLogfile, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $compactOutput, bool $teamCityOutput, bool $testDoxOutput, bool $testDoxOutputSummary, ?array $testsCovering, ?array $testsUsing, ?array $testsRequiringPhpExtension, ?string $filter, ?string $excludeFilter, ?string $testIdFilterFile, ?string $testIdFilter, array $groups, array $excludeGroups, int $randomOrderSeed, int $repeat, int $retry, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, bool $ignoreTestSelectionInXmlConfiguration, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection, ?string $generateBaseline, bool $debug, bool $withTelemetry, int $shortenArraysForExportThreshold)
+ public function __construct(array $cliArguments, ?string $testFilesFile, ?string $configurationFile, ?string $bootstrap, array $bootstrapForTestSuite, 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 $coverageHtmlColorSuccessLowDark, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessMediumDark, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorSuccessHighDark, string $coverageHtmlColorSuccessBar, string $coverageHtmlColorSuccessBarDark, string $coverageHtmlColorWarning, string $coverageHtmlColorWarningDark, string $coverageHtmlColorWarningBar, string $coverageHtmlColorWarningBarDark, string $coverageHtmlColorDanger, string $coverageHtmlColorDangerDark, string $coverageHtmlColorDangerBar, string $coverageHtmlColorDangerBarDark, string $coverageHtmlColorBreadcrumbs, string $coverageHtmlColorBreadcrumbsDark, ?string $coverageHtmlCustomCssFile, ?string $coverageOpenClover, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $coverageXmlIncludeSource, bool $pathCoverage, bool $branchCoverage, ?string $coverageDriver, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $disableCoverageTargeting, bool $failOnAllIssues, bool $failOnDeprecation, bool $failOnPhpunitDeprecation, bool $failOnPhpunitNotice, bool $failOnPhpunitWarning, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $doNotFailOnDeprecation, bool $doNotFailOnPhpunitDeprecation, bool $doNotFailOnPhpunitNotice, bool $doNotFailOnPhpunitWarning, bool $doNotFailOnEmptyTestSuite, bool $doNotFailOnIncomplete, bool $doNotFailOnNotice, bool $doNotFailOnRisky, bool $doNotFailOnSkipped, bool $doNotFailOnWarning, int $stopOnDefect, int $stopOnDeprecation, ?string $specificDeprecationToStopOn, int $stopOnError, int $stopOnFailure, int $stopOnIncomplete, int $stopOnNotice, int $stopOnRisky, int $stopOnSkipped, int $stopOnWarning, bool $outputToStandardErrorStream, int $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $diffContext, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $requireCoverageContribution, bool $disallowTestOutput, bool $displayDetailsOnAllIssues, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnPhpunitDeprecations, bool $displayDetailsOnPhpunitNotices, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $requireSealedMockObjects, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileOtr, bool $includeGitInformation, bool $includeGitInformationInOtrLogfile, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $compactOutput, bool $teamCityOutput, bool $testDoxOutput, bool $testDoxOutputSummary, ?array $testsCovering, ?array $testsUsing, ?array $testsRequiringPhpExtension, ?string $filter, ?string $excludeFilter, ?string $testIdFilterFile, ?string $testIdFilter, array $groups, array $excludeGroups, int $randomOrderSeed, int $repeat, int $retry, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, bool $ignoreTestSelectionInXmlConfiguration, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection, ?string $generateBaseline, bool $debug, bool $withTelemetry, int $shortenArraysForExportThreshold)
{
$this->cliArguments = $cliArguments;
$this->testFilesFile = $testFilesFile;
@@ -600,6 +601,7 @@ public function __construct(array $cliArguments, ?string $testFilesFile, ?string
$this->coverageXmlIncludeSource = $coverageXmlIncludeSource;
$this->pathCoverage = $pathCoverage;
$this->branchCoverage = $branchCoverage;
+ $this->coverageDriver = $coverageDriver;
$this->ignoreDeprecatedCodeUnitsFromCodeCoverage = $ignoreDeprecatedCodeUnitsFromCodeCoverage;
$this->disableCodeCoverageIgnore = $disableCodeCoverageIgnore;
$this->disableCoverageTargeting = $disableCoverageTargeting;
@@ -890,6 +892,26 @@ public function branchCoverage(): bool
return $this->branchCoverage;
}
+ /**
+ * @phpstan-assert-if-true !null $this->coverageDriver
+ */
+ public function hasCoverageDriver(): bool
+ {
+ return $this->coverageDriver !== null;
+ }
+
+ /**
+ * @throws CodeCoverageDriverNotConfiguredException
+ */
+ public function coverageDriver(): string
+ {
+ if (!$this->hasCoverageDriver()) {
+ throw new CodeCoverageDriverNotConfiguredException;
+ }
+
+ return $this->coverageDriver;
+ }
+
public function hasCoverageReport(): bool
{
return $this->hasCoverageClover() ||
diff --git a/src/TextUI/Configuration/Exception/CodeCoverageDriverNotConfiguredException.php b/src/TextUI/Configuration/Exception/CodeCoverageDriverNotConfiguredException.php
new file mode 100644
index 00000000000..936a266b1c4
--- /dev/null
+++ b/src/TextUI/Configuration/Exception/CodeCoverageDriverNotConfiguredException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TextUI\Configuration;
+
+use RuntimeException;
+
+/**
+ * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
+ *
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class CodeCoverageDriverNotConfiguredException extends RuntimeException implements Exception
+{
+}
diff --git a/src/TextUI/Configuration/Merger.php b/src/TextUI/Configuration/Merger.php
index 63ad74d8234..bbb0f308a95 100644
--- a/src/TextUI/Configuration/Merger.php
+++ b/src/TextUI/Configuration/Merger.php
@@ -413,6 +413,12 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$branchCoverage = $xmlConfiguration->codeCoverage()->branchCoverage();
}
+ $coverageDriver = null;
+
+ if ($xmlConfiguration->codeCoverage()->hasDriver()) {
+ $coverageDriver = $xmlConfiguration->codeCoverage()->driver();
+ }
+
$defaultColors = Colors::default();
$defaultThresholds = Thresholds::default();
@@ -1200,6 +1206,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$coverageXmlIncludeSource,
$pathCoverage,
$branchCoverage,
+ $coverageDriver,
$xmlConfiguration->codeCoverage()->ignoreDeprecatedCodeUnits(),
$disableCodeCoverageIgnore,
$disableCoverageTargeting,
diff --git a/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php b/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php
index 32f7943632a..35e6ecdffac 100644
--- a/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php
+++ b/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php
@@ -28,6 +28,7 @@
*/
final readonly class CodeCoverage
{
+ private ?string $driver;
private bool $pathCoverage;
private bool $branchCoverage;
private bool $includeUncoveredFiles;
@@ -42,8 +43,9 @@
private ?Text $text;
private ?Xml $xml;
- public function __construct(bool $pathCoverage, bool $branchCoverage, bool $includeUncoveredFiles, bool $ignoreDeprecatedCodeUnits, bool $disableCodeCoverageIgnore, ?Clover $clover, ?Cobertura $cobertura, ?Crap4j $crap4j, ?Html $html, ?OpenClover $openClover, ?Php $php, ?Text $text, ?Xml $xml)
+ public function __construct(?string $driver, bool $pathCoverage, bool $branchCoverage, bool $includeUncoveredFiles, bool $ignoreDeprecatedCodeUnits, bool $disableCodeCoverageIgnore, ?Clover $clover, ?Cobertura $cobertura, ?Crap4j $crap4j, ?Html $html, ?OpenClover $openClover, ?Php $php, ?Text $text, ?Xml $xml)
{
+ $this->driver = $driver;
$this->pathCoverage = $pathCoverage;
$this->branchCoverage = $branchCoverage;
$this->includeUncoveredFiles = $includeUncoveredFiles;
@@ -59,6 +61,28 @@ public function __construct(bool $pathCoverage, bool $branchCoverage, bool $incl
$this->xml = $xml;
}
+ /**
+ * @phpstan-assert-if-true !null $this->driver
+ */
+ public function hasDriver(): bool
+ {
+ return $this->driver !== null;
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function driver(): string
+ {
+ if (!$this->hasDriver()) {
+ throw new Exception(
+ 'Code Coverage driver has not been configured',
+ );
+ }
+
+ return $this->driver;
+ }
+
public function pathCoverage(): bool
{
return $this->pathCoverage;
diff --git a/src/TextUI/Configuration/Xml/DefaultConfiguration.php b/src/TextUI/Configuration/Xml/DefaultConfiguration.php
index cfa66aec381..c1d8d76d08b 100644
--- a/src/TextUI/Configuration/Xml/DefaultConfiguration.php
+++ b/src/TextUI/Configuration/Xml/DefaultConfiguration.php
@@ -64,6 +64,7 @@ public static function create(): self
true,
),
new CodeCoverage(
+ null,
false,
false,
true,
diff --git a/src/TextUI/Configuration/Xml/Loader.php b/src/TextUI/Configuration/Xml/Loader.php
index 0b4f464d195..640fc437e74 100644
--- a/src/TextUI/Configuration/Xml/Loader.php
+++ b/src/TextUI/Configuration/Xml/Loader.php
@@ -453,6 +453,7 @@ private function source(string $filename, DOMXPath $xpath): Source
private function codeCoverage(string $filename, DOMXPath $xpath): CodeCoverage
{
+ $driver = null;
$pathCoverage = false;
$branchCoverage = false;
$includeUncoveredFiles = true;
@@ -462,6 +463,8 @@ private function codeCoverage(string $filename, DOMXPath $xpath): CodeCoverage
$element = $this->element($xpath, 'coverage');
if ($element !== null) {
+ $driver = $this->parseStringAttribute($element, 'driver');
+
$pathCoverage = $this->parseBooleanAttribute(
$element,
'pathCoverage',
@@ -639,6 +642,7 @@ private function codeCoverage(string $filename, DOMXPath $xpath): CodeCoverage
}
return new CodeCoverage(
+ $driver,
$pathCoverage,
$branchCoverage,
$includeUncoveredFiles,
diff --git a/tests/_files/configuration_codecoverage_driver.xml b/tests/_files/configuration_codecoverage_driver.xml
new file mode 100644
index 00000000000..d3f162c160e
--- /dev/null
+++ b/tests/_files/configuration_codecoverage_driver.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ /path/to/files
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-does-not-exist.xml b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-does-not-exist.xml
new file mode 100644
index 00000000000..42cdf4822b3
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-does-not-exist.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-does-not-extend-driver.xml b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-does-not-extend-driver.xml
new file mode 100644
index 00000000000..437d998bf94
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-does-not-extend-driver.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-is-not-instantiable.xml b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-is-not-instantiable.xml
new file mode 100644
index 00000000000..10a891f1def
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-class-is-not-instantiable.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-branch-coverage.xml b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-branch-coverage.xml
new file mode 100644
index 00000000000..c6a40928b2c
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-branch-coverage.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src/Foo.php
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-fake-data.xml b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-fake-data.xml
new file mode 100644
index 00000000000..ab19f36f07b
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-fake-data.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src/Foo.php
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-no-required-constructor-arguments.xml b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-no-required-constructor-arguments.xml
new file mode 100644
index 00000000000..f53d9348fab
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-no-required-constructor-arguments.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-required-constructor-arguments.xml b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-required-constructor-arguments.xml
new file mode 100644
index 00000000000..d857e843ae0
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/phpunit-with-required-constructor-arguments.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/AbstractCustomDriver.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/AbstractCustomDriver.php
new file mode 100644
index 00000000000..9811fccf928
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/AbstractCustomDriver.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+use SebastianBergmann\CodeCoverage\Driver\Driver;
+
+abstract class AbstractCustomDriver extends Driver
+{
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriver.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriver.php
new file mode 100644
index 00000000000..772cc43aaeb
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriver.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
+use SebastianBergmann\CodeCoverage\Driver\Driver;
+
+final class CustomDriver extends Driver
+{
+ public function name(): string
+ {
+ return 'CustomDriver';
+ }
+
+ public function version(): string
+ {
+ return '1.0.0';
+ }
+
+ public function start(): void
+ {
+ }
+
+ public function stop(): RawCodeCoverageData
+ {
+ return RawCodeCoverageData::fromXdebugWithoutPathCoverage([]);
+ }
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithBranchCoverage.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithBranchCoverage.php
new file mode 100644
index 00000000000..15b91b19070
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithBranchCoverage.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+use function realpath;
+use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
+use SebastianBergmann\CodeCoverage\Driver\Driver;
+
+/**
+ * A driver that can collect branch coverage but not path coverage.
+ */
+final class CustomDriverWithBranchCoverage extends Driver
+{
+ public function name(): string
+ {
+ return 'CustomDriverWithBranchCoverage';
+ }
+
+ public function version(): string
+ {
+ return '1.0.0';
+ }
+
+ public function start(): void
+ {
+ }
+
+ public function stop(): RawCodeCoverageData
+ {
+ $file = realpath(__DIR__ . '/Foo.php');
+
+ return RawCodeCoverageData::fromLineAndBranchCoverage(
+ [
+ $file => [
+ 16 => Driver::LINE_EXECUTED,
+ ],
+ ],
+ [
+ $file => [
+ 'PHPUnit\TestFixture\CodeCoverageDriver\Foo->value' => [
+ 'branches' => [
+ 0 => [
+ 'op_start' => 0,
+ 'op_end' => 1,
+ 'line_start' => 14,
+ 'line_end' => 16,
+ 'hit' => Driver::BRANCH_HIT,
+ 'out' => [],
+ 'out_hit' => [],
+ ],
+ ],
+ 'paths' => [],
+ ],
+ ],
+ ],
+ );
+ }
+
+ protected function canCollectBranchCoverage(): bool
+ {
+ return true;
+ }
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithFakeData.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithFakeData.php
new file mode 100644
index 00000000000..17ca9672c80
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithFakeData.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+use function realpath;
+use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
+use SebastianBergmann\CodeCoverage\Driver\Driver;
+
+final class CustomDriverWithFakeData extends Driver
+{
+ public function name(): string
+ {
+ return 'CustomDriverWithFakeData';
+ }
+
+ public function version(): string
+ {
+ return '1.0.0';
+ }
+
+ public function start(): void
+ {
+ }
+
+ public function stop(): RawCodeCoverageData
+ {
+ return RawCodeCoverageData::fromXdebugWithoutPathCoverage(
+ [
+ realpath(__DIR__ . '/Foo.php') => [
+ 16 => Driver::LINE_EXECUTED,
+ ],
+ ],
+ );
+ }
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithFilter.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithFilter.php
new file mode 100644
index 00000000000..7416459e0a5
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/CustomDriverWithFilter.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
+use SebastianBergmann\CodeCoverage\Driver\Driver;
+use SebastianBergmann\CodeCoverage\Filter;
+
+final class CustomDriverWithFilter extends Driver
+{
+ private readonly Filter $filter;
+
+ public function __construct(Filter $filter)
+ {
+ $this->filter = $filter;
+ }
+
+ public function name(): string
+ {
+ return 'CustomDriverWithFilter';
+ }
+
+ public function version(): string
+ {
+ return '1.0.0';
+ }
+
+ public function start(): void
+ {
+ }
+
+ public function stop(): RawCodeCoverageData
+ {
+ return RawCodeCoverageData::fromXdebugWithoutPathCoverage([]);
+ }
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/Foo.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/Foo.php
new file mode 100644
index 00000000000..e89566876d9
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/Foo.php
@@ -0,0 +1,18 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+final class Foo
+{
+ public function value(): int
+ {
+ return 1;
+ }
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/NotADriver.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/NotADriver.php
new file mode 100644
index 00000000000..b8b8c31cd83
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/src/NotADriver.php
@@ -0,0 +1,14 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+final class NotADriver
+{
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/tests/FooTest.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/tests/FooTest.php
new file mode 100644
index 00000000000..0c94878f80e
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/tests/FooTest.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture\CodeCoverageDriver;
+
+use PHPUnit\Framework\TestCase;
+
+final class FooTest extends TestCase
+{
+ public function testValue(): void
+ {
+ $this->assertSame(1, (new Foo)->value());
+ }
+}
diff --git a/tests/end-to-end/code-coverage/_files/code-coverage-driver/vendor/autoload.php b/tests/end-to-end/code-coverage/_files/code-coverage-driver/vendor/autoload.php
new file mode 100644
index 00000000000..d9f42b5b801
--- /dev/null
+++ b/tests/end-to-end/code-coverage/_files/code-coverage-driver/vendor/autoload.php
@@ -0,0 +1,9 @@
+run($_SERVER['argv']);
+--CLEAN--
+run($_SERVER['argv']);
+--CLEAN--
+run($_SERVER['argv']);
+--CLEAN--
+run($_SERVER['argv']);
+--CLEAN--
+run($_SERVER['argv']);
+--CLEAN--
+run($_SERVER['argv']);
+--CLEAN--
+run($_SERVER['argv']);
+--CLEAN--
+coverageCacheDirectory();
}
+ public function testCoverageDriverThrowsWhenNotConfigured(): void
+ {
+ $configuration = $this->defaultConfiguration();
+
+ $this->assertFalse($configuration->hasCoverageDriver());
+
+ $this->expectException(CodeCoverageDriverNotConfiguredException::class);
+
+ $configuration->coverageDriver();
+ }
+
+ public function testReturnsConfiguredCoverageDriver(): void
+ {
+ $configuration = $this->configurationFromXml('configuration_codecoverage_driver.xml');
+
+ $this->assertTrue($configuration->hasCoverageDriver());
+ $this->assertSame('My\Custom\Driver', $configuration->coverageDriver());
+ }
+
public function testCoverageCloverThrowsWhenNotConfigured(): void
{
$configuration = $this->defaultConfiguration();
diff --git a/tests/unit/TextUI/Configuration/MergerTest.php b/tests/unit/TextUI/Configuration/MergerTest.php
index e48db8f6540..5f9dcd20de1 100644
--- a/tests/unit/TextUI/Configuration/MergerTest.php
+++ b/tests/unit/TextUI/Configuration/MergerTest.php
@@ -90,6 +90,18 @@ public function testCoverageTargetingIsNotDisabledByDefault(): void
$this->assertFalse($mergedConfig->disableCoverageTargeting());
}
+ public function testCoverageDriverIsCarriedOverFromXmlConfiguration(): void
+ {
+ $fromFile = (new Loader)->load(TEST_FILES_PATH . 'configuration_codecoverage_driver.xml');
+
+ $fromCli = (new Builder)->fromParameters([]);
+
+ $mergedConfig = (new Merger)->merge($fromCli, $fromFile);
+
+ $this->assertTrue($mergedConfig->hasCoverageDriver());
+ $this->assertSame('My\Custom\Driver', $mergedConfig->coverageDriver());
+ }
+
public function testNoCoverageShouldOnlyAffectXmlConfiguration(): void
{
$phpCoverage = uniqid('php_coverage_');
diff --git a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
index da1cc6e0a8a..343883df714 100644
--- a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
+++ b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
@@ -265,6 +265,7 @@ public function testCodeCoverageConfigurationIsReadCorrectly(): void
{
$codeCoverage = $this->configuration('configuration_codecoverage.xml')->codeCoverage();
+ $this->assertFalse($codeCoverage->hasDriver());
$this->assertTrue($codeCoverage->pathCoverage());
$this->assertTrue($codeCoverage->includeUncoveredFiles());
$this->assertTrue($codeCoverage->ignoreDeprecatedCodeUnits());
@@ -321,6 +322,26 @@ public function testCodeCoverageConfigurationIsReadCorrectly(): void
$this->assertSame(TEST_FILES_PATH . 'coverage', $codeCoverage->xml()->target()->path());
}
+ public function testCodeCoverageDriverConfigurationIsReadCorrectly(): void
+ {
+ $codeCoverage = $this->configuration('configuration_codecoverage_driver.xml')->codeCoverage();
+
+ $this->assertTrue($codeCoverage->hasDriver());
+ $this->assertSame('My\Custom\Driver', $codeCoverage->driver());
+ }
+
+ public function testCodeCoverageDriverAccessorThrowsWhenNotConfigured(): void
+ {
+ $codeCoverage = $this->configuration('configuration_codecoverage.xml')->codeCoverage();
+
+ $this->assertFalse($codeCoverage->hasDriver());
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Code Coverage driver has not been configured');
+
+ $codeCoverage->driver();
+ }
+
public function testGroupConfigurationIsReadCorrectly(): void
{
$groups = $this->configuration('configuration.xml')->groups();