text:
package.path = "../src/?.lua;src/?.lua;" .. package.path

pcall(require, "luarocks.require")

local unpack = _G.unpack or table.unpack

local tsc = require "telescope"
local redis = require "redis"

local settings = {
    host     = '127.0.0.1',
    port     = os.getenv("REDIS_PORT") or 6379,
    database = 14,
    password = nil,
}

function table.merge(self, tbl2)
    local new_table = {}
    for k,v in pairs(self) do new_table[k] = v end
    for k,v in pairs(tbl2) do new_table[k] = v end
    return new_table
end

function table.keys(self)
    local keys = {}
    for k, _ in pairs(self) do table.insert(keys, k) end
    return keys
end

function table.values(self)
    local values = {}
    for _, v in pairs(self) do table.insert(values, v) end
    return values
end

function table.contains(self, value)
    for _, v in pairs(self) do
        if v == value then return true end
    end
    return false
end

function table.slice(self, first, length)
    -- TODO: must be improved
    local new_table = {}
    for i = first, first + length - 1 do
        table.insert(new_table, self[i])
    end
    return new_table
end

function table.compare(self, other)
    -- NOTE: the body of this function was taken and slightly adapted from
    --       Penlight (http://github.com/stevedonovan/Penlight)
    if #self ~= #other then return false end
    local visited = {}
    for i = 1, #self do
        local val, gotcha = self[i], nil
        for j = 1, #other do
            if not visited[j] then
                if (type(val) == 'table') then
                    if (table.compare(val, other[j])) then
                        gotcha = j
                        break
                    end
                else
                    if val == other[j] then
                        gotcha = j
                        break
                    end
                end
            end
        end
        if not gotcha then return false end
        visited[gotcha] = true
    end
    return true
end

function parse_version(version_str)
    local major, minor, patch, status = version_str:match('^(%d+)%.(%d+)%.(%d+)%-?(%w-)$')

    local info = {
        string  = version_str,
        compare =  function(self, other)
            if type(other) == 'string' then
                other = parse_version(other)
            end
            if self.unrecognized or other.unrecognized then
                error('Cannot compare versions')
            end

            for _, part in ipairs({ 'major', 'minor', 'patch' }) do
                if self[part] < other[part] then
                    return -1
                end
                if self[part] > other[part] then
                    return 1
                end
            end

            return 0
        end,
        is = function(self, op, other)
            local comparation = self:compare(other);
            if op == '<' then return comparation < 0 end
            if op == '<=' then return comparation <= 0 end
            if op == '=' then return comparation == 0 end
            if op == '>=' then return comparation >= 0 end
            if op == '>' then return comparation > 0 end

            error('Invalid comparison operator: '..op)
        end,
    }

    if major and minor and patch then
        info.major  = tonumber(major)
        info.minor  = tonumber(minor)
        info.patch  = tonumber(patch)
        if status then
            info.status = status
        end
    else
        info.unrecognized = true
    end

    return info
end

local utils = {
    create_client = function(parameters)
        if parameters == nil then
            parameters = settings
        end

        local client = redis.connect(parameters.host, parameters.port)
        if parameters.password then client:auth(parameters.password) end
        if parameters.database then client:select(parameters.database) end
        client:flushdb()

        local info = client:info()
        local version = parse_version(info.redis_version or info.server.redis_version)

        if version:is('<', '1.2.0') then
            error("redis-lua does not support Redis < 1.2.0 (current: "..version.string..")")
        end

        return client, version
    end,
    rpush_return = function(client, key, values, wipe)
        if wipe then client:del(key) end
        for _, v in ipairs(values) do
            client:rpush(key, v)
        end
        return values
    end,
    sadd_return = function(client, key, values, wipe)
        if wipe then client:del(key) end
        for _, v in ipairs(values) do
            client:sadd(key, v)
        end
        return values
    end,
    zadd_return = function(client, key, values, wipe)
        if wipe then client:del(key) end
        for k, v in pairs(values) do
            client:zadd(key, v, k)
        end
        return values
    end,
    sleep = function(sec)
        local select = require('socket').select
        select(nil, nil, sec)
    end,
}

local shared = {
    kvs_table = function()
        return {
            foo    = 'bar',
            hoge   = 'piyo',
            foofoo = 'barbar',
        }
    end,
    kvs_ns_table = function()
        return {
            ['metavars:foo']    = 'bar',
            ['metavars:hoge']   = 'piyo',
            ['metavars:foofoo'] = 'barbar',
        }
    end,
    lang_table = function()
        return {
            italian  = "ciao",
            english  = "hello",
            japanese = "こんいちは!",
        }
    end,
    numbers = function()
        return { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }
    end,
    zset_sample = function()
        return { a = -10, b = 0, c = 10, d = 20, e = 20, f = 30 }
    end,
}

tsc.make_assertion("table_values", "'%s' to have the same values as '%s'", table.compare)
tsc.make_assertion("response_queued", "to be queued", function(response)
    if type(response) == 'table' and response.queued == true then
        return true
    else
        return false
    end
end)
tsc.make_assertion("error_message", "result to be an error with the expected message", function(msg, f)
    local ok, err = pcall(f)
    return not ok and err:match(msg)
end)

-- ------------------------------------------------------------------------- --

context("Client initialization", function()
    test("Can connect successfully", function()
        local client = redis.connect(settings.host, settings.port)
        assert_type(client, 'table')
        assert_true(table.contains(table.keys(client.network), 'socket'))

        client.network.socket:send("PING\r\n")
        assert_equal(client.network.socket:receive('*l'), '+PONG')
    end)

    test("Can handle connection failures", function()
        assert_error_message("could not connect to .*:%d+ %[connection refused%]", function()
            redis.connect(settings.host, settings.port + 100)
        end)
    end)

    test("Accepts an URI for connection parameters", function()
        local uri = 'redis://'..settings.host..':'..settings.port
        local client = redis.connect(uri)
        assert_type(client, 'table')
    end)

    test("Accepts a table for connection parameters", function()
        local client = redis.connect(settings)
        assert_type(client, 'table')
    end)

    test("Can use an already connected socket", function()
        local connection = require('socket').tcp()
        connection:connect(settings.host, settings.port)

        local client = redis.connect({ socket = connection })
        assert_type(client, 'table')
        assert_true(client:ping())
    end)

    test("Can specify a timeout for connecting", function()
        local time, timeout = os.time(), 2;

        assert_error_message("could not connect to .*:%d+ %[timeout%]", function()
            redis.connect({ host = '169.254.255.255', timeout = timeout })
        end)

        assert_equal(time + timeout, os.time())
    end)
end)

context("Client features", function()
    before(function()
        client = utils.create_client(settings)
    end)

    test("Send raw commands", function()
        assert_equal(client:raw_command({ 'PING' }), 'PONG')
        assert_true(client:raw_command({ 'SET', 'foo', 'bar' }))
        assert_equal(client:raw_command({ 'GET', 'foo' }), 'bar')
    end)

    test("Tansform nil command arguments into empty strings", function()
        assert_true(client:set(nil, 'bar'))
        assert_equal(client:get(''), 'bar')
    end)

    test("Create a new unbound command object", function()
        local cmd = redis.command('doesnotexist')
        assert_nil(client.doesnotexist)
        assert_error(function() cmd(client) end)

        local cmd = redis.command('ping', {
            response = function(response) return response == 'PONG' end
        })
        assert_equal(cmd(client), true)
    end)

    test("Define commands at module level", function()
        redis.commands.doesnotexist = redis.command('doesnotexist')
        local client2 = utils.create_client(settings)

        redis.commands.doesnotexist = nil
        local client3 = utils.create_client(settings)

        assert_nil(client.doesnotexist)
        assert_not_nil(client2.doesnotexist)
        assert_nil(client3.doesnotexist)
    end)

    test("Define new commands at client instance level", function()
        client.doesnotexist = redis.command('doesnotexist')
        assert_not_nil(client.doesnotexist)
        assert_error(function() client:doesnotexist() end)

        client.doesnotexist = nil
        assert_nil(client.doesnotexist)

        client.ping = redis.command('ping')
        assert_not_nil(client.ping)
        assert_equal(client:ping(), 'PONG')

        client.ping = redis.command('ping', {
            request = function(command, ...) return ... end
        })
        assert_not_nil(client.ping)
        assert_equal(client:ping(), 'PONG')

        client.ping = redis.command('ping', {
            request  = function(command, ...) return ... end,
            response = function(reply) return reply == 'PONG' end
        })
        assert_not_nil(client.ping)
        assert_true(client:ping())
    end)

    test("Pipelining commands", function()
        local replies, count = client:pipeline(function(p)
            p:ping()
            p:exists('counter')
            p:incrby('counter', 10)
            p:incrby('counter', 30)
            p:exists('counter')
            p:get('counter')
            p:mset({ foo = 'bar', hoge = 'piyo'})
            p:del('foo', 'hoge')
            p:mget('does_not_exist', 'counter')
            p:info()
            p:get('nilkey')
        end)

        assert_type(replies, 'table')
        assert_equal(count, 11)
        assert_equal(#replies, 10)
        assert_true(replies[1])
        assert_type(replies[9], 'table')
        assert_equal(replies[9][2], '40')
        assert_type(replies[10], 'table')
    end)

    after(function()
        client:quit()
    end)
end)

context("Redis commands", function()
    before(function()
        client, version = utils.create_client(settings)
    end)

    after(function()
        client:quit()
    end)

    context("Connection related commands", function()
        test("PING (client:ping)", function()
            assert_true(client:ping())
        end)

        test("ECHO (client:echo)", function()
            local str_ascii, str_utf8 = "Can you hear me?", "聞こえますか?"

            assert_equal(client:echo(str_ascii), str_ascii)
            assert_equal(client:echo(str_utf8), str_utf8)
        end)

        test("SELECT (client:select)", function()
            if not settings.database then return end

            assert_true(client:select(0))
            assert_true(client:select(settings.database))
            assert_error(function() client:select(100) end)
            assert_error(function() client:select(-1) end)
        end)
    end)

    context("Commands operating on the key space", function()
        test("KEYS (client:keys)", function()
            local kvs_prefixed   = shared.kvs_ns_table()
            local kvs_unprefixed = { aaa = 1, aba = 2, aca = 3 }
            local kvs_all = table.merge(kvs_prefixed, kvs_unprefixed)

            client:mset(kvs_all)

            assert_empty(client:keys('nokeys:*'))
            assert_table_values(
                table.values(client:keys('*')),
                table.keys(kvs_all)
            )
            assert_table_values(
                table.values(client:keys('metavars:*')),
                table.keys(kvs_prefixed)
            )
            assert_table_values(
                table.values(client:keys('a?a')),
                table.keys(kvs_unprefixed)
            )
        end)

        test("EXISTS (client:exists)", function()
            client:set('foo', 'bar')

            assert_true(client:exists('foo'))
            assert_false(client:exists('hoge'))
        end)

        test("DEL (client:del)", function()
            client:mset(shared.kvs_table())

            assert_equal(client:del('doesnotexist'), 0)
            assert_equal(client:del('foofoo'), 1)
            assert_equal(client:del('foo', 'hoge', 'doesnotexist'), 2)
        end)

        test("TYPE (client:type)", function()
            assert_equal(client:type('doesnotexist'), 'none')

            client:set('fooString', 'bar')
            assert_equal(client:type('fooString'), 'string')

            client:rpush('fooList', 'bar')
            assert_equal(client:type('fooList'), 'list')

            client:sadd('fooSet', 'bar')
            assert_equal(client:type('fooSet'), 'set')

            client:zadd('fooZSet', 0, 'bar')
            assert_equal(client:type('fooZSet'), 'zset')

            if version:is('>=', '2.0.0') then
                client:hset('fooHash', 'value', 'bar')
                assert_equal('hash', client:type('fooHash'))
            end
        end)

        test("RANDOMKEY (client:randomkey)", function()
            local kvs = shared.kvs_table()

            assert_nil(client:randomkey())
            client:mset(kvs)
            assert_true(table.contains(table.keys(kvs), client:randomkey()))
        end)

        test("RENAME (client:rename)", function()
            local kvs = shared.kvs_table()
            client:mset(kvs)

            assert_true(client:rename('hoge', 'hogehoge'))
            assert_false(client:exists('hoge'))
            assert_equal(client:get('hogehoge'), 'piyo')

            -- rename overwrites existing keys
            assert_true(client:rename('foo', 'foofoo'))
            assert_false(client:exists('foo'))
            assert_equal(client:get('foofoo'), 'bar')

            -- rename fails when the key does not exist
            assert_error(function()
                client:rename('doesnotexist', 'fuga')
            end)
        end)

        test("RENAMENX (client:renamenx)", function()
            local kvs = shared.kvs_table()
            client:mset(kvs)

            assert_true(client:renamenx('hoge', 'hogehoge'))
            assert_false(client:exists('hoge'))
            assert_equal(client:get('hogehoge'), 'piyo')

            -- rename overwrites existing keys
            assert_false(client:renamenx('foo', 'foofoo'))
            assert_true(client:exists('foo'))

            -- rename fails when the key does not exist
            assert_error(function()
                client:renamenx('doesnotexist', 'fuga')
            end)
        end)

        test("TTL (client:ttl)", function()
            client:set('foo', 'bar')
            assert_equal(client:ttl('foo'), -1)

            assert_true(client:expire('foo', 5))
            assert_lte(client:ttl('foo'), 5)
        end)

        test("PTTL (client:pttl)", function()
            if version:is('<', '2.5.0') then return end

            client:set('foo', 'bar')
            assert_equal(client:pttl('foo'), -1)

            local ttl = 5
            assert_true(client:expire('foo', ttl))
            assert_lte(client:pttl('foo'), 5 * 1000)
            assert_gte(client:pttl('foo'), 5 * 1000 - 500)
        end)

        test("EXPIRE (client:expire)", function()
            client:set('foo', 'bar')
            assert_true(client:expire('foo', 2))
            assert_true(client:exists('foo'))
            assert_lte(client:ttl('foo'), 2)
            utils.sleep(3)
            assert_false(client:exists('foo'))

            client:set('foo', 'bar')
            assert_true(client:expire('foo', 100))
            utils.sleep(3)
            assert_lte(client:ttl('foo'), 97)

            assert_true(client:expire('foo', -100))
            assert_false(client:exists('foo'))
        end)

        test("PEXPIRE (client:pexpire)", function()
            if version:is('<', '2.5.0') then return end

            local ttl = 1
            client:set('foo', 'bar')
            assert_true(client:pexpire('foo', ttl * 1000))
            assert_true(client:exists('foo'))
            assert_lte(client:pttl('foo'), ttl * 1000)
            assert_gte(client:pttl('foo'), ttl * 1000 - 500)
            utils.sleep(ttl)
            assert_false(client:exists('foo'))
        end)

        test("EXPIREAT (client:expireat)", function()
            client:set('foo', 'bar')
            assert_true(client:expireat('foo', os.time() + 2))
            assert_lte(client:ttl('foo'), 2)
            utils.sleep(3)
            assert_false(client:exists('foo'))

            client:set('foo', 'bar')
            assert_true(client:expireat('foo', os.time() - 100))
            assert_false(client:exists('foo'))
        end)

        test("PEXPIREAT (client:pexpireat)", function()
            if version:is('<', '2.5.0') then return end

            local ttl = 2
            client:set('foo', 'bar')
            assert_true(client:pexpireat('foo', os.time() + ttl * 1000))
            assert_lte(client:pttl('foo'), ttl * 1000)
            utils.sleep(ttl + 1)
            assert_false(client:exists('foo'))

            client:set('foo', 'bar')
            assert_true(client:pexpireat('foo', os.time() - 100 * 1000))
            assert_false(client:exists('foo'))
        end)

        test("MOVE (client:move)", function()
            if not settings.database then return end

            local other_db = settings.database + 1
            client:set('foo', 'bar')
            client:select(other_db)
            client:flushdb()
            client:select(settings.database)

            assert_true(client:move('foo', other_db))
            assert_false(client:move('foo', other_db))
            assert_false(client:move('doesnotexist', other_db))

            client:set('hoge', 'piyo')
            assert_error(function() client:move('hoge', 100) end)
        end)

        test("DBSIZE (client:dbsize)", function()
            assert_equal(client:dbsize(), 0)
            client:mset(shared.kvs_table())
            assert_greater_than(client:dbsize(), 0)
        end)

        test("PERSIST (client:persist)", function()
            if version:is('<', '2.1.0') then return end

            client:set('foo', 'bar')

            assert_true(client:expire('foo', 1))
            assert_equal(client:ttl('foo'), 1)
            assert_true(client:persist('foo'))
            assert_equal(client:ttl('foo'), -1)

            assert_false(client:persist('foo'))
            assert_false(client:persist('foobar'))
        end)

        test("SCAN (client:scan)", function()
            if version:is('<', '2.8.0') then return end

            client:mset(
                'scan:1', '1',
                'scan:2', '2',
                'scan:3', '3',
                'scan:4', '4'
            )

            local cursor, keys = unpack(client:scan(0, {
                match = 'scan:*', count = 10
            }))

            assert_type(cursor, 'string')
            assert_type(keys, 'table')

            assert_table_values(keys, {'scan:1','scan:2','scan:3','scan:4'})
        end)
    end)

    context("Commands operating on the key space - SORT", function()
        -- TODO: missing tests for params GET and BY

        before(function()
            -- TODO: code duplication!
            list01, list01_values = "list01", { "4","2","3","5","1" }
            for _,v in ipairs(list01_values) do client:rpush(list01,v) end

            list02, list02_values = "list02", { "1","10","2","20","3","30" }
            for _,v in ipairs(list02_values) do client:rpush(list02,v) end
        end)

        test("SORT (client:sort)", function()
            local sorted = client:sort(list01)
            assert_table_values(sorted, { "1","2","3","4","5" })
        end)

        test("SORT (client:sort) with parameter ASC/DESC", function()
            assert_table_values(client:sort(list01, { sort = 'asc'}),  { "1","2","3","4","5" })
            assert_table_values(client:sort(list01, { sort = 'desc'}), { "5","4","3","2","1" })
        end)

        test("SORT (client:sort) with parameter LIMIT", function()
            assert_table_values(client:sort(list01, { limit = { 0,3 } }), { "1","2", "3" })
            assert_table_values(client:sort(list01, { limit = { 3,2 } }), { "4","5" })
        end)

        test("SORT (client:sort) with parameter ALPHA", function()
            assert_table_values(client:sort(list02, { alpha = false }), { "1","2","3","10","20","30" })
            assert_table_values(client:sort(list02, { alpha = true }),  { "1","10","2","20","3","30" })
        end)

        test("SORT (client:sort) with parameter GET", function()
            client:rpush('uids', 1003)
            client:rpush('uids', 1001)
            client:rpush('uids', 1002)
            client:rpush('uids', 1000)
            local sortget = {
                ['uid:1000'] = 'foo',  ['uid:1001'] = 'bar',
                ['uid:1002'] = 'hoge', ['uid:1003'] = 'piyo',
            }
            client:mset(sortget)

            assert_table_values(client:sort('uids', { get = 'uid:*' }), table.values(sortget))
            assert_table_values(client:sort('uids', { get = { 'uid:*' } }), table.values(sortget))
        end)

        test("SORT (client:sort) with multiple parameters", function()
            assert_table_values(client:sort(list02, {
                alpha = false,
                sort  = 'desc',
                limit = { 1, 4 }
            }), { "20","10","3","2" })
        end)

        test("SORT (client:sort) with parameter STORE", function()
            assert_equal(client:sort(list01, { store = 'list01_ordered' }), 5)
            assert_true(client:exists('list01_ordered'))
        end)
    end)

    context("Commands operating on string values", function()
        test("SET (client:set)", function()
            assert_true(client:set('foo', 'bar'))
            assert_equal(client:get('foo'), 'bar')
        end)

        test("GET (client:get)", function()
            client:set('foo', 'bar')

            assert_equal(client:get('foo'), 'bar')
            assert_nil(client:get('hoge'))

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:get('metavars')
            end)
        end)

        test("SETNX (client:setnx)", function()
            assert_true(client:setnx('foo', 'bar'))
            assert_false(client:setnx('foo', 'baz'))
            assert_equal(client:get('foo'), 'bar')
        end)

        test("SETEX (client:setex)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:setex('foo', 10, 'bar'))
            assert_true(client:exists('foo'))
            assert_lte(client:ttl('foo'), 10)

            assert_true(client:setex('hoge', 1, 'piyo'))
            utils.sleep(2)
            assert_false(client:exists('hoge'))

            assert_error(function() client:setex('hoge', 2.5, 'piyo') end)
            assert_error(function() client:setex('hoge', 0, 'piyo') end)
            assert_error(function() client:setex('hoge', -10, 'piyo') end)
        end)

        test("PSETEX (client:psetex)", function()
            if version:is('<', '2.5.0') then return end

            local ttl = 10 * 1000
            assert_true(client:psetex('foo', ttl, 'bar'))
            assert_true(client:exists('foo'))
            assert_lte(client:pttl('foo'), ttl)
            assert_gte(client:pttl('foo'), ttl - 500)

            assert_true(client:psetex('hoge', 1 * 1000, 'piyo'))
            utils.sleep(2)
            assert_false(client:exists('hoge'))

            assert_error(function() client:psetex('hoge', 2.5, 'piyo') end)
            assert_error(function() client:psetex('hoge', 0, 'piyo') end)
            assert_error(function() client:psetex('hoge', -10, 'piyo') end)
        end)

        test("MSET (client:mset)", function()
            local kvs = shared.kvs_table()

            assert_true(client:mset(kvs))
            for k,v in pairs(kvs) do
                assert_equal(client:get(k), v)
            end

            assert_true(client:mset('a', '1', 'b', '2', 'c', '3'))
            assert_equal(client:get('a'), '1')
            assert_equal(client:get('b'), '2')
            assert_equal(client:get('c'), '3')
        end)

        test("MSETNX (client:msetnx)", function()
           assert_true(client:msetnx({ a = '1', b = '2' }))
           assert_false(client:msetnx({ c = '3', a = '100'}))
           assert_equal(client:get('a'), '1')
           assert_equal(client:get('b'), '2')
        end)

        test("MGET (client:mget)", function()
            local kvs = shared.kvs_table()
            local keys, values = table.keys(kvs), table.values(kvs)

            assert_true(client:mset(kvs))
            assert_table_values(client:mget(unpack(keys)), values)
        end)

        test("GETSET (client:getset)", function()
            assert_nil(client:getset('foo', 'bar'))
            assert_equal(client:getset('foo', 'barbar'), 'bar')
            assert_equal(client:getset('foo', 'baz'), 'barbar')
        end)

        test("INCR (client:incr)", function()
            assert_equal(client:incr('foo'), 1)
            assert_equal(client:incr('foo'), 2)

            assert_true(client:set('hoge', 'piyo'))

            if version:is('<', '2.0.0') then
                assert_equal(client:incr('hoge'), 1)
            else
                assert_error(function()
                    client:incr('hoge')
                end)
            end
        end)

        test("INCRBY (client:incrby)", function()
            client:set('foo', 2)
            assert_equal(client:incrby('foo', 20), 22)
            assert_equal(client:incrby('foo', -12), 10)
            assert_equal(client:incrby('foo', -110), -100)
        end)

        test("INCRBYFLOAT (client:incrbyfloat)", function()
            if version:is('<', '2.5.0') then return end

            client:set('foo', 2)
            assert_equal(client:incrbyfloat('foo', 20.123), 22.123)
            assert_equal(client:incrbyfloat('foo', -12.123), 10)
            assert_equal(client:incrbyfloat('foo', -110.01), -100.01)
        end)

        test("DECR (client:decr)", function()
            assert_equal(client:decr('foo'), -1)
            assert_equal(client:decr('foo'), -2)

            assert_true(client:set('hoge', 'piyo'))
            if version:is('<', '2.0.0') then
                assert_equal(client:decr('hoge'), -1)
            else
                assert_error(function()
                    client:decr('hoge')
                end)
            end
        end)

        test("DECRBY (client:decrby)", function()
            client:set('foo', -2)
            assert_equal(client:decrby('foo', 20), -22)
            assert_equal(client:decrby('foo', -12), -10)
            assert_equal(client:decrby('foo', -110), 100)
        end)

        test("APPEND (client:append)", function()
            if version:is('<', '2.0.0') then return end

            client:set('foo', 'bar')
            assert_equal(client:append('foo', '__'), 5)
            assert_equal(client:append('foo', 'bar'), 8)
            assert_equal(client:get('foo'), 'bar__bar')

            assert_equal(client:append('hoge', 'piyo'), 4)
            assert_equal(client:get('hoge'), 'piyo')

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:append('metavars', 'bar')
            end)
        end)

        test("SUBSTR (client:substr)", function()
            if version:is('<', '2.0.0') then return end

            client:set('var', 'foobar')
            assert_equal(client:substr('var', 0, 2), 'foo')
            assert_equal(client:substr('var', 3, 5), 'bar')
            assert_equal(client:substr('var', -3, -1), 'bar')

            assert_equal(client:substr('var', 5, 0), '')

            client:set('numeric', 123456789)
            assert_equal(client:substr('numeric', 0, 4), '12345')

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:substr('metavars', 0, 3)
            end)
        end)

        test("STRLEN (client:strlen)", function()
            if version:is('<', '2.1.0') then return end

            client:set('var', 'foobar')
            assert_equal(client:strlen('var'), 6)
            assert_equal(client:append('var', '___'), 9)
            assert_equal(client:strlen('var'), 9)

            assert_error(function()
                client:rpush('metavars', 'foo')
                qclient:strlen('metavars')
            end)
        end)

        test("SETRANGE (client:setrange)", function()
            if version:is('<', '2.1.0') then return end

            assert_equal(client:setrange('var', 0, 'foobar'), 6)
            assert_equal(client:get('var'), 'foobar')
            assert_equal(client:setrange('var', 3, 'foo'), 6)
            assert_equal(client:get('var'), 'foofoo')
            assert_equal(client:setrange('var', 10, 'barbar'), 16)
            assert_equal(client:get('var'), "foofoo\0\0\0\0barbar")

            assert_error(function()
                client:setrange('var', -1, 'bogus')
            end)

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:setrange('metavars', 0, 'hoge')
            end)
        end)

        test("GETRANGE (client:getrange)", function()
            if version:is('<', '2.1.0') then return end

            client:set('var', 'foobar')
            assert_equal(client:getrange('var', 0, 2), 'foo')
            assert_equal(client:getrange('var', 3, 5), 'bar')
            assert_equal(client:getrange('var', -3, -1), 'bar')

            assert_equal(client:substr('var', 5, 0), '')

            client:set('numeric', 123456789)
            assert_equal(client:getrange('numeric', 0, 4), '12345')

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:getrange('metavars', 0, 3)
            end)
        end)

        test("SETBIT (client:setbit)", function()
            if version:is('<', '2.1.0') then return end

            assert_equal(client:setbit('binary', 31, 1), 0)
            assert_equal(client:setbit('binary', 0, 1), 0)
            assert_equal(client:strlen('binary'), 4)
            assert_equal(client:get('binary'), "\128\0\0\1")

            assert_equal(client:setbit('binary', 0, 0), 1)
            assert_equal(client:setbit('binary', 0, 0), 0)
            assert_equal(client:get('binary'), "\0\0\0\1")

            assert_error(function()
              client:setbit('binary', -1, 1)
            end)

            assert_error(function()
                client:setbit('binary', 'invalid', 1)
            end)

            assert_error(function()
                client:setbit('binary', 'invalid', 1)
            end)

            assert_error(function()
                client:setbit('binary', 15, 255)
            end)

            assert_error(function()
                client:setbit('binary', 15, 'invalid')
            end)

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:setbit('metavars', 0, 1)
            end)
        end)

        test("GETBIT (client:getbit)", function()
            if version:is('<', '2.1.0') then return end

            client:set('binary', "\128\0\0\1")

            assert_equal(client:getbit('binary', 0), 1)
            assert_equal(client:getbit('binary', 15), 0)
            assert_equal(client:getbit('binary', 31), 1)
            assert_equal(client:getbit('binary', 63), 0)

            assert_error(function()
              client:getbit('binary', -1)
            end)

            assert_error(function()
              client:getbit('binary', 'invalid')
            end)

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:getbit('metavars', 0)
            end)
        end)

        test("BITOP (client:bitop)", function()
            if version:is('<', '2.5.10') then return end

            client:set('foo', 'a')
            client:set('bar', 'b')

            client:bitop('AND', 'foo&bar', 'foo', 'bar')
            client:bitop('OR', 'foo|bar', 'foo', 'bar')
            client:bitop('XOR', 'foo^bar', 'foo', 'bar')
            client:bitop('NOT', '-foo', 'foo')

            assert_equal(client:get('foo&bar'), '\96')
            assert_equal(client:get('foo|bar'), '\99')
            assert_equal(client:get('foo^bar'), '\3')
            assert_equal(client:get('-foo'), '\158')
        end)

        test("BITCOUNT (client:bitcount)", function()
            if version:is('<', '2.5.10') then return end

            client:set('foo', 'abcde')

            assert_equal(client:bitcount('foo', 1, 3), 10)
            assert_equal(client:bitcount('foo', 0, -1), 17)
        end)
    end)

    context("Commands operating on lists", function()
        test("RPUSH (client:rpush)", function()
            if version:is('<', '2.0.0') then
                assert_true(client:rpush('metavars', 'foo'))
                assert_true(client:rpush('metavars', 'hoge'))
            else
                assert_equal(client:rpush('metavars', 'foo'), 1)
                assert_equal(client:rpush('metavars', 'hoge'), 2)
            end
            assert_error(function()
                client:set('foo', 'bar')
                client:rpush('foo', 'baz')
            end)
        end)

        test("RPUSHX (client:rpushx)", function()
            if version:is('<', '2.1.0') then return end

            assert_equal(client:rpushx('numbers', 1), 0)
            assert_equal(client:rpush('numbers', 2), 1)
            assert_equal(client:rpushx('numbers', 3), 2)
            assert_equal(client:llen('numbers'), 2)
            assert_table_values(client:lrange('numbers', 0, -1), { '2', '3' })

            assert_error(function()
                client:set('foo', 'bar')
                client:rpushx('foo', 'baz')
            end)
        end)

        test("LPUSH (client:lpush)", function()
            if version:is('<', '2.0.0') then
                assert_true(client:lpush('metavars', 'foo'))
                assert_true(client:lpush('metavars', 'hoge'))
            else
                assert_equal(client:lpush('metavars', 'foo'), 1)
                assert_equal(client:lpush('metavars', 'hoge'), 2)
            end
            assert_error(function()
                client:set('foo', 'bar')
                client:lpush('foo', 'baz')
            end)
        end)

        test("LPUSHX (client:lpushx)", function()
            if version:is('<', '2.1.0') then return end

            assert_equal(client:lpushx('numbers', 1), 0)
            assert_equal(client:lpush('numbers', 2), 1)
            assert_equal(client:lpushx('numbers', 3), 2)

            assert_equal(client:llen('numbers'), 2)
            assert_table_values(client:lrange('numbers', 0, -1), { '3', '2' })

            assert_error(function()
                client:set('foo', 'bar')
                client:lpushx('foo', 'baz')
            end)
        end)

        test("LLEN (client:llen)", function()
            local kvs = shared.kvs_table()
            for _, v in pairs(kvs) do
                client:rpush('metavars', v)
            end

            assert_equal(client:llen('metavars'), 3)
            assert_equal(client:llen('doesnotexist'), 0)
            assert_error(function()
                client:set('foo', 'bar')
                client:llen('foo')
            end)
        end)

        test("LRANGE (client:lrange)", function()
            local numbers = utils.rpush_return(client, 'numbers', shared.numbers())

            assert_table_values(client:lrange('numbers', 0, 3), table.slice(numbers, 1, 4))
            assert_table_values(client:lrange('numbers', 4, 8), table.slice(numbers, 5, 5))
            assert_table_values(client:lrange('numbers', 0, 0), table.slice(numbers, 1, 1))
            assert_empty(client:lrange('numbers', 1, 0))
            assert_table_values(client:lrange('numbers', 0, -1), numbers)
            assert_table_values(client:lrange('numbers', 5, -5), { '5' })
            assert_empty(client:lrange('numbers', 7, -5))
            assert_table_values(client:lrange('numbers', -5, -2), table.slice(numbers, 6, 4))
            assert_table_values(client:lrange('numbers', -100, 100), numbers)
        end)

        test("LTRIM (client:ltrim)", function()
            local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true)
            assert_true(client:ltrim('numbers', 0, 2))
            assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 1, 3))

            local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true)
            assert_true(client:ltrim('numbers', 5, 9))
            assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 6, 5))

            local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true)
            assert_true(client:ltrim('numbers', 0, -6))
            assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 1, 5))

            local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true)
            assert_true(client:ltrim('numbers', -5, -3))
            assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 6, 3))

            local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true)
            assert_true(client:ltrim('numbers', -100, 100))
            assert_table_values(client:lrange('numbers', 0, -1), numbers)

            assert_error(function()
                client:set('foo', 'bar')
                client:ltrim('foo', 0, 1)
            end)
        end)

        test("LINDEX (client:lindex)", function()
            local numbers = utils.rpush_return(client, 'numbers', shared.numbers())

            assert_equal(client:lindex('numbers', 0), numbers[1])
            assert_equal(client:lindex('numbers', 5), numbers[6])
            assert_equal(client:lindex('numbers', 9), numbers[10])
            assert_nil(client:lindex('numbers', 100))

            assert_equal(client:lindex('numbers', -0), numbers[1])
            assert_equal(client:lindex('numbers', -1), numbers[10])
            assert_equal(client:lindex('numbers', -3), numbers[8])
            assert_nil(client:lindex('numbers', -100))

            assert_error(function()
                client:set('foo', 'bar')
                client:lindex('foo', 0)
            end)
        end)

        test("LSET (client:lset)", function()
            utils.rpush_return(client, 'numbers', shared.numbers())

            assert_true(client:lset('numbers', 5, -5))
            assert_equal(client:lindex('numbers', 5), '-5')

            assert_error(function()
                client:lset('numbers', 99, 99)
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:lset('foo', 0, 0)
            end)
        end)

        test("LREM (client:lrem)", function()
            local mixed = { '0', '_', '2', '_', '4', '_', '6', '_' }

            utils.rpush_return(client, 'mixed', mixed, true)
            assert_equal(client:lrem('mixed', 2, '_'), 2)
            assert_table_values(client:lrange('mixed', 0, -1), { '0', '2', '4', '_', '6', '_' })

            utils.rpush_return(client, 'mixed', mixed, true)
            assert_equal(client:lrem('mixed', 0, '_'), 4)
            assert_table_values(client:lrange('mixed', 0, -1), { '0', '2', '4', '6' })

            utils.rpush_return(client, 'mixed', mixed, true)
            assert_equal(client:lrem('mixed', -2, '_'), 2)
            assert_table_values(client:lrange('mixed', 0, -1), { '0', '_', '2', '_', '4', '6' })

            utils.rpush_return(client, 'mixed', mixed, true)
            assert_equal(client:lrem('mixed', 2, '|'), 0)
            assert_table_values(client:lrange('mixed', 0, -1), mixed)

            assert_equal(client:lrem('doesnotexist', 2, '_'), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:lrem('foo', 0, 0)
            end)
        end)

        test("LPOP (client:lpop)", function()
            local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2', '3', '4' })

            assert_equal(client:lpop('numbers'), numbers[1])
            assert_equal(client:lpop('numbers'), numbers[2])
            assert_equal(client:lpop('numbers'), numbers[3])

            assert_table_values(client:lrange('numbers', 0, -1), { '3', '4' })

            client:lpop('numbers')
            client:lpop('numbers')
            assert_nil(client:lpop('numbers'))

            assert_nil(client:lpop('doesnotexist'))

            assert_error(function()
                client:set('foo', 'bar')
                client:lpop('foo')
            end)
        end)

        test("RPOP (client:rpop)", function()
            local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2', '3', '4' })

            assert_equal(client:rpop('numbers'), numbers[5])
            assert_equal(client:rpop('numbers'), numbers[4])
            assert_equal(client:rpop('numbers'), numbers[3])

            assert_table_values(client:lrange('numbers', 0, -1), { '0', '1' })

            client:rpop('numbers')
            client:rpop('numbers')
            assert_nil(client:rpop('numbers'))

            assert_nil(client:rpop('doesnotexist'))

            assert_error(function()
                client:set('foo', 'bar')
                client:rpop('foo')
            end)
        end)

        test("RPOPLPUSH (client:rpoplpush)", function()
            local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2' }, true)
            assert_equal(client:llen('temporary'), 0)
            assert_equal(client:rpoplpush('numbers', 'temporary'), '2')
            assert_equal(client:rpoplpush('numbers', 'temporary'), '1')
            assert_equal(client:rpoplpush('numbers', 'temporary'), '0')
            assert_equal(client:llen('numbers'), 0)
            assert_equal(client:llen('temporary'), 3)

            local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2' }, true)
            client:rpoplpush('numbers', 'numbers')
            client:rpoplpush('numbers', 'numbers')
            client:rpoplpush('numbers', 'numbers')
            assert_table_values(client:lrange('numbers', 0, -1), numbers)

            assert_nil(client:rpoplpush('doesnotexist1', 'doesnotexist2'))

            assert_error(function()
                client:set('foo', 'bar')
                client:rpoplpush('foo', 'hoge')
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:rpoplpush('temporary', 'foo')
            end)
        end)

        test("BLPOP (client:blpop)", function()
            if version:is('<', '2.0.0') then return end
            -- TODO: implement tests
        end)

        test("BRPOP (client:brpop)", function()
            if version:is('<', '2.0.0') then return end
            -- TODO: implement tests
        end)

        test("BRPOPLPUSH (client:brpoplpush)", function()
            if version:is('<', '2.1.0') then return end
            -- TODO: implement tests
        end)

        test("LINSERT (client:linsert)", function()
            if version:is('<', '2.1.0') then return end

            utils.rpush_return(client, 'numbers', shared.numbers(), true)

            assert_equal(client:linsert('numbers', 'before', 0, -2), 11)
            assert_equal(client:linsert('numbers', 'after', -2, -1), 12)
            assert_table_values(client:lrange('numbers', 0, 3), { '-2', '-1', '0', '1' });

            assert_equal(client:linsert('numbers', 'before', 100, 200), -1)
            assert_equal(client:linsert('numbers', 'after', 100, 50), -1)

            assert_error(function()
                client:set('foo', 'bar')
                client:linsert('foo', 0, 0)
            end)
        end)
    end)

    context("Commands operating on sets", function()
        test("SADD (client:sadd)", function()
            assert_equal(client:sadd('set', 0), 1)
            assert_equal(client:sadd('set', 1), 1)
            assert_equal(client:sadd('set', 0), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:sadd('foo', 0)
            end)
        end)

        test("SREM (client:srem)", function()
            utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4' })

            assert_equal(client:srem('set', 0), 1)
            assert_equal(client:srem('set', 4), 1)
            assert_equal(client:srem('set', 10), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:srem('foo', 0)
            end)
        end)

        test("SPOP (client:spop)", function()
            local set = utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4' })

            assert_true(table.contains(set, client:spop('set')))
            assert_nil(client:spop('doesnotexist'))

            assert_error(function()
                client:set('foo', 'bar')
                client:spop('foo')
            end)
        end)

        test("SMOVE (client:smove)", function()
            utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5' })
            utils.sadd_return(client, 'setB', { '5', '6', '7', '8', '9', '10' })

            assert_true(client:smove('setA', 'setB', 0))
            assert_equal(client:srem('setA', 0), 0)
            assert_equal(client:srem('setB', 0), 1)

            assert_true(client:smove('setA', 'setB', 5))
            assert_false(client:smove('setA', 'setB', 100))

            assert_error(function()
                client:set('foo', 'bar')
                client:smove('foo', 'setB', 5)
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:smove('setA', 'foo', 5)
            end)
        end)

        test("SCARD (client:scard)", function()
            utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5' })

            assert_equal(client:scard('setA'), 6)

            -- empty set
            client:sadd('setB', 0)
            client:spop('setB')
            assert_equal(client:scard('doesnotexist'), 0)

            -- non-existent set
            assert_equal(client:scard('doesnotexist'), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:scard('foo')
            end)
        end)

        test("SISMEMBER (client:sismember)", function()
            utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4', '5' })

            assert_true(client:sismember('set', 3))
            assert_false(client:sismember('set', 100))
            assert_false(client:sismember('doesnotexist', 0))

            assert_error(function()
                client:set('foo', 'bar')
                client:sismember('foo', 0)
            end)
        end)

        test("SMEMBERS (client:smembers)", function()
            local set = utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4', '5' })

            assert_table_values(client:smembers('set'), set)

            if version:is('<', '2.0.0') then
                assert_nil(client:smembers('doesnotexist'))
            else
                assert_table_values(client:smembers('doesnotexist'), {})
            end

            assert_error(function()
                client:set('foo', 'bar')
                client:smembers('foo')
            end)
        end)

        test("SINTER (client:sinter)", function()
            local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' })
            local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' })

            assert_table_values(client:sinter('setA'), setA)
            assert_table_values(client:sinter('setA', 'setB'), { '3', '4', '6', '1' })

            if version:is('<', '2.0.0') then
                assert_nil(client:sinter('setA', 'doesnotexist'))
            else
                assert_table_values(client:sinter('setA', 'doesnotexist'), {})
            end

            assert_error(function()
                client:set('foo', 'bar')
                client:sinter('foo')
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:sinter('setA', 'foo')
            end)
        end)

        test("SINTERSTORE (client:sinterstore)", function()
            local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' })
            local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' })

            assert_equal(client:sinterstore('setC', 'setA'), #setA)
            assert_table_values(client:smembers('setC'), setA)

            client:del('setC')
            -- this behaviour has changed in redis 2.0
            assert_equal(client:sinterstore('setC', 'setA', 'setB'), 4)
            assert_table_values(client:smembers('setC'), { '1', '3', '4', '6' })

            client:del('setC')
            assert_equal(client:sinterstore('setC', 'doesnotexist'), 0)
            assert_false(client:exists('setC'))

            -- existing keys are replaced by SINTERSTORE
            client:set('foo', 'bar')
            assert_equal(client:sinterstore('foo', 'setA'), #setA)

            assert_error(function()
                client:set('foo', 'bar')
                client:sinterstore('setA', 'foo')
            end)
        end)

        test("SUNION (client:sunion)", function()
            local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' })
            local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' })

            assert_table_values(client:sunion('setA'), setA)
            assert_table_values(
                client:sunion('setA', 'setB'),
                { '0', '1', '10', '2', '3', '4', '5', '6', '9' }
            )

            -- this behaviour has changed in redis 2.0
            assert_table_values(client:sunion('setA', 'doesnotexist'), setA)

            assert_error(function()
                client:set('foo', 'bar')
                client:sunion('foo')
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:sunion('setA', 'foo')
            end)
        end)

        test("SUNIONSTORE (client:sunionstore)", function()
            local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' })
            local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' })

            assert_equal(client:sunionstore('setC', 'setA'), #setA)
            assert_table_values(client:smembers('setC'), setA)

            client:del('setC')
            assert_equal(client:sunionstore('setC', 'setA', 'setB'), 9)
            assert_table_values(
                client:smembers('setC'),
                { '0' ,'1' , '10', '2', '3', '4', '5', '6', '9' }
            )

            client:del('setC')
            assert_equal(client:sunionstore('setC', 'doesnotexist'), 0)
            if version:is('<', '2.0.0') then
                assert_true(client:exists('setC'))
            else
                assert_false(client:exists('setC'))
            end
            assert_equal(client:scard('setC'), 0)

            -- existing keys are replaced by SUNIONSTORE
            client:set('foo', 'bar')
            assert_equal(client:sunionstore('foo', 'setA'), #setA)

            assert_error(function()
                client:set('foo', 'bar')
                client:sunionstore('setA', 'foo')
            end)
        end)

        test("SDIFF (client:sdiff)", function()
            local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' })
            local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' })

            assert_table_values(client:sdiff('setA'), setA)
            assert_table_values(client:sdiff('setA', 'setB'), { '5', '0', '2' })
            assert_table_values(client:sdiff('setA', 'doesnotexist'), setA)

            assert_error(function()
                client:set('foo', 'bar')
                client:sdiff('foo')
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:sdiff('setA', 'foo')
            end)
        end)

        test("SDIFFSTORE (client:sdiffstore)", function()
            local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' })
            local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' })

            assert_equal(client:sdiffstore('setC', 'setA'), #setA)
            assert_table_values(client:smembers('setC'), setA)

            client:del('setC')
            assert_equal(client:sdiffstore('setC', 'setA', 'setB'), 3)
            assert_table_values(client:smembers('setC'), { '5', '0', '2' })

            client:del('setC')
            assert_equal(client:sdiffstore('setC', 'doesnotexist'), 0)
            if version:is('<', '2.0.0') then
                assert_true(client:exists('setC'))
            else
                assert_false(client:exists('setC'))
            end
            assert_equal(client:scard('setC'), 0)

            -- existing keys are replaced by SDIFFSTORE
            client:set('foo', 'bar')
            assert_equal(client:sdiffstore('foo', 'setA'), #setA)

            assert_error(function()
                client:set('foo', 'bar')
                client:sdiffstore('setA', 'foo')
            end)
        end)

        test("SRANDMEMBER (client:srandmember)", function()
            local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' })

            assert_true(table.contains(setA, client:srandmember('setA')))
            assert_nil(client:srandmember('doesnotexist'))

            assert_error(function()
                client:set('foo', 'bar')
                client:srandmember('foo')
            end)
        end)

        test("SSCAN (client:sscan)", function()
            if version:is('<', '2.8.0') then return end

            local args = { 'member:1st', 'member:2nd', 'member:3rd', 'member:4th' }
            client:sadd('key:set', unpack(args))

            local cursor, members = unpack(client:sscan('key:set', 0))
            assert_type(cursor, 'string')
            assert_type(members, 'table')
            assert_table_values(members, args)

            local cursor, members = unpack(client:sscan('key:set', 0, {
                match = 'member:*d', count = 10
            }))
            assert_type(cursor, 'string')
            assert_type(members, 'table')
            assert_table_values(members, { 'member:2nd', 'member:3rd' })
        end)
    end)

    context("Commands operating on zsets", function()
        test("ZADD (client:zadd)", function()
            assert_equal(client:zadd('zset', 0, 'a'), 1)
            assert_equal(client:zadd('zset', 1, 'b'), 1)
            assert_equal(client:zadd('zset', -1, 'c'), 1)

            assert_equal(client:zadd('zset', 2, 'b'), 0)
            assert_equal(client:zadd('zset', -22, 'b'), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:zadd('foo', 0, 'a')
            end)
        end)

        test("ZINCRBY (client:zincrby)", function()
            assert_equal(client:zincrby('doesnotexist', 1, 'foo'), 1)
            assert_equal(client:type('doesnotexist'), 'zset')

            utils.zadd_return(client, 'zset', shared.zset_sample())
            assert_equal(client:zincrby('zset', 5, 'a'), -5)
            assert_equal(client:zincrby('zset', 1, 'b'), 1)
            assert_equal(client:zincrby('zset', 0, 'c'), 10)
            assert_equal(client:zincrby('zset', -20, 'd'), 0)
            assert_equal(client:zincrby('zset', 2, 'd'), 2)
            assert_equal(client:zincrby('zset', -30, 'e'), -10)
            assert_equal(client:zincrby('zset', 1, 'x'), 1)

            assert_error(function()
                client:set('foo', 'bar')
                client:zincrby('foo', 1, 'a')
            end)
        end)

        test("ZREM (client:zrem)", function()
            utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_equal(client:zrem('zset', 'a'), 1)
            assert_equal(client:zrem('zset', 'x'), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:zrem('foo', 'bar')
            end)
        end)

        test("ZRANGE (client:zrange)", function()
            local zset = utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_table_values(client:zrange('zset', 0, 3), { 'a', 'b', 'c', 'd' })
            assert_table_values(client:zrange('zset', 0, 0), { 'a' })
            assert_empty(client:zrange('zset', 1, 0))
            assert_table_values(client:zrange('zset', 0, -1), table.keys(zset))
            assert_table_values(client:zrange('zset', 3, -3), { 'd' })
            assert_empty(client:zrange('zset', 5, -3))
            assert_table_values(client:zrange('zset', -100, 100), table.keys(zset))

            assert_table_values(
                client:zrange('zset', 0, 2, 'withscores'),
                  { { 'a', -10 }, { 'b', 0 }, { 'c', 10 } }
            )

            assert_table_values(
                client:zrange('zset', 0, 2, { withscores = true }),
                  { { 'a', -10 }, { 'b', 0 }, { 'c', 10 } }
            )

            assert_error(function()
                client:set('foo', 'bar')
                client:zrange('foo', 0, -1)
            end)
        end)

        test("ZREVRANGE (client:zrevrange)", function()
            local zset = utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_table_values(client:zrevrange('zset', 0, 3), { 'f', 'e', 'd', 'c' })
            assert_table_values(client:zrevrange('zset', 0, 0), { 'f' })
            assert_empty(client:zrevrange('zset', 1, 0))
            assert_table_values(client:zrevrange('zset', 0, -1), table.keys(zset))
            assert_table_values(client:zrevrange('zset', 3, -3), { 'c' })
            assert_empty(client:zrevrange('zset', 5, -3))
            assert_table_values(client:zrevrange('zset', -100, 100), table.keys(zset))

            assert_table_values(
                client:zrevrange('zset', 0, 2, 'withscores'),
                { { 'f', 30 }, { 'e', 20 }, { 'd', 20 } }
            )

            assert_table_values(
                client:zrevrange('zset', 0, 2, { withscores = true }),
                { { 'f', 30 }, { 'e', 20 }, { 'd', 20 } }
            )

            assert_error(function()
                client:set('foo', 'bar')
                client:zrevrange('foo', 0, -1)
            end)
        end)

        test("ZRANGEBYSCORE (client:zrangebyscore)", function()
            local zset = utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_table_values(client:zrangebyscore('zset', -10, -10), { 'a' })
            assert_table_values(client:zrangebyscore('zset', 10, 30), { 'c', 'd', 'e', 'f' })
            assert_table_values(client:zrangebyscore('zset', 20, 20), { 'd', 'e' })
            assert_empty(client:zrangebyscore('zset', 30, 0))

            assert_table_values(
                client:zrangebyscore('zset', 10, 20, 'withscores'),
                { { 'c', 10 }, { 'd', 20 }, { 'e', 20 } }
            )

            assert_table_values(
                client:zrangebyscore('zset', 10, 20, { withscores = true }),
                { { 'c', 10 }, { 'd', 20 }, { 'e', 20 } }
            )

            assert_table_values(
                client:zrangebyscore('zset', 10, 20, { limit = { 1, 2 } }),
                { 'd', 'e' }
            )

            assert_table_values(
                client:zrangebyscore('zset', 10, 20, {
                    limit = { offset = 1, count = 2 }
                }),
                { 'd', 'e' }
            )

            assert_table_values(
                client:zrangebyscore('zset', 10, 20, {
                    limit = { offset = 1, count = 2 },
                    withscores = true
                }),
                { { 'd', 20 }, { 'e', 20 } }
            )

            assert_error(function()
                client:set('foo', 'bar')
                client:zrangebyscore('foo', 0, -1)
            end)
        end)

        test("ZREVRANGEBYSCORE (client:zrevrangebyscore)", function()
            local zset = utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_table_values(client:zrevrangebyscore('zset', -10, -10), { 'a' })
            assert_table_values(client:zrevrangebyscore('zset', 0, -10), { 'b', 'a' })
            assert_table_values(client:zrevrangebyscore('zset', 20, 20), { 'e', 'd' })
            assert_table_values(client:zrevrangebyscore('zset', 30, 0), { 'f', 'e', 'd', 'c', 'b' })

            assert_table_values(
                client:zrevrangebyscore('zset', 20, 10, 'withscores'),
                { { 'e', 20 }, { 'd', 20 }, { 'c', 10 } }
            )

            assert_table_values(
                client:zrevrangebyscore('zset', 20, 10, { limit = { 1, 2 } }),
                { 'd', 'c' }
            )

            assert_table_values(
                client:zrevrangebyscore('zset', 20, 10, {
                    limit = { offset = 1, count = 2 }
                }),
                { 'd', 'c' }
            )

            assert_table_values(
                client:zrevrangebyscore('zset', 20, 10, {
                    limit = { offset = 1, count = 2 },
                    withscores = true
                }),
                { { 'd', 20 }, { 'c', 10 } }
            )

            assert_error(function()
                client:set('foo', 'bar')
                client:zrevrangebyscore('foo', 0, -1)
            end)
        end)

        test("ZUNIONSTORE (client:zunionstore)", function()
            if version:is('<', '2.0.0') then return end

            utils.zadd_return(client, 'zseta', { a = 1, b = 2, c = 3 })
            utils.zadd_return(client, 'zsetb', { b = 1, c = 2, d = 3 })

            -- basic ZUNIONSTORE
            assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb'), 4)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'a', 1 }, { 'b', 3 }, { 'd', 3 }, { 'c', 5 } }
            )

            assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetbNull'), 3)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'a', 1 }, { 'b', 2 }, { 'c', 3 }}
            )

            assert_equal(client:zunionstore('zsetc', 2, 'zsetaNull', 'zsetb'), 3)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'b', 1 }, { 'c', 2 }, { 'd', 3 }}
            )

            assert_equal(client:zunionstore('zsetc', 2, 'zsetaNull', 'zsetbNull'), 0)

            -- with WEIGHTS
            local opts =  { weights = { 2, 3 } }
            assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb', opts), 4)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'a', 2 }, { 'b', 7 }, { 'd', 9 }, { 'c', 12 } }
            )

            -- with AGGREGATE (min)
            local opts =  { aggregate = 'min' }
            assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb', opts), 4)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'a', 1 }, { 'b', 1 }, { 'c', 2 }, { 'd', 3 } }
            )

            -- with AGGREGATE (max)
            local opts =  { aggregate = 'max' }
            assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb', opts), 4)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'a', 1 }, { 'b', 2 }, { 'c', 3 }, { 'd', 3} }
            )

            assert_error(function()
                client:set('zsetFake', 'fake')
                client:zunionstore('zsetc', 2, 'zseta', 'zsetFake')
            end)
        end)

        test("ZINTERSTORE (client:zinterstore)", function()
            if version:is('<', '2.0.0') then return end

            utils.zadd_return(client, 'zseta', { a = 1, b = 2, c = 3 })
            utils.zadd_return(client, 'zsetb', { b = 1, c = 2, d = 3 })

            -- basic ZUNIONSTORE
            assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb'), 2)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'b', 3 }, { 'c', 5 } }
            )

            assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetbNull'), 0)
            assert_equal(client:zinterstore('zsetc', 2, 'zsetaNull', 'zsetb'), 0)
            assert_equal(client:zinterstore('zsetc', 2, 'zsetaNull', 'zsetbNull'), 0)

            -- with WEIGHTS
            local opts =  { weights = { 2, 3 } }
            assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb', opts), 2)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'b', 7 }, { 'c', 12 } }
            )

            -- with AGGREGATE (min)
            local opts =  { aggregate = 'min' }
            assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb', opts), 2)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'b', 1 }, { 'c', 2 } }
            )

            -- with AGGREGATE (max)
            local opts =  { aggregate = 'max' }
            assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb', opts), 2)
            assert_table_values(
                client:zrange('zsetc', 0, -1, 'withscores'),
                { { 'b', 2 }, { 'c', 3 } }
            )

            assert_error(function()
                client:set('zsetFake', 'fake')
                client:zinterstore('zsetc', 2, 'zseta', 'zsetFake')
            end)
        end)

        test("ZCOUNT (client:zcount)", function()
            if version:is('<', '2.0.0') then return end

            utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_equal(client:zcount('zset', 50, 100), 0)
            assert_equal(client:zcount('zset', -100, 100), 6)
            assert_equal(client:zcount('zset', 10, 20), 3)
            assert_equal(client:zcount('zset', '(10', 20), 2)
            assert_equal(client:zcount('zset', 10, '(20'), 1)
            assert_equal(client:zcount('zset', '(10', '(20'), 0)
            assert_equal(client:zcount('zset', '(0', '(30'), 3)

            assert_error(function()
                client:set('foo', 'bar')
                client:zcount('foo', 0, 0)
            end)
        end)

        test("ZCARD (client:zcard)", function()
            local zset = utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_equal(client:zcard('zset'), #table.keys(zset))

            client:zrem('zset', 'a')
            assert_equal(client:zcard('zset'), #table.keys(zset) - 1)

            client:zadd('zsetB', 0, 'a')
            client:zrem('zsetB', 'a')
            assert_equal(client:zcard('zsetB'), 0)

            assert_equal(client:zcard('doesnotexist'), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:zcard('foo')
            end)
        end)

        test("ZSCORE (client:zscore)", function()
            utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_equal(client:zscore('zset', 'a'), -10)
            assert_equal(client:zscore('zset', 'c'), 10)
            assert_equal(client:zscore('zset', 'e'), 20)

            assert_nil(client:zscore('zset', 'x'))
            assert_nil(client:zscore('doesnotexist', 'a'))

            assert_error(function()
                client:set('foo', 'bar')
                client:zscore('foo', 'a')
            end)
        end)

        test("ZREMRANGEBYSCORE (client:zremrangebyscore)", function()
            utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_equal(client:zremrangebyscore('zset', -10, 0), 2)
            assert_table_values(client:zrange('zset', 0, -1), { 'c', 'd', 'e', 'f' })

            assert_equal(client:zremrangebyscore('zset', 10, 10), 1)
            assert_table_values(client:zrange('zset', 0, -1), { 'd', 'e', 'f' })

            assert_equal(client:zremrangebyscore('zset', 100, 100), 0)

            assert_equal(client:zremrangebyscore('zset', 0, 100), 3)
            assert_equal(client:zremrangebyscore('zset', 0, 100), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:zremrangebyscore('foo', 0, 0)
            end)
        end)

        test("ZRANK (client:zrank)", function()
            if version:is('<', '2.0.0') then return end

            utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_equal(client:zrank('zset', 'a'), 0)
            assert_equal(client:zrank('zset', 'b'), 1)
            assert_equal(client:zrank('zset', 'e'), 4)

            client:zrem('zset', 'd')
            assert_equal(client:zrank('zset', 'e'), 3)

            assert_nil(client:zrank('zset', 'x'))

            assert_error(function()
                client:set('foo', 'bar')
                client:zrank('foo', 'a')
            end)
        end)

        test("ZREVRANK (client:zrevrank)", function()
            if version:is('<', '2.0.0') then return end

            utils.zadd_return(client, 'zset', shared.zset_sample())

            assert_equal(client:zrevrank('zset', 'a'), 5)
            assert_equal(client:zrevrank('zset', 'b'), 4)
            assert_equal(client:zrevrank('zset', 'e'), 1)

            client:zrem('zset', 'e')
            assert_equal(client:zrevrank('zset', 'd'), 1)

            assert_nil(client:zrevrank('zset', 'x'))

            assert_error(function()
                client:set('foo', 'bar')
                client:zrevrank('foo', 'a')
            end)
        end)

        test("ZREMRANGEBYRANK (client:zremrangebyrank)", function()
            if version:is('<', '2.0.0') then return end

            utils.zadd_return(client, 'zseta', shared.zset_sample())
            assert_equal(client:zremrangebyrank('zseta', 0, 2), 3)
            assert_table_values(client:zrange('zseta', 0, -1), { 'd', 'e', 'f' })
            assert_equal(client:zremrangebyrank('zseta', 0, 0), 1)
            assert_table_values(client:zrange('zseta', 0, -1), { 'e', 'f' })

            utils.zadd_return(client, 'zsetb', shared.zset_sample())
            assert_equal(client:zremrangebyrank('zsetb', -3, -1), 3)
            assert_table_values(client:zrange('zsetb', 0, -1), { 'a', 'b', 'c' })
            assert_equal(client:zremrangebyrank('zsetb', -1, -1), 1)
            assert_table_values(client:zrange('zsetb', 0, -1), { 'a', 'b' })
            assert_equal(client:zremrangebyrank('zsetb', -2, -1), 2)
            assert_table_values(client:zrange('zsetb', 0, -1), { })
            assert_false(client:exists('zsetb'))

            assert_equal(client:zremrangebyrank('zsetc', 0, 0), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:zremrangebyrank('foo', 0, 1)
            end)
        end)

        test("ZSCAN (client:zscan)", function()
            if version:is('<', '2.8.0') then return end

            client:zadd('key:zset',
                1, 'member:1st', 2, 'member:2nd', 3, 'member:3rd', 4, 'member:4th'
            )

            local cursor, members = unpack(client:zscan('key:zset', 0))
            assert_type(cursor, 'string')
            assert_type(members, 'table')
            assert_table_values(members, {
                { 'member:1st', 1 }, { 'member:2nd', 2 },
                { 'member:3rd', 3 }, { 'member:4th', 4 },
            })

            local cursor, members = unpack(client:zscan('key:zset', 0, {
                match = 'member:*d', count = 10
            }))
            assert_type(cursor, 'string')
            assert_type(members, 'table')
            assert_table_values(members, { { 'member:2nd', 2 }, { 'member:3rd', 3 } })
        end)
    end)

    context("Commands operating on hashes", function()
        test("HSET (client:hset)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:hset('metavars', 'foo', 'bar'))
            assert_true(client:hset('metavars', 'hoge', 'piyo'))
            assert_equal(client:hget('metavars', 'foo'), 'bar')
            assert_equal(client:hget('metavars', 'hoge'), 'piyo')

            assert_error(function()
                client:set('test', 'foobar')
                client:hset('test', 'hoge', 'piyo')
            end)
        end)

        test("HGET (client:hget)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:hset('metavars', 'foo', 'bar'))
            assert_equal(client:hget('metavars', 'foo'), 'bar')
            assert_nil(client:hget('metavars', 'hoge'))
            assert_nil(client:hget('hashDoesNotExist', 'field'))

            assert_error(function()
                client:rpush('metavars', 'foo')
                client:hget('metavars', 'foo')
            end)
        end)

        test("HEXISTS (client:hexists)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:hset('metavars', 'foo', 'bar'))
            assert_true(client:hexists('metavars', 'foo'))
            assert_false(client:hexists('metavars', 'hoge'))
            assert_false(client:hexists('hashDoesNotExist', 'field'))

            assert_error(function()
                client:set('foo', 'bar')
                client:hexists('foo')
            end)
        end)

        test("HDEL (client:hdel)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:hset('metavars', 'foo', 'bar'))
            assert_true(client:hexists('metavars', 'foo'))
            assert_equal(client:hdel('metavars', 'foo'), 1)
            assert_false(client:hexists('metavars', 'foo'))

            assert_equal(client:hdel('metavars', 'hoge'), 0)
            assert_equal(client:hdel('hashDoesNotExist', 'field'), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:hdel('foo', 'field')
            end)
        end)

        test("HLEN (client:hlen)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:hset('metavars', 'foo', 'bar'))
            assert_true(client:hset('metavars', 'hoge', 'piyo'))
            assert_true(client:hset('metavars', 'foofoo', 'barbar'))
            assert_true(client:hset('metavars', 'hogehoge', 'piyopiyo'))

            assert_equal(client:hlen('metavars'), 4)
            client:hdel('metavars', 'foo')
            assert_equal(client:hlen('metavars'), 3)
            assert_equal(client:hlen('hashDoesNotExist'), 0)

            assert_error(function()
                client:set('foo', 'bar')
                client:hlen('foo')
            end)
        end)

        test("HSETNX (client:hsetnx)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:hsetnx('metavars', 'foo', 'bar'))
            assert_false(client:hsetnx('metavars', 'foo', 'barbar'))
            assert_equal(client:hget('metavars', 'foo'), 'bar')

            assert_error(function()
                client:set('test', 'foobar')
                client:hsetnx('test', 'hoge', 'piyo')
            end)
        end)

        test("HMSET / HMGET (client:hmset, client:hmget)", function()
            if version:is('<', '2.0.0') then return end

            local hashKVs = { foo = 'bar', hoge = 'piyo' }

            -- key => value pairs via table
            assert_true(client:hmset('metavars', hashKVs))
            local retval = client:hmget('metavars', table.keys(hashKVs))
            assert_table_values(retval, table.values(hashKVs))

            -- key => value pairs via function arguments
            client:del('metavars')
            assert_true(client:hmset('metavars', 'foo', 'bar', 'hoge', 'piyo'))
            assert_table_values(retval, table.values(hashKVs))
        end)

        test("HINCRBY (client:hincrby)", function()
            if version:is('<', '2.0.0') then return end

            assert_equal(client:hincrby('hash', 'counter', 10), 10)
            assert_equal(client:hincrby('hash', 'counter', 10), 20)
            assert_equal(client:hincrby('hash', 'counter', -20), 0)

            assert_error(function()
                client:hset('hash', 'field', 'string_value')
                client:hincrby('hash', 'field', 10)
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:hincrby('foo', 'bar', 1)
            end)
        end)

        test("HINCRBYFLOAT (client:hincrbyfloat)", function()
            if version:is('<', '2.5.0') then return end

            assert_equal(client:hincrbyfloat('hash', 'counter', 10.1), 10.1)
            assert_equal(client:hincrbyfloat('hash', 'counter', 10.4), 20.5)
            assert_equal(client:hincrbyfloat('hash', 'counter', -20.000), 0.5)

            assert_error(function()
                client:hset('hash', 'field', 'string_value')
                client:hincrbyfloat('hash', 'field', 10.10)
            end)

            assert_error(function()
                client:set('foo', 'bar')
                client:hincrbyfloat('foo', 'bar', 1.10)
            end)
        end)

        test("HKEYS (client:hkeys)", function()
            if version:is('<', '2.0.0') then return end

            local hashKVs = { foo = 'bar', hoge = 'piyo' }
            assert_true(client:hmset('metavars', hashKVs))

            assert_table_values(client:hkeys('metavars'), table.keys(hashKVs))
            assert_table_values(client:hkeys('hashDoesNotExist'), { })

            assert_error(function()
                client:set('foo', 'bar')
                client:hkeys('foo')
            end)
        end)

        test("HVALS (client:hvals)", function()
            if version:is('<', '2.0.0') then return end

            local hashKVs = { foo = 'bar', hoge = 'piyo' }
            assert_true(client:hmset('metavars', hashKVs))

            assert_table_values(client:hvals('metavars'), table.values(hashKVs))
            assert_table_values(client:hvals('hashDoesNotExist'), { })

            assert_error(function()
                client:set('foo', 'bar')
                client:hvals('foo')
            end)
        end)

        test("HGETALL (client:hgetall)", function()
            if version:is('<', '2.0.0') then return end

            local hashKVs = { foo = 'bar', hoge = 'piyo' }
            assert_true(client:hmset('metavars', hashKVs))

            assert_true(table.compare(client:hgetall('metavars'), hashKVs))
            assert_true(table.compare(client:hgetall('hashDoesNotExist'), { }))

            assert_error(function()
                client:set('foo', 'bar')
                client:hgetall('foo')
            end)
        end)

        test("HSCAN (client:hscan)", function()
            if version:is('<', '2.8.0') then return end

            local args = {
                ['field:1st'] = 2, ['field:2nd'] = 2,
                ['field:3rd'] = 3, ['field:4th'] = 4,
            }

            client:hmset('key:hash', args)

            local cursor, kvs = unpack(client:hscan('key:hash', 0))
            assert_type(cursor, 'string')
            assert_type(kvs, 'table')
            assert_true(table.compare(kvs, args))

            local cursor, kvs = unpack(client:hscan('key:hash', 0, {
                match = 'field:*d', count = 10
            }))
            assert_type(cursor, 'string')
            assert_type(kvs, 'table')
            assert_table_values(kvs, { ['field:2nd'] = 2, ['field:3rd'] = 3 })
        end)
    end)

    context("Remote server control commands", function()
        test("INFO (client:info)", function()
            local info = client:info()
            assert_type(info, 'table')
            assert_not_nil(info.redis_version or info.server.redis_version)
        end)

        test("CONFIG GET (client:config)", function()
            if version:is('<', '2.0.0') then return end

            local config = client:config('get', '*')
            assert_type(config, 'table')
            assert_not_nil(config['list-max-ziplist-entries'])
            if version:is('>=', '2.4.0') then
                assert_not_nil(config.loglevel)
            end

            local config = client:config('get', '*max-*-entries*')
            assert_type(config, 'table')
            assert_not_nil(config['list-max-ziplist-entries'])
            if version:is('>=', '2.4.0') then
                assert_nil(config.loglevel)
            end
        end)

        test("CONFIG SET (client:config)", function()
            if version:is('<', '2.4.0') then return end

            local new, previous = 'notice', client:config('get', 'loglevel').loglevel

            assert_type(previous, 'string')

            assert_true(client:config('set', 'loglevel', new))
            assert_equal(client:config('get', 'loglevel').loglevel, new)

            assert_true(client:config('set', 'loglevel', previous))
        end)

        test("CONFIG RESETSTAT (client:config)", function()
            assert_true(client:config('resetstat'))
        end)

        test("SLOWLOG RESET (client:slowlog)", function()
            if version:is('<', '2.2.12') then return end

            assert_true(client:slowlog('reset'))
        end)

        test("SLOWLOG GET (client:slowlog)", function()
            if version:is('<', '2.2.12') then return end

            local previous = client:config('get', 'slowlog-log-slower-than')['slowlog-log-slower-than']

            client:config('set', 'slowlog-log-slower-than', 0)
            client:set('foo', 'bar')
            client:del('foo')

            local log = client:slowlog('get')
            assert_type(log, 'table')
            assert_greater_than(#log, 0)

            assert_type(log[1], 'table')
            assert_greater_than(log[1].id, 0)
            assert_greater_than(log[1].timestamp, 0)
            assert_greater_than(log[1].duration, 0)
            assert_type(log[1].command, 'table')

            local log = client:slowlog('get', 1)
            assert_type(log, 'table')
            assert_equal(#log, 1)

            client:config('set', 'slowlog-log-slower-than', previous or 10000)
        end)

        test("TIME (client:time)", function()
            if version:is('<', '2.5.0') then return end

            local redis_time = client:time()
            assert_type(redis_time, 'table')
            assert_not_nil(redis_time[1])
            assert_not_nil(redis_time[2])
        end)

        test("CLIENT (client:client)", function()
            if version:is('<', '2.4.0') then return end
            -- TODO: implement tests
        end)

        test("LASTSAVE (client:lastsave)", function()
            assert_not_nil(client:lastsave())
        end)

        test("FLUSHDB (client:flushdb)", function()
            assert_true(client:flushdb())
        end)
    end)

    context("Transactions", function()
        test("MULTI / EXEC (client:multi, client:exec)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:multi())
            assert_response_queued(client:ping())
            assert_response_queued(client:echo('hello'))
            assert_response_queued(client:echo('redis'))
            assert_table_values(client:exec(), { 'PONG', 'hello', 'redis' })

            assert_true(client:multi())
            assert_table_values(client:exec(), {})

            -- should raise an error when trying to EXEC without having previously issued MULTI
            assert_error(function() client:exec() end)
        end)
        test("DISCARD (client:discard)", function()
            if version:is('<', '2.0.0') then return end

            assert_true(client:multi())
            assert_response_queued(client:set('foo', 'bar'))
            assert_response_queued(client:set('hoge', 'piyo'))
            assert_true(client:discard())

            -- should raise an error when trying to EXEC after a DISCARD
            assert_error(function() client:exec() end)

            assert_false(client:exists('foo'))
            assert_false(client:exists('hoge'))
        end)

        test("WATCH", function()
            if version:is('<', '2.1.0') then return end

            local client2 = utils.create_client(settings)
            assert_true(client:set('foo', 'bar'))
            assert_true(client:watch('foo'))
            assert_true(client:multi())
            assert_response_queued(client:get('foo'))
            assert_true(client2:set('foo', 'hijacked'))
            assert_nil(client:exec())
        end)

        test("UNWATCH", function()
            if version:is('<', '2.1.0') then return end

            local client2 = utils.create_client(settings)
            assert_true(client:set('foo', 'bar'))
            assert_true(client:watch('foo'))
            assert_true(client:unwatch())
            assert_true(client:multi())
            assert_response_queued(client:get('foo'))
            assert_true(client2:set('foo', 'hijacked'))
            assert_table_values(client:exec(), { 'hijacked' })
        end)

        test("MULTI / EXEC / DISCARD abstraction", function()
            if version:is('<', '2.0.0') then return end

            local replies, processed

            replies, processed = client:transaction(function(t)
                -- empty transaction
            end)
            assert_table_values(replies, { })
            assert_equal(processed, 0)

            replies, processed = client:transaction(function(t)
                t:discard()
            end)
            assert_table_values(replies, { })
            assert_equal(processed, 0)

            replies, processed = client:transaction(function(t)
                assert_response_queued(t:set('foo', 'bar'))
                assert_true(t:discard())
                assert_response_queued(t:ping())
                assert_response_queued(t:echo('hello'))
                assert_response_queued(t:echo('redis'))
                assert_response_queued(t:exists('foo'))
            end)
            assert_table_values(replies, { true, 'hello', 'redis', false })
            assert_equal(processed, 4)

            -- clean up transaction after client-side errors
            assert_error(function()
                client:transaction(function(t)
                    t:lpush('metavars', 'foo')
                    error('whoops!')
                    t:lpush('metavars', 'hoge')
                end)
            end)
            assert_false(client:exists('metavars'))
        end)

        test("WATCH / MULTI / EXEC abstraction", function()
            if version:is('<', '2.1.0') then return end

            local redis2 = utils.create_client(settings)
            local watch_keys = { 'foo' }

            local replies, processed = client:transaction(watch_keys, function(t)
                -- empty transaction
            end)
            assert_table_values(replies, { })
            assert_equal(processed, 0)

            assert_error(function()
                client:transaction(watch_keys, function(t)
                    t:set('foo', 'bar')
                    redis2:set('foo', 'hijacked')
                    t:get('foo')
                end)
            end)
        end)

        test("WATCH / MULTI / EXEC with check-and-set (CAS) abstraction", function()
            if version:is('<', '2.1.0') then return end

            local opts, replies, processed

            opts = { cas = 'foo' }
            replies, processed = client:transaction(opts, function(t)
                -- empty transaction (with missing call to t:multi())
            end)
            assert_table_values(replies, { })
            assert_equal(processed, 0)

            opts = { watch = 'foo', cas = true }
            replies, processed = client:transaction(opts, function(t)
                t:multi()
                -- empty transaction
            end)
            assert_table_values(replies, { })
            assert_equal(processed, 0)

            local redis2 = utils.create_client(settings)
            local n = 5
            opts = { watch = 'foobarr', cas = true, retry = 5 }
            replies, processed = client:transaction(opts, function(t)
                t:set('foobar', 'bazaar')
                local val = t:get('foobar')
                t:multi()
                assert_response_queued(t:set('discardable', 'bar'))
                assert_equal(t:commands_queued(), 1)
                assert_true(t:discard())
                assert_response_queued(t:ping())
                assert_equal(t:commands_queued(), 1)
                assert_response_queued(t:echo('hello'))
                assert_response_queued(t:echo('redis'))
                assert_equal(t:commands_queued(), 3)
                if n>0 then
                    n = n-1
                    redis2:set("foobarr", n)
                end
                assert_response_queued(t:exists('foo'))
                assert_response_queued(t:get('foobar'))
                assert_response_queued(t:get('foobarr'))
            end)
            assert_table_values(replies, { true, 'hello', 'redis', false, "bazaar", '0' })
            assert_equal(processed, 6)
        end)

        test("Abstraction options", function()
            -- TODO: more in-depth tests (proxy calls to WATCH)
            local opts, replies, processed
            local tx_empty = function(t) end
            local tx_cas_empty = function(t) t:multi() end

            replies, processed = client:transaction(tx_empty)
            assert_table_values(replies, { })

            assert_error(function()
                client:transaction(opts, tx_empty)
            end)

            opts = 'foo'
            replies, processed = client:transaction(opts, tx_empty)
            assert_table_values(replies, { })
            assert_equal(processed, 0)

            opts = { 'foo', 'bar' }
            replies, processed = client:transaction(opts, tx_empty)
            assert_equal(processed, 0)

            opts = { watch = 'foo' }
            replies, processed = client:transaction(opts, tx_empty)
            assert_equal(processed, 0)

            opts = { watch = { 'foo', 'bar' } }
            replies, processed = client:transaction(opts, tx_empty)
            assert_equal(processed, 0)

            opts = { cas = true }
            replies, processed = client:transaction(opts, tx_cas_empty)
            assert_equal(processed, 0)

            opts = { 'foo', 'bar', cas = true }
            replies, processed = client:transaction(opts, tx_cas_empty)
            assert_equal(processed, 0)

            opts = { 'foo', nil, 'bar', cas = true }
            replies, processed = client:transaction(opts, tx_cas_empty)
            assert_equal(processed, 0)

            opts = { watch = { 'foo', 'bar' }, cas = true }
            replies, processed = client:transaction(opts, tx_cas_empty)
            assert_equal(processed, 0)

            opts = { nil, cas = true }
            replies, processed = client:transaction(opts, tx_cas_empty)
            assert_equal(processed, 0)
        end)
    end)

    context("Pub/Sub", function()
        test('PUBLISH (client:publish)', function()
            assert_equal(client:publish('redis-lua-publish', 'test'), 0)
        end)

        test('SUBSCRIBE (client:subscribe)', function()
            client:subscribe('redis-lua-publish')

            -- we have one subscriber
            data = 'data' .. tostring(math.random(1000))
            publisher = utils.create_client(settings)
            assert_equal(publisher:publish('redis-lua-publish', data), 1)
            -- we have data
            response = client:subscribe('redis-lua-publish')
            -- {"message","redis-lua-publish","testXXX"}
            assert_true(table.contains(response, 'message'))
            assert_true(table.contains(response, 'redis-lua-publish'))
            assert_true(table.contains(response, data))

            client:unsubscribe('redis-lua-publish')
        end)
    end)

    context("Scripting", function()
        test('EVAL (client:eval)', function()
            if version:is('<', '2.5.0') then return end
        end)

        test('EVALSHA (client:evalsha)', function()
            if version:is('<', '2.5.0') then return end
        end)

        test('SCRIPT (client:script)', function()
            if version:is('<', '2.5.0') then return end
        end)
    end)
end)

redpajama_set_name: RedPajamaGithub


This sweet Kenny K image named Jasmine can be useful for nearly all occasions. You can find Jasmine as a rubberstamp at the Whimsy Stamps online store here.
I've used Jasmine on an easel card to share happy wishes. Here's a closer view of my Copic-filled image. A layer of Diamond Stickles adds shimmer to her shirt.
Layers of Bazzill cardstock with SEI Diane Fresh Linen double-sided patterned paper form my easel card and faux stitching adds a contrasting border. A few Recollections black mini brads are lined in two corners. A Morex Corp 1 3/8" white lace ribbon provides a grounding border. A happiness sentiment from a Stampin' Up! Summer by the Sea set is stamped near my image. Cheery Lynn Designs Fanciful Flourish and Mini Fancy Flourish are layered with My Mind's Eye Lost and Found Blush "Sweet" Pearl Swirls. Flower clusters are formed with Prima's Gillian Raspberry and Fairytale Roses Baby Pink with Petaloo Dazzlers Florettes in fuschia.
Here's a view of the front of my card showing how I've used Jasmine.
The base of my card includes a stamped sentiment from a Stampin' Up! For Father set, a strip of KaiserCraft Pink Champagne pearls and a Creative Charms pink netted butterfly.
Here's a view of my easel card, open and ready for display . . .
Gorgeous easel card! Love it! Thanks for joining the challenge at Wags 'n Whiskers!
Great card! Fabulous coloring and beautiful flowers! Thanks for joining us for the Simon Says Stamp Challenge!
A fabulous easel-card, wonderful image and elements. Thanks for joining us at Fashionable Stamping Challenges this fortnight.
Absolutely gorgeous, fabulous image and love your flowers and lace...Thanks for joining us at Totally Papercrafts this week.
Such a beautiful card. I love the pretty image, wonderful colouring and all your detailing is so gorgeous. The sentiment is so special too.
Beautiful, great sentiment! Glad you joined us at Sweet Stampin'.
That's a beautiful sentiment you've used on this great card.
Hi Rosie...wow!!!! Love this! Very well balanced and the colours...perfect!
This is such a gorgeous image and love the pretty soft colours you have used and all the lovely embellishments. Really stunning card.
Crafts and Me Stamps Challenge: Bling It Up!


My condolences to the people of Norway, Oslo and the family of those young humans who were mercilessly gunned down or bombed to death.
This latest terrorist attack in Oslo just goes on to prove the fact that terrorists are actually lunatics. Lunatics who rightfully don't belong to any race or religion.
State terrorism practised by the Zionist Regime in the Middle East is the mother of all terrorism that is perpetuated by a bunch of lunatics who have hijacked Israel from the Jews.
Over time those victims of state terrorism turn out to be terrorists themselves and we have the suicide bombers everywhere.
The merciless killings in Oslo by a lone (non-Muslim) terrorist is yet another eye opener that terrorism is not the sole right or exclusive product of the Muslim fundamentalist.
Terrorism has become the weapon for lunatics and these days there are more lunatics in developed countries than the third world. Yes, the massacre in Oslo is an example.
No race or religion promotes terrorism or terrorist. FULL STOP.
In this case and in general, what do you wish the media did better? And what would you want readers/listeners/viewers to know?
The media needs to hold its horses…. People don't think that this speculation in mainstream media has any consequence. After all, they are "only" speculating. But in fact, most people cannot distinguish between speculative news, and factual news. Fortunately, there were no significant anti-Muslim backlash as Norway is generally a more educated, open-minded country. Had this happened in the States, we might have had some immediate serious repercussions for Muslims.
I twitted with a provocative question - "Is this Christian terrorism?", AFTER finding out the facts of the incident and details of the lone Christian terrorist.
The intention was to create the awareness or rather educate our NON-MUSLIM brethren and some smart MUSLIMs that not all terrorist are Muslims, and that there are enough lunatics among Christians too.
Guess if you're a reporter/journalist or blogger with some pride in what you do and quality in your work then you'll hold your horses and check your facts before SHOOTING in the DARK.
The Oslo terrorist is a Christian, and I believe he is a lunatic.
I was told that there are some lunatics here who are adamant about criticizing Foreign Minister Datuk Seri Anifah Aman for his presumably 'late delivery' of condolences to the people of Norway.
Come'on guys, is there now a competition somewhere for 'Who says its FIRST"?
Again, like good journalist/reporter and/or blogger, I think Ministers too should take the time to check the facts before blasting their horns.
p/s If Ministers make it a habit to shoot their mouth off about something that they are not sure of, we may then need a special Consultative COUNCIL to evaluate their performance and help overcome the after effects.
Enjoy la your moment of triumph of trying to whitewash terrorism being the sole preorgative of Moslems and Christians and thus the deep guilt of whatever religion you profess which does not match the perpetrator is somehow lessened or villified.
I do agree one thing though. It's the indoctrination of religious or political extremism into dumb fools which makes them turn into terrorists and kill unwantonly for their own misguided fame or fortune awaiting them.
So, is racism also the same as terrorism when brought to the extreme?
List down the 10 most troublesome spots in the world and you will get your answer. Then you can spin that too for your bosses.