mock_redis 0.19.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +161 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +41 -17
  5. data/lib/mock_redis/connection_method.rb +13 -0
  6. data/lib/mock_redis/database.rb +116 -41
  7. data/lib/mock_redis/expire_wrapper.rb +1 -1
  8. data/lib/mock_redis/future.rb +1 -1
  9. data/lib/mock_redis/geospatial_methods.rb +13 -21
  10. data/lib/mock_redis/hash_methods.rb +34 -15
  11. data/lib/mock_redis/indifferent_hash.rb +0 -8
  12. data/lib/mock_redis/info_method.rb +2 -2
  13. data/lib/mock_redis/list_methods.rb +39 -4
  14. data/lib/mock_redis/memory_method.rb +11 -0
  15. data/lib/mock_redis/multi_db_wrapper.rb +4 -4
  16. data/lib/mock_redis/pipelined_wrapper.rb +32 -15
  17. data/lib/mock_redis/set_methods.rb +34 -6
  18. data/lib/mock_redis/stream/id.rb +58 -0
  19. data/lib/mock_redis/stream.rb +88 -0
  20. data/lib/mock_redis/stream_methods.rb +102 -0
  21. data/lib/mock_redis/string_methods.rb +81 -30
  22. data/lib/mock_redis/transaction_wrapper.rb +32 -21
  23. data/lib/mock_redis/utility_methods.rb +9 -4
  24. data/lib/mock_redis/version.rb +1 -1
  25. data/lib/mock_redis/zset.rb +5 -8
  26. data/lib/mock_redis/zset_methods.rb +64 -12
  27. data/lib/mock_redis.rb +38 -10
  28. metadata +40 -336
  29. data/.gitignore +0 -5
  30. data/.mailmap +0 -2
  31. data/.overcommit.yml +0 -21
  32. data/.rspec +0 -1
  33. data/.rubocop.yml +0 -121
  34. data/.rubocop_todo.yml +0 -35
  35. data/.simplecov +0 -4
  36. data/.travis.yml +0 -33
  37. data/Gemfile +0 -12
  38. data/LICENSE +0 -19
  39. data/Rakefile +0 -2
  40. data/mock_redis.gemspec +0 -30
  41. data/spec/client_spec.rb +0 -17
  42. data/spec/cloning_spec.rb +0 -95
  43. data/spec/commands/append_spec.rb +0 -24
  44. data/spec/commands/auth_spec.rb +0 -7
  45. data/spec/commands/bgrewriteaof_spec.rb +0 -7
  46. data/spec/commands/bgsave_spec.rb +0 -7
  47. data/spec/commands/bitcount_spec.rb +0 -25
  48. data/spec/commands/bitfield_spec.rb +0 -169
  49. data/spec/commands/blpop_spec.rb +0 -59
  50. data/spec/commands/brpop_spec.rb +0 -58
  51. data/spec/commands/brpoplpush_spec.rb +0 -52
  52. data/spec/commands/connected_spec.rb +0 -7
  53. data/spec/commands/dbsize_spec.rb +0 -18
  54. data/spec/commands/decr_spec.rb +0 -34
  55. data/spec/commands/decrby_spec.rb +0 -34
  56. data/spec/commands/del_spec.rb +0 -35
  57. data/spec/commands/disconnect_spec.rb +0 -7
  58. data/spec/commands/echo_spec.rb +0 -11
  59. data/spec/commands/eval_spec.rb +0 -7
  60. data/spec/commands/evalsha_spec.rb +0 -10
  61. data/spec/commands/exists_spec.rb +0 -14
  62. data/spec/commands/expire_spec.rb +0 -111
  63. data/spec/commands/expireat_spec.rb +0 -47
  64. data/spec/commands/flushall_spec.rb +0 -38
  65. data/spec/commands/flushdb_spec.rb +0 -38
  66. data/spec/commands/future_spec.rb +0 -20
  67. data/spec/commands/geoadd_spec.rb +0 -58
  68. data/spec/commands/geodist_spec.rb +0 -114
  69. data/spec/commands/geohash_spec.rb +0 -52
  70. data/spec/commands/geopos_spec.rb +0 -55
  71. data/spec/commands/get_spec.rb +0 -30
  72. data/spec/commands/getbit_spec.rb +0 -34
  73. data/spec/commands/getrange_spec.rb +0 -22
  74. data/spec/commands/getset_spec.rb +0 -23
  75. data/spec/commands/hash_operator_spec.rb +0 -21
  76. data/spec/commands/hdel_spec.rb +0 -54
  77. data/spec/commands/hexists_spec.rb +0 -27
  78. data/spec/commands/hget_spec.rb +0 -28
  79. data/spec/commands/hgetall_spec.rb +0 -32
  80. data/spec/commands/hincrby_spec.rb +0 -58
  81. data/spec/commands/hincrbyfloat_spec.rb +0 -58
  82. data/spec/commands/hkeys_spec.rb +0 -19
  83. data/spec/commands/hlen_spec.rb +0 -19
  84. data/spec/commands/hmget_spec.rb +0 -40
  85. data/spec/commands/hmset_spec.rb +0 -43
  86. data/spec/commands/hscan_each_spec.rb +0 -48
  87. data/spec/commands/hscan_spec.rb +0 -27
  88. data/spec/commands/hset_spec.rb +0 -38
  89. data/spec/commands/hsetnx_spec.rb +0 -44
  90. data/spec/commands/hvals_spec.rb +0 -19
  91. data/spec/commands/incr_spec.rb +0 -34
  92. data/spec/commands/incrby_spec.rb +0 -44
  93. data/spec/commands/incrbyfloat_spec.rb +0 -44
  94. data/spec/commands/info_spec.rb +0 -62
  95. data/spec/commands/keys_spec.rb +0 -122
  96. data/spec/commands/lastsave_spec.rb +0 -8
  97. data/spec/commands/lindex_spec.rb +0 -49
  98. data/spec/commands/linsert_spec.rb +0 -68
  99. data/spec/commands/llen_spec.rb +0 -16
  100. data/spec/commands/lpop_spec.rb +0 -34
  101. data/spec/commands/lpush_spec.rb +0 -43
  102. data/spec/commands/lpushx_spec.rb +0 -46
  103. data/spec/commands/lrange_spec.rb +0 -51
  104. data/spec/commands/lrem_spec.rb +0 -80
  105. data/spec/commands/lset_spec.rb +0 -43
  106. data/spec/commands/ltrim_spec.rb +0 -45
  107. data/spec/commands/mapped_hmget_spec.rb +0 -29
  108. data/spec/commands/mapped_hmset_spec.rb +0 -47
  109. data/spec/commands/mapped_mget_spec.rb +0 -22
  110. data/spec/commands/mapped_mset_spec.rb +0 -19
  111. data/spec/commands/mapped_msetnx_spec.rb +0 -26
  112. data/spec/commands/mget_spec.rb +0 -34
  113. data/spec/commands/move_spec.rb +0 -147
  114. data/spec/commands/mset_spec.rb +0 -29
  115. data/spec/commands/msetnx_spec.rb +0 -40
  116. data/spec/commands/persist_spec.rb +0 -48
  117. data/spec/commands/pexpire_spec.rb +0 -86
  118. data/spec/commands/pexpireat_spec.rb +0 -48
  119. data/spec/commands/ping_spec.rb +0 -7
  120. data/spec/commands/pipelined_spec.rb +0 -42
  121. data/spec/commands/pttl_spec.rb +0 -41
  122. data/spec/commands/quit_spec.rb +0 -7
  123. data/spec/commands/randomkey_spec.rb +0 -20
  124. data/spec/commands/rename_spec.rb +0 -42
  125. data/spec/commands/renamenx_spec.rb +0 -41
  126. data/spec/commands/rpop_spec.rb +0 -34
  127. data/spec/commands/rpoplpush_spec.rb +0 -50
  128. data/spec/commands/rpush_spec.rb +0 -43
  129. data/spec/commands/rpushx_spec.rb +0 -46
  130. data/spec/commands/sadd_spec.rb +0 -45
  131. data/spec/commands/save_spec.rb +0 -7
  132. data/spec/commands/scan_each_spec.rb +0 -39
  133. data/spec/commands/scan_spec.rb +0 -55
  134. data/spec/commands/scard_spec.rb +0 -18
  135. data/spec/commands/script_spec.rb +0 -9
  136. data/spec/commands/sdiff_spec.rb +0 -47
  137. data/spec/commands/sdiffstore_spec.rb +0 -58
  138. data/spec/commands/select_spec.rb +0 -61
  139. data/spec/commands/set_spec.rb +0 -63
  140. data/spec/commands/setbit_spec.rb +0 -54
  141. data/spec/commands/setex_spec.rb +0 -22
  142. data/spec/commands/setnx_spec.rb +0 -25
  143. data/spec/commands/setrange_spec.rb +0 -30
  144. data/spec/commands/sinter_spec.rb +0 -39
  145. data/spec/commands/sinterstore_spec.rb +0 -53
  146. data/spec/commands/sismember_spec.rb +0 -29
  147. data/spec/commands/smembers_spec.rb +0 -28
  148. data/spec/commands/smove_spec.rb +0 -41
  149. data/spec/commands/sort_list_spec.rb +0 -21
  150. data/spec/commands/sort_set_spec.rb +0 -21
  151. data/spec/commands/sort_zset_spec.rb +0 -21
  152. data/spec/commands/spop_spec.rb +0 -25
  153. data/spec/commands/srandmember_spec.rb +0 -49
  154. data/spec/commands/srem_spec.rb +0 -40
  155. data/spec/commands/sscan_each_spec.rb +0 -48
  156. data/spec/commands/sscan_spec.rb +0 -39
  157. data/spec/commands/strlen_spec.rb +0 -18
  158. data/spec/commands/sunion_spec.rb +0 -42
  159. data/spec/commands/sunionstore_spec.rb +0 -59
  160. data/spec/commands/ttl_spec.rb +0 -40
  161. data/spec/commands/type_spec.rb +0 -36
  162. data/spec/commands/unwatch_spec.rb +0 -7
  163. data/spec/commands/watch_spec.rb +0 -16
  164. data/spec/commands/zadd_spec.rb +0 -123
  165. data/spec/commands/zcard_spec.rb +0 -19
  166. data/spec/commands/zcount_spec.rb +0 -39
  167. data/spec/commands/zincrby_spec.rb +0 -31
  168. data/spec/commands/zinterstore_spec.rb +0 -96
  169. data/spec/commands/zrange_spec.rb +0 -80
  170. data/spec/commands/zrangebyscore_spec.rb +0 -83
  171. data/spec/commands/zrank_spec.rb +0 -29
  172. data/spec/commands/zrem_spec.rb +0 -43
  173. data/spec/commands/zremrangebyrank_spec.rb +0 -27
  174. data/spec/commands/zremrangebyscore_spec.rb +0 -35
  175. data/spec/commands/zrevrange_spec.rb +0 -56
  176. data/spec/commands/zrevrangebyscore_spec.rb +0 -58
  177. data/spec/commands/zrevrank_spec.rb +0 -29
  178. data/spec/commands/zscan_each_spec.rb +0 -48
  179. data/spec/commands/zscan_spec.rb +0 -26
  180. data/spec/commands/zscore_spec.rb +0 -22
  181. data/spec/commands/zunionstore_spec.rb +0 -104
  182. data/spec/mock_redis_spec.rb +0 -86
  183. data/spec/spec_helper.rb +0 -63
  184. data/spec/support/redis_multiplexer.rb +0 -106
  185. data/spec/support/shared_examples/only_operates_on_hashes.rb +0 -13
  186. data/spec/support/shared_examples/only_operates_on_lists.rb +0 -13
  187. data/spec/support/shared_examples/only_operates_on_sets.rb +0 -13
  188. data/spec/support/shared_examples/only_operates_on_strings.rb +0 -13
  189. data/spec/support/shared_examples/only_operates_on_zsets.rb +0 -57
  190. data/spec/support/shared_examples/sorts_enumerables.rb +0 -56
  191. data/spec/transactions_spec.rb +0 -159
