Browse Source

Merge pull request #13220 from cakephp/completion-shell

4.x - Convert Completion shell to a command
Mark Story 7 years ago
parent
commit
81cc5676da

+ 274 - 0
src/Command/CompletionCommand.php

@@ -0,0 +1,274 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP Project
+ * @since         2.5.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Command;
+
+use Cake\Console\Arguments;
+use Cake\Console\Command;
+use Cake\Console\CommandCollection;
+use Cake\Console\CommandCollectionAwareInterface;
+use Cake\Console\ConsoleIo;
+use Cake\Console\ConsoleOptionParser;
+use Cake\Console\Shell;
+use Cake\Utility\Inflector;
+use ReflectionClass;
+use ReflectionMethod;
+
+/**
+ * Provide command completion shells such as bash.
+ */
+class CompletionCommand extends Command implements CommandCollectionAwareInterface
+{
+    /**
+     * @var \Cake\Command\Cake\Console\CommandCollection
+     */
+    protected $commands;
+
+    /**
+     * Set the command collection used to get completion data on.
+     *
+     * @param \Cake\Command\Cake\Console\CommandCollection $commands The command collection
+     * @return void
+     */
+    public function setCommandCollection(CommandCollection $commands): void
+    {
+        $this->commands = $commands;
+    }
+
+    /**
+     * Gets the option parser instance and configures it.
+     *
+     * @param \Cake\Console\ConsoleOptionParser $parser The parser to build
+     * @return \Cake\Console\ConsoleOptionParser
+     */
+    public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+    {
+        $modes = [
+            'commands' => 'Output a list of available commands',
+            'subcommands' => 'Output a list of available sub-commands for a command',
+            'options' => 'Output a list of available options for a command and possible subcommand.',
+            'fuzzy' => 'Does nothing. Only for backwards compatibility',
+        ];
+        $modeHelp = '';
+        foreach ($modes as $key => $help) {
+            $modeHelp .= "- <info>{$key}</info> {$help}\n";
+        }
+
+        $parser->setDescription(
+            'Used by shells like bash to autocomplete command name, options and arguments'
+        )->addArgument('mode', [
+            'help' => 'The type of thing to get completion on.',
+            'required' => true,
+            'choices' => array_keys($modes),
+        ])->addArgument('command', [
+            'help' => 'The command name to get information on.',
+            'required' => false,
+        ])->addArgument('subcommand', [
+            'help' => 'The sub-command related to command to get information on.',
+            'required' => false,
+        ])->setEpilog([
+            "The various modes allow you to get help information on commands and their arguments.",
+            "The available modes are:",
+            "",
+            $modeHelp,
+            "",
+            'This command is not intended to be called manually, and should be invoked from a ' .
+                'terminal completion script.',
+        ]);
+
+        return $parser;
+    }
+
+    /**
+     * Main function Prints out the list of commands.
+     *
+     * @param \Cake\Console\Arguments $args The command arguments.
+     * @param \Cake\Console\ConsoleIo $io The console io
+     * @return int
+     */
+    public function execute(Arguments $args, ConsoleIo $io): ?int
+    {
+        $mode = $args->getArgument('mode');
+        switch ($mode) {
+            case 'commands':
+                return $this->getCommands($args, $io);
+            case 'subcommands':
+                return $this->getSubcommands($args, $io);
+            case 'options':
+                return $this->getOptions($args, $io);
+            case 'fuzzy':
+                return static::CODE_SUCCESS;
+            default:
+                $io->err('Invalid mode chosen.');
+        }
+
+        return static::CODE_SUCCESS;
+    }
+
+    /**
+     * Get the list of defined commands.
+     *
+     * @param \Cake\Command\Cake\Console\Arguments $args The command arguments.
+     * @param \Cake\Console\ConsoleIo $io The console io
+     * @return int
+     */
+    protected function getCommands(Arguments $args, ConsoleIo $io): int
+    {
+        $options = [];
+        foreach ($this->commands as $key => $value) {
+            $parts = explode(' ', $key);
+            $options[] = $parts[0];
+        }
+        $options = array_unique($options);
+        $io->out(implode(' ', $options));
+
+        return static::CODE_SUCCESS;
+    }
+
+    /**
+     * Get the list of defined sub-commands.
+     *
+     * @param \Cake\Command\Cake\Console\Arguments $args The command arguments.
+     * @param \Cake\Console\ConsoleIo $io The console io
+     * @return int
+     */
+    protected function getSubcommands(Arguments $args, ConsoleIo $io): int
+    {
+        $name = $args->getArgument('command');
+        if ($name === null || !strlen($name)) {
+            return static::CODE_SUCCESS;
+        }
+
+        $options = [];
+        foreach ($this->commands as $key => $value) {
+            $parts = explode(' ', $key);
+            if ($parts[0] !== $name) {
+                continue;
+            }
+
+            // Space separate command name, collect
+            // hits as subcommands
+            if (count($parts) > 1) {
+                $options[] = implode(' ', array_slice($parts, 1));
+                continue;
+            }
+
+            // Handle class strings
+            if (is_string($value)) {
+                $reflection = new ReflectionClass($value);
+                $value = $reflection->newInstance();
+            }
+            if ($value instanceof Shell) {
+                $shellCommands = $this->shellSubcommands($value);
+                $options = array_merge($options, $shellCommands);
+            }
+        }
+        $options = array_unique($options);
+        $io->out(implode(' ', $options));
+
+        return static::CODE_SUCCESS;
+    }
+
+    /**
+     * Reflect the subcommands names out of a shell.
+     *
+     * @param \Cake\Console\Shell $shell The shell to get commands for
+     * @return array A list of commands
+     */
+    protected function shellSubcommands(Shell $shell): array
+    {
+        $shell->initialize();
+        $shell->loadTasks();
+
+        $optionParser = $shell->getOptionParser();
+        $subcommands = $optionParser->subcommands();
+
+        $output = array_keys($subcommands);
+
+        // If there are no formal subcommands all methods
+        // on a shell are 'subcommands'
+        if (count($subcommands) === 0) {
+            $reflection = new ReflectionClass($shell);
+            foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+                if ($shell->hasMethod($method->getName())) {
+                    $output[] = $method->getName();
+                }
+            }
+        }
+        $taskNames = array_map('Cake\Utility\Inflector::underscore', $shell->taskNames);
+        $output = array_merge($output, $taskNames);
+
+        return array_unique($output);
+    }
+
+    /**
+     * Get the options for a command or subcommand
+     *
+     * @param \Cake\Command\Cake\Console\Arguments $args The command arguments.
+     * @param \Cake\Console\ConsoleIo $io The console io
+     * @return int
+     */
+    protected function getOptions(Arguments $args, ConsoleIo $io): ?int
+    {
+        $name = $args->getArgument('command');
+        $subcommand = $args->getArgument('subcommand');
+
+        $options = [];
+        foreach ($this->commands as $key => $value) {
+            $parts = explode(' ', $key);
+            if ($parts[0] !== $name) {
+                continue;
+            }
+            if ($subcommand && isset($parts[1]) && $parts[1] !== $subcommand) {
+                continue;
+            }
+
+            // Handle class strings
+            if (is_string($value)) {
+                $reflection = new ReflectionClass($value);
+                $value = $reflection->newInstance();
+            }
+            $parser = null;
+            if ($value instanceof Command) {
+                $parser = $value->getOptionParser();
+            }
+            if ($value instanceof Shell) {
+                $value->initialize();
+                $value->loadTasks();
+
+                $parser = $value->getOptionParser();
+                $subcommand = Inflector::camelize((string)$subcommand);
+                if ($subcommand && $value->hasTask($subcommand)) {
+                    $parser = $value->{$subcommand}->getOptionParser();
+                }
+            }
+
+            if ($parser) {
+                foreach ($parser->options() as $name => $option) {
+                    $options[] = "--$name";
+                    $short = $option->short();
+                    if ($short) {
+                        $options[] = "-$short";
+                    }
+                }
+            }
+        }
+        $options = array_unique($options);
+        $io->out(implode(' ', $options));
+
+        return static::CODE_SUCCESS;
+    }
+}

