mock_redis 0.19.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +32 -5
  3. data/.rubocop_todo.yml +1 -1
  4. data/.travis.yml +9 -10
  5. data/CHANGELOG.md +46 -0
  6. data/Gemfile +2 -2
  7. data/LICENSE.md +21 -0
  8. data/README.md +39 -15
  9. data/lib/mock_redis.rb +0 -5
  10. data/lib/mock_redis/database.rb +59 -22
  11. data/lib/mock_redis/future.rb +1 -1
  12. data/lib/mock_redis/geospatial_methods.rb +14 -22
  13. data/lib/mock_redis/hash_methods.rb +23 -15
  14. data/lib/mock_redis/indifferent_hash.rb +0 -8
  15. data/lib/mock_redis/info_method.rb +2 -2
  16. data/lib/mock_redis/list_methods.rb +2 -2
  17. data/lib/mock_redis/multi_db_wrapper.rb +2 -2
  18. data/lib/mock_redis/pipelined_wrapper.rb +25 -6
  19. data/lib/mock_redis/set_methods.rb +16 -4
  20. data/lib/mock_redis/stream.rb +62 -0
  21. data/lib/mock_redis/stream/id.rb +60 -0
  22. data/lib/mock_redis/stream_methods.rb +87 -0
  23. data/lib/mock_redis/string_methods.rb +31 -20
  24. data/lib/mock_redis/transaction_wrapper.rb +27 -14
  25. data/lib/mock_redis/utility_methods.rb +6 -3
  26. data/lib/mock_redis/version.rb +1 -1
  27. data/lib/mock_redis/zset_methods.rb +54 -11
  28. data/mock_redis.gemspec +6 -6
  29. data/spec/client_spec.rb +12 -0
  30. data/spec/commands/blpop_spec.rb +0 -6
  31. data/spec/commands/brpop_spec.rb +6 -5
  32. data/spec/commands/dump_spec.rb +19 -0
  33. data/spec/commands/exists_spec.rb +34 -5
  34. data/spec/commands/future_spec.rb +11 -1
  35. data/spec/commands/geoadd_spec.rb +1 -1
  36. data/spec/commands/geodist_spec.rb +8 -4
  37. data/spec/commands/geohash_spec.rb +4 -4
  38. data/spec/commands/geopos_spec.rb +4 -4
  39. data/spec/commands/get_spec.rb +1 -0
  40. data/spec/commands/hdel_spec.rb +18 -2
  41. data/spec/commands/hmset_spec.rb +26 -0
  42. data/spec/commands/hset_spec.rb +6 -6
  43. data/spec/commands/keys_spec.rb +17 -0
  44. data/spec/commands/mget_spec.rb +34 -15
  45. data/spec/commands/move_spec.rb +5 -5
  46. data/spec/commands/mset_spec.rb +14 -0
  47. data/spec/commands/pipelined_spec.rb +72 -0
  48. data/spec/commands/restore_spec.rb +47 -0
  49. data/spec/commands/scan_spec.rb +9 -0
  50. data/spec/commands/set_spec.rb +12 -2
  51. data/spec/commands/setbit_spec.rb +1 -0
  52. data/spec/commands/setex_spec.rb +16 -0
  53. data/spec/commands/spop_spec.rb +15 -0
  54. data/spec/commands/srandmember_spec.rb +1 -1
  55. data/spec/commands/srem_spec.rb +5 -0
  56. data/spec/commands/watch_spec.rb +8 -3
  57. data/spec/commands/xadd_spec.rb +104 -0
  58. data/spec/commands/xlen_spec.rb +20 -0
  59. data/spec/commands/xrange_spec.rb +141 -0
  60. data/spec/commands/xrevrange_spec.rb +130 -0
  61. data/spec/commands/xtrim_spec.rb +30 -0
  62. data/spec/commands/zinterstore_spec.rb +34 -0
  63. data/spec/commands/zpopmax_spec.rb +60 -0
  64. data/spec/commands/zpopmin_spec.rb +60 -0
  65. data/spec/commands/zrange_spec.rb +1 -1
  66. data/spec/commands/zrangebyscore_spec.rb +1 -1
  67. data/spec/commands/zrevrange_spec.rb +1 -1
  68. data/spec/commands/zrevrangebyscore_spec.rb +1 -1
  69. data/spec/commands/zunionstore_spec.rb +33 -0
  70. data/spec/mock_redis_spec.rb +4 -6
  71. data/spec/spec_helper.rb +6 -2
  72. data/spec/support/redis_multiplexer.rb +18 -1
  73. data/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +14 -0
  74. data/spec/support/shared_examples/only_operates_on_hashes.rb +2 -0
  75. data/spec/support/shared_examples/only_operates_on_lists.rb +2 -0
  76. data/spec/support/shared_examples/only_operates_on_sets.rb +2 -0
  77. data/spec/support/shared_examples/only_operates_on_zsets.rb +2 -0
  78. data/spec/transactions_spec.rb +30 -26
  79. metadata +45 -31
  80. data/LICENSE +0 -19
  81. data/spec/commands/hash_operator_spec.rb +0 -21