@@ -12,7 +12,7 @@ class MockRedis
12
12
  D_R = Math::PI / 180.0
13
13
  EARTH_RADIUS_IN_METERS = 6_372_797.560856
14
14
 
15
- def geoadd(key, *args)
15
+ ruby2_keywords def geoadd(key, *args)
16
16
  points = parse_points(args)
17
17
 
18
18
  scored_points = points.map do |point|
@@ -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,17 @@ 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
11
+ orig_size = hash.size
12
+ fields = Array(fields).flatten.map(&:to_s)
13
+
14
+ if fields.empty?
15
+ raise Redis::CommandError, "ERR wrong number of arguments for 'hdel' command"
18
16
  end
17
+
18
+ hash.delete_if { |k, _v| fields.include?(k) }
19
+ orig_size - hash.size
19
20
  end
20
21
  end
21
22
 
@@ -74,8 +75,10 @@ class MockRedis
74
75
  end
75
76
 
76
77
  def hmget(key, *fields)
78
+ fields.flatten!
79
+
77
80
  assert_has_args(fields, 'hmget')
78
- fields.flatten.map { |f| hget(key, f) }
81
+ fields.map { |f| hget(key, f) }
79
82
  end
80
83
 
81
84
  def mapped_hmget(key, *fields)
@@ -88,10 +91,17 @@ class MockRedis
88
91
  end
