mock_redis 0.21.0 → 0.26.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 +33 -0
- data/Gemfile +2 -2
- data/LICENSE.md +21 -0
- data/README.md +37 -13
- data/lib/mock_redis.rb +0 -7
- data/lib/mock_redis/database.rb +42 -14
- data/lib/mock_redis/future.rb +1 -1
- data/lib/mock_redis/geospatial_methods.rb +4 -4
- data/lib/mock_redis/hash_methods.rb +18 -6
- data/lib/mock_redis/info_method.rb +2 -2
- data/lib/mock_redis/multi_db_wrapper.rb +2 -2
- data/lib/mock_redis/stream.rb +25 -2
- data/lib/mock_redis/stream/id.rb +1 -1
- data/lib/mock_redis/stream_methods.rb +16 -1
- data/lib/mock_redis/string_methods.rb +17 -9
- 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/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/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 +8 -4
- 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/xadd_spec.rb +20 -0
- data/spec/commands/xrange_spec.rb +13 -0
- data/spec/commands/xread_spec.rb +66 -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/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
@@ -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,14 @@ class MockRedis
|
|
45
60
|
items
|
46
61
|
end
|
47
62
|
|
63
|
+
def read(id, *opts_in)
|
64
|
+
opts = options opts_in, %w[count block]
|
65
|
+
stream_id = MockRedis::Stream::Id.new(id)
|
66
|
+
items = members.select { |m| (stream_id < m[0]) }.map { |m| [m[0].to_s, m[1]] }
|
67
|
+
return items.first(opts['count'].to_i) if opts.key?('count')
|
68
|
+
items
|
69
|
+
end
|
70
|
+
|
48
71
|
def each
|
49
72
|
members.each { |m| yield m }
|
50
73
|
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,22 @@ class MockRedis
|
|
67
66
|
end
|
68
67
|
end
|
69
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
|
+
|
70
85
|
private
|
71
86
|
|
72
87
|
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
|
@@ -233,7 +234,10 @@ class MockRedis
|
|
233
234
|
if duration == 0
|
234
235
|
raise Redis::CommandError, 'ERR invalid expire time in set'
|
235
236
|
end
|
236
|
-
|
237
|
+
pexpire(key, duration)
|
238
|
+
end
|
239
|
+
unless options.empty?
|
240
|
+
raise ArgumentError, "unknown keyword: #{options.keys[0]}"
|
237
241
|
end
|
238
242
|
|
239
243
|
return_true ? true : 'OK'
|
@@ -313,13 +317,17 @@ class MockRedis
|
|
313
317
|
end
|
314
318
|
|
315
319
|
def setex(key, seconds, value)
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
319
327
|
end
|
320
328
|
|
321
329
|
def setnx(key, value)
|
322
|
-
if exists(key)
|
330
|
+
if exists?(key)
|
323
331
|
false
|
324
332
|
else
|
325
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'
|
data/spec/commands/blpop_spec.rb
CHANGED
@@ -27,12 +27,6 @@ describe '#blpop(key [, key, ...,], timeout)' do
|
|
27
27
|
[@list1, 'one']
|
28
28
|
end
|
29
29
|
|
30
|
-
it 'raises an error on subsecond timeouts' do
|
31
|
-
lambda do
|
32
|
-
@redises.blpop(@list1, @list2, :timeout => 0.5)
|
33
|
-
end.should raise_error(Redis::CommandError)
|
34
|
-
end
|
35
|
-
|
36
30
|
it 'raises an error on negative timeout' do
|
37
31
|
lambda do
|
38
32
|
@redises.blpop(@list1, @list2, :timeout => -1)
|
data/spec/commands/brpop_spec.rb
CHANGED
@@ -26,11 +26,12 @@ describe '#brpop(key [, key, ...,], timeout)' do
|
|
26
26
|
[@list1, 'two']
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
29
|
+
# TODO: Not sure how redis-rb is handling this but they're not raising an error
|
30
|
+
# it 'raises an error on subsecond timeouts' do
|
31
|
+
# lambda do
|
32
|
+
# @redises.brpop(@list1, @list2, :timeout => 0.5)
|
33
|
+
# end.should raise_error(Redis::CommandError)
|
34
|
+
# end
|
34
35
|
|
35
36
|
it 'raises an error on negative timeout' do
|
36
37
|
lambda do
|
data/spec/commands/del_spec.rb
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe '#del(key [, key, ...])' do
|
4
|
+
before :all do
|
5
|
+
sleep 1 - (Time.now.to_f % 1)
|
6
|
+
end
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
@redises._gsub(/\d{3}-\d/, '...-.')
|
10
|
+
end
|
11
|
+
|
4
12
|
it 'returns the number of keys deleted' do
|
5
13
|
@redises.set('mock-redis-test:1', 1)
|
6
14
|
@redises.set('mock-redis-test:2', 1)
|
@@ -32,4 +40,11 @@ describe '#del(key [, key, ...])' do
|
|
32
40
|
it 'raises an error if an empty array is given' do
|
33
41
|
expect { @redises.del [] }.to raise_error Redis::CommandError
|
34
42
|
end
|
43
|
+
|
44
|
+
it 'removes a stream key' do
|
45
|
+
@redises.xadd('mock-redis-stream', { key: 'value' }, maxlen: 0)
|
46
|
+
expect(@redises.exists?('mock-redis-stream')).to eq true
|
47
|
+
@redises.del('mock-redis-stream')
|
48
|
+
expect(@redises.exists?('mock-redis-stream')).to eq false
|
49
|
+
end
|
35
50
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe '#dump(key)' do
|
4
|
+
before do
|
5
|
+
@key = 'mock-redis-test:45794'
|
6
|
+
# These are mock-only, since our dump/restore implementations
|
7
|
+
# aren't compatible with real redis.
|
8
|
+
@mock = @redises.mock
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns nil for keys that do not exist' do
|
12
|
+
@mock.dump(@key).should be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'returns a serialized value for keys that do exist' do
|
16
|
+
@mock.set(@key, '2')
|
17
|
+
@mock.dump(@key).should == Marshal.dump('2')
|
18
|
+
end
|
19
|
+
end
|