vanity 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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