89
92
 
90
93
  def hmset(key, *kvpairs)
94
+ if key.is_a? Array
95
+ err_msg = 'ERR wrong number of arguments for \'hmset\' command'
96
+ kvpairs = key[1..]
97
+ key = key[0]
98
+ end
99
+
91
100
  kvpairs.flatten!
92
101
  assert_has_args(kvpairs, 'hmset')
102
+
93
103
  if kvpairs.length.odd?
94
- raise Redis::CommandError, 'ERR wrong number of arguments for HMSET'
104
+ raise Redis::CommandError, err_msg || 'ERR wrong number of arguments for \'hmset\' command'
95
105
  end
96
106
 
97
107
  kvpairs.each_slice(2) do |(k, v)|
@@ -125,10 +135,19 @@ class MockRedis
125
135
  end
126
136
  end
127
137
 
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
138
+ def hset(key, *args)
139
+ added = 0
140
+ with_hash_at(key) do |hash|
141
+ if args.length == 1 && args[0].is_a?(Hash)
142
+ args = args[0].to_a.flatten
143
+ end
144
+
145
+ args.each_slice(2) do |field, value|
146
+ added += 1 unless hash.key?(field.to_s)
147
+ hash[field.to_s] = value.to_s
148
+ end
149
+ end
150
+ added
132
151
  end
