Skip to content

Commit

Permalink
Finish implementing tests for Command dispatching.
Browse files Browse the repository at this point in the history
Add complete coverage for command dispatching, and help generation.
  • Loading branch information
markstory committed Sep 30, 2017
1 parent a57945d commit 457f1b9
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 9 deletions.
100 changes: 92 additions & 8 deletions src/Console/Command.php
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -52,12 +68,20 @@ public function __construct()
* 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;
Expand All @@ -83,7 +107,10 @@ public function getName()
*/
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(
Expand Down Expand Up @@ -124,20 +151,77 @@ public function initialize()
* 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)
Expand Down
19 changes: 19 additions & 0 deletions tests/TestCase/Console/CommandRunnerTest.php
Expand Up @@ -297,6 +297,25 @@ public function testRunValidCommandClass()
$this->assertContains('Example Command!', $messages);
}

/**
* 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.
*
Expand Down
137 changes: 136 additions & 1 deletion tests/TestCase/Console/CommandTest.php
Expand Up @@ -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
Expand Down Expand Up @@ -61,6 +64,19 @@ public function testSetName()
$this->assertSame('routes show', $command->getName());
}

/**
* 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
*
Expand All @@ -72,7 +88,7 @@ public function testGetOptionParser()
$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());
}

/**
Expand All @@ -91,4 +107,123 @@ public function testGetOptionParserInvalid()
->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 changes: 2 additions & 0 deletions tests/test_app/TestApp/Command/ExampleCommand.php
Expand Up @@ -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!');
}
}

0 comments on commit 457f1b9

Please sign in to comment.