From 06f3e17be14b6c66d14b9e3dd4da1061ae1a81be Mon Sep 17 00:00:00 2001 From: Sandro Gehri Date: Wed, 8 Nov 2023 13:45:10 +0100 Subject: [PATCH] Refactor profiles to configurations and ensure arguments are merge in the correct order. --- .../{ProfileFactory.php => Configuration.php} | 2 +- src/Contracts/MutationTestRunner.php | 8 +- src/Decorators/TestCallDecorator.php | 38 +++--- src/Functions.php | 8 +- src/MutationTest.php | 5 +- src/Options/CoveredOnlyOption.php | 2 +- src/Options/MutateOption.php | 2 +- src/Plugins/Mutate.php | 95 ++------------ src/Profile.php | 42 ------- src/Profiles.php | 40 ------ src/Repositories/ConfigurationRepository.php | 86 +++++++++++++ .../DisplayInitialTestRunMessage.php | 3 +- src/Subscribers/EnsurePrinterIsRegistered.php | 4 +- .../Configuration/AbstractConfiguration.php} | 61 ++++++--- .../Configuration/CliConfiguration.php | 85 +++++++++++++ src/Support/Configuration/Configuration.php | 25 ++++ .../Configuration/GlobalConfiguration.php | 9 ++ .../Configuration/TestConfiguration.php | 9 ++ src/Tester/MutationTestRunner.php | 46 +++---- src/Tester/MutationTestRunnerFake.php | 21 +--- ...HandleTestCallProfileConfigurationTest.php | 46 +++---- .../HandlesCliProfileConfigurationTest.php | 117 ++++++++---------- .../HandlesGlobalProfileConfigurationTest.php | 73 ++++------- tests/Pest.php | 8 -- tests/Unit/ProfileFactoryTest.php | 11 -- 25 files changed, 425 insertions(+), 421 deletions(-) rename src/Contracts/{ProfileFactory.php => Configuration.php} (96%) delete mode 100644 src/Profile.php delete mode 100644 src/Profiles.php create mode 100644 src/Repositories/ConfigurationRepository.php rename src/{Factories/ProfileFactory.php => Support/Configuration/AbstractConfiguration.php} (52%) create mode 100644 src/Support/Configuration/CliConfiguration.php create mode 100644 src/Support/Configuration/Configuration.php create mode 100644 src/Support/Configuration/GlobalConfiguration.php create mode 100644 src/Support/Configuration/TestConfiguration.php delete mode 100644 tests/Unit/ProfileFactoryTest.php diff --git a/src/Contracts/ProfileFactory.php b/src/Contracts/Configuration.php similarity index 96% rename from src/Contracts/ProfileFactory.php rename to src/Contracts/Configuration.php index 0d330eb..7e3a250 100644 --- a/src/Contracts/ProfileFactory.php +++ b/src/Contracts/Configuration.php @@ -4,7 +4,7 @@ namespace Pest\Mutate\Contracts; -interface ProfileFactory +interface Configuration { /** * @param array|string ...$paths diff --git a/src/Contracts/MutationTestRunner.php b/src/Contracts/MutationTestRunner.php index 41b3faa..d14ffd8 100644 --- a/src/Contracts/MutationTestRunner.php +++ b/src/Contracts/MutationTestRunner.php @@ -4,11 +4,9 @@ namespace Pest\Mutate\Contracts; -use Pest\Mutate\Factories\ProfileFactory; - interface MutationTestRunner { - public function enable(string $profile): void; + public function enable(): void; public function isEnabled(): bool; @@ -19,9 +17,5 @@ public function setOriginalArguments(array $arguments): void; public function isCodeCoverageRequested(): bool; - public function getProfileFactory(): ProfileFactory; - - public function getEnabledProfile(): ?string; - public function run(): void; } diff --git a/src/Decorators/TestCallDecorator.php b/src/Decorators/TestCallDecorator.php index 2e15756..ba7e8d4 100644 --- a/src/Decorators/TestCallDecorator.php +++ b/src/Decorators/TestCallDecorator.php @@ -4,19 +4,22 @@ namespace Pest\Mutate\Decorators; +use Pest\Mutate\Contracts\Configuration; use Pest\Mutate\Contracts\MutationTestRunner; -use Pest\Mutate\Factories\ProfileFactory; -use Pest\Mutate\Profile; +use Pest\Mutate\Repositories\ConfigurationRepository; +use Pest\Mutate\Support\Configuration\TestConfiguration; use Pest\Mutate\Tester\MutationTestRunnerFake; use Pest\PendingCalls\TestCall; use Pest\Plugins\Only; use Pest\Support\Container; // @codeCoverageIgnoreStart -class TestCallDecorator implements \Pest\Mutate\Contracts\ProfileFactory +class TestCallDecorator implements Configuration { private MutationTestRunner $testRunner; + private TestConfiguration $configuration; + public function __construct(private readonly TestCall $testCall) { } @@ -31,16 +34,24 @@ public function __call(string $name, array $arguments): TestCall public function mutate(string $profile = 'default'): self { - if (! str_starts_with($profile, Profile::FAKE)) { + if (! str_starts_with($profile, ConfigurationRepository::FAKE)) { Only::enable($this->testCall); $this->testRunner = Container::getInstance() // @phpstan-ignore-line ->get(MutationTestRunner::class); + + Container::getInstance()->get(ConfigurationRepository::class)->setProfile($profile); // @phpstan-ignore-line + + $this->configuration = Container::getInstance()->get(ConfigurationRepository::class) // @phpstan-ignore-line + ->testConfiguration; } else { $this->testRunner = new MutationTestRunnerFake(); + + $this->configuration = Container::getInstance()->get(ConfigurationRepository::class) // @phpstan-ignore-line + ->fakeTestConfiguration($profile); } - $this->testRunner->enable($profile); // @phpstan-ignore-line + $this->testRunner->enable(); // @phpstan-ignore-line $this->coveredOnly(); @@ -49,14 +60,14 @@ public function mutate(string $profile = 'default'): self public function coveredOnly(bool $coveredOnly = true): self { - $this->_profileFactory()->coveredOnly($coveredOnly); + $this->configuration->coveredOnly($coveredOnly); return $this; } public function min(float $minMSI): self { - $this->_profileFactory()->min($minMSI); + $this->configuration->min($minMSI); return $this; } @@ -66,7 +77,7 @@ public function min(float $minMSI): self */ public function path(array|string ...$paths): self { - $this->_profileFactory()->path(...$paths); + $this->configuration->path(...$paths); return $this; } @@ -76,14 +87,14 @@ public function path(array|string ...$paths): self */ public function mutator(string|array ...$mutators): self { - $this->_profileFactory()->mutator(...$mutators); + $this->configuration->mutator(...$mutators); return $this; } public function parallel(bool $parallel = true): self { - $this->_profileFactory()->parallel($parallel); + $this->configuration->parallel($parallel); return $this; } @@ -93,14 +104,9 @@ public function parallel(bool $parallel = true): self */ public function class(string|array ...$classes): self { - $this->_profileFactory()->class(...$classes); + $this->configuration->class(...$classes); return $this; } - - private function _profileFactory(): ProfileFactory - { - return $this->testRunner->getProfileFactory(); - } } // @codeCoverageIgnoreEnd diff --git a/src/Functions.php b/src/Functions.php index 1f6b0ca..6057452 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -2,7 +2,9 @@ declare(strict_types=1); -use Pest\Mutate\Factories\ProfileFactory; +use Pest\Mutate\Repositories\ConfigurationRepository; +use Pest\Mutate\Support\Configuration\GlobalConfiguration; +use Pest\Support\Container; // @codeCoverageIgnoreStart if (! function_exists('mutate')) { @@ -11,8 +13,8 @@ /** * Returns a factory to configure the mutation testing profile. */ - function mutate(string $profile = 'default'): ProfileFactory + function mutate(string $profile = 'default'): GlobalConfiguration { - return new ProfileFactory($profile); + return Container::getInstance()->get(ConfigurationRepository::class)->globalConfiguration($profile); // @phpstan-ignore-line } } diff --git a/src/MutationTest.php b/src/MutationTest.php index 289c3ad..13f6944 100644 --- a/src/MutationTest.php +++ b/src/MutationTest.php @@ -6,6 +6,7 @@ use Pest\Mutate\Event\Facade; use Pest\Mutate\Plugins\Mutate; +use Pest\Mutate\Support\Configuration\Configuration; use Pest\Mutate\Support\MutationTestResult; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; @@ -32,7 +33,7 @@ public function updateResult(MutationTestResult $result): void * @param array>> $coveredLines * @param array $originalArguments */ - public function run(array $coveredLines, Profile $profile, array $originalArguments): void + public function run(array $coveredLines, Configuration $configuration, array $originalArguments): void { /** @var string $tmpfname */ $tmpfname = tempnam('/tmp', 'pest_mutation_'); @@ -62,7 +63,7 @@ public function run(array $coveredLines, Profile $profile, array $originalArgume ...$originalArguments, '--bail', '--filter="'.implode('|', $filters).'"', - $profile->parallel ? '--parallel' : '', + $configuration->parallel ? '--parallel' : '', ], env: [ Mutate::ENV_MUTATION_TESTING => $this->mutation->file->getRealPath(), diff --git a/src/Options/CoveredOnlyOption.php b/src/Options/CoveredOnlyOption.php index 134c3f9..7be1ff4 100644 --- a/src/Options/CoveredOnlyOption.php +++ b/src/Options/CoveredOnlyOption.php @@ -23,6 +23,6 @@ public static function match(string $argument): bool public static function inputOption(): InputOption { - return new InputOption('--covered-only', null, InputOption::VALUE_OPTIONAL, ''); + return new InputOption(sprintf('--%s', self::ARGUMENT), null, InputOption::VALUE_OPTIONAL, ''); } } diff --git a/src/Options/MutateOption.php b/src/Options/MutateOption.php index 82ddee0..4ae4855 100644 --- a/src/Options/MutateOption.php +++ b/src/Options/MutateOption.php @@ -23,6 +23,6 @@ public static function match(string $argument): bool public static function inputOption(): InputOption { - return new InputOption('--mutate', null, InputOption::VALUE_OPTIONAL, ''); + return new InputOption(sprintf('--%s', self::ARGUMENT), null, InputOption::VALUE_OPTIONAL, ''); } } diff --git a/src/Plugins/Mutate.php b/src/Plugins/Mutate.php index eb9f19f..1e6bfa4 100644 --- a/src/Plugins/Mutate.php +++ b/src/Plugins/Mutate.php @@ -11,25 +11,10 @@ use Pest\Mutate\Boostrappers\BootPhpUnitSubscribers; use Pest\Mutate\Boostrappers\BootSubscribers; use Pest\Mutate\Contracts\MutationTestRunner; -use Pest\Mutate\Contracts\Printer; -use Pest\Mutate\Factories\ProfileFactory; -use Pest\Mutate\Options\ClassOption; -use Pest\Mutate\Options\CoveredOnlyOption; -use Pest\Mutate\Options\MinMsiOption; -use Pest\Mutate\Options\MutateOption; -use Pest\Mutate\Options\MutatorsOption; -use Pest\Mutate\Options\ParallelOption; -use Pest\Mutate\Options\PathsOption; -use Pest\Mutate\Profile; -use Pest\Mutate\Support\Printers\CompactPrinter; -use Pest\Mutate\Support\Printers\DefaultPrinter; +use Pest\Mutate\Repositories\ConfigurationRepository; use Pest\Plugins\Concerns\HandleArguments; -use Pest\Plugins\Parallel; use Pest\Support\Container; use Pest\Support\Coverage; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Output\OutputInterface; /** * @internal @@ -44,16 +29,6 @@ class Mutate implements Bootable, HandlesArguments final public const ENV_MUTATION_FILE = 'PEST_MUTATION_FILE'; - private const OPTIONS = [ - MutateOption::class, - PathsOption::class, - MutatorsOption::class, - MinMsiOption::class, - CoveredOnlyOption::class, - ParallelOption::class, - ClassOption::class, - ]; - /** * The Kernel bootstrappers. * @@ -68,8 +43,7 @@ class Mutate implements Bootable, HandlesArguments * Creates a new Plugin instance. */ public function __construct( - private readonly Container $container, - private readonly OutputInterface $output + private readonly Container $container ) { // } @@ -99,26 +73,17 @@ public function handleArguments(array $arguments): array /** @var \Pest\Mutate\Tester\MutationTestRunner $mutationTestRunner */ $mutationTestRunner = Container::getInstance()->get(MutationTestRunner::class); - $filteredArguments = ['vendor/bin/pest']; - $inputOptions = []; - foreach ($arguments as $key => $argument) { - foreach (self::OPTIONS as $option) { - if ($option::match($argument)) { - $filteredArguments[] = $argument; - $inputOptions[] = $option::inputOption(); - - if ($option::remove()) { - unset($arguments[$key]); - } - } - } - } + if (array_filter($arguments, fn (string $argument): bool => str_starts_with($argument, '--mutate=') || $argument === '--mutate') === []) { + $mutationTestRunner->setOriginalArguments($arguments); - $originalArguments = $arguments; + return $arguments; + } - $inputDefinition = new InputDefinition($inputOptions); + $arguments = Container::getInstance()->get(ConfigurationRepository::class) // @phpstan-ignore-line + ->cliConfiguration->fromArguments($arguments); - $input = new ArgvInput($filteredArguments, $inputDefinition); + $mutationTestRunner->enable(); + $mutationTestRunner->setOriginalArguments($arguments); // always enable php coverage report, but it will be disabled if not required if (Coverage::isAvailable()) { @@ -129,46 +94,6 @@ public function handleArguments(array $arguments): array $arguments[] = '--coverage-php='.Coverage::getPath(); } - if (! $input->hasOption(MutateOption::ARGUMENT)) { - $mutationTestRunner->setOriginalArguments($originalArguments); - - return $arguments; - } - - $profileName = $input->getOption(MutateOption::ARGUMENT) ?? 'default'; - $profileFactory = new ProfileFactory($profileName); // @phpstan-ignore-line - - if (! str_starts_with((string) $profileName, Profile::FAKE)) { // @phpstan-ignore-line - $mutationTestRunner->enable($profileName); // @phpstan-ignore-line - $mutationTestRunner->setOriginalArguments($originalArguments); - } - - if ($input->hasOption(PathsOption::ARGUMENT)) { - $profileFactory->path(explode(',', (string) $input->getOption(PathsOption::ARGUMENT))); // @phpstan-ignore-line - } - - if ($input->hasOption(MutatorsOption::ARGUMENT)) { - $profileFactory->mutator(explode(',', (string) $input->getOption(MutatorsOption::ARGUMENT))); // @phpstan-ignore-line - } - - if ($input->hasOption(MinMsiOption::ARGUMENT)) { - $profileFactory->min((float) $input->getOption(MinMsiOption::ARGUMENT)); // @phpstan-ignore-line - } - - if ($input->hasOption(CoveredOnlyOption::ARGUMENT)) { - $profileFactory->coveredOnly($input->getOption(CoveredOnlyOption::ARGUMENT) !== 'false'); - } - - if ($input->hasOption(ParallelOption::ARGUMENT)) { - unset($arguments[array_search('--'.ParallelOption::ARGUMENT, $arguments, true)]); - $profileFactory->parallel(); - Parallel::disable(); - } - - if ($input->hasOption(ClassOption::ARGUMENT)) { - $profileFactory->class(explode(',', (string) $input->getOption(ClassOption::ARGUMENT))); // @phpstan-ignore-line - } - return $arguments; } } diff --git a/src/Profile.php b/src/Profile.php deleted file mode 100644 index 3496300..0000000 --- a/src/Profile.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - public array $paths = ['src']; - - /** - * @var array> - */ - public array $mutators; - - public float $minMSI = 0; - - public bool $coveredOnly = false; - - public bool $parallel = false; - - /** - * @var array - */ - public array $classes = []; - - public function __construct() - { - $this->mutators = DefaultSet::mutators(); - } -} diff --git a/src/Profiles.php b/src/Profiles.php deleted file mode 100644 index dcb164f..0000000 --- a/src/Profiles.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - private array $profiles = []; - - public static function get(string $name): Profile - { - return Container::getInstance()->get(Profiles::class) // @phpstan-ignore-line - ->getProfile($name); - } - - public function getProfile(string $name): Profile - { - if (! array_key_exists($name, $this->profiles)) { - $this->profiles[$name] = new Profile(); - } - - return $this->profiles[$name]; - } - - /** - * This is only for testing purposes. - * - * @internal - */ - public function resetFakeProfile(): void - { - unset($this->profiles[Profile::FAKE]); - } -} diff --git a/src/Repositories/ConfigurationRepository.php b/src/Repositories/ConfigurationRepository.php new file mode 100644 index 0000000..08fef14 --- /dev/null +++ b/src/Repositories/ConfigurationRepository.php @@ -0,0 +1,86 @@ + + */ + private array $globalConfigurations = []; + + /** + * @var array + */ + private array $fakeTestConfigurations = []; + + private string $profile = 'default'; + + public function __construct() + { + $this->cliConfiguration = new CliConfiguration(); + $this->testConfiguration = new TestConfiguration(); + } + + public function setProfile(string $profile): void + { + $this->profile = $profile; + } + + public function globalConfiguration(string $name = null): GlobalConfiguration + { + $name ??= $this->profile; + + if (! isset($this->globalConfigurations[$name])) { + $this->globalConfigurations[$name] = new GlobalConfiguration(); + } + + return $this->globalConfigurations[$name]; + } + + public function fakeTestConfiguration(string $name = null): TestConfiguration + { + $name ??= $this->profile; + + if (! isset($this->fakeTestConfigurations[$name])) { + $this->fakeTestConfigurations[$name] = new TestConfiguration(); + } + + return $this->fakeTestConfigurations[$name]; + } + + public function mergedConfiguration(): Configuration + { + $config = [ + ...$this->globalConfiguration()->toArray(), + ...$this->testConfiguration->toArray(), + ...$this->cliConfiguration->toArray(), + ]; + + return new Configuration( + coveredOnly: $config['covered_only'] ?? false, + paths: $config['paths'] ?? ['src'], + mutators: $config['mutators'] ?? DefaultSet::mutators(), + classes: $config['classes'] ?? [], + parallel: $config['parallel'] ?? false, + minMSI: $config['min_msi'] ?? 0.0, + ); + } +} diff --git a/src/Subscribers/DisplayInitialTestRunMessage.php b/src/Subscribers/DisplayInitialTestRunMessage.php index 6babaa9..e6e2d98 100644 --- a/src/Subscribers/DisplayInitialTestRunMessage.php +++ b/src/Subscribers/DisplayInitialTestRunMessage.php @@ -31,6 +31,7 @@ public function notify(Loaded $event): void } renderUsing(Container::getInstance()->get(OutputInterface::class)); // @phpstan-ignore-line - render('
Running initial test suite:'.($mutationTestRunner->getEnabledProfile() !== 'default' ? (' (Profile: '.$mutationTestRunner->getEnabledProfile().')') : '').'
'); + render('
Running initial test suite:
'); + // render('
Running initial test suite:'.($mutationTestRunner->getEnabledProfile() !== 'default' ? (' (Profile: '.$mutationTestRunner->getEnabledProfile().')') : '').'
'); } } diff --git a/src/Subscribers/EnsurePrinterIsRegistered.php b/src/Subscribers/EnsurePrinterIsRegistered.php index 7158d8a..2c554a3 100644 --- a/src/Subscribers/EnsurePrinterIsRegistered.php +++ b/src/Subscribers/EnsurePrinterIsRegistered.php @@ -47,9 +47,9 @@ public function notify(Started $event): void } if ($_SERVER['COLLISION_PRINTER_COMPACT'] ?? false) { - $printer = new CompactPrinter(Container::getInstance()->get(OutputInterface::class)); + $printer = new CompactPrinter(Container::getInstance()->get(OutputInterface::class)); // @phpstan-ignore-line } else { - $printer = new DefaultPrinter(Container::getInstance()->get(OutputInterface::class)); + $printer = new DefaultPrinter(Container::getInstance()->get(OutputInterface::class)); // @phpstan-ignore-line } Container::getInstance()->add(Printer::class, $printer); diff --git a/src/Factories/ProfileFactory.php b/src/Support/Configuration/AbstractConfiguration.php similarity index 52% rename from src/Factories/ProfileFactory.php rename to src/Support/Configuration/AbstractConfiguration.php index ebee6df..427f1c4 100644 --- a/src/Factories/ProfileFactory.php +++ b/src/Support/Configuration/AbstractConfiguration.php @@ -2,30 +2,42 @@ declare(strict_types=1); -namespace Pest\Mutate\Factories; +namespace Pest\Mutate\Support\Configuration; +use Pest\Mutate\Contracts\Configuration as ConfigurationContract; use Pest\Mutate\Contracts\Mutator; use Pest\Mutate\Contracts\MutatorSet; -use Pest\Mutate\Contracts\ProfileFactory as ProfileFactoryContract; use Pest\Mutate\Exceptions\InvalidMutatorException; -use Pest\Mutate\Profile; -use Pest\Mutate\Profiles; -class ProfileFactory implements ProfileFactoryContract +abstract class AbstractConfiguration implements ConfigurationContract { - private readonly Profile $profile; + /** + * @var string[]|null + */ + private ?array $paths = null; - public function __construct(string $name) - { - $this->profile = Profiles::get($name); - } + /** + * @var class-string[]|null + */ + private ?array $mutators = null; + + /** + * @var string[]|null + */ + private ?array $classes = null; + + private ?float $minMSI = null; + + private ?bool $coveredOnly = null; + + private ?bool $parallel = null; /** * {@inheritDoc} */ public function path(array|string ...$paths): self { - $this->profile->paths = array_merge(...array_map(fn (string|array $path): array => is_string($path) ? [$path] : $path, $paths)); + $this->paths = array_merge(...array_map(fn (string|array $path): array => is_string($path) ? [$path] : $path, $paths)); return $this; } @@ -59,28 +71,28 @@ function (string $mutator): string { } } - $this->profile->mutators = $mutators; // @phpstan-ignore-line + $this->mutators = $mutators; // @phpstan-ignore-line return $this; } public function min(float $minMSI): self { - $this->profile->minMSI = $minMSI; + $this->minMSI = $minMSI; return $this; } public function coveredOnly(bool $coveredOnly = true): self { - $this->profile->coveredOnly = $coveredOnly; + $this->coveredOnly = $coveredOnly; return $this; } public function parallel(bool $parallel = true): self { - $this->profile->parallel = $parallel; + $this->parallel = $parallel; return $this; } @@ -88,10 +100,25 @@ public function parallel(bool $parallel = true): self /** * {@inheritDoc} */ - public function class(string|array ...$classes): ProfileFactoryContract + public function class(string|array ...$classes): self { - $this->profile->classes = array_merge(...array_map(fn (string|array $class): array => is_string($class) ? [$class] : $class, $classes)); + $this->classes = array_merge(...array_map(fn (string|array $class): array => is_string($class) ? [$class] : $class, $classes)); return $this; } + + /** + * @return array{paths?: string[], mutators?: class-string[], classes?: string[], parallel?: bool, min_msi?: float, covered_only?: bool} + */ + public function toArray(): array + { + return array_filter([ + 'paths' => $this->paths, + 'mutators' => $this->mutators, + 'classes' => $this->classes, + 'parallel' => $this->parallel, + 'min_msi' => $this->minMSI, + 'covered_only' => $this->coveredOnly, + ], fn (mixed $value): bool => ! is_null($value)); + } } diff --git a/src/Support/Configuration/CliConfiguration.php b/src/Support/Configuration/CliConfiguration.php new file mode 100644 index 0000000..31f7b72 --- /dev/null +++ b/src/Support/Configuration/CliConfiguration.php @@ -0,0 +1,85 @@ + $arguments + * @return array + */ + public function fromArguments(array $arguments): array + { + $filteredArguments = ['vendor/bin/pest']; + $inputOptions = []; + foreach ($arguments as $key => $argument) { + foreach (self::OPTIONS as $option) { + if ($option::match($argument)) { + $filteredArguments[] = $argument; + $inputOptions[] = $option::inputOption(); + + if ($option::remove()) { + unset($arguments[$key]); + } + } + } + } + + $input = new ArgvInput($filteredArguments, new InputDefinition($inputOptions)); + + if ($input->hasOption(CoveredOnlyOption::ARGUMENT)) { + $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(MutatorsOption::ARGUMENT)) { + $this->mutator(explode(',', (string) $input->getOption(MutatorsOption::ARGUMENT))); // @phpstan-ignore-line + } + + if ($input->hasOption(MinMsiOption::ARGUMENT)) { + $this->min((float) $input->getOption(MinMsiOption::ARGUMENT)); // @phpstan-ignore-line + } + + if ($input->hasOption(CoveredOnlyOption::ARGUMENT)) { + $this->coveredOnly($input->getOption(CoveredOnlyOption::ARGUMENT) !== 'false'); + } + + if ($input->hasOption(ParallelOption::ARGUMENT)) { + unset($arguments[array_search('--'.ParallelOption::ARGUMENT, $arguments, true)]); + $this->parallel(); + Parallel::disable(); + } + + if ($input->hasOption(ClassOption::ARGUMENT)) { + $this->class(explode(',', (string) $input->getOption(ClassOption::ARGUMENT))); // @phpstan-ignore-line + } + + return $arguments; + } +} diff --git a/src/Support/Configuration/Configuration.php b/src/Support/Configuration/Configuration.php new file mode 100644 index 0000000..fa50019 --- /dev/null +++ b/src/Support/Configuration/Configuration.php @@ -0,0 +1,25 @@ + $paths + * @param array> $mutators + * @param array $classes + */ + public function __construct( + public readonly bool $coveredOnly, + public readonly array $paths, + public readonly array $mutators, + public readonly array $classes, + public readonly bool $parallel, + public readonly float $minMSI, + ) { + } +} diff --git a/src/Support/Configuration/GlobalConfiguration.php b/src/Support/Configuration/GlobalConfiguration.php new file mode 100644 index 0000000..3466981 --- /dev/null +++ b/src/Support/Configuration/GlobalConfiguration.php @@ -0,0 +1,9 @@ +originalArguments = $arguments; } - public function enable(string $profile): void + public function enable(): void { if (getenv(Mutate::ENV_MUTATION_TESTING) !== false) { return; } - $this->enabledProfile = $profile; + $this->enabled = true; } public function isEnabled(): bool { - if (str_starts_with((string) $this->enabledProfile, Profile::FAKE)) { + if (str_starts_with((string) $this->enabled, ConfigurationRepository::FAKE)) { return false; } - return $this->enabledProfile !== null; + return $this->enabled; } public function doNotDisableCodeCoverage(): void @@ -96,20 +94,20 @@ 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->getProfile()->paths); + $files = $this->getFiles($this->getConfiguration()->paths); /** @var MutationGenerator $generator */ $generator = Container::getInstance()->get(MutationGenerator::class); foreach ($files as $file) { - if ($this->getProfile()->coveredOnly && ! isset($coveredLines[$file->getRealPath()])) { + if ($this->getConfiguration()->coveredOnly && ! isset($coveredLines[$file->getRealPath()])) { continue; } foreach ($generator->generate( file: $file, - mutators: $this->getProfile()->mutators, - linesToMutate: $this->getProfile()->coveredOnly ? array_keys($coveredLines[$file->getRealPath()]) : [], - classesToMutate: $this->getProfile()->classes, + mutators: $this->getConfiguration()->mutators, + linesToMutate: $this->getConfiguration()->coveredOnly ? array_keys($coveredLines[$file->getRealPath()]) : [], + classesToMutate: $this->getConfiguration()->classes, ) as $mutation) { $mutationSuite->repository->add($mutation); } @@ -124,7 +122,7 @@ classesToMutate: $this->getProfile()->classes, Facade::instance()->emitter()->startTestCollection($testCollection); foreach ($testCollection->tests() as $test) { - $test->run($coveredLines, $this->getProfile(), $this->originalArguments); + $test->run($coveredLines, $this->getConfiguration(), $this->originalArguments); } } @@ -160,22 +158,8 @@ private function getFiles(array $paths): Finder ->files(); } - private function getProfile(): Profile + private function getConfiguration(): Configuration { - if ($this->enabledProfile === null) { - throw ShouldNotHappen::fromMessage('No profile enabled'); - } - - return Profiles::get($this->enabledProfile); - } - - public function getProfileFactory(): ProfileFactory - { - return new ProfileFactory($this->enabledProfile ?? 'default'); - } - - public function getEnabledProfile(): ?string - { - return $this->enabledProfile; + return Container::getInstance()->get(ConfigurationRepository::class)->mergedConfiguration(); // @phpstan-ignore-line } } diff --git a/src/Tester/MutationTestRunnerFake.php b/src/Tester/MutationTestRunnerFake.php index 082ad27..f3e6fc5 100644 --- a/src/Tester/MutationTestRunnerFake.php +++ b/src/Tester/MutationTestRunnerFake.php @@ -4,22 +4,17 @@ namespace Pest\Mutate\Tester; -use Pest\Exceptions\ShouldNotHappen; use Pest\Mutate\Contracts\MutationTestRunner as MutationTestRunnerContract; -use Pest\Mutate\Factories\ProfileFactory; class MutationTestRunnerFake implements MutationTestRunnerContract { - private ?string $enabledProfile = null; - public function run(): void { // TODO: Implement run() method. } - public function enable(string $profile): void + public function enable(): void { - $this->enabledProfile = $profile; } public function isEnabled(): bool @@ -36,18 +31,4 @@ public function setOriginalArguments(array $arguments): void { // TODO: Implement setOriginalArguments() method. } - - public function getProfileFactory(): ProfileFactory - { - if ($this->enabledProfile === null) { - throw ShouldNotHappen::fromMessage('No profile enabled'); - } - - return new ProfileFactory($this->enabledProfile); - } - - public function getEnabledProfile(): ?string - { - return $this->enabledProfile; - } } diff --git a/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php b/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php index a628ffc..1a78aec 100644 --- a/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php +++ b/tests/Features/Profiles/HandleTestCallProfileConfigurationTest.php @@ -3,59 +3,63 @@ declare(strict_types=1); use Pest\Mutate\Mutators\Arithmetic\PlusToMinus; use Pest\Mutate\Mutators\Equality\GreaterToGreaterOrEqual; -use Pest\Mutate\Profile; -use Pest\Mutate\Profiles; +use Pest\Mutate\Repositories\ConfigurationRepository; +use Pest\Support\Container; use Tests\Fixtures\Classes\AgeHelper; +beforeEach(function (): void { + $this->repository = Container::getInstance()->get(ConfigurationRepository::class); +}); + it('forwards calls to the original test call', function (): never { throw new Exception('test exception'); -})->mutate(Profile::FAKE) +})->mutate(ConfigurationRepository::FAKE) ->throws('test exception'); it('sets the min MSI from test', function (): void { - $profile = Profiles::get(Profile::FAKE.'_1'); + $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_1'); - expect($profile->minMSI) + expect($configuration->toArray()['min_msi']) ->toEqual(2.0); -})->mutate(Profile::FAKE.'_1') +})->mutate(ConfigurationRepository::FAKE.'_1') ->min(2); it('sets the covered only from test', function (): void { - $profile = Profiles::get(Profile::FAKE.'_2'); + $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_2'); - expect($profile->coveredOnly) + expect($configuration->toArray()['covered_only']) ->toBeTrue(); -})->mutate(Profile::FAKE.'_2') +})->mutate(ConfigurationRepository::FAKE.'_2') ->coveredOnly(true); it('sets the paths from test', function (): void { - $profile = Profiles::get(Profile::FAKE.'_3'); + $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_3'); - expect($profile->paths) + expect($configuration->toArray()['paths']) ->toBe(['src/folder-1', 'src/folder-2']); -})->mutate(Profile::FAKE.'_3') +})->mutate(ConfigurationRepository::FAKE.'_3') ->path('src/folder-1', 'src/folder-2'); it('sets the mutators from test', function (): void { - $profile = Profiles::get(Profile::FAKE.'_4'); + $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_4'); - expect($profile->mutators) + expect($configuration->toArray()['mutators']) ->toBe([PlusToMinus::class, GreaterToGreaterOrEqual::class]); -})->mutate(Profile::FAKE.'_4') +})->mutate(ConfigurationRepository::FAKE.'_4') ->mutator(PlusToMinus::class, GreaterToGreaterOrEqual::class); it('sets the parallel option from test', function (): void { - $profile = Profiles::get(Profile::FAKE.'_5'); + $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_5'); - expect($profile->parallel) + expect($configuration->toArray()['parallel']) ->toBeTrue(); -})->mutate(Profile::FAKE.'_5') +})->mutate(ConfigurationRepository::FAKE.'_5') ->parallel(true); it('sets the class option from test', function (): void { - $profile = Profiles::get(Profile::FAKE.'_6'); + $configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_6'); - expect($profile->classes) + expect($configuration->toArray()['classes']) ->toBe([AgeHelper::class]); -})->mutate(Profile::FAKE.'_6') +})->mutate(ConfigurationRepository::FAKE.'_6') ->class(AgeHelper::class); diff --git a/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php b/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php index e829c42..9a14f8c 100644 --- a/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php +++ b/tests/Features/Profiles/HandlesCliProfileConfigurationTest.php @@ -4,103 +4,88 @@ use Pest\Mutate\Mutators\Arithmetic\MinusToPlus; use Pest\Mutate\Mutators\Arithmetic\PlusToMinus; use Pest\Mutate\Mutators\Sets\ArithmeticSet; -use Pest\Mutate\Mutators\Sets\DefaultSet; -use Pest\Mutate\Plugins\Mutate; -use Pest\Mutate\Profile; -use Pest\Mutate\Profiles; -use Pest\Support\Container; +use Pest\Mutate\Repositories\ConfigurationRepository; +use Pest\Mutate\Support\Configuration\CliConfiguration; use Tests\Fixtures\Classes\AgeHelper; use Tests\Fixtures\Classes\SizeHelper; beforeEach(function (): void { - $this->plugin = Container::getInstance()->get(Mutate::class); - - $this->profile = Profiles::get(Profile::FAKE); -}); - -it('overrides global values', function (): void { - mutate(Profile::FAKE) - ->min(10.0); - - $this->plugin->handleArguments(['--mutate=fake-profile', '--min=2']); - - expect($this->profile->minMSI)->toEqual(2.0); + $this->configuration = new CliConfiguration(); }); it('sets the paths if --paths argument is passed', function (): void { - expect($this->profile->paths)->toEqual(['src']); + $this->configuration->fromArguments(['--path=app']); - $this->plugin->handleArguments(['--mutate=fake-profile', '--path=app']); - expect($this->profile->paths)->toEqual(['app']); + expect($this->configuration->toArray()) + ->paths->toEqual(['app']); - $this->plugin->handleArguments(['--mutate=fake-profile', '--path=src/path-1,src/path-2']); - expect($this->profile->paths)->toEqual(['src/path-1', 'src/path-2']); + $this->configuration->fromArguments(['--path=src/path-1,src/path-2']); + expect($this->configuration->toArray()) + ->paths->toEqual(['src/path-1', 'src/path-2']); }); it('sets the mutators if --mutators argument is passed', function (): void { - expect($this->profile->mutators)->toEqual(DefaultSet::mutators()); - - $this->plugin->handleArguments(['--mutate=fake-profile', '--mutator=SetArithmetic']); - expect($this->profile->mutators)->toEqual(ArithmeticSet::mutators()); + $this->configuration->fromArguments(['--mutator=SetArithmetic']); + expect($this->configuration->toArray()) + ->mutators->toEqual(ArithmeticSet::mutators()); - $this->plugin->handleArguments(['--mutate=fake-profile', '--mutator=ArithmeticPlusToMinus']); - expect($this->profile->mutators)->toEqual([PlusToMinus::class]); + $this->configuration->fromArguments(['--mutator=ArithmeticPlusToMinus']); + expect($this->configuration->toArray()) + ->mutators->toEqual([PlusToMinus::class]); - $this->plugin->handleArguments(['--mutate=fake-profile', '--mutator=ArithmeticPlusToMinus,ArithmeticMinusToPlus']); - expect($this->profile->mutators)->toEqual([PlusToMinus::class, MinusToPlus::class]); + $this->configuration->fromArguments(['--mutator=ArithmeticPlusToMinus,ArithmeticMinusToPlus']); + expect($this->configuration->toArray()) + ->mutators->toEqual([PlusToMinus::class, MinusToPlus::class]); }); it('sets MSI threshold if --min argument is passed', function (): void { - expect($this->profile->minMSI)->toEqual(0.0); + $this->configuration->fromArguments(['--mutate='.ConfigurationRepository::FAKE]); + expect($this->configuration->toArray()) + ->min_msi->toEqual(0.0); - $this->plugin->handleArguments(['--mutate=fake-profile']); - expect($this->profile->minMSI)->toEqual(0.0); + $this->configuration->fromArguments(['--min=2']); + expect($this->configuration->toArray()) + ->min_msi->toEqual(2.0); - $this->plugin->handleArguments(['--mutate=fake-profile', '--min=2']); - expect($this->profile->minMSI)->toEqual(2.0); - - $this->plugin->handleArguments(['--mutate=fake-profile', '--min=2.4']); - expect($this->profile->minMSI)->toEqual(2.4); + $this->configuration->fromArguments(['--min=2.4']); + expect($this->configuration->toArray()) + ->min_msi->toEqual(2.4); }); it('enables covered only option if --covered-only argument is passed', function (): void { - expect($this->profile->coveredOnly)->toBeFalse(); - - $this->plugin->handleArguments(['--mutate=fake-profile']); - expect($this->profile->coveredOnly)->toBeFalse(); + $this->configuration->fromArguments(['--mutate='.ConfigurationRepository::FAKE]); + expect($this->configuration->toArray()) + ->covered_only->toBeNull(); - $this->plugin->handleArguments(['--mutate=fake-profile', '--covered-only']); - expect($this->profile->coveredOnly)->toBeTrue(); + $this->configuration->fromArguments(['--covered-only']); + expect($this->configuration->toArray()) + ->covered_only->toBeTrue(); - $this->plugin->handleArguments(['--mutate=fake-profile', '--covered-only=true']); - expect($this->profile->coveredOnly)->toBeTrue(); + $this->configuration->fromArguments(['--covered-only=true']); + expect($this->configuration->toArray()) + ->covered_only->toBeTrue(); - $this->plugin->handleArguments(['--mutate=fake-profile', '--covered-only=false']); - expect($this->profile->coveredOnly)->toBeFalse(); + $this->configuration->fromArguments(['--covered-only=false']); + expect($this->configuration->toArray()) + ->covered_only->toBeFalse(); }); it('enables parallel option if --parallel argument is passed', function (): void { - expect($this->profile->parallel)->toBeFalse(); - - $this->plugin->handleArguments(['--mutate=fake-profile']); - expect($this->profile->parallel)->toBeFalse(); + $this->configuration->fromArguments(['--mutate='.ConfigurationRepository::FAKE]); + expect($this->configuration->toArray()) + ->parallel->toBeNull(); - $this->plugin->handleArguments(['--mutate=fake-profile', '--parallel']); - expect($this->profile->parallel)->toBeTrue(); - - $this->plugin->handleArguments(['--mutate=fake-profile', '--parallel=true']); - expect($this->profile->parallel)->toBeTrue(); - - $this->plugin->handleArguments(['--mutate=fake-profile', '--parallel=false']); - expect($this->profile->coveredOnly)->toBeFalse(); + $this->configuration->fromArguments(['--parallel']); + expect($this->configuration->toArray()) + ->parallel->toBeTrue(); }); it('sets the class if --class argument is passed', function (): void { - expect($this->profile->classes)->toEqual([]); - - $this->plugin->handleArguments(['--mutate=fake-profile', '--class='.AgeHelper::class]); - expect($this->profile->classes)->toEqual([AgeHelper::class]); + $this->configuration->fromArguments(['--class='.AgeHelper::class]); + expect($this->configuration->toArray()['classes']) + ->toEqual([AgeHelper::class]); - $this->plugin->handleArguments(['--mutate=fake-profile', '--class='.AgeHelper::class.','.SizeHelper::class]); - expect($this->profile->classes)->toEqual([AgeHelper::class, SizeHelper::class]); + $this->configuration->fromArguments(['--class='.AgeHelper::class.','.SizeHelper::class]); + expect($this->configuration->toArray()['classes']) + ->toEqual([AgeHelper::class, SizeHelper::class]); }); diff --git a/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php b/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php index 7ba0b8b..ae22cad 100644 --- a/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php +++ b/tests/Features/Profiles/HandlesGlobalProfileConfigurationTest.php @@ -5,115 +5,96 @@ use Pest\Mutate\Mutators\Arithmetic\MinusToPlus; use Pest\Mutate\Mutators\Arithmetic\PlusToMinus; use Pest\Mutate\Mutators\Sets\ArithmeticSet; -use Pest\Mutate\Mutators\Sets\DefaultSet; -use Pest\Mutate\Profile; -use Pest\Mutate\Profiles; +use Pest\Mutate\Repositories\ConfigurationRepository; +use Pest\Support\Container; use Tests\Fixtures\Classes\AgeHelper; beforeEach(function (): void { - $this->profile = Profiles::get(Profile::FAKE); + $this->configuration = Container::getInstance()->get(ConfigurationRepository::class)->globalConfiguration(ConfigurationRepository::FAKE); }); test('configure profile globally', function (): void { - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->min(20.0); - expect($this->profile->minMSI) + expect($this->configuration->toArray()['min_msi']) ->toEqual(20.0); }); test('globally configure paths', function (): void { - expect($this->profile->paths) - ->toEqual(['src']); - - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->path(['app']); - expect($this->profile->paths) + expect($this->configuration->toArray()['paths']) ->toEqual(['app']); - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->path(['src/path-1', 'src/path-2'], 'src/path-3'); - expect($this->profile->paths) + expect($this->configuration->toArray()['paths']) ->toEqual(['src/path-1', 'src/path-2', 'src/path-3']); }); test('globally configure mutators', function (): void { - expect($this->profile->mutators) - ->toEqual(DefaultSet::mutators()); - - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->mutator(Mutators::SET_ARITHMETIC); - expect($this->profile->mutators) + expect($this->configuration->toArray()['mutators']) ->toEqual(ArithmeticSet::mutators()); - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->mutator(Mutators::ARITHMETIC_PLUS_TO_MINUS); - expect($this->profile->mutators) + expect($this->configuration->toArray()['mutators']) ->toEqual([PlusToMinus::class]); - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->mutator(Mutators::ARITHMETIC_PLUS_TO_MINUS, Mutators::ARITHMETIC_MINUS_TO_PLUS); - expect($this->profile->mutators) + expect($this->configuration->toArray()['mutators']) ->toEqual([PlusToMinus::class, MinusToPlus::class]); }); test('globally configure min MSI threshold', function (): void { - expect($this->profile->minMSI) - ->toEqual(0); - - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->min(10.0); - expect($this->profile->minMSI) + expect($this->configuration->toArray()['min_msi']) ->toEqual(10.0); }); test('globally configure covered only option', function (): void { - expect($this->profile->coveredOnly) - ->toBeFalse(); - - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->coveredOnly(); - expect($this->profile->coveredOnly) + expect($this->configuration->toArray()['covered_only']) ->toBeTrue(); - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->coveredOnly(false); - expect($this->profile->coveredOnly) + expect($this->configuration->toArray()['covered_only']) ->toBeFalse(); }); test('globally configure parallel option', function (): void { - expect($this->profile->parallel) - ->toBeFalse(); - - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->parallel(); - expect($this->profile->parallel) + expect($this->configuration->toArray()['parallel']) ->toBeTrue(); - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->parallel(false); - expect($this->profile->parallel) + expect($this->configuration->toArray()['parallel']) ->toBeFalse(); }); test('globally configure class option', function (): void { - expect($this->profile->classes) - ->toBe([]); - - mutate(Profile::FAKE) + mutate(ConfigurationRepository::FAKE) ->class(AgeHelper::class); - expect($this->profile->classes) + expect($this->configuration->toArray()['classes']) ->toBe([AgeHelper::class]); }); diff --git a/tests/Pest.php b/tests/Pest.php index d677639..174d7fd 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,11 +1,3 @@ beforeEach(function (): void { - Container::getInstance()->get(Profiles::class)->resetFakeProfile(); - })->in(__DIR__); diff --git a/tests/Unit/ProfileFactoryTest.php b/tests/Unit/ProfileFactoryTest.php deleted file mode 100644 index 6f632f6..0000000 --- a/tests/Unit/ProfileFactoryTest.php +++ /dev/null @@ -1,11 +0,0 @@ -mutator('UnknownMutatorClass'); -})->throws(InvalidMutatorException::class, 'UnknownMutatorClass is not a valid mutator');