diff --git a/phpunit.xsd b/phpunit.xsd
index 231096f622..eb2e454c85 100644
--- a/phpunit.xsd
+++ b/phpunit.xsd
@@ -73,6 +73,7 @@
+
diff --git a/src/Runner/CodeCoverage.php b/src/Runner/CodeCoverage.php
index f273e04348..243b439151 100644
--- a/src/Runner/CodeCoverage.php
+++ b/src/Runner/CodeCoverage.php
@@ -10,6 +10,8 @@
namespace PHPUnit\Runner;
use function assert;
+use function class_exists;
+use function is_subclass_of;
use function sprintf;
use function sys_get_temp_dir;
use DateTimeImmutable;
@@ -19,6 +21,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\Selector;
use SebastianBergmann\CodeCoverage\Exception as CodeCoverageException;
@@ -76,7 +79,11 @@ public function init(Configuration $configuration, CodeCoverageFilterRegistry $c
return CodeCoverageInitializationStatus::NOT_REQUESTED;
}
- $this->activate($codeCoverageFilterRegistry->get(), $configuration->pathCoverage());
+ $this->activate(
+ $codeCoverageFilterRegistry->get(),
+ $configuration->pathCoverage(),
+ $configuration->hasCoverageDriver() ? $configuration->coverageDriver() : null,
+ );
if (!$this->isActive()) {
return CodeCoverageInitializationStatus::FAILED;
@@ -428,10 +435,12 @@ public function generateReports(Printer $printer, Configuration $configuration):
}
}
- private function activate(Filter $filter, bool $pathCoverage): void
+ private function activate(Filter $filter, bool $pathCoverage, ?string $driverClass = null): void
{
try {
- if ($pathCoverage) {
+ if ($driverClass !== null) {
+ $this->driver = $this->instantiateDriver($driverClass, $filter, $pathCoverage);
+ } elseif ($pathCoverage) {
$this->driver = (new Selector)->forLineAndPathCoverage($filter);
} else {
$this->driver = (new Selector)->forLineCoverage($filter);
@@ -448,6 +457,62 @@ private function activate(Filter $filter, bool $pathCoverage): void
}
}
+ /**
+ * @phpstan-ignore return.internalClass
+ */
+ private function instantiateDriver(string $driverClass, Filter $filter, bool $pathCoverage): 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);
+
+ if ($pathCoverage) {
+ /** @phpstan-ignore method.internalClass */
+ $driver->enableBranchAndPathCoverage();
+ }
+
+ 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 0000000000..cc85ced1af
--- /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 c77fa952b9..777a545989 100644
--- a/src/TextUI/Configuration/Configuration.php
+++ b/src/TextUI/Configuration/Configuration.php
@@ -40,6 +40,7 @@
private ?string $coverageCacheDirectory;
private Source $source;
private bool $pathCoverage;
+ private ?string $coverageDriver;
private ?string $coverageClover;
private ?string $coverageCobertura;
private ?string $coverageCrap4j;
@@ -232,7 +233,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 $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, 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, bool $stopOnDefect, bool $stopOnDeprecation, ?string $specificDeprecationToStopOn, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $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 $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, 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 $teamCityOutput, bool $testDoxOutput, bool $testDoxOutputSummary, ?array $testsCovering, ?array $testsUsing, ?array $testsRequiringPhpExtension, ?string $filter, ?string $excludeFilter, array $groups, array $excludeGroups, int $randomOrderSeed, 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, ?string $coverageDriver, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, 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, bool $stopOnDefect, bool $stopOnDeprecation, ?string $specificDeprecationToStopOn, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $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 $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, 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 $teamCityOutput, bool $testDoxOutput, bool $testDoxOutputSummary, ?array $testsCovering, ?array $testsUsing, ?array $testsRequiringPhpExtension, ?string $filter, ?string $excludeFilter, array $groups, array $excludeGroups, int $randomOrderSeed, 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;
@@ -278,6 +279,7 @@ public function __construct(array $cliArguments, ?string $testFilesFile, ?string
$this->coverageXml = $coverageXml;
$this->coverageXmlIncludeSource = $coverageXmlIncludeSource;
$this->pathCoverage = $pathCoverage;
+ $this->coverageDriver = $coverageDriver;
$this->ignoreDeprecatedCodeUnitsFromCodeCoverage = $ignoreDeprecatedCodeUnitsFromCodeCoverage;
$this->disableCodeCoverageIgnore = $disableCodeCoverageIgnore;
$this->failOnAllIssues = $failOnAllIssues;
@@ -537,6 +539,19 @@ public function pathCoverage(): bool
return $this->pathCoverage;
}
+ /**
+ * @phpstan-assert-if-true !null $this->coverageDriver
+ */
+ public function hasCoverageDriver(): bool
+ {
+ return $this->coverageDriver !== null;
+ }
+
+ public function coverageDriver(): string
+ {
+ return $this->coverageDriver;
+ }
+
public function hasCoverageReport(): bool
{
return $this->hasCoverageClover() ||
diff --git a/src/TextUI/Configuration/Merger.php b/src/TextUI/Configuration/Merger.php
index 64fa0d7d87..4a5ccc8758 100644
--- a/src/TextUI/Configuration/Merger.php
+++ b/src/TextUI/Configuration/Merger.php
@@ -355,6 +355,12 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$pathCoverage = $xmlConfiguration->codeCoverage()->pathCoverage();
}
+ $coverageDriver = null;
+
+ if ($xmlConfiguration->codeCoverage()->hasDriver()) {
+ $coverageDriver = $xmlConfiguration->codeCoverage()->driver();
+ }
+
$defaultColors = Colors::default();
$defaultThresholds = Thresholds::default();
@@ -1020,6 +1026,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$coverageXml,
$coverageXmlIncludeSource,
$pathCoverage,
+ $coverageDriver,
$xmlConfiguration->codeCoverage()->ignoreDeprecatedCodeUnits(),
$disableCodeCoverageIgnore,
$failOnAllIssues,
diff --git a/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php b/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php
index d66f58f1c4..90b05e439b 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 $includeUncoveredFiles;
private bool $ignoreDeprecatedCodeUnits;
@@ -41,8 +42,9 @@
private ?Text $text;
private ?Xml $xml;
- public function __construct(bool $pathCoverage, 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 $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->includeUncoveredFiles = $includeUncoveredFiles;
$this->ignoreDeprecatedCodeUnits = $ignoreDeprecatedCodeUnits;
@@ -57,6 +59,28 @@ public function __construct(bool $pathCoverage, bool $includeUncoveredFiles, boo
$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 73134c4d26..46fb850cab 100644
--- a/src/TextUI/Configuration/Xml/DefaultConfiguration.php
+++ b/src/TextUI/Configuration/Xml/DefaultConfiguration.php
@@ -63,6 +63,7 @@ public static function create(): self
true,
),
new CodeCoverage(
+ null,
false,
true,
false,
diff --git a/src/TextUI/Configuration/Xml/Loader.php b/src/TextUI/Configuration/Xml/Loader.php
index 1cd8ded5aa..6abdba2c36 100644
--- a/src/TextUI/Configuration/Xml/Loader.php
+++ b/src/TextUI/Configuration/Xml/Loader.php
@@ -403,6 +403,7 @@ private function source(string $filename, DOMXPath $xpath): Source
private function codeCoverage(string $filename, DOMXPath $xpath): CodeCoverage
{
+ $driver = null;
$pathCoverage = false;
$includeUncoveredFiles = true;
$ignoreDeprecatedCodeUnits = false;
@@ -411,6 +412,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',
@@ -582,6 +585,7 @@ private function codeCoverage(string $filename, DOMXPath $xpath): CodeCoverage
}
return new CodeCoverage(
+ $driver,
$pathCoverage,
$includeUncoveredFiles,
$ignoreDeprecatedCodeUnits,
diff --git a/tests/_files/configuration_codecoverage_driver.xml b/tests/_files/configuration_codecoverage_driver.xml
new file mode 100644
index 0000000000..d3f162c160
--- /dev/null
+++ b/tests/_files/configuration_codecoverage_driver.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ /path/to/files
+
+
+
+
+
+
+
+
+
diff --git a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
index ff3902155c..29500690f8 100644
--- a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
+++ b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
@@ -197,6 +197,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());
@@ -253,6 +254,14 @@ 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 testGroupConfigurationIsReadCorrectly(): void
{
$groups = $this->configuration('configuration.xml')->groups();