diff --git a/devTools/phpstan-src-baseline.neon b/devTools/phpstan-src-baseline.neon index 687b28cb7..4ba0ef6f4 100644 --- a/devTools/phpstan-src-baseline.neon +++ b/devTools/phpstan-src-baseline.neon @@ -305,6 +305,11 @@ parameters: count: 1 path: ../src/TestFramework/Coverage/XmlReport/XPathFactory.php + - + message: "#^Parameter \\#1 \\$it of function iterator_to_array expects Traversable, iterable\\ given\\.$#" + count: 1 + path: ../src/TestFramework/Factory.php + - message: "#^Method Infection\\\\TestFramework\\\\PhpUnit\\\\Adapter\\\\PestAdapterFactory\\:\\:create\\(\\) has parameter \\$sourceDirectories with no value type specified in iterable type array\\.$#" count: 1 @@ -360,11 +365,6 @@ parameters: count: 1 path: ../src/TestFramework/PhpUnit/Config/XmlConfigurationManipulator.php - - - message: "#^Only booleans are allowed in an if condition, int given\\.$#" - count: 2 - path: ../src/TestFramework/PhpUnit/Config/XmlConfigurationManipulator.php - - message: "#^Only booleans are allowed in an if condition, int given\\.$#" count: 6 diff --git a/src/Container.php b/src/Container.php index 00f6ee536..1530b8556 100644 --- a/src/Container.php +++ b/src/Container.php @@ -290,6 +290,7 @@ public static function create(): self $container->getTestFrameworkFinder(), $container->getDefaultJUnitFilePath(), $config, + $container->getSourceFileFilter(), GeneratedExtensionsConfig::EXTENSIONS ); }, diff --git a/src/FileSystem/SourceFileCollector.php b/src/FileSystem/SourceFileCollector.php index 3d54a4455..8b3533721 100644 --- a/src/FileSystem/SourceFileCollector.php +++ b/src/FileSystem/SourceFileCollector.php @@ -56,7 +56,7 @@ public function collectFiles( array $excludeDirectories ): iterable { if ($sourceDirectories === []) { - return; + return []; } $finder = Finder::create() @@ -70,7 +70,6 @@ public function collectFiles( $finder->notPath($excludeDirectory); } - // Generator here to make sure these files used only once - yield from $finder; + return $finder; } } diff --git a/src/TestFramework/Factory.php b/src/TestFramework/Factory.php index 13536f572..da00a221b 100644 --- a/src/TestFramework/Factory.php +++ b/src/TestFramework/Factory.php @@ -35,17 +35,22 @@ namespace Infection\TestFramework; +use function array_filter; +use function array_map; use function implode; use Infection\AbstractTestFramework\TestFrameworkAdapter; use Infection\AbstractTestFramework\TestFrameworkAdapterFactory; use Infection\Configuration\Configuration; use Infection\FileSystem\Finder\TestFrameworkFinder; +use Infection\FileSystem\SourceFileFilter; use Infection\TestFramework\Config\TestFrameworkConfigLocatorInterface; use Infection\TestFramework\PhpUnit\Adapter\PestAdapterFactory; use Infection\TestFramework\PhpUnit\Adapter\PhpUnitAdapterFactory; use InvalidArgumentException; use function is_a; +use function iterator_to_array; use function Safe\sprintf; +use SplFileInfo; use Webmozart\Assert\Assert; /** @@ -59,6 +64,7 @@ final class Factory private TestFrameworkFinder $testFrameworkFinder; private string $jUnitFilePath; private Configuration $infectionConfig; + private SourceFileFilter $sourceFileFilter; /** * @var array> @@ -75,6 +81,7 @@ public function __construct( TestFrameworkFinder $testFrameworkFinder, string $jUnitFilePath, Configuration $infectionConfig, + SourceFileFilter $sourceFileFilter, array $installedExtensions ) { $this->tmpDir = $tmpDir; @@ -83,11 +90,14 @@ public function __construct( $this->jUnitFilePath = $jUnitFilePath; $this->infectionConfig = $infectionConfig; $this->testFrameworkFinder = $testFrameworkFinder; + $this->sourceFileFilter = $sourceFileFilter; $this->installedExtensions = $installedExtensions; } public function create(string $adapterName, bool $skipCoverage): TestFrameworkAdapter { + $filteredSourceFilesToMutate = $this->getFilteredSourceFilesToMutate(); + if ($adapterName === TestFrameworkTypes::PHPUNIT) { $phpUnitConfigPath = $this->configLocator->locate(TestFrameworkTypes::PHPUNIT); @@ -103,7 +113,8 @@ public function create(string $adapterName, bool $skipCoverage): TestFrameworkAd $this->projectDir, $this->infectionConfig->getSourceDirectories(), $skipCoverage, - $this->infectionConfig->getExecuteOnlyCoveringTestCases() + $this->infectionConfig->getExecuteOnlyCoveringTestCases(), + $filteredSourceFilesToMutate ); } @@ -122,7 +133,8 @@ public function create(string $adapterName, bool $skipCoverage): TestFrameworkAd $this->projectDir, $this->infectionConfig->getSourceDirectories(), $skipCoverage, - $this->infectionConfig->getExecuteOnlyCoveringTestCases() + $this->infectionConfig->getExecuteOnlyCoveringTestCases(), + $filteredSourceFilesToMutate ); } @@ -159,4 +171,26 @@ public function create(string $adapterName, bool $skipCoverage): TestFrameworkAd implode(', ', $availableTestFrameworks) )); } + + /** + * Get only those source files that will be mutated to use them in coverage whitelist + * + * @return list + */ + private function getFilteredSourceFilesToMutate(): array + { + if ($this->sourceFileFilter->getFilters() === []) { + return []; + } + + /** @var list $filteredPaths */ + $filteredPaths = array_filter(array_map( + static function (SplFileInfo $file) { + return $file->getRealPath(); + }, + iterator_to_array($this->sourceFileFilter->filter($this->infectionConfig->getSourceFiles())) + )); + + return $filteredPaths; + } } diff --git a/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php b/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php index 57250d62b..f0e74e5ce 100644 --- a/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php +++ b/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php @@ -56,6 +56,9 @@ */ final class PestAdapterFactory implements TestFrameworkAdapterFactory { + /** + * @param list $filteredSourceFilesToMutate + */ public static function create( string $testFrameworkExecutable, string $tmpDir, @@ -65,7 +68,8 @@ public static function create( string $projectDir, array $sourceDirectories, bool $skipCoverage, - bool $executeOnlyCoveringTestCases = false + bool $executeOnlyCoveringTestCases = false, + array $filteredSourceFilesToMutate = [] ): TestFrameworkAdapter { Assert::string($testFrameworkConfigDir, 'Config dir is not allowed to be `null` for the Pest adapter'); @@ -89,7 +93,8 @@ public static function create( $testFrameworkConfigContent, $configManipulator, new XmlConfigurationVersionProvider(), - $sourceDirectories + $sourceDirectories, + $filteredSourceFilesToMutate ), new MutationConfigBuilder( $tmpDir, diff --git a/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php b/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php index cfb13dccf..e36f55178 100644 --- a/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php +++ b/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php @@ -58,6 +58,7 @@ final class PhpUnitAdapterFactory implements TestFrameworkAdapterFactory { /** * @param string[] $sourceDirectories + * @param list $filteredSourceFilesToMutate */ public static function create( string $testFrameworkExecutable, @@ -68,7 +69,8 @@ public static function create( string $projectDir, array $sourceDirectories, bool $skipCoverage, - bool $executeOnlyCoveringTestCases = false + bool $executeOnlyCoveringTestCases = false, + array $filteredSourceFilesToMutate = [] ): TestFrameworkAdapter { Assert::string($testFrameworkConfigDir, 'Config dir is not allowed to be `null` for the Pest adapter'); @@ -92,7 +94,8 @@ public static function create( $testFrameworkConfigContent, $configManipulator, new XmlConfigurationVersionProvider(), - $sourceDirectories + $sourceDirectories, + $filteredSourceFilesToMutate ), new MutationConfigBuilder( $tmpDir, diff --git a/src/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilder.php b/src/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilder.php index 1c519c358..c1b6726a0 100644 --- a/src/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilder.php +++ b/src/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilder.php @@ -56,16 +56,20 @@ class InitialConfigBuilder implements ConfigBuilder private XmlConfigurationVersionProvider $versionProvider; /** @var string[] */ private array $srcDirs; + /** @var list */ + private array $filteredSourceFilesToMutate; /** * @param string[] $srcDirs + * @param list $filteredSourceFilesToMutate */ public function __construct( string $tmpDir, string $originalXmlConfigContent, XmlConfigurationManipulator $configManipulator, XmlConfigurationVersionProvider $versionProvider, - array $srcDirs + array $srcDirs, + array $filteredSourceFilesToMutate ) { Assert::notEmpty( $originalXmlConfigContent, @@ -77,6 +81,7 @@ public function __construct( $this->configManipulator = $configManipulator; $this->versionProvider = $versionProvider; $this->srcDirs = $srcDirs; + $this->filteredSourceFilesToMutate = $filteredSourceFilesToMutate; } public function build(string $version): string @@ -118,25 +123,25 @@ private function buildPath(): string private function addCoverageNodes(string $version, SafeDOMXPath $xPath): void { if (version_compare($version, '10', '>=')) { - $this->configManipulator->addCoverageIncludeNodesUnlessTheyExist($xPath, $this->srcDirs); + $this->configManipulator->addOrUpdateCoverageIncludeNodes($xPath, $this->srcDirs, $this->filteredSourceFilesToMutate); return; } if (version_compare($version, '9.3', '<')) { - $this->configManipulator->addLegacyCoverageWhitelistNodesUnlessTheyExist($xPath, $this->srcDirs); + $this->configManipulator->addOrUpdateLegacyCoverageWhitelistNodes($xPath, $this->srcDirs, $this->filteredSourceFilesToMutate); return; } // For versions between 9.3 and 10.0, fallback to version provider if (version_compare($this->versionProvider->provide($xPath), '9.3', '>=')) { - $this->configManipulator->addCoverageIncludeNodesUnlessTheyExist($xPath, $this->srcDirs); + $this->configManipulator->addOrUpdateCoverageIncludeNodes($xPath, $this->srcDirs, $this->filteredSourceFilesToMutate); return; } - $this->configManipulator->addLegacyCoverageWhitelistNodesUnlessTheyExist($xPath, $this->srcDirs); + $this->configManipulator->addOrUpdateLegacyCoverageWhitelistNodes($xPath, $this->srcDirs, $this->filteredSourceFilesToMutate); } private function addRandomTestsOrderAttributesIfNotSet(string $version, SafeDOMXPath $xPath): void diff --git a/src/TestFramework/PhpUnit/Config/Path/PathReplacer.php b/src/TestFramework/PhpUnit/Config/Path/PathReplacer.php index 9922f7a0b..e99e7bf78 100644 --- a/src/TestFramework/PhpUnit/Config/Path/PathReplacer.php +++ b/src/TestFramework/PhpUnit/Config/Path/PathReplacer.php @@ -48,7 +48,7 @@ final class PathReplacer { private Filesystem $filesystem; - private ?string $phpUnitConfigDir = null; + private ?string $phpUnitConfigDir; public function __construct(Filesystem $filesystem, ?string $phpUnitConfigDir = null) { diff --git a/src/TestFramework/PhpUnit/Config/XmlConfigurationManipulator.php b/src/TestFramework/PhpUnit/Config/XmlConfigurationManipulator.php index 52415d572..8f22e29c6 100644 --- a/src/TestFramework/PhpUnit/Config/XmlConfigurationManipulator.php +++ b/src/TestFramework/PhpUnit/Config/XmlConfigurationManipulator.php @@ -132,18 +132,20 @@ public function removeExistingPrinters(SafeDOMXPath $xPath): void /** * @param string[] $srcDirs + * @param list $filteredSourceFilesToMutate */ - public function addLegacyCoverageWhitelistNodesUnlessTheyExist(SafeDOMXPath $xPath, array $srcDirs): void + public function addOrUpdateLegacyCoverageWhitelistNodes(SafeDOMXPath $xPath, array $srcDirs, array $filteredSourceFilesToMutate): void { - $this->addCoverageNodesUnlessTheyExist('filter', 'whitelist', $xPath, $srcDirs); + $this->addOrUpdateCoverageNodes('filter', 'whitelist', $xPath, $srcDirs, $filteredSourceFilesToMutate); } /** * @param string[] $srcDirs + * @param list $filteredSourceFilesToMutate */ - public function addCoverageIncludeNodesUnlessTheyExist(SafeDOMXPath $xPath, array $srcDirs): void + public function addOrUpdateCoverageIncludeNodes(SafeDOMXPath $xPath, array $srcDirs, array $filteredSourceFilesToMutate): void { - $this->addCoverageNodesUnlessTheyExist('coverage', 'include', $xPath, $srcDirs); + $this->addOrUpdateCoverageNodes('coverage', 'include', $xPath, $srcDirs, $filteredSourceFilesToMutate); } public function validate(string $configPath, SafeDOMXPath $xPath): bool @@ -182,24 +184,43 @@ public function removeDefaultTestSuite(SafeDOMXPath $xPath): void /** * @param string[] $srcDirs + * @param list $filteredSourceFilesToMutate */ - private function addCoverageNodesUnlessTheyExist(string $parentName, string $listName, SafeDOMXPath $xPath, array $srcDirs): void + private function addOrUpdateCoverageNodes(string $parentName, string $listName, SafeDOMXPath $xPath, array $srcDirs, array $filteredSourceFilesToMutate): void { - if ($this->nodeExists($xPath, "{$parentName}/{$listName}")) { - return; + $coverageNodeExists = $this->nodeExists($xPath, "{$parentName}/{$listName}"); + + if ($coverageNodeExists) { + if ($filteredSourceFilesToMutate === []) { + // use original phpunit.xml's coverage setting since all files need to be mutated (no filter is set) + return; + } + + $this->removeCoverageChildNode($xPath, "{$parentName}/{$listName}"); } - $filterNode = $this->createNode($xPath->document, $parentName); + $filterNode = $this->getOrCreateNode($xPath, $xPath->document, $parentName); $listNode = $xPath->document->createElement($listName); - foreach ($srcDirs as $srcDir) { - $directoryNode = $xPath->document->createElement( - 'directory', - $srcDir - ); + if ($filteredSourceFilesToMutate === []) { + foreach ($srcDirs as $srcDir) { + $directoryNode = $xPath->document->createElement( + 'directory', + $srcDir + ); + + $listNode->appendChild($directoryNode); + } + } else { + foreach ($filteredSourceFilesToMutate as $sourceFileRealPath) { + $directoryNode = $xPath->document->createElement( + 'file', + $sourceFileRealPath + ); - $listNode->appendChild($directoryNode); + $listNode->appendChild($directoryNode); + } } $filterNode->appendChild($listNode); @@ -264,7 +285,7 @@ private function removeAttribute(SafeDOMXPath $xPath, string $name): void $name )); - if ($nodeList->length) { + if ($nodeList->length > 0) { $document = $xPath->document->documentElement; Assert::isInstanceOf($document, DOMElement::class); $document->removeAttribute($name); @@ -278,7 +299,7 @@ private function setAttributeValue(SafeDOMXPath $xPath, string $name, string $va $name )); - if ($nodeList->length) { + if ($nodeList->length > 0) { $nodeList[0]->nodeValue = $value; } else { $node = $xPath->query('/phpunit')[0]; @@ -302,4 +323,22 @@ private function getErrorLevelName(LibXMLError $error): string throw new LogicException(sprintf('Unknown lib XML error level "%s"', $error->level)); } + + private function removeCoverageChildNode(SafeDOMXPath $xPath, string $nodeQuery): void + { + foreach ($xPath->query($nodeQuery) as $node) { + $node->parentNode->removeChild($node); + } + } + + private function getOrCreateNode(SafeDOMXPath $xPath, DOMDocument $dom, string $nodeName): DOMElement + { + $node = $xPath->query(sprintf('/phpunit/%s', $nodeName)); + + if ($node->length > 0) { + return $node[0]; + } + + return $this->createNode($dom, $nodeName); + } } diff --git a/tests/phpunit/Fixtures/Files/phpunit/phpunit_with_coverage_include_directories.xml b/tests/phpunit/Fixtures/Files/phpunit/phpunit_with_coverage_include_directories.xml new file mode 100644 index 000000000..4cc2d2bcb --- /dev/null +++ b/tests/phpunit/Fixtures/Files/phpunit/phpunit_with_coverage_include_directories.xml @@ -0,0 +1,24 @@ + + + + + ./*Bundle + + + + + + src/ + example/ + + + diff --git a/tests/phpunit/TestFramework/FactoryTest.php b/tests/phpunit/TestFramework/FactoryTest.php index 61348b71f..53fbfcf2b 100644 --- a/tests/phpunit/TestFramework/FactoryTest.php +++ b/tests/phpunit/TestFramework/FactoryTest.php @@ -37,6 +37,7 @@ use Infection\Configuration\Configuration; use Infection\FileSystem\Finder\TestFrameworkFinder; +use Infection\FileSystem\SourceFileFilter; use Infection\TestFramework\Config\TestFrameworkConfigLocatorInterface; use Infection\TestFramework\Factory; use Infection\Tests\Fixtures\TestFramework\DummyTestFrameworkAdapter; @@ -58,7 +59,8 @@ public function test_it_throws_an_exception_if_it_cant_find_the_testframework(): $this->createMock(TestFrameworkFinder::class), '', $this->createMock(Configuration::class), - [] + $this->createMock(SourceFileFilter::class), + [], ); $this->expectException(InvalidArgumentException::class); @@ -74,6 +76,7 @@ public function test_it_uses_installed_test_framework_adapters(): void $this->createMock(TestFrameworkFinder::class), '', $this->createMock(Configuration::class), + $this->createMock(SourceFileFilter::class), [ 'infection/codeception-adapter' => [ 'install_path' => '/path/to/dummy/adapter/factory.php', diff --git a/tests/phpunit/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilderTest.php b/tests/phpunit/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilderTest.php index 5741c8947..18bb21a3b 100644 --- a/tests/phpunit/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilderTest.php +++ b/tests/phpunit/TestFramework/PhpUnit/Config/Builder/InitialConfigBuilderTest.php @@ -269,6 +269,19 @@ public function test_it_creates_coverage_filter_whitelist_node_if_does_not_exist $this->assertSame(2, $whitelistedDirectories->length); } + public function test_it_replaces_coverage_filter_include_node_if_exists_but_filtered_source_files_provided(): void + { + $phpunitXmlPath = self::FIXTURES . '/phpunit_with_coverage_include_directories.xml'; + + $xml = file_get_contents($this->createConfigBuilder($phpunitXmlPath, ['src/File1.php'])->build('9.3')); + + $coverageIncludeFiles = $this->queryXpath($xml, '/phpunit/coverage/include/file'); + + $this->assertInstanceOf(DOMNodeList::class, $coverageIncludeFiles); + + $this->assertSame(1, $coverageIncludeFiles->length); + } + public function test_it_creates_coverage_include_node_if_does_not_exist_for_future_version_of_phpunit(): void { $phpunitXmlPath = self::FIXTURES . '/phpunit_without_coverage_whitelist.xml'; @@ -539,14 +552,14 @@ private function queryXpath(string $xml, string $query) return (new DOMXPath($dom))->query($query); } - private function createConfigBuilderForPHPUnit93( - ?string $originalPhpUnitXmlConfigPath = null - ): InitialConfigBuilder { + private function createConfigBuilderForPHPUnit93(): InitialConfigBuilder + { return $this->createConfigBuilder(self::FIXTURES . '/phpunit_93.xml'); } private function createConfigBuilder( - ?string $originalPhpUnitXmlConfigPath = null + ?string $originalPhpUnitXmlConfigPath = null, + array $filteredSourceFilesToMutate = [] ): InitialConfigBuilder { $phpunitXmlPath = $originalPhpUnitXmlConfigPath ?: self::FIXTURES . '/phpunit.xml'; @@ -559,7 +572,8 @@ private function createConfigBuilder( file_get_contents($phpunitXmlPath), new XmlConfigurationManipulator($replacer, ''), new XmlConfigurationVersionProvider(), - $srcDirs + $srcDirs, + $filteredSourceFilesToMutate ); } } diff --git a/tests/phpunit/TestFramework/PhpUnit/Config/XmlConfigurationManipulatorTest.php b/tests/phpunit/TestFramework/PhpUnit/Config/XmlConfigurationManipulatorTest.php index f3aee438c..7f4f0a209 100644 --- a/tests/phpunit/TestFramework/PhpUnit/Config/XmlConfigurationManipulatorTest.php +++ b/tests/phpunit/TestFramework/PhpUnit/Config/XmlConfigurationManipulatorTest.php @@ -146,7 +146,7 @@ static function (XmlConfigurationManipulator $configManipulator, SafeDOMXPath $x ); } - public function test_it_adds_coverage_whitelist_to_pre_93_configuration(): void + public function test_it_adds_coverage_whitelist_directories_to_pre_93_configuration(): void { $this->assertItChangesXML( <<<'XML' @@ -155,7 +155,7 @@ public function test_it_adds_coverage_whitelist_to_pre_93_configuration(): void XML , static function (XmlConfigurationManipulator $configManipulator, SafeDOMXPath $xPath): void { - $configManipulator->addLegacyCoverageWhitelistNodesUnlessTheyExist($xPath, ['src/', 'examples/']); + $configManipulator->addOrUpdateLegacyCoverageWhitelistNodes($xPath, ['src/', 'examples/'], []); }, <<<'XML' @@ -170,6 +170,85 @@ static function (XmlConfigurationManipulator $configManipulator, SafeDOMXPath $x ); } + public function test_it_adds_coverage_whitelist_files_to_pre_93_configuration(): void + { + $this->assertItChangesXML( + <<<'XML' + + +XML + , + static function (XmlConfigurationManipulator $configManipulator, SafeDOMXPath $xPath): void { + $configManipulator->addOrUpdateLegacyCoverageWhitelistNodes( + $xPath, + ['src/', 'examples/'], + ['src/File1.php', 'example/File2.php'] + ); + }, + <<<'XML' + + + + src/File1.php + example/File2.php + + + +XML + ); + } + + public function test_it_adds_coverage_whitelist_directories_to_post_93_configuration(): void + { + $this->assertItChangesXML( + <<<'XML' + + +XML + , + static function (XmlConfigurationManipulator $configManipulator, SafeDOMXPath $xPath): void { + $configManipulator->addOrUpdateCoverageIncludeNodes($xPath, ['src/', 'examples/'], []); + }, + <<<'XML' + + + + src/ + examples/ + + + +XML + ); + } + + public function test_it_adds_coverage_whitelist_files_to_post_93_configuration(): void + { + $this->assertItChangesXML( + <<<'XML' + + +XML + , + static function (XmlConfigurationManipulator $configManipulator, SafeDOMXPath $xPath): void { + $configManipulator->addOrUpdateCoverageIncludeNodes($xPath, + ['src/', 'examples/'], + ['src/File1.php', 'example/File2.php'] + ); + }, + <<<'XML' + + + + src/File1.php + example/File2.php + + + +XML + ); + } + public function test_it_removes_existing_loggers_from_post_93_configuration(): void { $this->assertItChangesPostPHPUnit93Configuration(