Skip to content

Commit

Permalink
[feature] add ability to test command completion
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond committed Oct 3, 2022
1 parent c0fbfda commit 0b5c861
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 1 deletion.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ class CreateUserCommandTest extends KernelTestCase
->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown
;

// test completion
$this->consoleCommand('create:user')
->complete('')
->is(['kevin', 'john', 'jane'])
->contains('kevin') // chain assertions
->back() // fluently go back to the TestCommand
->complete('j')->is(['john', 'jane'])->back()
->complete('kevin --role=')->is(['ROLE_EMPLOYEE', 'ROLE_MANAGER'])->back()
->complete('kevin --role=ROLE_M')->is(['ROLE_MANAGER'])
;

// access result
$result = $this->executeConsoleCommand('create:user');

Expand Down Expand Up @@ -143,6 +154,17 @@ class CreateUserCommandTest extends TestCase
->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown
;

// test completion
TestCommand::for(new CreateUserCommand(/** args... */))
->complete('')
->is(['kevin', 'john', 'jane'])
->contains('kevin') // chain assertions
->back() // fluently go back to the TestCommand
->complete('j')->is(['john', 'jane'])->back()
->complete('kevin --role=')->is(['ROLE_EMPLOYEE', 'ROLE_MANAGER'])->back()
->complete('kevin --role=ROLE_M')->is(['ROLE_MANAGER'])
;

// access result
$result = TestCommand::for(new CreateUserCommand(/** args... */))->execute();

Expand Down
1 change: 0 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ parameters:
level: 8
paths:
- src
- tests
41 changes: 41 additions & 0 deletions src/Assert/CompletionExpectation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Zenstruck\Console\Test\Assert;

use Zenstruck\Assert\Expectation;
use Zenstruck\Console\Test\TestCommand;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @mixin Expectation
*/
final class CompletionExpectation
{
private TestCommand $command;
private Expectation $expectation;

/**
* @internal
*/
public function __construct(TestCommand $command, Expectation $expectation)
{
$this->command = $command;
$this->expectation = $expectation;
}

/**
* @internal
*/
public function __call(string $name, array $arguments): self // @phpstan-ignore-line
{
$this->expectation->{$name}(...$arguments);

return $this;
}

public function back(): TestCommand
{
return $this->command;
}
}
40 changes: 40 additions & 0 deletions src/TestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Zenstruck\Assert;
use Zenstruck\Console\Test\Assert\CompletionExpectation;

/**
* @author Kevin Bond <kevinbond@gmail.com>
Expand Down Expand Up @@ -140,6 +143,43 @@ public function execute(?string $cli = null): CommandResult
return new CommandResult($cli, $status, $output);
}

/**
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*
* @source https://github.com/symfony/symfony/blob/53db097989b480a92e4e11deb2634afca6c40ac3/src/Symfony/Component/Console/Tester/CommandCompletionTester.php
*/
public function complete(string $cli): CompletionExpectation
{
$input = \explode(' ', $cli);

$currentIndex = \count($input);

if ('' === \end($input)) {
\array_pop($input);
}

\array_unshift($input, $name = \explode(' ', $this->cli)[0]);

$command = $this->application->get($name);
$input = CompletionInput::fromTokens($input, $currentIndex);
$input->bind($command->getDefinition());
$suggestions = new CompletionSuggestions();

$command->complete($input, $suggestions);

return new CompletionExpectation($this, Assert::that(
\array_values(
\array_map(
'strval',
\array_filter( // mimic how the shell script would complete
$suggestions->getValueSuggestions(),
static fn(string $v) => str_starts_with($v, $input->getCompletionValue()) // @phpstan-ignore-line
)
)
)
));
}

private function doRun(TestInput $input, TestOutput $output): int
{
$fn = fn() => $this->application->run($input, $output);
Expand Down
94 changes: 94 additions & 0 deletions tests/UnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
namespace Zenstruck\Console\Test\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\HttpKernel\Kernel;
use Zenstruck\Console\Test\TestCommand;
use Zenstruck\Console\Test\Tests\Fixture\FixtureCommand;

Expand Down Expand Up @@ -193,4 +198,93 @@ public function always_executes_with_console_output(): void
->assertOutputContains('table row 2')
;
}

/**
* @test
*/
public function completion(): void
{
if (!\class_exists(CompletionInput::class)) {
$this->markTestSkipped('Command completion not available.');
}

$command = TestCommand::for(new class() extends Command {
public function getName(): string
{
return 'my:command';
}

public function configure(): void
{
$this
->addArgument('user')
->addOption('message', null, InputOption::VALUE_REQUIRED)
;
}

public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('user')) {
$suggestions->suggestValues(['kevin', 'john', 'jane']);
}

if ($input->mustSuggestOptionValuesFor('message')) {
$suggestions->suggestValues(['hello', 'hi', 'greetings']);
}
}
});

$command
->complete('')
->is(['kevin', 'john', 'jane'])
->contains('kevin')
->back()
->complete('ke')->is(['kevin'])->back()
->complete('j')->is(['john', 'jane'])->back()
->complete('kevin --message=')->is(['hello', 'hi', 'greetings'])->back()
->complete('kevin --message=g')->is(['greetings'])->back()
->complete('kevin --message=h')->is(['hello', 'hi'])
;
}

/**
* @test
*/
public function completion_61(): void
{
if (!\class_exists(CompletionInput::class)) {
$this->markTestSkipped('Command completion not available.');
}

if (Kernel::VERSION_ID < 60100) {
$this->markTestSkipped('Using InputArgument/Option for defining suggestions requires 6.1+.');
}

$command = TestCommand::for(new class() extends Command {
public function getName(): string
{
return 'my:command';
}

public function configure(): void
{
$this
->addArgument('user', null, '', null, ['kevin', 'john', 'jane'])
->addOption('message', null, InputOption::VALUE_REQUIRED, '', null, ['hello', 'hi', 'greetings'])
;
}
});

$command
->complete('')
->is(['kevin', 'john', 'jane'])
->contains('kevin')
->back()
->complete('ke')->is(['kevin'])->back()
->complete('j')->is(['john', 'jane'])->back()
->complete('kevin --message=')->is(['hello', 'hi', 'greetings'])->back()
->complete('kevin --message=g')->is(['greetings'])->back()
->complete('kevin --message=h')->is(['hello', 'hi'])
;
}
}

0 comments on commit 0b5c861

Please sign in to comment.