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();