From 792b5a34e958d9fc7794d3e54f612dd56b23b08a Mon Sep 17 00:00:00 2001 From: Dhemy Date: Fri, 29 Mar 2024 01:22:12 +0100 Subject: [PATCH] [output] add custom output style (#25) * chore: make ci faster * chore: mark internal code * chore: replace helpers with traits * chore: add Style namespace * chore: add output helper * chore: remove un-used constructor * chore: set output and input * chore: organize namespaces * wip * wip * chore: replace method wrappers with extend * chore: convert output into a symfony style * chore: allow mocking output - for old symfony versions that don't type hint the `getFormatter()` method * feat: add custom style * chore(docs): update output style --- .github/workflows/ci.yaml | 79 ++----- docs/README.md | 17 +- docs/output-style.png | Bin 0 -> 48152 bytes src/Command.php | 195 ++-------------- src/IO/Helper/InputTrait.php | 55 +++++ src/IO/Helper/OutputTrait.php | 141 ++++++++++++ src/IO/Output.php | 98 ++++++++ src/LegacySymfonyStyle.php | 21 -- src/Parser.php | 3 + src/StyleFactory.php | 19 -- tests/CommandTest.php | 411 +--------------------------------- tests/Doubles/MyCommand.php | 36 --- 12 files changed, 354 insertions(+), 721 deletions(-) create mode 100644 docs/output-style.png create mode 100644 src/IO/Helper/InputTrait.php create mode 100644 src/IO/Helper/OutputTrait.php create mode 100644 src/IO/Output.php delete mode 100644 src/LegacySymfonyStyle.php delete mode 100644 src/StyleFactory.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7d9d61c..7eaf176 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,73 +1,36 @@ -name: "Continuous Integration" - -on: - pull_request: - push: - +name: CI +on: [ push, pull_request ] jobs: - phpunit: - name: "Unit Tests" - runs-on: ubuntu-latest + ci: + name: "CI" + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ ubuntu-latest, windows-latest, macos-latest ] + php-version: [ 8.3 ] steps: - - name: "Checkout" - uses: actions/checkout@v3 - - - name: "Install PHP" - uses: shivammathur/setup-php@v2 - with: - coverage: "pcov" - php-version: "8.1" - ini-values: memory_limit=-1 - extensions: sodium, fileinfo, redis - - - name: "Install dependencies" - uses: ramsey/composer-install@v2 - with: - dependency-versions: "locked" - - - name: "Run PHPUnit" - run: composer test - psalm: - name: "Static Analysis" - runs-on: ubuntu-latest - steps: + # --------- Setup steps --------- - name: "Checkout" uses: actions/checkout@v3 - - name: "Install PHP" + - name: "Setup PHP" uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + coverage: "pcov" + php-version: "${{ matrix.php-version }}" ini-values: memory_limit=-1 - extensions: sodium, fileinfo, redis + extensions: pcov, xdebug - name: "Install dependencies" uses: ramsey/composer-install@v2 - with: - dependency-versions: "locked" - - - name: "Run Psalm" - run: composer psalm - - phpcs: - name: "Code Style" - runs-on: ubuntu-latest - steps: - - name: "Checkout" - uses: actions/checkout@v3 - - name: "Install PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: "8.1" - ini-values: memory_limit=-1 - extensions: sodium, fileinfo, redis + # --------- Run steps --------- + - name: "Unit Tests" + run: "composer test" - - name: "Install dependencies" - uses: ramsey/composer-install@v2 - with: - dependency-versions: "locked" + - name: "Coding Style" + run: "composer cs-fix" - - name: "Run PHPCS" - run: composer cs-check + - name: "Static code analysis" + run: "composer psalm" diff --git a/docs/README.md b/docs/README.md index dc523b6..170e0fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -157,14 +157,15 @@ if ($this->hasOption('queue')) { ## Writing output -All helper methods from the Symfony `SymfonyStyle` class are available in the command class. +The package provides a handy Console style that you can use to write output to the console: +Output style ```php -$this->title('Lorem ipsum dolor sit amet'); -$this->section('Adding a new user'); -$this->text('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); -//And so on... +$this->output->comment('This is a comment'); +$this->output->success('This is a success message'); +$this->output->error('This is an error message'); +$this->output->warning('This is an info message'); +$this->output->note('This is an info message'); +$this->output->info('This is an info message'); +$this->output->caution('This is a line message'); ``` - -You can find the full list of available methods in Symfony's -documentation: [Helper methods](https://symfony.com/doc/current/console/style.html#helper-methods). diff --git a/docs/output-style.png b/docs/output-style.png new file mode 100644 index 0000000000000000000000000000000000000000..702a2c644d7160d17554dba1e760fef9520e97d2 GIT binary patch literal 48152 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 z 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 literal 0 HcmV?d00001 diff --git a/src/Command.php b/src/Command.php index fa0fcc2..eb8fa98 100644 --- a/src/Command.php +++ b/src/Command.php @@ -4,12 +4,12 @@ namespace Symblaze\Console; +use Symblaze\Console\IO\Helper\InputTrait; +use Symblaze\Console\IO\Helper\OutputTrait; +use Symblaze\Console\IO\Output; use Symfony\Component\Console\Command\Command as SymfonyCommand; -use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; /** * @psalm-api - This file is part of the symblaze/console package. @@ -18,16 +18,11 @@ */ abstract class Command extends SymfonyCommand { - protected InputInterface $input; - protected SymfonyStyle $output; + use InputTrait; + use OutputTrait; - private const VERBOSITY_MAP = [ - 'v' => OutputInterface::VERBOSITY_VERBOSE, - 'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE, - 'vvv' => OutputInterface::VERBOSITY_DEBUG, - 'quiet' => OutputInterface::VERBOSITY_QUIET, - 'normal' => OutputInterface::VERBOSITY_NORMAL, - ]; + protected InputInterface $input; + protected Output $output; protected function configure(): void { @@ -40,185 +35,33 @@ protected function configure(): void public function run(InputInterface $input, OutputInterface $output): int { - $this->input = $input; - $this->output = StyleFactory::create($input, $output); + $this->setInput($input); + $this->setOutput(new Output($input, $output)); return parent::run($input, $output); } - /** - * Determine if the given argument is present. - */ - protected function hasArgument($name): bool - { - return $this->input->hasArgument($name) && ! is_null($this->argument($name)); - } - - /** - * Determine if the given option is present. - */ - protected function hasOption($name): bool - { - return $this->input->hasOption($name) && ! is_null($this->option($name)); - } - - protected function option(string $key): bool|array|string|null - { - return $this->input->getOption($key); - } - - protected function options(): array - { - return $this->input->getOptions(); - } - - /** - * Get the value of a command argument. - */ - protected function argument(string $key): bool|array|string|null - { - return $this->input->getArgument($key); - } - - /** - * Get all the arguments passed to the command. - */ - protected function arguments(): array - { - return $this->input->getArguments(); - } - - /** - * Writes a message to the output and adds a newline at the end. - */ - protected function line(string $message, ?string $style = null, string|int $verbosity = 'normal'): void - { - $styled = $style ? "<$style>$message" : $message; - - $this->output->writeln($styled, $this->parseVerbosity($verbosity)); - } - - protected function info(string|array $message): void - { - $this->output->info($message); - } - - protected function comment(string|array $message): void - { - $this->output->comment($message); - } - - protected function question(string|array $message): void - { - $this->line($message, 'question'); - } - - protected function error(string|array $message): void - { - $this->output->error($message); - } - - protected function warning(string|array $message): void - { - $this->output->warning($message); - } - - protected function success(string|array $message): void - { - $this->output->success($message); - } - - protected function title(string $message): void + public function getInput(): InputInterface { - $this->output->title($message); + return $this->input; } - protected function section(string $message): void + public function setInput(InputInterface $input): static { - $this->output->section($message); - } - - protected function text(string|array $message): void - { - $this->output->text($message); - } - - protected function listing(array $elements): void - { - $this->output->listing($elements); - } - - protected function table(array $headers, array $rows): void - { - $this->output->table($headers, $rows); - } - - protected function horizontalTable(array $headers, array $rows): void - { - $this->output->horizontalTable($headers, $rows); - } - - protected function definitionList(string|array|TableSeparator ...$list): void - { - $this->output->definitionList(...$list); - } - - protected function note(string|array $message): void - { - $this->output->note($message); - } - - protected function caution(string|array $message): void - { - $this->output->caution($message); - } - - protected function ask(string $question, ?string $default = null, ?callable $validator = null): mixed - { - return $this->output->ask($question, $default, $validator); - } - - protected function askHidden(string $question, ?callable $validator = null): mixed - { - return $this->output->askHidden($question, $validator); - } - - protected function confirm(string $question, bool $default = true): bool - { - return $this->output->confirm($question, $default); - } - - protected function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed - { - return $this->output->choice($question, $choices, $default, $multiSelect); - } - - protected function progressStart(int $max = 0): void - { - $this->output->progressStart($max); - } - - protected function progressAdvance(int $step = 1): void - { - $this->output->progressAdvance($step); - } + $this->input = $input; - protected function progressFinish(): void - { - $this->output->progressFinish(); + return $this; } - protected function createProgressBar(int $max = 0): ProgressBar + public function getOutput(): Output { - return $this->output->createProgressBar($max); + return $this->output; } - private function parseVerbosity(int|string $level): int + public function setOutput(Output $output): static { - if (is_int($level)) { - return $level; - } + $this->output = $output; - return self::VERBOSITY_MAP[$level] ?? OutputInterface::VERBOSITY_NORMAL; + return $this; } } diff --git a/src/IO/Helper/InputTrait.php b/src/IO/Helper/InputTrait.php new file mode 100644 index 0000000..3bc9d08 --- /dev/null +++ b/src/IO/Helper/InputTrait.php @@ -0,0 +1,55 @@ +input->getArgument($key); + } + + protected function option(string $key): bool|array|string|null + { + return $this->input->getOption($key); + } + + protected function options(): array + { + return $this->input->getOptions(); + } + + /** + * Determine if the given argument is present. + */ + protected function hasArgument($name): bool + { + return $this->input->hasArgument($name) && ! is_null($this->argument($name)); + } + + /** + * Get all the arguments passed to the command. + */ + protected function arguments(): array + { + return $this->input->getArguments(); + } + + /** + * Determine if the given option is present. + */ + protected function hasOption($name): bool + { + return $this->input->hasOption($name) && ! is_null($this->option($name)); + } +} diff --git a/src/IO/Helper/OutputTrait.php b/src/IO/Helper/OutputTrait.php new file mode 100644 index 0000000..2991bd4 --- /dev/null +++ b/src/IO/Helper/OutputTrait.php @@ -0,0 +1,141 @@ +output->horizontalTable($headers, $rows); + } + + protected function progressAdvance(int $step = 1): void + { + $this->output->progressAdvance($step); + } + + protected function error(string|array $message): void + { + $this->output->error($message); + } + + protected function progressFinish(): void + { + $this->output->progressFinish(); + } + + protected function title(string $message): void + { + $this->output->title($message); + } + + protected function confirm(string $question, bool $default = true): bool + { + return $this->output->confirm($question, $default); + } + + /** + * Writes a message to the output and adds a newline at the end. + */ + protected function line(string $message, ?string $style = null): void + { + $styled = $style ? "<$style>$message" : $message; + + $this->output->writeln($styled); + } + + protected function note(string|array $message): void + { + $this->output->note($message); + } + + protected function question(string|array $message): void + { + $this->line($message, 'question'); + } + + protected function definitionList(string|array|TableSeparator ...$list): void + { + $this->output->definitionList(...$list); + } + + protected function table(array $headers, array $rows): void + { + $this->output->table($headers, $rows); + } + + protected function progressStart(int $max = 0): void + { + $this->output->progressStart($max); + } + + protected function warning(string|array $message): void + { + $this->output->warning($message); + } + + protected function createProgressBar(int $max = 0): ProgressBar + { + return $this->output->createProgressBar($max); + } + + protected function comment(string|array $message): void + { + $this->output->comment($message); + } + + protected function info(string|array $message): void + { + $this->output->info($message); + } + + protected function text(string|array $message): void + { + $this->output->text($message); + } + + protected function success(string|array $message): void + { + $this->output->success($message); + } + + protected function askHidden(string $question, ?callable $validator = null): mixed + { + return $this->output->askHidden($question, $validator); + } + + protected function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed + { + return $this->output->choice($question, $choices, $default, $multiSelect); + } + + protected function listing(array $elements): void + { + $this->output->listing($elements); + } + + protected function caution(string|array $message): void + { + $this->output->caution($message); + } + + protected function section(string $message): void + { + $this->output->section($message); + } + + protected function ask(string $question, ?string $default = null, ?callable $validator = null): mixed + { + return $this->output->ask($question, $default, $validator); + } +} diff --git a/src/IO/Output.php b/src/IO/Output.php new file mode 100644 index 0000000..4e7087e --- /dev/null +++ b/src/IO/Output.php @@ -0,0 +1,98 @@ +getFormatter())) { + $output = new NullOutput(); + } + + parent::__construct($input, $output); + + $this->bufferedOutput = new TrimmedBufferOutput( + DIRECTORY_SEPARATOR === '\\' ? 4 : 2, + $output->getVerbosity(), + false, + clone $output->getFormatter() + ); + } + + public function listing(array $elements): void + { + $this->autoPrependText(); + + $elements = array_map(static fn ($element) => sprintf(' ➜ %s', $element), $elements); + + $this->writeln($elements); + $this->newLine(); + } + + public function comment(string|array $message): void + { + $this->write(sprintf('➜ %s', $message)); + $this->newLine(); + } + + public function success(string|array $message): void + { + $this->write(sprintf('✔ %s', $message)); + $this->newLine(); + } + + public function error(string|array $message): void + { + $this->write(sprintf('✘ %s', $message)); + $this->newLine(); + } + + public function warning(string|array $message): void + { + $this->write(sprintf('⚠ %s', $message)); + $this->newLine(); + } + + public function note(string|array $message): void + { + $this->write(sprintf('➜ %s', $message)); + $this->newLine(); + } + + public function info(string|array $message): void + { + $this->write(sprintf('ℹ %s', $message)); + $this->newLine(); + } + + public function caution(string|array $message): void + { + $this->write(sprintf('! %s', $message)); + $this->newLine(); + } + + private function autoPrependText(): void + { + $fetched = $this->bufferedOutput->fetch(); + // Prepend new line if last char isn't EOL: + if ($fetched && ! str_ends_with($fetched, "\n")) { + $this->newLine(); + } + } +} diff --git a/src/LegacySymfonyStyle.php b/src/LegacySymfonyStyle.php deleted file mode 100644 index 9070783..0000000 --- a/src/LegacySymfonyStyle.php +++ /dev/null @@ -1,21 +0,0 @@ -getFormatter()) ? - new LegacySymfonyStyle($input) : - new SymfonyStyle($input, $output); - } -} diff --git a/tests/CommandTest.php b/tests/CommandTest.php index cf5a9b9..9d653b5 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -4,11 +4,10 @@ namespace Symblaze\Console\Tests; -use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Console\Helper\TableSeparator; +use PHPUnit\Framework\Attributes\Test; +use Symblaze\Console\IO\Output; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; final class CommandTest extends TestCase { @@ -197,409 +196,15 @@ public function get_all_options(): void $this->assertSame($expected, $command->options()); } - /** @test */ - public function write_message_with_line(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('writeln')->with('Hello world'); - - $command->line('Hello world'); - } - - /** @test */ - public function write_styled_message_with_line(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('writeln')->with('Hello world'); - - $command->line('Hello world', 'info'); - } - - /** @test */ - public function write_a_verbose_message_with_line(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('writeln')->with('Hello world', OutputInterface::VERBOSITY_DEBUG); - - $command->line('Hello world', null, 'vvv'); - } - - /** @test */ - public function write_an_info_message(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('info')->with('Hello world'); - - $command->info('Hello world'); - } - - /** @test */ - public function write_a_comment_message(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('comment')->with('Hello world'); - - $command->comment('Hello world'); - } - - /** @test */ - public function write_a_question(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('writeln')->with('Hello world'); - - $command->question('Hello world'); - } - - /** @test */ - public function write_an_error_message(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('error')->with('Hello world'); - - $command->error('Hello world'); - } - - /** @test */ - public function write_a_warn_message(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('warning')->with('Hello world'); - - $command->warning('Hello world'); - } - - /** @test */ - public function write_a_success_message(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('success')->with('Hello world'); - - $command->success('Hello world'); - } - - /** @test */ - public function write_a_title(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('title')->with('Hello world'); - - $command->title('Hello world'); - } - - /** @test */ - public function display_a_section(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('section')->with('Hello world'); - - $command->section('Hello world'); - } - - /** @test */ - public function display_a_single_text_message(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('text')->with('Hello world'); - - $command->text('Hello world'); - } - - /** @test */ - public function display_an_list_of_test_messages(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('text')->with(['Hello world', 'Hello world']); - - $command->text(['Hello world', 'Hello world']); - } - - /** @test */ - public function display_un_ordered_list(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('listing')->with(['Hello world', 'Hello world']); - - $command->listing(['Hello world', 'Hello world']); - } - - /** @test */ - public function display_a_table(): void - { - $headers = ['Header 1', 'Header 2']; - $rows = [ - ['Cell 1-1', 'Cell 1-2'], - ['Cell 2-1', 'Cell 2-2'], - ['Cell 3-1', 'Cell 3-2'], - ]; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('table')->with($headers, $rows); - - $command->table($headers, $rows); - } - - /** @test */ - public function display_horizontal_table(): void - { - $headers = ['Header 1', 'Header 2']; - $rows = [ - ['Cell 1-1', 'Cell 1-2'], - ['Cell 2-1', 'Cell 2-2'], - ['Cell 3-1', 'Cell 3-2'], - ]; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('horizontalTable')->with($headers, $rows); - - $command->horizontalTable($headers, $rows); - } - - /** @test */ - public function display_a_definition_list(): void - { - $list = [ - 'This is a title', - ['foo1' => 'bar1'], - ['foo2' => 'bar2'], - ['foo3' => 'bar3'], - new TableSeparator(), - 'This is another title', - ['foo4' => 'bar4'], - ]; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('definitionList')->with($list); - - $command->definitionList($list); - } - - /** @test */ - public function display_a_note(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('note')->with('Hello world'); - - $command->note('Hello world'); - } - - /** @test */ - public function display_a_list_of_notes(): void - { - $notes = ['Hello world', 'Hello world']; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('note')->with($notes); - - $command->note($notes); - } - - /** @test */ - public function display_a_caution(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('caution')->with('Hello world'); - - $command->caution('Hello world'); - } - - /** @test */ - public function display_a_list_of_cautions(): void - { - $cautions = ['Hello world', 'Hello world']; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('caution')->with($cautions); - - $command->caution($cautions); - } - - /** @test */ - public function ask_the_user_to_provide_a_value(): void - { - $question = 'What is your name?'; - $default = 'John Doe'; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('ask')->with($question, $default)->willReturn($default); - - $this->assertSame('John Doe', $command->ask($question, $default)); - } - - /** @test */ - public function ask_the_user_for_sensitive_data(): void - { - $question = 'What is your password?'; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $outputMock = $this->createMock(SymfonyStyle::class); - $command->setOutput($outputMock); - - $outputMock->expects($this->once())->method('askHidden')->with($question)->willReturn('secret'); - - $this->assertSame('secret', $command->askHidden($question)); - } - - /** @test */ - public function ask_a_yes_or_no_question(): void - { - $question = 'Do you want to continue?'; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $output = $this->createMock(SymfonyStyle::class); - $command->setOutput($output); - - $output->expects($this->once())->method('confirm')->with($question)->willReturn(true); - - $this->assertTrue($command->confirm($question)); - } - - /** @test */ - public function ask_a_question_whose_answer_is_constrained_to_a_given_list(): void - { - $question = 'Select the queue to analyze'; - $choices = ['queue1', 'queue2', 'queue3']; - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $output = $this->createMock(SymfonyStyle::class); - $command->setOutput($output); - - $output->expects($this->once())->method('choice')->with($question, $choices)->willReturn('queue1'); - - $this->assertSame('queue1', $command->choice($question, $choices)); - } - - /** @test */ - public function progress_bar_start(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $output = $this->createMock(SymfonyStyle::class); - $command->setOutput($output); - - $output->expects($this->once())->method('progressStart')->with(10); - - $command->progressStart(10); - } - - /** @test */ - public function progress_advance(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $output = $this->createMock(SymfonyStyle::class); - $command->setOutput($output); - - $output->expects($this->once())->method('progressAdvance')->with(10); - - $command->progressAdvance(10); - } - - /** @test */ - public function progress_finish(): void - { - $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $output = $this->createMock(SymfonyStyle::class); - $command->setOutput($output); - - $output->expects($this->once())->method('progressFinish'); - - $command->progressFinish(); - } - - /** @test */ - public function create_progress_bar(): void + #[Test] + public function run_sets_output_helper(): void { $command = new Doubles\MyCommand(); - $command->setInput($this->createMock(InputInterface::class)); - $output = $this->createMock(SymfonyStyle::class); - $command->setOutput($output); + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); - $output->expects($this->once())->method('createProgressBar')->with(10)->willReturn(new ProgressBar($output)); + $command->run($input, $output); - $command->createProgressBar(10); + $this->assertInstanceOf(Output::class, $command->getOutput()); } } diff --git a/tests/Doubles/MyCommand.php b/tests/Doubles/MyCommand.php index 4269407..33f6af1 100644 --- a/tests/Doubles/MyCommand.php +++ b/tests/Doubles/MyCommand.php @@ -6,10 +6,8 @@ use Symblaze\Console\Command; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; /** * @method hasArgument(string $name): bool @@ -18,30 +16,6 @@ * @method hasOption(string $name): bool * @method option(string $key): bool|array|string|null * @method options(): array - * @method line(string $message, ?string $style = null, string|int $verbosity = 'normal'): void - * @method info(string|array $message): void - * @method comment(string|array $message): void - * @method question(string|array $message): void - * @method error(string|array $message): void - * @method warning(string|array $message): void - * @method success(string|array $message): void - * @method title(string $message): void - * @method section(string $message): void - * @method text(string|array $message): void - * @method listing(array $elements): void - * @method table(array $headers, array $rows): void - * @method horizontalTable(array $headers, array $rows): void - * @method definitionList(string|array|TableSeparator ...$list): void - * @method note(string|array $message): void - * @method caution(string|array $message): void - * @method ask(string $question, string $default = null, callable $validator = null): mixed - * @method askHidden(string $question, callable $validator = null): mixed - * @method confirm(string $question, bool $default = true): bool - * @method choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed - * @method progressStart(int $max = 0): void - * @method progressAdvance(int $step = 1): void - * @method progressFinish(): void - * @method createProgressBar(int $max = 0): ProgressBar */ #[AsCommand(name: 'acme:command {required_argument} {optional_argument?} {argument_with_value=default} {--O|option} {--OWV|option_with_value=} {--OWDV|option_with_default=default}')] class MyCommand extends Command @@ -55,14 +29,4 @@ public function __call(string $name, array $arguments) { return parent::$name(...$arguments); } - - public function setOutput(SymfonyStyle $output): void - { - $this->output = $output; - } - - public function setInput(InputInterface $input): void - { - $this->input = $input; - } }