mock_redis 0.19.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +161 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +41 -17
  5. data/lib/mock_redis/connection_method.rb +13 -0
  6. data/lib/mock_redis/database.rb +116 -41
  7. data/lib/mock_redis/expire_wrapper.rb +1 -1
  8. data/lib/mock_redis/future.rb +1 -1
  9. data/lib/mock_redis/geospatial_methods.rb +13 -21
  10. data/lib/mock_redis/hash_methods.rb +34 -15
  11. data/lib/mock_redis/indifferent_hash.rb +0 -8
  12. data/lib/mock_redis/info_method.rb +2 -2
  13. data/lib/mock_redis/list_methods.rb +39 -4
  14. data/lib/mock_redis/memory_method.rb +11 -0
  15. data/lib/mock_redis/multi_db_wrapper.rb +4 -4
  16. data/lib/mock_redis/pipelined_wrapper.rb +32 -15
  17. data/lib/mock_redis/set_methods.rb +34 -6
  18. data/lib/mock_redis/stream/id.rb +58 -0
  19. data/lib/mock_redis/stream.rb +88 -0
  20. data/lib/mock_redis/stream_methods.rb +102 -0
  21. data/lib/mock_redis/string_methods.rb +81 -30
  22. data/lib/mock_redis/transaction_wrapper.rb +32 -21
  23. data/lib/mock_redis/utility_methods.rb +9 -4
  24. data/lib/mock_redis/version.rb +1 -1
  25. data/lib/mock_redis/zset.rb +5 -8
  26. data/lib/mock_redis/zset_methods.rb +64 -12
  27. data/lib/mock_redis.rb +38 -10
  28. metadata +40 -336
  29. data/.gitignore +0 -5
  30. data/.mailmap +0 -2
  31. data/.overcommit.yml +0 -21
  32. data/.rspec +0 -1
  33. data/.rubocop.yml +0 -121
  34. data/.rubocop_todo.yml +0 -35
  35. data/.simplecov +0 -4
  36. data/.travis.yml +0 -33
  37. data/Gemfile +0 -12
  38. data/LICENSE +0 -19
  39. data/Rakefile +0 -2
  40. data/mock_redis.gemspec +0 -30
  41. data/spec/client_spec.rb +0 -17
  42. data/spec/cloning_spec.rb +0 -95
  43. data/spec/commands/append_spec.rb +0 -24
  44. data/spec/commands/auth_spec.rb +0 -7
  45. data/spec/commands/bgrewriteaof_spec.rb +0 -7
  46. data/spec/commands/bgsave_spec.rb +0 -7
  47. data/spec/commands/bitcount_spec.rb +0 -25
  48. data/spec/commands/bitfield_spec.rb +0 -169
  49. data/spec/commands/blpop_spec.rb +0 -59
  50. data/spec/commands/brpop_spec.rb +0 -58
  51. data/spec/commands/brpoplpush_spec.rb +0 -52
  52. data/spec/commands/connected_spec.rb +0 -7
  53. data/spec/commands/dbsize_spec.rb +0 -18
  54. data/spec/commands/decr_spec.rb +0 -34
  55. data/spec/commands/decrby_spec.rb +0 -34
  56. data/spec/commands/del_spec.rb +0 -35
  57. data/spec/commands/disconnect_spec.rb +0 -7
  58. data/spec/commands/echo_spec.rb +0 -11
  59. data/spec/commands/eval_spec.rb +0 -7
  60. data/spec/commands/evalsha_spec.rb +0 -10
  61. data/spec/commands/exists_spec.rb +0 -14
  62. data/spec/commands/expire_spec.rb +0 -111
  63. data/spec/commands/expireat_spec.rb +0 -47
  64. data/spec/commands/flushall_spec.rb +0 -38
  65. data/spec/commands/flushdb_spec.rb +0 -38
  66. data/spec/commands/future_spec.rb +0 -20
  67. data/spec/commands/geoadd_spec.rb +0 -58
  68. data/spec/commands/geodist_spec.rb +0 -114
  69. data/spec/commands/geohash_spec.rb +0 -52
  70. data/spec/commands/geopos_spec.rb +0 -55
  71. data/spec/commands/get_spec.rb +0 -30
  72. data/spec/commands/getbit_spec.rb +0 -34
  73. data/spec/commands/getrange_spec.rb +0 -22
  74. data/spec/commands/getset_spec.rb +0 -23
  75. data/spec/commands/hash_operator_spec.rb +0 -21
  76. data/spec/commands/hdel_spec.rb +0 -54
  77. data/spec/commands/hexists_spec.rb +0 -27
  78. data/spec/commands/hget_spec.rb +0 -28
  79. data/spec/commands/hgetall_spec.rb +0 -32
  80. data/spec/commands/hincrby_spec.rb +0 -58
  81. data/spec/commands/hincrbyfloat_spec.rb +0 -58
  82. data/spec/commands/hkeys_spec.rb +0 -19
  83. data/spec/commands/hlen_spec.rb +0 -19
  84. data/spec/commands/hmget_spec.rb +0 -40
  85. data/spec/commands/hmset_spec.rb +0 -43
  86. data/spec/commands/hscan_each_spec.rb +0 -48
  87. data/spec/commands/hscan_spec.rb +0 -27
  88. data/spec/commands/hset_spec.rb +0 -38
  89. data/spec/commands/hsetnx_spec.rb +0 -44
  90. data/spec/commands/hvals_spec.rb +0 -19
  91. data/spec/commands/incr_spec.rb +0 -34
  92. data/spec/commands/incrby_spec.rb +0 -44
  93. data/spec/commands/incrbyfloat_spec.rb +0 -44
  94. data/spec/commands/info_spec.rb +0 -62
  95. data/spec/commands/keys_spec.rb +0 -122
  96. data/spec/commands/lastsave_spec.rb +0 -8
  97. data/spec/commands/lindex_spec.rb +0 -49
  98. data/spec/commands/linsert_spec.rb +0 -68
  99. data/spec/commands/llen_spec.rb +0 -16
  100. data/spec/commands/lpop_spec.rb +0 -34
  101. data/spec/commands/lpush_spec.rb +0 -43
  102. data/spec/commands/lpushx_spec.rb +0 -46
  103. data/spec/commands/lrange_spec.rb +0 -51
  104. data/spec/commands/lrem_spec.rb +0 -80
  105. data/spec/commands/lset_spec.rb +0 -43
  106. data/spec/commands/ltrim_spec.rb +0 -45
  107. data/spec/commands/mapped_hmget_spec.rb +0 -29
  108. data/spec/commands/mapped_hmset_spec.rb +0 -47
  109. data/spec/commands/mapped_mget_spec.rb +0 -22
  110. data/spec/commands/mapped_mset_spec.rb +0 -19
  111. data/spec/commands/mapped_msetnx_spec.rb +0 -26
  112. data/spec/commands/mget_spec.rb +0 -34
  113. data/spec/commands/move_spec.rb +0 -147
  114. data/spec/commands/mset_spec.rb +0 -29
  115. data/spec/commands/msetnx_spec.rb +0 -40
  116. data/spec/commands/persist_spec.rb +0 -48
  117. data/spec/commands/pexpire_spec.rb +0 -86
  118. data/spec/commands/pexpireat_spec.rb +0 -48
  119. data/spec/commands/ping_spec.rb +0 -7
  120. data/spec/commands/pipelined_spec.rb +0 -42
  121. data/spec/commands/pttl_spec.rb +0 -41
  122. data/spec/commands/quit_spec.rb +0 -7
  123. data/spec/commands/randomkey_spec.rb +0 -20
  124. data/spec/commands/rename_spec.rb +0 -42
  125. data/spec/commands/renamenx_spec.rb +0 -41
  126. data/spec/commands/rpop_spec.rb +0 -34
  127. data/spec/commands/rpoplpush_spec.rb +0 -50
  128. data/spec/commands/rpush_spec.rb +0 -43
  129. data/spec/commands/rpushx_spec.rb +0 -46
  130. data/spec/commands/sadd_spec.rb +0 -45
  131. data/spec/commands/save_spec.rb +0 -7
  132. data/spec/commands/scan_each_spec.rb +0 -39
  133. data/spec/commands/scan_spec.rb +0 -55
  134. data/spec/commands/scard_spec.rb +0 -18
  135. data/spec/commands/script_spec.rb +0 -9
  136. data/spec/commands/sdiff_spec.rb +0 -47
  137. data/spec/commands/sdiffstore_spec.rb +0 -58
  138. data/spec/commands/select_spec.rb +0 -61
  139. data/spec/commands/set_spec.rb +0 -63
  140. data/spec/commands/setbit_spec.rb +0 -54
  141. data/spec/commands/setex_spec.rb +0 -22
  142. data/spec/commands/setnx_spec.rb +0 -25
  143. data/spec/commands/setrange_spec.rb +0 -30
  144. data/spec/commands/sinter_spec.rb +0 -39
  145. data/spec/commands/sinterstore_spec.rb +0 -53
  146. data/spec/commands/sismember_spec.rb +0 -29
  147. data/spec/commands/smembers_spec.rb +0 -28
  148. data/spec/commands/smove_spec.rb +0 -41
  149. data/spec/commands/sort_list_spec.rb +0 -21
  150. data/spec/commands/sort_set_spec.rb +0 -21
  151. data/spec/commands/sort_zset_spec.rb +0 -21
  152. data/spec/commands/spop_spec.rb +0 -25
  153. data/spec/commands/srandmember_spec.rb +0 -49
  154. data/spec/commands/srem_spec.rb +0 -40
  155. data/spec/commands/sscan_each_spec.rb +0 -48
  156. data/spec/commands/sscan_spec.rb +0 -39
  157. data/spec/commands/strlen_spec.rb +0 -18
  158. data/spec/commands/sunion_spec.rb +0 -42
  159. data/spec/commands/sunionstore_spec.rb +0 -59
  160. data/spec/commands/ttl_spec.rb +0 -40
  161. data/spec/commands/type_spec.rb +0 -36
  162. data/spec/commands/unwatch_spec.rb +0 -7
  163. data/spec/commands/watch_spec.rb +0 -16
  164. data/spec/commands/zadd_spec.rb +0 -123
  165. data/spec/commands/zcard_spec.rb +0 -19
  166. data/spec/commands/zcount_spec.rb +0 -39
  167. data/spec/commands/zincrby_spec.rb +0 -31
  168. data/spec/commands/zinterstore_spec.rb +0 -96
  169. data/spec/commands/zrange_spec.rb +0 -80
  170. data/spec/commands/zrangebyscore_spec.rb +0 -83
  171. data/spec/commands/zrank_spec.rb +0 -29
  172. data/spec/commands/zrem_spec.rb +0 -43
  173. data/spec/commands/zremrangebyrank_spec.rb +0 -27
  174. data/spec/commands/zremrangebyscore_spec.rb +0 -35
  175. data/spec/commands/zrevrange_spec.rb +0 -56
  176. data/spec/commands/zrevrangebyscore_spec.rb +0 -58
  177. data/spec/commands/zrevrank_spec.rb +0 -29
  178. data/spec/commands/zscan_each_spec.rb +0 -48
  179. data/spec/commands/zscan_spec.rb +0 -26
  180. data/spec/commands/zscore_spec.rb +0 -22
  181. data/spec/commands/zunionstore_spec.rb +0 -104
  182. data/spec/mock_redis_spec.rb +0 -86
  183. data/spec/spec_helper.rb +0 -63
  184. data/spec/support/redis_multiplexer.rb +0 -106
  185. data/spec/support/shared_examples/only_operates_on_hashes.rb +0 -13
  186. data/spec/support/shared_examples/only_operates_on_lists.rb +0 -13
  187. data/spec/support/shared_examples/only_operates_on_sets.rb +0 -13
  188. data/spec/support/shared_examples/only_operates_on_strings.rb +0 -13
  189. data/spec/support/shared_examples/only_operates_on_zsets.rb +0 -57
  190. data/spec/support/shared_examples/sorts_enumerables.rb +0 -56
  191. data/spec/transactions_spec.rb +0 -159
