Browse Source

Finish implementing tests for Command dispatching.

Add complete coverage for command dispatching, and help generation.
Mark Story 8 years ago
parent
commit
457f1b93e2

+ 92 - 8
src/Console/Command.php

@@ -14,10 +14,12 @@
  */
 namespace Cake\Console;
 
+use Cake\Console\Exception\ConsoleException;
 use Cake\Datasource\ModelAwareTrait;
 use Cake\Log\LogTrait;
 use Cake\ORM\Locator\LocatorAwareTrait;
 use RuntimeException;
+use InvalidArgumentException;
 
 /**
  * Base class for console commands.
@@ -29,11 +31,25 @@ class Command
     use ModelAwareTrait;
 
     /**
-     * The name of this command. Inflected from the class name.
+     * Default error code
+     *
+     * @var int
+     */
+    const CODE_ERROR = 1;
+
+    /**
+     * Default success code
+     *
+     * @var int
+     */
+    const CODE_SUCCESS = 0;
+
+    /**
+     * The name of this command.
      *
      * @var string
      */
-    protected $name;
+    protected $name = 'cake unknown';
 
     /**
      * Constructor
@@ -52,12 +68,20 @@ class Command
      * Set the name this command uses in the collection.
      *
      * Generally invoked by the CommandCollection when the command is added.
+     * Required to have at least one space in the name so that the root
+     * command can be calculated.
      *
      * @param string $name The name the command uses in the collection.
      * @return $this;
+     * @throws \InvalidArgumentException
      */
     public function setName($name)
     {
+        if (strpos($name, ' ') === false) {
+            throw new InvalidArgumentException(
+                "The name '{$name}' is missing a space. Names should look like `cake routes`"
+            );
+        }
         $this->name = $name;
 
         return $this;
@@ -83,7 +107,10 @@ class Command
      */
     public function getOptionParser()
     {
-        $parser = new ConsoleOptionParser($this->name);
+        list($root, $name) = explode(' ', $this->name, 2);
+        $parser = new ConsoleOptionParser($name);
+        $parser->setRootName($root);
+
         $parser = $this->buildOptionParser($parser);
         if (!($parser instanceof ConsoleOptionParser)) {
             throw new RuntimeException(sprintf(
@@ -124,20 +151,77 @@ class Command
      * Run the command.
      *
      * @param array $argv
+     * @return int|null Exit code or null for success.
      */
     public function run(array $argv, ConsoleIo $io)
     {
-        // Initialize command
-        // Parse argv.
-        // If invalid, or help was requested show help
-        // execute the command
-        $args = new Arguments([], [], []);
+        $this->initialize();
+
+        $parser = $this->getOptionParser();
+        try {
+            list($options, $arguments) = $parser->parse($argv);
+            $args = new Arguments(
+                $arguments,
+                $options,
+                $parser->argumentNames()
+            );
+        } catch (ConsoleException $e) {
+            $io->err('Error: ' . $e->getMessage());
+
+            return static::CODE_ERROR;
+        }
+        $this->setOutputLevel($args, $io);
+
+        if ($args->getOption('help')) {
+            return $this->displayHelp($parser, $args, $io);
+        }
         return $this->execute($args, $io);
     }
 
     /**
+     * Output help content
+     *
+     * @param \Cake\Console\ConsoleOptionParser $parser The option parser.
+     * @param \Cake\Console\Arguments $args The command arguments.
+     * @param \Cake\Console\ConsoleIo $io The console io
+     * @return void
+     */
+    protected function displayHelp(ConsoleOptionParser $parser, Arguments $args, ConsoleIo $io)
+    {
+        $format = 'text';
+        if ($args->getArgumentAt(0) === 'xml') {
+            $format = 'xml';
+            $io->setOutputAs(ConsoleOutput::RAW);
+        }
+
+        return $io->out($parser->help(null, $format));
+    }
+
+    /**
+     * Set the output level based on the Arguments.
+     *
+     * @param \Cake\Console\Arguments $args The command arguments.
+     * @param \Cake\Console\ConsoleIo $io The console io
+     * @return void
+     */
+    protected function setOutputLevel(Arguments $args, ConsoleIo $io)
+    {
+        $io->setLoggers(ConsoleIo::NORMAL);
+        if ($args->getOption('quiet')) {
+            $io->level(ConsoleIo::QUIET);
+            $io->setLoggers(ConsoleIo::QUIET);
+        }
+        if ($args->getOption('verbose')) {
+            $io->level(ConsoleIo::VERBOSE);
+            $io->setLoggers(ConsoleIo::VERBOSE);
+        }
+    }
+
+    /**
      * Implement this method with your command's logic.
      *
+     * @param \Cake\Console\Arguments $args The command arguments.
+     * @param \Cake\Console\ConsoleIo $io The console io
      * @return null|int The exit code or null for success
      */
     public function execute(Arguments $args, ConsoleIo $io)

+ 19 - 0
tests/TestCase/Console/CommandRunnerTest.php

@@ -298,6 +298,25 @@ class CommandRunnerTest extends TestCase
     }
 
     /**
+     * Test running a command class' help
+     *
+     * @return void
+     */
+    public function testRunValidCommandClassHelp()
+    {
+        $app = $this->makeAppWithCommands(['ex' => ExampleCommand::class]);
+        $output = new ConsoleOutput();
+
+        $runner = new CommandRunner($app, 'cake');
+        $result = $runner->run(['cake', 'ex', '-h'], $this->getMockIo($output));
+        $this->assertSame(Shell::CODE_SUCCESS, $result);
+
+        $messages = implode("\n", $output->messages());
+        $this->assertContains("\ncake ex [-h]", $messages);
+        $this->assertNotContains('Example Command!', $messages);
+    }
+
+    /**
      * Test that run() fires off the buildCommands event.
      *
      * @return void

+ 136 - 1
tests/TestCase/Console/CommandTest.php

@@ -15,10 +15,13 @@
 namespace Cake\Test\TestCase\Console;
 
 use Cake\Console\Command;
+use Cake\Console\ConsoleIo;
 use Cake\Console\ConsoleOptionParser;
 use Cake\ORM\Locator\TableLocator;
 use Cake\ORM\Table;
+use Cake\TestSuite\Stub\ConsoleOutput;
 use Cake\TestSuite\TestCase;
+use TestApp\Command\ExampleCommand;
 
 /**
  * Test case for Console\Command
@@ -62,6 +65,19 @@ class CommandTest extends TestCase
     }
 
     /**
+     * Test invalid name
+     *
+     * @expectedException InvalidArgumentException
+     * @expectedExceptionMessage The name 'routes_show' is missing a space. Names should look like `cake routes`
+     * @return void
+     */
+    public function testSetNameInvalid()
+    {
+        $command = new Command();
+        $command->setName('routes_show');
+    }
+
+    /**
      * Test option parser fetching
      *
      * @return void
@@ -72,7 +88,7 @@ class CommandTest extends TestCase
         $command->setName('cake routes show');
         $parser = $command->getOptionParser();
         $this->assertInstanceOf(ConsoleOptionParser::class, $parser);
-        $this->assertSame('cake routes show', $parser->getCommand());
+        $this->assertSame('routes show', $parser->getCommand());
     }
 
     /**
@@ -91,4 +107,123 @@ class CommandTest extends TestCase
             ->will($this->returnValue(null));
         $command->getOptionParser();
     }
+
+    /**
+     * Test that initialize is called.
+     *
+     * @return void
+     */
+    public function testRunCallsInitialize()
+    {
+        $command = $this->getMockBuilder(Command::class)
+            ->setMethods(['initialize'])
+            ->getMock();
+        $command->setName('cake example');
+        $command->expects($this->once())->method('initialize');
+        $command->run([], $this->getMockIo(new ConsoleOutput()));
+    }
+
+    /**
+     * Test run() outputs help
+     *
+     * @return void
+     */
+    public function testRunOutputHelp()
+    {
+        $command = new Command();
+        $command->setName('cake example');
+        $output = new ConsoleOutput();
+
+        $this->assertNull($command->run(['-h'], $this->getMockIo($output)));
+        $messages = implode("\n", $output->messages());
+        $this->assertNotContains('Example', $messages);
+        $this->assertContains('cake example [-h]', $messages);
+    }
+
+    /**
+     * Test run() outputs help
+     *
+     * @return void
+     */
+    public function testRunOutputHelpLongOption()
+    {
+        $command = new Command();
+        $command->setName('cake example');
+        $output = new ConsoleOutput();
+
+        $this->assertNull($command->run(['--help'], $this->getMockIo($output)));
+        $messages = implode("\n", $output->messages());
+        $this->assertNotContains('Example', $messages);
+        $this->assertContains('cake example [-h]', $messages);
+    }
+
+    /**
+     * Test run() sets output level
+     *
+     * @return void
+     */
+    public function testRunVerboseOption()
+    {
+        $command = new ExampleCommand();
+        $command->setName('cake example');
+        $output = new ConsoleOutput();
+
+        $this->assertNull($command->run(['--verbose'], $this->getMockIo($output)));
+        $messages = implode("\n", $output->messages());
+        $this->assertContains('Verbose!', $messages);
+        $this->assertContains('Example Command!', $messages);
+        $this->assertContains('Quiet!', $messages);
+        $this->assertNotContains('cake example [-h]', $messages);
+    }
+
+    /**
+     * Test run() sets output level
+     *
+     * @return void
+     */
+    public function testRunQuietOption()
+    {
+        $command = new ExampleCommand();
+        $command->setName('cake example');
+        $output = new ConsoleOutput();
+
+        $this->assertNull($command->run(['--quiet'], $this->getMockIo($output)));
+        $messages = implode("\n", $output->messages());
+        $this->assertContains('Quiet!', $messages);
+        $this->assertNotContains('Verbose!', $messages);
+        $this->assertNotContains('Example Command!', $messages);
+    }
+
+    /**
+     * Test run() sets option parser failure
+     *
+     * @return void
+     */
+    public function testRunOptionParserFailure()
+    {
+        $command = $this->getMockBuilder(Command::class)
+            ->setMethods(['getOptionParser'])
+            ->getMock();
+        $parser = new ConsoleOptionParser('cake example');
+        $parser->addArgument('name', ['required' => true]);
+
+        $command->method('getOptionParser')->will($this->returnValue($parser));
+
+        $output = new ConsoleOutput();
+        $result = $command->run([], $this->getMockIo($output));
+        $this->assertSame(Command::CODE_ERROR, $result);
+
+        $messages = implode("\n", $output->messages());
+        $this->assertContains('Error: Missing required arguments. name is required', $messages);
+    }
+
+    protected function getMockIo($output)
+    {
+        $io = $this->getMockBuilder(ConsoleIo::class)
+            ->setConstructorArgs([$output, $output, null, null])
+            ->setMethods(['in'])
+            ->getMock();
+
+        return $io;
+    }
 }

+ 2 - 0
tests/test_app/TestApp/Command/ExampleCommand.php

@@ -9,6 +9,8 @@ class ExampleCommand extends Command
 {
     public function execute(Arguments $args, ConsoleIo $io)
     {
+        $io->quiet('Quiet!');
         $io->out('Example Command!');
+        $io->verbose('Verbose!');
     }
 }