mock_redis 0.19.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +32 -5
  3. data/.rubocop_todo.yml +1 -1
  4. data/.travis.yml +9 -10
  5. data/CHANGELOG.md +46 -0
  6. data/Gemfile +2 -2
  7. data/LICENSE.md +21 -0
  8. data/README.md +39 -15
  9. data/lib/mock_redis.rb +0 -5
  10. data/lib/mock_redis/database.rb +59 -22
  11. data/lib/mock_redis/future.rb +1 -1
  12. data/lib/mock_redis/geospatial_methods.rb +14 -22
  13. data/lib/mock_redis/hash_methods.rb +23 -15
  14. data/lib/mock_redis/indifferent_hash.rb +0 -8
  15. data/lib/mock_redis/info_method.rb +2 -2
  16. data/lib/mock_redis/list_methods.rb +2 -2
  17. data/lib/mock_redis/multi_db_wrapper.rb +2 -2
  18. data/lib/mock_redis/pipelined_wrapper.rb +25 -6
  19. data/lib/mock_redis/set_methods.rb +16 -4
  20. data/lib/mock_redis/stream.rb +62 -0
  21. data/lib/mock_redis/stream/id.rb +60 -0
  22. data/lib/mock_redis/stream_methods.rb +87 -0
  23. data/lib/mock_redis/string_methods.rb +31 -20
  24. data/lib/mock_redis/transaction_wrapper.rb +27 -14
  25. data/lib/mock_redis/utility_methods.rb +6 -3
  26. data/lib/mock_redis/version.rb +1 -1
  27. data/lib/mock_redis/zset_methods.rb +54 -11
  28. data/mock_redis.gemspec +6 -6
  29. data/spec/client_spec.rb +12 -0
  30. data/spec/commands/blpop_spec.rb +0 -6
  31. data/spec/commands/brpop_spec.rb +6 -5
  32. data/spec/commands/dump_spec.rb +19 -0
  33. data/spec/commands/exists_spec.rb +34 -5
  34. data/spec/commands/future_spec.rb +11 -1
  35. data/spec/commands/geoadd_spec.rb +1 -1
  36. data/spec/commands/geodist_spec.rb +8 -4
  37. data/spec/commands/geohash_spec.rb +4 -4
  38. data/spec/commands/geopos_spec.rb +4 -4
  39. data/spec/commands/get_spec.rb +1 -0
  40. data/spec/commands/hdel_spec.rb +18 -2
  41. data/spec/commands/hmset_spec.rb +26 -0
  42. data/spec/commands/hset_spec.rb +6 -6
  43. data/spec/commands/keys_spec.rb +17 -0
  44. data/spec/commands/mget_spec.rb +34 -15
  45. data/spec/commands/move_spec.rb +5 -5
  46. data/spec/commands/mset_spec.rb +14 -0
  47. data/spec/commands/pipelined_spec.rb +72 -0
  48. data/spec/commands/restore_spec.rb +47 -0
  49. data/spec/commands/scan_spec.rb +9 -0
  50. data/spec/commands/set_spec.rb +12 -2
  51. data/spec/commands/setbit_spec.rb +1 -0
  52. data/spec/commands/setex_spec.rb +16 -0
  53. data/spec/commands/spop_spec.rb +15 -0
  54. data/spec/commands/srandmember_spec.rb +1 -1
  55. data/spec/commands/srem_spec.rb +5 -0
  56. data/spec/commands/watch_spec.rb +8 -3
  57. data/spec/commands/xadd_spec.rb +104 -0
  58. data/spec/commands/xlen_spec.rb +20 -0
  59. data/spec/commands/xrange_spec.rb +141 -0
  60. data/spec/commands/xrevrange_spec.rb +130 -0
  61. data/spec/commands/xtrim_spec.rb +30 -0
  62. data/spec/commands/zinterstore_spec.rb +34 -0
  63. data/spec/commands/zpopmax_spec.rb +60 -0
  64. data/spec/commands/zpopmin_spec.rb +60 -0
  65. data/spec/commands/zrange_spec.rb +1 -1
  66. data/spec/commands/zrangebyscore_spec.rb +1 -1
  67. data/spec/commands/zrevrange_spec.rb +1 -1
  68. data/spec/commands/zrevrangebyscore_spec.rb +1 -1
  69. data/spec/commands/zunionstore_spec.rb +33 -0
  70. data/spec/mock_redis_spec.rb +4 -6
  71. data/spec/spec_helper.rb +6 -2
  72. data/spec/support/redis_multiplexer.rb +18 -1
  73. data/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +14 -0
  74. data/spec/support/shared_examples/only_operates_on_hashes.rb +2 -0
  75. data/spec/support/shared_examples/only_operates_on_lists.rb +2 -0
  76. data/spec/support/shared_examples/only_operates_on_sets.rb +2 -0
  77. data/spec/support/shared_examples/only_operates_on_zsets.rb +2 -0
  78. data/spec/transactions_spec.rb +30 -26
  79. metadata +45 -31
  80. data/LICENSE +0 -19
  81. data/spec/commands/hash_operator_spec.rb +0 -21
