mock_redis 0.22.0 → 0.27.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +26 -5
  3. data/.rubocop_todo.yml +1 -1
  4. data/.travis.yml +1 -0
  5. data/CHANGELOG.md +31 -0
  6. data/Gemfile +4 -2
  7. data/lib/mock_redis.rb +1 -8
  8. data/lib/mock_redis/connection_method.rb +13 -0
  9. data/lib/mock_redis/database.rb +44 -14
  10. data/lib/mock_redis/expire_wrapper.rb +1 -1
  11. data/lib/mock_redis/future.rb +1 -1
  12. data/lib/mock_redis/geospatial_methods.rb +5 -5
  13. data/lib/mock_redis/hash_methods.rb +9 -4
  14. data/lib/mock_redis/info_method.rb +2 -2
  15. data/lib/mock_redis/multi_db_wrapper.rb +3 -3
  16. data/lib/mock_redis/pipelined_wrapper.rb +1 -1
  17. data/lib/mock_redis/stream.rb +22 -2
  18. data/lib/mock_redis/stream/id.rb +1 -1
  19. data/lib/mock_redis/stream_methods.rb +16 -1
  20. data/lib/mock_redis/string_methods.rb +27 -20
  21. data/lib/mock_redis/transaction_wrapper.rb +3 -3
  22. data/lib/mock_redis/utility_methods.rb +1 -1
  23. data/lib/mock_redis/version.rb +1 -1
  24. data/lib/mock_redis/zset_methods.rb +34 -9
  25. data/mock_redis.gemspec +1 -1
  26. data/spec/commands/blpop_spec.rb +0 -6
  27. data/spec/commands/brpop_spec.rb +6 -5
  28. data/spec/commands/connection_spec.rb +15 -0
  29. data/spec/commands/del_spec.rb +17 -0
  30. data/spec/commands/dump_spec.rb +19 -0
  31. data/spec/commands/exists_spec.rb +34 -5
  32. data/spec/commands/future_spec.rb +11 -1
  33. data/spec/commands/geoadd_spec.rb +1 -1
  34. data/spec/commands/hset_spec.rb +6 -6
  35. data/spec/commands/keys_spec.rb +17 -0
  36. data/spec/commands/mget_spec.rb +6 -0
  37. data/spec/commands/move_spec.rb +5 -5
  38. data/spec/commands/pipelined_spec.rb +20 -0
  39. data/spec/commands/restore_spec.rb +47 -0
  40. data/spec/commands/set_spec.rb +59 -9
  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 +23 -3
  45. data/spec/commands/xlen_spec.rb +3 -1
  46. data/spec/commands/xrange_spec.rb +13 -0
  47. data/spec/commands/xread_spec.rb +66 -0
  48. data/spec/commands/xtrim_spec.rb +6 -0
  49. data/spec/commands/zinterstore_spec.rb +34 -0
  50. data/spec/commands/zrange_spec.rb +1 -1
  51. data/spec/commands/zrangebyscore_spec.rb +1 -1
  52. data/spec/commands/zrevrange_spec.rb +1 -1
  53. data/spec/commands/zrevrangebyscore_spec.rb +1 -1
  54. data/spec/commands/zunionstore_spec.rb +33 -0
  55. data/spec/spec_helper.rb +2 -1
  56. data/spec/support/redis_multiplexer.rb +2 -1
  57. data/spec/transactions_spec.rb +16 -0
  58. metadata +14 -5
@@ -18,7 +18,7 @@ class MockRedis
18
18
  @pipelined_futures = @pipelined_futures.clone
19
19
  end
20
20
 
21
- def method_missing(method, *args, &block)
21
+ ruby2_keywords def method_missing(method, *args, &block)
22
22
  if in_pipeline?
23
23
  future = MockRedis::Future.new([method, *args], block)
24
24
  @pipelined_futures << future
@@ -23,14 +23,26 @@ 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 must be greater than 0-0'
29
+ end
26
30
  members.add [@last_id, Hash[values.map { |k, v| [k.to_s, v.to_s] }]]
27
31
  @last_id.to_s
28
32
  end
29
33
 
30
34
  def trim(count)
31
35
  deleted = @members.size - count
32
- @members = @members.to_a[-count..-1].to_set
33
- deleted
36
+ if deleted > 0
37
+ @members = if count == 0
38
+ Set.new
39
+ else
40
+ @members.to_a[-count..-1].to_set
41
+ end
42
+ deleted
43
+ else
44
+ 0
45
+ end
34
46
  end
35
47
 
36
48
  def range(start, finish, reversed, *opts_in)
@@ -45,6 +57,14 @@ class MockRedis
45
57
  items
