feature #17255 [Console][2.3] ApplicationTester - test stdout and stderr (SpacePossum)

This PR was submitted for the 2.3 branch but it was merged into the 3.1-dev branch instead (closes #17255).

Discussion
----------

[Console][2.3] ApplicationTester - test stdout and stderr

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| License       | MIT

Currently it is not possible to test application output of both `stdout` _and_ `stderr` using the `ApplicationTester`. This makes it hard to check if an application writes to the correct output.

Commits
-------

6ff6a28 [Console][2.3] ApplicationTester - test stdout and stderr
This commit is contained in:
Fabien Potencier 2016-03-10 20:34:54 +01:00
commit af36c8320d
3 changed files with 98 additions and 41 deletions

View File

@ -14,15 +14,16 @@ namespace Symfony\Component\Console\Output;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
/**
* ConsoleOutput is the default class for all CLI output. It uses STDOUT.
* ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR.
*
* This class is a convenient wrapper around `StreamOutput`.
* This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR.
*
* $output = new ConsoleOutput();
*
* This is equivalent to:
*
* $output = new StreamOutput(fopen('php://stdout', 'w'));
* $stdErr = new StreamOutput(fopen('php://stderr', 'w'));
*
* @author Fabien Potencier <fabien@symfony.com>
*/
@ -139,9 +140,11 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
*/
private function openOutputStream()
{
$outputStream = $this->hasStdoutSupport() ? 'php://stdout' : 'php://output';
if (!$this->hasStdoutSupport()) {
return fopen('php://output', 'w');
}
return @fopen($outputStream, 'w') ?: fopen('php://output', 'w');
return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
}
/**
@ -149,8 +152,6 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
*/
private function openErrorStream()
{
$errorStream = $this->hasStderrSupport() ? 'php://stderr' : 'php://output';
return fopen($errorStream, 'w');
return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w');
}
}

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Console\Tester;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
@ -31,14 +32,13 @@ class ApplicationTester
{
private $application;
private $input;
private $output;
private $statusCode;
/**
* Constructor.
*
* @param Application $application An Application instance to test.
* @var OutputInterface
*/
private $output;
private $captureStreamsIndependently = false;
public function __construct(Application $application)
{
$this->application = $application;
@ -49,9 +49,10 @@ class ApplicationTester
*
* Available options:
*
* * interactive: Sets the input interactive flag
* * decorated: Sets the output decorated flag
* * verbosity: Sets the output verbosity flag
* * interactive: Sets the input interactive flag
* * decorated: Sets the output decorated flag
* * verbosity: Sets the output verbosity flag
* * capture_stderr_separately: Make output of stdOut and stdErr separately available
*
* @param array $input An array of arguments and options
* @param array $options An array of options
@ -65,12 +66,35 @@ class ApplicationTester
$this->input->setInteractive($options['interactive']);
}
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
if (isset($options['decorated'])) {
$this->output->setDecorated($options['decorated']);
}
if (isset($options['verbosity'])) {
$this->output->setVerbosity($options['verbosity']);
$this->captureStreamsIndependently = array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately'];
if (!$this->captureStreamsIndependently) {
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
if (isset($options['decorated'])) {
$this->output->setDecorated($options['decorated']);
}
if (isset($options['verbosity'])) {
$this->output->setVerbosity($options['verbosity']);
}
} else {
$this->output = new ConsoleOutput(
isset($options['verbosity']) ? $options['verbosity'] : ConsoleOutput::VERBOSITY_NORMAL,
isset($options['decorated']) ? $options['decorated'] : null
);
$errorOutput = new StreamOutput(fopen('php://memory', 'w', false));
$errorOutput->setFormatter($this->output->getFormatter());
$errorOutput->setVerbosity($this->output->getVerbosity());
$errorOutput->setDecorated($this->output->isDecorated());
$reflectedOutput = new \ReflectionObject($this->output);
$strErrProperty = $reflectedOutput->getProperty('stderr');
$strErrProperty->setAccessible(true);
$strErrProperty->setValue($this->output, $errorOutput);
$reflectedParent = $reflectedOutput->getParentClass();
$streamProperty = $reflectedParent->getProperty('stream');
$streamProperty->setAccessible(true);
$streamProperty->setValue($this->output, fopen('php://memory', 'w', false));
}
return $this->statusCode = $this->application->run($this->input, $this->output);
@ -96,6 +120,30 @@ class ApplicationTester
return $display;
}
/**
* Gets the output written to STDERR by the application.
*
* @param bool $normalize Whether to normalize end of lines to \n or not
*
* @return string
*/
public function getErrorOutput($normalize = false)
{
if (!$this->captureStreamsIndependently) {
throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.');
}
rewind($this->output->getErrorOutput()->getStream());
$display = stream_get_contents($this->output->getErrorOutput()->getStream());
if ($normalize) {
$display = str_replace(PHP_EOL, "\n", $display);
}
return $display;
}
/**
* Gets the input instance used by the last execution of the application.
*

View File

@ -485,9 +485,14 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$application->setCatchExceptions(true);
$this->assertTrue($application->areExceptionsCaught());
$tester->run(array('command' => 'foo'), array('decorated' => false));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->setCatchExceptions() sets the catch exception flag');
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->setCatchExceptions() sets the catch exception flag');
$this->assertSame('', $tester->getDisplay(true));
$application->setCatchExceptions(false);
try {
$tester->run(array('command' => 'foo'), array('decorated' => false));
@ -516,19 +521,19 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
->will($this->returnValue(120));
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'), array('decorated' => false));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->renderException() renders a pretty exception');
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exception');
$tester->run(array('command' => 'foo'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE));
$this->assertContains('Exception trace', $tester->getDisplay(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose');
$tester->run(array('command' => 'foo'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE, 'capture_stderr_separately' => true));
$this->assertContains('Exception trace', $tester->getErrorOutput(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose');
$tester->run(array('command' => 'list', '--foo' => true), array('decorated' => false));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getDisplay(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command');
$tester->run(array('command' => 'list', '--foo' => true), array('decorated' => false, 'capture_stderr_separately' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getErrorOutput(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command');
$application->add(new \Foo3Command());
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo3:bar'), array('decorated' => false));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
$tester->run(array('command' => 'foo3:bar'), array('decorated' => false, 'capture_stderr_separately' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
$tester->run(array('command' => 'foo3:bar'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE));
$this->assertRegExp('/\[Exception\]\s*First exception/', $tester->getDisplay(), '->renderException() renders a pretty exception without code exception when code exception is default and verbosity is verbose');
@ -538,6 +543,9 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$tester->run(array('command' => 'foo3:bar'), array('decorated' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3decorated.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
$tester->run(array('command' => 'foo3:bar'), array('decorated' => true, 'capture_stderr_separately' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3decorated.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
$application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
$application->setAutoExit(false);
$application->expects($this->any())
@ -545,8 +553,8 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
->will($this->returnValue(32));
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'), array('decorated' => false));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getErrorOutput(true), '->renderException() wraps messages when they are bigger than the terminal');
}
public function testRenderExceptionWithDoubleWidthCharacters()
@ -561,11 +569,11 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
});
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'), array('decorated' => false));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
$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');
$tester->run(array('command' => 'foo'), array('decorated' => true));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getDisplay(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');
$application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
$application->setAutoExit(false);
@ -576,8 +584,8 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
throw new \Exception('コマンドの実行中にエラーが発生しました。');
});
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'), array('decorated' => false));
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
$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');
}
public function testRun()
@ -702,8 +710,8 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$application = $this->getMock('Symfony\Component\Console\Application', array('doRun'));
$application->setAutoExit(false);
$application->expects($this->once())
->method('doRun')
->will($this->throwException($exception));
->method('doRun')
->will($this->throwException($exception));
$exitCode = $application->run(new ArrayInput(array()), new NullOutput());
@ -717,8 +725,8 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$application = $this->getMock('Symfony\Component\Console\Application', array('doRun'));
$application->setAutoExit(false);
$application->expects($this->once())
->method('doRun')
->will($this->throwException($exception));
->method('doRun')
->will($this->throwException($exception));
$exitCode = $application->run(new ArrayInput(array()), new NullOutput());