Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new --git-diff-lines option to generate Mutants only for the changed *lines* #1632

Merged
merged 2 commits into from Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}