pax_global_header00006660000000000000000000000064122207514020014505gustar00rootroot0000000000000052 comment=b325403954dbbcf77fc12adfc2e442f671aabebb lua-redis-2.0.4/000077500000000000000000000000001222075140200133755ustar00rootroot00000000000000lua-redis-2.0.4/.gitignore000066400000000000000000000000161222075140200153620ustar00rootroot00000000000000experiments/* lua-redis-2.0.4/CHANGELOG000066400000000000000000000076411222075140200146170ustar00rootroot00000000000000v2.0.4 (2012-07-15) * The library is now fully compatible with Lua 5.2. * Implemented some small optimizations in the handling of the Redis protocol. * Client instances can be initialized with an already connected socket using the `socket` field in the parameters table (it takes the precedence over other fields such as `host`, `port` or `path`). * The module now exposes its default command table in the `commands` field. * It is available a new way to define custom Redis commands on a module or client-level instance. The old way is still in place but it is considered deprecated and will be removed in the next major release. For more details see `examples/define_commands.lua` and the test suite. v2.0.3 (2012-04-01) * It is now possible to require redis-lua by assigning the returned module to a local variable like `local redis = require 'redis'`. Starting with this release this is the preferred way to require redis-lua. The `Redis` global name is still available for backwards compatibility but it will be removed in the next major version. * Changed to using an error function per client instead of a global one. * Added an abstraction for MONITOR that makes possible to consume messages the messages returned by Redis using a coroutine-based iterator. * Implemented all the new commands of Redis 2.6. * Implemented `CONFIG GET`, `CONFIG SET`, `CONFIG RESETSTAT` and `SLOWLOG`. * In order to support the variadic flavor of certain commands implemented in Redis >= 2.4, now `SADD`, `SREM`, `ZADD`, `ZREM` and `HDEL` now return the number of elements involved by the respective operation instead of a boolean value. * Fixed the parsing of INFO replies returned by Redis 2.6. v2.0.2 (2011-06-20) * Added an abstraction for PUB/SUB that makes possible to consume messages pushed to channels using a coroutine-based iterator. * Added support for connecting to Redis using UNIX domain sockets when they are available in LuaSocket. v2.0.1 (2011-01-24) * Vastly improved abstraction for Redis transactions (MULTI/EXEC) supporting check-and-set (CAS), automatic retries upon transaction failures and a few optional arguments for initialization (enable CAS support, list of keys to watch automatically and number of attempts upon failed transactions). The public interface is completely backwards compatible with previous versions. * Pipelines and transactions return the number of commands processed as the second return value, useful for iterating over a returned list of replies that contains holes (nil values). * Since SORT can accept multiple GET parameters, redis:sort() has been modified accordingly to accept either a string or a table for the 'get' parameter. v2.0.0 (2010-11-27) * The client library is no longer compatible with Redis 1.0. * Support for long names of Redis commands has been dropped, the client now uses the same command names as defined by Redis. * Inline and bulk requests are not supported anymore and the related code has been removed from the library. Commands are defined as multibulk requests by default. * The public interface for pipelining has been slightly changed (see the examples/pipeline.lua file for more details). * The public interface for Redis transactions (MULTI/EXEC) basically works in the same way of pipelining. * Developers can now define their own commands at module level and not only on client instances. v1.0.1 (2010-07-30) * Providing a more generalized version of the multibulk request serializer. * _G is now passed as the argument of a pipeline block. This will change in a future major release of redis-lua, but for now it is useful to enable the usage of global functions inside of a pipeline block. * Fix: user-added commands were not available when pipelining commands. v1.0.0 (2010-06-02) * First versioned release of redis-lua lua-redis-2.0.4/LICENSE000066400000000000000000000020531222075140200144020ustar00rootroot00000000000000Copyright (c) 2009-2012 Daniele Alessandri Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lua-redis-2.0.4/README.markdown000066400000000000000000000111731222075140200161010ustar00rootroot00000000000000# redis-lua # ## About ## redis-lua is a pure Lua client library for the Redis advanced key-value database. ## Main features ## - Support for Redis >= 1.2 - Command pipelining - Redis transactions (MULTI/EXEC) with CAS - User-definable commands - UNIX domain sockets (when available in LuaSocket) ## Compatibility ## This library is tested and works with __Lua 5.1__, __Lua 5.2__ (using a compatible version of LuaSocket) and __LuaJit 2.0__. ## Examples of usage ## ### Include redis-lua in your script ### Just require the `redis` module assigning it to a variable: ``` lua local redis = require 'redis' ``` Previous versions of the library defined a global `Redis` alias as soon as the module was imported by the user. This global alias is still defined but it is considered deprecated and it will be removed in the next major version. ### Connect to a redis-server instance and send a PING command ### ``` lua local redis = require 'redis' local client = redis.connect('127.0.0.1', 6379) local response = client:ping() -- true ``` It is also possible to connect to a local redis instance using __UNIX domain sockets__ if LuaSocket has been compiled with them enabled (unfortunately it is not the default): ``` lua local redis = require 'redis' local client = redis.connect('unix:///tmp/redis.sock') ``` ### Set keys and get their values ### ``` lua client:set('usr:nrk', 10) client:set('usr:nobody', 5) local value = client:get('usr:nrk') -- 10 ``` ### Sort list values by using various parameters supported by the server ### ``` lua for _,v in ipairs({ 10,3,2,6,1,4,23 }) do client:rpush('usr:nrk:ids',v) end local sorted = client:sort('usr:nrk:ids', { sort = 'asc', alpha = true, limit = { 1, 5 } }) -- {1=10,2=2,3=23,4=3,5=4} ``` ### Pipeline commands ``` lua local replies = client:pipeline(function(p) p:incrby('counter', 10) p:incrby('counter', 30) p:get('counter') end) ``` ### Variadic commands Some commands such as RPUSH, SADD, SINTER and others have been improved in Redis 2.4 to accept a list of values or keys depending on the nature of the command. Sometimes it can be useful to pass these arguments as a list in a table, but since redis-lua does not currently do anything to handle such a case you can use `unpack()` albeit with a limitation on the maximum number of items which is defined in Lua by LUAI_MAXCSTACK (the default on Lua 5.1 is set to `8000`, see `luaconf.h`): ```lua local values = { 'value1', 'value2', 'value3' } client:rpush('list', unpack(values)) -- the previous line has the same effect of the following one: client:rpush('list', 'value1', 'value2', 'value3') ``` ### Leverage Redis MULTI / EXEC transaction (Redis > 2.0) ``` lua local replies = client:transaction(function(t) t:incrby('counter', 10) t:incrby('counter', 30) t:get('counter') end) ``` ### Leverage WATCH / MULTI / EXEC for check-and-set (CAS) operations (Redis > 2.2) ``` lua local options = { watch = "key_to_watch", cas = true, retry = 2 } local replies = client:transaction(options, function(t) local val = t:get("key_to_watch") t:multi() t:set("akey", val) t:set("anotherkey", val) end) ``` ### Get useful information from the server ### ``` lua for k,v in pairs(client:info()) do print(k .. ' => ' .. tostring(v)) end --[[ redis_git_dirty => 0 redis_git_sha1 => aaed0894 process_id => 23115 vm_enabled => 0 hash_max_zipmap_entries => 64 expired_keys => 9 changes_since_last_save => 2 role => master last_save_time => 1283621624 used_memory => 537204 bgsave_in_progress => 0 redis_version => 2.0.0 multiplexing_api => epoll total_connections_received => 314 db0 => {keys=3,expires=0} pubsub_patterns => 0 used_memory_human => 524.61K pubsub_channels => 0 uptime_in_seconds => 1033 connected_slaves => 0 connected_clients => 1 bgrewriteaof_in_progress => 0 blocked_clients => 0 arch_bits => 32 total_commands_processed => 3982 hash_max_zipmap_value => 512 db15 => {keys=1,expires=0} uptime_in_days => 0 ]] ``` ## Dependencies ## - [Lua 5.1 and 5.2](http://www.lua.org/) or [LuaJIT 2.0](http://luajit.org/) - [LuaSocket 2.0](http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/) - [Telescope](http://telescope.luaforge.net/) (required to run the test suite) ## Links ## ### Project ### - [Source code](http://github.com/nrk/redis-lua/) - [Issue tracker](http://github.com/nrk/redis-lua/issues) ### Related ### - [Redis](http://redis.io/) - [Git](http://git-scm.com/) ## Authors ## [Daniele Alessandri](mailto:suppakilla@gmail.com) ### Contributors ### [Leo Ponomarev](http://github.com/slact/) ## License ## The code for redis-lua is distributed under the terms of the MIT/X11 license (see LICENSE). lua-redis-2.0.4/VERSION000066400000000000000000000000061222075140200144410ustar00rootroot000000000000002.0.4 lua-redis-2.0.4/examples/000077500000000000000000000000001222075140200152135ustar00rootroot00000000000000lua-redis-2.0.4/examples/define_commands.lua000066400000000000000000000022061222075140200210310ustar00rootroot00000000000000package.path = "../src/?.lua;src/?.lua;" .. package.path pcall(require, "luarocks.require") local redis = require 'redis' local params = { host = '127.0.0.1', port = 6379, } -- commands defined in the redis.commands table are available at module -- level and are used to populate each new client instance. redis.commands.hset = redis.command('hset') -- you can also specify a response callback to parse raw replies redis.commands.hgetall = redis.command('hgetall', { response = function(reply, command, ...) local new_reply = { } for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end return new_reply end }) local client = redis.connect(params) client:select(15) -- for testing purposes client:hset('user:1000', 'name', 'John Doe') client:hset('user:1000', 'nickname', 'anonymous') client:hset('user:1000', 'email', 'anything@anywhere.tld') local user = client:hgetall('user:1000') print(string.format('%s is also known as %s and his email address is %s.', user.name, user.nickname, user.email )) --[[ OUTPUT: John Doe is also known as anonymous and his email address is anything@anywhere.tld. ]] lua-redis-2.0.4/examples/monitor.lua000066400000000000000000000016161222075140200174110ustar00rootroot00000000000000package.path = "../src/?.lua;src/?.lua;" .. package.path pcall(require, "luarocks.require") local redis = require 'redis' local params = { host = '127.0.0.1', port = 6379, } local client = redis.connect(params) client:select(15) -- for testing purposes -- Start processing the monitor messages. Open a terminal and use redis-cli to -- send some commands to the server that will make MONITOR return some entries. local counter = 0 for msg, abort in client:monitor_messages() do counter = counter + 1 local feedback = string.format("[%d] Received %s on database %d", msg.timestamp, msg.command, msg.database) if msg.arguments then feedback = string.format('%s with arguments %s', feedback, msg.arguments) end print(feedback) if counter == 5 then abort() end end print(string.format("Closed the MONITOR context after receiving %d commands.", counter)) lua-redis-2.0.4/examples/pipeline.lua000066400000000000000000000012111222075140200175160ustar00rootroot00000000000000package.path = "../src/?.lua;src/?.lua;" .. package.path pcall(require, "luarocks.require") local redis = require 'redis' local params = { host = '127.0.0.1', port = 6379, } local client = redis.connect(params) client:select(15) -- for testing purposes local replies = client:pipeline(function(p) p:ping() p:flushdb() 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() end) for _, reply in pairs(replies) do print('*', reply) end lua-redis-2.0.4/examples/pubsub.lua000066400000000000000000000021761222075140200172240ustar00rootroot00000000000000package.path = "../src/?.lua;src/?.lua;" .. package.path pcall(require, "luarocks.require") local redis = require 'redis' local params = { host = '127.0.0.1', port = 6379, } local client = redis.connect(params) client:select(15) -- for testing purposes local channels = { 'control_channel', 'notifications' } -- Start processing the pubsup messages. Open a terminal and use redis-cli -- to push messages to the channels. Examples: -- ./redis-cli PUBLISH notifications "this is a test" -- ./redis-cli PUBLISH control_channel quit_loop for msg, abort in client:pubsub({ subscribe = channels }) do if msg.kind == 'subscribe' then print('Subscribed to channel '..msg.channel) elseif msg.kind == 'message' then if msg.channel == 'control_channel' then if msg.payload == 'quit_loop' then print('Aborting pubsub loop...') abort() else print('Received an unrecognized command: '..msg.payload) end else print('Received the following message from '..msg.channel.."\n "..msg.payload.."\n") end end end lua-redis-2.0.4/examples/simple.lua000066400000000000000000000005201222075140200172040ustar00rootroot00000000000000package.path = "../src/?.lua;src/?.lua;" .. package.path pcall(require, "luarocks.require") local redis = require 'redis' local params = { host = '127.0.0.1', port = 6379, } local client = redis.connect(params) client:select(15) -- for testing purposes client:set('foo', 'bar') local value = client:get('foo') print(value) lua-redis-2.0.4/examples/transaction.lua000066400000000000000000000013741222075140200202500ustar00rootroot00000000000000package.path = "../src/?.lua;src/?.lua;" .. package.path pcall(require, "luarocks.require") local redis = require 'redis' local params = { host = '127.0.0.1', port = 6379, } local client = redis.connect(params) client:select(15) -- for testing purposes local replies = client:transaction(function(t) t:incrby('counter', 10) t:incrby('counter', 30) t:decrby('counter', 15) end) -- check-and-set (CAS) client:set('foo', 'bar') local replies = client:transaction({ watch = 'foo', cas = true }, function(t) --executed after WATCH but before MULTI local val = t:get('foo') t:multi() --executing during MULTI block t:set('foo', 'foo' .. val) t:get('foo') end) for _, reply in pairs(replies) do print('*', reply) end lua-redis-2.0.4/rockspec/000077500000000000000000000000001222075140200152065ustar00rootroot00000000000000lua-redis-2.0.4/rockspec/redis-lua-1.0.1-0.rockspec000066400000000000000000000011161222075140200214150ustar00rootroot00000000000000package = "redis-lua" version = "1.0.1-0" source = { url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-1.0.1-0.tar.gz", md5 = "0e00178a8bc7d68d463007eec49117d5" } description = { summary = "A Lua client library for the redis key value storage system.", detailed = [[ A Lua client library for the redis key value storage system. ]], homepage = "http://github.com/nrk/redis-lua", license = "MIT/X11" } dependencies = { "lua >= 5.1", "luasocket" } build = { type = "none", install = { lua = { "redis.lua" } } } lua-redis-2.0.4/rockspec/redis-lua-2.0.0-0.rockspec000066400000000000000000000011321222075140200214130ustar00rootroot00000000000000package = "redis-lua" version = "2.0.0-0" source = { url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.0-0.tar.gz", md5 = "db1f9a74d13158c1b551a4fa054a92ba" } description = { summary = "A Lua client library for the redis key value storage system.", detailed = [[ A Lua client library for the redis key value storage system. ]], homepage = "http://github.com/nrk/redis-lua", license = "MIT/X11" } dependencies = { "lua >= 5.1", "luasocket" } build = { type = "none", install = { lua = { redis = "src/redis.lua" } } } lua-redis-2.0.4/rockspec/redis-lua-2.0.1-0.rockspec000066400000000000000000000011321222075140200214140ustar00rootroot00000000000000package = "redis-lua" version = "2.0.1-0" source = { url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.1-0.tar.gz", md5 = "824c9bd4e98b919747c6f1f3be322196" } description = { summary = "A Lua client library for the redis key value storage system.", detailed = [[ A Lua client library for the redis key value storage system. ]], homepage = "http://github.com/nrk/redis-lua", license = "MIT/X11" } dependencies = { "lua >= 5.1", "luasocket" } build = { type = "none", install = { lua = { redis = "src/redis.lua" } } } lua-redis-2.0.4/rockspec/redis-lua-2.0.2-0.rockspec000066400000000000000000000011321222075140200214150ustar00rootroot00000000000000package = "redis-lua" version = "2.0.2-0" source = { url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.2-0.tar.gz", md5 = "4fcfd73761f47470c59a30c3818bee97" } description = { summary = "A Lua client library for the redis key value storage system.", detailed = [[ A Lua client library for the redis key value storage system. ]], homepage = "http://github.com/nrk/redis-lua", license = "MIT/X11" } dependencies = { "lua >= 5.1", "luasocket" } build = { type = "none", install = { lua = { redis = "src/redis.lua" } } } lua-redis-2.0.4/rockspec/redis-lua-2.0.3-0.rockspec000066400000000000000000000011321222075140200214160ustar00rootroot00000000000000package = "redis-lua" version = "2.0.3-0" source = { url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.3-0.tar.gz", md5 = "28b370247c63a9cfd9e346e57c1a7f7a" } description = { summary = "A Lua client library for the redis key value storage system.", detailed = [[ A Lua client library for the redis key value storage system. ]], homepage = "http://github.com/nrk/redis-lua", license = "MIT/X11" } dependencies = { "lua >= 5.1", "luasocket" } build = { type = "none", install = { lua = { redis = "src/redis.lua" } } } lua-redis-2.0.4/rockspec/redis-lua-2.0.4-0.rockspec000066400000000000000000000011321222075140200214170ustar00rootroot00000000000000package = "redis-lua" version = "2.0.4-0" source = { url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.4-0.tar.gz", md5 = "46e962a4f5361c82473ccd33d4b18003" } description = { summary = "A Lua client library for the redis key value storage system.", detailed = [[ A Lua client library for the redis key value storage system. ]], homepage = "http://github.com/nrk/redis-lua", license = "MIT/X11" } dependencies = { "lua >= 5.1", "luasocket" } build = { type = "none", install = { lua = { redis = "src/redis.lua" } } } lua-redis-2.0.4/rockspec/redis-lua-scm-1.rockspec000066400000000000000000000010051222075140200215400ustar00rootroot00000000000000package = "redis-lua" version = "scm-1" source = { url = "git://github.com/nrk/redis-lua.git" } description = { summary = "A Lua client library for the redis key value storage system.", detailed = [[ A Lua client library for the redis key value storage system. ]], homepage = "http://github.com/nrk/redis-lua", license = "MIT/X11" } dependencies = { "lua >= 5.1", "luasocket" } build = { type = "none", install = { lua = { redis = "src/redis.lua" } } } lua-redis-2.0.4/src/000077500000000000000000000000001222075140200141645ustar00rootroot00000000000000lua-redis-2.0.4/src/redis.lua000066400000000000000000001054171222075140200160050ustar00rootroot00000000000000local redis = { _VERSION = 'redis-lua 2.0.4', _DESCRIPTION = 'A Lua client library for the redis key value storage system.', _COPYRIGHT = 'Copyright (C) 2009-2012 Daniele Alessandri', } -- The following line is used for backwards compatibility in order to keep the `Redis` -- global module name. Using `Redis` is now deprecated so you should explicitly assign -- the module to a local variable when requiring it: `local redis = require('redis')`. Redis = redis local unpack = _G.unpack or table.unpack local network, request, response = {}, {}, {} local defaults = { host = '127.0.0.1', port = 6379, tcp_nodelay = true, path = nil } local function merge_defaults(parameters) if parameters == nil then parameters = {} end for k, v in pairs(defaults) do if parameters[k] == nil then parameters[k] = defaults[k] end end return parameters end local function parse_boolean(v) if v == '1' or v == 'true' or v == 'TRUE' then return true elseif v == '0' or v == 'false' or v == 'FALSE' then return false else return nil end end local function toboolean(value) return value == 1 end local function sort_request(client, command, key, params) --[[ params = { by = 'weight_*', get = 'object_*', limit = { 0, 10 }, sort = 'desc', alpha = true, } ]] local query = { key } if params then if params.by then table.insert(query, 'BY') table.insert(query, params.by) end if type(params.limit) == 'table' then -- TODO: check for lower and upper limits table.insert(query, 'LIMIT') table.insert(query, params.limit[1]) table.insert(query, params.limit[2]) end if params.get then if (type(params.get) == 'table') then for _, getarg in pairs(params.get) do table.insert(query, 'GET') table.insert(query, getarg) end else table.insert(query, 'GET') table.insert(query, params.get) end end if params.sort then table.insert(query, params.sort) end if params.alpha == true then table.insert(query, 'ALPHA') end if params.store then table.insert(query, 'STORE') table.insert(query, params.store) end end request.multibulk(client, command, query) end local function zset_range_request(client, command, ...) local args, opts = {...}, { } if #args >= 1 and type(args[#args]) == 'table' then local options = table.remove(args, #args) if options.withscores then table.insert(opts, 'WITHSCORES') end end for _, v in pairs(opts) do table.insert(args, v) end request.multibulk(client, command, args) end local function zset_range_byscore_request(client, command, ...) local args, opts = {...}, { } if #args >= 1 and type(args[#args]) == 'table' then local options = table.remove(args, #args) if options.limit then table.insert(opts, 'LIMIT') table.insert(opts, options.limit.offset or options.limit[1]) table.insert(opts, options.limit.count or options.limit[2]) end if options.withscores then table.insert(opts, 'WITHSCORES') end end for _, v in pairs(opts) do table.insert(args, v) end request.multibulk(client, command, args) end local function zset_range_reply(reply, command, ...) local args = {...} local opts = args[4] if opts and (opts.withscores or string.lower(tostring(opts)) == 'withscores') then local new_reply = { } for i = 1, #reply, 2 do table.insert(new_reply, { reply[i], reply[i + 1] }) end return new_reply else return reply end end local function zset_store_request(client, command, ...) local args, opts = {...}, { } if #args >= 1 and type(args[#args]) == 'table' then local options = table.remove(args, #args) if options.weights and type(options.weights) == 'table' then table.insert(opts, 'WEIGHTS') for _, weight in ipairs(options.weights) do table.insert(opts, weight) end end if options.aggregate then table.insert(opts, 'AGGREGATE') table.insert(opts, options.aggregate) end end for _, v in pairs(opts) do table.insert(args, v) end request.multibulk(client, command, args) end local function mset_filter_args(client, command, ...) local args, arguments = {...}, {} if (#args == 1 and type(args[1]) == 'table') then for k,v in pairs(args[1]) do table.insert(arguments, k) table.insert(arguments, v) end else arguments = args end request.multibulk(client, command, arguments) end local function hash_multi_request_builder(builder_callback) return function(client, command, ...) local args, arguments = {...}, { } if #args == 2 then table.insert(arguments, args[1]) for k, v in pairs(args[2]) do builder_callback(arguments, k, v) end else arguments = args end request.multibulk(client, command, arguments) end end local function parse_info(response) local info = {} local current = info response:gsub('([^\r\n]*)\r\n', function(kv) if kv == '' then return end local section = kv:match('^# (%w+)$') if section then current = {} info[section:lower()] = current return end local k,v = kv:match(('([^:]*):([^:]*)'):rep(1)) if k:match('db%d+') then current[k] = {} v:gsub(',', function(dbkv) local dbk,dbv = kv:match('([^:]*)=([^:]*)') current[k][dbk] = dbv end) else current[k] = v end end) return info end local function load_methods(proto, commands) local client = setmetatable ({}, getmetatable(proto)) for cmd, fn in pairs(commands) do if type(fn) ~= 'function' then redis.error('invalid type for command ' .. cmd .. '(must be a function)') end client[cmd] = fn end for i, v in pairs(proto) do client[i] = v end return client end local function create_client(proto, client_socket, commands) local client = load_methods(proto, commands) client.error = redis.error client.network = { socket = client_socket, read = network.read, write = network.write, } client.requests = { multibulk = request.multibulk, } return client end -- ############################################################################ function network.write(client, buffer) local _, err = client.network.socket:send(buffer) if err then client.error(err) end end function network.read(client, len) if len == nil then len = '*l' end local line, err = client.network.socket:receive(len) if not err then return line else client.error('connection error: ' .. err) end end -- ############################################################################ function response.read(client) local payload = client.network.read(client) local prefix, data = payload:sub(1, -#payload), payload:sub(2) -- status reply if prefix == '+' then if data == 'OK' then return true elseif data == 'QUEUED' then return { queued = true } else return data end -- error reply elseif prefix == '-' then return client.error('redis error: ' .. data) -- integer reply elseif prefix == ':' then local number = tonumber(data) if not number then if res == 'nil' then return nil end client.error('cannot parse '..res..' as a numeric response.') end return number -- bulk reply elseif prefix == '$' then local length = tonumber(data) if not length then client.error('cannot parse ' .. length .. ' as data length') end if length == -1 then return nil end local nextchunk = client.network.read(client, length + 2) return nextchunk:sub(1, -3) -- multibulk reply elseif prefix == '*' then local count = tonumber(data) if count == -1 then return nil end local list = {} if count > 0 then local reader = response.read for i = 1, count do list[i] = reader(client) end end return list -- unknown type of reply else return client.error('unknown response prefix: ' .. prefix) end end -- ############################################################################ function request.raw(client, buffer) local bufferType = type(buffer) if bufferType == 'table' then client.network.write(client, table.concat(buffer)) elseif bufferType == 'string' then client.network.write(client, buffer) else client.error('argument error: ' .. bufferType) end end function request.multibulk(client, command, ...) local args = {...} local argsn = #args local buffer = { true, true } if argsn == 1 and type(args[1]) == 'table' then argsn, args = #args[1], args[1] end buffer[1] = '*' .. tostring(argsn + 1) .. "\r\n" buffer[2] = '$' .. #command .. "\r\n" .. command .. "\r\n" local table_insert = table.insert for _, argument in pairs(args) do local s_argument = tostring(argument) table_insert(buffer, '$' .. #s_argument .. "\r\n" .. s_argument .. "\r\n") end client.network.write(client, table.concat(buffer)) end -- ############################################################################ local function custom(command, send, parse) command = string.upper(command) return function(client, ...) send(client, command, ...) local reply = response.read(client) if type(reply) == 'table' and reply.queued then reply.parser = parse return reply else if parse then return parse(reply, command, ...) end return reply end end end local function command(command, opts) if opts == nil or type(opts) == 'function' then return custom(command, request.multibulk, opts) else return custom(command, opts.request or request.multibulk, opts.response) end end local define_command_impl = function(target, name, opts) local opts = opts or {} target[string.lower(name)] = custom( opts.command or string.upper(name), opts.request or request.multibulk, opts.response or nil ) end local undefine_command_impl = function(target, name) target[string.lower(name)] = nil end -- ############################################################################ local client_prototype = {} client_prototype.raw_cmd = function(client, buffer) request.raw(client, buffer .. "\r\n") return response.read(client) end -- obsolete client_prototype.define_command = function(client, name, opts) define_command_impl(client, name, opts) end -- obsolete client_prototype.undefine_command = function(client, name) undefine_command_impl(client, name) end client_prototype.quit = function(client) request.multibulk(client, 'QUIT') client.network.socket:shutdown() return true end client_prototype.shutdown = function(client) request.multibulk(client, 'SHUTDOWN') client.network.socket:shutdown() end -- Command pipelining client_prototype.pipeline = function(client, block) local requests, replies, parsers = {}, {}, {} local table_insert = table.insert local socket_write, socket_read = client.network.write, client.network.read client.network.write = function(_, buffer) table_insert(requests, buffer) end -- TODO: this hack is necessary to temporarily reuse the current -- request -> response handling implementation of redis-lua -- without further changes in the code, but it will surely -- disappear when the new command-definition infrastructure -- will finally be in place. client.network.read = function() return '+QUEUED' end local pipeline = setmetatable({}, { __index = function(env, name) local cmd = client[name] if not cmd then client.error('unknown redis command: ' .. name, 2) end return function(self, ...) local reply = cmd(client, ...) table_insert(parsers, #requests, reply.parser) return reply end end }) local success, retval = pcall(block, pipeline) client.network.write, client.network.read = socket_write, socket_read if not success then client.error(retval, 0) end client.network.write(client, table.concat(requests, '')) for i = 1, #requests do local reply, parser = response.read(client), parsers[i] if parser then reply = parser(reply) end table_insert(replies, i, reply) end return replies, #requests end -- Publish/Subscribe do local channels = function(channels) if type(channels) == 'string' then channels = { channels } end return channels end local subscribe = function(client, ...) request.multibulk(client, 'subscribe', ...) end local psubscribe = function(client, ...) request.multibulk(client, 'psubscribe', ...) end local unsubscribe = function(client, ...) request.multibulk(client, 'unsubscribe') end local punsubscribe = function(client, ...) request.multibulk(client, 'punsubscribe') end local consumer_loop = function(client) local aborting, subscriptions = false, 0 local abort = function() if not aborting then unsubscribe(client) punsubscribe(client) aborting = true end end return coroutine.wrap(function() while true do local message local response = response.read(client) if response[1] == 'pmessage' then message = { kind = response[1], pattern = response[2], channel = response[3], payload = response[4], } else message = { kind = response[1], channel = response[2], payload = response[3], } end if string.match(message.kind, '^p?subscribe$') then subscriptions = subscriptions + 1 end if string.match(message.kind, '^p?unsubscribe$') then subscriptions = subscriptions - 1 end if aborting and subscriptions == 0 then break end coroutine.yield(message, abort) end end) end client_prototype.pubsub = function(client, subscriptions) if type(subscriptions) == 'table' then if subscriptions.subscribe then subscribe(client, channels(subscriptions.subscribe)) end if subscriptions.psubscribe then psubscribe(client, channels(subscriptions.psubscribe)) end end return consumer_loop(client) end end -- Redis transactions (MULTI/EXEC) do local function identity(...) return ... end local emptytable = {} local function initialize_transaction(client, options, block, queued_parsers) local table_insert = table.insert local coro = coroutine.create(block) if options.watch then local watch_keys = {} for _, key in pairs(options.watch) do table_insert(watch_keys, key) end if #watch_keys > 0 then client:watch(unpack(watch_keys)) end end local transaction_client = setmetatable({}, {__index=client}) transaction_client.exec = function(...) client.error('cannot use EXEC inside a transaction block') end transaction_client.multi = function(...) coroutine.yield() end transaction_client.commands_queued = function() return #queued_parsers end assert(coroutine.resume(coro, transaction_client)) transaction_client.multi = nil transaction_client.discard = function(...) local reply = client:discard() for i, v in pairs(queued_parsers) do queued_parsers[i]=nil end coro = initialize_transaction(client, options, block, queued_parsers) return reply end transaction_client.watch = function(...) client.error('WATCH inside MULTI is not allowed') end setmetatable(transaction_client, { __index = function(t, k) local cmd = client[k] if type(cmd) == "function" then local function queuey(self, ...) local reply = cmd(client, ...) assert((reply or emptytable).queued == true, 'a QUEUED reply was expected') table_insert(queued_parsers, reply.parser or identity) return reply end t[k]=queuey return queuey else return cmd end end }) client:multi() return coro end local function transaction(client, options, coroutine_block, attempts) local queued_parsers, replies = {}, {} local retry = tonumber(attempts) or tonumber(options.retry) or 2 local coro = initialize_transaction(client, options, coroutine_block, queued_parsers) local success, retval if coroutine.status(coro) == 'suspended' then success, retval = coroutine.resume(coro) else -- do not fail if the coroutine has not been resumed (missing t:multi() with CAS) success, retval = true, 'empty transaction' end if #queued_parsers == 0 or not success then client:discard() assert(success, retval) return replies, 0 end local raw_replies = client:exec() if not raw_replies then if (retry or 0) <= 0 then client.error("MULTI/EXEC transaction aborted by the server") else --we're not quite done yet return transaction(client, options, coroutine_block, retry - 1) end end local table_insert = table.insert for i, parser in pairs(queued_parsers) do table_insert(replies, i, parser(raw_replies[i])) end return replies, #queued_parsers end client_prototype.transaction = function(client, arg1, arg2) local options, block if not arg2 then options, block = {}, arg1 elseif arg1 then --and arg2, implicitly options, block = type(arg1)=="table" and arg1 or { arg1 }, arg2 else client.error("Invalid parameters for redis transaction.") end if not options.watch then watch_keys = { } for i, v in pairs(options) do if tonumber(i) then table.insert(watch_keys, v) options[i] = nil end end options.watch = watch_keys elseif not (type(options.watch) == 'table') then options.watch = { options.watch } end if not options.cas then local tx_block = block block = function(client, ...) client:multi() return tx_block(client, ...) --can't wrap this in pcall because we're in a coroutine. end end return transaction(client, options, block) end end -- MONITOR context do local monitor_loop = function(client) local monitoring = true -- Tricky since the payload format changed starting from Redis 2.6. local pattern = '^(%d+%.%d+)( ?.- ?) ?"(%a+)" ?(.-)$' local abort = function() monitoring = false end return coroutine.wrap(function() client:monitor() while monitoring do local message, matched local response = response.read(client) local ok = response:gsub(pattern, function(time, info, cmd, args) message = { timestamp = tonumber(time), client = info:match('%d+.%d+.%d+.%d+:%d+'), database = tonumber(info:match('%d+')) or 0, command = cmd, arguments = args:match('.+'), } matched = true end) if not matched then client.error('Unable to match MONITOR payload: '..response) end coroutine.yield(message, abort) end end) end client_prototype.monitor_messages = function(client) return monitor_loop(client) end end -- ############################################################################ local function connect_tcp(socket, parameters) local host, port = parameters.host, tonumber(parameters.port) local ok, err = socket:connect(host, port) if not ok then redis.error('could not connect to '..host..':'..port..' ['..err..']') end socket:setoption('tcp-nodelay', parameters.tcp_nodelay) return socket end local function connect_unix(socket, parameters) local ok, err = socket:connect(parameters.path) if not ok then redis.error('could not connect to '..parameters.path..' ['..err..']') end return socket end local function create_connection(parameters) if parameters.socket then return parameters.socket end local perform_connection, socket if parameters.scheme == 'unix' then perform_connection, socket = connect_unix, require('socket.unix') assert(socket, 'your build of LuaSocket does not support UNIX domain sockets') else if parameters.scheme then local scheme = parameters.scheme assert(scheme == 'redis' or scheme == 'tcp', 'invalid scheme: '..scheme) end perform_connection, socket = connect_tcp, require('socket').tcp end return perform_connection(socket(), parameters) end -- ############################################################################ function redis.error(message, level) error(message, (level or 1) + 1) end function redis.connect(...) local args, parameters = {...}, nil if #args == 1 then if type(args[1]) == 'table' then parameters = args[1] else local uri = require('socket.url') parameters = uri.parse(select(1, ...)) if parameters.scheme then if parameters.query then for k, v in parameters.query:gmatch('([-_%w]+)=([-_%w]+)') do if k == 'tcp_nodelay' or k == 'tcp-nodelay' then parameters.tcp_nodelay = parse_boolean(v) end end end else parameters.host = parameters.path end end elseif #args > 1 then local host, port = unpack(args) parameters = { host = host, port = port } end local commands = redis.commands or {} if type(commands) ~= 'table' then redis.error('invalid type for the commands table') end local socket = create_connection(merge_defaults(parameters)) local client = create_client(client_prototype, socket, commands) return client end function redis.command(cmd, opts) return command(cmd, opts) end -- obsolete function redis.define_command(name, opts) define_command_impl(redis.commands, name, opts) end -- obsolete function redis.undefine_command(name) undefine_command_impl(redis.commands, name) end -- ############################################################################ -- Commands defined in this table do not take the precedence over -- methods defined in the client prototype table. redis.commands = { -- commands operating on the key space exists = command('EXISTS', { response = toboolean }), del = command('DEL'), type = command('TYPE'), rename = command('RENAME'), renamenx = command('RENAMENX', { response = toboolean }), expire = command('EXPIRE', { response = toboolean }), pexpire = command('PEXPIRE', { -- >= 2.6 response = toboolean }), expireat = command('EXPIREAT', { response = toboolean }), pexpireat = command('PEXPIREAT', { -- >= 2.6 response = toboolean }), ttl = command('TTL'), pttl = command('PTTL'), -- >= 2.6 move = command('MOVE', { response = toboolean }), dbsize = command('DBSIZE'), persist = command('PERSIST', { -- >= 2.2 response = toboolean }), keys = command('KEYS', { response = function(response) if type(response) == 'string' then -- backwards compatibility path for Redis < 2.0 local keys = {} response:gsub('[^%s]+', function(key) table.insert(keys, key) end) response = keys end return response end }), randomkey = command('RANDOMKEY', { response = function(response) if response == '' then return nil else return response end end }), sort = command('SORT', { request = sort_request, }), -- commands operating on string values set = command('SET'), setnx = command('SETNX', { response = toboolean }), setex = command('SETEX'), -- >= 2.0 psetex = command('PSETEX'), -- >= 2.6 mset = command('MSET', { request = mset_filter_args }), msetnx = command('MSETNX', { request = mset_filter_args, response = toboolean }), get = command('GET'), mget = command('MGET'), getset = command('GETSET'), incr = command('INCR'), incrby = command('INCRBY'), incrbyfloat = command('INCRBYFLOAT', { -- >= 2.6 response = function(reply, command, ...) return tonumber(reply) end, }), decr = command('DECR'), decrby = command('DECRBY'), append = command('APPEND'), -- >= 2.0 substr = command('SUBSTR'), -- >= 2.0 strlen = command('STRLEN'), -- >= 2.2 setrange = command('SETRANGE'), -- >= 2.2 getrange = command('GETRANGE'), -- >= 2.2 setbit = command('SETBIT'), -- >= 2.2 getbit = command('GETBIT'), -- >= 2.2 -- commands operating on lists rpush = command('RPUSH'), lpush = command('LPUSH'), llen = command('LLEN'), lrange = command('LRANGE'), ltrim = command('LTRIM'), lindex = command('LINDEX'), lset = command('LSET'), lrem = command('LREM'), lpop = command('LPOP'), rpop = command('RPOP'), rpoplpush = command('RPOPLPUSH'), blpop = command('BLPOP'), -- >= 2.0 brpop = command('BRPOP'), -- >= 2.0 rpushx = command('RPUSHX'), -- >= 2.2 lpushx = command('LPUSHX'), -- >= 2.2 linsert = command('LINSERT'), -- >= 2.2 brpoplpush = command('BRPOPLPUSH'), -- >= 2.2 -- commands operating on sets sadd = command('SADD'), srem = command('SREM'), spop = command('SPOP'), smove = command('SMOVE', { response = toboolean }), scard = command('SCARD'), sismember = command('SISMEMBER', { response = toboolean }), sinter = command('SINTER'), sinterstore = command('SINTERSTORE'), sunion = command('SUNION'), sunionstore = command('SUNIONSTORE'), sdiff = command('SDIFF'), sdiffstore = command('SDIFFSTORE'), smembers = command('SMEMBERS'), srandmember = command('SRANDMEMBER'), -- commands operating on sorted sets zadd = command('ZADD'), zincrby = command('ZINCRBY'), zrem = command('ZREM'), zrange = command('ZRANGE', { request = zset_range_request, response = zset_range_reply, }), zrevrange = command('ZREVRANGE', { request = zset_range_request, response = zset_range_reply, }), zrangebyscore = command('ZRANGEBYSCORE', { request = zset_range_byscore_request, response = zset_range_reply, }), zrevrangebyscore = command('ZREVRANGEBYSCORE', { -- >= 2.2 request = zset_range_byscore_request, response = zset_range_reply, }), zunionstore = command('ZUNIONSTORE', { -- >= 2.0 request = zset_store_request }), zinterstore = command('ZINTERSTORE', { -- >= 2.0 request = zset_store_request }), zcount = command('ZCOUNT'), zcard = command('ZCARD'), zscore = command('ZSCORE'), zremrangebyscore = command('ZREMRANGEBYSCORE'), zrank = command('ZRANK'), -- >= 2.0 zrevrank = command('ZREVRANK'), -- >= 2.0 zremrangebyrank = command('ZREMRANGEBYRANK'), -- >= 2.0 -- commands operating on hashes hset = command('HSET', { -- >= 2.0 response = toboolean }), hsetnx = command('HSETNX', { -- >= 2.0 response = toboolean }), hmset = command('HMSET', { -- >= 2.0 request = hash_multi_request_builder(function(args, k, v) table.insert(args, k) table.insert(args, v) end), }), hincrby = command('HINCRBY'), -- >= 2.0 hincrbyfloat = command('HINCRBYFLOAT', {-- >= 2.6 response = function(reply, command, ...) return tonumber(reply) end, }), hget = command('HGET'), -- >= 2.0 hmget = command('HMGET', { -- >= 2.0 request = hash_multi_request_builder(function(args, k, v) table.insert(args, v) end), }), hdel = command('HDEL'), -- >= 2.0 hexists = command('HEXISTS', { -- >= 2.0 response = toboolean }), hlen = command('HLEN'), -- >= 2.0 hkeys = command('HKEYS'), -- >= 2.0 hvals = command('HVALS'), -- >= 2.0 hgetall = command('HGETALL', { -- >= 2.0 response = function(reply, command, ...) local new_reply = { } for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end return new_reply end }), -- connection related commands ping = command('PING', { response = function(response) return response == 'PONG' end }), echo = command('ECHO'), auth = command('AUTH'), select = command('SELECT'), -- transactions multi = command('MULTI'), -- >= 2.0 exec = command('EXEC'), -- >= 2.0 discard = command('DISCARD'), -- >= 2.0 watch = command('WATCH'), -- >= 2.2 unwatch = command('UNWATCH'), -- >= 2.2 -- publish - subscribe subscribe = command('SUBSCRIBE'), -- >= 2.0 unsubscribe = command('UNSUBSCRIBE'), -- >= 2.0 psubscribe = command('PSUBSCRIBE'), -- >= 2.0 punsubscribe = command('PUNSUBSCRIBE'), -- >= 2.0 publish = command('PUBLISH'), -- >= 2.0 -- redis scripting eval = command('EVAL'), -- >= 2.6 evalsha = command('EVALSHA'), -- >= 2.6 script = command('SCRIPT'), -- >= 2.6 -- remote server control commands bgrewriteaof = command('BGREWRITEAOF'), config = command('CONFIG', { -- >= 2.0 response = function(reply, command, ...) if (type(reply) == 'table') then local new_reply = { } for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end return new_reply end return reply end }), client = command('CLIENT'), -- >= 2.4 slaveof = command('SLAVEOF'), save = command('SAVE'), bgsave = command('BGSAVE'), lastsave = command('LASTSAVE'), flushdb = command('FLUSHDB'), flushall = command('FLUSHALL'), monitor = command('MONITOR'), time = command('TIME'), -- >= 2.6 slowlog = command('SLOWLOG', { -- >= 2.2.13 response = function(reply, command, ...) if (type(reply) == 'table') then local structured = { } for index, entry in ipairs(reply) do structured[index] = { id = tonumber(entry[1]), timestamp = tonumber(entry[2]), duration = tonumber(entry[3]), command = entry[4], } end return structured end return reply end }), info = command('INFO', { response = parse_info, }), } -- ############################################################################ return redis lua-redis-2.0.4/test/000077500000000000000000000000001222075140200143545ustar00rootroot00000000000000lua-redis-2.0.4/test/test_client.lua000066400000000000000000002617471222075140200174150ustar00rootroot00000000000000package.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 = 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) socket.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) end) context("Client features", function() before(function() client = utils.create_client(settings) end) test("Send raw commands", function() assert_equal(client:raw_cmd("PING\r\n"), 'PONG') assert_true(client:raw_cmd("*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n")) assert_equal(client:raw_cmd("GET foo\r\n"), '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 commands at module level (OLD)", function() redis.define_command('doesnotexist') local client2 = utils.create_client(settings) redis.undefine_command('doesnotexist') 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 = client.requests.multibulk }) assert_not_nil(client.ping) assert_equal(client:ping(), 'PONG') client.ping = redis.command('ping', { request = client.requests.multibulk, response = function(reply) return reply == 'PONG' end }) assert_not_nil(client.ping) assert_true(client:ping()) end) test("Define new commands at client instance level (OLD)", function() client:define_command('doesnotexist') assert_not_nil(client.doesnotexist) assert_error(function() client:doesnotexist() end) client:undefine_command('doesnotexist') assert_nil(client.doesnotexist) client:define_command('ping') assert_not_nil(client.ping) assert_equal(client:ping(), 'PONG') client:define_command('ping', { request = client.requests.multibulk }) assert_not_nil(client.ping) assert_equal(client:ping(), 'PONG') client:define_command('ping', { request = client.requests.multibulk, 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) 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) 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) 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) 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) 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)