mock_redis 0.19.0 → 0.20.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 (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