@@ -0,0 +1,102 @@
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
+ # * xgroup
8
+ # * xreadgroup
9
+ # * xack
10
+ # * xpending
11
+ # * xclaim
12
+ # * xinfo
13
+ # * xtrim
14
+ # * xdel
15
+ #
16
+ # TODO: Complete support for
17
+ #
18
+ # * xtrim
19
+ # - `approximate: true` argument is currently ignored
20
+ # * xadd
21
+ # - `approximate: true` argument (for capped streams) is currently ignored
22
+ #
23
+ # For details of these commands see
24
+ # * https://redis.io/topics/streams-intro
25
+ # * https://redis.io/commands#stream
26
+
27
+ class MockRedis
28
+ module StreamMethods
29
+ include Assertions
30
+ include UtilityMethods
31
+
32
+ def xadd(key, entry, opts = {})
33
+ id = opts[:id] || '*'
34
+ with_stream_at(key) do |stream|
35
+ stream.add id, entry
36
+ stream.trim opts[:maxlen] if opts[:maxlen]
37
+ return stream.last_id
38
+ end
39
+ end
40
+
41
+ def xtrim(key, count)
42
+ with_stream_at(key) do |stream|
43
+ stream.trim count
44
+ end
45
+ end
46
+
47
+ def xlen(key)
48
+ with_stream_at(key) do |stream|
49
+ return stream.count
50
+ end
51
+ end
52
+
53
+ def xrange(key, first = '-', last = '+', count: nil)
54
+ args = [first, last, false]
55
+ args += ['COUNT', count] if count
56
+ with_stream_at(key) do |stream|
57
+ return stream.range(*args)
58
+ end
59
+ end
60
+
61
+ def xrevrange(key, last = '+', first = '-', count: nil)
62
+ args = [first, last, true]
63
+ args += ['COUNT', count] if count
64
+ with_stream_at(key) do |stream|
65
+ return stream.range(*args)
66
+ end
67
+ end
68
+
69
+ def xread(keys, ids, count: nil, block: nil)
70
+ args = []
71
+ args += ['COUNT', count] if count
72
+ args += ['BLOCK', block.to_i] if block
73
+ result = {}
74
+ keys = keys.is_a?(Array) ? keys : [keys]
75
+ ids = ids.is_a?(Array) ? ids : [ids]
76
+ keys.each_with_index do |key, index|
77
+ with_stream_at(key) do |stream|
78
+ data = stream.read(ids[index], *args)
79
+ result[key] = data unless data.empty?
80
+ end
81
+ end
82
+ result
83
+ end
84
+
85
+ private
86
+
87
+ def with_stream_at(key, &blk)
88
+ with_thing_at(key, :assert_streamy, proc { Stream.new }, &blk)
89
+ end
90
+
91
+ def streamy?(key)
92
+ data[key].nil? || data[key].is_a?(Stream)
93
+ end
94
+
95
+ def assert_streamy(key)
96
+ unless streamy?(key)
97
+ raise Redis::CommandError,
98
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
99
+ end
100
+ end
101
+ end
102
+ end
@@ -38,7 +38,7 @@ class MockRedis
38
38
  type, offset = args.shift(2)
