Skip to content
Browse files

[Console] added ProgressBar (to replace the stateful ProgressHelper c…

…lass)
  • Loading branch information...
1 parent afee085 commit 4a953b78ec29f30d624e33536b5a67221d11d809 @fabpot fabpot committed Mar 1, 2014
Showing with 727 additions and 3 deletions.
  1. +3 −2 CHANGELOG.md
  2. +1 −1 Helper/Helper.php
  3. +403 −0 Helper/ProgressBar.php
  4. +2 −0 Helper/ProgressHelper.php
  5. +318 −0 Tests/Helper/ProgressBarTest.php
View
5 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
-----
View
2 Helper/Helper.php
@@ -47,7 +47,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);
View
403 Helper/ProgressBar.php
@@ -0,0 +1,403 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * 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 <fabien@symfony.com>
+ * @author Chris Jones <leeked@gmail.com>
+ */
+class ProgressBar
+{
+ const FORMAT_QUIET = ' %percent%%';
+ const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent%%';
+ const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent%% Elapsed: %elapsed%';
+ const FORMAT_QUIET_NOMAX = ' %current%';
+ const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]';
+ const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed%';
+
+ // 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 $lastMessagesLength;
+ private $barCharOriginal;
+
+ /**
+ * List of formatting variables
+ *
+ * @var array
+ */
+ private $defaultFormatVars = array(
+ 'current',
+ 'max',
+ 'bar',
+ 'percent',
+ 'elapsed',
+ );
+
+ /**
+ * Available formatting variables
+ *
+ * @var array
+ */
+ private $formatVars;
+
+ /**
+ * Various time formats
+ *
+ * @var array
+ */
+ private $timeFormats = array(
+ array(0, '???'),
+ 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),
+ );
+
+ private $stepWidth;
+ private $percent;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Sets the progress bar width.
+ *
+ * @param int $size The progress bar size
+ */
+ public function setBarWidth($size)
+ {
+ $this->barWidth = (int) $size;
+ }
+
+ /**
+ * Sets the bar character.
+ *
+ * @param string $char A character
+ */
+ public function setBarCharacter($char)
+ {
+ $this->barChar = $char;
+ }
+
+ /**
+ * Sets the empty bar character.
+ *
+ * @param string $char A character
+ */
+ public function setEmptyBarCharacter($char)
+ {
+ $this->emptyBarChar = $char;
+ }
+
+ /**
+ * Sets the progress bar character.
+ *
+ * @param string $char A character
+ */
+ public function setProgressCharacter($char)
+ {
+ $this->progressChar = $char;
+ }
+
+ /**
+ * Sets the progress bar format.
+ *
+ * @param string $format The format
+ */
+ public function setFormat($format)
+ {
+ $this->format = $format;
+ }
+
+ /**
+ * Sets the redraw frequency.
+ *
+ * @param int $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 (null === $this->format) {
+ $this->format = $this->determineBestFormat();
+ }
+
+ $this->formatVars = array();
+ foreach ($this->defaultFormatVars as $var) {
+ if (false !== strpos($this->format, "%{$var}%")) {
+ $this->formatVars[$var] = true;
+ }
+ }
+
+ 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().');
+ }
+
+ $message = $this->format;
+ foreach ($this->generate() as $name => $value) {
+ $message = str_replace("%{$name}%", $value, $message);
+ }
+ $this->overwrite($message);
+ }
+
+ /**
+ * 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('');
+ }
+
+ /**
+ * Generates the array map of format variables to values.
+ *
+ * @return array Array of format vars and values
+ */
+ private function generate()
+ {
+ $vars = array();
+
+ if (isset($this->formatVars['bar'])) {
+ $completeBars = floor($this->max > 0 ? $this->percent * $this->barWidth : $this->step % $this->barWidth);
+ $emptyBars = $this->barWidth - $completeBars - Helper::strlen($this->progressChar);
+ $bar = str_repeat($this->barChar, $completeBars);
+ if ($completeBars < $this->barWidth) {
+ $bar .= $this->progressChar;
+ $bar .= str_repeat($this->emptyBarChar, $emptyBars);
+ }
+
+ $vars['bar'] = $bar;
+ }
+
+ if (isset($this->formatVars['elapsed'])) {
+ $elapsed = time() - $this->startTime;
+ $vars['elapsed'] = str_pad($this->humaneTime($elapsed), 6, ' ', STR_PAD_LEFT);
+ }
+
+ if (isset($this->formatVars['current'])) {
+ $vars['current'] = str_pad($this->step, $this->stepWidth, ' ', STR_PAD_LEFT);
+ }
+
+ if (isset($this->formatVars['max'])) {
+ $vars['max'] = $this->max;
+ }
+
+ if (isset($this->formatVars['percent'])) {
+ $vars['percent'] = str_pad(floor($this->percent * 100), 3, ' ', STR_PAD_LEFT);
+ }
+
+ return $vars;
+ }
+
+ /**
+ * Converts seconds into human-readable format.
+ *
+ * @param integer $secs Number of seconds
+ *
+ * @return string Time in readable format
+ */
+ private function humaneTime($secs)
+ {
+ $text = '';
+ foreach ($this->timeFormats as $format) {
+ if ($secs < $format[0]) {
+ if (count($format) == 2) {
+ $text = $format[1];
+ break;
+ } else {
+ $text = ceil($secs / $format[2]).' '.$format[1];
+ break;
+ }
+ }
+ }
+
+ return $text;
+ }
+
+ /**
+ * Overwrites a previous message to the output.
+ *
+ * @param string $message The message
+ */
+ private function overwrite($message)
+ {
+ $length = Helper::strlen($message);
+
+ // append whitespace to match the last line's length
+ if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) {
+ $message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
+ }
+
+ // carriage return
+ $this->output->write("\x0D");
+ $this->output->write($message);
+
+ $this->lastMessagesLength = Helper::strlen($message);
+ }
+
+ private function determineBestFormat()
+ {
+ switch ($this->output->getVerbosity()) {
+ case OutputInterface::VERBOSITY_QUIET:
+ $format = self::FORMAT_QUIET_NOMAX;
+ if ($this->max > 0) {
+ $format = self::FORMAT_QUIET;
+ }
+ break;
+ case OutputInterface::VERBOSITY_VERBOSE:
+ case OutputInterface::VERBOSITY_VERY_VERBOSE:
+ case OutputInterface::VERBOSITY_DEBUG:
+ $format = self::FORMAT_VERBOSE_NOMAX;
+ if ($this->max > 0) {
+ $format = self::FORMAT_VERBOSE;
+ }
+ break;
+ default:
+ $format = self::FORMAT_NORMAL_NOMAX;
+ if ($this->max > 0) {
+ $format = self::FORMAT_NORMAL;
+ }
+ break;
+ }
+
+ return $format;
+ }
+}
View
2 Helper/ProgressHelper.php
@@ -19,6 +19,8 @@
*
* @author Chris Jones <leeked@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @deprecated Deprecated since 2.5, to be removed in 3.0; use ProgressBar instead.
*/
class ProgressHelper extends Helper
{
View
318 Tests/Helper/ProgressBarTest.php
@@ -0,0 +1,318 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * 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\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%%');
+ $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%%');
+ $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())
+ );
+ }
+
+ protected function getOutputStream($decorated = true)
+ {
+ return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated);
+ }
+
+ protected function generateOutput($expected)
+ {
+ $expectedout = $expected;
+
+ if (null !== $this->lastMessagesLength) {
+ $expectedout = str_pad($expected, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
+ }
+
+ $this->lastMessagesLength = strlen($expectedout);
+
+ return "\x0D".$expectedout;
+ }
+}

0 comments on commit 4a953b7

Please sign in to comment.
Something went wrong with that request. Please try again.