bug #22435 [Console] Fix dispatching throwables from ConsoleEvents::COMMAND (nicolas-grekas)

This PR was merged into the 2.7 branch.

Discussion
----------

[Console] Fix dispatching throwables from ConsoleEvents::COMMAND

| Q             | A
| ------------- | ---
| Branch?       | 2.7
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

Exceptions thrown by `ConsoleEvents::COMMAND` listeners should be trigger `ConsoleEvents::EXCEPTION` events.
(best reviewed [ignoring whitespaces](https://github.com/symfony/symfony/pull/22435/files?w=1))

Commits
-------

6d6b04ae97 [Console] Fix dispatching throwables from ConsoleEvents::COMMAND
This commit is contained in:
Fabien Potencier 2017-04-25 10:03:21 -04:00
commit 81f48b80f3
2 changed files with 85 additions and 64 deletions

View File

@ -119,10 +119,17 @@ class Application
$this->configureIO($input, $output); $this->configureIO($input, $output);
try { try {
$e = null;
$exitCode = $this->doRun($input, $output); $exitCode = $this->doRun($input, $output);
} catch (\Exception $e) { } catch (\Exception $x) {
$e = $x;
} catch (\Throwable $x) {
$e = new FatalThrowableError($x);
}
if (null !== $e) {
if (!$this->catchExceptions) { if (!$this->catchExceptions) {
throw $e; throw $x;
} }
if ($output instanceof ConsoleOutputInterface) { if ($output instanceof ConsoleOutputInterface) {
@ -184,6 +191,7 @@ class Application
$input = new ArrayInput(array('command' => $this->defaultCommand)); $input = new ArrayInput(array('command' => $this->defaultCommand));
} }
$this->runningCommand = null;
// the command name MUST be the first element of the input // the command name MUST be the first element of the input
$command = $this->find($name); $command = $this->find($name);
@ -841,47 +849,41 @@ class Application
} }
if (null === $this->dispatcher) { if (null === $this->dispatcher) {
try { return $command->run($input, $output);
return $command->run($input, $output);
} catch (\Exception $e) {
throw $e;
} catch (\Throwable $e) {
throw new FatalThrowableError($e);
}
} }
$event = new ConsoleCommandEvent($command, $input, $output); $event = new ConsoleCommandEvent($command, $input, $output);
$this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event); $e = null;
if ($event->commandShouldRun()) { try {
try { $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
$e = null;
if ($event->commandShouldRun()) {
$exitCode = $command->run($input, $output); $exitCode = $command->run($input, $output);
} catch (\Exception $x) { } else {
$e = $x; $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
} catch (\Throwable $x) {
$e = new FatalThrowableError($x);
} }
if (null !== $e) { } catch (\Exception $e) {
$event = new ConsoleExceptionEvent($command, $input, $output, $e, $e->getCode()); } catch (\Throwable $e) {
$this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event); }
if (null !== $e) {
$x = $e instanceof \Exception ? $e : new FatalThrowableError($e);
$event = new ConsoleExceptionEvent($command, $input, $output, $x, $x->getCode());
$this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event);
if ($e !== $event->getException()) { if ($x !== $event->getException()) {
$x = $e = $event->getException(); $e = $event->getException();
}
$event = new ConsoleTerminateEvent($command, $input, $output, $e->getCode());
$this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
throw $x;
} }
} else { $exitCode = $e->getCode();
$exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
} }
$event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
$this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
if (null !== $e) {
throw $e;
}
return $event->getExitCode(); return $event->getExitCode();
} }

View File

@ -977,15 +977,28 @@ class ApplicationTest extends TestCase
$this->assertContains('before.foo.caught.after.', $tester->getDisplay()); $this->assertContains('before.foo.caught.after.', $tester->getDisplay());
} }
public function testRunDispatchesAllEventsWithExceptionInListener()
{
$dispatcher = $this->getDispatcher();
$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.caught.after.', $tester->getDisplay());
}
public function testRunWithError() public function testRunWithError()
{ {
if (method_exists($this, 'expectException')) {
$this->expectException('Exception');
$this->expectExceptionMessage('dymerr');
} else {
$this->setExpectedException('Exception', 'dymerr');
}
$application = new Application(); $application = new Application();
$application->setAutoExit(false); $application->setAutoExit(false);
$application->setCatchExceptions(false); $application->setCatchExceptions(false);
@ -997,7 +1010,13 @@ class ApplicationTest extends TestCase
}); });
$tester = new ApplicationTester($application); $tester = new ApplicationTester($application);
$tester->run(array('command' => 'dym'));
try {
$tester->run(array('command' => 'dym'));
$this->fail('Error expected.');
} catch (\Error $e) {
$this->assertSame('dymerr', $e->getMessage());
}
} }
/** /**
@ -1087,32 +1106,6 @@ class ApplicationTest extends TestCase
$this->assertSame(array($width, 80), $application->getTerminalDimensions()); $this->assertSame(array($width, 80), $application->getTerminalDimensions());
} }
protected function getDispatcher($skipCommand = false)
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener('console.command', function (ConsoleCommandEvent $event) use ($skipCommand) {
$event->getOutput()->write('before.');
if ($skipCommand) {
$event->disableCommand();
}
});
$dispatcher->addListener('console.terminate', function (ConsoleTerminateEvent $event) use ($skipCommand) {
$event->getOutput()->writeln('after.');
if (!$skipCommand) {
$event->setExitCode(113);
}
});
$dispatcher->addListener('console.exception', function (ConsoleExceptionEvent $event) {
$event->getOutput()->write('caught.');
$event->setException(new \LogicException('caught.', $event->getExitCode(), $event->getException()));
});
return $dispatcher;
}
public function testSetRunCustomDefaultCommand() public function testSetRunCustomDefaultCommand()
{ {
$command = new \FooCommand(); $command = new \FooCommand();
@ -1151,6 +1144,32 @@ class ApplicationTest extends TestCase
$inputStream = $application->getHelperSet()->get('question')->getInputStream(); $inputStream = $application->getHelperSet()->get('question')->getInputStream();
$this->assertEquals($tester->getInput()->isInteractive(), @posix_isatty($inputStream)); $this->assertEquals($tester->getInput()->isInteractive(), @posix_isatty($inputStream));
} }
protected function getDispatcher($skipCommand = false)
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener('console.command', function (ConsoleCommandEvent $event) use ($skipCommand) {
$event->getOutput()->write('before.');
if ($skipCommand) {
$event->disableCommand();
}
});
$dispatcher->addListener('console.terminate', function (ConsoleTerminateEvent $event) use ($skipCommand) {
$event->getOutput()->writeln('after.');
if (!$skipCommand) {
$event->setExitCode(ConsoleCommandEvent::RETURN_CODE_DISABLED);
}
});
$dispatcher->addListener('console.exception', function (ConsoleExceptionEvent $event) {
$event->getOutput()->write('caught.');
$event->setException(new \LogicException('caught.', $event->getExitCode(), $event->getException()));
});
return $dispatcher;
}
} }
class CustomApplication extends Application class CustomApplication extends Application