+ 1 - 0
src/Console/CommandScanner.php

@@ -65,6 +65,7 @@ class CommandScanner
     {
         foreach ($commands as $i => $command) {
             $command['name'] = str_replace('_', ' ', $command['name']);
+            $command['fullName'] = str_replace('_', ' ', $command['fullName']);
             $commands[$i] = $command;
         }
 

+ 0 - 183
src/Shell/CompletionShell.php

@@ -1,183 +0,0 @@
-<?php
-declare(strict_types=1);
-
-/**
- * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
- * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- *
- * Licensed under The MIT License
- * For full copyright and license information, please see the LICENSE.txt
- * Redistributions of files must retain the above copyright notice.
- *
- * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- * @link          https://cakephp.org CakePHP Project
- * @since         2.5.0
- * @license       https://opensource.org/licenses/mit-license.php MIT License
- */
-namespace Cake\Shell;
-
-use Cake\Console\ConsoleOptionParser;
-use Cake\Console\Shell;
-
-/**
- * Provide command completion shells such as bash.
- *
- * @property \Cake\Shell\Task\CommandTask $Command
- */
-class CompletionShell extends Shell
-{
-    /**
-     * Contains tasks to load and instantiate
-     *
-     * @var array
-     */
-    public $tasks = ['Command'];
-
-    /**
-     * The command collection for the application
-     *
-     * @var \Cake\Console\CommandCollection
-     */
-    protected $commandCollection;
-
-    /**
-     * Echo no header by overriding the startup method
-     *
-     * @return void
-     */
-    public function startup(): void
-    {
-    }
-
-    /**
-     * Not called by the autocomplete shell - this is for curious users
-     *
-     * @return int|bool Returns the number of bytes returned from writing to stdout.
-     */
-    public function main()
-    {
-        return $this->out($this->getOptionParser()->help());
-    }
-
-    /**
-     * list commands
-     *
-     * @return int|bool|null Returns the number of bytes returned from writing to stdout.
-     */
-    public function commands()
-    {
-        $options = $this->Command->commands();
-
-        return $this->_output($options);
-    }
-
-    /**
-     * list options for the named command
-     *
-     * @return int|bool|null Returns the number of bytes returned from writing to stdout.
-     */
-    public function options()
-    {
-        $commandName = $subCommandName = '';
-        if (!empty($this->args[0])) {
-            $commandName = $this->args[0];
-        }
-        if (!empty($this->args[1])) {
-            $subCommandName = $this->args[1];
-        }
-        $options = $this->Command->options($commandName, $subCommandName);
-
-        return $this->_output($options);
-    }
-
-    /**
-     * list subcommands for the named command
-     *
-     * @return int|bool|null Returns the number of bytes returned from writing to stdout.
-     * @throws \ReflectionException
-     */
-    public function subcommands()
-    {
-        if (!$this->args) {
-            return $this->_output();
-        }
-
-        $options = $this->Command->subCommands($this->args[0]);
-
-        return $this->_output($options);
-    }
-
-    /**
-     * Guess autocomplete from the whole argument string
-     *
-     * @return int|bool|null Returns the number of bytes returned from writing to stdout.
-     */
-    public function fuzzy()
-    {
-        return $this->_output();
-    }
-
-    /**
-     * Gets the option parser instance and configures it.
-     *
-     * @return \Cake\Console\ConsoleOptionParser
-     */
-    public function getOptionParser(): ConsoleOptionParser
-    {
-        $parser = parent::getOptionParser();
-
-        $parser->setDescription(
-            'Used by shells like bash to autocomplete command name, options and arguments'
-        )->addSubcommand('commands', [
-            'help' => 'Output a list of available commands',
-            'parser' => [
-                'description' => 'List all available',
-            ],
-        ])->addSubcommand('subcommands', [
-            'help' => 'Output a list of available subcommands',
-            'parser' => [
-                'description' => 'List subcommands for a command',
-                'arguments' => [
-                    'command' => [
-                        'help' => 'The command name',
-                        'required' => false,
-                    ],
-                ],
-            ],
-        ])->addSubcommand('options', [
-            'help' => 'Output a list of available options',
-            'parser' => [
-                'description' => 'List options',
-                'arguments' => [
-                    'command' => [
-                        'help' => 'The command name',
-                        'required' => false,
-                    ],
-                    'subcommand' => [
-                        'help' => 'The subcommand name',
-                        'required' => false,
-                    ],
-                ],
-            ],
-        ])->addSubcommand('fuzzy', [
-            'help' => 'Guess autocomplete',
-        ])->setEpilog([
-            'This command is not intended to be called manually',
-        ]);
-
-        return $parser;
-    }
-
-    /**
-     * Emit results as a string, space delimited
-     *
-     * @param array $options The options to output
-     * @return int|bool|null Returns the number of bytes returned from writing to stdout.
-     */
-    protected function _output(array $options = [])
-    {
-        if ($options) {
-            return $this->out(implode($options, ' '));
-        }
-    }
-}

+ 2 - 164
src/Shell/Task/CommandTask.php

@@ -20,13 +20,12 @@ use Cake\Console\Shell;
 use Cake\Core\App;
 use Cake\Core\Plugin;
 use Cake\Filesystem\Filesystem;
-use Cake\Utility\Hash;
 use Cake\Utility\Inflector;
-use ReflectionClass;
-use ReflectionMethod;
 
 /**
  * Base class for Shell Command reflection.
+ *
+ * @internal
  */
 class CommandTask extends Shell
 {
@@ -130,165 +129,4 @@ class CommandTask extends Shell
 
         return $shells;
     }
-
-    /**
-     * Return a list of all commands
-     *
-     * @return array
-     */
-    public function commands(): array
-    {
-        $shellList = $this->getShellList();
-        $flatten = Hash::flatten($shellList);
-        $duplicates = array_intersect($flatten, array_unique(array_diff_key($flatten, array_unique($flatten))));
-        $duplicates = Hash::expand($duplicates);
-
-        $options = [];
-        foreach ($shellList as $type => $commands) {
-            foreach ($commands as $shell) {
-                $prefix = '';
-                if (!in_array(strtolower($type), ['app', 'core'], true) &&
-                    isset($duplicates[$type]) &&
-                    in_array($shell, $duplicates[$type], true)
-                ) {
-                    $prefix = $type . '.';
-                }
-
-                $options[] = $prefix . $shell;
-            }
-        }
-
-        return $options;
-    }
-
-    /**
-     * Return a list of subcommands for a given command
-     *
-     * @param string $commandName The command you want subcommands from.
-     * @return string[]
-     * @throws \ReflectionException
-     */
-    public function subCommands(string $commandName): array
-    {
-        $shell = $this->getShell($commandName);
-
-        if (!$shell) {
-            return [];
-        }
-
-        $taskMap = $this->Tasks->normalizeArray((array)$shell->tasks);
-        $return = array_keys($taskMap);
-        $return = array_map('Cake\Utility\Inflector::underscore', $return);
-
-        $shellMethodNames = ['main', 'help', 'getOptionParser', 'initialize', 'runCommand'];
-
-        $baseClasses = ['Object', 'Shell', 'AppShell'];
-
-        $Reflection = new ReflectionClass($shell);
-        $methods = $Reflection->getMethods(ReflectionMethod::IS_PUBLIC);
-        $methodNames = [];
-        foreach ($methods as $method) {
-            $declaringClass = $method->getDeclaringClass()->getShortName();
-            if (!in_array($declaringClass, $baseClasses, true)) {
-                $methodNames[] = $method->getName();
-            }
-        }
-
-        $return = array_merge($return, array_diff($methodNames, $shellMethodNames));
-        sort($return);
-
-        return $return;
-    }
-
-    /**
-     * Get Shell instance for the given command
-     *
-     * @param string $commandName The command you want.
-     * @return \Cake\Console\Shell|false Shell instance if the command can be found, false otherwise.
-     */
-    public function getShell(string $commandName)
-    {
-        [$pluginDot, $name] = pluginSplit($commandName, true);
-
-        if (in_array(strtolower((string)$pluginDot), ['app.', 'core.'], true)) {
-            $commandName = $name;
-            $pluginDot = '';
-        }
-
-        if (!in_array($commandName, $this->commands(), true)
-            && empty($pluginDot)
-            && !in_array($name, $this->commands(), true)
-        ) {
-            return false;
-        }
-
-        if (empty($pluginDot)) {
-            $shellList = $this->getShellList();
-
-            if (!in_array($commandName, $shellList['app']) && !in_array($commandName, $shellList['CORE'], true)) {
-                unset($shellList['CORE'], $shellList['app']);
-                foreach ($shellList as $plugin => $commands) {
-                    if (in_array($commandName, $commands, true)) {
-                        $pluginDot = $plugin . '.';
-                        break;
-                    }
-                }
-            }
-        }
-
-        $name = Inflector::camelize($name);
-        $pluginDot = Inflector::camelize((string)$pluginDot);
-        $class = App::className($pluginDot . $name, 'Shell', 'Shell');
-        if (!$class) {
-            return false;
-        }
-
-        /** @var \Cake\Console\Shell $shell */
-        $shell = new $class();
-        $shell->plugin = trim($pluginDot, '.');
-        $shell->initialize();
-
-        return $shell;
-    }
-
-    /**
-     * Get options list for the given command or subcommand
-     *
-     * @param string $commandName The command to get options for.
-     * @param string $subCommandName The subcommand to get options for. Can be empty to get options for the command.
-     * If this parameter is used, the subcommand must be a valid subcommand of the command passed
-     * @return array Options list for the given command or subcommand
-     */
-    public function options(string $commandName, string $subCommandName = ''): array
-    {
-        $shell = $this->getShell($commandName);
-
-        if (!$shell) {
-            return [];
-        }
-
-        $parser = $shell->getOptionParser();
-
-        if (!empty($subCommandName)) {
-            $subCommandName = Inflector::camelize($subCommandName);
-            if ($shell->hasTask($subCommandName)) {
-                $parser = $shell->{$subCommandName}->getOptionParser();
-            } else {
-                return [];
-            }
-        }
-
-        $options = [];
-        $array = $parser->options();
-        /** @var \Cake\Console\ConsoleInputOption $obj */
-        foreach ($array as $name => $obj) {
-            $options[] = "--$name";
-            $short = $obj->short();
-            if ($short) {
-                $options[] = "-$short";
-            }
-        }
-
-        return $options;
-    }
 }

