mock_redis 0.20.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +26 -5
  3. data/.rubocop_todo.yml +1 -1
  4. data/.travis.yml +3 -3
  5. data/CHANGELOG.md +37 -0
  6. data/Gemfile +2 -2
  7. data/LICENSE.md +21 -0
  8. data/README.md +37 -13
  9. data/lib/mock_redis.rb +0 -5
  10. data/lib/mock_redis/database.rb +53 -17
  11. data/lib/mock_redis/future.rb +1 -1
  12. data/lib/mock_redis/geospatial_methods.rb +4 -4
  13. data/lib/mock_redis/hash_methods.rb +22 -14
  14. data/lib/mock_redis/info_method.rb +2 -2
  15. data/lib/mock_redis/multi_db_wrapper.rb +2 -2
  16. data/lib/mock_redis/set_methods.rb +1 -0
  17. data/lib/mock_redis/stream.rb +22 -2
  18. data/lib/mock_redis/stream/id.rb +1 -1
  19. data/lib/mock_redis/stream_methods.rb +14 -1
  20. data/lib/mock_redis/string_methods.rb +28 -13
  21. data/lib/mock_redis/transaction_wrapper.rb +2 -2
  22. data/lib/mock_redis/utility_methods.rb +6 -3
  23. data/lib/mock_redis/version.rb +1 -1
  24. data/lib/mock_redis/zset_methods.rb +52 -9
  25. data/mock_redis.gemspec +1 -2
  26. data/spec/client_spec.rb +12 -0
  27. data/spec/commands/blpop_spec.rb +0 -6
  28. data/spec/commands/brpop_spec.rb +6 -5
  29. data/spec/commands/del_spec.rb +15 -0
  30. data/spec/commands/dump_spec.rb +19 -0
  31. data/spec/commands/exists_spec.rb +34 -5
  32. data/spec/commands/future_spec.rb +11 -1
  33. data/spec/commands/geoadd_spec.rb +1 -1
  34. data/spec/commands/hdel_spec.rb +16 -0
  35. data/spec/commands/hmset_spec.rb +26 -0
  36. data/spec/commands/hset_spec.rb +6 -6
  37. data/spec/commands/keys_spec.rb +17 -0
  38. data/spec/commands/mget_spec.rb +6 -0
  39. data/spec/commands/move_spec.rb +5 -5
  40. data/spec/commands/pipelined_spec.rb +20 -0
  41. data/spec/commands/restore_spec.rb +47 -0
  42. data/spec/commands/scan_spec.rb +9 -0
  43. data/spec/commands/set_spec.rb +12 -2
  44. data/spec/commands/setbit_spec.rb +1 -0
  45. data/spec/commands/setex_spec.rb +16 -0
  46. data/spec/commands/srandmember_spec.rb +1 -1
  47. data/spec/commands/srem_spec.rb +5 -0
  48. data/spec/commands/xadd_spec.rb +20 -0
  49. data/spec/commands/xrange_spec.rb +13 -0
  50. data/spec/commands/xread_spec.rb +50 -0
  51. data/spec/commands/xtrim_spec.rb +6 -0
  52. data/spec/commands/zinterstore_spec.rb +34 -0
  53. data/spec/commands/zpopmax_spec.rb +60 -0
  54. data/spec/commands/zpopmin_spec.rb +60 -0
  55. data/spec/commands/zrange_spec.rb +1 -1
  56. data/spec/commands/zrangebyscore_spec.rb +1 -1
  57. data/spec/commands/zrevrange_spec.rb +1 -1
  58. data/spec/commands/zrevrangebyscore_spec.rb +1 -1
  59. data/spec/commands/zunionstore_spec.rb +33 -0
  60. data/spec/mock_redis_spec.rb +4 -6
  61. data/spec/spec_helper.rb +4 -2
  62. data/spec/support/redis_multiplexer.rb +1 -0
  63. data/spec/transactions_spec.rb +16 -0
  64. metadata +16 -26
  65. data/LICENSE +0 -19
@@ -8,14 +8,10 @@ class MockRedis
8
8
 
