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