Skip to content

Commit af36c83

Browse files
committed
feature symfony#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 symfony#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
2 parents 464a492 + 6ff6a28 commit af36c83

File tree

3 files changed

+98
-41
lines changed

3 files changed

+98
-41
lines changed

src/Symfony/Component/Console/Output/ConsoleOutput.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
1515

1616
/**
17-
* ConsoleOutput is the default class for all CLI output. It uses STDOUT.
17+
* ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR.
1818
*
19-
* This class is a convenient wrapper around `StreamOutput`.
19+
* This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR.
2020
*
2121
* $output = new ConsoleOutput();
2222
*
2323
* This is equivalent to:
2424
*
2525
* $output = new StreamOutput(fopen('php://stdout', 'w'));
26+
* $stdErr = new StreamOutput(fopen('php://stderr', 'w'));
2627
*
2728
* @author Fabien Potencier <fabien@symfony.com>
2829
*/
@@ -139,18 +140,18 @@ function_exists('php_uname') ? php_uname('s') : '',
139140
*/
140141
private function openOutputStream()
141142
{
142-
$outputStream = $this->hasStdoutSupport() ? 'php://stdout' : 'php://output';
143+
if (!$this->hasStdoutSupport()) {
144+
return fopen('php://output', 'w');
145+
}
143146

144-
return @fopen($outputStream, 'w') ?: fopen('php://output', 'w');
147+
return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
145148
}
146149

147150
/**
148151
* @return resource
149152
*/
150153
private function openErrorStream()
151154
{
152-
$errorStream = $this->hasStderrSupport() ? 'php://stderr' : 'php://output';
153-
154-
return fopen($errorStream, 'w');
155+
return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w');
155156
}
156157
}

src/Symfony/Component/Console/Tester/ApplicationTester.php

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Console\Application;
1515
use Symfony\Component\Console\Input\ArrayInput;
1616
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\ConsoleOutput;
1718
use Symfony\Component\Console\Output\OutputInterface;
1819
use Symfony\Component\Console\Output\StreamOutput;
1920

