├── .gitignore
├── History.txt
├── README.rdoc
├── Rakefile
├── lib
├── em-redis.rb
└── em-redis
│ └── redis_protocol.rb
└── spec
├── live_redis_protocol_spec.rb
├── redis_commands_spec.rb
├── redis_protocol_spec.rb
└── test_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | pkg
3 |
--------------------------------------------------------------------------------
/History.txt:
--------------------------------------------------------------------------------
1 | == 0.2.2 / 2009-12-29
2 | * reselect database after reconnecting
3 |
4 | == 0.2.1 / 2009-12-15
5 | * updated gem dependencies
6 |
7 | == 0.2 / 2009-12-15
8 | * rip off stock redis gem
9 | * sort is no longer compatible with 0.1 version
10 | * response of exists, sismember, sadd, srem, smove, zadd, zrem, move, setnx, del, renamenx, and expire is either true or false, not 0 or 1 as in 0.1
11 | * info returns hash of symbols now
12 | * lrem has different argument order
13 |
14 | == 0.1.1 / 2009-05-01
15 |
16 | * added a number of aliases to redis-based method names
17 | * refactored process_cmd method for greater clarity
18 |
19 | == 0.1.0 / 2009-04-28
20 |
21 | * initial release
22 | * compatible with Redis 0.093
23 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | == EM-REDIS
2 |
3 | == DESCRIPTION:
4 |
5 | An EventMachine[http://rubyeventmachine.com/] based library for interacting with the very cool Redis[http://code.google.com/p/redis/] data store by Salvatore 'antirez' Sanfilippo.
6 | Modeled after eventmachine's implementation of the memcached protocol, and influenced by Ezra Zygmuntowicz's {redis-rb}[http://github.com/ezmobius/redis-rb/tree/master] library (distributed as part of Redis).
7 |
8 | This library is only useful when used as part of an application that relies on
9 | Event Machine's event loop. It implements an EM-based client protocol, which
10 | leverages the non-blocking nature of the EM interface to achieve significant
11 | parallelization without threads.
12 |
13 |
14 | == FEATURES/PROBLEMS:
15 |
16 | Implements most Redis commands (see {the list of available commands here}[http://code.google.com/p/redis/wiki/CommandReference] with the notable
17 | exception of MONITOR.
18 |
19 | == SYNOPSIS:
20 |
21 | Like any Deferrable eventmachine-based protocol implementation, using EM-Redis involves making calls and passing blocks that serve as callbacks when the call returns.
22 |
23 | require 'em-redis'
24 |
25 | EM.run do
26 | redis = EM::Protocols::Redis.connect
27 | redis.errback do |code|
28 | puts "Error code: #{code}"
29 | end
30 | redis.set "a", "foo" do |response|
31 | redis.get "a" do |response|
32 | puts response
33 | end
34 | end
35 | # We get pipelining for free
36 | redis.set("b", "bar")
37 | redis.get("a") do |response|
38 | puts response # will be foo
39 | end
40 | end
41 |
42 | To run tests on a Redis server (currently compatible with 1.3)
43 |
44 | rake
45 |
46 | Because the EM::Protocol::Memcached code used Bacon for testing, test code is
47 | currently in the form of bacon specs.
48 |
49 | == REQUIREMENTS:
50 |
51 | * Redis (download[http://code.google.com/p/redis/downloads/list])
52 |
53 | == INSTALL:
54 |
55 | sudo gem install em-redis
56 |
57 | == LICENSE:
58 |
59 | (The MIT License)
60 |
61 | Copyright (c) 2008, 2009
62 |
63 | Permission is hereby granted, free of charge, to any person obtaining
64 | a copy of this software and associated documentation files (the
65 | 'Software'), to deal in the Software without restriction, including
66 | without limitation the rights to use, copy, modify, merge, publish,
67 | distribute, sublicense, and/or sell copies of the Software, and to
68 | permit persons to whom the Software is furnished to do so, subject to
69 | the following conditions:
70 |
71 | The above copyright notice and this permission notice shall be
72 | included in all copies or substantial portions of the Software.
73 |
74 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
75 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
76 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
77 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
78 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
79 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
80 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
81 |
82 | == CREDIT
83 |
84 | by Jonathan Broad (http://www.relativepath.org)
85 |
86 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Look in the tasks/setup.rb file for the various options that can be
2 | # configured in this Rakefile. The .rake files in the tasks directory
3 | # are where the options are used.
4 |
5 | begin
6 | require 'bones'
7 | rescue LoadError
8 | abort '### Please install the "bones" gem ###'
9 | end
10 |
11 | ensure_in_path 'lib'
12 | require 'em-redis'
13 |
14 | task :default => ['redis:test']
15 |
16 | Bones {
17 | name 'em-redis'
18 | authors ['Jonathan Broad', 'Eugene Pimenov']
19 | email 'libc@me.com'
20 | url 'http://github.com/libc/em-redis'
21 | summary 'An eventmachine-based implementation of the Redis protocol'
22 | description summary
23 | version EMRedis::VERSION
24 |
25 | readme_file 'README.rdoc'
26 | ignore_file '.gitignore'
27 |
28 | depend_on 'eventmachine', '>=0.12.10'
29 |
30 | depend_on "bacon", :development => true
31 | depend_on "em-spec", :development => true
32 | }
33 |
34 | namespace :redis do
35 | desc "Test em-redis against a live Redis"
36 | task :test do
37 | sh "bacon spec/live_redis_protocol_spec.rb spec/redis_commands_spec.rb spec/redis_protocol_spec.rb"
38 | end
39 | end
40 |
41 | # EOF
42 |
--------------------------------------------------------------------------------
/lib/em-redis.rb:
--------------------------------------------------------------------------------
1 |
2 | module EMRedis
3 |
4 | # :stopdoc:
5 | VERSION = '0.2.2'
6 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8 | # :startdoc:
9 |
10 | # Returns the version string for the library.
11 | #
12 | def self.version
13 | VERSION
14 | end
15 |
16 | # Returns the library path for the module. If any arguments are given,
17 | # they will be joined to the end of the libray path using
18 | # File.join.
19 | #
20 | def self.libpath( *args )
21 | args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
22 | end
23 |
24 | # Returns the lpath for the module. If any arguments are given,
25 | # they will be joined to the end of the path using
26 | # File.join.
27 | #
28 | def self.path( *args )
29 | args.empty? ? PATH : ::File.join(PATH, args.flatten)
30 | end
31 |
32 | # Utility method used to require all files ending in .rb that lie in the
33 | # directory below this file that has the same name as the filename passed
34 | # in. Optionally, a specific _directory_ name can be passed in such that
35 | # the _filename_ does not have to be equivalent to the directory.
36 | #
37 | def self.require_all_libs_relative_to( fname, dir = nil )
38 | dir ||= ::File.basename(fname, '.*')
39 | search_me = ::File.expand_path(
40 | ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
41 |
42 | Dir.glob(search_me).sort.each {|rb| require rb}
43 | end
44 |
45 | end # module EMRedis
46 |
47 | EMRedis.require_all_libs_relative_to(__FILE__)
48 |
--------------------------------------------------------------------------------
/lib/em-redis/redis_protocol.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'eventmachine'
3 |
4 | module EventMachine
5 | module Protocols
6 | module Redis
7 | include EM::Deferrable
8 |
9 | ##
10 | # constants
11 | #########################
12 |
13 | OK = "OK".freeze
14 | MINUS = "-".freeze
15 | PLUS = "+".freeze
16 | COLON = ":".freeze
17 | DOLLAR = "$".freeze
18 | ASTERISK = "*".freeze
19 | DELIM = "\r\n".freeze
20 |
21 | BULK_COMMANDS = {
22 | "set" => true,
23 | "setnx" => true,
24 | "rpush" => true,
25 | "lpush" => true,
26 | "lset" => true,
27 | "lrem" => true,
28 | "sadd" => true,
29 | "srem" => true,
30 | "sismember" => true,
31 | "echo" => true,
32 | "getset" => true,
33 | "smove" => true,
34 | "zadd" => true,
35 | "zincrby" => true,
36 | "zrem" => true,
37 | "zscore" => true
38 | }
39 |
40 | MULTI_BULK_COMMANDS = {
41 | "mset" => true,
42 | "msetnx" => true,
43 | # these aliases aren't in redis gem
44 | "multi_get" => true
45 | }
46 |
47 | BOOLEAN_PROCESSOR = lambda{|r| %w(1 OK).include? r.to_s }
48 |
49 | REPLY_PROCESSOR = {
50 | "exists" => BOOLEAN_PROCESSOR,
51 | "sismember" => BOOLEAN_PROCESSOR,
52 | "sadd" => BOOLEAN_PROCESSOR,
53 | "srem" => BOOLEAN_PROCESSOR,
54 | "smove" => BOOLEAN_PROCESSOR,
55 | "zadd" => BOOLEAN_PROCESSOR,
56 | "zrem" => BOOLEAN_PROCESSOR,
57 | "move" => BOOLEAN_PROCESSOR,
58 | "setnx" => BOOLEAN_PROCESSOR,
59 | "del" => BOOLEAN_PROCESSOR,
60 | "renamenx" => BOOLEAN_PROCESSOR,
61 | "expire" => BOOLEAN_PROCESSOR,
62 | "select" => BOOLEAN_PROCESSOR, # not in redis gem
63 | "keys" => lambda{|r| r.split(" ")},
64 | "info" => lambda{|r|
65 | info = {}
66 | r.each_line {|kv|
67 | k,v = kv.split(":",2).map{|x| x.chomp}
68 | info[k.to_sym] = v
69 | }
70 | info
71 | }
72 | }
73 |
74 | ALIASES = {
75 | "flush_db" => "flushdb",
76 | "flush_all" => "flushall",
77 | "last_save" => "lastsave",
78 | "key?" => "exists",
79 | "delete" => "del",
80 | "randkey" => "randomkey",
81 | "list_length" => "llen",
82 | "push_tail" => "rpush",
83 | "push_head" => "lpush",
84 | "pop_tail" => "rpop",
85 | "pop_head" => "lpop",
86 | "list_set" => "lset",
87 | "list_range" => "lrange",
88 | "list_trim" => "ltrim",
89 | "list_index" => "lindex",
90 | "list_rm" => "lrem",
91 | "set_add" => "sadd",
92 | "set_delete" => "srem",
93 | "set_count" => "scard",
94 | "set_member?" => "sismember",
95 | "set_members" => "smembers",
96 | "set_intersect" => "sinter",
97 | "set_intersect_store" => "sinterstore",
98 | "set_inter_store" => "sinterstore",
99 | "set_union" => "sunion",
100 | "set_union_store" => "sunionstore",
101 | "set_diff" => "sdiff",
102 | "set_diff_store" => "sdiffstore",
103 | "set_move" => "smove",
104 | "set_unless_exists" => "setnx",
105 | "rename_unless_exists" => "renamenx",
106 | "type?" => "type",
107 | "zset_add" => "zadd",
108 | "zset_count" => "zcard",
109 | "zset_range_by_score" => "zrangebyscore",
110 | "zset_reverse_range" => "zrevrange",
111 | "zset_range" => "zrange",
112 | "zset_delete" => "zrem",
113 | "zset_score" => "zscore",
114 | "zset_incr_by" => "zincrby",
115 | "zset_increment_by" => "zincrby",
116 | # these aliases aren't in redis gem
117 | "background_save" => 'bgsave',
118 | "async_save" => 'bgsave',
119 | "members" => 'smembers',
120 | "decrement_by" => "decrby",
121 | "decrement" => "decr",
122 | "increment_by" => "incrby",
123 | "increment" => "incr",
124 | "set_if_nil" => "setnx",
125 | "multi_get" => "mget",
126 | "random_key" => "randomkey",
127 | "random" => "randomkey",
128 | "rename_if_nil" => "renamenx",
129 | "tail_pop" => "rpop",
130 | "pop" => "rpop",
131 | "head_pop" => "lpop",
132 | "shift" => "lpop",
133 | "list_remove" => "lrem",
134 | "index" => "lindex",
135 | "trim" => "ltrim",
136 | "list_range" => "lrange",
137 | "range" => "lrange",
138 | "list_len" => "llen",
139 | "len" => "llen",
140 | "head_push" => "lpush",
141 | "unshift" => "lpush",
142 | "tail_push" => "rpush",
143 | "push" => "rpush",
144 | "add" => "sadd",
145 | "set_remove" => "srem",
146 | "set_size" => "scard",
147 | "member?" => "sismember",
148 | "intersect" => "sinter",
149 | "intersect_and_store" => "sinterstore",
150 | "members" => "smembers",
151 | "exists?" => "exists"
152 | }
153 |
154 | DISABLED_COMMANDS = {
155 | "monitor" => true,
156 | "sync" => true
157 | }
158 |
159 | def []=(key,value)
160 | set(key,value)
161 | end
162 |
163 | def set(key, value, expiry=nil)
164 | call_command([:set, key, value]) do |s|
165 | yield s if block_given?
166 | end
167 | expire(key, expiry) if expiry
168 | end
169 |
170 | def sort(key, options={}, &blk)
171 | cmd = ["SORT"]
172 | cmd << key
173 | cmd << "BY #{options[:by]}" if options[:by]
174 | cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
175 | cmd << "#{options[:order]}" if options[:order]
176 | cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
177 | call_command(cmd, &blk)
178 | end
179 |
180 | def incr(key, increment = nil, &blk)
181 | call_command(increment ? ["incrby",key,increment] : ["incr",key], &blk)
182 | end
183 |
184 | def decr(key, decrement = nil, &blk)
185 | call_command(decrement ? ["decrby",key,decrement] : ["decr",key], &blk)
186 | end
187 |
188 | def select(db, &blk)
189 | @db = db.to_i
190 | call_command(['select', @db], &blk)
191 | end
192 |
193 | def auth(password, &blk)
194 | @password = password
195 | call_command(['auth', password], &blk)
196 | end
197 |
198 | # Similar to memcache.rb's #get_multi, returns a hash mapping
199 | # keys to values.
200 | def mapped_mget(*keys)
201 | mget(*keys) do |response|
202 | result = {}
203 | response.each do |value|
204 | key = keys.shift
205 | result.merge!(key => value) unless value.nil?
206 | end
207 | yield result if block_given?
208 | end
209 | end
210 |
211 | # Ruby defines a now deprecated type method so we need to override it here
212 | # since it will never hit method_missing
213 | def type(key, &blk)
214 | call_command(['type', key], &blk)
215 | end
216 |
217 | def quit(&blk)
218 | call_command(['quit'], &blk)
219 | end
220 |
221 | def errback(&blk)
222 | @error_callback = blk
223 | end
224 | alias_method :on_error, :errback
225 |
226 | def method_missing(*argv, &blk)
227 | call_command(argv, &blk)
228 | end
229 |
230 | def call_command(argv, &blk)
231 | callback { raw_call_command(argv, &blk) }
232 | end
233 |
234 | def raw_call_command(argv, &blk)
235 | argv[0] = argv[0].to_s unless argv[0].kind_of? String
236 | argv[0] = argv[0].downcase
237 | send_command(argv)
238 | @redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk]
239 | end
240 |
241 | def call_commands(argvs, &blk)
242 | callback { raw_call_commands(argvs, &blk) }
243 | end
244 |
245 | def raw_call_commands(argvs, &blk)
246 | if argvs.empty? # Shortcut
247 | blk.call []
248 | return
249 | end
250 |
251 | argvs.each do |argv|
252 | argv[0] = argv[0].to_s unless argv[0].kind_of? String
253 | send_command argv
254 | end
255 | # FIXME: argvs may contain heterogenous commands, storing all
256 | # REPLY_PROCESSORs may turn out expensive and has been omitted
257 | # for now.
258 | @redis_callbacks << [nil, argvs.length, blk]
259 | end
260 |
261 | def send_command(argv)
262 | argv = argv.dup
263 |
264 | if MULTI_BULK_COMMANDS[argv.flatten[0].to_s]
265 | # TODO improve this code
266 | argvp = argv.flatten
267 | values = argvp.pop.to_a.flatten
268 | argvp = values.unshift(argvp[0])
269 | command = ["*#{argvp.size}"]
270 | argvp.each do |v|
271 | v = v.to_s
272 | command << "$#{get_size(v)}"
273 | command << v
274 | end
275 | command = command.map {|cmd| "#{cmd}\r\n"}.join
276 | else
277 | command = ""
278 | bulk = nil
279 | argv[0] = argv[0].to_s.downcase
280 | argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
281 | raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
282 | if BULK_COMMANDS[argv[0]] and argv.length > 1
283 | bulk = argv[-1].to_s
284 | argv[-1] = get_size(bulk)
285 | end
286 | command << "#{argv.join(' ')}\r\n"
287 | command << "#{bulk}\r\n" if bulk
288 | end
289 |
290 | @logger.debug { "*** sending: #{command}" } if @logger
291 | send_data command
292 | end
293 |
294 | ##
295 | # errors
296 | #########################
297 |
298 | class ParserError < StandardError; end
299 | class ProtocolError < StandardError; end
300 |
301 | class RedisError < StandardError
302 | attr_accessor :code
303 | end
304 |
305 |
306 | ##
307 | # em hooks
308 | #########################
309 |
310 | def self.connect(*args)
311 | case args.length
312 | when 0
313 | options = {}
314 | when 1
315 | arg = args.shift
316 | case arg
317 | when Hash then options = arg
318 | when String then options = {:host => arg}
319 | else raise ArgumentError, 'first argument must be Hash or String'
320 | end
321 | when 2
322 | options = {:host => args[1], :port => args[2]}
323 | else
324 | raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
325 | end
326 | options[:host] ||= '127.0.0.1'
327 | options[:port] = (options[:port] || 6379).to_i
328 | EM.connect options[:host], options[:port], self, options
329 | end
330 |
331 | def initialize(options = {})
332 | @host = options[:host]
333 | @port = options[:port]
334 | @db = (options[:db] || 0).to_i
335 | @password = options[:password]
336 | @logger = options[:logger]
337 | @error_callback = lambda do |code|
338 | err = RedisError.new
339 | err.code = code
340 | raise err, "Redis server returned error code: #{code}"
341 | end
342 | @values = []
343 |
344 | # These commands should be first
345 | auth_and_select_db
346 | end
347 |
348 | def auth_and_select_db
349 | call_command(["auth", @password]) if @password
350 | call_command(["select", @db]) unless @db == 0
351 | end
352 | private :auth_and_select_db
353 |
354 | def connection_completed
355 | @logger.debug { "Connected to #{@host}:#{@port}" } if @logger
356 |
357 | @redis_callbacks = []
358 | @multibulk_n = false
359 | @reconnecting = false
360 | @connected = true
361 |
362 | succeed
363 | end
364 |
365 | # 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
366 | # stack overflows when there is too much data.
367 | # include EM::P::LineText2
368 | def receive_data(data)
369 | (@buffer ||= '') << data
370 | while index = @buffer.index(DELIM)
371 | begin
372 | line = @buffer.slice!(0, index+2)
373 | process_cmd line
374 | rescue ParserError
375 | @buffer[0...0] = line
376 | break
377 | end
378 | end
379 | end
380 |
381 | def process_cmd(line)
382 | @logger.debug { "*** processing #{line}" } if @logger
383 | # first character of buffer will always be the response type
384 | reply_type = line[0, 1]
385 | reply_args = line.slice(1..-3) # remove type character and \r\n
386 | case reply_type
387 |
388 | #e.g. -MISSING
389 | when MINUS
390 | # Missing, dispatch empty response
391 | dispatch_response(nil)
392 | # e.g. +OK
393 | when PLUS
394 | dispatch_response(reply_args)
395 | # e.g. $3\r\nabc\r\n
396 | # 'bulk' is more complex because it could be part of multi-bulk
397 | when DOLLAR
398 | data_len = Integer(reply_args)
399 | if data_len == -1 # expect no data; return nil
400 | dispatch_response(nil)
401 | elsif @buffer.size >= data_len + 2 # buffer is full of expected data
402 | dispatch_response(@buffer.slice!(0, data_len))
403 | @buffer.slice!(0,2) # tossing \r\n
404 | else # buffer isn't full or nil
405 | # TODO: don't control execution with exceptions
406 | raise ParserError
407 | end
408 | #e.g. :8
409 | when COLON
410 | dispatch_response(Integer(reply_args))
411 | #e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n
412 | when ASTERISK
413 | multibulk_count = Integer(reply_args)
414 | if multibulk_count == -1
415 | dispatch_response([])
416 | else
417 | start_multibulk(multibulk_count)
418 | end
419 | # Whu?
420 | else
421 | # TODO: get rid of this exception
422 | raise ProtocolError, "reply type not recognized: #{line.strip}"
423 | end
424 | end
425 |
426 | def dispatch_response(value)
427 | if @multibulk_n
428 | @multibulk_values << value
429 | @multibulk_n -= 1
430 |
431 | if @multibulk_n == 0
432 | value = @multibulk_values
433 | @multibulk_n = false
434 | else
435 | return
436 | end
437 | end
438 |
439 | callback = @redis_callbacks.shift
440 | if callback.kind_of?(Array) && callback.length == 2
441 | processor, blk = callback
442 | value = processor.call(value) if processor
443 | blk.call(value) if blk
444 | elsif callback.kind_of?(Array) && callback.length == 3
445 | processor, pipeline_count, blk = callback
446 | value = processor.call(value) if processor
447 | @values << value
448 | if pipeline_count > 1
449 | @redis_callbacks.unshift [processor, pipeline_count - 1, blk]
450 | else
451 | blk.call(@values) if blk
452 | @values = []
453 | end
454 | end
455 | end
456 |
457 | def start_multibulk(multibulk_count)
458 | @multibulk_n = multibulk_count
459 | @multibulk_values = []
460 | end
461 |
462 | def unbind
463 | @logger.debug { "Disconnected" } if @logger
464 | if @connected || @reconnecting
465 | EM.add_timer(1) do
466 | @logger.debug { "Reconnecting to #{@host}:#{@port}" } if @logger
467 | reconnect @host, @port
468 | auth_and_select_db
469 | end
470 | @connected = false
471 | @reconnecting = true
472 | @deferred_status = nil
473 | else
474 | # TODO: get rid of this exception
475 | raise 'Unable to connect to redis server'
476 | end
477 | end
478 |
479 | private
480 | def get_size(string)
481 | string.respond_to?(:bytesize) ? string.bytesize : string.size
482 | end
483 |
484 | end
485 | end
486 | end
487 |
--------------------------------------------------------------------------------
/spec/live_redis_protocol_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb")
2 |
3 | EM.describe EM::Protocols::Redis, "connected to an empty db" do
4 |
5 | before do
6 | @c = EM::Protocols::Redis.connect :db => 14
7 | @c.flushdb
8 | end
9 |
10 | should "be able to set a string value" do
11 | @c.set("foo", "bar") do |r|
12 | r.should == "OK"
13 | done
14 | end
15 | end
16 |
17 | should "be able to increment the value of a string" do
18 | @c.incr "foo" do |r|
19 | r.should == 1
20 | @c.incr "foo" do |r|
21 | r.should == 2
22 | done
23 | end
24 | end
25 | end
26 |
27 | should "be able to increment the value of a string by an amount" do
28 | @c.incrby "foo", 10 do |r|
29 | r.should == 10
30 | done
31 | end
32 | end
33 |
34 | should "be able to decrement the value of a string" do
35 | @c.incr "foo" do |r|
36 | r.should == 1
37 | @c.decr "foo" do |r|
38 | r.should == 0
39 | done
40 | end
41 | end
42 | end
43 |
44 | should "be able to decrement the value of a string by an amount" do
45 | @c.incrby "foo", 20 do |r|
46 | r.should == 20
47 | @c.decrby "foo", 10 do |r|
48 | r.should == 10
49 | done
50 | end
51 | end
52 | end
53 |
54 | should "be able to 'lpush' to a nonexistent list" do
55 | @c.lpush("foo", "bar") do |r|
56 | r.should == "OK"
57 | done
58 | end
59 | end
60 |
61 | should "be able to 'rpush' to a nonexistent list" do
62 | @c.rpush("foo", "bar") do |r|
63 | r.should == "OK"
64 | done
65 | end
66 | end
67 |
68 |
69 | should "be able to get the size of the database" do
70 | @c.dbsize do |r|
71 | r.should == 0
72 | done
73 | end
74 | end
75 |
76 | should "be able to add a member to a nonexistent set" do
77 | @c.sadd("set_foo", "bar") do |r|
78 | r.should == true
79 | done
80 | end
81 | end
82 |
83 | should "be able to get info about the db as a hash" do
84 | @c.info do |r|
85 | r.should.key? :redis_version
86 | done
87 | end
88 | end
89 |
90 | should "be able to save db" do
91 | @c.save do |r|
92 | r.should == "OK"
93 | done
94 | end
95 | end
96 |
97 | should "be able to save db in the background" do
98 | @c.bgsave do |r|
99 | r.should == "Background saving started"
100 | done
101 | end
102 | end
103 |
104 | end
105 |
106 | EM.describe EM::Protocols::Redis, "connected to a db containing some simple string-valued keys" do
107 |
108 | before do
109 | @c = EM::Protocols::Redis.connect :db => 14
110 | @c.flushdb
111 | @c.set "a", "b"
112 | @c.set "x", "y"
113 | end
114 |
115 | should "be able to fetch the values of multiple keys" do
116 | @c.mget "a", "x" do |r|
117 | r.should == ["b", "y"]
118 | done
119 | end
120 | end
121 |
122 | should "be able to fetch the values of multiple keys in a hash" do
123 | @c.mapped_mget "a", "x" do |r|
124 | r.should == {"a" => "b", "x" => "y"}
125 | done
126 | end
127 | end
128 |
129 | should "be able to fetch all the keys" do
130 | @c.keys "*" do |r|
131 | r.sort.should == ["a", "x"]
132 | done
133 | end
134 | end
135 |
136 | should "be able to set a value if a key doesn't exist" do
137 | @c.setnx "a", "foo" do |r|
138 | r.should == false
139 | @c.setnx "zzz", "foo" do |r|
140 | r.should == true
141 | done
142 | end
143 | end
144 | end
145 |
146 | should "be able to test for the existence of a key" do
147 | @c.exists "a" do |r|
148 | r.should == true
149 | @c.exists "zzz" do |r|
150 | r.should == false
151 | done
152 | end
153 | end
154 | end
155 |
156 | should "be able to delete a key" do
157 | @c.del "a" do |r|
158 | r.should == true
159 | @c.exists "a" do |r|
160 | r.should == false
161 | @c.del "a" do |r|
162 | r.should == false
163 | done
164 | end
165 | end
166 | end
167 | end
168 |
169 | should "be able to detect the type of a key, existing or not" do
170 | @c.type "a" do |r|
171 | r.should == "string"
172 | @c.type "zzz" do |r|
173 | r.should == "none"
174 | done
175 | end
176 | end
177 | end
178 |
179 | should "be able to rename a key" do
180 | @c.rename "a", "x" do |r|
181 | @c.get "x" do |r|
182 | r.should == "b"
183 | done
184 | end
185 | end
186 | end
187 |
188 | should "be able to rename a key unless it exists" do
189 | @c.renamenx "a", "x" do |r|
190 | r.should == false
191 | @c.renamenx "a", "zzz" do |r|
192 | r.should == true
193 | @c.get "zzz" do |r|
194 | r.should == "b"
195 | done
196 | end
197 | end
198 | end
199 | end
200 |
201 |
202 | end
203 |
204 | EM.describe EM::Protocols::Redis, "connected to a db containing a list" do
205 |
206 | before do
207 | @c = EM::Protocols::Redis.connect :db => 14
208 | @c.flushdb
209 | @c.lpush "foo", "c"
210 | @c.lpush "foo", "b"
211 | @c.lpush "foo", "a"
212 | end
213 |
214 | should "be able to 'lset' a list member and 'lindex' to retrieve it" do
215 | @c.lset("foo", 1, "bar") do |r|
216 | @c.lindex("foo", 1) do |r|
217 | r.should == "bar"
218 | done
219 | end
220 | end
221 | end
222 |
223 | should "be able to 'rpush' onto the tail of the list" do
224 | @c.rpush "foo", "d" do |r|
225 | r.should == "OK"
226 | @c.rpop "foo" do |r|
227 | r.should == "d"
228 | done
229 | end
230 | end
231 | end
232 |
233 | should "be able to 'lpush' onto the head of the list" do
234 | @c.lpush "foo", "d" do |r|
235 | r.should == "OK"
236 | @c.lpop "foo" do |r|
237 | r.should == "d"
238 | done
239 | end
240 | end
241 | end
242 |
243 | should "be able to 'rpop' off the tail of the list" do
244 | @c.rpop("foo") do |r|
245 | r.should == "c"
246 | done
247 | end
248 | end
249 |
250 | should "be able to 'lpop' off the tail of the list" do
251 | @c.lpop("foo") do |r|
252 | r.should == "a"
253 | done
254 | end
255 | end
256 |
257 | should "be able to get a range of values from a list" do
258 | @c.lrange("foo", 0, 1) do |r|
259 | r.should == ["a", "b"]
260 | done
261 | end
262 | end
263 |
264 | should "be able to 'ltrim' a list" do
265 | @c.ltrim("foo", 0, 1) do |r|
266 | r.should == "OK"
267 | @c.llen("foo") do |r|
268 | r.should == 2
269 | done
270 | end
271 | end
272 | end
273 |
274 | should "be able to 'rem' a list element" do
275 | @c.lrem("foo", 0, "a") do |r|
276 | r.should == 1
277 | @c.llen("foo") do |r|
278 | r.should == 2
279 | done
280 | end
281 | end
282 | end
283 |
284 | should "be able to detect the type of a list" do
285 | @c.type "foo" do |r|
286 | r.should == "list"
287 | done
288 | end
289 | end
290 |
291 | end
292 |
293 | EM.describe EM::Protocols::Redis, "connected to a db containing two sets" do
294 | before do
295 | @c = EM::Protocols::Redis.connect :db => 14
296 | @c.flushdb
297 | @c.sadd "foo", "a"
298 | @c.sadd "foo", "b"
299 | @c.sadd "foo", "c"
300 | @c.sadd "bar", "c"
301 | @c.sadd "bar", "d"
302 | @c.sadd "bar", "e"
303 | end
304 |
305 | should "be able to find a set's cardinality" do
306 | @c.scard("foo") do |r|
307 | r.should == 3
308 | done
309 | end
310 | end
311 |
312 | should "be able to add a new member to a set unless it is a duplicate" do
313 | @c.sadd("foo", "d") do |r|
314 | r.should == true # success
315 | @c.sadd("foo", "a") do |r|
316 | r.should == false # failure
317 | @c.scard("foo") do |r|
318 | r.should == 4
319 | done
320 | end
321 | end
322 | end
323 | end
324 |
325 | should "be able to remove a set member if it exists" do
326 | @c.srem("foo", "a") do |r|
327 | r.should == true
328 | @c.srem("foo", "z") do |r|
329 | r.should == false
330 | @c.scard("foo") do |r|
331 | r.should == 2
332 | done
333 | end
334 | end
335 | end
336 | end
337 |
338 | should "be able to retrieve a set's members" do
339 | @c.smembers("foo") do |r|
340 | r.sort.should == ["a", "b", "c"]
341 | done
342 | end
343 | end
344 |
345 | should "be able to detect set membership" do
346 | @c.sismember("foo", "a") do |r|
347 | r.should == true
348 | @c.sismember("foo", "z") do |r|
349 | r.should == false
350 | done
351 | end
352 | end
353 | end
354 |
355 | should "be able to find the sets' intersection" do
356 | @c.sinter("foo", "bar") do |r|
357 | r.should == ["c"]
358 | done
359 | end
360 | end
361 |
362 | should "be able to find and store the sets' intersection" do
363 | @c.sinterstore("baz", "foo", "bar") do |r|
364 | r.should == 1
365 | @c.smembers("baz") do |r|
366 | r.should == ["c"]
367 | done
368 | end
369 | end
370 | end
371 |
372 | should "be able to find the sets' union" do
373 | @c.sunion("foo", "bar") do |r|
374 | r.sort.should == ["a","b","c","d","e"]
375 | done
376 | end
377 | end
378 |
379 | should "be able to find and store the sets' union" do
380 | @c.sunionstore("baz", "foo", "bar") do |r|
381 | r.should == 5
382 | @c.smembers("baz") do |r|
383 | r.sort.should == ["a","b","c","d","e"]
384 | done
385 | end
386 | end
387 | end
388 |
389 | should "be able to detect the type of a set" do
390 | @c.type "foo" do |r|
391 | r.should == "set"
392 | done
393 | end
394 | end
395 |
396 | end
397 |
398 |
399 | EM.describe EM::Protocols::Redis, "connected to a db containing three linked lists" do
400 | before do
401 | @c = EM::Protocols::Redis.connect :db => 14
402 | @c.flushdb
403 | @c.rpush "foo", "a"
404 | @c.rpush "foo", "b"
405 | @c.set "a_sort", "2"
406 | @c.set "b_sort", "1"
407 | @c.set "a_data", "foo"
408 | @c.set "b_data", "bar"
409 | end
410 |
411 | should "be able to collate a sorted set of data" do
412 | @c.sort("foo", :by => "*_sort", :get => "*_data") do |r|
413 | r.should == ["bar", "foo"]
414 | done
415 | end
416 | end
417 |
418 | should "be able to get keys selectively" do
419 | @c.keys "a_*" do |r|
420 | r.should == ["a_sort", "a_data"]
421 | done
422 | end
423 | end
424 | end
425 |
426 | EM.describe EM::Protocols::Redis, "when reconnecting" do
427 | before do
428 | @c = EM::Protocols::Redis.connect :db => 14
429 | @c.flushdb
430 | end
431 |
432 | should "select previously selected datase" do
433 | #simulate disconnect
434 | @c.set('foo', 'a') { @c.close_connection_after_writing }
435 |
436 | EM.add_timer(2) do
437 | @c.get('foo') do |r|
438 | r.should == 'a'
439 | @c.get('non_existing') do |r|
440 | r.should == nil
441 | done
442 | end
443 | end
444 | end
445 | end
446 | end
447 |
--------------------------------------------------------------------------------
/spec/redis_commands_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb")
2 | require 'logger'
3 |
4 | EM.describe EM::Protocols::Redis do
5 | default_timeout 1
6 |
7 | before do
8 | @r = EM::Protocols::Redis.connect :db => 14
9 | @r.flushdb
10 | @r['foo'] = 'bar'
11 | end
12 |
13 |
14 | should "be able to provide a logger" do
15 | log = StringIO.new
16 | r = EM::Protocols::Redis.connect :db => 14, :logger => Logger.new(log)
17 | r.ping do
18 | log.string.should.include "ping"
19 | done
20 | end
21 | end
22 |
23 | it "should be able to PING" do
24 | @r.ping { |r| r.should == 'PONG'; done }
25 | end
26 |
27 | it "should be able to GET a key" do
28 | @r.get('foo') { |r| r.should == 'bar'; done }
29 | end
30 |
31 | it "should be able to SET a key" do
32 | @r['foo'] = 'nik'
33 | @r.get('foo') { |r| r.should == 'nik'; done }
34 | end
35 |
36 | it "should properly handle trailing newline characters" do
37 | @r['foo'] = "bar\n"
38 | @r.get('foo') { |r| r.should == "bar\n"; done }
39 | end
40 |
41 | it "should store and retrieve all possible characters at the beginning and the end of a string" do
42 | (0..255).each do |char_idx|
43 | string = "#{char_idx.chr}---#{char_idx.chr}"
44 | @r['foo'] = string
45 | @r.get('foo') { |r| r.should == string }
46 | end
47 | @r.ping { done }
48 | end
49 |
50 | it "should be able to SET a key with an expiry" do
51 | timeout(3)
52 |
53 | @r.set('foo', 'bar', 1)
54 | @r.get('foo') { |r| r.should == 'bar' }
55 | EM.add_timer(2) do
56 | @r.get('foo') { |r| r.should == nil }
57 | @r.ping { done }
58 | end
59 | end
60 |
61 | it "should be able to return a TTL for a key" do
62 | @r.set('foo', 'bar', 1)
63 | @r.ttl('foo') { |r| r.should == 1; done }
64 | end
65 |
66 | it "should be able to SETNX" do
67 | @r['foo'] = 'nik'
68 | @r.get('foo') { |r| r.should == 'nik' }
69 | @r.setnx 'foo', 'bar'
70 | @r.get('foo') { |r| r.should == 'nik' }
71 |
72 | @r.ping { done }
73 | end
74 | #
75 | it "should be able to GETSET" do
76 | @r.getset('foo', 'baz') { |r| r.should == 'bar' }
77 | @r.get('foo') { |r| r.should == 'baz'; done }
78 | end
79 | #
80 | it "should be able to INCR a key" do
81 | @r.del('counter')
82 | @r.incr('counter') { |r| r.should == 1 }
83 | @r.incr('counter') { |r| r.should == 2 }
84 | @r.incr('counter') { |r| r.should == 3 }
85 |
86 | @r.ping { done }
87 | end
88 | #
89 | it "should be able to INCRBY a key" do
90 | @r.del('counter')
91 | @r.incrby('counter', 1) { |r| r.should == 1 }
92 | @r.incrby('counter', 2) { |r| r.should == 3 }
93 | @r.incrby('counter', 3) { |r| r.should == 6 }
94 |
95 | @r.ping { done }
96 | end
97 | #
98 | it "should be able to DECR a key" do
99 | @r.del('counter')
100 | @r.incr('counter') { |r| r.should == 1 }
101 | @r.incr('counter') { |r| r.should == 2 }
102 | @r.incr('counter') { |r| r.should == 3 }
103 | @r.decr('counter') { |r| r.should == 2 }
104 | @r.decr('counter', 2) { |r| r.should == 0; done }
105 | end
106 | #
107 | it "should be able to RANDKEY" do
108 | @r.randkey { |r| r.should.not == nil; done }
109 | end
110 | #
111 | it "should be able to RENAME a key" do
112 | @r.del 'foo'
113 | @r.del 'bar'
114 | @r['foo'] = 'hi'
115 | @r.rename 'foo', 'bar'
116 | @r.get('bar') { |r| r.should == 'hi' ; done }
117 | end
118 | #
119 | it "should be able to RENAMENX a key" do
120 | @r.del 'foo'
121 | @r.del 'bar'
122 | @r['foo'] = 'hi'
123 | @r['bar'] = 'ohai'
124 | @r.renamenx 'foo', 'bar'
125 | @r.get('bar') { |r| r.should == 'ohai' ; done }
126 | end
127 | #
128 | it "should be able to get DBSIZE of the database" do
129 | dbsize_without_foo, dbsize_with_foo = nil
130 | @r.delete 'foo'
131 | @r.dbsize { |r| dbsize_without_foo = r }
132 | @r['foo'] = 0
133 | @r.dbsize { |r| dbsize_with_foo = r }
134 |
135 | @r.ping do
136 | dbsize_with_foo.should == dbsize_without_foo + 1
137 | done
138 | end
139 | end
140 | #
141 | it "should be able to EXPIRE a key" do
142 | timeout(3)
143 |
144 | @r['foo'] = 'bar'
145 | @r.expire 'foo', 1
146 | @r.get('foo') { |r| r.should == "bar" }
147 | EM.add_timer(2) do
148 | @r.get('foo') { |r| r.should == nil }
149 | @r.ping { done }
150 | end
151 | end
152 | #
153 | it "should be able to EXISTS" do
154 | @r['foo'] = 'nik'
155 | @r.exists('foo') { |r| r.should == true }
156 | @r.del 'foo'
157 | @r.exists('foo') { |r| r.should == false ; done }
158 | end
159 | #
160 | it "should be able to KEYS" do
161 | @r.keys("f*") { |keys| keys.each { |key| @r.del key } }
162 | @r['f'] = 'nik'
163 | @r['fo'] = 'nak'
164 | @r['foo'] = 'qux'
165 | @r.keys("f*") { |r| r.sort.should == ['f', 'fo', 'foo'].sort }
166 |
167 | @r.ping { done }
168 | end
169 | #
170 | it "should be able to return a random key (RANDOMKEY)" do
171 | 3.times do |i|
172 | @r.randomkey do |r|
173 | @r.exists(r) do |e|
174 | e.should == true
175 | done if i == 2
176 | end
177 | end
178 | end
179 | end
180 | #
181 | it "should be able to check the TYPE of a key" do
182 | @r['foo'] = 'nik'
183 | @r.type('foo') { |r| r.should == "string" }
184 | @r.del 'foo'
185 | @r.type('foo') { |r| r.should == "none" ; done }
186 | end
187 | #
188 | it "should be able to push to the head of a list (LPUSH)" do
189 | @r.lpush "list", 'hello'
190 | @r.lpush "list", 42
191 | @r.type('list') { |r| r.should == "list" }
192 | @r.llen('list') { |r| r.should == 2 }
193 | @r.lpop('list') { |r| r.should == '42'; done }
194 | end
195 | #
196 | it "should be able to push to the tail of a list (RPUSH)" do
197 | @r.rpush "list", 'hello'
198 | @r.type('list') { |r| r.should == "list" }
199 | @r.llen('list') { |r| r.should == 1 ; done }
200 | end
201 | #
202 | it "should be able to pop the tail of a list (RPOP)" do
203 | @r.rpush "list", 'hello'
204 | @r.rpush"list", 'goodbye'
205 | @r.type('list') { |r| r.should == "list" }
206 | @r.llen('list') { |r| r.should == 2 }
207 | @r.rpop('list') { |r| r.should == 'goodbye'; done }
208 | end
209 | #
210 | it "should be able to pop the head of a list (LPOP)" do
211 | @r.rpush "list", 'hello'
212 | @r.rpush "list", 'goodbye'
213 | @r.type('list') { |r| r.should == "list" }
214 | @r.llen('list') { |r| r.should == 2 }
215 | @r.lpop('list') { |r| r.should == 'hello'; done }
216 | end
217 | #
218 | it "should be able to get the length of a list (LLEN)" do
219 | @r.rpush "list", 'hello'
220 | @r.rpush "list", 'goodbye'
221 | @r.type('list') { |r| r.should == "list" }
222 | @r.llen('list') { |r| r.should == 2 ; done }
223 | end
224 | #
225 | it "should be able to get a range of values from a list (LRANGE)" do
226 | @r.rpush "list", 'hello'
227 | @r.rpush "list", 'goodbye'
228 | @r.rpush "list", '1'
229 | @r.rpush "list", '2'
230 | @r.rpush "list", '3'
231 | @r.type('list') { |r| r.should == "list" }
232 | @r.llen('list') { |r| r.should == 5 }
233 | @r.lrange('list', 2, -1) { |r| r.should == ['1', '2', '3']; done }
234 | end
235 | #
236 | it "should be able to trim a list (LTRIM)" do
237 | @r.rpush "list", 'hello'
238 | @r.rpush "list", 'goodbye'
239 | @r.rpush "list", '1'
240 | @r.rpush "list", '2'
241 | @r.rpush "list", '3'
242 | @r.type('list') { |r| r.should == "list" }
243 | @r.llen('list') { |r| r.should == 5 }
244 | @r.ltrim 'list', 0, 1
245 | @r.llen('list') { |r| r.should == 2 }
246 | @r.lrange('list', 0, -1) { |r| r.should == ['hello', 'goodbye']; done }
247 | end
248 | #
249 | it "should be able to get a value by indexing into a list (LINDEX)" do
250 | @r.rpush "list", 'hello'
251 | @r.rpush "list", 'goodbye'
252 | @r.type('list') { |r| r.should == "list" }
253 | @r.llen('list') { |r| r.should == 2 }
254 | @r.lindex('list', 1) { |r| r.should == 'goodbye'; done }
255 | end
256 | #
257 | it "should be able to set a value by indexing into a list (LSET)" do
258 | @r.rpush "list", 'hello'
259 | @r.rpush "list", 'hello'
260 | @r.type('list') { |r| r.should == "list" }
261 | @r.llen('list') { |r| r.should == 2 }
262 | @r.lset('list', 1, 'goodbye') { |r| r.should == 'OK' }
263 | @r.lindex('list', 1) { |r| r.should == 'goodbye'; done }
264 | end
265 | #
266 | it "should be able to remove values from a list (LREM)" do
267 | @r.rpush "list", 'hello'
268 | @r.rpush "list", 'goodbye'
269 | @r.type('list') { |r| r.should == "list" }
270 | @r.llen('list') { |r| r.should == 2 }
271 | @r.lrem('list', 1, 'hello') { |r| r.should == 1 }
272 | @r.lrange('list', 0, -1) { |r| r.should == ['goodbye']; done }
273 | end
274 |
275 | it "should be able to pop values from a list and push them onto a temp list(RPOPLPUSH)" do
276 | @r.rpush "list", 'one'
277 | @r.rpush "list", 'two'
278 | @r.rpush "list", 'three'
279 | @r.type('list') { |r| r.should == "list" }
280 | @r.llen('list') { |r| r.should == 3 }
281 | @r.lrange('list', 0, -1) { |r| r.should == ['one', 'two', 'three'] }
282 | @r.lrange('tmp', 0, -1) { |r| r.should == [] }
283 | @r.rpoplpush('list', 'tmp') { |r| r.should == 'three' }
284 | @r.lrange('tmp', 0, -1) { |r| r.should == ['three'] }
285 | @r.rpoplpush('list', 'tmp') { |r| r.should == 'two' }
286 | @r.lrange('tmp', 0, -1) { |r| r.should == ['two', 'three'] }
287 | @r.rpoplpush('list', 'tmp') { |r| r.should == 'one' }
288 | @r.lrange('tmp', 0, -1) { |r| r.should == ['one', 'two', 'three']; done }
289 | end
290 | #
291 | it "should be able add members to a set (SADD)" do
292 | @r.sadd "set", 'key1'
293 | @r.sadd "set", 'key2'
294 | @r.type('set') { |r| r.should == "set" }
295 | @r.scard('set') { |r| r.should == 2 }
296 | @r.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort; done }
297 | end
298 | #
299 | it "should be able delete members to a set (SREM)" do
300 | @r.sadd "set", 'key1'
301 | @r.sadd "set", 'key2'
302 | @r.type('set') { |r| r.should == "set" }
303 | @r.scard('set') { |r| r.should == 2 }
304 | @r.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort }
305 | @r.srem('set', 'key1')
306 | @r.scard('set') { |r| r.should == 1 }
307 | @r.smembers('set') { |r| r.should == ['key2']; done }
308 | end
309 | #
310 | it "should be able to return and remove random key from set (SPOP)" do
311 | @r.sadd "set_pop", "key1"
312 | @r.sadd "set_pop", "key2"
313 | @r.spop("set_pop") { |r| r.should.not == nil }
314 | @r.scard("set_pop") { |r| r.should == 1; done }
315 | end
316 | #
317 | it "should be able to return random key without delete the key from a set (SRANDMEMBER)" do
318 | @r.sadd "set_srandmember", "key1"
319 | @r.sadd "set_srandmember", "key2"
320 | @r.srandmember("set_srandmember") { |r| r.should.not == nil }
321 | @r.scard("set_srandmember") { |r| r.should == 2; done }
322 | end
323 | #
324 | it "should be able count the members of a set (SCARD)" do
325 | @r.sadd "set", 'key1'
326 | @r.sadd "set", 'key2'
327 | @r.type('set') { |r| r.should == "set" }
328 | @r.scard('set') { |r| r.should == 2; done }
329 | end
330 | #
331 | it "should be able test for set membership (SISMEMBER)" do
332 | @r.sadd "set", 'key1'
333 | @r.sadd "set", 'key2'
334 | @r.type('set') { |r| r.should == "set" }
335 | @r.scard('set') { |r| r.should == 2 }
336 | @r.sismember('set', 'key1') { |r| r.should == true }
337 | @r.sismember('set', 'key2') { |r| r.should == true }
338 | @r.sismember('set', 'notthere') { |r| r.should == false; done }
339 | end
340 | #
341 | it "should be able to do set intersection (SINTER)" do
342 | @r.sadd "set", 'key1'
343 | @r.sadd "set", 'key2'
344 | @r.sadd "set2", 'key2'
345 | @r.sinter('set', 'set2') { |r| r.should == ['key2']; done }
346 | end
347 | #
348 | it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do
349 | @r.sadd "set", 'key1'
350 | @r.sadd "set", 'key2'
351 | @r.sadd "set2", 'key2'
352 | @r.sinterstore('newone', 'set', 'set2') { |r| r.should == 1 }
353 | @r.smembers('newone') { |r| r.should == ['key2']; done }
354 | end
355 | #
356 | it "should be able to do set union (SUNION)" do
357 | @r.sadd "set", 'key1'
358 | @r.sadd "set", 'key2'
359 | @r.sadd "set2", 'key2'
360 | @r.sadd "set2", 'key3'
361 | @r.sunion('set', 'set2') { |r| r.sort.should == ['key1','key2','key3'].sort; done }
362 | end
363 | #
364 | it "should be able to do set union and store the results in a key (SUNIONSTORE)" do
365 | @r.sadd "set", 'key1'
366 | @r.sadd "set", 'key2'
367 | @r.sadd "set2", 'key2'
368 | @r.sadd "set2", 'key3'
369 | @r.sunionstore('newone', 'set', 'set2') { |r| r.should == 3 }
370 | @r.smembers('newone') { |r| r.sort.should == ['key1','key2','key3'].sort; done }
371 | end
372 | #
373 | it "should be able to do set difference (SDIFF)" do
374 | @r.sadd "set", 'a'
375 | @r.sadd "set", 'b'
376 | @r.sadd "set2", 'b'
377 | @r.sadd "set2", 'c'
378 | @r.sdiff('set', 'set2') { |r| r.should == ['a']; done }
379 | end
380 | #
381 | it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do
382 | @r.sadd "set", 'a'
383 | @r.sadd "set", 'b'
384 | @r.sadd "set2", 'b'
385 | @r.sadd "set2", 'c'
386 | @r.sdiffstore('newone', 'set', 'set2')
387 | @r.smembers('newone') { |r| r.should == ['a']; done }
388 | end
389 | #
390 | it "should be able move elements from one set to another (SMOVE)" do
391 | @r.sadd 'set1', 'a'
392 | @r.sadd 'set1', 'b'
393 | @r.sadd 'set2', 'x'
394 | @r.smove('set1', 'set2', 'a') { |r| r.should == true }
395 | @r.sismember('set2', 'a') { |r| r.should == true }
396 | @r.delete('set1') { done }
397 | end
398 | #
399 | it "should be able to do crazy SORT queries" do
400 | # The 'Dogs' is capitialized on purpose
401 | @r['dog_1'] = 'louie'
402 | @r.rpush 'Dogs', 1
403 | @r['dog_2'] = 'lucy'
404 | @r.rpush 'Dogs', 2
405 | @r['dog_3'] = 'max'
406 | @r.rpush 'Dogs', 3
407 | @r['dog_4'] = 'taj'
408 | @r.rpush 'Dogs', 4
409 | @r.sort('Dogs', :get => 'dog_*', :limit => [0,1]) { |r| r.should == ['louie'] }
410 | @r.sort('Dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha') { |r| r.should == ['taj'] }
411 | @r.ping { done }
412 | end
413 |
414 | it "should be able to handle array of :get using SORT" do
415 | @r['dog:1:name'] = 'louie'
416 | @r['dog:1:breed'] = 'mutt'
417 | @r.rpush 'dogs', 1
418 | @r['dog:2:name'] = 'lucy'
419 | @r['dog:2:breed'] = 'poodle'
420 | @r.rpush 'dogs', 2
421 | @r['dog:3:name'] = 'max'
422 | @r['dog:3:breed'] = 'hound'
423 | @r.rpush 'dogs', 3
424 | @r['dog:4:name'] = 'taj'
425 | @r['dog:4:breed'] = 'terrier'
426 | @r.rpush 'dogs', 4
427 | @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]) { |r| r.should == ['louie', 'mutt'] }
428 | @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha') { |r| r.should == ['taj', 'terrier'] }
429 | @r.ping { done }
430 | end
431 | #
432 | it "should be able count the members of a zset" do
433 | @r.set_add "set", 'key1'
434 | @r.set_add "set", 'key2'
435 | @r.zset_add 'zset', 1, 'set'
436 | @r.zset_count('zset') { |r| r.should == 1 }
437 | @r.delete('set')
438 | @r.delete('zset') { done }
439 | end
440 | #
441 | it "should be able add members to a zset" do
442 | @r.set_add "set", 'key1'
443 | @r.set_add "set", 'key2'
444 | @r.zset_add 'zset', 1, 'set'
445 | @r.zset_range('zset', 0, 1) { |r| r.should == ['set'] }
446 | @r.zset_count('zset') { |r| r.should == 1 }
447 | @r.delete('set')
448 | @r.delete('zset') { done }
449 | end
450 | #
451 | it "should be able delete members to a zset" do
452 | @r.set_add "set", 'key1'
453 | @r.set_add "set", 'key2'
454 | @r.type?('set') { |r| r.should == "set" }
455 | @r.set_add "set2", 'key3'
456 | @r.set_add "set2", 'key4'
457 | @r.type?('set2') { |r| r.should == "set" }
458 | @r.zset_add 'zset', 1, 'set'
459 | @r.zset_count('zset') { |r| r.should == 1 }
460 | @r.zset_add 'zset', 2, 'set2'
461 | @r.zset_count('zset') { |r| r.should == 2 }
462 | @r.zset_delete 'zset', 'set'
463 | @r.zset_count('zset') { |r| r.should == 1 }
464 | @r.delete('set')
465 | @r.delete('set2')
466 | @r.delete('zset') { done }
467 | end
468 | #
469 | it "should be able to get a range of values from a zset" do
470 | @r.set_add "set", 'key1'
471 | @r.set_add "set", 'key2'
472 | @r.set_add "set2", 'key3'
473 | @r.set_add "set2", 'key4'
474 | @r.set_add "set3", 'key1'
475 | @r.type?('set') { |r| r.should == 'set' }
476 | @r.type?('set2') { |r| r.should == 'set' }
477 | @r.type?('set3') { |r| r.should == 'set' }
478 | @r.zset_add 'zset', 1, 'set'
479 | @r.zset_add 'zset', 2, 'set2'
480 | @r.zset_add 'zset', 3, 'set3'
481 | @r.zset_count('zset') { |r| r.should == 3 }
482 | @r.zset_range('zset', 0, 3) { |r| r.should == ['set', 'set2', 'set3'] }
483 | @r.delete('set')
484 | @r.delete('set2')
485 | @r.delete('set3')
486 | @r.delete('zset') { done }
487 | end
488 | #
489 | it "should be able to get a reverse range of values from a zset" do
490 | @r.set_add "set", 'key1'
491 | @r.set_add "set", 'key2'
492 | @r.set_add "set2", 'key3'
493 | @r.set_add "set2", 'key4'
494 | @r.set_add "set3", 'key1'
495 | @r.type?('set') { |r| r.should == 'set' }
496 | @r.type?('set2') { |r| r.should == 'set' }
497 | @r.type?('set3') { |r| r.should == 'set' }
498 | @r.zset_add 'zset', 1, 'set'
499 | @r.zset_add 'zset', 2, 'set2'
500 | @r.zset_add 'zset', 3, 'set3'
501 | @r.zset_count('zset') { |r| r.should == 3 }
502 | @r.zset_reverse_range('zset', 0, 3) { |r| r.should == ['set3', 'set2', 'set'] }
503 | @r.delete('set')
504 | @r.delete('set2')
505 | @r.delete('set3')
506 | @r.delete('zset') { done }
507 | end
508 | #
509 | it "should be able to get a range by score of values from a zset" do
510 | @r.set_add "set", 'key1'
511 | @r.set_add "set", 'key2'
512 | @r.set_add "set2", 'key3'
513 | @r.set_add "set2", 'key4'
514 | @r.set_add "set3", 'key1'
515 | @r.set_add "set4", 'key4'
516 | @r.zset_add 'zset', 1, 'set'
517 | @r.zset_add 'zset', 2, 'set2'
518 | @r.zset_add 'zset', 3, 'set3'
519 | @r.zset_add 'zset', 4, 'set4'
520 | @r.zset_count('zset') { |r| r.should == 4 }
521 | @r.zset_range_by_score('zset', 2, 3) { |r| r.should == ['set2', 'set3'] }
522 | @r.delete('set')
523 | @r.delete('set2')
524 | @r.delete('set3')
525 | @r.delete('set4')
526 | @r.delete('zset') { done }
527 | end
528 | #
529 | it "should be able to get a score for a specific value in a zset (ZSCORE)" do
530 | @r.zset_add "zset", 23, "value"
531 | @r.zset_score("zset", "value") { |r| r.should == "23" }
532 |
533 | @r.zset_score("zset", "value2") { |r| r.should == nil }
534 | @r.zset_score("unknown_zset", "value") { |r| r.should == nil }
535 |
536 | @r.delete("zset") { done }
537 | end
538 | #
539 | it "should be able to increment a range score of a zset (ZINCRBY)" do
540 | # create a new zset
541 | @r.zset_increment_by "hackers", 1965, "Yukihiro Matsumoto"
542 | @r.zset_score("hackers", "Yukihiro Matsumoto") { |r| r.should == "1965" }
543 |
544 | # add a new element
545 | @r.zset_increment_by "hackers", 1912, "Alan Turing"
546 | @r.zset_score("hackers", "Alan Turing") { |r| r.should == "1912" }
547 |
548 | # update the score
549 | @r.zset_increment_by "hackers", 100, "Alan Turing" # yeah, we are making Turing a bit younger
550 | @r.zset_score("hackers", "Alan Turing") { |r| r.should == "2012" }
551 |
552 | # attempt to update a key that's not a zset
553 | @r["i_am_not_a_zet"] = "value"
554 | # shouldn't raise error anymore
555 | @r.zset_incr_by("i_am_not_a_zet", 23, "element") { |r| r.should == nil }
556 |
557 | @r.delete("hackers")
558 | @r.delete("i_am_not_a_zet") { done }
559 | end
560 | #
561 | it "should provide info (INFO)" do
562 | @r.info do |r|
563 | [:last_save_time, :redis_version, :total_connections_received, :connected_clients, :total_commands_processed, :connected_slaves, :uptime_in_seconds, :used_memory, :uptime_in_days, :changes_since_last_save].each do |x|
564 | r.keys.include?(x).should == true
565 | end
566 | done
567 | end
568 | end
569 | #
570 | it "should be able to flush the database (FLUSHDB)" do
571 | @r['key1'] = 'keyone'
572 | @r['key2'] = 'keytwo'
573 | @r.keys('*') { |r| r.sort.should == ['foo', 'key1', 'key2'].sort } #foo from before
574 | @r.flushdb
575 | @r.keys('*') { |r| r.should == []; done }
576 | end
577 | #
578 | it "should be able to SELECT database" do
579 | @r.select(15)
580 | @r.get('foo') { |r| r.should == nil; done }
581 | end
582 | #
583 | it "should be able to provide the last save time (LASTSAVE)" do
584 | @r.lastsave do |savetime|
585 | Time.at(savetime).class.should == Time
586 | Time.at(savetime).should <= Time.now
587 | done
588 | end
589 | end
590 |
591 | it "should be able to MGET keys" do
592 | @r['foo'] = 1000
593 | @r['bar'] = 2000
594 | @r.mget('foo', 'bar') { |r| r.should == ['1000', '2000'] }
595 | @r.mget('foo', 'bar', 'baz') { |r| r.should == ['1000', '2000', nil] }
596 | @r.ping { done }
597 | end
598 |
599 | it "should be able to mapped MGET keys" do
600 | @r['foo'] = 1000
601 | @r['bar'] = 2000
602 | @r.mapped_mget('foo', 'bar') { |r| r.should == { 'foo' => '1000', 'bar' => '2000'} }
603 | @r.mapped_mget('foo', 'baz', 'bar') { |r| r.should == { 'foo' => '1000', 'bar' => '2000'} }
604 | @r.ping { done }
605 | end
606 |
607 | it "should be able to MSET values" do
608 | @r.mset :key1 => "value1", :key2 => "value2"
609 | @r.get('key1') { |r| r.should == "value1" }
610 | @r.get('key2') { |r| r.should == "value2"; done }
611 | end
612 |
613 | it "should be able to MSETNX values" do
614 | @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2"
615 | @r.mget('keynx1', 'keynx2') { |r| r.should == ["valuenx1", "valuenx2"] }
616 |
617 | @r["keynx1"] = "value1"
618 | @r["keynx2"] = "value2"
619 | @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2"
620 | @r.mget('keynx1', 'keynx2') { |r| r.should == ["value1", "value2"]; done }
621 | end
622 |
623 | it "should bgsave" do
624 | @r.bgsave do |r|
625 | ['OK', 'Background saving started'].include?(r).should == true
626 | done
627 | end
628 | end
629 |
630 | it "should be able to ECHO" do
631 | @r.echo("message in a bottle\n") { |r| r.should == "message in a bottle\n"; done }
632 | end
633 |
634 | # Tests are disabled due uncatchable exceptions. We should use on_error callback,
635 | # intead of raising exceptions in random places.
636 | #
637 | # it "should raise error when invoke MONITOR" do
638 | # # lambda { @r.monitor }.should.raise
639 | # done
640 | # end
641 | #
642 | # it "should raise error when invoke SYNC" do
643 | # # lambda { @r.sync }.should.raise
644 | # done
645 | # end
646 |
647 | it "should work with 10 commands" do
648 | @r.call_commands((1..10).map { |i|
649 | ['get', "foo"]
650 | }) do |rs|
651 | rs.length.should == 10
652 | rs.each { |r| r.should == "bar" }
653 | done
654 | end
655 | end
656 | it "should work with 1 command" do
657 | @r.call_commands([['get', "foo"]]) do |rs|
658 | rs.length.should == 1
659 | rs[0].should == "bar"
660 | done
661 | end
662 | end
663 | it "should work with zero commands" do
664 | @r.call_commands([]) do |rs|
665 | rs.should == []
666 | done
667 | end
668 | end
669 | end
670 |
--------------------------------------------------------------------------------
/spec/redis_protocol_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb")
2 |
3 | EM.describe EM::Protocols::Redis do
4 | default_timeout 1
5 |
6 | before do
7 | @c = TestConnection.new
8 | end
9 |
10 | # Inline request protocol
11 | should 'send inline commands correctly' do
12 | @c.call_command(["GET", 'a'])
13 | @c.sent_data.should == "get a\r\n"
14 | done
15 | end
16 |
17 | should "space-separate multiple inline arguments" do
18 | @c.call_command(["GET", 'a', 'b', 'c'])
19 | @c.sent_data.should == "get a b c\r\n"
20 | done
21 | end
22 |
23 | # Multiline request protocol
24 | should "send multiline commands correctly" do
25 | @c.call_command(["SET", "foo", "abc"])
26 | @c.sent_data.should == "set foo 3\r\nabc\r\n"
27 | done
28 | end
29 |
30 | should "send integers in multiline commands correctly" do
31 | @c.call_command(["SET", "foo", 1_000_000])
32 | @c.sent_data.should == "set foo 7\r\n1000000\r\n"
33 | done
34 | end
35 |
36 | # Specific calls
37 | #
38 | # SORT
39 | should "send sort command" do
40 | @c.sort "foo"
41 | @c.sent_data.should == "sort foo\r\n"
42 | done
43 | end
44 |
45 | should "send sort command with all optional parameters" do
46 | @c.sort "foo", :by => "foo_sort_*", :limit => [0, 10], :get => "data_*", :order => "DESC ALPHA"
47 | @c.sent_data.should == "sort foo BY foo_sort_* GET data_* DESC ALPHA LIMIT 0 10\r\n"
48 | done
49 | end
50 |
51 | should "parse keys response into an array" do
52 | @c.keys("*") do |resp|
53 | resp.should == ["a","b","c"]
54 | done
55 | end
56 | @c.receive_data "$5\r\na b c\r\n"
57 | end
58 |
59 |
60 | # Inline response
61 | should "parse an inline response" do
62 | @c.call_command(["PING"]) do |resp|
63 | resp.should == "OK"
64 | done
65 | end
66 | @c.receive_data "+OK\r\n"
67 | end
68 |
69 | should "parse an inline integer response" do
70 | @c.call_command(["integer"]) do |resp|
71 | resp.should == 0
72 | done
73 | end
74 | @c.receive_data ":0\r\n"
75 | end
76 |
77 | should "call processor if any" do
78 | @c.call_command(["EXISTS"]) do |resp|
79 | resp.should == false
80 | done
81 | end
82 | @c.receive_data ":0\r\n"
83 | end
84 |
85 | should "parse an inline error response" do
86 | @c.call_command(["blarg"]) do |resp|
87 | resp.should == nil
88 | done
89 | end
90 | @c.receive_data "-FAIL\r\n"
91 | end
92 |
93 | should "trigger a given error callback (specified with on_error) for inline error response instead of raising an error" do
94 | lambda do
95 | @c.call_command(["blarg"])
96 | @c.on_error {|code| code.should == "FAIL"; done }
97 | @c.receive_data "-FAIL\r\n"
98 | end.should.not.raise(EM::P::Redis::RedisError)
99 | end
100 |
101 | should "trigger a given error callback for inline error response instead of raising an error" do
102 | lambda do
103 | @c.call_command(["blarg"])
104 | @c.errback { |code| code.should == "FAIL"; done }
105 | @c.receive_data "-FAIL\r\n"
106 | end.should.not.raise(EM::P::Redis::RedisError)
107 | end
108 |
109 | # Bulk response
110 | should "parse a bulk response" do
111 | @c.call_command(["GET", "foo"]) do |resp|
112 | resp.should == "bar"
113 | done
114 | end
115 | @c.receive_data "$3\r\n"
116 | @c.receive_data "bar\r\n"
117 | end
118 |
119 | should "distinguish nil in a bulk response" do
120 | @c.call_command(["GET", "bar"]) do |resp|
121 | resp.should == nil
122 | done
123 | end
124 | @c.receive_data "$-1\r\n"
125 | end
126 |
127 | # Multi-bulk response
128 | should "parse a multi-bulk response" do
129 | @c.call_command(["RANGE", 0, 10]) do |resp|
130 | resp.should == ["a", "b", "foo"]
131 | done
132 | end
133 | @c.receive_data "*3\r\n"
134 | @c.receive_data "$1\r\na\r\n"
135 | @c.receive_data "$1\r\nb\r\n"
136 | @c.receive_data "$3\r\nfoo\r\n"
137 | end
138 |
139 | should "distinguish nil in a multi-bulk response" do
140 | @c.call_command(["RANGE", 0, 10]) do |resp|
141 | resp.should == ["a", nil, "foo"]
142 | done
143 | end
144 | @c.receive_data "*3\r\n"
145 | @c.receive_data "$1\r\na\r\n"
146 | @c.receive_data "$-1\r\n"
147 | @c.receive_data "$3\r\nfoo\r\n"
148 | end
149 | end
150 |
--------------------------------------------------------------------------------
/spec/test_helper.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + "/../lib/em-redis")
2 | require 'bacon'
3 | require 'em-spec/bacon'
4 |
5 | EM.spec_backend = EventMachine::Spec::Bacon
6 |
7 | class TestConnection
8 | include EM::P::Redis
9 |
10 | def send_data data
11 | sent_data << data
12 | end
13 |
14 | def sent_data
15 | @sent_data ||= ''
16 | end
17 |
18 | def initialize
19 | super
20 | connection_completed
21 | end
22 | end
23 |
--------------------------------------------------------------------------------