pax_global_header00006660000000000000000000000064114727032670014523gustar00rootroot0000000000000052 comment=6483d9c370d24faec4d36c3d774cd7c00c8edb20 em-redis-0.3.0/000077500000000000000000000000001147270326700132305ustar00rootroot00000000000000em-redis-0.3.0/.gitignore000066400000000000000000000000161147270326700152150ustar00rootroot00000000000000.DS_Store pkg em-redis-0.3.0/History.txt000066400000000000000000000011661147270326700154360ustar00rootroot00000000000000== 0.2.2 / 2009-12-29 * reselect database after reconnecting == 0.2.1 / 2009-12-15 * updated gem dependencies == 0.2 / 2009-12-15 * rip off stock redis gem * sort is no longer compatible with 0.1 version * 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 * info returns hash of symbols now * lrem has different argument order == 0.1.1 / 2009-05-01 * added a number of aliases to redis-based method names * refactored process_cmd method for greater clarity == 0.1.0 / 2009-04-28 * initial release * compatible with Redis 0.093 em-redis-0.3.0/README.rdoc000066400000000000000000000055541147270326700150470ustar00rootroot00000000000000== EM-REDIS == DESCRIPTION: 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. 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). This library is only useful when used as part of an application that relies on Event Machine's event loop. It implements an EM-based client protocol, which leverages the non-blocking nature of the EM interface to achieve significant parallelization without threads. == FEATURES/PROBLEMS: Implements most Redis commands (see {the list of available commands here}[http://code.google.com/p/redis/wiki/CommandReference] with the notable exception of MONITOR. == SYNOPSIS: Like any Deferrable eventmachine-based protocol implementation, using EM-Redis involves making calls and passing blocks that serve as callbacks when the call returns. require 'em-redis' EM.run do redis = EM::Protocols::Redis.connect redis.errback do |code| puts "Error code: #{code}" end redis.set "a", "foo" do |response| redis.get "a" do |response| puts response end end # We get pipelining for free redis.set("b", "bar") redis.get("a") do |response| puts response # will be foo end end To run tests on a Redis server (currently compatible with 2.2) rake Because the EM::Protocol::Memcached code used Bacon for testing, test code is currently in the form of bacon specs. == REQUIREMENTS: * Redis (download[http://code.google.com/p/redis/downloads/list]) == INSTALL: sudo gem install em-redis == LICENSE: (The MIT License) Copyright (c) 2008, 2009 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. == CREDIT by Jonathan Broad (http://www.relativepath.org) em-redis-0.3.0/Rakefile000066400000000000000000000011411147270326700146720ustar00rootroot00000000000000require 'rake/gempackagetask' require 'rubygems/specification' require File.expand_path('../lib/em-redis', __FILE__) task :default => ['redis:test'] spec = eval(File.read('em-redis.gemspec')) Rake::GemPackageTask.new(spec) do |pkg| pkg.gem_spec = spec end desc "install the gem locally" task :install => [:package] do require version_rb sh %{sudo gem install pkg/em-redis-#{EMRedis::VERSION}} end namespace :redis do desc "Test em-redis against a live Redis" task :test do sh "bacon spec/live_redis_protocol_spec.rb spec/redis_commands_spec.rb spec/redis_protocol_spec.rb" end end # EOF em-redis-0.3.0/em-redis.gemspec000066400000000000000000000010321147270326700162760ustar00rootroot00000000000000require File.expand_path('../lib/em-redis/version.rb', __FILE__) Gem::Specification.new do |s| s.name = "em-redis" s.version = EMRedis::VERSION s.authors = ['Jonathan Broad', 'Eugene Pimenov'] s.email = 'libc@me.com' s.homepage = 'http://github.com/libc/em-redis' s.files = Dir['lib/**/*', '*.txt'] s.require_paths = ["lib"] s.summary = "An eventmachine-based implementation of the Redis protocol" s.description = s.summary s.add_dependency "eventmachine" s.add_development_dependency "bundler", "~>1.0.rc.6" end em-redis-0.3.0/lib/000077500000000000000000000000001147270326700137765ustar00rootroot00000000000000em-redis-0.3.0/lib/em-redis.rb000066400000000000000000000001721147270326700160300ustar00rootroot00000000000000require File.expand_path('../em-redis/version', __FILE__) require File.expand_path('../em-redis/redis_protocol', __FILE__)em-redis-0.3.0/lib/em-redis/000077500000000000000000000000001147270326700155035ustar00rootroot00000000000000em-redis-0.3.0/lib/em-redis/redis_protocol.rb000066400000000000000000000337331147270326700210700ustar00rootroot00000000000000require 'rubygems' require 'eventmachine' module EventMachine module Protocols module Redis include EM::Deferrable ## # constants ######################### OK = "OK".freeze MINUS = "-".freeze PLUS = "+".freeze COLON = ":".freeze DOLLAR = "$".freeze ASTERISK = "*".freeze DELIM = "\r\n".freeze BOOLEAN_PROCESSOR = lambda{|r| r == 1 } REPLY_PROCESSOR = { "exists" => BOOLEAN_PROCESSOR, "sismember" => BOOLEAN_PROCESSOR, "sadd" => BOOLEAN_PROCESSOR, "srem" => BOOLEAN_PROCESSOR, "smove" => BOOLEAN_PROCESSOR, "zadd" => BOOLEAN_PROCESSOR, "zrem" => BOOLEAN_PROCESSOR, "move" => BOOLEAN_PROCESSOR, "setnx" => BOOLEAN_PROCESSOR, "del" => BOOLEAN_PROCESSOR, "renamenx" => BOOLEAN_PROCESSOR, "expire" => BOOLEAN_PROCESSOR, "select" => BOOLEAN_PROCESSOR, # not in redis gem "hset" => BOOLEAN_PROCESSOR, "hdel" => BOOLEAN_PROCESSOR, "hexists" => BOOLEAN_PROCESSOR, "keys" => lambda {|r| if r.is_a?(Array) r else r.split(" ") end }, "info" => lambda{|r| info = {} r.each_line {|kv| k,v = kv.split(":",2).map{|x| x.chomp} info[k.to_sym] = v } info }, "hgetall" => lambda{|r| Hash[*r] } } ALIASES = { "flush_db" => "flushdb", "flush_all" => "flushall", "last_save" => "lastsave", "key?" => "exists", "delete" => "del", "randkey" => "randomkey", "list_length" => "llen", "push_tail" => "rpush", "push_head" => "lpush", "pop_tail" => "rpop", "pop_head" => "lpop", "list_set" => "lset", "list_range" => "lrange", "list_trim" => "ltrim", "list_index" => "lindex", "list_rm" => "lrem", "set_add" => "sadd", "set_delete" => "srem", "set_count" => "scard", "set_member?" => "sismember", "set_members" => "smembers", "set_intersect" => "sinter", "set_intersect_store" => "sinterstore", "set_inter_store" => "sinterstore", "set_union" => "sunion", "set_union_store" => "sunionstore", "set_diff" => "sdiff", "set_diff_store" => "sdiffstore", "set_move" => "smove", "set_unless_exists" => "setnx", "rename_unless_exists" => "renamenx", "type?" => "type", "zset_add" => "zadd", "zset_count" => "zcard", "zset_range_by_score" => "zrangebyscore", "zset_reverse_range" => "zrevrange", "zset_range" => "zrange", "zset_delete" => "zrem", "zset_score" => "zscore", "zset_incr_by" => "zincrby", "zset_increment_by" => "zincrby", # these aliases aren't in redis gem "background_save" => 'bgsave', "async_save" => 'bgsave', "members" => 'smembers', "decrement_by" => "decrby", "decrement" => "decr", "increment_by" => "incrby", "increment" => "incr", "set_if_nil" => "setnx", "multi_get" => "mget", "random_key" => "randomkey", "random" => "randomkey", "rename_if_nil" => "renamenx", "tail_pop" => "rpop", "pop" => "rpop", "head_pop" => "lpop", "shift" => "lpop", "list_remove" => "lrem", "index" => "lindex", "trim" => "ltrim", "list_range" => "lrange", "range" => "lrange", "list_len" => "llen", "len" => "llen", "head_push" => "lpush", "unshift" => "lpush", "tail_push" => "rpush", "push" => "rpush", "add" => "sadd", "set_remove" => "srem", "set_size" => "scard", "member?" => "sismember", "intersect" => "sinter", "intersect_and_store" => "sinterstore", "members" => "smembers", "exists?" => "exists" } DISABLED_COMMANDS = { "monitor" => true, "sync" => true } def []=(key,value) set(key,value) end def set(key, value, expiry=nil) call_command([:set, key, value]) do |s| yield s if block_given? end expire(key, expiry) if expiry end def sort(key, options={}, &blk) cmd = ["SORT"] cmd << key cmd << ["BY", options[:by]] if options[:by] cmd << [options[:get]].flatten.map { |key| ["GET", key] } if options[:get] cmd << options[:order].split(/\s+/) if options[:order] cmd << ["LIMIT", options[:limit]] if options[:limit] call_command(cmd.flatten, &blk) end def incr(key, increment = nil, &blk) call_command(increment ? ["incrby",key,increment] : ["incr",key], &blk) end def decr(key, decrement = nil, &blk) call_command(decrement ? ["decrby",key,decrement] : ["decr",key], &blk) end def select(db, &blk) @db = db.to_i call_command(['select', @db], &blk) end def auth(password, &blk) @password = password call_command(['auth', password], &blk) end # Similar to memcache.rb's #get_multi, returns a hash mapping # keys to values. def mapped_mget(*keys) mget(*keys) do |response| result = {} response.each do |value| key = keys.shift result.merge!(key => value) unless value.nil? end yield result if block_given? end end # Ruby defines a now deprecated type method so we need to override it here # since it will never hit method_missing def type(key, &blk) call_command(['type', key], &blk) end def quit(&blk) call_command(['quit'], &blk) end def exec(&blk) call_command(['exec'], &blk) end # I'm not sure autocommit is a good idea. # For example: # r.multi { r.set('a', 'b') { raise "kaboom" } } # will commit "a" and will stop EM def multi call_command(['multi']) if block_given? begin yield self exec rescue Exception => e discard raise e end end end def mset(*args, &blk) hsh = args.pop if Hash === args.last if hsh call_command(hsh.to_a.flatten.unshift(:mset), &blk) else call_command(args.unshift(:mset), &blk) end end def msetnx(*args, &blk) hsh = args.pop if Hash === args.last if hsh call_command(hsh.to_a.flatten.unshift(:msetnx), &blk) else call_command(args.unshift(:msetnx), &blk) end end def errback(&blk) @error_callback = blk end alias_method :on_error, :errback def method_missing(*argv, &blk) call_command(argv, &blk) end def maybe_lock(&blk) if !EM.reactor_thread? EM.schedule { maybe_lock(&blk) } elsif @connected yield else callback { yield } end end def call_command(argv, &blk) argv = argv.dup argv[0] = argv[0].to_s.downcase argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]] raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]] command = "" command << "*#{argv.size}\r\n" argv.each do |a| a = a.to_s command << "$#{get_size(a)}\r\n" command << a command << "\r\n" end @logger.debug { "*** sending: #{command}" } if @logger maybe_lock do @redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk] send_data command end end ## # errors ######################### class ParserError < StandardError; end class ProtocolError < StandardError; end class RedisError < StandardError attr_accessor :code end ## # em hooks ######################### def self.connect(*args) case args.length when 0 options = {} when 1 arg = args.shift case arg when Hash then options = arg when String then options = {:host => arg} else raise ArgumentError, 'first argument must be Hash or String' end when 2 options = {:host => args[0], :port => args[1]} else raise ArgumentError, "wrong number of arguments (#{args.length} for 1)" end options[:host] ||= '127.0.0.1' options[:port] = (options[:port] || 6379).to_i EM.connect options[:host], options[:port], self, options end def initialize(options = {}) @host = options[:host] @port = options[:port] @db = (options[:db] || 0).to_i @password = options[:password] @logger = options[:logger] @error_callback = lambda do |code| err = RedisError.new err.code = code raise err, "Redis server returned error code: #{code}" end # These commands should be first auth_and_select_db end def auth_and_select_db call_command(["auth", @password]) if @password call_command(["select", @db]) unless @db == 0 end private :auth_and_select_db def connection_completed @logger.debug { "Connected to #{@host}:#{@port}" } if @logger @redis_callbacks = [] @previous_multibulks = [] @multibulk_n = false @reconnecting = false @connected = true succeed end # 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause # stack overflows when there is too much data. # include EM::P::LineText2 def receive_data(data) (@buffer ||= '') << data while index = @buffer.index(DELIM) begin line = @buffer.slice!(0, index+2) process_cmd line rescue ParserError @buffer[0...0] = line break end end end def process_cmd(line) @logger.debug { "*** processing #{line}" } if @logger # first character of buffer will always be the response type reply_type = line[0, 1] reply_args = line.slice(1..-3) # remove type character and \r\n case reply_type #e.g. -MISSING when MINUS @redis_callbacks.shift # throw away the cb? @error_callback.call(reply_args) # e.g. +OK when PLUS dispatch_response(reply_args) # e.g. $3\r\nabc\r\n # 'bulk' is more complex because it could be part of multi-bulk when DOLLAR data_len = Integer(reply_args) if data_len == -1 # expect no data; return nil dispatch_response(nil) elsif @buffer.size >= data_len + 2 # buffer is full of expected data dispatch_response(@buffer.slice!(0, data_len)) @buffer.slice!(0,2) # tossing \r\n else # buffer isn't full or nil # TODO: don't control execution with exceptions raise ParserError end #e.g. :8 when COLON dispatch_response(Integer(reply_args)) #e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n when ASTERISK multibulk_count = Integer(reply_args) if multibulk_count == -1 || multibulk_count == 0 dispatch_response([]) else if @multibulk_n @previous_multibulks << [@multibulk_n, @multibulk_values] end @multibulk_n = multibulk_count @multibulk_values = [] end # Whu? else # TODO: get rid of this exception raise ProtocolError, "reply type not recognized: #{line.strip}" end end def dispatch_response(value) if @multibulk_n @multibulk_values << value @multibulk_n -= 1 if @multibulk_n == 0 value = @multibulk_values @multibulk_n,@multibulk_values = @previous_multibulks.pop if @multibulk_n dispatch_response(value) return end else return end end processor, blk = @redis_callbacks.shift value = processor.call(value) if processor blk.call(value) if blk end def unbind @logger.debug { "Disconnected" } if @logger if @connected || @reconnecting EM.add_timer(1) do @logger.debug { "Reconnecting to #{@host}:#{@port}" } if @logger reconnect @host, @port auth_and_select_db end @connected = false @reconnecting = true @deferred_status = nil else # TODO: get rid of this exception raise 'Unable to connect to redis server' end end private def get_size(string) string.respond_to?(:bytesize) ? string.bytesize : string.size end end end end em-redis-0.3.0/lib/em-redis/version.rb000066400000000000000000000001111147270326700175060ustar00rootroot00000000000000module EMRedis VERSION = '0.3.0' unless defined? ::EMRedis::VERSION endem-redis-0.3.0/spec/000077500000000000000000000000001147270326700141625ustar00rootroot00000000000000em-redis-0.3.0/spec/live_redis_protocol_spec.rb000066400000000000000000000216101147270326700215670ustar00rootroot00000000000000require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb") EM.describe EM::Protocols::Redis, "connected to an empty db" do before do @c = EM::Protocols::Redis.connect :db => 14 @c.flushdb end should "be able to set a string value" do @c.set("foo", "bar") do |r| r.should == "OK" done end end should "be able to increment the value of a string" do @c.incr "foo" do |r| r.should == 1 @c.incr "foo" do |r| r.should == 2 done end end end should "be able to increment the value of a string by an amount" do @c.incrby "foo", 10 do |r| r.should == 10 done end end should "be able to decrement the value of a string" do @c.incr "foo" do |r| r.should == 1 @c.decr "foo" do |r| r.should == 0 done end end end should "be able to decrement the value of a string by an amount" do @c.incrby "foo", 20 do |r| r.should == 20 @c.decrby "foo", 10 do |r| r.should == 10 done end end end should "be able to 'lpush' to a nonexistent list" do @c.lpush("foo", "bar") do |r| r.should == 1 done end end should "be able to 'rpush' to a nonexistent list" do @c.rpush("foo", "bar") do |r| r.should == 1 done end end should "be able to get the size of the database" do @c.dbsize do |r| r.should == 0 done end end should "be able to add a member to a nonexistent set" do @c.sadd("set_foo", "bar") do |r| r.should == true done end end should "be able to get info about the db as a hash" do @c.info do |r| r.should.key? :redis_version done end end should "be able to save db" do @c.save do |r| r.should == "OK" done end end should "be able to save db in the background" do @c.bgsave do |r| r.should == "Background saving started" done end end end EM.describe EM::Protocols::Redis, "connected to a db containing some simple string-valued keys" do before do @c = EM::Protocols::Redis.connect :db => 14 @c.flushdb @c.set "a", "b" @c.set "x", "y" end should "be able to fetch the values of multiple keys" do @c.mget "a", "x" do |r| r.should == ["b", "y"] done end end should "be able to fetch the values of multiple keys in a hash" do @c.mapped_mget "a", "x" do |r| r.should == {"a" => "b", "x" => "y"} done end end should "be able to fetch all the keys" do @c.keys "*" do |r| r.sort.should == ["a", "x"] done end end should "be able to set a value if a key doesn't exist" do @c.setnx "a", "foo" do |r| r.should == false @c.setnx "zzz", "foo" do |r| r.should == true done end end end should "be able to test for the existence of a key" do @c.exists "a" do |r| r.should == true @c.exists "zzz" do |r| r.should == false done end end end should "be able to delete a key" do @c.del "a" do |r| r.should == true @c.exists "a" do |r| r.should == false @c.del "a" do |r| r.should == false done end end end end should "be able to detect the type of a key, existing or not" do @c.type "a" do |r| r.should == "string" @c.type "zzz" do |r| r.should == "none" done end end end should "be able to rename a key" do @c.rename "a", "x" do |r| @c.get "x" do |r| r.should == "b" done end end end should "be able to rename a key unless it exists" do @c.renamenx "a", "x" do |r| r.should == false @c.renamenx "a", "zzz" do |r| r.should == true @c.get "zzz" do |r| r.should == "b" done end end end end end EM.describe EM::Protocols::Redis, "connected to a db containing a list" do before do @c = EM::Protocols::Redis.connect :db => 14 @c.flushdb @c.lpush "foo", "c" @c.lpush "foo", "b" @c.lpush "foo", "a" end should "be able to 'lset' a list member and 'lindex' to retrieve it" do @c.lset("foo", 1, "bar") do |r| @c.lindex("foo", 1) do |r| r.should == "bar" done end end end should "be able to 'rpush' onto the tail of the list" do @c.rpush "foo", "d" do |r| r.should == 4 @c.rpop "foo" do |r| r.should == "d" done end end end should "be able to 'lpush' onto the head of the list" do @c.lpush "foo", "d" do |r| r.should == 4 @c.lpop "foo" do |r| r.should == "d" done end end end should "be able to 'rpop' off the tail of the list" do @c.rpop("foo") do |r| r.should == "c" done end end should "be able to 'lpop' off the tail of the list" do @c.lpop("foo") do |r| r.should == "a" done end end should "be able to get a range of values from a list" do @c.lrange("foo", 0, 1) do |r| r.should == ["a", "b"] done end end should "be able to 'ltrim' a list" do @c.ltrim("foo", 0, 1) do |r| r.should == "OK" @c.llen("foo") do |r| r.should == 2 done end end end should "be able to 'rem' a list element" do @c.lrem("foo", 0, "a") do |r| r.should == 1 @c.llen("foo") do |r| r.should == 2 done end end end should "be able to detect the type of a list" do @c.type "foo" do |r| r.should == "list" done end end end EM.describe EM::Protocols::Redis, "connected to a db containing two sets" do before do @c = EM::Protocols::Redis.connect :db => 14 @c.flushdb @c.sadd "foo", "a" @c.sadd "foo", "b" @c.sadd "foo", "c" @c.sadd "bar", "c" @c.sadd "bar", "d" @c.sadd "bar", "e" end should "be able to find a set's cardinality" do @c.scard("foo") do |r| r.should == 3 done end end should "be able to add a new member to a set unless it is a duplicate" do @c.sadd("foo", "d") do |r| r.should == true # success @c.sadd("foo", "a") do |r| r.should == false # failure @c.scard("foo") do |r| r.should == 4 done end end end end should "be able to remove a set member if it exists" do @c.srem("foo", "a") do |r| r.should == true @c.srem("foo", "z") do |r| r.should == false @c.scard("foo") do |r| r.should == 2 done end end end end should "be able to retrieve a set's members" do @c.smembers("foo") do |r| r.sort.should == ["a", "b", "c"] done end end should "be able to detect set membership" do @c.sismember("foo", "a") do |r| r.should == true @c.sismember("foo", "z") do |r| r.should == false done end end end should "be able to find the sets' intersection" do @c.sinter("foo", "bar") do |r| r.should == ["c"] done end end should "be able to find and store the sets' intersection" do @c.sinterstore("baz", "foo", "bar") do |r| r.should == 1 @c.smembers("baz") do |r| r.should == ["c"] done end end end should "be able to find the sets' union" do @c.sunion("foo", "bar") do |r| r.sort.should == ["a","b","c","d","e"] done end end should "be able to find and store the sets' union" do @c.sunionstore("baz", "foo", "bar") do |r| r.should == 5 @c.smembers("baz") do |r| r.sort.should == ["a","b","c","d","e"] done end end end should "be able to detect the type of a set" do @c.type "foo" do |r| r.should == "set" done end end end EM.describe EM::Protocols::Redis, "connected to a db containing three linked lists" do before do @c = EM::Protocols::Redis.connect :db => 14 @c.flushdb @c.rpush "foo", "a" @c.rpush "foo", "b" @c.set "a_sort", "2" @c.set "b_sort", "1" @c.set "a_data", "foo" @c.set "b_data", "bar" end should "be able to collate a sorted set of data" do @c.sort("foo", :by => "*_sort", :get => "*_data") do |r| r.should == ["bar", "foo"] done end end should "be able to get keys selectively" do @c.keys "a_*" do |r| r.sort.should == ["a_sort", "a_data"].sort done end end end EM.describe EM::Protocols::Redis, "when reconnecting" do before do @c = EM::Protocols::Redis.connect :db => 14 @c.flushdb end should "select previously selected datase" do #simulate disconnect @c.set('foo', 'a') { @c.close_connection_after_writing } EM.add_timer(2) do @c.get('foo') do |r| r.should == 'a' @c.get('non_existing') do |r| r.should == nil done end end end end end em-redis-0.3.0/spec/redis_commands_spec.rb000066400000000000000000000561131147270326700205160ustar00rootroot00000000000000require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb") require 'stringio' require 'logger' EM.describe EM::Protocols::Redis do default_timeout 1 before do @r = EM::Protocols::Redis.connect :db => 14 @r.flushdb @r['foo'] = 'bar' end after { @r.close_connection } should "be able to provide a logger" do log = StringIO.new r = EM::Protocols::Redis.connect :db => 14, :logger => Logger.new(log) r.ping do log.string.should.include "ping" done end end it "should be able to PING" do @r.ping { |r| r.should == 'PONG'; done } end it "should be able to GET a key" do @r.get('foo') { |r| r.should == 'bar'; done } end it "should be able to SET a key" do @r['foo'] = 'nik' @r.get('foo') { |r| r.should == 'nik'; done } end it "should properly handle trailing newline characters" do @r['foo'] = "bar\n" @r.get('foo') { |r| r.should == "bar\n"; done } end it "should store and retrieve all possible characters at the beginning and the end of a string" do (0..255).each do |char_idx| string = "#{char_idx.chr}---#{char_idx.chr}" @r['foo'] = string @r.get('foo') { |r| r.should == string } end @r.ping { done } end it "should be able to SET a key with an expiry" do timeout(3) @r.set('foo', 'bar', 1) @r.get('foo') { |r| r.should == 'bar' } EM.add_timer(2) do @r.get('foo') { |r| r.should == nil } @r.ping { done } end end it "should be able to return a TTL for a key" do @r.set('foo', 'bar', 1) @r.ttl('foo') { |r| r.should == 1; done } end it "should be able to SETNX" do @r['foo'] = 'nik' @r.get('foo') { |r| r.should == 'nik' } @r.setnx 'foo', 'bar' @r.get('foo') { |r| r.should == 'nik' } @r.ping { done } end # it "should be able to GETSET" do @r.getset('foo', 'baz') { |r| r.should == 'bar' } @r.get('foo') { |r| r.should == 'baz'; done } end # it "should be able to INCR a key" do @r.del('counter') @r.incr('counter') { |r| r.should == 1 } @r.incr('counter') { |r| r.should == 2 } @r.incr('counter') { |r| r.should == 3 } @r.ping { done } end # it "should be able to INCRBY a key" do @r.del('counter') @r.incrby('counter', 1) { |r| r.should == 1 } @r.incrby('counter', 2) { |r| r.should == 3 } @r.incrby('counter', 3) { |r| r.should == 6 } @r.ping { done } end # it "should be able to DECR a key" do @r.del('counter') @r.incr('counter') { |r| r.should == 1 } @r.incr('counter') { |r| r.should == 2 } @r.incr('counter') { |r| r.should == 3 } @r.decr('counter') { |r| r.should == 2 } @r.decr('counter', 2) { |r| r.should == 0; done } end # it "should be able to RANDKEY" do @r.randkey { |r| r.should.not == nil; done } end # it "should be able to RENAME a key" do @r.del 'foo' @r.del 'bar' @r['foo'] = 'hi' @r.rename 'foo', 'bar' @r.get('bar') { |r| r.should == 'hi' ; done } end # it "should be able to RENAMENX a key" do @r.del 'foo' @r.del 'bar' @r['foo'] = 'hi' @r['bar'] = 'ohai' @r.renamenx 'foo', 'bar' @r.get('bar') { |r| r.should == 'ohai' ; done } end # it "should be able to get DBSIZE of the database" do dbsize_without_foo, dbsize_with_foo = nil @r.delete 'foo' @r.dbsize { |r| dbsize_without_foo = r } @r['foo'] = 0 @r.dbsize { |r| dbsize_with_foo = r } @r.ping do dbsize_with_foo.should == dbsize_without_foo + 1 done end end # it "should be able to EXPIRE a key" do timeout(3) @r['foo'] = 'bar' @r.expire 'foo', 1 @r.get('foo') { |r| r.should == "bar" } EM.add_timer(2) do @r.get('foo') { |r| r.should == nil } @r.ping { done } end end # it "should be able to EXISTS" do @r['foo'] = 'nik' @r.exists('foo') { |r| r.should == true } @r.del 'foo' @r.exists('foo') { |r| r.should == false ; done } end # it "should be able to KEYS" do @r.keys("f*") { |keys| keys.each { |key| @r.del key } } @r['f'] = 'nik' @r['fo'] = 'nak' @r['foo'] = 'qux' @r.keys("f*") { |r| r.sort.should == ['f', 'fo', 'foo'].sort } @r.ping { done } end # it "should be able to return a random key (RANDOMKEY)" do 3.times do |i| @r.randomkey do |r| @r.exists(r) do |e| e.should == true done if i == 2 end end end end # it "should be able to check the TYPE of a key" do @r['foo'] = 'nik' @r.type('foo') { |r| r.should == "string" } @r.del 'foo' @r.type('foo') { |r| r.should == "none" ; done } end # it "should be able to push to the head of a list (LPUSH)" do @r.lpush "list", 'hello' @r.lpush "list", 42 @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 2 } @r.lpop('list') { |r| r.should == '42'; done } end # it "should be able to push to the tail of a list (RPUSH)" do @r.rpush "list", 'hello' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 1 ; done } end # it "should be able to pop the tail of a list (RPOP)" do @r.rpush "list", 'hello' @r.rpush"list", 'goodbye' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 2 } @r.rpop('list') { |r| r.should == 'goodbye'; done } end # it "should be able to pop the head of a list (LPOP)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 2 } @r.lpop('list') { |r| r.should == 'hello'; done } end # it "should be able to get the length of a list (LLEN)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 2 ; done } end # it "should be able to get a range of values from a list (LRANGE)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @r.rpush "list", '1' @r.rpush "list", '2' @r.rpush "list", '3' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 5 } @r.lrange('list', 2, -1) { |r| r.should == ['1', '2', '3']; done } end # it "should be able to trim a list (LTRIM)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @r.rpush "list", '1' @r.rpush "list", '2' @r.rpush "list", '3' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 5 } @r.ltrim 'list', 0, 1 @r.llen('list') { |r| r.should == 2 } @r.lrange('list', 0, -1) { |r| r.should == ['hello', 'goodbye']; done } end # it "should be able to get a value by indexing into a list (LINDEX)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 2 } @r.lindex('list', 1) { |r| r.should == 'goodbye'; done } end # it "should be able to set a value by indexing into a list (LSET)" do @r.rpush "list", 'hello' @r.rpush "list", 'hello' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 2 } @r.lset('list', 1, 'goodbye') { |r| r.should == 'OK' } @r.lindex('list', 1) { |r| r.should == 'goodbye'; done } end # it "should be able to remove values from a list (LREM)" do @r.rpush "list", 'hello' @r.rpush "list", 'goodbye' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 2 } @r.lrem('list', 1, 'hello') { |r| r.should == 1 } @r.lrange('list', 0, -1) { |r| r.should == ['goodbye']; done } end it "should be able to pop values from a list and push them onto a temp list(RPOPLPUSH)" do @r.rpush "list", 'one' @r.rpush "list", 'two' @r.rpush "list", 'three' @r.type('list') { |r| r.should == "list" } @r.llen('list') { |r| r.should == 3 } @r.lrange('list', 0, -1) { |r| r.should == ['one', 'two', 'three'] } @r.lrange('tmp', 0, -1) { |r| r.should == [] } @r.rpoplpush('list', 'tmp') { |r| r.should == 'three' } @r.lrange('tmp', 0, -1) { |r| r.should == ['three'] } @r.rpoplpush('list', 'tmp') { |r| r.should == 'two' } @r.lrange('tmp', 0, -1) { |r| r.should == ['two', 'three'] } @r.rpoplpush('list', 'tmp') { |r| r.should == 'one' } @r.lrange('tmp', 0, -1) { |r| r.should == ['one', 'two', 'three']; done } end # it "should be able add members to a set (SADD)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.type('set') { |r| r.should == "set" } @r.scard('set') { |r| r.should == 2 } @r.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort; done } end # it "should be able delete members to a set (SREM)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.type('set') { |r| r.should == "set" } @r.scard('set') { |r| r.should == 2 } @r.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort } @r.srem('set', 'key1') @r.scard('set') { |r| r.should == 1 } @r.smembers('set') { |r| r.should == ['key2']; done } end # it "should be able to return and remove random key from set (SPOP)" do @r.sadd "set_pop", "key1" @r.sadd "set_pop", "key2" @r.spop("set_pop") { |r| r.should.not == nil } @r.scard("set_pop") { |r| r.should == 1; done } end # it "should be able to return random key without delete the key from a set (SRANDMEMBER)" do @r.sadd "set_srandmember", "key1" @r.sadd "set_srandmember", "key2" @r.srandmember("set_srandmember") { |r| r.should.not == nil } @r.scard("set_srandmember") { |r| r.should == 2; done } end # it "should be able count the members of a set (SCARD)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.type('set') { |r| r.should == "set" } @r.scard('set') { |r| r.should == 2; done } end # it "should be able test for set membership (SISMEMBER)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.type('set') { |r| r.should == "set" } @r.scard('set') { |r| r.should == 2 } @r.sismember('set', 'key1') { |r| r.should == true } @r.sismember('set', 'key2') { |r| r.should == true } @r.sismember('set', 'notthere') { |r| r.should == false; done } end # it "should be able to do set intersection (SINTER)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.sadd "set2", 'key2' @r.sinter('set', 'set2') { |r| r.should == ['key2']; done } end # it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.sadd "set2", 'key2' @r.sinterstore('newone', 'set', 'set2') { |r| r.should == 1 } @r.smembers('newone') { |r| r.should == ['key2']; done } end # it "should be able to do set union (SUNION)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.sadd "set2", 'key2' @r.sadd "set2", 'key3' @r.sunion('set', 'set2') { |r| r.sort.should == ['key1','key2','key3'].sort; done } end # it "should be able to do set union and store the results in a key (SUNIONSTORE)" do @r.sadd "set", 'key1' @r.sadd "set", 'key2' @r.sadd "set2", 'key2' @r.sadd "set2", 'key3' @r.sunionstore('newone', 'set', 'set2') { |r| r.should == 3 } @r.smembers('newone') { |r| r.sort.should == ['key1','key2','key3'].sort; done } end # it "should be able to do set difference (SDIFF)" do @r.sadd "set", 'a' @r.sadd "set", 'b' @r.sadd "set2", 'b' @r.sadd "set2", 'c' @r.sdiff('set', 'set2') { |r| r.should == ['a']; done } end # it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do @r.sadd "set", 'a' @r.sadd "set", 'b' @r.sadd "set2", 'b' @r.sadd "set2", 'c' @r.sdiffstore('newone', 'set', 'set2') @r.smembers('newone') { |r| r.should == ['a']; done } end # it "should be able move elements from one set to another (SMOVE)" do @r.sadd 'set1', 'a' @r.sadd 'set1', 'b' @r.sadd 'set2', 'x' @r.smove('set1', 'set2', 'a') { |r| r.should == true } @r.sismember('set2', 'a') { |r| r.should == true } @r.delete('set1') { done } end # it "should be able to do crazy SORT queries" do # The 'Dogs' is capitialized on purpose @r['dog_1'] = 'louie' @r.rpush 'Dogs', 1 @r['dog_2'] = 'lucy' @r.rpush 'Dogs', 2 @r['dog_3'] = 'max' @r.rpush 'Dogs', 3 @r['dog_4'] = 'taj' @r.rpush 'Dogs', 4 @r.sort('Dogs', :get => 'dog_*', :limit => [0,1]) { |r| r.should == ['louie'] } @r.sort('Dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha') { |r| r.should == ['taj'] } @r.ping { done } end it "should be able to handle array of :get using SORT" do @r['dog:1:name'] = 'louie' @r['dog:1:breed'] = 'mutt' @r.rpush 'dogs', 1 @r['dog:2:name'] = 'lucy' @r['dog:2:breed'] = 'poodle' @r.rpush 'dogs', 2 @r['dog:3:name'] = 'max' @r['dog:3:breed'] = 'hound' @r.rpush 'dogs', 3 @r['dog:4:name'] = 'taj' @r['dog:4:breed'] = 'terrier' @r.rpush 'dogs', 4 @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]) { |r| r.should == ['louie', 'mutt'] } @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha') { |r| r.should == ['taj', 'terrier'] } @r.ping { done } end # it "should be able count the members of a zset" do @r.set_add "set", 'key1' @r.set_add "set", 'key2' @r.zset_add 'zset', 1, 'set' @r.zset_count('zset') { |r| r.should == 1 } @r.delete('set') @r.delete('zset') { done } end # it "should be able add members to a zset" do @r.set_add "set", 'key1' @r.set_add "set", 'key2' @r.zset_add 'zset', 1, 'set' @r.zset_range('zset', 0, 1) { |r| r.should == ['set'] } @r.zset_count('zset') { |r| r.should == 1 } @r.delete('set') @r.delete('zset') { done } end # it "should be able delete members to a zset" do @r.set_add "set", 'key1' @r.set_add "set", 'key2' @r.type?('set') { |r| r.should == "set" } @r.set_add "set2", 'key3' @r.set_add "set2", 'key4' @r.type?('set2') { |r| r.should == "set" } @r.zset_add 'zset', 1, 'set' @r.zset_count('zset') { |r| r.should == 1 } @r.zset_add 'zset', 2, 'set2' @r.zset_count('zset') { |r| r.should == 2 } @r.zset_delete 'zset', 'set' @r.zset_count('zset') { |r| r.should == 1 } @r.delete('set') @r.delete('set2') @r.delete('zset') { done } end # it "should be able to get a range of values from a zset" do @r.set_add "set", 'key1' @r.set_add "set", 'key2' @r.set_add "set2", 'key3' @r.set_add "set2", 'key4' @r.set_add "set3", 'key1' @r.type?('set') { |r| r.should == 'set' } @r.type?('set2') { |r| r.should == 'set' } @r.type?('set3') { |r| r.should == 'set' } @r.zset_add 'zset', 1, 'set' @r.zset_add 'zset', 2, 'set2' @r.zset_add 'zset', 3, 'set3' @r.zset_count('zset') { |r| r.should == 3 } @r.zset_range('zset', 0, 3) { |r| r.should == ['set', 'set2', 'set3'] } @r.delete('set') @r.delete('set2') @r.delete('set3') @r.delete('zset') { done } end # it "should be able to get a reverse range of values from a zset" do @r.set_add "set", 'key1' @r.set_add "set", 'key2' @r.set_add "set2", 'key3' @r.set_add "set2", 'key4' @r.set_add "set3", 'key1' @r.type?('set') { |r| r.should == 'set' } @r.type?('set2') { |r| r.should == 'set' } @r.type?('set3') { |r| r.should == 'set' } @r.zset_add 'zset', 1, 'set' @r.zset_add 'zset', 2, 'set2' @r.zset_add 'zset', 3, 'set3' @r.zset_count('zset') { |r| r.should == 3 } @r.zset_reverse_range('zset', 0, 3) { |r| r.should == ['set3', 'set2', 'set'] } @r.delete('set') @r.delete('set2') @r.delete('set3') @r.delete('zset') { done } end # it "should be able to get a range by score of values from a zset" do @r.set_add "set", 'key1' @r.set_add "set", 'key2' @r.set_add "set2", 'key3' @r.set_add "set2", 'key4' @r.set_add "set3", 'key1' @r.set_add "set4", 'key4' @r.zset_add 'zset', 1, 'set' @r.zset_add 'zset', 2, 'set2' @r.zset_add 'zset', 3, 'set3' @r.zset_add 'zset', 4, 'set4' @r.zset_count('zset') { |r| r.should == 4 } @r.zset_range_by_score('zset', 2, 3) { |r| r.should == ['set2', 'set3'] } @r.delete('set') @r.delete('set2') @r.delete('set3') @r.delete('set4') @r.delete('zset') { done } end # it "should be able to get a score for a specific value in a zset (ZSCORE)" do @r.zset_add "zset", 23, "value" @r.zset_score("zset", "value") { |r| r.should == "23" } @r.zset_score("zset", "value2") { |r| r.should == nil } @r.zset_score("unknown_zset", "value") { |r| r.should == nil } @r.delete("zset") { done } end # it "should be able to increment a range score of a zset (ZINCRBY)" do # create a new zset @r.zset_increment_by "hackers", 1965, "Yukihiro Matsumoto" @r.zset_score("hackers", "Yukihiro Matsumoto") { |r| r.should == "1965" } # add a new element @r.zset_increment_by "hackers", 1912, "Alan Turing" @r.zset_score("hackers", "Alan Turing") { |r| r.should == "1912" } # update the score @r.zset_increment_by "hackers", 100, "Alan Turing" # yeah, we are making Turing a bit younger @r.zset_score("hackers", "Alan Turing") { |r| r.should == "2012" } # attempt to update a key that's not a zset @r["i_am_not_a_zet"] = "value" # should raise error @r.on_error { true.should == true } @r.zset_incr_by("i_am_not_a_zet", 23, "element") { false.should == true } @r.delete("hackers") @r.delete("i_am_not_a_zet") { done } end # it "should provide info (INFO)" do @r.info do |r| [: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| r.keys.include?(x).should == true end done end end # it "should be able to flush the database (FLUSHDB)" do @r['key1'] = 'keyone' @r['key2'] = 'keytwo' @r.keys('*') { |r| r.sort.should == ['foo', 'key1', 'key2'].sort } #foo from before @r.flushdb @r.keys('*') { |r| r.should == []; done } end # it "should be able to SELECT database" do @r.select(15) @r.get('foo') { |r| r.should == nil; done } end # it "should be able to provide the last save time (LASTSAVE)" do @r.lastsave do |savetime| Time.at(savetime).class.should == Time Time.at(savetime).should <= Time.now done end end it "should be able to MGET keys" do @r['foo'] = 1000 @r['bar'] = 2000 @r.mget('foo', 'bar') { |r| r.should == ['1000', '2000'] } @r.mget('foo', 'bar', 'baz') { |r| r.should == ['1000', '2000', nil] } @r.ping { done } end it "should be able to mapped MGET keys" do @r['foo'] = 1000 @r['bar'] = 2000 @r.mapped_mget('foo', 'bar') { |r| r.should == { 'foo' => '1000', 'bar' => '2000'} } @r.mapped_mget('foo', 'baz', 'bar') { |r| r.should == { 'foo' => '1000', 'bar' => '2000'} } @r.ping { done } end it "should be able to MSET values" do @r.mset :key1 => "value1", :key2 => "value2" @r.get('key1') { |r| r.should == "value1" } @r.get('key2') { |r| r.should == "value2"; done } end it "should be able to MSETNX values" do @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2" @r.mget('keynx1', 'keynx2') { |r| r.should == ["valuenx1", "valuenx2"] } @r["keynx1"] = "value1" @r["keynx2"] = "value2" @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2" @r.mget('keynx1', 'keynx2') { |r| r.should == ["value1", "value2"]; done } end it "should bgsave" do @r.bgsave do |r| ['OK', 'Background saving started'].include?(r).should == true done end end it "should be able to ECHO" do @r.echo("message in a bottle\n") { |r| r.should == "message in a bottle\n"; done } end # Tests are disabled due uncatchable exceptions. We should use on_error callback, # intead of raising exceptions in random places. # # it "should raise error when invoke MONITOR" do # # lambda { @r.monitor }.should.raise # done # end # # it "should raise error when invoke SYNC" do # # lambda { @r.sync }.should.raise # done # end it "should run MULTI without a block" do @r.multi @r.get("key1") { |r| r.should == "QUEUED" } @r.discard { done } end it "should run MULTI/EXEC with a block" do @r.multi do @r.set "key1", "value1" end @r.get("key1") { |r| r.should == "value1" } begin @r.multi do @r.set "key2", "value2" raise "Some error" @r.set "key3", "value3" end rescue end @r.get("key2") { |r| r.should == nil } @r.get("key3") { |r| r.should == nil; done} end it "should yield the Redis object when using #multi with a block" do @r.multi do |multi| multi.set "key1", "value1" end @r.get("key1") { |r| r.should == "value1"; done } end it "can set and get hash values" do @r.hset("rush", "signals", "1982") { |r| r.should == true } @r.hexists("rush", "signals") { |r| r.should == true } @r.hget("rush", "signals") { |r| r.should == "1982"; done } end it "can delete hash values" do @r.hset("rush", "YYZ", "1981") @r.hdel("rush", "YYZ") { |r| r.should == true } @r.hexists("rush", "YYZ") { |r| r.should == false; done } end end # Yup, bacon can't handle nested describe blocks properly EM.describe EM::Protocols::Redis, "with some hash values" do default_timeout 1 before do @r = EM::Protocols::Redis.connect :db => 14 @r.flushdb @r['foo'] = 'bar' @r.hset("rush", "permanent waves", "1980") @r.hset("rush", "moving pictures", "1981") @r.hset("rush", "signals", "1982") end after { @r.close_connection } it "can get the length of the hash" do @r.hlen("rush") { |r| r.should == 3 } @r.hlen("yyz") { |r| r.should == 0; done } end it "can get the keys and values of the hash" do @r.hkeys("rush") { |r| r.should == ["permanent waves", "moving pictures", "signals"] } @r.hvals("rush") { |r| r.should == %w[1980 1981 1982] } @r.hvals("yyz") { |r| r.should == []; done } end it "returns a hash for HGETALL" do @r.hgetall("rush") do |r| r.should == { "permanent waves" => "1980", "moving pictures" => "1981", "signals" => "1982" } end @r.hgetall("yyz") { |r| r.should == {}; done } end end EM.describe EM::Protocols::Redis, "with nested multi-bulk response" do default_timeout 1 before do @r = EM::Protocols::Redis.connect :db => 14 @r.flushdb @r.set 'user:one:id', 'id-one' @r.set 'user:two:id', 'id-two' @r.sadd "user:one:interests", "first-interest" @r.sadd "user:one:interests", "second-interest" @r.sadd "user:two:interests", "third-interest" end after { @r.close_connection } it "returns array of arrays" do @r.multi @r.smembers "user:one:interests" @r.smembers "user:two:interests" @r.exec do |user_interests| user_interests.should == [["second-interest", "first-interest"], ['third-interest']] end @r.mget("user:one:id", "user:two:id") do |user_ids| user_ids.should == ['id-one', 'id-two'] done end end endem-redis-0.3.0/spec/redis_protocol_spec.rb000066400000000000000000000070301147270326700205500ustar00rootroot00000000000000require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb") EM.describe EM::Protocols::Redis do default_timeout 1 before do @c = TestConnection.new end # Converting array of arguments to Redis protocol should "send commands correctly" do @c.call_command(["SET", "foo", "abc"]) @c.sent_data.should == "*3\r\n$3\r\nset\r\n$3\r\nfoo\r\n$3\r\nabc\r\n" done end should "send integers in commands correctly" do @c.call_command(["SET", "foo", 1_000_000]) @c.sent_data.should == "*3\r\n$3\r\nset\r\n$3\r\nfoo\r\n$7\r\n1000000\r\n" done end # Specific calls # # SORT should "send sort command" do @c.sort "foo" @c.sent_data.should == "*2\r\n$4\r\nsort\r\n$3\r\nfoo\r\n" done end should "send sort command with all optional parameters" do @c.sort "foo", :by => "foo_sort_*", :limit => [0, 10], :get => "data_*", :order => "DESC ALPHA" @c.sent_data.should == "*11\r\n$4\r\nsort\r\n$3\r\nfoo\r\n$2\r\nBY\r\n$10\r\nfoo_sort_*\r\n$3\r\nGET\r\n$6\r\ndata_*\r\n$4\r\nDESC\r\n$5\r\nALPHA\r\n$5\r\nLIMIT\r\n$1\r\n0\r\n$2\r\n10\r\n" done end should "parse keys response into an array" do @c.keys("*") do |resp| resp.should == ["a","b","c"] done end @c.receive_data "$5\r\na b c\r\n" end # Inline response should "parse an inline response" do @c.call_command(["PING"]) do |resp| resp.should == "OK" done end @c.receive_data "+OK\r\n" end should "parse an inline integer response" do @c.call_command(["integer"]) do |resp| resp.should == 0 done end @c.receive_data ":0\r\n" end should "call processor if any" do @c.call_command(["EXISTS"]) do |resp| resp.should == false done end @c.receive_data ":0\r\n" end should "parse an inline error response" do lambda do @c.call_command(["blarg"]) @c.receive_data "-FAIL\r\n" end.should.raise(EM::P::Redis::RedisError) done end should "trigger a given error callback (specified with on_error) for inline error response instead of raising an error" do lambda do @c.call_command(["blarg"]) @c.on_error {|code| code.should == "FAIL"; done } @c.receive_data "-FAIL\r\n" end.should.not.raise(EM::P::Redis::RedisError) end should "trigger a given error callback for inline error response instead of raising an error" do lambda do @c.call_command(["blarg"]) @c.errback { |code| code.should == "FAIL"; done } @c.receive_data "-FAIL\r\n" end.should.not.raise(EM::P::Redis::RedisError) end # Bulk response should "parse a bulk response" do @c.call_command(["GET", "foo"]) do |resp| resp.should == "bar" done end @c.receive_data "$3\r\n" @c.receive_data "bar\r\n" end should "distinguish nil in a bulk response" do @c.call_command(["GET", "bar"]) do |resp| resp.should == nil done end @c.receive_data "$-1\r\n" end # Multi-bulk response should "parse a multi-bulk response" do @c.call_command(["RANGE", 0, 10]) do |resp| resp.should == ["a", "b", "foo"] done end @c.receive_data "*3\r\n" @c.receive_data "$1\r\na\r\n" @c.receive_data "$1\r\nb\r\n" @c.receive_data "$3\r\nfoo\r\n" end should "distinguish nil in a multi-bulk response" do @c.call_command(["RANGE", 0, 10]) do |resp| resp.should == ["a", nil, "foo"] done end @c.receive_data "*3\r\n" @c.receive_data "$1\r\na\r\n" @c.receive_data "$-1\r\n" @c.receive_data "$3\r\nfoo\r\n" end end em-redis-0.3.0/spec/test_helper.rb000066400000000000000000000005701147270326700170270ustar00rootroot00000000000000require File.expand_path(File.dirname(__FILE__) + "/../lib/em-redis") require 'em-spec/bacon' EM.spec_backend = EventMachine::Spec::Bacon class TestConnection include EM::P::Redis def send_data data sent_data << data end def sent_data @sent_data ||= '' end def maybe_lock yield end def initialize super connection_completed end end