46
58
  end
47
59
 
60
+ def read(id, *opts_in)
61
+ opts = options opts_in, %w[count block]
62
+ stream_id = MockRedis::Stream::Id.new(id)
63
+ items = members.select { |m| (stream_id < m[0]) }.map { |m| [m[0].to_s, m[1]] }
64
+ return items.first(opts['count'].to_i) if opts.key?('count')
65
+ items
66
+ end
67
+
48
68
  def each
49
69
  members.each { |m| yield m }
50
70
  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)
@@ -200,19 +201,20 @@ class MockRedis
200
201
  msetnx(*hash.to_a.flatten)
201
202
  end
202
203
 
203
- def set(key, value, options = {})
204
+ # Parameer list required to ensure the ArgumentError is returned correctly
205
+ # rubocop:disable Metrics/ParameterLists
206
+ def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil)
204
207
  key = key.to_s
205
208
  return_true = false
206
- options = options.dup
207
- if options.delete(:nx)
208
- if exists(key)
209
+ if nx
210
+ if exists?(key)
209
211
  return false
210
212
  else
211
213
  return_true = true
212
214
  end
213
215
  end
214
- if options.delete(:xx)
215
- if exists(key)
216
+ if xx
217
+ if exists?(key)
216
218
  return_true = true
217
219
  else
218
220
  return false
@@ -220,24 +222,24 @@ class MockRedis
220
222
  end
221
223
  data[key] = value.to_s
222
224
 
223
- duration = options.delete(:ex)
224
- if duration
225
- if duration == 0
225
+ remove_expiration(key) unless keepttl
226
+ if ex
227
+ if ex == 0
226
228
  raise Redis::CommandError, 'ERR invalid expire time in set'
227
229
  end
228
- expire(key, duration)
230
+ expire(key, ex)
229
231
  end
230
232
 
231
- duration = options.delete(:px)
232
- if duration
233
- if duration == 0
233
+ if px
234
+ if px == 0
234
235
  raise Redis::CommandError, 'ERR invalid expire time in set'
235
236
  end
236
- expire(key, duration / 1000.0)
237
+ pexpire(key, px)
237
238
  end
238
239
 
239
240
  return_true ? true : 'OK'
240
241
  end
242
+ # rubocop:enable Metrics/ParameterLists
241
243
 
242
244
  def setbit(key, offset, value)
243
245
  assert_stringy(key, 'ERR bit is not an integer or out of range')
@@ -245,6 +247,7 @@ class MockRedis
245
247
 
246
248
  str = data[key] || ''
247
249
 
250
+ offset = offset.to_i
248
251
  offset_of_byte = offset / 8
249
252
  offset_within_byte = offset % 8
250
253
 
@@ -313,13 +316,17 @@ class MockRedis
313
316
  end
314
317
 
315
318
  def setex(key, seconds, value)
316
- set(key, value)
317
- expire(key, seconds)
318
- 'OK'
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
319
326
  end
320
327
 
321
328
  def setnx(key, value)
322
- if exists(key)
329
+ if exists?(key)
323
330
  false
324
331
  else
325
332
  set(key, value)
@@ -15,9 +15,9 @@ class MockRedis
15
15
  @multi_block_given = false
16
16
  end
17
17
 
18
- def method_missing(method, *args, &block)
18
+ ruby2_keywords 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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  class MockRedis
5
- VERSION = '0.22.0'
5
+ VERSION = '0.27.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]
@@ -282,7 +282,7 @@ class MockRedis
282
282
  raise Redis::CommandError, 'ERR syntax error'
283
283
  end
284
284
 
285
- with_zsets_at(*keys) do |*zsets|
285
+ with_zsets_at(*keys, coercible: true) do |*zsets|
286
286
  zsets.zip(weights).map do |(zset, weight)|
287
287
  zset.reduce(Zset.new) do |acc, (score, member)|
288
288
  acc.add(score * weight, member)
@@ -293,16 +293,30 @@ class MockRedis
293
293
  end
294
294
  end
295
295
 
296
- def with_zset_at(key, &blk)
297
- 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
298
302
  end
299
303
 
300
- 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)
301
315
  if keys.length == 1
302
- with_zset_at(keys.first, &blk)
316
+ with_zset_at(keys.first, coercible: coercible, &blk)
303
317
  else
304
- with_zset_at(keys.first) do |set|
305
- 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|
306
320
  yield(*([set] + sets))
307
321
  end
308
322
  end
@@ -313,6 +327,10 @@ class MockRedis
313
327
  data[key].nil? || data[key].is_a?(Zset)
314
328
  end
315
329
 
