From 956b65158fa4188e8c8c9b22ef81db079ddca6ce Mon Sep 17 00:00:00 2001 From: maks-rafalko Date: Wed, 27 Oct 2021 18:47:51 +0300 Subject: [PATCH 1/4] Use Symfony's `Process::fromShellCommandLin()` to execute a command --- src/Logger/GitHub/GitDiffFileProvider.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Logger/GitHub/GitDiffFileProvider.php b/src/Logger/GitHub/GitDiffFileProvider.php index 75122603c..c54a0baed 100644 --- a/src/Logger/GitHub/GitDiffFileProvider.php +++ b/src/Logger/GitHub/GitDiffFileProvider.php @@ -37,7 +37,7 @@ use function escapeshellarg; use function Safe\sprintf; -use function shell_exec; +use Symfony\Component\Process\Process; /** * @final @@ -50,12 +50,12 @@ class GitDiffFileProvider public function provide(string $gitDiffFilter, string $gitDiffBase): string { - return (string) shell_exec( - sprintf( - 'git diff %s --diff-filter=%s --name-only | grep src/ | paste -s -d "," -', - escapeshellarg($gitDiffBase), - escapeshellarg($gitDiffFilter) - ) - ); + $process = Process::fromShellCommandline(sprintf( + 'git diff %s --diff-filter=%s --name-only | grep src/ | paste -s -d "," -', + escapeshellarg($gitDiffBase), + escapeshellarg($gitDiffFilter) + ))->mustRun(); + + return $process->getOutput(); } } From 7b995c277db78ffcbb10c7c53a3affe184e6bc5b Mon Sep 17 00:00:00 2001 From: maks-rafalko Date: Wed, 27 Oct 2021 19:13:19 +0300 Subject: [PATCH 2/4] Stop Infection execution with `0` exit code when git diff filter returns empty result (no files to be mutated) --- src/Container.php | 13 +++- src/Logger/GitHub/GitDiffFileProvider.php | 19 +++-- src/Process/ShellCommandLineExecutor.php | 52 ++++++++++++++ .../ProjectCode/ProjectCodeProvider.php | 4 +- .../Logger/GitHub/GitDiffFileProviderTest.php | 71 +++++++++++++++++++ 5 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 src/Process/ShellCommandLineExecutor.php create mode 100644 tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php diff --git a/src/Container.php b/src/Container.php index 1530b8556..8e0922964 100644 --- a/src/Container.php +++ b/src/Container.php @@ -104,6 +104,7 @@ use Infection\Process\Runner\MutationTestingRunner; use Infection\Process\Runner\ParallelProcessRunner; use Infection\Process\Runner\ProcessRunner; +use Infection\Process\ShellCommandLineExecutor; use Infection\Resource\Memory\MemoryFormatter; use Infection\Resource\Memory\MemoryLimiter; use Infection\Resource\Memory\MemoryLimiterEnvironment; @@ -654,8 +655,11 @@ public static function create(): self DiffSourceCodeMatcher::class => static function (): DiffSourceCodeMatcher { return new DiffSourceCodeMatcher(); }, - GitDiffFileProvider::class => static function (): GitDiffFileProvider { - return new GitDiffFileProvider(); + ShellCommandLineExecutor::class => static function (): ShellCommandLineExecutor { + return new ShellCommandLineExecutor(); + }, + GitDiffFileProvider::class => static function (self $container): GitDiffFileProvider { + return new GitDiffFileProvider($container->getShellCommandLineExecutor()); }, ]); @@ -1256,6 +1260,11 @@ public function getDiffSourceCodeMatcher(): DiffSourceCodeMatcher return $this->get(DiffSourceCodeMatcher::class); } + public function getShellCommandLineExecutor(): ShellCommandLineExecutor + { + return $this->get(ShellCommandLineExecutor::class); + } + public function getGitDiffFileProvider(): GitDiffFileProvider { return $this->get(GitDiffFileProvider::class); diff --git a/src/Logger/GitHub/GitDiffFileProvider.php b/src/Logger/GitHub/GitDiffFileProvider.php index c54a0baed..cf2211288 100644 --- a/src/Logger/GitHub/GitDiffFileProvider.php +++ b/src/Logger/GitHub/GitDiffFileProvider.php @@ -36,8 +36,8 @@ namespace Infection\Logger\GitHub; use function escapeshellarg; +use Infection\Process\ShellCommandLineExecutor; use function Safe\sprintf; -use Symfony\Component\Process\Process; /** * @final @@ -48,14 +48,25 @@ class GitDiffFileProvider { public const DEFAULT_BASE = 'origin/master'; + private ShellCommandLineExecutor $shellCommandLineExecutor; + + public function __construct(ShellCommandLineExecutor $shellCommandLineExecutor) + { + $this->shellCommandLineExecutor = $shellCommandLineExecutor; + } + public function provide(string $gitDiffFilter, string $gitDiffBase): string { - $process = Process::fromShellCommandline(sprintf( + $filter = $this->shellCommandLineExecutor->execute(sprintf( 'git diff %s --diff-filter=%s --name-only | grep src/ | paste -s -d "," -', escapeshellarg($gitDiffBase), escapeshellarg($gitDiffFilter) - ))->mustRun(); + )); + + if ($filter === '') { + throw NoFilesInDiffToMutate::create(); + } - return $process->getOutput(); + return $filter; } } diff --git a/src/Process/ShellCommandLineExecutor.php b/src/Process/ShellCommandLineExecutor.php new file mode 100644 index 000000000..773d779d8 --- /dev/null +++ b/src/Process/ShellCommandLineExecutor.php @@ -0,0 +1,52 @@ +mustRun()->getOutput()); + } +} diff --git a/tests/phpunit/AutoReview/ProjectCode/ProjectCodeProvider.php b/tests/phpunit/AutoReview/ProjectCode/ProjectCodeProvider.php index c1702e802..fac494948 100644 --- a/tests/phpunit/AutoReview/ProjectCode/ProjectCodeProvider.php +++ b/tests/phpunit/AutoReview/ProjectCode/ProjectCodeProvider.php @@ -58,7 +58,6 @@ use Infection\FileSystem\Finder\ComposerExecutableFinder; use Infection\FileSystem\Finder\NonExecutableFinder; use Infection\FileSystem\Finder\TestFrameworkFinder; -use Infection\Logger\GitHub\GitDiffFileProvider; use Infection\Logger\Http\StrykerCurlClient; use Infection\Logger\Http\StrykerDashboardClient; use Infection\Metrics\MetricsCalculator; @@ -67,6 +66,7 @@ use Infection\Mutator\NodeMutationGenerator; use Infection\Process\OriginalPhpProcess; use Infection\Process\Runner\IndexedProcessBearer; +use Infection\Process\ShellCommandLineExecutor; use Infection\TestFramework\AdapterInstaller; use Infection\TestFramework\Coverage\JUnit\TestFileTimeData; use Infection\TestFramework\Coverage\NodeLineRangeData; @@ -114,7 +114,7 @@ final class ProjectCodeProvider XdebugHandler::class, NullSubscriber::class, FormatterName::class, - GitDiffFileProvider::class, + ShellCommandLineExecutor::class, ]; /** diff --git a/tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php b/tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php new file mode 100644 index 000000000..6b7a970f5 --- /dev/null +++ b/tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php @@ -0,0 +1,71 @@ +createMock(ShellCommandLineExecutor::class); + $shellCommandLineExecutor->expects($this->once()) + ->method('execute') + ->willReturn(''); + + $this->expectException(NoFilesInDiffToMutate::class); + + $diffProvider = new GitDiffFileProvider($shellCommandLineExecutor); + $diffProvider->provide('AM', 'master'); + } + + public function test_it_executes_diff_and_returns_filter_as_a_string(): void + { + $shellCommandLineExecutor = $this->createMock(ShellCommandLineExecutor::class); + $shellCommandLineExecutor->expects($this->once()) + ->method('execute') + ->with('git diff \'master\' --diff-filter=\'AM\' --name-only | grep src/ | paste -s -d "," -') + ->willReturn('src/A.php,src/B.php'); + + $diffProvider = new GitDiffFileProvider($shellCommandLineExecutor); + $filter = $diffProvider->provide('AM', 'master'); + + $this->assertSame('src/A.php,src/B.php', $filter); + } +} From de6048b06700644ab6c7558eacfffc7d0164eaee Mon Sep 17 00:00:00 2001 From: maks-rafalko Date: Wed, 27 Oct 2021 19:14:48 +0300 Subject: [PATCH 3/4] Stop Infection execution with `0` exit code when git diff filter returns empty result (no files to be mutated) Fixes https://github.com/infection/infection/issues/1599 This will improve the speed of CI builds and immediately stop Infection execution. Example: if `README.md` is updated on the root of the folder, we don't want/need to run Infection against the whole project. --- src/Command/RunCommand.php | 39 ++++++++------ src/Logger/GitHub/NoFilesInDiffToMutate.php | 49 +++++++++++++++++ .../GitHub/NoFilesInDiffToMutateTest.php | 53 +++++++++++++++++++ 3 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 src/Logger/GitHub/NoFilesInDiffToMutate.php create mode 100644 tests/phpunit/Logger/GitHub/NoFilesInDiffToMutateTest.php diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 0a0969527..da22c4f1d 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -53,6 +53,7 @@ use Infection\FileSystem\Locator\FileOrDirectoryNotFound; use Infection\FileSystem\Locator\Locator; use Infection\Logger\ConsoleLogger; +use Infection\Logger\GitHub\NoFilesInDiffToMutate; use Infection\Metrics\MinMsiCheckFailed; use Infection\Process\Runner\InitialTestsFailed; use Infection\TestFramework\TestFrameworkTypes; @@ -329,26 +330,30 @@ protected function executeCommand(IO $io): bool $container = $this->createContainer($io, $logger); $consoleOutput = new ConsoleOutput($logger); - $this->startUp($container, $consoleOutput, $logger, $io); - - $engine = new Engine( - $container->getConfiguration(), - $container->getTestFrameworkAdapter(), - $container->getCoverageChecker(), - $container->getEventDispatcher(), - $container->getInitialTestsRunner(), - $container->getMemoryLimiter(), - $container->getMutationGenerator(), - $container->getMutationTestingRunner(), - $container->getMinMsiChecker(), - $consoleOutput, - $container->getMetricsCalculator(), - $container->getTestFrameworkExtraOptionsFilter() - ); - try { + $this->startUp($container, $consoleOutput, $logger, $io); + + $engine = new Engine( + $container->getConfiguration(), + $container->getTestFrameworkAdapter(), + $container->getCoverageChecker(), + $container->getEventDispatcher(), + $container->getInitialTestsRunner(), + $container->getMemoryLimiter(), + $container->getMutationGenerator(), + $container->getMutationTestingRunner(), + $container->getMinMsiChecker(), + $consoleOutput, + $container->getMetricsCalculator(), + $container->getTestFrameworkExtraOptionsFilter() + ); + $engine->execute(); + return true; + } catch (NoFilesInDiffToMutate $e) { + $io->success($e->getMessage()); + return true; } catch (InitialTestsFailed | MinMsiCheckFailed $exception) { // TODO: we can move that in a dedicated logger later and handle those cases in the diff --git a/src/Logger/GitHub/NoFilesInDiffToMutate.php b/src/Logger/GitHub/NoFilesInDiffToMutate.php new file mode 100644 index 000000000..91b416f09 --- /dev/null +++ b/src/Logger/GitHub/NoFilesInDiffToMutate.php @@ -0,0 +1,49 @@ +assertInstanceOf(NoFilesInDiffToMutate::class, $exception); + $this->assertSame( + 'No files in diff found, skipping mutation analysis.', + $exception->getMessage() + ); + } +} From 1193c152b7725c3e3ee6fbb1008cb73caf476a0e Mon Sep 17 00:00:00 2001 From: maks-rafalko Date: Thu, 28 Oct 2021 15:30:58 +0300 Subject: [PATCH 4/4] Depending on OS, check command line differently https://www.php.net/manual/en/function.escapeshellarg.php > On Windows, escapeshellarg() instead replaces percent signs, exclamation marks (delayed variable substitution) and double quotes with spaces and adds double quotes around the string. --- tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php b/tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php index 6b7a970f5..1ae7bd9a4 100644 --- a/tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php +++ b/tests/phpunit/Logger/GitHub/GitDiffFileProviderTest.php @@ -38,6 +38,7 @@ use Infection\Logger\GitHub\GitDiffFileProvider; use Infection\Logger\GitHub\NoFilesInDiffToMutate; use Infection\Process\ShellCommandLineExecutor; +use const PHP_OS_FAMILY; use PHPUnit\Framework\TestCase; final class GitDiffFileProviderTest extends TestCase @@ -57,10 +58,16 @@ public function test_it_throws_no_code_to_mutate_exception_when_diff_is_empty(): public function test_it_executes_diff_and_returns_filter_as_a_string(): void { + $expectedCommandLine = 'git diff \'master\' --diff-filter=\'AM\' --name-only | grep src/ | paste -s -d "," -'; + + if (PHP_OS_FAMILY === 'Windows') { + $expectedCommandLine = 'git diff "master" --diff-filter="AM" --name-only | grep src/ | paste -s -d "," -'; + } + $shellCommandLineExecutor = $this->createMock(ShellCommandLineExecutor::class); $shellCommandLineExecutor->expects($this->once()) ->method('execute') - ->with('git diff \'master\' --diff-filter=\'AM\' --name-only | grep src/ | paste -s -d "," -') + ->with($expectedCommandLine) ->willReturn('src/A.php,src/B.php'); $diffProvider = new GitDiffFileProvider($shellCommandLineExecutor);