mock_redis 0.20.0 → 0.25.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +26 -5
- data/.rubocop_todo.yml +1 -1
- data/.travis.yml +3 -3
- data/CHANGELOG.md +37 -0
- data/Gemfile +2 -2
- data/LICENSE.md +21 -0
- data/README.md +37 -13
- data/lib/mock_redis.rb +0 -5
- data/lib/mock_redis/database.rb +53 -17
- data/lib/mock_redis/future.rb +1 -1
- data/lib/mock_redis/geospatial_methods.rb +4 -4
- data/lib/mock_redis/hash_methods.rb +22 -14
- data/lib/mock_redis/info_method.rb +2 -2
- data/lib/mock_redis/multi_db_wrapper.rb +2 -2
- data/lib/mock_redis/set_methods.rb +1 -0
- data/lib/mock_redis/stream.rb +22 -2
- data/lib/mock_redis/stream/id.rb +1 -1
- data/lib/mock_redis/stream_methods.rb +14 -1
- data/lib/mock_redis/string_methods.rb +28 -13
- data/lib/mock_redis/transaction_wrapper.rb +2 -2
- data/lib/mock_redis/utility_methods.rb +6 -3
- data/lib/mock_redis/version.rb +1 -1
- data/lib/mock_redis/zset_methods.rb +52 -9
- data/mock_redis.gemspec +1 -2
- data/spec/client_spec.rb +12 -0
- data/spec/commands/blpop_spec.rb +0 -6
- data/spec/commands/brpop_spec.rb +6 -5
- data/spec/commands/del_spec.rb +15 -0
- data/spec/commands/dump_spec.rb +19 -0
- data/spec/commands/exists_spec.rb +34 -5
- data/spec/commands/future_spec.rb +11 -1
- data/spec/commands/geoadd_spec.rb +1 -1
- data/spec/commands/hdel_spec.rb +16 -0
- data/spec/commands/hmset_spec.rb +26 -0
- data/spec/commands/hset_spec.rb +6 -6
- data/spec/commands/keys_spec.rb +17 -0
- data/spec/commands/mget_spec.rb +6 -0
- data/spec/commands/move_spec.rb +5 -5
- data/spec/commands/pipelined_spec.rb +20 -0
- data/spec/commands/restore_spec.rb +47 -0
- data/spec/commands/scan_spec.rb +9 -0
- data/spec/commands/set_spec.rb +12 -2
- data/spec/commands/setbit_spec.rb +1 -0
- data/spec/commands/setex_spec.rb +16 -0
- data/spec/commands/srandmember_spec.rb +1 -1
- data/spec/commands/srem_spec.rb +5 -0
- data/spec/commands/xadd_spec.rb +20 -0
- data/spec/commands/xrange_spec.rb +13 -0
- data/spec/commands/xread_spec.rb +50 -0
- data/spec/commands/xtrim_spec.rb +6 -0
- data/spec/commands/zinterstore_spec.rb +34 -0
- data/spec/commands/zpopmax_spec.rb +60 -0
- data/spec/commands/zpopmin_spec.rb +60 -0
- data/spec/commands/zrange_spec.rb +1 -1
- data/spec/commands/zrangebyscore_spec.rb +1 -1
- data/spec/commands/zrevrange_spec.rb +1 -1
- data/spec/commands/zrevrangebyscore_spec.rb +1 -1
- data/spec/commands/zunionstore_spec.rb +33 -0
- data/spec/mock_redis_spec.rb +4 -6
- data/spec/spec_helper.rb +4 -2
- data/spec/support/redis_multiplexer.rb +1 -0
- data/spec/transactions_spec.rb +16 -0
- metadata +16 -26
- 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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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,
|
129
|
-
|
130
|
-
with_hash_at(key)
|
131
|
-
|
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
|
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
|
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.
|
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)
|
data/lib/mock_redis/stream.rb
CHANGED
@@ -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
|
-
|
33
|
-
|
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
|
data/lib/mock_redis/stream/id.rb
CHANGED
@@ -31,7 +31,7 @@ class MockRedis
|
|
31
31
|
@timestamp = id
|
32
32
|
end
|
33
33
|
@sequence = @sequence.nil? ? sequence : @sequence.to_i
|
34
|
-
if
|
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
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
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
|
-
|
310
|
-
|
311
|
-
|
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
|
-
|
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
|
-
|
36
|
-
|
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]
|
data/lib/mock_redis/version.rb
CHANGED
@@ -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
|
279
|
-
|
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
|
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)
|
data/mock_redis.gemspec
CHANGED
@@ -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 '
|
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'
|