330
+ def coercible_zsety?(key)
331
+ zsety?(key) || data[key].is_a?(Set)
332
+ end
333
+
316
334
  def assert_zsety(key)
317
335
  unless zsety?(key)
318
336
  raise Redis::CommandError,
@@ -320,13 +338,20 @@ class MockRedis
320
338
  end
321
339
  end
322
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
+
323
348
  def looks_like_float?(x)
324
349
  # ugh, exceptions for flow control.
325
350
  !!Float(x) rescue false
326
351
  end
327
352
 
328
353
  def assert_scorey(value, message = 'ERR value is not a valid float')
329
- return if value =~ /\(?(\-|\+)inf/
354
+ return if value.to_s =~ /\(?(\-|\+)inf/
330
355
 
331
356
  value = $1 if value.to_s =~ /\((.*)/
332
357
  unless looks_like_float?(value)
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.required_ruby_version = '>= 2.4'
25
25
 
26
- s.add_development_dependency 'redis', '~> 4.1.0'
26
+ s.add_development_dependency 'redis', '~> 4.2.0'
27
27
  s.add_development_dependency 'rspec', '~> 3.0'
28
28
  s.add_development_dependency 'rspec-its', '~> 1.0'
29
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
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe '#connection' do
4
+ let(:redis) { @redises.mock }
5
+
6
+ it 'returns the correct values' do
7
+ redis.connection.should == {
8
+ :host => '127.0.0.1',
9
+ :port => 6379,
10
+ :db => 0,
11
+ :id => 'redis://127.0.0.1:6379/0',
12
+ :location => '127.0.0.1:6379'
13
+ }
14
+ end
15
+ end
@@ -1,6 +1,16 @@
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
+ # TODO: Redis appears to be returning a timestamp a few seconds in the future
10
+ # so we're ignoring the last 5 digits (time in milliseconds)
11
+ @redises._gsub(/\d{5}-\d/, '...-.')
12
+ end
13
+
4
14
  it 'returns the number of keys deleted' do
5
15
  @redises.set('mock-redis-test:1', 1)
6
16
  @redises.set('mock-redis-test:2', 1)
@@ -32,4 +42,11 @@ describe '#del(key [, key, ...])' do
32
42
  it 'raises an error if an empty array is given' do
33
43
  expect { @redises.del [] }.to raise_error Redis::CommandError
34
44
  end
45
+
46
+ it 'removes a stream key' do
47
+ @redises.xadd('mock-redis-stream', { key: 'value' }, maxlen: 0)
48
+ expect(@redises.exists?('mock-redis-stream')).to eq true
49
+ @redises.del('mock-redis-stream')
50
+ expect(@redises.exists?('mock-redis-stream')).to eq false
51
+ end
35
52
  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
@@ -1,14 +1,43 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe '#exists(key)' do
4
- before { @key = 'mock-redis-test:45794' }
3
+ describe '#exists(*keys)' do
4
+ before { @key1 = 'mock-redis-test:exists1' }
5
+ before { @key2 = 'mock-redis-test:exists2' }
6
+
7
+ it 'returns 0 for keys that do not exist' do
8
+ @redises.exists(@key1).should == 0
9
+ @redises.exists(@key1, @key2).should == 0
10
+ end
11
+
12
+ it 'returns 1 for keys that do exist' do
13
+ @redises.set(@key1, 1)
14
+ @redises.exists(@key1).should == 1
15
+ end
16
+
17
+ it 'returns the count of all keys that exist' do
18
+ @redises.set(@key1, 1)
19
+ @redises.set(@key2, 1)
20
+ @redises.exists(@key1, @key2).should == 2
21
+ @redises.exists(@key1, @key2, 'does-not-exist').should == 2
22
+ end
23
+ end
24
+
25
+ describe '#exists?(*keys)' do
26
+ before { @key1 = 'mock-redis-test:exists1' }
27
+ before { @key2 = 'mock-redis-test:exists2' }
5
28
 
6
29
  it 'returns false for keys that do not exist' do
7
- @redises.exists(@key).should == false
30
+ @redises.exists?(@key1).should == false
31
+ @redises.exists?(@key1, @key2).should == false
8
32
  end
9
33
 
10
34
  it 'returns true for keys that do exist' do
11
- @redises.set(@key, 1)
12
- @redises.exists(@key).should == true
35
+ @redises.set(@key1, 1)
36
+ @redises.exists?(@key1).should == true
37
+ end
38
+
39
+ it 'returns true if any keys exist' do
40
+ @redises.set(@key2, 1)
41
+ @redises.exists?(@key1, @key2).should == true
13
42
  end
14
43
  end