From 9d99caf050ac75e3534eb40f5a5ac9d4fa8e2e48 Mon Sep 17 00:00:00 2001 From: "Thomas A. Hirsch" Date: Wed, 28 Dec 2022 13:01:00 +0100 Subject: [PATCH 1/3] sscan implementation based on existing scan method. (Fixes #98) --- README.md | 1 + src/M6Web/Component/RedisMock/RedisMock.php | 48 +++++++++++++++++++++ tests/units/RedisMock.php | 27 ++++++++++++ 3 files changed, 76 insertions(+) diff --git a/README.md b/README.md index e194d25..f259f29 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Redis command | Description **RPUSH** *key* *value* | Pushs values at the tail of a list **RPOP** *key* | Pops values at the tail of a list **SCAN** | Iterates the set of keys in the currently selected Redis database. +**SSCAN** | Iterates elements of Sets types. **SET** *key* *value* | Sets the string value of a key **SETEX** *key* *seconds* *value* | Sets the value and expiration of a key **SETNX** *key* *value* | Sets key to hold value if key does not exist diff --git a/src/M6Web/Component/RedisMock/RedisMock.php b/src/M6Web/Component/RedisMock/RedisMock.php index dc4177b..dd17ae8 100644 --- a/src/M6Web/Component/RedisMock/RedisMock.php +++ b/src/M6Web/Component/RedisMock/RedisMock.php @@ -464,6 +464,54 @@ public function sismember($key, $member) return $this->returnPipedInfo(1); } + + /** + * Mock the `sscan` command + * @see https://redis.io/commands/sscan + * @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 sscan($key, $cursor = 0, array $options = []) + { + $match = isset($options['MATCH']) ? $options['MATCH'] : '*'; + $count = isset($options['COUNT']) ? $options['COUNT'] : 10; + $maximumValue = $cursor + $count -1; + + if (!isset(self::$dataValues[$this->storage][$key]) || $this->deleteOnTtlExpired($key)) { + return $this->returnPipedInfo([0, []]); + } + + // List of all keys in the storage (already ordered by index). + $set = self::$dataValues[$this->storage][$key]; + $maximumListElement = count($set); + + // Next cursor position + $nextCursorPosition = 0; + // Matched values. + $values = []; + // Pattern, for find matched values. + $pattern = str_replace('*', '.*', sprintf('/^%s$/', $match)); + + for($i = $cursor; $i <= $maximumValue; $i++) + { + if (isset($set[$i])){ + $nextCursorPosition = $i >= $maximumListElement ? 0 : $i + 1; + + if ('*' === $match || 1 === preg_match($pattern, $set[$i])){ + $values[] = $set[$i]; + } + + } else { + // Out of the arrays values, return first element + $nextCursorPosition = 0; + } + } + + return $this->returnPipedInfo([$nextCursorPosition, $values]); + } + // Lists public function llen($key) diff --git a/tests/units/RedisMock.php b/tests/units/RedisMock.php index 243fe7f..773bdbe 100644 --- a/tests/units/RedisMock.php +++ b/tests/units/RedisMock.php @@ -1919,6 +1919,33 @@ public function testScanCommand() } + public function testSscanCommand() + { + $redisMock = new Redis(); + $redisMock->sadd('myKey', 'a1'); + $redisMock->sadd('myKey', ['b1', 'b2', 'b3', 'b4', 'b5', 'b6']); + $redisMock->sadd('myKey', ['c1', 'c2', 'c3']); + + // It must return two values, start cursor after the first value of the list. + $this->assert + ->array($redisMock->sscan('myKey', 1, ['COUNT' => 2])) + ->isEqualTo([3, [0 => 'b1', 1 => 'b2']]); + + + // It must return all the values with match with the regex 'our' (2 keys). + // And the cursor is defined after the default count (10) => the match has not terminate all the list. + $this->assert + ->array($redisMock->sscan('myKey', 0, ['MATCH' => 'c*'])) + ->isEqualTo([10, [0 => 'c1', 1 => 'c2', 2 => 'c3']]); + + // Execute the match at the end of this list, the match not return an element (no one element match with the regex), + // And the list is terminate, return the cursor to the start (0) + $this->assert + ->array($redisMock->sscan('myKey', 11, ['MATCH' => 'c*'])) + ->isEqualTo([0, []]); + + } + public function testBitcountCommand() { $redisMock = new Redis(); From 52c0fc34bd9fa98988ef44105c4d755ada0714c2 Mon Sep 17 00:00:00 2001 From: "Thomas A. Hirsch" Date: Wed, 28 Dec 2022 13:15:38 +0100 Subject: [PATCH 2/3] added more test cases. --- tests/units/RedisMock.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/units/RedisMock.php b/tests/units/RedisMock.php index 773bdbe..c09a12b 100644 --- a/tests/units/RedisMock.php +++ b/tests/units/RedisMock.php @@ -1926,6 +1926,11 @@ public function testSscanCommand() $redisMock->sadd('myKey', ['b1', 'b2', 'b3', 'b4', 'b5', 'b6']); $redisMock->sadd('myKey', ['c1', 'c2', 'c3']); + // It must return no values, as the key is unknown. + $this->assert + ->array($redisMock->sscan('unknown', 1, ['COUNT' => 2])) + ->isEqualTo([0, []]); + // It must return two values, start cursor after the first value of the list. $this->assert ->array($redisMock->sscan('myKey', 1, ['COUNT' => 2])) @@ -1944,6 +1949,13 @@ public function testSscanCommand() ->array($redisMock->sscan('myKey', 11, ['MATCH' => 'c*'])) ->isEqualTo([0, []]); + $redisMock->expire('myKey', 1); + sleep(2); + + // It must return no values, as the key is expired. + $this->assert + ->array($redisMock->sscan('myKey', 1, ['COUNT' => 2])) + ->isEqualTo([0, []]); } public function testBitcountCommand() From 6b74103d3500dde1470a4e1d58e2fa55fa7706c8 Mon Sep 17 00:00:00 2001 From: "Thomas A. Hirsch" Date: Wed, 28 Dec 2022 14:54:43 +0100 Subject: [PATCH 3/3] Handling / in match pattern as inspired from #101. --- src/M6Web/Component/RedisMock/RedisMock.php | 2 +- tests/units/RedisMock.php | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/M6Web/Component/RedisMock/RedisMock.php b/src/M6Web/Component/RedisMock/RedisMock.php index dd17ae8..a8aafb4 100644 --- a/src/M6Web/Component/RedisMock/RedisMock.php +++ b/src/M6Web/Component/RedisMock/RedisMock.php @@ -492,7 +492,7 @@ public function sscan($key, $cursor = 0, array $options = []) // Matched values. $values = []; // Pattern, for find matched values. - $pattern = str_replace('*', '.*', sprintf('/^%s$/', $match)); + $pattern = sprintf('/^%s$/', str_replace(['*', '/'], ['.*', '\/'], $match)); for($i = $cursor; $i <= $maximumValue; $i++) { diff --git a/tests/units/RedisMock.php b/tests/units/RedisMock.php index c09a12b..de0a9e8 100644 --- a/tests/units/RedisMock.php +++ b/tests/units/RedisMock.php @@ -1925,18 +1925,22 @@ public function testSscanCommand() $redisMock->sadd('myKey', 'a1'); $redisMock->sadd('myKey', ['b1', 'b2', 'b3', 'b4', 'b5', 'b6']); $redisMock->sadd('myKey', ['c1', 'c2', 'c3']); + $redisMock->sadd('a/b', 'c/d'); // It must return no values, as the key is unknown. $this->assert ->array($redisMock->sscan('unknown', 1, ['COUNT' => 2])) ->isEqualTo([0, []]); + $this->assert + ->array($redisMock->sscan('a/b', 0, ['MATCH' => 'c/*'])) + ->isEqualTo([0, [0 => 'c/d']]); + // It must return two values, start cursor after the first value of the list. $this->assert ->array($redisMock->sscan('myKey', 1, ['COUNT' => 2])) ->isEqualTo([3, [0 => 'b1', 1 => 'b2']]); - // It must return all the values with match with the regex 'our' (2 keys). // And the cursor is defined after the default count (10) => the match has not terminate all the list. $this->assert