/
githubmirror
/
symfony
Обзор
Документация
Войти
/
githubmirror
/
symfony
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
8.2
src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php
444 строки
16 KB
Fabien Potencier
Merge branch '8.0' into 8.1
10 июл 2026, 12:33
10 июл 2026, 12:33
963a249
Код
Авторство
О чём код?
<?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tests\Style; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\TreeHelper; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\Input; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Tester\CommandTester; class SymfonyStyleTest extends TestCase { protected Command $command; protected CommandTester $tester; private string|false $colSize; protected function setUp(): void { $this->colSize = getenv('COLUMNS'); putenv('COLUMNS=121'); $this->command = new Command('sfstyle'); $this->tester = new CommandTester($this->command); } protected function tearDown(): void { putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } #[DataProvider('inputCommandToOutputFilesProvider')] public function testOutputs($inputCommandFilepath, $outputFilepath) { $code = require $inputCommandFilepath; $this->command->setCode($code); $this->tester->execute([], ['interactive' => false, 'decorated' => false]); $this->assertStringEqualsFile($outputFilepath, $this->tester->getDisplay(true)); } #[DataProvider('inputInteractiveCommandToOutputFilesProvider')] public function testInteractiveOutputs($inputCommandFilepath, $outputFilepath) { $code = require $inputCommandFilepath; $this->command->setCode($code); $this->tester->execute([], ['interactive' => true, 'decorated' => false]); $this->assertStringEqualsFile($outputFilepath, $this->tester->getDisplay(true)); } public static function inputInteractiveCommandToOutputFilesProvider() { $baseDir = __DIR__.'/../Fixtures/Style/SymfonyStyle'; return array_map(null, glob($baseDir.'/command/interactive_command_*.php'), glob($baseDir.'/output/interactive_output_*.txt')); } public static function inputCommandToOutputFilesProvider() { $baseDir = __DIR__.'/../Fixtures/Style/SymfonyStyle'; return array_map(null, glob($baseDir.'/command/command_*.php'), glob($baseDir.'/output/output_*.txt')); } public function testOutputProgressIterate() { $code = require __DIR__.'/../Fixtures/Style/SymfonyStyle/progress/command_progress_iterate.php'; if ('\\' === \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) { $outputFilepath = __DIR__.'/../Fixtures/Style/SymfonyStyle/progress/output_progress_iterate_no_shade.txt'; } else { $outputFilepath = __DIR__.'/../Fixtures/Style/SymfonyStyle/progress/output_progress_iterate_shade.txt'; } $this->command->setCode($code); $this->tester->execute([], ['interactive' => false, 'decorated' => false]); $this->assertStringEqualsFile($outputFilepath, $this->tester->getDisplay(true)); } public function testProgressIterateWithCustomFormat() { $code = static function (InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); foreach ($io->progressIterate(range(1, 3), null, ' %current%/%max% [%bar%] %memory:6s%') as $step) { // noop } return Command::SUCCESS; }; $this->command->setCode($code); $this->tester->execute([], ['interactive' => false, 'decorated' => false]); $this->assertMatchesRegularExpression('/\d+(\.\d+)? (GiB|MiB|KiB|B)/', $this->tester->getDisplay(true)); } public function testProgressStartWithCustomFormat() { $code = static function (InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $io->progressStart(3, ' %current%/%max% [%bar%] %memory:6s%'); $io->progressAdvance(3); $io->progressFinish(); return Command::SUCCESS; }; $this->command->setCode($code); $this->tester->execute([], ['interactive' => false, 'decorated' => false]); $this->assertMatchesRegularExpression('/\d+(\.\d+)? (GiB|MiB|KiB|B)/', $this->tester->getDisplay(true)); } public function testBlockWithWindowsLineEndings() { $code = static function (InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $io->block("First line.\r\nSecond line.", 'INFO', 'fg=white;bg=blue', ' ', true); return Command::SUCCESS; }; $this->command->setCode($code); $this->tester->execute([], ['interactive' => false, 'decorated' => false]); $display = $this->tester->getDisplay(true); $this->assertStringNotContainsString("\r", $display); $this->assertStringContainsString('First line.', $display); $this->assertStringContainsString('Second line.', $display); } public function testStyledBlockUsesEchPaddingWhenDecorated() { $output = new StreamOutput(fopen('php://memory', 'w', false), decorated: true); $io = new SymfonyStyle(new ArrayInput([]), $output); $io->block('msg', null, 'fg=white;bg=blue', ' ', false); rewind($output->getStream()); $display = stream_get_contents($output->getStream()); $this->assertMatchesRegularExpression("/\e\[\d+X\e\[\d+C/", $display); } public function testStyledBlockKeepsSpacePaddingWhenNotDecorated() { $output = new StreamOutput(fopen('php://memory', 'w', false), decorated: false); $io = new SymfonyStyle(new ArrayInput([]), $output); $io->block('msg', null, 'fg=white;bg=blue', ' ', false); rewind($output->getStream()); $display = stream_get_contents($output->getStream()); $this->assertStringNotContainsString("\e[", $display); } public function testUnstyledBlockKeepsSpacePaddingEvenWhenDecorated() { $output = new StreamOutput(fopen('php://memory', 'w', false), decorated: true); $io = new SymfonyStyle(new ArrayInput([]), $output); $io->block('msg', null, null, ' ', false); rewind($output->getStream()); $display = stream_get_contents($output->getStream()); $this->assertStringNotContainsString("\e[", $display); } public function testGetErrorStyle() { $input = $this->createStub(InputInterface::class); $errorOutput = $this->createMock(OutputInterface::class); $errorOutput ->method('getFormatter') ->willReturn(new OutputFormatter()); $errorOutput ->expects($this->once()) ->method('write'); $output = $this->createMock(ConsoleOutputInterface::class); $output ->method('getFormatter') ->willReturn(new OutputFormatter()); $output ->expects($this->once()) ->method('getErrorOutput') ->willReturn($errorOutput); $io = new SymfonyStyle($input, $output); $io->getErrorStyle()->write(''); } public function testCreateTableWithConsoleOutput() { $input = $this->createStub(InputInterface::class); $output = $this->createMock(ConsoleOutputInterface::class); $output ->method('getFormatter') ->willReturn(new OutputFormatter()); $output ->expects($this->once()) ->method('section') ->willReturn($this->createStub(ConsoleSectionOutput::class)); $style = new SymfonyStyle($input, $output); $style->createTable(); } public function testCreateTableWithoutConsoleOutput() { $input = $this->createStub(InputInterface::class); $output = $this->createStub(OutputInterface::class); $output ->method('getFormatter') ->willReturn(new OutputFormatter()); $style = new SymfonyStyle($input, $output); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Output should be an instance of "Symfony\Component\Console\Output\ConsoleSectionOutput"'); $style->createTable()->appendRow(['row']); } public function testCreateTree() { $output = $this->createStub(OutputInterface::class); $output ->method('getFormatter') ->willReturn(new OutputFormatter()); $style = new SymfonyStyle($this->createStub(InputInterface::class), $output); $tree = $style->createTree([]); $this->assertInstanceOf(TreeHelper::class, $tree); } public function testTree() { $input = $this->createStub(InputInterface::class); $output = new BufferedOutput(); $style = new SymfonyStyle($input, $output); $tree = $style->createTree(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C'], 'root'); $tree->render(); $this->assertSame(<<<TREE root ├── A ├── B │ ├── B1 │ │ ├── B11 │ │ └── B12 │ └── B2 └── C TREE, self::normalizeLineBreaks(trim($output->fetch())) ); } public function testCreateTreeWithArray() { $input = $this->createStub(InputInterface::class); $output = new BufferedOutput(); $style = new SymfonyStyle($input, $output); $tree = $style->createTree(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C'], 'root'); $tree->render(); $this->assertSame($tree = <<<TREE root ├── A ├── B │ ├── B1 │ │ ├── B11 │ │ └── B12 │ └── B2 └── C TREE, self::normalizeLineBreaks(trim($output->fetch())) ); } public function testCreateTreeWithIterable() { $input = $this->createStub(InputInterface::class); $output = new BufferedOutput(); $style = new SymfonyStyle($input, $output); $tree = $style->createTree(new \ArrayIterator(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C']), 'root'); $tree->render(); $this->assertSame(<<<TREE root ├── A ├── B │ ├── B1 │ │ ├── B11 │ │ └── B12 │ └── B2 └── C TREE, self::normalizeLineBreaks(trim($output->fetch())) ); } public function testCreateTreeWithConsoleOutput() { $input = $this->createStub(InputInterface::class); $output = $this->createMock(ConsoleOutputInterface::class); $output ->method('getFormatter') ->willReturn(new OutputFormatter()); $output ->expects($this->once()) ->method('section') ->willReturn($this->createStub(ConsoleSectionOutput::class)); $style = new SymfonyStyle($input, $output); $style->createTree([]); } public function testGetErrorStyleUsesTheCurrentOutputIfNoErrorOutputIsAvailable() { $output = $this->createStub(OutputInterface::class); $output ->method('getFormatter') ->willReturn(new OutputFormatter()); $style = new SymfonyStyle($this->createStub(InputInterface::class), $output); $this->assertInstanceOf(SymfonyStyle::class, $style->getErrorStyle()); } public function testMemoryConsumption() { $io = new SymfonyStyle(new ArrayInput([]), new NullOutput()); $str = 'teststr'; $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); $start = memory_get_usage(); for ($i = 0; $i < 100; ++$i) { $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); } $this->assertSame(0, memory_get_usage() - $start); } public function testAskAndClearExpectFullSectionCleared() { $answer = 'Answer'; $inputStream = fopen('php://memory', 'r+'); fwrite($inputStream, $answer.\PHP_EOL); rewind($inputStream); $input = $this->createStub(Input::class); $sections = []; $output = new ConsoleSectionOutput(fopen('php://memory', 'r+', false), $sections, StreamOutput::VERBOSITY_NORMAL, true, new OutputFormatter()); $input ->method('isInteractive') ->willReturn(true); $input ->method('getStream') ->willReturn($inputStream); $style = new SymfonyStyle($input, $output); $style->writeln('start'); $style->write('foo'); $style->writeln(' and bar'); $givenAnswer = $style->ask('Dummy question?'); $style->write('foo2'.\PHP_EOL); $output->write('bar2'); $output->clear(); rewind($output->getStream()); $this->assertEquals($answer, $givenAnswer); $this->assertEquals(escapeshellcmd( 'start'.\PHP_EOL. // write start 'foo'.\PHP_EOL. // write foo "\x1b[1A\x1b[0Jfoo and bar".\PHP_EOL. // complete line \PHP_EOL." \033[32mDummy question?\033[39m:".\PHP_EOL.' > '.\PHP_EOL. // question 'foo2'.\PHP_EOL. // write foo2 'bar2'.\PHP_EOL. // write bar "\033[8A\033[0J"), // clear 8 lines (7 output lines and one from the answer input return) escapeshellcmd(stream_get_contents($output->getStream())) ); } public function testAskQuestionInSectionMatchesNonSectionOutput() { // asking a question inside a ConsoleSectionOutput must not leave an extra // blank line below the answer: the output must match a regular StreamOutput. $render = function (StreamOutput $output): string { $inputStream = fopen('php://memory', 'r+'); fwrite($inputStream, 'Answer'.\PHP_EOL); rewind($inputStream); $input = $this->createStub(Input::class); $input->method('isInteractive')->willReturn(true); $input->method('getStream')->willReturn($inputStream); $style = new SymfonyStyle($input, $output); $style->writeln('before'); $style->ask('Dummy question?'); $style->writeln('after'); rewind($output->getStream()); return stream_get_contents($output->getStream()); }; $sections = []; $sectionOutput = $render(new ConsoleSectionOutput(fopen('php://memory', 'r+', false), $sections, StreamOutput::VERBOSITY_NORMAL, true, new OutputFormatter())); $streamOutput = $render(new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, true, new OutputFormatter())); $this->assertSame($streamOutput, $sectionOutput); $this->assertStringNotContainsString(' > '.\PHP_EOL.\PHP_EOL, $sectionOutput); } private static function normalizeLineBreaks($text) { return str_replace(\PHP_EOL, "\n", $text); } }