Upgrade 1-11.38

This commit is contained in:
xesmyd
2026-03-30 14:10:30 +02:00
parent f2a7e6d1fc
commit ac648ef29d
24665 changed files with 69682 additions and 2205004 deletions
+61 -147
View File
@@ -17,11 +17,11 @@ use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
use Symfony\Component\Console\Helper\FormatterHelper;
@@ -40,6 +40,7 @@ use Symfony\Component\Console\Input\StreamableInputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -81,7 +82,7 @@ class Application
* @param string $name The name of the application
* @param string $version The version of the application
*/
public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
$this->name = $name;
$this->version = $version;
@@ -121,7 +122,7 @@ class Application
$renderException = function ($e) use ($output) {
if (!$e instanceof \Exception) {
$e = class_exists(FatalThrowableError::class) ? new FatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
$e = class_exists(FatalThrowableError::class) ? new FatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
}
if ($output instanceof ConsoleOutputInterface) {
$this->renderException($e, $output->getErrorOutput());
@@ -138,10 +139,6 @@ class Application
}
}
if (null !== $this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) {
@trigger_error(sprintf('The "ConsoleEvents::EXCEPTION" event is deprecated since Symfony 3.3 and will be removed in 4.0. Listen to the "ConsoleEvents::ERROR" event instead.'), \E_USER_DEPRECATED);
}
$this->configureIO($input, $output);
try {
@@ -231,24 +228,41 @@ class Application
}
try {
$e = $this->runningCommand = null;
$this->runningCommand = null;
// the command name MUST be the first element of the input
$command = $this->find($name);
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
if (null !== $e) {
if (null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e);
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
$e = $event->getError();
if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) {
if (null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e);
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
if (0 === $event->getExitCode()) {
return 0;
if (0 === $event->getExitCode()) {
return 0;
}
$e = $event->getError();
}
throw $e;
}
throw $e;
$alternative = $alternatives[0];
$style = new SymfonyStyle($input, $output);
$style->block(sprintf("\nCommand \"%s\" is not defined.\n", $name), null, 'error');
if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
if (null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e);
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
return $event->getExitCode();
}
return 1;
}
$command = $this->find($alternative);
}
$this->runningCommand = $command;
@@ -454,11 +468,12 @@ class Application
if (!$command->isEnabled()) {
$command->setApplication(null);
return null;
return;
}
// Will throw if the command is not correctly initialized.
$command->getDefinition();
if (null === $command->getDefinition()) {
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', \get_class($command)));
}
if (!$command->getName()) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', \get_class($command)));
@@ -490,11 +505,6 @@ class Application
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
}
// When the command has a different name than the one used at the command loader level
if (!isset($this->commands[$name])) {
throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
}
$command = $this->commands[$name];
if ($this->wantHelps) {
@@ -534,10 +544,6 @@ class Application
{
$namespaces = [];
foreach ($this->all() as $command) {
if ($command->isHidden()) {
continue;
}
$namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
foreach ($command->getAliases() as $alias) {
@@ -555,7 +561,7 @@ class Application
*
* @return string A registered namespace
*
* @throws CommandNotFoundException When namespace is incorrect or ambiguous
* @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
*/
public function findNamespace($namespace)
{
@@ -576,12 +582,12 @@ class Application
$message .= implode("\n ", $alternatives);
}
throw new CommandNotFoundException($message, $alternatives);
throw new NamespaceNotFoundException($message, $alternatives);
}
$exact = \in_array($namespace, $namespaces, true);
if (\count($namespaces) > 1 && !$exact) {
throw new CommandNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
}
return $exact ? $namespace : reset($namespaces);
@@ -613,10 +619,6 @@ class Application
}
}
if ($this->has($name)) {
return $this->get($name);
}
$allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
$expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
$commands = preg_grep('{^'.$expr.'}', $allCommands);
@@ -635,11 +637,6 @@ class Application
$message = sprintf('Command "%s" is not defined.', $name);
if ($alternatives = $this->findAlternatives($name, $allCommands)) {
// remove hidden commands
$alternatives = array_filter($alternatives, function ($name) {
return !$this->get($name)->isHidden();
});
if (1 == \count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
@@ -648,19 +645,14 @@ class Application
$message .= implode("\n ", $alternatives);
}
throw new CommandNotFoundException($message, array_values($alternatives));
throw new CommandNotFoundException($message, $alternatives);
}
// filter out aliases for commands which are already on the list
if (\count($commands) > 1) {
$commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands;
$commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) {
if (!$commandList[$nameOrAlias] instanceof Command) {
$commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
}
$commandName = $commandList[$nameOrAlias]->getName();
$commands = array_unique(array_filter($commands, function ($nameOrAlias) use ($commandList, $commands, &$aliases) {
$commandName = $commandList[$nameOrAlias] instanceof Command ? $commandList[$nameOrAlias]->getName() : $nameOrAlias;
$aliases[$nameOrAlias] = $commandName;
return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
@@ -676,17 +668,16 @@ class Application
$maxLen = max(Helper::strlen($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) {
if ($commandList[$cmd]->isHidden()) {
return false;
if (!$commandList[$cmd] instanceof Command) {
return $cmd;
}
$abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
}, array_values($commands));
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
$suggestions = $this->getAbbreviationSuggestions($abbrevs);
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
}
return $this->get($exact ? $name : reset($commands));
@@ -778,17 +769,21 @@ class Application
do {
$message = trim($e->getMessage());
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$title = sprintf(' [%s%s] ', \get_class($e), 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
$class = \get_class($e);
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
$title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
$len = Helper::strlen($title);
} else {
$len = 0;
}
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
// HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327
if (\defined('HHVM_VERSION') && $width > 1 << 31) {
$width = 1 << 31;
if (false !== strpos($message, "class@anonymous\0")) {
$message = preg_replace_callback('/class@anonymous\x00.*?\.php0x?[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
}, $message);
}
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;
$lines = [];
foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
@@ -832,11 +827,11 @@ class Application
for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
$class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
$type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
$function = isset($trace[$i]['function']) ? $trace[$i]['function'] : '';
$function = $trace[$i]['function'];
$file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
$line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
$output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
$output->writeln(sprintf(' %s%s%s() at <info>%s:%s</info>', $class, $type, $function, $file, $line), OutputInterface::VERBOSITY_QUIET);
}
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
@@ -844,70 +839,6 @@ class Application
} while ($e = $e->getPrevious());
}
/**
* Tries to figure out the terminal width in which this application runs.
*
* @return int|null
*
* @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
*/
protected function getTerminalWidth()
{
@trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->terminal->getWidth();
}
/**
* Tries to figure out the terminal height in which this application runs.
*
* @return int|null
*
* @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
*/
protected function getTerminalHeight()
{
@trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->terminal->getHeight();
}
/**
* Tries to figure out the terminal dimensions based on the current environment.
*
* @return array Array containing width and height
*
* @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
*/
public function getTerminalDimensions()
{
@trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), \E_USER_DEPRECATED);
return [$this->terminal->getWidth(), $this->terminal->getHeight()];
}
/**
* Sets terminal dimensions.
*
* Can be useful to force terminal dimensions for functional tests.
*
* @param int $width The width
* @param int $height The height
*
* @return $this
*
* @deprecated since version 3.2, to be removed in 4.0. Set the COLUMNS and LINES env vars instead.
*/
public function setTerminalDimensions($width, $height)
{
@trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Set the COLUMNS and LINES env vars instead.', __METHOD__), \E_USER_DEPRECATED);
putenv('COLUMNS='.$width);
putenv('LINES='.$height);
return $this;
}
/**
* Configures the input and output instances based on the user arguments and options.
*/
@@ -928,12 +859,6 @@ class Application
$inputStream = $input->getStream();
}
// This check ensures that calling QuestionHelper::setInputStream() works
// To be removed in 4.0 (in the same time as QuestionHelper::setInputStream)
if (!$inputStream && $this->getHelperSet()->has('question')) {
$inputStream = $this->getHelperSet()->get('question')->getInputStream(false);
}
if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) {
$input->setInteractive(false);
}
@@ -1011,19 +936,7 @@ class Application
} else {
$exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
if (null !== $e) {
if ($this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) {
$x = $e instanceof \Exception ? $e : new FatalThrowableError($e);
$event = new ConsoleExceptionEvent($command, $input, $output, $x, $x->getCode());
$this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event);
if ($x !== $event->getException()) {
$e = $event->getException();
}
}
$event = new ConsoleErrorEvent($input, $output, $e, $command);
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
$e = $event->getError();
@@ -1046,7 +959,7 @@ class Application
/**
* Gets the name of the command based on input.
*
* @return string|null
* @return string The command name
*/
protected function getCommandName(InputInterface $input)
{
@@ -1122,7 +1035,8 @@ class Application
*/
public function extractNamespace($name, $limit = null)
{
$parts = explode(':', $name, -1);
$parts = explode(':', $name);
array_pop($parts);
return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit));
}
@@ -1173,7 +1087,7 @@ class Application
}
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
return array_keys($alternatives);
}
+30 -2
View File
@@ -1,6 +1,34 @@
CHANGELOG
=========
4.2.0
-----
* allowed passing commands as `[$process, 'ENV_VAR' => 'value']` to
`ProcessHelper::run()` to pass environment variables
* deprecated passing a command as a string to `ProcessHelper::run()`,
pass it the command as an array of its arguments instead
* made the `ProcessHelper` class final
* added `WrappableOutputFormatterInterface::formatAndWrap()` (implemented in `OutputFormatter`)
* added `capture_stderr_separately` option to `CommandTester::execute()`
4.1.0
-----
* 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
* added support for iterable messages in output `write` and `writeln` methods
4.0.0
-----
* `OutputFormatter` throws an exception when unknown options are used
* removed `QuestionHelper::setInputStream()/getInputStream()`
* removed `Application::getTerminalWidth()/getTerminalHeight()` and
`Application::setTerminalDimensions()/getTerminalDimensions()`
* removed `ConsoleExceptionEvent`
* removed `ConsoleEvents::EXCEPTION`
3.4.0
-----
@@ -22,7 +50,7 @@ CHANGELOG
with value optional explicitly passed empty
* added console.error event to catch exceptions thrown by other listeners
* deprecated console.exception event in favor of console.error
* added ability to handle `CommandNotFoundException` through the
* added ability to handle `CommandNotFoundException` through the
`console.error` event
* deprecated default validation in `SymfonyQuestionHelper::ask`
@@ -38,7 +66,7 @@ CHANGELOG
-----
* added truncate method to FormatterHelper
* added setColumnWidth(s) method to Table
* added setColumnWidth(s) method to Table
2.8.3
-----
+14 -26
View File
@@ -40,8 +40,8 @@ class Command
private $aliases = [];
private $definition;
private $hidden = false;
private $help = '';
private $description = '';
private $help;
private $description;
private $ignoreValidationErrors = false;
private $applicationDefinitionMerged = false;
private $applicationDefinitionMergedWithArgs = false;
@@ -55,7 +55,7 @@ class Command
*/
public static function getDefaultName()
{
$class = static::class;
$class = \get_called_class();
$r = new \ReflectionProperty($class, 'defaultName');
return $class === $r->class ? static::$defaultName : null;
@@ -66,7 +66,7 @@ class Command
*
* @throws LogicException When the command name is empty
*/
public function __construct($name = null)
public function __construct(string $name = null)
{
$this->definition = new InputDefinition();
@@ -105,7 +105,7 @@ class Command
/**
* Gets the helper set.
*
* @return HelperSet|null A HelperSet instance
* @return HelperSet A HelperSet instance
*/
public function getHelperSet()
{
@@ -115,7 +115,7 @@ class Command
/**
* Gets the application instance for this command.
*
* @return Application|null An Application instance
* @return Application An Application instance
*/
public function getApplication()
{
@@ -223,7 +223,7 @@ class Command
if (null !== $this->processTitle) {
if (\function_exists('cli_set_process_title')) {
if (!@cli_set_process_title($this->processTitle)) {
if ('Darwin' === \PHP_OS) {
if ('Darwin' === PHP_OS) {
$output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
} else {
cli_set_process_title($this->processTitle);
@@ -250,7 +250,7 @@ class Command
$input->validate();
if ($this->code) {
$statusCode = \call_user_func($this->code, $input, $output);
$statusCode = ($this->code)($input, $output);
} else {
$statusCode = $this->execute($input, $output);
}
@@ -277,15 +277,7 @@ class Command
if ($code instanceof \Closure) {
$r = new \ReflectionFunction($code);
if (null === $r->getClosureThis()) {
if (\PHP_VERSION_ID < 70000) {
// Bug in PHP5: https://bugs.php.net/64761
// This means that we cannot bind static closures and therefore we must
// ignore any errors here. There is no way to test if the closure is
// bindable.
$code = @\Closure::bind($code, $this);
} else {
$code = \Closure::bind($code, $this);
}
$code = \Closure::bind($code, $this);
}
}
@@ -347,10 +339,6 @@ class Command
*/
public function getDefinition()
{
if (null === $this->definition) {
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
return $this->definition;
}
@@ -437,6 +425,8 @@ class Command
* This feature should be used only when creating a long process command,
* like a daemon.
*
* PHP 5.5+ or the proctitle PECL library is required
*
* @param string $title The process title
*
* @return $this
@@ -451,7 +441,7 @@ class Command
/**
* Returns the command name.
*
* @return string|null
* @return string The command name
*/
public function getName()
{
@@ -561,7 +551,7 @@ class Command
public function setAliases($aliases)
{
if (!\is_array($aliases) && !$aliases instanceof \Traversable) {
throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable.');
throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable');
}
foreach ($aliases as $alias) {
@@ -653,11 +643,9 @@ class Command
*
* It must be non-empty and parts can optionally be separated by ":".
*
* @param string $name
*
* @throws InvalidArgumentException When the name is invalid
*/
private function validateName($name)
private function validateName(string $name)
{
if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
+2 -3
View File
@@ -12,7 +12,6 @@
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\Store\FlockStore;
@@ -36,14 +35,14 @@ trait LockableTrait
private function lock($name = null, $blocking = false)
{
if (!class_exists(SemaphoreStore::class)) {
throw new RuntimeException('To enable the locking feature you must install the symfony/lock component.');
throw new LogicException('To enable the locking feature you must install the symfony/lock component.');
}
if (null !== $this->lock) {
throw new LogicException('A lock is already in place.');
}
if (SemaphoreStore::isSupported($blocking)) {
if (SemaphoreStore::isSupported()) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore();
-13
View File
@@ -35,19 +35,6 @@ final class ConsoleEvents
*/
const TERMINATE = 'console.terminate';
/**
* The EXCEPTION event occurs when an uncaught exception appears
* while executing Command#run().
*
* This event allows you to deal with the exception or
* to modify the thrown exception.
*
* @Event("Symfony\Component\Console\Event\ConsoleExceptionEvent")
*
* @deprecated The console.exception event is deprecated since version 3.3 and will be removed in 4.0. Use the console.error event instead.
*/
const EXCEPTION = 'console.exception';
/**
* The ERROR event occurs when an uncaught exception or error appears.
*
@@ -29,7 +29,7 @@ class AddConsoleCommandPass implements CompilerPassInterface
private $commandLoaderServiceId;
private $commandTag;
public function __construct($commandLoaderServiceId = 'console.command_loader', $commandTag = 'console.command')
public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command')
{
$this->commandLoaderServiceId = $commandLoaderServiceId;
$this->commandTag = $commandTag;
@@ -41,14 +41,11 @@ class AddConsoleCommandPass implements CompilerPassInterface
$lazyCommandMap = [];
$lazyCommandRefs = [];
$serviceIds = [];
$lazyServiceIds = [];
foreach ($commandServices as $id => $tags) {
$definition = $container->getDefinition($id);
$class = $container->getParameterBag()->resolveValue($definition->getClass());
$commandId = 'console.command.'.strtolower(str_replace('\\', '_', $class));
if (isset($tags[0]['command'])) {
$commandName = $tags[0]['command'];
} else {
@@ -62,20 +59,16 @@ class AddConsoleCommandPass implements CompilerPassInterface
}
if (null === $commandName) {
if (isset($serviceIds[$commandId]) || $container->hasAlias($commandId)) {
$commandId = $commandId.'_'.$id;
}
if (!$definition->isPublic() || $definition->isPrivate()) {
$commandId = 'console.command.public_alias.'.$id;
$container->setAlias($commandId, $id)->setPublic(true);
$id = $commandId;
}
$serviceIds[$commandId] = $id;
$serviceIds[] = $id;
continue;
}
$serviceIds[$commandId] = $id;
$lazyServiceIds[$id] = true;
unset($tags[0]);
$lazyCommandMap[$commandName] = $id;
$lazyCommandRefs[$id] = new TypedReference($id, $class);
@@ -101,6 +94,5 @@ class AddConsoleCommandPass implements CompilerPassInterface
->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);
$container->setParameter('console.command.ids', $serviceIds);
$container->setParameter('console.lazy_command.ids', $lazyServiceIds);
}
}
+12 -25
View File
@@ -43,11 +43,7 @@ class ApplicationDescription
*/
private $aliases;
/**
* @param string|null $namespace
* @param bool $showHidden
*/
public function __construct(Application $application, $namespace = null, $showHidden = false)
public function __construct(Application $application, string $namespace = null, bool $showHidden = false)
{
$this->application = $application;
$this->namespace = $namespace;
@@ -88,7 +84,7 @@ class ApplicationDescription
public function getCommand($name)
{
if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
throw new CommandNotFoundException(sprintf('Command %s does not exist.', $name));
}
return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name];
@@ -122,36 +118,27 @@ class ApplicationDescription
}
}
/**
* @return array
*/
private function sortCommands(array $commands)
private function sortCommands(array $commands): array
{
$namespacedCommands = [];
$globalCommands = [];
$sortedCommands = [];
foreach ($commands as $name => $command) {
$key = $this->application->extractNamespace($name, 1);
if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) {
$globalCommands[$name] = $command;
if (!$key) {
$globalCommands['_global'][$name] = $command;
} else {
$namespacedCommands[$key][$name] = $command;
}
}
ksort($namespacedCommands);
$namespacedCommands = array_merge($globalCommands, $namespacedCommands);
if ($globalCommands) {
ksort($globalCommands);
$sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands;
foreach ($namespacedCommands as &$commandsSet) {
ksort($commandsSet);
}
// unset reference to keep scope clear
unset($commandsSet);
if ($namespacedCommands) {
ksort($namespacedCommands);
foreach ($namespacedCommands as $key => $commandsSet) {
ksort($commandsSet);
$sortedCommands[$key] = $commandsSet;
}
}
return $sortedCommands;
return $namespacedCommands;
}
}
+3 -1
View File
@@ -23,7 +23,9 @@ interface DescriptorInterface
/**
* Describes an object if supported.
*
* @param object $object
* @param OutputInterface $output
* @param object $object
* @param array $options
*/
public function describe(OutputInterface $output, $object, array $options = []);
}
+4 -2
View File
@@ -92,6 +92,8 @@ class JsonDescriptor extends Descriptor
/**
* Writes data as json.
*
* @return array|string
*/
private function writeData(array $data, array $options)
{
@@ -110,7 +112,7 @@ class JsonDescriptor extends Descriptor
'is_required' => $argument->isRequired(),
'is_array' => $argument->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()),
'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
'default' => INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
];
}
@@ -126,7 +128,7 @@ class JsonDescriptor extends Descriptor
'is_value_required' => $option->isValueRequired(),
'is_multiple' => $option->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()),
'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(),
'default' => INF === $option->getDefault() ? 'INF' : $option->getDefault(),
];
}
+17 -17
View File
@@ -140,6 +140,13 @@ class TextDescriptor extends Descriptor
$command->getSynopsis(false);
$command->mergeApplicationDefinition(false);
if ($description = $command->getDescription()) {
$this->writeText('<comment>Description:</comment>', $options);
$this->writeText("\n");
$this->writeText(' '.$description);
$this->writeText("\n\n");
}
$this->writeText('<comment>Usage:</comment>', $options);
foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
$this->writeText("\n");
@@ -154,7 +161,8 @@ class TextDescriptor extends Descriptor
$this->writeText("\n");
}
if ($help = $command->getProcessedHelp()) {
$help = $command->getProcessedHelp();
if ($help && $help !== $description) {
$this->writeText("\n");
$this->writeText('<comment>Help:</comment>', $options);
$this->writeText("\n");
@@ -202,9 +210,9 @@ class TextDescriptor extends Descriptor
}
// calculate max. width based on available commands per namespace
$width = $this->getColumnWidth(\call_user_func_array('array_merge', array_map(function ($namespace) use ($commands) {
$width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) {
return array_intersect($namespace['commands'], array_keys($commands));
}, array_values($namespaces))));
}, $namespaces))));
if ($describedNamespace) {
$this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
@@ -252,10 +260,8 @@ class TextDescriptor extends Descriptor
/**
* Formats command aliases to show them in the command description.
*
* @return string
*/
private function getCommandAliasesText(Command $command)
private function getCommandAliasesText(Command $command): string
{
$text = '';
$aliases = $command->getAliases();
@@ -271,12 +277,10 @@ class TextDescriptor extends Descriptor
* Formats input option/argument default value.
*
* @param mixed $default
*
* @return string
*/
private function formatDefaultValue($default)
private function formatDefaultValue($default): string
{
if (\INF === $default) {
if (INF === $default) {
return 'INF';
}
@@ -290,15 +294,13 @@ class TextDescriptor extends Descriptor
}
}
return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
return str_replace('\\\\', '\\', json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
/**
* @param (Command|string)[] $commands
*
* @return int
*/
private function getColumnWidth(array $commands)
private function getColumnWidth(array $commands): int
{
$widths = [];
@@ -318,10 +320,8 @@ class TextDescriptor extends Descriptor
/**
* @param InputOption[] $options
*
* @return int
*/
private function calculateTotalWidthForOptions(array $options)
private function calculateTotalWidthForOptions(array $options): int
{
$totalWidth = 0;
foreach ($options as $option) {
+5 -8
View File
@@ -81,6 +81,7 @@ class XmlDescriptor extends Descriptor
}
/**
* @param Application $application
* @param string|null $namespace
*
* @return \DOMDocument
@@ -178,6 +179,8 @@ class XmlDescriptor extends Descriptor
/**
* Writes DOM document.
*
* @return \DOMDocument|string
*/
private function writeDocument(\DOMDocument $dom)
{
@@ -185,10 +188,7 @@ class XmlDescriptor extends Descriptor
$this->write($dom->saveXML());
}
/**
* @return \DOMDocument
*/
private function getInputArgumentDocument(InputArgument $argument)
private function getInputArgumentDocument(InputArgument $argument): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
@@ -209,10 +209,7 @@ class XmlDescriptor extends Descriptor
return $dom;
}
/**
* @return \DOMDocument
*/
private function getInputOptionDocument(InputOption $option)
private function getInputOptionDocument(InputOption $option): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
+14 -39
View File
@@ -12,7 +12,6 @@
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -26,57 +25,33 @@ final class ConsoleErrorEvent extends ConsoleEvent
private $error;
private $exitCode;
public function __construct(InputInterface $input, OutputInterface $output, $error, Command $command = null)
public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, Command $command = null)
{
parent::__construct($command, $input, $output);
$this->setError($error);
}
/**
* Returns the thrown error/exception.
*
* @return \Throwable
*/
public function getError()
{
return $this->error;
}
/**
* Replaces the thrown error/exception.
*
* @param \Throwable $error
*/
public function setError($error)
{
if (!$error instanceof \Throwable && !$error instanceof \Exception) {
throw new InvalidArgumentException(sprintf('The error passed to ConsoleErrorEvent must be an instance of \Throwable or \Exception, "%s" was passed instead.', \is_object($error) ? \get_class($error) : \gettype($error)));
}
$this->error = $error;
}
/**
* Sets the exit code.
*
* @param int $exitCode The command exit code
*/
public function setExitCode($exitCode)
public function getError(): \Throwable
{
$this->exitCode = (int) $exitCode;
return $this->error;
}
public function setError(\Throwable $error): void
{
$this->error = $error;
}
public function setExitCode(int $exitCode): void
{
$this->exitCode = $exitCode;
$r = new \ReflectionProperty($this->error, 'code');
$r->setAccessible(true);
$r->setValue($this->error, $this->exitCode);
}
/**
* Gets the exit code.
*
* @return int The command exit code
*/
public function getExitCode()
public function getExitCode(): int
{
return null !== $this->exitCode ? $this->exitCode : (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
}
-71
View File
@@ -1,71 +0,0 @@
<?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\Event;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 3.3 and will be removed in 4.0. Use the ConsoleErrorEvent instead.', ConsoleExceptionEvent::class), \E_USER_DEPRECATED);
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Allows to handle exception thrown in a command.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since version 3.3, to be removed in 4.0. Use ConsoleErrorEvent instead.
*/
class ConsoleExceptionEvent extends ConsoleEvent
{
private $exception;
private $exitCode;
public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode)
{
parent::__construct($command, $input, $output);
$this->setException($exception);
$this->exitCode = (int) $exitCode;
}
/**
* Returns the thrown exception.
*
* @return \Exception The thrown exception
*/
public function getException()
{
return $this->exception;
}
/**
* Replaces the thrown exception.
*
* This exception will be thrown if no response is set in the event.
*
* @param \Exception $exception The thrown exception
*/
public function setException(\Exception $exception)
{
$this->exception = $exception;
}
/**
* Gets the exit code.
*
* @return int The command exit code
*/
public function getExitCode()
{
return $this->exitCode;
}
}
+1 -6
View File
@@ -22,14 +22,9 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class ConsoleTerminateEvent extends ConsoleEvent
{
/**
* The exit code of the command.
*
* @var int
*/
private $exitCode;
public function __construct(Command $command, InputInterface $input, OutputInterface $output, $exitCode)
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $exitCode)
{
parent::__construct($command, $input, $output);
+2 -6
View File
@@ -40,9 +40,7 @@ class ErrorListener implements EventSubscriberInterface
$error = $event->getError();
if (!$inputString = $this->getInputString($event)) {
$this->logger->error('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
return;
return $this->logger->error('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
}
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
@@ -61,9 +59,7 @@ class ErrorListener implements EventSubscriberInterface
}
if (!$inputString = $this->getInputString($event)) {
$this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]);
return;
return $this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]);
}
$this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]);
@@ -26,7 +26,7 @@ class CommandNotFoundException extends \InvalidArgumentException implements Exce
* @param int $code Exception code
* @param \Exception $previous Previous exception used for the exception chaining
*/
public function __construct($message, array $alternatives = [], $code = 0, \Exception $previous = null)
public function __construct(string $message, array $alternatives = [], int $code = 0, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
+1 -1
View File
@@ -16,6 +16,6 @@ namespace Symfony\Component\Console\Exception;
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
interface ExceptionInterface
interface ExceptionInterface extends \Throwable
{
}
+66 -27
View File
@@ -17,8 +17,9 @@ use Symfony\Component\Console\Exception\InvalidArgumentException;
* Formatter class for console output.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class OutputFormatter implements OutputFormatterInterface
class OutputFormatter implements WrappableOutputFormatterInterface
{
private $decorated;
private $styles = [];
@@ -65,9 +66,9 @@ class OutputFormatter implements OutputFormatterInterface
* @param bool $decorated Whether this formatter should actually decorate strings
* @param OutputFormatterStyleInterface[] $styles Array of "name => FormatterStyle" instances
*/
public function __construct($decorated = false, array $styles = [])
public function __construct(bool $decorated = false, array $styles = [])
{
$this->decorated = (bool) $decorated;
$this->decorated = $decorated;
$this->setStyle('error', new OutputFormatterStyle('white', 'red'));
$this->setStyle('info', new OutputFormatterStyle('green'));
@@ -119,7 +120,7 @@ class OutputFormatter implements OutputFormatterInterface
public function getStyle($name)
{
if (!$this->hasStyle($name)) {
throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name));
throw new InvalidArgumentException(sprintf('Undefined style: %s', $name));
}
return $this->styles[strtolower($name)];
@@ -130,11 +131,19 @@ class OutputFormatter implements OutputFormatterInterface
*/
public function format($message)
{
$message = (string) $message;
return $this->formatAndWrap((string) $message, 0);
}
/**
* {@inheritdoc}
*/
public function formatAndWrap(string $message, int $width)
{
$offset = 0;
$output = '';
$tagRegex = '[a-z][a-z0-9,_=;-]*+';
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
$currentLineLength = 0;
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $i => $match) {
$pos = $match[1];
$text = $match[0];
@@ -144,7 +153,7 @@ class OutputFormatter implements OutputFormatterInterface
}
// add the text up to the next tag
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset));
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
$offset = $pos + \strlen($text);
// opening tag?
@@ -158,7 +167,7 @@ class OutputFormatter implements OutputFormatterInterface
// </>
$this->styleStack->pop();
} elseif (false === $style = $this->createStyleFromString($tag)) {
$output .= $this->applyCurrentStyle($text);
$output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength);
} elseif ($open) {
$this->styleStack->push($style);
} else {
@@ -166,7 +175,7 @@ class OutputFormatter implements OutputFormatterInterface
}
}
$output .= $this->applyCurrentStyle(substr($message, $offset));
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
if (false !== strpos($output, "\0")) {
return strtr($output, ["\0" => '\\', '\\<' => '<']);
@@ -186,17 +195,15 @@ class OutputFormatter implements OutputFormatterInterface
/**
* Tries to create new style instance from string.
*
* @param string $string
*
* @return OutputFormatterStyle|false false if string is not format string
* @return OutputFormatterStyle|false False if string is not format string
*/
private function createStyleFromString($string)
private function createStyleFromString(string $string)
{
if (isset($this->styles[$string])) {
return $this->styles[$string];
}
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) {
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, PREG_SET_ORDER)) {
return false;
}
@@ -213,13 +220,7 @@ class OutputFormatter implements OutputFormatterInterface
preg_match_all('([^,;]+)', strtolower($match[1]), $options);
$options = array_shift($options);
foreach ($options as $option) {
try {
$style->setOption($option);
} catch (\InvalidArgumentException $e) {
@trigger_error(sprintf('Unknown style options are deprecated since Symfony 3.2 and will be removed in 4.0. Exception "%s".', $e->getMessage()), \E_USER_DEPRECATED);
return false;
}
$style->setOption($option);
}
} else {
return false;
@@ -231,13 +232,51 @@ class OutputFormatter implements OutputFormatterInterface
/**
* Applies current style from stack to text, if must be applied.
*
* @param string $text Input text
*
* @return string Styled text
*/
private function applyCurrentStyle($text)
private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string
{
return $this->isDecorated() && \strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text;
if ('' === $text) {
return '';
}
if (!$width) {
return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text;
}
if (!$currentLineLength && '' !== $current) {
$text = ltrim($text);
}
if ($currentLineLength) {
$prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
$text = substr($text, $i);
} else {
$prefix = '';
}
preg_match('~(\\n)$~', $text, $matches);
$text = $prefix.preg_replace('~([^\\n]{'.$width.'})\\ *~', "\$1\n", $text);
$text = rtrim($text, "\n").($matches[1] ?? '');
if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) {
$text = "\n".$text;
}
$lines = explode("\n", $text);
foreach ($lines as $line) {
$currentLineLength += \strlen($line);
if ($width <= $currentLineLength) {
$currentLineLength = 0;
}
}
if ($this->isDecorated()) {
foreach ($lines as $i => $line) {
$lines[$i] = $this->styleStack->getCurrent()->apply($line);
}
}
return implode("\n", $lines);
}
}
+5 -5
View File
@@ -61,7 +61,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
* @param string|null $background The style background color name
* @param array $options The style options
*/
public function __construct($foreground = null, $background = null, array $options = [])
public function __construct(string $foreground = null, string $background = null, array $options = [])
{
if (null !== $foreground) {
$this->setForeground($foreground);
@@ -86,7 +86,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
}
if (!isset(static::$availableForegroundColors[$color])) {
throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableForegroundColors))));
throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors))));
}
$this->foreground = static::$availableForegroundColors[$color];
@@ -104,7 +104,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
}
if (!isset(static::$availableBackgroundColors[$color])) {
throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
}
$this->background = static::$availableBackgroundColors[$color];
@@ -116,7 +116,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
public function setOption($option)
{
if (!isset(static::$availableOptions[$option])) {
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions))));
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
}
if (!\in_array(static::$availableOptions[$option], $this->options)) {
@@ -130,7 +130,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
public function unsetOption($option)
{
if (!isset(static::$availableOptions[$option])) {
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions))));
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
}
$pos = array_search(static::$availableOptions[$option], $this->options);
@@ -12,11 +12,12 @@
namespace Symfony\Component\Console\Formatter;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Contracts\Service\ResetInterface;
/**
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class OutputFormatterStyleStack
class OutputFormatterStyleStack implements ResetInterface
{
/**
* @var OutputFormatterStyleInterface[]
+5 -2
View File
@@ -48,7 +48,9 @@ class DescriptorHelper extends Helper
* * format: string, the output format name
* * raw_text: boolean, sets output type as raw
*
* @param object $object
* @param OutputInterface $output
* @param object $object
* @param array $options
*
* @throws InvalidArgumentException when the given format is not supported
*/
@@ -70,7 +72,8 @@ class DescriptorHelper extends Helper
/**
* Registers a descriptor.
*
* @param string $format
* @param string $format
* @param DescriptorInterface $descriptor
*
* @return $this
*/
+9 -5
View File
@@ -54,12 +54,12 @@ class FormatterHelper extends Helper
foreach ($messages as $message) {
$message = OutputFormatter::escape($message);
$lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
$len = max(self::strlen($message) + ($large ? 4 : 2), $len);
$len = max($this->strlen($message) + ($large ? 4 : 2), $len);
}
$messages = $large ? [str_repeat(' ', $len)] : [];
for ($i = 0; isset($lines[$i]); ++$i) {
$messages[] = $lines[$i].str_repeat(' ', $len - self::strlen($lines[$i]));
$messages[] = $lines[$i].str_repeat(' ', $len - $this->strlen($lines[$i]));
}
if ($large) {
$messages[] = str_repeat(' ', $len);
@@ -83,13 +83,17 @@ class FormatterHelper extends Helper
*/
public function truncate($message, $length, $suffix = '...')
{
$computedLength = $length - self::strlen($suffix);
$computedLength = $length - $this->strlen($suffix);
if ($computedLength > self::strlen($message)) {
if ($computedLength > $this->strlen($message)) {
return $message;
}
return self::substr($message, 0, $length).$suffix;
if (false === $encoding = mb_detect_encoding($message, null, true)) {
return substr($message, 0, $length).$suffix;
}
return mb_substr($message, 0, $length, $encoding).$suffix;
}
/**
+25 -14
View File
@@ -20,27 +20,25 @@ use Symfony\Component\Process\Process;
* The ProcessHelper class provides helpers to run external processes.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.2
*/
class ProcessHelper extends Helper
{
/**
* Runs an external process.
*
* @param OutputInterface $output An OutputInterface instance
* @param string|array|Process $cmd An instance of Process or an array of arguments to escape and run or a command to run
* @param string|null $error An error message that must be displayed if something went wrong
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param int $verbosity The threshold for verbosity
* @param OutputInterface $output An OutputInterface instance
* @param array|Process $cmd An instance of Process or an array of the command and arguments
* @param string|null $error An error message that must be displayed if something went wrong
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param int $verbosity The threshold for verbosity
*
* @return Process The process that ran
*/
public function run(OutputInterface $output, $cmd, $error = null, callable $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE)
{
if (!class_exists(Process::class)) {
throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".');
}
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
@@ -48,9 +46,22 @@ class ProcessHelper extends Helper
$formatter = $this->getHelperSet()->get('debug_formatter');
if ($cmd instanceof Process) {
$process = $cmd;
} else {
$cmd = [$cmd];
}
if (!\is_array($cmd)) {
@trigger_error(sprintf('Passing a command as a string to "%s()" is deprecated since Symfony 4.2, pass it the command as an array of arguments instead.', __METHOD__), E_USER_DEPRECATED);
$cmd = [method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($cmd) : new Process($cmd)];
}
if (\is_string($cmd[0] ?? null)) {
$process = new Process($cmd);
$cmd = [];
} elseif (($cmd[0] ?? null) instanceof Process) {
$process = $cmd[0];
unset($cmd[0]);
} else {
throw new \InvalidArgumentException(sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__));
}
if ($verbosity <= $output->getVerbosity()) {
@@ -61,7 +72,7 @@ class ProcessHelper extends Helper
$callback = $this->wrapCallback($output, $process, $callback);
}
$process->run($callback);
$process->run($callback, $cmd);
if ($verbosity <= $output->getVerbosity()) {
$message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode());
@@ -125,7 +136,7 @@ class ProcessHelper extends Helper
$output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type));
if (null !== $callback) {
\call_user_func($callback, $type, $buffer);
$callback($type, $buffer);
}
};
}
+60 -147
View File
@@ -13,6 +13,7 @@ namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
@@ -50,7 +51,7 @@ final class ProgressBar
* @param OutputInterface $output An OutputInterface instance
* @param int $max Maximum steps (0 if unknown)
*/
public function __construct(OutputInterface $output, $max = 0)
public function __construct(OutputInterface $output, int $max = 0)
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
@@ -79,7 +80,7 @@ final class ProgressBar
* @param string $name The placeholder name (including the delimiter char like %)
* @param callable $callable A PHP callable
*/
public static function setPlaceholderFormatterDefinition($name, callable $callable)
public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
{
if (!self::$formatters) {
self::$formatters = self::initPlaceholderFormatters();
@@ -95,7 +96,7 @@ final class ProgressBar
*
* @return callable|null A PHP callable
*/
public static function getPlaceholderFormatterDefinition($name)
public static function getPlaceholderFormatterDefinition(string $name): ?callable
{
if (!self::$formatters) {
self::$formatters = self::initPlaceholderFormatters();
@@ -112,7 +113,7 @@ final class ProgressBar
* @param string $name The format name
* @param string $format A format string
*/
public static function setFormatDefinition($name, $format)
public static function setFormatDefinition(string $name, string $format): void
{
if (!self::$formats) {
self::$formats = self::initFormats();
@@ -128,7 +129,7 @@ final class ProgressBar
*
* @return string|null A format string
*/
public static function getFormatDefinition($name)
public static function getFormatDefinition(string $name): ?string
{
if (!self::$formats) {
self::$formats = self::initFormats();
@@ -147,102 +148,57 @@ final class ProgressBar
* @param string $message The text to associate with the placeholder
* @param string $name The name of the placeholder
*/
public function setMessage($message, $name = 'message')
public function setMessage(string $message, string $name = 'message')
{
$this->messages[$name] = $message;
}
public function getMessage($name = 'message')
public function getMessage(string $name = 'message')
{
return $this->messages[$name];
}
/**
* Gets the progress bar start time.
*
* @return int The progress bar start time
*/
public function getStartTime()
public function getStartTime(): int
{
return $this->startTime;
}
/**
* Gets the progress bar maximal steps.
*
* @return int The progress bar max steps
*/
public function getMaxSteps()
public function getMaxSteps(): int
{
return $this->max;
}
/**
* Gets the current step position.
*
* @return int The progress bar step
*/
public function getProgress()
public function getProgress(): int
{
return $this->step;
}
/**
* Gets the progress bar step width.
*
* @return int The progress bar step width
*/
private function getStepWidth()
private function getStepWidth(): int
{
return $this->stepWidth;
}
/**
* Gets the current progress bar percent.
*
* @return float The current progress bar percent
*/
public function getProgressPercent()
public function getProgressPercent(): float
{
return $this->percent;
}
/**
* Sets the progress bar width.
*
* @param int $size The progress bar size
*/
public function setBarWidth($size)
public function setBarWidth(int $size)
{
$this->barWidth = max(1, (int) $size);
$this->barWidth = max(1, $size);
}
/**
* Gets the progress bar width.
*
* @return int The progress bar size
*/
public function getBarWidth()
public function getBarWidth(): int
{
return $this->barWidth;
}
/**
* Sets the bar character.
*
* @param string $char A character
*/
public function setBarCharacter($char)
public function setBarCharacter(string $char)
{
$this->barChar = $char;
}
/**
* Gets the bar character.
*
* @return string A character
*/
public function getBarCharacter()
public function getBarCharacter(): string
{
if (null === $this->barChar) {
return $this->max ? '=' : $this->emptyBarChar;
@@ -251,52 +207,27 @@ final class ProgressBar
return $this->barChar;
}
/**
* Sets the empty bar character.
*
* @param string $char A character
*/
public function setEmptyBarCharacter($char)
public function setEmptyBarCharacter(string $char)
{
$this->emptyBarChar = $char;
}
/**
* Gets the empty bar character.
*
* @return string A character
*/
public function getEmptyBarCharacter()
public function getEmptyBarCharacter(): string
{
return $this->emptyBarChar;
}
/**
* Sets the progress bar character.
*
* @param string $char A character
*/
public function setProgressCharacter($char)
public function setProgressCharacter(string $char)
{
$this->progressChar = $char;
}
/**
* Gets the progress bar character.
*
* @return string A character
*/
public function getProgressCharacter()
public function getProgressCharacter(): string
{
return $this->progressChar;
}
/**
* Sets the progress bar format.
*
* @param string $format The format
*/
public function setFormat($format)
public function setFormat(string $format)
{
$this->format = null;
$this->internalFormat = $format;
@@ -307,9 +238,9 @@ final class ProgressBar
*
* @param int|float $freq The frequency in steps
*/
public function setRedrawFrequency($freq)
public function setRedrawFrequency(int $freq)
{
$this->redrawFreq = max((int) $freq, 1);
$this->redrawFreq = max($freq, 1);
}
/**
@@ -317,7 +248,7 @@ final class ProgressBar
*
* @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
*/
public function start($max = null)
public function start(int $max = null)
{
$this->startTime = time();
$this->step = 0;
@@ -335,30 +266,21 @@ final class ProgressBar
*
* @param int $step Number of steps to advance
*/
public function advance($step = 1)
public function advance(int $step = 1)
{
$this->setProgress($this->step + $step);
}
/**
* Sets whether to overwrite the progressbar, false for new line.
*
* @param bool $overwrite
*/
public function setOverwrite($overwrite)
public function setOverwrite(bool $overwrite)
{
$this->overwrite = (bool) $overwrite;
$this->overwrite = $overwrite;
}
/**
* Sets the current progress.
*
* @param int $step The current progress
*/
public function setProgress($step)
public function setProgress(int $step)
{
$step = (int) $step;
if ($this->max && $step > $this->max) {
$this->max = $step;
} elseif ($step < 0) {
@@ -374,10 +296,17 @@ final class ProgressBar
}
}
public function setMaxSteps(int $max)
{
$this->format = null;
$this->max = max(0, $max);
$this->stepWidth = $this->max ? Helper::strlen((string) $this->max) : 4;
}
/**
* Finishes the progress output.
*/
public function finish()
public function finish(): void
{
if (!$this->max) {
$this->max = $this->step;
@@ -394,7 +323,7 @@ final class ProgressBar
/**
* Outputs the current progress string.
*/
public function display()
public function display(): void
{
if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
return;
@@ -414,7 +343,7 @@ final class ProgressBar
* while a progress bar is running.
* Call display() to show the progress bar again.
*/
public function clear()
public function clear(): void
{
if (!$this->overwrite) {
return;
@@ -427,12 +356,7 @@ final class ProgressBar
$this->overwrite('');
}
/**
* Sets the progress bar format.
*
* @param string $format The format
*/
private function setRealFormat($format)
private function setRealFormat(string $format)
{
// try to use the _nomax variant if available
if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
@@ -446,36 +370,28 @@ final class ProgressBar
$this->formatLineCount = substr_count($this->format, "\n");
}
/**
* Sets the progress bar maximal steps.
*
* @param int $max The progress bar max steps
*/
private function setMaxSteps($max)
{
$this->max = max(0, (int) $max);
$this->stepWidth = $this->max ? Helper::strlen($this->max) : 4;
}
/**
* Overwrites a previous message to the output.
*
* @param string $message The message
*/
private function overwrite($message)
private function overwrite(string $message): void
{
if ($this->overwrite) {
if (!$this->firstRun) {
// Erase previous lines
if ($this->formatLineCount > 0) {
$message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message;
}
if ($this->output instanceof ConsoleSectionOutput) {
$lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
$this->output->clear($lines);
} else {
// Erase previous lines
if ($this->formatLineCount > 0) {
$message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message;
}
// Move the cursor to the beginning of the line and erase the line
$message = "\x0D\x1B[2K$message";
// Move the cursor to the beginning of the line and erase the line
$message = "\x0D\x1B[2K$message";
}
}
} elseif ($this->step > 0) {
$message = \PHP_EOL.$message;
$message = PHP_EOL.$message;
}
$this->firstRun = false;
@@ -483,7 +399,7 @@ final class ProgressBar
$this->output->write($message);
}
private function determineBestFormat()
private function determineBestFormat(): string
{
switch ($this->output->getVerbosity()) {
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
@@ -498,7 +414,7 @@ final class ProgressBar
}
}
private static function initPlaceholderFormatters()
private static function initPlaceholderFormatters(): array
{
return [
'bar' => function (self $bar, OutputInterface $output) {
@@ -544,7 +460,7 @@ final class ProgressBar
return Helper::formatMemory(memory_get_usage(true));
},
'current' => function (self $bar) {
return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT);
return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT);
},
'max' => function (self $bar) {
return $bar->getMaxSteps();
@@ -555,7 +471,7 @@ final class ProgressBar
];
}
private static function initFormats()
private static function initFormats(): array
{
return [
'normal' => ' %current%/%max% [%bar%] %percent:3s%%',
@@ -572,15 +488,12 @@ final class ProgressBar
];
}
/**
* @return string
*/
private function buildLine()
private function buildLine(): string
{
$regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
$callback = function ($matches) {
if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) {
$text = \call_user_func($formatter, $this, $this->output);
$text = $formatter($this, $this->output);
} elseif (isset($this->messages[$matches[1]])) {
$text = $this->messages[$matches[1]];
} else {
+7 -8
View File
@@ -34,11 +34,12 @@ class ProgressIndicator
private static $formats;
/**
* @param string|null $format Indicator format
* @param int $indicatorChangeInterval Change interval in milliseconds
* @param array|null $indicatorValues Animated indicator characters
* @param OutputInterface $output
* @param string|null $format Indicator format
* @param int $indicatorChangeInterval Change interval in milliseconds
* @param array|null $indicatorValues Animated indicator characters
*/
public function __construct(OutputInterface $output, $format = null, $indicatorChangeInterval = 100, $indicatorValues = null)
public function __construct(OutputInterface $output, string $format = null, int $indicatorChangeInterval = 100, array $indicatorValues = null)
{
$this->output = $output;
@@ -195,7 +196,7 @@ class ProgressIndicator
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) {
if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) {
return \call_user_func($formatter, $self);
return $formatter($self);
}
return $matches[0];
@@ -218,10 +219,8 @@ class ProgressIndicator
/**
* Overwrites a previous message to the output.
*
* @param string $message The message
*/
private function overwrite($message)
private function overwrite(string $message)
{
if ($this->output->isDecorated()) {
$this->output->write("\x0D\x1B[2K");
+41 -85
View File
@@ -11,17 +11,16 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\StreamableInputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;
/**
* The QuestionHelper class provides helpers to interact with the user.
@@ -32,7 +31,7 @@ class QuestionHelper extends Helper
{
private $inputStream;
private static $shell;
private static $stty = true;
private static $stty;
/**
* Asks a question to the user.
@@ -88,46 +87,6 @@ class QuestionHelper extends Helper
return $this->validateAttempts($interviewer, $output, $question);
}
/**
* Sets the input stream to read from when interacting with the user.
*
* This is mainly useful for testing purpose.
*
* @deprecated since version 3.2, to be removed in 4.0. Use
* StreamableInputInterface::setStream() instead.
*
* @param resource $stream The input stream
*
* @throws InvalidArgumentException In case the stream is not a resource
*/
public function setInputStream($stream)
{
@trigger_error(sprintf('The %s() method is deprecated since Symfony 3.2 and will be removed in 4.0. Use %s::setStream() instead.', __METHOD__, StreamableInputInterface::class), \E_USER_DEPRECATED);
if (!\is_resource($stream)) {
throw new InvalidArgumentException('Input stream must be a valid resource.');
}
$this->inputStream = $stream;
}
/**
* Returns the helper's input stream.
*
* @deprecated since version 3.2, to be removed in 4.0. Use
* StreamableInputInterface::getStream() instead.
*
* @return resource
*/
public function getInputStream()
{
if (0 === \func_num_args() || func_get_arg(0)) {
@trigger_error(sprintf('The %s() method is deprecated since Symfony 3.2 and will be removed in 4.0. Use %s::getStream() instead.', __METHOD__, StreamableInputInterface::class), \E_USER_DEPRECATED);
}
return $this->inputStream;
}
/**
* {@inheritdoc}
*/
@@ -155,15 +114,10 @@ class QuestionHelper extends Helper
{
$this->writePrompt($output, $question);
$inputStream = $this->inputStream ?: \STDIN;
$inputStream = $this->inputStream ?: STDIN;
$autocomplete = $question->getAutocompleterValues();
if (\function_exists('sapi_windows_cp_set')) {
// Codepage used by cmd.exe on Windows to allow special characters (éàüñ).
@sapi_windows_cp_set(1252);
}
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
if (null === $autocomplete || !$this->hasSttyAvailable()) {
$ret = false;
if ($question->isHidden()) {
try {
@@ -186,6 +140,10 @@ class QuestionHelper extends Helper
$ret = trim($this->autocomplete($output, $question, $inputStream, \is_array($autocomplete) ? $autocomplete : iterator_to_array($autocomplete, false)));
}
if ($output instanceof ConsoleSectionOutput) {
$output->addContent($ret);
}
$ret = \strlen($ret) > 0 ? $ret : $question->getDefault();
if ($normalizer = $question->getNormalizer()) {
@@ -203,9 +161,15 @@ class QuestionHelper extends Helper
$message = $question->getQuestion();
if ($question instanceof ChoiceQuestion) {
$output->writeln(array_merge([
$question->getQuestion(),
], $this->formatChoiceQuestionChoices($question, 'info')));
$maxWidth = max(array_map([$this, 'strlen'], array_keys($question->getChoices())));
$messages = (array) $question->getQuestion();
foreach ($question->getChoices() as $key => $value) {
$width = $maxWidth - $this->strlen($key);
$messages[] = ' [<info>'.$key.str_repeat(' ', $width).'</info>] '.$value;
}
$output->writeln($messages);
$message = $question->getPrompt();
}
@@ -213,26 +177,6 @@ class QuestionHelper extends Helper
$output->write($message);
}
/**
* @param string $tag
*
* @return string[]
*/
protected function formatChoiceQuestionChoices(ChoiceQuestion $question, $tag)
{
$messages = [];
$maxWidth = max(array_map('self::strlen', array_keys($choices = $question->getChoices())));
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::strlen($key));
$messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
}
return $messages;
}
/**
* Outputs an error message.
*/
@@ -250,11 +194,11 @@ class QuestionHelper extends Helper
/**
* Autocompletes a question.
*
* @param resource $inputStream
*
* @return string
* @param OutputInterface $output
* @param Question $question
* @param resource $inputStream
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete)
private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete): string
{
$fullChoice = '';
$ret = '';
@@ -283,7 +227,7 @@ class QuestionHelper extends Helper
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
--$i;
$fullChoice = self::substr($fullChoice, 0, $i);
$fullChoice = substr($fullChoice, 0, -1);
// Move cursor backwards
$output->write("\033[1D");
}
@@ -297,7 +241,7 @@ class QuestionHelper extends Helper
}
// Pop the last character off the end of our string
$ret = self::substr($ret, 0, $i);
$ret = substr($ret, 0, $i);
} elseif ("\033" === $c) {
// Did we read an escape sequence?
$c .= fread($inputStream, 2);
@@ -323,7 +267,7 @@ class QuestionHelper extends Helper
$remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
$output->write($remainingCharacters);
$fullChoice .= $remainingCharacters;
$i = self::strlen($fullChoice);
$i = \strlen($fullChoice);
}
if ("\n" === $c) {
@@ -403,11 +347,9 @@ class QuestionHelper extends Helper
* @param OutputInterface $output An Output instance
* @param resource $inputStream The handler resource
*
* @return string The answer
*
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
*/
private function getHiddenResponse(OutputInterface $output, $inputStream)
private function getHiddenResponse(OutputInterface $output, $inputStream): string
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
@@ -429,7 +371,7 @@ class QuestionHelper extends Helper
return $value;
}
if (self::$stty && Terminal::hasSttyAvailable()) {
if ($this->hasSttyAvailable()) {
$sttyMode = shell_exec('stty -g');
shell_exec('stty -echo');
@@ -479,7 +421,7 @@ class QuestionHelper extends Helper
}
try {
return \call_user_func($question->getValidator(), $interviewer());
return $question->getValidator()($interviewer());
} catch (RuntimeException $e) {
throw $e;
} catch (\Exception $error) {
@@ -515,4 +457,18 @@ class QuestionHelper extends Helper
return self::$shell;
}
/**
* Returns whether Stty is available or not.
*/
private function hasSttyAvailable(): bool
{
if (null !== self::$stty) {
return self::$stty;
}
exec('stty 2>&1', $output, $exitcode);
return self::$stty = 0 === $exitcode;
}
}
+5 -33
View File
@@ -11,9 +11,7 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
@@ -27,32 +25,6 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
class SymfonyQuestionHelper extends QuestionHelper
{
/**
* {@inheritdoc}
*
* To be removed in 4.0
*/
public function ask(InputInterface $input, OutputInterface $output, Question $question)
{
$validator = $question->getValidator();
$question->setValidator(function ($value) use ($validator) {
if (null !== $validator) {
$value = $validator($value);
} else {
// make required
if (!\is_array($value) && !\is_bool($value) && 0 === \strlen($value)) {
@trigger_error('The default question validator is deprecated since Symfony 3.3 and will not be used anymore in version 4.0. Set a custom question validator if needed.', \E_USER_DEPRECATED);
throw new LogicException('A value is required.');
}
}
return $value;
});
return parent::ask($input, $output, $question);
}
/**
* {@inheritdoc}
*/
@@ -96,15 +68,15 @@ class SymfonyQuestionHelper extends QuestionHelper
$output->writeln($text);
$prompt = ' > ';
if ($question instanceof ChoiceQuestion) {
$output->writeln($this->formatChoiceQuestionChoices($question, 'comment'));
$width = max(array_map('strlen', array_keys($question->getChoices())));
$prompt = $question->getPrompt();
foreach ($question->getChoices() as $key => $value) {
$output->writeln(sprintf(" [<comment>%-${width}s</comment>] %s", $key, $value));
}
}
$output->write($prompt);
$output->write(' > ');
}
/**
+217 -102
View File
@@ -12,6 +12,10 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -21,9 +25,20 @@ use Symfony\Component\Console\Output\OutputInterface;
* @author Саша Стаменковић <umpirsky@gmail.com>
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
* @author Max Grigorian <maxakawizard@gmail.com>
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class Table
{
private const SEPARATOR_TOP = 0;
private const SEPARATOR_TOP_BOTTOM = 1;
private const SEPARATOR_MID = 2;
private const SEPARATOR_BOTTOM = 3;
private const BORDER_OUTSIDE = 0;
private const BORDER_INSIDE = 1;
private $headerTitle;
private $footerTitle;
/**
* Table headers.
*/
@@ -67,9 +82,12 @@ class Table
* @var array
*/
private $columnWidths = [];
private $columnMaxWidths = [];
private static $styles;
private $rendered = false;
public function __construct(OutputInterface $output)
{
$this->output = $output;
@@ -168,11 +186,7 @@ class Table
*/
public function getColumnStyle($columnIndex)
{
if (isset($this->columnStyles[$columnIndex])) {
return $this->columnStyles[$columnIndex];
}
return $this->getStyle();
return $this->columnStyles[$columnIndex] ?? $this->getStyle();
}
/**
@@ -193,6 +207,8 @@ class Table
/**
* Sets the minimum width of all columns.
*
* @param array $widths
*
* @return $this
*/
public function setColumnWidths(array $widths)
@@ -205,6 +221,25 @@ class Table
return $this;
}
/**
* Sets the maximum width of a column.
*
* Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while
* formatted strings are preserved.
*
* @return $this
*/
public function setColumnMaxWidth(int $columnIndex, int $width): self
{
if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, \get_class($this->output->getFormatter())));
}
$this->columnMaxWidths[$columnIndex] = $width;
return $this;
}
public function setHeaders(array $headers)
{
$headers = array_values($headers);
@@ -250,6 +285,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;
@@ -257,6 +311,20 @@ class Table
return $this;
}
public function setHeaderTitle(?string $title): self
{
$this->headerTitle = $title;
return $this;
}
public function setFooterTitle(?string $title): self
{
$this->footerTitle = $title;
return $this;
}
/**
* Renders table to output.
*
@@ -272,31 +340,45 @@ class Table
*/
public function render()
{
$this->calculateNumberOfColumns();
$rows = $this->buildTableRows($this->rows);
$headers = $this->buildTableRows($this->headers);
$rows = array_merge($this->headers, [$divider = new TableSeparator()], $this->rows);
$this->calculateNumberOfColumns($rows);
$this->calculateColumnsWidth(array_merge($headers, $rows));
$rows = $this->buildTableRows($rows);
$this->calculateColumnsWidth($rows);
$this->renderRowSeparator();
if (!empty($headers)) {
foreach ($headers as $header) {
$this->renderRow($header, $this->style->getCellHeaderFormat());
$this->renderRowSeparator();
}
}
$isHeader = true;
$isFirstRow = false;
foreach ($rows as $row) {
if ($divider === $row) {
$isHeader = false;
$isFirstRow = true;
continue;
}
if ($row instanceof TableSeparator) {
$this->renderRowSeparator();
} else {
$this->renderRow($row, $this->style->getCellRowFormat());
continue;
}
if (!$row) {
continue;
}
if ($isHeader || $isFirstRow) {
if ($isFirstRow) {
$this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM);
$isFirstRow = false;
} else {
$this->renderRowSeparator(self::SEPARATOR_TOP, $this->headerTitle, $this->style->getHeaderTitleFormat());
}
}
$this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
}
if (!empty($rows)) {
$this->renderRowSeparator();
}
$this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
$this->cleanup();
$this->rendered = true;
}
/**
@@ -306,19 +388,49 @@ class Table
*
* +-----+-----------+-------+
*/
private function renderRowSeparator()
private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $title = null, string $titleFormat = null)
{
if (0 === $count = $this->numberOfColumns) {
return;
}
if (!$this->style->getHorizontalBorderChar() && !$this->style->getCrossingChar()) {
$borders = $this->style->getBorderChars();
if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) {
return;
}
$markup = $this->style->getCrossingChar();
$crossings = $this->style->getCrossingChars();
if (self::SEPARATOR_MID === $type) {
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
} elseif (self::SEPARATOR_TOP === $type) {
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
} elseif (self::SEPARATOR_TOP_BOTTOM === $type) {
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
} else {
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
}
$markup = $leftChar;
for ($column = 0; $column < $count; ++$column) {
$markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->effectiveColumnWidths[$column]).$this->style->getCrossingChar();
$markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
$markup .= $column === $count - 1 ? $rightChar : $midChar;
}
if (null !== $title) {
$titleLength = Helper::strlenWithoutDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title));
$markupLength = Helper::strlen($markup);
if ($titleLength > $limit = $markupLength - 4) {
$titleLength = $limit;
$formatLength = Helper::strlenWithoutDecoration($formatter, sprintf($titleFormat, ''));
$formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
}
$titleStart = ($markupLength - $titleLength) / 2;
if (false === mb_detect_encoding($markup, null, true)) {
$markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
} else {
$markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength);
}
}
$this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
@@ -327,9 +439,11 @@ class Table
/**
* Renders vertical column separator.
*/
private function renderColumnSeparator()
private function renderColumnSeparator($type = self::BORDER_OUTSIDE)
{
return sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar());
$borders = $this->style->getBorderChars();
return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
}
/**
@@ -338,30 +452,23 @@ class Table
* Example:
*
* | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
*
* @param string $cellFormat
*/
private function renderRow(array $row, $cellFormat)
private function renderRow(array $row, string $cellFormat)
{
if (empty($row)) {
return;
}
$rowContent = $this->renderColumnSeparator();
foreach ($this->getRowColumns($row) as $column) {
$rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
$columns = $this->getRowColumns($row);
$last = \count($columns) - 1;
foreach ($columns as $i => $column) {
$rowContent .= $this->renderCell($row, $column, $cellFormat);
$rowContent .= $this->renderColumnSeparator();
$rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE);
}
$this->output->writeln($rowContent);
}
/**
* Renders table cell with padding.
*
* @param int $column
* @param string $cellFormat
*/
private function renderCell(array $row, $column, $cellFormat)
private function renderCell(array $row, int $column, string $cellFormat)
{
$cell = isset($row[$column]) ? $row[$column] : '';
$width = $this->effectiveColumnWidths[$column];
@@ -380,7 +487,7 @@ class Table
$style = $this->getColumnStyle($column);
if ($cell instanceof TableSeparator) {
return sprintf($style->getBorderFormat(), str_repeat($style->getHorizontalBorderChar(), $width));
return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
}
$width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
@@ -392,14 +499,10 @@ class Table
/**
* Calculate number of columns for this table.
*/
private function calculateNumberOfColumns()
private function calculateNumberOfColumns($rows)
{
if (null !== $this->numberOfColumns) {
return;
}
$columns = [0];
foreach (array_merge($this->headers, $this->rows) as $row) {
foreach ($rows as $row) {
if ($row instanceof TableSeparator) {
continue;
}
@@ -412,19 +515,28 @@ class Table
private function buildTableRows($rows)
{
/** @var WrappableOutputFormatterInterface $formatter */
$formatter = $this->output->getFormatter();
$unmergedRows = [];
for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
$rows = $this->fillNextRows($rows, $rowKey);
// Remove any new line breaks and replace it with a new line
foreach ($rows[$rowKey] as $column => $cell) {
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) {
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
}
if (!strstr($cell, "\n")) {
continue;
}
$escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell)));
$cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
$lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
foreach ($lines as $lineKey => $line) {
if ($cell instanceof TableCell) {
$line = new TableCell($line, ['colspan' => $cell->getColspan()]);
if ($colspan > 1) {
$line = new TableCell($line, ['colspan' => $colspan]);
}
if (0 === $lineKey) {
$rows[$rowKey][$column] = $line;
@@ -435,32 +547,43 @@ class Table
}
}
$tableRows = [];
foreach ($rows as $rowKey => $row) {
$tableRows[] = $this->fillCells($row);
if (isset($unmergedRows[$rowKey])) {
$tableRows = array_merge($tableRows, $unmergedRows[$rowKey]);
return new TableRows(function () use ($rows, $unmergedRows) {
foreach ($rows as $rowKey => $row) {
yield $this->fillCells($row);
if (isset($unmergedRows[$rowKey])) {
foreach ($unmergedRows[$rowKey] as $row) {
yield $row;
}
}
}
});
}
private function calculateRowCount(): int
{
$numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows))));
if ($this->headers) {
++$numberOfRows; // Add row for header separator
}
return $tableRows;
++$numberOfRows; // Add row for footer separator
return $numberOfRows;
}
/**
* fill rows that contains rowspan > 1.
*
* @param int $line
*
* @return array
*
* @throws InvalidArgumentException
*/
private function fillNextRows(array $rows, $line)
private function fillNextRows(array $rows, int $line): array
{
$unmergedRows = [];
foreach ($rows[$line] as $column => $cell) {
if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', \gettype($cell)));
throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing __toString, %s given.', \gettype($cell)));
}
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
$nbLines = $cell->getRowspan() - 1;
@@ -508,8 +631,6 @@ class Table
/**
* fill cells for a row that contains colspan > 1.
*
* @return array
*/
private function fillCells($row)
{
@@ -527,12 +648,7 @@ class Table
return $newRow ?: $row;
}
/**
* @param int $line
*
* @return array
*/
private function copyRow(array $rows, $line)
private function copyRow(array $rows, int $line): array
{
$row = $rows[$line];
foreach ($row as $cellKey => $cellValue) {
@@ -547,10 +663,8 @@ class Table
/**
* Gets number of columns by row.
*
* @return int
*/
private function getNumberOfColumns(array $row)
private function getNumberOfColumns(array $row): int
{
$columns = \count($row);
foreach ($row as $column) {
@@ -562,10 +676,8 @@ class Table
/**
* Gets list of columns for the given row.
*
* @return array
*/
private function getRowColumns(array $row)
private function getRowColumns(array $row): array
{
$columns = range(0, $this->numberOfColumns - 1);
foreach ($row as $cellKey => $cell) {
@@ -581,7 +693,7 @@ class Table
/**
* Calculates columns widths.
*/
private function calculateColumnsWidth(array $rows)
private function calculateColumnsWidth(iterable $rows)
{
for ($column = 0; $column < $this->numberOfColumns; ++$column) {
$lengths = [];
@@ -610,24 +722,12 @@ class Table
}
}
/**
* Gets column width.
*
* @return int
*/
private function getColumnSeparatorWidth()
private function getColumnSeparatorWidth(): int
{
return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar()));
return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
}
/**
* Gets cell width.
*
* @param int $column
*
* @return int
*/
private function getCellWidth(array $row, $column)
private function getCellWidth(array $row, int $column): int
{
$cellWidth = 0;
@@ -637,8 +737,9 @@ class Table
}
$columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
$cellWidth = max($cellWidth, $columnWidth);
return max($cellWidth, $columnWidth);
return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
}
/**
@@ -654,32 +755,46 @@ class Table
{
$borderless = new TableStyle();
$borderless
->setHorizontalBorderChar('=')
->setVerticalBorderChar(' ')
->setCrossingChar(' ')
->setHorizontalBorderChars('=')
->setVerticalBorderChars(' ')
->setDefaultCrossingChar(' ')
;
$compact = new TableStyle();
$compact
->setHorizontalBorderChar('')
->setVerticalBorderChar(' ')
->setCrossingChar('')
->setHorizontalBorderChars('')
->setVerticalBorderChars(' ')
->setDefaultCrossingChar('')
->setCellRowContentFormat('%s')
;
$styleGuide = new TableStyle();
$styleGuide
->setHorizontalBorderChar('-')
->setVerticalBorderChar(' ')
->setCrossingChar(' ')
->setHorizontalBorderChars('-')
->setVerticalBorderChars(' ')
->setDefaultCrossingChar(' ')
->setCellHeaderFormat('%s')
;
$box = (new TableStyle())
->setHorizontalBorderChars('─')
->setVerticalBorderChars('│')
->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
;
$boxDouble = (new TableStyle())
->setHorizontalBorderChars('═', '─')
->setVerticalBorderChars('║', '│')
->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣')
;
return [
'default' => new TableStyle(),
'borderless' => $borderless,
'compact' => $compact,
'symfony-style-guide' => $styleGuide,
'box' => $box,
'box-double' => $boxDouble,
];
}
+1 -8
View File
@@ -24,15 +24,8 @@ class TableCell
'colspan' => 1,
];
/**
* @param string $value
*/
public function __construct($value = '', array $options = [])
public function __construct(string $value = '', array $options = [])
{
if (is_numeric($value) && !\is_string($value)) {
$value = (string) $value;
}
$this->value = $value;
// check option names
+213 -13
View File
@@ -19,18 +19,34 @@ use Symfony\Component\Console\Exception\LogicException;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Саша Стаменковић <umpirsky@gmail.com>
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class TableStyle
{
private $paddingChar = ' ';
private $horizontalBorderChar = '-';
private $verticalBorderChar = '|';
private $horizontalOutsideBorderChar = '-';
private $horizontalInsideBorderChar = '-';
private $verticalOutsideBorderChar = '|';
private $verticalInsideBorderChar = '|';
private $crossingChar = '+';
private $crossingTopRightChar = '+';
private $crossingTopMidChar = '+';
private $crossingTopLeftChar = '+';
private $crossingMidRightChar = '+';
private $crossingBottomRightChar = '+';
private $crossingBottomMidChar = '+';
private $crossingBottomLeftChar = '+';
private $crossingMidLeftChar = '+';
private $crossingTopLeftBottomChar = '+';
private $crossingTopMidBottomChar = '+';
private $crossingTopRightBottomChar = '+';
private $headerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
private $footerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
private $cellHeaderFormat = '<info>%s</info>';
private $cellRowFormat = '%s';
private $cellRowContentFormat = ' %s ';
private $borderFormat = '%s';
private $padType = \STR_PAD_RIGHT;
private $padType = STR_PAD_RIGHT;
/**
* Sets padding character, used for cell padding.
@@ -42,7 +58,7 @@ class TableStyle
public function setPaddingChar($paddingChar)
{
if (!$paddingChar) {
throw new LogicException('The padding char must not be empty.');
throw new LogicException('The padding char must not be empty');
}
$this->paddingChar = $paddingChar;
@@ -60,28 +76,85 @@ class TableStyle
return $this->paddingChar;
}
/**
* Sets horizontal border characters.
*
* <code>
* ╔═══════════════╤══════════════════════════╤══════════════════╗
* 1 ISBN 2 Title │ Author ║
* ╠═══════════════╪══════════════════════════╪══════════════════╣
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* ╚═══════════════╧══════════════════════════╧══════════════════╝
* </code>
*
* @param string $outside Outside border char (see #1 of example)
* @param string|null $inside Inside border char (see #2 of example), equals $outside if null
*/
public function setHorizontalBorderChars(string $outside, string $inside = null): self
{
$this->horizontalOutsideBorderChar = $outside;
$this->horizontalInsideBorderChar = $inside ?? $outside;
return $this;
}
/**
* Sets horizontal border character.
*
* @param string $horizontalBorderChar
*
* @return $this
*
* @deprecated since Symfony 4.1, use {@link setHorizontalBorderChars()} instead.
*/
public function setHorizontalBorderChar($horizontalBorderChar)
{
$this->horizontalBorderChar = $horizontalBorderChar;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setHorizontalBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this;
return $this->setHorizontalBorderChars($horizontalBorderChar, $horizontalBorderChar);
}
/**
* Gets horizontal border character.
*
* @return string
*
* @deprecated since Symfony 4.1, use {@link getBorderChars()} instead.
*/
public function getHorizontalBorderChar()
{
return $this->horizontalBorderChar;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->horizontalOutsideBorderChar;
}
/**
* Sets vertical border characters.
*
* <code>
* ╔═══════════════╤══════════════════════════╤══════════════════╗
* ║ ISBN │ Title │ Author ║
* ╠═══════1═══════╪══════════════════════════╪══════════════════╣
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
* ╟───────2───────┼──────────────────────────┼──────────────────╢
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* ╚═══════════════╧══════════════════════════╧══════════════════╝
* </code>
*
* @param string $outside Outside border char (see #1 of example)
* @param string|null $inside Inside border char (see #2 of example), equals $outside if null
*/
public function setVerticalBorderChars(string $outside, string $inside = null): self
{
$this->verticalOutsideBorderChar = $outside;
$this->verticalInsideBorderChar = $inside ?? $outside;
return $this;
}
/**
@@ -90,22 +163,100 @@ class TableStyle
* @param string $verticalBorderChar
*
* @return $this
*
* @deprecated since Symfony 4.1, use {@link setVerticalBorderChars()} instead.
*/
public function setVerticalBorderChar($verticalBorderChar)
{
$this->verticalBorderChar = $verticalBorderChar;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setVerticalBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this;
return $this->setVerticalBorderChars($verticalBorderChar, $verticalBorderChar);
}
/**
* Gets vertical border character.
*
* @return string
*
* @deprecated since Symfony 4.1, use {@link getBorderChars()} instead.
*/
public function getVerticalBorderChar()
{
return $this->verticalBorderChar;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->verticalOutsideBorderChar;
}
/**
* Gets border characters.
*
* @internal
*/
public function getBorderChars()
{
return [
$this->horizontalOutsideBorderChar,
$this->verticalOutsideBorderChar,
$this->horizontalInsideBorderChar,
$this->verticalInsideBorderChar,
];
}
/**
* Sets crossing characters.
*
* Example:
* <code>
* 1═══════════════2══════════════════════════2══════════════════3
* ║ ISBN │ Title │ Author ║
* 8'══════════════0'═════════════════════════0'═════════════════4'
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
* 8───────────────0──────────────────────────0──────────────────4
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* 7═══════════════6══════════════════════════6══════════════════5
* </code>
*
* @param string $cross Crossing char (see #0 of example)
* @param string $topLeft Top left char (see #1 of example)
* @param string $topMid Top mid char (see #2 of example)
* @param string $topRight Top right char (see #3 of example)
* @param string $midRight Mid right char (see #4 of example)
* @param string $bottomRight Bottom right char (see #5 of example)
* @param string $bottomMid Bottom mid char (see #6 of example)
* @param string $bottomLeft Bottom left char (see #7 of example)
* @param string $midLeft Mid left char (see #8 of example)
* @param string|null $topLeftBottom Top left bottom char (see #8' of example), equals to $midLeft if null
* @param string|null $topMidBottom Top mid bottom char (see #0' of example), equals to $cross if null
* @param string|null $topRightBottom Top right bottom char (see #4' of example), equals to $midRight if null
*/
public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, string $topLeftBottom = null, string $topMidBottom = null, string $topRightBottom = null): self
{
$this->crossingChar = $cross;
$this->crossingTopLeftChar = $topLeft;
$this->crossingTopMidChar = $topMid;
$this->crossingTopRightChar = $topRight;
$this->crossingMidRightChar = $midRight;
$this->crossingBottomRightChar = $bottomRight;
$this->crossingBottomMidChar = $bottomMid;
$this->crossingBottomLeftChar = $bottomLeft;
$this->crossingMidLeftChar = $midLeft;
$this->crossingTopLeftBottomChar = $topLeftBottom ?? $midLeft;
$this->crossingTopMidBottomChar = $topMidBottom ?? $cross;
$this->crossingTopRightBottomChar = $topRightBottom ?? $midRight;
return $this;
}
/**
* Sets default crossing character used for each cross.
*
* @see {@link setCrossingChars()} for setting each crossing individually.
*/
public function setDefaultCrossingChar(string $char): self
{
return $this->setCrossingChars($char, $char, $char, $char, $char, $char, $char, $char, $char);
}
/**
@@ -114,12 +265,14 @@ class TableStyle
* @param string $crossingChar
*
* @return $this
*
* @deprecated since Symfony 4.1. Use {@link setDefaultCrossingChar()} instead.
*/
public function setCrossingChar($crossingChar)
{
$this->crossingChar = $crossingChar;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1. Use setDefaultCrossingChar() instead.', __METHOD__), E_USER_DEPRECATED);
return $this;
return $this->setDefaultCrossingChar($crossingChar);
}
/**
@@ -132,6 +285,29 @@ class TableStyle
return $this->crossingChar;
}
/**
* Gets crossing characters.
*
* @internal
*/
public function getCrossingChars(): array
{
return [
$this->crossingChar,
$this->crossingTopLeftChar,
$this->crossingTopMidChar,
$this->crossingTopRightChar,
$this->crossingMidRightChar,
$this->crossingBottomRightChar,
$this->crossingBottomMidChar,
$this->crossingBottomLeftChar,
$this->crossingMidLeftChar,
$this->crossingTopLeftBottomChar,
$this->crossingTopMidBottomChar,
$this->crossingTopRightBottomChar,
];
}
/**
* Sets header cell format.
*
@@ -237,7 +413,7 @@ class TableStyle
*/
public function setPadType($padType)
{
if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) {
if (!\in_array($padType, [STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH], true)) {
throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
}
@@ -255,4 +431,28 @@ class TableStyle
{
return $this->padType;
}
public function getHeaderTitleFormat(): string
{
return $this->headerTitleFormat;
}
public function setHeaderTitleFormat(string $format): self
{
$this->headerTitleFormat = $format;
return $this;
}
public function getFooterTitleFormat(): string
{
return $this->footerTitleFormat;
}
public function setFooterTitleFormat(string $format): self
{
$this->footerTitleFormat = $format;
return $this;
}
}
+3 -8
View File
@@ -49,7 +49,9 @@ class ArgvInput extends Input
*/
public function __construct(array $argv = null, InputDefinition $definition = null)
{
$argv = null !== $argv ? $argv : (isset($_SERVER['argv']) ? $_SERVER['argv'] : []);
if (null === $argv) {
$argv = $_SERVER['argv'];
}
// strip the application name
array_shift($argv);
@@ -145,11 +147,6 @@ class ArgvInput extends Input
if (false !== $pos = strpos($name, '=')) {
if (0 === \strlen($value = substr($name, $pos + 1))) {
// if no value after "=" then substr() returns "" since php7 only, false before
// see https://php.net/migration70.incompatible.php#119151
if (\PHP_VERSION_ID < 70000 && false === $value) {
$value = '';
}
array_unshift($this->parsed, $value);
}
$this->addLongOption(substr($name, 0, $pos), $value);
@@ -286,8 +283,6 @@ class ArgvInput extends Input
return $token;
}
return null;
}
/**
+4 -6
View File
@@ -39,15 +39,13 @@ class ArrayInput extends Input
*/
public function getFirstArgument()
{
foreach ($this->parameters as $param => $value) {
if ($param && \is_string($param) && '-' === $param[0]) {
foreach ($this->parameters as $key => $value) {
if ($key && '-' === $key[0]) {
continue;
}
return $value;
}
return null;
}
/**
@@ -107,7 +105,7 @@ class ArrayInput extends Input
{
$params = [];
foreach ($this->parameters as $param => $val) {
if ($param && \is_string($param) && '-' === $param[0]) {
if ($param && '-' === $param[0]) {
if (\is_array($val)) {
foreach ($val as $v) {
$params[] = $param.('' != $v ? '='.$this->escapeToken($v) : '');
@@ -134,7 +132,7 @@ class ArrayInput extends Input
}
if (0 === strpos($key, '--')) {
$this->addLongOption(substr($key, 2), $value);
} elseif (0 === strpos($key, '-')) {
} elseif ('-' === $key[0]) {
$this->addShortOption(substr($key, 1), $value);
} else {
$this->addArgument($key, $value);
+2 -2
View File
@@ -38,11 +38,11 @@ class InputArgument
*
* @throws InvalidArgumentException When argument mode is not valid
*/
public function __construct($name, $mode = null, $description = '', $default = null)
public function __construct(string $name, int $mode = null, string $description = '', $default = null)
{
if (null === $mode) {
$mode = self::OPTIONAL;
} elseif (!\is_int($mode) || $mode > 7 || $mode < 1) {
} elseif ($mode > 7 || $mode < 1) {
throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode));
}
+8 -8
View File
@@ -171,7 +171,7 @@ class InputDefinition
*/
public function getArgumentCount()
{
return $this->hasAnArrayArgument ? \PHP_INT_MAX : \count($this->arguments);
return $this->hasAnArrayArgument ? PHP_INT_MAX : \count($this->arguments);
}
/**
@@ -384,21 +384,21 @@ class InputDefinition
$elements[] = '[--]';
}
$tail = '';
foreach ($this->getArguments() as $argument) {
$element = '<'.$argument->getName().'>';
if (!$argument->isRequired()) {
$element = '['.$element.']';
} elseif ($argument->isArray()) {
$element .= ' ('.$element.')';
}
if ($argument->isArray()) {
$element .= '...';
}
if (!$argument->isRequired()) {
$element = '['.$element;
$tail .= ']';
}
$elements[] = $element;
}
return implode(' ', $elements);
return implode(' ', $elements).$tail;
}
}
+2 -2
View File
@@ -41,7 +41,7 @@ class InputOption
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null)
public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
{
if (0 === strpos($name, '--')) {
$name = substr($name, 2);
@@ -70,7 +70,7 @@ class InputOption
if (null === $mode) {
$mode = self::VALUE_NONE;
} elseif (!\is_int($mode) || $mode > 15 || $mode < 1) {
} elseif ($mode > 15 || $mode < 1) {
throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
}
+2 -2
View File
@@ -30,7 +30,7 @@ class StringInput extends ArgvInput
/**
* @param string $input A string representing the parameters from the CLI
*/
public function __construct($input)
public function __construct(string $input)
{
parent::__construct([]);
@@ -61,7 +61,7 @@ class StringInput extends ArgvInput
$tokens[] = stripcslashes($match[1]);
} else {
// should never happen
throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10)));
throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10)));
}
$cursor += \strlen($match[0]);
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2004-2020 Fabien Potencier
Copyright (c) 2004-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+2 -6
View File
@@ -22,7 +22,7 @@ use Symfony\Component\Console\Output\OutputInterface;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @see https://www.php-fig.org/psr/psr-3/
* @see http://www.php-fig.org/psr/psr-3/
*/
class ConsoleLogger extends AbstractLogger
{
@@ -99,12 +99,8 @@ class ConsoleLogger extends AbstractLogger
* Interpolates context values into the message placeholders.
*
* @author PHP Framework Interoperability Group
*
* @param string $message
*
* @return string
*/
private function interpolate($message, array $context)
private function interpolate(string $message, array $context): string
{
if (false === strpos($message, '{')) {
return $message;
+1 -1
View File
@@ -39,7 +39,7 @@ class BufferedOutput extends Output
$this->buffer .= $message;
if ($newline) {
$this->buffer .= \PHP_EOL;
$this->buffer .= PHP_EOL;
}
}
}
+11 -2
View File
@@ -30,13 +30,14 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface;
class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
{
private $stderr;
private $consoleSectionOutputs = [];
/**
* @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
* @param bool|null $decorated Whether to decorate messages (null for auto-guessing)
* @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
*/
public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null)
public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
{
parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter);
@@ -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}
*/
@@ -124,7 +133,7 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
$checks = [
\function_exists('php_uname') ? php_uname('s') : '',
getenv('OSTYPE'),
\PHP_OS,
PHP_OS,
];
return false !== stripos(implode(';', $checks), 'OS400');
+3 -1
View File
@@ -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 <darek.krk@gmail.com>
*
* @method ConsoleSectionOutput section() Creates a new output section
*/
interface ConsoleOutputInterface extends OutputInterface
{
+4 -2
View File
@@ -37,7 +37,7 @@ abstract class Output implements OutputInterface
* @param bool $decorated Whether to decorate messages
* @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
*/
public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = false, OutputFormatterInterface $formatter = null)
public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
{
$this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity;
$this->formatter = $formatter ?: new OutputFormatter();
@@ -137,7 +137,9 @@ abstract class Output implements OutputInterface
*/
public function write($messages, $newline = false, $options = self::OUTPUT_NORMAL)
{
$messages = (array) $messages;
if (!is_iterable($messages)) {
$messages = [$messages];
}
$types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN;
$type = $types & $options ?: self::OUTPUT_NORMAL;
+5 -5
View File
@@ -33,17 +33,17 @@ interface OutputInterface
/**
* Writes a message to the output.
*
* @param string|array $messages The message as an array of strings or a single string
* @param bool $newline Whether to add a newline
* @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
* @param string|iterable $messages The message as an iterable of strings or a single string
* @param bool $newline Whether to add a newline
* @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
*/
public function write($messages, $newline = false, $options = 0);
/**
* Writes a message to the output and adds a newline at the end.
*
* @param string|array $messages The message as an array of strings or a single string
* @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
* @param string|iterable $messages The message as an iterable of strings or a single string
* @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
*/
public function writeln($messages, $options = 0);
+7 -3
View File
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Output;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
/**
@@ -39,7 +40,7 @@ class StreamOutput extends Output
*
* @throws InvalidArgumentException When first argument is not a real stream
*/
public function __construct($stream, $verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null)
public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
{
if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) {
throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.');
@@ -70,10 +71,13 @@ class StreamOutput extends Output
protected function doWrite($message, $newline)
{
if ($newline) {
$message .= \PHP_EOL;
$message .= PHP_EOL;
}
@fwrite($this->stream, $message);
if (false === @fwrite($this->stream, $message)) {
// should never happen
throw new RuntimeException('Unable to write output.');
}
fflush($this->stream);
}
+3 -8
View File
@@ -30,7 +30,7 @@ class ChoiceQuestion extends Question
* @param array $choices The list of available choices
* @param mixed $default The default answer to return
*/
public function __construct($question, array $choices, $default = null)
public function __construct(string $question, array $choices, $default = null)
{
if (!$choices) {
throw new \LogicException('Choice question must have at least 1 choice available.');
@@ -121,12 +121,7 @@ class ChoiceQuestion extends Question
return $this;
}
/**
* Returns the default answer validator.
*
* @return callable
*/
private function getDefaultValidator()
private function getDefaultValidator(): callable
{
$choices = $this->choices;
$errorMessage = $this->errorMessage;
@@ -155,7 +150,7 @@ class ChoiceQuestion extends Question
}
if (\count($results) > 1) {
throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results)));
}
$result = array_search($value, $choices);
+2 -2
View File
@@ -25,9 +25,9 @@ class ConfirmationQuestion extends Question
* @param bool $default The default answer to return, true or false
* @param string $trueAnswerRegex A regex to match the "yes" answer
*/
public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i')
public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i')
{
parent::__construct($question, (bool) $default);
parent::__construct($question, $default);
$this->trueAnswerRegex = $trueAnswerRegex;
$this->setNormalizer($this->getDefaultNormalizer());
+9 -8
View File
@@ -34,7 +34,7 @@ class Question
* @param string $question The question to ask to the user
* @param mixed $default The default answer to return if the user enters nothing
*/
public function __construct($question, $default = null)
public function __construct(string $question, $default = null)
{
$this->question = $question;
$this->default = $default;
@@ -141,7 +141,7 @@ class Question
}
if (null !== $values && !\is_array($values) && !$values instanceof \Traversable) {
throw new InvalidArgumentException('Autocompleter values can be either an array, `null` or a `Traversable` object.');
throw new InvalidArgumentException('Autocompleter values can be either an array, "null" or a "Traversable" object.');
}
if ($this->hidden) {
@@ -156,6 +156,8 @@ class Question
/**
* Sets a validator for the question.
*
* @param callable|null $validator
*
* @return $this
*/
public function setValidator(callable $validator = null)
@@ -188,11 +190,8 @@ class Question
*/
public function setMaxAttempts($attempts)
{
if (null !== $attempts) {
$attempts = (int) $attempts;
if ($attempts < 1) {
throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
}
if (null !== $attempts && $attempts < 1) {
throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
}
$this->attempts = $attempts;
@@ -217,6 +216,8 @@ class Question
*
* The normalizer can be a callable (a string), a closure or a class implementing __invoke.
*
* @param callable $normalizer
*
* @return $this
*/
public function setNormalizer(callable $normalizer)
@@ -231,7 +232,7 @@ class Question
*
* The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
*
* @return callable|null
* @return callable
*/
public function getNormalizer()
{
+1 -1
View File
@@ -7,7 +7,7 @@ interfaces.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/console.html)
* [Documentation](https://symfony.com/doc/current/components/console/index.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
+1 -1
View File
@@ -35,7 +35,7 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
*/
public function newLine($count = 1)
{
$this->output->write(str_repeat(\PHP_EOL, $count));
$this->output->write(str_repeat(PHP_EOL, $count));
}
/**
+1
View File
@@ -119,6 +119,7 @@ interface StyleInterface
* Asks a choice question.
*
* @param string $question
* @param array $choices
* @param string|int|null $default
*
* @return mixed
+25 -18
View File
@@ -229,7 +229,7 @@ class SymfonyStyle extends OutputStyle
{
if (null !== $default) {
$values = array_flip($choices);
$default = isset($values[$default]) ? $values[$default] : $default;
$default = $values[$default];
}
return $this->askQuestion(new ChoiceQuestion($question, $choices, $default));
@@ -306,8 +306,14 @@ class SymfonyStyle extends OutputStyle
*/
public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
parent::writeln($messages, $type);
$this->bufferedOutput->writeln($this->reduceBuffer($messages), $type);
if (!is_iterable($messages)) {
$messages = [$messages];
}
foreach ($messages as $message) {
parent::writeln($message, $type);
$this->writeBuffer($message, true, $type);
}
}
/**
@@ -315,8 +321,14 @@ class SymfonyStyle extends OutputStyle
*/
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
parent::write($messages, $newline, $type);
$this->bufferedOutput->write($this->reduceBuffer($messages), $newline, $type);
if (!is_iterable($messages)) {
$messages = [$messages];
}
foreach ($messages as $message) {
parent::write($message, $newline, $type);
$this->writeBuffer($message, $newline, $type);
}
}
/**
@@ -338,10 +350,7 @@ class SymfonyStyle extends OutputStyle
return new self($this->input, $this->getErrorOutput());
}
/**
* @return ProgressBar
*/
private function getProgressBar()
private function getProgressBar(): ProgressBar
{
if (!$this->progressBar) {
throw new RuntimeException('The ProgressBar is not started.');
@@ -350,9 +359,9 @@ class SymfonyStyle extends OutputStyle
return $this->progressBar;
}
private function autoPrependBlock()
private function autoPrependBlock(): void
{
$chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
$chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
if (!isset($chars[0])) {
$this->newLine(); //empty history, so we should start with a new line.
@@ -363,7 +372,7 @@ class SymfonyStyle extends OutputStyle
$this->newLine(2 - substr_count($chars, "\n"));
}
private function autoPrependText()
private function autoPrependText(): void
{
$fetched = $this->bufferedOutput->fetch();
//Prepend new line if last char isn't EOL:
@@ -372,16 +381,14 @@ class SymfonyStyle extends OutputStyle
}
}
private function reduceBuffer($messages)
private function writeBuffer(string $message, bool $newLine, int $type): void
{
// We need to know if the two last chars are PHP_EOL
// Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer
return array_map(function ($value) {
return substr($value, -4);
}, array_merge([$this->bufferedOutput->fetch()], (array) $messages));
$this->bufferedOutput->write(substr($message, -4), $newLine, $type);
}
private function createBlock($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = false)
private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false)
{
$indentLength = 0;
$prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix);
@@ -399,7 +406,7 @@ class SymfonyStyle extends OutputStyle
$message = OutputFormatter::escape($message);
}
$lines = array_merge($lines, explode(\PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, \PHP_EOL, true)));
$lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true)));
if (\count($messages) > 1 && $key < \count($messages) - 1) {
$lines[] = '';
+27 -71
View File
@@ -15,7 +15,6 @@ class Terminal
{
private static $width;
private static $height;
private static $stty;
/**
* Gets the terminal width.
@@ -55,27 +54,6 @@ class Terminal
return self::$height ?: 50;
}
/**
* @internal
*
* @return bool
*/
public static function hasSttyAvailable()
{
if (null !== self::$stty) {
return self::$stty;
}
// skip check if exec function is disabled
if (!\function_exists('exec')) {
return false;
}
exec('stty 2>&1', $output, $exitcode);
return self::$stty = 0 === $exitcode;
}
private static function initDimensions()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
@@ -84,34 +62,12 @@ class Terminal
// or [w, h] from "wxh"
self::$width = (int) $matches[1];
self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2];
} elseif (!self::hasVt100Support() && self::hasSttyAvailable()) {
// only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash)
// testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT
self::initDimensionsUsingStty();
} elseif (null !== $dimensions = self::getConsoleMode()) {
// extract [w, h] from "wxh"
self::$width = (int) $dimensions[0];
self::$height = (int) $dimensions[1];
}
} else {
self::initDimensionsUsingStty();
}
}
/**
* Returns whether STDOUT has vt100 support (some Windows 10+ configurations).
*/
private static function hasVt100Support()
{
return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w'));
}
/**
* Initializes dimensions using the output of an stty columns line.
*/
private static function initDimensionsUsingStty()
{
if ($sttyString = self::getSttyColumns()) {
} elseif ($sttyString = self::getSttyColumns()) {
if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
// extract [w, h] from "rows h; columns w;"
self::$width = (int) $matches[2];
@@ -131,13 +87,25 @@ class Terminal
*/
private static function getConsoleMode()
{
$info = self::readFromProcess('mode CON');
if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
return null;
if (!\function_exists('proc_open')) {
return;
}
return [(int) $matches[2], (int) $matches[1]];
$descriptorspec = [
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
if (\is_resource($process)) {
$info = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
return [(int) $matches[2], (int) $matches[1]];
}
}
}
/**
@@ -146,19 +114,9 @@ class Terminal
* @return string|null
*/
private static function getSttyColumns()
{
return self::readFromProcess('stty -a | grep columns');
}
/**
* @param string $command
*
* @return string|null
*/
private static function readFromProcess($command)
{
if (!\function_exists('proc_open')) {
return null;
return;
}
$descriptorspec = [
@@ -166,16 +124,14 @@ class Terminal
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
if (!\is_resource($process)) {
return null;
$process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
if (\is_resource($process)) {
$info = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
return $info;
}
$info = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
return $info;
}
}
+9 -108
View File
@@ -13,10 +13,6 @@ namespace Symfony\Component\Console\Tester;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
/**
* Eases the testing of console applications.
@@ -30,14 +26,11 @@ use Symfony\Component\Console\Output\StreamOutput;
*/
class ApplicationTester
{
use TesterTrait;
private $application;
private $input;
private $statusCode;
/**
* @var OutputInterface
*/
private $output;
private $captureStreamsIndependently = false;
public function __construct(Application $application)
{
@@ -66,111 +59,19 @@ class ApplicationTester
$this->input->setInteractive($options['interactive']);
}
$this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately'];
if (!$this->captureStreamsIndependently) {
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
if (isset($options['decorated'])) {
$this->output->setDecorated($options['decorated']);
}
if (isset($options['verbosity'])) {
$this->output->setVerbosity($options['verbosity']);
}
} else {
$this->output = new ConsoleOutput(
isset($options['verbosity']) ? $options['verbosity'] : ConsoleOutput::VERBOSITY_NORMAL,
isset($options['decorated']) ? $options['decorated'] : null
);
$shellInteractive = getenv('SHELL_INTERACTIVE');
$errorOutput = new StreamOutput(fopen('php://memory', 'w', false));
$errorOutput->setFormatter($this->output->getFormatter());
$errorOutput->setVerbosity($this->output->getVerbosity());
$errorOutput->setDecorated($this->output->isDecorated());
$reflectedOutput = new \ReflectionObject($this->output);
$strErrProperty = $reflectedOutput->getProperty('stderr');
$strErrProperty->setAccessible(true);
$strErrProperty->setValue($this->output, $errorOutput);
$reflectedParent = $reflectedOutput->getParentClass();
$streamProperty = $reflectedParent->getProperty('stream');
$streamProperty->setAccessible(true);
$streamProperty->setValue($this->output, fopen('php://memory', 'w', false));
if ($this->inputs) {
$this->input->setStream(self::createStream($this->inputs));
putenv('SHELL_INTERACTIVE=1');
}
return $this->statusCode = $this->application->run($this->input, $this->output);
}
$this->initOutput($options);
/**
* Gets the display returned by the last execution of the application.
*
* @param bool $normalize Whether to normalize end of lines to \n or not
*
* @return string The display
*/
public function getDisplay($normalize = false)
{
rewind($this->output->getStream());
$this->statusCode = $this->application->run($this->input, $this->output);
$display = stream_get_contents($this->output->getStream());
putenv($shellInteractive ? "SHELL_INTERACTIVE=$shellInteractive" : 'SHELL_INTERACTIVE');
if ($normalize) {
$display = str_replace(\PHP_EOL, "\n", $display);
}
return $display;
}
/**
* Gets the output written to STDERR by the application.
*
* @param bool $normalize Whether to normalize end of lines to \n or not
*
* @return string
*/
public function getErrorOutput($normalize = false)
{
if (!$this->captureStreamsIndependently) {
throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.');
}
rewind($this->output->getErrorOutput()->getStream());
$display = stream_get_contents($this->output->getErrorOutput()->getStream());
if ($normalize) {
$display = str_replace(\PHP_EOL, "\n", $display);
}
return $display;
}
/**
* Gets the input instance used by the last execution of the application.
*
* @return InputInterface The current input instance
*/
public function getInput()
{
return $this->input;
}
/**
* Gets the output instance used by the last execution of the application.
*
* @return OutputInterface The current output instance
*/
public function getOutput()
{
return $this->output;
}
/**
* Gets the status code returned by the last execution of the application.
*
* @return int The status code
*/
public function getStatusCode()
{
return $this->statusCode;
}
}
+10 -94
View File
@@ -13,9 +13,6 @@ namespace Symfony\Component\Console\Tester;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
/**
* Eases the testing of console commands.
@@ -25,10 +22,10 @@ use Symfony\Component\Console\Output\StreamOutput;
*/
class CommandTester
{
use TesterTrait;
private $command;
private $input;
private $output;
private $inputs = [];
private $statusCode;
public function __construct(Command $command)
@@ -41,9 +38,10 @@ class CommandTester
*
* Available execution options:
*
* * interactive: Sets the input interactive flag
* * decorated: Sets the output decorated flag
* * verbosity: Sets the output verbosity flag
* * interactive: Sets the input interactive flag
* * decorated: Sets the output decorated flag
* * verbosity: Sets the output verbosity flag
* * capture_stderr_separately: Make output of stdOut and stdErr separately available
*
* @param array $input An array of command arguments and options
* @param array $options An array of execution options
@@ -69,94 +67,12 @@ class CommandTester
$this->input->setInteractive($options['interactive']);
}
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
$this->output->setDecorated(isset($options['decorated']) ? $options['decorated'] : false);
if (isset($options['verbosity'])) {
$this->output->setVerbosity($options['verbosity']);
if (!isset($options['decorated'])) {
$options['decorated'] = false;
}
$this->initOutput($options);
return $this->statusCode = $this->command->run($this->input, $this->output);
}
/**
* Gets the display returned by the last execution of the command.
*
* @param bool $normalize Whether to normalize end of lines to \n or not
*
* @return string The display
*/
public function getDisplay($normalize = false)
{
if (null === $this->output) {
throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?');
}
rewind($this->output->getStream());
$display = stream_get_contents($this->output->getStream());
if ($normalize) {
$display = str_replace(\PHP_EOL, "\n", $display);
}
return $display;
}
/**
* Gets the input instance used by the last execution of the command.
*
* @return InputInterface The current input instance
*/
public function getInput()
{
return $this->input;
}
/**
* Gets the output instance used by the last execution of the command.
*
* @return OutputInterface The current output instance
*/
public function getOutput()
{
return $this->output;
}
/**
* Gets the status code returned by the last execution of the application.
*
* @return int The status code
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* Sets the user inputs.
*
* @param array $inputs An array of strings representing each input
* passed to the command input stream
*
* @return CommandTester
*/
public function setInputs(array $inputs)
{
$this->inputs = $inputs;
return $this;
}
private static function createStream(array $inputs)
{
$stream = fopen('php://memory', 'r+', false);
foreach ($inputs as $input) {
fwrite($stream, $input.\PHP_EOL);
}
rewind($stream);
return $stream;
}
}
+227 -177
View File
@@ -18,9 +18,9 @@ use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\ArgvInput;
@@ -72,14 +72,14 @@ class ApplicationTest extends TestCase
require_once self::$fixturesPath.'/BarBucCommand.php';
require_once self::$fixturesPath.'/FooSubnamespaced1Command.php';
require_once self::$fixturesPath.'/FooSubnamespaced2Command.php';
require_once self::$fixturesPath.'/FooWithoutAliasCommand.php';
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering.php';
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering2.php';
require_once self::$fixturesPath.'/FooHiddenCommand.php';
}
protected function normalizeLineBreaks($text)
{
return str_replace(\PHP_EOL, "\n", $text);
return str_replace(PHP_EOL, "\n", $text);
}
/**
@@ -184,7 +184,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['test']);
$this->assertStringContainsString('It works!', $tester->getDisplay(true));
$this->assertContains('It works!', $tester->getDisplay(true));
}
public function testAdd()
@@ -200,10 +200,12 @@ class ApplicationTest extends TestCase
$this->assertEquals([$foo, $foo1], [$commands['foo:bar'], $commands['foo:bar1']], '->addCommands() registers an array of commands');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Command class "Foo5Command" is not correctly initialized. You probably forgot to call the parent constructor.
*/
public function testAddCommandWithEmptyConstructor()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Command class "Foo5Command" is not correctly initialized. You probably forgot to call the parent constructor.');
$application = new Application();
$application->add(new \Foo5Command());
}
@@ -266,10 +268,12 @@ class ApplicationTest extends TestCase
$this->assertEmpty($tester->getDisplay(true));
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
* @expectedExceptionMessage The command "foofoo" does not exist.
*/
public function testGetInvalidCommand()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage('The command "foofoo" does not exist.');
$application = new Application();
$application->get('foofoo');
}
@@ -309,8 +313,12 @@ class ApplicationTest extends TestCase
$expectedMsg = "The namespace \"f\" is ambiguous.\nDid you mean one of these?\n foo\n foo1";
$this->expectException(CommandNotFoundException::class);
$this->expectExceptionMessage($expectedMsg);
if (method_exists($this, 'expectException')) {
$this->expectException(NamespaceNotFoundException::class);
$this->expectExceptionMessage($expectedMsg);
} else {
$this->setExpectedException(NamespaceNotFoundException::class, $expectedMsg);
}
$application->findNamespace('f');
}
@@ -323,18 +331,22 @@ class ApplicationTest extends TestCase
$this->assertEquals('test-ambiguous', $application->find('test')->getName());
}
/**
* @expectedException \Symfony\Component\Console\Exception\NamespaceNotFoundException
* @expectedExceptionMessage There are no commands defined in the "bar" namespace.
*/
public function testFindInvalidNamespace()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage('There are no commands defined in the "bar" namespace.');
$application = new Application();
$application->findNamespace('bar');
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
* @expectedExceptionMessage Command "foo1" is not defined
*/
public function testFindUniqueNameButNamespaceName()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage('Command "foo1" is not defined');
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \Foo1Command());
@@ -377,10 +389,12 @@ class ApplicationTest extends TestCase
$this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('FoO:BaR'), '->find() will fallback to case insensitivity');
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
* @expectedExceptionMessage Command "FoO:BaR" is ambiguous
*/
public function testFindCaseInsensitiveSuggestions()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage('Command "FoO:BaR" is ambiguous');
$application = new Application();
$application->add(new \FooSameCaseLowercaseCommand());
$application->add(new \FooSameCaseUppercaseCommand());
@@ -408,8 +422,12 @@ class ApplicationTest extends TestCase
public function testFindWithAmbiguousAbbreviations($abbreviation, $expectedExceptionMessage)
{
putenv('COLUMNS=120');
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage($expectedExceptionMessage);
if (method_exists($this, 'expectException')) {
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage($expectedExceptionMessage);
} else {
$this->setExpectedException('Symfony\Component\Console\Exception\CommandNotFoundException', $expectedExceptionMessage);
}
$application = new Application();
$application->add(new \FooCommand());
@@ -468,17 +486,63 @@ class ApplicationTest extends TestCase
}
/**
* @dataProvider provideInvalidCommandNamesSingle
* @dataProvider provideInvalidCommandNamesSingle
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
* @expectedExceptionMessage Did you mean this
*/
public function testFindAlternativeExceptionMessageSingle($name)
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage('Did you mean this');
$application = new Application();
$application->add(new \Foo3Command());
$application->find($name);
}
public function testDontRunAlternativeNamespaceName()
{
$application = new Application();
$application->add(new \Foo1Command());
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foos:bar1'], ['decorated' => false]);
$this->assertSame('
There are no commands defined in the "foos" namespace.
Did you mean this?
foo
', $tester->getDisplay(true));
}
public function testCanRunAlternativeCommandName()
{
$application = new Application();
$application->add(new \FooWithoutAliasCommand());
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->setInputs(['y']);
$tester->run(['command' => 'foos'], ['decorated' => false]);
$display = trim($tester->getDisplay(true));
$this->assertContains('Command "foos" is not defined', $display);
$this->assertContains('Do you want to run "foo" instead? (yes/no) [no]:', $display);
$this->assertContains('called', $display);
}
public function testDontRunAlternativeCommandName()
{
$application = new Application();
$application->add(new \FooWithoutAliasCommand());
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->setInputs(['n']);
$exitCode = $tester->run(['command' => 'foos'], ['decorated' => false]);
$this->assertSame(1, $exitCode);
$display = trim($tester->getDisplay(true));
$this->assertContains('Command "foos" is not defined', $display);
$this->assertContains('Do you want to run "foo" instead? (yes/no) [no]:', $display);
}
public function provideInvalidCommandNamesSingle()
{
return [
@@ -501,9 +565,9 @@ class ApplicationTest extends TestCase
$this->fail('->find() throws a CommandNotFoundException if command does not exist, with alternatives');
} catch (\Exception $e) {
$this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertMatchesRegularExpression('/Did you mean one of these/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertMatchesRegularExpression('/foo1:bar/', $e->getMessage());
$this->assertMatchesRegularExpression('/foo:bar/', $e->getMessage());
$this->assertRegExp('/Did you mean one of these/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertRegExp('/foo1:bar/', $e->getMessage());
$this->assertRegExp('/foo:bar/', $e->getMessage());
}
// Namespace + plural
@@ -512,8 +576,8 @@ class ApplicationTest extends TestCase
$this->fail('->find() throws a CommandNotFoundException if command does not exist, with alternatives');
} catch (\Exception $e) {
$this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertMatchesRegularExpression('/Did you mean one of these/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertMatchesRegularExpression('/foo1/', $e->getMessage());
$this->assertRegExp('/Did you mean one of these/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertRegExp('/foo1/', $e->getMessage());
}
$application->add(new \Foo3Command());
@@ -521,12 +585,12 @@ class ApplicationTest extends TestCase
// Subnamespace + plural
try {
$application->find('foo3:');
$a = $application->find('foo3:');
$this->fail('->find() should throw an Symfony\Component\Console\Exception\CommandNotFoundException if a command is ambiguous because of a subnamespace, with alternatives');
} catch (\Exception $e) {
$this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e);
$this->assertMatchesRegularExpression('/foo3:bar/', $e->getMessage());
$this->assertMatchesRegularExpression('/foo3:bar:toh/', $e->getMessage());
$this->assertRegExp('/foo3:bar/', $e->getMessage());
$this->assertRegExp('/foo3:bar:toh/', $e->getMessage());
}
}
@@ -555,10 +619,10 @@ class ApplicationTest extends TestCase
} catch (\Exception $e) {
$this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if command does not exist');
$this->assertSame(['afoobar1', 'foo:bar1'], $e->getAlternatives());
$this->assertMatchesRegularExpression(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertMatchesRegularExpression('/afoobar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "afoobar1"');
$this->assertMatchesRegularExpression('/foo:bar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "foo:bar1"');
$this->assertDoesNotMatchRegularExpression('/foo:bar(?!1)/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, without "foo:bar" alternative');
$this->assertRegExp(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertRegExp('/afoobar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "afoobar1"');
$this->assertRegExp('/foo:bar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "foo:bar1"');
$this->assertNotRegExp('/foo:bar(?>!1)/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, without "foo:bar" alternative');
}
}
@@ -568,9 +632,6 @@ class ApplicationTest extends TestCase
$fooCommand->setAliases(['foo2']);
$application = new Application();
$application->setCommandLoader(new FactoryCommandLoader([
'foo3' => static function () use ($fooCommand) { return $fooCommand; },
]));
$application->add($fooCommand);
$result = $application->find('foo');
@@ -600,15 +661,16 @@ class ApplicationTest extends TestCase
$application->find('foo2:command');
$this->fail('->find() throws a CommandNotFoundException if namespace does not exist');
} catch (\Exception $e) {
$this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if namespace does not exist');
$this->assertInstanceOf('Symfony\Component\Console\Exception\NamespaceNotFoundException', $e, '->find() throws a NamespaceNotFoundException if namespace does not exist');
$this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, 'NamespaceNotFoundException extends from CommandNotFoundException');
$this->assertCount(3, $e->getAlternatives());
$this->assertContains('foo', $e->getAlternatives());
$this->assertContains('foo1', $e->getAlternatives());
$this->assertContains('foo3', $e->getAlternatives());
$this->assertMatchesRegularExpression('/There are no commands defined in the "foo2" namespace./', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative');
$this->assertMatchesRegularExpression('/foo/', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative : "foo"');
$this->assertMatchesRegularExpression('/foo1/', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative : "foo1"');
$this->assertMatchesRegularExpression('/foo3/', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative : "foo3"');
$this->assertRegExp('/There are no commands defined in the "foo2" namespace./', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative');
$this->assertRegExp('/foo/', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative : "foo"');
$this->assertRegExp('/foo1/', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative : "foo1"');
$this->assertRegExp('/foo3/', $e->getMessage(), '->find() throws a CommandNotFoundException if namespace does not exist, with alternative : "foo3"');
}
}
@@ -620,7 +682,6 @@ class ApplicationTest extends TestCase
$application->add(new \Foo1Command());
$application->add(new \Foo2Command());
$application->add(new \Foo3Command());
$application->add(new \FooHiddenCommand());
$expectedAlternatives = [
'afoobar',
@@ -639,7 +700,7 @@ class ApplicationTest extends TestCase
$this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if command is not defined');
$this->assertSame($expectedAlternatives, $e->getAlternatives());
$this->assertMatchesRegularExpression('/Command "foo" is not defined\..*Did you mean one of these\?.*/Ums', $e->getMessage());
$this->assertRegExp('/Command "foo" is not defined\..*Did you mean one of these\?.*/Ums', $e->getMessage());
}
}
@@ -653,10 +714,12 @@ class ApplicationTest extends TestCase
$this->assertEquals('foo:sublong', $application->findNamespace('f:sub'));
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
* @expectedExceptionMessage Command "foo::bar" is not defined.
*/
public function testFindWithDoubleColonInNameThrowsException()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$this->expectExceptionMessage('Command "foo::bar" is not defined.');
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \Foo4Command());
@@ -710,7 +773,7 @@ class ApplicationTest extends TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exception');
$tester->run(['command' => 'foo'], ['decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE, 'capture_stderr_separately' => true]);
$this->assertStringContainsString('Exception trace', $tester->getErrorOutput(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose');
$this->assertContains('Exception trace', $tester->getErrorOutput(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose');
$tester->run(['command' => 'list', '--foo' => true], ['decorated' => false, 'capture_stderr_separately' => true]);
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getErrorOutput(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command');
@@ -721,9 +784,9 @@ class ApplicationTest extends TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions');
$tester->run(['command' => 'foo3:bar'], ['decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE]);
$this->assertMatchesRegularExpression('/\[Exception\]\s*First exception/', $tester->getDisplay(), '->renderException() renders a pretty exception without code exception when code exception is default and verbosity is verbose');
$this->assertMatchesRegularExpression('/\[Exception\]\s*Second exception/', $tester->getDisplay(), '->renderException() renders a pretty exception without code exception when code exception is 0 and verbosity is verbose');
$this->assertMatchesRegularExpression('/\[Exception \(404\)\]\s*Third exception/', $tester->getDisplay(), '->renderException() renders a pretty exception with code exception when code exception is 404 and verbosity is verbose');
$this->assertRegExp('/\[Exception\]\s*First exception/', $tester->getDisplay(), '->renderException() renders a pretty exception without code exception when code exception is default and verbosity is verbose');
$this->assertRegExp('/\[Exception\]\s*Second exception/', $tester->getDisplay(), '->renderException() renders a pretty exception without code exception when code exception is 0 and verbosity is verbose');
$this->assertRegExp('/\[Exception \(404\)\]\s*Third exception/', $tester->getDisplay(), '->renderException() renders a pretty exception with code exception when code exception is 404 and verbosity is verbose');
$tester->run(['command' => 'foo3:bar'], ['decorated' => true]);
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3decorated.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions');
@@ -800,18 +863,54 @@ class ApplicationTest extends TestCase
$this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_renderexception_linebreaks.txt', $tester->getDisplay(true), '->renderException() keep multiple line breaks');
}
public function testRenderAnonymousException()
{
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new class('') extends \InvalidArgumentException {
};
});
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo'], ['decorated' => false]);
$this->assertContains('[InvalidArgumentException@anonymous]', $tester->getDisplay(true));
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() {
})));
});
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo'], ['decorated' => false]);
$this->assertContains('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true));
}
public function testRenderExceptionStackTraceContainsRootException()
{
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new \Exception('Verbose exception');
throw new class('') extends \InvalidArgumentException {
};
});
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo'], ['decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE]);
$this->assertStringContainsString(sprintf('() at %s:', __FILE__), $tester->getDisplay());
$tester->run(['command' => 'foo'], ['decorated' => false]);
$this->assertContains('[InvalidArgumentException@anonymous]', $tester->getDisplay(true));
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() {
})));
});
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo'], ['decorated' => false]);
$this->assertContains('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true));
}
public function testRun()
@@ -902,10 +1001,10 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo:bar', '--no-interaction' => true], ['decorated' => false]);
$this->assertSame('called'.\PHP_EOL, $tester->getDisplay(), '->run() does not call interact() if --no-interaction is passed');
$this->assertSame('called'.PHP_EOL, $tester->getDisplay(), '->run() does not call interact() if --no-interaction is passed');
$tester->run(['command' => 'foo:bar', '-n' => true], ['decorated' => false]);
$this->assertSame('called'.\PHP_EOL, $tester->getDisplay(), '->run() does not call interact() if -n is passed');
$this->assertSame('called'.PHP_EOL, $tester->getDisplay(), '->run() does not call interact() if -n is passed');
}
public function testRunWithGlobalOptionAndNoCommand()
@@ -1026,10 +1125,12 @@ class ApplicationTest extends TestCase
$this->assertTrue($passedRightValue, '-> exit code 1 was passed in the console.terminate event');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage An option with shortcut "e" already exists.
*/
public function testAddingOptionWithDuplicateShortcut()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('An option with shortcut "e" already exists.');
$dispatcher = new EventDispatcher();
$application = new Application();
$application->setAutoExit(false);
@@ -1052,11 +1153,11 @@ class ApplicationTest extends TestCase
}
/**
* @expectedException \LogicException
* @dataProvider getAddingAlreadySetDefinitionElementData
*/
public function testAddingAlreadySetDefinitionElementData($def)
{
$this->expectException('LogicException');
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
@@ -1202,13 +1303,15 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo']);
$this->assertEquals('before.foo.after.'.\PHP_EOL, $tester->getDisplay());
$this->assertEquals('before.foo.after.'.PHP_EOL, $tester->getDisplay());
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage error
*/
public function testRunWithExceptionAndDispatcher()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('error');
$application = new Application();
$application->setDispatcher($this->getDispatcher());
$application->setAutoExit(false);
@@ -1236,7 +1339,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo']);
$this->assertStringContainsString('before.foo.error.after.', $tester->getDisplay());
$this->assertContains('before.foo.error.after.', $tester->getDisplay());
}
public function testRunDispatchesAllEventsWithExceptionInListener()
@@ -1256,12 +1359,9 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo']);
$this->assertStringContainsString('before.error.after.', $tester->getDisplay());
$this->assertContains('before.error.after.', $tester->getDisplay());
}
/**
* @requires PHP 7
*/
public function testRunWithError()
{
$application = new Application();
@@ -1307,7 +1407,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo']);
$this->assertStringContainsString('before.error.silenced.after.', $tester->getDisplay());
$this->assertContains('before.error.silenced.after.', $tester->getDisplay());
$this->assertEquals(ConsoleCommandEvent::RETURN_CODE_DISABLED, $tester->getStatusCode());
}
@@ -1326,40 +1426,10 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'unknown']);
$this->assertStringContainsString('silenced command not found', $tester->getDisplay());
$this->assertContains('silenced command not found', $tester->getDisplay());
$this->assertEquals(1, $tester->getStatusCode());
}
/**
* @group legacy
* @expectedDeprecation The "ConsoleEvents::EXCEPTION" event is deprecated since Symfony 3.3 and will be removed in 4.0. Listen to the "ConsoleEvents::ERROR" event instead.
*/
public function testLegacyExceptionListenersAreStillTriggered()
{
$dispatcher = $this->getDispatcher();
$dispatcher->addListener('console.exception', function (ConsoleExceptionEvent $event) {
$event->getOutput()->write('caught.');
$event->setException(new \RuntimeException('replaced in caught.'));
});
$application = new Application();
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
throw new \RuntimeException('foo');
});
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo']);
$this->assertStringContainsString('before.caught.error.after.', $tester->getDisplay());
$this->assertStringContainsString('replaced in caught.', $tester->getDisplay());
}
/**
* @requires PHP 7
*/
public function testErrorIsRethrownIfNotHandledByConsoleErrorEvent()
{
$application = new Application();
@@ -1367,8 +1437,8 @@ class ApplicationTest extends TestCase
$application->setCatchExceptions(false);
$application->setDispatcher(new EventDispatcher());
$application->register('dym')->setCode(function () {
throw new \Error('Something went wrong.');
$application->register('dym')->setCode(function (InputInterface $input, OutputInterface $output) {
new \UnknownClass();
});
$tester = new ApplicationTester($application);
@@ -1377,17 +1447,16 @@ class ApplicationTest extends TestCase
$tester->run(['command' => 'dym']);
$this->fail('->run() should rethrow PHP errors if not handled via ConsoleErrorEvent.');
} catch (\Error $e) {
$this->assertSame('Something went wrong.', $e->getMessage());
$this->assertSame($e->getMessage(), 'Class \'UnknownClass\' not found');
}
}
/**
* @requires PHP 7
* @expectedException \LogicException
* @expectedExceptionMessage error
*/
public function testRunWithErrorAndDispatcher()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('error');
$application = new Application();
$application->setDispatcher($this->getDispatcher());
$application->setAutoExit(false);
@@ -1401,12 +1470,9 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'dym']);
$this->assertStringContainsString('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
$this->assertContains('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
}
/**
* @requires PHP 7
*/
public function testRunDispatchesAllEventsWithError()
{
$application = new Application();
@@ -1421,12 +1487,9 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'dym']);
$this->assertStringContainsString('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
$this->assertContains('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events');
}
/**
* @requires PHP 7
*/
public function testRunWithErrorFailingStatusCode()
{
$application = new Application();
@@ -1456,7 +1519,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$exitCode = $tester->run(['command' => 'foo']);
$this->assertStringContainsString('before.after.', $tester->getDisplay());
$this->assertContains('before.after.', $tester->getDisplay());
$this->assertEquals(ConsoleCommandEvent::RETURN_CODE_DISABLED, $exitCode);
}
@@ -1517,24 +1580,6 @@ class ApplicationTest extends TestCase
$this->assertEquals('some test value', $extraValue);
}
/**
* @group legacy
*/
public function testTerminalDimensions()
{
$application = new Application();
$originalDimensions = $application->getTerminalDimensions();
$this->assertCount(2, $originalDimensions);
$width = 80;
if ($originalDimensions[0] == $width) {
$width = 100;
}
$application->setTerminalDimensions($width, 80);
$this->assertSame([$width, 80], $application->getTerminalDimensions());
}
public function testSetRunCustomDefaultCommand()
{
$command = new \FooCommand();
@@ -1546,7 +1591,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run([], ['interactive' => false]);
$this->assertEquals('called'.\PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command');
$this->assertEquals('called'.PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command');
$application = new CustomDefaultCommandApplication();
$application->setAutoExit(false);
@@ -1554,7 +1599,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run([], ['interactive' => false]);
$this->assertEquals('called'.\PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command');
$this->assertEquals('called'.PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command');
}
public function testSetRunCustomDefaultCommandWithOption()
@@ -1569,7 +1614,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['--fooopt' => 'opt'], ['interactive' => false]);
$this->assertEquals('called'.\PHP_EOL.'opt'.\PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command');
$this->assertEquals('called'.PHP_EOL.'opt'.PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command');
}
public function testSetRunCustomSingleCommand()
@@ -1584,10 +1629,27 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run([]);
$this->assertStringContainsString('called', $tester->getDisplay());
$this->assertContains('called', $tester->getDisplay());
$tester->run(['--help' => true]);
$this->assertStringContainsString('The foo:bar command', $tester->getDisplay());
$this->assertContains('The foo:bar command', $tester->getDisplay());
}
/**
* @requires function posix_isatty
*/
public function testCanCheckIfTerminalIsInteractive()
{
$application = new CustomDefaultCommandApplication();
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->run(['command' => 'help']);
$this->assertFalse($tester->getInput()->hasParameterOption(['--no-interaction', '-n']));
$inputStream = $tester->getInput()->getStream();
$this->assertEquals($tester->getInput()->isInteractive(), @posix_isatty($inputStream));
}
public function testRunLazyCommandService()
@@ -1620,9 +1682,11 @@ class ApplicationTest extends TestCase
$this->assertSame(['lazy:alias', 'lazy:alias2'], $command->getAliases());
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
*/
public function testGetDisabledLazyCommand()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
$application = new Application();
$application->setCommandLoader(new FactoryCommandLoader(['disabled' => function () { return new DisabledCommand(); }]));
$application->get('disabled');
@@ -1642,31 +1706,6 @@ class ApplicationTest extends TestCase
$this->assertArrayNotHasKey('disabled', $application->all());
}
public function testFindAlternativesDoesNotLoadSameNamespaceCommandsOnExactMatch()
{
$application = new Application();
$application->setAutoExit(false);
$loaded = [];
$application->setCommandLoader(new FactoryCommandLoader([
'foo:bar' => function () use (&$loaded) {
$loaded['foo:bar'] = true;
return (new Command('foo:bar'))->setCode(function () {});
},
'foo' => function () use (&$loaded) {
$loaded['foo'] = true;
return (new Command('foo'))->setCode(function () {});
},
]));
$application->run(new ArrayInput(['command' => 'foo']), new NullOutput());
$this->assertSame(['foo' => true], $loaded);
}
protected function getDispatcher($skipCommand = false)
{
$dispatcher = new EventDispatcher();
@@ -1693,17 +1732,14 @@ class ApplicationTest extends TestCase
return $dispatcher;
}
/**
* @requires PHP 7
*/
public function testErrorIsRethrownIfNotHandledByConsoleErrorEventWithCatchingEnabled()
{
$application = new Application();
$application->setAutoExit(false);
$application->setDispatcher(new EventDispatcher());
$application->register('dym')->setCode(function () {
throw new \Error('Something went wrong.');
$application->register('dym')->setCode(function (InputInterface $input, OutputInterface $output) {
new \UnknownClass();
});
$tester = new ApplicationTester($application);
@@ -1712,22 +1748,36 @@ class ApplicationTest extends TestCase
$tester->run(['command' => 'dym']);
$this->fail('->run() should rethrow PHP errors if not handled via ConsoleErrorEvent.');
} catch (\Error $e) {
$this->assertSame('Something went wrong.', $e->getMessage());
$this->assertSame($e->getMessage(), 'Class \'UnknownClass\' not found');
}
}
public function testCommandNameMismatchWithCommandLoaderKeyThrows()
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage foo
*/
public function testThrowingErrorListener()
{
$this->expectException(CommandNotFoundException::class);
$this->expectExceptionMessage('The "test" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".');
$dispatcher = $this->getDispatcher();
$dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) {
throw new \RuntimeException('foo');
});
$app = new Application();
$loader = new FactoryCommandLoader([
'test' => static function () { return new Command('test-command'); },
]);
$dispatcher->addListener('console.command', function () {
throw new \RuntimeException('bar');
});
$app->setCommandLoader($loader);
$app->get('test');
$application = new Application();
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
$application->setCatchExceptions(false);
$application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
$output->write('foo.');
});
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo']);
}
}
+35 -29
View File
@@ -40,10 +40,12 @@ class CommandTest extends TestCase
$this->assertEquals('foo:bar', $command->getName(), '__construct() takes the command name as its first argument');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage The command defined in "Symfony\Component\Console\Command\Command" cannot have an empty name.
*/
public function testCommandNameCannotBeEmpty()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('The command defined in "Symfony\Component\Console\Command\Command" cannot have an empty name.');
(new Application())->add(new Command());
}
@@ -115,8 +117,12 @@ class CommandTest extends TestCase
*/
public function testInvalidCommandNames($name)
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage(sprintf('Command name "%s" is invalid.', $name));
if (method_exists($this, 'expectException')) {
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage(sprintf('Command name "%s" is invalid.', $name));
} else {
$this->setExpectedException('InvalidArgumentException', sprintf('Command name "%s" is invalid.', $name));
}
$command = new \TestCommand();
$command->setName($name);
@@ -154,20 +160,20 @@ class CommandTest extends TestCase
{
$command = new \TestCommand();
$command->setHelp('The %command.name% command does... Example: php %command.full_name%.');
$this->assertStringContainsString('The namespace:name command does...', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.name% correctly');
$this->assertStringNotContainsString('%command.full_name%', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.full_name%');
$this->assertContains('The namespace:name command does...', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.name% correctly');
$this->assertNotContains('%command.full_name%', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.full_name%');
$command = new \TestCommand();
$command->setHelp('');
$this->assertStringContainsString('description', $command->getProcessedHelp(), '->getProcessedHelp() falls back to the description');
$this->assertContains('description', $command->getProcessedHelp(), '->getProcessedHelp() falls back to the description');
$command = new \TestCommand();
$command->setHelp('The %command.name% command does... Example: php %command.full_name%.');
$application = new Application();
$application->add($command);
$application->setDefaultCommand('namespace:name', true);
$this->assertStringContainsString('The namespace:name command does...', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.name% correctly in single command applications');
$this->assertStringNotContainsString('%command.full_name%', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.full_name% in single command applications');
$this->assertContains('The namespace:name command does...', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.name% correctly in single command applications');
$this->assertNotContains('%command.full_name%', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.full_name% in single command applications');
}
public function testGetSetAliases()
@@ -182,7 +188,7 @@ class CommandTest extends TestCase
public function testSetAliasesNull()
{
$command = new \TestCommand();
$this->expectException('InvalidArgumentException');
$this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('InvalidArgumentException');
$command->setAliases(null);
}
@@ -212,10 +218,12 @@ class CommandTest extends TestCase
$this->assertEquals($formatterHelper->getName(), $command->getHelper('formatter')->getName(), '->getHelper() returns the correct helper');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot retrieve helper "formatter" because there is no HelperSet defined.
*/
public function testGetHelperWithoutHelperSet()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Cannot retrieve helper "formatter" because there is no HelperSet defined.');
$command = new \TestCommand();
$command->getHelper('formatter');
}
@@ -271,7 +279,7 @@ class CommandTest extends TestCase
$tester->execute([], ['interactive' => true]);
$this->assertEquals('interact called'.\PHP_EOL.'execute called'.\PHP_EOL, $tester->getDisplay(), '->run() calls the interact() method if the input is interactive');
$this->assertEquals('interact called'.PHP_EOL.'execute called'.PHP_EOL, $tester->getDisplay(), '->run() calls the interact() method if the input is interactive');
}
public function testRunNonInteractive()
@@ -280,21 +288,25 @@ class CommandTest extends TestCase
$tester->execute([], ['interactive' => false]);
$this->assertEquals('execute called'.\PHP_EOL, $tester->getDisplay(), '->run() does not call the interact() method if the input is not interactive');
$this->assertEquals('execute called'.PHP_EOL, $tester->getDisplay(), '->run() does not call the interact() method if the input is not interactive');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage You must override the execute() method in the concrete command class.
*/
public function testExecuteMethodNeedsToBeOverridden()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('You must override the execute() method in the concrete command class.');
$command = new Command('foo');
$command->run(new StringInput(''), new NullOutput());
}
/**
* @expectedException \Symfony\Component\Console\Exception\InvalidOptionException
* @expectedExceptionMessage The "--bar" option does not exist.
*/
public function testRunWithInvalidOption()
{
$this->expectException('Symfony\Component\Console\Exception\InvalidOptionException');
$this->expectExceptionMessage('The "--bar" option does not exist.');
$command = new \TestCommand();
$tester = new CommandTester($command);
$tester->execute(['--bar' => true]);
@@ -337,7 +349,7 @@ class CommandTest extends TestCase
$command->setProcessTitle('foo');
$this->assertSame(0, $command->run(new StringInput(''), new NullOutput()));
if (\function_exists('cli_set_process_title')) {
if (null === @cli_get_process_title() && 'Darwin' === \PHP_OS) {
if (null === @cli_get_process_title() && 'Darwin' === PHP_OS) {
$this->markTestSkipped('Running "cli_get_process_title" as an unprivileged user is not supported on MacOS.');
}
$this->assertEquals('foo', cli_get_process_title());
@@ -353,7 +365,7 @@ class CommandTest extends TestCase
$this->assertEquals($command, $ret, '->setCode() implements a fluent interface');
$tester = new CommandTester($command);
$tester->execute([]);
$this->assertEquals('interact called'.\PHP_EOL.'from the code...'.\PHP_EOL, $tester->getDisplay());
$this->assertEquals('interact called'.PHP_EOL.'from the code...'.PHP_EOL, $tester->getDisplay());
}
public function getSetCodeBindToClosureTests()
@@ -378,7 +390,7 @@ class CommandTest extends TestCase
$command->setCode($code);
$tester = new CommandTester($command);
$tester->execute([]);
$this->assertEquals('interact called'.\PHP_EOL.$expected.\PHP_EOL, $tester->getDisplay());
$this->assertEquals('interact called'.PHP_EOL.$expected.PHP_EOL, $tester->getDisplay());
}
public function testSetCodeWithStaticClosure()
@@ -388,13 +400,7 @@ class CommandTest extends TestCase
$tester = new CommandTester($command);
$tester->execute([]);
if (\PHP_VERSION_ID < 70000) {
// Cannot bind static closures in PHP 5
$this->assertEquals('interact called'.\PHP_EOL.'not bound'.\PHP_EOL, $tester->getDisplay());
} else {
// Can bind static closures in PHP 7
$this->assertEquals('interact called'.\PHP_EOL.'bound'.\PHP_EOL, $tester->getDisplay());
}
$this->assertEquals('interact called'.PHP_EOL.'bound'.PHP_EOL, $tester->getDisplay());
}
private static function createClosure()
@@ -411,7 +417,7 @@ class CommandTest extends TestCase
$this->assertEquals($command, $ret, '->setCode() implements a fluent interface');
$tester = new CommandTester($command);
$tester->execute([]);
$this->assertEquals('interact called'.\PHP_EOL.'from the code...'.\PHP_EOL, $tester->getDisplay());
$this->assertEquals('interact called'.PHP_EOL.'from the code...'.PHP_EOL, $tester->getDisplay());
}
public function callableMethodCommand(InputInterface $input, OutputInterface $output)
+12 -12
View File
@@ -25,9 +25,9 @@ class HelpCommandTest extends TestCase
$command->setApplication(new Application());
$commandTester = new CommandTester($command);
$commandTester->execute(['command_name' => 'li'], ['decorated' => false]);
$this->assertStringContainsString('list [options] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command alias');
$this->assertStringContainsString('format=FORMAT', $commandTester->getDisplay(), '->execute() returns a text help for the given command alias');
$this->assertStringContainsString('raw', $commandTester->getDisplay(), '->execute() returns a text help for the given command alias');
$this->assertContains('list [options] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command alias');
$this->assertContains('format=FORMAT', $commandTester->getDisplay(), '->execute() returns a text help for the given command alias');
$this->assertContains('raw', $commandTester->getDisplay(), '->execute() returns a text help for the given command alias');
}
public function testExecuteForCommand()
@@ -36,9 +36,9 @@ class HelpCommandTest extends TestCase
$commandTester = new CommandTester($command);
$command->setCommand(new ListCommand());
$commandTester->execute([], ['decorated' => false]);
$this->assertStringContainsString('list [options] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertStringContainsString('format=FORMAT', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertStringContainsString('raw', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertContains('list [options] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertContains('format=FORMAT', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertContains('raw', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
}
public function testExecuteForCommandWithXmlOption()
@@ -47,7 +47,7 @@ class HelpCommandTest extends TestCase
$commandTester = new CommandTester($command);
$command->setCommand(new ListCommand());
$commandTester->execute(['--format' => 'xml']);
$this->assertStringContainsString('<command', $commandTester->getDisplay(), '->execute() returns an XML help text if --xml is passed');
$this->assertContains('<command', $commandTester->getDisplay(), '->execute() returns an XML help text if --xml is passed');
}
public function testExecuteForApplicationCommand()
@@ -55,9 +55,9 @@ class HelpCommandTest extends TestCase
$application = new Application();
$commandTester = new CommandTester($application->get('help'));
$commandTester->execute(['command_name' => 'list']);
$this->assertStringContainsString('list [options] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertStringContainsString('format=FORMAT', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertStringContainsString('raw', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertContains('list [options] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertContains('format=FORMAT', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertContains('raw', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
}
public function testExecuteForApplicationCommandWithXmlOption()
@@ -65,7 +65,7 @@ class HelpCommandTest extends TestCase
$application = new Application();
$commandTester = new CommandTester($application->get('help'));
$commandTester->execute(['command_name' => 'list', '--format' => 'xml']);
$this->assertStringContainsString('list [--raw] [--format FORMAT] [--] [&lt;namespace&gt;]', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertStringContainsString('<command', $commandTester->getDisplay(), '->execute() returns an XML help text if --format=xml is passed');
$this->assertContains('list [--raw] [--format FORMAT] [--] [&lt;namespace&gt;]', $commandTester->getDisplay(), '->execute() returns a text help for the given command');
$this->assertContains('<command', $commandTester->getDisplay(), '->execute() returns an XML help text if --format=xml is passed');
}
}
+2 -2
View File
@@ -23,7 +23,7 @@ class ListCommandTest extends TestCase
$commandTester = new CommandTester($command = $application->get('list'));
$commandTester->execute(['command' => $command->getName()], ['decorated' => false]);
$this->assertMatchesRegularExpression('/help\s{2,}Displays help for a command/', $commandTester->getDisplay(), '->execute() returns a list of available commands');
$this->assertRegExp('/help\s{2,}Displays help for a command/', $commandTester->getDisplay(), '->execute() returns a list of available commands');
}
public function testExecuteListsCommandsWithXmlOption()
@@ -31,7 +31,7 @@ class ListCommandTest extends TestCase
$application = new Application();
$commandTester = new CommandTester($command = $application->get('list'));
$commandTester->execute(['command' => $command->getName(), '--format' => 'xml']);
$this->assertMatchesRegularExpression('/<command id="list" name="list" hidden="0">/', $commandTester->getDisplay(), '->execute() returns a list of available commands in XML if --xml is passed');
$this->assertRegExp('/<command id="list" name="list" hidden="0">/', $commandTester->getDisplay(), '->execute() returns a list of available commands in XML if --xml is passed');
}
public function testExecuteListsCommandsWithRawOption()
+1 -1
View File
@@ -41,7 +41,7 @@ class LockableTraitTest extends TestCase
{
$command = new \FooLockCommand();
if (SemaphoreStore::isSupported(false)) {
if (SemaphoreStore::isSupported()) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore();
@@ -41,9 +41,11 @@ class ContainerCommandLoaderTest extends TestCase
$this->assertInstanceOf(Command::class, $loader->get('bar'));
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
*/
public function testGetUnknownCommandThrows()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
(new ContainerCommandLoader(new ServiceLocator([]), []))->get('unknown');
}
@@ -40,9 +40,11 @@ class FactoryCommandLoaderTest extends TestCase
$this->assertInstanceOf(Command::class, $loader->get('bar'));
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
*/
public function testGetUnknownCommandThrows()
{
$this->expectException('Symfony\Component\Console\Exception\CommandNotFoundException');
(new FactoryCommandLoader([]))->get('unknown');
}
@@ -33,28 +33,27 @@ class AddConsoleCommandPassTest extends TestCase
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->setParameter('my-command.class', 'Symfony\Component\Console\Tests\DependencyInjection\MyCommand');
$id = 'my-command';
$definition = new Definition('%my-command.class%');
$definition->setPublic($public);
$definition->addTag('console.command');
$container->setDefinition('my-command', $definition);
$container->setDefinition($id, $definition);
$container->compile();
$alias = 'console.command.symfony_component_console_tests_dependencyinjection_mycommand';
$alias = 'console.command.public_alias.my-command';
if ($public) {
$this->assertFalse($container->hasAlias($alias));
$id = 'my-command';
} else {
$id = $alias;
// The alias is replaced by a Definition by the ReplaceAliasByActualDefinitionPass
// in case the original service is private
$this->assertFalse($container->hasDefinition('my-command'));
$this->assertFalse($container->hasDefinition($id));
$this->assertTrue($container->hasDefinition($alias));
}
$this->assertTrue($container->hasParameter('console.command.ids'));
$this->assertSame([$alias => $id], $container->getParameter('console.command.ids'));
$this->assertSame([$public ? $id : $alias], $container->getParameter('console.command.ids'));
}
public function testProcessRegistersLazyCommands()
@@ -75,8 +74,7 @@ class AddConsoleCommandPassTest extends TestCase
$this->assertSame(ContainerCommandLoader::class, $commandLoader->getClass());
$this->assertSame(['my:command' => 'my-command', 'my:alias' => 'my-command'], $commandLoader->getArgument(1));
$this->assertEquals([['my-command' => new ServiceClosureArgument(new TypedReference('my-command', MyCommand::class))]], $commandLocator->getArguments());
$this->assertSame(['console.command.symfony_component_console_tests_dependencyinjection_mycommand' => 'my-command'], $container->getParameter('console.command.ids'));
$this->assertSame(['my-command' => true], $container->getParameter('console.lazy_command.ids'));
$this->assertSame([], $container->getParameter('console.command.ids'));
$this->assertSame([['setName', ['my:command']], ['setAliases', [['my:alias']]]], $command->getMethodCalls());
}
@@ -98,8 +96,7 @@ class AddConsoleCommandPassTest extends TestCase
$this->assertSame(ContainerCommandLoader::class, $commandLoader->getClass());
$this->assertSame(['default' => 'with-default-name'], $commandLoader->getArgument(1));
$this->assertEquals([['with-default-name' => new ServiceClosureArgument(new TypedReference('with-default-name', NamedCommand::class))]], $commandLocator->getArguments());
$this->assertSame(['console.command.symfony_component_console_tests_dependencyinjection_namedcommand' => 'with-default-name'], $container->getParameter('console.command.ids'));
$this->assertSame(['with-default-name' => true], $container->getParameter('console.lazy_command.ids'));
$this->assertSame([], $container->getParameter('console.command.ids'));
$container = new ContainerBuilder();
$container
@@ -121,10 +118,12 @@ class AddConsoleCommandPassTest extends TestCase
];
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The service "my-command" tagged "console.command" must not be abstract.
*/
public function testProcessThrowAnExceptionIfTheServiceIsAbstract()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The service "my-command" tagged "console.command" must not be abstract.');
$container = new ContainerBuilder();
$container->setResourceTracking(false);
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
@@ -137,10 +136,12 @@ class AddConsoleCommandPassTest extends TestCase
$container->compile();
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The service "my-command" tagged "console.command" must be a subclass of "Symfony\Component\Console\Command\Command".
*/
public function testProcessThrowAnExceptionIfTheServiceIsNotASubclassOfCommand()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The service "my-command" tagged "console.command" must be a subclass of "Symfony\Component\Console\Command\Command".');
$container = new ContainerBuilder();
$container->setResourceTracking(false);
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
@@ -168,10 +169,9 @@ class AddConsoleCommandPassTest extends TestCase
(new AddConsoleCommandPass())->process($container);
$alias1 = 'console.command.symfony_component_console_tests_dependencyinjection_mycommand';
$alias2 = $alias1.'_my-command2';
$this->assertTrue($container->hasAlias($alias1));
$this->assertTrue($container->hasAlias($alias2));
$aliasPrefix = 'console.command.public_alias.';
$this->assertTrue($container->hasAlias($aliasPrefix.'my-command1'));
$this->assertTrue($container->hasAlias($aliasPrefix.'my-command2'));
}
public function testProcessOnChildDefinitionWithClass()
@@ -223,10 +223,12 @@ class AddConsoleCommandPassTest extends TestCase
$this->assertInstanceOf($className, $command);
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage The definition for "my-child-command" has no class.
*/
public function testProcessOnChildDefinitionWithoutClass()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('The definition for "my-child-command" has no class.');
$container = new ContainerBuilder();
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
@@ -102,6 +102,6 @@ abstract class AbstractDescriptorTest extends TestCase
{
$output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true);
$this->getDescriptor()->describe($output, $describedObject, $options + ['raw_output' => true]);
$this->assertEquals(trim($expectedDescription), trim(str_replace(\PHP_EOL, "\n", $output->fetch())));
$this->assertEquals(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $output->fetch())));
}
}
@@ -1,53 +0,0 @@
<?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\Descriptor;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
final class ApplicationDescriptionTest extends TestCase
{
/**
* @dataProvider getNamespacesProvider
*/
public function testGetNamespaces(array $expected, array $names)
{
$application = new TestApplication();
foreach ($names as $name) {
$application->add(new Command($name));
}
$this->assertSame($expected, array_keys((new ApplicationDescription($application))->getNamespaces()));
}
public function getNamespacesProvider()
{
return [
[['_global'], ['foobar']],
[['a', 'b'], ['b:foo', 'a:foo', 'b:bar']],
[['_global', 'b', 'z', 22, 33], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']],
];
}
}
final class TestApplication extends Application
{
/**
* {@inheritdoc}
*/
protected function getDefaultCommands()
{
return [];
}
}
@@ -30,6 +30,6 @@ class JsonDescriptorTest extends AbstractDescriptorTest
{
$output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true);
$this->getDescriptor()->describe($output, $describedObject, $options + ['raw_output' => true]);
$this->assertEquals(json_decode(trim($expectedDescription), true), json_decode(trim(str_replace(\PHP_EOL, "\n", $output->fetch())), true));
$this->assertEquals(json_decode(trim($expectedDescription), true), json_decode(trim(str_replace(PHP_EOL, "\n", $output->fetch())), true));
}
}
@@ -32,7 +32,7 @@ class ObjectsProvider
'input_argument_3' => new InputArgument('argument_name', InputArgument::OPTIONAL, 'argument description', 'default_value'),
'input_argument_4' => new InputArgument('argument_name', InputArgument::REQUIRED, "multiline\nargument description"),
'input_argument_with_style' => new InputArgument('argument_name', InputArgument::OPTIONAL, 'argument description', '<comment>style</>'),
'input_argument_with_default_inf_value' => new InputArgument('argument_name', InputArgument::OPTIONAL, 'argument description', \INF),
'input_argument_with_default_inf_value' => new InputArgument('argument_name', InputArgument::OPTIONAL, 'argument description', INF),
];
}
@@ -47,7 +47,7 @@ class ObjectsProvider
'input_option_6' => new InputOption('option_name', ['o', 'O'], InputOption::VALUE_REQUIRED, 'option with multiple shortcuts'),
'input_option_with_style' => new InputOption('option_name', 'o', InputOption::VALUE_REQUIRED, 'option description', '<comment>style</>'),
'input_option_with_style_array' => new InputOption('option_name', 'o', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'option description', ['<comment>Hello</comment>', '<info>world</info>']),
'input_option_with_default_inf_value' => new InputOption('option_name', 'o', InputOption::VALUE_OPTIONAL, 'option description', \INF),
'input_option_with_default_inf_value' => new InputOption('option_name', 'o', InputOption::VALUE_OPTIONAL, 'option description', INF),
];
}
@@ -1,21 +0,0 @@
<?php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class FooHiddenCommand extends Command
{
protected function configure()
{
$this
->setName('foo:hidden')
->setAliases(['afoohidden'])
->setHidden(true)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
}
}
@@ -1,3 +1,6 @@
Description:
Lists commands
Usage:
list [options] [--] [<namespace>]
@@ -1,3 +1,6 @@
Description:
Lists commands
Usage:
list [options] [--] [<namespace>]
+3
View File
@@ -1,3 +1,6 @@
<comment>Description:</comment>
command 1 description
<comment>Usage:</comment>
descriptor:command1
alias1
+3
View File
@@ -1,3 +1,6 @@
<comment>Description:</comment>
command 2 description
<comment>Usage:</comment>
descriptor:command2 [options] [--] \<argument_name>
descriptor:command2 -o|--option_name \<argument_name>
@@ -1,3 +1,6 @@
<comment>Description:</comment>
command åèä description
<comment>Usage:</comment>
descriptor:åèä [options] [--] \<argument_åèä>
descriptor:åèä -o|--option_name \<argument_name>
@@ -59,9 +59,11 @@ class OutputFormatterStyleStackTest extends TestCase
$this->assertEquals($s1, $stack->pop());
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInvalidPop()
{
$this->expectException('InvalidArgumentException');
$stack = new OutputFormatterStyleStack();
$stack->push(new OutputFormatterStyle('white', 'black'));
$stack->pop(new OutputFormatterStyle('yellow', 'blue'));
@@ -41,7 +41,7 @@ class OutputFormatterStyleTest extends TestCase
$style->setForeground('default');
$this->assertEquals("\033[39mfoo\033[39m", $style->apply('foo'));
$this->expectException('InvalidArgumentException');
$this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('InvalidArgumentException');
$style->setForeground('undefined-color');
}
@@ -58,7 +58,7 @@ class OutputFormatterStyleTest extends TestCase
$style->setBackground('default');
$this->assertEquals("\033[49mfoo\033[49m", $style->apply('foo'));
$this->expectException('InvalidArgumentException');
$this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('InvalidArgumentException');
$style->setBackground('undefined-color');
}
@@ -86,7 +86,7 @@ class OutputFormatterStyleTest extends TestCase
$this->fail('->setOption() throws an \InvalidArgumentException when the option does not exist in the available options');
} catch (\Exception $e) {
$this->assertInstanceOf('\InvalidArgumentException', $e, '->setOption() throws an \InvalidArgumentException when the option does not exist in the available options');
$this->assertStringContainsString('Invalid option specified: "foo"', $e->getMessage(), '->setOption() throws an \InvalidArgumentException when the option does not exist in the available options');
$this->assertContains('Invalid option specified: "foo"', $e->getMessage(), '->setOption() throws an \InvalidArgumentException when the option does not exist in the available options');
}
try {
@@ -94,7 +94,7 @@ class OutputFormatterStyleTest extends TestCase
$this->fail('->unsetOption() throws an \InvalidArgumentException when the option does not exist in the available options');
} catch (\Exception $e) {
$this->assertInstanceOf('\InvalidArgumentException', $e, '->unsetOption() throws an \InvalidArgumentException when the option does not exist in the available options');
$this->assertStringContainsString('Invalid option specified: "foo"', $e->getMessage(), '->unsetOption() throws an \InvalidArgumentException when the option does not exist in the available options');
$this->assertContains('Invalid option specified: "foo"', $e->getMessage(), '->unsetOption() throws an \InvalidArgumentException when the option does not exist in the available options');
}
}
}
@@ -196,17 +196,6 @@ class OutputFormatterTest extends TestCase
];
}
/**
* @group legacy
* @dataProvider provideInlineStyleTagsWithUnknownOptions
* @expectedDeprecation Unknown style options are deprecated since Symfony 3.2 and will be removed in 4.0. Exception "Invalid option specified: "%s". Expected one of (bold, underscore, blink, reverse, conceal).".
*/
public function testInlineStyleOptionsUnknownAreDeprecated($tag, $option)
{
$formatter = new OutputFormatter(true);
$formatter->format($tag);
}
public function provideInlineStyleTagsWithUnknownOptions()
{
return [
@@ -333,6 +322,28 @@ more text
EOF
));
}
public function testFormatAndWrap()
{
$formatter = new OutputFormatter(true);
$this->assertSame("fo\no\e[37;41mb\e[39;49m\n\e[37;41mar\e[39;49m\nba\nz", $formatter->formatAndWrap('foo<error>bar</error> baz', 2));
$this->assertSame("pr\ne \e[37;41m\e[39;49m\n\e[37;41mfo\e[39;49m\n\e[37;41mo \e[39;49m\n\e[37;41mba\e[39;49m\n\e[37;41mr \e[39;49m\n\e[37;41mba\e[39;49m\n\e[37;41mz\e[39;49m \npo\nst", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 2));
$this->assertSame("pre\e[37;41m\e[39;49m\n\e[37;41mfoo\e[39;49m\n\e[37;41mbar\e[39;49m\n\e[37;41mbaz\e[39;49m\npos\nt", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 3));
$this->assertSame("pre \e[37;41m\e[39;49m\n\e[37;41mfoo \e[39;49m\n\e[37;41mbar \e[39;49m\n\e[37;41mbaz\e[39;49m \npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 4));
$this->assertSame("pre \e[37;41mf\e[39;49m\n\e[37;41moo ba\e[39;49m\n\e[37;41mr baz\e[39;49m\npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 5));
$this->assertSame("Lore\nm \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m \ndolo\nr \e[32msi\e[39m\n\e[32mt\e[39m am\net", $formatter->formatAndWrap('Lorem <error>ipsum</error> dolor <info>sit</info> amet', 4));
$this->assertSame("Lorem \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m dolo\nr \e[32msit\e[39m am\net", $formatter->formatAndWrap('Lorem <error>ipsum</error> dolor <info>sit</info> amet', 8));
$this->assertSame("Lorem \e[37;41mipsum\e[39;49m dolor \e[32m\e[39m\n\e[32msit\e[39m, \e[37;41mamet\e[39;49m et \e[32mlauda\e[39m\n\e[32mntium\e[39m architecto", $formatter->formatAndWrap('Lorem <error>ipsum</error> dolor <info>sit</info>, <error>amet</error> et <info>laudantium</info> architecto', 18));
$formatter = new OutputFormatter();
$this->assertSame("fo\nob\nar\nba\nz", $formatter->formatAndWrap('foo<error>bar</error> baz', 2));
$this->assertSame("pr\ne \nfo\no \nba\nr \nba\nz \npo\nst", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 2));
$this->assertSame("pre\nfoo\nbar\nbaz\npos\nt", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 3));
$this->assertSame("pre \nfoo \nbar \nbaz \npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 4));
$this->assertSame("pre f\noo ba\nr baz\npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 5));
}
}
class TableCell
+1 -1
View File
@@ -68,7 +68,7 @@ class HelperSetTest extends TestCase
} catch (\Exception $e) {
$this->assertInstanceOf('\InvalidArgumentException', $e, '->get() throws InvalidArgumentException when helper not found');
$this->assertInstanceOf('Symfony\Component\Console\Exception\ExceptionInterface', $e, '->get() throws domain specific exception when helper not found');
$this->assertStringContainsString('The helper "foo" is not defined.', $e->getMessage(), '->get() throws InvalidArgumentException when helper not found');
$this->assertContains('The helper "foo" is not defined.', $e->getMessage(), '->get() throws InvalidArgumentException when helper not found');
}
}
+20 -5
View File
@@ -25,6 +25,10 @@ class ProcessHelperTest extends TestCase
*/
public function testVariousProcessRuns($expected, $cmd, $verbosity, $error)
{
if (\is_string($cmd)) {
$cmd = method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($cmd) : new Process($cmd);
}
$helper = new ProcessHelper();
$helper->setHelperSet(new HelperSet([new DebugFormatterHelper()]));
$output = $this->getOutputStream($verbosity);
@@ -41,7 +45,7 @@ class ProcessHelperTest extends TestCase
$executed = false;
$callback = function () use (&$executed) { $executed = true; };
$helper->run($output, 'php -r "echo 42;"', null, $callback);
$helper->run($output, ['php', '-r', 'echo 42;'], null, $callback);
$this->assertTrue($executed);
}
@@ -81,12 +85,21 @@ EOT;
OUT out message
RES 252 Command did not run successfully
EOT;
$PHP = '\\' === \DIRECTORY_SEPARATOR ? '"!PHP!"' : '"$PHP"';
$successOutputPhp = <<<EOT
RUN php -r $PHP
OUT 42
RES Command ran successfully
EOT;
$errorMessage = 'An error occurred';
$args = new Process(['php', '-r', 'echo 42;']);
$args = $args->getCommandLine();
$successOutputProcessDebug = str_replace("'php' '-r' 'echo 42;'", $args, $successOutputProcessDebug);
$fromShellCommandline = method_exists(Process::class, 'fromShellCommandline') ? [Process::class, 'fromShellCommandline'] : function ($cmd) { return new Process($cmd); };
return [
['', 'php -r "echo 42;"', StreamOutput::VERBOSITY_VERBOSE, null],
@@ -96,11 +109,13 @@ EOT;
['', 'php -r "syntax error"', StreamOutput::VERBOSITY_VERBOSE, null],
[$syntaxErrorOutputVerbose, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, null],
[$syntaxErrorOutputDebug, 'php -r "fwrite(STDERR, \'error message\');usleep(500000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, null],
[$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERBOSE, $errorMessage],
[$syntaxErrorOutputVerbose.$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, $errorMessage],
[$syntaxErrorOutputDebug.$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(500000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, $errorMessage],
[$errorMessage.PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERBOSE, $errorMessage],
[$syntaxErrorOutputVerbose.$errorMessage.PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, $errorMessage],
[$syntaxErrorOutputDebug.$errorMessage.PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(500000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, $errorMessage],
[$successOutputProcessDebug, ['php', '-r', 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null],
[$successOutputDebug, new Process('php -r "echo 42;"'), StreamOutput::VERBOSITY_DEBUG, null],
[$successOutputDebug, $fromShellCommandline('php -r "echo 42;"'), StreamOutput::VERBOSITY_DEBUG, null],
[$successOutputProcessDebug, [new Process(['php', '-r', 'echo 42;'])], StreamOutput::VERBOSITY_DEBUG, null],
[$successOutputPhp, [$fromShellCommandline('php -r '.$PHP), 'PHP' => 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null],
];
}
+120 -13
View File
@@ -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\Helper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\StreamOutput;
/**
@@ -323,6 +325,88 @@ class ProgressBarTest extends TestCase
);
}
public function testOverwriteWithSectionOutput()
{
$sections = [];
$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 = [];
$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 = [];
$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());
@@ -477,16 +561,16 @@ class ProgressBarTest extends TestCase
rewind($output->getStream());
$this->assertEquals(
' 0/200 [>---------------------------] 0%'.\PHP_EOL.
' 20/200 [==>-------------------------] 10%'.\PHP_EOL.
' 40/200 [=====>----------------------] 20%'.\PHP_EOL.
' 60/200 [========>-------------------] 30%'.\PHP_EOL.
' 80/200 [===========>----------------] 40%'.\PHP_EOL.
' 100/200 [==============>-------------] 50%'.\PHP_EOL.
' 120/200 [================>-----------] 60%'.\PHP_EOL.
' 140/200 [===================>--------] 70%'.\PHP_EOL.
' 160/200 [======================>-----] 80%'.\PHP_EOL.
' 180/200 [=========================>--] 90%'.\PHP_EOL.
' 0/200 [>---------------------------] 0%'.PHP_EOL.
' 20/200 [==>-------------------------] 10%'.PHP_EOL.
' 40/200 [=====>----------------------] 20%'.PHP_EOL.
' 60/200 [========>-------------------] 30%'.PHP_EOL.
' 80/200 [===========>----------------] 40%'.PHP_EOL.
' 100/200 [==============>-------------] 50%'.PHP_EOL.
' 120/200 [================>-----------] 60%'.PHP_EOL.
' 140/200 [===================>--------] 70%'.PHP_EOL.
' 160/200 [======================>-----] 80%'.PHP_EOL.
' 180/200 [=========================>--] 90%'.PHP_EOL.
' 200/200 [============================] 100%',
stream_get_contents($output->getStream())
);
@@ -503,8 +587,8 @@ class ProgressBarTest extends TestCase
rewind($output->getStream());
$this->assertEquals(
' 0/50 [>---------------------------] 0%'.\PHP_EOL.
' 25/50 [==============>-------------] 50%'.\PHP_EOL.
' 0/50 [>---------------------------] 0%'.PHP_EOL.
' 25/50 [==============>-------------] 50%'.PHP_EOL.
' 50/50 [============================] 100%',
stream_get_contents($output->getStream())
);
@@ -518,7 +602,7 @@ class ProgressBarTest extends TestCase
rewind($output->getStream());
$this->assertEquals(
' 0 [>---------------------------]'.\PHP_EOL.
' 0 [>---------------------------]'.PHP_EOL.
' 1 [->--------------------------]',
stream_get_contents($output->getStream())
);
@@ -605,6 +689,29 @@ class ProgressBarTest extends TestCase
);
}
public function testSettingMaxStepsDuringProgressing()
{
$output = $this->getOutputStream();
$bar = new ProgressBar($output);
$bar->start();
$bar->setProgress(2);
$bar->setMaxSteps(10);
$bar->setProgress(5);
$bar->setMaxSteps(100);
$bar->setProgress(10);
$bar->finish();
rewind($output->getStream());
$this->assertEquals(
rtrim(' 0 [>---------------------------]').
rtrim($this->generateOutput(' 2 [-->-------------------------]')).
rtrim($this->generateOutput(' 5/10 [==============>-------------] 50%')).
rtrim($this->generateOutput(' 10/100 [==>-------------------------] 10%')).
rtrim($this->generateOutput(' 100/100 [============================] 100%')),
stream_get_contents($output->getStream())
);
}
public function testWithSmallScreen()
{
$output = $this->getOutputStream();
+22 -14
View File
@@ -46,11 +46,11 @@ class ProgressIndicatorTest extends TestCase
$this->generateOutput(' \\ Advancing...').
$this->generateOutput(' | Advancing...').
$this->generateOutput(' | Done...').
\PHP_EOL.
PHP_EOL.
$this->generateOutput(' - Starting Again...').
$this->generateOutput(' \\ Starting Again...').
$this->generateOutput(' \\ Done Again...').
\PHP_EOL,
PHP_EOL,
stream_get_contents($output->getStream())
);
}
@@ -70,9 +70,9 @@ class ProgressIndicatorTest extends TestCase
rewind($output->getStream());
$this->assertEquals(
' Starting...'.\PHP_EOL.
' Midway...'.\PHP_EOL.
' Done...'.\PHP_EOL.\PHP_EOL,
' Starting...'.PHP_EOL.
' Midway...'.PHP_EOL.
' Done...'.PHP_EOL.PHP_EOL,
stream_get_contents($output->getStream())
);
}
@@ -100,34 +100,42 @@ class ProgressIndicatorTest extends TestCase
);
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Must have at least 2 indicator value characters.
*/
public function testCannotSetInvalidIndicatorCharacters()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('Must have at least 2 indicator value characters.');
new ProgressIndicator($this->getOutputStream(), null, 100, ['1']);
$bar = new ProgressIndicator($this->getOutputStream(), null, 100, ['1']);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Progress indicator already started.
*/
public function testCannotStartAlreadyStartedIndicator()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Progress indicator already started.');
$bar = new ProgressIndicator($this->getOutputStream());
$bar->start('Starting...');
$bar->start('Starting Again.');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Progress indicator has not yet been started.
*/
public function testCannotAdvanceUnstartedIndicator()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Progress indicator has not yet been started.');
$bar = new ProgressIndicator($this->getOutputStream());
$bar->advance();
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Progress indicator has not yet been started.
*/
public function testCannotFinishUnstartedIndicator()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Progress indicator has not yet been started.');
$bar = new ProgressIndicator($this->getOutputStream());
$bar->finish('Finished');
}
+37 -406
View File
@@ -11,7 +11,6 @@
namespace Symfony\Component\Console\Tests\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\HelperSet;
@@ -20,7 +19,6 @@ use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;
/**
* @group tty
@@ -55,7 +53,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
rewind($output->getStream());
$stream = stream_get_contents($output->getStream());
$this->assertStringContainsString('Input "Fabien" is not a superhero!', $stream);
$this->assertContains('Input "Fabien" is not a superhero!', $stream);
try {
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '1');
@@ -169,27 +167,26 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
public function testAskWithAutocomplete()
{
if (!Terminal::hasSttyAvailable()) {
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
// Acm<NEWLINE>
// Ac<BACKSPACE><BACKSPACE>s<TAB>Test<NEWLINE>
// <NEWLINE>
// <UP ARROW><UP ARROW><UP ARROW><NEWLINE>
// <UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><TAB>Test<NEWLINE>
// <UP ARROW><UP ARROW><NEWLINE>
// <UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><TAB>Test<NEWLINE>
// <DOWN ARROW><NEWLINE>
// S<BACKSPACE><BACKSPACE><DOWN ARROW><DOWN ARROW><NEWLINE>
// F00<BACKSPACE><BACKSPACE>oo<TAB><NEWLINE>
// F⭐<TAB><BACKSPACE><BACKSPACE>⭐<TAB><NEWLINE>
$inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\nF⭐\t\177\177\t\n");
$inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\n");
$dialog = new QuestionHelper();
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$question = new Question('Please select a bundle', 'FrameworkBundle');
$question->setAutocompleterValues(['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle', 'F⭐Y']);
$question->setAutocompleterValues(['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle']);
$this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals('AsseticBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
@@ -199,12 +196,11 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals('AsseticBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
$this->assertEquals('F⭐Y', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
public function testAskWithAutocompleteWithNonSequentialKeys()
{
if (!Terminal::hasSttyAvailable()) {
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
@@ -223,7 +219,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
public function testAskWithAutocompleteWithExactMatch()
{
if (!Terminal::hasSttyAvailable()) {
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
@@ -259,7 +255,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
*/
public function testAskWithAutocompleteWithMultiByteCharacter($character)
{
if (!Terminal::hasSttyAvailable()) {
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
@@ -283,7 +279,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
public function testAutocompleteWithTrailingBackslash()
{
if (!Terminal::hasSttyAvailable()) {
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
@@ -522,10 +518,12 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$this->assertSame($expectedValue, $answer);
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The provided answer is ambiguous. Value should be one of env_2 or env_3.
*/
public function testAmbiguousChoiceFromChoicelist()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of "env_2" or "env_3".');
$possibleChoices = [
'env_1' => 'My first environment',
'env_2' => 'My environment',
@@ -590,382 +588,35 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
}
/**
* @group legacy
* @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testLegacyAskChoice()
{
$questionHelper = new QuestionHelper();
$helperSet = new HelperSet([new FormatterHelper()]);
$questionHelper->setHelperSet($helperSet);
$heroes = ['Superman', 'Batman', 'Spiderman'];
$questionHelper->setInputStream($this->getInputStream("\n1\n 1 \nFabien\n1\nFabien\n1\n0,2\n 0 , 2 \n\n\n"));
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '2');
$question->setMaxAttempts(1);
// first answer is an empty answer, we're supposed to receive the default value
$this->assertEquals('Spiderman', $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes);
$question->setMaxAttempts(1);
$this->assertEquals('Batman', $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('Batman', $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes);
$question->setErrorMessage('Input "%s" is not a superhero!');
$question->setMaxAttempts(2);
$this->assertEquals('Batman', $questionHelper->ask($this->createInputInterfaceMock(), $output = $this->createOutputInterface(), $question));
rewind($output->getStream());
$stream = stream_get_contents($output->getStream());
$this->assertStringContainsString('Input "Fabien" is not a superhero!', $stream);
try {
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '1');
$question->setMaxAttempts(1);
$questionHelper->ask($this->createInputInterfaceMock(), $output = $this->createOutputInterface(), $question);
$this->fail();
} catch (\InvalidArgumentException $e) {
$this->assertEquals('Value "Fabien" is invalid', $e->getMessage());
}
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes, null);
$question->setMaxAttempts(1);
$question->setMultiselect(true);
$this->assertEquals(['Batman'], $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals(['Superman', 'Spiderman'], $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals(['Superman', 'Spiderman'], $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '0,1');
$question->setMaxAttempts(1);
$question->setMultiselect(true);
$this->assertEquals(['Superman', 'Batman'], $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$question = new ChoiceQuestion('What is your favorite superhero?', $heroes, ' 0 , 1 ');
$question->setMaxAttempts(1);
$question->setMultiselect(true);
$this->assertEquals(['Superman', 'Batman'], $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
}
/**
* @group legacy
*/
public function testLegacyAsk()
{
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream("\n8AM\n"));
$question = new Question('What time is it?', '2PM');
$this->assertEquals('2PM', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$question = new Question('What time is it?', '2PM');
$this->assertEquals('8AM', $dialog->ask($this->createInputInterfaceMock(), $output = $this->createOutputInterface(), $question));
rewind($output->getStream());
$this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
}
/**
* @group legacy
*/
public function testLegacyAskWithAutocomplete()
{
if (!Terminal::hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
// Acm<NEWLINE>
// Ac<BACKSPACE><BACKSPACE>s<TAB>Test<NEWLINE>
// <NEWLINE>
// <UP ARROW><UP ARROW><UP ARROW><NEWLINE>
// <UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><TAB>Test<NEWLINE>
// <DOWN ARROW><NEWLINE>
// S<BACKSPACE><BACKSPACE><DOWN ARROW><DOWN ARROW><NEWLINE>
// F00<BACKSPACE><BACKSPACE>oo<TAB><NEWLINE>
// F⭐<TAB><BACKSPACE><BACKSPACE>⭐<TAB><NEWLINE>
$inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\nF⭐\t\t\n");
$dialog = new QuestionHelper();
$dialog->setInputStream($inputStream);
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$question = new Question('Please select a bundle', 'FrameworkBundle');
$question->setAutocompleterValues(['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle', 'F⭐Y']);
$this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('AsseticBundleTest', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('FrameworkBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('SecurityBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('FooBundleTest', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('AsseticBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('FooBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('F⭐Y', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
/**
* @group legacy
*/
public function testLegacyAskWithAutocompleteWithNonSequentialKeys()
{
if (!Terminal::hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
// <UP ARROW><UP ARROW><NEWLINE><DOWN ARROW><DOWN ARROW><NEWLINE>
$inputStream = $this->getInputStream("\033[A\033[A\n\033[B\033[B\n");
$dialog = new QuestionHelper();
$dialog->setInputStream($inputStream);
$dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
$question = new ChoiceQuestion('Please select a bundle', [1 => 'AcmeDemoBundle', 4 => 'AsseticBundle']);
$question->setMaxAttempts(1);
$this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('AsseticBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
}
/**
* @group legacy
*/
public function testLegacyAskHiddenResponse()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('This test is not supported on Windows');
}
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream("8AM\n"));
$question = new Question('What time is it?');
$question->setHidden(true);
$this->assertEquals('8AM', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
}
/**
* @group legacy
* @dataProvider getAskConfirmationData
*/
public function testLegacyAskConfirmation($question, $expected, $default = true)
{
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream($question."\n"));
$question = new ConfirmationQuestion('Do you like French fries?', $default);
$this->assertEquals($expected, $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel'));
}
/**
* @group legacy
*/
public function testLegacyAskConfirmationWithCustomTrueAnswer()
{
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream("j\ny\n"));
$question = new ConfirmationQuestion('Do you like French fries?', false, '/^(j|y)/i');
$this->assertTrue($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$question = new ConfirmationQuestion('Do you like French fries?', false, '/^(j|y)/i');
$this->assertTrue($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
}
/**
* @group legacy
*/
public function testLegacyAskAndValidate()
{
$dialog = new QuestionHelper();
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$error = 'This is not a color!';
$validator = function ($color) use ($error) {
if (!\in_array($color, ['white', 'black'])) {
throw new \InvalidArgumentException($error);
}
return $color;
};
$question = new Question('What color was the white horse of Henry IV?', 'white');
$question->setValidator($validator);
$question->setMaxAttempts(2);
$dialog->setInputStream($this->getInputStream("\nblack\n"));
$this->assertEquals('white', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$this->assertEquals('black', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question));
$dialog->setInputStream($this->getInputStream("green\nyellow\norange\n"));
try {
$dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question);
$this->fail();
} catch (\InvalidArgumentException $e) {
$this->assertEquals($error, $e->getMessage());
}
}
/**
* @group legacy
* @dataProvider simpleAnswerProvider
*/
public function testLegacySelectChoiceFromSimpleChoices($providedAnswer, $expectedValue)
{
$possibleChoices = [
'My environment 1',
'My environment 2',
'My environment 3',
];
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream($providedAnswer."\n"));
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
$question->setMaxAttempts(1);
$answer = $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question);
$this->assertSame($expectedValue, $answer);
}
/**
* @group legacy
* @dataProvider mixedKeysChoiceListAnswerProvider
*/
public function testLegacyChoiceFromChoicelistWithMixedKeys($providedAnswer, $expectedValue)
{
$possibleChoices = [
'0' => 'No environment',
'1' => 'My environment 1',
'env_2' => 'My environment 2',
3 => 'My environment 3',
];
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream($providedAnswer."\n"));
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
$question->setMaxAttempts(1);
$answer = $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question);
$this->assertSame($expectedValue, $answer);
}
/**
* @group legacy
* @dataProvider answerProvider
*/
public function testLegacySelectChoiceFromChoiceList($providedAnswer, $expectedValue)
{
$possibleChoices = [
'env_1' => 'My environment 1',
'env_2' => 'My environment',
'env_3' => 'My environment',
];
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream($providedAnswer."\n"));
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
$question->setMaxAttempts(1);
$answer = $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question);
$this->assertSame($expectedValue, $answer);
}
/**
* @group legacy
*/
public function testLegacyAmbiguousChoiceFromChoicelist()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of "env_2" or "env_3".');
$possibleChoices = [
'env_1' => 'My first environment',
'env_2' => 'My environment',
'env_3' => 'My environment',
];
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream("My environment\n"));
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
$question->setMaxAttempts(1);
$dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question);
}
/**
* @requires function mb_strwidth
* @group legacy
*/
public function testLegacyChoiceOutputFormattingQuestionForUtf8Keys()
{
$question = 'Lorem ipsum?';
$possibleChoices = [
'foo' => 'foo',
'żółw' => 'bar',
'łabądź' => 'baz',
];
$outputShown = [
$question,
' [<info>foo </info>] foo',
' [<info>żółw </info>] bar',
' [<info>łabądź</info>] baz',
];
$output = $this->getMockBuilder('\Symfony\Component\Console\Output\OutputInterface')->getMock();
$output->method('getFormatter')->willReturn(new OutputFormatter());
$dialog = new QuestionHelper();
$dialog->setInputStream($this->getInputStream("\n"));
$helperSet = new HelperSet([new FormatterHelper()]);
$dialog->setHelperSet($helperSet);
$output->expects($this->once())->method('writeln')->with($this->equalTo($outputShown));
$question = new ChoiceQuestion($question, $possibleChoices, 'foo');
$dialog->ask($this->createInputInterfaceMock(), $output, $question);
}
public function testAskThrowsExceptionOnMissingInput()
{
$this->expectException('Symfony\Component\Console\Exception\RuntimeException');
$this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper();
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
}
/**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testAskThrowsExceptionOnMissingInputForChoiceQuestion()
{
$this->expectException('Symfony\Component\Console\Exception\RuntimeException');
$this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper();
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b']));
}
/**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testAskThrowsExceptionOnMissingInputWithValidator()
{
$this->expectException('Symfony\Component\Console\Exception\RuntimeException');
$this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper();
$question = new Question('What\'s your name?');
$question->setValidator(function ($value) {
$question->setValidator(function () {
if (!$value) {
throw new \Exception('A value is required.');
}
@@ -974,16 +625,18 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), $question);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Choice question must have at least 1 choice available.
*/
public function testEmptyChoices()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Choice question must have at least 1 choice available.');
new ChoiceQuestion('Question', [], 'irrelevant');
}
public function testTraversableAutocomplete()
{
if (!Terminal::hasSttyAvailable()) {
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
@@ -1014,35 +667,6 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
public function testDisableStty()
{
if (!Terminal::hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('invalid');
QuestionHelper::disableStty();
$dialog = new QuestionHelper();
$dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
$question = new ChoiceQuestion('Please select a bundle', [1 => 'AcmeDemoBundle', 4 => 'AsseticBundle']);
$question->setMaxAttempts(1);
// <UP ARROW><UP ARROW><NEWLINE><DOWN ARROW><DOWN ARROW><NEWLINE>
// Gives `AcmeDemoBundle` with stty
$inputStream = $this->getInputStream("\033[A\033[A\n\033[B\033[B\n");
try {
$dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question);
} finally {
$reflection = new \ReflectionProperty(QuestionHelper::class, 'stty');
$reflection->setAccessible(true);
$reflection->setValue(null, true);
}
}
public function testTraversableMultiselectAutocomplete()
{
// <NEWLINE>
@@ -1097,6 +721,13 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
return $mock;
}
private function hasSttyAvailable()
{
exec('stty 2>&1', $output, $exitcode);
return 0 === $exitcode;
}
}
class AutocompleteValues implements \IteratorAggregate
@@ -122,57 +122,16 @@ class SymfonyQuestionHelperTest extends AbstractQuestionHelperTest
$this->assertOutputContains('Question with a trailing \\', $output);
}
/**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testAskThrowsExceptionOnMissingInput()
{
$this->expectException('Symfony\Component\Console\Exception\RuntimeException');
$this->expectExceptionMessage('Aborted.');
$dialog = new SymfonyQuestionHelper();
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
}
public function testChoiceQuestionPadding()
{
$choiceQuestion = new ChoiceQuestion('qqq', [
'foo' => 'foo',
'żółw' => 'bar',
'łabądź' => 'baz',
]);
(new SymfonyQuestionHelper())->ask(
$this->createStreamableInputInterfaceMock($this->getInputStream("foo\n")),
$output = $this->createOutputInterface(),
$choiceQuestion
);
$this->assertOutputContains(<<<EOT
qqq:
[foo ] foo
[żółw ] bar
[łabądź] baz
>
EOT
, $output, true);
}
public function testChoiceQuestionCustomPrompt()
{
$choiceQuestion = new ChoiceQuestion('qqq', ['foo']);
$choiceQuestion->setPrompt(' >ccc> ');
(new SymfonyQuestionHelper())->ask(
$this->createStreamableInputInterfaceMock($this->getInputStream("foo\n")),
$output = $this->createOutputInterface(),
$choiceQuestion
);
$this->assertOutputContains(<<<EOT
qqq:
[0] foo
>ccc>
EOT
, $output, true);
}
protected function getInputStream($input)
{
$stream = fopen('php://memory', 'r+', false);
@@ -200,15 +159,10 @@ EOT
return $mock;
}
private function assertOutputContains($expected, StreamOutput $output, $normalize = false)
private function assertOutputContains($expected, StreamOutput $output)
{
rewind($output->getStream());
$stream = stream_get_contents($output->getStream());
if ($normalize) {
$stream = str_replace(\PHP_EOL, "\n", $stream);
}
$this->assertStringContainsString($expected, $stream);
$this->assertContains($expected, $stream);
}
}
+4 -2
View File
@@ -16,10 +16,12 @@ use Symfony\Component\Console\Helper\TableStyle;
class TableStyleTest extends TestCase
{
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).
*/
public function testSetPadTypeWithInvalidType()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
$style = new TableStyle();
$style->setPadType('TEST');
}
+351 -20
View File
@@ -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\TableCell;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Helper\TableStyle;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\StreamOutput;
class TableTest extends TestCase
@@ -136,6 +138,45 @@ TABLE
80-902734-1-6 And Then There Were None Agatha Christie
=============== ========================== ==================
TABLE
],
[
['ISBN', 'Title', 'Author'],
$books,
'box',
<<<'TABLE'
┌───────────────┬──────────────────────────┬──────────────────┐
│ ISBN │ Title │ Author │
├───────────────┼──────────────────────────┼──────────────────┤
│ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │
│ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │
│ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │
│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │
└───────────────┴──────────────────────────┴──────────────────┘
TABLE
],
[
['ISBN', 'Title', 'Author'],
[
['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'],
['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'],
new TableSeparator(),
['960-425-059-0', 'The Lord of the Rings', 'J. R. R. Tolkien'],
['80-902734-1-6', 'And Then There Were None', 'Agatha Christie'],
],
'box-double',
<<<'TABLE'
╔═══════════════╤══════════════════════════╤══════════════════╗
║ ISBN │ Title │ Author ║
╠═══════════════╪══════════════════════════╪══════════════════╣
║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
╟───────────────┼──────────────────────────┼──────────────────╢
║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
╚═══════════════╧══════════════════════════╧══════════════════╝
TABLE
],
[
@@ -610,9 +651,9 @@ TABLE;
{
$style = new TableStyle();
$style
->setHorizontalBorderChar('.')
->setVerticalBorderChar('.')
->setCrossingChar('.')
->setHorizontalBorderChars('.')
->setVerticalBorderChars('.')
->setDefaultCrossingChar('.')
;
Table::setStyleDefinition('dotfull', $style);
@@ -707,7 +748,7 @@ TABLE;
]);
$style = new TableStyle();
$style->setPadType(\STR_PAD_LEFT);
$style->setPadType(STR_PAD_LEFT);
$table->setColumnStyle(3, $style);
$table->render();
@@ -726,10 +767,12 @@ TABLE;
$this->assertEquals($expected, $this->getOutputContent($output));
}
/**
* @expectedException \Symfony\Component\Console\Exception\InvalidArgumentException
* @expectedExceptionMessage A cell must be a TableCell, a scalar or an object implementing __toString, array given.
*/
public function testThrowsWhenTheCellInAnArray()
{
$this->expectException('Symfony\Component\Console\Exception\InvalidArgumentException');
$this->expectExceptionMessage('A cell must be a TableCell, a scalar or an object implementing "__toString()", "array" given.');
$table = new Table($output = $this->getOutputStream());
$table
->setHeaders(['ISBN', 'Title', 'Author', 'Price'])
@@ -753,7 +796,7 @@ TABLE;
->setColumnWidth(3, 10);
$style = new TableStyle();
$style->setPadType(\STR_PAD_LEFT);
$style->setPadType(STR_PAD_LEFT);
$table->setColumnStyle(3, $style);
$table->render();
@@ -784,7 +827,7 @@ TABLE;
->setColumnWidths([15, 0, -1, 10]);
$style = new TableStyle();
$style->setPadType(\STR_PAD_LEFT);
$style->setPadType(STR_PAD_LEFT);
$table->setColumnStyle(3, $style);
$table->render();
@@ -803,28 +846,264 @@ TABLE;
$this->assertEquals($expected, $this->getOutputContent($output));
}
public function testSectionOutput()
{
$sections = [];
$stream = $this->getOutputStream(true);
$output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
$table = new Table($output);
$table
->setHeaders(['ISBN', 'Title', 'Author', 'Price'])
->setRows([
['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'],
]);
$table->render();
$table->appendRow(['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25']);
$expected =
<<<TABLE
+---------------+---------------+-----------------+-------+
|\033[32m ISBN \033[39m|\033[32m Title \033[39m|\033[32m Author \033[39m|\033[32m Price \033[39m|
+---------------+---------------+-----------------+-------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95 |
+---------------+---------------+-----------------+-------+
\x1b[5A\x1b[0J+---------------+----------------------+-----------------+--------+
|\033[32m ISBN \033[39m|\033[32m Title \033[39m|\033[32m Author \033[39m|\033[32m Price \033[39m|
+---------------+----------------------+-----------------+--------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95 |
| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |
+---------------+----------------------+-----------------+--------+
TABLE;
$this->assertEquals($expected, $this->getOutputContent($output));
}
public function testSectionOutputDoesntClearIfTableIsntRendered()
{
$sections = [];
$stream = $this->getOutputStream(true);
$output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
$table = new Table($output);
$table
->setHeaders(['ISBN', 'Title', 'Author', 'Price'])
->setRows([
['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'],
]);
$table->appendRow(['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25']);
$expected =
<<<TABLE
+---------------+----------------------+-----------------+--------+
|\033[32m ISBN \033[39m|\033[32m Title \033[39m|\033[32m Author \033[39m|\033[32m Price \033[39m|
+---------------+----------------------+-----------------+--------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95 |
| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |
+---------------+----------------------+-----------------+--------+
TABLE;
$this->assertEquals($expected, $this->getOutputContent($output));
}
public function testSectionOutputWithoutDecoration()
{
$sections = [];
$stream = $this->getOutputStream();
$output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
$table = new Table($output);
$table
->setHeaders(['ISBN', 'Title', 'Author', 'Price'])
->setRows([
['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'],
]);
$table->render();
$table->appendRow(['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25']);
$expected =
<<<TABLE
+---------------+---------------+-----------------+-------+
| ISBN | Title | Author | Price |
+---------------+---------------+-----------------+-------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95 |
+---------------+---------------+-----------------+-------+
+---------------+----------------------+-----------------+--------+
| ISBN | Title | Author | Price |
+---------------+----------------------+-----------------+--------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95 |
| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 |
+---------------+----------------------+-----------------+--------+
TABLE;
$this->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(['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.
*/
public function testIsNotDefinedStyleException()
{
$this->expectException('Symfony\Component\Console\Exception\InvalidArgumentException');
$this->expectExceptionMessage('Style "absent" is not defined.');
$table = new Table($this->getOutputStream());
$table->setStyle('absent');
}
/**
* @expectedException \Symfony\Component\Console\Exception\InvalidArgumentException
* @expectedExceptionMessage Style "absent" is not defined.
*/
public function testGetStyleDefinition()
{
$this->expectException('Symfony\Component\Console\Exception\InvalidArgumentException');
$this->expectExceptionMessage('Style "absent" is not defined.');
Table::getStyleDefinition('absent');
}
/**
* @dataProvider renderSetTitle
*/
public function testSetTitle($headerTitle, $footerTitle, $style, $expected)
{
(new Table($output = $this->getOutputStream()))
->setHeaderTitle($headerTitle)
->setFooterTitle($footerTitle)
->setHeaders(['ISBN', 'Title', 'Author'])
->setRows([
['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'],
['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'],
['960-425-059-0', 'The Lord of the Rings', 'J. R. R. Tolkien'],
['80-902734-1-6', 'And Then There Were None', 'Agatha Christie'],
])
->setStyle($style)
->render()
;
$this->assertEquals($expected, $this->getOutputContent($output));
}
public function renderSetTitle()
{
return [
[
'Books',
'Page 1/2',
'default',
<<<'TABLE'
+---------------+----------- Books --------+------------------+
| ISBN | Title | Author |
+---------------+--------------------------+------------------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+---------------+--------- Page 1/2 -------+------------------+
TABLE
],
[
'Books',
'Page 1/2',
'box',
<<<'TABLE'
┌───────────────┬─────────── Books ────────┬──────────────────┐
│ ISBN │ Title │ Author │
├───────────────┼──────────────────────────┼──────────────────┤
│ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │
│ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │
│ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │
│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │
└───────────────┴───────── Page 1/2 ───────┴──────────────────┘
TABLE
],
[
'Boooooooooooooooooooooooooooooooooooooooooooooooooooooooks',
'Page 1/999999999999999999999999999999999999999999999999999',
'default',
<<<'TABLE'
+- Booooooooooooooooooooooooooooooooooooooooooooooooooooo... -+
| ISBN | Title | Author |
+---------------+--------------------------+------------------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+- Page 1/99999999999999999999999999999999999999999999999... -+
TABLE
],
];
}
public function testColumnMaxWidths()
{
$table = new Table($output = $this->getOutputStream());
$table
->setRows([
['Divine Comedy', 'A Tale of Two Cities', 'The Lord of the Rings', 'And Then There Were None'],
])
->setColumnMaxWidth(1, 5)
->setColumnMaxWidth(2, 10)
->setColumnMaxWidth(3, 15);
$table->render();
$expected =
<<<TABLE
+---------------+-------+------------+-----------------+
| Divine Comedy | A Tal | The Lord o | And Then There |
| | e of | f the Ring | Were None |
| | Two C | s | |
| | ities | | |
+---------------+-------+------------+-----------------+
TABLE;
$this->assertEquals($expected, $this->getOutputContent($output));
}
public function testColumnMaxWidthsWithTrailingBackslash()
{
(new Table($output = $this->getOutputStream()))
->setColumnMaxWidth(0, 5)
->setRows([['1234\6']])
->render()
;
$expected =
<<<'TABLE'
+-------+
| 1234\ |
| 6 |
+-------+
TABLE;
$this->assertEquals($expected, $this->getOutputContent($output));
}
public function testBoxedStyleWithColspan()
{
$boxed = new TableStyle();
$boxed
->setHorizontalBorderChar('─')
->setVerticalBorderChar('│')
->setCrossingChar('┼')
->setHorizontalBorderChars('─')
->setVerticalBorderChars('│')
->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
;
$table = new Table($output = $this->getOutputStream());
@@ -841,13 +1120,13 @@ TABLE;
$expected =
<<<TABLE
───────────────────────────────────────────────
───────────────────────────────────────────────
│ ISBN │ Title │ Author │
───────────────┼───────────────┼─────────────────
───────────────┼───────────────┼─────────────────
│ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │
───────────────┼───────────────┼─────────────────
───────────────┼───────────────┼─────────────────
│ This value spans 3 columns. │
───────────────────────────────────────────────
───────────────────────────────────────────────
TABLE;
@@ -863,6 +1142,58 @@ TABLE;
{
rewind($output->getStream());
return str_replace(\PHP_EOL, "\n", stream_get_contents($output->getStream()));
return str_replace(PHP_EOL, "\n", stream_get_contents($output->getStream()));
}
public function testWithColspanAndMaxWith(): void
{
$table = new Table($output = $this->getOutputStream());
$table->setColumnMaxWidth(0, 15);
$table->setColumnMaxWidth(1, 15);
$table->setColumnMaxWidth(2, 15);
$table->setRows([
[new TableCell('Lorem ipsum dolor sit amet, <fg=white;bg=green>consectetur</> adipiscing elit, <fg=white;bg=red>sed</> do <fg=white;bg=red>eiusmod</> tempor', ['colspan' => 3])],
new TableSeparator(),
[new TableCell('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor', ['colspan' => 3])],
new TableSeparator(),
[new TableCell('Lorem ipsum <fg=white;bg=red>dolor</> sit amet, consectetur ', ['colspan' => 2]), 'hello world'],
new TableSeparator(),
['hello <fg=white;bg=green>world</>', new TableCell('Lorem ipsum dolor sit amet, <fg=white;bg=green>consectetur</> adipiscing elit', ['colspan' => 2])],
new TableSeparator(),
['hello ', new TableCell('world', ['colspan' => 1]), 'Lorem ipsum dolor sit amet, consectetur'],
new TableSeparator(),
['Symfony ', new TableCell('Test', ['colspan' => 1]), 'Lorem <fg=white;bg=green>ipsum</> dolor sit amet, consectetur'],
])
;
$table->render();
$expected =
<<<TABLE
+-----------------+-----------------+-----------------+
| Lorem ipsum dolor sit amet, consectetur adipi |
| scing elit, sed do eiusmod tempor |
+-----------------+-----------------+-----------------+
| Lorem ipsum dolor sit amet, consectetur adipi |
| scing elit, sed do eiusmod tempor |
+-----------------+-----------------+-----------------+
| Lorem ipsum dolor sit amet, co | hello world |
| nsectetur | |
+-----------------+-----------------+-----------------+
| hello world | Lorem ipsum dolor sit amet, co |
| | nsectetur adipiscing elit |
+-----------------+-----------------+-----------------+
| hello | world | Lorem ipsum dol |
| | | or sit amet, co |
| | | nsectetur |
+-----------------+-----------------+-----------------+
| Symfony | Test | Lorem ipsum dol |
| | | or sit amet, co |
| | | nsectetur |
+-----------------+-----------------+-----------------+
TABLE;
$this->assertSame($expected, $this->getOutputContent($output));
}
}
+6 -2
View File
@@ -182,8 +182,12 @@ class ArgvInputTest extends TestCase
*/
public function testInvalidInput($argv, $definition, $expectedExceptionMessage)
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage($expectedExceptionMessage);
if (method_exists($this, 'expectException')) {
$this->expectException('RuntimeException');
$this->expectExceptionMessage($expectedExceptionMessage);
} else {
$this->setExpectedException('RuntimeException', $expectedExceptionMessage);
}
$input = new ArgvInput($argv);
$input->bind($definition);
+6 -2
View File
@@ -127,8 +127,12 @@ class ArrayInputTest extends TestCase
*/
public function testParseInvalidInput($parameters, $definition, $expectedExceptionMessage)
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage($expectedExceptionMessage);
if (method_exists($this, 'expectException')) {
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage($expectedExceptionMessage);
} else {
$this->setExpectedException('InvalidArgumentException', $expectedExceptionMessage);
}
new ArrayInput($parameters, $definition);
}
+12 -18
View File
@@ -38,22 +38,12 @@ class InputArgumentTest extends TestCase
}
/**
* @dataProvider provideInvalidModes
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Argument mode "-1" is not valid.
*/
public function testInvalidModes($mode)
public function testInvalidModes()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage(sprintf('Argument mode "%s" is not valid.', $mode));
new InputArgument('foo', $mode);
}
public function provideInvalidModes()
{
return [
['ANOTHER_ONE'],
[-1],
];
new InputArgument('foo', '-1');
}
public function testIsArray()
@@ -91,18 +81,22 @@ class InputArgumentTest extends TestCase
$this->assertEquals([1, 2], $argument->getDefault(), '->setDefault() changes the default value');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot set a default value except for InputArgument::OPTIONAL mode.
*/
public function testSetDefaultWithRequiredArgument()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Cannot set a default value except for InputArgument::OPTIONAL mode.');
$argument = new InputArgument('foo', InputArgument::REQUIRED);
$argument->setDefault('default');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage A default value for an array argument must be an array.
*/
public function testSetDefaultWithArrayArgument()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('A default value for an array argument must be an array.');
$argument = new InputArgument('foo', InputArgument::IS_ARRAY);
$argument->setDefault('default');
}
+39 -21
View File
@@ -20,7 +20,6 @@ class InputDefinitionTest extends TestCase
{
protected static $fixtures;
protected $multi;
protected $foo;
protected $bar;
protected $foo1;
@@ -87,10 +86,12 @@ class InputDefinitionTest extends TestCase
$this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getArguments(), '->addArgument() adds a InputArgument object');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage An argument with name "foo" already exists.
*/
public function testArgumentsMustHaveDifferentNames()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('An argument with name "foo" already exists.');
$this->initializeArguments();
$definition = new InputDefinition();
@@ -98,10 +99,12 @@ class InputDefinitionTest extends TestCase
$definition->addArgument($this->foo1);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot add an argument after an array argument.
*/
public function testArrayArgumentHasToBeLast()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Cannot add an argument after an array argument.');
$this->initializeArguments();
$definition = new InputDefinition();
@@ -109,10 +112,12 @@ class InputDefinitionTest extends TestCase
$definition->addArgument(new InputArgument('anotherbar'));
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot add a required argument after an optional one.
*/
public function testRequiredArgumentCannotFollowAnOptionalOne()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Cannot add a required argument after an optional one.');
$this->initializeArguments();
$definition = new InputDefinition();
@@ -129,10 +134,12 @@ class InputDefinitionTest extends TestCase
$this->assertEquals($this->foo, $definition->getArgument('foo'), '->getArgument() returns a InputArgument by its name');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "bar" argument does not exist.
*/
public function testGetInvalidArgument()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "bar" argument does not exist.');
$this->initializeArguments();
$definition = new InputDefinition();
@@ -199,10 +206,12 @@ class InputDefinitionTest extends TestCase
$this->assertEquals(['bar' => $this->bar], $definition->getOptions(), '->setOptions() clears all InputOption objects');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "-f" option does not exist.
*/
public function testSetOptionsClearsOptions()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "-f" option does not exist.');
$this->initializeOptions();
$definition = new InputDefinition([$this->foo]);
@@ -231,10 +240,12 @@ class InputDefinitionTest extends TestCase
$this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getOptions(), '->addOption() adds a InputOption object');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage An option named "foo" already exists.
*/
public function testAddDuplicateOption()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('An option named "foo" already exists.');
$this->initializeOptions();
$definition = new InputDefinition();
@@ -242,10 +253,12 @@ class InputDefinitionTest extends TestCase
$definition->addOption($this->foo2);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage An option with shortcut "f" already exists.
*/
public function testAddDuplicateShortcutOption()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('An option with shortcut "f" already exists.');
$this->initializeOptions();
$definition = new InputDefinition();
@@ -261,10 +274,12 @@ class InputDefinitionTest extends TestCase
$this->assertEquals($this->foo, $definition->getOption('foo'), '->getOption() returns a InputOption by its name');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "--bar" option does not exist.
*/
public function testGetInvalidOption()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "--bar" option does not exist.');
$this->initializeOptions();
$definition = new InputDefinition([$this->foo]);
@@ -306,10 +321,12 @@ class InputDefinitionTest extends TestCase
$this->assertEquals($this->multi, $definition->getOptionForShortcut('mmm'), '->getOptionForShortcut() returns a InputOption by its shortcut');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "-l" option does not exist.
*/
public function testGetOptionForInvalidShortcut()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "-l" option does not exist.');
$this->initializeOptions();
$definition = new InputDefinition([$this->foo]);
@@ -357,8 +374,9 @@ class InputDefinitionTest extends TestCase
[new InputDefinition([new InputArgument('foo', InputArgument::REQUIRED)]), '<foo>', 'puts arguments in angle brackets'],
[new InputDefinition([new InputArgument('foo')]), '[<foo>]', 'puts optional arguments in square brackets'],
[new InputDefinition([new InputArgument('foo', InputArgument::IS_ARRAY)]), '[<foo>]...', 'uses an ellipsis for array arguments'],
[new InputDefinition([new InputArgument('foo', InputArgument::REQUIRED | InputArgument::IS_ARRAY)]), '<foo> (<foo>)...', 'uses parenthesis and ellipsis for required array arguments'],
[new InputDefinition([new InputArgument('foo'), new InputArgument('bar')]), '[<foo> [<bar>]]', 'chains optional arguments inside brackets'],
[new InputDefinition([new InputArgument('foo', InputArgument::IS_ARRAY)]), '[<foo>...]', 'uses an ellipsis for array arguments'],
[new InputDefinition([new InputArgument('foo', InputArgument::REQUIRED | InputArgument::IS_ARRAY)]), '<foo>...', 'uses an ellipsis for required array arguments'],
[new InputDefinition([new InputOption('foo'), new InputArgument('foo', InputArgument::REQUIRED)]), '[--foo] [--] <foo>', 'puts [--] between options and arguments'],
];
+25 -23
View File
@@ -24,10 +24,12 @@ class InputOptionTest extends TestCase
$this->assertEquals('foo', $option->getName(), '__construct() removes the leading -- of the option name');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.
*/
public function testArrayModeWithoutValue()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
new InputOption('foo', 'f', InputOption::VALUE_IS_ARRAY);
}
@@ -72,39 +74,35 @@ class InputOptionTest extends TestCase
}
/**
* @dataProvider provideInvalidModes
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Option mode "-1" is not valid.
*/
public function testInvalidModes($mode)
public function testInvalidModes()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage(sprintf('Option mode "%s" is not valid.', $mode));
new InputOption('foo', 'f', $mode);
}
public function provideInvalidModes()
{
return [
['ANOTHER_ONE'],
[-1],
];
new InputOption('foo', 'f', '-1');
}
/**
* @expectedException \InvalidArgumentException
*/
public function testEmptyNameIsInvalid()
{
$this->expectException('InvalidArgumentException');
new InputOption('');
}
/**
* @expectedException \InvalidArgumentException
*/
public function testDoubleDashNameIsInvalid()
{
$this->expectException('InvalidArgumentException');
new InputOption('--');
}
/**
* @expectedException \InvalidArgumentException
*/
public function testSingleDashOptionIsInvalid()
{
$this->expectException('InvalidArgumentException');
new InputOption('foo', '-');
}
@@ -153,18 +151,22 @@ class InputOptionTest extends TestCase
$this->assertEquals([1, 2], $option->getDefault(), '->setDefault() changes the default value');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot set a default value when using InputOption::VALUE_NONE mode.
*/
public function testDefaultValueWithValueNoneMode()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Cannot set a default value when using InputOption::VALUE_NONE mode.');
$option = new InputOption('foo', 'f', InputOption::VALUE_NONE);
$option->setDefault('default');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage A default value for an array option must be an array.
*/
public function testDefaultValueWithIsArrayMode()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('A default value for an array option must be an array.');
$option = new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY);
$option->setDefault('default');
}
+24 -12
View File
@@ -47,18 +47,22 @@ class InputTest extends TestCase
$this->assertEquals(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "foo" option does not exist.
*/
public function testSetInvalidOption()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "foo" option does not exist.');
$input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
$input->setOption('foo', 'bar');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "foo" option does not exist.
*/
public function testGetInvalidOption()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "foo" option does not exist.');
$input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
$input->getOption('foo');
}
@@ -77,35 +81,43 @@ class InputTest extends TestCase
$this->assertEquals(['name' => 'foo', 'bar' => 'default'], $input->getArguments(), '->getArguments() returns all argument values, even optional ones');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "foo" argument does not exist.
*/
public function testSetInvalidArgument()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "foo" argument does not exist.');
$input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')]));
$input->setArgument('foo', 'bar');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The "foo" argument does not exist.
*/
public function testGetInvalidArgument()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The "foo" argument does not exist.');
$input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')]));
$input->getArgument('foo');
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Not enough arguments (missing: "name").
*/
public function testValidateWithMissingArguments()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('Not enough arguments (missing: "name").');
$input = new ArrayInput([]);
$input->bind(new InputDefinition([new InputArgument('name', InputArgument::REQUIRED)]));
$input->validate();
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Not enough arguments (missing: "name").
*/
public function testValidateWithMissingRequiredArguments()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('Not enough arguments (missing: "name").');
$input = new ArrayInput(['bar' => 'baz']);
$input->bind(new InputDefinition([new InputArgument('name', InputArgument::REQUIRED), new InputArgument('bar', InputArgument::OPTIONAL)]));
$input->validate();
+5 -3
View File
@@ -70,7 +70,7 @@ class ConsoleLoggerTest extends TestCase
$logger = new ConsoleLogger($out, $addVerbosityLevelMap);
$logger->log($logLevel, 'foo bar');
$logs = $out->fetch();
$this->assertEquals($isOutput ? "[$logLevel] foo bar".\PHP_EOL : '', $logs);
$this->assertEquals($isOutput ? "[$logLevel] foo bar".PHP_EOL : '', $logs);
}
public function provideOutputMappingParams()
@@ -141,9 +141,11 @@ class ConsoleLoggerTest extends TestCase
];
}
/**
* @expectedException \Psr\Log\InvalidArgumentException
*/
public function testThrowsOnInvalidLevel()
{
$this->expectException('Psr\Log\InvalidArgumentException');
$logger = $this->getLogger();
$logger->log('invalid level', 'Foo');
}
@@ -162,7 +164,7 @@ class ConsoleLoggerTest extends TestCase
if (method_exists($this, 'createPartialMock')) {
$dummy = $this->createPartialMock('Symfony\Component\Console\Tests\Logger\DummyTest', ['__toString']);
} else {
$dummy = $this->createPartialMock('Symfony\Component\Console\Tests\Logger\DummyTest', ['__toString']);
$dummy = $this->getMock('Symfony\Component\Console\Tests\Logger\DummyTest', ['__toString']);
}
$dummy->method('__toString')->willReturn('DUMMY');
+13
View File
@@ -81,6 +81,19 @@ class OutputTest extends TestCase
$this->assertEquals("foo\nbar\n", $output->output, '->writeln() can take an array of messages to output');
}
public function testWriteAnIterableOfMessages()
{
$output = new TestOutput();
$output->writeln($this->generateMessages());
$this->assertEquals("foo\nbar\n", $output->output, '->writeln() can take an iterable of messages to output');
}
private function generateMessages(): iterable
{
yield 'foo';
yield 'bar';
}
/**
* @dataProvider provideWriteArguments
*/
+5 -11
View File
@@ -36,10 +36,12 @@ class StreamOutputTest extends TestCase
$this->assertTrue($output->isDecorated(), '__construct() takes the decorated flag as its second argument');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The StreamOutput class needs a stream as its first argument.
*/
public function testStreamIsRequired()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('The StreamOutput class needs a stream as its first argument.');
new StreamOutput('foo');
}
@@ -54,14 +56,6 @@ class StreamOutputTest extends TestCase
$output = new StreamOutput($this->stream);
$output->writeln('foo');
rewind($output->getStream());
$this->assertEquals('foo'.\PHP_EOL, stream_get_contents($output->getStream()), '->doWrite() writes to the stream');
}
public function testDoWriteOnFailure()
{
$resource = fopen(__DIR__.'/../Fixtures/stream_output_file.txt', 'r', false);
$output = new StreamOutput($resource);
rewind($output->getStream());
$this->assertEquals('', stream_get_contents($output->getStream()));
$this->assertEquals('foo'.PHP_EOL, stream_get_contents($output->getStream()), '->doWrite() writes to the stream');
}
}
+1 -39
View File
@@ -18,31 +18,17 @@ class TerminalTest extends TestCase
{
private $colSize;
private $lineSize;
private $ansiCon;
protected function setUp()
{
$this->colSize = getenv('COLUMNS');
$this->lineSize = getenv('LINES');
$this->ansiCon = getenv('ANSICON');
$this->resetStatics();
}
protected function tearDown()
{
putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS');
putenv($this->lineSize ? 'LINES' : 'LINES='.$this->lineSize);
putenv($this->ansiCon ? 'ANSICON='.$this->ansiCon : 'ANSICON');
$this->resetStatics();
}
private function resetStatics()
{
foreach (['height', 'width', 'stty'] as $name) {
$property = new \ReflectionProperty(Terminal::class, $name);
$property->setAccessible(true);
$property->setValue(null);
}
}
public function test()
@@ -60,7 +46,7 @@ class TerminalTest extends TestCase
$this->assertSame(60, $terminal->getHeight());
}
public function testZeroValues()
public function test_zero_values()
{
putenv('COLUMNS=0');
putenv('LINES=0');
@@ -70,28 +56,4 @@ class TerminalTest extends TestCase
$this->assertSame(0, $terminal->getWidth());
$this->assertSame(0, $terminal->getHeight());
}
public function testSttyOnWindows()
{
if ('\\' !== \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Must be on windows');
}
$sttyString = exec('(stty -a | grep columns) 2>&1', $output, $exitcode);
if (0 !== $exitcode) {
$this->markTestSkipped('Must have stty support');
}
$matches = [];
if (0 === preg_match('/columns.(\d+)/i', $sttyString, $matches)) {
$this->fail('Could not determine existing stty columns');
}
putenv('COLUMNS');
putenv('LINES');
putenv('ANSICON');
$terminal = new Terminal();
$this->assertSame((int) $matches[1], $terminal->getWidth());
}
}
@@ -13,7 +13,9 @@ namespace Symfony\Component\Console\Tests\Tester;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Tester\ApplicationTester;
class ApplicationTesterTest extends TestCase
@@ -27,7 +29,9 @@ class ApplicationTesterTest extends TestCase
$this->application->setAutoExit(false);
$this->application->register('foo')
->addArgument('foo')
->setCode(function ($input, $output) { $output->writeln('foo'); })
->setCode(function ($input, $output) {
$output->writeln('foo');
})
;
$this->tester = new ApplicationTester($this->application);
@@ -55,16 +59,55 @@ class ApplicationTesterTest extends TestCase
public function testGetOutput()
{
rewind($this->tester->getOutput()->getStream());
$this->assertEquals('foo'.\PHP_EOL, stream_get_contents($this->tester->getOutput()->getStream()), '->getOutput() returns the current output instance');
$this->assertEquals('foo'.PHP_EOL, stream_get_contents($this->tester->getOutput()->getStream()), '->getOutput() returns the current output instance');
}
public function testGetDisplay()
{
$this->assertEquals('foo'.\PHP_EOL, $this->tester->getDisplay(), '->getDisplay() returns the display of the last execution');
$this->assertEquals('foo'.PHP_EOL, $this->tester->getDisplay(), '->getDisplay() returns the display of the last execution');
}
public function testSetInputs()
{
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function ($input, $output) {
$helper = new QuestionHelper();
$helper->ask($input, $output, new Question('Q1'));
$helper->ask($input, $output, new Question('Q2'));
$helper->ask($input, $output, new Question('Q3'));
});
$tester = new ApplicationTester($application);
$tester->setInputs(['I1', 'I2', 'I3']);
$tester->run(['command' => 'foo']);
$this->assertSame(0, $tester->getStatusCode());
$this->assertEquals('Q1Q2Q3', $tester->getDisplay(true));
}
public function testGetStatusCode()
{
$this->assertSame(0, $this->tester->getStatusCode(), '->getStatusCode() returns the status code');
}
public function testErrorOutput()
{
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')
->addArgument('foo')
->setCode(function ($input, $output) {
$output->getErrorOutput()->write('foo');
})
;
$tester = new ApplicationTester($application);
$tester->run(
['command' => 'foo', 'foo' => 'bar'],
['capture_stderr_separately' => true]
);
$this->assertSame('foo', $tester->getErrorOutput());
}
}
+30 -7
View File
@@ -59,12 +59,12 @@ class CommandTesterTest extends TestCase
public function testGetOutput()
{
rewind($this->tester->getOutput()->getStream());
$this->assertEquals('foo'.\PHP_EOL, stream_get_contents($this->tester->getOutput()->getStream()), '->getOutput() returns the current output instance');
$this->assertEquals('foo'.PHP_EOL, stream_get_contents($this->tester->getOutput()->getStream()), '->getOutput() returns the current output instance');
}
public function testGetDisplay()
{
$this->assertEquals('foo'.\PHP_EOL, $this->tester->getDisplay(), '->getDisplay() returns the display of the last execution');
$this->assertEquals('foo'.PHP_EOL, $this->tester->getDisplay(), '->getDisplay() returns the display of the last execution');
}
public function testGetStatusCode()
@@ -138,10 +138,12 @@ class CommandTesterTest extends TestCase
$this->assertEquals(implode('', $questions), $tester->getDisplay(true));
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testCommandWithWrongInputsNumber()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('Aborted.');
$questions = [
'What\'s your name?',
'How are you?',
@@ -163,10 +165,12 @@ class CommandTesterTest extends TestCase
$tester->execute([]);
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testCommandWithQuestionsButNoInputs()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('Aborted.');
$questions = [
'What\'s your name?',
'How are you?',
@@ -196,7 +200,7 @@ class CommandTesterTest extends TestCase
];
$command = new Command('foo');
$command->setCode(function ($input, $output) use ($questions) {
$command->setCode(function ($input, $output) use ($questions, $command) {
$io = new SymfonyStyle($input, $output);
$io->ask($questions[0]);
$io->ask($questions[1]);
@@ -209,4 +213,23 @@ class CommandTesterTest extends TestCase
$this->assertEquals(0, $tester->getStatusCode());
}
public function testErrorOutput()
{
$command = new Command('foo');
$command->addArgument('command');
$command->addArgument('foo');
$command->setCode(function ($input, $output) {
$output->getErrorOutput()->write('foo');
}
);
$tester = new CommandTester($command);
$tester->execute(
['foo' => 'bar'],
['capture_stderr_separately' => true]
);
$this->assertSame('foo', $tester->getErrorOutput());
}
}
+12 -7
View File
@@ -16,16 +16,16 @@
}
],
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/polyfill-mbstring": "~1.0",
"symfony/debug": "~2.8|~3.0|~4.0"
"php": "^7.1.3",
"symfony/contracts": "^1.0",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
"symfony/config": "~3.3|~4.0",
"symfony/event-dispatcher": "~2.8|~3.0|~4.0",
"symfony/config": "~3.4|~4.0",
"symfony/event-dispatcher": "~3.4|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/lock": "~3.4|~4.0",
"symfony/process": "~3.3|~4.0",
"symfony/process": "~3.4|~4.0",
"psr/log": "~1.0"
},
"provide": {
@@ -47,5 +47,10 @@
"/Tests/"
]
},
"minimum-stability": "dev"
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "4.2-dev"
}
}
}