39
39
 
40
40
  is_signed = type.slice(0) == 'i'
41
- type_size = type[1..-1].to_i
41
+ type_size = type[1..].to_i
42
42
 
43
43
  if (type_size > 64 && is_signed) || (type_size >= 64 && !is_signed)
44
44
  raise Redis::CommandError,
@@ -47,7 +47,7 @@ class MockRedis
47
47
  end
48
48
 
49
49
  if offset.to_s[0] == '#'
50
- offset = offset[1..-1].to_i * type_size
50
+ offset = offset[1..].to_i * type_size
51
51
  end
52
52
 
53
53
  bits = []
@@ -85,17 +85,15 @@ 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
 
96
+ offset = offset.to_i
99
97
  offset_of_byte = offset / 8
100
98
  offset_within_byte = offset % 8
101
99
 
@@ -109,6 +107,12 @@ class MockRedis
109
107
  end
110
108
  end
111
109
 
110
+ def getdel(key)
111
+ value = get(key)
112
+ del(key)
113
+ value
114
+ end
115
+
112
116
  def getrange(key, start, stop)
113
117
  assert_stringy(key)
114
118
  (data[key] || '')[start..stop]
@@ -156,12 +160,16 @@ class MockRedis
156
160
  new_value
157
161
  end
158
162
 