133
152
 
134
153
  def hsetnx(key, field, value)
@@ -147,7 +166,7 @@ class MockRedis
147
166
  private
148
167
 
149
168
  def with_hash_at(key, &blk)
150
- with_thing_at(key, :assert_hashy, proc { {} }, &blk)
169
+ with_thing_at(key.to_s, :assert_hashy, proc { {} }, &blk)
151
170
  end
152
171
 
153
172
  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,
@@ -6,6 +6,20 @@ class MockRedis
6
6
  include Assertions
7
7
  include UtilityMethods
8
8
 
9
+ def blmove(source, destination, wherefrom, whereto, options = {})
10
+ options = { :timeout => options } if options.is_a?(Integer)
11
+ timeout = options.is_a?(Hash) && options[:timeout] || 0
12
+ assert_valid_timeout(timeout)
13
+
14
+ if llen(source) > 0
15
+ lmove(source, destination, wherefrom, whereto)
16
+ elsif timeout > 0
17
+ nil
18
+ else
19
+ raise MockRedis::WouldBlock, "Can't block forever"
20
+ end
21
+ end
22
+
9
23
  def blpop(*args)
10
24
  lists, timeout = extract_timeout(args)
11
25
  nonempty_list = first_nonempty_list(lists)
@@ -78,8 +92,29 @@ class MockRedis
78
92
  with_list_at(key, &:length)
