Description
Symfony version(s) affected: 5.0.0, 5.1.0
Description
When using the QuestionHelper
and testing via an ApplicationTester
, if $maxAttempts
has not been set, the question will stop prompting for input after the first question response, regardless of whether or not more input are available.
How to reproduce
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Tester\ApplicationTester;
require './vendor/autoload.php';
class QuestionCommand extends Command
{
protected function configure(): void
{
$this->setDescription('A command that asks a question');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$question = new Question('This is a promptable question');
$question->setValidator(function ($value) {
if (! preg_match('/^[A-Z][A-Za-z0-9_]+$/', $value)) {
throw new RuntimeException('Question requires a valid class name');
}
return $value;
});
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$class = $helper->ask($input, $output, $question);
$output->writeln(sprintf('<info>%s</info>', $class));
return 0;
}
}
$application = new Application('test');
$application->add(new QuestionCommand('question'));
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->setInputs(['', 'not-a-class', 'also not a class', 'FinallyAClass']);
$statusCode = $tester->run(
['command' => 'question'],
['interactive' => true]
);
printf("Status code: %d\n", $statusCode);
Expected result:
Status code: 0
Actual result:
Status code: 1
Possible Solution
In both version 4 and version 5 code, QuestionHelper::validateAttempts()
has the following loop defined:
while (null === $attempts || $attempts--)
In version 5, the following line is added at the end of the loop:
$attempts = $attempts ?? -(int) $this->isTty();
This means that you will always run the loop exactly 1 time when $attempts
is set to null
, as isTty()
always returns false
when run under the ApplicationTester
(which uses a PHP memory stream to represent the TTY input stream).
An $attempts
value of null
is meant to indicate no maximum, and to prompt indefinitely; more importantly, it is the default value.
In order to preserve previous (and documented) behavior, I think this should likely read:
$attempts = $attempts === null ? null : -(int) $this->isTtty();