mock_redis 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/.travis.yml +9 -10
  4. data/CHANGELOG.md +15 -0
  5. data/Gemfile +2 -2
  6. data/README.md +2 -2
  7. data/lib/mock_redis/database.rb +6 -5
  8. data/lib/mock_redis/geospatial_methods.rb +10 -18
  9. data/lib/mock_redis/hash_methods.rb +4 -4
  10. data/lib/mock_redis/indifferent_hash.rb +0 -8
  11. data/lib/mock_redis/list_methods.rb +2 -2
  12. data/lib/mock_redis/pipelined_wrapper.rb +25 -6
  13. data/lib/mock_redis/set_methods.rb +15 -4
  14. data/lib/mock_redis/stream.rb +62 -0
  15. data/lib/mock_redis/stream/id.rb +53 -0
  16. data/lib/mock_redis/stream_methods.rb +87 -0
  17. data/lib/mock_redis/string_methods.rb +5 -8
  18. data/lib/mock_redis/transaction_wrapper.rb +25 -12
  19. data/lib/mock_redis/utility_methods.rb +1 -1
  20. data/lib/mock_redis/version.rb +1 -1
  21. data/lib/mock_redis/zset_methods.rb +2 -2
  22. data/mock_redis.gemspec +6 -5
  23. data/spec/commands/geodist_spec.rb +8 -4
  24. data/spec/commands/geohash_spec.rb +4 -4
  25. data/spec/commands/geopos_spec.rb +4 -4
  26. data/spec/commands/get_spec.rb +1 -0
  27. data/spec/commands/hdel_spec.rb +2 -2
  28. data/spec/commands/mget_spec.rb +34 -15
  29. data/spec/commands/mset_spec.rb +14 -0
  30. data/spec/commands/pipelined_spec.rb +52 -0
  31. data/spec/commands/spop_spec.rb +15 -0
  32. data/spec/commands/watch_spec.rb +8 -3
  33. data/spec/commands/xadd_spec.rb +102 -0
  34. data/spec/commands/xlen_spec.rb +20 -0
  35. data/spec/commands/xrange_spec.rb +141 -0
  36. data/spec/commands/xrevrange_spec.rb +130 -0
  37. data/spec/commands/xtrim_spec.rb +30 -0
  38. data/spec/spec_helper.rb +2 -0
  39. data/spec/support/redis_multiplexer.rb +17 -1
  40. data/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +14 -0
  41. data/spec/support/shared_examples/only_operates_on_hashes.rb +2 -0
  42. data/spec/support/shared_examples/only_operates_on_lists.rb +2 -0
  43. data/spec/support/shared_examples/only_operates_on_sets.rb +2 -0
  44. data/spec/support/shared_examples/only_operates_on_zsets.rb +2 -0
  45. data/spec/transactions_spec.rb +17 -29
  46. metadata +38 -12
  47. 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
@@ -201,6 +201,7 @@ 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)
@@ -231,10 +232,6 @@ class MockRedis
231
232
  return_true ? true : 'OK'
232
233
  end
233
234
 
234
- def []=(key, value, options = {})
235
- set(key, value, options)
236
- end
237
-
238
235
  def setbit(key, offset, value)
239
236
  assert_stringy(key, 'ERR bit is not an integer or out of range')
240
237
  retval = getbit(key, offset)
@@ -11,12 +11,12 @@ class MockRedis
11
11
  def initialize(db)
12
12
  @db = db
13
13
  @transaction_futures = []
14
- @in_multi = false
14
+ @multi_stack = []
15
15
  @multi_block_given = false
16
16
  end
17
17
 
18
18
  def method_missing(method, *args, &block)
19
- if @in_multi
19
+ if in_multi?
20
20
  future = MockRedis::Future.new([method, *args])
21
21
  @transaction_futures << future
22
22
 
@@ -35,23 +35,25 @@ 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 @in_multi
42
+ unless in_multi?
42
43
  raise Redis::CommandError, 'ERR DISCARD without MULTI'
43
44
  end
44
- @in_multi = false
45
- @multi_block_given = false
45
+ pop_multi
46
+
46
47
  @transaction_futures = []
47
48
  'OK'
48
49
  end
49
50
 
50
51
  def exec
51
- unless @in_multi
52
+ unless in_multi?
52
53
  raise Redis::CommandError, 'ERR EXEC without MULTI'
53
54
  end
54
- @in_multi = false
55
+ pop_multi
56
+ return if in_multi?
55
57
  @multi_block_given = false
56
58
 
57
59
  responses = @transaction_futures.map do |future|
@@ -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].empty?
21
+ if data[key]&.empty? && data[key] != ''
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.19.0'.freeze
5
+ VERSION = '0.20.0'
6
6
  end
