diff --git a/e2e/scenario7/expected.txt b/e2e/scenario7/expected.txt index f6bd9e9..c00cece 100644 --- a/e2e/scenario7/expected.txt +++ b/e2e/scenario7/expected.txt @@ -6,4 +6,5 @@ Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file. [bamarni-bin] Changed current directory to /path/to/project/e2e/scenario7. ––––––––––––––––––––– +[bamarni-bin] Checking namespace vendor-bin/ns1 No dependencies installed. Try running composer install or update. diff --git a/e2e/scenario8/expected.txt b/e2e/scenario8/expected.txt index 0bfcdd3..dbca4c5 100644 --- a/e2e/scenario8/expected.txt +++ b/e2e/scenario8/expected.txt @@ -1,22 +1,35 @@ Loading composer repositories with package information Updating dependencies +Dependency resolution completed in 0.000 seconds +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Dependency resolution completed in 0.000 seconds Lock file operations: 1 install, 0 updates, 0 removals +Installs: bamarni/composer-bin-plugin:dev-hash - Locking bamarni/composer-bin-plugin (dev-hash) Writing lock file -Installing dependencies from lock file +Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals +Installs: bamarni/composer-bin-plugin:dev-hash - Installing bamarni/composer-bin-plugin (dev-hash): Symlinking from ../.. Generating autoload files +> post-autoload-dump: Bamarni\Composer\Bin\Plugin->onPostAutoloadDump +[bamarni-bin] Calling onPostAutoloadDump(). [bamarni-bin] The command is being forwarded. +[bamarni-bin] Original input: update --prefer-lowest --verbose. +[bamarni-bin] Current working directory: /path/to/project/e2e/scenario8 +[bamarni-bin] Configuring bin directory to /path/to/project/e2e/scenario8/vendor/bin. +[bamarni-bin] Checking namespace vendor-bin/ns1 +[bamarni-bin] Changed current directory to vendor-bin/ns1. +[bamarni-bin] Running `@composer update --prefer-lowest --verbose --working-dir='.'`. Loading composer repositories with package information Updating dependencies -Lock file operations: 2 installs, 0 updates, 0 removals - - Locking nikic/iter (v1.6.0) - - Locking phpstan/phpstan (1.8.0) +Dependency resolution completed in 0.000 seconds +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Nothing to modify in lock file Writing lock file -Installing dependencies from lock file -Package operations: 1 install, 0 updates, 0 removals - - Installing nikic/iter (v1.6.0): Extracting archive +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove Generating autoload files -–––––––––––––– -nikic/iter +[bamarni-bin] Changed current directory to /path/to/project/e2e/scenario8. diff --git a/e2e/scenario8/script.sh b/e2e/scenario8/script.sh index c030a0a..93a8c2a 100755 --- a/e2e/scenario8/script.sh +++ b/e2e/scenario8/script.sh @@ -23,6 +23,4 @@ rm -rf vendor-bin/*/composer.lock || true rm -rf vendor-bin/*/vendor || true # Actual command to execute the test itself -composer update --no-dev 2>&1 | tee > actual.txt || true -echo "––––––––––––––" >> actual.txt -composer bin ns1 show --direct --name-only 2>&1 | tee >> actual.txt || true +composer update --prefer-lowest --verbose 2>&1 | tee >> actual.txt || true diff --git a/e2e/scenario8/vendor-bin/ns1/composer.json b/e2e/scenario8/vendor-bin/ns1/composer.json index 7b72340..0967ef4 100644 --- a/e2e/scenario8/vendor-bin/ns1/composer.json +++ b/e2e/scenario8/vendor-bin/ns1/composer.json @@ -1,8 +1 @@ -{ - "require": { - "nikic/iter": "v1.6.0" - }, - "require-dev": { - "phpstan/phpstan": "1.8.0" - } -} +{} diff --git a/src/BinCommand.php b/src/BinCommand.php index fb4e98c..9d7e9cc 100644 --- a/src/BinCommand.php +++ b/src/BinCommand.php @@ -52,10 +52,10 @@ protected function configure(): void { $this ->setDescription('Run a command inside a bin namespace') - ->setDefinition([ - new InputArgument(self::NAMESPACE_ARG, InputArgument::REQUIRED), - new InputArgument('args', InputArgument::REQUIRED | InputArgument::IS_ARRAY), - ]) + ->addArgument( + self::NAMESPACE_ARG, + InputArgument::REQUIRED + ) ->ignoreValidationErrors(); } diff --git a/src/BinInputFactory.php b/src/BinInputFactory.php index 2faac72..90fab1b 100644 --- a/src/BinInputFactory.php +++ b/src/BinInputFactory.php @@ -6,29 +6,84 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\StringInput; +use function array_filter; +use function array_map; +use function implode; +use function preg_match; use function preg_quote; -use function preg_replace; use function sprintf; final class BinInputFactory { + /** + * Extracts the input to execute in the bin namespace. + * + * For example: `bin namespace-name update --prefer-lowest` => `update --prefer-lowest` + * + * Note that no input definition is bound in the resulting input. + */ public static function createInput( string $namespace, InputInterface $previousInput ): InputInterface { - return new StringInput( - preg_replace( - sprintf('/bin\s+(--ansi\s)?%s(\s.+)/', preg_quote($namespace, '/')), - '$1$2', - (string) $previousInput, - 1 + $matchResult = preg_match( + sprintf( + '/^(?.+)?bin (?:(?.+?) )?(?:%1$s|\'%1$s\') (?.+?)(? -- .*)?$/', + preg_quote($namespace, '/') + ), + (string) $previousInput, + $matches + ); + + if (1 !== $matchResult) { + throw InvalidBinInput::forBinInput($previousInput); + } + + $inputParts = array_filter( + array_map( + 'trim', + [ + $matches['binCommand'], + $matches['preBinOptions2'] ?? '', + $matches['preBinOptions'] ?? '', + $matches['extraInput'] ?? '', + ] ) ); + + // Move the options present _before_ bin namespaceName to after, but + // before the end of option marker (--) if present. + $reorderedInput = implode(' ', $inputParts); + + return new StringInput($reorderedInput); } public static function createNamespaceInput(InputInterface $previousInput): InputInterface { - return new StringInput((string) $previousInput . ' --working-dir=.'); + $matchResult = preg_match( + '/^(.+?\s?)(--(?: .+)?)?$/', + (string) $previousInput, + $matches + ); + + if (1 !== $matchResult) { + throw InvalidBinInput::forNamespaceInput($previousInput); + } + + $inputParts = array_filter( + array_map( + 'trim', + [ + $matches[1], + '--working-dir=.', + $matches[2] ?? '', + ] + ) + ); + + $newInput = implode(' ', $inputParts); + + return new StringInput($newInput); } public static function createForwardedCommandInput(InputInterface $input): InputInterface diff --git a/src/InvalidBinInput.php b/src/InvalidBinInput.php new file mode 100644 index 0000000..a966b60 --- /dev/null +++ b/src/InvalidBinInput.php @@ -0,0 +1,32 @@ + ", for example "bin all update --prefer-lowest".', + $input + ) + ); + } + + public static function forNamespaceInput(InputInterface $input): self + { + return new self( + sprintf( + 'Could not parse the input (executed within the namespace) "%s".', + $input + ) + ); + } +} diff --git a/tests/BinInputFactoryTest.php b/tests/BinInputFactoryTest.php index 8bf6b97..2eca750 100644 --- a/tests/BinInputFactoryTest.php +++ b/tests/BinInputFactoryTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\StringInput; +use function sprintf; /** * @covers \Bamarni\Composer\Bin\BinInputFactory @@ -29,30 +30,110 @@ public function test_it_can_create_a_new_input( public static function inputProvider(): iterable { - yield [ - 'foo-namespace', - new StringInput('bin foo-namespace flex:update --prefer-lowest'), - new StringInput('flex:update --prefer-lowest'), + $namespaceNames = [ + 'simpleNamespaceName', + 'composed-namespaceName', + 'regexLimiter/namespaceName', + 'all', ]; - yield [ - 'foo-namespace', - new StringInput('bin foo-namespace flex:update --prefer-lowest --ansi'), - new StringInput('flex:update --prefer-lowest --ansi'), - ]; + foreach ($namespaceNames as $namespaceName) { + $labelPrefix = sprintf('[%s]', $namespaceName); + + yield $labelPrefix.'simple command' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show', + $namespaceName + ) + ), + new StringInput('show'), + ]; + + yield $labelPrefix.' namespaced command' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s check:platform', + $namespaceName + ) + ), + new StringInput('check:platform'), + ]; + + yield $labelPrefix.'command with options' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i', + $namespaceName + ) + ), + new StringInput('show --tree -i'), + ]; + + yield $labelPrefix.'command with annoyingly placed options' => [ + $namespaceName, + new StringInput( + sprintf( + '--ansi bin %s -o --quiet show --tree -i', + $namespaceName + ) + ), + new StringInput('-o --quiet show --tree -i --ansi'), + ]; + + yield $labelPrefix.'command with options with option separator' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i --', + $namespaceName + ) + ), + new StringInput('show --tree -i --'), + ]; + + yield $labelPrefix.'command with options with option separator and follow up argument' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i -- foo', + $namespaceName + ) + ), + new StringInput('show --tree -i -- foo'), + ]; + + yield $labelPrefix.'command with options with option separator and follow up option' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i -- --foo', + $namespaceName + ) + ), + new StringInput('show --tree -i -- --foo'), + ]; + + yield $labelPrefix.'command with annoyingly placed options and option separator and follow up option' => [ + $namespaceName, + new StringInput( + sprintf( + '--ansi bin %s -o --quiet show --tree -i -- --foo', + $namespaceName + ) + ), + new StringInput('-o --quiet show --tree -i --ansi -- --foo'), + ]; + } // See https://github.com/bamarni/composer-bin-plugin/pull/23 yield [ 'foo-namespace', new StringInput('bin --ansi foo-namespace flex:update --prefer-lowest'), - new StringInput('--ansi flex:update --prefer-lowest'), - ]; - - // See https://github.com/bamarni/composer-bin-plugin/pull/73 - yield [ - 'irrelevant', - new StringInput('update --dry-run --no-plugins roave/security-advisories'), - new StringInput('update --dry-run --no-plugins roave/security-advisories'), + new StringInput('flex:update --prefer-lowest --ansi'), ]; } @@ -70,14 +151,41 @@ public function test_it_can_create_a_new_input_for_a_namespace( public static function namespaceInputProvider(): iterable { - yield [ - new StringInput('flex:update --prefer-lowest'), - new StringInput('flex:update --prefer-lowest --working-dir=.'), + $namespaceNames = [ + 'simpleNamespaceName', + 'composed-namespaceName', + 'regexLimiter/namespaceName', + 'all', ]; - yield [ - new StringInput('flex:update --prefer-lowest --ansi'), - new StringInput('flex:update --prefer-lowest --ansi --working-dir=.'), + yield 'simple command' => [ + new StringInput('flex:update'), + new StringInput('flex:update --working-dir=.'), + ]; + + yield 'command with options' => [ + new StringInput('flex:update --prefer-lowest -i'), + new StringInput('flex:update --prefer-lowest -i --working-dir=.'), + ]; + + yield 'command with annoyingly placed options' => [ + new StringInput('-o --quiet flex:update --prefer-lowest -i'), + new StringInput('-o --quiet flex:update --prefer-lowest -i --working-dir=.'), + ]; + + yield 'command with options with option separator' => [ + new StringInput('flex:update --prefer-lowest -i --'), + new StringInput('flex:update --prefer-lowest -i --working-dir=. --'), + ]; + + yield 'command with options with option separator and follow up argument' => [ + new StringInput('flex:update --prefer-lowest -i -- foo'), + new StringInput('flex:update --prefer-lowest -i --working-dir=. -- foo'), + ]; + + yield 'command with annoyingly placed options and option separator and follow up option' => [ + new StringInput('-o --quiet flex:update --prefer-lowest -i -- --foo'), + new StringInput('-o --quiet flex:update --prefer-lowest -i --working-dir=. -- --foo'), ]; } diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index f4940ba..81ed11e 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -54,9 +54,7 @@ public function test_it_passes_the_e2e_test(string $scenarioPath): void $originalContent = 'File was not created.'; } - self::assertTrue( - $scenarioProcess->isSuccessful(), - <<isSuccessful(), + $errorMessage ); $actual = self::retrieveActualOutput( @@ -73,7 +75,7 @@ public function test_it_passes_the_e2e_test(string $scenarioPath): void $originalContent ); - self::assertSame($expected, $actual); + self::assertSame($expected, $actual, $errorMessage); } public static function scenarioProvider(): iterable