use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;
+use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Tester\ApplicationTester;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\Console\Exception\CommandNotFoundException;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\EventDispatcher;
class ApplicationTest extends TestCase
require_once self::$fixturesPath.'/Foo3Command.php';
require_once self::$fixturesPath.'/Foo4Command.php';
require_once self::$fixturesPath.'/Foo5Command.php';
+ require_once self::$fixturesPath.'/FooSameCaseUppercaseCommand.php';
+ require_once self::$fixturesPath.'/FooSameCaseLowercaseCommand.php';
require_once self::$fixturesPath.'/FoobarCommand.php';
require_once self::$fixturesPath.'/BarBucCommand.php';
require_once self::$fixturesPath.'/FooSubnamespaced1Command.php';
require_once self::$fixturesPath.'/FooSubnamespaced2Command.php';
+ require_once self::$fixturesPath.'/TestTiti.php';
+ require_once self::$fixturesPath.'/TestToto.php';
}
protected function normalizeLineBreaks($text)
$this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
}
+ public function testAllWithCommandLoader()
+ {
+ $application = new Application();
+ $commands = $application->all();
+ $this->assertInstanceOf('Symfony\\Component\\Console\\Command\\HelpCommand', $commands['help'], '->all() returns the registered commands');
+
+ $application->add(new \FooCommand());
+ $commands = $application->all('foo');
+ $this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
+
+ $application->setCommandLoader(new FactoryCommandLoader(array(
+ 'foo:bar1' => function () { return new \Foo1Command(); },
+ )));
+ $commands = $application->all('foo');
+ $this->assertCount(2, $commands, '->all() takes a namespace as its first argument');
+ $this->assertInstanceOf(\FooCommand::class, $commands['foo:bar'], '->all() returns the registered commands');
+ $this->assertInstanceOf(\Foo1Command::class, $commands['foo:bar1'], '->all() returns the registered commands');
+ }
+
public function testRegister()
{
$application = new Application();
$this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $command, '->get() returns the help command if --help is provided as the input');
}
+ public function testHasGetWithCommandLoader()
+ {
+ $application = new Application();
+ $this->assertTrue($application->has('list'), '->has() returns true if a named command is registered');
+ $this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered');
+
+ $application->add($foo = new \FooCommand());
+ $this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered');
+ $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name');
+ $this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias');
+
+ $application->setCommandLoader(new FactoryCommandLoader(array(
+ 'foo:bar1' => function () { return new \Foo1Command(); },
+ )));
+
+ $this->assertTrue($application->has('afoobar'), '->has() returns true if an instance is registered for an alias even with command loader');
+ $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns an instance by name even with command loader');
+ $this->assertEquals($foo, $application->get('afoobar'), '->get() returns an instance by alias even with command loader');
+ $this->assertTrue($application->has('foo:bar1'), '->has() returns true for commands registered in the loader');
+ $this->assertInstanceOf(\Foo1Command::class, $foo1 = $application->get('foo:bar1'), '->get() returns a command by name from the command loader');
+ $this->assertTrue($application->has('afoobar1'), '->has() returns true for commands registered in the loader');
+ $this->assertEquals($foo1, $application->get('afoobar1'), '->get() returns a command by name from the command loader');
+ }
+
public function testSilentHelp()
{
$application = new Application();
$this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns commands even if the commands are only contained in subnamespaces');
}
- /**
- * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
- * @expectedExceptionMessage The namespace "f" is ambiguous (foo, foo1).
- */
public function testFindAmbiguousNamespace()
{
$application = new Application();
$application->add(new \BarBucCommand());
$application->add(new \FooCommand());
$application->add(new \Foo2Command());
+
+ $expectedMsg = "The namespace \"f\" is ambiguous.\nDid you mean one of these?\n foo\n foo1";
+
+ if (method_exists($this, 'expectException')) {
+ $this->expectException(CommandNotFoundException::class);
+ $this->expectExceptionMessage($expectedMsg);
+ } else {
+ $this->setExpectedException(CommandNotFoundException::class, $expectedMsg);
+ }
+
$application->findNamespace('f');
}
+ public function testFindNonAmbiguous()
+ {
+ $application = new Application();
+ $application->add(new \TestTiti());
+ $application->add(new \TestToto());
+ $this->assertEquals('test-toto', $application->find('test')->getName());
+ }
+
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
* @expectedExceptionMessage There are no commands defined in the "bar" namespace.
$this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias');
}
+ public function testFindCaseSensitiveFirst()
+ {
+ $application = new Application();
+ $application->add(new \FooSameCaseUppercaseCommand());
+ $application->add(new \FooSameCaseLowercaseCommand());
+
+ $this->assertInstanceOf('FooSameCaseUppercaseCommand', $application->find('f:B'), '->find() returns a command if the abbreviation is the correct case');
+ $this->assertInstanceOf('FooSameCaseUppercaseCommand', $application->find('f:BAR'), '->find() returns a command if the abbreviation is the correct case');
+ $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:b'), '->find() returns a command if the abbreviation is the correct case');
+ $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:bar'), '->find() returns a command if the abbreviation is the correct case');
+ }
+
+ public function testFindCaseInsensitiveAsFallback()
+ {
+ $application = new Application();
+ $application->add(new \FooSameCaseLowercaseCommand());
+
+ $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:b'), '->find() returns a command if the abbreviation is the correct case');
+ $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:B'), '->find() will fallback to case insensitivity');
+ $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('FoO:BaR'), '->find() will fallback to case insensitivity');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
+ * @expectedExceptionMessage Command "FoO:BaR" is ambiguous
+ */
+ public function testFindCaseInsensitiveSuggestions()
+ {
+ $application = new Application();
+ $application->add(new \FooSameCaseLowercaseCommand());
+ $application->add(new \FooSameCaseUppercaseCommand());
+
+ $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('FoO:BaR'), '->find() will find two suggestions with case insensitivity');
+ }
+
+ public function testFindWithCommandLoader()
+ {
+ $application = new Application();
+ $application->setCommandLoader(new FactoryCommandLoader(array(
+ 'foo:bar' => $f = function () { return new \FooCommand(); },
+ )));
+
+ $this->assertInstanceOf('FooCommand', $application->find('foo:bar'), '->find() returns a command if its name exists');
+ $this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $application->find('h'), '->find() returns a command if its name exists');
+ $this->assertInstanceOf('FooCommand', $application->find('f:bar'), '->find() returns a command if the abbreviation for the namespace exists');
+ $this->assertInstanceOf('FooCommand', $application->find('f:b'), '->find() returns a command if the abbreviation for the namespace and the command name exist');
+ $this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias');
+ }
+
/**
* @dataProvider provideAmbiguousAbbreviations
*/
{
return array(
array('f', 'Command "f" is not defined.'),
- array('a', 'Command "a" is ambiguous (afoobar, afoobar1 and 1 more).'),
- array('foo:b', 'Command "foo:b" is ambiguous (foo:bar, foo:bar1 and 1 more).'),
+ array(
+ 'a',
+ "Command \"a\" is ambiguous.\nDid you mean one of these?\n".
+ " afoobar The foo:bar command\n".
+ " afoobar1 The foo:bar1 command\n".
+ ' afoobar2 The foo1:bar command',
+ ),
+ array(
+ 'foo:b',
+ "Command \"foo:b\" is ambiguous.\nDid you mean one of these?\n".
+ " foo:bar The foo:bar command\n".
+ " foo:bar1 The foo:bar1 command\n".
+ ' foo1:bar The foo1:bar command',
+ ),
);
}
public function provideInvalidCommandNamesSingle()
{
return array(
- array('foo3:baR'),
- array('foO3:bar'),
+ array('foo3:barr'),
+ array('fooo3:bar'),
);
}
}
}
+ public function testFindAlternativesOutput()
+ {
+ $application = new Application();
+
+ $application->add(new \FooCommand());
+ $application->add(new \Foo1Command());
+ $application->add(new \Foo2Command());
+ $application->add(new \Foo3Command());
+
+ $expectedAlternatives = array(
+ 'afoobar',
+ 'afoobar1',
+ 'afoobar2',
+ 'foo1:bar',
+ 'foo3:bar',
+ 'foo:bar',
+ 'foo:bar1',
+ );
+
+ try {
+ $application->find('foo');
+ $this->fail('->find() throws a CommandNotFoundException if command is not defined');
+ } catch (\Exception $e) {
+ $this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if command is not defined');
+ $this->assertSame($expectedAlternatives, $e->getAlternatives());
+
+ $this->assertRegExp('/Command "foo" is not defined\..*Did you mean one of these\?.*/Ums', $e->getMessage());
+ }
+ }
+
public function testFindNamespaceDoesNotFailOnDeepSimilarNamespaces()
{
$application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getNamespaces'))->getMock();
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
- $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
+ $this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
$tester->run(array('command' => 'foo'), array('decorated' => true, 'capture_stderr_separately' => true));
- $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
+ $this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
$application = new Application();
$application->setAutoExit(false);
});
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
- $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getErrorOutput(true), '->renderException() wraps messages when they are bigger than the terminal');
+ $this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getErrorOutput(true), '->renderException() wraps messages when they are bigger than the terminal');
putenv('COLUMNS=120');
}
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'), array('decorated' => false));
- $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_escapeslines.txt', $tester->getDisplay(true), '->renderException() escapes lines containing formatting');
+ $this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_renderexception_escapeslines.txt', $tester->getDisplay(true), '->renderException() escapes lines containing formatting');
putenv('COLUMNS=120');
}
+ public function testRenderExceptionLineBreaks()
+ {
+ $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock();
+ $application->setAutoExit(false);
+ $application->expects($this->any())
+ ->method('getTerminalWidth')
+ ->will($this->returnValue(120));
+ $application->register('foo')->setCode(function () {
+ throw new \InvalidArgumentException("\n\nline 1 with extra spaces \nline 2\n\nline 4\n");
+ });
+ $tester = new ApplicationTester($application);
+
+ $tester->run(array('command' => 'foo'), array('decorated' => false));
+ $this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_renderexception_linebreaks.txt', $tester->getDisplay(true), '->renderException() keep multiple line breaks');
+ }
+
public function testRun()
{
$application = new Application();
/**
* @expectedException \LogicException
- * @expectedExceptionMessage caught
+ * @expectedExceptionMessage error
*/
public function testRunWithExceptionAndDispatcher()
{
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'));
- $this->assertContains('before.foo.caught.after.', $tester->getDisplay());
+ $this->assertContains('before.foo.error.after.', $tester->getDisplay());
}
public function testRunDispatchesAllEventsWithExceptionInListener()
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'));
- $this->assertContains('before.caught.after.', $tester->getDisplay());
+ $this->assertContains('before.error.after.', $tester->getDisplay());
}
+ /**
+ * @requires PHP 7
+ */
public function testRunWithError()
{
$application = new Application();
}
}
+ public function testRunAllowsErrorListenersToSilenceTheException()
+ {
+ $dispatcher = $this->getDispatcher();
+ $dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) {
+ $event->getOutput()->write('silenced.');
+
+ $event->setExitCode(0);
+ });
+
+ $dispatcher->addListener('console.command', function () {
+ throw new \RuntimeException('foo');
+ });
+
+ $application = new Application();
+ $application->setDispatcher($dispatcher);
+ $application->setAutoExit(false);
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $output->write('foo.');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ $this->assertContains('before.error.silenced.after.', $tester->getDisplay());
+ $this->assertEquals(ConsoleCommandEvent::RETURN_CODE_DISABLED, $tester->getStatusCode());
+ }
+
+ public function testConsoleErrorEventIsTriggeredOnCommandNotFound()
+ {
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) {
+ $this->assertNull($event->getCommand());
+ $this->assertInstanceOf(CommandNotFoundException::class, $event->getError());
+ $event->getOutput()->write('silenced command not found');
+ });
+
+ $application = new Application();
+ $application->setDispatcher($dispatcher);
+ $application->setAutoExit(false);
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'unknown'));
+ $this->assertContains('silenced command not found', $tester->getDisplay());
+ $this->assertEquals(1, $tester->getStatusCode());
+ }
+
+ /**
+ * @group legacy
+ * @expectedDeprecation The "ConsoleEvents::EXCEPTION" event is deprecated since Symfony 3.3 and will be removed in 4.0. Listen to the "ConsoleEvents::ERROR" event instead.
+ */
+ public function testLegacyExceptionListenersAreStillTriggered()
+ {
+ $dispatcher = $this->getDispatcher();
+ $dispatcher->addListener('console.exception', function (ConsoleExceptionEvent $event) {
+ $event->getOutput()->write('caught.');
+
+ $event->setException(new \RuntimeException('replaced in caught.'));
+ });
+
+ $application = new Application();
+ $application->setDispatcher($dispatcher);
+ $application->setAutoExit(false);
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ throw new \RuntimeException('foo');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ $this->assertContains('before.caught.error.after.', $tester->getDisplay());
+ $this->assertContains('replaced in caught.', $tester->getDisplay());
+ }
+
+ /**
+ * @requires PHP 7
+ */
+ public function testErrorIsRethrownIfNotHandledByConsoleErrorEvent()
+ {
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+ $application->setDispatcher(new EventDispatcher());
+
+ $application->register('dym')->setCode(function (InputInterface $input, OutputInterface $output) {
+ new \UnknownClass();
+ });
+
+ $tester = new ApplicationTester($application);
+
+ try {
+ $tester->run(array('command' => 'dym'));
+ $this->fail('->run() should rethrow PHP errors if not handled via ConsoleErrorEvent.');
+ } catch (\Error $e) {
+ $this->assertSame($e->getMessage(), 'Class \'UnknownClass\' not found');
+ }
+ }
+
/**
+ * @requires PHP 7
* @expectedException \LogicException
- * @expectedExceptionMessage caught
+ * @expectedExceptionMessage error
*/
public function testRunWithErrorAndDispatcher()
{
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'dym'));
- $this->assertContains('before.dym.caught.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
+ $this->assertContains('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
}
+ /**
+ * @requires PHP 7
+ */
public function testRunDispatchesAllEventsWithError()
{
$application = new Application();
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'dym'));
- $this->assertContains('before.dym.caught.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
+ $this->assertContains('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
}
+ /**
+ * @requires PHP 7
+ */
public function testRunWithErrorFailingStatusCode()
{
$application = new Application();
$this->assertEquals($tester->getInput()->isInteractive(), @posix_isatty($inputStream));
}
+ public function testRunLazyCommandService()
+ {
+ $container = new ContainerBuilder();
+ $container->addCompilerPass(new AddConsoleCommandPass());
+ $container
+ ->register('lazy-command', LazyCommand::class)
+ ->addTag('console.command', array('command' => 'lazy:command'))
+ ->addTag('console.command', array('command' => 'lazy:alias'))
+ ->addTag('console.command', array('command' => 'lazy:alias2'));
+ $container->compile();
+
+ $application = new Application();
+ $application->setCommandLoader($container->get('console.command_loader'));
+ $application->setAutoExit(false);
+
+ $tester = new ApplicationTester($application);
+
+ $tester->run(array('command' => 'lazy:command'));
+ $this->assertSame("lazy-command called\n", $tester->getDisplay(true));
+
+ $tester->run(array('command' => 'lazy:alias'));
+ $this->assertSame("lazy-command called\n", $tester->getDisplay(true));
+
+ $tester->run(array('command' => 'lazy:alias2'));
+ $this->assertSame("lazy-command called\n", $tester->getDisplay(true));
+
+ $command = $application->get('lazy:command');
+ $this->assertSame(array('lazy:alias', 'lazy:alias2'), $command->getAliases());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
+ */
+ public function testGetDisabledLazyCommand()
+ {
+ $application = new Application();
+ $application->setCommandLoader(new FactoryCommandLoader(array('disabled' => function () { return new DisabledCommand(); })));
+ $application->get('disabled');
+ }
+
+ public function testHasReturnsFalseForDisabledLazyCommand()
+ {
+ $application = new Application();
+ $application->setCommandLoader(new FactoryCommandLoader(array('disabled' => function () { return new DisabledCommand(); })));
+ $this->assertFalse($application->has('disabled'));
+ }
+
+ public function testAllExcludesDisabledLazyCommand()
+ {
+ $application = new Application();
+ $application->setCommandLoader(new FactoryCommandLoader(array('disabled' => function () { return new DisabledCommand(); })));
+ $this->assertArrayNotHasKey('disabled', $application->all());
+ }
+
protected function getDispatcher($skipCommand = false)
{
$dispatcher = new EventDispatcher();
$event->setExitCode(ConsoleCommandEvent::RETURN_CODE_DISABLED);
}
});
- $dispatcher->addListener('console.exception', function (ConsoleExceptionEvent $event) {
- $event->getOutput()->write('caught.');
+ $dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) {
+ $event->getOutput()->write('error.');
- $event->setException(new \LogicException('caught.', $event->getExitCode(), $event->getException()));
+ $event->setError(new \LogicException('error.', $event->getExitCode(), $event->getError()));
});
return $dispatcher;
$this->assertSame($e->getMessage(), 'Class \'UnknownClass\' not found');
}
}
+
+ protected function tearDown()
+ {
+ putenv('SHELL_VERBOSITY');
+ unset($_ENV['SHELL_VERBOSITY']);
+ unset($_SERVER['SHELL_VERBOSITY']);
+ }
}
class CustomApplication extends Application
$this->setDefaultCommand($command->getName());
}
}
+
+class LazyCommand extends Command
+{
+ public function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('lazy-command called');
+ }
+}
+
+class DisabledCommand extends Command
+{
+ public function isEnabled()
+ {
+ return false;
+ }
+}