159
- def mget(*keys)
163
+ def mget(*keys, &blk)
164
+ keys.flatten!
165
+
160
166
  assert_has_args(keys, 'mget')
161
167
 
162
- keys.map do |key|
168
+ data = keys.map do |key|
163
169
  get(key) if stringy?(key)
164
170
  end
171
+
172
+ blk ? blk.call(data) : data
165
173
  end
166
174
 
167
175
  def mapped_mget(*keys)
@@ -170,6 +178,8 @@ class MockRedis
170
178
 
171
179
  def mset(*kvpairs)
172
180
  assert_has_args(kvpairs, 'mset')
181
+ kvpairs = kvpairs.first if kvpairs.size == 1 && kvpairs.first.is_a?(Enumerable)
182
+
173
183
  if kvpairs.length.odd?
174
184
  raise Redis::CommandError, 'ERR wrong number of arguments for MSET'
175
185
  end
@@ -188,7 +198,7 @@ class MockRedis
188
198
  def msetnx(*kvpairs)
189
199
  assert_has_args(kvpairs, 'msetnx')
190
200
 
191
- if kvpairs.each_slice(2).any? { |(k, _)| exists(k) }
201
+ if kvpairs.each_slice(2).any? { |(k, _)| exists?(k) }
192
202
  false