9
9
  def hdel(key, *fields)
10
10
  with_hash_at(key) do |hash|
11
- if fields.is_a?(Array)
12
- orig_size = hash.size
13
- fields = fields.map(&:to_s)
14
- hash.delete_if { |k, _v| fields.include?(k) }
15
- orig_size - hash.size
16
- else
17
- hash.delete(fields[0].to_s) ? 1 : 0
18
- end
11
+ orig_size = hash.size
12
+ fields = Array(fields).flatten.map(&:to_s)
13
+ hash.delete_if { |k, _v| fields.include?(k) }
14
+ orig_size - hash.size
19
15
  end
20
16
  end
21
17
 
@@ -88,10 +84,17 @@ class MockRedis
88
84
  end
89
85
 
90
86
  def hmset(key, *kvpairs)
87
+ if key.is_a? Array
88
+ err_msg = 'ERR wrong number of arguments for \'hmset\' command'
89
+ kvpairs = key[1..-1]
90
+ key = key[0]
91
+ end
92
+
91
93
  kvpairs.flatten!
92
94
  assert_has_args(kvpairs, 'hmset')
95
+
93
96
  if kvpairs.length.odd?
94
- raise Redis::CommandError, 'ERR wrong number of arguments for HMSET'
97
+ raise Redis::CommandError, err_msg || 'ERR wrong number of arguments for HMSET'
95
98
  end
96
99
 
97
100
  kvpairs.each_slice(2) do |(k, v)|
@@ -125,10 +128,15 @@ class MockRedis
125
128
  end
126
129
  end
127
130
 
128
- def hset(key, field, value)
129
- field_exists = hexists(key, field)
130
- with_hash_at(key) { |h| h[field.to_s] = value.to_s }
131
- !field_exists
131
+ def hset(key, *args)
132
+ added = 0
133
+ with_hash_at(key) do |hash|
134
+ args.each_slice(2) do |field, value|
135
+ added += 1 unless hash.key?(field.to_s)
136
+ hash[field.to_s] = value.to_s
137
+ end
138
+ end
139
+ added
132
140
  end
133
141
 
134
142
  def hsetnx(key, field, value)
@@ -147,7 +155,7 @@ class MockRedis
147
155
  private
148
156
 
149
157
  def with_hash_at(key, &blk)
150
- with_thing_at(key, :assert_hashy, proc { {} }, &blk)
158
+ with_thing_at(key.to_s, :assert_hashy, proc { {} }, &blk)
151
159
  end
152
160
 
153
161
  def hashy?(key)
@@ -83,7 +83,7 @@ class MockRedis
83
83
 
84
84
  # The Ruby Redis client returns commandstats differently when it's called as
85
85
  # "INFO commandstats".
86
- # rubocop:disable Metrics/LineLength
86
+ # rubocop:disable Layout/LineLength
87
87
  COMMAND_STATS_SOLO_INFO = {
88
88
  'auth' => { 'calls' => '572501', 'usec' => '2353163', 'usec_per_call' => '4.11' },
89
89
  'client' => { 'calls' => '1', 'usec' => '80', 'usec_per_call' => '80.00' },
@@ -123,7 +123,7 @@ class MockRedis
123
123
  'cmdstat_smembers' => 'calls=58,usec=231,usec_per_call=3.98',
124
124
  'cmdstat_sunionstore' => 'calls=4185027,usec=11762454022,usec_per_call=2810.60',
125
125
  }.freeze
126
- # rubocop:enable Metrics/LineLength
126
+ # rubocop:enable Layout/LineLength
127
127
 