79
93
  end
80
94
 
81
- def lpop(key)
82
- with_list_at(key, &:shift)
95
+ def lmove(source, destination, wherefrom, whereto)
96
+ assert_listy(source)
97
+ assert_listy(destination)
98
+
99
+ wherefrom = wherefrom.to_s.downcase
100
+ whereto = whereto.to_s.downcase
101
+
102
+ unless %w[left right].include?(wherefrom) && %w[left right].include?(whereto)
103
+ raise Redis::CommandError, 'ERR syntax error'
104
+ end
105
+
106
+ value = wherefrom == 'left' ? lpop(source) : rpop(source)
107
+ (whereto == 'left' ? lpush(destination, value) : rpush(destination, value)) unless value.nil?
108
+ value
109
+ end
110
+
111
+ def lpop(key, count = nil)
112
+ return with_list_at(key, &:shift) if count.nil?
113
+
114
+ record_count = llen(key)
115
+ return nil if record_count.zero?
116
+
117
+ [record_count, count].min.times.map { with_list_at(key, &:shift) }
83
118
  end
84
119
 
85
120
  def lpush(key, values)
@@ -146,13 +181,13 @@ class MockRedis
146
181
 
147
182
  def ltrim(key, start, stop)
148
183
  with_list_at(key) do |list|
149
- list.replace(list[[start.to_i, -list.length].max..stop.to_i] || []) if list
184
+ list&.replace(list[[start.to_i, -list.length].max..stop.to_i] || [])
150
185
  'OK'
151
186
  end
152
187
  end
153
188
 
154
189
  def rpop(key)
155
- with_list_at(key) { |list| list.pop if list }
190
+ with_list_at(key) { |list| list&.pop }
156
191
  end
157
192
 
158
193
  def rpoplpush(source, destination)
@@ -0,0 +1,11 @@
1
+ class MockRedis
2
+ module MemoryMethod
3
+ def memory(usage, key = nil, *_options)
4
+ raise ArgumentError, "unhandled command `memory #{usage}`" if usage != 'usage'
5
+
6
+ return nil unless @data.key?(key)
7
+
8
+ 160
9
+ end
10
+ end
11
+ end
@@ -17,21 +17,21 @@ class MockRedis
17
17
  super || current_db.respond_to?(method, include_private)
18
18
  end
19
19
 
20
- def method_missing(method, *args, &block)
20
+ ruby2_keywords def method_missing(method, *args, &block)
21
21
  current_db.send(method, *args, &block)
22
22
  end
23
23
 
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
31
31
 
32
32
  # Redis commands
33
33
  def flushall
34
- @databases.values.each(&:flushdb)
34
+ @databases.each_value(&:flushdb)
35
35
  'OK'
36
36
  end
37
37
 
@@ -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)
@@ -18,8 +18,8 @@ class MockRedis
18
18
  @pipelined_futures = @pipelined_futures.clone
19
19
  end
20
20
 
21
- def method_missing(method, *args, &block)
22
- if @in_pipeline
21
+ ruby2_keywords def method_missing(method, *args, &block)
22
+ if in_pipeline?
23
23
  future = MockRedis::Future.new([method, *args], block)
24
24
  @pipelined_futures << future
25
25
  future
@@ -29,24 +29,41 @@ 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
- begin
37
- result = if future.block
38
- send(*future.command, &future.block)
39
- else
40
- send(*future.command)
41
- end
42
- future.store_result(result)
44
+ result = if future.block
45
+ send(*future.command, &future.block)
46
+ else
47
+ send(*future.command)
48
+ end
49
+ future.store_result(result)
50
+
51
+ if future.block
43
52
  result
