diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 2bc06fdae..34483a165 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -116,6 +116,8 @@ final class RunCommand extends BaseCommand private const OPTION_USE_NOOP_MUTATORS = 'noop'; + private const OPTION_EXECUTE_ONLY_COVERING_TEST_CASES = 'only-covering-test-cases'; + /** @var string */ private const OPTION_MIN_MSI = 'min-msi'; @@ -257,6 +259,12 @@ protected function configure(): void InputOption::VALUE_NONE, 'Use noop mutators that do not change AST. For debugging purposes.', ) + ->addOption( + self::OPTION_EXECUTE_ONLY_COVERING_TEST_CASES, + null, + InputOption::VALUE_NONE, + 'Execute only those test cases that cover mutated line, not the whole file with covering test cases. Can dramatically speed up Mutation Testing for slow test suites. For PHPUnit / Pest it uses `--filter` option', + ) ->addOption( self::OPTION_MIN_MSI, null, @@ -444,7 +452,8 @@ private function createContainer(IO $io, LoggerInterface $logger): Container $gitDiffFilter, $gitDiffBase, (bool) $input->getOption(self::OPTION_LOGGER_GITHUB), - (bool) $input->getOption(self::OPTION_USE_NOOP_MUTATORS) + (bool) $input->getOption(self::OPTION_USE_NOOP_MUTATORS), + (bool) $input->getOption(self::OPTION_EXECUTE_ONLY_COVERING_TEST_CASES) ); } diff --git a/src/Configuration/Configuration.php b/src/Configuration/Configuration.php index fd6649bf0..87df313a3 100644 --- a/src/Configuration/Configuration.php +++ b/src/Configuration/Configuration.php @@ -87,6 +87,7 @@ class Configuration private bool $dryRun; /** @var array> */ private array $ignoreSourceCodeMutatorsMap; + private bool $executeOnlyCoveringTestCases; /** * @param string[] $sourceDirectories @@ -123,7 +124,8 @@ public function __construct( int $msiPrecision, int $threadCount, bool $dryRun, - array $ignoreSourceCodeMutatorsMap + array $ignoreSourceCodeMutatorsMap, + bool $executeOnlyCoveringTestCases ) { Assert::nullOrGreaterThanEq($timeout, 0); Assert::allString($sourceDirectories); @@ -161,6 +163,7 @@ public function __construct( $this->threadCount = $threadCount; $this->dryRun = $dryRun; $this->ignoreSourceCodeMutatorsMap = $ignoreSourceCodeMutatorsMap; + $this->executeOnlyCoveringTestCases = $executeOnlyCoveringTestCases; } public function getProcessTimeout(): float @@ -317,4 +320,9 @@ public function getIgnoreSourceCodeMutatorsMap(): array { return $this->ignoreSourceCodeMutatorsMap; } + + public function getExecuteOnlyCoveringTestCases(): bool + { + return $this->executeOnlyCoveringTestCases; + } } diff --git a/src/Configuration/ConfigurationFactory.php b/src/Configuration/ConfigurationFactory.php index 601bdd42e..07907742f 100644 --- a/src/Configuration/ConfigurationFactory.php +++ b/src/Configuration/ConfigurationFactory.php @@ -118,7 +118,8 @@ public function create( ?string $gitDiffFilter, ?string $gitDiffBase, bool $useGitHubLogger, - bool $useNoopMutators + bool $useNoopMutators, + bool $executeOnlyCoveringTestCases ): Configuration { $configDir = dirname($schema->getFile()); @@ -170,7 +171,8 @@ public function create( $msiPrecision, $threadCount, $dryRun, - $ignoreSourceCodeMutatorsMap + $ignoreSourceCodeMutatorsMap, + $executeOnlyCoveringTestCases ); } diff --git a/src/Container.php b/src/Container.php index 44c5e7820..00f6ee536 100644 --- a/src/Container.php +++ b/src/Container.php @@ -166,6 +166,7 @@ final class Container public const DEFAULT_GIT_DIFF_BASE = null; public const DEFAULT_USE_GITHUB_LOGGER = false; public const DEFAULT_USE_NOOP_MUTATORS = false; + public const DEFAULT_EXECUTE_ONLY_COVERING_TEST_CASES = false; public const DEFAULT_NO_PROGRESS = false; public const DEFAULT_FORCE_PROGRESS = false; public const DEFAULT_EXISTING_COVERAGE_PATH = null; @@ -684,7 +685,8 @@ public static function create(): self self::DEFAULT_GIT_DIFF_FILTER, self::DEFAULT_GIT_DIFF_BASE, self::DEFAULT_USE_GITHUB_LOGGER, - self::DEFAULT_USE_NOOP_MUTATORS + self::DEFAULT_USE_NOOP_MUTATORS, + self::DEFAULT_EXECUTE_ONLY_COVERING_TEST_CASES ); } @@ -715,7 +717,8 @@ public function withValues( ?string $gitDiffFilter, ?string $gitDiffBase, bool $useGitHubLogger, - bool $useNoopMutators + bool $useNoopMutators, + bool $executeOnlyCoveringTestCases ): self { $clone = clone $this; @@ -790,7 +793,8 @@ static function (self $container) use ( $gitDiffFilter, $gitDiffBase, $useGitHubLogger, - $useNoopMutators + $useNoopMutators, + $executeOnlyCoveringTestCases ): Configuration { return $container->getConfigurationFactory()->create( $container->getSchemaConfiguration(), @@ -815,7 +819,8 @@ static function (self $container) use ( $gitDiffFilter, $gitDiffBase, $useGitHubLogger, - $useNoopMutators + $useNoopMutators, + $executeOnlyCoveringTestCases ); } ); diff --git a/src/TestFramework/Factory.php b/src/TestFramework/Factory.php index 68f5fc7ed..13536f572 100644 --- a/src/TestFramework/Factory.php +++ b/src/TestFramework/Factory.php @@ -102,7 +102,8 @@ public function create(string $adapterName, bool $skipCoverage): TestFrameworkAd $this->jUnitFilePath, $this->projectDir, $this->infectionConfig->getSourceDirectories(), - $skipCoverage + $skipCoverage, + $this->infectionConfig->getExecuteOnlyCoveringTestCases() ); } @@ -120,7 +121,8 @@ public function create(string $adapterName, bool $skipCoverage): TestFrameworkAd $this->jUnitFilePath, $this->projectDir, $this->infectionConfig->getSourceDirectories(), - $skipCoverage + $skipCoverage, + $this->infectionConfig->getExecuteOnlyCoveringTestCases() ); } diff --git a/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php b/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php index 591b5d6f9..57250d62b 100644 --- a/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php +++ b/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php @@ -64,7 +64,8 @@ public static function create( string $jUnitFilePath, string $projectDir, array $sourceDirectories, - bool $skipCoverage + bool $skipCoverage, + bool $executeOnlyCoveringTestCases = false ): TestFrameworkAdapter { Assert::string($testFrameworkConfigDir, 'Config dir is not allowed to be `null` for the Pest adapter'); @@ -97,7 +98,7 @@ public static function create( $projectDir, new JUnitTestCaseSorter() ), - new ArgumentsAndOptionsBuilder(), + new ArgumentsAndOptionsBuilder($executeOnlyCoveringTestCases), new VersionParser(), new CommandLineBuilder() ); diff --git a/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php b/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php index cbc52b6c1..cfb13dccf 100644 --- a/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php +++ b/src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php @@ -67,7 +67,8 @@ public static function create( string $jUnitFilePath, string $projectDir, array $sourceDirectories, - bool $skipCoverage + bool $skipCoverage, + bool $executeOnlyCoveringTestCases = false ): TestFrameworkAdapter { Assert::string($testFrameworkConfigDir, 'Config dir is not allowed to be `null` for the Pest adapter'); @@ -100,7 +101,7 @@ public static function create( $projectDir, new JUnitTestCaseSorter() ), - new ArgumentsAndOptionsBuilder(), + new ArgumentsAndOptionsBuilder($executeOnlyCoveringTestCases), new VersionParser(), new CommandLineBuilder() ); diff --git a/src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php b/src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php index 77e2a4717..eecac103f 100644 --- a/src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php +++ b/src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php @@ -51,6 +51,13 @@ */ final class ArgumentsAndOptionsBuilder implements CommandLineArgumentsAndOptionsBuilder { + private bool $executeOnlyCoveringTestCases; + + public function __construct(bool $executeOnlyCoveringTestCases) + { + $this->executeOnlyCoveringTestCases = $executeOnlyCoveringTestCases; + } + public function buildForInitialTestsRun(string $configPath, string $extraOptions): array { $options = [ @@ -77,7 +84,7 @@ public function buildForMutant(string $configPath, string $extraOptions, array $ { $options = $this->buildForInitialTestsRun($configPath, $extraOptions); - if (count($tests) > 0) { + if ($this->executeOnlyCoveringTestCases && count($tests) > 0) { $filterString = '/'; $usedTestCases = []; diff --git a/tests/phpunit/Configuration/ConfigurationAssertions.php b/tests/phpunit/Configuration/ConfigurationAssertions.php index dd0e482bd..5aa9edd46 100644 --- a/tests/phpunit/Configuration/ConfigurationAssertions.php +++ b/tests/phpunit/Configuration/ConfigurationAssertions.php @@ -83,7 +83,8 @@ private function assertConfigurationStateIs( int $expectedMsiPrecision, int $expectedThreadCount, bool $expectedDryRyn, - array $expectedIgnoreSourceCodeMutatorsMap + array $expectedIgnoreSourceCodeMutatorsMap, + bool $expectedExecuteOnlyCoveringTestCases ): void { $this->assertSame($expectedTimeout, $configuration->getProcessTimeout()); $this->assertSame($expectedSourceDirectories, $configuration->getSourceDirectories()); @@ -132,6 +133,7 @@ private function assertConfigurationStateIs( $this->assertSame($expectedThreadCount, $configuration->getThreadCount()); $this->assertSame($expectedDryRyn, $configuration->isDryRun()); $this->assertSame($expectedIgnoreSourceCodeMutatorsMap, $configuration->getIgnoreSourceCodeMutatorsMap()); + $this->assertSame($expectedExecuteOnlyCoveringTestCases, $configuration->getExecuteOnlyCoveringTestCases()); } /** diff --git a/tests/phpunit/Configuration/ConfigurationFactoryTest.php b/tests/phpunit/Configuration/ConfigurationFactoryTest.php index 4b27ea5d2..f746aed8d 100644 --- a/tests/phpunit/Configuration/ConfigurationFactoryTest.php +++ b/tests/phpunit/Configuration/ConfigurationFactoryTest.php @@ -138,7 +138,8 @@ public function test_it_can_create_a_configuration( ?float $expectedMinMsi, bool $expectedShowMutations, ?float $expectedMinCoveredMsi, - array $expectedIgnoreSourceCodeMutatorsMap + array $expectedIgnoreSourceCodeMutatorsMap, + bool $inputExecuteOnlyCoveringTestCases ): void { $config = $this ->createConfigurationFactory($ciDetected) @@ -165,7 +166,8 @@ public function test_it_can_create_a_configuration( $inputGitDiffFilter, $inputGitDiffBase, $inputUseGitHubAnnotationsLogger, - $inputUseNoopMutators + $inputUseNoopMutators, + $inputExecuteOnlyCoveringTestCases ) ; @@ -198,7 +200,8 @@ public function test_it_can_create_a_configuration( $inputMsiPrecision, $inputThreadsCount, $inputDryRun, - $expectedIgnoreSourceCodeMutatorsMap + $expectedIgnoreSourceCodeMutatorsMap, + $inputExecuteOnlyCoveringTestCases ); } @@ -272,6 +275,7 @@ public function valueProvider(): iterable false, null, [], + true, ]; yield 'null timeout' => self::createValueForTimeout( @@ -717,6 +721,7 @@ public function valueProvider(): iterable false, null, [], + false, ]; yield 'complete' => [ @@ -813,6 +818,7 @@ public function valueProvider(): iterable true, 81.5, [], + false, ]; } @@ -885,6 +891,7 @@ private static function createValueForTimeout( false, null, [], + false, ]; } @@ -957,6 +964,7 @@ private static function createValueForTmpDir( false, null, [], + false, ]; } @@ -1030,6 +1038,7 @@ private static function createValueForCoveragePath( false, null, [], + false, ]; } @@ -1102,6 +1111,7 @@ private static function createValueForPhpUnitConfigDir( false, null, [], + false, ]; } @@ -1175,6 +1185,7 @@ private static function createValueForNoProgress( false, null, [], + false, ]; } @@ -1248,6 +1259,7 @@ private static function createValueForIgnoreMsiWithNoMutations( false, null, [], + false, ]; } @@ -1321,6 +1333,7 @@ private static function createValueForMinMsi( false, null, [], + false, ]; } @@ -1394,6 +1407,7 @@ private static function createValueForMinCoveredMsi( false, $expectedMinCoveredMsi, [], + false, ]; } @@ -1468,6 +1482,7 @@ private static function createValueForTestFramework( false, null, [], + false, ]; } @@ -1541,6 +1556,7 @@ private static function createValueForInitialTestsPhpOptions( false, null, [], + false, ]; } @@ -1615,6 +1631,7 @@ private static function createValueForTestFrameworkExtraOptions( false, null, [], + false, ]; } @@ -1688,6 +1705,7 @@ private static function createValueForTestFrameworkKey( false, null, [], + false, ]; } @@ -1765,6 +1783,7 @@ private static function createValueForMutators( false, null, [], + false, ]; } @@ -1843,6 +1862,7 @@ private static function createValueForIgnoreSourceCodeByRegex( false, null, $expectedIgnoreSourceCodeMutatorsMap, + false, ]; } diff --git a/tests/phpunit/Configuration/ConfigurationTest.php b/tests/phpunit/Configuration/ConfigurationTest.php index 93746ea10..3686b8514 100644 --- a/tests/phpunit/Configuration/ConfigurationTest.php +++ b/tests/phpunit/Configuration/ConfigurationTest.php @@ -87,7 +87,8 @@ public function test_it_can_be_instantiated( int $msiPrecision, int $threadsCount, bool $dryRun, - array $ignoreSourceCodeMutatorsMap + array $ignoreSourceCodeMutatorsMap, + bool $executeOnlyCoveringTestCases ): void { $config = new Configuration( $timeout, @@ -117,7 +118,8 @@ public function test_it_can_be_instantiated( $msiPrecision, $threadsCount, $dryRun, - $ignoreSourceCodeMutatorsMap + $ignoreSourceCodeMutatorsMap, + $executeOnlyCoveringTestCases ); $this->assertConfigurationStateIs( @@ -149,7 +151,8 @@ public function test_it_can_be_instantiated( $msiPrecision, $threadsCount, $dryRun, - $ignoreSourceCodeMutatorsMap + $ignoreSourceCodeMutatorsMap, + $executeOnlyCoveringTestCases ); } @@ -184,6 +187,7 @@ public function valueProvider(): iterable 0, false, [], + false, ]; yield 'nominal' => [ @@ -230,6 +234,7 @@ public function valueProvider(): iterable [ 'For_' => ['.*someMethod.*'], ], + true, ]; } } diff --git a/tests/phpunit/ContainerTest.php b/tests/phpunit/ContainerTest.php index a46f66965..7503c08d1 100644 --- a/tests/phpunit/ContainerTest.php +++ b/tests/phpunit/ContainerTest.php @@ -95,7 +95,8 @@ public function test_it_can_build_lazy_source_file_data_factory_that_fails_on_us Container::DEFAULT_GIT_DIFF_FILTER, Container::DEFAULT_GIT_DIFF_BASE, Container::DEFAULT_USE_GITHUB_LOGGER, - Container::DEFAULT_USE_NOOP_MUTATORS + Container::DEFAULT_USE_NOOP_MUTATORS, + Container::DEFAULT_EXECUTE_ONLY_COVERING_TEST_CASES ); $traces = $newContainer->getUnionTraceProvider()->provideTraces(); @@ -144,7 +145,8 @@ public function test_it_provides_a_friendly_error_when_attempting_to_configure_i Container::DEFAULT_GIT_DIFF_FILTER, Container::DEFAULT_GIT_DIFF_BASE, Container::DEFAULT_USE_GITHUB_LOGGER, - Container::DEFAULT_USE_NOOP_MUTATORS + Container::DEFAULT_USE_NOOP_MUTATORS, + Container::DEFAULT_EXECUTE_ONLY_COVERING_TEST_CASES, ); } } diff --git a/tests/phpunit/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilderTest.php b/tests/phpunit/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilderTest.php index 8bfe9c213..3b99ff924 100644 --- a/tests/phpunit/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilderTest.php +++ b/tests/phpunit/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilderTest.php @@ -50,7 +50,7 @@ final class ArgumentsAndOptionsBuilderTest extends TestCase protected function setUp(): void { - $this->builder = new ArgumentsAndOptionsBuilder(); + $this->builder = new ArgumentsAndOptionsBuilder(false); } public function test_it_can_build_the_command_without_extra_options(): void @@ -98,19 +98,26 @@ public function test_it_can_build_the_command_with_extra_options_that_contains_s /** * @dataProvider provideTestCases */ - public function test_it_can_build_the_command_with_filter_option_for_covering_tests_for_mutant(array $testCases, string $expectedFilterOptionValue): void + public function test_it_can_build_the_command_with_filter_option_for_covering_tests_for_mutant(bool $executeOnlyCoveringTestCases, array $testCases, ?string $expectedFilterOptionValue = null): void { $configPath = '/the config/path'; + $builder = new ArgumentsAndOptionsBuilder($executeOnlyCoveringTestCases); + + $expectedArgumentsAndOptions = [ + '--configuration', + $configPath, + '--path=/a path/with spaces', + ]; + + if ($executeOnlyCoveringTestCases) { + $expectedArgumentsAndOptions[] = '--filter'; + $expectedArgumentsAndOptions[] = $expectedFilterOptionValue; + } + $this->assertSame( - [ - '--configuration', - $configPath, - '--path=/a path/with spaces', - '--filter', - $expectedFilterOptionValue, - ], - $this->builder->buildForMutant( + $expectedArgumentsAndOptions, + $builder->buildForMutant( $configPath, '--path=/a path/with spaces', array_map( @@ -123,7 +130,15 @@ public function test_it_can_build_the_command_with_filter_option_for_covering_te public function provideTestCases(): Generator { + yield '--only-covering-test-cases is disabled' => [ + false, + [ + 'App\Test::test_case1', + ], + ]; + yield '1 test case' => [ + true, [ 'App\Test::test_case1', ], @@ -131,6 +146,7 @@ public function provideTestCases(): Generator ]; yield '2 test cases' => [ + true, [ 'App\Test::test_case1', 'App\Test::test_case2', @@ -139,6 +155,7 @@ public function provideTestCases(): Generator ]; yield '2 simple test cases, 1 with data set and special character >' => [ + true, [ 'App\Test::test_case1 with data set "With special character >"', 'App\Test::test_case2', @@ -147,6 +164,7 @@ public function provideTestCases(): Generator ]; yield '2 simple test cases, 1 with data set and special character @' => [ + true, [ 'App\Test::test_case1 with data set "With special character @"', 'App\Test::test_case2',