From fdef627f640c63e95d804c11ac44d90d0baf2f99 Mon Sep 17 00:00:00 2001 From: Robert M <48888686+robertmarney@users.noreply.github.com> Date: Tue, 2 May 2023 13:20:41 -0600 Subject: [PATCH] Adds zScan method to Mock (#112) * Add ZScan Method Co-authored-by: Thomas --- README.md | 1 + src/M6Web/Component/RedisMock/RedisMock.php | 42 +++++++++++++ .../Component/RedisMock/RedisMockFactory.php | 1 + tests/units/RedisMock.php | 61 +++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/README.md b/README.md index 10b627e..7980340 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Redis command | Description **ZREMRANGEBYSCORE** *key* *min* *max* | Removes all members in a sorted set within the given scores **ZREVRANGE** *key* *start* *stop* *[withscores]*| Returns the specified range of members in a sorted set, with scores ordered from high to low **ZREVRANGEBYSCORE** *key* *min* *max* *options* | Returns a range of members in a sorted set, by score, with scores ordered from high to low +**ZSCAN** | Iterates elements of Sorted Sets types. **ZSCORE** *key* *member* | Returns the score of *member* in the sorted set at *key* **ZUNIONSTORE** *dest* *numkeys* *key* ... *[weights ...]* *[aggregate SUM/MIN/MAX]* | Computes the union of the stored sets given by the specified keys, store the result in the destination key, and returns the number of elements of the new sorted set. diff --git a/src/M6Web/Component/RedisMock/RedisMock.php b/src/M6Web/Component/RedisMock/RedisMock.php index a4947e5..c19ce23 100644 --- a/src/M6Web/Component/RedisMock/RedisMock.php +++ b/src/M6Web/Component/RedisMock/RedisMock.php @@ -1211,6 +1211,48 @@ public function zunionstore($destination, array $keys, array $options = array()) return $this->zcount($destination, '-inf', '+inf'); } + /** + * Mock the `zscan` command + * @see https://redis.io/commands/zscan + * @param string $key + * @param int $cursor + * @param array $options contain options of the command, with values (ex ['MATCH' => 'st*', 'COUNT' => 42] ) + * @return $this|array|mixed + */ + public function zscan($key, $cursor, $options = []) + { + $options = array_change_key_case($options, CASE_UPPER); // normalize to match Laravel/Symfony + $count = isset($options[ 'COUNT' ]) ? (int)$options[ 'COUNT' ] : 10; + $match = isset($options[ 'MATCH' ]) ? $options[ 'MATCH' ] : '*'; + $pattern = sprintf('/^%s$/', str_replace(['*', '/'], ['.*', '\/'], $match)); + + $iterator = $cursor; + + if (!isset(self::$dataValues[$this->storage][$key]) || $this->deleteOnTtlExpired($key)) { + return $this->returnPipedInfo([0, []]); + } + + $set = self::$dataValues[$this->storage][$key]; + + if ($match !== '*') { + $set = array_filter($set, function($key) use ($pattern) { + return preg_match($pattern, $key); + }, ARRAY_FILTER_USE_KEY); + } + + $results = array_slice($set, $iterator, $count, true); + $iterator += count($results); + + + if ($count <= count($results)) { + // there are more elements to scan + return $this->returnPipedInfo([$iterator, $results]); + } else { + // the end of the list has been reached + return $this->returnPipedInfo([0, $results]); + } + } + // Server public function dbsize() diff --git a/src/M6Web/Component/RedisMock/RedisMockFactory.php b/src/M6Web/Component/RedisMock/RedisMockFactory.php index b8b2ea1..7497ed3 100644 --- a/src/M6Web/Component/RedisMock/RedisMockFactory.php +++ b/src/M6Web/Component/RedisMock/RedisMockFactory.php @@ -150,6 +150,7 @@ class RedisMockFactory 'zrevrange', 'zrevrangebyscore', 'zrevrank', + 'zscan', 'zscore', 'zunionstore', 'scan', diff --git a/tests/units/RedisMock.php b/tests/units/RedisMock.php index ad7651c..9f78987 100644 --- a/tests/units/RedisMock.php +++ b/tests/units/RedisMock.php @@ -2074,6 +2074,67 @@ public function testSscanCommand() ->isEqualTo([0, []]); } + public function testZscanCommand() + { + $redisMock = new Redis(); + $redisMock->zadd('set1', 1, 'a:1'); + $redisMock->zadd('set1', 2, 'b:1'); + $redisMock->zadd('set1', 3, 'c:1'); + $redisMock->zadd('set1', 4, 'd:1'); + + // Could be removed: ensure we have some noise of multiple sets + $redisMock->zadd('set2', 1, 'x:1'); + $redisMock->zadd('set2', 2, 'y:1'); + $redisMock->zadd('set2', 3, 'z:1'); + + // It must return no values, as the key is unknown. + $this->assert + ->array($redisMock->zscan('unknown', 0, ['COUNT' => 10])) + ->isEqualTo([0, []]); + + + // It must return all the values with score greater than or equal to 1. + $this->assert + ->array($redisMock->zscan('set1', 0, ['MATCH' => '*', 'COUNT' => 10])) + ->isEqualTo([0 => 0, 1 => ['a:1' => 1, 'b:1' => 2, 'c:1' => 3, 'd:1' => 4]]); + + // It must return only the matched value + $this->assert + ->array($redisMock->zscan('set1', 0, ['MATCH' => 'c*', 'COUNT' => 10])) + ->isEqualTo([0 => 0, 1 => ['c:1' => 3]]); + + // It must return all of the values based on the match of *1 + $this->assert + ->array($redisMock->zscan('set1', 0, ['MATCH' => '*1', 'COUNT' => 10])) + ->isEqualTo([0 => 0, 1 => ['a:1' => 1, 'b:1' => 2, 'c:1' => 3, 'd:1' => 4]]); + + // It must return two values, starting cursor after the first value of the list. + + $this->assert + ->array($redisMock->zscan('set1', 1, ['COUNT' => 2])) + ->isEqualTo([3, ['b:1' => 2, 'c:1' => 3]]); + + // Ensure if our results are complete we return a zero cursor + $this->assert + ->array($redisMock->zscan('set1', 3, ['COUNT' => 2])) + ->isEqualTo([0, ['d:1' => 4]]); + + // It must return all the values with score greater than or equal to 3, + // starting cursor after the last value of the previous scan. + $this->assert + ->array($redisMock->zscan('set1', 4, ['MATCH' => '*', 'COUNT' => 10])) + ->isEqualTo([0 => 0, 1 => []]); + + $redisMock->expire('set1', 1); + sleep(2); + + // It must return no values, as the key is expired. + $this->assert + ->array($redisMock->zscan('set1', 0, ['COUNT' => 2])) + ->isEqualTo([0, []]); + + } + public function testBitcountCommand() { $redisMock = new Redis();