mock_redis 0.21.0 → 0.26.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) 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 +33 -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 -7
  10. data/lib/mock_redis/database.rb +42 -14
  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 +18 -6
  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/stream.rb +25 -2
  17. data/lib/mock_redis/stream/id.rb +1 -1
  18. data/lib/mock_redis/stream_methods.rb +16 -1
  19. data/lib/mock_redis/string_methods.rb +17 -9
  20. data/lib/mock_redis/transaction_wrapper.rb +2 -2
  21. data/lib/mock_redis/utility_methods.rb +6 -3
  22. data/lib/mock_redis/version.rb +1 -1
  23. data/lib/mock_redis/zset_methods.rb +52 -9
  24. data/mock_redis.gemspec +1 -2
  25. data/spec/commands/blpop_spec.rb +0 -6
  26. data/spec/commands/brpop_spec.rb +6 -5
  27. data/spec/commands/del_spec.rb +15 -0
  28. data/spec/commands/dump_spec.rb +19 -0
  29. data/spec/commands/exists_spec.rb +34 -5
  30. data/spec/commands/future_spec.rb +11 -1
  31. data/spec/commands/geoadd_spec.rb +1 -1
  32. data/spec/commands/hmset_spec.rb +26 -0
  33. data/spec/commands/hset_spec.rb +6 -6
  34. data/spec/commands/keys_spec.rb +17 -0
  35. data/spec/commands/mget_spec.rb +6 -0
  36. data/spec/commands/move_spec.rb +5 -5
  37. data/spec/commands/pipelined_spec.rb +20 -0
  38. data/spec/commands/restore_spec.rb +47 -0
  39. data/spec/commands/scan_spec.rb +9 -0
  40. data/spec/commands/set_spec.rb +8 -4
  41. data/spec/commands/setbit_spec.rb +1 -0
  42. data/spec/commands/setex_spec.rb +16 -0
  43. data/spec/commands/srandmember_spec.rb +1 -1
  44. data/spec/commands/xadd_spec.rb +20 -0
  45. data/spec/commands/xrange_spec.rb +13 -0
  46. data/spec/commands/xread_spec.rb +66 -0
  47. data/spec/commands/xtrim_spec.rb +6 -0
  48. data/spec/commands/zinterstore_spec.rb +34 -0
  49. data/spec/commands/zpopmax_spec.rb +60 -0
  50. data/spec/commands/zpopmin_spec.rb +60 -0
  51. data/spec/commands/zrange_spec.rb +1 -1
  52. data/spec/commands/zrangebyscore_spec.rb +1 -1
  53. data/spec/commands/zrevrange_spec.rb +1 -1
  54. data/spec/commands/zrevrangebyscore_spec.rb +1 -1
  55. data/spec/commands/zunionstore_spec.rb +33 -0
  56. data/spec/spec_helper.rb +4 -2
  57. data/spec/support/redis_multiplexer.rb +1 -0
  58. data/spec/transactions_spec.rb +16 -0
  59. metadata +16 -26
  60. 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 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)
@@ -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,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
@@ -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,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
- expire(key, duration / 1000.0)
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
- set(key, value)
317
- expire(key, seconds)
318
- '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
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
- 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.21.0'
5
+ VERSION = '0.26.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'
@@ -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)
@@ -26,11 +26,12 @@ describe '#brpop(key [, key, ...,], timeout)' do
26
26
  [@list1, 'two']
27
27
  end
28
28
 
29
- it 'raises an error on subsecond timeouts' do
30
- lambda do
31
- @redises.brpop(@list1, @list2, :timeout => 0.5)
32
- end.should raise_error(Redis::CommandError)
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
@@ -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