44
- rescue StandardError => e
45
- e
53
+ else
54
+ [result]
46
55
  end
56
+ rescue StandardError => e
57
+ e
47
58
  end
48
59
  @pipelined_futures = []
49
60
  responses
50
61
  end
62
+
63
+ private
64
+
65
+ def in_pipeline?
66
+ @nesting_level > 0
67
+ end
51
68
  end
52
69
  end
@@ -8,7 +8,7 @@ class MockRedis
8
8
 
9
9
  def sadd(key, members)
10
10
  members_class = members.class
11
- members = [members].flatten.map(&:to_s)
11
+ members = Array(members).map(&:to_s)
12
12
  assert_has_args(members, 'sadd')
13
13
 
14
14
  with_set_at(key) do |s|
@@ -27,6 +27,11 @@ class MockRedis
27
27
  end
28
28
  end
29
29
 
30
+ def sadd?(key, members)
31
+ res = sadd(key, members)
32
+ res.is_a?(Numeric) ? res > 0 : res
33
+ end
34
+
30
35
  def scard(key)
31
36
  with_set_at(key, &:length)
32
37
  end
@@ -64,6 +69,12 @@ class MockRedis
64
69
  with_set_at(key) { |s| s.include?(member.to_s) }
65
70
  end
66
71
 
72
+ def smismember(key, *members)
73
+ with_set_at(key) do |set|
74
+ members.flatten.map { |m| set.include?(m.to_s) }
75
+ end
76
+ end
77
+
67
78
  def smembers(key)
68
79
  with_set_at(key, &:to_a).map(&:dup).reverse
69
80
  end
@@ -81,11 +92,22 @@ class MockRedis
81
92
  end
82
93
  end
83
94
 
84
- def spop(key)
95
+ def spop(key, count = nil)
85
96
  with_set_at(key) do |set|
86
- member = set.first
87
- set.delete(member)
88
- member
97
+ if count.nil?
98
+ member = set.first
99
+ set.delete(member)
100
+ member
101
+ else
102
+ members = []
103
+ count.times do
104
+ member = set.first
105
+ break if member.nil?
106
+ set.delete(member)
107
+ members << member
108
+ end
109
+ members
110
+ end
89
111
  end
90
112
  end
91
113
 
@@ -106,6 +128,7 @@ class MockRedis
106
128
  with_set_at(key) do |s|
107
129
  if members.is_a?(Array)
108
130
  orig_size = s.size
131
+ members = members.map(&:to_s)
109
132
  s.delete_if { |m| members.include?(m) }
110
133
  orig_size - s.size
111
134
  else
@@ -114,6 +137,11 @@ class MockRedis
114
137
  end
115
138
  end
116
139
 
140
+ def srem?(key, members)
141
+ res = srem(key, members)
142
+ res.is_a?(Numeric) ? res > 0 : res
143
+ end
144
+
117
145
  def sscan(key, cursor, opts = {})
118
146
  common_scan(smembers(key), cursor, opts)
119
147
  end
@@ -153,7 +181,7 @@ class MockRedis
153
181
  with_set_at(keys.first, &blk)
154
182
  else
155
183
  with_set_at(keys.first) do |set|
156
- with_sets_at(*(keys[1..-1])) do |*sets|
184
+ with_sets_at(*(keys[1..])) do |*sets|
157
185
  yield(*([set] + sets))
158
186
  end
159
187
  end