@@ -31,14 +32,13 @@ class ApplicationTester
3132
{
3233
private $application;
3334
private $input;
34-
private $output;
3535
private $statusCode;
36-
3736
/**
38-
* Constructor.
39-
*
40-
* @param Application $application An Application instance to test.
37+
* @var OutputInterface
4138
*/
39+
private $output;
40+
private $captureStreamsIndependently = false;
41+
4242
public function __construct(Application $application)
4343
{
4444
$this->application = $application;
@@ -49,9 +49,10 @@ public function __construct(Application $application)
4949
*
5050
* Available options:
5151
*
52-
* * interactive: Sets the input interactive flag
53-
* * decorated: Sets the output decorated flag
54-
* * verbosity: Sets the output verbosity flag
52+
* * interactive: Sets the input interactive flag
53+
* * decorated: Sets the output decorated flag
54+
* * verbosity: Sets the output verbosity flag
55+
* * capture_stderr_separately: Make output of stdOut and stdErr separately available
5556
*
5657
* @param array $input An array of arguments and options
5758
* @param array $options An array of options
@@ -65,12 +66,35 @@ public function run(array $input, $options = array())
6566
$this->input->setInteractive($options['interactive']);
6667
}
6768

68-
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
69-
if (isset($options['decorated'])) {
70-
$this->output->setDecorated($options['decorated']);
71-
}
72-
if (isset($options['verbosity'])) {
73-
$this->output->setVerbosity($options['verbosity']);
69+
$this->captureStreamsIndependently = array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately'];
70+
if (!$this->captureStreamsIndependently) {
71+
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
72+
if (isset($options['decorated'])) {
73+
$this->output->setDecorated($options['decorated']);
74+
}
75+
if (isset($options['verbosity'])) {
76+
$this->output->setVerbosity($options['verbosity']);
77+
}
78+
} else {
79+
$this->output = new ConsoleOutput(
80+
isset($options['verbosity']) ? $options['verbosity'] : ConsoleOutput::VERBOSITY_NORMAL,
81+
isset($options['decorated']) ? $options['decorated'] : null
82+
);
83+
84+
$errorOutput = new StreamOutput(fopen('php://memory', 'w', false));
85+
$errorOutput->setFormatter($this->output->getFormatter());
86+
$errorOutput->setVerbosity($this->output->getVerbosity());
87+
$errorOutput->setDecorated($this->output->isDecorated());
88+
89+
$reflectedOutput = new \ReflectionObject($this->output);
90+
$strErrProperty = $reflectedOutput->getProperty('stderr');
91+
$strErrProperty->setAccessible(true);
92+
$strErrProperty->setValue($this->output, $errorOutput);
93+
94+
$reflectedParent = $reflectedOutput->getParentClass();
95+
$streamProperty = $reflectedParent->getProperty('stream');
96+
$streamProperty->setAccessible(true);
97+
$streamProperty->setValue($this->output, fopen('php://memory', 'w', false));
7498
}
7599

76100
return $this->statusCode = $this->application->run($this->input, $this->output);
@@ -96,6 +120,30 @@ public function getDisplay($normalize = false)
96120
return $display;
97121
}
98122

123+
/**
124+
* Gets the output written to STDERR by the application.
125+
*
126+
* @param bool $normalize Whether to normalize end of lines to \n or not
127+
*
128+
* @return string
129+
*/
130+
public function getErrorOutput($normalize = false)
131+
{
132+
if (!$this->captureStreamsIndependently) {
133+
throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.');
134+
}
135+
136+
rewind($this->output->getErrorOutput()->getStream());
137+
138+
$display = stream_get_contents($this->output->getErrorOutput()->getStream());
139+
140+
if ($normalize) {
141+
$display = str_replace(PHP_EOL, "\n", $display);
142+
}
143+
144+
return $display;
145+
}
146+
99147
/**
100148
* Gets the input instance used by the last execution of the application.
101149
*

src/Symfony/Component/Console/Tests/ApplicationTest.php

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,14 @@ public function testSetCatchExceptions()
485485

486486
$application->setCatchExceptions(true);
487487
$this->assertTrue($application->areExceptionsCaught());
488+
488489
$tester->run(array('command' => 'foo'), array('decorated' => false));
489490
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->setCatchExceptions() sets the catch exception flag');
490491

492+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
493+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->setCatchExceptions() sets the catch exception flag');
494+
$this->assertSame('', $tester->getDisplay(true));
495+
491496
$application->setCatchExceptions(false);
492497
try {
493498
$tester->run(array('command' => 'foo'), array('decorated' => false));
@@ -516,19 +521,19 @@ public function testRenderException()
516521
->will($this->returnValue(120));
517522
$tester = new ApplicationTester($application);
518523

519-
$tester->run(array('command' => 'foo'), array('decorated' => false));
520-
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->renderException() renders a pretty exception');
524+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
525+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exception');
521526

522-
$tester->run(array('command' => 'foo'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE));
523-
$this->assertContains('Exception trace', $tester->getDisplay(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose');
527+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE, 'capture_stderr_separately' => true));
528+
$this->assertContains('Exception trace', $tester->getErrorOutput(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose');
524529

525-
$tester->run(array('command' => 'list', '--foo' => true), array('decorated' => false));
526-
$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');
530+
$tester->run(array('command' => 'list', '--foo' => true), array('decorated' => false, 'capture_stderr_separately' => true));
531+
$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');
527532

528533
$application->add(new \Foo3Command());
529534
$tester = new ApplicationTester($application);
530-
$tester->run(array('command' => 'foo3:bar'), array('decorated' => false));
531-
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
535+
$tester->run(array('command' => 'foo3:bar'), array('decorated' => false, 'capture_stderr_separately' => true));
536+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
532537

533538
$tester->run(array('command' => 'foo3:bar'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE));
534539
$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,15 +543,18 @@ public function testRenderException()
538543
$tester->run(array('command' => 'foo3:bar'), array('decorated' => true));
539544
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3decorated.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
540545

546+
$tester->run(array('command' => 'foo3:bar'), array('decorated' => true, 'capture_stderr_separately' => true));
547+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3decorated.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
548+
541549
$application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
542550
$application->setAutoExit(false);
543551
$application->expects($this->any())
544552
->method('getTerminalWidth')
545553
->will($this->returnValue(32));
546554
$tester = new ApplicationTester($application);
547555

548-
$tester->run(array('command' => 'foo'), array('decorated' => false));
549-
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
556+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
557+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getErrorOutput(true), '->renderException() wraps messages when they are bigger than the terminal');
550558
}
551559

552560
public function testRenderExceptionWithDoubleWidthCharacters()
@@ -561,11 +569,11 @@ public function testRenderExceptionWithDoubleWidthCharacters()
561569
});
562570
$tester = new ApplicationTester($application);
563571

564-
$tester->run(array('command' => 'foo'), array('decorated' => false));
565-
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
572+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
573+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
566574

567-
$tester->run(array('command' => 'foo'), array('decorated' => true));
568-
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
575+
$tester->run(array('command' => 'foo'), array('decorated' => true, 'capture_stderr_separately' => true));
576+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
569577

570578
$application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth'));
571579
$application->setAutoExit(false);
@@ -576,8 +584,8 @@ public function testRenderExceptionWithDoubleWidthCharacters()
576584
throw new \Exception('コマンドの実行中にエラーが発生しました。');
577585
});
578586
$tester = new ApplicationTester($application);
579-
$tester->run(array('command' => 'foo'), array('decorated' => false));
580-
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
587+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true));
588+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getErrorOutput(true), '->renderException() wraps messages when they are bigger than the terminal');
581589
}
582590

583591
public function testRun()
@@ -702,8 +710,8 @@ public function testRunReturnsIntegerExitCode()
702710
$application = $this->getMock('Symfony\Component\Console\Application', array('doRun'));
703711
$application->setAutoExit(false);
704712
$application->expects($this->once())
705-
->method('doRun')
706-
->will($this->throwException($exception));
713+
->method('doRun')
714+
->will($this->throwException($exception));
707715

708716
$exitCode = $application->run(new ArrayInput(array()), new NullOutput());
709717

@@ -717,8 +725,8 @@ public function testRunReturnsExitCodeOneForExceptionCodeZero()
717725
$application = $this->getMock('Symfony\Component\Console\Application', array('doRun'));
718726
$application->setAutoExit(false);
719727
$application->expects($this->once())
720-
->method('doRun')
721-
->will($this->throwException($exception));
728+
->method('doRun')
729+
->will($this->throwException($exception));
722730

723731
$exitCode = $application->run(new ArrayInput(array()), new NullOutput());
724732

0 commit comments

Comments
 (0)