Skip to content

Commit

Permalink
Adds zScan method to Mock (#112)
Browse files Browse the repository at this point in the history
* Add ZScan Method

Co-authored-by: Thomas <[email protected]>
  • Loading branch information
robertmarney and thirsch authored May 2, 2023
1 parent db379bd commit fdef627
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
42 changes: 42 additions & 0 deletions src/M6Web/Component/RedisMock/RedisMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/M6Web/Component/RedisMock/RedisMockFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ class RedisMockFactory
'zrevrange',
'zrevrangebyscore',
'zrevrank',
'zscan',
'zscore',
'zunionstore',
'scan',
Expand Down
61 changes: 61 additions & 0 deletions tests/units/RedisMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit fdef627

Please sign in to comment.