From ccb38e818180e6ff438fb1118798a7b41aedb141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 7 Dec 2024 12:28:12 +0100 Subject: [PATCH 1/7] feat: Add parameters to the command tester class to avoid having to rely on execute($options) # Conflicts: # src/Symfony/Component/Console/Tester/CommandTester.php --- .../Component/Console/Output/TestOutput.php | 173 ++++++++++++++++++ .../Console/Tester/CommandTester.php | 22 ++- .../Console/Tester/ExecutionResult.php | 171 +++++++++++++++++ 3 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/Console/Output/TestOutput.php create mode 100644 src/Symfony/Component/Console/Tester/ExecutionResult.php diff --git a/src/Symfony/Component/Console/Output/TestOutput.php b/src/Symfony/Component/Console/Output/TestOutput.php new file mode 100644 index 0000000000000..897ddc1b876a5 --- /dev/null +++ b/src/Symfony/Component/Console/Output/TestOutput.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Symfony\Component\Console\Output; + +use DomainException; +use Fidry\Console\Test\CombinedOutput; +use RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; +use function fopen; +use function func_get_args; +use function rewind; +use function stream_get_contents; + +final class TestOutput implements ConsoleOutputInterface +{ + private OutputInterface $innerOutput; + private OutputInterface $innerErrorOutput; + private OutputInterface $displayOutput; + private CombinedOutput $output; + private CombinedOutput $errorOutput; + + /** + * @param OutputInterface::VERBOSITY_* $verbosity + */ + public function __construct( + private int $verbosity, + private bool $decorated, + private OutputFormatterInterface $formatter, + ) { + $this->innerOutput = self::createOutput($this); + $this->innerErrorOutput = self::createOutput($this); + $this->displayOutput = self::createOutput($this); + + $this->output = new CombinedOutput( + $this->innerOutput, + $this->displayOutput, + ); + $this->errorOutput = new CombinedOutput( + $this->innerErrorOutput, + $this->displayOutput, + ); + } + + public function getOutputContents(): string + { + return self::getStreamContents($this->innerOutput); + } + + public function getErrorOutputContents(): string + { + return self::getStreamContents($this->innerErrorOutput); + } + + public function getDisplayContents(): string + { + return self::getStreamContents($this->displayOutput); + } + + public function getErrorOutput(): OutputInterface + { + return $this->errorOutput; + } + + public function setErrorOutput(OutputInterface $error): void + { + throw new DomainException('Should not be modified.'); + } + + public function section(): ConsoleSectionOutput + { + throw new DomainException('Not supported (yet).'); + } + + public function write(iterable|string $messages, bool $newline = false, int $options = 0): void + { + $this->output->write(...func_get_args()); + } + + public function writeln(iterable|string $messages, int $options = 0): void + { + $this->output->writeln(...func_get_args()); + } + + public function setVerbosity(int $level): void + { + throw new DomainException('Should not be modified.'); + } + + public function getVerbosity(): int + { + return $this->verbosity; + } + + public function isQuiet(): bool + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose(): bool + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose(): bool + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug(): bool + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + public function setDecorated(bool $decorated): void + { + throw new DomainException('Should not be modified.'); + } + + public function isDecorated(): bool + { + return $this->formatter->isDecorated(); + } + + public function setFormatter(OutputFormatterInterface $formatter): void + { + throw new DomainException('Should not be modified.'); + } + + public function getFormatter(): OutputFormatterInterface + { + return $this->formatter; + } + + private static function createOutput(OutputInterface $config): StreamOutput + { + $stream = fopen('php://memory', 'wb'); + + if (false === $stream) { + throw new RuntimeException('Failed to open stream.'); + } + + return new StreamOutput( + $stream, + $config->getVerbosity(), + $config->isDecorated(), + $config->getFormatter(), + ); + } + + private static function getStreamContents(StreamOutput $output): string + { + $stream = $output->getStream(); + + rewind($stream); + + return stream_get_contents($stream); + } +} diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index 714d88ad51dea..e3a8d67b89bb1 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -28,10 +28,28 @@ class CommandTester public function __construct( callable|Command $command, + private ?bool $interactive = null, + private bool $decorated = false, ) { $this->command = $command instanceof Command ? $command : new Command(null, $command); } + /** + * Sets the input interactivity. + */ + public function setInteractive(bool $interactive): void + { + $this->interactive = $interactive; + } + + /** + * Sets the decorated flag. + */ + public function setDecorated(bool $decorated): void + { + $this->decorated = $decorated; + } + /** * Executes the command. * @@ -63,11 +81,11 @@ public function execute(array $input, array $options = []): int $this->input->setStream(self::createStream($this->inputs)); if (isset($options['interactive'])) { - $this->input->setInteractive($options['interactive']); + $this->input->setInteractive($options['interactive'] ?? $this->interactive); } if (!isset($options['decorated'])) { - $options['decorated'] = false; + $options['decorated'] = $this->decorated; } $this->initOutput($options); diff --git a/src/Symfony/Component/Console/Tester/ExecutionResult.php b/src/Symfony/Component/Console/Tester/ExecutionResult.php new file mode 100644 index 0000000000000..96b79c2c48b91 --- /dev/null +++ b/src/Symfony/Component/Console/Tester/ExecutionResult.php @@ -0,0 +1,171 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use PHPUnit\Framework\Assert; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; +use function array_filter; +use function call_user_func; +use function function_exists; +use function implode; +use function str_replace; +use const PHP_EOL; + +/** + * @author Théo FIDRY + */ +class ExecutionResult +{ + public static function fromTestOutput( + string $intput, + int $statusCode, + OutputInterface $output, + ) { + return new self( + $statusCode, + $output->(), + $output->getErrorOutputContents(), + $output->getDisplayContents(), + ); + } + + public function __construct( + private string $input, + private int $statusCode, + private string $output, + private string $errorOutput, + private string $display, + ) { + } + + /** + * Gets the status code returned by the execution of the command or application. + */ + public function getStatusCode(): int + { + return $this->statusCode; + } + + /** + * Gets the display returned by the execution of the command or application. The display combines what was + * written on both the output and error output. + * + * @param bool $normalize Whether to normalize end of lines to \n or not. + */ + public function getDisplay(bool $normalize = false): string + { + return $normalize + ? self::normalizeEndOfLines($this->display) + : $this->display; + } + + /** + * Gets the output written to the output by the command or application. + * + * @param bool $normalize Whether to normalize end of lines to \n or not. + */ + public function getOutput(bool $normalize = false): string + { + return $normalize + ? self::normalizeEndOfLines($this->output) + : $this->output; + } + + /** + * Gets the output written to the error output by the command or application. + * + * @param bool $normalize Whether to normalize end of lines to \n or not. + */ + public function getErrorOutput(bool $normalize = false): string + { + return $normalize + ? self::normalizeEndOfLines($this->errorOutput) + : $this->errorOutput; + } + + public function assertCommandIsSuccessful(string $message = ''): void + { + Assert::assertThat($this->statusCode, new CommandIsSuccessful(), $message); + } + + public function assertOutputContains(string $expected): self + { + Assert::that($this->output())->contains($expected); + + return $this; + } + + public function assertOutputNotContains(string $expected): self + { + Assert::that($this->output())->doesNotContain($expected); + + return $this; + } + + public function assertErrorOutputContains(string $expected): self + { + Assert::that($this->errorOutput())->contains($expected); + + return $this; + } + + public function assertErrorOutputNotContains(string $expected): self + { + Assert::that($this->errorOutput())->doesNotContain($expected); + + return $this; + } + + public function assertSuccessful(): self + { + return $this->assertStatusCode(0); + } + + public function assertStatusCode(int $expected): self + { + Assert::that($this->statusCode())->is($expected); + + return $this; + } + + public function dump(): self + { + $summary = "CLI: {$this->input}, Status: {$this->statusCode()}"; + $output = [ + $summary, + $this->output(), + $this->errorOutput(), + $summary, + ]; + + call_user_func( + function_exists('dump') ? 'dump' : 'var_dump', + implode("\n\n", array_filter($output)), + ); + + return $this; + } + + public function dd(): void + { + $this->dump(); + exit(1); + } + + private static function normalizeEndOfLines(string $value): string + { + return str_replace(PHP_EOL, "\n", $value); + } +} From 629c58a4592c053dc714f700253c555e96460a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 7 Dec 2024 16:13:26 +0100 Subject: [PATCH 2/7] add run with execution result output --- .../Component/Console/Output/StreamOutput.php | 3 + .../Console/Tester/CommandTester.php | 75 ++++++++++++++ .../Console/Tester/ConsoleAssertionsTrait.php | 46 +++++++++ .../Console/Tester/ExecutionResult.php | 81 +++++---------- .../Component/Console/Tester/TesterTrait.php | 2 + .../Console/Tests/ApplicationTest.php | 2 +- .../Tests/Tester/CommandTesterTest.php | 98 +++++++++++++++++++ 7 files changed, 252 insertions(+), 55 deletions(-) create mode 100644 src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php diff --git a/src/Symfony/Component/Console/Output/StreamOutput.php b/src/Symfony/Component/Console/Output/StreamOutput.php index ce5a825e87808..61a89825a3f98 100644 --- a/src/Symfony/Component/Console/Output/StreamOutput.php +++ b/src/Symfony/Component/Console/Output/StreamOutput.php @@ -13,6 +13,8 @@ use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use function rewind; +use function stream_get_contents; /** * StreamOutput writes the output to a given stream. @@ -74,6 +76,7 @@ protected function doWrite(string $message, bool $newline): void fflush($this->stream); } + // TODO: should be public & static? /** * Returns true if the stream supports colorization. * diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index e3a8d67b89bb1..a2083617a1b0e 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -13,12 +13,14 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\ConsoleOutput; /** * Eases the testing of console commands. * * @author Fabien Potencier * @author Robin Chalas + * @author Théo FIDRY */ class CommandTester { @@ -26,14 +28,20 @@ class CommandTester private Command $command; + /** + * @param OutputInterface::VERBOSITY_* $verbosity + */ public function __construct( callable|Command $command, + // TODO: to discuss if we want it in the constructor with the setters or only in the ::run() method private ?bool $interactive = null, private bool $decorated = false, + private int $verbosity = ConsoleOutput::VERBOSITY_NORMAL, ) { $this->command = $command instanceof Command ? $command : new Command(null, $command); } + // TODO: to discuss if we want fluent setters /** * Sets the input interactivity. */ @@ -50,6 +58,16 @@ public function setDecorated(bool $decorated): void $this->decorated = $decorated; } + /** + * Sets the verbosity of the output. + * + * @param self::VERBOSITY_* $level + */ + public function setVerbosity(int $level): void + { + $this->verbosity = $level; + } + /** * Executes the command. * @@ -92,4 +110,61 @@ public function execute(array $input, array $options = []): int return $this->statusCode = $this->command->run($this->input, $this->output); } + + /** + * Runs the command. + * + * @param array $input An array of command arguments and options + * @param string[] $interactiveInputs An array of strings representing each input + * passed to the command input stream + * @param OutputInterface::VERBOSITY_* $verbosity + */ + public function run( + array $input = [], + array $interactiveInputs = [], + ?bool $interactive = null, + ?bool $decorated = null, + ?int $verbosity = null, + ): ExecutionResult + { + // set the command name automatically if the application requires + // this argument and no command name was passed + if (!isset($input['command']) + && (null !== $application = $this->command->getApplication()) + && $application->getDefinition()->hasArgument('command') + ) { + $input = array_merge(['command' => $this->command->getName()], $input); + } + + $input = new ArrayInput($input); + // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. + $input->setStream(self::createStream($interactiveInputs)); + + $interactive ??= $this->interactive; + + if (null !== $interactive) { + $input->setInteractive($interactive); + } + + $options = [ + 'decorated' => $decorated ?? $this->decorated, + 'verbosity' => $verbosity ?? $this->verbosity, + ]; + + // This is purely for BC. A different way to create the output will be introduced later. + if (isset($this->output)) { + $previousOutput = $this->output; + $this->initOutput($options); + $output = $this->output; + $this->output = $previousOutput; + } else { + $this->initOutput($options); + $output = $this->output; + unset($this->output); + } + + $statusCode = $this->command->run($input, $output); + + return ExecutionResult::fromExecution($input, $statusCode, $output); + } } diff --git a/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php b/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php new file mode 100644 index 0000000000000..647b094297e86 --- /dev/null +++ b/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use PHPUnit\Framework\Assert; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; +use function rewind; +use function str_replace; +use function stream_get_contents; + +/** + * @psalm-require-extends \PHPUnit\Framework\TestCase + * + * @author Théo FIDRY + */ +trait ConsoleAssertionsTrait +{ + public function assertIsSuccessful(ExecutionResult $result, string $message = ''): void + { + Assert::assertThat($result->statusCode, new CommandIsSuccessful(), $message); + } + + public function assertStatusCodeEquals(int $expected, ExecutionResult $result, string $message = ''): void + { + Assert::assertSame($expected, $result->statusCode, $message); + } + + // TODO: move this to its own utility + public static function normalizeLineReturns(string $output): string + { + return str_replace(\PHP_EOL, "\n", $output); + } +} diff --git a/src/Symfony/Component/Console/Tester/ExecutionResult.php b/src/Symfony/Component/Console/Tester/ExecutionResult.php index 96b79c2c48b91..1e06bbcfa3b27 100644 --- a/src/Symfony/Component/Console/Tester/ExecutionResult.php +++ b/src/Symfony/Component/Console/Tester/ExecutionResult.php @@ -14,50 +14,45 @@ use PHPUnit\Framework\Assert; use Symfony\Component\Console\Command\Command; 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; use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; use function array_filter; use function call_user_func; use function function_exists; use function implode; +use function rewind; use function str_replace; +use function stream_get_contents; use const PHP_EOL; /** * @author Théo FIDRY */ -class ExecutionResult +final readonly class ExecutionResult { - public static function fromTestOutput( - string $intput, - int $statusCode, - OutputInterface $output, - ) { + public static function fromExecution( + InputInterface $input, + int $statusCode, + StreamOutput $output, + ): self + { return new self( + $input->__toString(), $statusCode, - $output->(), - $output->getErrorOutputContents(), - $output->getDisplayContents(), + self::getStreamContents($output), ); } public function __construct( - private string $input, - private int $statusCode, - private string $output, - private string $errorOutput, - private string $display, + public string $input, + public int $statusCode, + public string $output, ) { } - /** - * Gets the status code returned by the execution of the command or application. - */ - public function getStatusCode(): int - { - return $this->statusCode; - } - /** * Gets the display returned by the execution of the command or application. The display combines what was * written on both the output and error output. @@ -95,44 +90,13 @@ public function getErrorOutput(bool $normalize = false): string : $this->errorOutput; } - public function assertCommandIsSuccessful(string $message = ''): void + public function assertSuccessful(string $message = ''): self { Assert::assertThat($this->statusCode, new CommandIsSuccessful(), $message); - } - - public function assertOutputContains(string $expected): self - { - Assert::that($this->output())->contains($expected); - - return $this; - } - - public function assertOutputNotContains(string $expected): self - { - Assert::that($this->output())->doesNotContain($expected); return $this; } - public function assertErrorOutputContains(string $expected): self - { - Assert::that($this->errorOutput())->contains($expected); - - return $this; - } - - public function assertErrorOutputNotContains(string $expected): self - { - Assert::that($this->errorOutput())->doesNotContain($expected); - - return $this; - } - - public function assertSuccessful(): self - { - return $this->assertStatusCode(0); - } - public function assertStatusCode(int $expected): self { Assert::that($this->statusCode())->is($expected); @@ -168,4 +132,13 @@ private static function normalizeEndOfLines(string $value): string { return str_replace(PHP_EOL, "\n", $value); } + + private static function getStreamContents(StreamOutput $output): string + { + $stream = $output->getStream(); + + rewind($stream); + + return stream_get_contents($stream); + } } diff --git a/src/Symfony/Component/Console/Tester/TesterTrait.php b/src/Symfony/Component/Console/Tester/TesterTrait.php index 1ab7a70aa22d9..63ade1915bff0 100644 --- a/src/Symfony/Component/Console/Tester/TesterTrait.php +++ b/src/Symfony/Component/Console/Tester/TesterTrait.php @@ -19,6 +19,8 @@ use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; /** + * @deprecated + * * @author Amrouche Hamza */ trait TesterTrait diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 684ebeb91ef15..77ad461b41a99 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -2361,7 +2361,7 @@ public function testAlarmSubscriber() $dispatcher->addSubscriber($subscriber1); $dispatcher->addSubscriber($subscriber2); - $application = $this->createSignalableApplication($command, $dispatcher); + $application = $this->createSignalableaApplication($command, $dispatcher); $this->assertSame(1, $application->run(new ArrayInput(['signal']))); $this->assertTrue($subscriber1->signaled); diff --git a/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php b/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php index b7490ad3f6965..b26de1b231cba 100644 --- a/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php +++ b/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php @@ -25,9 +25,12 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tests\Fixtures\InvokableExtendingCommandTestCommand; use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand; +use Symfony\Component\Console\Tester\ConsoleAssertionsTrait; class CommandTesterTest extends TestCase { + use ConsoleAssertionsTrait; + protected Command $command; protected CommandTester $tester; @@ -289,4 +292,99 @@ public function testAInvokableExtendedCommand() $tester->assertCommandIsSuccessful(); } + + public function testItExecutesTheTestedCommandWithTheSameConfigAsThePreviousApiByDefault() + { + $oldConfig = []; + $newConfig = []; + + $oldTesterCommand = self::createDisplayConfigurationCommand($oldConfig); + $newTesterCommand = self::createDisplayConfigurationCommand($newConfig); + + $oldTester = new CommandTester($oldTesterCommand); + $oldTester->execute([]); + + $newTester = new CommandTester($newTesterCommand); + $newTester->run(); + + // Sanity check + $this->assertNotEquals([], $oldConfig); + $this->assertEquals($oldConfig, $newConfig); + } + + public function testItCanConfigureTheExecutedCommand() + { + $config = []; + + $test = new CommandTester( + self::createDisplayConfigurationCommand($config), + ); + $test->run( + interactive: true, + decorated: false, + verbosity: OutputInterface::VERBOSITY_VERY_VERBOSE, + ); + + $expectedConfig = [ + 'interactive' => true, + 'decorated' => false, + 'verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE, + ]; + + $this->assertEquals($expectedConfig, $config); + } + + public function testItCanTestTheExecutionResult() + { + $command = new Command('foo'); + $command->setCode(function (InputInterface $input, OutputInterface $output) { + $output->writeln('bar'); + + return 0; + }); + + $result = (new CommandTester($command))->run(); + + $this->assertIsSuccessful($result); + $this->assertStatusCodeEquals(0, $result); + $this->assertSame("bar\n", $result->output); + } + + public function testItProvidesUserInputs() + { + $questions = [ + 'What\'s your name?', + 'How are you?', + 'Where do you come from?', + ]; + + $command = new Command('foo'); + $command->setHelperSet(new HelperSet([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); + $result = $tester->run(interactiveInputs: ['Bobby', 'Fine', 'France']); + + $this->assertIsSuccessful($result); + $this->assertEquals(implode('', $questions), self::normalizeLineReturns($result->output)); + } + + private static function createDisplayConfigurationCommand(array &$config): Command + { + $command = new Command('foo'); + $command->setCode(function (InputInterface $input, OutputInterface $output) use (&$config) { + $config['interactive'] = $input->isInteractive(); + $config['verbosity'] = $output->getVerbosity(); + $config['decorated'] = $output->isDecorated(); + + return 0; + }); + + return $command; + } } From 24ae82655d88628153c671473a754cb5ba48310d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 7 Dec 2024 17:02:31 +0100 Subject: [PATCH 3/7] refactor: extract code for the creation of the input --- .../Console/Tester/CommandTester.php | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index a2083617a1b0e..c6ef332f65f64 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; /** @@ -85,22 +86,11 @@ public function setVerbosity(int $level): void */ public function execute(array $input, array $options = []): int { - // set the command name automatically if the application requires - // this argument and no command name was passed - if (!isset($input['command']) - && (null !== $application = $this->command->getApplication()) - && $application->getDefinition()->hasArgument('command') - ) { - $input = array_merge(['command' => $this->command->getName()], $input); - } - - $this->input = new ArrayInput($input); - // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. - $this->input->setStream(self::createStream($this->inputs)); - - if (isset($options['interactive'])) { - $this->input->setInteractive($options['interactive'] ?? $this->interactive); - } + $this->input = $this->createInput( + self::addCommandArgumentIfNecessary($input), + $this->inputs, + $options['interactive'] ?? $this->interactive, + ); if (!isset($options['decorated'])) { $options['decorated'] = $this->decorated; @@ -127,24 +117,11 @@ public function run( ?int $verbosity = null, ): ExecutionResult { - // set the command name automatically if the application requires - // this argument and no command name was passed - if (!isset($input['command']) - && (null !== $application = $this->command->getApplication()) - && $application->getDefinition()->hasArgument('command') - ) { - $input = array_merge(['command' => $this->command->getName()], $input); - } - - $input = new ArrayInput($input); - // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. - $input->setStream(self::createStream($interactiveInputs)); - - $interactive ??= $this->interactive; - - if (null !== $interactive) { - $input->setInteractive($interactive); - } + $input = $this->createInput( + self::addCommandArgumentIfNecessary($input), + $interactiveInputs, + $interactive, + ); $options = [ 'decorated' => $decorated ?? $this->decorated, @@ -167,4 +144,38 @@ public function run( return ExecutionResult::fromExecution($input, $statusCode, $output); } + + private function addCommandArgumentIfNecessary(array $input): array + { + if (isset($input['command'])) { + return $input; + } + + $application = $this->command->getApplication(); + + return null !== $application && $application->getDefinition()->hasArgument('command') + ? array_merge( + ['command' => $this->command->getName()], + $input, + ) + : $input; + } + + private function createInput( + array $rawInput, + array $interactiveInputs = [], + ?bool $interactive = null, + ): InputInterface + { + $input = new ArrayInput($rawInput); + // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. + $input->setStream(self::createStream($interactiveInputs)); + + $interactive ??= $this->interactive; + if (null !== $interactive) { + $input->setInteractive($interactive); + } + + return $input; + } } From c87d6b025e545d562c2d4f9647d48bfd0434a646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 7 Dec 2024 17:55:43 +0100 Subject: [PATCH 4/7] combine outputs --- .../Console/Output/CombinedOutput.php | 123 ++++++++++++++++++ .../Component/Console/Output/TestOutput.php | 11 +- .../Console/Tester/CommandTester.php | 34 +++-- .../Console/Tester/ConsoleAssertionsTrait.php | 32 ++++- .../Console/Tester/ExecutionResult.php | 32 +---- .../Tests/Tester/CommandTesterTest.php | 10 +- 6 files changed, 193 insertions(+), 49 deletions(-) create mode 100644 src/Symfony/Component/Console/Output/CombinedOutput.php diff --git a/src/Symfony/Component/Console/Output/CombinedOutput.php b/src/Symfony/Component/Console/Output/CombinedOutput.php new file mode 100644 index 0000000000000..752fdb433536c --- /dev/null +++ b/src/Symfony/Component/Console/Output/CombinedOutput.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Symfony\Component\Console\Output; + +use DomainException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Output\OutputInterface; +use function count; +use function func_get_args; + +/** + * @internal + */ +final class CombinedOutput implements OutputInterface +{ + /** + * @var OutputInterface[] + */ + private array $outputs; + + public function __construct( + OutputInterface ...$outputs, + ) { + self::assertHasAtLeastOneOutput($outputs); + + $this->outputs = $outputs; + } + + private function doWrite(string $message, bool $newline): void + { + // TODO: Implement doWrite() method. + } + + /** + * @param OutputInterface[] $outputs + */ + private static function assertHasAtLeastOneOutput(array $outputs): void + { + if (count($outputs) < 1) { + throw new DomainException('Expected at least one output.'); + } + } + + public function write(iterable|string $messages, bool $newline = false, int $options = 0): void + { + foreach ($this->outputs as $output) { + $output->write(...func_get_args()); + } + } + + public function writeln(iterable|string $messages, int $options = 0): void + { + foreach ($this->outputs as $output) { + $output->writeln(...func_get_args()); + } + } + + public function setVerbosity(int $level): void + { + throw new DomainException('Should not called.'); + } + + public function getVerbosity(): int + { + throw new DomainException('Should not called.'); + } + + public function isSilent(): bool + { + throw new DomainException('Should not called.'); + } + + public function isQuiet(): bool + { + throw new DomainException('Should not called.'); + } + + public function isVerbose(): bool + { + throw new DomainException('Should not called.'); + } + + public function isVeryVerbose(): bool + { + throw new DomainException('Should not called.'); + } + + public function isDebug(): bool + { + throw new DomainException('Should not called.'); + } + + public function setDecorated(bool $decorated): void + { + throw new DomainException('Should not called.'); + } + + public function isDecorated(): bool + { + throw new DomainException('Should not called.'); + } + + public function setFormatter(OutputFormatterInterface $formatter): void + { + throw new DomainException('Should not called.'); + } + + public function getFormatter(): OutputFormatterInterface + { + throw new DomainException('Should not called.'); + } +} diff --git a/src/Symfony/Component/Console/Output/TestOutput.php b/src/Symfony/Component/Console/Output/TestOutput.php index 897ddc1b876a5..bcf6432717801 100644 --- a/src/Symfony/Component/Console/Output/TestOutput.php +++ b/src/Symfony/Component/Console/Output/TestOutput.php @@ -14,7 +14,6 @@ namespace Symfony\Component\Console\Output; use DomainException; -use Fidry\Console\Test\CombinedOutput; use RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -26,6 +25,9 @@ use function rewind; use function stream_get_contents; +/** + * @internal + */ final class TestOutput implements ConsoleOutputInterface { private OutputInterface $innerOutput; @@ -38,8 +40,8 @@ final class TestOutput implements ConsoleOutputInterface * @param OutputInterface::VERBOSITY_* $verbosity */ public function __construct( - private int $verbosity, private bool $decorated, + private int $verbosity, private OutputFormatterInterface $formatter, ) { $this->innerOutput = self::createOutput($this); @@ -106,6 +108,11 @@ public function getVerbosity(): int return $this->verbosity; } + public function isSilent(): bool + { + return self::VERBOSITY_SILENT <= $this->verbosity; + } + public function isQuiet(): bool { return self::VERBOSITY_QUIET === $this->verbosity; diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index c6ef332f65f64..c286aca5e14a4 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -12,9 +12,12 @@ namespace Symfony\Component\Console\Tester; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\TestOutput; /** * Eases the testing of console commands. @@ -38,6 +41,7 @@ public function __construct( private ?bool $interactive = null, private bool $decorated = false, private int $verbosity = ConsoleOutput::VERBOSITY_NORMAL, + private OutputFormatterInterface $outputFormatter = new OutputFormatter(), ) { $this->command = $command instanceof Command ? $command : new Command(null, $command); } @@ -69,6 +73,11 @@ public function setVerbosity(int $level): void $this->verbosity = $level; } + public function setOutputFormatter(OutputFormatterInterface $outputFormatter): void + { + $this->outputFormatter = $outputFormatter; + } + /** * Executes the command. * @@ -123,26 +132,15 @@ public function run( $interactive, ); - $options = [ - 'decorated' => $decorated ?? $this->decorated, - 'verbosity' => $verbosity ?? $this->verbosity, - ]; - - // This is purely for BC. A different way to create the output will be introduced later. - if (isset($this->output)) { - $previousOutput = $this->output; - $this->initOutput($options); - $output = $this->output; - $this->output = $previousOutput; - } else { - $this->initOutput($options); - $output = $this->output; - unset($this->output); - } + $testOutput = new TestOutput( + $decorated ?? $this->decorated, + $verbosity ?? $this->verbosity, + $this->outputFormatter, + ); - $statusCode = $this->command->run($input, $output); + $statusCode = $this->command->run($input, $testOutput); - return ExecutionResult::fromExecution($input, $statusCode, $output); + return ExecutionResult::fromExecution($input, $statusCode, $testOutput); } private function addCommandArgumentIfNecessary(array $input): array diff --git a/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php b/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php index 647b094297e86..f90632d3845f2 100644 --- a/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php +++ b/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php @@ -30,12 +30,40 @@ trait ConsoleAssertionsTrait { public function assertIsSuccessful(ExecutionResult $result, string $message = ''): void { - Assert::assertThat($result->statusCode, new CommandIsSuccessful(), $message); + $this->assertThat($result->statusCode, new CommandIsSuccessful(), $message); } public function assertStatusCodeEquals(int $expected, ExecutionResult $result, string $message = ''): void { - Assert::assertSame($expected, $result->statusCode, $message); + $this->assertSame($expected, $result->statusCode, $message); + } + + // TODO: here we need to pass the result first if we want to keep some arguments optional. + // Not wanting this is an argument for having the assertions in the ExecutionResult instead. + public function assertResultEquals( + ExecutionResult $result, + ?int $expectedStatusCode = null, + ?string $expectedOutput = null, + ?string $expectedErrorOutput = null, + ?string $expectedDisplay = null, + string $message = '', + ): void + { + $expected = [ + 'statusCode' => $expectedStatusCode, + 'output' => $expectedOutput, + 'errorOutput' => $expectedErrorOutput, + 'display' => $expectedDisplay, + ]; + + $actual = [ + 'statusCode' => $result->statusCode, + 'output' => $result->output, + 'errorOutput' => $result->errorOutput, + 'display' => $result->display, + ]; + + $this->assertEquals($expected, $actual, $message); } // TODO: move this to its own utility diff --git a/src/Symfony/Component/Console/Tester/ExecutionResult.php b/src/Symfony/Component/Console/Tester/ExecutionResult.php index 1e06bbcfa3b27..86fa0699049f1 100644 --- a/src/Symfony/Component/Console/Tester/ExecutionResult.php +++ b/src/Symfony/Component/Console/Tester/ExecutionResult.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Output\TestOutput; use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; use function array_filter; use function call_user_func; @@ -36,13 +37,15 @@ public static function fromExecution( InputInterface $input, int $statusCode, - StreamOutput $output, + TestOutput $output, ): self { return new self( $input->__toString(), $statusCode, - self::getStreamContents($output), + $output->getOutputContents(), + $output->getErrorOutputContents(), + $output->getDisplayContents(), ); } @@ -50,6 +53,8 @@ public function __construct( public string $input, public int $statusCode, public string $output, + public string $errorOutput, + public string $display, ) { } @@ -90,20 +95,6 @@ public function getErrorOutput(bool $normalize = false): string : $this->errorOutput; } - public function assertSuccessful(string $message = ''): self - { - Assert::assertThat($this->statusCode, new CommandIsSuccessful(), $message); - - return $this; - } - - public function assertStatusCode(int $expected): self - { - Assert::that($this->statusCode())->is($expected); - - return $this; - } - public function dump(): self { $summary = "CLI: {$this->input}, Status: {$this->statusCode()}"; @@ -132,13 +123,4 @@ private static function normalizeEndOfLines(string $value): string { return str_replace(PHP_EOL, "\n", $value); } - - private static function getStreamContents(StreamOutput $output): string - { - $stream = $output->getStream(); - - rewind($stream); - - return stream_get_contents($stream); - } } diff --git a/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php b/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php index b26de1b231cba..7769f94ea33db 100644 --- a/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php +++ b/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php @@ -26,6 +26,7 @@ use Symfony\Component\Console\Tests\Fixtures\InvokableExtendingCommandTestCommand; use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand; use Symfony\Component\Console\Tester\ConsoleAssertionsTrait; +use function implode; class CommandTesterTest extends TestCase { @@ -370,8 +371,13 @@ public function testItProvidesUserInputs() $tester = new CommandTester($command); $result = $tester->run(interactiveInputs: ['Bobby', 'Fine', 'France']); - $this->assertIsSuccessful($result); - $this->assertEquals(implode('', $questions), self::normalizeLineReturns($result->output)); + $this->assertResultEquals( + $result, + 0, + '', + implode('', $questions), + implode('', $questions), + ); } private static function createDisplayConfigurationCommand(array &$config): Command From 6468952e2b80238abeecc2455965b1e74390401e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 7 Dec 2024 18:16:40 +0100 Subject: [PATCH 5/7] add combined output --- .../Component/Console/Output/TestOutput.php | 8 +- .../Console/Tester/CommandTester.php | 43 ++++++++-- .../Console/Tester/ConsoleAssertionsTrait.php | 12 +-- .../Console/Tester/ExecutionResult.php | 82 +++++++++++++------ .../Console/Tester/OutputNormalizers.php | 36 ++++++++ .../Tests/Tester/CommandTesterTest.php | 2 +- 6 files changed, 137 insertions(+), 46 deletions(-) create mode 100644 src/Symfony/Component/Console/Tester/OutputNormalizers.php diff --git a/src/Symfony/Component/Console/Output/TestOutput.php b/src/Symfony/Component/Console/Output/TestOutput.php index bcf6432717801..40eec9d14d593 100644 --- a/src/Symfony/Component/Console/Output/TestOutput.php +++ b/src/Symfony/Component/Console/Output/TestOutput.php @@ -60,17 +60,17 @@ public function __construct( public function getOutputContents(): string { - return self::getStreamContents($this->innerOutput); + return $this->getStreamContents($this->innerOutput); } public function getErrorOutputContents(): string { - return self::getStreamContents($this->innerErrorOutput); + return $this->getStreamContents($this->innerErrorOutput); } public function getDisplayContents(): string { - return self::getStreamContents($this->displayOutput); + return $this->getStreamContents($this->displayOutput); } public function getErrorOutput(): OutputInterface @@ -169,7 +169,7 @@ private static function createOutput(OutputInterface $config): StreamOutput ); } - private static function getStreamContents(StreamOutput $output): string + private function getStreamContents(StreamOutput $output): string { $stream = $output->getStream(); diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index c286aca5e14a4..c37dd4ba8041a 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tester; +use Closure; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; @@ -32,18 +33,29 @@ class CommandTester private Command $command; + /** + * @var array + */ + private array $outputNormalizers; + /** * @param OutputInterface::VERBOSITY_* $verbosity + * @param array $outputNormalizers */ public function __construct( - callable|Command $command, + callable|Command $command, // TODO: to discuss if we want it in the constructor with the setters or only in the ::run() method - private ?bool $interactive = null, - private bool $decorated = false, - private int $verbosity = ConsoleOutput::VERBOSITY_NORMAL, + private ?bool $interactive = null, + private bool $decorated = false, + private int $verbosity = ConsoleOutput::VERBOSITY_NORMAL, private OutputFormatterInterface $outputFormatter = new OutputFormatter(), + array $outputNormalizers = null, ) { $this->command = $command instanceof Command ? $command : new Command(null, $command); + $this->outputNormalizers = $outputNormalizers ?? [ + OutputNormalizers::normalizeEndOfLines(...), + OutputNormalizers::removeTrailingSpaces(...), + ]; } // TODO: to discuss if we want fluent setters @@ -78,6 +90,22 @@ public function setOutputFormatter(OutputFormatterInterface $outputFormatter): v $this->outputFormatter = $outputFormatter; } + /** + * @param array $outputNormalizers + */ + public function setOutputNormalizers(array $outputNormalizers): void + { + $this->outputNormalizers = $outputNormalizers; + } + + /** + * @param Closure(string): string $outputNormalizer + */ + public function addOutputNormalizer(Closure $outputNormalizer): void + { + $this->outputNormalizers[] = $outputNormalizer; + } + /** * Executes the command. * @@ -140,7 +168,12 @@ public function run( $statusCode = $this->command->run($input, $testOutput); - return ExecutionResult::fromExecution($input, $statusCode, $testOutput); + return ExecutionResult::fromExecution( + $input, + $statusCode, + $testOutput, + $this->outputNormalizers, + ); } private function addCommandArgumentIfNecessary(array $input): array diff --git a/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php b/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php index f90632d3845f2..b562e2afd8116 100644 --- a/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php +++ b/src/Symfony/Component/Console/Tester/ConsoleAssertionsTrait.php @@ -58,17 +58,11 @@ public function assertResultEquals( $actual = [ 'statusCode' => $result->statusCode, - 'output' => $result->output, - 'errorOutput' => $result->errorOutput, - 'display' => $result->display, + 'output' => $result->getOutput(), + 'errorOutput' => $result->getErrorOutput(), + 'display' => $result->getDisplay(), ]; $this->assertEquals($expected, $actual, $message); } - - // TODO: move this to its own utility - public static function normalizeLineReturns(string $output): string - { - return str_replace(\PHP_EOL, "\n", $output); - } } diff --git a/src/Symfony/Component/Console/Tester/ExecutionResult.php b/src/Symfony/Component/Console/Tester/ExecutionResult.php index 86fa0699049f1..da8c003effd85 100644 --- a/src/Symfony/Component/Console/Tester/ExecutionResult.php +++ b/src/Symfony/Component/Console/Tester/ExecutionResult.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tester; +use Closure; use PHPUnit\Framework\Assert; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; @@ -32,67 +33,84 @@ /** * @author Théo FIDRY */ -final readonly class ExecutionResult +final class ExecutionResult { + // This is purely for memoizing purposes + private array $results = []; + + /** + * @param array $normalizers + */ public static function fromExecution( InputInterface $input, int $statusCode, TestOutput $output, + array $normalizers, ): self { return new self( $input->__toString(), $statusCode, - $output->getOutputContents(), - $output->getErrorOutputContents(), - $output->getDisplayContents(), + $output, + $normalizers, ); } + /** + * @param array $normalizers + */ public function __construct( - public string $input, - public int $statusCode, - public string $output, - public string $errorOutput, - public string $display, + public readonly string $input, + public readonly int $statusCode, + private readonly TestOutput $output, + private readonly array $normalizers, ) { } /** * Gets the display returned by the execution of the command or application. The display combines what was * written on both the output and error output. - * - * @param bool $normalize Whether to normalize end of lines to \n or not. */ - public function getDisplay(bool $normalize = false): string + public function getDisplay(bool $normalize = true): string { - return $normalize - ? self::normalizeEndOfLines($this->display) - : $this->display; + if (!isset($this->results['display'][$normalize])) { + $this->results['display'][$normalize] = $this->normalize( + $this->output->getDisplayContents($normalize), + $normalize, + ); + } + + return $this->results['display'][$normalize]; } /** * Gets the output written to the output by the command or application. - * - * @param bool $normalize Whether to normalize end of lines to \n or not. */ public function getOutput(bool $normalize = false): string { - return $normalize - ? self::normalizeEndOfLines($this->output) - : $this->output; + if (!isset($this->results['output'][$normalize])) { + $this->results['output'][$normalize] = $this->normalize( + $this->output->getOutputContents(), + $normalize, + ); + } + + return $this->results['output'][$normalize]; } /** * Gets the output written to the error output by the command or application. - * - * @param bool $normalize Whether to normalize end of lines to \n or not. */ public function getErrorOutput(bool $normalize = false): string { - return $normalize - ? self::normalizeEndOfLines($this->errorOutput) - : $this->errorOutput; + if (!isset($this->results['errorOutput'][$normalize])) { + $this->results['errorOutput'][$normalize] = $this->normalize( + $this->output->getErrorOutputContents(), + $normalize, + ); + } + + return $this->results['errorOutput'][$normalize]; } public function dump(): self @@ -119,8 +137,18 @@ public function dd(): void exit(1); } - private static function normalizeEndOfLines(string $value): string + private function normalize(string $value, bool $normalize): string { - return str_replace(PHP_EOL, "\n", $value); + if (!$normalize) { + return $value; + } + + $normalizedValue = $value; + + foreach ($this->normalizers as $normalizer) { + $normalizedValue = $normalizer($normalizedValue); + } + + return $normalizedValue; } } diff --git a/src/Symfony/Component/Console/Tester/OutputNormalizers.php b/src/Symfony/Component/Console/Tester/OutputNormalizers.php new file mode 100644 index 0000000000000..f9b957a269b21 --- /dev/null +++ b/src/Symfony/Component/Console/Tester/OutputNormalizers.php @@ -0,0 +1,36 @@ +assertIsSuccessful($result); $this->assertStatusCodeEquals(0, $result); - $this->assertSame("bar\n", $result->output); + $this->assertSame("bar\n", $result->getDisplay()); } public function testItProvidesUserInputs() From 78ae0918bd3ba98fc61223f808a95cbbbd64b702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Thu, 21 Aug 2025 20:52:17 +0200 Subject: [PATCH 6/7] fix cs --- src/Symfony/Component/Console/Tester/ExecutionResult.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Console/Tester/ExecutionResult.php b/src/Symfony/Component/Console/Tester/ExecutionResult.php index da8c003effd85..7fafffec300a3 100644 --- a/src/Symfony/Component/Console/Tester/ExecutionResult.php +++ b/src/Symfony/Component/Console/Tester/ExecutionResult.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Console\Tester; use Closure; -use PHPUnit\Framework\Assert; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; From a455715cee5a2d566d67fcd2a41aa3dfb32effff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Thu, 21 Aug 2025 21:13:58 +0200 Subject: [PATCH 7/7] minor cleanups --- .../Console/Output/CombinedOutput.php | 26 ++++------ .../Component/Console/Output/TestOutput.php | 7 +-- .../Console/Tester/CommandTester.php | 49 +++---------------- .../Console/Tester/OutputNormalizers.php | 4 +- .../Console/Tests/ApplicationTest.php | 2 +- 5 files changed, 24 insertions(+), 64 deletions(-) diff --git a/src/Symfony/Component/Console/Output/CombinedOutput.php b/src/Symfony/Component/Console/Output/CombinedOutput.php index 752fdb433536c..f1fe20d8323ed 100644 --- a/src/Symfony/Component/Console/Output/CombinedOutput.php +++ b/src/Symfony/Component/Console/Output/CombinedOutput.php @@ -15,7 +15,6 @@ use DomainException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; -use Symfony\Component\Console\Output\OutputInterface; use function count; use function func_get_args; @@ -37,21 +36,6 @@ public function __construct( $this->outputs = $outputs; } - private function doWrite(string $message, bool $newline): void - { - // TODO: Implement doWrite() method. - } - - /** - * @param OutputInterface[] $outputs - */ - private static function assertHasAtLeastOneOutput(array $outputs): void - { - if (count($outputs) < 1) { - throw new DomainException('Expected at least one output.'); - } - } - public function write(iterable|string $messages, bool $newline = false, int $options = 0): void { foreach ($this->outputs as $output) { @@ -120,4 +104,14 @@ public function getFormatter(): OutputFormatterInterface { throw new DomainException('Should not called.'); } + + /** + * @param OutputInterface[] $outputs + */ + private static function assertHasAtLeastOneOutput(array $outputs): void + { + if (count($outputs) < 1) { + throw new DomainException('Expected at least one output.'); + } + } } diff --git a/src/Symfony/Component/Console/Output/TestOutput.php b/src/Symfony/Component/Console/Output/TestOutput.php index 40eec9d14d593..1ace8ecd17490 100644 --- a/src/Symfony/Component/Console/Output/TestOutput.php +++ b/src/Symfony/Component/Console/Output/TestOutput.php @@ -16,10 +16,6 @@ use DomainException; use RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; -use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Console\Output\ConsoleSectionOutput; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Output\StreamOutput; use function fopen; use function func_get_args; use function rewind; @@ -27,6 +23,8 @@ /** * @internal + * + * @author Théo FIDRY */ final class TestOutput implements ConsoleOutputInterface { @@ -40,7 +38,6 @@ final class TestOutput implements ConsoleOutputInterface * @param OutputInterface::VERBOSITY_* $verbosity */ public function __construct( - private bool $decorated, private int $verbosity, private OutputFormatterInterface $formatter, ) { diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index c37dd4ba8041a..77083fd5dd2f5 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Console\Tester; -use Closure; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; @@ -31,31 +30,20 @@ class CommandTester { use TesterTrait; - private Command $command; - - /** - * @var array - */ - private array $outputNormalizers; + private OutputFormatterInterface $outputFormatter; /** * @param OutputInterface::VERBOSITY_* $verbosity - * @param array $outputNormalizers */ public function __construct( - callable|Command $command, + private Command $command, // TODO: to discuss if we want it in the constructor with the setters or only in the ::run() method - private ?bool $interactive = null, - private bool $decorated = false, - private int $verbosity = ConsoleOutput::VERBOSITY_NORMAL, - private OutputFormatterInterface $outputFormatter = new OutputFormatter(), - array $outputNormalizers = null, + private ?bool $interactive = null, + private bool $decorated = false, + private int $verbosity = ConsoleOutput::VERBOSITY_NORMAL, + OutputFormatterInterface $outputFormatter = null, ) { - $this->command = $command instanceof Command ? $command : new Command(null, $command); - $this->outputNormalizers = $outputNormalizers ?? [ - OutputNormalizers::normalizeEndOfLines(...), - OutputNormalizers::removeTrailingSpaces(...), - ]; + $this->outputFormatter = $outputFormatter ?? new OutputFormatter(); } // TODO: to discuss if we want fluent setters @@ -90,22 +78,6 @@ public function setOutputFormatter(OutputFormatterInterface $outputFormatter): v $this->outputFormatter = $outputFormatter; } - /** - * @param array $outputNormalizers - */ - public function setOutputNormalizers(array $outputNormalizers): void - { - $this->outputNormalizers = $outputNormalizers; - } - - /** - * @param Closure(string): string $outputNormalizer - */ - public function addOutputNormalizer(Closure $outputNormalizer): void - { - $this->outputNormalizers[] = $outputNormalizer; - } - /** * Executes the command. * @@ -168,12 +140,7 @@ public function run( $statusCode = $this->command->run($input, $testOutput); - return ExecutionResult::fromExecution( - $input, - $statusCode, - $testOutput, - $this->outputNormalizers, - ); + return ExecutionResult::fromExecution($input, $statusCode, $testOutput); } private function addCommandArgumentIfNecessary(array $input): array diff --git a/src/Symfony/Component/Console/Tester/OutputNormalizers.php b/src/Symfony/Component/Console/Tester/OutputNormalizers.php index f9b957a269b21..228de32212511 100644 --- a/src/Symfony/Component/Console/Tester/OutputNormalizers.php +++ b/src/Symfony/Component/Console/Tester/OutputNormalizers.php @@ -12,7 +12,9 @@ use const PHP_EOL; /** - * Collection of output normalizers + * Collection of output normalizers. + * + * @author Théo FIDRY */ final class OutputNormalizers { diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 77ad461b41a99..684ebeb91ef15 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -2361,7 +2361,7 @@ public function testAlarmSubscriber() $dispatcher->addSubscriber($subscriber1); $dispatcher->addSubscriber($subscriber2); - $application = $this->createSignalableaApplication($command, $dispatcher); + $application = $this->createSignalableApplication($command, $dispatcher); $this->assertSame(1, $application->run(new ArrayInput(['signal']))); $this->assertTrue($subscriber1->signaled);