From 1279cb89cfd4fff623f2e1e809728140e2142af2 Mon Sep 17 00:00:00 2001 From: Evgeniy Zyubin Date: Thu, 21 Oct 2021 14:54:16 +0300 Subject: [PATCH] Cleanup, add more tests, remove use yiisoft/di package from tests (#129) --- .editorconfig | 3 + .gitattributes | 21 ++-- .github/dependabot.yml | 26 ++--- .github/workflows/bc.yml_ | 22 ++-- .github/workflows/build.yml | 13 +-- .github/workflows/mutation.yml | 9 +- .github/workflows/static.yml | 7 +- .gitignore | 11 +- .phpunit-watcher.yml | 16 +-- .scrutinizer.yml | 59 +++++----- .styleci.yml | 3 + CHANGELOG.md | 5 + composer.json | 34 +++--- config/console.php | 11 +- config/params.php | 6 +- phpunit.xml.dist | 48 +++++---- psalm.xml | 4 - src/Application.php | 34 +++--- src/Command/Serve.php | 23 ++-- src/CommandLoader.php | 21 ++-- src/ErrorListener.php | 14 +-- src/ExitCode.php | 10 +- src/Output/ConsoleBufferedOutput.php | 3 +- tests/ApplicationTest.php | 33 +++++- tests/CommandLoaderTest.php | 61 +++++++++++ tests/ErrorListenerTest.php | 18 +++- tests/ServeCommandTest.php | 33 +++--- tests/Stub/StubCommand.php | 8 +- tests/TestCase.php | 155 ++++++++++++++------------- 29 files changed, 416 insertions(+), 295 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 tests/CommandLoaderTest.php diff --git a/.editorconfig b/.editorconfig index 257221d23..5e9a93ea5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,6 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index d3353e511..efe6f14c3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,16 +22,19 @@ *.ttf binary # Ignore some meta files when creating an archive of this repository -/.github export-ignore -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.scrutinizer.yml export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore -/docs export-ignore +/.github export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.phpunit-watcher.yml export-ignore +/.scrutinizer.yml export-ignore +/.styleci.yml export-ignore +/infection.json.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml export-ignore +/tests export-ignore +/docs export-ignore # Avoid merge conflicts in CHANGELOG # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ /CHANGELOG.md merge=union - diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d7ebdbfdb..db86156d1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,16 @@ version: 2 updates: - # Maintain dependencies for GitHub Actions. - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - # Too noisy. See https://github.community/t/increase-if-necessary-for-github-actions-in-dependabot/179581 - open-pull-requests-limit: 0 + # Maintain dependencies for GitHub Actions. + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + # Too noisy. See https://github.community/t/increase-if-necessary-for-github-actions-in-dependabot/179581 + open-pull-requests-limit: 0 - # Maintain dependencies for Composer - - package-ecosystem: "composer" - directory: "/" - schedule: - interval: "daily" - versioning-strategy: increase-if-necessary + # Maintain dependencies for Composer + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "daily" + versioning-strategy: increase-if-necessary diff --git a/.github/workflows/bc.yml_ b/.github/workflows/bc.yml_ index 35b3a8624..75f97a7da 100644 --- a/.github/workflows/bc.yml_ +++ b/.github/workflows/bc.yml_ @@ -1,15 +1,15 @@ on: - - pull_request - - push + - pull_request + - push name: backwards compatibility jobs: - roave_bc_check: - name: Roave BC Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: fetch tags - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - name: Roave BC Check - uses: docker://nyholm/roave-bc-check-ga + roave_bc_check: + name: Roave BC Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: fetch tags + run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Roave BC Check + uses: docker://nyholm/roave-bc-check-ga diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df20a1988..7d03f1ee4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,12 +22,12 @@ jobs: - windows-latest php: - - "7.4" - - "8.0" + - 7.4 + - 8.0 steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -56,13 +56,8 @@ jobs: - name: Update composer run: composer self-update - - name: Install dependencies with composer php 7.4 - if: matrix.php == '7.4' + - name: Install dependencies with composer run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - name: Install dependencies with composer php 8.0 - if: matrix.php == '8.0' - run: composer update --ignore-platform-reqs --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - name: Run tests with phpunit run: vendor/bin/phpunit --colors=always diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 58e67d378..3f07217f0 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -18,18 +18,19 @@ jobs: - ubuntu-latest php: - - "7.4" + - 7.4 + - 8.0 steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2 - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: "${{ matrix.php }}" + php-version: ${{ matrix.php }} ini-values: memory_limit=-1 - coverage: "pcov" + coverage: pcov tools: composer:v2 - name: Determine composer cache directory diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ab28b1d21..2584c66f8 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -18,16 +18,17 @@ jobs: - ubuntu-latest php: - - "7.4" + - 7.4 + - 8.0 steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2 - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: "${{ matrix.php }}" + php-version: ${{ matrix.php }} tools: composer:v2, cs2pr coverage: none diff --git a/.gitignore b/.gitignore index 807fbf377..ffca5ff00 100644 --- a/.gitignore +++ b/.gitignore @@ -9,13 +9,6 @@ nbproject .project .settings -# sublime text project / workspace files -*.sublime-project -*.sublime-workspace - -# visual studio code project files -.vscode - # windows thumbnail cache Thumbs.db @@ -24,16 +17,14 @@ Thumbs.db # composer vendor dir /vendor +# composer lock file /composer.lock - # composer itself is not needed composer.phar # phpunit itself is not needed phpunit.phar - # local phpunit config /phpunit.xml - # phpunit cache .phpunit.result.cache diff --git a/.phpunit-watcher.yml b/.phpunit-watcher.yml index 0e4d76634..035a80aeb 100644 --- a/.phpunit-watcher.yml +++ b/.phpunit-watcher.yml @@ -1,11 +1,11 @@ watch: - directories: - - src - - tests - fileMask: '*.php' + directories: + - src + - tests + fileMask: '*.php' notifications: - passingTests: false - failingTests: false + passingTests: false + failingTests: false phpunit: - binaryPath: vendor/bin/phpunit - timeout: 180 + binaryPath: vendor/bin/phpunit + timeout: 180 diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 45b5dcaff..09f881737 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,36 +1,33 @@ checks: - php: true + php: true filter: - paths: - - "src/*" + paths: + - "src/*" build: - nodes: - analysis: - environment: - php: 7.4.12 - - tests: - override: - - php-scrutinizer-run - - tests-and-coverage: - environment: - php: 7.4.12 - - dependencies: - override: - - composer self-update - - composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - tests: - override: - - - command: "./vendor/bin/phpunit --coverage-clover ./coverage.xml" - on_node: 1 - coverage: - file: coverage.xml - format: php-clover - - + environment: + php: + version: 8.0.11 + ini: + "xdebug.mode": coverage + + nodes: + analysis: + tests: + override: + - php-scrutinizer-run + + phpunit: + dependencies: + override: + - composer self-update + - composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + tests: + override: + - command: "./vendor/bin/phpunit --coverage-clover ./coverage.xml" + on_node: 1 + coverage: + file: coverage.xml + format: php-clover diff --git a/.styleci.yml b/.styleci.yml index 63e26a594..b72de4219 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -86,3 +86,6 @@ enabled: - trailing_comma_in_multiline_array - unalign_double_arrow - unalign_equals + - empty_loop_body_braces + - integer_literal_case + - union_type_without_spaces diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e0bbbb908 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Yii Console Change Log + +## 1.0.0 under development + +- Initial release. diff --git a/composer.json b/composer.json index 3f2dd90c0..b9ca5e70e 100644 --- a/composer.json +++ b/composer.json @@ -6,15 +6,26 @@ "yii", "console" ], - "homepage": "http://www.yiiframework.com/", + "homepage": "https://www.yiiframework.com/", "license": "BSD-3-Clause", "support": { - "source": "https://github.com/yiisoft/yii-console", - "issues": "https://github.com/yiisoft/yii-console/issues", - "forum": "http://www.yiiframework.com/forum/", - "wiki": "http://www.yiiframework.com/wiki/", - "irc": "irc://irc.freenode.net/yii" + "issues": "https://github.com/yiisoft/yii-console/issues?state=open", + "forum": "https://www.yiiframework.com/forum/", + "wiki": "https://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "chat": "https://t.me/yii3en", + "source": "https://github.com/yiisoft/yii-console" }, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/yiisoft" + }, + { + "type": "github", + "url": "https://github.com/sponsors/yiisoft" + } + ], "minimum-stability": "dev", "prefer-stable": true, "require": { @@ -22,17 +33,14 @@ "psr/container": "^1.0|^2.0", "symfony/console": "^5.1.8", "symfony/event-dispatcher-contracts": "^2.2", - "yiisoft/friendly-exception": "^1.0", - "yiisoft/injector": "^1.0", - "yiisoft/yii-event": "^1.0" + "yiisoft/friendly-exception": "^1.0" }, "require-dev": { "phpunit/phpunit": "^9.5", - "roave/infection-static-analysis-plugin": "^1.6", + "roave/infection-static-analysis-plugin": "^1.9", "spatie/phpunit-watcher": "^1.23", - "vimeo/psalm": "^4.3", - "yiisoft/di": "@dev", - "yiisoft/test-support": "^1.3.0" + "vimeo/psalm": "^4.9", + "yiisoft/test-support": "^1.3" }, "autoload": { "psr-4": { diff --git a/config/console.php b/config/console.php index 61db82681..2c2ac32a7 100644 --- a/config/console.php +++ b/config/console.php @@ -20,8 +20,12 @@ ], Application::class => [ - 'class' => Application::class, - 'setDispatcher()' => [Reference::to(SymfonyEventDispatcher::class)], + '__construct()' => [ + Reference::to(SymfonyEventDispatcher::class), + $params['yiisoft/yii-console']['name'], + $params['yiisoft/yii-console']['version'], + ], + 'setAutoExit()' => [$params['yiisoft/yii-console']['autoExit']], 'setCommandLoader()' => [Reference::to(CommandLoaderInterface::class)], 'addOptions()' => [ new InputOption( @@ -31,8 +35,5 @@ 'Set alternative configuration name' ), ], - 'setName()' => [$params['yiisoft/yii-console']['name']], - 'setVersion()' => [$params['yiisoft/yii-console']['version']], - 'setAutoExit()' => [$params['yiisoft/yii-console']['autoExit']], ], ]; diff --git a/config/params.php b/config/params.php index 96ee3a367..c2a830d5d 100644 --- a/config/params.php +++ b/config/params.php @@ -2,16 +2,16 @@ declare(strict_types=1); +use Yiisoft\Yii\Console\Application; use Yiisoft\Yii\Console\Command\Serve; return [ 'yiisoft/yii-console' => [ - 'id' => 'yii-console', - 'name' => 'Yii Console', + 'name' => Application::NAME, + 'version' => Application::VERSION, 'autoExit' => false, 'commands' => [ 'serve' => Serve::class, ], - 'version' => '3.0', ], ]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0f6af0fa1..779d2e578 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,31 @@ - - - - ./ - - - ./tests - ./vendor - ./config - - - - - - - - ./tests/ - - + + + + + + + + + ./tests + + + + + + ./src + + diff --git a/psalm.xml b/psalm.xml index 32408861d..5a95a3aa4 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,15 +1,11 @@ - - - diff --git a/src/Application.php b/src/Application.php index 39e8e7ce9..56fe47abf 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,37 +4,33 @@ namespace Yiisoft\Yii\Console; -use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\StyleInterface; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Throwable; use Yiisoft\FriendlyException\FriendlyExceptionInterface; use Yiisoft\Yii\Console\Event\ApplicationShutdown; use Yiisoft\Yii\Console\Event\ApplicationStartup; +use function array_slice; +use function explode; +use function implode; + class Application extends \Symfony\Component\Console\Application { - public const VERSION = '3.0.0-dev'; + public const NAME = 'Yii Console'; + public const VERSION = '1.0.0-dev'; - /** @psalm-suppress PropertyNotSetInConstructor */ private EventDispatcherInterface $dispatcher; - public function __construct(string $name = 'Yii Console', string $version = self::VERSION) - { - parent::__construct($name, $version); - } - - public function setDispatcher(SymfonyEventDispatcherInterface $dispatcher): void - { + public function __construct( + EventDispatcherInterface $dispatcher, + string $name = self::NAME, + string $version = self::VERSION + ) { $this->dispatcher = $dispatcher; - parent::setDispatcher($dispatcher); - } - - public function getDispatcher(): EventDispatcherInterface - { - return $this->dispatcher; + parent::__construct($name, $version); } public function start(): void @@ -82,10 +78,10 @@ public function addOptions(InputOption $options): void $this->getDefinition()->addOption($options); } - public function extractNamespace(string $name, int $limit = null) + public function extractNamespace(string $name, int $limit = null): string { $parts = explode('/', $name, -1); - return implode('/', null === $limit ? $parts : \array_slice($parts, 0, $limit)); + return implode('/', null === $limit ? $parts : array_slice($parts, 0, $limit)); } } diff --git a/src/Command/Serve.php b/src/Command/Serve.php index cbce6cf56..2bad523e1 100644 --- a/src/Command/Serve.php +++ b/src/Command/Serve.php @@ -12,7 +12,16 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Yiisoft\Yii\Console\ExitCode; -class Serve extends Command +use function explode; +use function fclose; +use function file_exists; +use function fsockopen; +use function getcwd; +use function is_dir; +use function passthru; +use function strpos; + +final class Serve extends Command { public const EXIT_CODE_NO_DOCUMENT_ROOT = 2; public const EXIT_CODE_NO_ROUTING_FILE = 3; @@ -81,8 +90,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::EXIT_CODE_NO_ROUTING_FILE; } - $output->writeLn("Server started on http://{$address}/"); - $output->writeLn("Document root is \"{$documentRoot}\""); + $output->writeLn("Server started on http://$address/"); + $output->writeLn("Document root is \"$documentRoot\""); if ($router) { $output->writeLn("Routing file is \"$router\""); @@ -94,23 +103,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int return ExitCode::OK; } - passthru('"' . PHP_BINARY . '"' . " -S {$address} -t \"{$documentRoot}\" $router"); + passthru('"' . PHP_BINARY . '"' . " -S $address -t \"$documentRoot\" $router"); return ExitCode::OK; } /** - * @param string $address server address + * @param string $address The server address. * - * @return bool if address is already in use + * @return bool If address is already in use. */ private function isAddressTaken(string $address): bool { [$hostname, $port] = explode(':', $address); $fp = @fsockopen($hostname, (int)$port, $errno, $errstr, 3); + if ($fp === false) { return false; } + fclose($fp); return true; } diff --git a/src/CommandLoader.php b/src/CommandLoader.php index 397bb1bca..f5ec3b4bb 100644 --- a/src/CommandLoader.php +++ b/src/CommandLoader.php @@ -11,17 +11,20 @@ use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Exception\CommandNotFoundException; +use function array_shift; +use function explode; + final class CommandLoader implements CommandLoaderInterface { private ContainerInterface $container; /** * @psalm-var array, - * hidden: bool, - * }> + * name: non-empty-string, + * aliases: non-empty-string[], + * class: class-string, + * hidden: bool, + * }> */ private array $commandMap; @@ -31,7 +34,7 @@ final class CommandLoader implements CommandLoaderInterface private array $commandNames; /** - * @param array $commandMap An array with command names as keys and service ids as values + * @param array $commandMap An array with command names as keys and service ids as values. * * @psalm-param array $commandMap */ @@ -63,9 +66,7 @@ public function get(string $name) $commandAliases, $description, $commandHidden, - function () use ($name) { - return $this->getCommandInstance($name); - } + fn () => $this->getCommandInstance($name), ); } @@ -91,6 +92,7 @@ private function getCommandInstance(string $name): Command if ($command->getName() !== $commandName) { $command->setName($commandName); } + if ($command->getAliases() !== $commandAliases) { $command->setAliases($commandAliases); } @@ -128,6 +130,7 @@ private function setCommandMap(array $commandMap): void $this->commandMap[$primaryName] = $item; $this->commandNames[] = $primaryName; + foreach ($aliases as $alias) { $this->commandMap[$alias] = $item; } diff --git a/src/ErrorListener.php b/src/ErrorListener.php index 304f78aae..bd2c11a25 100644 --- a/src/ErrorListener.php +++ b/src/ErrorListener.php @@ -7,6 +7,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Console\Event\ConsoleErrorEvent; +use function get_class; +use function sprintf; + final class ErrorListener { private LoggerInterface $logger; @@ -24,20 +27,17 @@ public function onError(ConsoleErrorEvent $event): void $exception = $event->getError(); $command = $event->getCommand(); - if ($command !== null && $command->getName() !== null) { - $commandName = $command->getName(); - } else { - $commandName = 'unknown'; - } + $commandName = ($command !== null && $command->getName() !== null) ? $command->getName() : 'unknown'; $message = sprintf( - '%s: %s in %s:%s while running console command `%s`', + '%s: %s in %s:%s while running console command "%s".', get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine(), - $commandName + $commandName, ); + $this->logger->error($message, ['exception' => $exception]); } } diff --git a/src/ExitCode.php b/src/ExitCode.php index 80106b5ec..35204fd7c 100644 --- a/src/ExitCode.php +++ b/src/ExitCode.php @@ -25,7 +25,7 @@ * } * ``` * - * @see http://man.openbsd.org/sysexits + * @see https://man.openbsd.org/sysexits */ class ExitCode { @@ -133,7 +133,7 @@ class ExitCode public const CONFIG = 78; /** - * @var array a map of reason descriptions for exit codes. + * @var array A map of reason descriptions for exit codes. */ public static array $reasons = [ self::OK => 'Success', @@ -160,11 +160,9 @@ class ExitCode * * This method uses {@see $reasons} to determine the reason for an exit code. * - * @param int $exitCode one of the constants defined in this class. + * @param int $exitCode One of the constants defined in this class. * - * @return string the reason text, or `"Unknown exit code"` if the code is not listed in {@see $reasons}. - * - * @psalm-suppress MixedInferredReturnType, MixedReturnStatement + * @return string The reason text, or `"Unknown exit code"` if the code is not listed in {@see $reasons}. */ public static function getReason(int $exitCode): string { diff --git a/src/Output/ConsoleBufferedOutput.php b/src/Output/ConsoleBufferedOutput.php index cc3d9fc14..19c1c6411 100644 --- a/src/Output/ConsoleBufferedOutput.php +++ b/src/Output/ConsoleBufferedOutput.php @@ -9,7 +9,7 @@ /** * @author Jean-François Simon */ -class ConsoleBufferedOutput extends ConsoleOutput +final class ConsoleBufferedOutput extends ConsoleOutput { private string $buffer = ''; @@ -23,6 +23,7 @@ class ConsoleBufferedOutput extends ConsoleOutput public function fetch(bool $clearBuffer = false): string { $content = $this->buffer; + if ($clearBuffer) { $this->buffer = ''; } diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 059ba46ba..b1dfab46e 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -15,7 +15,7 @@ public function testDispatcherEventApplicationStartup(): void { $event = new ApplicationStartup(); - $dispatcher = $this->application->getDispatcher(); + $dispatcher = $this->getInaccessibleProperty($this->application(), 'dispatcher'); $result = $dispatcher->dispatch($event); $this->assertSame($event, $result); @@ -25,7 +25,7 @@ public function testDispatcherEventApplicationShutdown(): void { $event = new ApplicationShutdown(ExitCode::OK); - $dispatcher = $this->application->getDispatcher(); + $dispatcher = $this->getInaccessibleProperty($this->application(), 'dispatcher'); $result = $dispatcher->dispatch($event); $this->assertSame($event, $result); @@ -34,7 +34,7 @@ public function testDispatcherEventApplicationShutdown(): void public function testDoRenderThrowable(): void { - $command = $this->application->find('stub'); + $command = $this->application()->find('stub'); $commandCreate = new CommandTester($command); @@ -65,7 +65,7 @@ public function testDoRenderThrowable(): void public function testDoRenderThrowableWithStyledOutput(): void { - $command = $this->application->find('stub'); + $command = $this->application()->find('stub'); $commandCreate = new CommandTester($command); @@ -94,7 +94,7 @@ public function testDoRenderThrowableWithStyledOutput(): void public function testRenamedCommand(): void { - $command = $this->application->find('stub/rename'); + $command = $this->application()->find('stub/rename'); $commandCreate = new CommandTester($command); @@ -103,4 +103,27 @@ public function testRenamedCommand(): void $commandCreate->execute(['command' => $command->getName()]) ); } + + public function namespaceProvider(): array + { + return [ + ['first/second/third', null, 'first/second'], + ['first/second/third', 1, 'first'], + ['first/second/third', 2, 'first/second'], + ['first/second/third', 3, 'first/second'], + ['first/second/third', 4, 'first/second'], + ]; + } + + /** + * @dataProvider namespaceProvider + * + * @param string $name + * @param int|null $limit + * @param string $expected + */ + public function testExtractNamespace(string $name, ?int $limit, string $expected): void + { + $this->assertSame($expected, $this->application()->extractNamespace($name, $limit)); + } } diff --git a/tests/CommandLoaderTest.php b/tests/CommandLoaderTest.php new file mode 100644 index 000000000..b788291fe --- /dev/null +++ b/tests/CommandLoaderTest.php @@ -0,0 +1,61 @@ +loader = $this->container()->get(CommandLoaderInterface::class); + } + + public function testGetNames(): void + { + $this->assertSame(['serve', 'stub', 'stub/rename'], $this->loader->getNames()); + } + + public function testGetAlias(): void + { + $this->assertSame([], $this->loader->get('serve')->getAliases()); + $this->assertSame(['st'], $this->loader->get('st')->getAliases()); + $this->assertSame(['st'], $this->loader->get('stub')->getAliases()); + $this->assertSame([], $this->loader->get('stub/rename')->getAliases()); + } + + public function testGetThrowExceptionIfCommandDoesNotExist(): void + { + $this->expectException(CommandNotFoundException::class); + $this->expectExceptionMessage('Command "not-found" does not exist.'); + + $this->loader->get('not-found'); + } + + public function testConstructThrowExceptionIfCommandNameIsNotValid(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Do not allow empty command name or alias.'); + + new CommandLoader(new SimpleContainer([Serve::class => new Serve()]), ['|' => Serve::class]); + } + + public function testConstructThrowExceptionIfCommandAliasIsNotValid(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Do not allow empty command name or alias.'); + + new CommandLoader(new SimpleContainer([Serve::class => new Serve()]), ['serve|' => Serve::class]); + } +} diff --git a/tests/ErrorListenerTest.php b/tests/ErrorListenerTest.php index 19d6e5e99..e158d84f5 100644 --- a/tests/ErrorListenerTest.php +++ b/tests/ErrorListenerTest.php @@ -4,11 +4,11 @@ namespace Yiisoft\Yii\Console\Tests; +use RuntimeException; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\ConsoleOutput; use Yiisoft\Test\Support\Log\SimpleLogger; -use Yiisoft\Yii\Console\Application; use Yiisoft\Yii\Console\ErrorListener; final class ErrorListenerTest extends TestCase @@ -18,10 +18,14 @@ public function testOnError() $logger = new SimpleLogger(); $errorListener = new ErrorListener($logger); - $application = $this->container->get(Application::class); - $command = $application->find('serve'); + $command = $this->application()->find('serve'); - $consoleErrorEvent = new ConsoleErrorEvent(new ArrayInput([]), new ConsoleOutput(), new \RuntimeException('Can not execute command'), $command); + $consoleErrorEvent = new ConsoleErrorEvent( + new ArrayInput([]), + new ConsoleOutput(), + new RuntimeException('Can not execute command'), + $command, + ); $errorListener->onError($consoleErrorEvent); @@ -34,7 +38,11 @@ public function testOnErrorWithoutCommand() $logger = new SimpleLogger(); $errorListener = new ErrorListener($logger); - $consoleErrorEvent = new ConsoleErrorEvent(new ArrayInput([]), new ConsoleOutput(), new \RuntimeException('Can not execute command')); + $consoleErrorEvent = new ConsoleErrorEvent( + new ArrayInput([]), + new ConsoleOutput(), + new RuntimeException('Can not execute command'), + ); $errorListener->onError($consoleErrorEvent); diff --git a/tests/ServeCommandTest.php b/tests/ServeCommandTest.php index 6ede67598..654d5dbc8 100644 --- a/tests/ServeCommandTest.php +++ b/tests/ServeCommandTest.php @@ -5,15 +5,14 @@ namespace Yiisoft\Yii\Console\Tests; use Symfony\Component\Console\Tester\CommandTester; -use Yiisoft\Yii\Console\Application; +use Yiisoft\Yii\Console\Command\Serve; +use Yiisoft\Yii\Console\ExitCode; final class ServeCommandTest extends TestCase { public function testServeCommandExecuteWithoutArguments(): void { - $application = $this->container->get(Application::class); - - $command = $application->find('serve'); + $command = $this->application()->find('serve'); $commandCreate = new CommandTester($command); @@ -26,6 +25,8 @@ public function testServeCommandExecuteWithoutArguments(): void $output = $commandCreate->getDisplay(true); + $this->assertSame(Serve::EXIT_CODE_NO_DOCUMENT_ROOT, $commandCreate->getStatusCode()); + $this->assertStringContainsString( '[ERROR] Document root', $output @@ -34,9 +35,7 @@ public function testServeCommandExecuteWithoutArguments(): void public function testServeCommandExecuteWithDocRoot(): void { - $application = $this->container->get(Application::class); - - $command = $application->find('serve'); + $command = $this->application()->find('serve'); $commandCreate = new CommandTester($command); @@ -49,6 +48,8 @@ public function testServeCommandExecuteWithDocRoot(): void $output = $commandCreate->getDisplay(true); + $this->assertSame(ExitCode::OK, $commandCreate->getStatusCode()); + $this->assertStringContainsString( 'Server started on http://localhost:8080/', $output @@ -67,9 +68,7 @@ public function testServeCommandExecuteWithDocRoot(): void public function testErrorWhenAddressIsTaken(): void { - $application = $this->container->get(Application::class); - - $command = $application->find('serve'); + $command = $this->application()->find('serve'); $commandCreate = new CommandTester($command); @@ -99,6 +98,8 @@ public function testErrorWhenAddressIsTaken(): void $output = $commandCreate->getDisplay(true); + $this->assertSame(Serve::EXIT_CODE_ADDRESS_TAKEN_BY_ANOTHER_PROCESS, $commandCreate->getStatusCode()); + $this->assertStringContainsString( '[ERROR] http://127.0.0.1:8080 is taken by another process.', $output @@ -110,9 +111,7 @@ public function testErrorWhenAddressIsTaken(): void public function testErrorWhenRouterDoesNotExist(): void { - $application = $this->container->get(Application::class); - - $command = $application->find('serve'); + $command = $this->application()->find('serve'); $commandCreate = new CommandTester($command); @@ -126,6 +125,8 @@ public function testErrorWhenRouterDoesNotExist(): void $output = $commandCreate->getDisplay(true); + $this->assertSame(Serve::EXIT_CODE_NO_ROUTING_FILE, $commandCreate->getStatusCode()); + $this->assertStringContainsString( '[ERROR] Routing file "index.php" does not exist.', $output @@ -134,9 +135,7 @@ public function testErrorWhenRouterDoesNotExist(): void public function testSuccess(): void { - $application = $this->container->get(Application::class); - - $command = $application->find('serve'); + $command = $this->application()->find('serve'); $commandCreate = new CommandTester($command); @@ -148,6 +147,8 @@ public function testSuccess(): void '--env' => 'test', ]); + $this->assertSame(ExitCode::OK, $commandCreate->getStatusCode()); + $output = $commandCreate->getDisplay(true); $this->assertStringContainsString( diff --git a/tests/Stub/StubCommand.php b/tests/Stub/StubCommand.php index 278ec3d77..f74205e56 100644 --- a/tests/Stub/StubCommand.php +++ b/tests/Stub/StubCommand.php @@ -12,16 +12,16 @@ use Yiisoft\Yii\Console\Application; use Yiisoft\Yii\Console\ExitCode; -class StubCommand extends Command +final class StubCommand extends Command { private Application $application; + protected static $defaultName = 'stub'; + protected static $defaultDescription = 'Stub command tests'; public function configure(): void { - $this - ->setDescription('Stub command tests') - ->addOption('styled', 's', InputOption::VALUE_OPTIONAL); + $this->addOption('styled', 's', InputOption::VALUE_OPTIONAL); } public function __construct(Application $application) diff --git a/tests/TestCase.php b/tests/TestCase.php index 514bde921..8125bdd2d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,126 +4,131 @@ namespace Yiisoft\Yii\Console\Tests; -use PHPUnit\Framework\TestCase as AbstractTestCase; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\EventDispatcher\ListenerProviderInterface; use ReflectionObject; use ReflectionException; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Input\InputOption; -use Yiisoft\Di\Container; -use Yiisoft\EventDispatcher\Dispatcher\Dispatcher; -use Yiisoft\EventDispatcher\Provider\Provider; -use Yiisoft\Definitions\Reference; +use Yiisoft\Test\Support\Container\SimpleContainer; +use Yiisoft\Test\Support\EventDispatcher\SimpleEventDispatcher; use Yiisoft\Yii\Console\Application; use Yiisoft\Yii\Console\Command\Serve; use Yiisoft\Yii\Console\CommandLoader; use Yiisoft\Yii\Console\SymfonyEventDispatcher; use Yiisoft\Yii\Console\Tests\Stub\StubCommand; -class TestCase extends AbstractTestCase +abstract class TestCase extends \PHPUnit\Framework\TestCase { - protected Application $application; - protected ContainerInterface $container; + private ?ContainerInterface $container = null; protected function setUp(): void { parent::setUp(); - $this->configContainer(); + $this->container = $this->createContainer(); } protected function tearDown(): void { - unset($this->container); + $this->container = null; + $this->application = null; + + parent::tearDown(); + } + + protected function container(): ContainerInterface + { + if ($this->container === null) { + $this->container = $this->createContainer(); + } + + return $this->container; + } + + protected function application(): Application + { + return $this->container()->get(Application::class); } /** - * Invokes a inaccessible method. + * Gets an inaccessible object property. + * + * @param object $object + * @param string $propertyName * - * @param $object - * @param $method + * @return mixed + */ + protected function getInaccessibleProperty(object $object, string $propertyName) + { + $reflection = new ReflectionObject($object); + + while (!$reflection->hasProperty($propertyName)) { + $reflection = $reflection->getParentClass(); + } + + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + $result = $property->getValue($object); + $property->setAccessible(false); + + return $result; + } + + /** + * Invokes an inaccessible method. + * + * @param object $object + * @param string $method * @param array $args - * @param bool $revoke whether to make method inaccessible after execution * * @throws ReflectionException * * @return mixed */ - protected function invokeMethod(object $object, string $method, array $args = [], bool $revoke = true) + protected function invokeMethod(object $object, string $method, array $args = []) { $reflection = new ReflectionObject($object); $method = $reflection->getMethod($method); - $method->setAccessible(true); - $result = $method->invokeArgs($object, $args); - - if ($revoke) { - $method->setAccessible(false); - } + $method->setAccessible(false); return $result; } - protected function configContainer(): void - { - $this->container = new Container($this->config()); - $this->application = $this->container->get(Application::class); - } - - private function config(): array + private function createContainer(): ContainerInterface { - $params = $this->params(); - - return [ - ListenerProviderInterface::class => Provider::class, - - EventDispatcherInterface::class => Dispatcher::class, - - CommandLoaderInterface::class => [ - 'class' => CommandLoader::class, - '__construct()' => [ - 'commandMap' => $params['yiisoft/yii-console']['commands'], - ], + $dispatcher = new SimpleEventDispatcher(); + + $application = new Application(new SymfonyEventDispatcher($dispatcher)); + $application->setAutoExit(false); + $application->addOptions(new InputOption( + 'config', + 'c', + InputOption::VALUE_REQUIRED, + 'Set alternative configuration name' + )); + + $commandLoader = new CommandLoader( + new SimpleContainer([ + Serve::class => new Serve(), + StubCommand::class => new StubCommand($application), + ]), + [ + 'serve' => Serve::class, + 'stub|st' => StubCommand::class, + 'stub/rename' => StubCommand::class, ], + ); - Application::class => [ - 'class' => Application::class, - 'setDispatcher()' => [Reference::to(SymfonyEventDispatcher::class)], - 'setCommandLoader()' => [Reference::to(CommandLoaderInterface::class)], - 'addOptions()' => [ - new InputOption( - 'config', - 'c', - InputOption::VALUE_REQUIRED, - 'Set alternative configuration name' - ), - ], - 'setName()' => [$params['yiisoft/yii-console']['name']], - 'setVersion()' => [$params['yiisoft/yii-console']['version']], - 'setAutoExit()' => [$params['yiisoft/yii-console']['autoExit']], - ], - ]; - } + $application->setCommandLoader($commandLoader); - private function params(): array - { - return [ - 'yiisoft/yii-console' => [ - 'id' => 'yii-console', - 'name' => 'Yii Console', - 'autoExit' => false, - 'commands' => [ - 'serve' => Serve::class, - 'stub' => StubCommand::class, - 'stub/rename' => StubCommand::class, - ], - 'version' => '3.0', - 'rebuildConfig' => static fn () => getenv('APP_ENV') === 'dev', - ], - ]; + return new SimpleContainer([ + Application::class => $application, + EventDispatcherInterface::class => $dispatcher, + CommandLoaderInterface::class => $commandLoader, + ]); } }