Skip to content

[Console] Simplify simulation of user inputs in CommandTester #18710

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/Symfony/Component/Console/Tester/CommandTester.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
* Eases the testing of console commands.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class CommandTester
{
private $command;
private $input;
private $output;
private $inputs = array();
private $statusCode;

/**
Expand Down Expand Up @@ -65,6 +67,10 @@ public function execute(array $input, array $options = array())
}

$this->input = new ArrayInput($input);
if ($this->inputs) {
$this->input->setStream(self::createStream($this->inputs));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong to me. Here we are only dealing with one specific case, which is the question helper. We should rather find a way to make this "generic" if possible.

Copy link
Member Author

@chalasr chalasr Jun 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabpot Only the QuestionHelper and SymfonyQuestionHelper have an inputStream, so I don't see a real benefit to make this looking for another object that the question helper.

I made this covering the case of SymfonyStyle is used (SymfonyQuestionHelper) in #18902, as this one is not part of the command helperset, we don't have any way to make it generic. It's planned for 4.0 to move the setInputStream method in the InputInterface, but ATM the QuesitonHelper is the only available place (and documented as is).

Copy link
Member Author

@chalasr chalasr Jun 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an interface with a setInputStream method then implement it in the Input object could be a good solution. We could then automatically use Input::$inputStream in every helper that has an inputStream, working for both QuestionHelper and SymfonyQuestionHelper. At first we could look for the Input::$inputStream while letting the QuestionHelper::$inputStream as deprecated, for finally remove it in 4.0.
What do you think? It would close #18902 and ease interactive command testing properly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabpot I just opened #18999 that would BTW solve this problem.
It gives:

if ($this->inputs) {
-     if (null === $this->command->getHelperSet()) {
-         $helper = new QuestionHelper();
-         $this->command->setHelperSet(new HelperSet(array($helper)));
-     } else {
-         $helper = $this->command->getHelper('question');
-     }
-     $helper->setInputStream($this->createInputStream());

+    $this->input->setStream($this->createInputStream());
}


if (isset($options['interactive'])) {
$this->input->setInteractive($options['interactive']);
}
Expand Down Expand Up @@ -129,4 +135,29 @@ public function getStatusCode()
{
return $this->statusCode;
}

/**
* Sets the user inputs.
*
* @param array An array of strings representing each input
* passed to the command input stream.
*
* @return CommandTester
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo this docblock as it is now is quite useless and could be removed as well (input types and the return type can be inferred by IDEs from the method itself) and the description doesn't add any value to the user apart from what the method name already states. Though explaining what $inputs exactly is expected to be would be something that justified its presence.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a description.

*/
public function setInputs(array $inputs)
{
$this->inputs = $inputs;

return $this;
}

private static function createStream(array $inputs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of marking it public? I would have kept it private.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabpot : Did you mean static ?
I guess it makes sense, as this method looks like a utility one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I meant public. I think it should be private, I don't see why an end user would have to use this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabpot : But the createStream method is actually private. Not public. 😕

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, sorry about the confusion.

{
$stream = fopen('php://memory', 'r+', false);

fputs($stream, implode(PHP_EOL, $inputs));
rewind($stream);

return $stream;
}
}
78 changes: 78 additions & 0 deletions src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Style\SymfonyStyle;

class CommandTesterTest extends \PHPUnit_Framework_TestCase
{
Expand Down Expand Up @@ -81,4 +85,78 @@ public function testCommandFromApplication()
// check that there is no need to pass the command name here
$this->assertEquals(0, $tester->execute(array()));
}

public function testCommandWithInputs()
{
$questions = array(
'What\'s your name?',
'How are you?',
'Where do you come from?',
);

$command = new Command('foo');
$command->setHelperSet(new HelperSet(array(new QuestionHelper())));
$command->setCode(function ($input, $output) use ($questions, $command) {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new Question($questions[0]));
$helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2]));
});

$tester = new CommandTester($command);
$tester->setInputs(array('Bobby', 'Fine', 'France'));
$tester->execute(array());

$this->assertEquals(0, $tester->getStatusCode());
$this->assertEquals(implode('', $questions), $tester->getDisplay(true));
}

/**
* @expectedException \RuntimeException
* @expectedMessage Aborted
*/
public function testCommandWithWrongInputsNumber()
{
$questions = array(
'What\'s your name?',
'How are you?',
'Where do you come from?',
);

$command = new Command('foo');
$command->setHelperSet(new HelperSet(array(new QuestionHelper())));
$command->setCode(function ($input, $output) use ($questions, $command) {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new Question($questions[0]));
$helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2]));
});

$tester = new CommandTester($command);
$tester->setInputs(array('Bobby', 'Fine'));
$tester->execute(array());
}

public function testSymfonyStyleCommandWithInputs()
{
$questions = array(
'What\'s your name?',
'How are you?',
'Where do you come from?',
);

$command = new Command('foo');
$command->setCode(function ($input, $output) use ($questions, $command) {
$io = new SymfonyStyle($input, $output);
$io->ask($questions[0]);
$io->ask($questions[1]);
$io->ask($questions[2]);
});

$tester = new CommandTester($command);
$tester->setInputs(array('Bobby', 'Fine', 'France'));
$tester->execute(array());

$this->assertEquals(0, $tester->getStatusCode());
}
}