From 9ec51a17979692da9c7f40831f42cd18d0c69aa7 Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Thu, 28 Sep 2017 17:14:48 +0200 Subject: [PATCH] [Console] Modify console output and print multiple modifyable sections --- src/Symfony/Component/Console/CHANGELOG.md | 1 + .../Component/Console/Helper/ProgressBar.php | 20 ++- .../Component/Console/Helper/Table.php | 37 +++++ .../Console/Output/ConsoleOutput.php | 9 ++ .../Console/Output/ConsoleOutputInterface.php | 4 +- .../Console/Output/ConsoleSectionOutput.php | 133 +++++++++++++++++ .../Console/Tests/Helper/ProgressBarTest.php | 84 +++++++++++ .../Console/Tests/Helper/TableTest.php | 111 ++++++++++++++ .../Tests/Output/ConsoleSectionOutputTest.php | 140 ++++++++++++++++++ 9 files changed, 531 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/Console/Output/ConsoleSectionOutput.php create mode 100644 src/Symfony/Component/Console/Tests/Output/ConsoleSectionOutputTest.php diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 887ac2f83a..46d7f40453 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 7c0c09a97d..5024720476 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 @@ final class ProgressBar { 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 d54f63684a..2f537cc494 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 @@ class Table 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 @@ class Table $this->renderRowSeparator(); $this->cleanup(); + $this->rendered = true; } /** @@ -445,6 +469,19 @@ class Table }); } + 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 cff6cd5a4a..87d93b88a5 100644 --- a/src/Symfony/Component/Console/Output/ConsoleOutput.php +++ b/src/Symfony/Component/Console/Output/ConsoleOutput.php @@ -30,6 +30,7 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface; 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 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface } } + /** + * 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 b44ea7e058..f4c2fa623a 100644 --- a/src/Symfony/Component/Console/Output/ConsoleOutputInterface.php +++ b/src/Symfony/Component/Console/Output/ConsoleOutputInterface.php @@ -13,9 +13,11 @@ namespace Symfony\Component\Console\Output; /** * 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 0000000000..98c778964a --- /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 aca64914a9..7159678b72 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 @@ class ProgressBarTest extends TestCase ); } + 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 380699528d..872c9c0af5 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 @@ TABLE; $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 0000000000..abf3911aa0 --- /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())); + } +}