diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 887ac2f83a8dc..46d7f40453f63 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * added option to run suggested command if command is not found and only 1 alternative is available + * added option to modify console output and print multiple modifiable sections 4.0.0 ----- diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 7c0c09a97d375..5024720476432 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Terminal; @@ -375,15 +376,20 @@ private function overwrite(string $message): void { if ($this->overwrite) { if (!$this->firstRun) { - // Move the cursor to the beginning of the line - $this->output->write("\x0D"); + if ($this->output instanceof ConsoleSectionOutput) { + $lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1; + $this->output->clear($lines); + } else { + // Move the cursor to the beginning of the line + $this->output->write("\x0D"); - // Erase the line - $this->output->write("\x1B[2K"); + // Erase the line + $this->output->write("\x1B[2K"); - // Erase previous lines - if ($this->formatLineCount > 0) { - $this->output->write(str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount)); + // Erase previous lines + if ($this->formatLineCount > 0) { + $this->output->write(str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount)); + } } } } elseif ($this->step > 0) { diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index d54f63684a410..2f537cc494a58 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Console\Helper; +use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; /** * Provides helpers to display a table. @@ -70,6 +72,8 @@ class Table private static $styles; + private $rendered = false; + public function __construct(OutputInterface $output) { $this->output = $output; @@ -252,6 +256,25 @@ public function addRow($row) return $this; } + /** + * Adds a row to the table, and re-renders the table. + */ + public function appendRow($row): self + { + if (!$this->output instanceof ConsoleSectionOutput) { + throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); + } + + if ($this->rendered) { + $this->output->clear($this->calculateRowCount()); + } + + $this->addRow($row); + $this->render(); + + return $this; + } + public function setRow($column, array $row) { $this->rows[$column] = $row; @@ -311,6 +334,7 @@ public function render() $this->renderRowSeparator(); $this->cleanup(); + $this->rendered = true; } /** @@ -445,6 +469,19 @@ private function buildTableRows($rows) }); } + private function calculateRowCount(): int + { + $numberOfRows = count(iterator_to_array($this->buildTableRows(array_merge($this->headers, array(new TableSeparator()), $this->rows)))); + + if ($this->headers) { + ++$numberOfRows; // Add row for header separator + } + + ++$numberOfRows; // Add row for footer separator + + return $numberOfRows; + } + /** * fill rows that contains rowspan > 1. * diff --git a/src/Symfony/Component/Console/Output/ConsoleOutput.php b/src/Symfony/Component/Console/Output/ConsoleOutput.php index cff6cd5a4a420..87d93b88a54a5 100644 --- a/src/Symfony/Component/Console/Output/ConsoleOutput.php +++ b/src/Symfony/Component/Console/Output/ConsoleOutput.php @@ -30,6 +30,7 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface { private $stderr; + private $consoleSectionOutputs = array(); /** * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) @@ -48,6 +49,14 @@ public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decor } } + /** + * Creates a new output section. + */ + public function section(): ConsoleSectionOutput + { + return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Console/Output/ConsoleOutputInterface.php b/src/Symfony/Component/Console/Output/ConsoleOutputInterface.php index b44ea7e058de6..f4c2fa623a426 100644 --- a/src/Symfony/Component/Console/Output/ConsoleOutputInterface.php +++ b/src/Symfony/Component/Console/Output/ConsoleOutputInterface.php @@ -13,9 +13,11 @@ /** * ConsoleOutputInterface is the interface implemented by ConsoleOutput class. - * This adds information about stderr output stream. + * This adds information about stderr and section output stream. * * @author Dariusz Górecki + * + * @method ConsoleSectionOutput section() Creates a new output section */ interface ConsoleOutputInterface extends OutputInterface { diff --git a/src/Symfony/Component/Console/Output/ConsoleSectionOutput.php b/src/Symfony/Component/Console/Output/ConsoleSectionOutput.php new file mode 100644 index 0000000000000..98c778964a4de --- /dev/null +++ b/src/Symfony/Component/Console/Output/ConsoleSectionOutput.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Terminal; + +/** + * @author Pierre du Plessis + * @author Gabriel Ostrolucký + */ +class ConsoleSectionOutput extends StreamOutput +{ + private $content = array(); + private $lines = 0; + private $sections; + private $terminal; + + /** + * @param resource $stream + * @param ConsoleSectionOutput[] $sections + */ + public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter) + { + parent::__construct($stream, $verbosity, $decorated, $formatter); + array_unshift($sections, $this); + $this->sections = &$sections; + $this->terminal = new Terminal(); + } + + /** + * Clears previous output for this section. + * + * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared + */ + public function clear(int $lines = null) + { + if (empty($this->content) || !$this->isDecorated()) { + return; + } + + if ($lines) { + \array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content + } else { + $lines = $this->lines; + $this->content = array(); + } + + $this->lines -= $lines; + + parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false); + } + + /** + * Overwrites the previous output with a new message. + * + * @param array|string $message + */ + public function overwrite($message) + { + $this->clear(); + $this->writeln($message); + } + + public function getContent(): string + { + return implode('', $this->content); + } + + /** + * {@inheritdoc} + */ + protected function doWrite($message, $newline) + { + if (!$this->isDecorated()) { + return parent::doWrite($message, $newline); + } + + $erasedContent = $this->popStreamContentUntilCurrentSection(); + + foreach (explode(PHP_EOL, $message) as $lineContent) { + $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1; + $this->content[] = $lineContent; + $this->content[] = PHP_EOL; + } + + parent::doWrite($message, true); + parent::doWrite($erasedContent, false); + } + + /** + * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits + * current section. Then it erases content it crawled through. Optionally, it erases part of current section too. + */ + private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string + { + $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection; + $erasedContent = array(); + + foreach ($this->sections as $section) { + if ($section === $this) { + break; + } + + $numberOfLinesToClear += $section->lines; + $erasedContent[] = $section->getContent(); + } + + if ($numberOfLinesToClear > 0) { + // move cursor up n lines + parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false); + // erase to end of screen + parent::doWrite("\x1b[0J", false); + } + + return implode('', array_reverse($erasedContent)); + } + + private function getDisplayLength(string $text): string + { + return Helper::strlenWithoutDecoration($this->getFormatter(), str_replace("\t", ' ', $text)); + } +} diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index aca64914a9e08..7159678b725ff 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Console\Tests\Helper; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\StreamOutput; /** @@ -310,6 +312,88 @@ public function testOverwriteWithShorterLine() ); } + public function testOverwriteWithSectionOutput() + { + $sections = array(); + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + $bar = new ProgressBar($output, 50); + $bar->start(); + $bar->display(); + $bar->advance(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + ' 0/50 [>---------------------------] 0%'.PHP_EOL. + "\x1b[1A\x1b[0J".' 0/50 [>---------------------------] 0%'.PHP_EOL. + "\x1b[1A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL. + "\x1b[1A\x1b[0J".' 2/50 [=>--------------------------] 4%'.PHP_EOL, + stream_get_contents($output->getStream()) + ); + } + + public function testOverwriteMultipleProgressBarsWithSectionOutputs() + { + $sections = array(); + $stream = $this->getOutputStream(true); + $output1 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + $output2 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + $progress = new ProgressBar($output1, 50); + $progress2 = new ProgressBar($output2, 50); + + $progress->start(); + $progress2->start(); + + $progress2->advance(); + $progress->advance(); + + rewind($stream->getStream()); + + $this->assertEquals( + ' 0/50 [>---------------------------] 0%'.PHP_EOL. + ' 0/50 [>---------------------------] 0%'.PHP_EOL. + "\x1b[1A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL. + "\x1b[2A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL. + "\x1b[1A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL. + ' 1/50 [>---------------------------] 2%'.PHP_EOL, + stream_get_contents($stream->getStream()) + ); + } + + public function testMultipleSectionsWithCustomFormat() + { + $sections = array(); + $stream = $this->getOutputStream(true); + $output1 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + $output2 = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + ProgressBar::setFormatDefinition('test', '%current%/%max% [%bar%] %percent:3s%% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'); + + $progress = new ProgressBar($output1, 50); + $progress2 = new ProgressBar($output2, 50); + $progress2->setFormat('test'); + + $progress->start(); + $progress2->start(); + + $progress->advance(); + $progress2->advance(); + + rewind($stream->getStream()); + + $this->assertEquals(' 0/50 [>---------------------------] 0%'.PHP_EOL. + ' 0/50 [>] 0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL. + "\x1b[4A\x1b[0J".' 0/50 [>] 0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL. + "\x1b[3A\x1b[0J".' 1/50 [>---------------------------] 2%'.PHP_EOL. + ' 0/50 [>] 0% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL. + "\x1b[3A\x1b[0J".' 1/50 [>] 2% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.PHP_EOL, + stream_get_contents($stream->getStream()) + ); + } + public function testStartWithMax() { $bar = new ProgressBar($output = $this->getOutputStream()); diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php index 380699528d941..872c9c0af5516 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -12,10 +12,12 @@ namespace Symfony\Component\Console\Tests\Helper; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableStyle; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\StreamOutput; class TableTest extends TestCase @@ -821,6 +823,115 @@ public function testColumnWiths() $this->assertEquals($expected, $this->getOutputContent($output)); } + public function testSectionOutput() + { + $sections = array(); + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + $table = new Table($output); + $table + ->setHeaders(array('ISBN', 'Title', 'Author', 'Price')) + ->setRows(array( + array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'), + )); + + $table->render(); + + $table->appendRow(array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25')); + + $expected = + <<assertEquals($expected, $this->getOutputContent($output)); + } + + public function testSectionOutputDoesntClearIfTableIsntRendered() + { + $sections = array(); + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + $table = new Table($output); + $table + ->setHeaders(array('ISBN', 'Title', 'Author', 'Price')) + ->setRows(array( + array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'), + )); + + $table->appendRow(array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25')); + + $expected = + <<
assertEquals($expected, $this->getOutputContent($output)); + } + + public function testSectionOutputWithoutDecoration() + { + $sections = array(); + $stream = $this->getOutputStream(); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + $table = new Table($output); + $table + ->setHeaders(array('ISBN', 'Title', 'Author', 'Price')) + ->setRows(array( + array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'), + )); + + $table->render(); + + $table->appendRow(array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25')); + + $expected = + <<
assertEquals($expected, $this->getOutputContent($output)); + } + + /** + * @expectedException \Symfony\Component\Console\Exception\RuntimeException + * @expectedExceptionMessage Output should be an instance of "Symfony\Component\Console\Output\ConsoleSectionOutput" when calling "Symfony\Component\Console\Helper\Table::appendRow". + */ + public function testAppendRowWithoutSectionOutput() + { + $table = new Table($this->getOutputStream()); + + $table->appendRow(array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25')); + } + /** * @expectedException \Symfony\Component\Console\Exception\InvalidArgumentException * @expectedExceptionMessage Style "absent" is not defined. diff --git a/src/Symfony/Component/Console/Tests/Output/ConsoleSectionOutputTest.php b/src/Symfony/Component/Console/Tests/Output/ConsoleSectionOutputTest.php new file mode 100644 index 0000000000000..abf3911aa0d6e --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Output/ConsoleSectionOutputTest.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Output; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; + +class ConsoleSectionOutputTest extends TestCase +{ + private $stream; + + protected function setUp() + { + $this->stream = fopen('php://memory', 'r+', false); + } + + protected function tearDown() + { + $this->stream = null; + } + + public function testClearAll() + { + $sections = array(); + $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $output->writeln('Foo'.PHP_EOL.'Bar'); + $output->clear(); + + rewind($output->getStream()); + $this->assertEquals('Foo'.PHP_EOL.'Bar'.PHP_EOL.sprintf("\x1b[%dA", 2)."\x1b[0J", stream_get_contents($output->getStream())); + } + + public function testClearNumberOfLines() + { + $sections = array(); + $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $output->writeln("Foo\nBar\nBaz\nFooBar"); + $output->clear(2); + + rewind($output->getStream()); + $this->assertEquals("Foo\nBar\nBaz\nFooBar".PHP_EOL.sprintf("\x1b[%dA", 2)."\x1b[0J", stream_get_contents($output->getStream())); + } + + public function testClearNumberOfLinesWithMultipleSections() + { + $output = new StreamOutput($this->stream); + $sections = array(); + $output1 = new ConsoleSectionOutput($output->getStream(), $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + $output2 = new ConsoleSectionOutput($output->getStream(), $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $output2->writeln('Foo'); + $output2->writeln('Bar'); + $output2->clear(1); + $output1->writeln('Baz'); + + rewind($output->getStream()); + + $this->assertEquals('Foo'.PHP_EOL.'Bar'.PHP_EOL."\x1b[1A\x1b[0J\e[1A\e[0J".'Baz'.PHP_EOL.'Foo'.PHP_EOL, stream_get_contents($output->getStream())); + } + + public function testClearPreservingEmptyLines() + { + $output = new StreamOutput($this->stream); + $sections = array(); + $output1 = new ConsoleSectionOutput($output->getStream(), $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + $output2 = new ConsoleSectionOutput($output->getStream(), $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $output2->writeln(PHP_EOL.'foo'); + $output2->clear(1); + $output1->writeln('bar'); + + rewind($output->getStream()); + + $this->assertEquals(PHP_EOL.'foo'.PHP_EOL."\x1b[1A\x1b[0J\x1b[1A\x1b[0J".'bar'.PHP_EOL.PHP_EOL, stream_get_contents($output->getStream())); + } + + public function testOverwrite() + { + $sections = array(); + $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $output->writeln('Foo'); + $output->overwrite('Bar'); + + rewind($output->getStream()); + $this->assertEquals('Foo'.PHP_EOL."\x1b[1A\x1b[0JBar".PHP_EOL, stream_get_contents($output->getStream())); + } + + public function testOverwriteMultipleLines() + { + $sections = array(); + $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $output->writeln('Foo'.PHP_EOL.'Bar'.PHP_EOL.'Baz'); + $output->overwrite('Bar'); + + rewind($output->getStream()); + $this->assertEquals('Foo'.PHP_EOL.'Bar'.PHP_EOL.'Baz'.PHP_EOL.sprintf("\x1b[%dA", 3)."\x1b[0J".'Bar'.PHP_EOL, stream_get_contents($output->getStream())); + } + + public function testAddingMultipleSections() + { + $sections = array(); + $output1 = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + $output2 = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $this->assertCount(2, $sections); + } + + public function testMultipleSectionsOutput() + { + $output = new StreamOutput($this->stream); + $sections = array(); + $output1 = new ConsoleSectionOutput($output->getStream(), $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + $output2 = new ConsoleSectionOutput($output->getStream(), $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter()); + + $output1->writeln('Foo'); + $output2->writeln('Bar'); + + $output1->overwrite('Baz'); + $output2->overwrite('Foobar'); + + rewind($output->getStream()); + $this->assertEquals('Foo'.PHP_EOL.'Bar'.PHP_EOL."\x1b[2A\x1b[0JBar".PHP_EOL."\x1b[1A\x1b[0JBaz".PHP_EOL.'Bar'.PHP_EOL."\x1b[1A\x1b[0JFoobar".PHP_EOL, stream_get_contents($output->getStream())); + } +}