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.
- checksums.yaml +4 -4
- data/.rubocop.yml +32 -5
- data/.rubocop_todo.yml +1 -1
- data/.travis.yml +9 -10
- data/CHANGELOG.md +46 -0
- data/Gemfile +2 -2
- data/LICENSE.md +21 -0
- data/README.md +39 -15
- data/lib/mock_redis.rb +0 -5
- data/lib/mock_redis/database.rb +59 -22
- data/lib/mock_redis/future.rb +1 -1
- data/lib/mock_redis/geospatial_methods.rb +14 -22
- data/lib/mock_redis/hash_methods.rb +23 -15
- data/lib/mock_redis/indifferent_hash.rb +0 -8
- data/lib/mock_redis/info_method.rb +2 -2
- data/lib/mock_redis/list_methods.rb +2 -2
- data/lib/mock_redis/multi_db_wrapper.rb +2 -2
- data/lib/mock_redis/pipelined_wrapper.rb +25 -6
- data/lib/mock_redis/set_methods.rb +16 -4
- data/lib/mock_redis/stream.rb +62 -0
- data/lib/mock_redis/stream/id.rb +60 -0
- data/lib/mock_redis/stream_methods.rb +87 -0
- data/lib/mock_redis/string_methods.rb +31 -20
- data/lib/mock_redis/transaction_wrapper.rb +27 -14
- data/lib/mock_redis/utility_methods.rb +6 -3
- data/lib/mock_redis/version.rb +1 -1
- data/lib/mock_redis/zset_methods.rb +54 -11
- data/mock_redis.gemspec +6 -6
- 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/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/geodist_spec.rb +8 -4
- data/spec/commands/geohash_spec.rb +4 -4
- data/spec/commands/geopos_spec.rb +4 -4
- data/spec/commands/get_spec.rb +1 -0
- data/spec/commands/hdel_spec.rb +18 -2
- 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 +34 -15
- data/spec/commands/move_spec.rb +5 -5
- data/spec/commands/mset_spec.rb +14 -0
- data/spec/commands/pipelined_spec.rb +72 -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/spop_spec.rb +15 -0
- data/spec/commands/srandmember_spec.rb +1 -1
- data/spec/commands/srem_spec.rb +5 -0
- data/spec/commands/watch_spec.rb +8 -3
- data/spec/commands/xadd_spec.rb +104 -0
- data/spec/commands/xlen_spec.rb +20 -0
- data/spec/commands/xrange_spec.rb +141 -0
- data/spec/commands/xrevrange_spec.rb +130 -0
- data/spec/commands/xtrim_spec.rb +30 -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 +6 -2
- data/spec/support/redis_multiplexer.rb +18 -1
- data/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +14 -0
- data/spec/support/shared_examples/only_operates_on_hashes.rb +2 -0
- data/spec/support/shared_examples/only_operates_on_lists.rb +2 -0
- data/spec/support/shared_examples/only_operates_on_sets.rb +2 -0
- data/spec/support/shared_examples/only_operates_on_zsets.rb +2 -0
- data/spec/transactions_spec.rb +30 -26
- metadata +45 -31
- data/LICENSE +0 -19
- 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
|
-
|
223
|
-
|
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,
|
228
|
+
expire(key, duration)
|
230
229
|
end
|
231
|
-
return_true ? true : 'OK'
|
232
|
-
end
|
233
230
|
|
234
|
-
|
235
|
-
|
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
|
-
|
313
|
-
|
314
|
-
|
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
|
-
@
|
14
|
+
@multi_stack = []
|
15
15
|
@multi_block_given = false
|
16
16
|
end
|
17
17
|
|
18
18
|
def method_missing(method, *args, &block)
|
19
|
-
if
|
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
|
42
|
+
unless in_multi?
|
42
43
|
raise Redis::CommandError, 'ERR DISCARD without MULTI'
|
43
44
|
end
|
44
|
-
|
45
|
-
|
45
|
+
pop_multi
|
46
|
+
|
46
47
|
@transaction_futures = []
|
47
48
|
'OK'
|
48
49
|
end
|
49
50
|
|
50
51
|
def exec
|
51
|
-
unless
|
52
|
+
unless in_multi?
|
52
53
|
raise Redis::CommandError, 'ERR EXEC without MULTI'
|
53
54
|
end
|
54
|
-
|
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
|
-
|
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]
|
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
|
-
|
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
@@ -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
|
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
|
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
|
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)
|