Browse Source

Rewrite completion shell into a command.

Convert the completion shell into a command. I've not made it multiple
commands as all modes of the completion shell share the same options and
help information.

The CommandTask is still in use by ShellDispatcher but I've removed all
the methods that are no longer in use.
Mark Story 7 years ago
parent
commit
a4e221170b

+ 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;
-    }
 }

+ 124 - 46
tests/TestCase/Shell/CompletionShellTest.php

@@ -14,15 +14,17 @@ declare(strict_types=1);
  * @since         2.5.0
  * @license       https://opensource.org/licenses/mit-license.php MIT License
  */
-namespace Cake\Test\TestCase\Shell;
+namespace Cake\Test\TestCase\Command;
 
 use Cake\Console\Shell;
+use Cake\Core\Configure;
+use Cake\Routing\Router;
 use Cake\TestSuite\ConsoleIntegrationTestCase;
 
 /**
- * CompletionShellTest
+ * CompletionCommandTest
  */
-class CompletionShellTest extends ConsoleIntegrationTestCase
+class CompletionCommandTest extends ConsoleIntegrationTestCase
 {
     /**
      * setUp method
@@ -33,7 +35,9 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
     {
         parent::setUp();
         static::setAppNamespace();
-        $this->loadPlugins(['TestPlugin', 'TestPluginTwo']);
+        Configure::write('Plugins.autoload', ['TestPlugin', 'TestPluginTwo']);
+
+        $this->useCommandRunner();
     }
 
     /**
@@ -44,7 +48,7 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
     public function tearDown(): void
     {
         parent::tearDown();
-        static::setAppNamespace('App');
+        Router::reload();
         $this->clearPlugins();
     }
 
@@ -56,42 +60,49 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
     public function testStartup()
     {
         $this->exec('completion');
-        $this->assertExitCode(Shell::CODE_SUCCESS);
+        $this->assertExitCode(Shell::CODE_ERROR);
 
         $this->assertOutputNotContains('Welcome to CakePHP');
     }
 
     /**
-     * test that main displays a warning
-     *
-     * @return void
-     */
-    public function testMain()
-    {
-        $this->exec('completion');
-        $this->assertExitCode(Shell::CODE_SUCCESS);
-
-        $expected = 'This command is not intended to be called manually';
-        $this->assertOutputContains($expected);
-    }
-
-    /**
      * test commands method that list all available commands
      *
      * @return void
      */
     public function testCommands()
     {
-        $this->markTestIncomplete('pending rebuild');
         $this->exec('completion commands');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
-        // 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->assertOutputContains($expected);
+        $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);
+        }
     }
 
     /**
@@ -123,13 +134,20 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      *
      * @return void
      */
-    public function testOptions()
+    public function testOptionsShell()
     {
         $this->exec('completion options schema_cache');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
-        $expected = "--connection -c --help -h --quiet -q --verbose -v";
-        $this->assertOutputContains($expected);
+        $expected = [
+            '--connection -c',
+            '--help -h',
+            '--quiet -q',
+            '--verbose -v',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
     }
 
     /**
@@ -137,13 +155,40 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      *
      * @return void
      */
-    public function testOptionsTask()
+    public function testOptionsShellTask()
     {
         $this->exec('completion options sample sample');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
-        $expected = "--help -h --quiet -q --sample -s --verbose -v";
-        $this->assertOutputContains($expected);
+        $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);
+        }
     }
 
     /**
@@ -153,7 +198,7 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      */
     public function testSubCommandsCorePlugin()
     {
-        $this->exec('completion subcommands CORE.schema_cache');
+        $this->exec('completion subcommands schema_cache');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
         $expected = "build clear";
@@ -167,11 +212,19 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      */
     public function testSubCommandsAppPlugin()
     {
-        $this->exec('completion subcommands app.sample');
+        $this->exec('completion subcommands sample');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
-        $expected = "derp load returnValue sample withAbort";
-        $this->assertOutputContains($expected);
+        $expected = [
+            'derp',
+            'load',
+            'returnValue',
+            'sample',
+            'withAbort',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
     }
 
     /**
@@ -181,12 +234,15 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      */
     public function testSubCommandsCoreMultiwordCommand()
     {
-        $this->markTestIncomplete();
-        $this->exec('completion subcommands CORE.cache');
+        $this->exec('completion subcommands cache');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
-        $expected = "list clear clearall";
-        $this->assertOutputContains($expected);
+        $expected = [
+            'list', 'clear', 'clearall',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
     }
 
     /**
@@ -211,7 +267,7 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      */
     public function testSubCommandsPluginDotNotationBackwardCompatibility()
     {
-        $this->exec('completion subcommands TestPluginTwo.welcome');
+        $this->exec('completion subcommands test_plugin_two.welcome');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
         $expected = "say_hello";
@@ -225,7 +281,7 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      */
     public function testSubCommandsPluginDotNotation()
     {
-        $this->exec('completion subcommands TestPluginTwo.example');
+        $this->exec('completion subcommands test_plugin_two.example');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
         $expected = "say_hello";
@@ -243,8 +299,16 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
         $this->exec('completion subcommands sample');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
-        $expected = "derp load returnValue sample withAbort";
-        $this->assertOutputContains($expected);
+        $expected = [
+            'derp',
+            'load',
+            'returnValue',
+            'sample',
+            'withAbort',
+        ];
+        foreach ($expected as $value) {
+            $this->assertOutputContains($value);
+        }
     }
 
     /**
@@ -254,7 +318,7 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
      */
     public function testSubCommandsPluginDuplicateApp()
     {
-        $this->exec('completion subcommands TestPlugin.sample');
+        $this->exec('completion subcommands test_plugin.sample');
         $this->assertExitCode(Shell::CODE_SUCCESS);
 
         $expected = "example";
@@ -311,4 +375,18 @@ class CompletionShellTest extends ConsoleIntegrationTestCase
         $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');
+    }
 }

+ 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);
+            }
+        }
     }
 
     /**