193
203
  else
194
204
  mset(*kvpairs)
@@ -200,18 +210,23 @@ class MockRedis
200
210
  msetnx(*hash.to_a.flatten)
201
211
  end
202
212
 
203
- def set(key, value, options = {})
213
+ # Parameter list required to ensure the ArgumentError is returned correctly
214
+ # rubocop:disable Metrics/ParameterLists
215
+ def set(key, value, _hash = nil, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil,
216
+ keepttl: nil, get: nil)
217
+ key = key.to_s
218
+ retval = self.get(key) if get
219
+
204
220
  return_true = false
205
- options = options.dup
206
- if options.delete(:nx)
207
- if exists(key)
221
+ if nx
222
+ if exists?(key)
208
223
  return false
209
224
  else
210
225
  return_true = true
211
226
  end
212
227
  end
213
- if options.delete(:xx)
214
- if exists(key)
228
+ if xx
229
+ if exists?(key)
215
230
  return_true = true
216
231
  else
217
232
  return false
@@ -219,21 +234,42 @@ class MockRedis
219
234
  end
220
235
  data[key] = value.to_s
221
236
 
222
- # take latter
223
- expire_option = options.to_a.last
224
- if expire_option
225
- type, duration = expire_option
226
- if duration == 0
237
+ remove_expiration(key) unless keepttl
238
+ if ex
239
+ if ex == 0
227
240
  raise Redis::CommandError, 'ERR invalid expire time in set'
228
241
  end
229
- expire(key, type.to_sym == :ex ? duration : duration / 1000.0)
242
+ expire(key, ex)
243
+ end
244
+
245
+ if px
246
+ if px == 0
247
+ raise Redis::CommandError, 'ERR invalid expire time in set'
248
+ end
249
+ pexpire(key, px)
250
+ end
251
+
252
+ if exat
253
+ if exat == 0
254
+ raise Redis::CommandError, 'ERR invalid expire time in set'
255
+ end
256
+ expireat(key, exat)
230
257
  end
231
- return_true ? true : 'OK'
232
- end
233
258
 
234
- def []=(key, value, options = {})
235
- set(key, value, options)
259
+ if pxat
260
+ if pxat == 0
261
+ raise Redis::CommandError, 'ERR invalid expire time in set'
262
+ end
263
+ pexpireat(key, pxat)
264
+ end
265
+
266
+ if get
267
+ retval
268
+ else
269
+ return_true ? true : 'OK'
270
+ end
236
271
  end
272
+ # rubocop:enable Metrics/ParameterLists
237
273
 
238
274
  def setbit(key, offset, value)
239
275
  assert_stringy(key, 'ERR bit is not an integer or out of range')
@@ -241,6 +277,7 @@ class MockRedis
241
277
 
242
278
  str = data[key] || ''
243
279
 
280
+ offset = offset.to_i
244
281
  offset_of_byte = offset / 8
245
282
  offset_within_byte = offset % 8
246
283
 
@@ -309,13 +346,27 @@ class MockRedis
309
346
  end
310
347
 
311
348
  def setex(key, seconds, value)
312
- set(key, value)
313
- expire(key, seconds)
314
- 'OK'
349
+ if seconds <= 0
350
+ raise Redis::CommandError, 'ERR invalid expire time in setex'
351
+ else
352
+ set(key, value)
353
+ expire(key, seconds)
354
+ 'OK'
355
+ end
356
+ end
357
+
358
+ def psetex(key, milliseconds, value)
359
+ if milliseconds <= 0
360
+ raise Redis::CommandError, 'ERR invalid expire time in psetex'
361
+ else
362
+ set(key, value)
363
+ pexpire(key, milliseconds)
364
+ 'OK'
365
+ end
315
366
  end