+ 392 - 0
tests/TestCase/Command/CompletionCommandTest.php

@@ -0,0 +1,392 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * CakePHP :  Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP Project
+ * @since         2.5.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Command;
+
+use Cake\Console\Shell;
+use Cake\Core\Configure;
+use Cake\Routing\Router;
+use Cake\TestSuite\ConsoleIntegrationTestCase;
+
+/**
+ * CompletionCommandTest
+ */
+class CompletionCommandTest extends ConsoleIntegrationTestCase
+{
+    /**
+     * setUp method
+     *
+     * @return void
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+        static::setAppNamespace();
+        Configure::write('Plugins.autoload', ['TestPlugin', 'TestPluginTwo']);
+
+        $this->useCommandRunner();
+    }
+
+    /**
+     * tearDown
+     *
+     * @return void
+     */
+    public function tearDown(): void
+    {
+        parent::tearDown();
+        Router::reload();
+        $this->clearPlugins();
+    }
+
+    /**
+     * test that the startup method suppresses the shell header
+     *
+     * @return void
+     */
+    public function testStartup()
+    {
+        $this->exec('completion');
+        $this->assertExitCode(Shell::CODE_ERROR);
+
+        $this->assertOutputNotContains('Welcome to CakePHP');
+    }
+
+    /**
+     * test commands method that list all available commands
+     *
+     * @return void
+     */
+    public function testCommands()
+    {
+        $this->exec('completion commands');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = [
+            'test_plugin.example',
+            'test_plugin.sample',
+            'test_plugin_two.example',
+            'unique',
+            'welcome',
+            'cache',
+            'help',
+            'i18n',
+            'plugin',
+            'routes',
+            'schema_cache',
+            'server',
+            'upgrade',
+            'version',
+            'abort',
+            'auto_load_model',
+            'demo',
+            'i18m',
+            'integration',
+            'merge',
+            'sample',
+            'shell_test',
+            'testing_dispatch',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
+    }
+
+    /**
+     * test that options without argument returns nothing
+     *
+     * @return void
+     */
+    public function testOptionsNoArguments()
+    {
+        $this->exec('completion options');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+        $this->assertOutputEmpty();
+    }
+
+    /**
+     * test that options with a non-existing command returns nothing
+     *
+     * @return void
+     */
+    public function testOptionsNonExistingCommand()
+    {
+        $this->exec('completion options foo');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+        $this->assertOutputEmpty();
+    }
+
+    /**
+     * test that options with an existing command returns the proper options
+     *
+     * @return void
+     */
+    public function testOptionsShell()
+    {
+        $this->exec('completion options schema_cache');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = [
+            '--connection -c',
+            '--help -h',
+            '--quiet -q',
+            '--verbose -v',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
+    }
+
+    /**
+     * test that options with an existing command / subcommand pair returns the proper options
+     *
+     * @return void
+     */
+    public function testOptionsShellTask()
+    {
+        $this->exec('completion options sample sample');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = [
+            '--help -h',
+            '--quiet -q',
+            '--sample -s',
+            '--verbose -v',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
+    }
+
+    /**
+     * test that options with an existing command / subcommand pair returns the proper options
+     *
+     * @return void
+     */
+    public function testOptionsShellCommand()
+    {
+        $this->exec('completion options cache list');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = [
+            '--help -h',
+            '--quiet -q',
+            '--verbose -v',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
+    }
+
+    /**
+     * test that subCommands with a existing CORE command returns the proper sub commands
+     *
+     * @return void
+     */
+    public function testSubCommandsCorePlugin()
+    {
+        $this->exec('completion subcommands schema_cache');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = "build clear";
+        $this->assertOutputContains($expected);
+    }
+
+    /**
+     * test that subCommands with a existing APP command returns the proper sub commands (in this case none)
+     *
+     * @return void
+     */
+    public function testSubCommandsAppPlugin()
+    {
+        $this->exec('completion subcommands sample');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = [
+            'derp',
+            'load',
+            'returnValue',
+            'sample',
+            'withAbort',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
+    }
+
+    /**
+     * test that subCommands with a existing CORE command
+     *
+     * @return void
+     */
+    public function testSubCommandsCoreMultiwordCommand()
+    {
+        $this->exec('completion subcommands cache');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = [
+            'list', 'clear', 'clearall',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
+    }
+
+    /**
+     * test that subCommands with an existing plugin command returns the proper sub commands
+     * when the Shell name is unique and the dot notation not mandatory
+     *
+     * @return void
+     */
+    public function testSubCommandsPlugin()
+    {
+        $this->exec('completion subcommands welcome');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = "say_hello";
+        $this->assertOutputContains($expected);
+    }
+
+    /**
+     * test that using the dot notation when not mandatory works to provide backward compatibility
+     *
+     * @return void
+     */
+    public function testSubCommandsPluginDotNotationBackwardCompatibility()
+    {
+        $this->exec('completion subcommands test_plugin_two.welcome');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = "say_hello";
+        $this->assertOutputContains($expected);
+    }
+
+    /**
+     * test that subCommands with an existing plugin command returns the proper sub commands
+     *
+     * @return void
+     */
+    public function testSubCommandsPluginDotNotation()
+    {
+        $this->exec('completion subcommands test_plugin_two.example');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = "say_hello";
+        $this->assertOutputContains($expected);
+    }
+
+    /**
+     * test that subCommands with an app shell that is also defined in a plugin and without the prefix "app."
+     * returns proper sub commands
+     *
+     * @return void
+     */
+    public function testSubCommandsAppDuplicatePluginNoDot()
+    {
+        $this->exec('completion subcommands sample');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = [
+            'derp',
+            'load',
+            'returnValue',
+            'sample',
+            'withAbort',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
+    }
+
+    /**
+     * test that subCommands with a plugin shell that is also defined in the returns proper sub commands
+     *
+     * @return void
+     */
+    public function testSubCommandsPluginDuplicateApp()
+    {
+        $this->exec('completion subcommands test_plugin.sample');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = "example";
+        $this->assertOutputContains($expected);
+    }
+
+    /**
+     * test that subcommands without arguments returns nothing
+     *
+     * @return void
+     */
+    public function testSubCommandsNoArguments()
+    {
+        $this->exec('completion subcommands');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $this->assertOutputEmpty();
+    }
+
+    /**
+     * test that subcommands with a non-existing command returns nothing
+     *
+     * @return void
+     */
+    public function testSubCommandsNonExistingCommand()
+    {
+        $this->exec('completion subcommands foo');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $this->assertOutputEmpty();
+    }
+
+    /**
+     * test that subcommands returns the available subcommands for the given command
+     *
+     * @return void
+     */
+    public function testSubCommands()
+    {
+        $this->exec('completion subcommands schema_cache');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $expected = "build clear";
+        $this->assertOutputContains($expected);
+    }
+
+    /**
+     * test that fuzzy returns nothing
+     *
+     * @return void
+     */
+    public function testFuzzy()
+    {
+        $this->exec('completion fuzzy');
+        $this->assertOutputEmpty();
+    }
+
+    /**
+     * test that help returns content
+     *
+     * @return void
+     */
+    public function testHelp()
+    {
+        $this->exec('completion --help');
+        $this->assertExitCode(Shell::CODE_SUCCESS);
+
+        $this->assertOutputContains('Output a list of available commands');
+        $this->assertOutputContains('Output a list of available sub-commands');
+    }
+}

+ 0 - 339
tests/TestCase/Shell/CompletionShellTest.php

@@ -1,339 +0,0 @@
-<?php
-declare(strict_types=1);
-
-/**
- * CakePHP :  Rapid Development Framework (https://cakephp.org)
- * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- *
- * Licensed under The MIT License
- * For full copyright and license information, please see the LICENSE.txt
- * Redistributions of files must retain the above copyright notice.
- *
- * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- * @link          https://cakephp.org CakePHP Project
- * @since         2.5.0
- * @license       https://opensource.org/licenses/mit-license.php MIT License
- */
-namespace Cake\Test\TestCase\Shell;
-
-use Cake\Console\ConsoleIo;
-use Cake\TestSuite\Stub\ConsoleOutput as StubOutput;
-use Cake\TestSuite\TestCase;
-
-/**
- * CompletionShellTest
- */
-class CompletionShellTest extends TestCase
-{
-    /**
-     * setUp method
-     *
-     * @return void
-     */
-    public function setUp(): void
-    {
-        parent::setUp();
-        static::setAppNamespace();
-        $this->loadPlugins(['TestPlugin', 'TestPluginTwo']);
-
-        $this->out = new StubOutput();
-        $io = new ConsoleIo($this->out);
-
-        $this->Shell = $this->getMockBuilder('Cake\Shell\CompletionShell')
-            ->setMethods(['in', '_stop', 'clear'])
-            ->setConstructorArgs([$io])
-            ->getMock();
-
-        $this->Shell->Command = $this->getMockBuilder('Cake\Shell\Task\CommandTask')
-            ->setMethods(['in', '_stop', 'clear'])
-            ->setConstructorArgs([$io])
-            ->getMock();
-    }
-
-    /**
-     * tearDown
-     *
-     * @return void
-     */
-    public function tearDown(): void
-    {
-        parent::tearDown();
-        unset($this->Shell);
-        static::setAppNamespace('App');
-        $this->clearPlugins();
-    }
-
-    /**
-     * test that the startup method suppresses the shell header
-     *
-     * @return void
-     */
-    public function testStartup()
-    {
-        $this->Shell->runCommand(['main']);
-        $output = $this->out->output();
-
-        $needle = 'Welcome to CakePHP';
-        $this->assertTextNotContains($needle, $output);
-    }
-
-    /**
-     * test that main displays a warning
-     *
-     * @return void
-     */
-    public function testMain()
-    {
-        $this->Shell->runCommand(['main']);
-        $output = $this->out->output();
-
-        $expected = '/This command is not intended to be called manually/';
-        $this->assertRegExp($expected, $output);
-    }
-
-    /**
-     * test commands method that list all available commands
-     *
-     * @return void
-     */
-    public function testCommands()
-    {
-        $this->markTestIncomplete('pending completion shell rebuild');
-        $this->Shell->runCommand(['commands']);
-        $output = $this->out->output();
-
-        // This currently incorrectly shows `cache clearall` when it should only show `cache`
-        // The subcommands method also needs rework to handle multi-word subcommands
-        $expected = 'TestPlugin.example TestPlugin.sample TestPluginTwo.example unique welcome ' .
-            'cache help i18n plugin routes schema_cache server upgrade version ' .
-            "abort auto_load_model demo i18m integration merge sample shell_test testing_dispatch";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that options without argument returns nothing
-     *
-     * @return void
-     */
-    public function testOptionsNoArguments()
-    {
-        $this->Shell->runCommand(['options']);
-        $output = $this->out->output();
-
-        $expected = '';
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that options with a non-existing command returns nothing
-     *
-     * @return void
-     */
-    public function testOptionsNonExistingCommand()
-    {
-        $this->Shell->runCommand(['options', 'foo']);
-        $output = $this->out->output();
-        $expected = '';
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that options with an existing command returns the proper options
-     *
-     * @return void
-     */
-    public function testOptions()
-    {
-        $this->Shell->runCommand(['options', 'schema_cache']);
-        $output = $this->out->output();
-
-        $expected = "--connection -c --help -h --quiet -q --verbose -v";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that options with an existing command / subcommand pair returns the proper options
-     *
-     * @return void
-     */
-    public function testOptionsTask()
-    {
-        $this->Shell->runCommand(['options', 'sample', 'sample']);
-        $output = $this->out->output();
-
-        $expected = "--help -h --quiet -q --sample -s --verbose -v";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subCommands with a existing CORE command returns the proper sub commands
-     *
-     * @return void
-     */
-    public function testSubCommandsCorePlugin()
-    {
-        $this->Shell->runCommand(['subcommands', 'CORE.schema_cache']);
-        $output = $this->out->output();
-
-        $expected = "build clear";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subCommands with a existing APP command returns the proper sub commands (in this case none)
-     *
-     * @return void
-     */
-    public function testSubCommandsAppPlugin()
-    {
-        $this->Shell->runCommand(['subcommands', 'app.sample']);
-        $output = $this->out->output();
-
-        $expected = "derp load returnValue sample withAbort";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subCommands with a existing CORE command
-     *
-     * @return void
-     */
-    public function testSubCommandsCoreMultiwordCommand()
-    {
-        $this->markTestIncomplete('pending completion shell rebuild');
-
-        $this->Shell->runCommand(['subcommands', 'CORE.cache']);
-        $output = $this->out->output();
-
-        $expected = "list clear clearall";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subCommands with an existing plugin command returns the proper sub commands
-     * when the Shell name is unique and the dot notation not mandatory
-     *
-     * @return void
-     */
-    public function testSubCommandsPlugin()
-    {
-        $this->Shell->runCommand(['subcommands', 'welcome']);
-        $output = $this->out->output();
-
-        $expected = "say_hello";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that using the dot notation when not mandatory works to provide backward compatibility
-     *
-     * @return void
-     */
-    public function testSubCommandsPluginDotNotationBackwardCompatibility()
-    {
-        $this->Shell->runCommand(['subcommands', 'TestPluginTwo.welcome']);
-        $output = $this->out->output();
-
-        $expected = "say_hello";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subCommands with an existing plugin command returns the proper sub commands
-     *
-     * @return void
-     */
-    public function testSubCommandsPluginDotNotation()
-    {
-        $this->Shell->runCommand(['subcommands', 'TestPluginTwo.example']);
-        $output = $this->out->output();
-
-        $expected = "say_hello";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subCommands with an app shell that is also defined in a plugin and without the prefix "app."
-     * returns proper sub commands
-     *
-     * @return void
-     */
-    public function testSubCommandsAppDuplicatePluginNoDot()
-    {
-        $this->Shell->runCommand(['subcommands', 'sample']);
-        $output = $this->out->output();
-
-        $expected = "derp load returnValue sample withAbort";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subCommands with a plugin shell that is also defined in the returns proper sub commands
-     *
-     * @return void
-     */
-    public function testSubCommandsPluginDuplicateApp()
-    {
-        $this->Shell->runCommand(['subcommands', 'TestPlugin.sample']);
-        $output = $this->out->output();
-
-        $expected = "example";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that subcommands without arguments returns nothing
-     *
-     * @return void
-     */
-    public function testSubCommandsNoArguments()
-    {
-        $this->Shell->runCommand(['subcommands']);
-        $output = $this->out->output();
-
-        $expected = '';
-        $this->assertEquals($expected, $output);
-    }
-
-    /**
-     * test that subcommands with a non-existing command returns nothing
-     *
-     * @return void
-     */
-    public function testSubCommandsNonExistingCommand()
-    {
-        $this->Shell->runCommand(['subcommands', 'foo']);
-        $output = $this->out->output();
-
-        $expected = '';
-        $this->assertEquals($expected, $output);
-    }
-
-    /**
-     * test that subcommands returns the available subcommands for the given command
-     *
-     * @return void
-     */
-    public function testSubCommands()
-    {
-        $this->Shell->runCommand(['subcommands', 'schema_cache']);
-        $output = $this->out->output();
-
-        $expected = "build clear";
-        $this->assertTextEquals($expected, $output);
-    }
-
-    /**
-     * test that fuzzy returns nothing
-     *
-     * @return void
-     */
-    public function testFuzzy()
-    {
-        $this->Shell->runCommand(['fuzzy']);
-        $output = $this->out->output();
-
-        $expected = '';
-        $this->assertEquals($expected, $output);
-    }
-}

+ 8 - 0
tests/test_app/TestApp/Application.php

@@ -17,6 +17,7 @@ declare(strict_types=1);
 namespace TestApp;
 
 use Cake\Console\CommandCollection;
+use Cake\Core\Configure;
 use Cake\Http\BaseApplication;
 use Cake\Http\MiddlewareQueue;
 use Cake\Routing\Middleware\RoutingMiddleware;
@@ -31,6 +32,13 @@ class Application extends BaseApplication
     public function bootstrap(): void
     {
         parent::bootstrap();
+
+        // Load plugins defined in Configure.
+        if (Configure::check('Plugins.autoload')) {
+            foreach (Configure::read('Plugins.autoload') as $value) {
+                $this->addPlugin($value);
+            }
+        }
     }
 
     /**