diff --git a/devTools/phpstan-src.neon b/devTools/phpstan-src.neon index 22071ae01..dcd4b8605 100644 --- a/devTools/phpstan-src.neon +++ b/devTools/phpstan-src.neon @@ -34,6 +34,9 @@ parameters: - path: '../src/TestFramework/Coverage/CoverageChecker.php' message: '#Function ini_get is unsafe to use#' + - + path: '../src/Differ/DiffChangedLinesParser.php' + message: '#Method Infection\\Differ\\DiffChangedLinesParser::parse\(\) should return array\\>#' level: 8 paths: - ../src diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index da22c4f1d..8154b78be 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -109,6 +109,9 @@ final class RunCommand extends BaseCommand /** @var string */ private const OPTION_GIT_DIFF_FILTER = 'git-diff-filter'; + /** @var string */ + private const OPTION_GIT_DIFF_LINES = 'git-diff-lines'; + /** @var string */ private const OPTION_GIT_DIFF_BASE = 'git-diff-base'; @@ -214,7 +217,7 @@ protected function configure(): void self::OPTION_MUTATORS, null, InputOption::VALUE_REQUIRED, - sprintf('Specify particular mutators, e.g. "--%s=Plus,PublicVisibility"', self::OPTION_MUTATORS), + sprintf('Specify particular mutators, e.g. "--%s=Plus,PublicVisibility"', self::OPTION_MUTATORS), Container::DEFAULT_MUTATORS_INPUT ) ->addOption( @@ -238,14 +241,21 @@ protected function configure(): void self::OPTION_GIT_DIFF_FILTER, null, InputOption::VALUE_REQUIRED, - 'Filter files to mutate git `--diff-filter` options. A - only for added files, AM - for added and modified.', + 'Filter files to mutate by git "--diff-filter" option. A - only for added files, AM - for added and modified.', + Container::DEFAULT_GIT_DIFF_FILTER + ) + ->addOption( + self::OPTION_GIT_DIFF_LINES, + null, + InputOption::VALUE_NONE, + 'Mutates only added and modified lines in files.', Container::DEFAULT_GIT_DIFF_FILTER ) ->addOption( self::OPTION_GIT_DIFF_BASE, null, InputOption::VALUE_REQUIRED, - sprintf('Base branch for `--%1$s` option. Must be used only together with `--%1$s`.', self::OPTION_GIT_DIFF_FILTER), + sprintf('Base branch for "--%1$s" option. Must be used only together with "--%1$s".', self::OPTION_GIT_DIFF_FILTER), Container::DEFAULT_GIT_DIFF_BASE ) ->addOption( @@ -264,7 +274,7 @@ protected function configure(): void 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', + '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, @@ -292,7 +302,7 @@ protected function configure(): void null, InputOption::VALUE_REQUIRED, sprintf( - 'PHP options passed to the PHP executable when executing the initial tests. Will be ignored if "--%s" option presented', + 'PHP options passed to the PHP executable when executing the initial tests. Will be ignored if "--%s" option presented', self::OPTION_COVERAGE ), Container::DEFAULT_INITIAL_TESTS_PHP_OPTIONS @@ -301,7 +311,7 @@ protected function configure(): void self::OPTION_SKIP_INITIAL_TESTS, null, InputOption::VALUE_NONE, - sprintf('Skips the initial test runs. Requires the coverage to be provided via the "--%s" option', self::OPTION_COVERAGE) + sprintf('Skips the initial test runs. Requires the coverage to be provided via the "--%s" option', self::OPTION_COVERAGE) ) ->addOption( self::OPTION_IGNORE_MSI_WITH_NO_MUTATIONS, @@ -399,9 +409,14 @@ private function createContainer(IO $io, LoggerInterface $logger): Container } $gitDiffFilter = $input->getOption(self::OPTION_GIT_DIFF_FILTER); + $isForGitDiffLines = (bool) $input->getOption(self::OPTION_GIT_DIFF_LINES); $gitDiffBase = $input->getOption(self::OPTION_GIT_DIFF_BASE); - if ($gitDiffBase !== Container::DEFAULT_GIT_DIFF_BASE && $gitDiffFilter === Container::DEFAULT_GIT_DIFF_FILTER) { + if ($isForGitDiffLines && $gitDiffFilter !== Container::DEFAULT_GIT_DIFF_FILTER) { + throw new InvalidArgumentException(sprintf('Cannot pass both "--%s" and "--%s" options: use none or only one of them', self::OPTION_GIT_DIFF_LINES, self::OPTION_GIT_DIFF_FILTER)); + } + + if ($gitDiffBase !== Container::DEFAULT_GIT_DIFF_BASE && $gitDiffFilter === Container::DEFAULT_GIT_DIFF_FILTER && $isForGitDiffLines === Container::DEFAULT_GIT_DIFF_LINES) { throw new InvalidArgumentException(sprintf('Cannot pass "--%s" without "--%s"', self::OPTION_GIT_DIFF_BASE, self::OPTION_GIT_DIFF_FILTER)); } @@ -409,7 +424,7 @@ private function createContainer(IO $io, LoggerInterface $logger): Container if ($filter !== '' && $gitDiffFilter !== Container::DEFAULT_GIT_DIFF_BASE) { throw new InvalidArgumentException( - sprintf('Cannot pass both "--%s" and "--%s" option: use none or only one of them', self::OPTION_FILTER, self::OPTION_GIT_DIFF_FILTER) + sprintf('Cannot pass both "--%s" and "--%s" options: use none or only one of them', self::OPTION_FILTER, self::OPTION_GIT_DIFF_FILTER) ); } @@ -455,6 +470,7 @@ private function createContainer(IO $io, LoggerInterface $logger): Container // To keep in sync with Container::DEFAULT_DRY_RUN (bool) $input->getOption(self::OPTION_DRY_RUN), $gitDiffFilter, + $isForGitDiffLines, $gitDiffBase, (bool) $input->getOption(self::OPTION_LOGGER_GITHUB), (bool) $input->getOption(self::OPTION_USE_NOOP_MUTATORS), diff --git a/src/Configuration/Configuration.php b/src/Configuration/Configuration.php index 87df313a3..c60538974 100644 --- a/src/Configuration/Configuration.php +++ b/src/Configuration/Configuration.php @@ -88,6 +88,8 @@ class Configuration /** @var array> */ private array $ignoreSourceCodeMutatorsMap; private bool $executeOnlyCoveringTestCases; + private bool $isForGitDiffLines; + private ?string $gitDiffBase; /** * @param string[] $sourceDirectories @@ -125,7 +127,9 @@ public function __construct( int $threadCount, bool $dryRun, array $ignoreSourceCodeMutatorsMap, - bool $executeOnlyCoveringTestCases + bool $executeOnlyCoveringTestCases, + bool $isForGitDiffLines, + ?string $gitDiffBase ) { Assert::nullOrGreaterThanEq($timeout, 0); Assert::allString($sourceDirectories); @@ -164,6 +168,8 @@ public function __construct( $this->dryRun = $dryRun; $this->ignoreSourceCodeMutatorsMap = $ignoreSourceCodeMutatorsMap; $this->executeOnlyCoveringTestCases = $executeOnlyCoveringTestCases; + $this->isForGitDiffLines = $isForGitDiffLines; + $this->gitDiffBase = $gitDiffBase; } public function getProcessTimeout(): float @@ -325,4 +331,14 @@ public function getExecuteOnlyCoveringTestCases(): bool { return $this->executeOnlyCoveringTestCases; } + + public function isForGitDiffLines(): bool + { + return $this->isForGitDiffLines; + } + + public function getGitDiffBase(): ?string + { + return $this->gitDiffBase; + } } diff --git a/src/Configuration/ConfigurationFactory.php b/src/Configuration/ConfigurationFactory.php index 07907742f..4e3d09fe1 100644 --- a/src/Configuration/ConfigurationFactory.php +++ b/src/Configuration/ConfigurationFactory.php @@ -116,6 +116,7 @@ public function create( int $threadCount, bool $dryRun, ?string $gitDiffFilter, + bool $isForGitDiffLines, ?string $gitDiffBase, bool $useGitHubLogger, bool $useNoopMutators, @@ -147,7 +148,7 @@ public function create( $schema->getSource()->getDirectories(), $schema->getSource()->getExcludes() ), - $this->retrieveFilter($filter, $gitDiffFilter, $gitDiffBase), + $this->retrieveFilter($filter, $gitDiffFilter, $isForGitDiffLines, $gitDiffBase), $schema->getSource()->getExcludes(), $this->retrieveLogs($schema->getLogs(), $useGitHubLogger), $logVerbosity, @@ -172,7 +173,9 @@ public function create( $threadCount, $dryRun, $ignoreSourceCodeMutatorsMap, - $executeOnlyCoveringTestCases + $executeOnlyCoveringTestCases, + $isForGitDiffLines, + $gitDiffBase ); } @@ -297,13 +300,19 @@ private function retrieveIgnoreSourceCodeMutatorsMap(array $resolvedMutatorsMap) return $map; } - private function retrieveFilter(string $filter, ?string $gitDiffFilter, ?string $gitDiffBase): string + private function retrieveFilter(string $filter, ?string $gitDiffFilter, bool $isForGitDiffLines, ?string $gitDiffBase): string { - if ($gitDiffFilter === null) { + if ($gitDiffFilter === null && !$isForGitDiffLines) { return $filter; } - return $this->gitDiffFileProvider->provide($gitDiffFilter, $gitDiffBase ?? GitDiffFileProvider::DEFAULT_BASE); + $baseBranch = $gitDiffBase ?? GitDiffFileProvider::DEFAULT_BASE; + + if ($isForGitDiffLines) { + return $this->gitDiffFileProvider->provide('AM', $baseBranch); + } + + return $this->gitDiffFileProvider->provide($gitDiffFilter, $baseBranch); } private function retrieveLogs(Logs $logs, bool $useGitHubLogger): Logs diff --git a/src/Container.php b/src/Container.php index 8e0922964..689f8dd78 100644 --- a/src/Container.php +++ b/src/Container.php @@ -53,9 +53,11 @@ use Infection\Console\OutputFormatter\FormatterFactory; use Infection\Console\OutputFormatter\FormatterName; use Infection\Console\OutputFormatter\OutputFormatter; +use Infection\Differ\DiffChangedLinesParser; use Infection\Differ\DiffColorizer; use Infection\Differ\Differ; use Infection\Differ\DiffSourceCodeMatcher; +use Infection\Differ\FilesDiffChangedLines; use Infection\Event\EventDispatcher\EventDispatcher; use Infection\Event\EventDispatcher\SyncEventDispatcher; use Infection\Event\Subscriber\ChainSubscriberFactory; @@ -164,6 +166,7 @@ final class Container public const DEFAULT_ONLY_COVERED = false; public const DEFAULT_FORMATTER_NAME = FormatterName::DOT; public const DEFAULT_GIT_DIFF_FILTER = null; + public const DEFAULT_GIT_DIFF_LINES = false; public const DEFAULT_GIT_DIFF_BASE = null; public const DEFAULT_USE_GITHUB_LOGGER = false; public const DEFAULT_USE_NOOP_MUTATORS = false; @@ -527,12 +530,23 @@ public static function create(): self return new NodeTraverserFactory(); }, FileMutationGenerator::class => static function (self $container): FileMutationGenerator { + $configuration = $container->getConfiguration(); + return new FileMutationGenerator( $container->getFileParser(), $container->getNodeTraverserFactory(), - $container->getLineRangeCalculator() + $container->getLineRangeCalculator(), + $container->getFilesDiffChangedLines(), + $configuration->isForGitDiffLines(), + $configuration->getGitDiffBase() ); }, + DiffChangedLinesParser::class => static function (self $container): DiffChangedLinesParser { + return new DiffChangedLinesParser(); + }, + FilesDiffChangedLines::class => static function (self $container): FilesDiffChangedLines { + return new FilesDiffChangedLines($container->getDiffChangedLinesParser(), $container->getGitDiffFileProvider()); + }, BadgeLoggerFactory::class => static function (self $container): BadgeLoggerFactory { return new BadgeLoggerFactory( $container->getMetricsCalculator(), @@ -688,6 +702,7 @@ public static function create(): self self::DEFAULT_THREAD_COUNT, self::DEFAULT_DRY_RUN, self::DEFAULT_GIT_DIFF_FILTER, + self::DEFAULT_GIT_DIFF_LINES, self::DEFAULT_GIT_DIFF_BASE, self::DEFAULT_USE_GITHUB_LOGGER, self::DEFAULT_USE_NOOP_MUTATORS, @@ -720,6 +735,7 @@ public function withValues( int $threadCount, bool $dryRun, ?string $gitDiffFilter, + bool $isForGitDiffLines, ?string $gitDiffBase, bool $useGitHubLogger, bool $useNoopMutators, @@ -796,6 +812,7 @@ static function (self $container) use ( $threadCount, $dryRun, $gitDiffFilter, + $isForGitDiffLines, $gitDiffBase, $useGitHubLogger, $useNoopMutators, @@ -822,6 +839,7 @@ static function (self $container) use ( $threadCount, $dryRun, $gitDiffFilter, + $isForGitDiffLines, $gitDiffBase, $useGitHubLogger, $useNoopMutators, @@ -1205,6 +1223,16 @@ public function getLineRangeCalculator(): LineRangeCalculator return $this->get(LineRangeCalculator::class); } + public function getFilesDiffChangedLines(): FilesDiffChangedLines + { + return $this->get(FilesDiffChangedLines::class); + } + + public function getDiffChangedLinesParser(): DiffChangedLinesParser + { + return $this->get(DiffChangedLinesParser::class); + } + public function getTestFrameworkFinder(): TestFrameworkFinder { return $this->get(TestFrameworkFinder::class); diff --git a/src/Differ/ChangedLinesRange.php b/src/Differ/ChangedLinesRange.php new file mode 100644 index 000000000..a223fbecf --- /dev/null +++ b/src/Differ/ChangedLinesRange.php @@ -0,0 +1,61 @@ +startLine = $startLine; + $this->endLine = $endLine; + } + + public function getStartLine(): int + { + return $this->startLine; + } + + public function getEndLine(): int + { + return $this->endLine; + } +} diff --git a/src/Differ/DiffChangedLinesParser.php b/src/Differ/DiffChangedLinesParser.php new file mode 100644 index 000000000..0d94ece5c --- /dev/null +++ b/src/Differ/DiffChangedLinesParser.php @@ -0,0 +1,113 @@ + [ChangedLinesRange(1, 2)] + * src/File2.php => [ChangedLinesRange(1, 20), ChangedLinesRange(33, 33),] + * ] + * + * Diff provided by command line: `git diff --unified=0 --diff-filter=AM master | grep -v -e '^[+-]' -e '^index'` + * + * @return array> + */ + public function parse(string $unifiedGreppedDiff): array + { + $lines = preg_split('/\n|\r\n?/', $unifiedGreppedDiff); + + $filePath = null; + + $resultMap = []; + + foreach ($lines as $line) { + if (strpos($line, 'diff ') === 0) { + preg_match('/diff.*a\/.*\sb\/(.*)/', $line, $matches); + + Assert::keyExists( + $matches, + self::MATCH_INDEX, + sprintf('Source file can not be found in the following diff line: "%s"', $line) + ); + + $filePath = realpath($matches[self::MATCH_INDEX]); + } elseif (strpos($line, '@@ ') === 0) { + Assert::string($filePath, sprintf('Real path for file from diff can not be calculated. Diff: %s', $unifiedGreppedDiff)); + + preg_match('/\s\+(.*)\s@/', $line, $matches); + + Assert::keyExists( + $matches, + self::MATCH_INDEX, + sprintf('Added/modified lines can not be found in the following diff line: "%s"', $line) + ); + + // can be "523,12", meaning from 523 lines new 12 are added; or just "532" + $linesText = $matches[self::MATCH_INDEX]; + + $lineParts = array_map('\intval', explode(',', $linesText)); + + Assert::minCount($lineParts, 1); + + $startLine = $lineParts[0]; + $endLine = count($lineParts) > 1 ? $lineParts[0] + $lineParts[1] - 1 : $startLine; + + $resultMap[$filePath][] = new ChangedLinesRange($startLine, $endLine); + } + } + + return $resultMap; + } +} diff --git a/src/Differ/FilesDiffChangedLines.php b/src/Differ/FilesDiffChangedLines.php new file mode 100644 index 000000000..d9f6f62be --- /dev/null +++ b/src/Differ/FilesDiffChangedLines.php @@ -0,0 +1,72 @@ + */ + private ?array $memoizedFilesChangedLinesMap; + + public function __construct(DiffChangedLinesParser $diffChangedLinesParser, GitDiffFileProvider $diffFileProvider) + { + $this->diffChangedLinesParser = $diffChangedLinesParser; + $this->diffFileProvider = $diffFileProvider; + } + + public function contains(string $fileRealPath, int $mutationStartLine, int $mutationEndLine, ?string $gitDiffBase): bool + { + $map = $this->memoizedFilesChangedLinesMap + ?? $this->memoizedFilesChangedLinesMap = $this->diffChangedLinesParser->parse( + $this->diffFileProvider->provideWithLines($gitDiffBase ?? GitDiffFileProvider::DEFAULT_BASE) + ); + + foreach ($map[$fileRealPath] ?? [] as $changedLinesRange) { + if ($mutationEndLine >= $changedLinesRange->getStartLine() && $mutationStartLine <= $changedLinesRange->getEndLine()) { + return true; + } + } + + return false; + } +} diff --git a/src/Logger/GitHub/GitDiffFileProvider.php b/src/Logger/GitHub/GitDiffFileProvider.php index cf2211288..10493d2b1 100644 --- a/src/Logger/GitHub/GitDiffFileProvider.php +++ b/src/Logger/GitHub/GitDiffFileProvider.php @@ -69,4 +69,12 @@ public function provide(string $gitDiffFilter, string $gitDiffBase): string return $filter; } + + public function provideWithLines(string $gitDiffBase): string + { + return $this->shellCommandLineExecutor->execute(sprintf( + "git diff %s --unified=0 --diff-filter=AM | grep -v -e '^[+-]' -e '^index'", + escapeshellarg($gitDiffBase) + )); + } } diff --git a/src/Mutation/FileMutationGenerator.php b/src/Mutation/FileMutationGenerator.php index 4c307f383..d703deda0 100644 --- a/src/Mutation/FileMutationGenerator.php +++ b/src/Mutation/FileMutationGenerator.php @@ -35,6 +35,7 @@ namespace Infection\Mutation; +use Infection\Differ\FilesDiffChangedLines; use Infection\Mutator\Mutator; use Infection\Mutator\NodeMutationGenerator; use Infection\PhpParser\FileParser; @@ -55,15 +56,24 @@ class FileMutationGenerator private FileParser $parser; private NodeTraverserFactory $traverserFactory; private LineRangeCalculator $lineRangeCalculator; + private FilesDiffChangedLines $filesDiffChangedLines; + private bool $isForGitDiffLines; + private ?string $gitDiffBase; public function __construct( FileParser $parser, NodeTraverserFactory $traverserFactory, - LineRangeCalculator $lineRangeCalculator + LineRangeCalculator $lineRangeCalculator, + FilesDiffChangedLines $filesDiffChangedLines, + bool $isForGitDiffLines, + ?string $gitDiffBase ) { $this->parser = $parser; $this->traverserFactory = $traverserFactory; $this->lineRangeCalculator = $lineRangeCalculator; + $this->filesDiffChangedLines = $filesDiffChangedLines; + $this->isForGitDiffLines = $isForGitDiffLines; + $this->gitDiffBase = $gitDiffBase; } /** @@ -98,7 +108,10 @@ public function generate( $initialStatements, $trace, $onlyCovered, - $this->lineRangeCalculator + $this->isForGitDiffLines, + $this->gitDiffBase, + $this->lineRangeCalculator, + $this->filesDiffChangedLines ) ); diff --git a/src/Mutator/NodeMutationGenerator.php b/src/Mutator/NodeMutationGenerator.php index 0b1b13eb7..3f7057e66 100644 --- a/src/Mutator/NodeMutationGenerator.php +++ b/src/Mutator/NodeMutationGenerator.php @@ -38,6 +38,8 @@ use function count; use function get_class; use Infection\AbstractTestFramework\Coverage\TestLocation; +use Infection\Differ\FilesDiffChangedLines; +use Infection\Logger\GitHub\GitDiffFileProvider; use Infection\Mutation\Mutation; use Infection\PhpParser\MutatedNode; use Infection\PhpParser\Visitor\ReflectionVisitor; @@ -69,6 +71,9 @@ class NodeMutationGenerator private ?array $testsMemoized = null; private ?bool $isOnFunctionSignatureMemoized = null; private ?bool $isInsideFunctionMemoized = null; + private FilesDiffChangedLines $filesDiffChangedLines; + private bool $isForGitDiffLines; + private ?string $gitDiffBase; /** * @param Mutator[] $mutators @@ -80,7 +85,10 @@ public function __construct( array $fileNodes, Trace $trace, bool $onlyCovered, - LineRangeCalculator $lineRangeCalculator + bool $isForGitDiffLines, + ?string $gitDiffBase, + LineRangeCalculator $lineRangeCalculator, + FilesDiffChangedLines $filesDiffChangedLines ) { Assert::allIsInstanceOf($mutators, Mutator::class); @@ -89,7 +97,10 @@ public function __construct( $this->fileNodes = $fileNodes; $this->trace = $trace; $this->onlyCovered = $onlyCovered; + $this->isForGitDiffLines = $isForGitDiffLines; + $this->gitDiffBase = $gitDiffBase; $this->lineRangeCalculator = $lineRangeCalculator; + $this->filesDiffChangedLines = $filesDiffChangedLines; } /** @@ -108,6 +119,10 @@ public function generate(Node $node): iterable return; } + if ($this->isForGitDiffLines && !$this->filesDiffChangedLines->contains($this->filePath, $node->getStartLine(), $node->getEndLine(), $this->gitDiffBase ?? GitDiffFileProvider::DEFAULT_BASE)) { + return; + } + foreach ($this->mutators as $mutator) { yield from $this->generateForMutator($node, $mutator); } diff --git a/tests/benchmark/Tracing/provide-traces-closure.php b/tests/benchmark/Tracing/provide-traces-closure.php index 5140cad96..a741ce153 100644 --- a/tests/benchmark/Tracing/provide-traces-closure.php +++ b/tests/benchmark/Tracing/provide-traces-closure.php @@ -68,6 +68,7 @@ Container::DEFAULT_THREAD_COUNT, Container::DEFAULT_DRY_RUN, Container::DEFAULT_GIT_DIFF_FILTER, + Container::DEFAULT_GIT_DIFF_LINES, Container::DEFAULT_GIT_DIFF_BASE, Container::DEFAULT_USE_GITHUB_LOGGER, true, diff --git a/tests/phpunit/Configuration/ConfigurationAssertions.php b/tests/phpunit/Configuration/ConfigurationAssertions.php index 5aa9edd46..31237a8d8 100644 --- a/tests/phpunit/Configuration/ConfigurationAssertions.php +++ b/tests/phpunit/Configuration/ConfigurationAssertions.php @@ -84,7 +84,9 @@ private function assertConfigurationStateIs( int $expectedThreadCount, bool $expectedDryRyn, array $expectedIgnoreSourceCodeMutatorsMap, - bool $expectedExecuteOnlyCoveringTestCases + bool $expectedExecuteOnlyCoveringTestCases, + bool $expectedIsForGitDiffLines, + ?string $expectedGitDiffBase ): void { $this->assertSame($expectedTimeout, $configuration->getProcessTimeout()); $this->assertSame($expectedSourceDirectories, $configuration->getSourceDirectories()); @@ -134,6 +136,8 @@ private function assertConfigurationStateIs( $this->assertSame($expectedDryRyn, $configuration->isDryRun()); $this->assertSame($expectedIgnoreSourceCodeMutatorsMap, $configuration->getIgnoreSourceCodeMutatorsMap()); $this->assertSame($expectedExecuteOnlyCoveringTestCases, $configuration->getExecuteOnlyCoveringTestCases()); + $this->assertSame($expectedIsForGitDiffLines, $configuration->isForGitDiffLines()); + $this->assertSame($expectedGitDiffBase, $configuration->getGitDiffBase()); } /** diff --git a/tests/phpunit/Configuration/ConfigurationFactoryTest.php b/tests/phpunit/Configuration/ConfigurationFactoryTest.php index 91624f65f..14c7d9f3f 100644 --- a/tests/phpunit/Configuration/ConfigurationFactoryTest.php +++ b/tests/phpunit/Configuration/ConfigurationFactoryTest.php @@ -110,6 +110,7 @@ public function test_it_can_create_a_configuration( int $inputThreadsCount, bool $inputDryRun, ?string $inputGitDiffFilter, + bool $inputIsForGitDiffLines, string $inputGitDiffBase, bool $inputUseGitHubAnnotationsLogger, bool $inputUseNoopMutators, @@ -164,6 +165,7 @@ public function test_it_can_create_a_configuration( $inputThreadsCount, $inputDryRun, $inputGitDiffFilter, + $inputIsForGitDiffLines, $inputGitDiffBase, $inputUseGitHubAnnotationsLogger, $inputUseNoopMutators, @@ -201,7 +203,9 @@ public function test_it_can_create_a_configuration( $inputThreadsCount, $inputDryRun, $expectedIgnoreSourceCodeMutatorsMap, - $inputExecuteOnlyCoveringTestCases + $inputExecuteOnlyCoveringTestCases, + $inputIsForGitDiffLines, + $inputGitDiffBase ); } @@ -246,6 +250,7 @@ public function valueProvider(): iterable 0, false, 'AM', + false, 'master', true, false, @@ -689,6 +694,7 @@ public function valueProvider(): iterable 0, false, null, + false, 'master', false, false, @@ -771,6 +777,7 @@ public function valueProvider(): iterable 4, true, null, + false, 'master', false, false, @@ -862,6 +869,7 @@ private static function createValueForTimeout( 0, false, null, + false, 'master', false, false, @@ -935,6 +943,7 @@ private static function createValueForTmpDir( 0, false, null, + false, 'master', false, false, @@ -1009,6 +1018,7 @@ private static function createValueForCoveragePath( 0, false, null, + false, 'master', false, false, @@ -1082,6 +1092,7 @@ private static function createValueForPhpUnitConfigDir( 0, false, null, + false, 'master', false, false, @@ -1156,6 +1167,7 @@ private static function createValueForNoProgress( 0, false, null, + false, 'master', false, false, @@ -1230,6 +1242,7 @@ private static function createValueForIgnoreMsiWithNoMutations( 0, false, null, + false, 'master', false, false, @@ -1304,6 +1317,7 @@ private static function createValueForMinMsi( 0, false, null, + false, 'master', false, false, @@ -1378,6 +1392,7 @@ private static function createValueForMinCoveredMsi( 0, false, null, + false, 'master', false, false, @@ -1453,6 +1468,7 @@ private static function createValueForTestFramework( 0, false, null, + false, 'master', false, false, @@ -1527,6 +1543,7 @@ private static function createValueForInitialTestsPhpOptions( 0, false, null, + false, 'master', false, false, @@ -1602,6 +1619,7 @@ private static function createValueForTestFrameworkExtraOptions( 0, false, null, + false, 'master', false, false, @@ -1676,6 +1694,7 @@ private static function createValueForTestFrameworkKey( 0, false, null, + false, 'master', false, false, @@ -1754,6 +1773,7 @@ private static function createValueForMutators( 0, false, null, + false, 'master', false, $useNoopMutatos, @@ -1831,6 +1851,7 @@ private static function createValueForIgnoreSourceCodeByRegex( 0, false, null, + false, 'master', false, false, diff --git a/tests/phpunit/Configuration/ConfigurationTest.php b/tests/phpunit/Configuration/ConfigurationTest.php index 3686b8514..10b494cfc 100644 --- a/tests/phpunit/Configuration/ConfigurationTest.php +++ b/tests/phpunit/Configuration/ConfigurationTest.php @@ -88,7 +88,9 @@ public function test_it_can_be_instantiated( int $threadsCount, bool $dryRun, array $ignoreSourceCodeMutatorsMap, - bool $executeOnlyCoveringTestCases + bool $executeOnlyCoveringTestCases, + bool $isForGitDiffLines, + ?string $gitDiffBase ): void { $config = new Configuration( $timeout, @@ -119,7 +121,9 @@ public function test_it_can_be_instantiated( $threadsCount, $dryRun, $ignoreSourceCodeMutatorsMap, - $executeOnlyCoveringTestCases + $executeOnlyCoveringTestCases, + $isForGitDiffLines, + $gitDiffBase ); $this->assertConfigurationStateIs( @@ -152,7 +156,9 @@ public function test_it_can_be_instantiated( $threadsCount, $dryRun, $ignoreSourceCodeMutatorsMap, - $executeOnlyCoveringTestCases + $executeOnlyCoveringTestCases, + $isForGitDiffLines, + $gitDiffBase ); } @@ -188,6 +194,8 @@ public function valueProvider(): iterable false, [], false, + false, + 'master', ]; yield 'nominal' => [ @@ -235,6 +243,8 @@ public function valueProvider(): iterable 'For_' => ['.*someMethod.*'], ], true, + false, + 'master', ]; } } diff --git a/tests/phpunit/ContainerTest.php b/tests/phpunit/ContainerTest.php index 1e29ad125..2a6427db9 100644 --- a/tests/phpunit/ContainerTest.php +++ b/tests/phpunit/ContainerTest.php @@ -93,6 +93,7 @@ public function test_it_can_build_lazy_source_file_data_factory_that_fails_on_us Container::DEFAULT_THREAD_COUNT, Container::DEFAULT_DRY_RUN, Container::DEFAULT_GIT_DIFF_FILTER, + Container::DEFAULT_GIT_DIFF_LINES, Container::DEFAULT_GIT_DIFF_BASE, Container::DEFAULT_USE_GITHUB_LOGGER, Container::DEFAULT_USE_NOOP_MUTATORS, @@ -141,6 +142,7 @@ public function test_it_provides_a_friendly_error_when_attempting_to_configure_i Container::DEFAULT_THREAD_COUNT, Container::DEFAULT_DRY_RUN, Container::DEFAULT_GIT_DIFF_FILTER, + Container::DEFAULT_GIT_DIFF_LINES, Container::DEFAULT_GIT_DIFF_BASE, Container::DEFAULT_USE_GITHUB_LOGGER, Container::DEFAULT_USE_NOOP_MUTATORS, diff --git a/tests/phpunit/Differ/ChangedLinesRangeTest.php b/tests/phpunit/Differ/ChangedLinesRangeTest.php new file mode 100644 index 000000000..8d7af7915 --- /dev/null +++ b/tests/phpunit/Differ/ChangedLinesRangeTest.php @@ -0,0 +1,50 @@ +assertSame(3, $range->getStartLine()); + $this->assertSame(6, $range->getEndLine()); + } +} diff --git a/tests/phpunit/Differ/DiffChangedLinesParserTest.php b/tests/phpunit/Differ/DiffChangedLinesParserTest.php new file mode 100644 index 000000000..a60a7ed59 --- /dev/null +++ b/tests/phpunit/Differ/DiffChangedLinesParserTest.php @@ -0,0 +1,125 @@ +parse($diff); + + $this->assertSame($this->convertToArray($expectedMap), $this->convertToArray($resultMap)); + } + + public function provideDiffs(): Generator + { + yield 'one file with added lines in different places' => [ + <<<'DIFF' + diff --git a/src/Container.php b/src/Container.php + @@ -37,0 +38 @@ namespace Infection; + @@ -533 +534,2 @@ final class Container + @@ -535,0 +538,3 @@ final class Container + @@ -1207,0 +1213,5 @@ final class Container + DIFF, + [ + realpath('src/Container.php') => [ + new ChangedLinesRange(38, 38), + new ChangedLinesRange(534, 535), + new ChangedLinesRange(538, 540), + new ChangedLinesRange(1213, 1217), + ], + ], + ]; + + yield 'two files, second one is new created' => [ + <<<'DIFF' + diff --git a/src/Container.php b/src/Container.php + @@ -37,0 +38 @@ namespace Infection; + @@ -533 +534,2 @@ final class Container + @@ -535,0 +538,3 @@ final class Container + @@ -1207,0 +1213,5 @@ final class Container + diff --git a/src/Differ/FilesDiffChangedLines.php b/src/Differ/FilesDiffChangedLines.php + new file mode 100644 + @@ -0,0 +1,18 @@ + DIFF, + [ + realpath('src/Container.php') => [ + new ChangedLinesRange(38, 38), + new ChangedLinesRange(534, 535), + new ChangedLinesRange(538, 540), + new ChangedLinesRange(1213, 1217), + ], + realpath('src/Differ/FilesDiffChangedLines.php') => [ + new ChangedLinesRange(1, 18), + ], + ], + ]; + } + + /** + * @param array> $map + */ + private function convertToArray(array $map): array + { + $convertedMap = []; + + foreach ($map as $filePath => $changedLinesRanges) { + $convertedMap[$filePath] = array_map( + static function (ChangedLinesRange $changedLinesRange): array { + return [$changedLinesRange->getStartLine(), $changedLinesRange->getEndLine()]; + }, + $changedLinesRanges + ); + } + + return $convertedMap; + } +} diff --git a/tests/phpunit/Differ/FilesDiffChangedLinesTest.php b/tests/phpunit/Differ/FilesDiffChangedLinesTest.php new file mode 100644 index 000000000..a709d0d6e --- /dev/null +++ b/tests/phpunit/Differ/FilesDiffChangedLinesTest.php @@ -0,0 +1,209 @@ +prepareServices([]); + + $filesDiffChangedLines = new FilesDiffChangedLines( + $parser, + $diffProvider + ); + + $filesDiffChangedLines->contains('/path/to/File.php', 1, 1, 'master'); + + // the second call should reuse memoized results cached previously + $filesDiffChangedLines->contains('/path/to/File.php', 1, 1, 'master'); + } + + /** + * @dataProvider provideLines + */ + public function test_it_finds_line_in_changed_lines_from_diff( + bool $expectedIsFound, + array $returnedFilesDiffChangedLinesMap, + int $mutationStartLine, + int $mutationEndLine + ): void { + [$parser, $diffProvider] = $this->prepareServices($returnedFilesDiffChangedLinesMap); + + $filesDiffChangedLines = new FilesDiffChangedLines( + $parser, + $diffProvider + ); + + $isLineFoundInDiff = $filesDiffChangedLines->contains('/path/to/File.php', $mutationStartLine, $mutationEndLine, 'master'); + + $this->assertSame($expectedIsFound, $isLineFoundInDiff, sprintf('Line %d was not found in diff', $mutationStartLine)); + } + + public function provideLines(): Generator + { + yield 'not found line in one-line range before' => [ + false, + [ + '/path/to/File.php' => [new ChangedLinesRange(3, 3)], + ], + 1, + 1, + ]; + + yield 'not found line in one-line range after' => [ + false, + [ + '/path/to/File.php' => [new ChangedLinesRange(3, 3)], + ], + 5, + 5, + ]; + + yield 'line in one-line range' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(3, 3)], + ], + 3, + 3, + ]; + + yield 'line in multi-line range in the beginning' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(3, 5)], + ], + 3, + 3, + ]; + + yield 'line in multi-line range in the middle' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(1, 5)], + ], + 3, + 3, + ]; + + yield 'line in multi-line range in the end' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(1, 3)], + ], + 3, + 3, + ]; + + yield 'line in the second range' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(1, 1), new ChangedLinesRange(3, 5)], + ], + 4, + 4, + ]; + + yield 'mutation range in one-line range, around' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(3, 3)], + ], + 1, + 4, + ]; + + yield 'mutation range in one-line range, before' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(3, 3)], + ], + 1, + 3, + ]; + + yield 'mutation range in one-line range, after' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(3, 3)], + ], + 3, + 5, + ]; + + yield 'mutation range in one-line range, inside' => [ + true, + [ + '/path/to/File.php' => [new ChangedLinesRange(1, 30)], + ], + 3, + 5, + ]; + } + + /** + * @return array{0: DiffChangedLinesParser, 1: GitDiffFileProvider} + */ + private function prepareServices(array $returnedFilesDiffChangedLinesMap): array + { + /** @var DiffChangedLinesParser&MockObject $parser */ + $parser = $this->createMock(DiffChangedLinesParser::class); + $parser + ->expects($this->once()) + ->method('parse') + ->willReturn($returnedFilesDiffChangedLinesMap); + + /** @var GitDiffFileProvider&MockObject $diffProvider */ + $diffProvider = $this->createMock(GitDiffFileProvider::class); + $diffProvider + ->expects($this->once()) + ->method('provideWithLines') + ->with('master') + ->willReturn(''); + + return [$parser, $diffProvider]; + } +} diff --git a/tests/phpunit/Mutation/FileMutationGeneratorTest.php b/tests/phpunit/Mutation/FileMutationGeneratorTest.php index 82a48d6cb..1560adf78 100644 --- a/tests/phpunit/Mutation/FileMutationGeneratorTest.php +++ b/tests/phpunit/Mutation/FileMutationGeneratorTest.php @@ -36,6 +36,7 @@ namespace Infection\Tests\Mutation; use function current; +use Infection\Differ\FilesDiffChangedLines; use Infection\Mutation\FileMutationGenerator; use Infection\Mutation\Mutation; use Infection\Mutator\Arithmetic\Plus; @@ -62,12 +63,12 @@ final class FileMutationGeneratorTest extends TestCase private const FIXTURES_DIR = __DIR__ . '/../Fixtures/Files'; /** - * @var FileParser|MockObject + * @var FileParser&MockObject */ private $fileParserMock; /** - * @var NodeTraverserFactory|MockObject + * @var NodeTraverserFactory&MockObject */ private $traverserFactoryMock; @@ -76,15 +77,24 @@ final class FileMutationGeneratorTest extends TestCase */ private $mutationGenerator; + /** + * @var FilesDiffChangedLines&MockObject + */ + private $filesDiffChangedLines; + protected function setUp(): void { $this->fileParserMock = $this->createMock(FileParser::class); $this->traverserFactoryMock = $this->createMock(NodeTraverserFactory::class); + $this->filesDiffChangedLines = $this->createMock(FilesDiffChangedLines::class); $this->mutationGenerator = new FileMutationGenerator( $this->fileParserMock, $this->traverserFactoryMock, - new LineRangeCalculator() + new LineRangeCalculator(), + $this->filesDiffChangedLines, + false, + 'master' ); } @@ -197,7 +207,10 @@ public function test_it_skips_the_mutation_generation_if_checks_only_covered_cod $mutationGenerator = new FileMutationGenerator( $this->fileParserMock, $this->traverserFactoryMock, - new LineRangeCalculator() + new LineRangeCalculator(), + $this->filesDiffChangedLines, + false, + 'master' ); $mutations = $mutationGenerator->generate(