@@ -0,0 +1,87 @@
1
+ require 'mock_redis/assertions'
2
+ require 'mock_redis/utility_methods'
3
+ require 'mock_redis/stream'
4
+
5
+ # TODO: Implement the following commands
6
+ #
7
+ # * xread
8
+ # * xgroup
9
+ # * xreadgroup
10
+ # * xack
11
+ # * xpending
12
+ # * xclaim
13
+ # * xinfo
14
+ # * xtrim
15
+ # * xdel
16
+ #
17
+ # TODO: Complete support for
18
+ #
19
+ # * xtrim
20
+ # - `approximate: true` argument is currently ignored
21
+ # * xadd
22
+ # - `approximate: true` argument (for capped streams) is currently ignored
23
+ #
24
+ # For details of these commands see
25
+ # * https://redis.io/topics/streams-intro
26
+ # * https://redis.io/commands#stream
27
+
28
+ class MockRedis
29
+ module StreamMethods
30
+ include Assertions
31
+ include UtilityMethods
32
+
33
+ def xadd(key, entry, opts = {})
34
+ id = opts[:id] || '*'
35
+ with_stream_at(key) do |stream|
36
+ stream.add id, entry
37
+ stream.trim opts[:maxlen] if opts[:maxlen]
38
+ return stream.last_id
39
+ end
40
+ end
41
+
42
+ def xtrim(key, count)
43
+ with_stream_at(key) do |stream|
44
+ stream.trim count
45
+ end
46
+ end
47
+
48
+ def xlen(key)
49
+ with_stream_at(key) do |stream|
50
+ return stream.count
51
+ end
52
+ end
53
+
54
+ def xrange(key, first = '-', last = '+', count: nil)
55
+ args = [first, last, false]
56
+ args += ['COUNT', count] if count
57
+ with_stream_at(key) do |stream|
58
+ return stream.range(*args)
59
+ end
60
+ end
61
+
62
+ def xrevrange(key, last = '+', first = '-', count: nil)
63
+ args = [first, last, true]
64
+ args += ['COUNT', count] if count
65
+ with_stream_at(key) do |stream|
66
+ return stream.range(*args)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def with_stream_at(key, &blk)
73
+ with_thing_at(key, :assert_streamy, proc { Stream.new }, &blk)
74
+ end
75
+
76
+ def streamy?(key)
77
+ data[key].nil? || data[key].is_a?(Stream)
78
+ end
79
+
80
+ def assert_streamy(key)
81
+ unless streamy?(key)
82
+ raise Redis::CommandError,
83
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
84
+ end
85
+ end
86
+ end
87
+ end
@@ -85,14 +85,11 @@ class MockRedis
85
85
  end
86
86
 
87
87
  def get(key)
88
+ key = key.to_s
88
89
  assert_stringy(key)
89
90
  data[key]
90
91
  end
91
92
 
92
- def [](key)
93
- get(key)
94
- end
95
-
96
93
  def getbit(key, offset)
97
94
  assert_stringy(key)
98
95
 
@@ -159,6 +156,7 @@ class MockRedis
159
156
  def mget(*keys)
160
157
  assert_has_args(keys, 'mget')
161
158
 
159
+ keys.flatten!
162
160
  keys.map do |key|
163
161
  get(key) if stringy?(key)
164
162
  end
@@ -170,6 +168,8 @@ class MockRedis
170
168
 
171
169
  def mset(*kvpairs)
172
170
  assert_has_args(kvpairs, 'mset')
