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.
Files changed (66) hide show
  1. data/CHANGELOG +153 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.rdoc +83 -0
  4. data/bin/vanity +53 -0
  5. data/lib/vanity.rb +38 -0
  6. data/lib/vanity/backport.rb +43 -0
  7. data/lib/vanity/commands.rb +2 -0
  8. data/lib/vanity/commands/list.rb +21 -0
  9. data/lib/vanity/commands/report.rb +60 -0
  10. data/lib/vanity/experiment/ab_test.rb +477 -0
  11. data/lib/vanity/experiment/base.rb +212 -0
  12. data/lib/vanity/helpers.rb +59 -0
  13. data/lib/vanity/metric/active_record.rb +77 -0
  14. data/lib/vanity/metric/base.rb +221 -0
  15. data/lib/vanity/metric/google_analytics.rb +70 -0
  16. data/lib/vanity/mock_redis.rb +76 -0
  17. data/lib/vanity/playground.rb +197 -0
  18. data/lib/vanity/rails.rb +22 -0
  19. data/lib/vanity/rails/dashboard.rb +24 -0
  20. data/lib/vanity/rails/helpers.rb +158 -0
  21. data/lib/vanity/rails/testing.rb +11 -0
  22. data/lib/vanity/templates/_ab_test.erb +26 -0
  23. data/lib/vanity/templates/_experiment.erb +5 -0
  24. data/lib/vanity/templates/_experiments.erb +7 -0
  25. data/lib/vanity/templates/_metric.erb +14 -0
  26. data/lib/vanity/templates/_metrics.erb +13 -0
  27. data/lib/vanity/templates/_report.erb +27 -0
  28. data/lib/vanity/templates/flot.min.js +1 -0
  29. data/lib/vanity/templates/jquery.min.js +19 -0
  30. data/lib/vanity/templates/vanity.css +26 -0
  31. data/lib/vanity/templates/vanity.js +82 -0
  32. data/test/ab_test_test.rb +656 -0
  33. data/test/experiment_test.rb +136 -0
  34. data/test/experiments/age_and_zipcode.rb +19 -0
  35. data/test/experiments/metrics/cheers.rb +3 -0
  36. data/test/experiments/metrics/signups.rb +2 -0
  37. data/test/experiments/metrics/yawns.rb +3 -0
  38. data/test/experiments/null_abc.rb +5 -0
  39. data/test/metric_test.rb +518 -0
  40. data/test/playground_test.rb +10 -0
  41. data/test/rails_test.rb +104 -0
  42. data/test/test_helper.rb +135 -0
  43. data/vanity.gemspec +18 -0
  44. data/vendor/redis-rb/LICENSE +20 -0
  45. data/vendor/redis-rb/README.markdown +36 -0
  46. data/vendor/redis-rb/Rakefile +62 -0
  47. data/vendor/redis-rb/bench.rb +44 -0
  48. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  49. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  50. data/vendor/redis-rb/bin/distredis +33 -0
  51. data/vendor/redis-rb/examples/basic.rb +16 -0
  52. data/vendor/redis-rb/examples/incr-decr.rb +18 -0
  53. data/vendor/redis-rb/examples/list.rb +26 -0
  54. data/vendor/redis-rb/examples/sets.rb +36 -0
  55. data/vendor/redis-rb/lib/dist_redis.rb +124 -0
  56. data/vendor/redis-rb/lib/hash_ring.rb +128 -0
  57. data/vendor/redis-rb/lib/pipeline.rb +21 -0
  58. data/vendor/redis-rb/lib/redis.rb +370 -0
  59. data/vendor/redis-rb/lib/redis/raketasks.rb +1 -0
  60. data/vendor/redis-rb/profile.rb +22 -0
  61. data/vendor/redis-rb/redis-rb.gemspec +30 -0
  62. data/vendor/redis-rb/spec/redis_spec.rb +637 -0
  63. data/vendor/redis-rb/spec/spec_helper.rb +4 -0
  64. data/vendor/redis-rb/speed.rb +16 -0
  65. data/vendor/redis-rb/tasks/redis.tasks.rb +140 -0
  66. 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