Browse Source

Merge pull request #10804 from cakephp/3next-command-runner

3.next - Start CommandRunner
Mark Story 8 years ago
parent
commit
443b7a798c

+ 127 - 0
src/Console/CommandRunner.php

@@ -0,0 +1,127 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.5.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Console;
+
+use Cake\Console\CommandCollection;
+use Cake\Console\ConsoleIo;
+use Cake\Console\Exception\StopException;
+use Cake\Console\Shell;
+use Cake\Http\BaseApplication;
+use RuntimeException;
+
+/**
+ * Run CLI commands for the provided application.
+ */
+class CommandRunner
+{
+    /**
+     * The application console commands are being run for.
+     *
+     * @var \Cake\Http\BaseApplication
+     */
+    protected $app;
+
+    /**
+     * The root command name. Defaults to `cake`.
+     *
+     * @var string
+     */
+    protected $root;
+
+    /**
+     * Constructor
+     *
+     * @param \Cake\Http\BaseApplication $app The application to run CLI commands for.
+     * @param string $root The root command name to be removed from argv.
+     */
+    public function __construct(BaseApplication $app, $root = 'cake')
+    {
+        $this->app = $app;
+        $this->root = $root;
+    }
+
+    /**
+     * Run the command contained in $argv.
+     *
+     * @param array $argv The arguments from the CLI environment.
+     * @param \Cake\Console\ConsoleIo $io The ConsoleIo instance. Used primarily for testing.
+     * @return int The exit code of the command.
+     * @throws \RuntimeException
+     */
+    public function run(array $argv, ConsoleIo $io = null)
+    {
+        $this->app->bootstrap();
+
+        $commands = $this->app->console(new CommandCollection());
+        if (!($commands instanceof CommandCollection)) {
+            $type = is_object($commands) ? get_class($commands) : gettype($commands);
+            throw new RuntimeException(
+                "The application's `console` method did not return a CommandCollection." .
+                " Got '{$type}' instead."
+            );
+        }
+        if (empty($argv) || $argv[0] !== $this->root) {
+            $command = empty($argv) ? '' : " `{$argv[0]}`";
+            throw new RuntimeException(
+                "Unknown root command{$command}. Was expecting `{$this->root}`."
+            );
+        }
+        // Remove the root executable segment
+        array_shift($argv);
+
+        $io = $io ?: new ConsoleIo();
+        $shell = $this->getShell($io, $commands, array_shift($argv));
+
+        try {
+            $shell->initialize();
+            $result = $shell->runCommand($argv, true);
+        } catch (StopException $e) {
+            return $e->getCode();
+        }
+
+        if ($result === null || $result === true) {
+            return Shell::CODE_SUCCESS;
+        }
+        if (is_int($result)) {
+            return $result;
+        }
+
+        return Shell::CODE_ERROR;
+    }
+
+    /**
+     * Get the shell instance for a given command name
+     *
+     * @param \Cake\Console\ConsoleIo $io The io wrapper for the created shell class.
+     * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in.
+     * @param string $name The command name to find
+     * @return \Cake\Console\Shell
+     */
+    protected function getShell(ConsoleIo $io, CommandCollection $commands, $name)
+    {
+        if (!$commands->has($name)) {
+            throw new RuntimeException(
+                "Unknown command `{$this->root} {$name}`." .
+                " Run `{$this->root} --help` to get the list of valid commands."
+            );
+        }
+        $classOrInstance = $commands->get($name);
+        if (is_string($classOrInstance)) {
+            return new $classOrInstance($io);
+        }
+
+        return $classOrInstance;
+    }
+}

+ 14 - 0
src/Http/BaseApplication.php

