From 0fab9b477261a30f62b3d98438339f28db277ca7 Mon Sep 17 00:00:00 2001 From: Sandro Gehri Date: Fri, 24 Nov 2023 08:06:43 +0100 Subject: [PATCH] Add ignore option --- README.md | 35 ++++++++++++++++--- WIP.md | 1 - docs/mutation-testing.md | 13 +++++-- src/Contracts/Configuration.php | 5 +++ src/Decorators/TestCallDecorator.php | 10 ++++++ src/Options/IgnoreOption.php | 27 ++++++++++++++ .../{PathsOption.php => PathOption.php} | 2 +- src/Repositories/ConfigurationRepository.php | 1 + .../Configuration/AbstractConfiguration.php | 18 +++++++++- .../Configuration/CliConfiguration.php | 14 +++++--- src/Support/Configuration/Configuration.php | 2 ++ src/Tester/MutationTestRunner.php | 11 +++--- ...HandleTestCallProfileConfigurationTest.php | 8 +++++ .../HandlesCliProfileConfigurationTest.php | 13 ++++++- .../HandlesGlobalProfileConfigurationTest.php | 14 ++++++++ 15 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 src/Options/IgnoreOption.php rename src/Options/{PathsOption.php => PathOption.php} (96%) diff --git a/README.md b/README.md index 8dae6ce..a77417f 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ The following options are available.
- [`path()`](#path) +- [`ignore()`](#ignore) - [`class()`](#class) - [`mutator()`](#mutator) - [`except()`](#except) @@ -152,12 +153,20 @@ Limit the directories or files to mutate by providing one or more paths to a dir If no paths are provided, it defaults to the source directories configured in your `phpunit.xml` file. ```php +mutate() + ->path('src'); +``` + -You can also use patterns. + +### `ignore()` +CLI: `--ignore` + +Ignore one or more directory or file paths. ```php mutate() - ->path('src'); + ->ignore('src/Contracts'); ``` @@ -350,8 +359,9 @@ vendor/bin/pest --mutate --covered-only ### Run tests in parallel -> Attention: This may slow down your tests if you have a small test suite or only a small part of your test suite is run for a certain mutation. -> Probably I am going to replace this in favor of running multiple mutations in parallel. +Run tests against multiple mutations in parallel. This can significantly reduce the time it takes to run mutation tests. + +Against a single mutation the tests are not run in parallel, regardless of the parallel option. ```bash vendor/bin/pest --mutate --parallel @@ -624,8 +634,10 @@ This set consists of various mutators from different sets. Mutators included are
+- [RemoveArrayItem](#removearrayitem-) (*) - [RemoveFunctionCall](#removefunctioncall-) (*) - [RemoveMethodCall](#removemethodcall-) (*) +- [RemoveNullSafeOperator](#removenullsafeoperator-) (*)
@@ -1671,6 +1683,19 @@ $a = (array) $b; // [tl! remove] $a = $b; // [tl! add] ``` + +### RemoveArrayItem (*) +Set: Removal + +Removes an item from an array + +```php +return [ + 'foo' => 1, // [tl! remove] + 'bar' => 2, +]; +``` + ### RemoveBooleanCast (*) Set: Casting @@ -1738,7 +1763,7 @@ if ($a) { // [tl! add] ``` -### RemoveNullSafeOperator +### RemoveNullSafeOperator (*) Set: Removal Converts nullsafe method and property calls to regular calls. diff --git a/WIP.md b/WIP.md index 10766bd..3449756 100644 --- a/WIP.md +++ b/WIP.md @@ -29,7 +29,6 @@ # Backlog Prio 1 - [ ] Fix PHPStorm indexing Issues. Maybe do not end cached files with .php? - [ ] Automatically empty cache when package version changes -- [ ] Exclude paths or files - [ ] What should we do with interfaces? ignore them completely? - [ ] Finish: Disable mutations by annotation - [ ] Run mutations in a reasonable order: New, Survived, NotCovered, Skipped, Killed (Survived first, if --stop-on-survived or --bail; NotCovered first, if --stop-on-uncovered) diff --git a/docs/mutation-testing.md b/docs/mutation-testing.md index 36a041d..53856ef 100644 --- a/docs/mutation-testing.md +++ b/docs/mutation-testing.md @@ -120,6 +120,7 @@ The following options are available.
- [`path()`](#path) +- [`ignore()`](#ignore) - [`class()`](#class) - [`mutator()`](#mutator) - [`except()`](#except) @@ -144,12 +145,20 @@ Limit the directories or files to mutate by providing one or more paths to a dir If no paths are provided, it defaults to the source directories configured in your `phpunit.xml` file. ```php +mutate() + ->path('src'); +``` -You can also use patterns. + + +### `ignore()` +CLI: `--ignore` + +Ignore one or more directory or file paths. ```php mutate() - ->path('src'); + ->ignore('src/Contracts'); ``` diff --git a/src/Contracts/Configuration.php b/src/Contracts/Configuration.php index 77efe04..1f3077a 100644 --- a/src/Contracts/Configuration.php +++ b/src/Contracts/Configuration.php @@ -11,6 +11,11 @@ interface Configuration */ public function path(array|string ...$paths): self; + /** + * @param array|string ...$paths + */ + public function ignore(array|string ...$paths): self; + /** * @param array>|class-string ...$mutators */ diff --git a/src/Decorators/TestCallDecorator.php b/src/Decorators/TestCallDecorator.php index 8d8f5cf..ea1d412 100644 --- a/src/Decorators/TestCallDecorator.php +++ b/src/Decorators/TestCallDecorator.php @@ -82,6 +82,16 @@ public function path(array|string ...$paths): self return $this; } + /** + * @param array|string ...$paths + */ + public function ignore(array|string ...$paths): self + { + $this->configuration->ignore(...$paths); + + return $this; + } + /** * {@inheritDoc} */ diff --git a/src/Options/IgnoreOption.php b/src/Options/IgnoreOption.php new file mode 100644 index 0000000..b9d9802 --- /dev/null +++ b/src/Options/IgnoreOption.php @@ -0,0 +1,27 @@ +mergedConfiguration = new Configuration( coveredOnly: $config['covered_only'] ?? false, paths: $config['paths'] ?? $this->pathsFromPhpunitConfiguration(), + pathsToIgnore: $config['paths_to_ignore'] ?? [], mutators: array_diff($config['mutators'] ?? DefaultSet::mutators(), $config['excluded_mutators'] ?? []), classes: $config['classes'] ?? [], parallel: $parallel, diff --git a/src/Support/Configuration/AbstractConfiguration.php b/src/Support/Configuration/AbstractConfiguration.php index 121b9af..e8012ce 100644 --- a/src/Support/Configuration/AbstractConfiguration.php +++ b/src/Support/Configuration/AbstractConfiguration.php @@ -16,6 +16,11 @@ abstract class AbstractConfiguration implements ConfigurationContract */ private ?array $paths = null; + /** + * @var string[]|null + */ + private ?array $pathsToIgnore = null; + /** * @var class-string[]|null */ @@ -57,6 +62,16 @@ public function path(array|string ...$paths): self return $this; } + /** + * {@inheritDoc} + */ + public function ignore(array|string ...$paths): self + { + $this->pathsToIgnore = array_merge(...array_map(fn (string|array $path): array => is_string($path) ? [$path] : $path, $paths)); + + return $this; + } + /** * {@inheritDoc} */ @@ -152,12 +167,13 @@ public function changedOnly(?string $branch = 'main'): self } /** - * @return array{paths?: string[], mutators?: class-string[], excluded_mutators?: class-string[], classes?: string[], parallel?: bool, processes?: int, min_score?: float, covered_only?: bool, stop_on_survived?: bool, stop_on_not_covered?: bool, uncommitted_only?: bool, changed_only?: string} + * @return array{paths?: string[], paths_to_ignore?: string[], mutators?: class-string[], excluded_mutators?: class-string[], classes?: string[], parallel?: bool, processes?: int, min_score?: float, covered_only?: bool, stop_on_survived?: bool, stop_on_not_covered?: bool, uncommitted_only?: bool, changed_only?: string} */ public function toArray(): array { return array_filter([ 'paths' => $this->paths, + 'paths_to_ignore' => $this->pathsToIgnore, 'mutators' => $this->mutators !== null ? array_values(array_diff($this->mutators, $this->excludedMutators ?? [])) : null, 'excluded_mutators' => $this->excludedMutators, 'classes' => $this->classes, diff --git a/src/Support/Configuration/CliConfiguration.php b/src/Support/Configuration/CliConfiguration.php index a8bc4d7..c01531b 100644 --- a/src/Support/Configuration/CliConfiguration.php +++ b/src/Support/Configuration/CliConfiguration.php @@ -9,11 +9,12 @@ use Pest\Mutate\Options\ClassOption; use Pest\Mutate\Options\CoveredOnlyOption; use Pest\Mutate\Options\ExceptOption; +use Pest\Mutate\Options\IgnoreOption; use Pest\Mutate\Options\MinScoreOption; use Pest\Mutate\Options\MutateOption; use Pest\Mutate\Options\MutatorsOption; use Pest\Mutate\Options\ParallelOption; -use Pest\Mutate\Options\PathsOption; +use Pest\Mutate\Options\PathOption; use Pest\Mutate\Options\ProcessesOption; use Pest\Mutate\Options\StopOnNotCoveredOption; use Pest\Mutate\Options\StopOnSurvivedOption; @@ -30,7 +31,8 @@ class CliConfiguration extends AbstractConfiguration MinScoreOption::class, MutatorsOption::class, ExceptOption::class, - PathsOption::class, + PathOption::class, + IgnoreOption::class, ParallelOption::class, ProcessesOption::class, StopOnSurvivedOption::class, @@ -67,8 +69,12 @@ public function fromArguments(array $arguments): array $this->coveredOnly($input->getOption(CoveredOnlyOption::ARGUMENT) !== 'false'); } - if ($input->hasOption(PathsOption::ARGUMENT)) { - $this->path(explode(',', (string) $input->getOption(PathsOption::ARGUMENT))); // @phpstan-ignore-line + if ($input->hasOption(PathOption::ARGUMENT)) { + $this->path(explode(',', (string) $input->getOption(PathOption::ARGUMENT))); // @phpstan-ignore-line + } + + if ($input->hasOption(IgnoreOption::ARGUMENT)) { + $this->ignore(explode(',', (string) $input->getOption(IgnoreOption::ARGUMENT))); // @phpstan-ignore-line } if ($input->hasOption(MutatorsOption::ARGUMENT)) { diff --git a/src/Support/Configuration/Configuration.php b/src/Support/Configuration/Configuration.php index 72f4ca5..1f08162 100644 --- a/src/Support/Configuration/Configuration.php +++ b/src/Support/Configuration/Configuration.php @@ -10,12 +10,14 @@ class Configuration { /** * @param array $paths + * @param array $pathsToIgnore * @param array> $mutators * @param array $classes */ public function __construct( public readonly bool $coveredOnly, public readonly array $paths, + public readonly array $pathsToIgnore, public readonly array $mutators, public readonly array $classes, public readonly bool $parallel, diff --git a/src/Tester/MutationTestRunner.php b/src/Tester/MutationTestRunner.php index 98ba366..be1c480 100644 --- a/src/Tester/MutationTestRunner.php +++ b/src/Tester/MutationTestRunner.php @@ -108,7 +108,7 @@ public function run(): void $coveredLines = array_map(fn (array $lines): array => array_filter($lines, fn (array $tests): bool => $tests !== []), $codeCoverage->getData()->lineCoverage()); $coveredLines = array_filter($coveredLines, fn (array $lines): bool => $lines !== []); - $files = $this->getFiles($this->getConfiguration()->paths); + $files = $this->getFiles($this->getConfiguration()->paths, $this->getConfiguration()->pathsToIgnore); /** @var MutationGenerator $generator */ $generator = Container::getInstance()->get(MutationGenerator::class); @@ -182,8 +182,9 @@ classesToMutate: $this->getConfiguration()->classes, /** * @param array $paths + * @param array $pathsToIgnore */ - private function getFiles(array $paths): Finder + private function getFiles(array $paths, array $pathsToIgnore): Finder { $dirs = []; $filePaths = []; @@ -199,12 +200,14 @@ private function getFiles(array $paths): Finder } } + $pathsToIgnore = array_map(fn (string $path): string => str_starts_with($path, DIRECTORY_SEPARATOR) ? $path : getcwd().DIRECTORY_SEPARATOR.ltrim($path, '/'), $pathsToIgnore); + return Finder::create() ->in($dirs) ->name('*.php') -// ->notPath($options->ignoring) ->append($filePaths) - ->files(); + ->files() + ->filter(fn (SplFileInfo $file): bool => array_filter($pathsToIgnore, fn (string $pathToIgnore): bool => str_starts_with($file->getRealPath(), $pathToIgnore)) === []); } private function getConfiguration(): Configuration diff --git a/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php b/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php index b11a9fd..62ca5bd 100644 --- a/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php +++ b/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php @@ -43,6 +43,14 @@ })->mutate(ConfigurationRepository::FAKE.'_3') ->path('src/folder-1', 'src/folder-2'); +it('sets the paths to ignore from test', function (): void { + $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_14'); + + expect($configuration->toArray()['paths_to_ignore']) + ->toBe(['src/folder-1', 'src/folder-2']); +})->mutate(ConfigurationRepository::FAKE.'_14') + ->ignore('src/folder-1', 'src/folder-2'); + it('sets the mutators from test', function (): void { $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_4'); diff --git a/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php b/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php index 4300919..a0bd577 100644 --- a/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php +++ b/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php @@ -13,7 +13,7 @@ $this->configuration = new CliConfiguration(); }); -it('sets the paths if --paths argument is passed', function (): void { +it('sets the paths if --path argument is passed', function (): void { $this->configuration->fromArguments(['--path=app']); expect($this->configuration->toArray()) @@ -24,6 +24,17 @@ ->paths->toEqual(['src/path-1', 'src/path-2']); }); +it('sets the paths to ignore if --ignore argument is passed', function (): void { + $this->configuration->fromArguments(['--ignore=src/path-1']); + + expect($this->configuration->toArray()) + ->paths_to_ignore->toEqual(['src/path-1']); + + $this->configuration->fromArguments(['--ignore=src/path-1,src/path-2']); + expect($this->configuration->toArray()) + ->paths_to_ignore->toEqual(['src/path-1', 'src/path-2']); +}); + it('sets the mutators if --mutators argument is passed', function (): void { $this->configuration->fromArguments(['--mutator=SetArithmetic']); expect($this->configuration->toArray()) diff --git a/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php b/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php index 92c4d0f..c5a4027 100644 --- a/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php +++ b/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php @@ -36,6 +36,20 @@ ->toEqual(['src/path-1', 'src/path-2', 'src/path-3']); }); +test('globally configure paths to ignore', function (): void { + mutate(ConfigurationRepository::FAKE) + ->ignore(['src/path-1']); + + expect($this->configuration->toArray()['paths_to_ignore']) + ->toEqual(['src/path-1']); + + mutate(ConfigurationRepository::FAKE) + ->ignore(['src/path-1', 'src/path-2'], 'src/path-3'); + + expect($this->configuration->toArray()['paths_to_ignore']) + ->toEqual(['src/path-1', 'src/path-2', 'src/path-3']); +}); + test('globally configure mutators', function (): void { mutate(ConfigurationRepository::FAKE) ->mutator(Mutators::SET_ARITHMETIC);