@@ -17,7 +17,7 @@ class MockRedis
17
17
 
18
18
  def store_result(result)
19
19
  @result_set = true
20
- @result = result
20
+ @result = @block ? @block.call(result) : result
21
21
  end
22
22
  end
23
23
  end
@@ -1,7 +1,7 @@
1
1
  class MockRedis
2
2
  module GeospatialMethods
3
- LNG_RANGE = (-180..180)
4
- LAT_RANGE = (-85.05112878..85.05112878)
3
+ LNG_RANGE = (-180..180).freeze
4
+ LAT_RANGE = (-85.05112878..85.05112878).freeze
5
5
  STEP = 26
6
6
  UNITS = {
7
7
  m: 1,
@@ -23,21 +23,13 @@ class MockRedis
23
23
  zadd(key, scored_points)
24
24
  end
25
25
 
26
- def geodist(key, *args)
27
- if args.length < 2
28
- raise Redis::CommandError,
29
- "ERR wrong number of arguments for 'geodist' command"
30
- end
31
-
32
- raise Redis::CommandError, 'ERR syntax error' if args.length > 3
33
-
34
- to_meter = 1
35
- to_meter = parse_unit(args[2]) if args.length == 3
26
+ def geodist(key, member1, member2, unit = 'm')
27
+ to_meter = parse_unit(unit)
36
28
 
37
29
  return nil if zcard(key).zero?
38
30
 
39
- score1 = zscore(key, args[0])
40
- score2 = zscore(key, args[1])
31
+ score1 = zscore(key, member1)
32
+ score2 = zscore(key, member2)
41
33
  return nil if score1.nil? || score2.nil?
42
34
  hash1 = { bits: score1.to_i, step: STEP }
43
35
  hash2 = { bits: score2.to_i, step: STEP }
@@ -46,15 +38,15 @@ class MockRedis
46
38
  lng2, lat2 = geohash_decode(hash2)
47
39
 
48
40
  distance = geohash_distance(lng1, lat1, lng2, lat2) / to_meter
49
- format('%.4f', distance)
41
+ format('%<distance>.4f', distance: distance)
50
42
  end
51
43
 
52
- def geohash(key, *members)
44
+ def geohash(key, members)
53
45
  lng_range = (-180..180)
54
46
  lat_range = (-90..90)
55
47
  geoalphabet = '0123456789bcdefghjkmnpqrstuvwxyz'
56
48
 
57
- members.map do |member|
49
+ Array(members).map do |member|
58
50
  score = zscore(key, member)
59
51
  next nil unless score
60
52
  score = score.to_i
@@ -71,8 +63,8 @@ class MockRedis
71
63
  end
72
64
  end
73
65
 
74
- def geopos(key, *members)
75
- members.map do |member|
66
+ def geopos(key, members)
67
+ Array(members).map do |member|
76
68
  score = zscore(key, member)
77
69
  next nil unless score
78
70
  hash = { bits: score.to_i, step: STEP }
@@ -103,8 +95,8 @@ class MockRedis
103
95
  lat = Float(point[1])
104
96
 
105
97
  unless LNG_RANGE.include?(lng) && LAT_RANGE.include?(lat)
106
- lng = format('%.6f', lng)
107
- lat = format('%.6f', lat)
98
+ lng = format('%<long>.6f', long: lng)
99
+ lat = format('%<lat>.6f', lat: lat)
108
100
  raise Redis::CommandError,
109
101
  "ERR invalid longitude,latitude pair #{lng},#{lat}"
110
102
  end
@@ -209,7 +201,7 @@ class MockRedis
209
201
  end
210
202
 
211
203
  def format_decoded_coord(coord)
212
- coord = format('%.17f', coord)
204
+ coord = format('%<coord>.17f', coord: coord)
213
205
  l = 1
214
206
  l += 1 while coord[-l] == '0'
215
207
  coord = coord[0..-l]
@@ -6,16 +6,12 @@ class MockRedis
6
6
  include Assertions
7
7
  include UtilityMethods
8
8
 
9
- def hdel(key, field)
9
+ def hdel(key, *fields)
10
10
  with_hash_at(key) do |hash|
11
- if field.is_a?(Array)
12
- orig_size = hash.size
13
- fields = field.map(&:to_s)
14
- hash.delete_if { |k, _v| fields.include?(k) }
15
- orig_size - hash.size
16
- else
17
- hash.delete(field.to_s) ? 1 : 0
18
- end
11
+ orig_size = hash.size
12
+ fields = Array(fields).flatten.map(&:to_s)
13
+ hash.delete_if { |k, _v| fields.include?(k) }
14
+ orig_size - hash.size
19
15
  end
20
16
  end
21
17
 
@@ -88,10 +84,17 @@ class MockRedis
88
84
  end
89
85
 
90
86
  def hmset(key, *kvpairs)
87
+ if key.is_a? Array
88
+ err_msg = 'ERR wrong number of arguments for \'hmset\' command'
89
+ kvpairs = key[1..-1]
90
+ key = key[0]
91
+ end
92
+
91
93
  kvpairs.flatten!
92
94
  assert_has_args(kvpairs, 'hmset')
95
+
93
96
  if kvpairs.length.odd?
94
- raise Redis::CommandError, 'ERR wrong number of arguments for HMSET'
97
+ raise Redis::CommandError, err_msg || 'ERR wrong number of arguments for HMSET'
95
98
  end
96
99
 
97
100
  kvpairs.each_slice(2) do |(k, v)|
@@ -125,10 +128,15 @@ class MockRedis
125
128
  end
126
129
  end
127
130
 
128
- def hset(key, field, value)
129
- field_exists = hexists(key, field)
130
- with_hash_at(key) { |h| h[field.to_s] = value.to_s }
131
- !field_exists
131
+ def hset(key, *args)
132
+ added = 0
133
+ with_hash_at(key) do |hash|
134
+ args.each_slice(2) do |field, value|
135
+ added += 1 unless hash.key?(field.to_s)
136
+ hash[field.to_s] = value.to_s
137
+ end
138
+ end
139
+ added
132
140
  end
133
141
 
134
142
  def hsetnx(key, field, value)
@@ -147,7 +155,7 @@ class MockRedis
147
155
  private
148
156
 
149
157
  def with_hash_at(key, &blk)
150
- with_thing_at(key, :assert_hashy, proc { {} }, &blk)
158
+ with_thing_at(key.to_s, :assert_hashy, proc { {} }, &blk)
151
159
  end
152
160
 
153
161
  def hashy?(key)
@@ -1,13 +1,5 @@
1
1
  class MockRedis
2
2
  class IndifferentHash < Hash
3
- def [](key)
4
- super(key.to_s)
5
- end
6
-
7
- def []=(key, value)
8
- super(key.to_s, value)
9
- end
10
-
11
3
  def has_key?(key)
12
4
  super(key.to_s)
13
5
  end
@@ -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,
@@ -146,13 +146,13 @@ class MockRedis
146
146
 
147
147
  def ltrim(key, start, stop)
148
148
  with_list_at(key) do |list|
149
- list.replace(list[[start.to_i, -list.length].max..stop.to_i] || []) if list
149
+ list&.replace(list[[start.to_i, -list.length].max..stop.to_i] || [])
150
150
  'OK'
151
151
  end
152
152
  end
153
153
 
154
154
  def rpop(key)
155
- with_list_at(key) { |list| list.pop if list }
155
+ with_list_at(key) { |list| list&.pop }
156
156
  end
157
157
 
158
158
  def rpoplpush(source, destination)
@@ -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)
@@ -9,7 +9,7 @@ class MockRedis
9
9
  def initialize(db)