@@ -75,6 +75,20 @@ abstract class BaseApplication
     }
 
     /**
+     * Define the console commands for an application.
+     *
+     * By default all commands in CakePHP, plugins and the application will be
+     * loaded using conventions based names.
+     *
+     * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
+     * @return \Cake\Console\CommandCollection The updated collection.
+     */
+    public function console($commands)
+    {
+        return $commands->addMany($commands->autoDiscover());
+    }
+
+    /**
      * Invoke the application.
      *
      * - Convert the PSR response into CakePHP equivalents.

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

@@ -0,0 +1,232 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.5.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\Console;
+
+use Cake\Console\CommandCollection;
+use Cake\Console\CommandRunner;
+use Cake\Console\ConsoleIo;
+use Cake\Console\Shell;
+use Cake\Core\Configure;
+use Cake\Http\BaseApplication;
+use Cake\TestSuite\Stub\ConsoleOutput;
+use Cake\TestSuite\TestCase;
+use TestApp\Shell\SampleShell;
+
+/**
+ * Test case for the CommandCollection
+ */
+class CommandRunnerTest extends TestCase
+{
+    /**
+     * setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        Configure::write('App.namespace', 'TestApp');
+        $this->config = dirname(dirname(__DIR__));
+    }
+
+    /**
+     * Test that the console hook not returning a command collection
+     * raises an error.
+     *
+     * @expectedException \RuntimeException
+     * @expectedExceptionMessage The application's `console` method did not return a CommandCollection.
+     * @return void
+     */
+    public function testRunConsoleHookFailure()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['console', 'middleware', 'bootstrap'])
+            ->setConstructorArgs([$this->config])
+            ->getMock();
+        $runner = new CommandRunner($app);
+        $runner->run(['cake', '-h']);
+    }
+
+    /**
+     * Test that running with empty argv fails
+     *
+     * @expectedException \RuntimeException
+     * @expectedExceptionMessage Unknown root command. Was expecting `cake`
+     * @return void
+     */
+    public function testRunMissingRootCommand()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['middleware', 'bootstrap'])
+            ->setConstructorArgs([$this->config])
+            ->getMock();
+
+        $runner = new CommandRunner($app);
+        $runner->run([]);
+    }
+
+    /**
+     * Test that running an unknown command raises an error.
+     *
+     * @expectedException \RuntimeException
+     * @expectedExceptionMessage Unknown root command `bad`. Was expecting `cake`
+     * @return void
+     */
+    public function testRunInvalidRootCommand()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['middleware', 'bootstrap'])
+            ->setConstructorArgs([$this->config])
+            ->getMock();
+
+        $runner = new CommandRunner($app);
+        $runner->run(['bad', 'i18n']);
+    }
+
+    /**
+     * Test that running an unknown command raises an error.
+     *
+     * @expectedException \RuntimeException
+     * @expectedExceptionMessage Unknown command `cake nope`. Run `cake --help` to get the list of valid commands.
+     * @return void
+     */
+    public function testRunInvalidCommand()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['middleware', 'bootstrap'])
+            ->setConstructorArgs([$this->config])
+            ->getMock();
+
+        $runner = new CommandRunner($app);
+        $runner->run(['cake', 'nope', 'nope', 'nope']);
+    }
+
+    /**
+     * Test using `cake --help` invokes the help command
+     *
+     * @return void
+     */
+    public function testRunHelpLongOption()
+    {
+        $this->markTestIncomplete();
+    }
+
+    /**
+     * Test using `cake -h` invokes the help command
+     *
+     * @return void
+     */
+    public function testRunHelpShortOption()
+    {
+        $this->markTestIncomplete();
+    }
+
+    /**
+     * Test using `cake --verson` invokes the version command
+     *
+     * @return void
+     */
+    public function testRunVersionLongOption()
+    {
+        $this->markTestIncomplete();
+    }
+
+    /**
+     * Test using `cake -v` invokes the version command
+     *
+     * @return void
+     */
+    public function testRunVersionShortOption()
+    {
+        $this->markTestIncomplete();
+    }
+
+    /**
+     * Test running a valid command
+     *
+     * @return void
+     */
+    public function testRunValidCommand()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['middleware', 'bootstrap'])
+            ->setConstructorArgs([$this->config])
+            ->getMock();
+
+        $output = new ConsoleOutput();
+
+        $runner = new CommandRunner($app, 'cake');
+        $result = $runner->run(['cake', 'routes'], $this->getMockIo($output));
+        $this->assertSame(Shell::CODE_SUCCESS, $result);
+
+        $contents = implode("\n", $output->messages());
+        $this->assertContains('URI template', $contents);
+        $this->assertContains('Welcome to CakePHP', $contents);
+    }
+
+    /**
+     * Test running a valid raising an error
+     *
+     * @return void
+     */
+    public function testRunValidCommandWithAbort()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['middleware', 'bootstrap', 'console'])
+            ->setConstructorArgs([$this->config])
+            ->getMock();
+
+        $commands = new CommandCollection(['failure' => SampleShell::class]);
+        $app->method('console')->will($this->returnValue($commands));
+
+        $output = new ConsoleOutput();
+
+        $runner = new CommandRunner($app, 'cake');
+        $result = $runner->run(['cake', 'failure', 'with_abort'], $this->getMockIo($output));
+        $this->assertSame(Shell::CODE_ERROR, $result);
+    }
+
+    /**
+     * Test returning a non-zero value
+     *
+     * @return void
+     */
+    public function testRunValidCommandReturnInteger()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['middleware', 'bootstrap', 'console'])
+            ->setConstructorArgs([$this->config])
+            ->getMock();
+
+        $commands = new CommandCollection(['failure' => SampleShell::class]);
+        $app->method('console')->will($this->returnValue($commands));
+
+        $output = new ConsoleOutput();
+
+        $runner = new CommandRunner($app, 'cake');
+        $result = $runner->run(['cake', 'failure', 'returnValue'], $this->getMockIo($output));
+        $this->assertSame(99, $result);
+    }
+
+    protected function getMockIo($output)
+    {
+        $io = $this->getMockBuilder(ConsoleIo::class)
+            ->setConstructorArgs([$output, $output, null, null])
+            ->setMethods(['in'])
+            ->getMock();
+
+        return $io;
+    }
+}

+ 2 - 2
tests/TestCase/Shell/CompletionShellTest.php

@@ -200,7 +200,7 @@ class CompletionShellTest extends TestCase
         $this->Shell->runCommand(['subcommands', 'app.sample']);
         $output = $this->out->output;
 
-        $expected = "derp load sample\n";
+        $expected = "derp load returnValue sample withAbort\n";
         $this->assertTextEquals($expected, $output);
     }
 
@@ -258,7 +258,7 @@ class CompletionShellTest extends TestCase
         $this->Shell->runCommand(['subcommands', 'sample']);
         $output = $this->out->output;
 
-        $expected = "derp load sample\n";
+        $expected = "derp load returnValue sample withAbort\n";
         $this->assertTextEquals($expected, $output);
     }
 

+ 10 - 0
tests/test_app/TestApp/Shell/SampleShell.php

@@ -46,4 +46,14 @@ class SampleShell extends Shell
     {
         $this->out('This is the example method called from TestPlugin.SampleShell');
     }
+
+    public function withAbort()
+    {
+        $this->abort('Bad things');
+    }
+
+    public function returnValue()
+    {
+        return 99;
+    }
 }