Skip to content

Commit fdef627

Browse files
Adds zScan method to Mock (#112)
* Add ZScan Method Co-authored-by: Thomas <[email protected]>
1 parent db379bd commit fdef627

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Redis command | Description
7979
**ZREMRANGEBYSCORE** *key* *min* *max* | Removes all members in a sorted set within the given scores
8080
**ZREVRANGE** *key* *start* *stop* *[withscores]*| Returns the specified range of members in a sorted set, with scores ordered from high to low
8181
**ZREVRANGEBYSCORE** *key* *min* *max* *options* | Returns a range of members in a sorted set, by score, with scores ordered from high to low
82+
**ZSCAN** | Iterates elements of Sorted Sets types.
8283
**ZSCORE** *key* *member* | Returns the score of *member* in the sorted set at *key*
8384
**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.
8485

src/M6Web/Component/RedisMock/RedisMock.php

+42
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,48 @@ public function zunionstore($destination, array $keys, array $options = array())
12111211
return $this->zcount($destination, '-inf', '+inf');
12121212
}
12131213

1214+
/**
1215+
* Mock the `zscan` command
1216+
* @see https://redis.io/commands/zscan
1217+
* @param string $key
1218+
* @param int $cursor
1219+
* @param array $options contain options of the command, with values (ex ['MATCH' => 'st*', 'COUNT' => 42] )
1220+
* @return $this|array|mixed
1221+
*/
1222+
public function zscan($key, $cursor, $options = [])
1223+
{
1224+
$options = array_change_key_case($options, CASE_UPPER); // normalize to match Laravel/Symfony
1225+
$count = isset($options[ 'COUNT' ]) ? (int)$options[ 'COUNT' ] : 10;
1226+
$match = isset($options[ 'MATCH' ]) ? $options[ 'MATCH' ] : '*';
1227+
$pattern = sprintf('/^%s$/', str_replace(['*', '/'], ['.*', '\/'], $match));
1228+
1229+
$iterator = $cursor;
1230+
1231+
if (!isset(self::$dataValues[$this->storage][$key]) || $this->deleteOnTtlExpired($key)) {
1232+
return $this->returnPipedInfo([0, []]);
1233+
}
1234+
1235+
$set = self::$dataValues[$this->storage][$key];
1236+
1237+
if ($match !== '*') {
1238+
$set = array_filter($set, function($key) use ($pattern) {
1239+
return preg_match($pattern, $key);
1240+
}, ARRAY_FILTER_USE_KEY);
1241+
}
1242+
1243+
$results = array_slice($set, $iterator, $count, true);
1244+
$iterator += count($results);
1245+
1246+
1247+
if ($count <= count($results)) {
1248+
// there are more elements to scan
1249+
return $this->returnPipedInfo([$iterator, $results]);
1250+
} else {
1251+
// the end of the list has been reached
1252+
return $this->returnPipedInfo([0, $results]);
1253+
}
1254+
}
1255+
12141256
// Server
12151257

12161258
public function dbsize()

src/M6Web/Component/RedisMock/RedisMockFactory.php

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class RedisMockFactory
150150
'zrevrange',
151151
'zrevrangebyscore',
152152
'zrevrank',
153+
'zscan',
153154
'zscore',
154155
'zunionstore',
155156
'scan',

tests/units/RedisMock.php

+61
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,67 @@ public function testSscanCommand()
20742074
->isEqualTo([0, []]);
20752075
}
20762076

2077+
public function testZscanCommand()
2078+
{
2079+
$redisMock = new Redis();
2080+
$redisMock->zadd('set1', 1, 'a:1');
2081+
$redisMock->zadd('set1', 2, 'b:1');
2082+
$redisMock->zadd('set1', 3, 'c:1');
2083+
$redisMock->zadd('set1', 4, 'd:1');
2084+
2085+
// Could be removed: ensure we have some noise of multiple sets
2086+
$redisMock->zadd('set2', 1, 'x:1');
2087+
$redisMock->zadd('set2', 2, 'y:1');
2088+
$redisMock->zadd('set2', 3, 'z:1');
2089+
2090+
// It must return no values, as the key is unknown.
2091+
$this->assert
2092+
->array($redisMock->zscan('unknown', 0, ['COUNT' => 10]))
2093+
->isEqualTo([0, []]);
2094+
2095+
2096+
// It must return all the values with score greater than or equal to 1.
2097+
$this->assert
2098+
->array($redisMock->zscan('set1', 0, ['MATCH' => '*', 'COUNT' => 10]))
2099+
->isEqualTo([0 => 0, 1 => ['a:1' => 1, 'b:1' => 2, 'c:1' => 3, 'd:1' => 4]]);
2100+
2101+
// It must return only the matched value
2102+
$this->assert
2103+
->array($redisMock->zscan('set1', 0, ['MATCH' => 'c*', 'COUNT' => 10]))
2104+
->isEqualTo([0 => 0, 1 => ['c:1' => 3]]);
2105+
2106+
// It must return all of the values based on the match of *1
2107+
$this->assert
2108+
->array($redisMock->zscan('set1', 0, ['MATCH' => '*1', 'COUNT' => 10]))
2109+
->isEqualTo([0 => 0, 1 => ['a:1' => 1, 'b:1' => 2, 'c:1' => 3, 'd:1' => 4]]);
2110+
2111+
// It must return two values, starting cursor after the first value of the list.
2112+
2113+
$this->assert
2114+
->array($redisMock->zscan('set1', 1, ['COUNT' => 2]))
2115+
->isEqualTo([3, ['b:1' => 2, 'c:1' => 3]]);
2116+
2117+
// Ensure if our results are complete we return a zero cursor
2118+
$this->assert
2119+
->array($redisMock->zscan('set1', 3, ['COUNT' => 2]))
2120+
->isEqualTo([0, ['d:1' => 4]]);
2121+
2122+
// It must return all the values with score greater than or equal to 3,
2123+
// starting cursor after the last value of the previous scan.
2124+
$this->assert
2125+
->array($redisMock->zscan('set1', 4, ['MATCH' => '*', 'COUNT' => 10]))
2126+
->isEqualTo([0 => 0, 1 => []]);
2127+
2128+
$redisMock->expire('set1', 1);
2129+
sleep(2);
2130+
2131+
// It must return no values, as the key is expired.
2132+
$this->assert
2133+
->array($redisMock->zscan('set1', 0, ['COUNT' => 2]))
2134+
->isEqualTo([0, []]);
2135+
2136+
}
2137+
20772138
public function testBitcountCommand()
20782139
{
20792140
$redisMock = new Redis();

0 commit comments

Comments
 (0)