From 39661681aa442f5c77eb4a66679805ae9dae9725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Fri, 18 Apr 2025 21:56:48 +0200 Subject: [PATCH] feat: Leverage the new unparse method --- src/Input/ChildCommandFactory.php | 14 +- src/Input/InputOptionsSerializer.php | 102 ------ src/Input/RawInput.php | 99 ------ tests/Input/InputOptionsSerializerTest.php | 353 --------------------- tests/Input/RawInputTest.php | 284 ----------------- 5 files changed, 10 insertions(+), 842 deletions(-) delete mode 100644 src/Input/InputOptionsSerializer.php delete mode 100644 src/Input/RawInput.php delete mode 100644 tests/Input/InputOptionsSerializerTest.php delete mode 100644 tests/Input/RawInputTest.php diff --git a/src/Input/ChildCommandFactory.php b/src/Input/ChildCommandFactory.php index 6056c2e..5dd0900 100644 --- a/src/Input/ChildCommandFactory.php +++ b/src/Input/ChildCommandFactory.php @@ -16,8 +16,11 @@ use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Webmozart\Assert\Assert; +use function array_diff; use function array_filter; +use function array_keys; use function array_map; +use function count; use function Safe\getcwd; use function sprintf; @@ -69,11 +72,14 @@ private function getForwardedOptions(InputInterface $input): array // Forward all the options except for "processes" to the children // this way the children can inherit the options such as env // or no-debug. - return InputOptionsSerializer::serialize( - $this->commandDefinition, - $input, + $forwardedOptionNames = array_diff( + array_keys($input->getRawOptions()), ParallelizationInput::OPTIONS, ); + + return count($forwardedOptionNames) === 0 + ? [] + : $input->unparse($forwardedOptionNames); } /** @@ -81,7 +87,7 @@ private function getForwardedOptions(InputInterface $input): array */ private static function getArguments(InputInterface $input): array { - $arguments = RawInput::getRawArguments($input); + $arguments = $input->getRawArguments(); // Remove the item: we do not want it to be passed to child processes // ever. diff --git a/src/Input/InputOptionsSerializer.php b/src/Input/InputOptionsSerializer.php deleted file mode 100644 index 612dd25..0000000 --- a/src/Input/InputOptionsSerializer.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Webmozarts\Console\Parallelization\Input; - -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use function array_diff_key; -use function array_fill_keys; -use function array_keys; -use function array_map; -use function array_merge; -use function is_array; -use function sprintf; - -/** - * @internal - */ -final class InputOptionsSerializer -{ - private function __construct() - { - } - - /** - * @param list $excludedOptionNames - * - * @return list - */ - public static function serialize( - InputDefinition $commandDefinition, - InputInterface $input, - array $excludedOptionNames, - ): array { - $filteredOptions = array_diff_key( - RawInput::getRawOptions($input), - array_fill_keys($excludedOptionNames, ''), - ); - - $serializedOptionsList = []; - - foreach (array_keys($filteredOptions) as $name) { - $serializedOption = self::serializeOption( - $commandDefinition->getOption($name), - $name, - $filteredOptions[$name], - ); - - $serializedOptionsList[] = is_array($serializedOption) ? $serializedOption : [$serializedOption]; - } - - return array_merge(...$serializedOptionsList); - } - - /** - * @param string|bool|int|float|null|array $value - * - * @return string|list - */ - private static function serializeOption( - InputOption $option, - string $name, - array|bool|float|int|string|null $value, - ): string|array { - return match (true) { - $option->isNegatable() => sprintf('--%s%s', $value ? '' : 'no-', $name), - !$option->acceptValue() => sprintf('--%s', $name), - self::isArray($option, $value) => array_map(static fn ($item) => self::serializeOptionWithValue($name, $item), $value), - default => self::serializeOptionWithValue($name, $value), - }; - } - - /** - * @param string|bool|int|float|null|array $value - * - * @phpstan-assert-if-true array $value - */ - private static function isArray( - InputOption $option, - array|bool|float|int|string|null $value, - ): bool { - return $option->isArray(); - } - - private static function serializeOptionWithValue( - string $name, - bool|float|int|string|null $value, - ): string { - return sprintf('--%s=%s', $name, $value); - } -} diff --git a/src/Input/RawInput.php b/src/Input/RawInput.php deleted file mode 100644 index 71fa9f6..0000000 --- a/src/Input/RawInput.php +++ /dev/null @@ -1,99 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Webmozarts\Console\Parallelization\Input; - -use DomainException; -use Symfony\Component\Console\Input\Input; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputInterface; - -/** - * @internal - */ -final class RawInput extends Input -{ - /** - * @codeCoverageIgnore - */ - private function __construct(?InputDefinition $definition) - { - parent::__construct($definition); - } - - /** - * Returns all the given arguments NOT merged with the default values. - * - * @return array> - */ - public static function getRawArguments(InputInterface $input): array - { - return $input instanceof Input - ? $input->arguments - : []; - } - - /** - * Returns all the given options NOT merged with the default values. - * - * @return array> - */ - public static function getRawOptions(InputInterface $input): array - { - return $input instanceof Input - ? $input->options - : []; - } - - /** - * @codeCoverageIgnore - */ - public function getFirstArgument(): ?string - { - throw new DomainException('Not implemented.'); - } - - /** - * @codeCoverageIgnore - * - * @param mixed[]|string $values - */ - public function hasParameterOption(array|string $values, bool $onlyParams = false): bool - { - throw new DomainException('Not implemented.'); - } - - /** - * @codeCoverageIgnore - * - * @param mixed[]|string $values - * @param null|array|bool|mixed[]|int|string $default - */ - public function getParameterOption(array|string $values, array|bool|float|int|string|null $default = false, bool $onlyParams = false): mixed - { - throw new DomainException('Not implemented.'); - } - - /** - * @codeCoverageIgnore - */ - protected function parse(): void - { - throw new DomainException('Not implemented.'); - } - - public function __toString(): string - { - return 'RawInput'; - } -} diff --git a/tests/Input/InputOptionsSerializerTest.php b/tests/Input/InputOptionsSerializerTest.php deleted file mode 100644 index 1cd840f..0000000 --- a/tests/Input/InputOptionsSerializerTest.php +++ /dev/null @@ -1,353 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Webmozarts\Console\Parallelization\Input; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Webmozarts\Console\Parallelization\Fixtures\Command\NoSubProcessCommand; -use Webmozarts\Console\Parallelization\Integration\App\Kernel; -use Webmozarts\Console\Parallelization\PHPUnitProviderUtil; -use Webmozarts\Console\Parallelization\SymfonyVersion; - -/** - * @internal - */ -#[CoversClass(InputOptionsSerializer::class)] -final class InputOptionsSerializerTest extends TestCase -{ - #[DataProvider('optionsProvider')] - public function test_it_can_reverse_the_options_parsing( - InputDefinition $commandDefinition, - InputInterface $input, - array $excludedParameters, - array $expected - ): void { - $input->bind($commandDefinition); - - $actual = InputOptionsSerializer::serialize( - $commandDefinition, - $input, - $excludedParameters, - ); - - self::assertSame($expected, $actual); - } - - public static function optionsProvider(): iterable - { - $isSymfony4 = SymfonyVersion::isSymfony4(); - - $completeInputDefinition = new InputDefinition([ - new InputArgument( - 'arg1', - InputArgument::REQUIRED, - 'Argument without a default value', - ), - new InputArgument( - 'arg2', - InputArgument::OPTIONAL, - 'Argument with a default value', - 'arg2DefaultValue', - ), - new InputOption( - 'opt1', - null, - InputOption::VALUE_REQUIRED, - 'Option with a default value', - 'opt1DefaultValue', - ), - new InputOption( - 'opt2', - null, - InputOption::VALUE_REQUIRED, - 'Option without a default value', - ), - ]); - - yield 'empty input and empty definition' => [ - new InputDefinition(), - new ArrayInput([]), - [], - [], - ]; - - yield 'empty input and definition with default values' => [ - new InputDefinition([ - new InputArgument( - 'arg1', - InputArgument::OPTIONAL, - 'Argument with a default value', - 'arg1DefaultValue', - ), - new InputOption( - 'opt1', - null, - InputOption::VALUE_REQUIRED, - 'Option with a default value', - 'opt1DefaultValue', - ), - ]), - new ArrayInput([]), - [], - [], - ]; - - yield 'input & options' => [ - $completeInputDefinition, - new ArrayInput([ - 'arg2' => 'arg2Value', - '--opt2' => 'opt2Value', - ]), - [], - ['--opt2=opt2Value'], - ]; - - yield 'input & options with default value' => [ - $completeInputDefinition, - new ArrayInput([ - 'arg2' => 'arg2Value', - '--opt1' => 'opt1DefaultValue', - '--opt2' => 'opt2Value', - ]), - [], - [ - '--opt1=opt1DefaultValue', - '--opt2=opt2Value', - ], - ]; - - yield 'input & options with excluded option passed option' => [ - $completeInputDefinition, - new ArrayInput([ - 'arg2' => 'arg2Value', - '--opt2' => 'opt2Value', - ]), - ['opt2'], - [], - ]; - - yield 'input & options with excluded option default option' => [ - $completeInputDefinition, - new ArrayInput([ - 'arg2' => 'arg2Value', - '--opt2' => 'opt2Value', - ]), - ['opt1'], - ['--opt2=opt2Value'], - ]; - - yield 'nominal' => [ - self::createIntegrationInputDefinition(), - new ArrayInput([ - 'command' => 'test:no-subprocess', - 'item' => null, - '--processes' => '1', - '--env' => 'dev', - '--ansi' => null, - ]), - ['child', 'processes'], - [ - '--env=dev', - '--ansi', - ], - ]; - - yield from self::optionSerializationProvider(); - } - - private static function optionSerializationProvider(): iterable - { - $isSymfony4 = SymfonyVersion::isSymfony4(); - - $createSet = static fn ( - InputOption $option, - array $input, - array $expected - ) => [ - new InputDefinition([$option]), - new ArrayInput($input), - [], - $expected, - ]; - - yield 'option without value' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_NONE, - ), - ['--opt' => null], - ['--opt'], - ); - - yield 'option without value by shortcut' => $createSet( - new InputOption( - 'opt', - 'o', - InputOption::VALUE_NONE, - ), - ['-o' => null], - ['--opt'], - ); - - yield 'option with value required' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_REQUIRED, - ), - ['--opt' => 'foo'], - ['--opt=foo'], - ); - - yield 'option with non string value (bool)' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_REQUIRED, - ), - ['--opt' => true], - ['--opt=1'], - ); - - yield 'option with non string value (int)' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_REQUIRED, - ), - ['--opt' => 20], - ['--opt=20'], - ); - - yield 'option with non string value (float)' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_REQUIRED, - ), - ['--opt' => 5.3], - ['--opt=5.3'], - ); - - yield 'option with non string value (array of strings)' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - ), - ['--opt' => ['v1', 'v2', 'v3']], - [ - '--opt=v1', - '--opt=v2', - '--opt=v3', - ], - ); - - if (!$isSymfony4) { - yield 'negatable option (positive)' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_NEGATABLE, - ), - ['--opt' => null], - ['--opt'], - ); - - yield 'negatable option (negative)' => $createSet( - new InputOption( - 'opt', - null, - InputOption::VALUE_NEGATABLE, - ), - ['--no-opt' => null], - ['--no-opt'], - ); - } - - yield from PHPUnitProviderUtil::prefixWithLabel( - '[escape token] ', - self::escapedValuesProvider(), - ); - } - - private static function escapedValuesProvider(): iterable - { - $createSet = static fn ( - string $optionValue, - ?string $expected - ) => [ - new InputDefinition([ - new InputOption( - 'opt', - null, - InputOption::VALUE_REQUIRED, - ), - ]), - new ArrayInput([ - '--opt' => $optionValue, - ]), - [], - [ - '--opt='.$expected, - ], - ]; - - yield $createSet( - 'foo', - 'foo', - ); - - yield $createSet( - '"foo"', - '"foo"', - ); - - yield $createSet( - '"o_id in(\'20\')"', - '"o_id in(\'20\')"', - ); - - yield $createSet( - 'a b c d', - 'a b c d', - ); - - yield $createSet( - "A\nB'C", - "A\nB'C", - ); - } - - private static function createIntegrationInputDefinition(): InputDefinition - { - // We need the framework bundle application because the env and no-debug - // options are defined there. - $frameworkBundleApplication = new Application(new Kernel()); - - $command = new NoSubProcessCommand(); - $command->setApplication($frameworkBundleApplication); - $command->mergeApplicationDefinition(); - - return $command->getDefinition(); - } -} diff --git a/tests/Input/RawInputTest.php b/tests/Input/RawInputTest.php deleted file mode 100644 index 07e29cf..0000000 --- a/tests/Input/RawInputTest.php +++ /dev/null @@ -1,284 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Webmozarts\Console\Parallelization\Input; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Webmozarts\Console\Parallelization\SymfonyVersion; - -/** - * @internal - */ -#[CoversClass(RawInput::class)] -final class RawInputTest extends TestCase -{ - #[DataProvider('inputArgumentProvider')] - public function test_it_can_get_an_input_arguments( - InputInterface $input, - array $expected - ): void { - $actual = RawInput::getRawArguments($input); - - self::assertSame($expected, $actual); - } - - public static function inputArgumentProvider(): iterable - { - $isSymfony4 = SymfonyVersion::isSymfony4(); - - yield 'input with no arguments' => [ - new ArrayInput([], null), - [], - ]; - - yield 'input with arguments default arguments' => [ - new ArrayInput( - [], - new InputDefinition([ - new InputArgument( - 'arg1', - InputArgument::OPTIONAL, - ), - new InputArgument( - 'arg2', - InputArgument::OPTIONAL | InputArgument::IS_ARRAY, - ), - ]), - ), - [], - ]; - - yield 'input with minimum arguments' => [ - new ArrayInput( - [ - 'arg1' => 'value1', - 'arg2' => null, - 'arg3' => null, - ], - new InputDefinition([ - new InputArgument( - 'arg1', - InputArgument::REQUIRED, - ), - new InputArgument( - 'arg2', - InputArgument::OPTIONAL, - ), - new InputArgument( - 'arg3', - InputArgument::OPTIONAL | InputArgument::IS_ARRAY, - ), - ]), - ), - [ - 'arg1' => 'value1', - 'arg2' => null, - 'arg3' => null, - ], - ]; - - yield 'input with all arguments' => [ - new ArrayInput( - [ - 'arg1' => 'value1', - 'arg2' => 'value2', - 'arg3' => 'value3 value4', - ], - new InputDefinition([ - new InputArgument( - 'arg1', - InputArgument::REQUIRED, - ), - new InputArgument( - 'arg2', - InputArgument::OPTIONAL, - ), - new InputArgument( - 'arg3', - InputArgument::OPTIONAL | InputArgument::IS_ARRAY, - ), - ]), - ), - [ - 'arg1' => 'value1', - 'arg2' => 'value2', - 'arg3' => 'value3 value4', - ], - ]; - - yield 'non standard input' => [ - new FakeInput(), - [], - ]; - } - - #[DataProvider('inputOptionProvider')] - public function test_it_can_get_an_input_options( - InputInterface $input, - array $expected - ): void { - $actual = RawInput::getRawOptions($input); - - self::assertSame($expected, $actual); - } - - public static function inputOptionProvider(): iterable - { - $isSymfony4 = SymfonyVersion::isSymfony4(); - - yield 'input with no options' => [ - new ArrayInput([], null), - [], - ]; - - yield 'input with options default options' => [ - new ArrayInput( - [], - new InputDefinition([ - new InputOption( - 'opt1', - null, - InputOption::VALUE_OPTIONAL, - ), - new InputOption( - 'opt2', - null, - InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - ), - new InputOption( - 'opt3', - null, - InputOption::VALUE_REQUIRED, - ), - new InputOption( - 'opt5', - null, - InputOption::VALUE_NONE, - ), - ]), - ), - [], - ]; - - if (!$isSymfony4) { - // TODO: move this within the test up once we drop support for Symfony 4.4 - yield 'input with negatable options default options' => [ - new ArrayInput( - [], - new InputDefinition([ - new InputOption( - 'opt4', - null, - InputOption::VALUE_NEGATABLE, - ), - ]), - ), - [], - ]; - } - - yield 'input with options' => [ - new ArrayInput( - [ - '--opt1' => 'value1', - '--opt3' => 'value3', - '--opt5' => null, - ], - new InputDefinition([ - new InputOption( - 'opt1', - null, - InputOption::VALUE_OPTIONAL, - ), - new InputOption( - 'opt2', - null, - InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - ), - new InputOption( - 'opt3', - null, - InputOption::VALUE_REQUIRED, - ), - new InputOption( - 'opt5', - null, - InputOption::VALUE_NONE, - ), - ]), - ), - [ - 'opt1' => 'value1', - 'opt3' => 'value3', - 'opt5' => true, - ], - ]; - - if (!$isSymfony4) { - // TODO: move this within the test up once we drop support for Symfony 4.4 - yield 'input with negatable options' => [ - new ArrayInput( - [ - '--opt1' => 'value1', - '--opt3' => 'value3', - '--opt5' => null, - ], - new InputDefinition([ - new InputOption( - 'opt1', - null, - InputOption::VALUE_OPTIONAL, - ), - new InputOption( - 'opt2', - null, - InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, - ), - new InputOption( - 'opt3', - null, - InputOption::VALUE_REQUIRED, - ), - new InputOption( - 'opt4', - null, - InputOption::VALUE_NEGATABLE, - ), - new InputOption( - 'opt5', - null, - InputOption::VALUE_NONE, - ), - ]), - ), - [ - 'opt1' => 'value1', - 'opt3' => 'value3', - 'opt5' => true, - ], - ]; - } - - yield 'non standard input' => [ - new FakeInput(), - [], - ]; - } -}