Преглед на файлове

Merge branch 'feature/scan-iterators' into v0.8

Daniele Alessandri преди 11 години
родител
ревизия
463f2d655c

+ 2 - 0
CHANGELOG.md

@@ -6,6 +6,8 @@ v0.8.5 (2013-xx-xx)
 
 - Added `SCAN`, `SSCAN`, `ZSCAN`, `HSCAN` to the server profile for Redis 2.8.
 
+- Implemented iterator-based abstraction for `SCAN`, `SSCAN`, `ZSCAN`, `HSCAN`.
+
 - `Predis\Client::pubSubLoop()` should now be used instead of the deprecated
   `Predis\Client::pubSub()` (which still works like usual to avoid B/C breaks).
   `Predis\Client::pubSub()` will change in the next major version of Predis to

+ 1 - 0
README.md

@@ -24,6 +24,7 @@ More details are available on the [official wiki](http://wiki.github.com/nrk/pre
 - Command pipelining on single and aggregated connections.
 - Abstraction for Redis transactions (Redis >= 2.0) with support for CAS operations (Redis >= 2.2).
 - Abstraction for Lua scripting (Redis >= 2.6) capable of automatically switching between `EVAL` and `EVALSHA`.
+- Abstraction based on PHP iterators for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8).
 - Connections to Redis instances are lazily established upon the first call to a command by the client.
 - Ability to connect to Redis using TCP/IP or UNIX domain sockets with support for persistent connections.
 - Ability to specify alternative connection classes to use different types of network or protocol backends.

+ 96 - 0
examples/RedisCollectionsIterators.php

@@ -0,0 +1,96 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require 'SharedConfigurations.php';
+
+use Predis\Collection\Iterator;
+
+// Redis 2.8 features new commands allowing clients to incrementally
+// iterate over collections without blocking the server like it happens
+// when a command such as KEYS is executed on a Redis instance storing
+// millions of keys. These commands are SCAN (iterates over the keyspace),
+// SSCAN (iterates over members of a set), ZSCAN (iterates over members
+// and ranks of a sorted set) and HSCAN (iterates over fields and values
+// of an hash). Predis provides a specialized abstraction for each command
+// based on SPL iterators making it possible to easily consume SCAN-based
+// iterations in your PHP code.
+//
+// See http://redis.io/commands/scan for more details.
+//
+
+// Create a client using `2.8` as a server profile (needs Redis 2.8!)
+$client = new Predis\Client($single_server, array('profile' => '2.8'));
+
+// Prepare some keys for our example
+$client->del('predis:set', 'predis:zset', 'predis:hash');
+for ($i = 0; $i < 5; $i++) {
+    $client->sadd('predis:set', "member:$i");
+    $client->zadd('predis:zset', -$i, "member:$i");
+    $client->hset('predis:hash', "field:$i", "value:$i");
+}
+
+// === Keyspace iterator based on SCAN ===
+echo 'Scan the keyspace matching only our prefixed keys:', PHP_EOL;
+foreach (new Iterator\Keyspace($client, 'predis:*') as $key) {
+    echo " - $key", PHP_EOL;
+}
+
+/* OUTPUT
+Scan the keyspace matching only our prefixed keys:
+ - predis:zset
+ - predis:set
+ - predis:hash
+*/
+
+// === Set iterator based on SSCAN ===
+echo 'Scan members of `predis:set`:', PHP_EOL;
+foreach (new Iterator\SetKey($client, 'predis:set') as $member) {
+    echo " - $member", PHP_EOL;
+}
+
+/* OUTPUT
+Scan members of `predis:set`:
+ - member:1
+ - member:4
+ - member:0
+ - member:3
+ - member:2
+*/
+
+// === Sorted set iterator based on ZSCAN ===
+echo 'Scan members and ranks of `predis:zset`:', PHP_EOL;
+foreach (new Iterator\SortedSetKey($client, 'predis:zset') as $member => $rank) {
+    echo " - $member [rank: $rank]", PHP_EOL;
+}
+
+/* OUTPUT
+Scan members and ranks of `predis:zset`:
+ - member:4 [rank: -4]
+ - member:3 [rank: -3]
+ - member:2 [rank: -2]
+ - member:1 [rank: -1]
+ - member:0 [rank: 0]
+*/
+
+// === Hash iterator based on HSCAN ===
+echo 'Scan fields and values of `predis:hash`:', PHP_EOL;
+foreach (new Iterator\HashKey($client, 'predis:hash') as $field => $value) {
+    echo " - $field => $value", PHP_EOL;
+}
+
+/* OUTPUT
+Scan fields and values of `predis:hash`:
+ - field:0 => value:0
+ - field:1 => value:1
+ - field:2 => value:2
+ - field:3 => value:3
+ - field:4 => value:4
+*/

+ 189 - 0
lib/Predis/Collection/Iterator/CursorBasedIterator.php

@@ -0,0 +1,189 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Iterator;
+use Predis\ClientInterface;
+use Predis\NotSupportedException;
+
+/**
+ * Provides the base implementation for a fully-rewindable PHP iterator
+ * that can incrementally iterate over cursor-based collections stored
+ * on Redis using commands in the `SCAN` family.
+ *
+ * Given their incremental nature with multiple fetches, these kind of
+ * iterators offer limited guarantees about the returned elements because
+ * the collection can change several times during the iteration process.
+ *
+ * @see http://redis.io/commands/scan
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+abstract class CursorBasedIterator implements Iterator
+{
+    protected $client;
+    protected $match;
+    protected $count;
+
+    protected $valid;
+    protected $scanmore;
+    protected $elements;
+    protected $cursor;
+    protected $position;
+    protected $current;
+
+    /**
+     * @param ClientInterface $client Client connected to Redis.
+     * @param string $match Pattern to match during the server-side iteration.
+     * @param int $count Hints used by Redis to compute the number of results per iteration.
+     */
+    public function __construct(ClientInterface $client, $match = null, $count = null)
+    {
+        $this->client = $client;
+        $this->match = $match;
+        $this->count = $count;
+
+        $this->reset();
+    }
+
+    /**
+     * Ensures that the client instance supports the specified Redis
+     * command required to fetch elements from the server to perform
+     * the iteration.
+     *
+     * @param ClientInterface Client connected to Redis.
+     * @param string $commandID Command ID.
+     */
+    protected function requiredCommand(ClientInterface $client, $commandID)
+    {
+        if (!$client->getProfile()->supportsCommand($commandID)) {
+            throw new NotSupportedException("The specified server profile does not support the `$commandID` command.");
+        }
+    }
+
+    /**
+     * Resets the inner state of the iterator.
+     */
+    protected function reset()
+    {
+        $this->valid = true;
+        $this->scanmore = true;
+        $this->elements = array();
+        $this->cursor = 0;
+        $this->position = -1;
+        $this->current = null;
+    }
+
+    /**
+     * Returns an array of options for the `SCAN` command.
+     *
+     * @return array
+     */
+    protected function getScanOptions()
+    {
+        $options = array();
+
+        if (strlen($this->match) > 0) {
+            $options['MATCH'] = $this->match;
+        }
+
+        if ($this->count > 0) {
+            $options['COUNT'] = $this->count;
+        }
+
+        return $options;
+    }
+
+    /**
+     * Fetches a new set of elements from the remote collection,
+     * effectively advancing the iteration process.
+     *
+     * @return array
+     */
+    protected abstract function executeCommand();
+
+    /**
+     * Populates the local buffer of elements fetched from the
+     * server during the iteration.
+     */
+    protected function fetch()
+    {
+        list($cursor, $elements) = $this->executeCommand();
+
+        if (!$cursor) {
+            $this->scanmore = false;
+        }
+
+        $this->cursor = $cursor;
+        $this->elements = $elements;
+    }
+
+    /**
+     * Extracts next values for key() and current().
+     */
+    protected function extractNext()
+    {
+        $this->position++;
+        $this->current = array_shift($this->elements);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rewind()
+    {
+        $this->reset();
+        $this->next();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function current()
+    {
+        return $this->current;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function key()
+    {
+        return $this->position;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function next()
+    {
+        if (!$this->elements && $this->scanmore) {
+            $this->fetch();
+        }
+
+        if ($this->elements) {
+            $this->extractNext();
+        } else if ($this->cursor) {
+            $this->next();
+        } else {
+            $this->valid = false;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function valid()
+    {
+        return $this->valid;
+    }
+}

+ 56 - 0
lib/Predis/Collection/Iterator/HashKey.php

@@ -0,0 +1,56 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of fields and values of an hash
+ * by leveraging the HSCAN command (Redis >= 2.8) wrapped
+ * in a fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ * @link http://redis.io/commands/scan
+ */
+class HashKey extends CursorBasedIterator
+{
+    protected $key;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'HSCAN');
+
+        parent::__construct($client, $match, $count);
+
+        $this->key = $key;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->hscan($this->key, $this->cursor, $this->getScanOptions());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function extractNext()
+    {
+        $this->position = key($this->elements);
+        $this->current = array_shift($this->elements);
+    }
+}

+ 43 - 0
lib/Predis/Collection/Iterator/Keyspace.php

@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of the keyspace on a Redis instance
+ * by leveraging the SCAN command (Redis >= 2.8) wrapped in a
+ * fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ * @link http://redis.io/commands/scan
+ */
+class Keyspace extends CursorBasedIterator
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'SCAN');
+
+        parent::__construct($client, $match, $count);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->scan($this->cursor, $this->getScanOptions());
+    }
+}

+ 47 - 0
lib/Predis/Collection/Iterator/SetKey.php

@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of members stored in a set by
+ * leveraging the SSCAN command (Redis >= 2.8) wrapped in
+ * a fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ * @link http://redis.io/commands/scan
+ */
+class SetKey extends CursorBasedIterator
+{
+    protected $key;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'SSCAN');
+
+        parent::__construct($client, $match, $count);
+
+        $this->key = $key;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->sscan($this->key, $this->cursor, $this->getScanOptions());
+    }
+}

