mock_redis 0.19.0 → 0.24.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 (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)