171
+ kvpairs = kvpairs.first if kvpairs.size == 1 && kvpairs.first.is_a?(Enumerable)
172
+
173
173
  if kvpairs.length.odd?
174
174
  raise Redis::CommandError, 'ERR wrong number of arguments for MSET'
175
175
  end
@@ -188,7 +188,7 @@ class MockRedis
188
188
  def msetnx(*kvpairs)
189
189
  assert_has_args(kvpairs, 'msetnx')
190
190
 
191
- if kvpairs.each_slice(2).any? { |(k, _)| exists(k) }
191
+ if kvpairs.each_slice(2).any? { |(k, _)| exists?(k) }
192
192
  false
193
193
  else
194
194
  mset(*kvpairs)
@@ -201,17 +201,18 @@ class MockRedis
201
201
  end
202
202
 
203
203
  def set(key, value, options = {})
204
+ key = key.to_s
204
205
  return_true = false
205
206
  options = options.dup
206
207
  if options.delete(:nx)
207
- if exists(key)
208
+ if exists?(key)
208
209
  return false
209
210
  else
210
211
  return_true = true
211
212
  end
212
213
  end
213
214
  if options.delete(:xx)
214
- if exists(key)
215
+ if exists?(key)
215
216
  return_true = true
216
217
  else
217
218
  return false
@@ -219,20 +220,26 @@ class MockRedis
219
220
  end
220
221
  data[key] = value.to_s
221
222
 
222
- # take latter
223
- expire_option = options.to_a.last
224
- if expire_option
225
- type, duration = expire_option
223
+ duration = options.delete(:ex)
224
+ if duration
226
225
  if duration == 0
227
226
  raise Redis::CommandError, 'ERR invalid expire time in set'
228
227
  end
229
- expire(key, type.to_sym == :ex ? duration : duration / 1000.0)
228
+ expire(key, duration)
230
229
  end
231
- return_true ? true : 'OK'
232
- end
233
230
 
234
- def []=(key, value, options = {})
235
- set(key, value, options)
231
+ duration = options.delete(:px)
232
+ if duration
233
+ if duration == 0
234
+ raise Redis::CommandError, 'ERR invalid expire time in set'
235
+ end
236
+ pexpire(key, duration)
237
+ end
238
+ unless options.empty?
239
+ raise ArgumentError, "unknown keyword: #{options.keys[0]}"
240
+ end
241
+
242
+ return_true ? true : 'OK'
236
243
  end
237
244
 
238
245
  def setbit(key, offset, value)
@@ -309,13 +316,17 @@ class MockRedis
309
316
  end
310
317
 
311
318
  def setex(key, seconds, value)
312
- set(key, value)
313
- expire(key, seconds)
314
- 'OK'
319
+ if seconds <= 0
320
+ raise Redis::CommandError, 'ERR invalid expire time in setex'
321
+ else
322
+ set(key, value)
323
+ expire(key, seconds)
324
+ 'OK'
325
+ end
315
326
  end
316
327
 
317
328
  def setnx(key, value)
318
- if exists(key)
329
+ if exists?(key)
319
330
  false
320
331
  else
321
332
  set(key, value)
@@ -11,13 +11,13 @@ class MockRedis
11
11
  def initialize(db)
12
12
  @db = db
13
13
  @transaction_futures = []
14
- @in_multi = false
14
+ @multi_stack = []
15
15
  @multi_block_given = false
16
16
  end
17
17
 
18
18
  def method_missing(method, *args, &block)
19
- if @in_multi
20
- future = MockRedis::Future.new([method, *args])
19
+ if in_multi?
20
+ future = MockRedis::Future.new([method, *args], block)
21
21
  @transaction_futures << future
22
22
 
23
23
  if @multi_block_given
@@ -35,30 +35,32 @@ class MockRedis
35
35
  super
36
36
  @db = @db.clone
37
37
  @transaction_futures = @transaction_futures.clone
38
+ @multi_stack = @multi_stack.clone
38
39
  end
39
40
 
40
41
  def discard
41
- unless @in_multi
42
+ unless in_multi?
42
43
  raise Redis::CommandError, 'ERR DISCARD without MULTI'
43
44
  end
44
- @in_multi = false
45
- @multi_block_given = false
45
+ pop_multi
46
+
46
47
  @transaction_futures = []