+ 58 - 0
lib/Predis/Collection/Iterator/SortedSetKey.php

@@ -0,0 +1,58 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of members stored in a sorted set
+ * by leveraging the ZSCAN command (Redis >= 2.8) wrapped in
+ * a fully-rewindable PHP iterator.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ * @link http://redis.io/commands/scan
+ */
+class SortedSetKey extends CursorBasedIterator
+{
+    protected $key;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'ZSCAN');
+
+        parent::__construct($client, $match, $count);
+
+        $this->key = $key;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->zscan($this->key, $this->cursor, $this->getScanOptions());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function extractNext()
+    {
+        $element = array_shift($this->elements);
+
+        $this->position = $element[0];
+        $this->current = $element[1];
+    }
+}

+ 481 - 0
tests/Predis/Collection/Iterator/HashKeyTest.php

@@ -0,0 +1,481 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use \PHPUnit_Framework_TestCase as StandardTestCase;
+
+use Predis\Client;
+use Predis\Profile\ServerProfile;
+
+/**
+ * @group realm-iterators
+ */
+class HashKeyTest extends StandardTestCase
+{
+    /**
+     * @group disconnected
+     * @expectedException Predis\NotSupportedException
+     * @expectedExceptionMessage The specified server profile does not support the `HSCAN` command.
+     */
+    public function testThrowsExceptionOnInvalidServerProfile()
+    {
+        $client = $this->getMock('Predis\ClientInterface');
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.0')));
+
+        $iterator = new HashKey($client, 'key:hash');
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithNoResults()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('hscan')
+               ->with('key:hash', 0, array())
+               ->will($this->returnValue(array(0, array())));
+
+        $iterator = new HashKey($client, 'key:hash');
+
+        $iterator->rewind();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnSingleFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('hscan')
+               ->with('key:hash', 0, array())
+               ->will($this->returnValue(array(0, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd', 'field:3rd' => 'value:3rd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:3rd', $iterator->current());
+        $this->assertSame('field:3rd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array())
+               ->will($this->returnValue(array(2, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd',
+               ))));
+        $client->expects($this->at(2))
+               ->method('hscan')
+               ->with('key:hash', 2, array())
+               ->will($this->returnValue(array(0, array(
+                    'field:3rd' => 'value:3rd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:3rd', $iterator->current());
+        $this->assertSame('field:3rd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInFirstFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array())
+               ->will($this->returnValue(array(4, array())));
+        $client->expects($this->at(2))
+               ->method('hscan')
+               ->with('key:hash', 4, array())
+               ->will($this->returnValue(array(0, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInMidFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array())
+               ->will($this->returnValue(array(2, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd',
+               ))));
+        $client->expects($this->at(2))
+               ->method('hscan')
+               ->with('key:hash', 2, array())
+               ->will($this->returnValue(array(5, array())));
+        $client->expects($this->at(3))
+               ->method('hscan')
+               ->with('key:hash', 5, array())
+               ->will($this->returnValue(array(0, array(
+                    'field:3rd' => 'value:3rd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:3rd', $iterator->current());
+        $this->assertSame('field:3rd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array('MATCH' => 'field:*'))
+               ->will($this->returnValue(array(2, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash', 'field:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatchOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array('MATCH' => 'field:*'))
+               ->will($this->returnValue(array(1, array(
+                    'field:1st' => 'value:1st',
+                ))));
+        $client->expects($this->at(2))
+               ->method('hscan')
+               ->with('key:hash', 1, array('MATCH' => 'field:*'))
+               ->will($this->returnValue(array(0, array(
+                    'field:2nd' => 'value:2nd',
+                ))));
+
+        $iterator = new HashKey($client, 'key:hash', 'field:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array('COUNT' => 2))
+               ->will($this->returnValue(array(0, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash', null, 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array('COUNT' => 1))
+               ->will($this->returnValue(array(1, array(
+                    'field:1st' => 'value:1st',
+                ))));
+        $client->expects($this->at(2))
+               ->method('hscan')
+               ->with('key:hash', 1, array('COUNT' => 1))
+               ->will($this->returnValue(array(0, array(
+                    'field:2nd' => 'value:2nd',
+                ))));
+
+        $iterator = new HashKey($client, 'key:hash', null, 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array('MATCH' => 'field:*', 'COUNT' => 2))
+               ->will($this->returnValue(array(0, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash', 'field:*', 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('hscan')
+               ->with('key:hash', 0, array('MATCH' => 'field:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(1, array(
+                    'field:1st' => 'value:1st',
+                ))));
+        $client->expects($this->at(2))
+               ->method('hscan')
+               ->with('key:hash', 1, array('MATCH' => 'field:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(0, array(
+                    'field:2nd' => 'value:2nd',
+                ))));
+
+        $iterator = new HashKey($client, 'key:hash', 'field:*', 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationRewindable()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->exactly(2))
+               ->method('hscan')
+               ->with('key:hash', 0, array())
+               ->will($this->returnValue(array(0, array(
+                    'field:1st' => 'value:1st', 'field:2nd' => 'value:2nd',
+               ))));
+
+        $iterator = new HashKey($client, 'key:hash');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:1st', $iterator->current());
+        $this->assertSame('field:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('value:2nd', $iterator->current());
+        $this->assertSame('field:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+}

+ 449 - 0
tests/Predis/Collection/Iterator/KeyspaceTest.php

@@ -0,0 +1,449 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use \PHPUnit_Framework_TestCase as StandardTestCase;
+
+use Predis\Client;
+use Predis\Profile\ServerProfile;
+
+/**
+ * @group realm-iterators
+ */
+class KeyspaceTest extends StandardTestCase
+{
+    /**
+     * @group disconnected
+     * @expectedException Predis\NotSupportedException
+     * @expectedExceptionMessage The specified server profile does not support the `SCAN` command.
+     */
+    public function testThrowsExceptionOnInvalidServerProfile()
+    {
+        $client = $this->getMock('Predis\ClientInterface');
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.0')));
+
+        $iterator = new Keyspace($client);
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithNoResults()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('scan')
+               ->with(0, array())
+               ->will($this->returnValue(array(0, array())));
+
+        $iterator = new Keyspace($client);
+
+        $iterator->rewind();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnSingleFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('scan')
+               ->with(0, array())
+               ->will($this->returnValue(array(0, array('key:1st', 'key:2nd', 'key:3rd'))));
+
+        $iterator = new Keyspace($client);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:3rd', $iterator->current());
+        $this->assertSame(2, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array())
+               ->will($this->returnValue(array(2, array('key:1st', 'key:2nd'))));
+        $client->expects($this->at(2))
+               ->method('scan')
+               ->with(2, array())
+               ->will($this->returnValue(array(0, array('key:3rd'))));
+
+        $iterator = new Keyspace($client);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:3rd', $iterator->current());
+        $this->assertSame(2, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInFirstFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array())
+               ->will($this->returnValue(array(4, array())));
+        $client->expects($this->at(2))
+               ->method('scan')
+               ->with(4, array())
+               ->will($this->returnValue(array(0, array('key:1st', 'key:2nd'))));
+
+        $iterator = new Keyspace($client);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInMidFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array())
+               ->will($this->returnValue(array(2, array('key:1st', 'key:2nd'))));
+        $client->expects($this->at(2))
+               ->method('scan')
+               ->with(2, array())
+               ->will($this->returnValue(array(5, array())));
+        $client->expects($this->at(3))
+               ->method('scan')
+               ->with(5, array())
+               ->will($this->returnValue(array(0, array('key:3rd'))));
+
+        $iterator = new Keyspace($client);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:3rd', $iterator->current());
+        $this->assertSame(2, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array('MATCH' => 'key:*'))
+               ->will($this->returnValue(array(0, array('key:1st', 'key:2nd'))));
+
+        $iterator = new Keyspace($client, 'key:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatchOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array('MATCH' => 'key:*'))
+               ->will($this->returnValue(array(1, array('key:1st'))));
+        $client->expects($this->at(2))
+               ->method('scan')
+               ->with(1, array('MATCH' => 'key:*'))
+               ->will($this->returnValue(array(0, array('key:2nd'))));
+
+        $iterator = new Keyspace($client, 'key:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array('COUNT' => 2))
+               ->will($this->returnValue(array(0, array('key:1st', 'key:2nd'))));
+
+        $iterator = new Keyspace($client, null, 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array('COUNT' => 1))
+               ->will($this->returnValue(array(1, array('key:1st'))));
+        $client->expects($this->at(2))
+               ->method('scan')
+               ->with(1, array('COUNT' => 1))
+               ->will($this->returnValue(array(0, array('key:2nd'))));
+
+        $iterator = new Keyspace($client, null, 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array('MATCH' => 'key:*', 'COUNT' => 2))
+               ->will($this->returnValue(array(0, array('key:1st', 'key:2nd'))));
+
+        $iterator = new Keyspace($client, 'key:*', 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('scan')
+               ->with(0, array('MATCH' => 'key:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(1, array('key:1st'))));
+        $client->expects($this->at(2))
+               ->method('scan')
+               ->with(1, array('MATCH' => 'key:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(0, array('key:2nd'))));
+
+        $iterator = new Keyspace($client, 'key:*', 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationRewindable()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'scan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->exactly(2))
+               ->method('scan')
+               ->with(0, array())
+               ->will($this->returnValue(array(0, array('key:1st', 'key:2nd'))));
+
+        $iterator = new Keyspace($client);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('key:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1, $iterator->key());
+        $this->assertSame('key:2nd', $iterator->current());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+}

+ 449 - 0
tests/Predis/Collection/Iterator/SetKeyTest.php

@@ -0,0 +1,449 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use \PHPUnit_Framework_TestCase as StandardTestCase;
+
+use Predis\Client;
+use Predis\Profile\ServerProfile;
+
+/**
+ * @group realm-iterators
+ */
+class SetKeyTest extends StandardTestCase
+{
+    /**
+     * @group disconnected
+     * @expectedException Predis\NotSupportedException
+     * @expectedExceptionMessage The specified server profile does not support the `SSCAN` command.
+     */
+    public function testThrowsExceptionOnInvalidServerProfile()
+    {
+        $client = $this->getMock('Predis\ClientInterface');
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.0')));
+
+        $iterator = new SetKey($client, 'key:set');
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithNoResults()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('sscan')
+               ->with('key:set', 0, array())
+               ->will($this->returnValue(array(0, array())));
+
+        $iterator = new SetKey($client, 'key:set');
+
+        $iterator->rewind();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnSingleFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('sscan')
+               ->with('key:set', 0, array())
+               ->will($this->returnValue(array(0, array('member:1st', 'member:2nd', 'member:3rd'))));
+
+        $iterator = new SetKey($client, 'key:set');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:3rd', $iterator->current());
+        $this->assertSame(2, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array())
+               ->will($this->returnValue(array(2, array('member:1st', 'member:2nd'))));
+        $client->expects($this->at(2))
+               ->method('sscan')
+               ->with('key:set', 2, array())
+               ->will($this->returnValue(array(0, array('member:3rd'))));
+
+        $iterator = new SetKey($client, 'key:set');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:3rd', $iterator->current());
+        $this->assertSame(2, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInFirstFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array())
+               ->will($this->returnValue(array(4, array())));
+        $client->expects($this->at(2))
+               ->method('sscan')
+               ->with('key:set', 4, array())
+               ->will($this->returnValue(array(0, array('member:1st', 'member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInMidFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array())
+               ->will($this->returnValue(array(2, array('member:1st', 'member:2nd'))));
+        $client->expects($this->at(2))
+               ->method('sscan')
+               ->with('key:set', 2, array())
+               ->will($this->returnValue(array(5, array())));
+        $client->expects($this->at(3))
+               ->method('sscan')
+               ->with('key:set', 5, array())
+               ->will($this->returnValue(array(0, array('member:3rd'))));
+
+        $iterator = new SetKey($client, 'key:set');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:3rd', $iterator->current());
+        $this->assertSame(2, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array('MATCH' => 'member:*'))
+               ->will($this->returnValue(array(0, array('member:1st', 'member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set', 'member:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatchOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array('MATCH' => 'member:*'))
+               ->will($this->returnValue(array(1, array('member:1st'))));
+        $client->expects($this->at(2))
+               ->method('sscan')
+               ->with('key:set', 1, array('MATCH' => 'member:*'))
+               ->will($this->returnValue(array(0, array('member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set', 'member:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array('COUNT' => 2))
+               ->will($this->returnValue(array(0, array('member:1st', 'member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set', null, 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array('COUNT' => 1))
+               ->will($this->returnValue(array(1, array('member:1st'))));
+        $client->expects($this->at(2))
+               ->method('sscan')
+               ->with('key:set', 1, array('COUNT' => 1))
+               ->will($this->returnValue(array(0, array('member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set', null, 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array('MATCH' => 'member:*', 'COUNT' => 2))
+               ->will($this->returnValue(array(0, array('member:1st', 'member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set', 'member:*', 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('sscan')
+               ->with('key:set', 0, array('MATCH' => 'member:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(1, array('member:1st'))));
+        $client->expects($this->at(2))
+               ->method('sscan')
+               ->with('key:set', 1, array('MATCH' => 'member:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(0, array('member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set', 'member:*', 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationRewindable()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'sscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->exactly(2))
+               ->method('sscan')
+               ->with('key:set', 0, array())
+               ->will($this->returnValue(array(0, array('member:1st', 'member:2nd'))));
+
+        $iterator = new SetKey($client, 'key:set');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:1st', $iterator->current());
+        $this->assertSame(0, $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame('member:2nd', $iterator->current());
+        $this->assertSame(1, $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+}

+ 481 - 0
tests/Predis/Collection/Iterator/SortedSetKeyTest.php

@@ -0,0 +1,481 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use \PHPUnit_Framework_TestCase as StandardTestCase;
+
+use Predis\Client;
+use Predis\Profile\ServerProfile;
+
+/**
+ * @group realm-iterators
+ */
+class SortedSetTest extends StandardTestCase
+{
+    /**
+     * @group disconnected
+     * @expectedException Predis\NotSupportedException
+     * @expectedExceptionMessage The specified server profile does not support the `ZSCAN` command.
+     */
+    public function testThrowsExceptionOnInvalidServerProfile()
+    {
+        $client = $this->getMock('Predis\ClientInterface');
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.0')));
+
+        $iterator = new SortedSetKey($client, 'key:zset');
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithNoResults()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('zscan')
+               ->with('key:zset', 0, array())
+               ->will($this->returnValue(array(0, array())));
+
+        $iterator = new SortedSetKey($client, 'key:zset');
+
+        $iterator->rewind();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnSingleFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->once())
+               ->method('zscan')
+               ->with('key:zset', 0, array())
+               ->will($this->returnValue(array(0, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0), array('member:3rd', 3.0),
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(3.0, $iterator->current());
+        $this->assertSame('member:3rd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array())
+               ->will($this->returnValue(array(2, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0),
+               ))));
+        $client->expects($this->at(2))
+               ->method('zscan')
+               ->with('key:zset', 2, array())
+               ->will($this->returnValue(array(0, array(
+                    array('member:3rd', 3.0),
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(3.0, $iterator->current());
+        $this->assertSame('member:3rd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInFirstFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array())
+               ->will($this->returnValue(array(4, array())));
+        $client->expects($this->at(2))
+               ->method('zscan')
+               ->with('key:zset', 4, array())
+               ->will($this->returnValue(array(0, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0),
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationOnMultipleFetchesAndHoleInMidFetch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array())
+               ->will($this->returnValue(array(2, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0),
+               ))));
+        $client->expects($this->at(2))
+               ->method('zscan')
+               ->with('key:zset', 2, array())
+               ->will($this->returnValue(array(5, array())));
+        $client->expects($this->at(3))
+               ->method('zscan')
+               ->with('key:zset', 5, array())
+               ->will($this->returnValue(array(0, array(
+                    array('member:3rd', 3.0)
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(3.0, $iterator->current());
+        $this->assertSame('member:3rd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatch()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array('MATCH' => 'member:*'))
+               ->will($this->returnValue(array(2, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0),
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset', 'member:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionMatchOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array('MATCH' => 'member:*'))
+               ->will($this->returnValue(array(1, array(
+                    array('member:1st', 1.0),
+                ))));
+        $client->expects($this->at(2))
+               ->method('zscan')
+               ->with('key:zset', 1, array('MATCH' => 'member:*'))
+               ->will($this->returnValue(array(0, array(
+                    array('member:2nd', 2.0),
+                ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset', 'member:*');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array('COUNT' => 2))
+               ->will($this->returnValue(array(0, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0),
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset', null, 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array('COUNT' => 1))
+               ->will($this->returnValue(array(1, array(
+                    array('member:1st', 1.0),
+                ))));
+        $client->expects($this->at(2))
+               ->method('zscan')
+               ->with('key:zset', 1, array('COUNT' => 1))
+               ->will($this->returnValue(array(0, array(
+                    array('member:2nd', 2.0),
+                ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset', null, 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCount()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array('MATCH' => 'member:*', 'COUNT' => 2))
+               ->will($this->returnValue(array(0, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0),
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset', 'member:*', 2);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationWithOptionsMatchAndCountOnMultipleFetches()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->at(1))
+               ->method('zscan')
+               ->with('key:zset', 0, array('MATCH' => 'member:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(1, array(
+                    array('member:1st', 1.0),
+                ))));
+        $client->expects($this->at(2))
+               ->method('zscan')
+               ->with('key:zset', 1, array('MATCH' => 'member:*', 'COUNT' => 1))
+               ->will($this->returnValue(array(0, array(
+                    array('member:2nd', 2.0),
+                ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset', 'member:*', 1);
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testIterationRewindable()
+    {
+        $client = $this->getMock('Predis\Client', array('getProfile', 'zscan'));
+
+        $client->expects($this->any())
+               ->method('getProfile')
+               ->will($this->returnValue(ServerProfile::get('2.8')));
+        $client->expects($this->exactly(2))
+               ->method('zscan')
+               ->with('key:zset', 0, array())
+               ->will($this->returnValue(array(0, array(
+                    array('member:1st', 1.0), array('member:2nd', 2.0),
+               ))));
+
+        $iterator = new SortedSetKey($client, 'key:zset');
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->rewind();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(1.0, $iterator->current());
+        $this->assertSame('member:1st', $iterator->key());
+
+        $iterator->next();
+        $this->assertTrue($iterator->valid());
+        $this->assertSame(2.0, $iterator->current());
+        $this->assertSame('member:2nd', $iterator->key());
+
+        $iterator->next();
+        $this->assertFalse($iterator->valid());
+    }
+}