Skip to content

Console ApplicationTester does not allow multiple prompts on invalid Question input #37046

Closed
@weierophinney

Description

@weierophinney

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();

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions