From f8797aaa29501eee4f1797f2ce03faa2e8d90f5b Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Wed, 25 Jan 2023 14:51:22 +0200 Subject: [PATCH] Added support for Redis JSON module commands, added JSON.SET and JSON.GET commands (#868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added CommandResolver, moved resolve command logic there, added ClientConfiguration object * Fixed test, added dependecies * Added resolved command to commands array * Used aggregation approach for modules * Added decorator to check Redis JSON module version * Added support for JSON.SET and JSON.GET commands * Added separate workflow for redis-stack tests * Changed docker imange name to correct one * Fixed indentation * Added test coverage for JSON.GET command * Changed module version resolving using annotations mapping * Re-written CommandResolver test * Update ClientInterface.php * Changes to CI, readme, removed unused modules from configuration * Fixed build badge URL * Refactored annotation check to be generic for each module * Fixed bug with incorrect tests skip * Added CommandResolver, moved resolve command logic there, added ClientConfiguration object * Fixed test, added dependecies * Added resolved command to commands array * Used aggregation approach for modules * Added decorator to check Redis JSON module version * Added support for JSON.SET and JSON.GET commands * Added separate workflow for redis-stack tests * Changed docker imange name to correct one * Fixed indentation * Added test coverage for JSON.GET command * Changed module version resolving using annotations mapping * Re-written CommandResolver test * Update ClientInterface.php * Changes to CI, readme, removed unused modules from configuration * Fixed build badge URL * Refactored annotation check to be generic for each module * Fixed bug with incorrect tests skip * Fixed naming issue with nxXx argument * Removed redundant trait * Fixed NxXxArgument test * add Redis stack tests * don't run tests twice * use * Update stack.yml * Rename workflows * Added version 6.x to workflow * Removed exception thrown to avoid version bug * Resolve conflicts within tests.yml * Codestyle fixes * Removew trailing whitespaces Co-authored-by: Vladyslav Vildanov Co-authored-by: Till Krüss --- .github/workflows/stack.yml | 62 ++++++ .github/workflows/tests.yml | 9 +- src/ClientConfiguration.php | 35 +++ src/ClientContextInterface.php | 2 + src/ClientInterface.php | 2 + src/Command/Redis/INFO.php | 30 +++ src/Command/Redis/Json/JSONGET.php | 57 +++++ src/Command/Redis/Json/JSONSET.php | 41 ++++ src/Command/RedisFactory.php | 23 +- src/Command/Resolver/CommandResolver.php | 63 ++++++ .../Resolver/CommandResolverInterface.php | 24 ++ src/Command/Traits/Json/Indent.php | 54 +++++ src/Command/Traits/Json/Newline.php | 54 +++++ src/Command/Traits/Json/NxXxArgument.php | 64 ++++++ src/Command/Traits/Json/Space.php | 54 +++++ src/Configuration/Option/Commands.php | 3 +- tests/PHPUnit/PredisCommandTestCase.php | 3 +- tests/PHPUnit/PredisTestCase.php | 136 +++++++++++- .../Command/Redis/Json/JSONGET_Test.php | 209 ++++++++++++++++++ .../Command/Redis/Json/JSONSET_Test.php | 186 ++++++++++++++++ tests/Predis/Command/RedisFactoryTest.php | 33 +-- .../Command/Resolver/CommandResolverTest.php | 43 ++++ .../Predis/Command/Traits/Json/IndentTest.php | 87 ++++++++ .../Command/Traits/Json/NewlineTest.php | 87 ++++++++ .../Command/Traits/Json/NxXxArgumentTest.php | 92 ++++++++ .../Predis/Command/Traits/Json/SpaceTest.php | 87 ++++++++ .../Configuration/Option/CommandsTest.php | 3 +- 27 files changed, 1510 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/stack.yml create mode 100644 src/ClientConfiguration.php create mode 100644 src/Command/Redis/Json/JSONGET.php create mode 100644 src/Command/Redis/Json/JSONSET.php create mode 100644 src/Command/Resolver/CommandResolver.php create mode 100644 src/Command/Resolver/CommandResolverInterface.php create mode 100644 src/Command/Traits/Json/Indent.php create mode 100644 src/Command/Traits/Json/Newline.php create mode 100644 src/Command/Traits/Json/NxXxArgument.php create mode 100644 src/Command/Traits/Json/Space.php create mode 100644 tests/Predis/Command/Redis/Json/JSONGET_Test.php create mode 100644 tests/Predis/Command/Redis/Json/JSONSET_Test.php create mode 100644 tests/Predis/Command/Resolver/CommandResolverTest.php create mode 100644 tests/Predis/Command/Traits/Json/IndentTest.php create mode 100644 tests/Predis/Command/Traits/Json/NewlineTest.php create mode 100644 tests/Predis/Command/Traits/Json/NxXxArgumentTest.php create mode 100644 tests/Predis/Command/Traits/Json/SpaceTest.php diff --git a/.github/workflows/stack.yml b/.github/workflows/stack.yml new file mode 100644 index 00000000..df25cac9 --- /dev/null +++ b/.github/workflows/stack.yml @@ -0,0 +1,62 @@ +name: Stack + +on: + push: + branches: + - main + - v2.** + pull_request: + +jobs: + + predis: + name: PHP ${{ matrix.php }} (Redis Stack ${{ matrix.redis }}) + runs-on: ubuntu-latest + + services: + redis: + image: redis/redis-stack-server:${{ matrix.redis }} + options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 + ports: + - 6379:6379 + + strategy: + fail-fast: false + matrix: + php: + - '7.2' + - '7.3' + - '7.4' + - '8.0' + - '8.1' + - '8.2' + redis: + - latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP with Composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Get Composer cache directory + id: composer-cache + run: echo "directory=$(composer config cache-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.directory }} + key: tests-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: tests-php-${{ matrix.php }}-composer + + - name: Install Composer dependencies + env: + PHP_VERSION: ${{ matrix.php }} + run: composer install --ansi --no-progress --prefer-dist + + - name: Run PHPUnit tests + run: vendor/bin/phpunit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35b38266..b8a59d71 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,11 +28,10 @@ jobs: - '8.1' - '8.2' redis: - - '3' - - '4' - - '5' - - '6' - - '7' + - 3 + - 4 + - 5 + - 6 services: redis: diff --git a/src/ClientConfiguration.php b/src/ClientConfiguration.php new file mode 100644 index 00000000..20653d11 --- /dev/null +++ b/src/ClientConfiguration.php @@ -0,0 +1,35 @@ + [ + ['name' => 'Json', 'commandPrefix' => 'JSON'], + ], + ]; + + /** + * Returns available modules with configuration. + * + * @return array|string[][] + */ + public static function getModules(): array + { + return self::$config['modules']; + } +} diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 4913bf14..779b88c6 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -88,6 +88,8 @@ use Predis\Command\CommandInterface; * @method $this hsetnx($key, $field, $value) * @method $this hvals($key) * @method $this hstrlen($key, $field) + * @method $this jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths) + * @method $this jsonset(string $key, string $path, string $value, ?string $subcommand = null) * @method $this blmove(string $source, string $destination, string $where, string $to, int $timeout) * @method $this blpop(array|string $keys, $timeout) * @method $this brpop(array|string $keys, $timeout) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 3d8ddf16..52cccf95 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -97,6 +97,8 @@ use Predis\Response\Status; * @method int hsetnx(string $key, string $field, string $value) * @method array hvals(string $key) * @method int hstrlen(string $key, string $field) + * @method string jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths) + * @method string jsonset(string $key, string $path, string $value, ?string $subcommand = null) * @method string blmove(string $source, string $destination, string $where, string $to, int $timeout) * @method array|null blpop(array|string $keys, int|float $timeout) * @method array|null brpop(array|string $keys, int|float $timeout) diff --git a/src/Command/Redis/INFO.php b/src/Command/Redis/INFO.php index cb13a4cb..26a62c2a 100644 --- a/src/Command/Redis/INFO.php +++ b/src/Command/Redis/INFO.php @@ -97,6 +97,10 @@ class INFO extends RedisCommand */ protected function parseRow($row) { + if (preg_match('/^module:name/', $row)) { + return $this->parseModuleRow($row); + } + [$k, $v] = explode(':', $row, 2); if (preg_match('/^db\d+$/', $k)) { @@ -124,4 +128,30 @@ class INFO extends RedisCommand return $db; } + + /** + * Parsing module rows because of different format. + * + * @param string $row + * @return array + */ + protected function parseModuleRow(string $row): array + { + [$moduleKeyword, $moduleData] = explode(':', $row); + $explodedData = explode(',', $moduleData); + $parsedData = []; + + foreach ($explodedData as $moduleDataRow) { + [$k, $v] = explode('=', $moduleDataRow); + + if ($k === 'name') { + $parsedData[0] = $v; + continue; + } + + $parsedData[1][$k] = $v; + } + + return $parsedData; + } } diff --git a/src/Command/Redis/Json/JSONGET.php b/src/Command/Redis/Json/JSONGET.php new file mode 100644 index 00000000..df8fddb7 --- /dev/null +++ b/src/Command/Redis/Json/JSONGET.php @@ -0,0 +1,57 @@ +setSpace($arguments); + $arguments = $this->getArguments(); + + $this->setNewline($arguments); + $arguments = $this->getArguments(); + + $this->setIndent($arguments); + $this->filterArguments(); + } +} diff --git a/src/Command/Redis/Json/JSONSET.php b/src/Command/Redis/Json/JSONSET.php new file mode 100644 index 00000000..c044cff8 --- /dev/null +++ b/src/Command/Redis/Json/JSONSET.php @@ -0,0 +1,41 @@ +setSubcommand($arguments); + $this->filterArguments(); + } +} diff --git a/src/Command/RedisFactory.php b/src/Command/RedisFactory.php index 4e945573..e0d4b818 100644 --- a/src/Command/RedisFactory.php +++ b/src/Command/RedisFactory.php @@ -12,6 +12,8 @@ namespace Predis\Command; +use Predis\Command\Resolver\CommandResolverInterface; + /** * Command factory for mainline Redis servers. * @@ -23,13 +25,20 @@ namespace Predis\Command; */ class RedisFactory extends Factory { - public function __construct() + /** + * @var CommandResolverInterface + */ + private $commandResolver; + + public function __construct(CommandResolverInterface $commandResolver) { $this->commands = [ 'ECHO' => 'Predis\Command\Redis\ECHO_', 'EVAL' => 'Predis\Command\Redis\EVAL_', 'OBJECT' => 'Predis\Command\Redis\OBJECT_', ]; + + $this->commandResolver = $commandResolver; } /** @@ -40,13 +49,17 @@ class RedisFactory extends Factory $commandID = strtoupper($commandID); if (isset($this->commands[$commandID]) || array_key_exists($commandID, $this->commands)) { - $commandClass = $this->commands[$commandID]; - } elseif (class_exists($commandClass = "Predis\Command\Redis\\$commandID")) { - $this->commands[$commandID] = $commandClass; - } else { + return $this->commands[$commandID]; + } + + $commandClass = $this->commandResolver->resolve($commandID); + + if (null === $commandClass) { return null; } + $this->commands[$commandID] = $commandClass; + return $commandClass; } diff --git a/src/Command/Resolver/CommandResolver.php b/src/Command/Resolver/CommandResolver.php new file mode 100644 index 00000000..eaed290b --- /dev/null +++ b/src/Command/Resolver/CommandResolver.php @@ -0,0 +1,63 @@ +modules = ClientConfiguration::getModules(); + } + + /** + * {@inheritDoc} + */ + public function resolve(string $commandID): ?string + { + if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandID)) { + return $commandClass; + } + + $commandModule = $this->resolveCommandModuleByPrefix($commandID); + + if (null === $commandModule) { + return null; + } + + if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandModule . '\\' . $commandID)) { + return $commandClass; + } + + return null; + } + + private function resolveCommandModuleByPrefix(string $commandID): ?string + { + foreach ($this->modules as $module) { + if (preg_match("/^{$module['commandPrefix']}/", $commandID)) { + return $module['name']; + } + } + + return null; + } +} diff --git a/src/Command/Resolver/CommandResolverInterface.php b/src/Command/Resolver/CommandResolverInterface.php new file mode 100644 index 00000000..341e7811 --- /dev/null +++ b/src/Command/Resolver/CommandResolverInterface.php @@ -0,0 +1,24 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$indentArgumentPositionOffset] === '') { + array_splice($arguments, static::$indentArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$indentArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Indent argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$indentArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$indentArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$indentModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/src/Command/Traits/Json/Newline.php b/src/Command/Traits/Json/Newline.php new file mode 100644 index 00000000..7bab8205 --- /dev/null +++ b/src/Command/Traits/Json/Newline.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$newlineArgumentPositionOffset] === '') { + array_splice($arguments, static::$newlineArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$newlineArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Newline argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$newlineArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$newlineArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$newlineModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/src/Command/Traits/Json/NxXxArgument.php b/src/Command/Traits/Json/NxXxArgument.php new file mode 100644 index 00000000..39d3cb23 --- /dev/null +++ b/src/Command/Traits/Json/NxXxArgument.php @@ -0,0 +1,64 @@ + 'NX', + 'xx' => 'XX', + ]; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$nxXxArgumentPositionOffset >= $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (null === $arguments[static::$nxXxArgumentPositionOffset]) { + array_splice($arguments, static::$nxXxArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$nxXxArgumentPositionOffset]; + + if (!in_array(strtoupper($argument), self::$argumentEnum, true)) { + $enumValues = implode(', ', array_keys(self::$argumentEnum)); + throw new UnexpectedValueException("Argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$nxXxArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$nxXxArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$argumentEnum[strtolower($argument)]], + $argumentsAfter + )); + } +} diff --git a/src/Command/Traits/Json/Space.php b/src/Command/Traits/Json/Space.php new file mode 100644 index 00000000..5c99828f --- /dev/null +++ b/src/Command/Traits/Json/Space.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$spaceArgumentPositionOffset] === '') { + array_splice($arguments, static::$spaceArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$spaceArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Space argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$spaceArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$spaceArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$spaceModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/src/Configuration/Option/Commands.php b/src/Configuration/Option/Commands.php index 2fbe00e7..cb6fc65f 100644 --- a/src/Configuration/Option/Commands.php +++ b/src/Configuration/Option/Commands.php @@ -16,6 +16,7 @@ use InvalidArgumentException; use Predis\Command\FactoryInterface; use Predis\Command\RawFactory; use Predis\Command\RedisFactory; +use Predis\Command\Resolver\CommandResolver; use Predis\Configuration\OptionInterface; use Predis\Configuration\OptionsInterface; @@ -135,7 +136,7 @@ class Commands implements OptionInterface */ public function getDefault(OptionsInterface $options) { - $commands = new RedisFactory(); + $commands = new RedisFactory(new CommandResolver()); if (isset($options->prefix)) { $commands->setProcessor($options->prefix); diff --git a/tests/PHPUnit/PredisCommandTestCase.php b/tests/PHPUnit/PredisCommandTestCase.php index 8e9fb270..9aa6f516 100644 --- a/tests/PHPUnit/PredisCommandTestCase.php +++ b/tests/PHPUnit/PredisCommandTestCase.php @@ -109,9 +109,10 @@ abstract class PredisCommandTestCase extends PredisTestCase public function testCommandId(): void { $command = $this->getCommand(); + $sanitizedCommandId = str_replace('.', '', $command->getId()); $this->assertInstanceOf('Predis\Command\CommandInterface', $command); - $this->assertEquals($this->getExpectedId(), $command->getId()); + $this->assertEquals($this->getExpectedId(), $sanitizedCommandId); } /** diff --git a/tests/PHPUnit/PredisTestCase.php b/tests/PHPUnit/PredisTestCase.php index aad03117..4cd50d3a 100644 --- a/tests/PHPUnit/PredisTestCase.php +++ b/tests/PHPUnit/PredisTestCase.php @@ -23,6 +23,21 @@ use Predis\Connection; abstract class PredisTestCase extends \PHPUnit\Framework\TestCase { protected $redisServerVersion = null; + protected $redisJsonVersion; + + /** + * @var string[] + */ + private $modulesMapping = [ + 'json' => ['annotation' => 'requiresRedisJsonVersion', 'name' => 'ReJSON'], + ]; + + /** + * Info of current Redis instance. + * + * @var array + */ + private $info; /** * {@inheritdoc} @@ -30,6 +45,7 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase protected function setUp(): void { $this->checkRequiredRedisServerVersion(); + $this->checkRequiredRedisModuleVersion('json'); } /** @@ -147,7 +163,9 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase protected function getDefaultOptionsArray(): array { return [ - 'commands' => new Command\RedisFactory(), + 'commands' => new Command\RedisFactory( + new Command\Resolver\CommandResolver() + ), ]; } @@ -187,7 +205,7 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase */ protected function getCommandFactory(): Command\Factory { - return new Command\RedisFactory(); + return new Command\RedisFactory(new Command\Resolver\CommandResolver()); } /** @@ -301,8 +319,13 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase return $this->redisServerVersion; } - $client = $this->createClient(null, null, true); - $info = array_change_key_case($client->info()); + if (isset($this->info)) { + $info = $this->info; + } else { + $client = $this->createClient(null, null, true); + $info = array_change_key_case($client->info()); + $this->info = $info; + } if (isset($info['server']['redis_version'])) { // Redis >= 2.6 @@ -311,6 +334,7 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase // Redis < 2.6 $version = $info['redis_version']; } else { + $client = $this->createClient(null, null, true); $connection = $client->getConnection(); throw new RuntimeException("Unable to retrieve a valid server info payload from $connection"); } @@ -396,6 +420,110 @@ abstract class PredisTestCase extends \PHPUnit\Framework\TestCase } } + /** + * Ensures the current Redis JSON module matches version requirements for tests. + * + * @param string $module + * @return void + */ + protected function checkRequiredRedisModuleVersion(string $module): void + { + if (null === $requiredVersion = $this->getRequiredModuleVersion($module)) { + return; + } + + if (version_compare($this->getRedisServerVersion(), '6.0.0', '<')) { + $this->markTestSkipped( + 'Test skipped because Redis JSON module available since Redis 6.x' + ); + } + + $requiredVersion = explode(' ', $requiredVersion, 2); + + if (count($requiredVersion) === 1) { + $reqVersion = $requiredVersion[0]; + } else { + $reqVersion = $requiredVersion[1]; + } + + if (!$this->isSatisfiedRedisModuleVersion($reqVersion, $module)) { + $redisModuleVersion = $this->getRedisModuleVersion($module); + $module = strtoupper($module); + + $this->markTestSkipped( + "Test requires a Redis $module module >= $reqVersion but target module is $redisModuleVersion" + ); + } + } + + /** + * @param string $versionToCheck + * @param string $module + * @return bool + */ + protected function isSatisfiedRedisModuleVersion(string $versionToCheck, string $module): bool + { + $currentVersion = $this->getRedisModuleVersion($this->modulesMapping[$module]['name']); + $versionToCheck = str_replace('.', '0', $versionToCheck); + + return $currentVersion >= (int) $versionToCheck; + } + + /** + * Returns version of Redis JSON module if it's available. + * + * @param string $module + * @return string + */ + protected function getRedisModuleVersion(string $module): string + { + if (isset($this->info)) { + $info = $this->info; + } else { + $client = $this->createClient(null, null, true); + $info = array_change_key_case($client->info()); + $this->info = $info; + } + + if (isset($info['modules'][$module]['ver'])) { + $this->redisJsonVersion = $info['modules'][$module]['ver']; + + return $info['modules'][$module]['ver']; + } + + return '0'; + } + + /** + * Returns version of given module for current Redis instance. + * Runs if command belong to one of modules and marked with appropriate annotation + * Runs on @connected tests. + * + * @param string $module + * @return string + */ + protected function getRequiredModuleVersion(string $module): ?string + { + if (!isset($this->modulesMapping[$module])) { + throw new InvalidArgumentException('No existing annotation for given module'); + } + + $moduleAnnotation = $this->modulesMapping[$module]['annotation']; + $annotations = TestUtil::parseTestMethodAnnotations( + get_class($this), + $this->getName(false) + ); + + if (isset($annotations['method'][$moduleAnnotation], $annotations['method']['group']) && + !empty($annotations['method'][$moduleAnnotation]) && + in_array('connected', $annotations['method']['group'], true) + ) { + return $annotations['method'][$moduleAnnotation][0]; + } + + return null; + } + /** * Marks current test skipped when test suite is running on CI environments. * diff --git a/tests/Predis/Command/Redis/Json/JSONGET_Test.php b/tests/Predis/Command/Redis/Json/JSONGET_Test.php new file mode 100644 index 00000000..365b3018 --- /dev/null +++ b/tests/Predis/Command/Redis/Json/JSONGET_Test.php @@ -0,0 +1,209 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse(): void + { + $this->assertSame(1, $this->getCommand()->parseResponse(1)); + } + + /** + * @group connected + * @dataProvider jsonProvider + * @param array $jsonData + * @param string $key + * @param string $indent + * @param string $newline + * @param string $space + * @param string $expectedResponse + * @return void + * @requiresRedisJsonVersion >= 1.0.0 + */ + public function testReturnsCorrectJsonResponse( + array $jsonData, + string $key, + string $indent, + string $newline, + string $space, + string $expectedResponse + ): void { + $redis = $this->getClient(); + + $redis->jsonset(...$jsonData); + $this->assertSame($expectedResponse, $redis->jsonget($key, $indent, $newline, $space)); + } + + /** + * @group connected + * @return void + * @requiresRedisJsonVersion >= 1.0.0 + */ + public function testReturnsJsonValuesArrayOnMultiplePathsProvided(): void + { + $redis = $this->getClient(); + + $redis->jsonset('key', '$', '{"key1":"value1","key2":{"key3":"value3"}}'); + $actualResponse = $redis->jsonget('key', '', '', '', '$.key1', '$..key3'); + + $this->assertStringContainsString('"$.key1":["value1"]', $actualResponse); + $this->assertStringContainsString('"$..key3":["value3"]', $actualResponse); + } + + /** + * @group connected + * @dataProvider unexpectedValuesProvider + * @param array $arguments + * @param string $expectedExceptionMessage + * @return void + * @requiresRedisJsonVersion >= 1.0.0 + */ + public function testThrowsExceptionOnUnexpectedValueGiven(array $arguments, string $expectedExceptionMessage): void + { + $redis = $this->getClient(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $redis->jsonget(...$arguments); + } + + public function argumentsProvider(): array + { + return [ + 'with default arguments' => [ + ['key'], + ['key'], + ], + 'with INDENT modifier' => [ + ['key', '\t'], + ['key', 'INDENT', '\t'], + ], + 'with NEWLINE modifier' => [ + ['key', '', '\n'], + ['key', 'NEWLINE', '\n'], + ], + 'with SPACE modifier' => [ + ['key', '', '', ' '], + ['key', 'SPACE', ' '], + ], + 'with multiple paths' => [ + ['key', '', '', '', 'path1', 'path2'], + ['key', 'path1', 'path2'], + ], + 'with all arguments' => [ + ['key', '\t', '\n', ' '], + ['key', 'INDENT', '\t', 'NEWLINE', '\n', 'SPACE', ' '], + ], + ]; + } + + public function jsonProvider(): array + { + return [ + 'with key only' => [ + ['key', '$', '{"key1":"value1","key2":"value2"}'], + 'key', + '', + '', + '', + '{"key1":"value1","key2":"value2"}', + ], + 'with INDENT modifier only' => [ + ['key', '$', '{"key1":"value1","key2":"value2"}'], + 'key', + '\t', + '', + '', + '{\t"key1":"value1",\t"key2":"value2"}', + ], + 'with NEWLINE modifier only' => [ + ['key', '$', '{"key1":"value1","key2":"value2"}'], + 'key', + '', + '\n', + '', + '{\n"key1":"value1",\n"key2":"value2"\n}', + ], + 'with SPACE modifier only' => [ + ['key', '$', '{"key1":"value1","key2":"value2"}'], + 'key', + '', + '', + ' ', + '{"key1": "value1","key2": "value2"}', + ], + 'with all modifiers' => [ + ['key', '$', '{"key1":"value1","key2":"value2"}'], + 'key', + '\t', + '\n', + ' ', + '{\n\t"key1": "value1",\n\t"key2": "value2"\n}', + ], + ]; + } + + public function unexpectedValuesProvider(): array + { + return [ + 'with wrong INDENT modifier' => [ + ['key', 1, '', ''], + 'Indent argument value should be a string', + ], + 'with wrong NEWLINE modifier' => [ + ['key', '', 1, ''], + 'Newline argument value should be a string', + ], + 'with wrong SPACE modifier' => [ + ['key', '', '', 1], + 'Space argument value should be a string', + ], + ]; + } +} diff --git a/tests/Predis/Command/Redis/Json/JSONSET_Test.php b/tests/Predis/Command/Redis/Json/JSONSET_Test.php new file mode 100644 index 00000000..1ff36545 --- /dev/null +++ b/tests/Predis/Command/Redis/Json/JSONSET_Test.php @@ -0,0 +1,186 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse(): void + { + $this->assertSame(1, $this->getCommand()->parseResponse(1)); + } + + /** + * @group connected + * @dataProvider jsonProvider + * @param string $key + * @param string $defaultJson + * @param string $appendedJson + * @param string $path + * @param string|null $nxXxArgument + * @param string|null $expectedResponse + * @param string $expectedJson + * @return void + * @requiresRedisJsonVersion >= 1.0.0 + */ + public function testSetCorrectJsonValueAndReturnsCorrespondingResponse( + string $key, + string $defaultJson, + string $appendedJson, + string $path, + ?string $nxXxArgument, + ?string $expectedResponse, + string $expectedJson + ): void { + $redis = $this->getClient(); + + $this->assertEquals('OK', $redis->jsonset($key, '$', $defaultJson)); + $this->assertEquals($expectedResponse, $redis->jsonset($key, $path, $appendedJson, $nxXxArgument)); + $this->assertSame($expectedJson, $redis->jsonget($key)); + } + + /** + * @group connected + * @return void + * @requiresRedisJsonVersion >= 1.0.0 + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $redis = $this->getClient(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Argument accepts only: nx, xx values'); + + $redis->jsonset('key', '$', 'value', 'wrong'); + } + + public function argumentsProvider(): array + { + return [ + 'with default arguments' => [ + ['key', 'path', 'value'], + ['key', 'path', 'value'], + ], + 'with NX argument' => [ + ['key', 'path', 'value', 'nx'], + ['key', 'path', 'value', 'NX'], + ], + 'with XX argument' => [ + ['key', 'path', 'value', 'xx'], + ['key', 'path', 'value', 'XX'], + ], + ]; + } + + public function jsonProvider(): array + { + return [ + 'override json' => [ + 'key', + '{"key1":"value1","key2":"value2"}', + '{"key3":"value3"}', + '$', + null, + 'OK', + '{"key3":"value3"}', + ], + 'override certain key - without nxXx argument' => [ + 'key', + '{"key1":"value1","key2":"value2"}', + '"value3"', + '$.key2', + null, + 'OK', + '{"key1":"value1","key2":"value3"}', + ], + 'append to json - without nxXx argument' => [ + 'key', + '{"key1":"value1","key2":"value2"}', + '"value3"', + '$.key3', + null, + 'OK', + '{"key1":"value1","key2":"value2","key3":"value3"}', + ], + 'override certain key - with XX argument' => [ + 'key', + '{"key1":"value1","key2":"value2"}', + '"value3"', + '$.key2', + 'xx', + 'OK', + '{"key1":"value1","key2":"value3"}', + ], + 'append to json - with NX argument' => [ + 'key', + '{"key1":"value1","key2":"value2"}', + '"value3"', + '$.key3', + 'nx', + 'OK', + '{"key1":"value1","key2":"value2","key3":"value3"}', + ], + 'override failed with XX argument' => [ + 'key', + '{"key1":"value1","key2":"value2"}', + '"value3"', + '$.key3', + 'xx', + null, + '{"key1":"value1","key2":"value2"}', + ], + 'append failed with NX argument' => [ + 'key', + '{"key1":"value1","key2":"value2"}', + '"value2"', + '$.key2', + 'nx', + null, + '{"key1":"value1","key2":"value2"}', + ], + ]; + } +} diff --git a/tests/Predis/Command/RedisFactoryTest.php b/tests/Predis/Command/RedisFactoryTest.php index 81cfac33..13786ce0 100644 --- a/tests/Predis/Command/RedisFactoryTest.php +++ b/tests/Predis/Command/RedisFactoryTest.php @@ -14,6 +14,7 @@ namespace Predis\Command; use Predis\Command\Processor\ProcessorChain; use Predis\Command\Processor\ProcessorInterface; +use Predis\Command\Resolver\CommandResolver; use PredisTestCase; class RedisFactoryTest extends PredisTestCase @@ -23,7 +24,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testSupportedCommands(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); foreach ($this->getExpectedCommands() as $commandID) { $this->assertTrue($factory->supports($commandID), "Command factory does not support $commandID"); @@ -35,7 +36,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testSupportCommand(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $this->assertTrue($factory->supports('info')); $this->assertTrue($factory->supports('INFO')); @@ -49,7 +50,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testSupportCommands(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $this->assertTrue($factory->supports('get', 'set')); $this->assertTrue($factory->supports('GET', 'SET')); @@ -64,7 +65,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testGetCommandClass(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $this->assertSame('Predis\Command\Redis\PING', $factory->getCommandClass('ping')); $this->assertSame('Predis\Command\Redis\PING', $factory->getCommandClass('PING')); @@ -78,7 +79,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testDefineCommand(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $command = $this->getMockBuilder('Predis\Command\CommandInterface') ->getMock(); @@ -96,7 +97,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testUndefineCommandInClassAutoload(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $this->assertTrue($factory->supports('PING')); $this->assertSame('Predis\Command\Redis\PING', $factory->getCommandClass('PING')); @@ -112,7 +113,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testUndefineCommandInClassMap(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $commandClass = get_class($this->getMockBuilder('Predis\Command\CommandInterface')->getMock()); $factory->define('MOCK', $commandClass); @@ -134,7 +135,7 @@ class RedisFactoryTest extends PredisTestCase $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage("Class stdClass must implement Predis\Command\CommandInterface"); - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $factory->define('mock', 'stdClass'); } @@ -144,7 +145,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testCreateCommandWithoutArguments(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $command = $factory->create('info'); @@ -158,7 +159,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testCreateCommandWithArguments(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $arguments = ['foo', 'bar']; $command = $factory->create('set', $arguments); @@ -176,7 +177,7 @@ class RedisFactoryTest extends PredisTestCase $this->expectException('Predis\ClientException'); $this->expectExceptionMessage('Command `UNKNOWN` is not a registered Redis command.'); - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $factory->create('unknown'); } @@ -186,7 +187,7 @@ class RedisFactoryTest extends PredisTestCase */ public function testGetDefaultProcessor(): void { - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $this->assertNull($factory->getProcessor()); } @@ -201,7 +202,7 @@ class RedisFactoryTest extends PredisTestCase ->getMockBuilder('Predis\Command\Processor\ProcessorInterface') ->getMock(); - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $factory->setProcessor($processor); $this->assertSame($processor, $factory->getProcessor()); @@ -217,7 +218,7 @@ class RedisFactoryTest extends PredisTestCase ->getMockBuilder('Predis\Command\Processor\ProcessorInterface') ->getMock(); - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $factory->setProcessor($processor); $this->assertSame($processor, $factory->getProcessor()); @@ -249,7 +250,7 @@ class RedisFactoryTest extends PredisTestCase } ); - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $factory->setProcessor($processor); $factory->create('set', ['foo', 'bar']); @@ -273,7 +274,7 @@ class RedisFactoryTest extends PredisTestCase $chain->add($processor); $chain->add($processor); - $factory = new RedisFactory(); + $factory = new RedisFactory(new CommandResolver()); $factory->setProcessor($chain); $factory->create('info'); diff --git a/tests/Predis/Command/Resolver/CommandResolverTest.php b/tests/Predis/Command/Resolver/CommandResolverTest.php new file mode 100644 index 00000000..8909f2df --- /dev/null +++ b/tests/Predis/Command/Resolver/CommandResolverTest.php @@ -0,0 +1,43 @@ +assertSame($expectedCommandClass, $resolver->resolve($commandID)); + } + + public function commandsProvider(): array + { + return [ + 'core command exists' => ['SET', SET::class], + 'module not exist' => ['FOOBAR', null], + 'module exists, module command not exists' => ['JSONFOO', null], + ]; + } +} diff --git a/tests/Predis/Command/Traits/Json/IndentTest.php b/tests/Predis/Command/Traits/Json/IndentTest.php new file mode 100644 index 00000000..1b31bed0 --- /dev/null +++ b/tests/Predis/Command/Traits/Json/IndentTest.php @@ -0,0 +1,87 @@ +testClass = new class() extends RedisCommand { + use Indent; + + public static $indentArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $actualArguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments( + int $offset, + array $actualArguments, + array $expectedResponse + ): void { + $this->testClass::$indentArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Indent argument value should be a string'); + + $this->testClass->setArguments([1]); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 2, + ['argument1'], + ['argument1'], + ], + 'with default value' => [ + 0, + [''], + [false], + ], + 'with correct argument' => [ + 0, + ['\t'], + ['INDENT', '\t'], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/Json/NewlineTest.php b/tests/Predis/Command/Traits/Json/NewlineTest.php new file mode 100644 index 00000000..f5bacefa --- /dev/null +++ b/tests/Predis/Command/Traits/Json/NewlineTest.php @@ -0,0 +1,87 @@ +testClass = new class() extends RedisCommand { + use Newline; + + public static $newlineArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $actualArguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments( + int $offset, + array $actualArguments, + array $expectedResponse + ): void { + $this->testClass::$newlineArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Newline argument value should be a string'); + + $this->testClass->setArguments([1]); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 2, + ['argument1'], + ['argument1'], + ], + 'with default value' => [ + 0, + [''], + [false], + ], + 'with correct argument' => [ + 0, + ['\n'], + ['NEWLINE', '\n'], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/Json/NxXxArgumentTest.php b/tests/Predis/Command/Traits/Json/NxXxArgumentTest.php new file mode 100644 index 00000000..04ad1c3f --- /dev/null +++ b/tests/Predis/Command/Traits/Json/NxXxArgumentTest.php @@ -0,0 +1,92 @@ +testClass = new class() extends RedisCommand { + use NxXxArgument; + + public static $nxXxArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $actualArguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments( + int $offset, + array $actualArguments, + array $expectedResponse + ): void { + $this->testClass::$nxXxArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Argument accepts only: nx, xx values'); + + $this->testClass->setArguments(['wrong']); + } + + public function argumentsProvider(): array + { + return [ + 'with correct argument - NX' => [ + 0, + ['nx'], + ['NX'], + ], + 'with correct argument - XX' => [ + 0, + ['xx'], + ['XX'], + ], + 'with wrong offset' => [ + 2, + ['argument1'], + ['argument1'], + ], + 'with null argument' => [ + 0, + [null], + [false], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/Json/SpaceTest.php b/tests/Predis/Command/Traits/Json/SpaceTest.php new file mode 100644 index 00000000..ef5c456b --- /dev/null +++ b/tests/Predis/Command/Traits/Json/SpaceTest.php @@ -0,0 +1,87 @@ +testClass = new class() extends RedisCommand { + use Space; + + public static $spaceArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $actualArguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments( + int $offset, + array $actualArguments, + array $expectedResponse + ): void { + $this->testClass::$spaceArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Space argument value should be a string'); + + $this->testClass->setArguments([1]); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 2, + ['argument1'], + ['argument1'], + ], + 'with default value' => [ + 0, + [''], + [false], + ], + 'with correct argument' => [ + 0, + [' '], + ['SPACE', ' '], + ], + ]; + } +} diff --git a/tests/Predis/Configuration/Option/CommandsTest.php b/tests/Predis/Configuration/Option/CommandsTest.php index 8390cbf6..0776d036 100644 --- a/tests/Predis/Configuration/Option/CommandsTest.php +++ b/tests/Predis/Configuration/Option/CommandsTest.php @@ -15,6 +15,7 @@ namespace Predis\Configuration\Option; use PHPUnit\Framework\MockObject\MockObject; use Predis\Command\Processor\KeyPrefixProcessor; use Predis\Command\RedisFactory; +use Predis\Command\Resolver\CommandResolver; use Predis\Configuration\OptionsInterface; use PredisTestCase; use stdClass; @@ -76,7 +77,7 @@ class CommandsTest extends PredisTestCase /** @var OptionsInterface */ $options = $this->getMockBuilder('Predis\Configuration\OptionsInterface')->getMock(); - $input = new RedisFactory(); + $input = new RedisFactory(new CommandResolver()); $commands = $option->filter($options, $input);