47
48
  'OK'
48
49
  end
49
50
 
50
51
  def exec
51
- unless @in_multi
52
+ unless in_multi?
52
53
  raise Redis::CommandError, 'ERR EXEC without MULTI'
53
54
  end
54
- @in_multi = false
55
+ pop_multi
56
+ return if in_multi?
55
57
  @multi_block_given = false
56
58
 
57
59
  responses = @transaction_futures.map do |future|
58
60
  begin
59
61
  result = send(*future.command)
60
62
  future.store_result(result)
61
- result
63
+ future.value
62
64
  rescue StandardError => e
63
65
  e
64
66
  end
@@ -68,12 +70,21 @@ class MockRedis
68
70
  responses
69
71
  end
70
72
 
73
+ def in_multi?
74
+ @multi_stack.any?
75
+ end
76
+
77
+ def push_multi
78
+ @multi_stack.push(@multi_stack.size + 1)
79
+ end
80
+
81
+ def pop_multi
82
+ @multi_stack.pop
83
+ end
84
+
71
85
  def multi
72
- if @in_multi
73
- raise Redis::CommandError, 'ERR MULTI calls can not be nested'
74
- end
75
- @in_multi = true
76
86
  if block_given?
87
+ push_multi
77
88
  @multi_block_given = true
78
89
  begin
79
90
  yield(self)
@@ -83,6 +94,8 @@ class MockRedis
83
94
  raise e
84
95
  end
85
96
  else
97
+ raise Redis::CommandError, 'ERR MULTI calls can not be nested' if in_multi?
98
+ push_multi
86
99
  'OK'
87
100
  end
88
101
  end
@@ -95,7 +108,7 @@ class MockRedis
95
108
  'OK'
96
109
  end
97
110
 
98
- def watch(_)
111
+ def watch(*_)
99
112
  if block_given?
100
113
  yield self
101
114
  else
@@ -18,7 +18,7 @@ class MockRedis
18
18
  end
19
19
 
20
20
  def clean_up_empties_at(key)
21
- if data[key] && data[key].empty?
21
+ if data[key]&.empty? && data[key] != ''
22
22
  del(key)
23
23
  end
24
24
  end
@@ -28,12 +28,15 @@ class MockRedis
28
28
  cursor = cursor.to_i
29
29
  match = opts[:match] || '*'
30
30
  key = opts[:key] || lambda { |x| x }
31
+ filtered_values = []
31
32
 
32
33
  limit = cursor + count
33
34
  next_cursor = limit >= values.length ? '0' : limit.to_s
34
35
 
35
- filtered_values = values[cursor...limit].select do |val|
36
- redis_pattern_to_ruby_regex(match).match(key.call(val))
36
+ unless values[cursor...limit].nil?
37
+ filtered_values = values[cursor...limit].select do |val|
38
+ redis_pattern_to_ruby_regex(match).match(key.call(val))
39
+ end
37
40
  end
38
41
 
39
42
  [next_cursor, filtered_values]
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  class MockRedis
5
- VERSION = '0.19.0'.freeze
5
+ VERSION = '0.24.0'
6
6
  end
@@ -11,7 +11,7 @@ class MockRedis
11
11
  zadd_options = {}
12
12
  zadd_options = args.pop if args.last.is_a?(Hash)
13
13
 
14
- if zadd_options && zadd_options.include?(:nx) && zadd_options.include?(:xx)
14
+ if zadd_options&.include?(:nx) && zadd_options&.include?(:xx)
15
15
  raise Redis::CommandError, 'ERR XX and NX options at the same time are not compatible'
16
16
  end
17
17
 
@@ -26,7 +26,7 @@ class MockRedis
26
26
  end
27
27
 
28
28
  def zadd_one_member(key, score, member, zadd_options = {})
29
- assert_scorey(score) unless score =~ /(\+|\-)inf/
29
+ assert_scorey(score) unless score.to_s =~ /(\+|\-)inf/
30
30
 
31
31
  with_zset_at(key) do |zset|
32
32
  if zadd_options[:incr]
@@ -155,6 +155,24 @@ class MockRedis
155
155
  retval
156
156
  end
157
157
 
