diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index a87d61d48ae0a..afb0600602d2f 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -23,6 +23,31 @@ UPGRADE FROM 2.x to 3.0 * The methods `isQuiet`, `isVerbose`, `isVeryVerbose` and `isDebug` were added to `Symfony\Component\Console\Output\OutputInterface`. + * `ProgressHelper` has been removed in favor of `ProgressBar`. + + Before: + + ``` + $h = new ProgressHelper(); + $h->start($output, 10); + for ($i = 1; $i < 5; $i++) { + usleep(200000); + $h->advance(); + } + $h->finish(); + ``` + + After: + + ``` + $bar = new ProgressBar($output, 10); + $bar->start(); + for ($i = 1; $i < 5; $i++) { + usleep(200000); + $bar->advance(); + } + ``` + ### EventDispatcher * The interface `Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface` diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 19d03ca3039ea..00bb1303838ff 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -4,8 +4,9 @@ CHANGELOG 2.5.0 ----- -* added a way to set a default command instead of `ListCommand` -* added a way to set the process name of a command + * deprecated ProgressHelper in favor of ProgressBar + * added a way to set a default command instead of `ListCommand` + * added a way to set the process name of a command 2.4.0 ----- diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 534b9f4319737..39d5d884620a9 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Console\Helper; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + /** * Helper is the base class for all helper classes. * @@ -47,7 +49,7 @@ public function getHelperSet() * * @return integer The length of the string */ - protected function strlen($string) + public static function strlen($string) { if (!function_exists('mb_strlen')) { return strlen($string); @@ -59,4 +61,61 @@ protected function strlen($string) return mb_strlen($string, $encoding); } + + public static function formatTime($secs) + { + static $timeFormats = array( + array(0, '< 1 sec'), + array(2, '1 sec'), + array(59, 'secs', 1), + array(60, '1 min'), + array(3600, 'mins', 60), + array(5400, '1 hr'), + array(86400, 'hrs', 3600), + array(129600, '1 day'), + array(604800, 'days', 86400), + ); + + foreach ($timeFormats as $format) { + if ($secs >= $format[0]) { + continue; + } + + if (2 == count($format)) { + return $format[1]; + } + + return ceil($secs / $format[2]).' '.$format[1]; + } + } + + public static function formatMemory($memory) + { + if ($memory >= 1024 * 1024 * 1024) { + return sprintf('%.1f GB', $memory / 1024 / 1024 / 1024); + } + + if ($memory >= 1024 * 1024) { + return sprintf('%.1f MB', $memory / 1024 / 1024); + } + + if ($memory >= 1024) { + return sprintf('%d kB', $memory / 1024); + } + + return sprintf('%d B', $memory); + } + + public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string) + { + $isDecorated = $formatter->isDecorated(); + $formatter->setDecorated(false); + // remove <...> formatting + $string = $formatter->format($string); + // remove already formatted characters + $string = preg_replace("/\033\[[^m]*m/", '', $string); + $formatter->setDecorated($isDecorated); + + return self::strlen($string); + } } diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php new file mode 100644 index 0000000000000..8cd7ba34e9704 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -0,0 +1,551 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * The ProgressBar provides helpers to display progress output. + * + * @author Fabien Potencier + * @author Chris Jones + */ +class ProgressBar +{ + // options + private $barWidth = 28; + private $barChar = '='; + private $emptyBarChar = '-'; + private $progressChar = '>'; + private $format = null; + private $redrawFreq = 1; + + /** + * @var OutputInterface + */ + private $output; + private $step; + private $max; + private $startTime; + private $stepWidth; + private $percent; + private $lastMessagesLength; + private $barCharOriginal; + private $formatLineCount; + private $messages; + + private static $formatters; + private static $formats; + + /** + * Constructor. + * + * @param OutputInterface $output An OutputInterface instance + * @param integer $max Maximum steps (0 if unknown) + */ + public function __construct(OutputInterface $output, $max = 0) + { + // Disabling output when it does not support ANSI codes as it would result in a broken display anyway. + $this->output = $output->isDecorated() ? $output : new NullOutput(); + $this->max = (int) $max; + $this->stepWidth = $this->max > 0 ? Helper::strlen($this->max) : 4; + + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + + if (!self::$formats) { + self::$formats = self::initFormats(); + } + + $this->setFormat($this->determineBestFormat()); + } + + /** + * Sets a placeholder formatter for a given name. + * + * This method also allow you to override an existing placeholder. + * + * @param string $name The placeholder name (including the delimiter char like %) + * @param callable $callable A PHP callable + */ + public static function setPlaceholderFormatterDefinition($name, $callable) + { + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + + self::$formatters[$name] = $callable; + } + + /** + * Gets the placeholder formatter for a given name. + * + * @param string $name The placeholder name (including the delimiter char like %) + * + * @return callable|null A PHP callable + */ + public static function getPlaceholderFormatterDefinition($name) + { + return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; + } + + /** + * Sets a format for a given name. + * + * This method also allow you to override an existing format. + * + * @param string $name The format name + * @param string $format A format string + */ + public static function setFormatDefinition($name, $format) + { + if (!self::$formats) { + self::$formats = self::initFormats(); + } + + self::$formats[$name] = $format; + } + + /** + * Gets the format for a given name. + * + * @param string $name The format name + * + * @return string|null A format string + */ + public static function getFormatDefinition($name) + { + return isset(self::$formats[$name]) ? self::$formats[$name] : null; + } + + public function setMessage($message, $name = 'message') + { + $this->messages[$name] = $message; + } + + public function getMessage($name = 'message') + { + return $this->messages[$name]; + } + + /** + * Gets the progress bar start time. + * + * @return integer The progress bar start time + */ + public function getStartTime() + { + return $this->startTime; + } + + /** + * Gets the progress bar maximal steps. + * + * @return integer The progress bar max steps + */ + public function getMaxSteps() + { + return $this->max; + } + + /** + * Gets the progress bar step. + * + * @return integer The progress bar step + */ + public function getStep() + { + return $this->step; + } + + /** + * Gets the progress bar step width. + * + * @return integer The progress bar step width + */ + public function getStepWidth() + { + return $this->stepWidth; + } + + /** + * Gets the current progress bar percent. + * + * @return integer The current progress bar percent + */ + public function getProgressPercent() + { + return $this->percent; + } + + /** + * Sets the progress bar width. + * + * @param integer $size The progress bar size + */ + public function setBarWidth($size) + { + $this->barWidth = (int) $size; + } + + /** + * Gets the progress bar width. + * + * @return integer The progress bar size + */ + public function getBarWidth() + { + return $this->barWidth; + } + + /** + * Sets the bar character. + * + * @param string $char A character + */ + public function setBarCharacter($char) + { + $this->barChar = $char; + } + + /** + * Gets the bar character. + * + * @return string A character + */ + public function getBarCharacter() + { + return $this->barChar; + } + + /** + * Sets the empty bar character. + * + * @param string $char A character + */ + public function setEmptyBarCharacter($char) + { + $this->emptyBarChar = $char; + } + + /** + * Gets the empty bar character. + * + * @return string A character + */ + public function getEmptyBarCharacter() + { + return $this->emptyBarChar; + } + + /** + * Sets the progress bar character. + * + * @param string $char A character + */ + public function setProgressCharacter($char) + { + $this->progressChar = $char; + } + + /** + * Gets the progress bar character. + * + * @return string A character + */ + public function getProgressCharacter() + { + return $this->progressChar; + } + + /** + * Sets the progress bar format. + * + * @param string $format The format + */ + public function setFormat($format) + { + // try to use the _nomax variant if available + if (!$this->max && isset(self::$formats[$format.'_nomax'])) { + $this->format = self::$formats[$format.'_nomax']; + } elseif (isset(self::$formats[$format])) { + $this->format = self::$formats[$format]; + } else { + $this->format = $format; + } + + $this->formatLineCount = substr_count($this->format, "\n"); + } + + /** + * Sets the redraw frequency. + * + * @param integer $freq The frequency in steps + */ + public function setRedrawFrequency($freq) + { + $this->redrawFreq = (int) $freq; + } + + /** + * Starts the progress output. + */ + public function start() + { + $this->startTime = time(); + $this->step = 0; + $this->percent = 0; + $this->lastMessagesLength = 0; + $this->barCharOriginal = ''; + + if (!$this->max) { + $this->barCharOriginal = $this->barChar; + $this->barChar = $this->emptyBarChar; + } + + $this->display(); + } + + /** + * Advances the progress output X steps. + * + * @param integer $step Number of steps to advance + * + * @throws \LogicException + */ + public function advance($step = 1) + { + $this->setCurrent($this->step + $step); + } + + /** + * Sets the current progress. + * + * @param integer $step The current progress + * + * @throws \LogicException + */ + public function setCurrent($step) + { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling setCurrent().'); + } + + $step = (int) $step; + if ($step < $this->step) { + throw new \LogicException('You can\'t regress the progress bar.'); + } + + if ($this->max > 0 && $step > $this->max) { + throw new \LogicException('You can\'t advance the progress bar past the max value.'); + } + + $prevPeriod = intval($this->step / $this->redrawFreq); + $currPeriod = intval($step / $this->redrawFreq); + $this->step = $step; + $this->percent = $this->max > 0 ? (float) $this->step / $this->max : 0; + if ($prevPeriod !== $currPeriod || $this->max === $step) { + $this->display(); + } + } + + /** + * Finishes the progress output. + */ + public function finish() + { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling finish().'); + } + + if (!$this->max) { + $this->barChar = $this->barCharOriginal; + $this->max = $this->step; + $this->setCurrent($this->max); + $this->max = 0; + $this->barChar = $this->emptyBarChar; + } else { + $this->setCurrent($this->max); + } + + $this->startTime = null; + } + + /** + * Outputs the current progress string. + * + * @throws \LogicException + */ + public function display() + { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling display().'); + } + + // these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped. + $self = $this; + $output = $this->output; + $messages = $this->messages; + $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) { + if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { + $text = call_user_func($formatter, $self, $output); + } elseif (isset($messages[$matches[1]])) { + $text = $messages[$matches[1]]; + } else { + return $matches[0]; + } + + if (isset($matches[2])) { + $text = sprintf('%'.$matches[2], $text); + } + + return $text; + }, $this->format)); + } + + /** + * Removes the progress bar from the current line. + * + * This is useful if you wish to write some output + * while a progress bar is running. + * Call display() to show the progress bar again. + */ + public function clear() + { + $this->overwrite(str_repeat("\n", $this->formatLineCount)); + } + + /** + * Overwrites a previous message to the output. + * + * @param string $message The message + */ + private function overwrite($message) + { + $lines = explode("\n", $message); + + // append whitespace to match the line's length + if (null !== $this->lastMessagesLength) { + foreach ($lines as $i => $line) { + if ($this->lastMessagesLength > Helper::strlenWithoutDecoration($this->output->getFormatter(), $line)) { + $lines[$i] = str_pad($line, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); + } + } + } + + // move back to the beginning of the progress bar before redrawing it + $this->output->write("\x0D"); + if ($this->formatLineCount) { + $this->output->write(sprintf("\033[%dA", $this->formatLineCount)); + } + $this->output->write(implode("\n", $lines)); + + $this->lastMessagesLength = 0; + foreach ($lines as $line) { + $len = Helper::strlenWithoutDecoration($this->output->getFormatter(), $line); + if ($len > $this->lastMessagesLength) { + $this->lastMessagesLength = $len; + } + } + } + + private function determineBestFormat() + { + switch ($this->output->getVerbosity()) { + // OutputInterface::VERBOSITY_QUIET: display is disabled anyway + case OutputInterface::VERBOSITY_VERBOSE: + return $this->max > 0 ? 'verbose' : 'verbose_nomax'; + case OutputInterface::VERBOSITY_VERY_VERBOSE: + return $this->max > 0 ? 'very_verbose' : 'very_verbose_nomax'; + case OutputInterface::VERBOSITY_DEBUG: + return $this->max > 0 ? 'debug' : 'debug_nomax'; + default: + return $this->max > 0 ? 'normal' : 'normal_nomax'; + } + } + + private static function initPlaceholderFormatters() + { + return array( + 'bar' => function (ProgressBar $bar, OutputInterface $output) { + $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getStep() % $bar->getBarWidth()); + $display = str_repeat($bar->getBarCharacter(), $completeBars); + if ($completeBars < $bar->getBarWidth()) { + $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter()); + $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); + } + + return $display; + }, + 'elapsed' => function (ProgressBar $bar) { + return Helper::formatTime(time() - $bar->getStartTime()); + }, + 'remaining' => function (ProgressBar $bar) { + if (!$bar->getMaxSteps()) { + throw new \LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); + } + + if (!$bar->getStep()) { + $remaining = 0; + } else { + $remaining = round((time() - $bar->getStartTime()) / $bar->getStep() * ($bar->getMaxSteps() - $bar->getStep())); + } + + return Helper::formatTime($remaining); + }, + 'estimated' => function (ProgressBar $bar) { + if (!$bar->getMaxSteps()) { + throw new \LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); + } + + if (!$bar->getStep()) { + $estimated = 0; + } else { + $estimated = round((time() - $bar->getStartTime()) / $bar->getStep() * $bar->getMaxSteps()); + } + + return Helper::formatTime($estimated); + }, + 'memory' => function (ProgressBar $bar) { + return Helper::formatMemory(memory_get_usage(true)); + }, + 'current' => function (ProgressBar $bar) { + return str_pad($bar->getStep(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); + }, + 'max' => function (ProgressBar $bar) { + return $bar->getMaxSteps(); + }, + 'percent' => function (ProgressBar $bar) { + return floor($bar->getProgressPercent() * 100); + }, + ); + } + + private static function initFormats() + { + return array( + 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', + 'normal_nomax' => ' %current% [%bar%]', + + 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', + 'verbose_nomax' => ' %current% [%bar%] %percent:3s%% %elapsed:6s%', + + 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', + 'very_verbose_nomax' => ' %current% [%bar%] %percent:3s%% %elapsed:6s%', + + 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', + 'debug_nomax' => ' %current% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', + ); + } +} diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php index 7a400874bc03c..eeae23afcbaaa 100644 --- a/src/Symfony/Component/Console/Helper/ProgressHelper.php +++ b/src/Symfony/Component/Console/Helper/ProgressHelper.php @@ -19,6 +19,8 @@ * * @author Chris Jones * @author Fabien Potencier + * + * @deprecated Deprecated since 2.5, to be removed in 3.0; use ProgressBar instead. */ class ProgressHelper extends Helper { diff --git a/src/Symfony/Component/Console/Helper/TableHelper.php b/src/Symfony/Component/Console/Helper/TableHelper.php index d6ad0e9e01804..36237c8323b5e 100644 --- a/src/Symfony/Component/Console/Helper/TableHelper.php +++ b/src/Symfony/Component/Console/Helper/TableHelper.php @@ -427,7 +427,7 @@ private function renderCell(array $row, $column, $cellFormat) $width += strlen($cell) - mb_strlen($cell, $encoding); } - $width += $this->strlen($cell) - $this->computeLengthWithoutDecoration($cell); + $width += $this->strlen($cell) - self::strlenWithoutDecoration($this->output->getFormatter(), $cell); $content = sprintf($this->cellRowContentFormat, $cell); @@ -486,7 +486,7 @@ private function getColumnWidth($column) */ private function getCellWidth(array $row, $column) { - return isset($row[$column]) ? $this->computeLengthWithoutDecoration($row[$column]) : 0; + return isset($row[$column]) ? self::strlenWithoutDecoration($this->output->getFormatter(), $row[$column]) : 0; } /** @@ -498,18 +498,6 @@ private function cleanup() $this->numberOfColumns = null; } - private function computeLengthWithoutDecoration($string) - { - $formatter = $this->output->getFormatter(); - $isDecorated = $formatter->isDecorated(); - $formatter->setDecorated(false); - - $string = $formatter->format($string); - $formatter->setDecorated($isDecorated); - - return $this->strlen($string); - } - /** * {@inheritDoc} */ diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php new file mode 100644 index 0000000000000..7bcfed026b9a9 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -0,0 +1,418 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Output\StreamOutput; + +class ProgressBarTest extends \PHPUnit_Framework_TestCase +{ + protected $lastMessagesLength; + + public function testAdvance() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 1 [->--------------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testAdvanceWithStep() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->advance(5); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 5 [----->----------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testAdvanceMultipleTimes() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->advance(3); + $bar->advance(2); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 3 [--->------------------------]'). + $this->generateOutput(' 5 [----->----------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testCustomizations() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 10); + $bar->setBarWidth(10); + $bar->setBarCharacter('_'); + $bar->setEmptyBarCharacter(' '); + $bar->setProgressCharacter('/'); + $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%%'); + $bar->start(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/10 [/ ] 0%'). + $this->generateOutput(' 1/10 [_/ ] 10%'), + stream_get_contents($output->getStream()) + ); + } + + public function testPercent() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->display(); + $bar->advance(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 1/50 [>---------------------------] 2%'). + $this->generateOutput(' 2/50 [=>--------------------------] 4%'), + stream_get_contents($output->getStream()) + ); + } + + public function testOverwriteWithShorterLine() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%%'); + $bar->start(); + $bar->display(); + $bar->advance(); + + // set shorter format + $bar->setFormat(' %current%/%max% [%bar%]'); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 1/50 [>---------------------------] 2%'). + $this->generateOutput(' 2/50 [=>--------------------------] '), + stream_get_contents($output->getStream()) + ); + } + + public function testSetCurrentProgress() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->display(); + $bar->advance(); + $bar->setCurrent(15); + $bar->setCurrent(25); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 1/50 [>---------------------------] 2%'). + $this->generateOutput(' 15/50 [========>-------------------] 30%'). + $this->generateOutput(' 25/50 [==============>-------------] 50%'), + stream_get_contents($output->getStream()) + ); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage You must start the progress bar + */ + public function testSetCurrentBeforeStarting() + { + $bar = new ProgressBar($this->getOutputStream()); + $bar->setCurrent(15); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage You can't regress the progress bar + */ + public function testRegressProgress() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->setCurrent(15); + $bar->setCurrent(10); + } + + public function testRedrawFrequency() + { + $bar = $this->getMock('Symfony\Component\Console\Helper\ProgressBar', array('display'), array($output = $this->getOutputStream(), 6)); + $bar->expects($this->exactly(4))->method('display'); + + $bar->setRedrawFrequency(2); + $bar->start(); + $bar->setCurrent(1); + $bar->advance(2); + $bar->advance(2); + $bar->advance(1); + } + + public function testMultiByteSupport() + { + if (!function_exists('mb_strlen') || (false === $encoding = mb_detect_encoding('■'))) { + $this->markTestSkipped('The mbstring extension is needed for multi-byte support'); + } + + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->setBarCharacter('■'); + $bar->advance(3); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 3 [■■■>------------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testClear() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->setCurrent(25); + $bar->clear(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 25/50 [==============>-------------] 50%'). + $this->generateOutput(' '), + stream_get_contents($output->getStream()) + ); + } + + public function testPercentNotHundredBeforeComplete() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 200); + $bar->start(); + $bar->display(); + $bar->advance(199); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/200 [>---------------------------] 0%'). + $this->generateOutput(' 0/200 [>---------------------------] 0%'). + $this->generateOutput(' 199/200 [===========================>] 99%'). + $this->generateOutput(' 200/200 [============================] 100%'), + stream_get_contents($output->getStream()) + ); + } + + public function testNonDecoratedOutput() + { + $bar = new ProgressBar($output = $this->getOutputStream(false)); + $bar->start(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals('', stream_get_contents($output->getStream())); + } + + public function testParallelBars() + { + $output = $this->getOutputStream(); + $bar1 = new ProgressBar($output, 2); + $bar2 = new ProgressBar($output, 3); + $bar2->setProgressCharacter('#'); + $bar3 = new ProgressBar($output); + + $bar1->start(); + $output->write("\n"); + $bar2->start(); + $output->write("\n"); + $bar3->start(); + + for ($i = 1; $i <= 3; $i++) { + // up two lines + $output->write("\033[2A"); + if ($i <= 2) { + $bar1->advance(); + } + $output->write("\n"); + $bar2->advance(); + $output->write("\n"); + $bar3->advance(); + } + $output->write("\033[2A"); + $output->write("\n"); + $output->write("\n"); + $bar3->finish(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/2 [>---------------------------] 0%')."\n". + $this->generateOutput(' 0/3 [#---------------------------] 0%')."\n". + rtrim($this->generateOutput(' 0 [>---------------------------]')). + + "\033[2A". + $this->generateOutput(' 1/2 [==============>-------------] 50%')."\n". + $this->generateOutput(' 1/3 [=========#------------------] 33%')."\n". + rtrim($this->generateOutput(' 1 [->--------------------------]')). + + "\033[2A". + $this->generateOutput(' 2/2 [============================] 100%')."\n". + $this->generateOutput(' 2/3 [==================#---------] 66%')."\n". + rtrim($this->generateOutput(' 2 [-->-------------------------]')). + + "\033[2A". + "\n". + $this->generateOutput(' 3/3 [============================] 100%')."\n". + rtrim($this->generateOutput(' 3 [--->------------------------]')). + + "\033[2A". + "\n". + "\n". + rtrim($this->generateOutput(' 3 [============================]')), + stream_get_contents($output->getStream()) + ); + } + + public function testAddingPlaceholderFormatter() + { + ProgressBar::setPlaceholderFormatterDefinition('remaining_steps', function (ProgressBar $bar) { + return $bar->getMaxSteps() - $bar->getStep(); + }); + $bar = new ProgressBar($output = $this->getOutputStream(), 3); + $bar->setFormat(' %remaining_steps% [%bar%]'); + + $bar->start(); + $bar->advance(); + $bar->finish(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 3 [>---------------------------]'). + $this->generateOutput(' 2 [=========>------------------]'). + $this->generateOutput(' 0 [============================]'), + stream_get_contents($output->getStream()) + ); + } + + public function testMultilineFormat() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 3); + $bar->setFormat("%bar%\nfoobar"); + + $bar->start(); + $bar->advance(); + $bar->clear(); + $bar->finish(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(">---------------------------\nfoobar"). + $this->generateOutput("=========>------------------\nfoobar "). + $this->generateOutput(" \n "). + $this->generateOutput("============================\nfoobar "), + stream_get_contents($output->getStream()) + ); + } + + public function testAnsiColorsAndEmojis() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 15); + ProgressBar::setPlaceholderFormatterDefinition('memory', function (ProgressBar $bar) { + static $i = 0; + $mem = 100000 * $i; + $colors = $i++ ? '41;37' : '44;37'; + + return "\033[".$colors."m ".Helper::formatMemory($mem)." \033[0m"; + }); + $bar->setFormat(" \033[44;37m %title:-37s% \033[0m\n %current%/%max% %bar% %percent:3s%%\n 🏁 %remaining:-10s% %memory:37s%"); + $bar->setBarCharacter($done = "\033[32m●\033[0m"); + $bar->setEmptyBarCharacter($empty = "\033[31m●\033[0m"); + $bar->setProgressCharacter($progress = "\033[32m➤ \033[0m"); + + $bar->setMessage('Starting the demo... fingers crossed', 'title'); + $bar->start(); + $bar->setMessage('Looks good to me...', 'title'); + $bar->advance(4); + $bar->setMessage('Thanks, bye', 'title'); + $bar->finish(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput( + " \033[44;37m Starting the demo... fingers crossed \033[0m\n". + " 0/15 ".$progress.str_repeat($empty, 26)." 0%\n". + " 🏁 1 sec \033[44;37m 0 B \033[0m" + ). + $this->generateOutput( + " \033[44;37m Looks good to me... \033[0m\n". + " 4/15 ".str_repeat($done, 7).$progress.str_repeat($empty, 19)." 26%\n". + " 🏁 1 sec \033[41;37m 97 kB \033[0m" + ). + $this->generateOutput( + " \033[44;37m Thanks, bye \033[0m\n". + " 15/15 ".str_repeat($done, 28)." 100%\n". + " 🏁 1 sec \033[41;37m 195 kB \033[0m" + ), + stream_get_contents($output->getStream()) + ); + } + + public function testSetFormat() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->setFormat('normal'); + $bar->start(); + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'), + stream_get_contents($output->getStream()) + ); + + $bar = new ProgressBar($output = $this->getOutputStream(), 10); + $bar->setFormat('normal'); + $bar->start(); + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/10 [>---------------------------] 0%'), + stream_get_contents($output->getStream()) + ); + } + + protected function getOutputStream($decorated = true) + { + return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated); + } + + protected function generateOutput($expected) + { + $count = substr_count($expected, "\n"); + + return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expected; + } +}