@@ -0,0 +1,58 @@
1
+ class MockRedis
2
+ class Stream
3
+ class Id
4
+ include Comparable
5
+
6
+ attr_accessor :timestamp, :sequence, :exclusive
7
+
8
+ def initialize(id, min: nil, sequence: 0)
9
+ @exclusive = false
10
+ case id
11
+ when '*'
12
+ @timestamp = (Time.now.to_f * 1000).to_i
13
+ @sequence = 0
14
+ if self <= min
15
+ @timestamp = min.timestamp
16
+ @sequence = min.sequence + 1
17
+ end
18
+ when '-'
19
+ @timestamp = @sequence = 0
20
+ when '+'
21
+ @timestamp = @sequence = Float::INFINITY
22
+ else
23
+ if id.is_a? String
24
+ # See https://redis.io/topics/streams-intro
25
+ # Ids are a unix timestamp in milliseconds followed by an
26
+ # optional dash sequence number, e.g. -0. They can also optionally
27
+ # be prefixed with '(' to change the XRANGE to exclusive.
28
+ (_, @timestamp, @sequence) = id.match(/^\(?(\d+)-?(\d+)?$/).to_a
29
+ @exclusive = true if id[0] == '('
30
+ if @timestamp.nil?
31
+ raise Redis::CommandError,
32
+ 'ERR Invalid stream ID specified as stream command argument'
33
+ end
34
+ @timestamp = @timestamp.to_i
35
+ else
36
+ @timestamp = id
37
+ end
38
+ @sequence = @sequence.nil? ? sequence : @sequence.to_i
39
+ if self <= min
40
+ raise Redis::CommandError,
41
+ 'ERR The ID specified in XADD is equal or smaller than ' \
42
+ 'the target stream top item'
43
+ end
44
+ end
45
+ end
46
+
47
+ def to_s
48
+ "#{@timestamp}-#{@sequence}"
49
+ end
50
+
51
+ def <=>(other)
52
+ return 1 if other.nil?
53
+ return @sequence <=> other.sequence if @timestamp == other.timestamp
54
+ @timestamp <=> other.timestamp
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,88 @@
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
+ 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
30
+ members.add [@last_id, Hash[values.map { |k, v| [k.to_s, v.to_s] }]]
31
+ @last_id.to_s
32
+ end
33
+
34
+ def trim(count)
35
+ deleted = @members.size - count
36
+ if deleted > 0
37
+ @members = if count == 0
38
+ Set.new
39
+ else
40
+ @members.to_a[-count..].to_set
41
+ end
42
+ deleted
43
+ else
44
+ 0
45
+ end
46
+ end
47
+
48
+ def range(start, finish, reversed, *opts_in)
49
+ opts = options opts_in, ['count']
50
+ start_id = MockRedis::Stream::Id.new(start)
51
+ finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY)
52
+ items = if start_id.exclusive
53
+ members
54
+ .select { |m| (start_id < m[0]) && (finish_id >= m[0]) }
55
+ .map { |m| [m[0].to_s, m[1]] }
56
+ else
57
+ members
58
+ .select { |m| (start_id <= m[0]) && (finish_id >= m[0]) }
59
+ .map { |m| [m[0].to_s, m[1]] }
60
+ end
61
+ items.reverse! if reversed
62
+ return items.first(opts['count'].to_i) if opts.key?('count')
63
+ items
64
+ end
65
+
66
+ def read(id, *opts_in)
67
+ opts = options opts_in, %w[count block]
68
+ stream_id = MockRedis::Stream::Id.new(id)
69
+ items = members.select { |m| (stream_id < m[0]) }.map { |m| [m[0].to_s, m[1]] }
70
+ return items.first(opts['count'].to_i) if opts.key?('count')
71
+ items
72
+ end
73
+
74
+ def each(&block)
75
+ members.each(&block)
76
+ end
77
+
78
+ private
79
+
80
+ def options(opts_in, permitted)
81
+ opts_out = {}
82
+ raise Redis::CommandError, 'ERR syntax error' unless (opts_in.length % 2).zero?
83
+ opts_in.each_slice(2).map { |pair| opts_out[pair[0].downcase] = pair[1] }
84
+ raise Redis::CommandError, 'ERR syntax error' unless (opts_out.keys - permitted).empty?
85
+ opts_out
86
+ end
87
+ end
88
+ end