vanity 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,352 @@
1
+ require 'socket'
2
+ require File.join(File.dirname(__FILE__),'pipeline')
3
+
4
+ begin
5
+ if RUBY_VERSION >= '1.9'
6
+ require 'timeout'
7
+ RedisTimer = Timeout
8
+ else
9
+ require 'system_timer'
10
+ RedisTimer = SystemTimer
11
+ end
12
+ rescue LoadError
13
+ RedisTimer = nil
14
+ end
15
+
16
+ class Redis
17
+ OK = "OK".freeze
18
+ MINUS = "-".freeze
19
+ PLUS = "+".freeze
20
+ COLON = ":".freeze
21
+ DOLLAR = "$".freeze
22
+ ASTERISK = "*".freeze
23
+
24
+ BULK_COMMANDS = {
25
+ "set" => true,
26
+ "setnx" => true,
27
+ "rpush" => true,
28
+ "lpush" => true,
29
+ "lset" => true,
30
+ "lrem" => true,
31
+ "sadd" => true,
32
+ "srem" => true,
33
+ "sismember" => true,
34
+ "echo" => true,
35
+ "getset" => true,
36
+ "smove" => true
37
+ }
38
+
39
+ MULTI_BULK_COMMANDS = {
40
+ "mset" => true,
41
+ "msetnx" => true
42
+ }
43
+
44
+ BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
45
+
46
+ REPLY_PROCESSOR = {
47
+ "exists" => BOOLEAN_PROCESSOR,
48
+ "sismember" => BOOLEAN_PROCESSOR,
49
+ "sadd" => BOOLEAN_PROCESSOR,
50
+ "srem" => BOOLEAN_PROCESSOR,
51
+ "smove" => BOOLEAN_PROCESSOR,
52
+ "move" => BOOLEAN_PROCESSOR,
53
+ "setnx" => BOOLEAN_PROCESSOR,
54
+ "del" => BOOLEAN_PROCESSOR,
55
+ "renamenx" => BOOLEAN_PROCESSOR,
56
+ "expire" => BOOLEAN_PROCESSOR,
57
+ "keys" => lambda{|r| r.split(" ")},
58
+ "info" => lambda{|r|
59
+ info = {}
60
+ r.each_line {|kv|
61
+ k,v = kv.split(":",2).map{|x| x.chomp}
62
+ info[k.to_sym] = v
63
+ }
64
+ info
65
+ }
66
+ }
67
+
68
+ ALIASES = {
69
+ "flush_db" => "flushdb",
70
+ "flush_all" => "flushall",
71
+ "last_save" => "lastsave",
72
+ "key?" => "exists",
73
+ "delete" => "del",
74
+ "randkey" => "randomkey",
75
+ "list_length" => "llen",
76
+ "push_tail" => "rpush",
77
+ "push_head" => "lpush",
78
+ "pop_tail" => "rpop",
79
+ "pop_head" => "lpop",
80
+ "list_set" => "lset",
81
+ "list_range" => "lrange",
82
+ "list_trim" => "ltrim",
83
+ "list_index" => "lindex",
84
+ "list_rm" => "lrem",
85
+ "set_add" => "sadd",
86
+ "set_delete" => "srem",
87
+ "set_count" => "scard",
88
+ "set_member?" => "sismember",
89
+ "set_members" => "smembers",
90
+ "set_intersect" => "sinter",
91
+ "set_intersect_store" => "sinterstore",
92
+ "set_inter_store" => "sinterstore",
93
+ "set_union" => "sunion",
94
+ "set_union_store" => "sunionstore",
95
+ "set_diff" => "sdiff",
96
+ "set_diff_store" => "sdiffstore",
97
+ "set_move" => "smove",
98
+ "set_unless_exists" => "setnx",
99
+ "rename_unless_exists" => "renamenx",
100
+ "type?" => "type"
101
+ }
102
+
103
+ DISABLED_COMMANDS = {
104
+ "monitor" => true,
105
+ "sync" => true
106
+ }
107
+
108
+ def initialize(options = {})
109
+ @host = options[:host] || '127.0.0.1'
110
+ @port = (options[:port] || 6379).to_i
111
+ @db = (options[:db] || 0).to_i
112
+ @timeout = (options[:timeout] || 5).to_i
113
+ @password = options[:password]
114
+ @logger = options[:logger]
115
+ @thread_safe = options[:thread_safe]
116
+ @mutex = Mutex.new if @thread_safe
117
+
118
+ @logger.info { self.to_s } if @logger
119
+ end
120
+
121
+ def to_s
122
+ "Redis Client connected to #{server} against DB #{@db}"
123
+ end
124
+
125
+ def server
126
+ "#{@host}:#{@port}"
127
+ end
128
+
129
+ def connect_to_server
130
+ @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
131
+ call_command(["auth",@password]) if @password
132
+ call_command(["select",@db]) unless @db == 0
133
+ end
134
+
135
+ def connect_to(host, port, timeout=nil)
136
+ # We support connect() timeout only if system_timer is availabe
137
+ # or if we are running against Ruby >= 1.9
138
+ # Timeout reading from the socket instead will be supported anyway.
139
+ if @timeout != 0 and RedisTimer
140
+ begin
141
+ sock = TCPSocket.new(host, port)
142
+ rescue Timeout::Error
143
+ @sock = nil
144
+ raise Timeout::Error, "Timeout connecting to the server"
145
+ end
146
+ else
147
+ sock = TCPSocket.new(host, port)
148
+ end
149
+ sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
150
+
151
+ # If the timeout is set we set the low level socket options in order
152
+ # to make sure a blocking read will return after the specified number
153
+ # of seconds. This hack is from memcached ruby client.
154
+ if timeout
155
+ secs = Integer(timeout)
156
+ usecs = Integer((timeout - secs) * 1_000_000)
157
+ optval = [secs, usecs].pack("l_2")
158
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
159
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
160
+ end
161
+ sock
162
+ end
163
+
164
+ def method_missing(*argv)
165
+ call_command(argv)
166
+ end
167
+
168
+ def call_command(argv)
169
+ @logger.debug { argv.inspect } if @logger
170
+
171
+ # this wrapper to raw_call_command handle reconnection on socket
172
+ # error. We try to reconnect just one time, otherwise let the error
173
+ # araise.
174
+ connect_to_server if !@sock
175
+
176
+ begin
177
+ raw_call_command(argv.dup)
178
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
179
+ @sock.close
180
+ @sock = nil
181
+ connect_to_server
182
+ raw_call_command(argv.dup)
183
+ end
184
+ end
185
+
186
+ def raw_call_command(argvp)
187
+ pipeline = argvp[0].is_a?(Array)
188
+
189
+ unless pipeline
190
+ argvv = [argvp]
191
+ else
192
+ argvv = argvp
193
+ end
194
+
195
+ if MULTI_BULK_COMMANDS[argvv.flatten[0].to_s]
196
+ # TODO improve this code
197
+ argvp = argvv.flatten
198
+ values = argvp.pop.to_a.flatten
199
+ argvp = values.unshift(argvp[0])
200
+ command = ["*#{argvp.size}"]
201
+ argvp.each do |v|
202
+ v = v.to_s
203
+ command << "$#{get_size(v)}"
204
+ command << v
205
+ end
206
+ command = command.map {|cmd| "#{cmd}\r\n"}.join
207
+ else
208
+ command = ""
209
+ argvv.each do |argv|
210
+ bulk = nil
211
+ argv[0] = argv[0].to_s.downcase
212
+ argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
213
+ raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
214
+ if BULK_COMMANDS[argv[0]] and argv.length > 1
215
+ bulk = argv[-1].to_s
216
+ argv[-1] = get_size(bulk)
217
+ end
218
+ command << "#{argv.join(' ')}\r\n"
219
+ command << "#{bulk}\r\n" if bulk
220
+ end
221
+ end
222
+ results = maybe_lock { process_command(command, argvv) }
223
+
224
+ return pipeline ? results : results[0]
225
+ end
226
+
227
+ def process_command(command, argvv)
228
+ @sock.write(command)
229
+ argvv.map do |argv|
230
+ processor = REPLY_PROCESSOR[argv[0]]
231
+ processor ? processor.call(read_reply) : read_reply
232
+ end
233
+ end
234
+
235
+ def maybe_lock(&block)
236
+ if @thread_safe
237
+ @mutex.synchronize &block
238
+ else
239
+ block.call
240
+ end
241
+ end
242
+
243
+ def select(*args)
244
+ raise "SELECT not allowed, use the :db option when creating the object"
245
+ end
246
+
247
+ def [](key)
248
+ self.get(key)
249
+ end
250
+
251
+ def []=(key,value)
252
+ set(key,value)
253
+ end
254
+
255
+ def set(key, value, expiry=nil)
256
+ s = call_command([:set, key, value]) == OK
257
+ expire(key, expiry) if s && expiry
258
+ s
259
+ end
260
+
261
+ def sort(key, options = {})
262
+ cmd = ["SORT"]
263
+ cmd << key
264
+ cmd << "BY #{options[:by]}" if options[:by]
265
+ cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
266
+ cmd << "#{options[:order]}" if options[:order]
267
+ cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
268
+ call_command(cmd)
269
+ end
270
+
271
+ def incr(key, increment = nil)
272
+ call_command(increment ? ["incrby",key,increment] : ["incr",key])
273
+ end
274
+
275
+ def decr(key,decrement = nil)
276
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
277
+ end
278
+
279
+ # Similar to memcache.rb's #get_multi, returns a hash mapping
280
+ # keys to values.
281
+ def mapped_mget(*keys)
282
+ result = {}
283
+ mget(*keys).each do |value|
284
+ key = keys.shift
285
+ result.merge!(key => value) unless value.nil?
286
+ end
287
+ result
288
+ end
289
+
290
+ # Ruby defines a now deprecated type method so we need to override it here
291
+ # since it will never hit method_missing
292
+ def type(key)
293
+ call_command(['type', key])
294
+ end
295
+
296
+ def quit
297
+ call_command(['quit'])
298
+ rescue Errno::ECONNRESET
299
+ end
300
+
301
+ def pipelined(&block)
302
+ pipeline = Pipeline.new self
303
+ yield pipeline
304
+ pipeline.execute
305
+ end
306
+
307
+ def read_reply
308
+ # We read the first byte using read() mainly because gets() is
309
+ # immune to raw socket timeouts.
310
+ begin
311
+ rtype = @sock.read(1)
312
+ rescue Errno::EAGAIN
313
+ # We want to make sure it reconnects on the next command after the
314
+ # timeout. Otherwise the server may reply in the meantime leaving
315
+ # the protocol in a desync status.
316
+ @sock = nil
317
+ raise Errno::EAGAIN, "Timeout reading from the socket"
318
+ end
319
+
320
+ raise Errno::ECONNRESET,"Connection lost" if !rtype
321
+ line = @sock.gets
322
+ case rtype
323
+ when MINUS
324
+ raise MINUS + line.strip
325
+ when PLUS
326
+ line.strip
327
+ when COLON
328
+ line.to_i
329
+ when DOLLAR
330
+ bulklen = line.to_i
331
+ return nil if bulklen == -1
332
+ data = @sock.read(bulklen)
333
+ @sock.read(2) # CRLF
334
+ data
335
+ when ASTERISK
336
+ objects = line.to_i
337
+ return nil if bulklen == -1
338
+ res = []
339
+ objects.times {
340
+ res << read_reply
341
+ }
342
+ res
343
+ else
344
+ raise "Protocol error, got '#{rtype}' as initial reply byte"
345
+ end
346
+ end
347
+
348
+ private
349
+ def get_size(string)
350
+ string.respond_to?(:bytesize) ? string.bytesize : string.size
351
+ end
352
+ end
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/../../tasks/redis.tasks"
@@ -0,0 +1,524 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'redis/raketasks'
3
+ require 'logger'
4
+
5
+ class Foo
6
+ attr_accessor :bar
7
+ def initialize(bar)
8
+ @bar = bar
9
+ end
10
+
11
+ def ==(other)
12
+ @bar == other.bar
13
+ end
14
+ end
15
+
16
+ describe "redis" do
17
+ before(:all) do
18
+ result = RedisRunner.start_detached
19
+ raise("Could not start redis-server, aborting") unless result
20
+
21
+ # yea, this sucks, but it seems like sometimes we try to connect too quickly w/o it
22
+ sleep 1
23
+
24
+ # use database 15 for testing so we dont accidentally step on you real data
25
+ @r = Redis.new :db => 15
26
+ end
27
+
28
+ before(:each) do
29
+ @r['foo'] = 'bar'
30
+ end
31
+
32
+ after(:each) do
33
+ @r.keys('*').each {|k| @r.del k}
34
+ end
35
+
36
+ after(:all) do
37
+ begin
38
+ @r.quit
39
+ ensure
40
+ RedisRunner.stop
41
+ end
42
+ end
43
+
44
+ it "should be able connect without a timeout" do
45
+ lambda { Redis.new :timeout => 0 }.should_not raise_error
46
+ end
47
+
48
+ it "should be able to provide a logger" do
49
+ log = StringIO.new
50
+ r = Redis.new :db => 15, :logger => Logger.new(log)
51
+ r.ping
52
+ log.string.should include("ping")
53
+ end
54
+
55
+ it "should be able to PING" do
56
+ @r.ping.should == 'PONG'
57
+ end
58
+
59
+ it "should be able to GET a key" do
60
+ @r['foo'].should == 'bar'
61
+ end
62
+
63
+ it "should be able to SET a key" do
64
+ @r['foo'] = 'nik'
65
+ @r['foo'].should == 'nik'
66
+ end
67
+
68
+ it "should properly handle trailing newline characters" do
69
+ @r['foo'] = "bar\n"
70
+ @r['foo'].should == "bar\n"
71
+ end
72
+
73
+ it "should store and retrieve all possible characters at the beginning and the end of a string" do
74
+ (0..255).each do |char_idx|
75
+ string = "#{char_idx.chr}---#{char_idx.chr}"
76
+ @r['foo'] = string
77
+ @r['foo'].should == string
78
+ end
79
+ end
80
+
81
+ it "should be able to SET a key with an expiry" do
82
+ @r.set('foo', 'bar', 1)
83
+ @r['foo'].should == 'bar'
84
+ sleep 2
85
+ @r['foo'].should == nil
86
+ end
87
+
88
+ it "should be able to return a TTL for a key" do
89
+ @r.set('foo', 'bar', 1)
90
+ @r.ttl('foo').should == 1
91
+ end
92
+
93
+ it "should be able to SETNX" do
94
+ @r['foo'] = 'nik'
95
+ @r['foo'].should == 'nik'
96
+ @r.setnx 'foo', 'bar'
97
+ @r['foo'].should == 'nik'
98
+ end
99
+ #
100
+ it "should be able to GETSET" do
101
+ @r.getset('foo', 'baz').should == 'bar'
102
+ @r['foo'].should == 'baz'
103
+ end
104
+ #
105
+ it "should be able to INCR a key" do
106
+ @r.del('counter')
107
+ @r.incr('counter').should == 1
108
+ @r.incr('counter').should == 2
109
+ @r.incr('counter').should == 3
110
+ end
111
+ #
112
+ it "should be able to INCRBY a key" do
113
+ @r.del('counter')
114
+ @r.incrby('counter', 1).should == 1
115
+ @r.incrby('counter', 2).should == 3
116
+ @r.incrby('counter', 3).should == 6
117
+ end
118
+ #
119
+ it "should be able to DECR a key" do
120
+ @r.del('counter')
121
+ @r.incr('counter').should == 1
122
+ @r.incr('counter').should == 2
123
+ @r.incr('counter').should == 3
124
+ @r.decr('counter').should == 2
125
+ @r.decr('counter', 2).should == 0
126
+ end
127
+ #
128
+ it "should be able to RANDKEY" do
129
+ @r.randkey.should_not be_nil
130
+ end
131
+ #
132
+ it "should be able to RENAME a key" do
133
+ @r.del 'foo'
134
+ @r.del'bar'
135
+ @r['foo'] = 'hi'
136
+ @r.rename 'foo', 'bar'
137
+ @r['bar'].should == 'hi'
138
+ end
139
+ #
140
+ it "should be able to RENAMENX a key" do
141
+ @r.del 'foo'
142
+ @r.del 'bar'
143
+ @r['foo'] = 'hi'
144
+ @r['bar'] = 'ohai'
145
+ @r.renamenx 'foo', 'bar'
146
+ @r['bar'].should == 'ohai'
147
+ end
148
+ #
149
+ it "should be able to get DBSIZE of the database" do
150
+ @r.delete 'foo'
151
+ dbsize_without_foo = @r.dbsize
152
+ @r['foo'] = 0
153
+ dbsize_with_foo = @r.dbsize
154
+
155
+ dbsize_with_foo.should == dbsize_without_foo + 1
156
+ end
157
+ #
158
+ it "should be able to EXPIRE a key" do
159
+ @r['foo'] = 'bar'
160
+ @r.expire 'foo', 1
161
+ @r['foo'].should == "bar"
162
+ sleep 2
163
+ @r['foo'].should == nil
164
+ end
165
+ #
166
+ it "should be able to EXISTS" do
167
+ @r['foo'] = 'nik'
168
+ @r.exists('foo').should be_true
169
+ @r.del 'foo'
170
+ @r.exists('foo').should be_false
171
+ end
172
+ #
173
+ it "should be able to KEYS" do
174
+ @r.keys("f*").each { |key| @r.del key }
175
+ @r['f'] = 'nik'
176
+ @r['fo'] = 'nak'
177
+ @r['foo'] = 'qux'
178
+ @r.keys("f*").sort.should == ['f','fo', 'foo'].sort
179
+ end
180
+ #
181
+ it "should be able to return a random key (RANDOMKEY)" do
182
+ 3.times { @r.exists(@r.randomkey).should be_true }
183
+ end
184
+ #
185
+ it "should be able to check the TYPE of a key" do
186
+ @r['foo'] = 'nik'
187
+ @r.type('foo').should == "string"
188
+ @r.del 'foo'
189
+ @r.type('foo').should == "none"
190
+ end
191
+ #
192
+ it "should be able to push to the head of a list (LPUSH)" do
193
+ @r.lpush "list", 'hello'
194
+ @r.lpush "list", 42
195
+ @r.type('list').should == "list"
196
+ @r.llen('list').should == 2
197
+ @r.lpop('list').should == '42'
198
+ end
199
+ #
200
+ it "should be able to push to the tail of a list (RPUSH)" do
201
+ @r.rpush "list", 'hello'
202
+ @r.type('list').should == "list"
203
+ @r.llen('list').should == 1
204
+ end
205
+ #
206
+ it "should be able to pop the tail of a list (RPOP)" do
207
+ @r.rpush "list", 'hello'
208
+ @r.rpush"list", 'goodbye'
209
+ @r.type('list').should == "list"
210
+ @r.llen('list').should == 2
211
+ @r.rpop('list').should == 'goodbye'
212
+ end
213
+ #
214
+ it "should be able to pop the head of a list (LPOP)" do
215
+ @r.rpush "list", 'hello'
216
+ @r.rpush "list", 'goodbye'
217
+ @r.type('list').should == "list"
218
+ @r.llen('list').should == 2
219
+ @r.lpop('list').should == 'hello'
220
+ end
221
+ #
222
+ it "should be able to get the length of a list (LLEN)" do
223
+ @r.rpush "list", 'hello'
224
+ @r.rpush "list", 'goodbye'
225
+ @r.type('list').should == "list"
226
+ @r.llen('list').should == 2
227
+ end
228
+ #
229
+ it "should be able to get a range of values from a list (LRANGE)" do
230
+ @r.rpush "list", 'hello'
231
+ @r.rpush "list", 'goodbye'
232
+ @r.rpush "list", '1'
233
+ @r.rpush "list", '2'
234
+ @r.rpush "list", '3'
235
+ @r.type('list').should == "list"
236
+ @r.llen('list').should == 5
237
+ @r.lrange('list', 2, -1).should == ['1', '2', '3']
238
+ end
239
+ #
240
+ it "should be able to trim a list (LTRIM)" do
241
+ @r.rpush "list", 'hello'
242
+ @r.rpush "list", 'goodbye'
243
+ @r.rpush "list", '1'
244
+ @r.rpush "list", '2'
245
+ @r.rpush "list", '3'
246
+ @r.type('list').should == "list"
247
+ @r.llen('list').should == 5
248
+ @r.ltrim 'list', 0, 1
249
+ @r.llen('list').should == 2
250
+ @r.lrange('list', 0, -1).should == ['hello', 'goodbye']
251
+ end
252
+ #
253
+ it "should be able to get a value by indexing into a list (LINDEX)" do
254
+ @r.rpush "list", 'hello'
255
+ @r.rpush "list", 'goodbye'
256
+ @r.type('list').should == "list"
257
+ @r.llen('list').should == 2
258
+ @r.lindex('list', 1).should == 'goodbye'
259
+ end
260
+ #
261
+ it "should be able to set a value by indexing into a list (LSET)" do
262
+ @r.rpush "list", 'hello'
263
+ @r.rpush "list", 'hello'
264
+ @r.type('list').should == "list"
265
+ @r.llen('list').should == 2
266
+ @r.lset('list', 1, 'goodbye').should == 'OK'
267
+ @r.lindex('list', 1).should == 'goodbye'
268
+ end
269
+ #
270
+ it "should be able to remove values from a list (LREM)" do
271
+ @r.rpush "list", 'hello'
272
+ @r.rpush "list", 'goodbye'
273
+ @r.type('list').should == "list"
274
+ @r.llen('list').should == 2
275
+ @r.lrem('list', 1, 'hello').should == 1
276
+ @r.lrange('list', 0, -1).should == ['goodbye']
277
+ end
278
+ #
279
+ it "should be able add members to a set (SADD)" do
280
+ @r.sadd "set", 'key1'
281
+ @r.sadd "set", 'key2'
282
+ @r.type('set').should == "set"
283
+ @r.scard('set').should == 2
284
+ @r.smembers('set').sort.should == ['key1', 'key2'].sort
285
+ end
286
+ #
287
+ it "should be able delete members to a set (SREM)" do
288
+ @r.sadd "set", 'key1'
289
+ @r.sadd "set", 'key2'
290
+ @r.type('set').should == "set"
291
+ @r.scard('set').should == 2
292
+ @r.smembers('set').sort.should == ['key1', 'key2'].sort
293
+ @r.srem('set', 'key1')
294
+ @r.scard('set').should == 1
295
+ @r.smembers('set').should == ['key2']
296
+ end
297
+ #
298
+ it "should be able to return and remove random key from set (SPOP)" do
299
+ @r.sadd "set_pop", "key1"
300
+ @r.sadd "set_pop", "key2"
301
+ @r.spop("set_pop").should_not be_nil
302
+ @r.scard("set_pop").should == 1
303
+ end
304
+ #
305
+ it "should be able to return random key without delete the key from a set (SRANDMEMBER)" do
306
+ @r.sadd "set_srandmember", "key1"
307
+ @r.sadd "set_srandmember", "key2"
308
+ @r.srandmember("set_srandmember").should_not be_nil
309
+ @r.scard("set_srandmember").should == 2
310
+ end
311
+ #
312
+ it "should be able count the members of a set (SCARD)" do
313
+ @r.sadd "set", 'key1'
314
+ @r.sadd "set", 'key2'
315
+ @r.type('set').should == "set"
316
+ @r.scard('set').should == 2
317
+ end
318
+ #
319
+ it "should be able test for set membership (SISMEMBER)" do
320
+ @r.sadd "set", 'key1'
321
+ @r.sadd "set", 'key2'
322
+ @r.type('set').should == "set"
323
+ @r.scard('set').should == 2
324
+ @r.sismember('set', 'key1').should be_true
325
+ @r.sismember('set', 'key2').should be_true
326
+ @r.sismember('set', 'notthere').should be_false
327
+ end
328
+ #
329
+ it "should be able to do set intersection (SINTER)" do
330
+ @r.sadd "set", 'key1'
331
+ @r.sadd "set", 'key2'
332
+ @r.sadd "set2", 'key2'
333
+ @r.sinter('set', 'set2').should == ['key2']
334
+ end
335
+ #
336
+ it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do
337
+ @r.sadd "set", 'key1'
338
+ @r.sadd "set", 'key2'
339
+ @r.sadd "set2", 'key2'
340
+ @r.sinterstore('newone', 'set', 'set2').should == 1
341
+ @r.smembers('newone').should == ['key2']
342
+ end
343
+ #
344
+ it "should be able to do set union (SUNION)" do
345
+ @r.sadd "set", 'key1'
346
+ @r.sadd "set", 'key2'
347
+ @r.sadd "set2", 'key2'
348
+ @r.sadd "set2", 'key3'
349
+ @r.sunion('set', 'set2').sort.should == ['key1','key2','key3'].sort
350
+ end
351
+ #
352
+ it "should be able to do set union and store the results in a key (SUNIONSTORE)" do
353
+ @r.sadd "set", 'key1'
354
+ @r.sadd "set", 'key2'
355
+ @r.sadd "set2", 'key2'
356
+ @r.sadd "set2", 'key3'
357
+ @r.sunionstore('newone', 'set', 'set2').should == 3
358
+ @r.smembers('newone').sort.should == ['key1','key2','key3'].sort
359
+ end
360
+ #
361
+ it "should be able to do set difference (SDIFF)" do
362
+ @r.sadd "set", 'a'
363
+ @r.sadd "set", 'b'
364
+ @r.sadd "set2", 'b'
365
+ @r.sadd "set2", 'c'
366
+ @r.sdiff('set', 'set2').should == ['a']
367
+ end
368
+ #
369
+ it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do
370
+ @r.sadd "set", 'a'
371
+ @r.sadd "set", 'b'
372
+ @r.sadd "set2", 'b'
373
+ @r.sadd "set2", 'c'
374
+ @r.sdiffstore('newone', 'set', 'set2')
375
+ @r.smembers('newone').should == ['a']
376
+ end
377
+ #
378
+ it "should be able move elements from one set to another (SMOVE)" do
379
+ @r.sadd 'set1', 'a'
380
+ @r.sadd 'set1', 'b'
381
+ @r.sadd 'set2', 'x'
382
+ @r.smove('set1', 'set2', 'a').should be_true
383
+ @r.sismember('set2', 'a').should be_true
384
+ @r.delete('set1')
385
+ end
386
+ #
387
+ it "should be able to do crazy SORT queries" do
388
+ # The 'Dogs' is capitialized on purpose
389
+ @r['dog_1'] = 'louie'
390
+ @r.rpush 'Dogs', 1
391
+ @r['dog_2'] = 'lucy'
392
+ @r.rpush 'Dogs', 2
393
+ @r['dog_3'] = 'max'
394
+ @r.rpush 'Dogs', 3
395
+ @r['dog_4'] = 'taj'
396
+ @r.rpush 'Dogs', 4
397
+ @r.sort('Dogs', :get => 'dog_*', :limit => [0,1]).should == ['louie']
398
+ @r.sort('Dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha').should == ['taj']
399
+ end
400
+
401
+ it "should be able to handle array of :get using SORT" do
402
+ @r['dog:1:name'] = 'louie'
403
+ @r['dog:1:breed'] = 'mutt'
404
+ @r.rpush 'dogs', 1
405
+ @r['dog:2:name'] = 'lucy'
406
+ @r['dog:2:breed'] = 'poodle'
407
+ @r.rpush 'dogs', 2
408
+ @r['dog:3:name'] = 'max'
409
+ @r['dog:3:breed'] = 'hound'
410
+ @r.rpush 'dogs', 3
411
+ @r['dog:4:name'] = 'taj'
412
+ @r['dog:4:breed'] = 'terrier'
413
+ @r.rpush 'dogs', 4
414
+ @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]).should == ['louie', 'mutt']
415
+ @r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha').should == ['taj', 'terrier']
416
+ end
417
+ #
418
+ it "should provide info (INFO)" do
419
+ [: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|
420
+ @r.info.keys.should include(x)
421
+ end
422
+ end
423
+ #
424
+ it "should be able to flush the database (FLUSHDB)" do
425
+ @r['key1'] = 'keyone'
426
+ @r['key2'] = 'keytwo'
427
+ @r.keys('*').sort.should == ['foo', 'key1', 'key2'].sort #foo from before
428
+ @r.flushdb
429
+ @r.keys('*').should == []
430
+ end
431
+ #
432
+ it "should raise exception when manually try to change the database" do
433
+ lambda { @r.select(0) }.should raise_error
434
+ end
435
+ #
436
+ it "should be able to provide the last save time (LASTSAVE)" do
437
+ savetime = @r.lastsave
438
+ Time.at(savetime).class.should == Time
439
+ Time.at(savetime).should <= Time.now
440
+ end
441
+
442
+ it "should be able to MGET keys" do
443
+ @r['foo'] = 1000
444
+ @r['bar'] = 2000
445
+ @r.mget('foo', 'bar').should == ['1000', '2000']
446
+ @r.mget('foo', 'bar', 'baz').should == ['1000', '2000', nil]
447
+ end
448
+
449
+ it "should be able to mapped MGET keys" do
450
+ @r['foo'] = 1000
451
+ @r['bar'] = 2000
452
+ @r.mapped_mget('foo', 'bar').should == { 'foo' => '1000', 'bar' => '2000'}
453
+ @r.mapped_mget('foo', 'baz', 'bar').should == { 'foo' => '1000', 'bar' => '2000'}
454
+ end
455
+
456
+ it "should be able to MSET values" do
457
+ @r.mset :key1 => "value1", :key2 => "value2"
458
+ @r['key1'].should == "value1"
459
+ @r['key2'].should == "value2"
460
+ end
461
+
462
+ it "should be able to MSETNX values" do
463
+ @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2"
464
+ @r.mget('keynx1', 'keynx2').should == ["valuenx1", "valuenx2"]
465
+
466
+ @r["keynx1"] = "value1"
467
+ @r["keynx2"] = "value2"
468
+ @r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2"
469
+ @r.mget('keynx1', 'keynx2').should == ["value1", "value2"]
470
+ end
471
+
472
+ it "should bgsave" do
473
+ @r.bgsave.should == 'OK'
474
+ end
475
+
476
+ it "should be able to ECHO" do
477
+ @r.echo("message in a bottle\n").should == "message in a bottle\n"
478
+ end
479
+
480
+ it "should raise error when invoke MONITOR" do
481
+ lambda { @r.monitor }.should raise_error
482
+ end
483
+
484
+ it "should raise error when invoke SYNC" do
485
+ lambda { @r.sync }.should raise_error
486
+ end
487
+
488
+ it "should handle multiple servers" do
489
+ require 'dist_redis'
490
+ @r = DistRedis.new(:hosts=> ['localhost:6379', '127.0.0.1:6379'], :db => 15)
491
+
492
+ 100.times do |idx|
493
+ @r[idx] = "foo#{idx}"
494
+ end
495
+
496
+ 100.times do |idx|
497
+ @r[idx].should == "foo#{idx}"
498
+ end
499
+ end
500
+
501
+ it "should be able to pipeline writes" do
502
+ @r.pipelined do |pipeline|
503
+ pipeline.lpush 'list', "hello"
504
+ pipeline.lpush 'list', 42
505
+ end
506
+
507
+ @r.type('list').should == "list"
508
+ @r.llen('list').should == 2
509
+ @r.lpop('list').should == '42'
510
+ end
511
+
512
+ it "should do nothing when pipelining no commands" do
513
+ @r.pipelined do |pipeline|
514
+ end
515
+ end
516
+
517
+ it "should AUTH when connecting with a password" do
518
+ r = Redis.new(:password => 'secret')
519
+ r.stub!(:connect_to)
520
+ r.should_receive(:call_command).with(['auth', 'secret'])
521
+ r.connect_to_server
522
+ end
523
+
524
+ end