Skip to content

Commit

Permalink
Add new --git-diff-lines option to generated Mutants only for the c…
Browse files Browse the repository at this point in the history
…hanged *lines* (#1632)

* Add new `--git-diff-lines` option to generated Mutants only for the changed *lines*

This option automatically mutants only **A**dded and **M**odified files, and finds and mutates only changed/created lines.

Useful to check how your changes impacts MSI in a feature branch

* Allow passing `--git-diff-lines` with `--git-diff-base`
  • Loading branch information
maks-rafalko committed Jan 3, 2022
1 parent 0590b79 commit e55f980
Show file tree
Hide file tree
Showing 20 changed files with 816 additions and 27 deletions.
3 changes: 3 additions & 0 deletions devTools/phpstan-src.neon
Expand Up @@ -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\<string, array\<int, Infection\\Differ\\ChangedLinesRange\>\>#'
level: 8
paths:
- ../src
Expand Down
32 changes: 24 additions & 8 deletions src/Command/RunCommand.php
Expand Up @@ -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';

Expand Down Expand Up @@ -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. <comment>"--%s=Plus,PublicVisibility"</comment>', self::OPTION_MUTATORS),
Container::DEFAULT_MUTATORS_INPUT
)
->addOption(
Expand All @@ -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 <comment>"--diff-filter"</comment> option. <comment>A</comment> - only for added files, <comment>AM</comment> - for added and modified.',
Container::DEFAULT_GIT_DIFF_FILTER
)
->addOption(
self::OPTION_GIT_DIFF_LINES,
null,
InputOption::VALUE_NONE,
'Mutates only added and modified <comment>lines</comment> 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 <comment>"--%1$s"</comment> option. Must be used only together with <comment>"--%1$s"</comment>.', self::OPTION_GIT_DIFF_FILTER),
Container::DEFAULT_GIT_DIFF_BASE
)
->addOption(
Expand All @@ -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 <comment>"--filter"</comment> option',
)
->addOption(
self::OPTION_MIN_MSI,
Expand Down Expand Up @@ -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 <comment>"--%s"</comment> option presented',
self::OPTION_COVERAGE
),
Container::DEFAULT_INITIAL_TESTS_PHP_OPTIONS
Expand All @@ -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 <comment>"--%s"</comment> option', self::OPTION_COVERAGE)
)
->addOption(
self::OPTION_IGNORE_MSI_WITH_NO_MUTATIONS,
Expand Down Expand Up @@ -399,17 +409,22 @@ 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));
}

$filter = trim((string) $input->getOption(self::OPTION_FILTER));

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

Expand Down Expand Up @@ -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),
Expand Down
18 changes: 17 additions & 1 deletion src/Configuration/Configuration.php
Expand Up @@ -88,6 +88,8 @@ class Configuration
/** @var array<string, array<int, string>> */
private array $ignoreSourceCodeMutatorsMap;
private bool $executeOnlyCoveringTestCases;
private bool $isForGitDiffLines;
private ?string $gitDiffBase;

/**
* @param string[] $sourceDirectories
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
19 changes: 14 additions & 5 deletions src/Configuration/ConfigurationFactory.php
Expand Up @@ -116,6 +116,7 @@ public function create(
int $threadCount,
bool $dryRun,
?string $gitDiffFilter,
bool $isForGitDiffLines,
?string $gitDiffBase,
bool $useGitHubLogger,
bool $useNoopMutators,
Expand Down Expand Up @@ -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,
Expand All @@ -172,7 +173,9 @@ public function create(
$threadCount,
$dryRun,
$ignoreSourceCodeMutatorsMap,
$executeOnlyCoveringTestCases
$executeOnlyCoveringTestCases,
$isForGitDiffLines,
$gitDiffBase
);
}

Expand Down Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion src/Container.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -720,6 +735,7 @@ public function withValues(
int $threadCount,
bool $dryRun,
?string $gitDiffFilter,
bool $isForGitDiffLines,
?string $gitDiffBase,
bool $useGitHubLogger,
bool $useNoopMutators,
Expand Down Expand Up @@ -796,6 +812,7 @@ static function (self $container) use (
$threadCount,
$dryRun,
$gitDiffFilter,
$isForGitDiffLines,
$gitDiffBase,
$useGitHubLogger,
$useNoopMutators,
Expand All @@ -822,6 +839,7 @@ static function (self $container) use (
$threadCount,
$dryRun,
$gitDiffFilter,
$isForGitDiffLines,
$gitDiffBase,
$useGitHubLogger,
$useNoopMutators,
Expand Down Expand Up @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions src/Differ/ChangedLinesRange.php
@@ -0,0 +1,61 @@
<?php
/**
* This code is licensed under the BSD 3-Clause License.
*
* Copyright (c) 2017, Maks Rafalko
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

declare(strict_types=1);

namespace Infection\Differ;

/**
* @internal
*/
final class ChangedLinesRange
{
private int $startLine;
private int $endLine;

public function __construct(int $startLine, int $endLine)
{
$this->startLine = $startLine;
$this->endLine = $endLine;
}

public function getStartLine(): int
{
return $this->startLine;
}

public function getEndLine(): int
{
return $this->endLine;
}
}

0 comments on commit e55f980

Please sign in to comment.