@@ -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 && zadd_options.include?(:nx) && zadd_options.include?(:xx)
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
 
@@ -211,7 +211,7 @@ class MockRedis
211
211
  def zscore(key, member)
212
212
  with_zset_at(key) do |z|
213
213
  score = z.score(member.to_s)
214
- score.to_f if score
214
+ score&.to_f
215
215
  end
216
216
  end
217
217
 
@@ -6,9 +6,9 @@ Gem::Specification.new do |s|
6
6
  s.version = MockRedis::VERSION
7
7
  s.license = 'MIT'
8
8
  s.platform = Gem::Platform::RUBY
9
- s.authors = ['Brigade Engineering', 'Samuel Merritt']
10
- s.email = ['eng@brigade.com']
11
- s.homepage = 'https://github.com/brigade/mock_redis'
9
+ s.authors = ['Shane da Silva', 'Samuel Merritt']
10
+ s.email = ['shane@dasilva.io']
11
+ s.homepage = 'https://github.com/sds/mock_redis'
12
12
  s.summary = 'Redis mock that just lives in memory; useful for testing.'
13
13
 
14
14
  s.description = <<-MSG.strip.gsub(/\s+/, ' ')
@@ -21,10 +21,11 @@ Gem::Specification.new do |s|
21
21
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
22
22
  s.require_paths = ['lib']
23
23
 
24
- s.required_ruby_version = '>= 2.2'
24
+ s.required_ruby_version = '>= 2.4'
25
25
 
26
26
  s.add_development_dependency 'rake', '>= 10', '< 12'
27
- s.add_development_dependency 'redis', '~> 3.3.0'
27
+ s.add_development_dependency 'redis', '~>4.1.0'
28
28
  s.add_development_dependency 'rspec', '~> 3.0'
29
29
  s.add_development_dependency 'rspec-its', '~> 1.0'
30
+ s.add_development_dependency 'timecop', '~> 0.9.1'
30
31
  end
@@ -80,13 +80,14 @@ describe '#geodist' do
80
80
 
81
81
  context 'with less than 3 arguments' do
82
82
  [1, 2].each do |count|
83
- let(:message) { "ERR wrong number of arguments for 'geodist' command" }
84
-
85
83
  context "with #{count} arguments" do
86
84
  it 'raises an error' do
87
85
  args = list.slice(0, count)
88
86
  expect { @redises.geodist(*args) }