128
128
  DEFAULT_INFO = [
129
129
  SERVER_INFO,
@@ -24,7 +24,7 @@ class MockRedis
24
24
  def initialize_copy(source)
25
25
  super
26
26
  @databases = @databases.clone
27
- @databases.keys.each do |k|
27
+ @databases.each_key do |k|
28
28
  @databases[k] = @databases[k].clone
29
29
  end
30
30
  end
@@ -39,7 +39,7 @@ class MockRedis
39
39
  src = current_db
40
40
  dest = db(db_index)
41
41
 
42
- if !src.exists(key) || dest.exists(key)
42
+ if !src.exists?(key) || dest.exists?(key)
43
43
  false
44
44
  else
45
45
  case current_db.type(key)
@@ -117,6 +117,7 @@ class MockRedis
117
117
  with_set_at(key) do |s|
118
118
  if members.is_a?(Array)
119
119
  orig_size = s.size
120
+ members = members.map(&:to_s)
120
121
  s.delete_if { |m| members.include?(m) }
121
122
  orig_size - s.size
122
123
  else
@@ -23,14 +23,29 @@ class MockRedis
23
23
 
24
24
  def add(id, values)
25
25
  @last_id = MockRedis::Stream::Id.new(id, min: @last_id)
26
+ if @last_id.to_s == '0-0'
27
+ raise Redis::CommandError,
28
+ 'ERR The ID specified in XADD is equal or smaller than ' \
29
+ 'the target stream top item'
30
+ # TOOD: Redis version 6.0.4, w redis 4.2.1 generates the following error message:
31
+ # 'ERR The ID specified in XADD must be greater than 0-0'
32
+ end
26
33
  members.add [@last_id, Hash[values.map { |k, v| [k.to_s, v.to_s] }]]
27
34
  @last_id.to_s
28
35
  end
29
36
 
30
37
  def trim(count)
31
38
  deleted = @members.size - count
32
- @members = @members.to_a[-count..-1].to_set
33
- deleted
39
+ if deleted > 0
40
+ @members = if count == 0
41
+ Set.new
42
+ else
43
+ @members.to_a[-count..-1].to_set
44
+ end
45
+ deleted
46
+ else
47
+ 0
48
+ end
34
49
  end
35
50
 
36
51
  def range(start, finish, reversed, *opts_in)
@@ -45,6 +60,11 @@ class MockRedis
45
60
  items
46
61
  end
47
62
 
63
+ def read(id)
64
+ stream_id = MockRedis::Stream::Id.new(id)
65
+ members.select { |m| (stream_id < m[0]) }.map { |m| [m[0].to_s, m[1]] }
66
+ end
67
+
48
68
  def each
49
69
  members.each { |m| yield m }
50
70
  end
@@ -31,7 +31,7 @@ class MockRedis
31
31
  @timestamp = id
32
32
  end
33
33
  @sequence = @sequence.nil? ? sequence : @sequence.to_i
34
- if (@timestamp == 0 && @sequence == 0) || self <= min
34
+ if self <= min
35
35
  raise Redis::CommandError,
36
36
  'ERR The ID specified in XADD is equal or smaller than ' \
37
37
  'the target stream top item'
@@ -4,7 +4,6 @@ require 'mock_redis/stream'
4
4
 
5
5
  # TODO: Implement the following commands
6
6
  #
7
- # * xread
8
7
  # * xgroup
9
8
  # * xreadgroup
10
9
  # * xack
@@ -67,6 +66,20 @@ class MockRedis
67
66
  end
68
67
  end
69
68
 
69
+ # TODO: Implement count and block parameters
70
+ def xread(keys, ids)
71
+ result = {}
72
+ keys = keys.is_a?(Array) ? keys : [keys]
73
+ ids = ids.is_a?(Array) ? ids : [ids]
74
+ keys.each_with_index do |key, index|
75
+ with_stream_at(key) do |stream|
76
+ data = stream.read(ids[index])
77
+ result[key] = data unless data.empty?
78
+ end
79
+ end
80
+ result
81
+ end
82
+
70
83
  private
71
84
 
72
85
  def with_stream_at(key, &blk)
@@ -154,9 +154,10 @@ class MockRedis
154
154
  end
155
155
 
156
156
  def mget(*keys)
157
+ keys.flatten!
158
+
157
159
  assert_has_args(keys, 'mget')
158
160
 
159
- keys.flatten!
160
161
  keys.map do |key|
161
162
  get(key) if stringy?(key)
162
163
  end
@@ -188,7 +189,7 @@ class MockRedis
188
189
  def msetnx(*kvpairs)
189
190
  assert_has_args(kvpairs, 'msetnx')
190
191
 
191
- if kvpairs.each_slice(2).any? { |(k, _)| exists(k) }
192
+ if kvpairs.each_slice(2).any? { |(k, _)| exists?(k) }
192
193
  false
193
194
  else
194
195
  mset(*kvpairs)
@@ -205,14 +206,14 @@ class MockRedis
205
206
  return_true = false
206
207
  options = options.dup
207
208
  if options.delete(:nx)
208
- if exists(key)
209
+ if exists?(key)
209
210
  return false
210
211
  else
211
212
  return_true = true
212
213
  end
213
214
  end
214
215
  if options.delete(:xx)
215
- if exists(key)
216
+ if exists?(key)
216
217
  return_true = true
217
218
  else
218
219
  return false
@@ -220,15 +221,25 @@ class MockRedis
220
221
  end
221
222
  data[key] = value.to_s
222
223
 
223
- # take latter
224
- expire_option = options.to_a.last
225
- if expire_option
226
- type, duration = expire_option
224
+ duration = options.delete(:ex)
225
+ if duration
226
+ if duration == 0
227
+ raise Redis::CommandError, 'ERR invalid expire time in set'
228
+ end
229
+ expire(key, duration)
230
+ end
231
+
232
+ duration = options.delete(:px)
233
+ if duration
227
234
  if duration == 0
228
235
  raise Redis::CommandError, 'ERR invalid expire time in set'
229
236
  end
230
- expire(key, type.to_sym == :ex ? duration : duration / 1000.0)
237
+ pexpire(key, duration)
231
238
  end
239
+ unless options.empty?
240
+ raise ArgumentError, "unknown keyword: #{options.keys[0]}"
241
+ end
242
+
232
243
  return_true ? true : 'OK'
233
244
  end
234
245
 
@@ -306,13 +317,17 @@ class MockRedis
306
317
  end
307
318
 
308
319
  def setex(key, seconds, value)
309
- set(key, value)
310
- expire(key, seconds)
311
- 'OK'
320
+ if seconds <= 0
321
+ raise Redis::CommandError, 'ERR invalid expire time in setex'
322
+ else
323
+ set(key, value)
324
+ expire(key, seconds)
325
+ 'OK'
326
+ end
312
327
  end
313
328
 
314
329
  def setnx(key, value)
315
- if exists(key)
330
+ if exists?(key)
316
331
  false
317
332
  else
318
333
  set(key, value)
@@ -17,7 +17,7 @@ class MockRedis
17
17
 
18
18
  def method_missing(method, *args, &block)
19
19
  if in_multi?
20
- future = MockRedis::Future.new([method, *args])
20
+ future = MockRedis::Future.new([method, *args], block)
21
21
  @transaction_futures << future
22
22
 
23
23
  if @multi_block_given
@@ -60,7 +60,7 @@ class MockRedis
60
60
  begin
61
61
  result = send(*future.command)
62
62
  future.store_result(result)
63
- result
63
+ future.value
64
64
  rescue StandardError => e
65
65
  e
66
66
  end
@@ -18,7 +18,7 @@ class MockRedis
18
18
  end
19
19
 
20
20
  def clean_up_empties_at(key)
21
- if data[key]&.empty? && data[key] != ''
21
+ if data[key]&.empty? && data[key] != '' && !data[key].is_a?(Stream)
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.20.0'
5
+ VERSION = '0.25.0'
6
6
  end
@@ -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)
@@ -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)
@@ -23,8 +23,7 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.required_ruby_version = '>= 2.4'
25
25
 
26
- s.add_development_dependency 'rake', '>= 10', '< 12'
27
- s.add_development_dependency 'redis', '~>4.1.0'
26
+ s.add_development_dependency 'redis', '~> 4.2.0'
28
27
  s.add_development_dependency 'rspec', '~> 3.0'
29
28
  s.add_development_dependency 'rspec-its', '~> 1.0'
30
29
  s.add_development_dependency 'timecop', '~> 0.9.1'