316
367
 
317
368
  def setnx(key, value)
318
- if exists(key)
369
+ if exists?(key)
319
370
  false
320
371
  else
321
372
  set(key, value)
@@ -329,7 +380,7 @@ class MockRedis
329
380
  old_value = (data[key] || '')
330
381
 
331
382
  prefix = zero_pad(old_value[0...offset], offset)
332
- data[key] = prefix + value + (old_value[(offset + value.length)..-1] || '')
383
+ data[key] = prefix + value + (old_value[(offset + value.length)..] || '')
333
384
  data[key].length
334
385
  end
335
386
 
@@ -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
- def method_missing(method, *args, &block)
19
- if @in_multi
20
- future = MockRedis::Future.new([method, *args])
18
+ ruby2_keywords def method_missing(method, *args, &block)
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,45 +35,54 @@ 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
- begin
59
- result = send(*future.command)
60
- future.store_result(result)
61
- result
62
- rescue StandardError => e
63
- e
64
- end
60
+ result = send(*future.command)
61
+ future.store_result(result)
62
+ future.value
63
+ rescue StandardError => e
64
+ e
65
65
  end
66
66
 
67
67
  @transaction_futures = []
68
68
  responses
69
69
  end
70
70
 
71
+ def in_multi?
72
+ @multi_stack.any?
73
+ end
74
+
75
+ def push_multi
76
+ @multi_stack.push(@multi_stack.size + 1)
77
+ end
78
+
79
+ def pop_multi
80
+ @multi_stack.pop
81
+ end
82
+
71
83
  def multi
72
- if @in_multi
73
- raise Redis::CommandError, 'ERR MULTI calls can not be nested'
74
- end
75
- @in_multi = true
76
84
  if block_given?
85
+ push_multi
77
86
  @multi_block_given = true
78
87
  begin
79
88
  yield(self)
@@ -83,6 +92,8 @@ class MockRedis
83
92
  raise e
84
93
  end
85
94
  else
95
+ raise Redis::CommandError, 'ERR MULTI calls can not be nested' if in_multi?
96
+ push_multi
86
97
  'OK'
87
98
  end
88
99
  end
@@ -95,7 +106,7 @@ class MockRedis
95
106
  'OK'
96
107
  end
97
108
 
98
- def watch(_)
109
+ def watch(*_)
99
110
  if block_given?
100
111
  yield self
101
112
  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] != '' && !data[key].is_a?(Stream)
22
22
  del(key)
23
23
  end
24
24
  end
@@ -28,12 +28,17 @@ class MockRedis
28
28
  cursor = cursor.to_i
29
29
  match = opts[:match] || '*'
30
30
  key = opts[:key] || lambda { |x| x }
31
+ type_opt = opts[:type]
32
+ filtered_values = []
31
33
 
32
34
  limit = cursor + count
33
35
  next_cursor = limit >= values.length ? '0' : limit.to_s
34
36
 
35
- filtered_values = values[cursor...limit].select do |val|
36
- redis_pattern_to_ruby_regex(match).match(key.call(val))
37
+ unless values[cursor...limit].nil?
38
+ filtered_values = values[cursor...limit].select do |val|
39
+ redis_pattern_to_ruby_regex(match).match(key.call(val)) &&
40
+ (type_opt.nil? || type(val) == type_opt)
41
+ end
37
42
  end
38
43
 
39
44
  [next_cursor, filtered_values]
@@ -65,7 +70,7 @@ class MockRedis
65
70
  end
66
71
 
67
72
  def left_pad(str, size)
68
- str = '0' + str while str.length < size
73
+ str = "0#{str}" while str.length < size
69
74
 
70
75
  str
71
76
  end
@@ -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.44.0'
6
6
  end
@@ -10,9 +10,11 @@ class MockRedis
10
10
 
11
11
  def_delegators :members, :empty?, :include?, :size
12
12
 
13
- def initialize
13
+ def initialize(enum = nil)
14
14
  @members = Set.new
15
- @scores = {}
15
+ @members.merge(enum) if enum
16
+
17
+ @scores = {}
16
18
  end
17
19
 
18
20
  def initialize_copy(source)