89
- .to raise_error(Redis::CommandError, message)
87
+ .to raise_error(
88
+ ArgumentError,
89
+ /wrong number of arguments \((given\s)?#{count}(,\sexpected\s|\sfor\s)3?\.\.4\)$/
90
+ )
90
91
  end
91
92
  end
92
93
  end
@@ -98,7 +99,10 @@ describe '#geodist' do
98
99
  it 'raises an error' do
99
100
  args = list.slice(0, 5)
100
101
  expect { @redises.geodist(*args) }
101
- .to raise_error(Redis::CommandError, message)
102
+ .to raise_error(
103
+ ArgumentError,
104
+ /wrong number of arguments \((given\s)?5(,\sexpected\s|\sfor\s)3?\.\.4\)$/
105
+ )
102
106
  end
103
107
  end
104
108
  end
@@ -17,13 +17,13 @@ describe '#geohash' do
17
17
  end
18
18
 
19
19
  it 'returns decoded coordinates pairs for each point' do
20
- results = @redises.geohash(key, 'SF', 'LA')
20
+ results = @redises.geohash(key, %w[SF LA])
21
21
  expect(results).to be == expected_result
22
22
  end
23
23
 
24
24
  context 'with non-existing points only' do
25
25
  it 'returns array filled with nils' do
26
- results = @redises.geohash(key, 'FF', 'FA')
26
+ results = @redises.geohash(key, %w[FF FA])
27
27
  expect(results).to be == [nil, nil]
28
28
  end
29
29
  end
@@ -34,7 +34,7 @@ describe '#geohash' do
34
34
  end
35
35
 
36
36
  it 'returns mixture of nil and coordinates pair' do
37
- results = @redises.geohash(key, 'SF', 'FA')
37
+ results = @redises.geohash(key, %w[SF FA])
38
38
  expect(results).to be == expected_result
39
39
  end
40
40
  end
@@ -45,7 +45,7 @@ describe '#geohash' do
45
45
  before { @redises.del(key) }
46
46
 
47
47
  it 'returns empty array' do
48
- results = @redises.geohash(key, 'SF', 'LA')
48
+ results = @redises.geohash(key, %w[SF LA])
49
49
  expect(results).to be == [nil, nil]
50
50
  end
51
51
  end
@@ -20,13 +20,13 @@ describe '#geopos' do
20
20
  end
21
21
 
22
22
  it 'returns decoded coordinates pairs for each point' do
23
- coords = @redises.geopos(key, 'SF', 'LA')
23
+ coords = @redises.geopos(key, %w[SF LA])
24
24
  expect(coords).to be == expected_result
25
25
  end
26
26
 
27
27
  context 'with non-existing points only' do
28
28
  it 'returns array filled with nils' do
29
- coords = @redises.geopos(key, 'FF', 'FA')
29
+ coords = @redises.geopos(key, %w[FF FA])
30
30
  expect(coords).to be == [nil, nil]
31
31
  end
32
32
  end
@@ -37,7 +37,7 @@ describe '#geopos' do
37
37
  end
38
38
 
39
39
  it 'returns mixture of nil and coordinates pair' do
40
- coords = @redises.geopos(key, 'SF', 'FA')
40
+ coords = @redises.geopos(key, %w[SF FA])
41
41
  expect(coords).to be == expected_result
42
42
  end
43
43
  end
@@ -48,7 +48,7 @@ describe '#geopos' do
48
48
  before { @redises.del(key) }
49
49
 
50
50
  it 'returns empty array' do
51
- coords = @redises.geopos(key, 'SF', 'LA')
51
+ coords = @redises.geopos(key, %w[SF LA])
52
52
  expect(coords).to be == [nil, nil]
53
53
  end
54
54
  end
@@ -24,6 +24,7 @@ describe '#get(key)' do
24
24
 
25
25
  @redises.set(key, 'hello')
26
26
  @redises.get(key.to_s).should == 'hello'
27
+ @redises.get(key).should == 'hello'
27
28
  end
28
29
 
29
30
  it_should_behave_like 'a string-only command'
@@ -39,14 +39,14 @@ describe '#hdel(key, field)' do
39
39
  end
40
40
 
41
41
  it 'supports a variable number of arguments' do
42
- @redises.hdel(@key, %w[k1 k2])
42
+ @redises.hdel(@key, 'k1', 'k2')
43
43
  @redises.get(@key).should be_nil
44
44
  end
45
45
 
46
46
  it 'treats variable arguments as strings' do
47
47
  field = 2
48
48
  @redises.hset(@key, field, 'two')
49
- @redises.hdel(@key, [field])
49
+ @redises.hdel(@key, field)
50
50
  @redises.hget(@key, field).should be_nil
51
51
  end
52
52
 
@@ -9,26 +9,45 @@ describe '#mget(key [, key, ...])' do
9
9
  @redises.set(@key2, 2)
10
10
  end
11
11
 
12
- it 'returns an array of values' do
13
- @redises.mget(@key1, @key2).should == %w[1 2]
14
- end
12
+ context 'emulate param array' do
13
+ it 'returns an array of values' do
14
+ @redises.mget([@key1, @key2]).should == %w[1 2]
15
+ end
15
16
 
16
- it 'returns nil for missing keys' do
17
- @redises.mget(@key1, 'mock-redis-test:not-found', @key2).
18
- should == ['1', nil, '2']
19
- end
17
+ it 'returns an array of values' do
18
+ @redises.mget([@key1, @key2]).should == %w[1 2]
19
+ end
20
20
 
21
- it 'returns nil for non-string keys' do
22
- list = 'mock-redis-test:mget-list'
21
+ it 'returns nil for non-string keys' do
22
+ list = 'mock-redis-test:mget-list'
23
23
 
24
- @redises.lpush(list, 'bork bork bork')
24
+ @redises.lpush(list, 'bork bork bork')
25
25
 
26
- @redises.mget(@key1, @key2, list).should == ['1', '2', nil]
26
+ @redises.mget([@key1, @key2, list]).should == ['1', '2', nil]
27
+ end
27
28
  end
28
29
 
29
- it 'raises an error if you pass it 0 arguments' do
30
- lambda do
31
- @redises.mget
32
- end.should raise_error(Redis::CommandError)
30
+ context 'emulate params strings' do
31
+ it 'returns an array of values' do
32
+ @redises.mget(@key1, @key2).should == %w[1 2]
33
+ end
34
+
35
+ it 'returns nil for missing keys' do
36
+ @redises.mget(@key1, 'mock-redis-test:not-found', @key2).should == ['1', nil, '2']
37
+ end
38
+
39
+ it 'returns nil for non-string keys' do
40
+ list = 'mock-redis-test:mget-list'
41
+
42
+ @redises.lpush(list, 'bork bork bork')
43
+
44
+ @redises.mget(@key1, @key2, list).should == ['1', '2', nil]
45
+ end
46
+
47
+ it 'raises an error if you pass it 0 arguments' do
48
+ lambda do
49
+ @redises.mget
50
+ end.should raise_error(Redis::CommandError)
51
+ end
33
52
  end
34
53
  end