10
10
  @db = db
11
11
  @pipelined_futures = []
12
- @in_pipeline = false
12
+ @nesting_level = 0
13
13
  end
14
14
 
15
15
  def initialize_copy(source)
@@ -19,7 +19,7 @@ class MockRedis
19
19
  end
20
20
 
21
21
  def method_missing(method, *args, &block)
22
- if @in_pipeline
22
+ if in_pipeline?
23
23
  future = MockRedis::Future.new([method, *args], block)
24
24
  @pipelined_futures << future
25
25
  future
@@ -29,9 +29,17 @@ class MockRedis
29
29
  end
30
30
 
31
31
  def pipelined(_options = {})
32
- @in_pipeline = true
33
- yield self
34
- @in_pipeline = false
32
+ begin
33
+ @nesting_level += 1
34
+ yield self
35
+ ensure
36
+ @nesting_level -= 1
37
+ end
38
+
39
+ if in_pipeline?
40
+ return
41
+ end
42
+
35
43
  responses = @pipelined_futures.flat_map do |future|
36
44
  begin
37
45
  result = if future.block
@@ -40,7 +48,12 @@ class MockRedis
40
48
  send(*future.command)
41
49
  end
42
50
  future.store_result(result)
43
- result
51
+
52
+ if future.block
53
+ result
54
+ else
55
+ [result]
56
+ end
44
57
  rescue StandardError => e