158
+ def zpopmin(key, count = 1)
159
+ with_zset_at(key) do |z|
160
+ pairs = z.sorted.first(count)
161
+ pairs.each { |pair| z.delete?(pair.last) }
162
+ retval = to_response(pairs, with_scores: true)
163
+ count == 1 ? retval.first : retval
164
+ end
165
+ end
166
+
167
+ def zpopmax(key, count = 1)
168
+ with_zset_at(key) do |z|
169
+ pairs = z.sorted.reverse.first(count)
170
+ pairs.each { |pair| z.delete?(pair.last) }
171
+ retval = to_response(pairs, with_scores: true)
172
+ count == 1 ? retval.first : retval
173
+ end
174
+ end
175
+
158
176
  def zrevrange(key, start, stop, options = {})
159
177
  with_zset_at(key) do |z|
160
178
  to_response(z.sorted.reverse[start..stop] || [], options)
@@ -211,7 +229,7 @@ class MockRedis
211
229
  def zscore(key, member)
212
230
  with_zset_at(key) do |z|
213
231
  score = z.score(member.to_s)
214
- score.to_f if score
232
+ score&.to_f
215
233
  end
216
234
  end
217
235
 
@@ -264,7 +282,7 @@ class MockRedis
264
282
  raise Redis::CommandError, 'ERR syntax error'
265
283
  end
266
284
 
267
- with_zsets_at(*keys) do |*zsets|
285
+ with_zsets_at(*keys, coercible: true) do |*zsets|
268
286
  zsets.zip(weights).map do |(zset, weight)|
269
287
  zset.reduce(Zset.new) do |acc, (score, member)|
270
288
  acc.add(score * weight, member)
@@ -275,16 +293,30 @@ class MockRedis
275
293
  end
276
294
  end
277
295
 
278
- def with_zset_at(key, &blk)
279
- with_thing_at(key, :assert_zsety, proc { Zset.new }, &blk)
296
+ def coerce_to_zset(set)
297
+ zset = Zset.new
298
+ set.each do |member|
299
+ zset.add(1.0, member)
300
+ end
301
+ zset
280
302
  end
281
303
 
282
- def with_zsets_at(*keys, &blk)
304
+ def with_zset_at(key, coercible: false, &blk)
305
+ if coercible
306
+ with_thing_at(key, :assert_coercible_zsety, proc { Zset.new }) do |value|
307
+ blk.call value.is_a?(Set) ? coerce_to_zset(value) : value
308
+ end
309
+ else
310
+ with_thing_at(key, :assert_zsety, proc { Zset.new }, &blk)
311
+ end
312
+ end
313
+
314
+ def with_zsets_at(*keys, coercible: false, &blk)
283
315
  if keys.length == 1
284
- with_zset_at(keys.first, &blk)
316
+ with_zset_at(keys.first, coercible: coercible, &blk)
285
317
  else
286
- with_zset_at(keys.first) do |set|
287
- with_zsets_at(*(keys[1..-1])) do |*sets|
318
+ with_zset_at(keys.first, coercible: coercible) do |set|
319
+ with_zsets_at(*(keys[1..-1]), coercible: coercible) do |*sets|
288
320
  yield(*([set] + sets))
289
321
  end
290
322
  end
@@ -295,6 +327,10 @@ class MockRedis
295
327
  data[key].nil? || data[key].is_a?(Zset)
296
328
  end
297
329
 
330
+ def coercible_zsety?(key)
331
+ zsety?(key) || data[key].is_a?(Set)
332
+ end
333
+
298
334
  def assert_zsety(key)
299
335
  unless zsety?(key)
300
336
  raise Redis::CommandError,
@@ -302,13 +338,20 @@ class MockRedis
302
338
  end
303
339
  end
304
340
 
341
+ def assert_coercible_zsety(key)
342
+ unless coercible_zsety?(key)
343
+ raise Redis::CommandError,
344
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
345
+ end
346
+ end
347
+
305
348
  def looks_like_float?(x)
306
349
  # ugh, exceptions for flow control.
307
350
  !!Float(x) rescue false
308
351
  end
309
352
 
310
353
  def assert_scorey(value, message = 'ERR value is not a valid float')
311
- return if value =~ /\(?(\-|\+)inf/
354
+ return if value.to_s =~ /\(?(\-|\+)inf/
312
355
 
313
356
  value = $1 if value.to_s =~ /\((.*)/
314
357
  unless looks_like_float?(value)