<?php defined('PHPREDIS_TESTRUN') or die('Use TestRedis.php to run tests!\n');

require_once __DIR__ . '/TestSuite.php';
require_once __DIR__ . '/SessionHelpers.php';

class Redis_Test extends TestSuite {
    /**
     * @var Redis
     */
    public $redis;

    /* City lat/long */
    protected $cities = [
        'Chico'         => [-121.837478, 39.728494],
        'Sacramento'    => [-121.494400, 38.581572],
        'Gridley'       => [-121.693583, 39.363777],
        'Marysville'    => [-121.591355, 39.145725],
        'Cupertino'     => [-122.032182, 37.322998]
    ];

    protected $serializers = [
        Redis::SERIALIZER_NONE,
        Redis::SERIALIZER_PHP,
    ];

    protected function getNilValue() {
        return FALSE;
    }

    protected function getSerializers() {
        $result = [Redis::SERIALIZER_NONE, Redis::SERIALIZER_PHP];

        if (defined('Redis::SERIALIZER_IGBINARY'))
            $result[] = Redis::SERIALIZER_IGBINARY;
        if (defined('Redis::SERIALIZER_JSON'))
            $result[] = Redis::SERIALIZER_JSON;
        if (defined('Redis::SERIALIZER_MSGPACK'))
            $result[] = Redis::SERIALIZER_MSGPACK;

        return $result;
    }

    protected function getCompressors() {
        $result['none'] = Redis::COMPRESSION_NONE;
        if (defined('Redis::COMPRESSION_LZF'))
            $result['lzf'] = Redis::COMPRESSION_LZF;
        if (defined('Redis::COMPRESSION_LZ4'))
            $result['lz4'] = Redis::COMPRESSION_LZ4;
        if (defined('Redis::COMPRESSION_ZSTD'))
            $result['zstd'] = Redis::COMPRESSION_ZSTD;

        return $result;
    }

    /* Overridable left/right constants */
    protected function getLeftConstant() {
        return Redis::LEFT;
    }

    protected function getRightConstant() {
        return Redis::RIGHT;
    }

    protected function detectKeyDB(array $info) {
        return strpos($info['executable'] ?? '', 'keydb') !== false ||
               isset($info['keydb']) ||
               isset($info['mvcc_depth']);
    }

    protected function detectValkey(array $info) {
        return isset($info['server_name']) && $info['server_name'] === 'valkey';
    }

    public function setUp() {
        $this->redis = $this->newInstance();
        $info = $this->redis->info();
        $this->version = (isset($info['redis_version'])?$info['redis_version']:'0.0.0');
        $this->is_keydb = $this->detectKeyDB($info);
        $this->is_valkey = $this->detectValKey($info);
        $this->valkey_version = $info['valkey_version'] ?? '0.0.0';
    }

    protected function haveCommand(string $cmd): bool {
        $info = $this->redis->command('info', $cmd);
        $name = $info[0][0] ?? null;

        return $name && strcasecmp($cmd, $name) === 0;
    }

    protected function minVersionCheck($version) {
        return version_compare($this->version, $version) >= 0;
    }

    protected function minValkeyVersionCheck($version) {
        return $this->is_valkey && version_compare($this->valkey_version, $version) >= 0;
    }

    protected function mstime() {
        return round(microtime(true)*1000);
    }

    protected function getAuthParts(&$user, &$pass) {
        $user = $pass = NULL;

        $auth = $this->getAuth();
        if ( ! $auth)
            return;

        if (is_array($auth)) {
            if (count($auth) > 1) {
                list($user, $pass) = $auth;
            } else {
                $pass = $auth[0];
            }
        } else {
            $pass = $auth;
        }
    }

    protected function sessionPrefix(): string {
        return 'PHPREDIS_SESSION:';
    }

    protected function sessionSaveHandler(): string {
        return 'redis';
    }

    protected function sessionSavePath(): string {
        return sprintf('tcp://%s:%d?%s', $this->getHost(), $this->getPort(),
                       $this->getAuthFragment());
    }

    protected function getAuthFragment() {
        $this->getAuthParts($user, $pass);

        if ($user && $pass) {
            return sprintf('auth[user]=%s&auth[pass]=%s', $user, $pass);
        } else if ($pass) {
            return sprintf('auth[pass]=%s', $pass);
        } else {
            return '';
        }
    }

    protected function newInstance() {
        $r = new Redis([
            'host' => $this->getHost(),
            'port' => $this->getPort(),
        ]);

        if ($this->getAuth()) {
            $this->assertTrue($r->auth($this->getAuth()));
        }
        return $r;
    }

    public function tearDown() {
        if ($this->redis) {
            $this->redis->close();
        }
    }

    public function reset() {
        $this->setUp();
        $this->tearDown();
    }

    /* Helper function to determine if the class has pipeline support */
    protected function havePipeline() {
        return defined(get_class($this->redis) . '::PIPELINE');
    }

    protected function haveMulti() {
        return defined(get_class($this->redis) . '::MULTI');
    }

    public function testMinimumVersion() {
        $this->assertTrue(version_compare($this->version, '2.4.0') >= 0);
    }

    public function testPing() {
        /* Reply literal off */
        $this->assertTrue($this->redis->ping());
        $this->assertTrue($this->redis->ping(NULL));
        $this->assertEquals('BEEP', $this->redis->ping('BEEP'));

        /* Make sure we're good in MULTI mode */
        if ($this->haveMulti()) {
            $this->assertEquals(
                [true, 'BEEP'],
                $this->redis->multi()
                    ->ping()
                    ->ping('BEEP')
                    ->exec()
            );
        }
    }

    public function testPipelinePublish() {
        $ret = $this->redis->pipeline()
            ->publish('chan', 'msg')
            ->exec();

        $this->assertIsArray($ret, 1);
        $this->assertGT(-1, $ret[0] ?? -1);
    }

    // Run some simple tests against the PUBSUB command.  This is problematic, as we
    // can't be sure what's going on in the instance, but we can do some things.
    public function testPubSub() {
        // Only available since 2.8.0
        if (version_compare($this->version, '2.8.0') < 0)
            $this->markTestSkipped();

        // PUBSUB CHANNELS ...
        $result = $this->redis->pubsub('channels', '*');
        $this->assertIsArray($result);
        $result = $this->redis->pubsub('channels');
        $this->assertIsArray($result);

        // PUBSUB NUMSUB

        $c1 = uniqid();
        $c2 = uniqid();

        $result = $this->redis->pubsub('numsub', [$c1, $c2]);

        // Should get an array back, with two elements
        $this->assertIsArray($result);
        $this->assertEquals(2, count($result));

        // Make sure the elements are correct, and have zero counts
        foreach ([$c1,$c2] as $channel) {
            $this->assertArrayKeyEquals($result, $channel, 0);
        }

        // PUBSUB NUMPAT
        $result = $this->redis->pubsub('numpat');
        $this->assertIsInt($result);

        // Invalid calls
        $this->assertFalse(@$this->redis->pubsub('notacommand'));
        $this->assertFalse(@$this->redis->pubsub('numsub', 'not-an-array'));
    }

    /* These test cases were generated randomly.  We're just trying to test
       that PhpRedis handles all combination of arguments correctly. */
    public function testBitcount() {
        /* key */
        $this->redis->set('bitcountkey', hex2bin('bd906b854ca76cae'));
        $this->assertEquals(33, $this->redis->bitcount('bitcountkey'));

        /* key, start */
        $this->redis->set('bitcountkey', hex2bin('400aac171382a29bebaab554f178'));
        $this->assertEquals(4, $this->redis->bitcount('bitcountkey', 13));

        /* key, start, end */
        $this->redis->set('bitcountkey', hex2bin('b1f32405'));
        $this->assertEquals(2, $this->redis->bitcount('bitcountkey', 3, 3));

        /* key, start, end BYTE */
        $this->redis->set('bitcountkey', hex2bin('10eb8939e68bfdb640260f0629f3'));
        $this->assertEquals(1, $this->redis->bitcount('bitcountkey', 8, 8, false));

        if ( ! $this->is_keydb && $this->minVersionCheck('7.0')) {
            /* key, start, end, BIT */
            $this->redis->set('bitcountkey', hex2bin('cd0e4c80f9e4590d888a10'));
            $this->assertEquals(5, $this->redis->bitcount('bitcountkey', 0, 9, true));
        }
    }

    public function testBitop() {
        if ( ! $this->minVersionCheck('2.6.0'))
            $this->markTestSkipped();

        $this->redis->set('{key}1', 'foobar');
        $this->redis->set('{key}2', 'abcdef');

        // Regression test for GitHub issue #2210
        $this->assertEquals(6, $this->redis->bitop('AND', '{key}1', '{key}2'));

        // Make sure RedisCluster doesn't even send the command.  We don't care
        // about what Redis returns
        @$this->redis->bitop('AND', 'key1', 'key2', 'key3');
        $this->assertNull($this->redis->getLastError());

        $this->redis->del('{key}1', '{key}2');
    }

    public function testBitsets() {
        $this->redis->del('key');
        $this->assertEquals(0, $this->redis->getBit('key', 0));
        $this->assertFalse($this->redis->getBit('key', -1));
        $this->assertEquals(0, $this->redis->getBit('key', 100000));

        $this->redis->set('key', "\xff");
        for ($i = 0; $i < 8; $i++) {
            $this->assertEquals(1, $this->redis->getBit('key', $i));
        }
        $this->assertEquals(0, $this->redis->getBit('key', 8));

        // change bit 0
        $this->assertEquals(1, $this->redis->setBit('key', 0, 0));
        $this->assertEquals(0, $this->redis->setBit('key', 0, 0));
        $this->assertEquals(0, $this->redis->getBit('key', 0));
        $this->assertKeyEquals("\x7f", 'key');

        // change bit 1
        $this->assertEquals(1, $this->redis->setBit('key', 1, 0));
        $this->assertEquals(0, $this->redis->setBit('key', 1, 0));
        $this->assertEquals(0, $this->redis->getBit('key', 1));
        $this->assertKeyEquals("\x3f", 'key');

        // change bit > 1
        $this->assertEquals(1, $this->redis->setBit('key', 2, 0));
        $this->assertEquals(0, $this->redis->setBit('key', 2, 0));
        $this->assertEquals(0, $this->redis->getBit('key', 2));
        $this->assertKeyEquals("\x1f", 'key');

        // values above 1 are changed to 1 but don't overflow on bits to the right.
        $this->assertEquals(0, $this->redis->setBit('key', 0, 0xff));
        $this->assertKeyEquals("\x9f", 'key');

        // Verify valid offset ranges
        $this->assertFalse($this->redis->getBit('key', -1));

        $this->redis->setBit('key', 0x7fffffff, 1);
        $this->assertEquals(1, $this->redis->getBit('key', 0x7fffffff));
    }

    public function testLcs() {
        if ( ! $this->minVersionCheck('7.0.0') || $this->is_keydb)
            $this->markTestSkipped();

        $key1 = '{lcs}1'; $key2 = '{lcs}2';
        $this->assertTrue($this->redis->set($key1, '12244447777777'));
        $this->assertTrue($this->redis->set($key2, '6666662244441'));

        $this->assertEquals('224444', $this->redis->lcs($key1, $key2));

        $this->assertEquals(
            ['matches', [[[1, 6], [6, 11]]], 'len', 6],
            $this->redis->lcs($key1, $key2, ['idx'])
        );
        $this->assertEquals(
            ['matches', [[[1, 6], [6, 11], 6]], 'len', 6],
            $this->redis->lcs($key1, $key2, ['idx', 'withmatchlen'])
        );

        $this->assertEquals(6, $this->redis->lcs($key1, $key2, ['len']));

        $this->redis->del([$key1, $key2]);
    }

    public function testLmpop() {
        if (version_compare($this->version, '7.0.0') < 0)
            $this->markTestSkipped();

        $key1 = '{l}1';
        $key2 = '{l}2';

        $this->redis->del($key1, $key2);

        $this->assertEquals(6, $this->redis->rpush($key1, 'A', 'B', 'C', 'D', 'E', 'F'));
        $this->assertEquals(6, $this->redis->rpush($key2, 'F', 'E', 'D', 'C', 'B', 'A'));

        $this->assertEquals([$key1, ['A']], $this->redis->lmpop([$key1, $key2], 'LEFT'));
        $this->assertEquals([$key1, ['F']], $this->redis->lmpop([$key1, $key2], 'RIGHT'));
        $this->assertEquals([$key1, ['B', 'C', 'D']], $this->redis->lmpop([$key1, $key2], 'LEFT',  3));

        $this->assertEquals(2, $this->redis->del($key1, $key2));
    }

    public function testBLmpop() {
        if (version_compare($this->version, '7.0.0') < 0)
            $this->markTestSkipped();

        $key1 = '{bl}1';
        $key2 = '{bl}2';

        $this->redis->del($key1, $key2);

        $this->assertEquals(2, $this->redis->rpush($key1, 'A', 'B'));
        $this->assertEquals(2, $this->redis->rpush($key2, 'C', 'D'));

        $this->assertEquals([$key1, ['B', 'A']], $this->redis->blmpop(.2, [$key1, $key2], 'RIGHT', 2));
        $this->assertEquals([$key2, ['C']], $this->redis->blmpop(.2, [$key1, $key2], 'LEFT'));
        $this->assertEquals([$key2, ['D']], $this->redis->blmpop(.2, [$key1, $key2], 'LEFT'));

        $st = microtime(true);
        $this->assertFalse($this->redis->blmpop(.2, [$key1, $key2], 'LEFT'));
        $et = microtime(true);

        // Very loose tolerance because CI is run on a potato
        $this->assertBetween($et - $st, .05, .75);
    }

    function testZmpop() {
        if (version_compare($this->version, '7.0.0') < 0)
            $this->markTestSkipped();

        $key1 = '{z}1';
        $key2 = '{z}2';

        $this->redis->del($key1, $key2);

        $this->assertEquals(4, $this->redis->zadd($key1, 0, 'zero', 2, 'two', 4, 'four', 6, 'six'));
        $this->assertEquals(4, $this->redis->zadd($key2, 1, 'one', 3, 'three', 5, 'five', 7, 'seven'));

        $this->assertEquals([$key1, ['zero' => 0.0]], $this->redis->zmpop([$key1, $key2], 'MIN'));
        $this->assertEquals([$key1, ['six' => 6.0]], $this->redis->zmpop([$key1, $key2], 'MAX'));
        $this->assertEquals([$key1, ['two' => 2.0, 'four' => 4.0]], $this->redis->zmpop([$key1, $key2], 'MIN', 3));

        $this->assertEquals(
            [$key2, ['one' => 1.0, 'three' => 3.0, 'five' => 5.0, 'seven' => 7.0]],
            $this->redis->zmpop([$key1, $key2], 'MIN', 128)
        );

        $this->assertFalse($this->redis->zmpop([$key1, $key2], 'MIN'));

        $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, true);
        $this->assertNull($this->redis->zmpop([$key1, $key2], 'MIN'));
        $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, false);
    }

    function testBZmpop() {
        if (version_compare($this->version, '7.0.0') < 0)
            $this->markTestSkipped();

        $key1 = '{z}1';
        $key2 = '{z}2';

        $this->redis->del($key1, $key2);

        $this->assertEquals(2, $this->redis->zadd($key1, 0, 'zero', 2, 'two'));
        $this->assertEquals(2, $this->redis->zadd($key2, 1, 'one', 3, 'three'));

        $this->assertEquals(
            [$key1, ['zero' => 0.0, 'two' => 2.0]],
            $this->redis->bzmpop(.1, [$key1, $key2], 'MIN', 2)
        );

        $this->assertEquals([$key2, ['three' => 3.0]], $this->redis->bzmpop(.1, [$key1, $key2], 'MAX'));
        $this->assertEquals([$key2, ['one' => 1.0]], $this->redis->bzmpop(.1, [$key1, $key2], 'MAX'));

        $st = microtime(true);
        $this->assertFalse($this->redis->bzmpop(.2, [$key1, $key2], 'MIN'));
        $et = microtime(true);

        $this->assertBetween($et - $st, .05, .75);
    }

    public function testBitPos() {
        if (version_compare($this->version, '2.8.7') < 0) {
            $this->MarkTestSkipped();
            return;
        }

        $this->redis->del('bpkey');

        $this->redis->set('bpkey', "\xff\xf0\x00");
        $this->assertEquals(12, $this->redis->bitpos('bpkey', 0));

        $this->redis->set('bpkey', "\x00\xff\xf0");
        $this->assertEquals(8, $this->redis->bitpos('bpkey', 1, 0));
        $this->assertEquals(8, $this->redis->bitpos('bpkey', 1, 1));

        $this->redis->set('bpkey', "\x00\x00\x00");
        $this->assertEquals(-1, $this->redis->bitpos('bpkey', 1));

        if ( ! $this->minVersionCheck('7.0.0'))
            return;

        $this->redis->set('bpkey', "\xF");
        $this->assertEquals(4, $this->redis->bitpos('bpkey', 1, 0, -1, true));
        $this->assertEquals(-1,  $this->redis->bitpos('bpkey', 1, 1, -1));
        $this->assertEquals(-1,  $this->redis->bitpos('bpkey', 1, 1, -1, false));
    }

    public function testSetLargeKeys() {
        foreach ([1000, 100000, 1000000] as $size) {
            $value = str_repeat('A', $size);
            $this->assertTrue($this->redis->set('x', $value));
            $this->assertKeyEquals($value, 'x');
        }
    }

    public function testEcho() {
        $this->assertEquals('hello', $this->redis->echo('hello'));
        $this->assertEquals('', $this->redis->echo(''));
        $this->assertEquals(' 0123 ', $this->redis->echo(' 0123 '));
    }

    public function testErr() {
        $this->redis->set('x', '-ERR');
        $this->assertKeyEquals('-ERR', 'x');
    }

    public function testSet() {
        $this->assertTrue($this->redis->set('key', 'nil'));
        $this->assertKeyEquals('nil', 'key');

        $this->assertTrue($this->redis->set('key', 'val'));

        $this->assertKeyEquals('val', 'key');
        $this->assertKeyEquals('val', 'key');
        $this->redis->del('keyNotExist');
        $this->assertKeyMissing('keyNotExist');

        $this->redis->set('key2', 'val');
        $this->assertKeyEquals('val', 'key2');

        $value1 = bin2hex(random_bytes(rand(64, 128)));
        $value2 = random_bytes(rand(65536, 65536 * 2));;

        $this->redis->set('key2', $value1);
        $this->assertKeyEquals($value1, 'key2');
        $this->assertKeyEquals($value1, 'key2');

        $this->redis->del('key');
        $this->redis->del('key2');


        $this->redis->set('key', $value2);
        $this->assertKeyEquals($value2, 'key');
        $this->redis->del('key');
        $this->assertKeyMissing('key');

        $data = gzcompress('42');
        $this->assertTrue($this->redis->set('key', $data));
        $this->assertEquals('42', gzuncompress($this->redis->get('key')));

        $this->redis->del('key');
        $data = gzcompress('value1');
        $this->assertTrue($this->redis->set('key', $data));
        $this->assertEquals('value1', gzuncompress($this->redis->get('key')));

        $this->redis->del('key');
        $this->assertTrue($this->redis->set('key', 0));
        $this->assertKeyEquals('0', 'key');
        $this->assertTrue($this->redis->set('key', 1));
        $this->assertKeyEquals('1', 'key');
        $this->assertTrue($this->redis->set('key', 0.1));
        $this->assertKeyEquals('0.1', 'key');
        $this->assertTrue($this->redis->set('key', '0.1'));
        $this->assertKeyEquals('0.1', 'key');
        $this->assertTrue($this->redis->set('key', true));
        $this->assertKeyEquals('1', 'key');

        $this->assertTrue($this->redis->set('key', ''));
        $this->assertKeyEquals('', 'key');
        $this->assertTrue($this->redis->set('key', NULL));
        $this->assertKeyEquals('', 'key');

        $this->assertTrue($this->redis->set('key', gzcompress('42')));
        $this->assertEquals('42', gzuncompress($this->redis->get('key')));
    }

    /* Extended SET options for Redis >= 2.6.12 */
    public function testExtendedSet() {
        // Skip the test if we don't have a new enough version of Redis
        if (version_compare($this->version, '2.6.12') < 0)
            $this->markTestSkipped();

        /* Legacy SETEX redirection */
        $this->redis->del('foo');
        $this->assertTrue($this->redis->set('foo', 'bar', 20));
        $this->assertKeyEquals('bar', 'foo');
        $this->assertEquals(20, $this->redis->ttl('foo'));

        /* Should coerce doubles into long */
        $this->assertTrue($this->redis->set('foo', 'bar-20.5', 20.5));
        $this->assertEquals(20, $this->redis->ttl('foo'));
        $this->assertKeyEquals('bar-20.5', 'foo');

        /* Invalid third arguments */
        $this->assertFalse(@$this->redis->set('foo', 'bar', 'baz'));
        $this->assertFalse(@$this->redis->set('foo', 'bar',new StdClass()));

        /* Set if not exist */
        $this->redis->del('foo');
        $this->assertTrue($this->redis->set('foo', 'bar', ['nx']));
        $this->assertKeyEquals('bar', 'foo');
        $this->assertFalse($this->redis->set('foo', 'bar', ['nx']));

        /* Set if exists */
        $this->assertTrue($this->redis->set('foo', 'bar', ['xx']));
        $this->assertKeyEquals('bar', 'foo');
        $this->redis->del('foo');
        $this->assertFalse($this->redis->set('foo', 'bar', ['xx']));

        /* Set with a TTL */
        $this->assertTrue($this->redis->set('foo', 'bar', ['ex' => 100]));
        $this->assertEquals(100, $this->redis->ttl('foo'));

        /* Set with a PTTL */
        $this->assertTrue($this->redis->set('foo', 'bar', ['px' => 100000]));
        $this->assertBetween($this->redis->pttl('foo'), 99000, 100001);

        /* Set if exists, with a TTL */
        $this->assertTrue($this->redis->set('foo', 'bar', ['xx', 'ex' => 105]));
        $this->assertEquals(105, $this->redis->ttl('foo'));
        $this->assertKeyEquals('bar', 'foo');

        /* Set if not exists, with a TTL */
        $this->redis->del('foo');
        $this->assertTrue($this->redis->set('foo', 'bar', ['nx', 'ex' => 110]));
        $this->assertEquals(110, $this->redis->ttl('foo'));
        $this->assertKeyEquals('bar', 'foo');
        $this->assertFalse($this->redis->set('foo', 'bar', ['nx', 'ex' => 110]));

        /* Throw some nonsense into the array, and check that the TTL came through */
        $this->redis->del('foo');
        $this->assertTrue($this->redis->set('foo', 'barbaz', ['not-valid', 'nx', 'invalid', 'ex' => 200]));
        $this->assertEquals(200, $this->redis->ttl('foo'));
        $this->assertKeyEquals('barbaz', 'foo');

        /* Pass NULL as the optional arguments which should be ignored */
        $this->redis->del('foo');
        $this->redis->set('foo', 'bar', NULL);
        $this->assertKeyEquals('bar', 'foo');
        $this->assertLT(0, $this->redis->ttl('foo'));

        /* Make sure we ignore bad/non-string options (regression test for #1835) */
        $this->assertTrue($this->redis->set('foo', 'bar', [NULL, 'EX' => 60]));
        $this->assertTrue($this->redis->set('foo', 'bar', [NULL, new stdClass(), 'EX' => 60]));
        $this->assertFalse(@$this->redis->set('foo', 'bar', [NULL, 'EX' => []]));

        if (version_compare($this->version, '6.0.0') < 0)
            return;

        /* KEEPTTL works by itself */
        $this->redis->set('foo', 'bar', ['EX' => 100]);
        $this->redis->set('foo', 'bar', ['KEEPTTL']);
        $this->assertBetween($this->redis->ttl('foo'), 90, 100);

        /* Works with other options */
        $this->redis->set('foo', 'bar', ['XX', 'KEEPTTL']);
        $this->assertBetween($this->redis->ttl('foo'), 90, 100);
        $this->redis->set('foo', 'bar', ['XX']);
        $this->assertEquals(-1, $this->redis->ttl('foo'));

        if (version_compare($this->version, '6.2.0') < 0)
            return;

        $this->assertEquals('bar', $this->redis->set('foo', 'baz', ['GET']));
    }

    /* Test Valkey >= 8.1 IFEQ SET option */
    public function testValkeyIfEq() {
        if ( ! $this->is_valkey || ! $this->minVersionCheck('8.1.0'))
            $this->markTestSkipped();

        $this->redis->del('foo');
        $this->assertTrue($this->redis->set('foo', 'bar'));
        $this->assertTrue($this->redis->set('foo', 'bar2', ['IFEQ' => 'bar']));
        $this->assertFalse($this->redis->set('foo', 'bar4', ['IFEQ' => 'bar3']));

        $this->assertEquals('bar2', $this->redis->set('foo', 'bar3', ['IFEQ' => 'bar2', 'GET']));
    }

    public function testGetSet() {
        $this->redis->del('key');
        $this->assertFalse($this->redis->getSet('key', '42'));
        $this->assertEquals('42', $this->redis->getSet('key', '123'));
        $this->assertEquals('123', $this->redis->getSet('key', '123'));
    }

    public function testGetDel() {
        $this->redis->del('key');
        $this->assertTrue($this->redis->set('key', 'iexist'));
        $this->assertEquals('iexist', $this->redis->getDel('key'));
        $this->assertEquals(0, $this->redis->exists('key'));
    }

    public function testRandomKey() {
        for ($i = 0; $i < 1000; $i++) {
            $k = $this->redis->randomKey();
            $this->assertKeyExists($k);
        }
    }

    public function testRename() {
        // strings
        $this->redis->del('{key}0');
        $this->redis->set('{key}0', 'val0');
        $this->redis->rename('{key}0', '{key}1');
        $this->assertKeyMissing('{key}0');
        $this->assertKeyEquals('val0', '{key}1');
    }

    public function testRenameNx() {
        // strings
        $this->redis->del('{key}0', '{key}1');
        $this->redis->set('{key}0', 'val0');
        $this->redis->set('{key}1', 'val1');
        $this->assertFalse($this->redis->renameNx('{key}0', '{key}1'));
        $this->assertKeyEquals('val0', '{key}0');
        $this->assertKeyEquals('val1', '{key}1');

        // lists
        $this->redis->del('{key}0');
        $this->redis->del('{key}1');
        $this->redis->lPush('{key}0', 'val0');
        $this->redis->lPush('{key}0', 'val1');
        $this->redis->lPush('{key}1', 'val1-0');
        $this->redis->lPush('{key}1', 'val1-1');
        $this->assertFalse($this->redis->renameNx('{key}0', '{key}1'));
        $this->assertEquals(['val1', 'val0'], $this->redis->lRange('{key}0', 0, -1));
        $this->assertEquals(['val1-1', 'val1-0'], $this->redis->lRange('{key}1', 0, -1));

        $this->redis->del('{key}2');
        $this->assertTrue($this->redis->renameNx('{key}0', '{key}2'));
        $this->assertEquals([], $this->redis->lRange('{key}0', 0, -1));
        $this->assertEquals(['val1', 'val0'], $this->redis->lRange('{key}2', 0, -1));
    }

    public function testMultiple() {
        $kvals = [
            'mget1' => 'v1',
            'mget2' => 'v2',
            'mget3' => 'v3'
        ];

        $this->redis->mset($kvals);

        $this->redis->set(1, 'test');

        $this->assertEquals([$kvals['mget1']], $this->redis->mget(['mget1']));

        $this->assertEquals(['v1', 'v2', false], $this->redis->mget(['mget1', 'mget2', 'NoKey']));
        $this->assertEquals(['v1', 'v2', 'v3'], $this->redis->mget(['mget1', 'mget2', 'mget3']));
        $this->assertEquals(['v1', 'v2', 'v3'], $this->redis->mget(['mget1', 'mget2', 'mget3']));

        $this->redis->set('k5', '$1111111111');
        $this->assertEquals(['$1111111111'], $this->redis->mget(['k5']));

        $this->assertEquals(['test'], $this->redis->mget([1])); // non-string
    }

    public function testMultipleBin() {
        $kvals = [
            'binkey-1' => random_bytes(16),
            'binkey-2' => random_bytes(16),
            'binkey-3' => random_bytes(16),
        ];

        foreach ($kvals as $k => $v) {
            $this->redis->set($k, $v);
        }

        $this->assertEquals(array_values($kvals),
                            $this->redis->mget(array_keys($kvals)));
    }

    public function testExpireMember() {
        if ( ! $this->is_keydb)
            $this->markTestSkipped();

        $this->redis->del('h');
        $this->redis->hmset('h', ['f1' => 'v1', 'f2' => 'v2', 'f3' => 'v3', 'f4' => 'v4']);

        $this->assertEquals(1, $this->redis->expiremember('h', 'f1', 1));
        $this->assertEquals(1, $this->redis->expiremember('h', 'f2', 1000, 'ms'));
        $this->assertEquals(1, $this->redis->expiremember('h', 'f3', 1000,  null));
        $this->assertEquals(0, $this->redis->expiremember('h', 'nk', 10));

        $this->assertEquals(1, $this->redis->expirememberat('h', 'f4', time() + 1));
        $this->assertEquals(0, $this->redis->expirememberat('h', 'nk', time() + 1));
    }

    public function testExpire() {
        $this->redis->del('key');
        $this->redis->set('key', 'value');

        $this->assertKeyEquals('value', 'key');
        $this->redis->expire('key', 1);
        $this->assertKeyEquals('value', 'key');
        sleep(2);
        $this->assertKeyMissing('key');
    }

    /* This test is prone to failure in the Travis container, so attempt to
       mitigate this by running more than once */
    public function testExpireAt() {
        $success = false;

        for ($i = 0; !$success && $i < 3; $i++) {
            $this->redis->del('key');
            $this->redis->set('key', 'value');
            $this->redis->expireAt('key', time() + 1);
            usleep(1500000);
            $success = FALSE === $this->redis->get('key');
        }

        $this->assertTrue($success);
    }

    function testExpireOptions() {
        if ( ! $this->minVersionCheck('7.0.0'))
            $this->markTestSkipped();

        $this->redis->set('eopts', 'value');

        /* NX -- Only if expiry isn't set so success, then failure */
        $this->assertTrue($this->redis->expire('eopts', 1000, 'NX'));
        $this->assertFalse($this->redis->expire('eopts', 1000, 'NX'));

        /* XX -- Only set if the key has an existing expiry */
        $this->assertTrue($this->redis->expire('eopts', 1000, 'XX'));
        $this->assertTrue($this->redis->persist('eopts'));
        $this->assertFalse($this->redis->expire('eopts', 1000, 'XX'));

        /* GT -- Only set when new expiry > current expiry */
        $this->assertTrue($this->redis->expire('eopts', 200));
        $this->assertTrue($this->redis->expire('eopts', 300, 'GT'));
        $this->assertFalse($this->redis->expire('eopts', 100, 'GT'));

        /* LT -- Only set when expiry < current expiry */
        $this->assertTrue($this->redis->expire('eopts', 200));
        $this->assertTrue($this->redis->expire('eopts', 100, 'LT'));
        $this->assertFalse($this->redis->expire('eopts', 300, 'LT'));

        /* Sending a nonsensical mode fails without sending a command */
        $this->redis->clearLastError();
        $this->assertFalse(@$this->redis->expire('eopts', 999, 'nonsense'));
        $this->assertNull($this->redis->getLastError());

        $this->redis->del('eopts');
    }

    public function testExpiretime() {
        if (version_compare($this->version, '7.0.0') < 0)
            $this->markTestSkipped();

        $now = time();

        $this->assertTrue($this->redis->set('key1', 'value'));
        $this->assertTrue($this->redis->expireat('key1', $now + 10));
        $this->assertEquals($now + 10, $this->redis->expiretime('key1'));
        $this->assertEquals(1000 * ($now + 10), $this->redis->pexpiretime('key1'));

        $this->redis->del('key1');
    }

    public function testGetEx() {
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->assertTrue($this->redis->set('key', 'value'));

        $this->assertEquals('value', $this->redis->getEx('key', ['EX' => 100]));
        $this->assertBetween($this->redis->ttl('key'), 95, 100);

        $this->assertEquals('value', $this->redis->getEx('key', ['PX' => 100000]));
        $this->assertBetween($this->redis->pttl('key'), 95000, 100000);

        $this->assertEquals('value', $this->redis->getEx('key', ['EXAT' => time() + 200]));
        $this->assertBetween($this->redis->ttl('key'), 195, 200);

        $this->assertEquals('value', $this->redis->getEx('key', ['PXAT' => (time()*1000) + 25000]));
        $this->assertBetween($this->redis->pttl('key'), 24000, 25000);

        $this->assertEquals('value', $this->redis->getEx('key', ['PERSIST' => true]));
        $this->assertEquals(-1, $this->redis->ttl('key'));

        $this->assertTrue($this->redis->expire('key', 100));
        $this->assertBetween($this->redis->ttl('key'), 95, 100);

        $this->assertEquals('value', $this->redis->getEx('key', ['PERSIST']));
        $this->assertEquals(-1, $this->redis->ttl('key'));
    }

    public function testSetEx() {
        $this->redis->del('key');
        $this->assertTrue($this->redis->setex('key', 7, 'val'));
        $this->assertEquals(7, $this->redis->ttl('key'));
        $this->assertKeyEquals('val', 'key');
    }

    public function testPSetEx() {
        $this->redis->del('key');
        $this->assertTrue($this->redis->psetex('key', 7 * 1000, 'val'));
        $this->assertEquals(7, $this->redis->ttl('key'));
        $this->assertKeyEquals('val', 'key');
    }

    public function testSetNX() {

        $this->redis->set('key', 42);
        $this->assertFalse($this->redis->setnx('key', 'err'));
        $this->assertKeyEquals('42', 'key');

        $this->redis->del('key');
        $this->assertTrue($this->redis->setnx('key', '42'));
        $this->assertKeyEquals('42', 'key');
    }

    public function testExpireAtWithLong() {
        if (PHP_INT_SIZE != 8)
            $this->markTestSkipped('64 bits only');

        $large_expiry = 3153600000;
        $this->redis->del('key');
        $this->assertTrue($this->redis->setex('key', $large_expiry, 'val'));
        $this->assertEquals($large_expiry, $this->redis->ttl('key'));
    }

    public function testIncr() {
        $this->redis->set('key', 0);

        $this->redis->incr('key');
        $this->assertKeyEqualsWeak(1, 'key');

        $this->redis->incr('key');
        $this->assertKeyEqualsWeak(2, 'key');

        $this->redis->incrBy('key', 3);
        $this->assertKeyEqualsWeak(5, 'key');

        $this->redis->incrBy('key', 1);
        $this->assertKeyEqualsWeak(6, 'key');

        $this->redis->incrBy('key', -1);
        $this->assertKeyEqualsWeak(5, 'key');

        $this->redis->incr('key', 5);
        $this->assertKeyEqualsWeak(10, 'key');

        $this->redis->del('key');

        $this->redis->set('key', 'abc');

        $this->redis->incr('key');
        $this->assertKeyEquals('abc', 'key');

        $this->redis->incr('key');
        $this->assertKeyEquals('abc', 'key');

        $this->redis->set('key', 0);
        $this->assertEquals(PHP_INT_MAX, $this->redis->incrby('key', PHP_INT_MAX));
    }

    public function testIncrByFloat() {
        // incrbyfloat is new in 2.6.0
        if (version_compare($this->version, '2.5.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('key');

        $this->redis->set('key', 0);

        $this->redis->incrbyfloat('key', 1.5);
        $this->assertKeyEquals('1.5', 'key');

        $this->redis->incrbyfloat('key', 2.25);
        $this->assertKeyEquals('3.75', 'key');

        $this->redis->incrbyfloat('key', -2.25);
        $this->assertKeyEquals('1.5', 'key');

        $this->redis->set('key', 'abc');

        $this->redis->incrbyfloat('key', 1.5);
        $this->assertKeyEquals('abc', 'key');

        $this->redis->incrbyfloat('key', -1.5);
        $this->assertKeyEquals('abc', 'key');

        // Test with prefixing
        $this->redis->setOption(Redis::OPT_PREFIX, 'someprefix:');
        $this->redis->del('key');
        $this->redis->incrbyfloat('key',1.8);
        $this->assertKeyEqualsWeak(1.8, 'key');
        $this->redis->setOption(Redis::OPT_PREFIX, '');
        $this->assertKeyExists('someprefix:key');
        $this->redis->del('someprefix:key');
    }

    public function testDecr() {
        $this->redis->set('key', 5);

        $this->redis->decr('key');
        $this->assertKeyEqualsWeak(4, 'key');

        $this->redis->decr('key');
        $this->assertKeyEqualsWeak(3, 'key');

        $this->redis->decrBy('key', 2);
        $this->assertKeyEqualsWeak(1, 'key');

        $this->redis->decrBy('key', 1);
        $this->assertKeyEqualsWeak(0, 'key');

        $this->redis->decrBy('key', -10);
        $this->assertKeyEqualsWeak(10, 'key');

        $this->redis->decr('key', 10);
        $this->assertKeyEqualsWeak(0, 'key');
    }


    public function testExists() {
        /* Single key */
        $this->redis->del('key');
        $this->assertKeyMissing('key');
        $this->redis->set('key', 'val');
        $this->assertKeyExists('key');

        /* Add multiple keys */
        $mkeys = [];
        for ($i = 0; $i < 10; $i++) {
            if (rand(1, 2) == 1) {
                $mkey = "{exists}key:$i";
                $this->redis->set($mkey, $i);
                $mkeys[] = $mkey;
            }
        }

        /* Test passing an array as well as the keys variadic */
        $this->assertEquals(count($mkeys), $this->redis->exists($mkeys));
        if (count($mkeys))
            $this->assertEquals(count($mkeys), $this->redis->exists(...$mkeys));
    }

    public function testTouch() {
        if ( ! $this->minVersionCheck('3.2.1'))
            $this->markTestSkipped();

        $this->redis->del('notakey');

        $this->assertTrue($this->redis->mset(['{idle}1' => 'beep', '{idle}2' => 'boop']));
        usleep(1100000);
        $this->assertGT(0, $this->redis->object('idletime', '{idle}1'));
        $this->assertGT(0, $this->redis->object('idletime', '{idle}2'));

        $this->assertEquals(2, $this->redis->touch('{idle}1', '{idle}2', '{idle}notakey'));
        $idle1 = $this->redis->object('idletime', '{idle}1');
        $idle2 = $this->redis->object('idletime', '{idle}2');

        /* We're not testing if idle is 0 because CPU scheduling on GitHub CI
         * potatoes can cause that to erroneously fail. */
        $this->assertLT(2, $idle1);
        $this->assertLT(2, $idle2);
    }

    public function testKeys() {
        $pattern = 'keys-test-';
        for ($i = 1; $i < 10; $i++) {
            $this->redis->set($pattern.$i, $i);
        }
        $this->redis->del($pattern.'3');
        $keys = $this->redis->keys($pattern.'*');

        $this->redis->set($pattern.'3', 'something');

        $keys2 = $this->redis->keys($pattern.'*');

        $this->assertEquals((count($keys) + 1), count($keys2));

        // empty array when no key matches
        $this->assertEquals([], $this->redis->keys(uniqid() . '*'));
    }

    protected function genericDelUnlink($cmd) {
        $key = uniqid('key:');
        $this->redis->set($key, 'val');
        $this->assertKeyEquals('val', $key);
        $this->assertEquals(1, $this->redis->$cmd($key));
        $this->assertFalse($this->redis->get($key));

        // multiple, all existing
        $this->redis->set('x', 0);
        $this->redis->set('y', 1);
        $this->redis->set('z', 2);
        $this->assertEquals(3, $this->redis->$cmd('x', 'y', 'z'));
        $this->assertFalse($this->redis->get('x'));
        $this->assertFalse($this->redis->get('y'));
        $this->assertFalse($this->redis->get('z'));

        // multiple, none existing
        $this->assertEquals(0, $this->redis->$cmd('x', 'y', 'z'));
        $this->assertFalse($this->redis->get('x'));
        $this->assertFalse($this->redis->get('y'));
        $this->assertFalse($this->redis->get('z'));

        // multiple, some existing
        $this->redis->set('y', 1);
        $this->assertEquals(1, $this->redis->$cmd('x', 'y', 'z'));
        $this->assertFalse($this->redis->get('y'));

        $this->redis->set('x', 0);
        $this->redis->set('y', 1);
        $this->assertEquals(2, $this->redis->$cmd(['x', 'y']));
    }

    public function testDelete() {
        $this->genericDelUnlink('DEL');
    }

    public function testDelIfEq() {
        if ( ! $this->haveCommand('DELIFEQ'))
            $this->markTestSkipped();

        $this->assertTrue($this->redis->set('key', 'value'));
        $this->assertEquals(0, $this->redis->delifeq('key', 'notvalue'));
        $this->assertEquals(1, $this->redis->delifeq('key', 'value'));
        $this->assertEquals(0, $this->redis->exists('key'));
    }

    public function testUnlink() {
        if (version_compare($this->version, '4.0.0') < 0)
            $this->markTestSkipped();

        $this->genericDelUnlink('UNLINK');
    }

    public function testType() {
        // string
        $this->redis->set('key', 'val');
        $this->assertEquals(Redis::REDIS_STRING, $this->redis->type('key'));

        // list
        $this->redis->lPush('keyList', 'val0');
        $this->redis->lPush('keyList', 'val1');
        $this->assertEquals(Redis::REDIS_LIST, $this->redis->type('keyList'));

        // set
        $this->redis->del('keySet');
        $this->redis->sAdd('keySet', 'val0');
        $this->redis->sAdd('keySet', 'val1');
        $this->assertEquals(Redis::REDIS_SET, $this->redis->type('keySet'));

        // sadd with numeric key
        $this->redis->del(123);
        $this->assertEquals(1, $this->redis->sAdd(123, 'val0'));
        $this->assertEquals(['val0'], $this->redis->sMembers(123));

        // zset
        $this->redis->del('keyZSet');
        $this->redis->zAdd('keyZSet', 0, 'val0');
        $this->redis->zAdd('keyZSet', 1, 'val1');
        $this->assertEquals(Redis::REDIS_ZSET, $this->redis->type('keyZSet'));

        // hash
        $this->redis->del('keyHash');
        $this->redis->hSet('keyHash', 'key0', 'val0');
        $this->redis->hSet('keyHash', 'key1', 'val1');
        $this->assertEquals(Redis::REDIS_HASH, $this->redis->type('keyHash'));

        // stream
        if ($this->minVersionCheck('5.0')) {
            $this->redis->del('stream');
            $this->redis->xAdd('stream', '*', ['foo' => 'bar']);
            $this->assertEquals(Redis::REDIS_STREAM, $this->redis->type('stream'));
        }

        if ($this->haveCommand('vadd')) {
            $this->redis->del('vset');
            $this->assertEquals(1, $this->redis->vadd('vset', [1.5, 2.5], 'foo'));
            $this->assertEquals(Redis::REDIS_VECTORSET, $this->redis->type('vset'));
        }

        // None
        $this->redis->del('keyNotExists');
        $this->assertEquals(Redis::REDIS_NOT_FOUND, $this->redis->type('keyNotExists'));
    }

    public function testStr() {
        $this->redis->set('key', 'val1');
        $this->assertEquals(8, $this->redis->append('key', 'val2'));
        $this->assertKeyEquals('val1val2', 'key');

        $this->redis->del('keyNotExist');
        $this->assertEquals(5, $this->redis->append('keyNotExist', 'value'));
        $this->assertKeyEquals('value', 'keyNotExist');

        $this->redis->set('key', 'This is a string') ;
        $this->assertEquals('This', $this->redis->getRange('key', 0, 3));
        $this->assertEquals('string', $this->redis->getRange('key', -6, -1));
        $this->assertEquals('string', $this->redis->getRange('key', -6, 100000));
        $this->assertKeyEquals('This is a string', 'key');

        $this->redis->set('key', 'This is a string') ;
        $this->assertEquals(16, $this->redis->strlen('key'));

        $this->redis->set('key', 10) ;
        $this->assertEquals(2, $this->redis->strlen('key'));
        $this->redis->set('key', '') ;
        $this->assertEquals(0, $this->redis->strlen('key'));
        $this->redis->set('key', '000') ;
        $this->assertEquals(3, $this->redis->strlen('key'));
    }

    public function testlPop() {
        $this->redis->del('list');

        $this->redis->lPush('list', 'val');
        $this->redis->lPush('list', 'val2');
        $this->redis->rPush('list', 'val3');

        $this->assertEquals('val2', $this->redis->lPop('list'));
        if (version_compare($this->version, '6.2.0') < 0) {
            $this->assertEquals('val', $this->redis->lPop('list'));
            $this->assertEquals('val3', $this->redis->lPop('list'));
        } else {
            $this->assertEquals(['val', 'val3'], $this->redis->lPop('list', 2));
        }

        $this->assertFalse($this->redis->lPop('list'));

        $this->redis->del('list');
        $this->assertEquals(1, $this->redis->lPush('list', gzcompress('val1')));
        $this->assertEquals(2, $this->redis->lPush('list', gzcompress('val2')));
        $this->assertEquals(3, $this->redis->lPush('list', gzcompress('val3')));

        $this->assertEquals('val3', gzuncompress($this->redis->lPop('list')));
        $this->assertEquals('val2', gzuncompress($this->redis->lPop('list')));
        $this->assertEquals('val1', gzuncompress($this->redis->lPop('list')));
    }

    public function testrPop() {
        $this->redis->del('list');

        $this->redis->rPush('list', 'val');
        $this->redis->rPush('list', 'val2');
        $this->redis->lPush('list', 'val3');

        $this->assertEquals('val2', $this->redis->rPop('list'));
        if (version_compare($this->version, '6.2.0') < 0) {
            $this->assertEquals('val', $this->redis->rPop('list'));
            $this->assertEquals('val3', $this->redis->rPop('list'));
        } else {
            $this->assertEquals(['val', 'val3'], $this->redis->rPop('list', 2));
        }

        $this->assertFalse($this->redis->rPop('list'));

        $this->redis->del('list');
        $this->assertEquals(1, $this->redis->rPush('list', gzcompress('val1')));
        $this->assertEquals(2, $this->redis->rPush('list', gzcompress('val2')));
        $this->assertEquals(3, $this->redis->rPush('list', gzcompress('val3')));

        $this->assertEquals('val3', gzuncompress($this->redis->rPop('list')));
        $this->assertEquals('val2', gzuncompress($this->redis->rPop('list')));
        $this->assertEquals('val1', gzuncompress($this->redis->rPop('list')));
    }

    /* Regression test for GH #2329 */
    public function testrPopSerialization() {
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);

        $this->redis->del('rpopkey');
        $this->redis->rpush('rpopkey', ['foo'], ['bar']);
        $this->assertEquals([['bar'], ['foo']], $this->redis->rpop('rpopkey', 2));

        $this->redis->rpush('rpopkey', ['foo'], ['bar']);
        $this->assertEquals([['foo'], ['bar']], $this->redis->lpop('rpopkey', 2));

        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
    }

    public function testblockingPop() {
        /* Test with a double timeout in Redis >= 6.0.0 */
        if (version_compare($this->version, '6.0.0') >= 0) {
            $this->redis->del('list');
            $this->redis->lpush('list', 'val1', 'val2');
            $this->assertEquals(['list', 'val2'], $this->redis->blpop(['list'], .1));
            $this->assertEquals(['list', 'val1'], $this->redis->blpop(['list'], .1));
        }

        // non blocking blPop, brPop
        $this->redis->del('list');
        $this->redis->lPush('list', 'val1', 'val2');
        $this->assertEquals(['list', 'val2'], $this->redis->blPop(['list'], 2));
        $this->assertEquals(['list', 'val1'], $this->redis->blPop(['list'], 2));

        $this->redis->del('list');
        $this->redis->lPush('list', 'val1', 'val2');
        $this->assertEquals(['list', 'val1'], $this->redis->brPop(['list'], 1));
        $this->assertEquals(['list', 'val2'], $this->redis->brPop(['list'], 1));

        // blocking blpop, brpop
        $this->redis->del('list');

        /* Also test our option that we want *-1 to be returned as NULL */
        foreach ([false => [], true => NULL] as $opt => $val) {
            $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, $opt);
            $this->assertEquals($val, $this->redis->blPop(['list'], 1));
            $this->assertEquals($val, $this->redis->brPop(['list'], 1));
        }

        $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, false);
    }

    public function testLLen() {
        $this->redis->del('list');

        $this->redis->lPush('list', 'val');
        $this->assertEquals(1, $this->redis->llen('list'));

        $this->redis->lPush('list', 'val2');
        $this->assertEquals(2, $this->redis->llen('list'));

        $this->assertEquals('val2', $this->redis->lPop('list'));
        $this->assertEquals(1, $this->redis->llen('list'));

        $this->assertEquals('val', $this->redis->lPop('list'));
        $this->assertEquals(0, $this->redis->llen('list'));

        $this->assertFalse($this->redis->lPop('list'));
        $this->assertEquals(0, $this->redis->llen('list'));    // empty returns 0

        $this->redis->del('list');
        $this->assertEquals(0, $this->redis->llen('list'));    // non-existent returns 0

        $this->redis->set('list', 'actually not a list');
        $this->assertFalse($this->redis->llen('list'));// not a list returns FALSE
    }

    public function testlPopx() {
        $this->redis->del('keyNotExists');
        $this->assertEquals(0, $this->redis->lPushx('keyNotExists', 'value'));
        $this->assertEquals(0, $this->redis->rPushx('keyNotExists', 'value'));

        $this->redis->del('key');
        $this->redis->lPush('key', 'val0');
        $this->assertEquals(2, $this->redis->lPushx('key', 'val1'));
        $this->assertEquals(3, $this->redis->rPushx('key', 'val2'));
        $this->assertEquals(['val1', 'val0', 'val2'], $this->redis->lrange('key', 0, -1));

        //test linsert
        $this->redis->del('key');
        $this->redis->lPush('key', 'val0');
        $this->assertEquals(0, $this->redis->lInsert('keyNotExists', Redis::AFTER, 'val1', 'val2'));
        $this->assertEquals(-1, $this->redis->lInsert('key', Redis::BEFORE, 'valX', 'val2'));

        $this->assertEquals(2, $this->redis->lInsert('key', Redis::AFTER, 'val0', 'val1'));
        $this->assertEquals(3, $this->redis->lInsert('key', Redis::BEFORE, 'val0', 'val2'));
        $this->assertEquals(['val2', 'val0', 'val1'], $this->redis->lrange('key', 0, -1));
    }

    public function testlPos() {
        $this->redis->del('key');
        $this->redis->lPush('key', 'val0', 'val1', 'val1');
        $this->assertEquals(2, $this->redis->lPos('key', 'val0'));
        $this->assertEquals(0, $this->redis->lPos('key', 'val1'));
        $this->assertEquals(1, $this->redis->lPos('key', 'val1', ['rank' => 2]));
        $this->assertEquals([0, 1], $this->redis->lPos('key', 'val1', ['count' => 2]));
        $this->assertEquals([0], $this->redis->lPos('key', 'val1', ['count' => 2, 'maxlen' => 1]));
        $this->assertEquals([], $this->redis->lPos('key', 'val2', ['count' => 1]));

        foreach ([[true, NULL], [false, false]] as $optpack) {
            list ($setting, $expected) = $optpack;
            $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, $setting);
            $this->assertEquals($expected, $this->redis->lPos('key', 'val2'));
        }
    }

    // ltrim, lLen, lpop
    public function testltrim() {
        $this->redis->del('list');

        $this->redis->lPush('list', 'val');
        $this->redis->lPush('list', 'val2');
        $this->redis->lPush('list', 'val3');
        $this->redis->lPush('list', 'val4');

        $this->assertTrue($this->redis->ltrim('list', 0, 2));
        $this->assertEquals(3, $this->redis->llen('list'));

        $this->redis->ltrim('list', 0, 0);
        $this->assertEquals(1, $this->redis->llen('list'));
        $this->assertEquals('val4', $this->redis->lPop('list'));

        $this->assertTrue($this->redis->ltrim('list', 10, 10000));
        $this->assertTrue($this->redis->ltrim('list', 10000, 10));

        // test invalid type
        $this->redis->set('list', 'not a list...');
        $this->assertFalse($this->redis->ltrim('list', 0, 2));
    }

    public function setupSort() {
        // people with name, age, salary
        $this->redis->set('person:name_1', 'Alice');
        $this->redis->set('person:age_1', 27);
        $this->redis->set('person:salary_1', 2500);

        $this->redis->set('person:name_2', 'Bob');
        $this->redis->set('person:age_2', 34);
        $this->redis->set('person:salary_2', 2000);

        $this->redis->set('person:name_3', 'Carol');
        $this->redis->set('person:age_3', 25);
        $this->redis->set('person:salary_3', 2800);

        $this->redis->set('person:name_4', 'Dave');
        $this->redis->set('person:age_4', 41);
        $this->redis->set('person:salary_4', 3100);

        // set-up
        $this->redis->del('person:id');
        foreach ([1, 2, 3, 4] as $id) {
            $this->redis->lPush('person:id', $id);
        }
    }

    public function testSortPrefix() {
        // Make sure that sorting works with a prefix
        $this->redis->setOption(Redis::OPT_PREFIX, 'some-prefix:');
        $this->redis->del('some-item');
        $this->redis->sadd('some-item', 1);
        $this->redis->sadd('some-item', 2);
        $this->redis->sadd('some-item', 3);

        $this->assertEquals(['1', '2', '3'], $this->redis->sort('some-item', ['sort' => 'asc']));
        $this->assertEquals(['3', '2', '1'], $this->redis->sort('some-item', ['sort' => 'desc']));
        $this->assertEquals(['1', '2', '3'], $this->redis->sort('some-item'));

        // Kill our set/prefix
        $this->redis->del('some-item');
        $this->redis->setOption(Redis::OPT_PREFIX, '');
    }

    public function testSortAsc() {
        $this->setupSort();
        // sort by age and get IDs
        $byAgeAsc = ['3', '1', '2', '4'];
        $this->assertEquals($byAgeAsc, $this->redis->sort('person:id', ['by' => 'person:age_*']));
        $this->assertEquals($byAgeAsc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'sort' => 'asc']));
        $this->assertEquals(['1', '2', '3', '4'], $this->redis->sort('person:id', ['by' => NULL]));   // check that NULL works.
        $this->assertEquals(['1', '2', '3', '4'], $this->redis->sort('person:id', ['by' => NULL, 'get' => NULL])); // for all fields.
        $this->assertEquals(['1', '2', '3', '4'], $this->redis->sort('person:id', ['sort' => 'asc']));

        // sort by age and get names
        $byAgeAsc = ['Carol', 'Alice', 'Bob', 'Dave'];
        $this->assertEquals($byAgeAsc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*']));
        $this->assertEquals($byAgeAsc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'sort' => 'asc']));

        $this->assertEquals(array_slice($byAgeAsc, 0, 2), $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [0, 2]]));
        $this->assertEquals(array_slice($byAgeAsc, 0, 2), $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [0, 2], 'sort' => 'asc']));

        $this->assertEquals(array_slice($byAgeAsc, 1, 2), $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [1, 2]]));
        $this->assertEquals(array_slice($byAgeAsc, 1, 2), $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [1, 2], 'sort' => 'asc']));
        $this->assertEquals($byAgeAsc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [0, 4]]));
        $this->assertEquals($byAgeAsc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [0, '4']])); // with strings
        $this->assertEquals($byAgeAsc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => ['0', 4]]));

        // sort by salary and get ages
        $agesBySalaryAsc = ['34', '27', '25', '41'];
        $this->assertEquals($agesBySalaryAsc, $this->redis->sort('person:id', ['by' => 'person:salary_*', 'get' => 'person:age_*']));
        $this->assertEquals($agesBySalaryAsc, $this->redis->sort('person:id', ['by' => 'person:salary_*', 'get' => 'person:age_*', 'sort' => 'asc']));

        $agesAndSalaries = $this->redis->sort('person:id', ['by' => 'person:salary_*', 'get' => ['person:age_*', 'person:salary_*'], 'sort' => 'asc']);
        $this->assertEquals(['34', '2000', '27', '2500', '25', '2800', '41', '3100'], $agesAndSalaries);

        // sort non-alpha doesn't change all-string lists
        // list → [ghi, def, abc]
        $list = ['abc', 'def', 'ghi'];
        $this->redis->del('list');
        foreach ($list as $i) {
            $this->redis->lPush('list', $i);
        }

        // SORT list → [ghi, def, abc]
        if (version_compare($this->version, '2.5.0') < 0) {
            $this->assertEquals(array_reverse($list), $this->redis->sort('list'));
            $this->assertEquals(array_reverse($list), $this->redis->sort('list', ['sort' => 'asc']));
        } else {
            // TODO rewrite, from 2.6.0 release notes:
            // SORT now will refuse to sort in numerical mode elements that can't be parsed
            // as numbers
        }

        // SORT list ALPHA → [abc, def, ghi]
        $this->assertEquals($list, $this->redis->sort('list', ['alpha' => true]));
        $this->assertEquals($list, $this->redis->sort('list', ['sort' => 'asc', 'alpha' => true]));
    }

    public function testSortDesc() {
        $this->setupSort();

        // sort by age and get IDs
        $byAgeDesc = ['4', '2', '1', '3'];
        $this->assertEquals($byAgeDesc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'sort' => 'desc']));

        // sort by age and get names
        $byAgeDesc = ['Dave', 'Bob', 'Alice', 'Carol'];
        $this->assertEquals($byAgeDesc, $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'sort' => 'desc']));

        $this->assertEquals(array_slice($byAgeDesc, 0, 2), $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [0, 2], 'sort' => 'desc']));
        $this->assertEquals(array_slice($byAgeDesc, 1, 2), $this->redis->sort('person:id', ['by' => 'person:age_*', 'get' => 'person:name_*', 'limit' => [1, 2], 'sort' => 'desc']));

        // sort by salary and get ages
        $agesBySalaryDesc = ['41', '25', '27', '34'];
        $this->assertEquals($agesBySalaryDesc, $this->redis->sort('person:id', ['by' => 'person:salary_*', 'get' => 'person:age_*', 'sort' => 'desc']));

        // sort non-alpha doesn't change all-string lists
        $list = ['def', 'abc', 'ghi'];
        $this->redis->del('list');
        foreach ($list as $i) {
            $this->redis->lPush('list', $i);
        }

        // SORT list ALPHA → [abc, def, ghi]
        $this->assertEquals(['ghi', 'def', 'abc'], $this->redis->sort('list', ['sort' => 'desc', 'alpha' => true]));
    }

    /* This test is just to make sure SORT and SORT_RO are both callable */
    public function testSortHandler() {
        $this->redis->del('list');

        $this->redis->rpush('list', 'c', 'b', 'a');

        $methods = ['sort'];
        if ($this->minVersionCheck('7.0.0')) $methods[] = 'sort_ro';

        foreach ($methods as $method) {
            $this->assertEquals(['a', 'b', 'c'], $this->redis->$method('list', ['sort' => 'asc', 'alpha' => true]));
        }
    }

    public function testLindex() {
        $this->redis->del('list');

        $this->redis->lPush('list', 'val');
        $this->redis->lPush('list', 'val2');
        $this->redis->lPush('list', 'val3');

        $this->assertEquals('val3', $this->redis->lIndex('list', 0));
        $this->assertEquals('val2', $this->redis->lIndex('list', 1));
        $this->assertEquals('val', $this->redis->lIndex('list', 2));
        $this->assertEquals('val', $this->redis->lIndex('list', -1));
        $this->assertEquals('val2', $this->redis->lIndex('list', -2));
        $this->assertEquals('val3', $this->redis->lIndex('list', -3));
        $this->assertFalse($this->redis->lIndex('list', -4));

        $this->redis->rPush('list', 'val4');
        $this->assertEquals('val4', $this->redis->lIndex('list', 3));
        $this->assertEquals('val4', $this->redis->lIndex('list', -1));
    }

    public function testlMove() {
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        [$list1, $list2] = ['{l}0', '{l}1'];
        $left  = $this->getLeftConstant();
        $right = $this->getRightConstant();

        $this->redis->del($list1, $list2);
        $this->redis->lPush($list1, 'a');
        $this->redis->lPush($list1, 'b');
        $this->redis->lPush($list1, 'c');

        $return = $this->redis->lMove($list1, $list2, $left, $right);
        $this->assertEquals('c', $return);

        $return = $this->redis->lMove($list1, $list2, $right, $left);
        $this->assertEquals('a', $return);

        $this->assertEquals(['b'], $this->redis->lRange($list1, 0, -1));
        $this->assertEquals(['a', 'c'], $this->redis->lRange($list2, 0, -1));

    }

    public function testBlmove() {
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        [$list1, $list2] = ['{l}0', '{l}1'];
        $left = $this->getLeftConstant();

        $this->redis->del($list1, $list2);
        $this->redis->rpush($list1, 'a');


        $this->assertEquals('a', $this->redis->blmove($list1, $list2, $left, $left, 1.));

        $st = microtime(true);
        $ret = $this->redis->blmove($list1, $list2, $left, $left, .1);
        $et = microtime(true);

        $this->assertFalse($ret);
        $this->assertGT(.09, $et - $st);
    }

    // lRem testing
    public function testLRem() {
        $this->redis->del('list');
        $this->redis->lPush('list', 'a');
        $this->redis->lPush('list', 'b');
        $this->redis->lPush('list', 'c');
        $this->redis->lPush('list', 'c');
        $this->redis->lPush('list', 'b');
        $this->redis->lPush('list', 'c');

        // ['c', 'b', 'c', 'c', 'b', 'a']
        $return = $this->redis->lrem('list', 'b', 2);
        // ['c', 'c', 'c', 'a']
        $this->assertEquals(2, $return);
        $this->assertEquals('c', $this->redis->lIndex('list', 0));
        $this->assertEquals('c', $this->redis->lIndex('list', 1));
        $this->assertEquals('c', $this->redis->lIndex('list', 2));
        $this->assertEquals('a', $this->redis->lIndex('list', 3));

        $this->redis->del('list');
        $this->redis->lPush('list', 'a');
        $this->redis->lPush('list', 'b');
        $this->redis->lPush('list', 'c');
        $this->redis->lPush('list', 'c');
        $this->redis->lPush('list', 'b');
        $this->redis->lPush('list', 'c');

        // ['c', 'b', 'c', 'c', 'b', 'a']
        $this->redis->lrem('list', 'c', -2);
        // ['c', 'b', 'b', 'a']
        $this->assertEquals(2, $return);
        $this->assertEquals('c', $this->redis->lIndex('list', 0));
        $this->assertEquals('b', $this->redis->lIndex('list', 1));
        $this->assertEquals('b', $this->redis->lIndex('list', 2));
        $this->assertEquals('a', $this->redis->lIndex('list', 3));

        // remove each element
        $this->assertEquals(1, $this->redis->lrem('list', 'a', 0));
        $this->assertEquals(0, $this->redis->lrem('list', 'x', 0));
        $this->assertEquals(2, $this->redis->lrem('list', 'b', 0));
        $this->assertEquals(1, $this->redis->lrem('list', 'c', 0));
        $this->assertFalse($this->redis->get('list'));

        $this->redis->set('list', 'actually not a list');
        $this->assertFalse($this->redis->lrem('list', 'x'));
    }

    public function testSAdd() {
        $this->redis->del('set');

        $this->assertEquals(1, $this->redis->sAdd('set', 'val'));
        $this->assertEquals(0, $this->redis->sAdd('set', 'val'));

        $this->assertTrue($this->redis->sismember('set', 'val'));
        $this->assertFalse($this->redis->sismember('set', 'val2'));

        $this->assertEquals(1, $this->redis->sAdd('set', 'val2'));

        $this->assertTrue($this->redis->sismember('set', 'val2'));
    }

    public function testSCard() {
        $this->redis->del('set');
        $this->assertEquals(1, $this->redis->sAdd('set', 'val'));
        $this->assertEquals(1, $this->redis->scard('set'));
        $this->assertEquals(1, $this->redis->sAdd('set', 'val2'));
        $this->assertEquals(2, $this->redis->scard('set'));
    }

    public function testSRem() {
        $this->redis->del('set');
        $this->redis->sAdd('set', 'val');
        $this->redis->sAdd('set', 'val2');
        $this->redis->srem('set', 'val');
        $this->assertEquals(1, $this->redis->scard('set'));
        $this->redis->srem('set', 'val2');
        $this->assertEquals(0, $this->redis->scard('set'));
    }

    public function testsMove() {
        $this->redis->del('{set}0');
        $this->redis->del('{set}1');

        $this->redis->sAdd('{set}0', 'val');
        $this->redis->sAdd('{set}0', 'val2');

        $this->assertTrue($this->redis->sMove('{set}0', '{set}1', 'val'));
        $this->assertFalse($this->redis->sMove('{set}0', '{set}1', 'val'));
        $this->assertFalse($this->redis->sMove('{set}0', '{set}1', 'val-what'));

        $this->assertEquals(1, $this->redis->scard('{set}0'));
        $this->assertEquals(1, $this->redis->scard('{set}1'));

        $this->assertEquals(['val2'], $this->redis->smembers('{set}0'));
        $this->assertEquals(['val'], $this->redis->smembers('{set}1'));
    }

    public function testsPop() {
        $this->redis->del('set0');
        $this->assertFalse($this->redis->sPop('set0'));

        $this->redis->sAdd('set0', 'val');
        $this->redis->sAdd('set0', 'val2');

        $v0 = $this->redis->sPop('set0');
        $this->assertEquals(1, $this->redis->scard('set0'));
        $this->assertInArray($v0, ['val', 'val2']);
        $v1 = $this->redis->sPop('set0');
        $this->assertEquals(0, $this->redis->scard('set0'));
        $this->assertEqualsCanonicalizing(['val', 'val2'], [$v0, $v1]);

        $this->assertFalse($this->redis->sPop('set0'));
    }

    public function testsPopWithCount() {
        if ( ! $this->minVersionCheck('3.2'))
            $this->markTestSkipped();

        $set = 'set0';
        $prefix = 'member';
        $count = 5;

        /* Add a few members */
        $this->redis->del($set);
        for ($i = 0; $i < $count; $i++) {
            $this->redis->sadd($set, $prefix.$i);
        }

        /* Pop them all */
        $ret = $this->redis->sPop($set, $i);

        /* Make sure we got an arary and the count is right */
        if ($this->assertIsArray($ret, $count)) {
            /* Probably overkill but validate the actual returned members */
            for ($i = 0; $i < $count; $i++) {
                $this->assertInArray($prefix.$i, $ret);
            }
        }
    }

    public function testsRandMember() {
        $this->redis->del('set0');
        $this->assertFalse($this->redis->sRandMember('set0'));

        $this->redis->sAdd('set0', 'val');
        $this->redis->sAdd('set0', 'val2');

        $got = [];
        while (true) {
            $v = $this->redis->sRandMember('set0');
            $this->assertEquals(2, $this->redis->scard('set0')); // no change.
            $this->assertInArray($v, ['val', 'val2']);

            $got[$v] = $v;
            if (count($got) == 2) {
                break;
            }
        }

        //
        // With and without count, while serializing
        //

        $this->redis->del('set0');
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
        for ($i = 0; $i < 5; $i++) {
            $member = "member:$i";
            $this->redis->sAdd('set0', $member);
            $mems[] = $member;
        }

        $member = $this->redis->srandmember('set0');
        $this->assertInArray($member, $mems);

        $rmembers = $this->redis->srandmember('set0', $i);
        foreach ($rmembers as $reply_mem) {
            $this->assertInArray($reply_mem, $mems);
        }

        /* Ensure we can handle basically any return type */
        foreach ([3.1415, new stdClass(), 42, 'hello', NULL] as $val) {
            $this->assertEquals(1, $this->redis->del('set0'));
            $this->assertEquals(1, $this->redis->sadd('set0', $val));
            $this->assertSameType($val, $this->redis->srandmember('set0'));
        }

        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
    }

    public function testSRandMemberWithCount() {
        // Make sure the set is nuked
        $this->redis->del('set0');

        // Run with a count (positive and negative) on an empty set
        $ret_pos = $this->redis->sRandMember('set0', 10);
        $ret_neg = $this->redis->sRandMember('set0', -10);

        // Should both be empty arrays
        $this->assertEquals([], $ret_pos);
        $this->assertEquals([], $ret_neg);

        // Add a few items to the set
        for ($i = 0; $i < 100; $i++) {
            $this->redis->sadd('set0', "member$i");
        }

        // Get less than the size of the list
        $ret_slice = $this->redis->srandmember('set0', 20);

        // Should be an array with 20 items
        $this->assertIsArray($ret_slice, 20);

        // Ask for more items than are in the list (but with a positive count)
        $ret_slice = $this->redis->srandmember('set0', 200);

        // Should be an array, should be however big the set is, exactly
        $this->assertIsArray($ret_slice, $i);

        // Now ask for too many items but negative
        $ret_slice = $this->redis->srandmember('set0', -200);

        // Should be an array, should have exactly the # of items we asked for (will be dups)
        $this->assertIsArray($ret_slice, 200);

        //
        // Test in a pipeline
        //

        if ($this->havePipeline()) {
            $pipe = $this->redis->pipeline();

            $pipe->srandmember('set0', 20);
            $pipe->srandmember('set0', 200);
            $pipe->srandmember('set0', -200);

            $ret = $this->redis->exec();

            $this->assertIsArray($ret[0], 20);
            $this->assertIsArray($ret[1], $i);
            $this->assertIsArray($ret[2], 200);

            // Kill the set
            $this->redis->del('set0');
        }
    }

    public function testSIsMember() {
        $this->redis->del('set');

        $this->redis->sAdd('set', 'val');

        $this->assertTrue($this->redis->sismember('set', 'val'));
        $this->assertFalse($this->redis->sismember('set', 'val2'));
    }

    public function testSMembers() {
        $this->redis->del('set');

        $data = ['val', 'val2', 'val3'];
        foreach ($data as $member) {
            $this->redis->sAdd('set', $member);
        }

        $this->assertEqualsCanonicalizing($data, $this->redis->smembers('set'));
    }

    public function testsMisMember() {
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('set');

        $this->redis->sAdd('set', 'val');
        $this->redis->sAdd('set', 'val2');
        $this->redis->sAdd('set', 'val3');

        $misMembers = $this->redis->sMisMember('set', 'val', 'notamember', 'val3');
        $this->assertEquals([1, 0, 1], $misMembers);

        $misMembers = $this->redis->sMisMember('wrongkey', 'val', 'val2', 'val3');
        $this->assertEquals([0, 0, 0], $misMembers);
    }

    public function testlSet() {
        $this->redis->del('list');
        $this->redis->lPush('list', 'val');
        $this->redis->lPush('list', 'val2');
        $this->redis->lPush('list', 'val3');

        $this->assertEquals('val3', $this->redis->lIndex('list', 0));
        $this->assertEquals('val2', $this->redis->lIndex('list', 1));
        $this->assertEquals('val', $this->redis->lIndex('list', 2));

        $this->assertTrue($this->redis->lSet('list', 1, 'valx'));

        $this->assertEquals('val3', $this->redis->lIndex('list', 0));
        $this->assertEquals('valx', $this->redis->lIndex('list', 1));
        $this->assertEquals('val', $this->redis->lIndex('list', 2));
    }

    public function testsInter() {
        $this->redis->del('{set}odd');    // set of odd numbers
        $this->redis->del('{set}prime');  // set of prime numbers
        $this->redis->del('{set}square'); // set of squares
        $this->redis->del('{set}seq');    // set of numbers of the form n^2 - 1

        $x = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25];
        foreach ($x as $i) {
            $this->redis->sAdd('{set}odd', $i);
        }

        $y = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
        foreach ($y as $i) {
            $this->redis->sAdd('{set}prime', $i);
        }

        $z = [1, 4, 9, 16, 25];
        foreach ($z as $i) {
            $this->redis->sAdd('{set}square', $i);
        }

        $t = [2, 5, 10, 17, 26];
        foreach ($t as $i) {
            $this->redis->sAdd('{set}seq', $i);
        }

        $xy = $this->redis->sInter('{set}odd', '{set}prime');   // odd prime numbers
        foreach ($xy as $i) {
            $i = (int)$i;
            $this->assertInArray($i, array_intersect($x, $y));
        }

        $xy = $this->redis->sInter(['{set}odd', '{set}prime']);    // odd prime numbers, as array.
        foreach ($xy as $i) {
            $i = (int)$i;
            $this->assertInArray($i, array_intersect($x, $y));
        }

        $yz = $this->redis->sInter('{set}prime', '{set}square');   // set of prime squares
        foreach ($yz as $i) {
            $i = (int)$i;
            $this->assertInArray($i, array_intersect($y, $z));
        }

        $yz = $this->redis->sInter(['{set}prime', '{set}square']);    // set of odd squares, as array
        foreach ($yz as $i) {
        $i = (int)$i;
            $this->assertInArray($i, array_intersect($y, $z));
        }

        $zt = $this->redis->sInter('{set}square', '{set}seq');   // prime squares
        $this->assertEquals([], $zt);
        $zt = $this->redis->sInter(['{set}square', '{set}seq']);    // prime squares, as array
        $this->assertEquals([], $zt);

        $xyz = $this->redis->sInter('{set}odd', '{set}prime', '{set}square');// odd prime squares
        $this->assertEquals(['1'], $xyz);

        $xyz = $this->redis->sInter(['{set}odd', '{set}prime', '{set}square']);// odd prime squares, with an array as a parameter
        $this->assertEquals(['1'], $xyz);

        $nil = $this->redis->sInter([]);
        $this->assertFalse($nil);
    }

    public function testsInterStore() {
        $this->redis->del('{set}x', '{set}y', '{set}z', '{set}t');

        $x = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25];
        foreach ($x as $i) {
            $this->redis->sAdd('{set}x', $i);
        }

        $y = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
        foreach ($y as $i) {
            $this->redis->sAdd('{set}y', $i);
        }

        $z = [1, 4, 9, 16, 25];
        foreach ($z as $i) {
            $this->redis->sAdd('{set}z', $i);
        }

        $t = [2, 5, 10, 17, 26];
        foreach ($t as $i) {
            $this->redis->sAdd('{set}t', $i);
        }

        /* Regression test for passing a single array */
        $this->assertEquals(
            count(array_intersect($x,$y)),
            $this->redis->sInterStore(['{set}k', '{set}x', '{set}y'])
        );

        $count = $this->redis->sInterStore('{set}k', '{set}x', '{set}y');  // odd prime numbers
        $this->assertEquals($count, $this->redis->scard('{set}k'));
        foreach (array_intersect($x, $y) as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sInterStore('{set}k', '{set}y', '{set}z');  // set of odd squares
        $this->assertEquals($count, $this->redis->scard('{set}k'));
        foreach (array_intersect($y, $z) as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sInterStore('{set}k', '{set}z', '{set}t');  // squares of the form n^2 + 1
        $this->assertEquals($count, 0);
        $this->assertEquals($count, $this->redis->scard('{set}k'));

        $this->redis->del('{set}z');
        $xyz = $this->redis->sInterStore('{set}k', '{set}x', '{set}y', '{set}z'); // only z missing, expect 0.
        $this->assertEquals(0, $xyz);

        $this->redis->del('{set}y');
        $xyz = $this->redis->sInterStore('{set}k', '{set}x', '{set}y', '{set}z'); // y and z missing, expect 0.
        $this->assertEquals(0, $xyz);

        $this->redis->del('{set}x');
        $xyz = $this->redis->sInterStore('{set}k', '{set}x', '{set}y', '{set}z'); // x y and z ALL missing, expect 0.
        $this->assertEquals(0, $xyz);
    }

    public function testsUnion() {
        $this->redis->del('{set}x');  // set of odd numbers
        $this->redis->del('{set}y');  // set of prime numbers
        $this->redis->del('{set}z');  // set of squares
        $this->redis->del('{set}t');  // set of numbers of the form n^2 - 1

        $x = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25];
        foreach ($x as $i) {
            $this->redis->sAdd('{set}x', $i);
        }

        $y = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
        foreach ($y as $i) {
            $this->redis->sAdd('{set}y', $i);
        }

        $z = [1, 4, 9, 16, 25];
        foreach ($z as $i) {
            $this->redis->sAdd('{set}z', $i);
        }

        $t = [2, 5, 10, 17, 26];
        foreach ($t as $i) {
            $this->redis->sAdd('{set}t', $i);
        }

        $xy = $this->redis->sUnion('{set}x', '{set}y');   // x U y
        foreach ($xy as $i) {
            $this->assertInArray($i, array_merge($x, $y));
        }

        $yz = $this->redis->sUnion('{set}y', '{set}z');   // y U Z
        foreach ($yz as $i) {
        $i = (int)$i;
            $this->assertInArray($i, array_merge($y, $z));
        }

        $zt = $this->redis->sUnion('{set}z', '{set}t');   // z U t
        foreach ($zt as $i) {
        $i = (int)$i;
            $this->assertInArray($i, array_merge($z, $t));
        }

        $xyz = $this->redis->sUnion('{set}x', '{set}y', '{set}z'); // x U y U z
        foreach ($xyz as $i) {
            $this->assertInArray($i, array_merge($x, $y, $z));
        }
    }

    public function testsUnionStore() {
        $this->redis->del('{set}x', '{set}y', '{set}z', '{set}t');

        $x = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25];
        foreach ($x as $i) {
            $this->redis->sAdd('{set}x', $i);
        }

        $y = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
        foreach ($y as $i) {
            $this->redis->sAdd('{set}y', $i);
        }

        $z = [1, 4, 9, 16, 25];
        foreach ($z as $i) {
            $this->redis->sAdd('{set}z', $i);
        }

        $t = [2, 5, 10, 17, 26];
        foreach ($t as $i) {
            $this->redis->sAdd('{set}t', $i);
        }

        $count = $this->redis->sUnionStore('{set}k', '{set}x', '{set}y');  // x U y
        $xy = array_unique(array_merge($x, $y));
        $this->assertEquals($count, count($xy));
        foreach ($xy as $i) {
        $i = (int)$i;
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sUnionStore('{set}k', '{set}y', '{set}z');  // y U z
        $yz = array_unique(array_merge($y, $z));
        $this->assertEquals($count, count($yz));
        foreach ($yz as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sUnionStore('{set}k', '{set}z', '{set}t');  // z U t
        $zt = array_unique(array_merge($z, $t));
        $this->assertEquals($count, count($zt));
        foreach ($zt as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sUnionStore('{set}k', '{set}x', '{set}y', '{set}z'); // x U y U z
        $xyz = array_unique(array_merge($x, $y, $z));
        $this->assertEquals($count, count($xyz));
        foreach ($xyz as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $this->redis->del('{set}x');  // x missing now
        $count = $this->redis->sUnionStore('{set}k', '{set}x', '{set}y', '{set}z'); // x U y U z
        $this->assertEquals($count, count(array_unique(array_merge($y, $z))));

        $this->redis->del('{set}y');  // x and y missing
        $count = $this->redis->sUnionStore('{set}k', '{set}x', '{set}y', '{set}z'); // x U y U z
        $this->assertEquals($count, count(array_unique($z)));

        $this->redis->del('{set}z');  // x, y, and z ALL missing
        $count = $this->redis->sUnionStore('{set}k', '{set}x', '{set}y', '{set}z'); // x U y U z
        $this->assertEquals(0, $count);
    }

    public function testsDiff() {
        $this->redis->del('{set}x');  // set of odd numbers
        $this->redis->del('{set}y');  // set of prime numbers
        $this->redis->del('{set}z');  // set of squares
        $this->redis->del('{set}t');  // set of numbers of the form n^2 - 1

        $x = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25];
        foreach ($x as $i) {
            $this->redis->sAdd('{set}x', $i);
        }

        $y = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
        foreach ($y as $i) {
            $this->redis->sAdd('{set}y', $i);
        }

        $z = [1, 4, 9, 16, 25];
        foreach ($z as $i) {
            $this->redis->sAdd('{set}z', $i);
        }

        $t = [2, 5, 10, 17, 26];
        foreach ($t as $i) {
            $this->redis->sAdd('{set}t', $i);
        }

        $xy = $this->redis->sDiff('{set}x', '{set}y');    // x U y
        foreach ($xy as $i) {
        $i = (int)$i;
            $this->assertInArray($i, array_diff($x, $y));
        }

        $yz = $this->redis->sDiff('{set}y', '{set}z');    // y U Z
        foreach ($yz as $i) {
        $i = (int)$i;
            $this->assertInArray($i, array_diff($y, $z));
        }

        $zt = $this->redis->sDiff('{set}z', '{set}t');    // z U t
        foreach ($zt as $i) {
        $i = (int)$i;
            $this->assertInArray($i, array_diff($z, $t));
        }

        $xyz = $this->redis->sDiff('{set}x', '{set}y', '{set}z'); // x U y U z
        foreach ($xyz as $i) {
        $i = (int)$i;
            $this->assertInArray($i, array_diff($x, $y, $z));
        }
    }

    public function testsDiffStore() {
        $this->redis->del('{set}x', '{set}y', '{set}z', '{set}t');

        $x = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25];
        foreach ($x as $i) {
            $this->redis->sAdd('{set}x', $i);
        }

        $y = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
        foreach ($y as $i) {
            $this->redis->sAdd('{set}y', $i);
        }

        $z = [1, 4, 9, 16, 25];
        foreach ($z as $i) {
            $this->redis->sAdd('{set}z', $i);
        }

        $t = [2, 5, 10, 17, 26];
        foreach ($t as $i) {
            $this->redis->sAdd('{set}t', $i);
        }

        $count = $this->redis->sDiffStore('{set}k', '{set}x', '{set}y');   // x - y
        $xy = array_unique(array_diff($x, $y));
        $this->assertEquals($count, count($xy));
        foreach ($xy as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sDiffStore('{set}k', '{set}y', '{set}z');   // y - z
        $yz = array_unique(array_diff($y, $z));
        $this->assertEquals($count, count($yz));
        foreach ($yz as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sDiffStore('{set}k', '{set}z', '{set}t');   // z - t
        $zt = array_unique(array_diff($z, $t));
        $this->assertEquals($count, count($zt));
        foreach ($zt as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $count = $this->redis->sDiffStore('{set}k', '{set}x', '{set}y', '{set}z');  // x - y - z
        $xyz = array_unique(array_diff($x, $y, $z));
        $this->assertEquals($count, count($xyz));
        foreach ($xyz as $i) {
            $this->assertTrue($this->redis->sismember('{set}k', $i));
        }

        $this->redis->del('{set}x');  // x missing now
        $count = $this->redis->sDiffStore('{set}k', '{set}x', '{set}y', '{set}z');  // x - y - z
        $this->assertEquals(0, $count);

        $this->redis->del('{set}y');  // x and y missing
        $count = $this->redis->sDiffStore('{set}k', '{set}x', '{set}y', '{set}z');  // x - y - z
        $this->assertEquals(0, $count);

        $this->redis->del('{set}z');  // x, y, and z ALL missing
        $count = $this->redis->sDiffStore('{set}k', '{set}x', '{set}y', '{set}z');  // x - y - z
        $this->assertEquals(0, $count);
    }

    public function testInterCard() {
        if (version_compare($this->version, '7.0.0') < 0)
            $this->markTestSkipped();

        $set_data = [
            ['aardvark', 'dog', 'fish', 'squirrel', 'tiger'],
            ['bear', 'coyote', 'fish', 'gorilla', 'dog']
        ];

        $ssets = $zsets = [];

        foreach ($set_data as $n => $values) {
            $sset = "s{set}:$n";
            $zset = "z{set}:$n";

            $this->redis->del([$sset, $zset]);

            $ssets[] = $sset;
            $zsets[] = $zset;

            foreach ($values as $score => $value) {
                $this->assertEquals(1, $this->redis->sAdd("s{set}:$n", $value));
                $this->assertEquals(1, $this->redis->zAdd("z{set}:$n", $score, $value));
            }
        }

        $exp = count(array_intersect(...$set_data));

        $act = $this->redis->sintercard($ssets);
        $this->assertEquals($exp, $act);
        $act = $this->redis->zintercard($zsets);
        $this->assertEquals($exp, $act);

        $this->assertEquals(1, $this->redis->sintercard($ssets, 1));
        $this->assertEquals(2, $this->redis->sintercard($ssets, 2));

        $this->assertEquals(1, $this->redis->zintercard($zsets, 1));
        $this->assertEquals(2, $this->redis->zintercard($zsets, 2));

        $this->assertFalse(@$this->redis->sintercard($ssets, -1));
        $this->assertFalse(@$this->redis->zintercard($ssets, -1));

        $this->assertFalse(@$this->redis->sintercard([]));
        $this->assertFalse(@$this->redis->zintercard([]));

        $this->redis->del(array_merge($ssets, $zsets));
    }

    public function testLRange() {
        $this->redis->del('list');
        $this->redis->lPush('list', 'val');
        $this->redis->lPush('list', 'val2');
        $this->redis->lPush('list', 'val3');

        $this->assertEquals(['val3'], $this->redis->lrange('list', 0, 0));
        $this->assertEquals(['val3', 'val2'], $this->redis->lrange('list', 0, 1));
        $this->assertEquals(['val3', 'val2', 'val'], $this->redis->lrange('list', 0, 2));
        $this->assertEquals(['val3', 'val2', 'val'], $this->redis->lrange('list', 0, 3));

        $this->assertEquals(['val3', 'val2', 'val'], $this->redis->lrange('list', 0, -1));
        $this->assertEquals(['val3', 'val2'], $this->redis->lrange('list', 0, -2));
        $this->assertEquals(['val2', 'val'], $this->redis->lrange('list', -2, -1));

        $this->redis->del('list');
        $this->assertEquals([], $this->redis->lrange('list', 0, -1));
    }

    public function testdbSize() {
        $this->assertTrue($this->redis->flushDB());
        $this->redis->set('x', 'y');
        $this->assertEquals(1, $this->redis->dbSize());
    }

    public function testFlushDB() {
        $this->assertTrue($this->redis->flushdb());
        $this->assertTrue($this->redis->flushdb(NULL));
        $this->assertTrue($this->redis->flushdb(false));
        $this->assertTrue($this->redis->flushdb(true));
    }

    public function testTTL() {
        $this->redis->set('x', 'y');
        $this->redis->expire('x', 5);
        $ttl = $this->redis->ttl('x');
        $this->assertBetween($ttl, 1, 5);

        // A key with no TTL
        $this->redis->del('x'); $this->redis->set('x', 'bar');
        $this->assertEquals(-1, $this->redis->ttl('x'));

        // A key that doesn't exist (> 2.8 will return -2)
        if (version_compare($this->version, '2.8.0') >= 0) {
            $this->redis->del('x');
            $this->assertEquals(-2, $this->redis->ttl('x'));
        }
    }

    public function testPersist() {
        $this->redis->set('x', 'y');
        $this->redis->expire('x', 100);
        $this->assertTrue($this->redis->persist('x'));     // true if there is a timeout
        $this->assertEquals(-1, $this->redis->ttl('x'));       // -1: timeout has been removed.
        $this->assertFalse($this->redis->persist('x'));    // false if there is no timeout
        $this->redis->del('x');
        $this->assertFalse($this->redis->persist('x'));    // false if the key doesn’t exist.
    }

    public function testClient() {
        /* CLIENT SETNAME */
        $this->assertTrue($this->redis->client('setname', 'phpredis_unit_tests'));

        /* CLIENT LIST */
        $clients = $this->redis->client('list');
        $this->assertIsArray($clients);

        // Figure out which ip:port is us!
        $address = NULL;
        foreach ($clients as $client) {
            if ($client['name'] == 'phpredis_unit_tests') {
                $address = $client['addr'];
            }
        }

        // We should have found our connection
        $this->assertIsString($address);

        /* CLIENT GETNAME */
        $this->assertEquals('phpredis_unit_tests', $this->redis->client('getname'));

        if (version_compare($this->version, '5.0.0') >= 0) {
            $this->assertGT(0, $this->redis->client('id'));
            if (version_compare($this->version, '6.0.0') >= 0) {
                $this->assertEquals(-1, $this->redis->client('getredir'));
                $this->assertTrue($this->redis->client('tracking', 'on', ['optin' => true]));
                $this->assertEquals(0, $this->redis->client('getredir'));
                $this->assertTrue($this->redis->client('caching', 'yes'));
                $this->assertTrue($this->redis->client('tracking', 'off'));
                if (version_compare($this->version, '6.2.0') >= 0) {
                    $this->assertFalse(empty($this->redis->client('info')));
                    $this->assertEquals([
                        'flags' => ['off'],
                        'redirect' => -1,
                        'prefixes' => [],
                    ], $this->redis->client('trackinginfo'));

                    if (version_compare($this->version, '7.0.0') >= 0) {
                        $this->assertTrue($this->redis->client('no-evict', 'on'));
                    }
                }
            }
        }

        /* CLIENT KILL -- phpredis will reconnect, so we can do this */
        $this->assertTrue($this->redis->client('kill', $address));

    }

    public function testSlowlog() {
        // We don't really know what's going to be in the slowlog, but make sure
        // the command returns proper types when called in various ways
        $this->assertIsArray($this->redis->slowlog('get'));
        $this->assertIsArray($this->redis->slowlog('get', 10));
        $this->assertIsInt($this->redis->slowlog('len'));
        $this->assertTrue($this->redis->slowlog('reset'));
        $this->assertFalse(@$this->redis->slowlog('notvalid'));
    }

    public function testWait() {
        // Closest we can check based on redis commit history
        if (version_compare($this->version, '2.9.11') < 0)
            $this->markTestSkipped();

        // We could have slaves here, so determine that
        $info     = $this->redis->info();
        $replicas = $info['connected_slaves'];

        // Send a couple commands
        $this->redis->set('wait-foo', 'over9000');
        $this->redis->set('wait-bar', 'revo9000');

        // Make sure we get the right replication count
        $this->assertEquals($replicas, $this->redis->wait($replicas, 100));

        // Pass more slaves than are connected
        $this->redis->set('wait-foo', 'over9000');
        $this->redis->set('wait-bar', 'revo9000');
        $this->assertLT($replicas + 1, $this->redis->wait($replicas + 1, 100));

        // Make sure when we pass with bad arguments we just get back false
        $this->assertFalse($this->redis->wait(-1, -1));
    }

    public function testInfo() {
        $sequence = [false];
        if ($this->haveMulti())
            $sequence[] = true;

        foreach ($sequence as $boo_multi) {
            if ($boo_multi) {
                $this->redis->multi();
                $this->redis->info();
                $info = $this->redis->exec();
                $info = $info[0];
            } else {
                $info = $this->redis->info();
            }

            $keys = [
                'redis_version',
                'arch_bits',
                'uptime_in_seconds',
                'uptime_in_days',
                'connected_clients',
                'connected_slaves',
                'used_memory',
                'total_connections_received',
                'total_commands_processed',
                'role'
            ];
            if (version_compare($this->version, '2.5.0') < 0) {
                array_push($keys,
                    'changes_since_last_save',
                    'bgsave_in_progress',
                    'last_save_time'
                );
            } else {
                array_push($keys,
                    'rdb_changes_since_last_save',
                    'rdb_bgsave_in_progress',
                    'rdb_last_save_time'
                );
            }

            foreach ($keys as $k) {
                $this->assertInArray($k, array_keys($info));
            }
        }

        if ( ! $this->minVersionCheck('7.0.0'))
            return;

        $res = $this->redis->info('server', 'memory');
        $this->assertTrue(is_array($res) && isset($res['redis_version']) && isset($res['used_memory']));
    }

    protected function execHello() {
        $zipped = [];

        $result = $this->redis->rawCommand('HELLO');
        if ( ! is_array($result) || count($result) % 2 != 0)
            return false;

        for ($i = 0; $i < count($result); $i += 2) {
            $zipped[$result[$i]] = $result[$i + 1];
        }

        return $zipped;
    }

    public function testServerInfo() {
        if ( ! $this->minVersionCheck('6.0.0'))
            $this->markTestSkipped();

        $hello = $this->execHello();

        if ( ! $this->assertArrayKey($hello, 'server') ||
             ! $this->assertArrayKey($hello, 'version'))
        {
            return false;
        }

        $this->assertEquals($hello['server'], $this->redis->serverName());
        $this->assertEquals($hello['version'], $this->redis->serverVersion());

        $info = $this->redis->info();

        $cmd1 = $info['total_commands_processed'];

        /* Shouldn't hit the server */
        $this->assertEquals($hello['server'], $this->redis->serverName());
        $this->assertEquals($hello['version'], $this->redis->serverVersion());

        $info = $this->redis->info();
        $cmd2 = $info['total_commands_processed'];

        $this->assertEquals(1 + $cmd1, $cmd2);
    }

    public function testServerInfoOldRedis() {
        if ($this->minVersionCheck('6.0.0'))
            $this->markTestSkipped();

        $this->assertFalse($this->redis->serverName());
        $this->assertFalse($this->redis->serverVersion());
    }

    public function testInfoCommandStats() {
        // INFO COMMANDSTATS is new in 2.6.0
        if (version_compare($this->version, '2.5.0') < 0)
            $this->markTestSkipped();

        $info = $this->redis->info('COMMANDSTATS');
        if ( ! $this->assertIsArray($info))
            return;

        foreach ($info as $k => $value) {
            $this->assertStringContains('cmdstat_', $k);
        }
    }

    public function testSelect() {
        $this->assertFalse(@$this->redis->select(-1));
        $this->assertTrue($this->redis->select(0));
    }

    public function testSwapDB() {
        if (version_compare($this->version, '4.0.0') < 0)
            $this->markTestSkipped();

        $this->assertTrue($this->redis->swapdb(0, 1));
        $this->assertTrue($this->redis->swapdb(0, 1));
    }

    public function testMset() {
        $this->redis->del('x', 'y', 'z');    // remove x y z
        $this->assertTrue($this->redis->mset(['x' => 'a', 'y' => 'b', 'z' => 'c']));   // set x y z

        $this->assertEquals(['a', 'b', 'c'], $this->redis->mget(['x', 'y', 'z']));    // check x y z

        $this->redis->del('x');  // delete just x
        $this->assertTrue($this->redis->mset(['x' => 'a', 'y' => 'b', 'z' => 'c']));   // set x y z
        $this->assertEquals(['a', 'b', 'c'], $this->redis->mget(['x', 'y', 'z']));    // check x y z

        $this->assertFalse($this->redis->mset([])); // set ø → FALSE

        /*
         * Integer keys
         */

        // No prefix
        $set_array = [-1 => 'neg1', -2 => 'neg2', -3 => 'neg3', 1 => 'one', 2 => 'two', '3' => 'three'];
        $this->redis->del(array_keys($set_array));
        $this->assertTrue($this->redis->mset($set_array));
        $this->assertEquals(array_values($set_array), $this->redis->mget(array_keys($set_array)));
        $this->redis->del(array_keys($set_array));

        // With a prefix
        $this->redis->setOption(Redis::OPT_PREFIX, 'pfx:');
        $this->redis->del(array_keys($set_array));
        $this->assertTrue($this->redis->mset($set_array));
        $this->assertEquals(array_values($set_array), $this->redis->mget(array_keys($set_array)));
        $this->redis->del(array_keys($set_array));
        $this->redis->setOption(Redis::OPT_PREFIX, '');
    }

    public function testMsetNX() {
        $this->redis->del('x', 'y', 'z');    // remove x y z
        $this->assertTrue($this->redis->msetnx(['x' => 'a', 'y' => 'b', 'z' => 'c']));    // set x y z

        $this->assertEquals(['a', 'b', 'c'], $this->redis->mget(['x', 'y', 'z']));    // check x y z

        $this->redis->del('x');  // delete just x
        $this->assertFalse($this->redis->msetnx(['x' => 'A', 'y' => 'B', 'z' => 'C']));   // set x y z
        $this->assertEquals([FALSE, 'b', 'c'], $this->redis->mget(['x', 'y', 'z']));  // check x y z

        $this->assertFalse($this->redis->msetnx([])); // set ø → FALSE
    }

    public function testRpopLpush() {
        // standard case.
        $this->redis->del('{list}x', '{list}y');
        $this->redis->lpush('{list}x', 'abc');
        $this->redis->lpush('{list}x', 'def');    // x = [def, abc]

        $this->redis->lpush('{list}y', '123');
        $this->redis->lpush('{list}y', '456');    // y = [456, 123]

        $this->assertEquals('abc', $this->redis->rpoplpush('{list}x', '{list}y'));  // we RPOP x, yielding abc.
        $this->assertEquals(['def'], $this->redis->lrange('{list}x', 0, -1)); // only def remains in x.
        $this->assertEquals(['abc', '456', '123'], $this->redis->lrange('{list}y', 0, -1));   // abc has been lpushed to y.

        // with an empty source, expecting no change.
        $this->redis->del('{list}x', '{list}y');
        $this->assertFalse($this->redis->rpoplpush('{list}x', '{list}y'));
        $this->assertEquals([], $this->redis->lrange('{list}x', 0, -1));
        $this->assertEquals([], $this->redis->lrange('{list}y', 0, -1));
    }

    public function testBRpopLpush() {
        // standard case.
        $this->redis->del('{list}x', '{list}y');
        $this->redis->lpush('{list}x', 'abc');
        $this->redis->lpush('{list}x', 'def');    // x = [def, abc]

        $this->redis->lpush('{list}y', '123');
        $this->redis->lpush('{list}y', '456');    // y = [456, 123]

        $this->assertEquals('abc', $this->redis->brpoplpush('{list}x', '{list}y', 1));  // we RPOP x, yielding abc.

        $this->assertEquals(['def'], $this->redis->lrange('{list}x', 0, -1)); // only def remains in x.
        $this->assertEquals(['abc', '456', '123'], $this->redis->lrange('{list}y', 0, -1));   // abc has been lpushed to y.

        // with an empty source, expecting no change.
        $this->redis->del('{list}x', '{list}y');
        $this->assertFalse($this->redis->brpoplpush('{list}x', '{list}y', 1));
        $this->assertEquals([], $this->redis->lrange('{list}x', 0, -1));
        $this->assertEquals([], $this->redis->lrange('{list}y', 0, -1));

        if ( ! $this->minVersionCheck('6.0.0'))
            return;

        // Redis >= 6.0.0 allows floating point timeouts
        $st = microtime(true);
        $this->assertFalse($this->redis->brpoplpush('{list}x', '{list}y', .1));
        $et = microtime(true);
        $this->assertLT(1.0, $et - $st);
    }

    public function testZAddFirstArg() {
        $zsetName = 100; // not a string!

        $this->redis->del($zsetName);

        $this->assertEquals(1, $this->redis->zAdd($zsetName, 0, 'val0'));
        $this->assertEquals(1, $this->redis->zAdd($zsetName, 1, 'val1'));

        $this->assertEquals(['val0', 'val1'], $this->redis->zRange($zsetName, 0, -1));
    }

    public function testZaddIncr() {
        $this->redis->del('zset');

        $this->assertEquals(10.0, $this->redis->zAdd('zset', ['incr'], 10, 'value'));
        $this->assertEquals(20.0, $this->redis->zAdd('zset', ['incr'], 10, 'value'));

        $this->assertFalse($this->redis->zAdd('zset', ['incr'], 10, 'value', 20, 'value2'));
    }

    /* Regression test for GitHub issue #2697 */
    public function testZAddLargeLong() {
        $this->redis->del('key');

        $val = 5000000000;

        $this->assertEquals(1, $this->redis->zAdd('key', $val, 'val0'));

        $this->assertEquals($this->redis->zscore('key', 'val0'), (float)$val);
    }

    public function testZX() {
        $this->redis->del('key');

        $this->assertEquals([], $this->redis->zRange('key', 0, -1));
        $this->assertEquals([], $this->redis->zRange('key', 0, -1, true));

        $this->assertEquals(1, $this->redis->zAdd('key', 0, 'val0'));
        $this->assertEquals(1, $this->redis->zAdd('key', 2, 'val2'));
        $this->assertEquals(2, $this->redis->zAdd('key', 4, 'val4', 5, 'val5')); // multiple parameters
        if (version_compare($this->version, '3.0.2') < 0) {
            $this->assertEquals(1, $this->redis->zAdd('key', 1, 'val1'));
            $this->assertEquals(1, $this->redis->zAdd('key', 3, 'val3'));
        } else {
            $this->assertEquals(1, $this->redis->zAdd('key', [], 1, 'val1')); // empty options
            $this->assertEquals(1, $this->redis->zAdd('key', ['nx'], 3, 'val3')); // nx option
            $this->assertEquals(0, $this->redis->zAdd('key', ['xx'], 3, 'val3')); // xx option

            if (version_compare($this->version, '6.2.0') >= 0) {
                $this->assertEquals(0, $this->redis->zAdd('key', ['lt'], 4, 'val3')); // lt option
                $this->assertEquals(0, $this->redis->zAdd('key', ['gt'], 2, 'val3')); // gt option
            }
        }

        $this->assertEquals(['val0', 'val1', 'val2', 'val3', 'val4', 'val5'], $this->redis->zRange('key', 0, -1));

        // withscores
        $ret = $this->redis->zRange('key', 0, -1, true);
        $this->assertEquals(6, count($ret));
        $this->assertEquals(0.0, $ret['val0']);
        $this->assertEquals(1.0, $ret['val1']);
        $this->assertEquals(2.0, $ret['val2']);
        $this->assertEquals(3.0, $ret['val3']);
        $this->assertEquals(4.0, $ret['val4']);
        $this->assertEquals(5.0, $ret['val5']);

        $this->assertEquals(0, $this->redis->zRem('key', 'valX'));
        $this->assertEquals(1, $this->redis->zRem('key', 'val3'));
        $this->assertEquals(1, $this->redis->zRem('key', 'val4'));
        $this->assertEquals(1, $this->redis->zRem('key', 'val5'));

        $this->assertEquals(['val0', 'val1', 'val2'], $this->redis->zRange('key', 0, -1));

        // zGetReverseRange

        $this->assertEquals(1, $this->redis->zAdd('key', 3, 'val3'));
        $this->assertEquals(1, $this->redis->zAdd('key', 3, 'aal3'));

        $zero_to_three = $this->redis->zRangeByScore('key', 0, 3);
        $this->assertEquals(['val0', 'val1', 'val2', 'aal3', 'val3'], $zero_to_three);

        $three_to_zero = $this->redis->zRevRangeByScore('key', 3, 0);
        $this->assertEquals(array_reverse(['val0', 'val1', 'val2', 'aal3', 'val3']), $three_to_zero);

        $this->assertEquals(5, $this->redis->zCount('key', 0, 3));

        // withscores
        $this->redis->zRem('key', 'aal3');
        $zero_to_three = $this->redis->zRangeByScore('key', 0, 3, ['withscores' => true]);
        $this->assertEquals(['val0' => 0.0, 'val1' => 1.0, 'val2' => 2.0, 'val3' => 3.0], $zero_to_three);
        $this->assertEquals(4, $this->redis->zCount('key', 0, 3));

        // limit
        $this->assertEquals(['val0'], $this->redis->zRangeByScore('key', 0, 3, ['limit' => [0, 1]]));
        $this->assertEquals(['val0', 'val1'],
                            $this->redis->zRangeByScore('key', 0, 3, ['limit' => [0, 2]]));
        $this->assertEquals(['val1', 'val2'],
                            $this->redis->zRangeByScore('key', 0, 3, ['limit' => [1, 2]]));
        $this->assertEquals(['val0', 'val1'],
                            $this->redis->zRangeByScore('key', 0, 1, ['limit' => [0, 100]]));

        if ($this->minVersionCheck('6.2.0'))
            $this->assertEquals(['val0', 'val1'], $this->redis->zrange('key', 0, 1, ['byscore', 'limit' => [0, 100]]));

        // limits as references
        $limit = [0, 100];
        foreach ($limit as &$val) {}
        $this->assertEquals(['val0', 'val1'], $this->redis->zRangeByScore('key', 0, 1, ['limit' => $limit]));

        $this->assertEquals(
            ['val3'], $this->redis->zRevRangeByScore('key', 3, 0, ['limit' => [0, 1]])
        );
        $this->assertEquals(
            ['val3', 'val2'], $this->redis->zRevRangeByScore('key', 3, 0, ['limit' => [0, 2]])
        );
        $this->assertEquals(
            ['val2', 'val1'], $this->redis->zRevRangeByScore('key', 3, 0, ['limit' => [1, 2]])
        );
        $this->assertEquals(
            ['val1', 'val0'], $this->redis->zRevRangeByScore('key', 1, 0, ['limit' => [0, 100]])
        );

        if ($this->minVersionCheck('6.2.0')) {
            $this->assertEquals(['val1', 'val0'],
                                $this->redis->zrange('key', 1, 0, ['byscore', 'rev', 'limit' => [0, 100]]));
            $this->assertEquals(2, $this->redis->zrangestore('dst{key}', 'key', 1, 0,
                                ['byscore', 'rev', 'limit' => [0, 100]]));
            $this->assertEquals(['val0', 'val1'], $this->redis->zRange('dst{key}', 0, -1));

            $this->assertEquals(1, $this->redis->zrangestore('dst{key}', 'key', 1, 0,
                                ['byscore', 'rev', 'limit' => [0, 1]]));
            $this->assertEquals(['val1'], $this->redis->zrange('dst{key}', 0, -1));
        }

        $this->assertEquals(4, $this->redis->zCard('key'));
        $this->assertEquals(1.0, $this->redis->zScore('key', 'val1'));
        $this->assertFalse($this->redis->zScore('key', 'val'));
        $this->assertFalse($this->redis->zScore(3, 2));

        // with () and +inf, -inf
        $this->redis->del('zset');
        $this->redis->zAdd('zset', 1, 'foo');
        $this->redis->zAdd('zset', 2, 'bar');
        $this->redis->zAdd('zset', 3, 'biz');
        $this->redis->zAdd('zset', 4, 'foz');
        $this->assertEquals(
            ['foo' => 1.0, 'bar' => 2.0, 'biz' => 3.0, 'foz' => 4.0],
            $this->redis->zRangeByScore('zset', '-inf', '+inf', ['withscores' => true])
        );
        $this->assertEquals(
            ['foo' => 1.0, 'bar' => 2.0],
            $this->redis->zRangeByScore('zset', 1, 2, ['withscores' => true])
        );
        $this->assertEquals(
            ['bar' => 2.0],
            $this->redis->zRangeByScore('zset', '(1', 2, ['withscores' => true])
        );
        $this->assertEquals([], $this->redis->zRangeByScore('zset', '(1', '(2', ['withscores' => true]));

        $this->assertEquals(4, $this->redis->zCount('zset', '-inf', '+inf'));
        $this->assertEquals(2, $this->redis->zCount('zset', 1, 2));
        $this->assertEquals(1, $this->redis->zCount('zset', '(1', 2));
        $this->assertEquals(0, $this->redis->zCount('zset', '(1', '(2'));

        // zincrby
        $this->redis->del('key');
        $this->assertEquals(1.0, $this->redis->zIncrBy('key', 1, 'val1'));
        $this->assertEquals(1.0, $this->redis->zScore('key', 'val1'));
        $this->assertEquals(2.5, $this->redis->zIncrBy('key', 1.5, 'val1'));
        $this->assertEquals(2.5, $this->redis->zScore('key', 'val1'));

        // zUnionStore
        $this->redis->del('{zset}1');
        $this->redis->del('{zset}2');
        $this->redis->del('{zset}3');
        $this->redis->del('{zset}U');

        $this->redis->zAdd('{zset}1', 0, 'val0');
        $this->redis->zAdd('{zset}1', 1, 'val1');

        $this->redis->zAdd('{zset}2', 2, 'val2');
        $this->redis->zAdd('{zset}2', 3, 'val3');

        $this->redis->zAdd('{zset}3', 4, 'val4');
        $this->redis->zAdd('{zset}3', 5, 'val5');

        $this->assertEquals(4, $this->redis->zUnionStore('{zset}U', ['{zset}1', '{zset}3']));
        $this->assertEquals(['val0', 'val1', 'val4', 'val5'], $this->redis->zRange('{zset}U', 0, -1));

        // Union on non existing keys
        $this->redis->del('{zset}U');
        $this->assertEquals(0, $this->redis->zUnionStore('{zset}U', ['{zset}X', '{zset}Y']));
        $this->assertEquals([],$this->redis->zRange('{zset}U', 0, -1));

        // !Exist U Exist → copy of existing zset.
        $this->redis->del('{zset}U', 'X');
        $this->assertEquals(2, $this->redis->zUnionStore('{zset}U', ['{zset}1', '{zset}X']));

        // test weighted zUnion
        $this->redis->del('{zset}Z');
        $this->assertEquals(4, $this->redis->zUnionStore('{zset}Z', ['{zset}1', '{zset}2'], [1, 1]));
        $this->assertEquals(['val0', 'val1', 'val2', 'val3'], $this->redis->zRange('{zset}Z', 0, -1));

        $this->redis->zRemRangeByScore('{zset}Z', 0, 10);
        $this->assertEquals(4, $this->redis->zUnionStore('{zset}Z', ['{zset}1', '{zset}2'], [5, 1]));
        $this->assertEquals(['val0', 'val2', 'val3', 'val1'], $this->redis->zRange('{zset}Z', 0, -1));

        $this->redis->del('{zset}1');
        $this->redis->del('{zset}2');
        $this->redis->del('{zset}3');

        //test zUnion with weights and aggegration function
        $this->redis->zadd('{zset}1', 1, 'duplicate');
        $this->redis->zadd('{zset}2', 2, 'duplicate');
        $this->redis->zUnionStore('{zset}U', ['{zset}1', '{zset}2'], [1, 1], 'MIN');
        $this->assertEquals(1.0, $this->redis->zScore('{zset}U', 'duplicate'));
        $this->redis->del('{zset}U');

        //now test zUnion *without* weights but with aggregate function
        $this->redis->zUnionStore('{zset}U', ['{zset}1', '{zset}2'], null, 'MIN');
        $this->assertEquals(1.0, $this->redis->zScore('{zset}U', 'duplicate'));
        $this->redis->del('{zset}U', '{zset}1', '{zset}2');

        // test integer and float weights (GitHub issue #109).
        $this->redis->del('{zset}1', '{zset}2', '{zset}3');

        $this->redis->zadd('{zset}1', 1, 'one');
        $this->redis->zadd('{zset}1', 2, 'two');
        $this->redis->zadd('{zset}2', 1, 'one');
        $this->redis->zadd('{zset}2', 2, 'two');
        $this->redis->zadd('{zset}2', 3, 'three');

        $this->assertEquals(3, $this->redis->zUnionStore('{zset}3', ['{zset}1', '{zset}2'], [2, 3.0]));

        $this->redis->del('{zset}1');
        $this->redis->del('{zset}2');
        $this->redis->del('{zset}3');

        // Test 'inf', '-inf', and '+inf' weights (GitHub issue #336)
        $this->redis->zadd('{zset}1', 1, 'one', 2, 'two', 3, 'three');
        $this->redis->zadd('{zset}2', 3, 'three', 4, 'four', 5, 'five');

        // Make sure phpredis handles these weights
        $this->assertEquals(5, $this->redis->zUnionStore('{zset}3', ['{zset}1', '{zset}2'], [1, 'inf']) );
        $this->assertEquals(5, $this->redis->zUnionStore('{zset}3', ['{zset}1', '{zset}2'], [1, '-inf']));
        $this->assertEquals(5, $this->redis->zUnionStore('{zset}3', ['{zset}1', '{zset}2'], [1, '+inf']));

        // Now, confirm that they're being sent, and that it works
        $weights = ['inf', '-inf', '+inf'];

        foreach ($weights as $weight) {
            $r = $this->redis->zUnionStore('{zset}3', ['{zset}1', '{zset}2'], [1, $weight]);
            $this->assertEquals(5, $r);
            $r = $this->redis->zrangebyscore('{zset}3', '(-inf', '(inf',['withscores'=>true]);
            $this->assertEquals(2, count($r));
            $this->assertArrayKey($r, 'one');
            $this->assertArrayKey($r, 'two');
        }

        $this->redis->del('{zset}1', '{zset}2', '{zset}3');

        $this->redis->zadd('{zset}1', 2000.1, 'one');
        $this->redis->zadd('{zset}1', 3000.1, 'two');
        $this->redis->zadd('{zset}1', 4000.1, 'three');

        $ret = $this->redis->zRange('{zset}1', 0, -1, true);
        $this->assertEquals(3, count($ret));
        $retValues = array_keys($ret);

        $this->assertEquals(['one', 'two', 'three'], $retValues);

        // + 0 converts from string to float OR integer
        $this->assertArrayKeyEquals($ret, 'one', 2000.1);
        $this->assertArrayKeyEquals($ret, 'two', 3000.1);
        $this->assertArrayKeyEquals($ret, 'three', 4000.1);

        $this->redis->del('{zset}1');

        // ZREMRANGEBYRANK
        $this->redis->zAdd('{zset}1', 1, 'one');
        $this->redis->zAdd('{zset}1', 2, 'two');
        $this->redis->zAdd('{zset}1', 3, 'three');
        $this->assertEquals(2, $this->redis->zremrangebyrank('{zset}1', 0, 1));
        $this->assertEquals(['three' => 3.], $this->redis->zRange('{zset}1', 0, -1, true));

        $this->redis->del('{zset}1');

        // zInterStore

        $this->redis->zAdd('{zset}1', 0, 'val0');
        $this->redis->zAdd('{zset}1', 1, 'val1');
        $this->redis->zAdd('{zset}1', 3, 'val3');

        $this->redis->zAdd('{zset}2', 2, 'val1');
        $this->redis->zAdd('{zset}2', 3, 'val3');

        $this->redis->zAdd('{zset}3', 4, 'val3');
        $this->redis->zAdd('{zset}3', 5, 'val5');

        $this->redis->del('{zset}I');
        $this->assertEquals(2, $this->redis->zInterStore('{zset}I', ['{zset}1', '{zset}2']));
        $this->assertEquals(['val1', 'val3'], $this->redis->zRange('{zset}I', 0, -1));

        // Union on non existing keys
        $this->assertEquals(0, $this->redis->zInterStore('{zset}X', ['{zset}X', '{zset}Y']));
        $this->assertEquals([], $this->redis->zRange('{zset}X', 0, -1));

        // !Exist U Exist
        $this->assertEquals(0, $this->redis->zInterStore('{zset}Y', ['{zset}1', '{zset}X']));
        $this->assertEquals([], $this->redis->zRange('keyY', 0, -1));


        // test weighted zInterStore
        $this->redis->del('{zset}1');
        $this->redis->del('{zset}2');
        $this->redis->del('{zset}3');

        $this->redis->zAdd('{zset}1', 0, 'val0');
        $this->redis->zAdd('{zset}1', 1, 'val1');
        $this->redis->zAdd('{zset}1', 3, 'val3');


        $this->redis->zAdd('{zset}2', 2, 'val1');
        $this->redis->zAdd('{zset}2', 1, 'val3');

        $this->redis->zAdd('{zset}3', 7, 'val1');
        $this->redis->zAdd('{zset}3', 3, 'val3');

        $this->redis->del('{zset}I');
        $this->assertEquals(2, $this->redis->zInterStore('{zset}I', ['{zset}1', '{zset}2'], [1, 1]));
        $this->assertEquals(['val1', 'val3'], $this->redis->zRange('{zset}I', 0, -1));

        $this->redis->del('{zset}I');
        $this->assertEquals(2, $this->redis->zInterStore('{zset}I', ['{zset}1', '{zset}2', '{zset}3'], [1, 5, 1], 'min'));
        $this->assertEquals(['val1', 'val3'], $this->redis->zRange('{zset}I', 0, -1));
        $this->redis->del('{zset}I');
        $this->assertEquals(2, $this->redis->zInterStore('{zset}I', ['{zset}1', '{zset}2', '{zset}3'], [1, 5, 1], 'max'));
        $this->assertEquals(['val3', 'val1'], $this->redis->zRange('{zset}I', 0, -1));

        $this->redis->del('{zset}I');
        $this->assertEquals(2, $this->redis->zInterStore('{zset}I', ['{zset}1', '{zset}2', '{zset}3'], null, 'max'));
        $this->assertEquals(7., $this->redis->zScore('{zset}I', 'val1'));

        // zrank, zrevrank
        $this->redis->del('z');
        $this->redis->zadd('z', 1, 'one');
        $this->redis->zadd('z', 2, 'two');
        $this->redis->zadd('z', 5, 'five');

        $this->assertEquals(0, $this->redis->zRank('z', 'one'));
        $this->assertEquals(1, $this->redis->zRank('z', 'two'));
        $this->assertEquals(2, $this->redis->zRank('z', 'five'));

        $this->assertEquals(2, $this->redis->zRevRank('z', 'one'));
        $this->assertEquals(1, $this->redis->zRevRank('z', 'two'));
        $this->assertEquals(0, $this->redis->zRevRank('z', 'five'));
    }

    public function testZRangeScoreArg() {
        $this->redis->del('{z}');

        $mems = ['one' => 1.0, 'two' => 2.0, 'three' => 3.0];
        foreach ($mems as $mem => $score) {
            $this->redis->zAdd('{z}', $score, $mem);
        }

        /* Verify we can pass true and ['withscores' => true] */
        $this->assertEquals($mems, $this->redis->zRange('{z}', 0, -1, true));
        $this->assertEquals($mems, $this->redis->zRange('{z}', 0, -1, ['withscores' => true]));
    }

    public function testZRangeByLex() {
        /* ZRANGEBYLEX available on versions >= 2.8.9 */
        if (version_compare($this->version, '2.8.9') < 0) {
            $this->MarkTestSkipped();
            return;
        }

        $this->redis->del('key');
        foreach (range('a', 'g') as $c) {
            $this->redis->zAdd('key', 0, $c);
        }

        $this->assertEquals(['a', 'b', 'c'], $this->redis->zRangeByLex('key', '-', '[c'));
        $this->assertEquals(['f', 'g'], $this->redis->zRangeByLex('key', '(e', '+'));


        // with limit offset
        $this->assertEquals(['b', 'c'], $this->redis->zRangeByLex('key', '-', '[c', 1, 2) );
        $this->assertEquals(['b'], $this->redis->zRangeByLex('key', '-', '(c', 1, 2));

        /* Test getting the same functionality via ZRANGE and options */
        if ($this->minVersionCheck('6.2.0')) {
            $this->assertEquals(['a', 'b', 'c'], $this->redis->zRange('key', '-', '[c', ['BYLEX']));
            $this->assertEquals(['b', 'c'], $this->redis->zRange('key', '-', '[c', ['BYLEX', 'LIMIT' => [1, 2]]));
            $this->assertEquals(['b'], $this->redis->zRange('key', '-', '(c', ['BYLEX', 'LIMIT' => [1, 2]]));

            $this->assertEquals(['b', 'a'], $this->redis->zRange('key', '[c', '-', ['BYLEX', 'REV', 'LIMIT' => [1, 2]]));
        }
    }

    public function testZLexCount() {
        if (version_compare($this->version, '2.8.9') < 0) {
            $this->MarkTestSkipped();
            return;
        }

        $this->redis->del('key');
        foreach (range('a', 'g') as $c) {
            $entries[] = $c;
            $this->redis->zAdd('key', 0, $c);
        }

        /* Special -/+ values */
        $this->assertEquals(0, $this->redis->zLexCount('key', '-', '-'));
        $this->assertEquals(count($entries), $this->redis->zLexCount('key', '-', '+'));

        /* Verify invalid arguments return FALSE */
        $this->assertFalse(@$this->redis->zLexCount('key', '[a', 'bad'));
        $this->assertFalse(@$this->redis->zLexCount('key', 'bad', '[a'));

        /* Now iterate through */
        $start = $entries[0];
        for ($i = 1; $i < count($entries); $i++) {
            $end = $entries[$i];
            $this->assertEquals($i + 1, $this->redis->zLexCount('key', "[$start", "[$end"));
            $this->assertEquals($i, $this->redis->zLexCount('key', "[$start", "($end"));
            $this->assertEquals($i - 1, $this->redis->zLexCount('key', "($start", "($end"));
        }
    }

    public function testzDiff() {
        // Only available since 6.2.0
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('key');
        foreach (range('a', 'c') as $c) {
            $this->redis->zAdd('key', 1, $c);
        }

        $this->assertEquals(['a', 'b', 'c'], $this->redis->zDiff(['key']));
        $this->assertEquals(['a' => 1.0, 'b' => 1.0, 'c' => 1.0], $this->redis->zDiff(['key'], ['withscores' => true]));
    }

    public function testzInter() {
        // Only available since 6.2.0
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('key');
        foreach (range('a', 'c') as $c) {
            $this->redis->zAdd('key', 1, $c);
        }

        $this->assertEquals(['a', 'b', 'c'], $this->redis->zInter(['key']));
        $this->assertEquals(['a' => 1.0, 'b' => 1.0, 'c' => 1.0], $this->redis->zInter(['key'], null, ['withscores' => true]));
    }

    public function testzUnion() {
        // Only available since 6.2.0
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('key');
        foreach (range('a', 'c') as $c) {
            $this->redis->zAdd('key', 1, $c);
        }

        $this->assertEquals(['a', 'b', 'c'], $this->redis->zUnion(['key']));
        $this->assertEquals(['a' => 1.0, 'b' => 1.0, 'c' => 1.0], $this->redis->zUnion(['key'], null, ['withscores' => true]));
    }

    public function testzDiffStore() {
        // Only available since 6.2.0
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('{zkey}src');
        foreach (range('a', 'c') as $c) {
            $this->redis->zAdd('{zkey}src', 1, $c);
        }
        $this->assertEquals(3, $this->redis->zDiffStore('{zkey}dst', ['{zkey}src']));
        $this->assertEquals(['a', 'b', 'c'], $this->redis->zRange('{zkey}dst', 0, -1));
    }

    public function testzMscore() {
        // Only available since 6.2.0
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('key');
        foreach (range('a', 'c') as $c) {
            $this->redis->zAdd('key', 1, $c);
        }

        $scores = $this->redis->zMscore('key', 'a', 'notamember', 'c');
        $this->assertEquals([1.0, false, 1.0], $scores);

        $scores = $this->redis->zMscore('wrongkey', 'a', 'b', 'c');
        $this->assertEquals([false, false, false], $scores);
    }

    public function testZRemRangeByLex() {
        if (version_compare($this->version, '2.8.9') < 0) {
            $this->MarkTestSkipped();
            return;
        }

        $this->redis->del('key');
        $this->redis->zAdd('key', 0, 'a', 0, 'b', 0, 'c');
        $this->assertEquals(3, $this->redis->zRemRangeByLex('key', '-', '+'));

        $this->redis->zAdd('key', 0, 'a', 0, 'b', 0, 'c');
        $this->assertEquals(3, $this->redis->zRemRangeByLex('key', '[a', '[c'));

        $this->redis->zAdd('key', 0, 'a', 0, 'b', 0, 'c');
        $this->assertEquals(0, $this->redis->zRemRangeByLex('key', '[a', '(a'));
        $this->assertEquals(1, $this->redis->zRemRangeByLex('key', '(a', '(c'));
        $this->assertEquals(2, $this->redis->zRemRangeByLex('key', '[a', '[c'));
    }

    public function testBZPop() {
        if (version_compare($this->version, '5.0.0') < 0) {
            $this->MarkTestSkipped();
            return;
        }

        $this->redis->del('{zs}1', '{zs}2');
        $this->redis->zAdd('{zs}1', 0, 'a', 1, 'b', 2, 'c');
        $this->redis->zAdd('{zs}2', 3, 'A', 4, 'B', 5, 'D');

        $this->assertEquals(['{zs}1', 'a', '0'], $this->redis->bzPopMin('{zs}1', '{zs}2', 0));
        $this->assertEquals(['{zs}1', 'c', '2'], $this->redis->bzPopMax(['{zs}1', '{zs}2'], 0));
        $this->assertEquals(['{zs}2', 'A', '3'], $this->redis->bzPopMin('{zs}2', '{zs}1', 0));

        /* Verify timeout is being sent */
        $this->redis->del('{zs}1', '{zs}2');
        $st = microtime(true) * 1000;
        $this->redis->bzPopMin('{zs}1', '{zs}2', 1);
        $et = microtime(true) * 1000;
        $this->assertGT(100, $et - $st);
    }

    public function testZPop() {
        if (version_compare($this->version, '5.0.0') < 0) {
            $this->MarkTestSkipped();
            return;
        }

        // zPopMax and zPopMin without a COUNT argument
        $this->redis->del('key');
        $this->redis->zAdd('key', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e');
        $this->assertEquals(['e' => 4.0], $this->redis->zPopMax('key'));
        $this->assertEquals(['a' => 0.0], $this->redis->zPopMin('key'));

        // zPopMax with a COUNT argument
        $this->redis->del('key');
        $this->redis->zAdd('key', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e');
        $this->assertEquals(['e' => 4.0, 'd' => 3.0, 'c' => 2.0], $this->redis->zPopMax('key', 3));

        // zPopMin with a COUNT argument
        $this->redis->del('key');
        $this->redis->zAdd('key', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e');
        $this->assertEquals(['a' => 0.0, 'b' => 1.0, 'c' => 2.0], $this->redis->zPopMin('key', 3));
    }

    public function testZRandMember() {
        if (version_compare($this->version, '6.2.0') < 0) {
            $this->MarkTestSkipped();
            return;
        }
        $this->redis->del('key');
        $this->redis->zAdd('key', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e');
        $this->assertInArray($this->redis->zRandMember('key'), ['a', 'b', 'c', 'd', 'e']);

        $result = $this->redis->zRandMember('key', ['count' => 3]);
        $this->assertEquals(3, count($result));
        $this->assertEquals(array_intersect($result, ['a', 'b', 'c', 'd', 'e']), $result);

        $result = $this->redis->zRandMember('key', ['count' => 2, 'withscores' => true]);
        $this->assertEquals(2, count($result));
        $this->assertEquals(array_intersect_key($result, ['a' => 0, 'b' => 1, 'c' => 2, 'd' => 3, 'e' => 4]), $result);
    }

    public function testHashes() {
        $this->redis->del('h', 'key');
        $this->assertEquals(0, $this->redis->hLen('h'));
        $this->assertEquals(1, $this->redis->hSet('h', 'a', 'a-value'));
        $this->assertEquals(1, $this->redis->hLen('h'));
        $this->assertEquals(1, $this->redis->hSet('h', 'b', 'b-value'));
        $this->assertEquals(2, $this->redis->hLen('h'));

        $this->assertEquals('a-value', $this->redis->hGet('h', 'a'));  // simple get
        $this->assertEquals('b-value', $this->redis->hGet('h', 'b'));  // simple get

        $this->assertEquals(0, $this->redis->hSet('h', 'a', 'another-value')); // replacement
        $this->assertEquals('another-value', $this->redis->hGet('h', 'a'));    // get the new value

        $this->assertEquals('b-value', $this->redis->hGet('h', 'b'));  // simple get
        $this->assertFalse($this->redis->hGet('h', 'c'));  // unknown hash member
        $this->assertFalse($this->redis->hGet('key', 'c'));    // unknownkey

        // hDel
        $this->assertEquals(1, $this->redis->hDel('h', 'a')); // 1 on success
        $this->assertEquals(0, $this->redis->hDel('h', 'a')); // 0 on failure

        $this->redis->del('h');
        $this->redis->hSet('h', 'x', 'a');
        $this->redis->hSet('h', 'y', 'b');
        $this->assertEquals(2, $this->redis->hDel('h', 'x', 'y')); // variadic

        // hsetnx
        $this->redis->del('h');
        $this->assertTrue($this->redis->hSetNx('h', 'x', 'a'));
        $this->assertTrue($this->redis->hSetNx('h', 'y', 'b'));
        $this->assertFalse($this->redis->hSetNx('h', 'x', '?'));
        $this->assertFalse($this->redis->hSetNx('h', 'y', '?'));
        $this->assertEquals('a', $this->redis->hGet('h', 'x'));
        $this->assertEquals('b', $this->redis->hGet('h', 'y'));

        // keys
        $keys = $this->redis->hKeys('h');
        $this->assertEqualsCanonicalizing(['x', 'y'], $keys);

        // values
        $values = $this->redis->hVals('h');
        $this->assertEqualsCanonicalizing(['a', 'b'], $values);

        // keys + values
        $all = $this->redis->hGetAll('h');
        $this->assertEqualsCanonicalizing(['x' => 'a', 'y' => 'b'], $all, true);

        // hExists
        $this->assertTrue($this->redis->hExists('h', 'x'));
        $this->assertTrue($this->redis->hExists('h', 'y'));
        $this->assertFalse($this->redis->hExists('h', 'w'));
        $this->redis->del('h');
        $this->assertFalse($this->redis->hExists('h', 'x'));

        // hIncrBy
        $this->redis->del('h');
        $this->assertEquals(2, $this->redis->hIncrBy('h', 'x', 2));
        $this->assertEquals(3, $this->redis->hIncrBy('h', 'x', 1));
        $this->assertEquals(2, $this->redis->hIncrBy('h', 'x', -1));
        $this->assertEquals('2', $this->redis->hGet('h', 'x'));
        $this->assertEquals(PHP_INT_MAX, $this->redis->hIncrBy('h', 'x', PHP_INT_MAX-2));
        $this->assertEquals(''.PHP_INT_MAX, $this->redis->hGet('h', 'x'));

        $this->redis->hSet('h', 'y', 'not-a-number');
        $this->assertFalse($this->redis->hIncrBy('h', 'y', 1));

        if (version_compare($this->version, '2.5.0') >= 0) {
            // hIncrByFloat
            $this->redis->del('h');
            $this->assertEquals(1.5, $this->redis->hIncrByFloat('h', 'x', 1.5));
            $this->assertEquals(3.0, $this->redis->hincrByFloat('h', 'x', 1.5));
            $this->assertEquals(1.5, $this->redis->hincrByFloat('h', 'x', -1.5));
            $this->assertEquals(1000000000001.5, $this->redis->hincrByFloat('h', 'x', 1000000000000));

            $this->redis->hset('h', 'y', 'not-a-number');
            $this->assertFalse($this->redis->hIncrByFloat('h', 'y', 1.5));
        }

        // hmset
        $this->redis->del('h');
        $this->assertTrue($this->redis->hMset('h', ['x' => 123, 'y' => 456, 'z' => 'abc']));
        $this->assertEquals('123', $this->redis->hGet('h', 'x'));
        $this->assertEquals('456', $this->redis->hGet('h', 'y'));
        $this->assertEquals('abc', $this->redis->hGet('h', 'z'));
        $this->assertFalse($this->redis->hGet('h', 't'));

        // hmget
        $this->assertEquals(['x' => '123', 'y' => '456'], $this->redis->hMget('h', ['x', 'y']));
        $this->assertEquals(['z' => 'abc'], $this->redis->hMget('h', ['z']));
        $this->assertEquals(['x' => '123', 't' => FALSE, 'y' => '456'], $this->redis->hMget('h', ['x', 't', 'y']));
        $this->assertEquals(['x' => '123', 't' => FALSE, 'y' => '456'], $this->redis->hMget('h', ['x', 't', 'y']));
        $this->assertNotEquals([123 => 'x'], $this->redis->hMget('h', [123]));
        $this->assertEquals([123 => FALSE], $this->redis->hMget('h', [123]));

        // Test with an array populated with things we can't use as keys
        $this->assertFalse(@$this->redis->hmget('h', [false,NULL,false]));

        // Test with some invalid keys mixed in (which should just be ignored)
        $this->assertEquals(
            ['x' => '123', 'y' => '456', 'z' => 'abc'],
            $this->redis->hMget('h', ['x', null, 'y', '', 'z', false])
        );

        // hmget/hmset with numeric fields
        $this->redis->del('h');
        $this->assertTrue($this->redis->hMset('h', [123 => 'x', 'y' => 456]));
        $this->assertEquals('x', $this->redis->hGet('h', 123));
        $this->assertEquals('x', $this->redis->hGet('h', '123'));
        $this->assertEquals('456', $this->redis->hGet('h', 'y'));
        $this->assertEquals([123 => 'x', 'y' => '456'], $this->redis->hMget('h', ['123', 'y']));

        // references
        $keys = [123, 'y'];
        foreach ($keys as &$key) {}
        $this->assertEquals([123 => 'x', 'y' => '456'], $this->redis->hMget('h', $keys));

        // check non-string types.
        $this->redis->del('h1');
        $this->assertTrue($this->redis->hMSet('h1', ['x' => 0, 'y' => [], 'z' => new stdclass(), 't' => NULL]));
        $h1 = $this->redis->hGetAll('h1');
        $this->assertEquals('0', $h1['x']);
        $this->assertEquals('Array', $h1['y']);
        $this->assertEquals('Object', $h1['z']);
        $this->assertEquals('', $h1['t']);

        // hset with fields + values as an associative array
        if (version_compare($this->version, '4.0.0') >= 0) {
            $this->redis->del('h');
            $this->assertEquals(3, $this->redis->hSet('h', ['x' => 123, 'y' => 456, 'z' => 'abc']));
            $this->assertEquals(['x' => '123', 'y' => '456', 'z' => 'abc'], $this->redis->hGetAll('h'));
            $this->assertEquals(0, $this->redis->hSet('h', ['x' => 789]));
            $this->assertEquals(['x' => '789', 'y' => '456', 'z' => 'abc'], $this->redis->hGetAll('h'));
        }

        // hset with variadic fields + values
        if (version_compare($this->version, '4.0.0') >= 0) {
            $this->redis->del('h');
            $this->assertEquals(3, $this->redis->hSet('h', 'x', 123, 'y', 456, 'z', 'abc'));
            $this->assertEquals(['x' => '123', 'y' => '456', 'z' => 'abc'], $this->redis->hGetAll('h'));
            $this->assertEquals(0, $this->redis->hSet('h', 'x', 789));
            $this->assertEquals(['x' => '789', 'y' => '456', 'z' => 'abc'], $this->redis->hGetAll('h'));
        }

        // hstrlen
        if (version_compare($this->version, '3.2.0') >= 0) {
            $this->redis->del('h');
            $this->assertEquals(0, $this->redis->hStrLen('h', 'x')); // key doesn't exist
            $this->redis->hSet('h', 'foo', 'bar');
            $this->assertEquals(0, $this->redis->hStrLen('h', 'x')); // field is not present in the hash
            $this->assertEquals(3, $this->redis->hStrLen('h', 'foo'));
	}
    }

    /* Regression test for GitHub issue 2731 */
    public function testNumericPrefixHashFields() {
        $hash = [
            '86deaeb05e3f7760b67e92897a1325e0fdd8618d' => 'one',
            '-86deaeb05e3f7760b67e92897a1325e0fdd8618d' => 'two',
            12345 => 'a_real_number',
            -12345 => 'a_negative_real_number',
        ];

        $this->assertIsInt($this->redis->del('hash'));
        $this->assertTrue($this->redis->hmset('hash', $hash));

        $res = $this->redis->hmget('hash', array_keys($hash));

        // The keys from our local variable and res should be equal
        $this->assertEqualsCanonicalizing(array_keys($hash), array_keys($res));
    }

    public function testHRandField() {
        if (version_compare($this->version, '6.2.0') < 0)
            $this->MarkTestSkipped();

        $this->redis->del('key');
        $this->redis->hMSet('key', ['a' => 0, 'b' => 1, 'c' => 'foo', 'd' => 'bar', 'e' => null]);
        $this->assertInArray($this->redis->hRandField('key'), ['a', 'b', 'c', 'd', 'e']);

        $result = $this->redis->hRandField('key', ['count' => 3]);
        $this->assertEquals(3, count($result));
        $this->assertEquals(array_intersect($result, ['a', 'b', 'c', 'd', 'e']), $result);

        $result = $this->redis->hRandField('key', ['count' => 2, 'withvalues' => true]);
        $this->assertEquals(2, count($result));
        $this->assertEquals(array_intersect_key($result, ['a' => 0, 'b' => 1, 'c' => 'foo', 'd' => 'bar', 'e' => null]), $result);

        /* Make sure PhpRedis sends COUNt (1) when `WITHVALUES` is set */
        $result = $this->redis->hRandField('key', ['withvalues' => true]);
        $this->assertNull($this->redis->getLastError());
        $this->assertIsArray($result);
        $this->assertEquals(1, count($result));

        /* We can return false if the key doesn't exist */
        $this->assertIsInt($this->redis->del('notahash'));
        $this->assertFalse($this->redis->hRandField('notahash'));
    }

    public function testSetRange() {

        $this->redis->del('key');
        $this->redis->set('key', 'hello world');
        $this->redis->setRange('key', 6, 'redis');
        $this->assertKeyEquals('hello redis', 'key');
        $this->redis->setRange('key', 6, 'you'); // don't cut off the end
        $this->assertKeyEquals('hello youis', 'key');

        $this->redis->set('key', 'hello world');

        // fill with zeros if needed
        $this->redis->del('key');
        $this->redis->setRange('key', 6, 'foo');
        $this->assertKeyEquals("\x00\x00\x00\x00\x00\x00foo", 'key');
    }

    public function testObject() {
        /* Version 3.0.0 (represented as >= 2.9.0 in redis info)  and moving
         * forward uses 'embstr' instead of 'raw' for small string values */
        if (version_compare($this->version, '2.9.0') < 0) {
            $small_encoding = 'raw';
        } else {
            $small_encoding = 'embstr';
        }

        $this->redis->del('key');
        $this->assertFalse($this->redis->object('encoding', 'key'));
        $this->assertFalse($this->redis->object('refcount', 'key'));
        $this->assertFalse($this->redis->object('idletime', 'key'));

        $this->redis->set('key', 'value');
        $this->assertEquals($small_encoding, $this->redis->object('encoding', 'key'));
        $this->assertEquals(1, $this->redis->object('refcount', 'key'));
        $this->assertTrue(is_numeric($this->redis->object('idletime', 'key')));

        $this->redis->del('key');
        $this->redis->lpush('key', 'value');

        /* Redis has improved the encoding here throughout the various versions.  The value
           can either be 'ziplist', 'quicklist', or 'listpack' */
        $encoding = $this->redis->object('encoding', 'key');
        $this->assertInArray($encoding, ['ziplist', 'quicklist', 'listpack']);

        $this->assertEquals(1, $this->redis->object('refcount', 'key'));
        $this->assertTrue(is_numeric($this->redis->object('idletime', 'key')));

        $this->redis->del('key');
        $this->redis->sadd('key', 'value');

        /* Redis 7.2.0 switched to 'listpack' for small sets */
        $encoding = $this->redis->object('encoding', 'key');
        $this->assertInArray($encoding, ['hashtable', 'listpack']);
        $this->assertEquals(1, $this->redis->object('refcount', 'key'));
        $this->assertTrue(is_numeric($this->redis->object('idletime', 'key')));

        $this->redis->del('key');
        $this->redis->sadd('key', 42);
        $this->redis->sadd('key', 1729);
        $this->assertEquals('intset', $this->redis->object('encoding', 'key'));
        $this->assertEquals(1, $this->redis->object('refcount', 'key'));
        $this->assertTrue(is_numeric($this->redis->object('idletime', 'key')));

        $this->redis->del('key');
        $this->redis->lpush('key', str_repeat('A', pow(10, 6))); // 1M elements, too big for a ziplist.

        $encoding = $this->redis->object('encoding', 'key');
        $this->assertInArray($encoding, ['linkedlist', 'quicklist']);

        $this->assertEquals(1, $this->redis->object('refcount', 'key'));
        $this->assertTrue(is_numeric($this->redis->object('idletime', 'key')));
    }

    public function testMultiExec() {
        $this->sequence(Redis::MULTI);
        $this->differentType(Redis::MULTI);

        // with prefix as well
        $this->redis->setOption(Redis::OPT_PREFIX, 'test:');
        $this->sequence(Redis::MULTI);
        $this->differentType(Redis::MULTI);
        $this->redis->setOption(Redis::OPT_PREFIX, '');

        $this->redis->set('x', '42');

        $this->assertTrue($this->redis->watch('x'));
        $ret = $this->redis->multi()->get('x')->exec();

        // successful transaction
        $this->assertEquals(['42'], $ret);
    }

    public function testFailedTransactions() {
        $this->redis->set('x', 42);

        // failed transaction
        $this->redis->watch('x');

        $r = $this->newInstance(); // new instance, modifying `x'.
        $r->incr('x');

        $ret = $this->redis->multi()->get('x')->exec();
        $this->assertFalse($ret); // failed because another client changed our watched key between WATCH and EXEC.

        // watch and unwatch
        $this->redis->watch('x');
        $r->incr('x'); // other instance
        $this->redis->unwatch(); // cancel transaction watch

        $ret = $this->redis->multi()->get('x')->exec();

        // succeeded since we've cancel the WATCH command.
        $this->assertEquals(['44'], $ret);
    }

    public function testPipeline() {
        if ( ! $this->havePipeline())
            $this->markTestSkipped();

        $this->sequence(Redis::PIPELINE);
        $this->differentType(Redis::PIPELINE);

        // with prefix as well
        $this->redis->setOption(Redis::OPT_PREFIX, 'test:');
        $this->sequence(Redis::PIPELINE);
        $this->differentType(Redis::PIPELINE);
        $this->redis->setOption(Redis::OPT_PREFIX, '');
    }

    public function testPipelineMultiExec() {
        if ( ! $this->havePipeline())
            $this->markTestSkipped();

        $ret = $this->redis->pipeline()->multi()->exec()->exec();
        $this->assertIsArray($ret);
        $this->assertEquals(1, count($ret)); // empty transaction

        $ret = $this->redis->pipeline()
            ->ping()
            ->multi()->set('x', 42)->incr('x')->exec()
            ->ping()
            ->multi()->get('x')->del('x')->exec()
            ->ping()
            ->exec();
        $this->assertIsArray($ret);
        $this->assertEquals(5, count($ret)); // should be 5 atomic operations
    }

    public function testMultiEmpty()
    {
        $ret = $this->redis->multi()->exec();
        $this->assertEquals([], $ret);
    }

    public function testPipelineEmpty()
    {
        if (!$this->havePipeline()) {
            $this->markTestSkipped();
        }

        $ret = $this->redis->pipeline()->exec();
        $this->assertEquals([], $ret);
    }

    /* GitHub issue #1211 (ignore redundant calls to pipeline or multi) */
    public function testDoublePipeNoOp() {
        /* Only the first pipeline should be honored */
        for ($i = 0; $i < 6; $i++) {
            $this->redis->pipeline();
        }

        /* Set and get in our pipeline */
        $this->redis->set('pipecount', 'over9000')->get('pipecount');

        $data = $this->redis->exec();
        $this->assertEquals([true,'over9000'], $data);

        /* Only the first MULTI should be honored */
        for ($i = 0; $i < 6; $i++) {
            $this->redis->multi();
        }

        /* Set and get in our MULTI block */
        $this->redis->set('multicount', 'over9000')->get('multicount');

        $data = $this->redis->exec();
        $this->assertEquals([true, 'over9000'], $data);
    }

    public function testDiscard() {
        foreach ([Redis::PIPELINE, Redis::MULTI] as $mode) {
            /* start transaction */
            $this->redis->multi($mode);

            /* Set and get in our transaction */
            $this->redis->set('pipecount', 'over9000')->get('pipecount');

            /* first call closes transaction and clears commands queue */
            $this->assertTrue($this->redis->discard());

            /* next call fails because mode is ATOMIC */
            $this->assertFalse($this->redis->discard());
        }
    }

    protected function sequence($mode) {
        $ret = $this->redis->multi($mode)
            ->set('x', 42)
            ->type('x')
            ->get('x')
            ->exec();

        $this->assertIsArray($ret);
        $i = 0;
        $this->assertTrue($ret[$i++]);
        $this->assertEquals(Redis::REDIS_STRING, $ret[$i++]);
        $this->assertEqualsWeak('42', $ret[$i]);

        $serializer = $this->redis->getOption(Redis::OPT_SERIALIZER);
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); // testing incr, which doesn't work with the serializer
        $ret = $this->redis->multi($mode)
            ->del('{key}1')
            ->set('{key}1', 'value1')
            ->get('{key}1')
            ->getSet('{key}1', 'value2')
            ->get('{key}1')
            ->set('{key}2', 4)
            ->incr('{key}2')
            ->get('{key}2')
            ->decr('{key}2')
            ->get('{key}2')
            ->rename('{key}2', '{key}3')
            ->get('{key}3')
            ->renameNx('{key}3', '{key}1')
            ->rename('{key}3', '{key}2')
            ->incrby('{key}2', 5)
            ->get('{key}2')
            ->decrby('{key}2', 5)
            ->get('{key}2')
            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertTrue(is_long($ret[$i++]));
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEqualsWeak('value1', $ret[$i++]);
        $this->assertEqualsWeak('value1', $ret[$i++]);
        $this->assertEqualsWeak('value2', $ret[$i++]);
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEqualsWeak(5, $ret[$i++]);
        $this->assertEqualsWeak(5, $ret[$i++]);
        $this->assertEqualsWeak(4, $ret[$i++]);
        $this->assertEqualsWeak(4, $ret[$i++]);
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEqualsWeak(4, $ret[$i++]);
        $this->assertEqualsWeak(FALSE, $ret[$i++]);
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEqualsWeak(9, $ret[$i++]);
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEqualsWeak(4, $ret[$i++]);
        $this->assertEquals($i, count($ret));

        $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);

        $ret = $this->redis->multi($mode)
            ->del('{key}1')
            ->del('{key}2')
            ->set('{key}1', 'val1')
            ->setnx('{key}1', 'valX')
            ->setnx('{key}2', 'valX')
            ->exists('{key}1')
            ->exists('{key}3')
            ->exec();

        $this->assertIsArray($ret);
        $this->assertEqualsWeak(true, $ret[0]);
        $this->assertEqualsWeak(true, $ret[1]);
        $this->assertEqualsWeak(true, $ret[2]);
        $this->assertEqualsWeak(false, $ret[3]);
        $this->assertEqualsWeak(true, $ret[4]);
        $this->assertEqualsWeak(true, $ret[5]);
        $this->assertEqualsWeak(false, $ret[6]);

        // ttl, mget, mset, msetnx, expire, expireAt
        $this->redis->del('key');
        $ret = $this->redis->multi($mode)
            ->ttl('key')
            ->mget(['{key}1', '{key}2', '{key}3'])
            ->mset(['{key}3' => 'value3', '{key}4' => 'value4'])
            ->set('key', 'value')
            ->expire('key', 5)
            ->ttl('key')
            ->expireAt('key', '0000')
            ->exec();

        $this->assertIsArray($ret);
        $i = 0;
        $ttl = $ret[$i++];
        $this->assertBetween($ttl, -2, -1);
        $this->assertEquals(['val1', 'valX', false], $ret[$i++]); // mget
        $this->assertTrue($ret[$i++]); // mset
        $this->assertTrue($ret[$i++]); // set
        $this->assertTrue($ret[$i++]); // expire
        $this->assertEquals(5, $ret[$i++]);    // ttl
        $this->assertTrue($ret[$i++]); // expireAt
        $this->assertEquals($i, count($ret));

        $ret = $this->redis->multi($mode)
            ->set('{list}lkey', 'x')
            ->set('{list}lDest', 'y')
            ->del('{list}lkey', '{list}lDest')
            ->rpush('{list}lkey', 'lvalue')
            ->lpush('{list}lkey', 'lvalue')
            ->lpush('{list}lkey', 'lvalue')
            ->lpush('{list}lkey', 'lvalue')
            ->lpush('{list}lkey', 'lvalue')
            ->lpush('{list}lkey', 'lvalue')
            ->rpoplpush('{list}lkey', '{list}lDest')
            ->lrange('{list}lDest', 0, -1)
            ->lpop('{list}lkey')
            ->llen('{list}lkey')
            ->lrem('{list}lkey', 'lvalue', 3)
            ->llen('{list}lkey')
            ->lIndex('{list}lkey', 0)
            ->lrange('{list}lkey', 0, -1)
            ->lSet('{list}lkey', 1, 'newValue')    // check errors on key not exists
            ->lrange('{list}lkey', 0, -1)
            ->llen('{list}lkey')
            ->exec();

        $this->assertIsArray($ret);
        $i = 0;
        $this->assertTrue($ret[$i++]); // SET
        $this->assertTrue($ret[$i++]); // SET
        $this->assertEquals(2, $ret[$i++]); // deleting 2 keys
        $this->assertEquals(1, $ret[$i++]); // rpush, now 1 element
        $this->assertEquals(2, $ret[$i++]); // lpush, now 2 elements
        $this->assertEquals(3, $ret[$i++]); // lpush, now 3 elements
        $this->assertEquals(4, $ret[$i++]); // lpush, now 4 elements
        $this->assertEquals(5, $ret[$i++]); // lpush, now 5 elements
        $this->assertEquals(6, $ret[$i++]); // lpush, now 6 elements
        $this->assertEquals('lvalue', $ret[$i++]); // rpoplpush returns the element: 'lvalue'
        $this->assertEquals(['lvalue'], $ret[$i++]); // lDest contains only that one element.
        $this->assertEquals('lvalue', $ret[$i++]); // removing a second element from lkey, now 4 elements left ↓
        $this->assertEquals(4, $ret[$i++]); // 4 elements left, after 2 pops.
        $this->assertEquals(3, $ret[$i++]); // removing 3 elements, now 1 left.
        $this->assertEquals(1, $ret[$i++]); // 1 element left
        $this->assertEquals('lvalue', $ret[$i++]); // this is the current head.
        $this->assertEquals(['lvalue'], $ret[$i++]); // this is the current list.
        $this->assertFalse($ret[$i++]); // updating a non-existent element fails.
        $this->assertEquals(['lvalue'], $ret[$i++]); // this is the current list.
        $this->assertEquals(1, $ret[$i++]); // 1 element left
        $this->assertEquals($i, count($ret));

        $ret = $this->redis->multi($mode)
            ->del('{list}lkey', '{list}lDest')
            ->rpush('{list}lkey', 'lvalue')
            ->lpush('{list}lkey', 'lvalue')
            ->lpush('{list}lkey', 'lvalue')
            ->rpoplpush('{list}lkey', '{list}lDest')
            ->lrange('{list}lDest', 0, -1)
            ->lpop('{list}lkey')
            ->exec();
        $this->assertIsArray($ret);

        $i = 0;

        $this->assertLTE(2, $ret[$i++]);      // deleting 2 keys
        $this->assertEquals(1, $ret[$i++]); // 1 element in the list
        $this->assertEquals(2, $ret[$i++]); // 2 elements in the list
        $this->assertEquals(3, $ret[$i++]); // 3 elements in the list
        $this->assertEquals('lvalue', $ret[$i++]); // rpoplpush returns the element: 'lvalue'
        $this->assertEquals(['lvalue'], $ret[$i++]); // rpoplpush returns the element: 'lvalue'
        $this->assertEquals('lvalue', $ret[$i++]); // pop returns the front element: 'lvalue'
        $this->assertEquals($i, count($ret));


        $serializer = $this->redis->getOption(Redis::OPT_SERIALIZER);
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); // testing incr, which doesn't work with the serializer
        $ret = $this->redis->multi($mode)
            ->del('{key}1')
            ->set('{key}1', 'value1')
            ->get('{key}1')
            ->getSet('{key}1', 'value2')
            ->get('{key}1')
            ->set('{key}2', 4)
            ->incr('{key}2')
            ->get('{key}2')
            ->decr('{key}2')
            ->get('{key}2')
            ->rename('{key}2', '{key}3')
            ->get('{key}3')
            ->renameNx('{key}3', '{key}1')
            ->rename('{key}3', '{key}2')
            ->incrby('{key}2', 5)
            ->get('{key}2')
            ->decrby('{key}2', 5)
            ->get('{key}2')
            ->set('{key}3', 'value3')
            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertLTE(1, $ret[$i++]);
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEquals('value1', $ret[$i++]);
        $this->assertEquals('value1', $ret[$i++]);
        $this->assertEquals('value2', $ret[$i++]);
        $this->assertEqualsWeak(true, $ret[$i++]);
        $this->assertEqualsWeak(5, $ret[$i++]);
        $this->assertEqualsWeak(5, $ret[$i++]);
        $this->assertEqualsWeak(4, $ret[$i++]);
        $this->assertEqualsWeak(4, $ret[$i++]);
        $this->assertTrue($ret[$i++]);
        $this->assertEqualsWeak(4, $ret[$i++]);
        $this->assertFalse($ret[$i++]);
        $this->assertTrue($ret[$i++]);
        $this->assertEquals(9, $ret[$i++]);          // incrby('{key}2', 5)
        $this->assertEqualsWeak(9, $ret[$i++]);      // get('{key}2')
        $this->assertEquals(4, $ret[$i++]);          // decrby('{key}2', 5)
        $this->assertEqualsWeak(4, $ret[$i++]);      // get('{key}2')
        $this->assertTrue($ret[$i++]);
        $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);

        $ret = $this->redis->multi($mode)
            ->del('{key}1')
            ->del('{key}2')
            ->del('{key}3')
            ->set('{key}1', 'val1')
            ->setnx('{key}1', 'valX')
            ->setnx('{key}2', 'valX')
            ->exists('{key}1')
            ->exists('{key}3')
            ->exec();

        $this->assertIsArray($ret);
        $this->assertEquals(1, $ret[0]); // del('{key}1')
        $this->assertEquals(1, $ret[1]); // del('{key}2')
        $this->assertEquals(1, $ret[2]); // del('{key}3')
        $this->assertTrue($ret[3]);      // set('{key}1', 'val1')
        $this->assertFalse($ret[4]);     // setnx('{key}1', 'valX')
        $this->assertTrue($ret[5]);      // setnx('{key}2', 'valX')
        $this->assertEquals(1, $ret[6]); // exists('{key}1')
        $this->assertEquals(0, $ret[7]); // exists('{key}3')

        // ttl, mget, mset, msetnx, expire, expireAt
        $ret = $this->redis->multi($mode)
            ->ttl('key')
            ->mget(['{key}1', '{key}2', '{key}3'])
            ->mset(['{key}3' => 'value3', '{key}4' => 'value4'])
            ->set('key', 'value')
            ->expire('key', 5)
            ->ttl('key')
            ->expireAt('key', '0000')
            ->exec();
        $i = 0;
        $this->assertIsArray($ret);
        $this->assertTrue(is_long($ret[$i++]));
        $this->assertIsArray($ret[$i++], 3);
//        $i++;
        $this->assertTrue($ret[$i++]); // mset always returns true
        $this->assertTrue($ret[$i++]); // set always returns true
        $this->assertTrue($ret[$i++]); // expire always returns true
        $this->assertEquals(5, $ret[$i++]); // TTL was just set.
        $this->assertTrue($ret[$i++]); // expireAt returns true for an existing key
        $this->assertEquals($i, count($ret));

        // lists
        $ret = $this->redis->multi($mode)
            ->del('{l}key', '{l}Dest')
            ->rpush('{l}key', 'lvalue')
            ->lpush('{l}key', 'lvalue')
            ->lpush('{l}key', 'lvalue')
            ->lpush('{l}key', 'lvalue')
            ->lpush('{l}key', 'lvalue')
            ->lpush('{l}key', 'lvalue')
            ->rpoplpush('{l}key', '{l}Dest')
            ->lrange('{l}Dest', 0, -1)
            ->lpop('{l}key')
            ->llen('{l}key')
            ->lrem('{l}key', 'lvalue', 3)
            ->llen('{l}key')
            ->lIndex('{l}key', 0)
            ->lrange('{l}key', 0, -1)
            ->lSet('{l}key', 1, 'newValue')    // check errors on missing key
            ->lrange('{l}key', 0, -1)
            ->llen('{l}key')
            ->exec();

        $this->assertIsArray($ret);
        $i = 0;
        $this->assertBetween($ret[$i++], 0, 2); // del
        $this->assertEquals(1, $ret[$i++]); // 1 value
        $this->assertEquals(2, $ret[$i++]); // 2 values
        $this->assertEquals(3, $ret[$i++]); // 3 values
        $this->assertEquals(4, $ret[$i++]); // 4 values
        $this->assertEquals(5, $ret[$i++]); // 5 values
        $this->assertEquals(6, $ret[$i++]); // 6 values
        $this->assertEquals('lvalue', $ret[$i++]);
        $this->assertEquals(['lvalue'], $ret[$i++]); // 1 value only in lDest
        $this->assertEquals('lvalue', $ret[$i++]); // now 4 values left
        $this->assertEquals(4, $ret[$i++]);
        $this->assertEquals(3, $ret[$i++]); // removing 3 elements.
        $this->assertEquals(1, $ret[$i++]); // length is now 1
        $this->assertEquals('lvalue', $ret[$i++]); // this is the head
        $this->assertEquals(['lvalue'], $ret[$i++]); // 1 value only in lkey
        $this->assertFalse($ret[$i++]); // can't set list[1] if we only have a single value in it.
        $this->assertEquals(['lvalue'], $ret[$i++]); // the previous error didn't touch anything.
        $this->assertEquals(1, $ret[$i++]); // the previous error didn't change the length
        $this->assertEquals($i, count($ret));


        // sets
        $ret = $this->redis->multi($mode)
            ->del('{s}key1', '{s}key2', '{s}keydest', '{s}keyUnion', '{s}DiffDest')
            ->sadd('{s}key1', 'sValue1')
            ->sadd('{s}key1', 'sValue2')
            ->sadd('{s}key1', 'sValue3')
            ->sadd('{s}key1', 'sValue4')
            ->sadd('{s}key2', 'sValue1')
            ->sadd('{s}key2', 'sValue2')
            ->scard('{s}key1')
            ->srem('{s}key1', 'sValue2')
            ->scard('{s}key1')
            ->sMove('{s}key1', '{s}key2', 'sValue4')
            ->scard('{s}key2')
            ->sismember('{s}key2', 'sValue4')
            ->sMembers('{s}key1')
            ->sMembers('{s}key2')
            ->sInter('{s}key1', '{s}key2')
            ->sInterStore('{s}keydest', '{s}key1', '{s}key2')
            ->sMembers('{s}keydest')
            ->sUnion('{s}key2', '{s}keydest')
            ->sUnionStore('{s}keyUnion', '{s}key2', '{s}keydest')
            ->sMembers('{s}keyUnion')
            ->sDiff('{s}key1', '{s}key2')
            ->sDiffStore('{s}DiffDest', '{s}key1', '{s}key2')
            ->sMembers('{s}DiffDest')
            ->sPop('{s}key2')
            ->scard('{s}key2')
            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertBetween($ret[$i++], 0, 5); // we deleted at most 5 values.
        $this->assertEquals(1, $ret[$i++]);     // skey1 now has 1 element.
        $this->assertEquals(1, $ret[$i++]);     // skey1 now has 2 elements.
        $this->assertEquals(1, $ret[$i++]);     // skey1 now has 3 elements.
        $this->assertEquals(1, $ret[$i++]);     // skey1 now has 4 elements.
        $this->assertEquals(1, $ret[$i++]);     // skey2 now has 1 element.
        $this->assertEquals(1, $ret[$i++]);     // skey2 now has 2 elements.
        $this->assertEquals(4, $ret[$i++]);
        $this->assertEquals(1, $ret[$i++]);     // we did remove that value.
        $this->assertEquals(3, $ret[$i++]);     // now 3 values only.

        $this->assertTrue($ret[$i++]); // the move did succeed.
        $this->assertEquals(3, $ret[$i++]); // sKey2 now has 3 values.
        $this->assertTrue($ret[$i++]); // sKey2 does contain sValue4.
        foreach (['sValue1', 'sValue3'] as $k) { // sKey1 contains sValue1 and sValue3.
            $this->assertInArray($k, $ret[$i]);
        }
        $this->assertEquals(2, count($ret[$i++]));
        foreach (['sValue1', 'sValue2', 'sValue4'] as $k) { // sKey2 contains sValue1, sValue2, and sValue4.
            $this->assertInArray($k, $ret[$i]);
        }
        $this->assertEquals(3, count($ret[$i++]));
        $this->assertEquals(['sValue1'], $ret[$i++]); // intersection
        $this->assertEquals(1, $ret[$i++]); // intersection + store → 1 value in the destination set.
        $this->assertEquals(['sValue1'], $ret[$i++]); // sinterstore destination contents

        foreach (['sValue1', 'sValue2', 'sValue4'] as $k) { // (skeydest U sKey2) contains sValue1, sValue2, and sValue4.
            $this->assertInArray($k, $ret[$i]);
        }
        $this->assertEquals(3, count($ret[$i++])); // union size

        $this->assertEquals(3, $ret[$i++]); // unionstore size
        foreach (['sValue1', 'sValue2', 'sValue4'] as $k) { // (skeyUnion) contains sValue1, sValue2, and sValue4.
            $this->assertInArray($k, $ret[$i]);
        }
        $this->assertEquals(3, count($ret[$i++])); // skeyUnion size

        $this->assertEquals(['sValue3'], $ret[$i++]); // diff skey1, skey2 : only sValue3 is not shared.
        $this->assertEquals(1, $ret[$i++]); // sdiffstore size == 1
        $this->assertEquals(['sValue3'], $ret[$i++]); // contents of sDiffDest

        $this->assertInArray($ret[$i++], ['sValue1', 'sValue2', 'sValue4']); // we removed an element from sKey2
        $this->assertEquals(2, $ret[$i++]); // sKey2 now has 2 elements only.

        $this->assertEquals($i, count($ret));

        // sorted sets
        $ret = $this->redis->multi($mode)
            ->del('{z}key1', '{z}key2', '{z}key5', '{z}Inter', '{z}Union')
            ->zadd('{z}key1', 1, 'zValue1')
            ->zadd('{z}key1', 5, 'zValue5')
            ->zadd('{z}key1', 2, 'zValue2')
            ->zRange('{z}key1', 0, -1)
            ->zRem('{z}key1', 'zValue2')
            ->zRange('{z}key1', 0, -1)
            ->zadd('{z}key1', 11, 'zValue11')
            ->zadd('{z}key1', 12, 'zValue12')
            ->zadd('{z}key1', 13, 'zValue13')
            ->zadd('{z}key1', 14, 'zValue14')
            ->zadd('{z}key1', 15, 'zValue15')
            ->zRemRangeByScore('{z}key1', 11, 13)
            ->zrange('{z}key1', 0, -1)
            ->zRevRange('{z}key1', 0, -1)
            ->zRangeByScore('{z}key1', 1, 6)
            ->zCard('{z}key1')
            ->zScore('{z}key1', 'zValue15')
            ->zadd('{z}key2', 5, 'zValue5')
            ->zadd('{z}key2', 2, 'zValue2')
            ->zInterStore('{z}Inter', ['{z}key1', '{z}key2'])
            ->zRange('{z}key1', 0, -1)
            ->zRange('{z}key2', 0, -1)
            ->zRange('{z}Inter', 0, -1)
            ->zUnionStore('{z}Union', ['{z}key1', '{z}key2'])
            ->zRange('{z}Union', 0, -1)
            ->zadd('{z}key5', 5, 'zValue5')
            ->zIncrBy('{z}key5', 3, 'zValue5') // fix this
            ->zScore('{z}key5', 'zValue5')
            ->zScore('{z}key5', 'unknown')
            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertBetween($ret[$i++], 0, 5); // we deleted at most 5 values.
        $this->assertEquals(1, $ret[$i++]);
        $this->assertEquals(1, $ret[$i++]);
        $this->assertEquals(1, $ret[$i++]);
        $this->assertEquals(['zValue1', 'zValue2', 'zValue5'], $ret[$i++]);
        $this->assertEquals(1, $ret[$i++]);
        $this->assertEquals(['zValue1', 'zValue5'], $ret[$i++]);
        $this->assertEquals(1, $ret[$i++]); // adding zValue11
        $this->assertEquals(1, $ret[$i++]); // adding zValue12
        $this->assertEquals(1, $ret[$i++]); // adding zValue13
        $this->assertEquals(1, $ret[$i++]); // adding zValue14
        $this->assertEquals(1, $ret[$i++]); // adding zValue15
        $this->assertEquals(3, $ret[$i++]); // deleted zValue11, zValue12, zValue13
        $this->assertEquals(['zValue1', 'zValue5', 'zValue14', 'zValue15'], $ret[$i++]);
        $this->assertEquals(['zValue15', 'zValue14', 'zValue5', 'zValue1'], $ret[$i++]);
        $this->assertEquals(['zValue1', 'zValue5'], $ret[$i++]);
        $this->assertEquals(4, $ret[$i++]); // 4 elements
        $this->assertEquals(15.0, $ret[$i++]);
        $this->assertEquals(1, $ret[$i++]); // added value
        $this->assertEquals(1, $ret[$i++]); // added value
        $this->assertEquals(1, $ret[$i++]); // zinter only has 1 value
        $this->assertEquals(['zValue1', 'zValue5', 'zValue14', 'zValue15'], $ret[$i++]); // {z}key1 contents
        $this->assertEquals(['zValue2', 'zValue5'], $ret[$i++]); // {z}key2 contents
        $this->assertEquals(['zValue5'], $ret[$i++]); // {z}inter contents
        $this->assertEquals(5, $ret[$i++]); // {z}Union has 5 values (1, 2, 5, 14, 15)
        $this->assertEquals(['zValue1', 'zValue2', 'zValue5', 'zValue14', 'zValue15'], $ret[$i++]); // {z}Union contents
        $this->assertEquals(1, $ret[$i++]); // added value to {z}key5, with score 5
        $this->assertEquals(8.0, $ret[$i++]); // incremented score by 3 → it is now 8.
        $this->assertEquals(8.0, $ret[$i++]); // current score is 8.
        $this->assertFalse($ret[$i++]); // score for unknown element.

        $this->assertEquals($i, count($ret));

        // hash
        $ret = $this->redis->multi($mode)
            ->del('hkey1')
            ->hset('hkey1', 'key1', 'value1')
            ->hset('hkey1', 'key2', 'value2')
            ->hset('hkey1', 'key3', 'value3')
            ->hmget('hkey1', ['key1', 'key2', 'key3'])
            ->hget('hkey1', 'key1')
            ->hlen('hkey1')
            ->hdel('hkey1', 'key2')
            ->hdel('hkey1', 'key2')
            ->hexists('hkey1', 'key2')
            ->hkeys('hkey1')
            ->hvals('hkey1')
            ->hgetall('hkey1')
            ->hset('hkey1', 'valn', 1)
            ->hset('hkey1', 'val-fail', 'non-string')
            ->hget('hkey1', 'val-fail')
            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertLT(2, $ret[$i++]); // delete
        $this->assertEquals(1, $ret[$i++]); // added 1 element
        $this->assertEquals(1, $ret[$i++]); // added 1 element
        $this->assertEquals(1, $ret[$i++]); // added 1 element
        $this->assertEquals(['key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3'], $ret[$i++]); // hmget, 3 elements
        $this->assertEquals('value1', $ret[$i++]); // hget
        $this->assertEquals(3, $ret[$i++]); // hlen
        $this->assertEquals(1, $ret[$i++]); // hdel succeeded
        $this->assertEquals(0, $ret[$i++]); // hdel failed
        $this->assertFalse($ret[$i++]); // hexists didn't find the deleted key
        $this->assertEqualsCanonicalizing(['key1', 'key3'], $ret[$i++]); // hkeys
        $this->assertEqualsCanonicalizing(['value1', 'value3'], $ret[$i++]); // hvals
        $this->assertEqualsCanonicalizing(['key1' => 'value1', 'key3' => 'value3'], $ret[$i++]); // hgetall
        $this->assertEquals(1, $ret[$i++]); // added 1 element
        $this->assertEquals(1, $ret[$i++]); // added the element, so 1.
        $this->assertEquals('non-string', $ret[$i++]); // hset succeeded
        $this->assertEquals($i, count($ret));

        $ret = $this->redis->multi($mode) // default to MULTI, not PIPELINE.
            ->del('test')
            ->set('test', 'xyz')
            ->get('test')
            ->exec();
        $i = 0;
        $this->assertIsArray($ret);
        $this->assertLTE(1, $ret[$i++]); // delete
        $this->assertTrue($ret[$i++]); // added 1 element
        $this->assertEquals('xyz', $ret[$i++]);
        $this->assertEquals($i, count($ret));

        // GitHub issue 78
        $this->redis->del('test');
        for ($i = 1; $i <= 5; $i++)
            $this->redis->zadd('test', $i, (string)$i);

        $result = $this->redis->multi($mode)
            ->zscore('test', '1')
            ->zscore('test', '6')
            ->zscore('test', '8')
            ->zscore('test', '2')
            ->exec();

        $this->assertEquals([1.0, FALSE, FALSE, 2.0], $result);
    }

    protected function differentType($mode) {
        // string
        $key = '{hash}string';
        $dkey = '{hash}' . __FUNCTION__;

        $ret = $this->redis->multi($mode)
            ->del($key)
            ->set($key, 'value')

            // lists I/F
            ->rPush($key, 'lvalue')
            ->lPush($key, 'lvalue')
            ->lLen($key)
            ->lPop($key)
            ->lrange($key, 0, -1)
            ->lTrim($key, 0, 1)
            ->lIndex($key, 0)
            ->lSet($key, 0, 'newValue')
            ->lrem($key, 'lvalue', 1)
            ->lPop($key)
            ->rPop($key)
            ->rPoplPush($key, $dkey . 'lkey1')

            // sets I/F
            ->sAdd($key, 'sValue1')
            ->srem($key, 'sValue1')
            ->sPop($key)
            ->sMove($key, $dkey . 'skey1', 'sValue1')

            ->scard($key)
            ->sismember($key, 'sValue1')
            ->sInter($key, $dkey . 'skey2')

            ->sUnion($key, $dkey . 'skey4')
            ->sDiff($key, $dkey . 'skey7')
            ->sMembers($key)
            ->sRandMember($key)

            // sorted sets I/F
            ->zAdd($key, 1, 'zValue1')
            ->zRem($key, 'zValue1')
            ->zIncrBy($key, 1, 'zValue1')
            ->zRank($key, 'zValue1')
            ->zRevRank($key, 'zValue1')
            ->zRange($key, 0, -1)
            ->zRevRange($key, 0, -1)
            ->zRangeByScore($key, 1, 2)
            ->zCount($key, 0, -1)
            ->zCard($key)
            ->zScore($key, 'zValue1')
            ->zRemRangeByRank($key, 1, 2)
            ->zRemRangeByScore($key, 1, 2)

            // hash I/F
            ->hSet($key, 'key1', 'value1')
            ->hGet($key, 'key1')
            ->hMGet($key, ['key1'])
            ->hMSet($key, ['key1' => 'value1'])
            ->hIncrBy($key, 'key2', 1)
            ->hExists($key, 'key2')
            ->hDel($key, 'key2')
            ->hLen($key)
            ->hKeys($key)
            ->hVals($key)
            ->hGetAll($key)

            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertTrue(is_long($ret[$i++])); // delete
        $this->assertTrue($ret[$i++]); // set

        $this->assertFalse($ret[$i++]); // rpush
        $this->assertFalse($ret[$i++]); // lpush
        $this->assertFalse($ret[$i++]); // llen
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // lrange
        $this->assertFalse($ret[$i++]); // ltrim
        $this->assertFalse($ret[$i++]); // lindex
        $this->assertFalse($ret[$i++]); // lset
        $this->assertFalse($ret[$i++]); // lrem
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // rpop
        $this->assertFalse($ret[$i++]); // rpoplush

        $this->assertFalse($ret[$i++]); // sadd
        $this->assertFalse($ret[$i++]); // srem
        $this->assertFalse($ret[$i++]); // spop
        $this->assertFalse($ret[$i++]); // smove
        $this->assertFalse($ret[$i++]); // scard
        $this->assertFalse($ret[$i++]); // sismember
        $this->assertFalse($ret[$i++]); // sinter
        $this->assertFalse($ret[$i++]); // sunion
        $this->assertFalse($ret[$i++]); // sdiff
        $this->assertFalse($ret[$i++]); // smembers
        $this->assertFalse($ret[$i++]); // srandmember

        $this->assertFalse($ret[$i++]); // zadd
        $this->assertFalse($ret[$i++]); // zrem
        $this->assertFalse($ret[$i++]); // zincrby
        $this->assertFalse($ret[$i++]); // zrank
        $this->assertFalse($ret[$i++]); // zrevrank
        $this->assertFalse($ret[$i++]); // zrange
        $this->assertFalse($ret[$i++]); // zreverserange
        $this->assertFalse($ret[$i++]); // zrangebyscore
        $this->assertFalse($ret[$i++]); // zcount
        $this->assertFalse($ret[$i++]); // zcard
        $this->assertFalse($ret[$i++]); // zscore
        $this->assertFalse($ret[$i++]); // zremrangebyrank
        $this->assertFalse($ret[$i++]); // zremrangebyscore

        $this->assertFalse($ret[$i++]); // hset
        $this->assertFalse($ret[$i++]); // hget
        $this->assertFalse($ret[$i++]); // hmget
        $this->assertFalse($ret[$i++]); // hmset
        $this->assertFalse($ret[$i++]); // hincrby
        $this->assertFalse($ret[$i++]); // hexists
        $this->assertFalse($ret[$i++]); // hdel
        $this->assertFalse($ret[$i++]); // hlen
        $this->assertFalse($ret[$i++]); // hkeys
        $this->assertFalse($ret[$i++]); // hvals
        $this->assertFalse($ret[$i++]); // hgetall

        $this->assertEquals($i, count($ret));

        // list
        $key = '{hash}list';
        $dkey = '{hash}' . __FUNCTION__;
        $ret = $this->redis->multi($mode)
            ->del($key)
            ->lpush($key, 'lvalue')

            // string I/F
            ->get($key)
            ->getset($key, 'value2')
            ->append($key, 'append')
            ->getRange($key, 0, 8)
            ->mget([$key])
            ->incr($key)
            ->incrBy($key, 1)
            ->decr($key)
            ->decrBy($key, 1)

            // sets I/F
            ->sAdd($key, 'sValue1')
            ->srem($key, 'sValue1')
            ->sPop($key)
            ->sMove($key, $dkey . 'skey1', 'sValue1')
            ->scard($key)
            ->sismember($key, 'sValue1')
            ->sInter($key, $dkey . 'skey2')
            ->sUnion($key, $dkey . 'skey4')
            ->sDiff($key, $dkey . 'skey7')
            ->sMembers($key)
            ->sRandMember($key)

            // sorted sets I/F
            ->zAdd($key, 1, 'zValue1')
            ->zRem($key, 'zValue1')
            ->zIncrBy($key, 1, 'zValue1')
            ->zRank($key, 'zValue1')
            ->zRevRank($key, 'zValue1')
            ->zRange($key, 0, -1)
            ->zRevRange($key, 0, -1)
            ->zRangeByScore($key, 1, 2)
            ->zCount($key, 0, -1)
            ->zCard($key)
            ->zScore($key, 'zValue1')
            ->zRemRangeByRank($key, 1, 2)
            ->zRemRangeByScore($key, 1, 2)

            // hash I/F
            ->hSet($key, 'key1', 'value1')
            ->hGet($key, 'key1')
            ->hMGet($key, ['key1'])
            ->hMSet($key, ['key1' => 'value1'])
            ->hIncrBy($key, 'key2', 1)
            ->hExists($key, 'key2')
            ->hDel($key, 'key2')
            ->hLen($key)
            ->hKeys($key)
            ->hVals($key)
            ->hGetAll($key)

            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertTrue(is_long($ret[$i++])); // delete
        $this->assertEquals(1, $ret[$i++]); // lpush

        $this->assertFalse($ret[$i++]); // get
        $this->assertFalse($ret[$i++]); // getset
        $this->assertFalse($ret[$i++]); // append
        $this->assertFalse($ret[$i++]); // getRange
        $this->assertEquals([false], $ret[$i++]); // mget
        $this->assertFalse($ret[$i++]); // incr
        $this->assertFalse($ret[$i++]); // incrBy
        $this->assertFalse($ret[$i++]); // decr
        $this->assertFalse($ret[$i++]); // decrBy

        $this->assertFalse($ret[$i++]); // sadd
        $this->assertFalse($ret[$i++]); // srem
        $this->assertFalse($ret[$i++]); // spop
        $this->assertFalse($ret[$i++]); // smove
        $this->assertFalse($ret[$i++]); // scard
        $this->assertFalse($ret[$i++]); // sismember
        $this->assertFalse($ret[$i++]); // sinter
        $this->assertFalse($ret[$i++]); // sunion
        $this->assertFalse($ret[$i++]); // sdiff
        $this->assertFalse($ret[$i++]); // smembers
        $this->assertFalse($ret[$i++]); // srandmember

        $this->assertFalse($ret[$i++]); // zadd
        $this->assertFalse($ret[$i++]); // zrem
        $this->assertFalse($ret[$i++]); // zincrby
        $this->assertFalse($ret[$i++]); // zrank
        $this->assertFalse($ret[$i++]); // zrevrank
        $this->assertFalse($ret[$i++]); // zrange
        $this->assertFalse($ret[$i++]); // zreverserange
        $this->assertFalse($ret[$i++]); // zrangebyscore
        $this->assertFalse($ret[$i++]); // zcount
        $this->assertFalse($ret[$i++]); // zcard
        $this->assertFalse($ret[$i++]); // zscore
        $this->assertFalse($ret[$i++]); // zremrangebyrank
        $this->assertFalse($ret[$i++]); // zremrangebyscore

        $this->assertFalse($ret[$i++]); // hset
        $this->assertFalse($ret[$i++]); // hget
        $this->assertFalse($ret[$i++]); // hmget
        $this->assertFalse($ret[$i++]); // hmset
        $this->assertFalse($ret[$i++]); // hincrby
        $this->assertFalse($ret[$i++]); // hexists
        $this->assertFalse($ret[$i++]); // hdel
        $this->assertFalse($ret[$i++]); // hlen
        $this->assertFalse($ret[$i++]); // hkeys
        $this->assertFalse($ret[$i++]); // hvals
        $this->assertFalse($ret[$i++]); // hgetall

        $this->assertEquals($i, count($ret));

        // set
        $key = '{hash}set';
        $dkey = '{hash}' . __FUNCTION__;
        $ret = $this->redis->multi($mode)
            ->del($key)
            ->sAdd($key, 'sValue')

            // string I/F
            ->get($key)
            ->getset($key, 'value2')
            ->append($key, 'append')
            ->getRange($key, 0, 8)
            ->mget([$key])
            ->incr($key)
            ->incrBy($key, 1)
            ->decr($key)
            ->decrBy($key, 1)

            // lists I/F
            ->rPush($key, 'lvalue')
            ->lPush($key, 'lvalue')
            ->lLen($key)
            ->lPop($key)
            ->lrange($key, 0, -1)
            ->lTrim($key, 0, 1)
            ->lIndex($key, 0)
            ->lSet($key, 0, 'newValue')
            ->lrem($key, 'lvalue', 1)
            ->lPop($key)
            ->rPop($key)
            ->rPoplPush($key, $dkey . 'lkey1')

            // sorted sets I/F
            ->zAdd($key, 1, 'zValue1')
            ->zRem($key, 'zValue1')
            ->zIncrBy($key, 1, 'zValue1')
            ->zRank($key, 'zValue1')
            ->zRevRank($key, 'zValue1')
            ->zRange($key, 0, -1)
            ->zRevRange($key, 0, -1)
            ->zRangeByScore($key, 1, 2)
            ->zCount($key, 0, -1)
            ->zCard($key)
            ->zScore($key, 'zValue1')
            ->zRemRangeByRank($key, 1, 2)
            ->zRemRangeByScore($key, 1, 2)

            // hash I/F
            ->hSet($key, 'key1', 'value1')
            ->hGet($key, 'key1')
            ->hMGet($key, ['key1'])
            ->hMSet($key, ['key1' => 'value1'])
            ->hIncrBy($key, 'key2', 1)
            ->hExists($key, 'key2')
            ->hDel($key, 'key2')
            ->hLen($key)
            ->hKeys($key)
            ->hVals($key)
            ->hGetAll($key)

            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertTrue(is_long($ret[$i++])); // delete
        $this->assertEquals(1, $ret[$i++]); // zadd

        $this->assertFalse($ret[$i++]); // get
        $this->assertFalse($ret[$i++]); // getset
        $this->assertFalse($ret[$i++]); // append
        $this->assertFalse($ret[$i++]); // getRange
        $this->assertEquals([false], $ret[$i++]); // mget
        $this->assertFalse($ret[$i++]); // incr
        $this->assertFalse($ret[$i++]); // incrBy
        $this->assertFalse($ret[$i++]); // decr
        $this->assertFalse($ret[$i++]); // decrBy

        $this->assertFalse($ret[$i++]); // rpush
        $this->assertFalse($ret[$i++]); // lpush
        $this->assertFalse($ret[$i++]); // llen
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // lrange
        $this->assertFalse($ret[$i++]); // ltrim
        $this->assertFalse($ret[$i++]); // lindex
        $this->assertFalse($ret[$i++]); // lset
        $this->assertFalse($ret[$i++]); // lrem
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // rpop
        $this->assertFalse($ret[$i++]); // rpoplush

        $this->assertFalse($ret[$i++]); // zadd
        $this->assertFalse($ret[$i++]); // zrem
        $this->assertFalse($ret[$i++]); // zincrby
        $this->assertFalse($ret[$i++]); // zrank
        $this->assertFalse($ret[$i++]); // zrevrank
        $this->assertFalse($ret[$i++]); // zrange
        $this->assertFalse($ret[$i++]); // zreverserange
        $this->assertFalse($ret[$i++]); // zrangebyscore
        $this->assertFalse($ret[$i++]); // zcount
        $this->assertFalse($ret[$i++]); // zcard
        $this->assertFalse($ret[$i++]); // zscore
        $this->assertFalse($ret[$i++]); // zremrangebyrank
        $this->assertFalse($ret[$i++]); // zremrangebyscore

        $this->assertFalse($ret[$i++]); // hset
        $this->assertFalse($ret[$i++]); // hget
        $this->assertFalse($ret[$i++]); // hmget
        $this->assertFalse($ret[$i++]); // hmset
        $this->assertFalse($ret[$i++]); // hincrby
        $this->assertFalse($ret[$i++]); // hexists
        $this->assertFalse($ret[$i++]); // hdel
        $this->assertFalse($ret[$i++]); // hlen
        $this->assertFalse($ret[$i++]); // hkeys
        $this->assertFalse($ret[$i++]); // hvals
        $this->assertFalse($ret[$i++]); // hgetall

        $this->assertEquals($i, count($ret));

        // sorted set
        $key = '{hash}sortedset';
        $dkey = '{hash}' . __FUNCTION__;
        $ret = $this->redis->multi($mode)
            ->del($key)
            ->zAdd($key, 0, 'zValue')

            // string I/F
            ->get($key)
            ->getset($key, 'value2')
            ->append($key, 'append')
            ->getRange($key, 0, 8)
            ->mget([$key])
            ->incr($key)
            ->incrBy($key, 1)
            ->decr($key)
            ->decrBy($key, 1)

            // lists I/F
            ->rPush($key, 'lvalue')
            ->lPush($key, 'lvalue')
            ->lLen($key)
            ->lPop($key)
            ->lrange($key, 0, -1)
            ->lTrim($key, 0, 1)
            ->lIndex($key, 0)
            ->lSet($key, 0, 'newValue')
            ->lrem($key, 'lvalue', 1)
            ->lPop($key)
            ->rPop($key)
            ->rPoplPush($key, $dkey . 'lkey1')

            // sets I/F
            ->sAdd($key, 'sValue1')
            ->srem($key, 'sValue1')
            ->sPop($key)
            ->sMove($key, $dkey . 'skey1', 'sValue1')
            ->scard($key)
            ->sismember($key, 'sValue1')
            ->sInter($key, $dkey . 'skey2')
            ->sUnion($key, $dkey . 'skey4')
            ->sDiff($key, $dkey . 'skey7')
            ->sMembers($key)
            ->sRandMember($key)

            // hash I/F
            ->hSet($key, 'key1', 'value1')
            ->hGet($key, 'key1')
            ->hMGet($key, ['key1'])
            ->hMSet($key, ['key1' => 'value1'])
            ->hIncrBy($key, 'key2', 1)
            ->hExists($key, 'key2')
            ->hDel($key, 'key2')
            ->hLen($key)
            ->hKeys($key)
            ->hVals($key)
            ->hGetAll($key)

            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertTrue(is_long($ret[$i++])); // delete
        $this->assertEquals(1, $ret[$i++]); // zadd

        $this->assertFalse($ret[$i++]); // get
        $this->assertFalse($ret[$i++]); // getset
        $this->assertFalse($ret[$i++]); // append
        $this->assertFalse($ret[$i++]); // getRange
        $this->assertEquals([false], $ret[$i++]); // mget
        $this->assertFalse($ret[$i++]); // incr
        $this->assertFalse($ret[$i++]); // incrBy
        $this->assertFalse($ret[$i++]); // decr
        $this->assertFalse($ret[$i++]); // decrBy

        $this->assertFalse($ret[$i++]); // rpush
        $this->assertFalse($ret[$i++]); // lpush
        $this->assertFalse($ret[$i++]); // llen
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // lrange
        $this->assertFalse($ret[$i++]); // ltrim
        $this->assertFalse($ret[$i++]); // lindex
        $this->assertFalse($ret[$i++]); // lset
        $this->assertFalse($ret[$i++]); // lrem
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // rpop
        $this->assertFalse($ret[$i++]); // rpoplush

        $this->assertFalse($ret[$i++]); // sadd
        $this->assertFalse($ret[$i++]); // srem
        $this->assertFalse($ret[$i++]); // spop
        $this->assertFalse($ret[$i++]); // smove
        $this->assertFalse($ret[$i++]); // scard
        $this->assertFalse($ret[$i++]); // sismember
        $this->assertFalse($ret[$i++]); // sinter
        $this->assertFalse($ret[$i++]); // sunion
        $this->assertFalse($ret[$i++]); // sdiff
        $this->assertFalse($ret[$i++]); // smembers
        $this->assertFalse($ret[$i++]); // srandmember

        $this->assertFalse($ret[$i++]); // hset
        $this->assertFalse($ret[$i++]); // hget
        $this->assertFalse($ret[$i++]); // hmget
        $this->assertFalse($ret[$i++]); // hmset
        $this->assertFalse($ret[$i++]); // hincrby
        $this->assertFalse($ret[$i++]); // hexists
        $this->assertFalse($ret[$i++]); // hdel
        $this->assertFalse($ret[$i++]); // hlen
        $this->assertFalse($ret[$i++]); // hkeys
        $this->assertFalse($ret[$i++]); // hvals
        $this->assertFalse($ret[$i++]); // hgetall

        $this->assertEquals($i, count($ret));

        // hash
        $key = '{hash}hash';
        $dkey = '{hash}' . __FUNCTION__;
        $ret = $this->redis->multi($mode)
            ->del($key)
            ->hset($key, 'key1', 'hValue')

            // string I/F
            ->get($key)
            ->getset($key, 'value2')
            ->append($key, 'append')
            ->getRange($key, 0, 8)
            ->mget([$key])
            ->incr($key)
            ->incrBy($key, 1)
            ->decr($key)
            ->decrBy($key, 1)

            // lists I/F
            ->rPush($key, 'lvalue')
            ->lPush($key, 'lvalue')
            ->lLen($key)
            ->lPop($key)
            ->lrange($key, 0, -1)
            ->lTrim($key, 0, 1)
            ->lIndex($key, 0)
            ->lSet($key, 0, 'newValue')
            ->lrem($key, 'lvalue', 1)
            ->lPop($key)
            ->rPop($key)
            ->rPoplPush($key, $dkey . 'lkey1')

            // sets I/F
            ->sAdd($key, 'sValue1')
            ->srem($key, 'sValue1')
            ->sPop($key)
            ->sMove($key, $dkey . 'skey1', 'sValue1')
            ->scard($key)
            ->sismember($key, 'sValue1')
            ->sInter($key, $dkey . 'skey2')
            ->sUnion($key, $dkey . 'skey4')
            ->sDiff($key, $dkey . 'skey7')
            ->sMembers($key)
            ->sRandMember($key)

            // sorted sets I/F
            ->zAdd($key, 1, 'zValue1')
            ->zRem($key, 'zValue1')
            ->zIncrBy($key, 1, 'zValue1')
            ->zRank($key, 'zValue1')
            ->zRevRank($key, 'zValue1')
            ->zRange($key, 0, -1)
            ->zRevRange($key, 0, -1)
            ->zRangeByScore($key, 1, 2)
            ->zCount($key, 0, -1)
            ->zCard($key)
            ->zScore($key, 'zValue1')
            ->zRemRangeByRank($key, 1, 2)
            ->zRemRangeByScore($key, 1, 2)

            ->exec();

        $i = 0;
        $this->assertIsArray($ret);
        $this->assertTrue(is_long($ret[$i++])); // delete
        $this->assertEquals(1, $ret[$i++]); // hset

        $this->assertFalse($ret[$i++]); // get
        $this->assertFalse($ret[$i++]); // getset
        $this->assertFalse($ret[$i++]); // append
        $this->assertFalse($ret[$i++]); // getRange
        $this->assertEquals([false], $ret[$i++]); // mget
        $this->assertFalse($ret[$i++]); // incr
        $this->assertFalse($ret[$i++]); // incrBy
        $this->assertFalse($ret[$i++]); // decr
        $this->assertFalse($ret[$i++]); // decrBy

        $this->assertFalse($ret[$i++]); // rpush
        $this->assertFalse($ret[$i++]); // lpush
        $this->assertFalse($ret[$i++]); // llen
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // lrange
        $this->assertFalse($ret[$i++]); // ltrim
        $this->assertFalse($ret[$i++]); // lindex
        $this->assertFalse($ret[$i++]); // lset
        $this->assertFalse($ret[$i++]); // lrem
        $this->assertFalse($ret[$i++]); // lpop
        $this->assertFalse($ret[$i++]); // rpop
        $this->assertFalse($ret[$i++]); // rpoplush

        $this->assertFalse($ret[$i++]); // sadd
        $this->assertFalse($ret[$i++]); // srem
        $this->assertFalse($ret[$i++]); // spop
        $this->assertFalse($ret[$i++]); // smove
        $this->assertFalse($ret[$i++]); // scard
        $this->assertFalse($ret[$i++]); // sismember
        $this->assertFalse($ret[$i++]); // sinter
        $this->assertFalse($ret[$i++]); // sunion
        $this->assertFalse($ret[$i++]); // sdiff
        $this->assertFalse($ret[$i++]); // smembers
        $this->assertFalse($ret[$i++]); // srandmember

        $this->assertFalse($ret[$i++]); // zadd
        $this->assertFalse($ret[$i++]); // zrem
        $this->assertFalse($ret[$i++]); // zincrby
        $this->assertFalse($ret[$i++]); // zrank
        $this->assertFalse($ret[$i++]); // zrevrank
        $this->assertFalse($ret[$i++]); // zrange
        $this->assertFalse($ret[$i++]); // zreverserange
        $this->assertFalse($ret[$i++]); // zrangebyscore
        $this->assertFalse($ret[$i++]); // zcount
        $this->assertFalse($ret[$i++]); // zcard
        $this->assertFalse($ret[$i++]); // zscore
        $this->assertFalse($ret[$i++]); // zremrangebyrank
        $this->assertFalse($ret[$i++]); // zremrangebyscore

        $this->assertEquals($i, count($ret));
    }

    public function testDifferentTypeString() {
        $key = '{hash}string';
        $dkey = '{hash}' . __FUNCTION__;

        $this->redis->del($key);
        $this->assertTrue($this->redis->set($key, 'value'));

        // lists I/F
        $this->assertFalse($this->redis->rPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lLen($key));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->lrange($key, 0, -1));
        $this->assertFalse($this->redis->lTrim($key, 0, 1));
        $this->assertFalse($this->redis->lIndex($key, 0));
        $this->assertFalse($this->redis->lSet($key, 0, 'newValue'));
        $this->assertFalse($this->redis->lrem($key, 'lvalue', 1));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->rPop($key));
        $this->assertFalse($this->redis->rPoplPush($key, $dkey . 'lkey1'));

        // sets I/F
        $this->assertFalse($this->redis->sAdd($key, 'sValue1'));
        $this->assertFalse($this->redis->srem($key, 'sValue1'));
        $this->assertFalse($this->redis->sPop($key));
        $this->assertFalse($this->redis->sMove($key, $dkey . 'skey1', 'sValue1'));
        $this->assertFalse($this->redis->scard($key));
        $this->assertFalse($this->redis->sismember($key, 'sValue1'));
        $this->assertFalse($this->redis->sInter($key, $dkey. 'skey2'));
        $this->assertFalse($this->redis->sUnion($key, $dkey . 'skey4'));
        $this->assertFalse($this->redis->sDiff($key, $dkey . 'skey7'));
        $this->assertFalse($this->redis->sMembers($key));
        $this->assertFalse($this->redis->sRandMember($key));

        // sorted sets I/F
        $this->assertFalse($this->redis->zAdd($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRem($key, 'zValue1'));
        $this->assertFalse($this->redis->zIncrBy($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRevRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRange($key, 0, -1));
        $this->assertFalse($this->redis->zRevRange($key, 0, -1));
        $this->assertFalse($this->redis->zRangeByScore($key, 1, 2));
        $this->assertFalse($this->redis->zCount($key, 0, -1));
        $this->assertFalse($this->redis->zCard($key));
        $this->assertFalse($this->redis->zScore($key, 'zValue1'));
        $this->assertFalse($this->redis->zRemRangeByRank($key, 1, 2));
        $this->assertFalse($this->redis->zRemRangeByScore($key, 1, 2));

        // hash I/F
        $this->assertFalse($this->redis->hSet($key, 'key1', 'value1'));
        $this->assertFalse($this->redis->hGet($key, 'key1'));
        $this->assertFalse($this->redis->hMGet($key, ['key1']));
        $this->assertFalse($this->redis->hMSet($key, ['key1' => 'value1']));
        $this->assertFalse($this->redis->hIncrBy($key, 'key2', 1));
        $this->assertFalse($this->redis->hExists($key, 'key2'));
        $this->assertFalse($this->redis->hDel($key, 'key2'));
        $this->assertFalse($this->redis->hLen($key));
        $this->assertFalse($this->redis->hKeys($key));
        $this->assertFalse($this->redis->hVals($key));
        $this->assertFalse($this->redis->hGetAll($key));
    }

    public function testDifferentTypeList() {
        $key = '{hash}list';
        $dkey = '{hash}' . __FUNCTION__;

        $this->redis->del($key);
        $this->assertEquals(1, $this->redis->lPush($key, 'value'));

        // string I/F
        $this->assertFalse($this->redis->get($key));
        $this->assertFalse($this->redis->getset($key, 'value2'));
        $this->assertFalse($this->redis->append($key, 'append'));
        $this->assertFalse($this->redis->getRange($key, 0, 8));
        $this->assertEquals([FALSE], $this->redis->mget([$key]));
        $this->assertFalse($this->redis->incr($key));
        $this->assertFalse($this->redis->incrBy($key, 1));
        $this->assertFalse($this->redis->decr($key));
        $this->assertFalse($this->redis->decrBy($key, 1));

        // sets I/F
        $this->assertFalse($this->redis->sAdd($key, 'sValue1'));
        $this->assertFalse($this->redis->srem($key, 'sValue1'));
        $this->assertFalse($this->redis->sPop($key));
        $this->assertFalse($this->redis->sMove($key, $dkey . 'skey1', 'sValue1'));
        $this->assertFalse($this->redis->scard($key));
        $this->assertFalse($this->redis->sismember($key, 'sValue1'));
        $this->assertFalse($this->redis->sInter($key, $dkey . 'skey2'));
        $this->assertFalse($this->redis->sUnion($key, $dkey . 'skey4'));
        $this->assertFalse($this->redis->sDiff($key, $dkey . 'skey7'));
        $this->assertFalse($this->redis->sMembers($key));
        $this->assertFalse($this->redis->sRandMember($key));

        // sorted sets I/F
        $this->assertFalse($this->redis->zAdd($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRem($key, 'zValue1'));
        $this->assertFalse($this->redis->zIncrBy($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRevRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRange($key, 0, -1));
        $this->assertFalse($this->redis->zRevRange($key, 0, -1));
        $this->assertFalse($this->redis->zRangeByScore($key, 1, 2));
        $this->assertFalse($this->redis->zCount($key, 0, -1));
        $this->assertFalse($this->redis->zCard($key));
        $this->assertFalse($this->redis->zScore($key, 'zValue1'));
        $this->assertFalse($this->redis->zRemRangeByRank($key, 1, 2));
        $this->assertFalse($this->redis->zRemRangeByScore($key, 1, 2));

        // hash I/F
        $this->assertFalse($this->redis->hSet($key, 'key1', 'value1'));
        $this->assertFalse($this->redis->hGet($key, 'key1'));
        $this->assertFalse($this->redis->hMGet($key, ['key1']));
        $this->assertFalse($this->redis->hMSet($key, ['key1' => 'value1']));
        $this->assertFalse($this->redis->hIncrBy($key, 'key2', 1));
        $this->assertFalse($this->redis->hExists($key, 'key2'));
        $this->assertFalse($this->redis->hDel($key, 'key2'));
        $this->assertFalse($this->redis->hLen($key));
        $this->assertFalse($this->redis->hKeys($key));
        $this->assertFalse($this->redis->hVals($key));
        $this->assertFalse($this->redis->hGetAll($key));
    }

    public function testDifferentTypeSet() {
        $key = '{hash}set';
        $dkey = '{hash}' . __FUNCTION__;
        $this->redis->del($key);
        $this->assertEquals(1, $this->redis->sAdd($key, 'value'));

        // string I/F
        $this->assertFalse($this->redis->get($key));
        $this->assertFalse($this->redis->getset($key, 'value2'));
        $this->assertFalse($this->redis->append($key, 'append'));
        $this->assertFalse($this->redis->getRange($key, 0, 8));
        $this->assertEquals([FALSE], $this->redis->mget([$key]));
        $this->assertFalse($this->redis->incr($key));
        $this->assertFalse($this->redis->incrBy($key, 1));
        $this->assertFalse($this->redis->decr($key));
        $this->assertFalse($this->redis->decrBy($key, 1));

        // lists I/F
        $this->assertFalse($this->redis->rPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lLen($key));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->lrange($key, 0, -1));
        $this->assertFalse($this->redis->lTrim($key, 0, 1));
        $this->assertFalse($this->redis->lIndex($key, 0));
        $this->assertFalse($this->redis->lSet($key, 0, 'newValue'));
        $this->assertFalse($this->redis->lrem($key, 'lvalue', 1));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->rPop($key));
        $this->assertFalse($this->redis->rPoplPush($key, $dkey  . 'lkey1'));

        // sorted sets I/F
        $this->assertFalse($this->redis->zAdd($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRem($key, 'zValue1'));
        $this->assertFalse($this->redis->zIncrBy($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRevRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRange($key, 0, -1));
        $this->assertFalse($this->redis->zRevRange($key, 0, -1));
        $this->assertFalse($this->redis->zRangeByScore($key, 1, 2));
        $this->assertFalse($this->redis->zCount($key, 0, -1));
        $this->assertFalse($this->redis->zCard($key));
        $this->assertFalse($this->redis->zScore($key, 'zValue1'));
        $this->assertFalse($this->redis->zRemRangeByRank($key, 1, 2));
        $this->assertFalse($this->redis->zRemRangeByScore($key, 1, 2));

        // hash I/F
        $this->assertFalse($this->redis->hSet($key, 'key1', 'value1'));
        $this->assertFalse($this->redis->hGet($key, 'key1'));
        $this->assertFalse($this->redis->hMGet($key, ['key1']));
        $this->assertFalse($this->redis->hMSet($key, ['key1' => 'value1']));
        $this->assertFalse($this->redis->hIncrBy($key, 'key2', 1));
        $this->assertFalse($this->redis->hExists($key, 'key2'));
        $this->assertFalse($this->redis->hDel($key, 'key2'));
        $this->assertFalse($this->redis->hLen($key));
        $this->assertFalse($this->redis->hKeys($key));
        $this->assertFalse($this->redis->hVals($key));
        $this->assertFalse($this->redis->hGetAll($key));
    }

    public function testDifferentTypeSortedSet() {
        $key = '{hash}sortedset';
        $dkey = '{hash}' . __FUNCTION__;

        $this->redis->del($key);
        $this->assertEquals(1, $this->redis->zAdd($key, 0, 'value'));

        // string I/F
        $this->assertFalse($this->redis->get($key));
        $this->assertFalse($this->redis->getset($key, 'value2'));
        $this->assertFalse($this->redis->append($key, 'append'));
        $this->assertFalse($this->redis->getRange($key, 0, 8));
        $this->assertEquals([FALSE], $this->redis->mget([$key]));
        $this->assertFalse($this->redis->incr($key));
        $this->assertFalse($this->redis->incrBy($key, 1));
        $this->assertFalse($this->redis->decr($key));
        $this->assertFalse($this->redis->decrBy($key, 1));

        // lists I/F
        $this->assertFalse($this->redis->rPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lLen($key));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->lrange($key, 0, -1));
        $this->assertFalse($this->redis->lTrim($key, 0, 1));
        $this->assertFalse($this->redis->lIndex($key, 0));
        $this->assertFalse($this->redis->lSet($key, 0, 'newValue'));
        $this->assertFalse($this->redis->lrem($key, 'lvalue', 1));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->rPop($key));
        $this->assertFalse($this->redis->rPoplPush($key, $dkey . 'lkey1'));

        // sets I/F
        $this->assertFalse($this->redis->sAdd($key, 'sValue1'));
        $this->assertFalse($this->redis->srem($key, 'sValue1'));
        $this->assertFalse($this->redis->sPop($key));
        $this->assertFalse($this->redis->sMove($key, $dkey . 'skey1', 'sValue1'));
        $this->assertFalse($this->redis->scard($key));
        $this->assertFalse($this->redis->sismember($key, 'sValue1'));
        $this->assertFalse($this->redis->sInter($key, $dkey . 'skey2'));
        $this->assertFalse($this->redis->sUnion($key, $dkey . 'skey4'));
        $this->assertFalse($this->redis->sDiff($key, $dkey . 'skey7'));
        $this->assertFalse($this->redis->sMembers($key));
        $this->assertFalse($this->redis->sRandMember($key));

        // hash I/F
        $this->assertFalse($this->redis->hSet($key, 'key1', 'value1'));
        $this->assertFalse($this->redis->hGet($key, 'key1'));
        $this->assertFalse($this->redis->hMGet($key, ['key1']));
        $this->assertFalse($this->redis->hMSet($key, ['key1' => 'value1']));
        $this->assertFalse($this->redis->hIncrBy($key, 'key2', 1));
        $this->assertFalse($this->redis->hExists($key, 'key2'));
        $this->assertFalse($this->redis->hDel($key, 'key2'));
        $this->assertFalse($this->redis->hLen($key));
        $this->assertFalse($this->redis->hKeys($key));
        $this->assertFalse($this->redis->hVals($key));
        $this->assertFalse($this->redis->hGetAll($key));
    }

    public function testDifferentTypeHash() {
        $key = '{hash}hash';
        $dkey = '{hash}hash';

        $this->redis->del($key);
        $this->assertEquals(1, $this->redis->hSet($key, 'key', 'value'));

        // string I/F
        $this->assertFalse($this->redis->get($key));
        $this->assertFalse($this->redis->getset($key, 'value2'));
        $this->assertFalse($this->redis->append($key, 'append'));
        $this->assertFalse($this->redis->getRange($key, 0, 8));
        $this->assertEquals([FALSE], $this->redis->mget([$key]));
        $this->assertFalse($this->redis->incr($key));
        $this->assertFalse($this->redis->incrBy($key, 1));
        $this->assertFalse($this->redis->decr($key));
        $this->assertFalse($this->redis->decrBy($key, 1));

        // lists I/F
        $this->assertFalse($this->redis->rPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lPush($key, 'lvalue'));
        $this->assertFalse($this->redis->lLen($key));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->lrange($key, 0, -1));
        $this->assertFalse($this->redis->lTrim($key, 0, 1));
        $this->assertFalse($this->redis->lIndex($key, 0));
        $this->assertFalse($this->redis->lSet($key, 0, 'newValue'));
        $this->assertFalse($this->redis->lrem($key, 'lvalue', 1));
        $this->assertFalse($this->redis->lPop($key));
        $this->assertFalse($this->redis->rPop($key));
        $this->assertFalse($this->redis->rPoplPush($key, $dkey . 'lkey1'));

        // sets I/F
        $this->assertFalse($this->redis->sAdd($key, 'sValue1'));
        $this->assertFalse($this->redis->srem($key, 'sValue1'));
        $this->assertFalse($this->redis->sPop($key));
        $this->assertFalse($this->redis->sMove($key, $dkey . 'skey1', 'sValue1'));
        $this->assertFalse($this->redis->scard($key));
        $this->assertFalse($this->redis->sismember($key, 'sValue1'));
        $this->assertFalse($this->redis->sInter($key, $dkey . 'skey2'));
        $this->assertFalse($this->redis->sUnion($key, $dkey . 'skey4'));
        $this->assertFalse($this->redis->sDiff($key, $dkey . 'skey7'));
        $this->assertFalse($this->redis->sMembers($key));
        $this->assertFalse($this->redis->sRandMember($key));

        // sorted sets I/F
        $this->assertFalse($this->redis->zAdd($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRem($key, 'zValue1'));
        $this->assertFalse($this->redis->zIncrBy($key, 1, 'zValue1'));
        $this->assertFalse($this->redis->zRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRevRank($key, 'zValue1'));
        $this->assertFalse($this->redis->zRange($key, 0, -1));
        $this->assertFalse($this->redis->zRevRange($key, 0, -1));
        $this->assertFalse($this->redis->zRangeByScore($key, 1, 2));
        $this->assertFalse($this->redis->zCount($key, 0, -1));
        $this->assertFalse($this->redis->zCard($key));
        $this->assertFalse($this->redis->zScore($key, 'zValue1'));
        $this->assertFalse($this->redis->zRemRangeByRank($key, 1, 2));
        $this->assertFalse($this->redis->zRemRangeByScore($key, 1, 2));
    }

    public function testSerializerPHP() {
        $this->checkSerializer(Redis::SERIALIZER_PHP);

        // with prefix
        $this->redis->setOption(Redis::OPT_PREFIX, 'test:');
        $this->checkSerializer(Redis::SERIALIZER_PHP);
        $this->redis->setOption(Redis::OPT_PREFIX, '');
    }

    private function cartesianProduct(array $arrays) {
        $result = [[]];

        foreach ($arrays as $array) {
            $append = [];
            foreach ($result as $product) {
                foreach ($array as $item) {
                    $newProduct = $product;
                    $newProduct[] = $item;
                    $append[] = $newProduct;
                }
            }

            $result = $append;
        }

        return $result;
    }

    public function testIgnoreNumbers() {
        $combinations = $this->cartesianProduct([
            [false, true, false],
            $this->getSerializers(),
            $this->getCompressors(),
        ]);

        foreach ($combinations as [$ignore, $serializer, $compression]) {
            $this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, $ignore);
            $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
            $this->redis->setOption(Redis::OPT_COMPRESSION, $compression);

            $this->assertIsInt($this->redis->del('answer'));
            $this->assertIsInt($this->redis->del('hash'));

            $transparent = $compression === Redis::COMPRESSION_NONE &&
                           ($serializer === Redis::SERIALIZER_NONE ||
                            $serializer === Redis::SERIALIZER_JSON);

            if ($transparent || $ignore) {
                $expected_answer = 42;
                $expected_pi = 3.14;
            } else {
                $expected_answer = false;
                $expected_pi = false;
            }

            $this->assertTrue($this->redis->set('answer', 32));
            $this->assertEquals($expected_answer, $this->redis->incr('answer', 10));

            $this->assertTrue($this->redis->set('pi', 3.04));
            $this->assertEquals($expected_pi, $this->redis->incrByFloat('pi', 0.1));

            $this->assertEquals(1, $this->redis->hset('hash', 'answer', 32));
            $this->assertEquals($expected_answer, $this->redis->hIncrBy('hash', 'answer', 10));

            $this->assertEquals(1, $this->redis->hset('hash', 'pi', 3.04));
            $this->assertEquals($expected_pi, $this->redis->hIncrByFloat('hash', 'pi', 0.1));
        }

        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
        $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);
        $this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, false);
    }

    function testIgnoreNumbersReturnTypes() {
        $combinations = $this->cartesianProduct([
            [false, true],
            array_filter($this->getSerializers(), function($s) {
                return $s !== Redis::SERIALIZER_NONE;
            }),
            array_filter($this->getCompressors(), function($c) {
                return $c !== Redis::COMPRESSION_NONE;
            }),
        ]);

        foreach ($combinations as [$ignore, $serializer, $compression]) {
            $this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, $ignore);
            $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
            $this->redis->setOption(Redis::OPT_COMPRESSION, $compression);

            foreach ([42, 3.14] as $value) {
                $this->assertTrue($this->redis->set('key', $value));

                /* There's a known issue in the PHP JSON parser, which
                   can stringify numbers. Unclear the root cause */
                if ($serializer == Redis::SERIALIZER_JSON) {
                    $this->assertEqualsWeak($value, $this->redis->get('key'));
                } else {
                    $this->assertEquals($value, $this->redis->get('key'));
                }
            }
        }

        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
        $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);
        $this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, false);
    }

    public function testSerializerIGBinary() {
        if ( ! defined('Redis::SERIALIZER_IGBINARY'))
            $this->markTestSkipped('Redis::SERIALIZER_IGBINARY is not defined');

        $this->checkSerializer(Redis::SERIALIZER_IGBINARY);

        // with prefix
        $this->redis->setOption(Redis::OPT_PREFIX, 'test:');
        $this->checkSerializer(Redis::SERIALIZER_IGBINARY);
        $this->redis->setOption(Redis::OPT_PREFIX, '');

        /* Test our igbinary header check logic.  The check allows us to do
           simple INCR type operations even with the serializer enabled, and
           should also protect against igbinary-like data from being erroneously
           deserialized */
        $this->redis->del('incrkey');

        $this->redis->set('spoof-1', "\x00\x00\x00\x00");
        $this->redis->set('spoof-2', "\x00\x00\x00\x00bad-version1");
        $this->redis->set('spoof-3', "\x00\x00\x00\x05bad-version2");
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY);

        $this->assertEquals(16, $this->redis->incrby('incrkey', 16));
        $this->assertKeyEquals('16', 'incrkey');

        $this->assertKeyEquals("\x00\x00\x00\x00", 'spoof-1');
        $this->assertKeyEquals("\x00\x00\x00\x00bad-version1", 'spoof-2');
        $this->assertKeyEquals("\x00\x00\x00\x05bad-version2", 'spoof-3');
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);

        $this->redis->del('incrkey', 'spoof-1', 'spoof-2', 'spoof-3');
    }

    public function testSerializerMsgPack() {
        if ( ! defined('Redis::SERIALIZER_MSGPACK'))
            $this->markTestSkipped('Redis::SERIALIZER_MSGPACK is not defined');

        $this->checkSerializer(Redis::SERIALIZER_MSGPACK);

        // with prefix
        $this->redis->setOption(Redis::OPT_PREFIX, 'test:');
        $this->checkSerializer(Redis::SERIALIZER_MSGPACK);
        $this->redis->setOption(Redis::OPT_PREFIX, '');
    }

    public function testSerializerJSON() {
        $this->checkSerializer(Redis::SERIALIZER_JSON);

        // with prefix
        $this->redis->setOption(Redis::OPT_PREFIX, 'test:');
        $this->checkSerializer(Redis::SERIALIZER_JSON);
        $this->redis->setOption(Redis::OPT_PREFIX, '');
    }

    private function checkSerializer($mode) {
        $this->redis->del('key');
        $this->assertEquals(Redis::SERIALIZER_NONE, $this->redis->getOption(Redis::OPT_SERIALIZER));   // default

        $this->assertTrue($this->redis->setOption(Redis::OPT_SERIALIZER, $mode));  // set ok
        $this->assertEquals($mode, $this->redis->getOption(Redis::OPT_SERIALIZER));    // get ok

        // lPush, rPush
        $a = ['hello world', 42, true, ['<tag>' => 1729]];
        $this->redis->del('key');
        $this->redis->lPush('key', $a[0]);
        $this->redis->rPush('key', $a[1]);
        $this->redis->rPush('key', $a[2]);
        $this->redis->rPush('key', $a[3]);

        // lrange
        $this->assertEquals($a, $this->redis->lrange('key', 0, -1));

        // lIndex
        $this->assertEquals($a[0], $this->redis->lIndex('key', 0));
        $this->assertEquals($a[1], $this->redis->lIndex('key', 1));
        $this->assertEquals($a[2], $this->redis->lIndex('key', 2));
        $this->assertEquals($a[3], $this->redis->lIndex('key', 3));

        // lrem
        $this->assertEquals(1, $this->redis->lrem('key', $a[3]));
        $this->assertEquals(array_slice($a, 0, 3), $this->redis->lrange('key', 0, -1));

        // lSet
        $a[0] = ['k' => 'v']; // update
        $this->assertTrue($this->redis->lSet('key', 0, $a[0]));
        $this->assertEquals($a[0], $this->redis->lIndex('key', 0));

        // lInsert
        $this->assertEquals(4, $this->redis->lInsert('key', Redis::BEFORE, $a[0], [1, 2, 3]));
        $this->assertEquals(5, $this->redis->lInsert('key', Redis::AFTER, $a[0], [4, 5, 6]));

        $a = [[1, 2, 3], $a[0], [4, 5, 6], $a[1], $a[2]];
        $this->assertEquals($a, $this->redis->lrange('key', 0, -1));

        // sAdd
        $this->redis->del('{set}key');
        $s = [1,'a', [1, 2, 3], ['k' => 'v']];

        $this->assertEquals(1, $this->redis->sAdd('{set}key', $s[0]));
        $this->assertEquals(1, $this->redis->sAdd('{set}key', $s[1]));
        $this->assertEquals(1, $this->redis->sAdd('{set}key', $s[2]));
        $this->assertEquals(1, $this->redis->sAdd('{set}key', $s[3]));

        // variadic sAdd
        $this->redis->del('k');
        $this->assertEquals(3, $this->redis->sAdd('k', 'a', 'b', 'c'));
        $this->assertEquals(1, $this->redis->sAdd('k', 'a', 'b', 'c', 'd'));

        // srem
        $this->assertEquals(1, $this->redis->srem('{set}key', $s[3]));
        $this->assertEquals(0, $this->redis->srem('{set}key', $s[3]));

        // variadic
        $this->redis->del('k');
        $this->redis->sAdd('k', 'a', 'b', 'c', 'd');
        $this->assertEquals(2, $this->redis->sRem('k', 'a', 'd'));
        $this->assertEquals(2, $this->redis->sRem('k', 'b', 'c', 'e'));
        $this->assertKeyMissing('k');

        // sismember
        $this->assertTrue($this->redis->sismember('{set}key', $s[0]));
        $this->assertTrue($this->redis->sismember('{set}key', $s[1]));
        $this->assertTrue($this->redis->sismember('{set}key', $s[2]));
        $this->assertFalse($this->redis->sismember('{set}key', $s[3]));
        unset($s[3]);

        // sMove
        $this->redis->del('{set}tmp');
        $this->redis->sMove('{set}key', '{set}tmp', $s[0]);
        $this->assertFalse($this->redis->sismember('{set}key', $s[0]));
        $this->assertTrue($this->redis->sismember('{set}tmp', $s[0]));
        unset($s[0]);

        // sorted sets
        $z = ['z0', ['k' => 'v'], FALSE, NULL];
        $this->redis->del('key');

        // zAdd
        $this->assertEquals(1, $this->redis->zAdd('key', 0, $z[0]));
        $this->assertEquals(1, $this->redis->zAdd('key', 1, $z[1]));
        $this->assertEquals(1, $this->redis->zAdd('key', 2, $z[2]));
        $this->assertEquals(1, $this->redis->zAdd('key', 3, $z[3]));

        // zRem
        $this->assertEquals(1, $this->redis->zRem('key', $z[3]));
        $this->assertEquals(0, $this->redis->zRem('key', $z[3]));
        unset($z[3]);

        // variadic
        $this->redis->del('k');
        $this->redis->zAdd('k', 0, 'a');
        $this->redis->zAdd('k', 1, 'b');
        $this->redis->zAdd('k', 2, 'c');
        $this->assertEquals(2, $this->redis->zRem('k', 'a', 'c'));
        $this->assertEquals(1.0, $this->redis->zScore('k', 'b'));
        $this->assertEquals(['b' => 1.0], $this->redis->zRange('k', 0, -1, true));

        // zRange
        $this->assertEquals($z, $this->redis->zRange('key', 0, -1));

        // zScore
        $this->assertEquals(0.0, $this->redis->zScore('key', $z[0]));
        $this->assertEquals(1.0, $this->redis->zScore('key', $z[1]));
        $this->assertEquals(2.0, $this->redis->zScore('key', $z[2]));

        // zRank
        $this->assertEquals(0, $this->redis->zRank('key', $z[0]));
        $this->assertEquals(1, $this->redis->zRank('key', $z[1]));
        $this->assertEquals(2, $this->redis->zRank('key', $z[2]));

        // zRevRank
        $this->assertEquals(2, $this->redis->zRevRank('key', $z[0]));
        $this->assertEquals(1, $this->redis->zRevRank('key', $z[1]));
        $this->assertEquals(0, $this->redis->zRevRank('key', $z[2]));

        // zIncrBy
        $this->assertEquals(3.0, $this->redis->zIncrBy('key', 1.0, $z[2]));
        $this->assertEquals(3.0, $this->redis->zScore('key', $z[2]));

        $this->assertEquals(5.0, $this->redis->zIncrBy('key', 2.0, $z[2]));
        $this->assertEquals(5.0, $this->redis->zScore('key', $z[2]));

        $this->assertEquals(2.0, $this->redis->zIncrBy('key', -3.0, $z[2]));
        $this->assertEquals(2.0, $this->redis->zScore('key', $z[2]));

        // mset
        $a = ['k0' => 1, 'k1' => 42, 'k2' => NULL, 'k3' => FALSE, 'k4' => ['a' => 'b']];
        $this->assertTrue($this->redis->mset($a));
        foreach ($a as $k => $v) {
            $this->assertKeyEquals($v, $k);
        }

        $a = ['f0' => 1, 'f1' => 42, 'f2' => NULL, 'f3' => FALSE, 'f4' => ['a' => 'b']];

        // hSet
        $this->redis->del('hash');
        foreach ($a as $k => $v) {
            $this->assertEquals(1, $this->redis->hSet('hash', $k, $v));
        }

        // hGet
        foreach ($a as $k => $v) {
            $this->assertEquals($v, $this->redis->hGet('hash', $k));
        }

        // hGetAll
        $this->assertEquals($a, $this->redis->hGetAll('hash'));
        $this->assertTrue($this->redis->hExists('hash', 'f0'));
        $this->assertTrue($this->redis->hExists('hash', 'f1'));
        $this->assertTrue($this->redis->hExists('hash', 'f2'));
        $this->assertTrue($this->redis->hExists('hash', 'f3'));
        $this->assertTrue($this->redis->hExists('hash', 'f4'));

        // hMSet
        $this->redis->del('hash');
        $this->redis->hMSet('hash', $a);
        foreach ($a as $k => $v) {
            $this->assertEquals($v, $this->redis->hGet('hash', $k));
        }

        // hMget
        $hmget = $this->redis->hMget('hash', array_keys($a));
        foreach ($hmget as $k => $v) {
            $this->assertEquals($a[$k], $v);
        }

        // mGet
        $this->redis->set('a', NULL);
        $this->redis->set('b', FALSE);
        $this->redis->set('c', 42);
        $this->redis->set('d', ['x' => 'y']);

        $this->assertEquals([NULL, FALSE, 42, ['x' => 'y']], $this->redis->mGet(['a', 'b', 'c', 'd']));

        // pipeline
        if ($this->havePipeline()) {
            $this->sequence(Redis::PIPELINE);
        }

        // multi-exec
        if ($this->haveMulti()) {
            $this->sequence(Redis::MULTI);
        }

        $this->assertIsArray($this->redis->keys('*'));

        // issue #62, hgetall
        $this->redis->del('hash1');
        $this->redis->hSet('hash1', 'data', 'test 1');
        $this->redis->hSet('hash1', 'session_id', 'test 2');

        $data = $this->redis->hGetAll('hash1');
        $this->assertEquals('test 1', $data['data']);
        $this->assertEquals('test 2', $data['session_id']);

        // issue #145, serializer with objects.
        $this->redis->set('x', [new stdClass, new stdClass]);
        $x = $this->redis->get('x');
        $this->assertIsArray($x);
        if ($mode === Redis::SERIALIZER_JSON) {
            $this->assertIsArray($x[0]);
            $this->assertIsArray($x[1]);
        } else {
            $this->assertIsObject($x[0], 'stdClass');
            $this->assertIsObject($x[1], 'stdClass');
        }

        // revert
        $this->assertTrue($this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE));     // set ok
        $this->assertEquals(Redis::SERIALIZER_NONE, $this->redis->getOption(Redis::OPT_SERIALIZER));       // get ok
    }

    public function testCompressionLZF() {
        if ( ! defined('Redis::COMPRESSION_LZF'))
            $this->markTestSkipped();

        /* Don't crash on improperly compressed LZF data */
        $payload = 'not-actually-lzf-compressed';
        $this->redis->set('badlzf', $payload);
        $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZF);
        $this->assertKeyEquals($payload, 'badlzf');
        $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);

        $this->checkCompression(Redis::COMPRESSION_LZF, 0);
    }

    public function testCompressionZSTD() {
        if ( ! defined('Redis::COMPRESSION_ZSTD'))
            $this->markTestSkipped();

        /* Issue 1936 regression.  Make sure we don't overflow on bad data */
        $this->redis->del('badzstd');
        $this->redis->set('badzstd', '123');
        $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_ZSTD);
        $this->assertKeyEquals('123', 'badzstd');
        $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);

        $this->checkCompression(Redis::COMPRESSION_ZSTD, 0);
        $this->checkCompression(Redis::COMPRESSION_ZSTD, 9);
    }


    public function testCompressionLZ4() {
        if ( ! defined('Redis::COMPRESSION_LZ4'))
            $this->markTestSkipped();

        $this->checkCompression(Redis::COMPRESSION_LZ4, 0);
        $this->checkCompression(Redis::COMPRESSION_LZ4, 9);
    }

    private function checkCompression($mode, $level) {
        $set_cmp = $this->redis->setOption(Redis::OPT_COMPRESSION, $mode);
        $this->assertTrue($set_cmp);
        if ($set_cmp !== true)
            return;

        $get_cmp = $this->redis->getOption(Redis::OPT_COMPRESSION);
        $this->assertEquals($get_cmp, $mode);
        if ($get_cmp !== $mode)
            return;

        $set_lvl = $this->redis->setOption(Redis::OPT_COMPRESSION_LEVEL, $level);
        $this->assertTrue($set_lvl);
        if ($set_lvl !== true)
            return;

        $get_lvl = $this->redis->getOption(Redis::OPT_COMPRESSION_LEVEL);
        $this->assertEquals($get_lvl, $level);
        if ($get_lvl !== $level)
            return;

        $val = 'xxxxxxxxxx';
        $this->redis->set('key', $val);
        $this->assertKeyEquals($val, 'key');

        /* Empty data */
        $this->redis->set('key', '');
        $this->assertKeyEquals('', 'key');

        /* Iterate through class sizes */
        for ($i = 1; $i <= 65536; $i *= 2) {
            foreach ([str_repeat('A', $i), random_bytes($i)] as $val) {
                $this->redis->set('key', $val);
                $this->assertKeyEquals($val, 'key');
            }
        }

        // Issue 1945. Ensure we decompress data with hmget.
        $this->redis->hset('hkey', 'data', 'abc');
        $this->assertEquals('abc', current($this->redis->hmget('hkey', ['data'])));
    }

    public function testDumpRestore() {

        if (version_compare($this->version, '2.5.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('foo');
        $this->redis->del('bar');

        $this->redis->set('foo', 'this-is-foo');
        $this->redis->set('bar', 'this-is-bar');

        $d_foo = $this->redis->dump('foo');
        $d_bar = $this->redis->dump('bar');

        $this->redis->del('foo');
        $this->redis->del('bar');

        // Assert returns from restore
        $this->assertTrue($this->redis->restore('foo', 0, $d_bar));
        $this->assertTrue($this->redis->restore('bar', 0, $d_foo));

        // Now check that the keys have switched
        $this->assertKeyEquals('this-is-bar', 'foo');
        $this->assertKeyEquals('this-is-foo', 'bar');

        /* Test that we can REPLACE a key */
        $this->assertTrue($this->redis->set('foo', 'some-value'));
        $this->assertTrue($this->redis->restore('foo', 0, $d_bar, ['REPLACE']));

        /* Ensure we can set an absolute TTL */
        $this->assertTrue($this->redis->restore('foo', time() + 10, $d_bar, ['REPLACE', 'ABSTTL']));
        $this->assertLTE(10, $this->redis->ttl('foo'));

        /* Ensure we can set an IDLETIME */
        $this->assertTrue($this->redis->restore('foo', 0, $d_bar, ['REPLACE', 'IDLETIME' => 200]));
        $this->assertGT(100, $this->redis->object('idletime', 'foo'));

        /* We can't neccissarily check this depending on LRU policy, but at least attempt to use
           the FREQ option */
        $this->assertTrue($this->redis->restore('foo', 0, $d_bar, ['REPLACE', 'FREQ' => 200]));

        $this->redis->del('foo');
        $this->redis->del('bar');
    }

    public function testGetLastError() {
        // We shouldn't have any errors now
        $this->assertNull($this->redis->getLastError());

        // test getLastError with a regular command
        $this->redis->set('x', 'a');
        $this->assertFalse($this->redis->incr('x'));
        $incrError = $this->redis->getLastError();
        $this->assertGT(0, strlen($incrError));

        // clear error
        $this->redis->clearLastError();
        $this->assertNull($this->redis->getLastError());
    }

    // Helper function to compare nested results -- from the php.net array_diff page, I believe
    private function array_diff_recursive($aArray1, $aArray2) {
        $aReturn = [];

        foreach ($aArray1 as $mKey => $mValue) {
            if (array_key_exists($mKey, $aArray2)) {
                if (is_array($mValue)) {
                    $aRecursiveDiff = $this->array_diff_recursive($mValue, $aArray2[$mKey]);
                    if (count($aRecursiveDiff)) {
                        $aReturn[$mKey] = $aRecursiveDiff;
                    }
                } else {
                    if ($mValue != $aArray2[$mKey]) {
                        $aReturn[$mKey] = $mValue;
                    }
                }
            } else {
                $aReturn[$mKey] = $mValue;
            }
        }

        return $aReturn;
    }

    public function testScript() {
        if (version_compare($this->version, '2.5.0') < 0)
            $this->markTestSkipped();

        // Flush any scripts we have
        $this->assertTrue($this->redis->script('flush'));

        // Silly scripts to test against
        $s1_src = 'return 1';
        $s1_sha = sha1($s1_src);
        $s2_src = 'return 2';
        $s2_sha = sha1($s2_src);
        $s3_src = 'return 3';
        $s3_sha = sha1($s3_src);

        // None should exist
        $result = $this->redis->script('exists', $s1_sha, $s2_sha, $s3_sha);
        $this->assertIsArray($result, 3);
        $this->assertTrue(is_array($result) && count(array_filter($result)) == 0);

        // Load them up
        $this->assertEquals($s1_sha, $this->redis->script('load', $s1_src));
        $this->assertEquals($s2_sha, $this->redis->script('load', $s2_src));
        $this->assertEquals($s3_sha, $this->redis->script('load', $s3_src));

        // They should all exist
        $result = $this->redis->script('exists', $s1_sha, $s2_sha, $s3_sha);
        $this->assertTrue(is_array($result) && count(array_filter($result)) == 3);
    }

    public function testEval() {
        if (version_compare($this->version, '2.5.0') < 0)
            $this->markTestSkipped();

        /* The eval_ro method uses the same underlying handlers as eval so we
           only need to verify we can call it. */
        if ($this->minVersionCheck('7.0.0'))
            $this->assertEquals('1.55', $this->redis->eval_ro("return '1.55'"));

        // Basic single line response tests
        $this->assertEquals(1, $this->redis->eval('return 1'));
        $this->assertEqualsWeak(1.55, $this->redis->eval("return '1.55'"));
        $this->assertEquals('hello, world', $this->redis->eval("return 'hello, world'"));

        /*
         * Keys to be incorporated into lua results
         */
        // Make a list
        $this->redis->del('{eval-key}-list');
        $this->redis->rpush('{eval-key}-list', 'a');
        $this->redis->rpush('{eval-key}-list', 'b');
        $this->redis->rpush('{eval-key}-list', 'c');

        // Make a set
        $this->redis->del('{eval-key}-zset');
        $this->redis->zadd('{eval-key}-zset', 0, 'd');
        $this->redis->zadd('{eval-key}-zset', 1, 'e');
        $this->redis->zadd('{eval-key}-zset', 2, 'f');

        // Basic keys
        $this->redis->set('{eval-key}-str1', 'hello, world');
        $this->redis->set('{eval-key}-str2', 'hello again!');

        // Use a script to return our list, and verify its response
        $list = $this->redis->eval("return redis.call('lrange', KEYS[1], 0, -1)", ['{eval-key}-list'], 1);
        $this->assertEquals(['a', 'b', 'c'], $list);

        // Use a script to return our zset
        $zset = $this->redis->eval("return redis.call('zrange', KEYS[1], 0, -1)", ['{eval-key}-zset'], 1);
        $this->assertEquals(['d', 'e', 'f'], $zset);

        // Test an empty MULTI BULK response
        $this->redis->del('{eval-key}-nolist');
        $empty_resp = $this->redis->eval("return redis.call('lrange', '{eval-key}-nolist', 0, -1)",
            ['{eval-key}-nolist'], 1);
        $this->assertEquals([], $empty_resp);

        // Now test a nested reply
        $nested_script = "
            return {
                1,2,3, {
                    redis.call('get', '{eval-key}-str1'),
                    redis.call('get', '{eval-key}-str2'),
                    redis.call('lrange', 'not-any-kind-of-list', 0, -1),
                    {
                        redis.call('zrange', '{eval-key}-zset', 0, -1),
                        redis.call('lrange', '{eval-key}-list', 0, -1)
                    }
                }
            }
        ";

        $expected = [
            1, 2, 3, [
                'hello, world',
                'hello again!',
                [],
                [
                    ['d', 'e', 'f'],
                    ['a', 'b', 'c']
                ]
            ]
        ];

        // Now run our script, and check our values against each other
        $eval_result = $this->redis->eval($nested_script, ['{eval-key}-str1', '{eval-key}-str2', '{eval-key}-zset', '{eval-key}-list'], 4);
        $this->assertTrue(
            is_array($eval_result) &&
            count($this->array_diff_recursive($eval_result, $expected)) == 0
        );

        /*
         * Nested reply wihin a multi/pipeline block
         */

        $num_scripts = 10;

        $modes = [Redis::MULTI];
        if ($this->havePipeline()) $modes[] = Redis::PIPELINE;

        foreach ($modes as $mode) {
            $this->redis->multi($mode);
            for ($i = 0; $i < $num_scripts; $i++) {
                $this->redis->eval($nested_script, ['{eval-key}-dummy'], 1);
            }
            $replies = $this->redis->exec();

            foreach ($replies as $reply) {
                $this->assertTrue(
                    is_array($reply) &&
                    count($this->array_diff_recursive($reply, $expected)) == 0
                );
            }
        }

        /*
         * KEYS/ARGV
         */

        $args_script = 'return {KEYS[1],KEYS[2],KEYS[3],ARGV[1],ARGV[2],ARGV[3]}';
        $args_args   = ['{k}1', '{k}2', '{k}3', 'v1', 'v2', 'v3'];
        $args_result = $this->redis->eval($args_script, $args_args, 3);
        $this->assertEquals($args_args, $args_result);

        // turn on key prefixing
        $this->redis->setOption(Redis::OPT_PREFIX, 'prefix:');
        $args_result = $this->redis->eval($args_script, $args_args, 3);

        // Make sure our first three are prefixed
        for ($i = 0; $i < count($args_result); $i++) {
            if ($i < 3) {
                $this->assertEquals('prefix:' . $args_args[$i], $args_result[$i]);
            } else {
                $this->assertEquals($args_args[$i], $args_result[$i]);
            }
        }
    }

    public function testEvalSHA() {
        if (version_compare($this->version, '2.5.0') < 0)
            $this->markTestSkipped();

        // Flush any loaded scripts
        $this->redis->script('flush');

        // Non existent script (but proper sha1), and a random (not) sha1 string
        $this->assertFalse($this->redis->evalsha(sha1(uniqid())));
        $this->assertFalse($this->redis->evalsha('some-random-data'));

        // Load a script
        $cb  = uniqid(); // To ensure the script is new
        $scr = "local cb='$cb' return 1";
        $sha = sha1($scr);

        // Run it when it doesn't exist, run it with eval, and then run it with sha1
        $this->assertFalse($this->redis->evalsha($scr));
        $this->assertEquals(1, $this->redis->eval($scr));
        $this->assertEquals(1, $this->redis->evalsha($sha));

        /* Our evalsha_ro handler is the same as evalsha so just make sure
           we can invoke the command */
        if ($this->minVersionCheck('7.0.0'))
            $this->assertEquals(1, $this->redis->evalsha_ro($sha));
    }

    /* Regression test for #2681 where there was undefined behavior when sending
       one or more arguments but no keys in cluster mode. */
    public function testEvalNoKeys() {
        for ($i = 0; $i < 10; $i++) {
            $this->assertEqualsWeak($i, $this->redis->eval("return $i", [42], 0));
            $this->assertEqualsWeak($i, $this->redis->eval('return ARGV[1]', [$i], 0));
        }
    }

    public function testSerialize() {
        $vals = [1, 1.5, 'one', ['here', 'is', 'an', 'array']];

        // Test with no serialization at all
        $this->assertEquals('test', $this->redis->_serialize('test'));
        $this->assertEquals('1', $this->redis->_serialize(1));
        $this->assertEquals('Array', $this->redis->_serialize([]));
        $this->assertEquals('Object', $this->redis->_serialize(new stdClass));

        foreach ($this->getSerializers() as $mode) {
            $enc = [];
            $dec = [];

            foreach ($vals as $k => $v) {
                $enc = $this->redis->_serialize($v);
                $dec = $this->redis->_unserialize($enc);

                // They should be the same
                $this->assertEquals($enc, $dec);
            }
        }
    }

    public function testUnserialize() {
        $vals = [1, 1.5,'one',['this', 'is', 'an', 'array']];

        /* We want to skip SERIALIZER_NONE because strict type checking will
           fail on the assertions (which is expected). */
        $serializers = array_filter($this->getSerializers(), function($v) {
            return $v != Redis::SERIALIZER_NONE;
        });

        foreach ($serializers as $mode) {
            $vals_enc = [];

            // Pass them through redis so they're serialized
            foreach ($vals as $key => $val) {
                $this->redis->setOption(Redis::OPT_SERIALIZER, $mode);

                $key = 'key' . ++$key;
                $this->redis->del($key);
                $this->redis->set($key, $val);

                // Clear serializer, get serialized value
                $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
                $vals_enc[] = $this->redis->get($key);
            }

            // Run through our array comparing values
            for ($i = 0; $i < count($vals); $i++) {
                // reset serializer
                $this->redis->setOption(Redis::OPT_SERIALIZER, $mode);
                $this->assertEquals($vals[$i], $this->redis->_unserialize($vals_enc[$i]));
                $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
            }
        }
    }

    public function testCompressHelpers() {
        $compressors = $this->getCompressors();

        $vals = ['foo', 12345, random_bytes(128), ''];

        $oldcmp = $this->redis->getOption(Redis::OPT_COMPRESSION);

        foreach ($compressors as $cmp) {
            foreach ($vals as $val) {
                $this->redis->setOption(Redis::OPT_COMPRESSION, $cmp);
                $this->redis->set('cmpkey', $val);

                /* Get the value raw */
                $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);
                $raw = $this->redis->get('cmpkey');
                $this->redis->setOption(Redis::OPT_COMPRESSION, $cmp);

                $this->assertEquals($raw, $this->redis->_compress($val));

                $uncompressed = $this->redis->get('cmpkey');
                $this->assertEquals($uncompressed, $this->redis->_uncompress($raw));
            }
        }

        $this->redis->setOption(Redis::OPT_COMPRESSION, $oldcmp);
    }

    public function testPackHelpers() {
        list ($oldser, $oldcmp) = [
            $this->redis->getOption(Redis::OPT_SERIALIZER),
            $this->redis->getOption(Redis::OPT_COMPRESSION)
        ];

        foreach ($this->getSerializers() as $ser) {
            $compressors = $this->getCompressors();
            foreach ($compressors as $cmp) {
                $this->redis->setOption(Redis::OPT_SERIALIZER, $ser);
                $this->redis->setOption(Redis::OPT_COMPRESSION, $cmp);

		foreach (['foo', 12345, random_bytes(128), '', ['an', 'array']] as $v) {
                    /* Can only attempt the array if we're serializing */
                    if (is_array($v) && $ser == Redis::SERIALIZER_NONE)
                        continue;

                    $this->redis->set('packkey', $v);

                    /* Get the value raw */
                    $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
                    $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);
                    $raw = $this->redis->get('packkey');
                    $this->redis->setOption(Redis::OPT_SERIALIZER, $ser);
                    $this->redis->setOption(Redis::OPT_COMPRESSION, $cmp);

                    $this->assertEquals($raw, $this->redis->_pack($v));

                    $unpacked = $this->redis->get('packkey');
		    $this->assertEquals($unpacked, $this->redis->_unpack($raw));
		}
	    }
        }

        $this->redis->setOption(Redis::OPT_SERIALIZER, $oldser);
        $this->redis->setOption(Redis::OPT_COMPRESSION, $oldcmp);
    }

    public function testGetWithMeta() {
        $this->redis->del('key');
        $this->assertFalse($this->redis->get('key'));

        $result = $this->redis->getWithMeta('key');
        $this->assertIsArray($result, 2);
        $this->assertArrayKeyEquals($result, 0, false);
        $this->assertArrayKey($result, 1, function ($metadata) {
            $this->assertIsArray($metadata);
            $this->assertArrayKeyEquals($metadata, 'length', -1);
            return true;
        });

        if ($this->havePipeline()) {
            $batch = $this->redis->pipeline()
                ->get('key')
                ->getWithMeta('key')
                ->exec();
            $this->assertIsArray($batch, 2);
            $this->assertArrayKeyEquals($batch, 0, false);
            $this->assertArrayKey($batch, 1, function ($result) {
                $this->assertIsArray($result, 2);
                $this->assertArrayKeyEquals($result, 0, false);
                $this->assertArrayKey($result, 1, function ($metadata) {
                    $this->assertIsArray($metadata);
                    $this->assertArrayKeyEquals($metadata, 'length', -1);
                    return true;
                });
                return true;
            });
        }

        $batch = $this->redis->multi()
            ->set('key', 'value')
            ->getWithMeta('key')
            ->exec();
        $this->assertIsArray($batch, 2);
        $this->assertArrayKeyEquals($batch, 0, true);
        $this->assertArrayKey($batch, 1, function ($result) {
            $this->assertIsArray($result, 2);
            $this->assertArrayKeyEquals($result, 0, 'value');
            $this->assertArrayKey($result, 1, function ($metadata) {
                $this->assertIsArray($metadata);
                $this->assertArrayKeyEquals($metadata, 'length', strlen('value'));
                return true;
            });
            return true;
        });

        $serializer = $this->redis->getOption(Redis::OPT_SERIALIZER);
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
        $this->assertTrue($this->redis->set('key', false));

        $result = $this->redis->getWithMeta('key');
        $this->assertIsArray($result, 2);
        $this->assertArrayKeyEquals($result, 0, false);
        $this->assertArrayKey($result, 1, function ($metadata) {
            $this->assertIsArray($metadata);
            $this->assertArrayKeyEquals($metadata, 'length', strlen(serialize(false)));
            return true;
        });

        $this->assertFalse($this->redis->get('key'));
        $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
    }

    public function testHGetWithMeta() {
        $this->redis->del('hash');
        $this->assertFalse($this->redis->hget('hash', 'member'));

        $result = $this->redis->hgetWithMeta('hash', 'member');
        $this->assertIsArray($result, 2);
        $this->assertArrayKeyEquals($result, 0, false);
        $this->assertArrayKey($result, 1, function ($metadata) {
            $this->assertIsArray($metadata);
            $this->assertArrayKeyEquals($metadata, 'length', -1);
            return true;
        });

        if ($this->havePipeline()) {
            $batch = $this->redis->pipeline()
                ->hget('hash', 'member')
                ->hgetWithMeta('hash', 'member')
                ->exec();
            $this->assertIsArray($batch, 2);
            $this->assertArrayKeyEquals($batch, 0, false);
            $this->assertArrayKey($batch, 1, function ($result) {
                $this->assertIsArray($result, 2);
                $this->assertArrayKeyEquals($result, 0, false);
                $this->assertArrayKey($result, 1, function ($metadata) {
                    $this->assertIsArray($metadata);
                    $this->assertArrayKeyEquals($metadata, 'length', -1);
                    return true;
                });
                return true;
            });
        }

        $batch = $this->redis->multi()
            ->hset('hash', 'member', 'value')
            ->hgetWithMeta('hash', 'member')
            ->exec();
        $this->assertIsArray($batch, 2);
        $this->assertArrayKeyEquals($batch, 0, 1);
        $this->assertArrayKey($batch, 1, function ($result) {
            $this->assertIsArray($result, 2);
            $this->assertArrayKeyEquals($result, 0, 'value');
            $this->assertArrayKey($result, 1, function ($metadata) {
                $this->assertIsArray($metadata);
                $this->assertArrayKeyEquals($metadata, 'length', strlen('value'));
                return true;
            });
            return true;
        });

        $serializer = $this->redis->getOption(Redis::OPT_SERIALIZER);
        $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
        $this->assertEquals(0, $this->redis->hset('hash', 'member', false));

        $result = $this->redis->hgetWithMeta('hash', 'member');
        $this->assertIsArray($result, 2);
        $this->assertArrayKeyEquals($result, 0, false);
        $this->assertArrayKey($result, 1, function ($metadata) {
            $this->assertIsArray($metadata);
            $this->assertArrayKeyEquals($metadata, 'length', strlen(serialize(false)));
            return true;
        });

        $this->assertFalse($this->redis->hget('hash', 'member'));
        $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
    }

    public function testPrefix() {
        // no prefix
        $this->redis->setOption(Redis::OPT_PREFIX, '');
        $this->assertEquals('key', $this->redis->_prefix('key'));

        // with a prefix
        $this->redis->setOption(Redis::OPT_PREFIX, 'some-prefix:');
        $this->assertEquals('some-prefix:key', $this->redis->_prefix('key'));

        // Clear prefix
        $this->redis->setOption(Redis::OPT_PREFIX, '');

    }

    public function testReplyLiteral() {
        $this->redis->setOption(Redis::OPT_REPLY_LITERAL, false);
        $this->assertTrue($this->redis->rawCommand('set', 'foo', 'bar'));
        $this->assertTrue($this->redis->eval("return redis.call('set', 'foo', 'bar')", [], 0));

        $rv = $this->redis->eval("return {redis.call('set', KEYS[1], 'bar'), redis.call('ping')}", ['foo'], 1);
        $this->assertEquals([true, true], $rv);

        $this->redis->setOption(Redis::OPT_REPLY_LITERAL, true);
        $this->assertEquals('OK', $this->redis->rawCommand('set', 'foo', 'bar'));
        $this->assertEquals('OK', $this->redis->eval("return redis.call('set', 'foo', 'bar')", [], 0));

        // Nested
        $rv = $this->redis->eval("return {redis.call('set', KEYS[1], 'bar'), redis.call('ping')}", ['foo'], 1);
        $this->assertEquals(['OK', 'PONG'], $rv);

        // Reset
        $this->redis->setOption(Redis::OPT_REPLY_LITERAL, false);
    }

    public function testNullArray() {
        $key = 'key:arr';
        $this->redis->del($key);

        foreach ([false => [], true => NULL] as $opt => $test) {
            $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, $opt);

            $r = $this->redis->rawCommand('BLPOP', $key, .05);
            $this->assertEquals($test, $r);

            $this->redis->multi();
            $this->redis->rawCommand('BLPOP', $key, .05);
            $r = $this->redis->exec();
            $this->assertEquals([$test], $r);
        }

        $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, false);
    }

    /* Test that we can configure PhpRedis to return NULL for *-1 even nestedwithin replies */
    public function testNestedNullArray() {
        $this->redis->del('{notaset}');

        foreach ([false => [], true => NULL] as $opt => $test) {
            $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, $opt);
            $this->assertEquals([$test, $test], $this->redis->geoPos('{notaset}', 'm1', 'm2'));

            $this->redis->multi();
            $this->redis->geoPos('{notaset}', 'm1', 'm2');
            $this->assertEquals([[$test, $test]], $this->redis->exec());
        }

        $this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, false);
    }

    public function testConfig() {
        /* GET */
        $cfg = $this->redis->config('GET', 'timeout');
        $this->assertArrayKey($cfg, 'timeout');
        $sec = $cfg['timeout'];

        /* SET */
        foreach ([$sec + 30, $sec] as $val) {
            $this->assertTrue($this->redis->config('SET', 'timeout', $val));
            $cfg = $this->redis->config('GET', 'timeout');
            $this->assertArrayKey($cfg, 'timeout', function ($v) use ($val) {
                return $v == $val;
            });
        }

        /* RESETSTAT */
        $c1 = count($this->redis->info('commandstats'));
        $this->assertTrue($this->redis->config('resetstat'));
        $this->assertLT($c1, count($this->redis->info('commandstats')));

        /* Ensure invalid calls are handled by PhpRedis */
        foreach (['notacommand', 'get', 'set'] as $cmd) {
            $this->assertFalse(@$this->redis->config($cmd));
        }
        $this->assertFalse(@$this->redis->config('set', 'foo'));

        /* REWRITE.  We don't care if it actually works, just that the
           command be attempted */
        $res = $this->redis->config('rewrite');
        $this->assertIsBool($res);
        if ($res == false) {
            $this->assertPatternMatch('/.*config.*/', $this->redis->getLastError());
            $this->redis->clearLastError();
        }

        if ( ! $this->minVersionCheck('7.0.0'))
            return;

        /* Test getting multiple values */
        $settings = $this->redis->config('get', ['timeout', 'databases', 'set-max-intset-entries']);
        $this->assertTrue(is_array($settings) && isset($settings['timeout']) &&
                          isset($settings['databases']) && isset($settings['set-max-intset-entries']));

        /* Short circuit if the above assertion would have failed */
        if ( ! is_array($settings) || ! isset($settings['timeout']) || ! isset($settings['set-max-intset-entries']))
            return;

        list($timeout, $max_intset) = [$settings['timeout'], $settings['set-max-intset-entries']];

        $updates = [
            ['timeout' => $timeout + 30, 'set-max-intset-entries' => $max_intset + 128],
            ['timeout' => $timeout,      'set-max-intset-entries' => $max_intset],
        ];

        foreach ($updates as $update) {
            $this->assertTrue($this->redis->config('set', $update));
            $vals = $this->redis->config('get', array_keys($update));
            $this->assertEqualsWeak($vals, $update, true);
        }

        /* Make sure PhpRedis catches malformed multiple get/set calls */
        $this->assertFalse(@$this->redis->config('get', []));
        $this->assertFalse(@$this->redis->config('set', []));
        $this->assertFalse(@$this->redis->config('set', [0, 1, 2]));
    }

    public function testReconnectSelect() {
        $key = 'reconnect-select';
        $value = 'Has been set!';

        $original_cfg = $this->redis->config('GET', 'timeout');

        // Make sure the default DB doesn't have the key.
        $this->redis->select(0);
        $this->redis->del($key);

        // Set the key on a different DB.
        $this->redis->select(5);
        $this->redis->set($key, $value);

        // Time out after 1 second.
        $this->redis->config('SET', 'timeout', '1');

        // Wait for the connection to time out.  On very old versions
        // of Redis we need to wait much longer (TODO:  Investigate
        // which version exactly)
        sleep($this->minVersionCheck('3.0.0') ? 2 : 11);

        // Make sure we're still using the same DB.
        $this->assertKeyEquals($value, $key);

        // Revert the setting.
        $this->redis->config('SET', 'timeout', $original_cfg['timeout']);
    }

    public function testTime() {
        if (version_compare($this->version, '2.5.0') < 0)
            $this->markTestSkipped();

        $time_arr = $this->redis->time();
        $this->assertTrue(is_array($time_arr) && count($time_arr) == 2 &&
                          strval(intval($time_arr[0])) === strval($time_arr[0]) &&
                          strval(intval($time_arr[1])) === strval($time_arr[1]));
    }

    public function testReadTimeoutOption() {
        $this->assertTrue(defined('Redis::OPT_READ_TIMEOUT'));

        $this->redis->setOption(Redis::OPT_READ_TIMEOUT, '12.3');
        $this->assertEquals(12.3, $this->redis->getOption(Redis::OPT_READ_TIMEOUT));
    }

    public function testIntrospection() {
        // Simple introspection tests
        $this->assertEquals($this->getHost(), $this->redis->getHost());
        $this->assertEquals($this->getPort(), $this->redis->getPort());
        $this->assertEquals($this->getAuth(), $this->redis->getAuth());
    }

    public function testTransferredBytes() {
        $this->redis->set('key', 'val');

        $this->redis->clearTransferredBytes();

        $get_tx_resp = "*3\r\n$3\r\nGET\r\n$3\r\nkey\r\n";
        $get_rx_resp = "$3\r\nval\r\n";

        $this->assertKeyEquals('val', 'key');
        list ($tx, $rx) = $this->redis->getTransferredBytes();
        $this->assertEquals(strlen($get_tx_resp), $tx);
        $this->assertEquals(strlen($get_rx_resp), $rx);

        $this->redis->clearTransferredBytes();

        $this->redis->multi()->get('key')->get('key')->exec();
        list($tx, $rx) = $this->redis->getTransferredBytes();

        $this->assertEquals($tx, strlen("*1\r\n$5\r\nMULTI\r\n*1\r\n$4\r\nEXEC\r\n") +
                                 2 * strlen($get_tx_resp));

        $this->assertEquals($rx, strlen("+OK\r\n") + strlen("+QUEUED\r\n+QUEUED\r\n") +
                                 strlen("*2\r\n")  + 2 * strlen($get_rx_resp));
    }

    /**
     * Scan and variants
     */

    protected function get_keyspace_count($db) {
        $info = $this->redis->info();
        if (isset($info[$db])) {
            $info = $info[$db];
            $info = explode(',', $info);
            $info = explode('=', $info[0]);
            return $info[1];
        } else {
            return 0;
        }
    }

    public function testScan() {
        if (version_compare($this->version, '2.8.0') < 0)
            $this->markTestSkipped();

        // Key count
        $key_count = $this->get_keyspace_count('db0');

        // Have scan retry
        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

        // Scan them all
        $it = NULL;
        while ($keys = $this->redis->scan($it)) {
            $key_count -= count($keys);
        }
        // Should have iterated all keys
        $this->assertEquals(0, $key_count);

        // Unique keys, for pattern matching
        $uniq = uniqid();
        for ($i = 0; $i < 10; $i++) {
            $this->redis->set($uniq . "::$i", "bar::$i");
        }

        // Scan just these keys using a pattern match
        $it = NULL;
        while ($keys = $this->redis->scan($it, "*$uniq*")) {
            $i -= count($keys);
        }
        $this->assertEquals(0, $i);

        // SCAN with type is scheduled for release in Redis 6.
        if (version_compare($this->version, '6.0.0') >= 0) {
            // Use a unique ID so we can find our type keys
            $id = uniqid();

            $keys = [];
            // Create some simple keys and lists
            for ($i = 0; $i < 3; $i++) {
                $simple = "simple:{$id}:$i";
                $list = "list:{$id}:$i";

                $this->redis->set($simple, $i);
                $this->redis->del($list);
                $this->redis->rpush($list, ['foo']);

                $keys['STRING'][] = $simple;
                $keys['LIST'][] = $list;
            }

            // Make sure we can scan for specific types
            foreach ($keys as $type => $vals) {
                foreach ([0, 10] as $count) {
                    $resp = [];

                    $it = NULL;
                    while ($scan = $this->redis->scan($it, "*$id*", $count, $type)) {
                        $resp = array_merge($resp, $scan);
                    }

                    $this->assertEqualsCanonicalizing($vals, $resp);
                }
            }
        }
    }

    public function testScanPrefix() {
        $keyid = uniqid();

        /* Set some keys with different prefixes */
        $prefixes = ['prefix-a:', 'prefix-b:'];
        foreach ($prefixes as $prefix) {
            $this->redis->setOption(Redis::OPT_PREFIX, $prefix);
            $this->redis->set("$keyid", 'LOLWUT');
            $all_keys["{$prefix}{$keyid}"] = true;
        }

        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_PREFIX);

        foreach ($prefixes as $prefix) {
            $this->redis->setOption(Redis::OPT_PREFIX, $prefix);
            $it = NULL;
            $keys = $this->redis->scan($it, "*$keyid*");
            $this->assertEquals($keys, ["{$prefix}{$keyid}"]);
        }

        /* Unset the prefix option */
        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NOPREFIX);

        $it = NULL;
        while ($keys = $this->redis->scan($it, "*$keyid*")) {
            foreach ($keys as $key) {
                unset($all_keys[$key]);
            }
        }

        /* Should have touched every key */
        $this->assertEquals(0, count($all_keys));
    }

    public function testMaxRetriesOption() {
        $maxRetriesExpected = 5;
        $this->redis->setOption(Redis::OPT_MAX_RETRIES, $maxRetriesExpected);
        $maxRetriesActual=$this->redis->getOption(Redis::OPT_MAX_RETRIES);
        $this->assertEquals($maxRetriesActual, $maxRetriesExpected);
    }

    public function testBackoffOptions() {
        $algorithms = [
            Redis::BACKOFF_ALGORITHM_DEFAULT,
            Redis::BACKOFF_ALGORITHM_CONSTANT,
            Redis::BACKOFF_ALGORITHM_UNIFORM,
            Redis::BACKOFF_ALGORITHM_EXPONENTIAL,
            Redis::BACKOFF_ALGORITHM_EQUAL_JITTER,
            Redis::BACKOFF_ALGORITHM_FULL_JITTER,
            Redis::BACKOFF_ALGORITHM_DECORRELATED_JITTER
        ];

        foreach ($algorithms as $algorithm) {
            $this->assertTrue($this->redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, $algorithm));
            $this->assertEquals($algorithm, $this->redis->getOption(Redis::OPT_BACKOFF_ALGORITHM));
        }

        // Invalid algorithm
        $this->assertFalse($this->redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, 55555));

        foreach ([Redis::OPT_BACKOFF_BASE, Redis::OPT_BACKOFF_CAP] as $option) {
            foreach ([500, 750] as $value) {
                $this->redis->setOption($option, $value);
                $this->assertEquals($value, $this->redis->getOption($option));
            }
        }
    }

    public function testHashExpiration() {
        if ( ! $this->haveCommand('HEXPIRE'))
            $this->markTestSkipped();

        $hexpire_cmds = [
            'hexpire'    => 10,
            'hpexpire'   => 10000,
            'hexpireat'  => time() + 10,
            'hpexpireat' => time() * 1000 + 10000,
        ];

        $httl_cmds = ['httl', 'hpttl', 'hexpiretime', 'hpexpiretime'];

        $hash = ['Picard' => 'Enterprise', 'Sisko' => 'Defiant'];
        $keys = array_keys($hash);

        foreach ($hexpire_cmds as $exp_cmd => $ttl) {
            $this->redis->del('hash');
            $this->redis->hmset('hash', $hash);

            /* Set a TTL on one existing and one non-existing field */
            $res = $this->redis->{$exp_cmd}('hash', $ttl, ['Picard', 'nofield']);

            $this->assertEquals($res, [1, -2]);

            foreach ($httl_cmds as $ttl_cmd) {
                $res = $this->redis->{$ttl_cmd}('hash', $keys);
                $this->assertIsArray($res);
                $this->assertEquals(count($keys), count($res));

                /* Picard: has an expiry (>0), Siskto does not (<0) */
                $this->assertTrue($res[0] > 0);
                $this->assertTrue($res[1] < 0);
            }

            $this->redis->del('m');
            $this->redis->hmset('m', ['F' => 'V']);

            // NX - Only set expiry if it doesn't have one
            foreach ([[1], [0]] as $expected) {
                $res = $this->redis->{$exp_cmd}('m', $ttl, ['F'], 'NX');
                $this->assertEquals($expected, $res);
            }

            // XX - Set if it has one
            $res = $this->redis->{$exp_cmd}('m', $ttl, ['F'], 'XX');
            $this->assertEquals([1], $res);
            $this->redis->hpersist('m', ['F']);
            $res = $this->redis->{$exp_cmd}('m', $ttl, ['F'], 'XX');
            $this->assertEquals([0], $res);

            // GT - should set if the new expiration is larger
            $res = $this->redis->{$exp_cmd}('m', $ttl, ['F']);
            $res = $this->redis->{$exp_cmd}('m', $ttl + 100, ['F'], 'GT');
            $this->assertEquals([1], $res);

            // LT - should not set if the new expiration is smaller
            $res = $this->redis->{$exp_cmd}('m', intval($ttl / 2), ['F'], 'LT');
            $this->assertTrue(is_array($res) && $res[0] > 0);
        }
    }

    public function testHGetEx() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $now = time();

        $tests = [
            [['EX' => 10], 'httl', 0, 10],
            [['PX' => 10000], 'hpttl', 0, 10000],
            [['EXAT' => $now + 10], 'hexpiretime', $now, $now + 10],
            [['PXAT' => $now * 1000 + 10000], 'hpexpiretime', $now * 1000, $now * 1000 + 10000],
            ['PERSIST', 'httl', -1, -1],
            [['PERSIST'], 'httl', -1,-1],
        ];

        $hash = ['ship' => 'Defiant', 'captain' => 'Sisko'];

        foreach ($tests as [$expireArg, $ttlFn, $minTtl, $maxTtl]) {
            $this->redis->del('hash');
            $this->assertTrue($this->redis->hmset('hash', $hash));

            $v = $this->redis->hgetex('hash', ['ship', 'captain'], $expireArg);
            $this->assertEquals($hash, $v);

            $ttls = $this->redis->{$ttlFn}('hash', ['ship', 'captain']);
            $this->assertIsArray($ttls);

            foreach ($ttls as $val) {
                $this->assertBetween($val, $minTtl, $maxTtl);
            }
        }
    }

    public function testHSetEx(): void {
        if ( ! $this->minVersionCheck('8.0')) {
            $this->markTestSkipped('HSETEX requires Redis 8+');
        }

        $now  = time();
        $nowMs = $now * 1000;
        $hash = ['a' => 'foo', 'b' => 'bar'];

        $cases = [
            [['EX', 5], 'httl'],
            [['PX', 1234], 'hpttl'],
            [['EXAT', $now + 5], 'hexpiretime'],
            [['PXAT', $nowMs + 5000], 'hpexpiretime'],
        ];

        foreach ($cases as [[$ex, $n], $ttlFn]) {
            $this->redis->del('hash');
            $this->assertTrue($this->redis->hMSet('hash', $hash));

            $expireArg = [$ex => $n];
            $result = $this->redis->hsetex('hash', $hash, $expireArg);
            $this->assertLTE(count($hash), $result);

            $got = $this->redis->hMGet('hash', array_keys($hash));
            $this->assertEquals($hash, $got);

            $ttls = $this->redis->{$ttlFn}('hash', array_keys($hash));
            $this->assertIsArray($ttls);

            foreach ($ttls as $ttl) {
                $this->assertLTE($n, $ttl);
            }
        }

        $this->assertIsInt($this->redis->del('hash'));
        $this->assertEquals(
            1, $this->redis->hsetex('hash', ['a' => 'v'], ['EX' => 20])
        );
        $this->assertEquals(
            1, $this->redis->hsetex('hash', ['a' => 'v2'], ['KEEPTTL'])
        );

        $ttl = $this->redis->httl('hash', ['a']);
        $this->assertLTE(20, $ttl[0]);

        $this->assertIsInt($this->redis->del('hash'));

        // FNX - Only if none exist
        foreach ([1, 0] as $expected) {
            $ex = ['EX' => 20, 'FNX'];
            $result = $this->redis->hsetex('hash', $hash, $ex);
            $this->assertEquals($expected, $result);
        }

        // FXX - Only if they all exist
        foreach ([1, 0] as $expected) {
            $ex = ['EX' => 20, 'FXX'];
            $result = $this->redis->hsetex('hash', $hash, $ex);
            $this->assertEquals($expected, $result);

            $k = array_rand($hash);
            $this->redis->hdel('hash', $k);
        }
    }

    public function testHGetDel() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('hash'));
        $hash = ['ship' => 'Defiant', 'captain' => 'Sisko'];

        $this->assertTrue($this->redis->hmset('hash', $hash));
        $this->assertEquals($hash, $this->redis->hgetall('hash'));

        $this->assertEquals(['captain' => 'Sisko'], $this->redis->hgetdel('hash', ['captain']));
        $this->assertEquals(['ship' => 'Defiant'], $this->redis->hgetall('hash'));
    }

    public function testHScan() {
        if (version_compare($this->version, '2.8.0') < 0)
            $this->markTestSkipped();

        // Never get empty sets
        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

        $this->redis->del('hash');
        $foo_mems = 0;

        for ($i = 0; $i < 100; $i++) {
            if ($i > 3) {
                $this->redis->hset('hash', "member:$i", "value:$i");
            } else {
                $this->redis->hset('hash', "foomember:$i", "value:$i");
                $foo_mems++;
            }
        }

        // Scan all of them
        $it = NULL;
        while ($keys = $this->redis->hscan('hash', $it)) {
            $i -= count($keys);
        }
        $this->assertEquals(0, $i);

        // Scan just *foomem* (should be 4)
        $it = NULL;
        while ($keys = $this->redis->hscan('hash', $it, '*foomember*')) {
            $foo_mems -= count($keys);
            foreach ($keys as $mem => $val) {
                $this->assertStringContains('member', $mem);
                $this->assertStringContains('value', $val);
            }
        }
        $this->assertEquals(0, $foo_mems);
    }

    public function testSScan() {
        if (version_compare($this->version, '2.8.0') < 0)
            $this->markTestSkipped();

        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

        $this->redis->del('set');
        for ($i = 0; $i < 100; $i++) {
            $this->redis->sadd('set', "member:$i");
        }

        // Scan all of them
        $it = NULL;
        while ($keys = $this->redis->sscan('set', $it)) {
            $i -= count($keys);
            foreach ($keys as $mem) {
                $this->assertStringContains('member', $mem);
            }
        }
        $this->assertEquals(0, $i);

        // Scan just ones with zero in them (0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
        $it = NULL;
        $w_zero = 0;
        while ($keys = $this->redis->sscan('set', $it, '*0*')) {
            $w_zero += count($keys);
        }
        $this->assertEquals(10, $w_zero);
    }

    public function testZScan() {
        if (version_compare($this->version, '2.8.0') < 0)
            $this->markTestSkipped();

        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

        $this->redis->del('zset');

        [$t_score, $p_score, $p_count] = [0, 0, 0];
        for ($i = 0; $i < 2000; $i++) {
            if ($i < 10) {
                $this->redis->zadd('zset', $i, "pmem:$i");
                $p_score += $i;
                $p_count++;
            } else {
                $this->redis->zadd('zset', $i, "mem:$i");
            }

            $t_score += $i;
        }

        // Scan them all
        $it = NULL;
        while ($keys = $this->redis->zscan('zset', $it)) {
            foreach ($keys as $mem => $f_score) {
                $t_score -= $f_score;
                $i--;
            }
        }

        $this->assertEquals(0, $i);
        $this->assertEquals(0., $t_score);

        // Just scan 'pmem' members
        $it = NULL;
        $p_score_old = $p_score;
        $p_count_old = $p_count;
        while ($keys = $this->redis->zscan('zset', $it, '*pmem*')) {
            foreach ($keys as $mem => $f_score) {
                $p_score -= $f_score;
                $p_count -= 1;
            }
        }
        $this->assertEquals(0., $p_score);
        $this->assertEquals(0, $p_count);

        // Turn off retrying and we should get some empty results
        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NORETRY);
        [$skips, $p_score, $p_count] = [0, $p_score_old, $p_count_old];

        $it = NULL;
        while (($keys = $this->redis->zscan('zset', $it, '*pmem*')) !== FALSE) {
            if (count($keys) == 0) $skips++;
            foreach ($keys as $mem => $f_score) {
                $p_score -= $f_score;
                $p_count -= 1;
            }
        }
        // We should still get all the keys, just with several empty results
        $this->assertGT(0, $skips);
        $this->assertEquals(0., $p_score);
        $this->assertEquals(0, $p_count);
    }

    /* Make sure we capture errors when scanning */
    public function testScanErrors() {
        $this->redis->set('scankey', 'simplekey');

        foreach (['sScan', 'hScan', 'zScan'] as $method) {
            $it = NULL;
            $this->redis->$method('scankey', $it);
            $this->assertEquals(0, strpos($this->redis->getLastError(), 'WRONGTYPE'));
        }
    }

    //
    // HyperLogLog (PF) commands
    //

    protected function createPFKey($key, $count) {
        $mems = [];
        for ($i = 0; $i < $count; $i++) {
            $mems[] = uniqid('pfmem:');
        }

        // Estimation by Redis
        $this->redis->pfAdd($key, $count);
    }

    public function testPFCommands() {
        if (version_compare($this->version, '2.8.9') < 0)
            $this->markTestSkipped();

        $mems = [];

        for ($i = 0; $i < 1000; $i++) {
            if ($i % 2 == 0) {
                $mems[] = uniqid();
            } else {
                $mems[] = $i;
            }
        }

        // How many keys to create
        $key_count = 10;

        // Iterate prefixing/serialization options
        foreach ($this->getSerializers() as $ser) {
            foreach (['', 'hl-key-prefix:'] as $prefix) {
                $keys = [];

                // Now add for each key
                for ($i = 0; $i < $key_count; $i++) {
                    $key    = "{key}:$i";
                    $keys[] = $key;

                    // Clean up this key
                    $this->redis->del($key);

                    // Add to our cardinality set, and confirm we got a valid response
                    $this->assertGT(0, $this->redis->pfadd($key, $mems));

                    // Grab estimated cardinality
                    $card = $this->redis->pfcount($key);
                    $this->assertIsInt($card);

                    // Count should be close
                    $this->assertBetween($card, count($mems) * .9, count($mems) * 1.1);

                    // The PFCOUNT on this key should be the same as the above returned response
                    $this->assertEquals($card, $this->redis->pfcount($key));
                }

                // Clean up merge key
                $this->redis->del('pf-merge-{key}');

                // Merge the counters
                $this->assertTrue($this->redis->pfmerge('pf-merge-{key}', $keys));

                // Validate our merged count
                $redis_card = $this->redis->pfcount('pf-merge-{key}');

                // Merged cardinality should still be roughly 1000
                $this->assertBetween($redis_card, count($mems) * .9,
                                     count($mems) * 1.1);

                // Clean up merge key
                $this->redis->del('pf-merge-{key}');
            }
        }
    }

    //
    // GEO* command tests
    //

    protected function rawCommandArray($key, $args) {
        return call_user_func_array([$this->redis, 'rawCommand'], $args);
    }

    protected function addCities($key) {
        $this->redis->del($key);
        foreach ($this->cities as $city => $longlat) {
            $this->redis->geoadd($key, $longlat[0], $longlat[1], $city);
        }
    }

    /* GEOADD */
    public function testGeoAdd() {
        if ( ! $this->minVersionCheck('3.2'))
            $this->markTestSkipped();

        $this->redis->del('geokey');

        /* Add them one at a time */
        foreach ($this->cities as $city => $longlat) {
            $this->assertEquals(1, $this->redis->geoadd('geokey', $longlat[0], $longlat[1], $city));
        }

        /* Add them again, all at once */
        $args = ['geokey'];
        foreach ($this->cities as $city => $longlat) {
            $args = array_merge($args, [$longlat[0], $longlat[1], $city]);
        }

        /* They all exist, should be nothing added */
        $this->assertEquals(call_user_func_array([$this->redis, 'geoadd'], $args), 0);
    }

    /* GEORADIUS */
    public function genericGeoRadiusTest($cmd) {
        if ( ! $this->minVersionCheck('3.2.0'))
            $this->markTestSkipped();

        /* Chico */
        $city = 'Chico';
        $lng = -121.837478;
        $lat = 39.728494;

        $this->addCities('{gk}');

        /* Pre tested with redis-cli.  We're just verifying proper delivery of distance and unit */
        if ($cmd == 'georadius' || $cmd == 'georadius_ro') {
            $this->assertEquals(['Chico'], $this->redis->$cmd('{gk}', $lng, $lat, 10, 'mi'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $lng, $lat, 30, 'mi'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $lng, $lat, 50, 'km'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $lng, $lat, 50000, 'm'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $lng, $lat, 150000, 'ft'));
            $args = [$cmd, '{gk}', $lng, $lat, 500, 'mi'];

            /* Test a bad COUNT argument */
            foreach ([-1, 0, 'notanumber'] as $count) {
                $this->assertFalse(@$this->redis->$cmd('{gk}', $lng, $lat, 10, 'mi', ['count' => $count]));
            }
        } else {
            $this->assertEquals(['Chico'], $this->redis->$cmd('{gk}', $city, 10, 'mi'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $city, 30, 'mi'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $city, 50, 'km'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $city, 50000, 'm'));
            $this->assertEquals(['Gridley', 'Chico'], $this->redis->$cmd('{gk}', $city, 150000, 'ft'));
            $args = [$cmd, '{gk}', $city, 500, 'mi'];

            /* Test a bad COUNT argument */
            foreach ([-1, 0, 'notanumber'] as $count) {
                $this->assertFalse(@$this->redis->$cmd('{gk}', $city, 10, 'mi', ['count' => $count]));
            }
        }

        /* Options */
        $opts = ['WITHCOORD', 'WITHDIST', 'WITHHASH'];
        $sortopts = ['', 'ASC', 'DESC'];
        $storeopts = ['', 'STORE', 'STOREDIST'];

        for ($i = 0; $i < count($opts); $i++) {
            $subopts = array_slice($opts, 0, $i);
            shuffle($subopts);

            $subargs = $args;
            foreach ($subopts as $opt) {
                $subargs[] = $opt;
            }

            /* Cannot mix STORE[DIST] with the WITH* arguments */
            $realstoreopts = count($subopts) == 0 ? $storeopts : [];

            $base_subargs = $subargs;
            $base_subopts = $subopts;

            foreach ($realstoreopts as $store_type) {
                for ($c = 0; $c < 3; $c++) {
                    $subargs = $base_subargs;
                    $subopts = $base_subopts;

                    /* Add a count if we're past first iteration */
                    if ($c > 0) {
                        $subopts['count'] = $c;
                        $subargs[] = 'count';
                        $subargs[] = $c;
                    }

                    /* Adding optional sort */
                    foreach ($sortopts as $sortopt) {
                        $realargs = $subargs;
                        $realopts = $subopts;

                        if ($sortopt) {
                            $realargs[] = $sortopt;
                            $realopts[] = $sortopt;
                        }

                        if ($store_type) {
                            $realopts[$store_type] = "{gk}-$store_type";
                            $realargs[] = $store_type;
                            $realargs[] = "{gk}-$store_type";
                        }

                        $ret1 = $this->rawCommandArray('{gk}', $realargs);
                        if ($cmd == 'georadius' || $cmd == 'georadius_ro') {
                            $ret2 = $this->redis->$cmd('{gk}', $lng, $lat, 500, 'mi', $realopts);
                        } else {
                            $ret2 = $this->redis->$cmd('{gk}', $city, 500, 'mi', $realopts);
                        }

                        $this->assertEquals($ret1, $ret2);
                    }
                }
            }
        }
    }

    public function testGeoRadius() {
        if ( ! $this->minVersionCheck('3.2.0'))
            $this->markTestSkipped();

        $this->genericGeoRadiusTest('georadius');
        $this->genericGeoRadiusTest('georadius_ro');
    }

    public function testGeoRadiusByMember() {
        if ( ! $this->minVersionCheck('3.2.0'))
            $this->markTestSkipped();

        $this->genericGeoRadiusTest('georadiusbymember');
        $this->genericGeoRadiusTest('georadiusbymember_ro');
    }

    public function testGeoPos() {
        if ( ! $this->minVersionCheck('3.2.0'))
            $this->markTestSkipped();

        $this->addCities('gk');
        $this->assertEquals($this->rawCommandArray('gk', ['geopos', 'gk', 'Chico', 'Sacramento']), $this->redis->geopos('gk', 'Chico', 'Sacramento'));
        $this->assertEquals($this->rawCommandArray('gk', ['geopos', 'gk', 'Cupertino']), $this->redis->geopos('gk', 'Cupertino'));
    }

    public function testGeoHash() {
        if ( ! $this->minVersionCheck('3.2.0'))
            $this->markTestSkipped();

        $this->addCities('gk');
        $this->assertEquals($this->rawCommandArray('gk', ['geohash', 'gk', 'Chico', 'Sacramento']), $this->redis->geohash('gk', 'Chico', 'Sacramento'));
        $this->assertEquals($this->rawCommandArray('gk', ['geohash', 'gk', 'Chico']), $this->redis->geohash('gk', 'Chico'));
    }

    public function testGeoDist() {
        if ( ! $this->minVersionCheck('3.2.0'))
            $this->markTestSkipped();

        $this->addCities('gk');

        $r1 = $this->redis->geodist('gk', 'Chico', 'Cupertino');
        $r2 = $this->rawCommandArray('gk', ['geodist', 'gk', 'Chico', 'Cupertino']);
        $this->assertEquals(round($r1, 8), round($r2, 8));

        $r1 = $this->redis->geodist('gk', 'Chico', 'Cupertino', 'km');
        $r2 = $this->rawCommandArray('gk', ['geodist', 'gk', 'Chico', 'Cupertino', 'km']);
        $this->assertEquals(round($r1, 8), round($r2, 8));
    }

    public function testGeoSearch() {
        if ( ! $this->minVersionCheck('6.2.0'))
            $this->markTestSkipped();

        $this->addCities('gk');

        $this->assertEquals(['Chico'], $this->redis->geosearch('gk', 'Chico', 1, 'm'));
        $this->assertValidate($this->redis->geosearch('gk', 'Chico', 1, 'm', ['withcoord', 'withdist', 'withhash']), function ($v) {
            $this->assertArrayKey($v, 'Chico', 'is_array');
            $this->assertEquals(count($v['Chico']), 3);
            $this->assertArrayKey($v['Chico'], 0, 'is_float');
            $this->assertArrayKey($v['Chico'], 1, 'is_int');
            $this->assertArrayKey($v['Chico'], 2, 'is_array');
            return true;
        });
    }

    public function testGeoSearchByPolygon() {
        if ( ! $this->minValkeyVersionCheck('9.0.0'))
            $this->markTestSkipped();

        $this->addCities('ca:cities');

        $res = $this->redis->geosearch('ca:cities', '', [
            -121.90, 39.65, -121.77, 39.65, -121.77, 39.80, -121.90, 39.80
        ], '');

        $this->assertEquals(['Chico'], $res);
    }

    public function testGeoSearchStoreByPolygon() {
        if ( ! $this->minValkeyVersionCheck('9.0.0'))
            $this->markTestSkipped();

        $this->addCities('{geo}:cities');
        $this->assertEquals(1, $this->redis->geosearchstore('{geo}:dst', '{geo}:cities', '', [
            -121.90, 39.65, -121.77, 39.65, -121.77, 39.80, -121.90, 39.80
        ], ''));

        $this->assertEquals(['Chico'], $this->redis->geosearch('{geo}:dst', 'Chico', 1, 'm'));
    }

    public function testGeoSearchStore() {
        if ( ! $this->minVersionCheck('6.2.0'))
            $this->markTestSkipped();

        $this->addCities('{gk}src');
        $this->assertEquals(3, $this->redis->geosearchstore('{gk}dst', '{gk}src', 'Chico', 100, 'km'));
        $this->assertEquals(['Chico'], $this->redis->geosearch('{gk}dst', 'Chico', 1, 'm'));
    }

    /* Test a 'raw' command */
    public function testRawCommand() {
        $key = uniqid();

        $this->redis->set($key,'some-value');
        $result = $this->redis->rawCommand('get', $key);
        $this->assertEquals($result, 'some-value');

        $this->redis->del('mylist');
        $this->redis->rpush('mylist', 'A', 'B', 'C', 'D');
        $this->assertEquals(['A', 'B', 'C', 'D'], $this->redis->lrange('mylist', 0, -1));
    }

    /* STREAMS */

    protected function addStreamEntries($key, $count) {
        $ids = [];

        $this->redis->del($key);

        for ($i = 0; $i < $count; $i++) {
            $ids[] = $this->redis->xAdd($key, '*', ['field' => "value:$i"]);
        }

        return $ids;
    }

    protected function addStreamsAndGroups($streams, $count, $groups) {
        $ids = [];

        foreach ($streams as $stream) {
            $ids[$stream] = $this->addStreamEntries($stream, $count);
            foreach ($groups as $group => $id) {
                $this->redis->xGroup('CREATE', $stream, $group, $id);
            }
        }

        return $ids;
    }

    public function testXAdd() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        $this->redis->del('stream');
        for ($i = 0; $i < 5; $i++) {
            $id = $this->redis->xAdd('stream', '*', ['k1' => 'v1', 'k2' => 'v2']);
            $this->assertEquals($i+1, $this->redis->xLen('stream'));

            /* Redis should return <timestamp>-<sequence> */
            $bits = explode('-', $id);
            $this->assertEquals(count($bits), 2);
            $this->assertTrue(is_numeric($bits[0]));
            $this->assertTrue(is_numeric($bits[1]));
        }

        /* Test an absolute maximum length */
        for ($i = 0; $i < 100; $i++) {
            $this->redis->xAdd('stream', '*', ['k' => 'v'], 10);
        }
        $this->assertEquals(10, $this->redis->xLen('stream'));

        /* Not the greatest test but I'm unsure if approximate trimming is
         * totally deterministic, so just make sure we are able to add with
         * an approximate maxlen argument structure */
        $id = $this->redis->xAdd('stream', '*', ['k' => 'v'], 10, true);
        $this->assertEquals(count(explode('-', $id)), 2);

        /* Empty message should fail */
        @$this->redis->xAdd('stream', '*', []);
    }

    protected function doXRangeTest($reverse) {
        $key = '{stream}';

        if ($reverse) {
            list($cmd,$a1,$a2) = ['xRevRange', '+', 0];
        } else {
            list($cmd,$a1,$a2) = ['xRange', 0, '+'];
        }

        $this->redis->del($key);
        for ($i = 0; $i < 3; $i++) {
            $msg = ['field' => "value:$i"];
            $id = $this->redis->xAdd($key, '*', $msg);
            $rows[$id] = $msg;
        }

        $messages = $this->redis->$cmd($key, $a1, $a2);
        $this->assertEquals(count($messages), 3);

        $i = $reverse ? 2 : 0;
        foreach ($messages as $seq => $v) {
            $this->assertEquals(count(explode('-', $seq)), 2);
            $this->assertEquals($v, ['field' => "value:$i"]);
            $i += $reverse ? -1 : 1;
        }

        /* Test COUNT option */
        for ($count = 1; $count <= 3; $count++) {
            $messages = $this->redis->$cmd($key, $a1, $a2, $count);
            $this->assertEquals(count($messages), $count);
        }
    }

    public function testXRange() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        foreach ([false, true] as $reverse) {
            foreach ($this->getSerializers() as $serializer) {
                foreach ([NULL, 'prefix:'] as $prefix) {
                    $this->redis->setOption(Redis::OPT_PREFIX, $prefix);
                    $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
                    $this->doXRangeTest($reverse);
                }
            }
        }
    }

    protected function testXLen() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        $this->redis->del('{stream}');
        for ($i = 0; $i < 5; $i++) {
            $this->redis->xadd('{stream}', '*', ['foo' => 'bar']);
            $this->assertEquals($i+1, $this->redis->xLen('{stream}'));
        }
    }

    public function testXGroup() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        /* CREATE MKSTREAM */
        $key = 's:' . uniqid();
        $this->assertFalse($this->redis->xGroup('CREATE', $key, 'g0', 0));
        $this->assertTrue($this->redis->xGroup('CREATE', $key, 'g1', 0, true));

        /* XGROUP DESTROY */
        $this->assertEquals(1, $this->redis->xGroup('DESTROY', $key, 'g1'));

        /* Populate some entries in stream 's' */
        $this->addStreamEntries('s', 2);

        /* CREATE */
        $this->assertTrue($this->redis->xGroup('CREATE', 's', 'mygroup', '$'));
        $this->assertFalse($this->redis->xGroup('CREATE', 's', 'mygroup', 'BAD_ID'));

        /* BUSYGROUP */
        $this->redis->xGroup('CREATE', 's', 'mygroup', '$');
        $this->assertEquals(0, strpos($this->redis->getLastError(), 'BUSYGROUP'));

        /* SETID */
        $this->assertTrue($this->redis->xGroup('SETID', 's', 'mygroup', '$'));
        $this->assertFalse($this->redis->xGroup('SETID', 's', 'mygroup', 'BAD_ID'));

        $this->assertEquals(0, $this->redis->xGroup('DELCONSUMER', 's', 'mygroup', 'myconsumer'));

        if ( ! $this->minVersionCheck('6.2.0'))
            return;

        /* CREATECONSUMER */
        $this->assertEquals(1, $this->redis->del('s'));
        $this->assertTrue($this->redis->xgroup('create', 's', 'mygroup', '$', true));
        for ($i = 0; $i < 3; $i++) {
            $this->assertEquals(1, $this->redis->xgroup('createconsumer', 's', 'mygroup', "c:$i"));
            $info = $this->redis->xinfo('consumers', 's', 'mygroup');
            $this->assertIsArray($info, $i + 1);
            for ($j = 0; $j <= $i; $j++) {
                $this->assertTrue(isset($info[$j]) && isset($info[$j]['name']) && $info[$j]['name'] == "c:$j");
            }
        }

        /* Make sure we don't erroneously send options that don't belong to the operation */
        $this->assertEquals(1,
            $this->redis->xGroup('CREATECONSUMER', 's', 'mygroup', 'fake-consumer', true, 1337));

        /* Make sure we handle the case where the user doesn't send enough arguments */
        $this->redis->clearLastError();
        $this->assertFalse(@$this->redis->xGroup('CREATECONSUMER'));
        $this->assertNull($this->redis->getLastError());
        $this->assertFalse(@$this->redis->xGroup('create'));
        $this->assertNull($this->redis->getLastError());

        if ( ! $this->minVersionCheck('7.0.0'))
            return;

        /* ENTRIESREAD */
        $this->assertEquals(1, $this->redis->del('s'));
        $this->assertTrue($this->redis->xGroup('create', 's', 'mygroup', '$', true, 1337));
        $info = $this->redis->xinfo('groups', 's');
        $this->assertTrue(isset($info[0]['entries-read']));
        /* Starting with redis 8.2.2 returns 0 */
        $this->assertTrue((int)$info[0]['entries-read'] === 1337 || (int)$info[0]['entries-read'] === 0);
    }

    public function testXAck() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        for ($n = 1; $n <= 3; $n++) {
            $this->addStreamsAndGroups(['{s}'], 3, ['g1' => 0]);
            $msg = $this->redis->xReadGroup('g1', 'c1', ['{s}' => '>']);

            /* Extract IDs */
            $smsg = array_shift($msg);
            $ids = array_keys($smsg);

            /* Now ACK $n messages */
            $ids = array_slice($ids, 0, $n);
            $this->assertEquals($n, $this->redis->xAck('{s}', 'g1', $ids));
        }

        /* Verify sending no IDs is a failure */
        $this->assertFalse($this->redis->xAck('{s}', 'g1', []));
    }

    protected function doXReadTest() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        $row = ['f1' => 'v1', 'f2' => 'v2'];
        $msgdata = [
            '{stream}-1' => $row,
            '{stream}-2' => $row,
        ];

        /* Append a bit of data and populate STREAM queries */
        $this->redis->del(array_keys($msgdata));
        foreach ($msgdata as $key => $message) {
            for ($r = 0; $r < 2; $r++) {
                $id = $this->redis->xAdd($key, '*', $message);
                $qresult[$key][$id] = $message;
            }
            $qzero[$key] = 0;
            $qnew[$key] = '$';
            $keys[] = $key;
        }

        /* Everything from both streams */
        $rmsg = $this->redis->xRead($qzero);
        $this->assertEquals($rmsg, $qresult);

        /* Test COUNT option */
        for ($count = 1; $count <= 2; $count++) {
            $rmsg = $this->redis->xRead($qzero, $count);
            foreach ($keys as $key) {
                $this->assertEquals(count($rmsg[$key]), $count);
            }
        }

        /* Should be empty (no new entries) */
        $this->assertEquals(count($this->redis->xRead($qnew)),0);

        /* Test against a specific ID */
        $id = $this->redis->xAdd('{stream}-1', '*', $row);
        $new_id = $this->redis->xAdd('{stream}-1', '*', ['final' => 'row']);
        $rmsg = $this->redis->xRead(['{stream}-1' => $id]);
        $this->assertEquals(
            $this->redis->xRead(['{stream}-1' => $id]),
            ['{stream}-1' => [$new_id => ['final' => 'row']]]
        );

        /* Empty query should fail */
        $this->assertFalse(@$this->redis->xRead([]));
    }

    public function testXRead() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        foreach ($this->getSerializers() as $serializer) {
            $this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
            $this->doXReadTest();
        }

        /* Don't need to test BLOCK multiple times */
        $m1 = round(microtime(true)*1000);
        $this->redis->xRead(['somestream' => '$'], -1, 100);
        $m2 = round(microtime(true)*1000);
        $this->assertGT(99, $m2 - $m1);
    }

    protected function compareStreamIds($redis, $control) {
        foreach ($control as $stream => $ids) {
            $rcount = count($redis[$stream]);
            $lcount = count($control[$stream]);

            /* We should have the same number of messages */
            $this->assertEquals($rcount, $lcount);

            /* We should have the exact same IDs */
            foreach ($ids as $k => $id) {
                $this->assertTrue(isset($redis[$stream][$id]));
            }
        }
    }

    public function testXReadGroup() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        /* Create some streams and groups */
        $streams = ['{s}-1', '{s}-2'];
        $groups = ['group1' => 0, 'group2' => 0];

        /* I'm not totally sure why Redis behaves this way, but we have to
         * send '>' first and then send ID '0' for subsequent xReadGroup calls
         * or Redis will not return any messages.  This behavior changed from
         * redis 5.0.1 and 5.0.2 but doing it this way works for both versions. */
        $qcount = 0;
        $query1 = ['{s}-1' => '>', '{s}-2' => '>'];
        $query2 = ['{s}-1' => '0', '{s}-2' => '0'];

        $ids = $this->addStreamsAndGroups($streams, 1, $groups);

        /* Test that we get get the IDs we should */
        foreach (['group1', 'group2'] as $group) {
            foreach ($ids as $stream => $messages) {
                while ($ids[$stream]) {
                    /* Read more messages */
                    $query = !$qcount++ ? $query1 : $query2;
                    $resp = $this->redis->xReadGroup($group, 'consumer', $query);

                    /* They should match with our local control array */
                    $this->compareStreamIds($resp, $ids);

                    /* Remove a message from our control *and* XACK it in Redis */
                    $id = array_shift($ids[$stream]);
                    $this->redis->xAck($stream, $group, [$id]);
                }
            }
        }

        /* Test COUNT option */
        for ($c = 1; $c <= 3; $c++) {
            $this->addStreamsAndGroups($streams, 3, $groups);
            $resp = $this->redis->xReadGroup('group1', 'consumer', $query1, $c);

            foreach ($resp as $stream => $smsg) {
                $this->assertEquals(count($smsg), $c);
            }
        }

        /* Test COUNT option with NULL (should be ignored) */
        $this->addStreamsAndGroups($streams, 3, $groups, NULL);
        $resp = $this->redis->xReadGroup('group1', 'consumer', $query1, NULL);
        foreach ($resp as $stream => $smsg) {
            $this->assertEquals(count($smsg), 3);
        }

        /* Finally test BLOCK with a sloppy timing test */
        $tm1 = $this->mstime();
        $qnew = ['{s}-1' => '>', '{s}-2' => '>'];
        $this->redis->xReadGroup('group1', 'c1', $qnew, 0, 100);
        $this->assertGTE(100, $this->mstime() - $tm1);

        /* Make sure passing NULL to block doesn't block */
        $tm1 = $this->mstime();
        $this->redis->xReadGroup('group1', 'c1', $qnew, NULL, NULL);
        $this->assertLT(100, $this->mstime() - $tm1);

        /* Make sure passing bad values to BLOCK or COUNT immediately fails */
        $this->assertFalse(@$this->redis->xReadGroup('group1', 'c1', $qnew, -1));
        $this->assertFalse(@$this->redis->xReadGroup('group1', 'c1', $qnew, NULL, -1));
    }

    public function testXPending() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        $rows = 5;
        $this->addStreamsAndGroups(['s'], $rows, ['group' => 0]);

        $msg = $this->redis->xReadGroup('group', 'consumer', ['s' => 0]);
        $ids = array_keys($msg['s']);

        for ($n = count($ids); $n >= 0; $n--) {
            $xp = $this->redis->xPending('s', 'group');

            $this->assertEquals(count($ids), $xp[0]);

            /* Verify we're seeing the IDs themselves */
            for ($idx = 1; $idx <= 2; $idx++) {
                if ($xp[$idx]) {
                    $this->assertPatternMatch('/^[0-9].*-[0-9].*/', $xp[$idx]);
                }
            }

            if ($ids) {
                $id = array_shift($ids);
                $this->redis->xAck('s', 'group', [$id]);
            }
        }

        /* Ensure we can have NULL trailing arguments */
        $this->assertTrue(is_array($this->redis->xpending('s', 'group', '-', '+', 1, null)));
        $this->assertTrue(is_array($this->redis->xpending('s', 'group', NULL, NULL, -1, NULL)));
    }

    public function testXDel() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        for ($n = 5; $n > 0; $n--) {
            $ids = $this->addStreamEntries('s', 5);
            $todel = array_slice($ids, 0, $n);
            $this->assertEquals(count($todel), $this->redis->xDel('s', $todel));
        }

        /* Empty array should fail */
        $this->assertFalse(@$this->redis->xDel('s', []));
    }

    public function testXTrim() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        for ($maxlen = 0; $maxlen <= 50; $maxlen += 10) {
            $this->addStreamEntries('stream', 100);
            $trimmed = $this->redis->xTrim('stream', $maxlen);
            $this->assertEquals(100 - $maxlen, $trimmed);
        }

        /* APPROX trimming isn't easily deterministic, so just make sure we
           can call it with the flag */
        $this->addStreamEntries('stream', 100);
        $this->assertEquals(0, $this->redis->xTrim('stream', 1, true));

        /* We need Redis >= 6.2.0 for MINID and LIMIT options */
        if ( ! $this->minVersionCheck('6.2.0'))
            return;

        $this->assertEquals(1, $this->redis->del('stream'));

        /* Test minid by generating a stream with more than one */
        for ($i = 1; $i < 3; $i++) {
            for ($j = 0; $j < 3; $j++) {
                $this->redis->xadd('stream', "$i-$j", ['foo' => 'bar']);
            }
        }

        /* MINID of 2-0 */
        $this->assertEquals(3, $this->redis->xtrim('stream', 2, false, true));
        $this->assertEquals(['2-0', '2-1', '2-2'], array_keys($this->redis->xrange('stream', '0', '+')));

        /* TODO:  Figure oiut how to test LIMIT deterministically.  For now just
                  send a LIMIT and verify we don't get a failure from Redis. */
        $this->assertIsInt(@$this->redis->xtrim('stream', 2, false, false, 3));
    }

    /* XCLAIM is one of the most complicated commands, with a great deal of different options
     * The following test attempts to verify every combination of every possible option. */
    public function testXClaim() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        foreach ([0, 100] as $min_idle_time) {
            foreach ([false, true] as $justid) {
                foreach ([0, 10] as $retrycount) {
                    /* We need to test not passing TIME/IDLE as well as passing either */
                    if ($min_idle_time == 0) {
                        $topts = [[], ['IDLE', 1000000], ['TIME', time() * 1000]];
                    } else {
                        $topts = [NULL];
                    }

                    foreach ($topts as $tinfo) {
                        if ($tinfo) {
                            list($ttype, $tvalue) = $tinfo;
                        } else {
                            $ttype = NULL; $tvalue = NULL;
                        }

                        /* Add some messages and create a group */
                        $this->addStreamsAndGroups(['s'], 10, ['group1' => 0]);

                        /* Create a second stream we can FORCE ownership on */
                        $fids = $this->addStreamsAndGroups(['f'], 10, ['group1' => 0]);
                        $fids = $fids['f'];

                        /* Have consumer 'Mike' read the messages */
                        $oids = $this->redis->xReadGroup('group1', 'Mike', ['s' => '>']);
                        $oids = array_keys($oids['s']); /* We're only dealing with stream 's' */

                        /* Construct our options array */
                        $opts = [];
                        if ($justid) $opts[] = 'JUSTID';
                        if ($retrycount) $opts['RETRYCOUNT'] = $retrycount;
                        if ($tvalue !== NULL) $opts[$ttype] = $tvalue;

                        /* Now have pavlo XCLAIM them */
                        $cids = $this->redis->xClaim('s', 'group1', 'Pavlo', $min_idle_time, $oids, $opts);
                        if ( ! $justid) $cids = array_keys($cids);

                        if ($min_idle_time == 0) {
                            $this->assertEquals($cids, $oids);

                            /* Append the FORCE option to our second stream where we have not already
                             * assigned to a PEL group */
                            $opts[] = 'FORCE';
                            $freturn = $this->redis->xClaim('f', 'group1', 'Test', 0, $fids, $opts);
                            if ( ! $justid) $freturn = array_keys($freturn);
                            $this->assertEquals($freturn, $fids);

                            if ($retrycount || $tvalue !== NULL) {
                                $pending = $this->redis->xPending('s', 'group1', 0, '+', 1, 'Pavlo');

                                if ($retrycount) {
                                    $this->assertEquals($pending[0][3], $retrycount);
                                }
                                if ($tvalue !== NULL) {
                                    if ($ttype == 'IDLE') {
                                        /* If testing IDLE the value must be >= what we set */
                                        $this->assertGTE($tvalue, $pending[0][2]);
                                    } else {
                                        /* Timing tests are notoriously irritating but I don't see
                                         * how we'll get >= 20,000 ms between XCLAIM and XPENDING no
                                         * matter how slow the machine/VM running the tests is */
                                        $this->assertLT(20000, $pending[0][2]);
                                    }
                                }
                            }
                        } else {
                            /* We're verifying that we get no messages when we've set 100 seconds
                             * as our idle time, which should match nothing */
                            $this->assertEquals([], $cids);
                        }
                    }
                }
            }
        }
    }

    /* Make sure our XAUTOCLAIM handler works */
    public function testXAutoClaim() {
        $this->redis->del('ships');
        $this->redis->xGroup('CREATE', 'ships', 'combatants', '0-0', true);

        // Test an empty xautoclaim reply
        $res = $this->redis->xAutoClaim('ships', 'combatants', 'Sisko', 0, '0-0');
        $this->assertTrue(is_array($res) && (count($res) == 2 || count($res) == 3));
        if (count($res) == 2) {
            $this->assertEquals(['0-0', []], $res);
        } else {
            $this->assertEquals(['0-0', [], []], $res);
        }

        $this->redis->xAdd('ships', '1424-74205', ['name' => 'Defiant']);

        // Consume the ['name' => 'Defiant'] message
        $this->redis->xReadGroup('combatants', "Jem'Hadar", ['ships' => '>'], 1);

        // The "Jem'Hadar" consumer has the message presently
        $pending = $this->redis->xPending('ships', 'combatants');
        $this->assertTrue($pending && isset($pending[3][0][0]) && $pending[3][0][0] == "Jem'Hadar");

        // Assume control of the pending message with a different consumer.
        $res = $this->redis->xAutoClaim('ships', 'combatants', 'Sisko', 0, '0-0');

        $this->assertTrue($res && (count($res) == 2 || count($res) == 3));
        $this->assertTrue(isset($res[1]['1424-74205']['name']) &&
                          $res[1]['1424-74205']['name'] == 'Defiant');

        // Now the 'Sisko' consumer should own the message
        $pending = $this->redis->xPending('ships', 'combatants');
        $this->assertTrue(isset($pending[3][0][0]) && $pending[3][0][0] == 'Sisko');
    }

    public function testXInfo() {
        if ( ! $this->minVersionCheck('5.0'))
            $this->markTestSkipped();

        /* Create some streams and groups */
        $stream = 's';
        $groups = ['g1' => 0, 'g2' => 0];
        $this->addStreamsAndGroups([$stream], 1, $groups);

        $info = $this->redis->xInfo('GROUPS', $stream);
        $this->assertIsArray($info);
        $this->assertEquals(count($info), count($groups));
        foreach ($info as $group) {
            $this->assertArrayKey($group, 'name');
            $this->assertArrayKey($groups, $group['name']);
        }

        $info = $this->redis->xInfo('STREAM', $stream);
        $this->assertIsArray($info);
        $this->assertArrayKey($info, 'groups', function ($v) use ($groups) {
            return count($groups) == $v;
        });

        foreach (['first-entry', 'last-entry'] as $key) {
            $this->assertArrayKey($info, $key, 'is_array');
        }

        /* Ensure that default/NULL arguments are ignored */
        $info = $this->redis->xInfo('STREAM', $stream, NULL);
        $this->assertIsArray($info);
        $info = $this->redis->xInfo('STREAM', $stream, NULL, -1);
        $this->assertIsArray($info);

        /* XINFO STREAM FULL [COUNT N] Requires >= 6.0.0 */
        if ( ! $this->minVersionCheck('6.0'))
            return;

        /* Add some items to the stream so we can test COUNT */
        for ($i = 0; $i < 5; $i++) {
            $this->redis->xAdd($stream, '*', ['foo' => 'bar']);
        }

        $info = $this->redis->xInfo('STREAM', $stream, 'full');
        $this->assertArrayKey($info, 'length', 'is_numeric');

        for ($count = 1; $count < 5; $count++) {
            $info = $this->redis->xInfo('STREAM', $stream, 'full', $count);
            $n = isset($info['entries']) ? count($info['entries']) : 0;
            $this->assertEquals($n, $count);
        }

        /* Count <= 0 should be ignored */
        foreach ([-1, 0] as $count) {
            $info = $this->redis->xInfo('STREAM', $stream, 'full', 0);
            $n = isset($info['entries']) ? count($info['entries']) : 0;
            $this->assertEquals($n, $this->redis->xLen($stream));
        }

        /* Make sure we can't erroneously send non-null args after null ones */
        $this->redis->clearLastError();
        $this->assertFalse(@$this->redis->xInfo('FOO', NULL, 'fail', 25));
        $this->assertNull($this->redis->getLastError());
        $this->assertFalse(@$this->redis->xInfo('FOO', NULL, NULL, -2));
        $this->assertNull($this->redis->getLastError());
    }

    /* Regression test for issue-1831 (XINFO STREAM on an empty stream) */
    public function testXInfoEmptyStream() {
        /* Configure an empty stream */
        $this->redis->del('s');
        $this->redis->xAdd('s', '*', ['foo' => 'bar']);
        $this->redis->xTrim('s', 0);

        $info = $this->redis->xInfo('STREAM', 's');

        $this->assertIsArray($info);
        $this->assertEquals(0, $info['length']);
        $this->assertNull($info['first-entry']);
        $this->assertNull($info['last-entry']);
    }

    public function testVAdd() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));

        foreach ([1, 0] as $expected) {
            $this->assertEquals($expected, $this->redis->vadd('v', [0.5, 1.0], 'e'));
        }

        $this->assertEquals(1, $this->redis->del('v'));

        foreach ([1, 0] as $expected) {
            /* With tons of options */
            $res = $this->redis->vadd('v', [3.14, 2.71], 'e', [
                'REDUCE' => 2, 'EF' => 16, 'M' => 28, 'CAS', 'VALUES', 'Q8',
                'SETATTR' => ['foo' => 'bar'],
            ]);
            $this->assertEquals($expected, $res);
        }
    }

    public function testVSim() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $captains = [
            'Archer'  => [[0.7628, 0.5403, 0.0729], 'Enterprise-NX01'],
            'Janeway' => [[0.7073, 0.2171, 0.2673], 'Voyager'],
            'Kirk'    => [[0.2555, 0.4938, 0.6968], 'Enterprise'],
            'Picard'  => [[0.0570, 0.3547, 0.0721], 'Enterprise-D'],
            'Pike'    => [[0.7916, 0.8514, 0.2733], 'Enterprise'],
            'Sisko'   => [[0.0697, 0.1455, 0.2886], 'Defiant'],
        ];

        $this->redis->del('captains');

        foreach ($captains as $captain => [$vector, $ship]) {
            $opt = ['SETATTR' => ['ship' => $ship]];
            $res = $this->redis->vadd('captains', $vector, $captain, $opt);
            $this->assertEquals(1, $res);
        }

        /* We should infer ELE mode */
        $res = $this->redis->vSim('captains', 'Archer');
        $this->assertIsArray($res);
        $this->assertEquals($res[0], 'Archer');

        /* We should infer FP32 mode */
        $res = $this->redis->vsim('captains', $captains['Archer'][0]);
        $this->assertIsArray($res);
        $this->assertEquals($res[0], 'Archer');

        /* Reject FP32/VALUE mode with non-arrays */
        foreach (['Archer', 3.14, 42, new stdClass] as $e) {
            foreach (['FP32', 'VALUES'] as $mode) {
                $res = @$this->redis->vsim('captains', $e, [$mode]);
                $this->assertFalse($res);
            }
        }

        /* VALUES */
        $opt = ['VALUES'];
        $res = $this->redis->vsim('captains', $captains['Kirk'][0], $opt);
        $this->assertIsArray($res);
        $this->assertEquals($res[0], 'Kirk');

        /* EF */
        $opt = ['EF' => 24];
        $res = $this->redis->vsim('captains', $captains['Pike'][0], $opt);
        $this->assertIsArray($res);
        $this->assertEquals($res[0], 'Pike');

        /* FILTER + FILTER-EF */
        $opt = ['FILTER' => '.ship == "Defiant"', 'FILTER-EF' => 24];
        $res = $this->redis->vsim('captains', 'Archer', $opt);
        $this->assertIsArray($res);
        $this->assertEquals($res[0], 'Sisko');

        /* COUNT */
        $opt = ['COUNT' => 1];
        $res = $this->redis->vsim('captains', 'Sisko', $opt);
        $this->assertIsArray($res);
        $this->assertEquals($res[0], 'Sisko');
        $this->assertEquals(1, count($res));

        /* WITHSCORES */
        $opt = ['WITHSCORES'];
        $res = $this->redis->vsim('captains', 'Janeway', $opt);
        $this->assertIsArray($res);
        $this->assertGT(1, count($res));

        foreach ($res as $captain => $score) {
            $this->assertIsString($captain);
            $this->assertIsFloat($score);
        }

        /* NOTHREAD + TRUTH */
        $opt = ['NOTHREAD', 'TRUTH'];
        $res = $this->redis->vsim('captains', 'Picard', $opt);
        $this->assertEquals($res[0], 'Picard');
    }

    public function testVCard() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));

        for ($i = 0; $i < 5; $i++) {
            $this->assertEquals(1, $this->redis->vadd('v', [0.5, 1.0], "e{$i}"));
            $this->assertEquals($i + 1, $this->redis->vcard('v'));
        }

        $this->assertEquals(1, $this->redis->del('v'));
        $this->assertEquals(0, $this->redis->vcard('v'));
    }

    public function testVDim() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        foreach ([[0.5, 1.0], [0.5, 1.0, 1.5]] as $v) {
            $this->assertIsInt($this->redis->del('v'));
            $this->assertEquals(1, $this->redis->vadd('v', $v, 'e'));
            $this->assertEquals(count($v), $this->redis->vdim('v'));
        }

        $this->assertEquals(1, $this->redis->del('v'));
    }

    public function testVInfo() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));
        $this->assertEquals(1, $this->redis->vadd('v', [0.5, 1.0, 1.5], 'e'));

        $res = $this->redis->vinfo('v');
        $this->assertIsArray($res);
        $this->assertArrayKey($res, 'vector-dim', function ($v) {
            return $v === 3;
        });

        $this->assertEquals(1, $this->redis->del('v'));
    }

    public function testVIsMember() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));
        $this->assertEquals(1, $this->redis->vadd('v', [0.5, 1.0], 'exists'));

        $this->assertEquals(true, $this->redis->vismember('v', 'exists'));
        $this->assertEquals(false, $this->redis->vismember('v', 'doesnotexist'));
    }

    public function testVEmb() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));

        $this->assertEquals(1, $this->redis->vadd('v', [0.5, 1.0], 'e'));

        $res = $this->redis->vemb('v', 'e');
        $this->assertIsArray($res);
        $this->assertTrue(filter_var($res[0], FILTER_VALIDATE_FLOAT) !== false);

        $res = $this->redis->vemb('v', 'e', true);
        $this->assertIsArray($res);
        $this->assertEquals('int8', $res[0]);

        $this->assertEquals(1, $this->redis->del('v'));
    }

    public function testVRem() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));

        $captains = ['Sisko', 'Janeway'];
        foreach ($captains as $captain) {
            $v = [mt_rand() / mt_getrandmax(), mt_rand() / mt_getrandmax()];
            $this->assertEquals(1, $this->redis->vadd('v', $v, $captain));
        }

        while ($captains) {
            $captain = array_shift($captains);
            foreach ([1, 0] as $e) {
                $this->assertEquals($e, $this->redis->vrem('v', $captain));
            }

            $this->assertEquals(count($captains), $this->redis->vcard('v'));
        }

        $this->assertEquals(0, $this->redis->vcard('v'));
    }

    public function testVGetAttr() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $attr = ['foo' => 'bar'];
        $json = json_encode($attr);

        $this->assertIsInt($this->redis->del('v'));
        $this->assertEquals(1, $this->redis->vadd(
            'v', [1.5, 2.5], 'e1', ['SETATTR' => $attr])
        );

        $this->assertEquals(1, $this->redis->vadd('v', [1.5, 2.5], 'e2'));

        if (defined('Redis::SERIALIZER_JSON')) {
            $expected = [false => $attr, true => $json];
        } else {
            $expected = [false => $json, true => $json];
        }

        foreach ($expected as $raw => $e) {
            $this->assertEquals($e, $this->redis->vgetattr('v', 'e1', $raw));
        }

        foreach ([[], [false], [true]] as $arg) {
            $this->assertEquals(false, $this->redis->vgetattr('v', 'e2', ...$arg));
        }
    }

    public function testVSetAttr() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));
        $this->assertEquals(1, $this->redis->vadd('v', [1.5, 2.5], 'e'));

        $attr = ['foo' => 'bar'];

        $this->assertEquals(1, $this->redis->vsetattr('v', 'e', json_encode($attr)));

        if (defined('Redis::SERIALIZER_JSON')) {
            $this->assertEquals(1, $this->redis->vsetattr('v', 'e', $attr));
        }
    }

    public function testVRandMember() {
        if ( ! $this->minVersionCheck('8.0'))
            $this->markTestSkipped();

        $ships = ['Enterprise', 'Defiant', 'Voyager'];

        $this->assertIsInt($this->redis->del('v'));
        foreach ($ships as $ship) {
            $this->assertEquals(1, $this->redis->vadd('v', [0.5, 1.0], $ship));
        }

        $this->assertInArray($this->redis->vrandmember('v'), $ships);
        $this->assertEqualsCanonicalizing(
            $ships,
            $this->redis->vrandmember('v', 2 * count($ships))
        );

        $this->assertEquals(
            2 * count($ships),
            count($this->redis->vrandmember('v', -2 * count($ships)))
        );
    }

    public function testVRange() {
        if ($this->haveCommand('VRANGE') === false)
            $this->markTestSkipped();

        $this->assertIsInt($this->redis->del('v'));

        for ($i = 0; $i < 10; $i++) {
            $this->assertEquals(1, $this->redis->vadd('v', [$i / 10, $i / 10], "e{$i}"));
        }

        $res = $this->redis->vrange('v', '-', '+');
        $this->assertIsArray($res);
        $this->assertEquals(10, count($res));

        $res = $this->redis->vrange('v', '-', '+', 3);
        $this->assertIsArray($res);
        $this->assertEquals(3, count($res));
    }

    public function testInvalidAuthArgs() {
        $client = $this->newInstance();

        $args = [
            [],
            [NULL, NULL],
            ['foo', 'bar', 'baz'],
            ['a', 'b', 'c', 'd'],
            ['a', 'b', 'c'],
            [['a', 'b'], 'a'],
            [['a', 'b', 'c']],
            [[NULL, 'pass']],
            [[NULL, NULL]],
        ];

        foreach ($args as $arg) {
            try {
                if (is_array($arg)) {
                    @call_user_func_array([$client, 'auth'], $arg);
                }
            } catch (Exception $ex) {
                unset($ex); /* Suppress intellisense warning */
            } catch (ArgumentCountError $ex) {
                unset($ex); /* Suppress intellisense warning */
            }
        }
    }

    public function testAcl() {
        if ( ! $this->minVersionCheck('6.0'))
            $this->markTestSkipped();

        /* ACL USERS/SETUSER */
        $this->assertTrue($this->redis->acl('SETUSER', 'admin', 'on', '>admin', '+@all'));
        $this->assertTrue($this->redis->acl('SETUSER', 'noperm', 'on', '>noperm', '-@all'));
        $this->assertInArray('default', $this->redis->acl('USERS'));

        /* Verify ACL GETUSER has the correct hash and is in 'nice' format */
        $admin = $this->redis->acl('GETUSER', 'admin');
        $this->assertInArray(hash('sha256', 'admin'), $admin['passwords']);

        /* Now nuke our 'admin' user and make sure it went away */
        $this->assertEquals(1, $this->redis->acl('DELUSER', 'admin'));
        $this->assertFalse(in_array('admin', $this->redis->acl('USERS')));

        /* Try to log in with a bad username/password */
        $this->assertThrowsMatch($this->redis,
            function($o) { $o->auth(['1337haxx00r', 'lolwut']); }, '/^WRONGPASS.*$/');

        /* We attempted a bad login.  We should have an ACL log entry */
        $log = $this->redis->acl('log');
        if ( !  $log || !is_array($log)) {
            $this->assert('Expected an array from ACL LOG, got: ' . var_export($log, true));
            return;
        }

        /* Make sure our ACL LOG entries are nice for the user */
        $entry = array_shift($log);
        $this->assertArrayKey($entry, 'age-seconds', 'is_numeric');
        $this->assertArrayKey($entry, 'count', 'is_int');

        /* ACL CAT */
        $cats = $this->redis->acl('CAT');
        foreach (['read', 'write', 'slow'] as $cat) {
            $this->assertInArray($cat, $cats);
        }

        /* ACL CAT <string> */
        $cats = $this->redis->acl('CAT', 'string');
        foreach (['get', 'set', 'setnx'] as $cat) {
            $this->assertInArray($cat, $cats);
        }

        /* ctype_xdigit even if PHP doesn't have it */
        $ctype_xdigit = function($v) {
            if (function_exists('ctype_xdigit')) {
                return ctype_xdigit($v);
            } else {
                return strspn(strtoupper($v), '0123456789ABCDEF') == strlen($v);
            }
        };

        /* ACL GENPASS/ACL GENPASS <bits> */
        $this->assertValidate($this->redis->acl('GENPASS'), $ctype_xdigit);
        $this->assertValidate($this->redis->acl('GENPASS', 1024), $ctype_xdigit);

        /* ACL WHOAMI */
        $this->assertValidate($this->redis->acl('WHOAMI'), 'strlen');

        /* Finally make sure AUTH errors throw an exception */
        $r2 = $this->newInstance(true);

        /* Test NOPERM exception */
        $this->assertTrue($r2->auth(['noperm', 'noperm']));
        $this->assertThrowsMatch($r2, function($r) { $r->set('foo', 'bar'); }, '/^NOPERM.*$/');
    }

    /* If we detect a unix socket make sure we can connect to it in a variety of ways */
    public function testUnixSocket() {
        if ( ! file_exists('/tmp/redis.sock'))
            $this->markTestSkipped();

        $sock_tests = [
            ['/tmp/redis.sock'],
            ['/tmp/redis.sock', null],
            ['/tmp/redis.sock', 0],
            ['/tmp/redis.sock', -1],
        ];

        try {
            foreach ($sock_tests as $args) {
                $redis = new Redis();

                if (count($args) == 2) {
                    @$redis->connect($args[0], $args[1]);
                } else {
                    @$redis->connect($args[0]);
                }
                if ($this->getAuth()) {
                    $this->assertTrue($redis->auth($this->getAuth()));
                }
                $this->assertTrue($redis->ping());
            }
        } catch (Exception $ex) {
            $this->assert("Exception: {$ex}");
        }
    }

    protected function detectRedis($host, $port) {
        $sock = @fsockopen($host, $port, $errno, $errstr, .1);
        if ( !  $sock)
            return false;

        stream_set_timeout($sock, 0, 100000);

        $ping_cmd = "*1\r\n$4\r\nPING\r\n";
        if (fwrite($sock, $ping_cmd) != strlen($ping_cmd))
            return false;

        return fread($sock, strlen("+PONG\r\n")) == "+PONG\r\n";
    }

    /* Test high ports if we detect Redis running there */
    public function testHighPorts() {
        $ports = array_filter(array_map(function ($port) {
            return $this->detectRedis('localhost', $port) ? $port : 0;
        }, [32768, 32769, 32770]));

        if ( ! $ports)
            $this->markTestSkipped();

        foreach ($ports as $port) {
            $redis = new Redis();
            try {
                @$redis->connect('localhost', $port);
                if ($this->getAuth()) {
                    $this->assertTrue($redis->auth($this->getAuth()));
                }
                $this->assertTrue($redis->ping());
            } catch(Exception $ex) {
                $this->assert("Exception: $ex");
            }
        }
    }

    protected function sessionRunner() {
        $this->getAuthParts($user, $pass);

        return (new SessionHelpers\Runner())
            ->prefix($this->sessionPrefix())
            ->handler($this->sessionSaveHandler())
            ->savePath($this->sessionSavePath());
    }

    protected function testRequiresMode(string $mode) {
        if (php_sapi_name() != $mode) {
            $this->markTestSkipped("Test requires PHP running in '$mode' mode");
        }
    }

    public function testSession_compression() {
        $this->testRequiresMode('cli');

        foreach ($this->getCompressors() as $name => $val) {
            $data = "testing_compression_$name";

            $runner = $this->sessionRunner()
                ->maxExecutionTime(300)
                ->lockingEnabled(true)
                ->lockWaitTime(-1)
                ->lockExpires(0)
                ->data($data)
                ->compression($name);

            $this->assertEquals('SUCCESS', $runner->execFg());

            $this->redis->setOption(Redis::OPT_COMPRESSION, $val);
            $this->assertPatternMatch("/.*$data.*/", $this->redis->get($runner->getSessionKey()));
            $this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);
        }
    }

    public function testSession_savedToRedis() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner();

        $this->assertEquals('SUCCESS', $runner->execFg());
        $this->assertKeyExists($runner->getSessionKey());
    }

    protected function sessionWaitUsec() {
        return ini_get('redis.session.lock_wait_time') *
               ini_get('redis.session.lock_retries');
    }

    protected function sessionWaitSec() {
        return $this->sessionWaitUsec() / 1000000.0;
    }

    public function testSession_lockKeyCorrect() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()->sleep(5);

        $this->assertTrue($runner->execBg());

        if ( ! $runner->waitForLockKey($this->redis, $this->sessionWaitSec())) {
            $this->externalCmdFailure($runner->getCmd(), $runner->output(),
                                 "Failed waiting for session lock key '{$runner->getSessionLockKey()}'",
                                 $runner->getExitCode());
        }
    }

    public function testSession_lockingDisabledByDefault() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()
            ->lockingEnabled(false)
            ->sleep(5);

        $this->assertEquals('SUCCESS', $runner->execFg());
        $this->assertKeyMissing($runner->getSessionLockKey());
    }

    public function testSession_lockReleasedOnClose() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()
            ->sleep(1)
            ->lockingEnabled(true);

        $this->assertTrue($runner->execBg());
        usleep($this->sessionWaitUsec() + 100000);
        $this->assertKeyMissing($runner->getSessionLockKey());
    }

    public function testSession_lock_ttlMaxExecutionTime() {
        $this->testRequiresMode('cli');

        $runner1 = $this->sessionRunner()
            ->sleep(10)
            ->maxExecutionTime(2);

        $this->assertTrue($runner1->execBg());
        usleep(100000);

        $runner2 = $this->sessionRunner()
            ->id($runner1->getId())
            ->sleep(0);

        $st = microtime(true);
        $this->assertEquals('SUCCESS', $runner2->execFg());
        $el = microtime(true) - $st;
        $this->assertLT(4, $el);
    }

    public function testSession_lock_ttlLockExpire() {
        $this->testRequiresMode('cli');

        $runner1 = $this->sessionRunner()
            ->sleep(10)
            ->maxExecutionTime(300)
            ->lockingEnabled(true)
            ->lockExpires(2);

        $this->assertTrue($runner1->execBg());
        usleep(100000);

        $runner2 = $this->sessionRunner()
            ->id($runner1->getId())
            ->sleep(0);

        $st = microtime(true);
        $this->assertEquals('SUCCESS', $runner2->execFg());
        $this->assertLT(3, microtime(true) - $st);
    }

    public function testSession_lockHoldCheckBeforeWrite_otherProcessHasLock() {
        $this->testRequiresMode('cli');

        $id = 'test-id';

        $runner = $this->sessionRunner()
            ->sleep(2)
            ->lockingEnabled(true)
            ->lockExpires(1)
            ->data('firstProcess');

        $runner2 = $this->sessionRunner()
            ->id($runner->getId())
            ->sleep(0)
            ->lockingEnabled(true)
            ->lockExpires(10)
            ->data('secondProcess');

        $this->assertTrue($runner->execBg());
        usleep(1500000); // 1.5 sec
        $this->assertEquals('SUCCESS', $runner2->execFg());

        $this->assertEquals('secondProcess', $runner->getData());
    }

    public function testSession_lockHoldCheckBeforeWrite_nobodyHasLock() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()
            ->sleep(2)
            ->lockingEnabled(true)
            ->lockExpires(1)
            ->data('firstProcess');

        $this->assertNotEquals('SUCCESS', $runner->execFg());
        $this->assertNotEquals('firstProcess', $runner->getData());
    }

    public function testSession_correctLockRetryCount() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()
            ->sleep(10);

        $this->assertTrue($runner->execBg());
        if ( ! $runner->waitForLockKey($this->redis, 2)) {
            $this->externalCmdFailure($runner->getCmd(), $runner->output(),
                                      'Failed waiting for session lock key',
                                      $runner->getExitCode());
        }

        $runner2 = $this->sessionRunner()
            ->id($runner->getId())
            ->sleep(0)
            ->maxExecutionTime(10)
            ->lockingEnabled(true)
            ->lockWaitTime(100000)
            ->lockRetries(10);

        $st = microtime(true);
        $ex = $runner2->execFg();
        if (stripos($ex, 'SUCCESS') !== false) {
            $this->externalCmdFailure($runner2->getCmd(), $ex,
                                      'Expected failure but lock was acquired!',
                                      $runner2->getExitCode());
        }
        $et = microtime(true);

        $this->assertBetween($et - $st, 1, 3);
    }

    public function testSession_defaultLockRetryCount() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()
            ->sleep(10);

        $runner2 = $this->sessionRunner()
            ->id($runner->getId())
            ->sleep(0)
            ->lockingEnabled(true)
            ->maxExecutionTime(10)
            ->lockWaitTime(20000)
            ->lockRetries(0);

        $this->assertTrue($runner->execBg());

        if ( ! $runner->waitForLockKey($this->redis, 3)) {
            $this->externalCmdFailure($runner->getCmd(), $runner->output(),
                                      'Failed waiting for session lock key',
                                      $runner->getExitCode());
        }

        $st = microtime(true);
        $this->assertNotEquals('SUCCESS', $runner2->execFg());
        $et = microtime(true);
        $this->assertBetween($et - $st, 2, 3);
    }

    public function testSession_noUnlockOfOtherProcess() {
        $this->testRequiresMode('cli');

        $st = microtime(true);

        $sleep = 3;

        $runner = $this->sessionRunner()
            ->sleep($sleep)
            ->maxExecutionTime(3);

        $tm1 = microtime(true);

        /* 1.  Start a background process, and wait until we are certain
         *     the lock was attained. */
        $this->assertTrue($runner->execBg());
        if ( ! $runner->waitForLockKey($this->redis, 1)) {
            $this->assert('Failed waiting for session lock key');
            return;
        }

        /* 2.  Attempt to lock the same session.  This should force us to
         *     wait until the first lock is released. */
        $runner2 = $this->sessionRunner()
            ->id($runner->getId())
            ->sleep(0);

        $tm2 = microtime(true);
        $this->assertEquals('SUCCESS', $runner2->execFg());
        $tm3 = microtime(true);

        /* 3. Verify we had to wait for this lock */
        $this->assertGTE($sleep - ($tm2 - $tm1), $tm3 - $tm2);
    }

    public function testSession_lockWaitTime() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()
            ->sleep(1)
            ->maxExecutionTime(300);

        $runner2 = $this->sessionRunner()
            ->id($runner->getId())
            ->sleep(0)
            ->maxExecutionTime(300)
            ->lockingEnabled(true)
            ->lockWaitTime(3000000);

        $this->assertTrue($runner->execBg());
        usleep(100000);

        $st = microtime(true);
        $this->assertEquals('SUCCESS', $runner2->execFg());
        $et = microtime(true);

        $this->assertBetween($et - $st, 2.5, 3.5);
    }

    public function testMultipleConnect() {
        $host = $this->redis->GetHost();
        $port = $this->redis->GetPort();

        for ($i = 0; $i < 5; $i++) {
            $this->redis->connect($host, $port);
            if ($this->getAuth()) {
                $this->assertTrue($this->redis->auth($this->getAuth()));
            }
            $this->assertTrue($this->redis->ping());
        }
    }

    public function testConnectDatabaseSelect() {
        $options = [
            'host' => $this->getHost(),
            'port' => $this->getPort(),
            'database' => 2,
        ];

        if ($this->getAuth()) {
            $options['auth'] = $this->getAuth();
        }

        $redis = new Redis($options);
        $this->assertEquals(2, $redis->getDBNum());
        $this->assertEquals(2, $redis->client('info')['db']);

        $this->assertTrue($redis->select(1));

        $this->assertEquals(1, $redis->getDBNum());
        $this->assertEquals(1, $redis->client('info')['db']);
    }

    public function testConnectException() {
        $host = 'github.com';
        if (gethostbyname($host) === $host)
            $this->markTestSkipped('online test');

        $redis = new Redis();
        try {
            $redis->connect($host, 6379, 0.01);
        }  catch (Exception $e) {
            $this->assertStringContains('timed out', $e->getMessage());
        }
    }

    public function testTlsConnect() {
        if (($fp = @fsockopen($this->getHost(), 6378)) == NULL)
            $this->markTestSkipped();

        fclose($fp);

        foreach (['localhost' => true, '127.0.0.1' => false] as $host => $verify) {
            $redis = new Redis();
            $this->assertTrue($redis->connect('tls://' . $host, 6378, 0, null, 0, 0, [
                'stream' => ['verify_peer_name' => $verify, 'verify_peer' => false]
            ]));
        }
    }

    public function testReset() {
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->assertTrue($this->redis->multi()->select(2)->set('foo', 'bar')->reset());
        $this->assertEquals(Redis::ATOMIC, $this->redis->getMode());
        $this->assertEquals(0, $this->redis->getDBNum());
    }

    public function testCopy() {
        if (version_compare($this->version, '6.2.0') < 0)
            $this->markTestSkipped();

        $this->redis->del('{key}dst');
        $this->redis->set('{key}src', 'foo');
        $this->assertTrue($this->redis->copy('{key}src', '{key}dst'));
        $this->assertKeyEquals('foo', '{key}dst');

        $this->redis->set('{key}src', 'bar');
        $this->assertFalse($this->redis->copy('{key}src', '{key}dst'));
        $this->assertKeyEquals('foo', '{key}dst');

        $this->assertTrue($this->redis->copy('{key}src', '{key}dst', ['replace' => true]));
        $this->assertKeyEquals('bar', '{key}dst');
    }

    public function testCommand() {
        $commands = $this->redis->command();
        $this->assertIsArray($commands);
        $this->assertEquals(count($commands), $this->redis->command('count'));

        if ( ! $this->is_keydb && $this->minVersionCheck('7.0')) {
            $infos = $this->redis->command('info');
            $this->assertIsArray($infos);
            $this->assertEquals(count($infos), count($commands));
        }

        if (version_compare($this->version, '7.0') >= 0) {
            $docs = $this->redis->command('docs');
            $this->assertIsArray($docs);
            $this->assertEquals(count($docs), 2 * count($commands));

            $list = $this->redis->command('list', 'filterby', 'pattern', 'lol*');
            $this->assertIsArray($list);
            $this->assertEquals(['lolwut'], $list);
        }
    }

    public function testFunction() {
        if (version_compare($this->version, '7.0') < 0)
            $this->markTestSkipped();

        $this->assertTrue($this->redis->function('flush', 'sync'));
        $this->assertEquals('mylib', $this->redis->function('load', "#!lua name=mylib\nredis.register_function('myfunc', function(keys, args) return args[1] end)"));
        $this->assertEquals('foo', $this->redis->fcall('myfunc', [], ['foo']));
        $payload = $this->redis->function('dump');
        $this->assertEquals('mylib', $this->redis->function('load', 'replace', "#!lua name=mylib\nredis.register_function{function_name='myfunc', callback=function(keys, args) return args[1] end, flags={'no-writes'}}"));
        $this->assertEquals('foo', $this->redis->fcall_ro('myfunc', [], ['foo']));
        $this->assertEquals(['running_script' => false, 'engines' => ['LUA' => ['libraries_count' => 1, 'functions_count' => 1]]], $this->redis->function('stats'));
        $this->assertTrue($this->redis->function('delete', 'mylib'));
        $this->assertTrue($this->redis->function('restore', $payload));
        $this->assertEquals([['library_name' => 'mylib', 'engine' => 'LUA', 'functions' => [['name' => 'myfunc', 'description' => false,'flags' => []]]]], $this->redis->function('list'));
        $this->assertTrue($this->redis->function('delete', 'mylib'));
    }

    protected function execWaitAOF() {
        return $this->redis->waitaof(0, 0, 0);
    }

    public function testWaitAOF() {
        if ( ! $this->minVersionCheck('7.2.0'))
            $this->markTestSkipped();

        $res = $this->execWaitAOF();
        $this->assertValidate($res, function ($v) {
            if ( ! is_array($v) || count($v) != 2)
                return false;
            return isset($v[0]) && is_int($v[0]) &&
                   isset($v[1]) && is_int($v[1]);
        });
    }

    /* Make sure we handle a bad option value gracefully */
    public function testBadOptionValue() {
        $this->assertFalse(@$this->redis->setOption(pow(2, 32), false));
    }

    protected function regenerateIdHelper(bool $lock, bool $destroy, bool $proxy) {
        $this->testRequiresMode('cli');

        $data   = uniqid('regenerate-id:');
        $runner = $this->sessionRunner()
            ->sleep(0)
            ->maxExecutionTime(300)
            ->lockingEnabled(true)
            ->lockRetries(1)
            ->data($data);

        $this->assertEquals('SUCCESS', $runner->execFg());

        $new_id = $runner->regenerateId($lock, $destroy, $proxy);

        $this->assertNotEquals($runner->getId(), $new_id);
        $this->assertEquals($runner->getData(), $runner->getData());
    }

    public  function testSession_regenerateSessionId_noLock_noDestroy() {
        $this->regenerateIdHelper(false, false, false);
    }

    public function testSession_regenerateSessionId_noLock_withDestroy() {
        $this->regenerateIdHelper(false, true, false);
    }

    public function testSession_regenerateSessionId_withLock_noDestroy() {
        $this->regenerateIdHelper(true, false, false);
    }

    public  function testSession_regenerateSessionId_withLock_withDestroy() {
        $this->regenerateIdHelper(true, true, false);
    }

    public  function testSession_regenerateSessionId_noLock_noDestroy_withProxy() {
        $this->regenerateIdHelper(false, false, true);
    }

    public  function testSession_regenerateSessionId_noLock_withDestroy_withProxy() {
        $this->regenerateIdHelper(false, true, true);
    }

    public  function testSession_regenerateSessionId_withLock_noDestroy_withProxy() {
        $this->regenerateIdHelper(true, false, true);
    }

    public  function testSession_regenerateSessionId_withLock_withDestroy_withProxy() {
        $this->regenerateIdHelper(true, true, true);
    }

    public function testSession_ttl_equalsToSessionLifetime() {
        $this->testRequiresMode('cli');

        $runner = $this->sessionRunner()->lifetime(600);
        $this->assertEquals('SUCCESS', $runner->execFg());
        $this->assertEquals(600, $this->redis->ttl($runner->getSessionKey()));
    }

    public function testSession_ttl_resetOnWrite() {
        $this->testRequiresMode('cli');

        $runner1 = $this->sessionRunner()->lifetime(600);
        $this->assertEquals('SUCCESS', $runner1->execFg());

        $runner2 = $this->sessionRunner()->id($runner1->getId())->lifetime(1800);
        $this->assertEquals('SUCCESS', $runner2->execFg());

        $this->assertEquals(1800, $this->redis->ttl($runner2->getSessionKey()));
    }

    public function testSession_ttl_resetOnRead() {
        $this->testRequiresMode('cli');

        $data = uniqid(__FUNCTION__);

        $runner = $this->sessionRunner()->lifetime(600)->data($data);
        $this->assertEquals('SUCCESS', $runner->execFg());
        $this->redis->expire($runner->getSessionKey(), 9999);

        $this->assertEquals($data, $runner->getData());
        $this->assertEquals(600, $this->redis->ttl($runner->getSessionKey()));
    }
}
?>