45
58
  e
46
59
  end
@@ -48,5 +61,11 @@ class MockRedis
48
61
  @pipelined_futures = []
49
62
  responses
50
63
  end
64
+
65
+ private
66
+
67
+ def in_pipeline?
68
+ @nesting_level > 0
69
+ end
51
70
  end
52
71
  end
@@ -81,11 +81,22 @@ class MockRedis
81
81
  end
82
82
  end
83
83
 
84
- def spop(key)
84
+ def spop(key, count = nil)
85
85
  with_set_at(key) do |set|
86
- member = set.first
87
- set.delete(member)
88
- member
86
+ if count.nil?
87
+ member = set.first
88
+ set.delete(member)
89
+ member
90
+ else
91
+ members = []
92
+ count.times do
93
+ member = set.first
94
+ break if member.nil?
95
+ set.delete(member)
96
+ members << member
97
+ end
98
+ members
99
+ end
89
100
  end
90
101
  end
91
102
 
@@ -106,6 +117,7 @@ class MockRedis
106
117
  with_set_at(key) do |s|
107
118
  if members.is_a?(Array)
108
119
  orig_size = s.size
120
+ members = members.map(&:to_s)
109
121
  s.delete_if { |m| members.include?(m) }
110
122
  orig_size - s.size
111
123
  else