@@ -23,12 +25,7 @@ class MockRedis
23
25
 
24
26
  def add(score, member)
25
27
  members.add(member)
26
- scores[member] =
27
- if score.to_f.to_i == score.to_f
28
- score.to_f.to_i
29
- else
30
- score.to_f
31
- end
28
+ scores[member] = score.to_f
32
29
  self
33
30
  end
34
31
 
@@ -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]
@@ -147,7 +147,7 @@ class MockRedis
147
147
  else
148
148
  retval = args.map { |member| !!zscore(key, member.to_s) }.count(true)
149
149
  with_zset_at(key) do |z|
150
- args.each { |member| z.delete?(member) }
150
+ args.each { |member| z.delete?(member.to_s) }
151
151
  end
152
152
  end
153
153
  end
@@ -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,16 @@ 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
233
+ end
234
+ end
235
+
236
+ def zmscore(key, *members)
237
+ with_zset_at(key) do |z|
238
+ members.map do |member|
239
+ score = z.score(member.to_s)
240
+ score&.to_f
241
+ end
215
242
  end
216
243
  end
217
244
 
@@ -264,7 +291,7 @@ class MockRedis
264
291
  raise Redis::CommandError, 'ERR syntax error'
265
292
  end
266
293
 
267
- with_zsets_at(*keys) do |*zsets|
294
+ with_zsets_at(*keys, coercible: true) do |*zsets|
268
295
  zsets.zip(weights).map do |(zset, weight)|
269
296
  zset.reduce(Zset.new) do |acc, (score, member)|
270
297
  acc.add(score * weight, member)
@@ -275,16 +302,30 @@ class MockRedis
275
302
  end
276
303
  end
277
304
 
278
- def with_zset_at(key, &blk)
279
- with_thing_at(key, :assert_zsety, proc { Zset.new }, &blk)
305
+ def coerce_to_zset(set)
306
+ zset = Zset.new
307
+ set.each do |member|
308
+ zset.add(1.0, member)
309
+ end
310
+ zset
280
311
  end
281
312
 
282
- def with_zsets_at(*keys, &blk)
313
+ def with_zset_at(key, coercible: false, &blk)
314
+ if coercible
315
+ with_thing_at(key, :assert_coercible_zsety, proc { Zset.new }) do |value|
316
+ blk.call value.is_a?(Set) ? coerce_to_zset(value) : value
317
+ end
318
+ else
319
+ with_thing_at(key, :assert_zsety, proc { Zset.new }, &blk)
320
+ end
321
+ end
322
+
323
+ def with_zsets_at(*keys, coercible: false, &blk)
283
324
  if keys.length == 1
284
- with_zset_at(keys.first, &blk)
325
+ with_zset_at(keys.first, coercible: coercible, &blk)
285
326
  else
286
- with_zset_at(keys.first) do |set|
287
- with_zsets_at(*(keys[1..-1])) do |*sets|
327
+ with_zset_at(keys.first, coercible: coercible) do |set|
328
+ with_zsets_at(*(keys[1..]), coercible: coercible) do |*sets|
288
329
  yield(*([set] + sets))
289
330
  end
290
331
  end
@@ -295,6 +336,10 @@ class MockRedis
295
336
  data[key].nil? || data[key].is_a?(Zset)
296
337
  end
297
338
 
339
+ def coercible_zsety?(key)
340
+ zsety?(key) || data[key].is_a?(Set)
341
+ end
342
+
298
343
  def assert_zsety(key)
299
344
  unless zsety?(key)
300
345
  raise Redis::CommandError,
@@ -302,13 +347,20 @@ class MockRedis
302
347
  end
303
348
  end
304
349
 
350
+ def assert_coercible_zsety(key)
351
+ unless coercible_zsety?(key)
352
+ raise Redis::CommandError,
353
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
354
+ end
355
+ end
356
+
305
357
  def looks_like_float?(x)
306
358
  # ugh, exceptions for flow control.
307
359
  !!Float(x) rescue false
308
360
  end
309
361
 
310
362
  def assert_scorey(value, message = 'ERR value is not a valid float')
311
- return if value =~ /\(?(\-|\+)inf/
363
+ return if value.to_s =~ /\(?(-|\+)inf/
312
364
 
313
365
  value = $1 if value.to_s =~ /\((.*)/
314
366
  unless looks_like_float?(value)