mikeg-vanity 1.3.0
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.
- data/CHANGELOG +153 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +83 -0
- data/bin/vanity +53 -0
- data/lib/vanity.rb +38 -0
- data/lib/vanity/backport.rb +43 -0
- data/lib/vanity/commands.rb +2 -0
- data/lib/vanity/commands/list.rb +21 -0
- data/lib/vanity/commands/report.rb +60 -0
- data/lib/vanity/experiment/ab_test.rb +477 -0
- data/lib/vanity/experiment/base.rb +212 -0
- data/lib/vanity/helpers.rb +59 -0
- data/lib/vanity/metric/active_record.rb +77 -0
- data/lib/vanity/metric/base.rb +221 -0
- data/lib/vanity/metric/google_analytics.rb +70 -0
- data/lib/vanity/mock_redis.rb +76 -0
- data/lib/vanity/playground.rb +197 -0
- data/lib/vanity/rails.rb +22 -0
- data/lib/vanity/rails/dashboard.rb +24 -0
- data/lib/vanity/rails/helpers.rb +158 -0
- data/lib/vanity/rails/testing.rb +11 -0
- data/lib/vanity/templates/_ab_test.erb +26 -0
- data/lib/vanity/templates/_experiment.erb +5 -0
- data/lib/vanity/templates/_experiments.erb +7 -0
- data/lib/vanity/templates/_metric.erb +14 -0
- data/lib/vanity/templates/_metrics.erb +13 -0
- data/lib/vanity/templates/_report.erb +27 -0
- data/lib/vanity/templates/flot.min.js +1 -0
- data/lib/vanity/templates/jquery.min.js +19 -0
- data/lib/vanity/templates/vanity.css +26 -0
- data/lib/vanity/templates/vanity.js +82 -0
- data/test/ab_test_test.rb +656 -0
- data/test/experiment_test.rb +136 -0
- data/test/experiments/age_and_zipcode.rb +19 -0
- data/test/experiments/metrics/cheers.rb +3 -0
- data/test/experiments/metrics/signups.rb +2 -0
- data/test/experiments/metrics/yawns.rb +3 -0
- data/test/experiments/null_abc.rb +5 -0
- data/test/metric_test.rb +518 -0
- data/test/playground_test.rb +10 -0
- data/test/rails_test.rb +104 -0
- data/test/test_helper.rb +135 -0
- data/vanity.gemspec +18 -0
- data/vendor/redis-rb/LICENSE +20 -0
- data/vendor/redis-rb/README.markdown +36 -0
- data/vendor/redis-rb/Rakefile +62 -0
- data/vendor/redis-rb/bench.rb +44 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/bin/distredis +33 -0
- data/vendor/redis-rb/examples/basic.rb +16 -0
- data/vendor/redis-rb/examples/incr-decr.rb +18 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/redis-rb/lib/dist_redis.rb +124 -0
- data/vendor/redis-rb/lib/hash_ring.rb +128 -0
- data/vendor/redis-rb/lib/pipeline.rb +21 -0
- data/vendor/redis-rb/lib/redis.rb +370 -0
- data/vendor/redis-rb/lib/redis/raketasks.rb +1 -0
- data/vendor/redis-rb/profile.rb +22 -0
- data/vendor/redis-rb/redis-rb.gemspec +30 -0
- data/vendor/redis-rb/spec/redis_spec.rb +637 -0
- data/vendor/redis-rb/spec/spec_helper.rb +4 -0
- data/vendor/redis-rb/speed.rb +16 -0
- data/vendor/redis-rb/tasks/redis.tasks.rb +140 -0
- metadata +125 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
class Redis
|
2
|
+
class Pipeline < Redis
|
3
|
+
BUFFER_SIZE = 50_000
|
4
|
+
|
5
|
+
def initialize(redis)
|
6
|
+
@redis = redis
|
7
|
+
@commands = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def call_command(command)
|
11
|
+
@commands << command
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute
|
15
|
+
return if @commands.empty?
|
16
|
+
@redis.call_command(@commands)
|
17
|
+
@commands.clear
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,370 @@
|
|
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
|
+
"rpoplpush" => true,
|
35
|
+
"echo" => true,
|
36
|
+
"getset" => true,
|
37
|
+
"smove" => true,
|
38
|
+
"zadd" => true,
|
39
|
+
"zrem" => true,
|
40
|
+
"zscore" => true
|
41
|
+
}
|
42
|
+
|
43
|
+
MULTI_BULK_COMMANDS = {
|
44
|
+
"mset" => true,
|
45
|
+
"msetnx" => true
|
46
|
+
}
|
47
|
+
|
48
|
+
BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
|
49
|
+
|
50
|
+
REPLY_PROCESSOR = {
|
51
|
+
"exists" => BOOLEAN_PROCESSOR,
|
52
|
+
"sismember" => BOOLEAN_PROCESSOR,
|
53
|
+
"sadd" => BOOLEAN_PROCESSOR,
|
54
|
+
"srem" => BOOLEAN_PROCESSOR,
|
55
|
+
"smove" => BOOLEAN_PROCESSOR,
|
56
|
+
"zadd" => BOOLEAN_PROCESSOR,
|
57
|
+
"zrem" => BOOLEAN_PROCESSOR,
|
58
|
+
"move" => BOOLEAN_PROCESSOR,
|
59
|
+
"setnx" => BOOLEAN_PROCESSOR,
|
60
|
+
"del" => BOOLEAN_PROCESSOR,
|
61
|
+
"renamenx" => BOOLEAN_PROCESSOR,
|
62
|
+
"expire" => BOOLEAN_PROCESSOR,
|
63
|
+
"keys" => lambda{|r| r.split(" ")},
|
64
|
+
"info" => lambda{|r|
|
65
|
+
info = {}
|
66
|
+
r.each_line {|kv|
|
67
|
+
k,v = kv.split(":",2).map{|x| x.chomp}
|
68
|
+
info[k.to_sym] = v
|
69
|
+
}
|
70
|
+
info
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
ALIASES = {
|
75
|
+
"flush_db" => "flushdb",
|
76
|
+
"flush_all" => "flushall",
|
77
|
+
"last_save" => "lastsave",
|
78
|
+
"key?" => "exists",
|
79
|
+
"delete" => "del",
|
80
|
+
"randkey" => "randomkey",
|
81
|
+
"list_length" => "llen",
|
82
|
+
"push_tail" => "rpush",
|
83
|
+
"push_head" => "lpush",
|
84
|
+
"pop_tail" => "rpop",
|
85
|
+
"pop_head" => "lpop",
|
86
|
+
"list_set" => "lset",
|
87
|
+
"list_range" => "lrange",
|
88
|
+
"list_trim" => "ltrim",
|
89
|
+
"list_index" => "lindex",
|
90
|
+
"list_rm" => "lrem",
|
91
|
+
"set_add" => "sadd",
|
92
|
+
"set_delete" => "srem",
|
93
|
+
"set_count" => "scard",
|
94
|
+
"set_member?" => "sismember",
|
95
|
+
"set_members" => "smembers",
|
96
|
+
"set_intersect" => "sinter",
|
97
|
+
"set_intersect_store" => "sinterstore",
|
98
|
+
"set_inter_store" => "sinterstore",
|
99
|
+
"set_union" => "sunion",
|
100
|
+
"set_union_store" => "sunionstore",
|
101
|
+
"set_diff" => "sdiff",
|
102
|
+
"set_diff_store" => "sdiffstore",
|
103
|
+
"set_move" => "smove",
|
104
|
+
"set_unless_exists" => "setnx",
|
105
|
+
"rename_unless_exists" => "renamenx",
|
106
|
+
"type?" => "type",
|
107
|
+
"zset_add" => "zadd",
|
108
|
+
"zset_count" => 'zcard',
|
109
|
+
"zset_range_by_score" => 'zrangebyscore',
|
110
|
+
"zset_reverse_range" => 'zrevrange',
|
111
|
+
"zset_range" => 'zrange',
|
112
|
+
"zset_delete" => 'zrem',
|
113
|
+
"zset_score" => 'zscore'
|
114
|
+
}
|
115
|
+
|
116
|
+
DISABLED_COMMANDS = {
|
117
|
+
"monitor" => true,
|
118
|
+
"sync" => true
|
119
|
+
}
|
120
|
+
|
121
|
+
def initialize(options = {})
|
122
|
+
@host = options[:host] || '127.0.0.1'
|
123
|
+
@port = (options[:port] || 6379).to_i
|
124
|
+
@db = (options[:db] || 0).to_i
|
125
|
+
@timeout = (options[:timeout] || 5).to_i
|
126
|
+
@password = options[:password]
|
127
|
+
@logger = options[:logger]
|
128
|
+
@thread_safe = options[:thread_safe]
|
129
|
+
@mutex = Mutex.new if @thread_safe
|
130
|
+
|
131
|
+
@logger.info { self.to_s } if @logger
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_s
|
135
|
+
"Redis Client connected to #{server} against DB #{@db}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def server
|
139
|
+
"#{@host}:#{@port}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def connect_to_server
|
143
|
+
@sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
|
144
|
+
call_command(["auth",@password]) if @password
|
145
|
+
call_command(["select",@db]) unless @db == 0
|
146
|
+
end
|
147
|
+
|
148
|
+
def connect_to(host, port, timeout=nil)
|
149
|
+
# We support connect() timeout only if system_timer is availabe
|
150
|
+
# or if we are running against Ruby >= 1.9
|
151
|
+
# Timeout reading from the socket instead will be supported anyway.
|
152
|
+
if @timeout != 0 and RedisTimer
|
153
|
+
begin
|
154
|
+
sock = TCPSocket.new(host, port)
|
155
|
+
rescue Timeout::Error
|
156
|
+
@sock = nil
|
157
|
+
raise Timeout::Error, "Timeout connecting to the server"
|
158
|
+
end
|
159
|
+
else
|
160
|
+
sock = TCPSocket.new(host, port)
|
161
|
+
end
|
162
|
+
sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
163
|
+
|
164
|
+
# If the timeout is set we set the low level socket options in order
|
165
|
+
# to make sure a blocking read will return after the specified number
|
166
|
+
# of seconds. This hack is from memcached ruby client.
|
167
|
+
if timeout
|
168
|
+
secs = Integer(timeout)
|
169
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
170
|
+
optval = [secs, usecs].pack("l_2")
|
171
|
+
begin
|
172
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
173
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
174
|
+
rescue Exception => ex
|
175
|
+
# Solaris, for one, does not like/support socket timeouts.
|
176
|
+
@logger.info "Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}" if @logger
|
177
|
+
end
|
178
|
+
end
|
179
|
+
sock
|
180
|
+
end
|
181
|
+
|
182
|
+
def method_missing(*argv)
|
183
|
+
call_command(argv)
|
184
|
+
end
|
185
|
+
|
186
|
+
def call_command(argv)
|
187
|
+
@logger.debug { argv.inspect } if @logger
|
188
|
+
|
189
|
+
# this wrapper to raw_call_command handle reconnection on socket
|
190
|
+
# error. We try to reconnect just one time, otherwise let the error
|
191
|
+
# araise.
|
192
|
+
connect_to_server if !@sock
|
193
|
+
|
194
|
+
begin
|
195
|
+
raw_call_command(argv.dup)
|
196
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
|
197
|
+
@sock.close
|
198
|
+
@sock = nil
|
199
|
+
connect_to_server
|
200
|
+
raw_call_command(argv.dup)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def raw_call_command(argvp)
|
205
|
+
pipeline = argvp[0].is_a?(Array)
|
206
|
+
|
207
|
+
unless pipeline
|
208
|
+
argvv = [argvp]
|
209
|
+
else
|
210
|
+
argvv = argvp
|
211
|
+
end
|
212
|
+
|
213
|
+
if MULTI_BULK_COMMANDS[argvv.flatten[0].to_s]
|
214
|
+
# TODO improve this code
|
215
|
+
argvp = argvv.flatten
|
216
|
+
values = argvp.pop.to_a.flatten
|
217
|
+
argvp = values.unshift(argvp[0])
|
218
|
+
command = ["*#{argvp.size}"]
|
219
|
+
argvp.each do |v|
|
220
|
+
v = v.to_s
|
221
|
+
command << "$#{get_size(v)}"
|
222
|
+
command << v
|
223
|
+
end
|
224
|
+
command = command.map {|cmd| "#{cmd}\r\n"}.join
|
225
|
+
else
|
226
|
+
command = ""
|
227
|
+
argvv.each do |argv|
|
228
|
+
bulk = nil
|
229
|
+
argv[0] = argv[0].to_s.downcase
|
230
|
+
argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
|
231
|
+
raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
|
232
|
+
if BULK_COMMANDS[argv[0]] and argv.length > 1
|
233
|
+
bulk = argv[-1].to_s
|
234
|
+
argv[-1] = get_size(bulk)
|
235
|
+
end
|
236
|
+
command << "#{argv.join(' ')}\r\n"
|
237
|
+
command << "#{bulk}\r\n" if bulk
|
238
|
+
end
|
239
|
+
end
|
240
|
+
results = maybe_lock { process_command(command, argvv) }
|
241
|
+
|
242
|
+
return pipeline ? results : results[0]
|
243
|
+
end
|
244
|
+
|
245
|
+
def process_command(command, argvv)
|
246
|
+
@sock.write(command)
|
247
|
+
argvv.map do |argv|
|
248
|
+
processor = REPLY_PROCESSOR[argv[0]]
|
249
|
+
processor ? processor.call(read_reply) : read_reply
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def maybe_lock(&block)
|
254
|
+
if @thread_safe
|
255
|
+
@mutex.synchronize &block
|
256
|
+
else
|
257
|
+
block.call
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def select(*args)
|
262
|
+
raise "SELECT not allowed, use the :db option when creating the object"
|
263
|
+
end
|
264
|
+
|
265
|
+
def [](key)
|
266
|
+
self.get(key)
|
267
|
+
end
|
268
|
+
|
269
|
+
def []=(key,value)
|
270
|
+
set(key,value)
|
271
|
+
end
|
272
|
+
|
273
|
+
def set(key, value, expiry=nil)
|
274
|
+
s = call_command([:set, key, value]) == OK
|
275
|
+
expire(key, expiry) if s && expiry
|
276
|
+
s
|
277
|
+
end
|
278
|
+
|
279
|
+
def sort(key, options = {})
|
280
|
+
cmd = ["SORT"]
|
281
|
+
cmd << key
|
282
|
+
cmd << "BY #{options[:by]}" if options[:by]
|
283
|
+
cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
|
284
|
+
cmd << "#{options[:order]}" if options[:order]
|
285
|
+
cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
|
286
|
+
call_command(cmd)
|
287
|
+
end
|
288
|
+
|
289
|
+
def incr(key, increment = nil)
|
290
|
+
call_command(increment ? ["incrby",key,increment] : ["incr",key])
|
291
|
+
end
|
292
|
+
|
293
|
+
def decr(key,decrement = nil)
|
294
|
+
call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
|
295
|
+
end
|
296
|
+
|
297
|
+
# Similar to memcache.rb's #get_multi, returns a hash mapping
|
298
|
+
# keys to values.
|
299
|
+
def mapped_mget(*keys)
|
300
|
+
result = {}
|
301
|
+
mget(*keys).each do |value|
|
302
|
+
key = keys.shift
|
303
|
+
result.merge!(key => value) unless value.nil?
|
304
|
+
end
|
305
|
+
result
|
306
|
+
end
|
307
|
+
|
308
|
+
# Ruby defines a now deprecated type method so we need to override it here
|
309
|
+
# since it will never hit method_missing
|
310
|
+
def type(key)
|
311
|
+
call_command(['type', key])
|
312
|
+
end
|
313
|
+
|
314
|
+
def quit
|
315
|
+
call_command(['quit'])
|
316
|
+
rescue Errno::ECONNRESET
|
317
|
+
end
|
318
|
+
|
319
|
+
def pipelined(&block)
|
320
|
+
pipeline = Pipeline.new self
|
321
|
+
yield pipeline
|
322
|
+
pipeline.execute
|
323
|
+
end
|
324
|
+
|
325
|
+
def read_reply
|
326
|
+
# We read the first byte using read() mainly because gets() is
|
327
|
+
# immune to raw socket timeouts.
|
328
|
+
begin
|
329
|
+
rtype = @sock.read(1)
|
330
|
+
rescue Errno::EAGAIN
|
331
|
+
# We want to make sure it reconnects on the next command after the
|
332
|
+
# timeout. Otherwise the server may reply in the meantime leaving
|
333
|
+
# the protocol in a desync status.
|
334
|
+
@sock = nil
|
335
|
+
raise Errno::EAGAIN, "Timeout reading from the socket"
|
336
|
+
end
|
337
|
+
|
338
|
+
raise Errno::ECONNRESET,"Connection lost" if !rtype
|
339
|
+
line = @sock.gets
|
340
|
+
case rtype
|
341
|
+
when MINUS
|
342
|
+
raise MINUS + line.strip
|
343
|
+
when PLUS
|
344
|
+
line.strip
|
345
|
+
when COLON
|
346
|
+
line.to_i
|
347
|
+
when DOLLAR
|
348
|
+
bulklen = line.to_i
|
349
|
+
return nil if bulklen == -1
|
350
|
+
data = @sock.read(bulklen)
|
351
|
+
@sock.read(2) # CRLF
|
352
|
+
data
|
353
|
+
when ASTERISK
|
354
|
+
objects = line.to_i
|
355
|
+
return nil if bulklen == -1
|
356
|
+
res = []
|
357
|
+
objects.times {
|
358
|
+
res << read_reply
|
359
|
+
}
|
360
|
+
res
|
361
|
+
else
|
362
|
+
raise "Protocol error, got '#{rtype}' as initial reply byte"
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
private
|
367
|
+
def get_size(string)
|
368
|
+
string.respond_to?(:bytesize) ? string.bytesize : string.size
|
369
|
+
end
|
370
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../../tasks/redis.tasks"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ruby-prof'
|
3
|
+
require "#{File.dirname(__FILE__)}/lib/redis"
|
4
|
+
|
5
|
+
|
6
|
+
mode = ARGV.shift || 'process_time'
|
7
|
+
n = (ARGV.shift || 200).to_i
|
8
|
+
|
9
|
+
r = Redis.new
|
10
|
+
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
|
11
|
+
RubyProf.start
|
12
|
+
|
13
|
+
n.times do |i|
|
14
|
+
key = "foo#{i}"
|
15
|
+
r[key] = key * 10
|
16
|
+
r[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
results = RubyProf.stop
|
20
|
+
File.open("profile.#{mode}", 'w') do |out|
|
21
|
+
RubyProf::CallTreePrinter.new(results).print(out)
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{redis}
|
5
|
+
s.version = "0.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Ezra Zygmuntowicz", "Taylor Weibley", "Matthew Clark", "Brian McKinney", "Salvatore Sanfilippo", "Luca Guidi"]
|
9
|
+
# s.autorequire = %q{redis-rb}
|
10
|
+
s.date = %q{2009-06-23}
|
11
|
+
s.description = %q{Ruby client library for redis key value storage server}
|
12
|
+
s.email = %q{ez@engineyard.com}
|
13
|
+
s.extra_rdoc_files = ["LICENSE"]
|
14
|
+
s.files = ["LICENSE", "README.markdown", "Rakefile", "lib/dist_redis.rb", "lib/hash_ring.rb", "lib/pipeline.rb", "lib/redis.rb", "spec/redis_spec.rb", "spec/spec_helper.rb"]
|
15
|
+
s.has_rdoc = true
|
16
|
+
s.homepage = %q{http://github.com/ezmobius/redis-rb}
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.rubygems_version = %q{1.3.1}
|
19
|
+
s.summary = %q{Ruby client library for redis key value storage server}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 2
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
else
|
27
|
+
end
|
28
|
+
else
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,637 @@
|
|
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 to pop values from a list and push them onto a temp list(RPOPLPUSH)" do
|
280
|
+
@r.rpush "list", 'one'
|
281
|
+
@r.rpush "list", 'two'
|
282
|
+
@r.rpush "list", 'three'
|
283
|
+
@r.type('list').should == "list"
|
284
|
+
@r.llen('list').should == 3
|
285
|
+
@r.lrange('list',0,-1).should == ['one', 'two', 'three']
|
286
|
+
@r.lrange('tmp',0,-1).should == []
|
287
|
+
@r.rpoplpush('list', 'tmp').should == 'three'
|
288
|
+
@r.lrange('tmp',0,-1).should == ['three']
|
289
|
+
@r.rpoplpush('list', 'tmp').should == 'two'
|
290
|
+
@r.lrange('tmp',0,-1).should == ['two', 'three']
|
291
|
+
@r.rpoplpush('list', 'tmp').should == 'one'
|
292
|
+
@r.lrange('tmp',0,-1).should == ['one','two','three']
|
293
|
+
end
|
294
|
+
#
|
295
|
+
it "should be able add members to a set (SADD)" do
|
296
|
+
@r.sadd "set", 'key1'
|
297
|
+
@r.sadd "set", 'key2'
|
298
|
+
@r.type('set').should == "set"
|
299
|
+
@r.scard('set').should == 2
|
300
|
+
@r.smembers('set').sort.should == ['key1', 'key2'].sort
|
301
|
+
end
|
302
|
+
#
|
303
|
+
it "should be able delete members to a set (SREM)" do
|
304
|
+
@r.sadd "set", 'key1'
|
305
|
+
@r.sadd "set", 'key2'
|
306
|
+
@r.type('set').should == "set"
|
307
|
+
@r.scard('set').should == 2
|
308
|
+
@r.smembers('set').sort.should == ['key1', 'key2'].sort
|
309
|
+
@r.srem('set', 'key1')
|
310
|
+
@r.scard('set').should == 1
|
311
|
+
@r.smembers('set').should == ['key2']
|
312
|
+
end
|
313
|
+
#
|
314
|
+
it "should be able to return and remove random key from set (SPOP)" do
|
315
|
+
@r.sadd "set_pop", "key1"
|
316
|
+
@r.sadd "set_pop", "key2"
|
317
|
+
@r.spop("set_pop").should_not be_nil
|
318
|
+
@r.scard("set_pop").should == 1
|
319
|
+
end
|
320
|
+
#
|
321
|
+
it "should be able to return random key without delete the key from a set (SRANDMEMBER)" do
|
322
|
+
@r.sadd "set_srandmember", "key1"
|
323
|
+
@r.sadd "set_srandmember", "key2"
|
324
|
+
@r.srandmember("set_srandmember").should_not be_nil
|
325
|
+
@r.scard("set_srandmember").should == 2
|
326
|
+
end
|
327
|
+
#
|
328
|
+
it "should be able count the members of a set (SCARD)" do
|
329
|
+
@r.sadd "set", 'key1'
|
330
|
+
@r.sadd "set", 'key2'
|
331
|
+
@r.type('set').should == "set"
|
332
|
+
@r.scard('set').should == 2
|
333
|
+
end
|
334
|
+
#
|
335
|
+
it "should be able test for set membership (SISMEMBER)" do
|
336
|
+
@r.sadd "set", 'key1'
|
337
|
+
@r.sadd "set", 'key2'
|
338
|
+
@r.type('set').should == "set"
|
339
|
+
@r.scard('set').should == 2
|
340
|
+
@r.sismember('set', 'key1').should be_true
|
341
|
+
@r.sismember('set', 'key2').should be_true
|
342
|
+
@r.sismember('set', 'notthere').should be_false
|
343
|
+
end
|
344
|
+
#
|
345
|
+
it "should be able to do set intersection (SINTER)" do
|
346
|
+
@r.sadd "set", 'key1'
|
347
|
+
@r.sadd "set", 'key2'
|
348
|
+
@r.sadd "set2", 'key2'
|
349
|
+
@r.sinter('set', 'set2').should == ['key2']
|
350
|
+
end
|
351
|
+
#
|
352
|
+
it "should be able to do set intersection and store the results in a key (SINTERSTORE)" do
|
353
|
+
@r.sadd "set", 'key1'
|
354
|
+
@r.sadd "set", 'key2'
|
355
|
+
@r.sadd "set2", 'key2'
|
356
|
+
@r.sinterstore('newone', 'set', 'set2').should == 1
|
357
|
+
@r.smembers('newone').should == ['key2']
|
358
|
+
end
|
359
|
+
#
|
360
|
+
it "should be able to do set union (SUNION)" do
|
361
|
+
@r.sadd "set", 'key1'
|
362
|
+
@r.sadd "set", 'key2'
|
363
|
+
@r.sadd "set2", 'key2'
|
364
|
+
@r.sadd "set2", 'key3'
|
365
|
+
@r.sunion('set', 'set2').sort.should == ['key1','key2','key3'].sort
|
366
|
+
end
|
367
|
+
#
|
368
|
+
it "should be able to do set union and store the results in a key (SUNIONSTORE)" do
|
369
|
+
@r.sadd "set", 'key1'
|
370
|
+
@r.sadd "set", 'key2'
|
371
|
+
@r.sadd "set2", 'key2'
|
372
|
+
@r.sadd "set2", 'key3'
|
373
|
+
@r.sunionstore('newone', 'set', 'set2').should == 3
|
374
|
+
@r.smembers('newone').sort.should == ['key1','key2','key3'].sort
|
375
|
+
end
|
376
|
+
#
|
377
|
+
it "should be able to do set difference (SDIFF)" do
|
378
|
+
@r.sadd "set", 'a'
|
379
|
+
@r.sadd "set", 'b'
|
380
|
+
@r.sadd "set2", 'b'
|
381
|
+
@r.sadd "set2", 'c'
|
382
|
+
@r.sdiff('set', 'set2').should == ['a']
|
383
|
+
end
|
384
|
+
#
|
385
|
+
it "should be able to do set difference and store the results in a key (SDIFFSTORE)" do
|
386
|
+
@r.sadd "set", 'a'
|
387
|
+
@r.sadd "set", 'b'
|
388
|
+
@r.sadd "set2", 'b'
|
389
|
+
@r.sadd "set2", 'c'
|
390
|
+
@r.sdiffstore('newone', 'set', 'set2')
|
391
|
+
@r.smembers('newone').should == ['a']
|
392
|
+
end
|
393
|
+
#
|
394
|
+
it "should be able move elements from one set to another (SMOVE)" do
|
395
|
+
@r.sadd 'set1', 'a'
|
396
|
+
@r.sadd 'set1', 'b'
|
397
|
+
@r.sadd 'set2', 'x'
|
398
|
+
@r.smove('set1', 'set2', 'a').should be_true
|
399
|
+
@r.sismember('set2', 'a').should be_true
|
400
|
+
@r.delete('set1')
|
401
|
+
end
|
402
|
+
#
|
403
|
+
it "should be able to do crazy SORT queries" do
|
404
|
+
# The 'Dogs' is capitialized on purpose
|
405
|
+
@r['dog_1'] = 'louie'
|
406
|
+
@r.rpush 'Dogs', 1
|
407
|
+
@r['dog_2'] = 'lucy'
|
408
|
+
@r.rpush 'Dogs', 2
|
409
|
+
@r['dog_3'] = 'max'
|
410
|
+
@r.rpush 'Dogs', 3
|
411
|
+
@r['dog_4'] = 'taj'
|
412
|
+
@r.rpush 'Dogs', 4
|
413
|
+
@r.sort('Dogs', :get => 'dog_*', :limit => [0,1]).should == ['louie']
|
414
|
+
@r.sort('Dogs', :get => 'dog_*', :limit => [0,1], :order => 'desc alpha').should == ['taj']
|
415
|
+
end
|
416
|
+
|
417
|
+
it "should be able to handle array of :get using SORT" do
|
418
|
+
@r['dog:1:name'] = 'louie'
|
419
|
+
@r['dog:1:breed'] = 'mutt'
|
420
|
+
@r.rpush 'dogs', 1
|
421
|
+
@r['dog:2:name'] = 'lucy'
|
422
|
+
@r['dog:2:breed'] = 'poodle'
|
423
|
+
@r.rpush 'dogs', 2
|
424
|
+
@r['dog:3:name'] = 'max'
|
425
|
+
@r['dog:3:breed'] = 'hound'
|
426
|
+
@r.rpush 'dogs', 3
|
427
|
+
@r['dog:4:name'] = 'taj'
|
428
|
+
@r['dog:4:breed'] = 'terrier'
|
429
|
+
@r.rpush 'dogs', 4
|
430
|
+
@r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1]).should == ['louie', 'mutt']
|
431
|
+
@r.sort('dogs', :get => ['dog:*:name', 'dog:*:breed'], :limit => [0,1], :order => 'desc alpha').should == ['taj', 'terrier']
|
432
|
+
end
|
433
|
+
#
|
434
|
+
it "should be able count the members of a zset" do
|
435
|
+
@r.set_add "set", 'key1'
|
436
|
+
@r.set_add "set", 'key2'
|
437
|
+
@r.zset_add 'zset', 1, 'set'
|
438
|
+
@r.zset_count('zset').should == 1
|
439
|
+
@r.delete('set')
|
440
|
+
@r.delete('zset')
|
441
|
+
end
|
442
|
+
#
|
443
|
+
it "should be able add members to a zset" do
|
444
|
+
@r.set_add "set", 'key1'
|
445
|
+
@r.set_add "set", 'key2'
|
446
|
+
@r.zset_add 'zset', 1, 'set'
|
447
|
+
@r.zset_range('zset', 0, 1).should == ['set']
|
448
|
+
@r.zset_count('zset').should == 1
|
449
|
+
@r.delete('set')
|
450
|
+
@r.delete('zset')
|
451
|
+
end
|
452
|
+
#
|
453
|
+
it "should be able delete members to a zset" do
|
454
|
+
@r.set_add "set", 'key1'
|
455
|
+
@r.set_add "set", 'key2'
|
456
|
+
@r.type?('set').should == "set"
|
457
|
+
@r.set_add "set2", 'key3'
|
458
|
+
@r.set_add "set2", 'key4'
|
459
|
+
@r.type?('set2').should == "set"
|
460
|
+
@r.zset_add 'zset', 1, 'set'
|
461
|
+
@r.zset_count('zset').should == 1
|
462
|
+
@r.zset_add 'zset', 2, 'set2'
|
463
|
+
@r.zset_count('zset').should == 2
|
464
|
+
@r.zset_delete 'zset', 'set'
|
465
|
+
@r.zset_count('zset').should == 1
|
466
|
+
@r.delete('set')
|
467
|
+
@r.delete('set2')
|
468
|
+
@r.delete('zset')
|
469
|
+
end
|
470
|
+
#
|
471
|
+
it "should be able to get a range of values from a zset" do
|
472
|
+
@r.set_add "set", 'key1'
|
473
|
+
@r.set_add "set", 'key2'
|
474
|
+
@r.set_add "set2", 'key3'
|
475
|
+
@r.set_add "set2", 'key4'
|
476
|
+
@r.set_add "set3", 'key1'
|
477
|
+
@r.type?('set').should == 'set'
|
478
|
+
@r.type?('set2').should == 'set'
|
479
|
+
@r.type?('set3').should == 'set'
|
480
|
+
@r.zset_add 'zset', 1, 'set'
|
481
|
+
@r.zset_add 'zset', 2, 'set2'
|
482
|
+
@r.zset_add 'zset', 3, 'set3'
|
483
|
+
@r.zset_count('zset').should == 3
|
484
|
+
@r.zset_range('zset', 0, 3).should == ['set', 'set2', 'set3']
|
485
|
+
@r.delete('set')
|
486
|
+
@r.delete('set2')
|
487
|
+
@r.delete('set3')
|
488
|
+
@r.delete('zset')
|
489
|
+
end
|
490
|
+
#
|
491
|
+
it "should be able to get a reverse range of values from a zset" do
|
492
|
+
@r.set_add "set", 'key1'
|
493
|
+
@r.set_add "set", 'key2'
|
494
|
+
@r.set_add "set2", 'key3'
|
495
|
+
@r.set_add "set2", 'key4'
|
496
|
+
@r.set_add "set3", 'key1'
|
497
|
+
@r.type?('set').should == 'set'
|
498
|
+
@r.type?('set2').should == 'set'
|
499
|
+
@r.type?('set3').should == 'set'
|
500
|
+
@r.zset_add 'zset', 1, 'set'
|
501
|
+
@r.zset_add 'zset', 2, 'set2'
|
502
|
+
@r.zset_add 'zset', 3, 'set3'
|
503
|
+
@r.zset_count('zset').should == 3
|
504
|
+
@r.zset_reverse_range('zset', 0, 3).should == ['set3', 'set2', 'set']
|
505
|
+
@r.delete('set')
|
506
|
+
@r.delete('set2')
|
507
|
+
@r.delete('set3')
|
508
|
+
@r.delete('zset')
|
509
|
+
end
|
510
|
+
#
|
511
|
+
it "should be able to get a range by score of values from a zset" do
|
512
|
+
@r.set_add "set", 'key1'
|
513
|
+
@r.set_add "set", 'key2'
|
514
|
+
@r.set_add "set2", 'key3'
|
515
|
+
@r.set_add "set2", 'key4'
|
516
|
+
@r.set_add "set3", 'key1'
|
517
|
+
@r.set_add "set4", 'key4'
|
518
|
+
@r.zset_add 'zset', 1, 'set'
|
519
|
+
@r.zset_add 'zset', 2, 'set2'
|
520
|
+
@r.zset_add 'zset', 3, 'set3'
|
521
|
+
@r.zset_add 'zset', 4, 'set4'
|
522
|
+
@r.zset_count('zset').should == 4
|
523
|
+
@r.zset_range_by_score('zset', 2, 3).should == ['set2', 'set3']
|
524
|
+
@r.delete('set')
|
525
|
+
@r.delete('set2')
|
526
|
+
@r.delete('set3')
|
527
|
+
@r.delete('set4')
|
528
|
+
@r.delete('zset')
|
529
|
+
end
|
530
|
+
|
531
|
+
it "should provide info (INFO)" do
|
532
|
+
[: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|
|
533
|
+
@r.info.keys.should include(x)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
#
|
537
|
+
it "should be able to flush the database (FLUSHDB)" do
|
538
|
+
@r['key1'] = 'keyone'
|
539
|
+
@r['key2'] = 'keytwo'
|
540
|
+
@r.keys('*').sort.should == ['foo', 'key1', 'key2'].sort #foo from before
|
541
|
+
@r.flushdb
|
542
|
+
@r.keys('*').should == []
|
543
|
+
end
|
544
|
+
#
|
545
|
+
it "should raise exception when manually try to change the database" do
|
546
|
+
lambda { @r.select(0) }.should raise_error
|
547
|
+
end
|
548
|
+
#
|
549
|
+
it "should be able to provide the last save time (LASTSAVE)" do
|
550
|
+
savetime = @r.lastsave
|
551
|
+
Time.at(savetime).class.should == Time
|
552
|
+
Time.at(savetime).should <= Time.now
|
553
|
+
end
|
554
|
+
|
555
|
+
it "should be able to MGET keys" do
|
556
|
+
@r['foo'] = 1000
|
557
|
+
@r['bar'] = 2000
|
558
|
+
@r.mget('foo', 'bar').should == ['1000', '2000']
|
559
|
+
@r.mget('foo', 'bar', 'baz').should == ['1000', '2000', nil]
|
560
|
+
end
|
561
|
+
|
562
|
+
it "should be able to mapped MGET keys" do
|
563
|
+
@r['foo'] = 1000
|
564
|
+
@r['bar'] = 2000
|
565
|
+
@r.mapped_mget('foo', 'bar').should == { 'foo' => '1000', 'bar' => '2000'}
|
566
|
+
@r.mapped_mget('foo', 'baz', 'bar').should == { 'foo' => '1000', 'bar' => '2000'}
|
567
|
+
end
|
568
|
+
|
569
|
+
it "should be able to MSET values" do
|
570
|
+
@r.mset :key1 => "value1", :key2 => "value2"
|
571
|
+
@r['key1'].should == "value1"
|
572
|
+
@r['key2'].should == "value2"
|
573
|
+
end
|
574
|
+
|
575
|
+
it "should be able to MSETNX values" do
|
576
|
+
@r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2"
|
577
|
+
@r.mget('keynx1', 'keynx2').should == ["valuenx1", "valuenx2"]
|
578
|
+
|
579
|
+
@r["keynx1"] = "value1"
|
580
|
+
@r["keynx2"] = "value2"
|
581
|
+
@r.msetnx :keynx1 => "valuenx1", :keynx2 => "valuenx2"
|
582
|
+
@r.mget('keynx1', 'keynx2').should == ["value1", "value2"]
|
583
|
+
end
|
584
|
+
|
585
|
+
it "should bgsave" do
|
586
|
+
@r.bgsave.should == 'OK'
|
587
|
+
end
|
588
|
+
|
589
|
+
it "should be able to ECHO" do
|
590
|
+
@r.echo("message in a bottle\n").should == "message in a bottle\n"
|
591
|
+
end
|
592
|
+
|
593
|
+
it "should raise error when invoke MONITOR" do
|
594
|
+
lambda { @r.monitor }.should raise_error
|
595
|
+
end
|
596
|
+
|
597
|
+
it "should raise error when invoke SYNC" do
|
598
|
+
lambda { @r.sync }.should raise_error
|
599
|
+
end
|
600
|
+
|
601
|
+
it "should handle multiple servers" do
|
602
|
+
require 'dist_redis'
|
603
|
+
@r = DistRedis.new(:hosts=> ['localhost:6379', '127.0.0.1:6379'], :db => 15)
|
604
|
+
|
605
|
+
100.times do |idx|
|
606
|
+
@r[idx] = "foo#{idx}"
|
607
|
+
end
|
608
|
+
|
609
|
+
100.times do |idx|
|
610
|
+
@r[idx].should == "foo#{idx}"
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
it "should be able to pipeline writes" do
|
615
|
+
@r.pipelined do |pipeline|
|
616
|
+
pipeline.lpush 'list', "hello"
|
617
|
+
pipeline.lpush 'list', 42
|
618
|
+
end
|
619
|
+
|
620
|
+
@r.type('list').should == "list"
|
621
|
+
@r.llen('list').should == 2
|
622
|
+
@r.lpop('list').should == '42'
|
623
|
+
end
|
624
|
+
|
625
|
+
it "should do nothing when pipelining no commands" do
|
626
|
+
@r.pipelined do |pipeline|
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
it "should AUTH when connecting with a password" do
|
631
|
+
r = Redis.new(:password => 'secret')
|
632
|
+
r.stub!(:connect_to)
|
633
|
+
r.should_receive(:call_command).with(['auth', 'secret'])
|
634
|
+
r.connect_to_server
|
635
|
+
end
|
636
|
+
|
637
|
+
end
|