From f1ce6dca320787c074f58a8f0d5c730981180183 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Tue, 4 Jul 2023 16:14:22 +0300 Subject: [PATCH 1/2] Issue #13: Implemented unit tests, cs-check, static analysis. Signed-off-by: alexmerlin --- .github/workflows/cs-tests.yml | 46 +++++++ .github/workflows/static-analysis.yml | 46 +++++++ .github/workflows/unit-tests.yml | 47 +++++++ .gitignore | 2 + OSSMETADATA | 2 +- README.md | 29 +++-- composer.json | 34 ++++- config/autoload/cli.global.php | 15 +-- phpcs.xml | 22 ++++ phpunit.xml | 17 +++ psalm-baseline.xml | 13 ++ psalm.xml | 18 +++ src/Application.php | 18 +-- src/Command/DemoCommand.php | 30 +---- src/ConfigProvider.php | 18 +-- src/Factory/ApplicationFactory.php | 62 +++++---- src/Factory/FileLockerFactory.php | 59 +++++++-- src/FileLocker.php | 110 +++++++--------- src/FileLockerInterface.php | 45 ++----- test/ApplicationTest.php | 38 ++++++ test/Command/DemoCommandTest.php | 41 ++++++ test/CommonTrait.php | 51 ++++++++ test/ConfigProviderTest.php | 39 ++++++ test/Factory/ApplicationFactoryTest.php | 144 +++++++++++++++++++++ test/Factory/FileLockerFactoryTest.php | 145 +++++++++++++++++++++ test/FileLockerTest.php | 162 ++++++++++++++++++++++++ 26 files changed, 1041 insertions(+), 212 deletions(-) create mode 100644 .github/workflows/cs-tests.yml create mode 100644 .github/workflows/static-analysis.yml create mode 100644 .github/workflows/unit-tests.yml create mode 100644 phpcs.xml create mode 100644 phpunit.xml create mode 100644 psalm-baseline.xml create mode 100644 psalm.xml create mode 100644 test/ApplicationTest.php create mode 100644 test/Command/DemoCommandTest.php create mode 100644 test/CommonTrait.php create mode 100644 test/ConfigProviderTest.php create mode 100644 test/Factory/ApplicationFactoryTest.php create mode 100644 test/Factory/FileLockerFactoryTest.php create mode 100644 test/FileLockerTest.php diff --git a/.github/workflows/cs-tests.yml b/.github/workflows/cs-tests.yml new file mode 100644 index 0000000..3da9965 --- /dev/null +++ b/.github/workflows/cs-tests.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run phpcs checks + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run phpcs checks + run: vendor/bin/phpcs diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..74550fc --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run static analysis + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run static analysis + run: vendor/bin/psalm --no-cache --output-format=github --show-info=false --threads=4 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..d2ab8e7 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,47 @@ +on: + - push + +name: Run PHPUnit tests + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always diff --git a/.gitignore b/.gitignore index f3e6777..be50580 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ ### Composer template composer.phar /vendor/ +.phpcs-cache +.phpunit.result.cache # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file diff --git a/OSSMETADATA b/OSSMETADATA index 6c7e106..b96d4a4 100644 --- a/OSSMETADATA +++ b/OSSMETADATA @@ -1 +1 @@ -osslifecycle=active \ No newline at end of file +osslifecycle=active diff --git a/README.md b/README.md index 75aefe7..8bbe150 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,38 @@ # dot-cli ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-cli) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-cli/3.2.0) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-cli/3.4.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-cli)](https://github.com/dotkernel/dot-cli/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-cli)](https://github.com/dotkernel/dot-cli/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-cli)](https://github.com/dotkernel/dot-cli/stargazers) [![GitHub license](https://img.shields.io/github/license/dotkernel/dot-cli)](https://github.com/dotkernel/dot-cli/blob/3.0/LICENSE) +[![Build Static](https://github.com/dotkernel/dot-cli/actions/workflows/static-analysis.yml/badge.svg?branch=3.0)](https://github.com/dotkernel/dot-cli/actions/workflows/static-analysis.yml) + +[![SymfonyInsight](https://insight.symfony.com/projects/b9489f03-14e3-441f-aefd-e3b549b4917e/big.svg)](https://insight.symfony.com/projects/b9489f03-14e3-441f-aefd-e3b549b4917e) + DotKernel component to build console applications based on [laminas-cli](https://github.com/laminas/laminas-cli). ### Requirements -- PHP >= 7.4 -- laminas/laminas-servicemanager >= 3.6, -- laminas/laminas-cli >= 1.0 +- PHP >= 8.1 +- laminas/laminas-servicemanager >= 3.11, +- laminas/laminas-cli >= 1.4 ### Setup #### 1. Install package Run the following command in your application's root directory: -```bash -$ composer require dotkernel/dot-cli -``` + + composer require dotkernel/dot-cli #### 2. Register ConfigProvider -Open your application's `config/config.php` and add `Dot\Cli\ConfigProvider::class,` under the _DK packages_ comment. +Open your application's `config/config.php` and the following line under the _DK packages_ comment: + + Dot\Cli\ConfigProvider::class, #### 3. Copy bootstrap file -Locate in this package the following file `bin/cli.php` then copy it to your application's `bin/` directory. +Locate file `bin/cli.php` in this package, then copy it to your application's `bin/` directory. This is the bootstrap file you will use to execute your commands with. #### 4. Copy config file @@ -37,9 +42,9 @@ This is the config file you will add your commands to. ### Testing Using the command line, go to your application's root directory, then type the following command: -```bash -$ php /bin/cli.php -``` + + php /bin/cli.php + The output should look similar to this, containing information on how to start using dot-cli: ```text DotKernel CLI 1.0.0 diff --git a/composer.json b/composer.json index 73ef0d8..8bfbb7d 100644 --- a/composer.json +++ b/composer.json @@ -17,18 +17,42 @@ "email": "team@dotkernel.com" } ], + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, "require": { - "php": "~7.4.0 || ~8.0.0 || ~8.1.0", + "php": "~8.1.0 || ~8.2.0", "laminas/laminas-cli": "^1.4.0", "laminas/laminas-servicemanager": "^3.11.1" }, - "require-dev": { - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.5" - }, "autoload": { "psr-4": { "Dot\\Cli\\": "src" } + }, + "require-dev": { + "laminas/laminas-coding-standard": "^2.5", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.13" + }, + "autoload-dev": { + "psr-4": { + "DotTest\\Cli\\": "test/" + } + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "test": "phpunit --colors=always", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", + "static-analysis": "psalm --shepherd --stats" } } diff --git a/config/autoload/cli.global.php b/config/autoload/cli.global.php index 864af0e..b5e83bf 100644 --- a/config/autoload/cli.global.php +++ b/config/autoload/cli.global.php @@ -1,21 +1,20 @@ [ - 'version' => '1.0.0', - 'name' => 'DotKernel CLI', + 'dot_cli' => [ + 'version' => '1.0.0', + 'name' => 'DotKernel CLI', 'commands' => [ DemoCommand::getDefaultName() => DemoCommand::class, - ] + ], ], FileLockerInterface::class => [ 'enabled' => true, 'dirPath' => getcwd() . '/data/lock', - ] + ], ]; diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..c294658 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + bin + config + src + test + + + + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..5122d89 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./test + + + + + + ./src + + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..9352203 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,13 @@ + + + + + new TerminateListener($config) + new ContainerCommandLoader($container, $config['commands']) + + + new TerminateListener($config) + new ContainerCommandLoader($container, $config['commands']) + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..9dd8f07 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/src/Application.php b/src/Application.php index e516af3..aed2143 100644 --- a/src/Application.php +++ b/src/Application.php @@ -5,23 +5,15 @@ namespace Dot\Cli; use Exception; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; -/** - * Class Application - * @package Dot\Cli - */ class Application extends \Symfony\Component\Console\Application { private FileLockerInterface $fileLocker; - /** - * Application constructor. - * @param FileLockerInterface $fileLocker - * @param array $config - */ public function __construct(FileLockerInterface $fileLocker, array $config) { parent::__construct($config['name'] ?? 'UNKNOWN', $config['version'] ?? 'UNKNOWN'); @@ -29,18 +21,12 @@ public function __construct(FileLockerInterface $fileLocker, array $config) $this->fileLocker = $fileLocker; } - /** - * Application destructor. - */ public function __destruct() { $this->fileLocker->unlock(); } /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int * @throws Throwable */ public function doRun(InputInterface $input, OutputInterface $output): int @@ -51,7 +37,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int $this->fileLocker->lock(); } catch (Exception $exception) { $output->writeln($exception->getMessage()); - return 0; + return Command::FAILURE; } return parent::doRun($input, $output); diff --git a/src/Command/DemoCommand.php b/src/Command/DemoCommand.php index 99130bc..88b47a2 100644 --- a/src/Command/DemoCommand.php +++ b/src/Command/DemoCommand.php @@ -5,47 +5,23 @@ namespace Dot\Cli\Command; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -/** - * Class DemoCommand - * @package Dot\Cli\Command - */ class DemoCommand extends Command { + /** @var string $defaultName */ protected static $defaultName = 'demo:command'; - /** - * @return void - */ protected function configure(): void { $this ->setName(self::$defaultName) - ->setDescription('Demo command description.') - ->addOption('opt_required', 'r', InputOption::VALUE_REQUIRED, 'Required parameter') - ->addOption('opt_optional', 'o', InputOption::VALUE_OPTIONAL, 'Optional parameter') - ->addArgument('arg_required', InputArgument::REQUIRED, 'Required argument') - ->addArgument('arg_optional', InputArgument::OPTIONAL, 'Optional argument', 'arg') - ->addArgument('arg_array', InputArgument::IS_ARRAY, 'Array argument'); + ->setDescription('Demo command description.'); } - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int - */ protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('opt_required=' . $input->getOption('opt_required')); - $output->writeln('opt_optional=' . $input->getOption('opt_optional')); - $output->writeln('arg_required=' . $input->getArgument('arg_required')); - $output->writeln('arg_optional=' . $input->getArgument('arg_optional')); - $output->writeln('arg_array=[' . implode(', ', $input->getArgument('arg_array')) . ']'); - - return 0; + return Command::SUCCESS; } } diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 18b3ed3..c1762be 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -7,15 +7,8 @@ use Dot\Cli\Factory\ApplicationFactory; use Dot\Cli\Factory\FileLockerFactory; -/** - * Class ConfigProvider - * @package Dot\Cli - */ class ConfigProvider { - /** - * @return array - */ public function __invoke(): array { return [ @@ -23,19 +16,16 @@ public function __invoke(): array ]; } - /** - * @return string[][] - */ public function getDependencyConfig(): array { return [ - 'aliases' => [ - FileLockerInterface::class => FileLocker::class + 'aliases' => [ + FileLockerInterface::class => FileLocker::class, ], 'factories' => [ Application::class => ApplicationFactory::class, - FileLocker::class => FileLockerFactory::class - ] + FileLocker::class => FileLockerFactory::class, + ], ]; } } diff --git a/src/Factory/ApplicationFactory.php b/src/Factory/ApplicationFactory.php index 99c4717..f7cc7f7 100644 --- a/src/Factory/ApplicationFactory.php +++ b/src/Factory/ApplicationFactory.php @@ -5,51 +5,69 @@ namespace Dot\Cli\Factory; use Dot\Cli\FileLockerInterface; +use Exception; use Laminas\Cli\ContainerCommandLoader; use Laminas\Cli\Listener\TerminateListener; -use Composer\InstalledVersions; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Webmozart\Assert\Assert; -/** - * Class ApplicationFactory - */ +use function array_key_exists; +use function is_array; + class ApplicationFactory { + public const MESSAGE_MISSING_CONFIG = 'Unable to find config.'; + public const MESSAGE_MISSING_PACKAGE_CONFIG = 'Unable to find dot-cli config.'; + public const MESSAGE_MISSING_CONFIG_COMMANDS = 'Unable to find dot-cli config: commands.'; + /** - * @param ContainerInterface $container - * @return Application + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception */ public function __invoke(ContainerInterface $container): Application { - $config = $container->get('config')['dot_cli'] ?? []; - Assert::isMap($config); + if (! $container->has('config')) { + throw new Exception(self::MESSAGE_MISSING_CONFIG); + } + $config = $container->get('config'); - $version = InstalledVersions::getPrettyVersion('laminas/laminas-cli'); - Assert::string($version); + if ( + ! array_key_exists('dot_cli', $config) + || ! is_array($config['dot_cli']) + || empty($config['dot_cli']) + ) { + throw new Exception(self::MESSAGE_MISSING_PACKAGE_CONFIG); + } + $config = $config['dot_cli']; - $commands = $config['commands'] ?? []; - Assert::isMap($commands); - Assert::allString($commands); + if ( + ! array_key_exists('commands', $config) + || ! is_array($config['commands']) + || empty($config['commands']) + ) { + throw new Exception(self::MESSAGE_MISSING_CONFIG_COMMANDS); + } $eventDispatcherServiceName = __NAMESPACE__ . '\SymfonyEventDispatcher'; $dispatcher = $container->has($eventDispatcherServiceName) ? $container->get($eventDispatcherServiceName) : new EventDispatcher(); - Assert::isInstanceOf($dispatcher, EventDispatcherInterface::class); - $dispatcher->addListener(ConsoleEvents::TERMINATE, new TerminateListener($config)); - $fileLocker = $container->get(FileLockerInterface::class); - Assert::isInstanceOf($fileLocker, FileLockerInterface::class); + if (! $container->has(FileLockerInterface::class)) { + throw new Exception(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER); + } - $application = new \Dot\Cli\Application($fileLocker, $config); - // phpcs:ignore WebimpressCodingStandard.PHP.CorrectClassNameCase - $application->setCommandLoader(new ContainerCommandLoader($container, $commands)); + $application = new \Dot\Cli\Application( + $container->get(FileLockerInterface::class), + $config + ); + $application->setCommandLoader(new ContainerCommandLoader($container, $config['commands'])); $application->setDispatcher($dispatcher); $application->setAutoExit(false); diff --git a/src/Factory/FileLockerFactory.php b/src/Factory/FileLockerFactory.php index 7970e1e..34d1649 100644 --- a/src/Factory/FileLockerFactory.php +++ b/src/Factory/FileLockerFactory.php @@ -6,30 +6,61 @@ use Dot\Cli\FileLocker; use Dot\Cli\FileLockerInterface; +use Exception; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; -use Webmozart\Assert\Assert; +use Psr\Container\NotFoundExceptionInterface; + +use function array_key_exists; +use function is_array; +use function is_bool; +use function is_string; -/** - * Class FileLockerFactory - * @package Dot\Cli\Factory - */ class FileLockerFactory { + public const MESSAGE_MISSING_FILE_LOCKER = 'Unable to find FileLocker service.'; + public const MESSAGE_MISSING_FILE_LOCKER_CONFIG = 'Missing/invalid dot-cli FileLocker config.'; + public const MESSAGE_MISSING_FILE_LOCKER_CONFIG_ENABLED = 'Missing/invalid dot-cli FileLocker config: enabled'; + public const MESSAGE_MISSING_FILE_LOCKER_CONFIG_DIR_PATH = 'Missing/invalid dot-cli FileLocker config: dirPath'; + /** - * @param ContainerInterface $container - * @return FileLockerInterface + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception */ public function __invoke(ContainerInterface $container): FileLockerInterface { - $config = $container->get('config')[FileLockerInterface::class] ?? []; - Assert::isMap($config); + if (! $container->has('config')) { + throw new Exception(ApplicationFactory::MESSAGE_MISSING_CONFIG); + } + $config = $container->get('config'); + + if ( + ! array_key_exists(FileLockerInterface::class, $config) + || ! is_array($config[FileLockerInterface::class]) + || empty($config[FileLockerInterface::class]) + ) { + throw new Exception(self::MESSAGE_MISSING_FILE_LOCKER_CONFIG); + } + $config = $config[FileLockerInterface::class]; - $dirPath = $config['dirPath']; - Assert::stringNotEmpty($dirPath); + if ( + ! array_key_exists('enabled', $config) + || ! is_bool($config['enabled']) + ) { + throw new Exception(self::MESSAGE_MISSING_FILE_LOCKER_CONFIG_ENABLED); + } - $fileLocker = new FileLocker(); - $fileLocker->setEnabled($config['enabled'] ?? false)->setDirPath($dirPath); + if ( + ! array_key_exists('dirPath', $config) + || ! is_string($config['dirPath']) + || empty($config['dirPath']) + ) { + throw new Exception(self::MESSAGE_MISSING_FILE_LOCKER_CONFIG_DIR_PATH); + } - return $fileLocker; + return (new FileLocker()) + ->setEnabled($config['enabled']) + ->setDirPath($config['dirPath']); } } diff --git a/src/FileLocker.php b/src/FileLocker.php index bf232e2..b1337c5 100644 --- a/src/FileLocker.php +++ b/src/FileLocker.php @@ -7,48 +7,61 @@ use Exception; use function fclose; -use function file_exists; use function flock; use function fopen; -use function is_dir; -use function mkdir; use function rtrim; use function sprintf; -use function unlink; +use function str_replace; + +use const LOCK_EX; +use const LOCK_NB; +use const LOCK_UN; -/** - * Class FileLocker - * @package Dot\Cli - */ class FileLocker implements FileLockerInterface { private bool $enabled; private ?string $dirPath; private ?string $commandName; - private $lockFile; - - /** - * @return $this - */ + private mixed $lockFile; + + public function __construct( + bool $enabled = false, + ?string $dirPath = null, + ?string $commandName = null, + mixed $lockFile = null, + ) { + $this->enabled = $enabled; + $this->dirPath = $dirPath; + $this->commandName = $commandName; + $this->lockFile = $lockFile; + } + public function initLockFile(): self { $this->lockFile = fopen($this->getLockFilePath(), 'w+'); return $this; } - - /** - * @return bool - */ + + public function enable(): self + { + $this->enabled = true; + + return $this; + } + + public function disable(): self + { + $this->enabled = false; + + return $this; + } + public function isEnabled(): bool { return $this->enabled; } - /** - * @param bool $enabled - * @return $this - */ public function setEnabled(bool $enabled): self { $this->enabled = $enabled; @@ -56,39 +69,25 @@ public function setEnabled(bool $enabled): self return $this; } - /** - * @return string|null - */ public function getDirPath(): ?string { return $this->dirPath; } - /** - * @param string|null $dirPath - * @return $this - */ public function setDirPath(?string $dirPath): self { - $dirPath = rtrim($dirPath, '/'); - $dirPath = rtrim($dirPath, '\\'); + $dirPath = rtrim($dirPath, '/'); + $dirPath = rtrim($dirPath, '\\'); $this->dirPath = $dirPath; return $this; } - /** - * @return string|null - */ public function getCommandName(): ?string { return $this->commandName; } - /** - * @param string|null $commandName - * @return $this - */ public function setCommandName(?string $commandName): self { $this->commandName = $commandName; @@ -96,41 +95,33 @@ public function setCommandName(?string $commandName): self return $this; } - /** - * @return false|resource - */ - public function getLockFile(): bool + public function getLockFile(): mixed { return $this->lockFile; } - /** - * @param bool $lockFile - * @return $this - */ - public function setLockFile(bool $lockFile): self + public function setLockFile(mixed $lockFile): self { $this->lockFile = $lockFile; - + return $this; } - /** - * @return string - */ public function getLockFilePath(): string { - $commandName = str_replace(':', '-', $this->commandName); - return sprintf('%s/command-%s.lock', $this->dirPath, $commandName); + return sprintf( + '%s/command-%s.lock', + $this->dirPath, + str_replace(':', '-', $this->commandName) + ); } /** - * @throws Exception - * @return void + * @inheritDoc */ public function lock(): void { - if (!$this->enabled) { + if (! $this->enabled) { return; } @@ -140,19 +131,16 @@ public function lock(): void $this->initLockFile(); - if (!flock($this->lockFile, LOCK_EX|LOCK_NB, $wouldBlock)) { + if (! flock($this->lockFile, LOCK_EX | LOCK_NB, $wouldBlock)) { if ($wouldBlock) { - throw new \Exception('Another process holds the lock!'); + throw new Exception('The file lock is being held by a different process'); } } } - /** - * @return void - */ public function unlock(): void { - if (!$this->enabled) { + if (! $this->enabled) { return; } diff --git a/src/FileLockerInterface.php b/src/FileLockerInterface.php index 58bada8..f8b47e1 100644 --- a/src/FileLockerInterface.php +++ b/src/FileLockerInterface.php @@ -4,57 +4,38 @@ namespace Dot\Cli; -/** - * Interface FileLocker - * @package Dot\Cli - */ +use Exception; + interface FileLockerInterface { - /** - * @return bool - */ + public function initLockFile(): self; + + public function enable(): self; + + public function disable(): self; + public function isEnabled(): bool; - /** - * @param bool $enabled - * @return self - */ public function setEnabled(bool $enabled): self; - /** - * @return string|null - */ public function getDirPath(): ?string; - /** - * @param string|null $dirPath - * @return $this - */ public function setDirPath(?string $dirPath): self; - /** - * @return string|null - */ public function getCommandName(): ?string; - /** - * @param string|null $commandName - * @return $this - */ public function setCommandName(?string $commandName): self; - /** - * @return string - */ + public function getLockFile(): mixed; + + public function setLockFile(mixed $lockFile): self; + public function getLockFilePath(): string; /** - * @return void + * @throws Exception */ public function lock(): void; - /** - * @return void - */ public function unlock(): void; } diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php new file mode 100644 index 0000000..316ae82 --- /dev/null +++ b/test/ApplicationTest.php @@ -0,0 +1,38 @@ +getConfig(); + + $fileLocker = new FileLocker( + $config[FileLockerInterface::class]['enabled'], + $config[FileLockerInterface::class]['dirPath'] + ); + $input = new StringInput('list'); + $output = new BufferedOutput(); + + $application = new Application($fileLocker, $this->getConfig()['dot_cli']); + $result = $application->doRun($input, $output); + $this->assertSame($result, Command::SUCCESS); + } +} diff --git a/test/Command/DemoCommandTest.php b/test/Command/DemoCommandTest.php new file mode 100644 index 0000000..1987287 --- /dev/null +++ b/test/Command/DemoCommandTest.php @@ -0,0 +1,41 @@ +assertInstanceOf(DemoCommand::class, $command); + } + + /** + * @throws ReflectionException + */ + public function testCommandWillExecute(): void + { + $command = new DemoCommand(); + $reflection = new ReflectionMethod(DemoCommand::class, 'execute'); + + $result = $reflection->invoke( + $command, + new ArgvInput(), + new BufferedOutput() + ); + $this->assertSame($result, Command::SUCCESS); + } +} diff --git a/test/CommonTrait.php b/test/CommonTrait.php new file mode 100644 index 0000000..fb1589e --- /dev/null +++ b/test/CommonTrait.php @@ -0,0 +1,51 @@ +fileSystem = vfsStream::setup('root', 0644, [ + 'data' => [ + 'lock' => [], + ], + ]); + $this->config = $this->generateConfig( + sprintf('%s/data/lock', $this->fileSystem->url()) + ); + } + + protected function getConfig(): array + { + return $this->config; + } + + protected function generateConfig(string $dirPath): array + { + return [ + 'dot_cli' => [ + 'version' => '1.0.0', + 'name' => 'DotKernel CLI', + 'commands' => [ + 'test' => 'test', + ], + ], + FileLockerInterface::class => [ + 'enabled' => true, + 'dirPath' => $dirPath, + ], + ]; + } +} diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php new file mode 100644 index 0000000..07d36b1 --- /dev/null +++ b/test/ConfigProviderTest.php @@ -0,0 +1,39 @@ +config = (new ConfigProvider())(); + } + + public function testHasDependencies(): void + { + $this->assertArrayHasKey('dependencies', $this->config); + } + + public function testDependenciesHasFactories(): void + { + $this->assertArrayHasKey('factories', $this->config['dependencies']); + $this->assertArrayHasKey(Application::class, $this->config['dependencies']['factories']); + $this->assertArrayHasKey(FileLocker::class, $this->config['dependencies']['factories']); + } + + public function testDependenciesHasAliases(): void + { + $this->assertArrayHasKey('aliases', $this->config['dependencies']); + $this->assertArrayHasKey(FileLockerInterface::class, $this->config['dependencies']['aliases']); + } +} diff --git a/test/Factory/ApplicationFactoryTest.php b/test/Factory/ApplicationFactoryTest.php new file mode 100644 index 0000000..04cb48b --- /dev/null +++ b/test/Factory/ApplicationFactoryTest.php @@ -0,0 +1,144 @@ +createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(false); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_CONFIG); + (new ApplicationFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateApplicationWithoutPackageConfig(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + 'test', + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_PACKAGE_CONFIG); + (new ApplicationFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateApplicationWithoutConfigCommands(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + 'dot_cli' => [ + 'test', + ], + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_CONFIG_COMMANDS); + (new ApplicationFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateApplicationWithoutFileLockerService(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->method('has')->willReturnMap([ + ['config', true], + ['Dot\Cli\Factory\SymfonyEventDispatcher', false], + [FileLockerInterface::class, false], + ]); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn($this->getConfig()); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER); + (new ApplicationFactory())($container); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testWillCreateApplication(): void + { + $container = $this->createMock(ContainerInterface::class); + $fileLocker = $this->createMock(FileLockerInterface::class); + + $container->method('has')->willReturnMap([ + ['config', true], + ['Dot\Cli\Factory\SymfonyEventDispatcher', false], + [FileLockerInterface::class, true], + ]); + + $container->method('get')->willReturnMap([ + ['config', $this->getConfig()], + [FileLockerInterface::class, $fileLocker], + ]); + + $application = (new ApplicationFactory())($container); + $this->assertInstanceOf(Application::class, $application); + } +} diff --git a/test/Factory/FileLockerFactoryTest.php b/test/Factory/FileLockerFactoryTest.php new file mode 100644 index 0000000..0e2442f --- /dev/null +++ b/test/Factory/FileLockerFactoryTest.php @@ -0,0 +1,145 @@ +createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(false); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(ApplicationFactory::MESSAGE_MISSING_CONFIG); + (new FileLockerFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateServiceWithoutFileLockerConfig(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + 'test', + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER_CONFIG); + (new FileLockerFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateServiceWithoutFileLockerConfigEnabled(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + FileLockerInterface::class => [ + 'dirPath' => 'test', + ], + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER_CONFIG_ENABLED); + (new FileLockerFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotCreateServiceWithoutFileLockerConfigDirPath(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn([ + FileLockerInterface::class => [ + 'enabled' => false, + ], + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage(FileLockerFactory::MESSAGE_MISSING_FILE_LOCKER_CONFIG_DIR_PATH); + (new FileLockerFactory())($container); + } + + /** + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testCanCreateService(): void + { + $container = $this->createMock(ContainerInterface::class); + + $container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn($this->getConfig()); + + $service = (new FileLockerFactory())($container); + $this->assertInstanceOf(FileLocker::class, $service); + } +} diff --git a/test/FileLockerTest.php b/test/FileLockerTest.php new file mode 100644 index 0000000..dde3fe0 --- /dev/null +++ b/test/FileLockerTest.php @@ -0,0 +1,162 @@ +fileSystem = vfsStream::setup('root', 0644, [ + 'data' => [ + 'lock' => [], + ], + ]); + $this->config = $this->generateConfig( + sprintf('%s/data/lock', $this->fileSystem->url()) + ); + } + + public function testAccessors(): void + { + $fileLocker = new FileLocker(); + $this->assertFalse($fileLocker->isEnabled()); + $fileLocker->enable(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertTrue($fileLocker->isEnabled()); + $fileLocker->disable(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertFalse($fileLocker->isEnabled()); + $fileLocker->setEnabled(true); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertTrue($fileLocker->isEnabled()); + $fileLocker->setEnabled(false); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertFalse($fileLocker->isEnabled()); + $this->assertNull($fileLocker->getDirPath()); + $fileLocker->setDirPath('test'); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertSame('test', $fileLocker->getDirPath()); + $this->assertNull($fileLocker->getCommandName()); + $fileLocker->setCommandName('test'); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertSame('test', $fileLocker->getCommandName()); + $this->assertNull($fileLocker->getLockFile()); + $fileLocker->setLockFile('test'); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertSame('test', $fileLocker->getLockFile()); + $this->assertSame('test/command-test.lock', $fileLocker->getLockFilePath()); + } + + public function testWillInitLockFile(): void + { + $config = $this->getConfig(); + + $fileLocker = new FileLocker(true, $config['dirPath'], 'test'); + $this->assertNull($fileLocker->getLockFile()); + $fileLocker->initLockFile(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertIsResource($fileLocker->getLockFile()); + } + + /** + * @throws Exception + */ + public function testWillNotLockWhenDisabled(): void + { + $config = $this->getConfig(); + + $fileLocker = new FileLocker(false, $config['dirPath'], 'test'); + $this->assertNull($fileLocker->getLockFile()); + $fileLocker->lock(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertNull($fileLocker->getLockFile()); + } + + /** + * @throws Exception + */ + public function testWillNotLockWithoutValidCommandName(): void + { + $config = $this->getConfig(); + + $fileLocker = new FileLocker(true, $config['dirPath']); + $this->assertNull($fileLocker->getLockFile()); + $fileLocker->lock(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertNull($fileLocker->getLockFile()); + } + + /** + * @throws Exception + */ + public function testWillLockWhenLockedAndEnabledAndHasValidCommandName(): void + { + $config = $this->getConfig(); + + $fileLocker = new FileLocker(true, $config['dirPath'], 'test'); + $this->assertNull($fileLocker->getLockFile()); + $fileLocker->lock(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertIsResource($fileLocker->getLockFile()); + $this->assertFileExists($fileLocker->getLockFilePath()); + } + + public function testWillNotUnlockWhenDisabled(): void + { + $config = $this->getConfig(); + + $fileLocker = new FileLocker(false, $config['dirPath'], 'test'); + $this->assertNull($fileLocker->getLockFile()); + $fileLocker->unlock(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertNull($fileLocker->getLockFile()); + } + + public function testWillNotUnlockWithoutValidCommandName(): void + { + $config = $this->getConfig(); + + $fileLocker = new FileLocker(false, $config['dirPath']); + $this->assertNull($fileLocker->getLockFile()); + $fileLocker->unlock(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertNull($fileLocker->getLockFile()); + } + + /** + * @throws Exception + */ + public function testWillUnlockWhenLockedAndEnabledAndHasValidCommandName(): void + { + $config = $this->getConfig(); + + $fileLocker = new FileLocker(true, $config['dirPath'], 'test'); + $fileLocker->lock(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertIsResource($fileLocker->getLockFile()); + $this->assertFileExists($fileLocker->getLockFilePath()); + $fileLocker->unlock(); + $this->assertInstanceOf(FileLocker::class, $fileLocker); + $this->assertFileIsReadable($fileLocker->getLockFilePath()); + $this->assertFileIsWritable($fileLocker->getLockFilePath()); + } + + protected function generateConfig(string $dirPath): array + { + return [ + 'enabled' => true, + 'dirPath' => $dirPath, + ]; + } +} From 1377bf23898a3263beaf610fbbe1d8c0401085ab Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Wed, 5 Jul 2023 09:37:55 +0300 Subject: [PATCH 2/2] typo fix Signed-off-by: alexmerlin --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 5122d89..4a1bf00 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,7 +4,7 @@ bootstrap="./vendor/autoload.php" colors="true"> - + ./test