@@ -0,0 +1,62 @@
1
+ require 'forwardable'
2
+ require 'set'
3
+ require 'date'
4
+ require 'mock_redis/stream/id'
5
+
6
+ class MockRedis
7
+ class Stream
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ attr_accessor :members
12
+
13
+ def_delegators :members, :empty?
14
+
15
+ def initialize
16
+ @members = Set.new
17
+ @last_id = nil
18
+ end
19
+
20
+ def last_id
21
+ @last_id.to_s
22
+ end
23
+
24
+ def add(id, values)
25
+ @last_id = MockRedis::Stream::Id.new(id, min: @last_id)
26
+ members.add [@last_id, Hash[values.map { |k, v| [k.to_s, v.to_s] }]]
27
+ @last_id.to_s
28
+ end
29
+
30
+ def trim(count)
31
+ deleted = @members.size - count
32
+ @members = @members.to_a[-count..-1].to_set
33
+ deleted
34
+ end
35
+
36
+ def range(start, finish, reversed, *opts_in)
37
+ opts = options opts_in, ['count']
38
+ start_id = MockRedis::Stream::Id.new(start)
39
+ finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY)
40
+ items = members
41
+ .select { |m| (start_id <= m[0]) && (finish_id >= m[0]) }
42
+ .map { |m| [m[0].to_s, m[1]] }
43
+ items.reverse! if reversed
44
+ return items.first(opts['count'].to_i) if opts.key?('count')
45
+ items
46
+ end
47
+
48
+ def each
49
+ members.each { |m| yield m }
50
+ end
51
+
52
+ private
53
+
54
+ def options(opts_in, permitted)
55
+ opts_out = {}
56
+ raise Redis::CommandError, 'ERR syntax error' unless (opts_in.length % 2).zero?
57
+ opts_in.each_slice(2).map { |pair| opts_out[pair[0].downcase] = pair[1] }
58
+ raise Redis::CommandError, 'ERR syntax error' unless (opts_out.keys - permitted).empty?
59
+ opts_out
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,60 @@
1
+ class MockRedis
2
+ class Stream
3
+ class Id
4
+ include Comparable
5
+
6
+ attr_accessor :timestamp, :sequence
7
+
8
+ def initialize(id, min: nil, sequence: 0)
9
+ case id
10
+ when '*'
11
+ @timestamp = (Time.now.to_f * 1000).to_i
12
+ @sequence = 0
13
+ if self <= min
14
+ @timestamp = min.timestamp
15
+ @sequence = min.sequence + 1
16
+ end
17
+ when '-'
18
+ @timestamp = @sequence = 0
19
+ when '+'
20
+ @timestamp = @sequence = Float::INFINITY
21
+ else
22
+ if id.is_a? String
23
+ (_, @timestamp, @sequence) = id.match(/^(\d+)-?(\d+)?$/)
24
+ .to_a
25
+ if @timestamp.nil?
26
+ raise Redis::CommandError,
27
+ 'ERR Invalid stream ID specified as stream command argument'
28
+ end
29
+ @timestamp = @timestamp.to_i
30
+ else
31
+ @timestamp = id
32
+ end
33
+ @sequence = @sequence.nil? ? sequence : @sequence.to_i
34
+ if @timestamp == 0 && @sequence == 0
35
+ raise Redis::CommandError,
36
+ 'ERR The ID specified in XADD is equal or smaller than ' \
37
+ 'the target stream top item'
38
+ # TOOD: Redis version 6.0.4, w redis 4.2.1 generates the following error message:
39
+ # 'ERR The ID specified in XADD must be greater than 0-0'
40
+ end
41
+ if self <= min
42
+ raise Redis::CommandError,
43
+ 'ERR The ID specified in XADD is equal or smaller than ' \
44
+ 'the target stream top item'
45
+ end
46
+ end
47
+ end
48
+
49
+ def to_s
50
+ "#{@timestamp}-#{@sequence}"
51
+ end
52
+
53
+ def <=>(other)
54
+ return 1 if other.nil?
55
+ return @sequence <=> other.sequence if @timestamp == other.timestamp
56
+ @timestamp <=> other.timestamp
57
+ end
58
+ end
59
+ end
60
+ end