mock_redis 0.5.4 → 0.31.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 (201) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/lint.yml +31 -0
  3. data/.github/workflows/tests.yml +63 -0
  4. data/.gitignore +1 -1
  5. data/.overcommit.yml +21 -0
  6. data/.rspec +1 -1
  7. data/.rubocop.yml +148 -0
  8. data/.rubocop_todo.yml +35 -0
  9. data/.simplecov +4 -0
  10. data/CHANGELOG.md +278 -0
  11. data/Gemfile +9 -5
  12. data/LICENSE.md +21 -0
  13. data/README.md +52 -16
  14. data/Rakefile +0 -8
  15. data/lib/mock_redis/assertions.rb +0 -1
  16. data/lib/mock_redis/connection_method.rb +13 -0
  17. data/lib/mock_redis/database.rb +193 -257
  18. data/lib/mock_redis/expire_wrapper.rb +2 -2
  19. data/lib/mock_redis/future.rb +23 -0
  20. data/lib/mock_redis/geospatial_methods.rb +240 -0
  21. data/lib/mock_redis/hash_methods.rb +83 -24
  22. data/lib/mock_redis/indifferent_hash.rb +11 -0
  23. data/lib/mock_redis/info_method.rb +160 -0
  24. data/lib/mock_redis/list_methods.rb +34 -19
  25. data/lib/mock_redis/multi_db_wrapper.rb +8 -7
  26. data/lib/mock_redis/pipelined_wrapper.rb +42 -16
  27. data/lib/mock_redis/set_methods.rb +62 -19
  28. data/lib/mock_redis/sort_method.rb +81 -0
  29. data/lib/mock_redis/stream/id.rb +58 -0
  30. data/lib/mock_redis/stream.rb +88 -0
  31. data/lib/mock_redis/stream_methods.rb +102 -0
  32. data/lib/mock_redis/string_methods.rb +235 -42
  33. data/lib/mock_redis/transaction_wrapper.rb +62 -28
  34. data/lib/mock_redis/utility_methods.rb +62 -11
  35. data/lib/mock_redis/version.rb +4 -1
  36. data/lib/mock_redis/zset.rb +24 -29
  37. data/lib/mock_redis/zset_methods.rb +187 -59
  38. data/lib/mock_redis.rb +77 -27
  39. data/mock_redis.gemspec +23 -15
  40. data/spec/client_spec.rb +29 -0
  41. data/spec/cloning_spec.rb +17 -18
  42. data/spec/commands/append_spec.rb +4 -4
  43. data/spec/commands/auth_spec.rb +1 -1
  44. data/spec/commands/bgrewriteaof_spec.rb +2 -2
  45. data/spec/commands/bgsave_spec.rb +2 -2
  46. data/spec/commands/bitcount_spec.rb +25 -0
  47. data/spec/commands/bitfield_spec.rb +169 -0
  48. data/spec/commands/blpop_spec.rb +19 -21
  49. data/spec/commands/brpop_spec.rb +25 -20
  50. data/spec/commands/brpoplpush_spec.rb +16 -17
  51. data/spec/commands/connected_spec.rb +7 -0
  52. data/spec/commands/connection_spec.rb +15 -0
  53. data/spec/commands/dbsize_spec.rb +3 -3
  54. data/spec/commands/decr_spec.rb +8 -8
  55. data/spec/commands/decrby_spec.rb +8 -8
  56. data/spec/commands/del_spec.rb +35 -3
  57. data/spec/commands/disconnect_spec.rb +7 -0
  58. data/spec/commands/dump_spec.rb +19 -0
  59. data/spec/commands/echo_spec.rb +4 -4
  60. data/spec/commands/eval_spec.rb +7 -0
  61. data/spec/commands/evalsha_spec.rb +10 -0
  62. data/spec/commands/exists_spec.rb +36 -7
  63. data/spec/commands/expire_spec.rb +48 -20
  64. data/spec/commands/expireat_spec.rb +12 -13
  65. data/spec/commands/flushall_spec.rb +5 -5
  66. data/spec/commands/flushdb_spec.rb +5 -5
  67. data/spec/commands/future_spec.rb +30 -0
  68. data/spec/commands/geoadd_spec.rb +58 -0
  69. data/spec/commands/geodist_spec.rb +118 -0
  70. data/spec/commands/geohash_spec.rb +52 -0
  71. data/spec/commands/geopos_spec.rb +55 -0
  72. data/spec/commands/get_spec.rb +14 -6
  73. data/spec/commands/getbit_spec.rb +7 -7
  74. data/spec/commands/getrange_spec.rb +9 -9
  75. data/spec/commands/getset_spec.rb +7 -7
  76. data/spec/commands/hdel_spec.rb +41 -11
  77. data/spec/commands/hexists_spec.rb +11 -11
  78. data/spec/commands/hget_spec.rb +7 -7
  79. data/spec/commands/hgetall_spec.rb +15 -5
  80. data/spec/commands/hincrby_spec.rb +16 -16
  81. data/spec/commands/hincrbyfloat_spec.rb +58 -0
  82. data/spec/commands/hkeys_spec.rb +5 -5
  83. data/spec/commands/hlen_spec.rb +5 -5
  84. data/spec/commands/hmget_spec.rb +19 -9
  85. data/spec/commands/hmset_spec.rb +38 -12
  86. data/spec/commands/hscan_each_spec.rb +48 -0
  87. data/spec/commands/hscan_spec.rb +27 -0
  88. data/spec/commands/hset_spec.rb +26 -12
  89. data/spec/commands/hsetnx_spec.rb +16 -16
  90. data/spec/commands/hvals_spec.rb +5 -5
  91. data/spec/commands/incr_spec.rb +8 -8
  92. data/spec/commands/incrby_spec.rb +13 -13
  93. data/spec/commands/incrbyfloat_spec.rb +13 -13
  94. data/spec/commands/info_spec.rb +54 -5
  95. data/spec/commands/keys_spec.rb +83 -31
  96. data/spec/commands/lastsave_spec.rb +2 -2
  97. data/spec/commands/lindex_spec.rb +20 -10
  98. data/spec/commands/linsert_spec.rb +14 -14
  99. data/spec/commands/llen_spec.rb +4 -4
  100. data/spec/commands/lpop_spec.rb +6 -6
  101. data/spec/commands/lpush_spec.rb +21 -15
  102. data/spec/commands/lpushx_spec.rb +24 -11
  103. data/spec/commands/lrange_spec.rb +24 -8
  104. data/spec/commands/lrem_spec.rb +16 -16
  105. data/spec/commands/lset_spec.rb +17 -12
  106. data/spec/commands/ltrim_spec.rb +17 -7
  107. data/spec/commands/mapped_hmget_spec.rb +13 -9
  108. data/spec/commands/mapped_hmset_spec.rb +12 -12
  109. data/spec/commands/mapped_mget_spec.rb +22 -0
  110. data/spec/commands/mapped_mset_spec.rb +19 -0
  111. data/spec/commands/mapped_msetnx_spec.rb +26 -0
  112. data/spec/commands/mget_spec.rb +48 -17
  113. data/spec/commands/move_spec.rb +37 -37
  114. data/spec/commands/mset_spec.rb +20 -6
  115. data/spec/commands/msetnx_spec.rb +14 -14
  116. data/spec/commands/persist_spec.rb +15 -16
  117. data/spec/commands/pexpire_spec.rb +86 -0
  118. data/spec/commands/pexpireat_spec.rb +48 -0
  119. data/spec/commands/ping_spec.rb +6 -2
  120. data/spec/commands/pipelined_spec.rb +98 -7
  121. data/spec/commands/pttl_spec.rb +41 -0
  122. data/spec/commands/randomkey_spec.rb +3 -3
  123. data/spec/commands/rename_spec.rb +16 -12
  124. data/spec/commands/renamenx_spec.rb +13 -15
  125. data/spec/commands/restore_spec.rb +47 -0
  126. data/spec/commands/rpop_spec.rb +6 -6
  127. data/spec/commands/rpoplpush_spec.rb +13 -8
  128. data/spec/commands/rpush_spec.rb +21 -15
  129. data/spec/commands/rpushx_spec.rb +24 -11
  130. data/spec/commands/sadd_spec.rb +14 -10
  131. data/spec/commands/scan_each_spec.rb +39 -0
  132. data/spec/commands/scan_spec.rb +64 -0
  133. data/spec/commands/scard_spec.rb +3 -3
  134. data/spec/commands/script_spec.rb +9 -0
  135. data/spec/commands/sdiff_spec.rb +13 -13
  136. data/spec/commands/sdiffstore_spec.rb +13 -13
  137. data/spec/commands/select_spec.rb +13 -5
  138. data/spec/commands/set_spec.rb +112 -0
  139. data/spec/commands/setbit_spec.rb +25 -16
  140. data/spec/commands/setex_spec.rb +20 -4
  141. data/spec/commands/setnx_spec.rb +6 -6
  142. data/spec/commands/setrange_spec.rb +12 -12
  143. data/spec/commands/sinter_spec.rb +11 -13
  144. data/spec/commands/sinterstore_spec.rb +12 -12
  145. data/spec/commands/sismember_spec.rb +10 -10
  146. data/spec/commands/smembers_spec.rb +15 -5
  147. data/spec/commands/smove_spec.rb +13 -13
  148. data/spec/commands/sort_list_spec.rb +21 -0
  149. data/spec/commands/sort_set_spec.rb +21 -0
  150. data/spec/commands/sort_zset_spec.rb +21 -0
  151. data/spec/commands/spop_spec.rb +19 -4
  152. data/spec/commands/srandmember_spec.rb +28 -4
  153. data/spec/commands/srem_spec.rb +17 -12
  154. data/spec/commands/sscan_each_spec.rb +48 -0
  155. data/spec/commands/sscan_spec.rb +39 -0
  156. data/spec/commands/strlen_spec.rb +4 -5
  157. data/spec/commands/sunion_spec.rb +13 -11
  158. data/spec/commands/sunionstore_spec.rb +12 -12
  159. data/spec/commands/ttl_spec.rb +11 -6
  160. data/spec/commands/type_spec.rb +1 -1
  161. data/spec/commands/watch_spec.rb +9 -4
  162. data/spec/commands/xadd_spec.rb +122 -0
  163. data/spec/commands/xlen_spec.rb +22 -0
  164. data/spec/commands/xrange_spec.rb +164 -0
  165. data/spec/commands/xread_spec.rb +66 -0
  166. data/spec/commands/xrevrange_spec.rb +130 -0
  167. data/spec/commands/xtrim_spec.rb +36 -0
  168. data/spec/commands/zadd_spec.rb +100 -11
  169. data/spec/commands/zcard_spec.rb +4 -4
  170. data/spec/commands/zcount_spec.rb +18 -10
  171. data/spec/commands/zincrby_spec.rb +6 -6
  172. data/spec/commands/zinterstore_spec.rb +54 -20
  173. data/spec/commands/zpopmax_spec.rb +60 -0
  174. data/spec/commands/zpopmin_spec.rb +60 -0
  175. data/spec/commands/zrange_spec.rb +54 -13
  176. data/spec/commands/zrangebyscore_spec.rb +42 -27
  177. data/spec/commands/zrank_spec.rb +4 -4
  178. data/spec/commands/zrem_spec.rb +18 -12
  179. data/spec/commands/zremrangebyrank_spec.rb +5 -5
  180. data/spec/commands/zremrangebyscore_spec.rb +12 -5
  181. data/spec/commands/zrevrange_spec.rb +35 -10
  182. data/spec/commands/zrevrangebyscore_spec.rb +26 -15
  183. data/spec/commands/zrevrank_spec.rb +4 -4
  184. data/spec/commands/zscan_each_spec.rb +48 -0
  185. data/spec/commands/zscan_spec.rb +26 -0
  186. data/spec/commands/zscore_spec.rb +7 -7
  187. data/spec/commands/zunionstore_spec.rb +54 -21
  188. data/spec/mock_redis_spec.rb +61 -0
  189. data/spec/spec_helper.rb +35 -8
  190. data/spec/support/redis_multiplexer.rb +62 -37
  191. data/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +14 -0
  192. data/spec/support/shared_examples/only_operates_on_hashes.rb +5 -3
  193. data/spec/support/shared_examples/only_operates_on_lists.rb +5 -3
  194. data/spec/support/shared_examples/only_operates_on_sets.rb +5 -3
  195. data/spec/support/shared_examples/only_operates_on_strings.rb +4 -4
  196. data/spec/support/shared_examples/only_operates_on_zsets.rb +18 -16
  197. data/spec/support/shared_examples/sorts_enumerables.rb +56 -0
  198. data/spec/transactions_spec.rb +79 -29
  199. metadata +162 -42
  200. data/LICENSE +0 -19
  201. data/spec/commands/hash_operator_spec.rb +0 -21
@@ -32,7 +32,9 @@ class MockRedis
32
32
  end
33
33
  end
34
34
 
35
- def brpoplpush(source, destination, timeout)
35
+ def brpoplpush(source, destination, options = {})
36
+ options = { :timeout => options } if options.is_a?(Integer)
37
+ timeout = options.is_a?(Hash) && options[:timeout] || 0
36
38
  assert_valid_timeout(timeout)
37
39
 
38
40
  if llen(source) > 0
@@ -45,12 +47,12 @@ class MockRedis
45
47
  end
46
48
 
47
49
  def lindex(key, index)
48
- with_list_at(key) {|l| l[index]}
50
+ with_list_at(key) { |l| l[index.to_i] }
49
51
  end
50
52
 
51
53
  def linsert(key, position, pivot, value)
52
54
  unless %w[before after].include?(position.to_s)
53
- raise Redis::CommandError, "ERR syntax error"
55
+ raise Redis::CommandError, 'ERR syntax error'
54
56
  end
55
57
 
56
58
  assert_listy(key)
@@ -82,23 +84,29 @@ class MockRedis
82
84
 
83
85
  def lpush(key, values)
84
86
  values = [values] unless values.is_a?(Array)
85
- with_list_at(key) {|l| values.each {|v| l.unshift(v.to_s)}}
87
+ assert_has_args(values, 'lpush')
88
+ with_list_at(key) { |l| values.each { |v| l.unshift(v.to_s) } }
86
89
  llen(key)
87
90
  end
88
91
 
89
92
  def lpushx(key, value)
93
+ value = [value] unless value.is_a?(Array)
94
+ if value.empty?
95
+ raise Redis::CommandError, "ERR wrong number of arguments for 'lpushx' command"
96
+ end
90
97
  assert_listy(key)
91
98
  return 0 unless list_at?(key)
92
99
  lpush(key, value)
93
100
  end
94
101
 
95
102
  def lrange(key, start, stop)
96
- with_list_at(key) {|l| l[start..stop]}
103
+ start = start.to_i
104
+ with_list_at(key) { |l| start < l.size ? l[[start, -l.length].max..stop.to_i] : [] }
97
105
  end
98
106
 
99
107
  def lrem(key, count, value)
100
108
  unless looks_like_integer?(count.to_s)
101
- raise Redis::CommandError, "ERR value is not an integer or out of range"
109
+ raise Redis::CommandError, 'ERR value is not an integer or out of range'
102
110
  end
103
111
  count = count.to_i
104
112
  value = value.to_s
@@ -116,7 +124,7 @@ class MockRedis
116
124
  indices_with_value.reverse.take(-count)
117
125
  end
118
126
 
119
- indices_to_delete.each {|i| list.delete_at(i)}.length
127
+ indices_to_delete.each { |i| list.delete_at(i) }.length
120
128
  end
121
129
  end
122
130
 
@@ -124,11 +132,12 @@ class MockRedis
124
132
  assert_listy(key)
125
133
 
126
134
  unless list_at?(key)
127
- raise Redis::CommandError, "ERR no such key"
135
+ raise Redis::CommandError, 'ERR no such key'
128
136
  end
129
137
 
130
- unless (0...llen(key)).include?(index)
131
- raise Redis::CommandError, "ERR index out of range"
138
+ index = index.to_i
139
+ unless (0...llen(key)).cover?(index)
140
+ raise Redis::CommandError, 'ERR index out of range'
132
141
  end
133
142
 
134
143
  data[key][index] = value.to_s
@@ -137,56 +146,62 @@ class MockRedis
137
146
 
138
147
  def ltrim(key, start, stop)
139
148
  with_list_at(key) do |list|
140
- list.replace(list[start..stop] || []) if list
149
+ list&.replace(list[[start.to_i, -list.length].max..stop.to_i] || [])
141
150
  'OK'
142
151
  end
143
152
  end
144
153
 
145
154
  def rpop(key)
146
- with_list_at(key) {|list| list.pop if list}
155
+ with_list_at(key) { |list| list&.pop }
147
156
  end
148
157
 
149
158
  def rpoplpush(source, destination)
150
159
  value = rpop(source)
151
- lpush(destination, value)
160
+ lpush(destination, value) unless value.nil?
152
161
  value
153
162
  end
154
163
 
155
164
  def rpush(key, values)
156
165
  values = [values] unless values.is_a?(Array)
157
- with_list_at(key) {|l| values.each {|v| l.push(v.to_s)}}
166
+ assert_has_args(values, 'rpush')
167
+ with_list_at(key) { |l| values.each { |v| l.push(v.to_s) } }
158
168
  llen(key)
159
169
  end
160
170
 
161
171
  def rpushx(key, value)
172
+ value = [value] unless value.is_a?(Array)
173
+ if value.empty?
174
+ raise Redis::CommandError, "ERR wrong number of arguments for 'rpushx' command"
175
+ end
162
176
  assert_listy(key)
163
177
  return 0 unless list_at?(key)
164
178
  rpush(key, value)
165
179
  end
166
180
 
167
181
  private
182
+
168
183
  def list_at?(key)
169
184
  data[key] && listy?(key)
170
185
  end
171
186
 
172
187
  def with_list_at(key, &blk)
173
- with_thing_at(key, :assert_listy, proc {[]}, &blk)
188
+ with_thing_at(key, :assert_listy, proc { [] }, &blk)
174
189
  end
175
190
 
176
191
  def listy?(key)
177
- data[key].nil? || data[key].kind_of?(Array)
192
+ data[key].nil? || data[key].is_a?(Array)
178
193
  end
179
194
 
180
195
  def assert_listy(key)
181
196
  unless listy?(key)
182
197
  # Not the most helpful error, but it's what redis-rb barfs up
183
- raise Redis::CommandError, "ERR Operation against a key holding the wrong kind of value"
198
+ raise Redis::CommandError,
199
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
184
200
  end
185
201
  end
186
202
 
187
203
  def first_nonempty_list(keys)
188
- keys.find{|k| llen(k) > 0}
204
+ keys.find { |k| llen(k) > 0 }
189
205
  end
190
-
191
206
  end
192
207
  end
@@ -9,22 +9,22 @@ class MockRedis
9
9
 
10
10
  @prototype_db = db.clone
11
11
 
12
- @databases = Hash.new {|h,k| h[k] = @prototype_db.clone}
12
+ @databases = Hash.new { |h, k| h[k] = @prototype_db.clone }
13
13
  @databases[@db_index] = db
14
14
  end
15
15
 
16
- def respond_to?(method, include_private=false)
16
+ def respond_to?(method, include_private = false)
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
@@ -39,12 +39,12 @@ 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)
46
46
  when 'hash'
47
- dest.hmset(key, *(src.hgetall(key).map{|k,v| [k,v]}.flatten))
47
+ dest.hmset(key, *src.hgetall(key).map { |k, v| [k, v] }.flatten)
48
48
  when 'list'
49
49
  while value = src.rpop(key)
50
50
  dest.lpush(key, value)
@@ -56,7 +56,7 @@ class MockRedis
56
56
  when 'string'
57
57
  dest.set(key, src.get(key))
58
58
  when 'zset'
59
- src.zrange(key, 0, -1, :with_scores => true).each do |(m,s)|
59
+ src.zrange(key, 0, -1, :with_scores => true).each do |(m, s)|
60
60
  dest.zadd(key, s, m)
61
61
  end
62
62
  else
@@ -75,6 +75,7 @@ class MockRedis
75
75
  end
76
76
 
77
77
  private
78
+
78
79
  def current_db
79
80
  @databases[@db_index]
80
81
  end
@@ -2,44 +2,70 @@ class MockRedis
2
2
  class PipelinedWrapper
3
3
  include UndefRedisMethods
4
4
 
5
- def respond_to?(method, include_private=false)
5
+ def respond_to?(method, include_private = false)
6
6
  super || @db.respond_to?(method)
7
7
  end
8
8
 
9
9
  def initialize(db)
10
10
  @db = db
11
- @pipelined_commands = []
12
- @in_pipeline = false
11
+ @pipelined_futures = []
12
+ @nesting_level = 0
13
13
  end
14
14
 
15
15
  def initialize_copy(source)
16
16
  super
17
17
  @db = @db.clone
18
- @pipelined_commands = @pipelined_commands.clone
18
+ @pipelined_futures = @pipelined_futures.clone
19
19
  end
20
20
 
21
- def method_missing(method, *args, &block)
22
- if @in_pipeline
23
- @pipelined_commands << [method, *args]
24
- nil
21
+ ruby2_keywords def method_missing(method, *args, &block)
22
+ if in_pipeline?
23
+ future = MockRedis::Future.new([method, *args], block)
24
+ @pipelined_futures << future
25
+ future
25
26
  else
26
27
  @db.send(method, *args, &block)
27
28
  end
28
29
  end
29
30
 
30
- def pipelined(options = {})
31
- @in_pipeline = true
32
- yield
33
- @in_pipeline = false
34
- responses = @pipelined_commands.map do |cmd|
31
+ def pipelined(_options = {})
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
+
43
+ responses = @pipelined_futures.flat_map do |future|
35
44
  begin
36
- send(*cmd)
37
- rescue => e
45
+ result = if future.block
46
+ send(*future.command, &future.block)
47
+ else
48
+ send(*future.command)
49
+ end
50
+ future.store_result(result)
51
+
52
+ if future.block
53
+ result
54
+ else
55
+ [result]
56
+ end
57
+ rescue StandardError => e
38
58
  e
39
59
  end
40
60
  end
41
- @pipelined_commands = []
61
+ @pipelined_futures = []
42
62
  responses
43
63
  end
64
+
65
+ private
66
+
67
+ def in_pipeline?
68
+ @nesting_level > 0
69
+ end
44
70
  end
45
71
  end
@@ -7,26 +7,33 @@ class MockRedis
7
7
  include UtilityMethods
8
8
 
9
9
  def sadd(key, members)
10
+ members_class = members.class
10
11
  members = [members].flatten.map(&:to_s)
12
+ assert_has_args(members, 'sadd')
11
13
 
12
14
  with_set_at(key) do |s|
15
+ size_before = s.size
13
16
  if members.size > 1
14
- size_before = s.size
15
- members.reverse.each {|m| s << m}
17
+ members.reverse_each { |m| s << m }
16
18
  s.size - size_before
17
19
  else
18
- !!s.add?(members.first)
20
+ added = !!s.add?(members.first)
21
+ if members_class == Array
22
+ s.size - size_before
23
+ else
24
+ added
25
+ end
19
26
  end
20
27
  end
21
28
  end
22
29
 
23
30
  def scard(key)
24
- with_set_at(key) {|s| s.length}
31
+ with_set_at(key, &:length)
25
32
  end
26
33
 
27
34
  def sdiff(*keys)
28
35
  assert_has_args(keys, 'sdiff')
29
- with_sets_at(*keys) {|*sets| sets.reduce(&:-)}.to_a
36
+ with_sets_at(*keys) { |*sets| sets.reduce(&:-) }.to_a
30
37
  end
31
38
 
32
39
  def sdiffstore(destination, *keys)
@@ -54,11 +61,11 @@ class MockRedis
54
61
  end
55
62
 
56
63
  def sismember(key, member)
57
- with_set_at(key) {|s| s.include?(member.to_s)}
64
+ with_set_at(key) { |s| s.include?(member.to_s) }
58
65
  end
59
66
 
60
67
  def smembers(key)
61
- with_set_at(key, &:to_a).reverse
68
+ with_set_at(key, &:to_a).map(&:dup).reverse
62
69
  end
63
70
 
64
71
  def smove(src, dest, member)
@@ -74,23 +81,43 @@ class MockRedis
74
81
  end
75
82
  end
76
83
 
77
- def spop(key)
84
+ def spop(key, count = nil)
78
85
  with_set_at(key) do |set|
79
- member = set.first
80
- set.delete(member)
81
- 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
82
100
  end
83
101
  end
84
102
 
85
- def srandmember(key)
103
+ def srandmember(key, count = nil)
86
104
  members = with_set_at(key, &:to_a)
87
- members[rand(members.length)]
105
+ if count
106
+ if count > 0
107
+ members.sample(count)
108
+ else
109
+ Array.new(count.abs) { members[rand(members.length)] }
110
+ end
111
+ else
112
+ members[rand(members.length)]
113
+ end
88
114
  end
89
115
 
90
116
  def srem(key, members)
91
117
  with_set_at(key) do |s|
92
118
  if members.is_a?(Array)
93
119
  orig_size = s.size
120
+ members = members.map(&:to_s)
94
121
  s.delete_if { |m| members.include?(m) }
95
122
  orig_size - s.size
96
123
  else
@@ -99,9 +126,23 @@ class MockRedis
99
126
  end
100
127
  end
101
128
 
129
+ def sscan(key, cursor, opts = {})
130
+ common_scan(smembers(key), cursor, opts)
131
+ end
132
+
133
+ def sscan_each(key, opts = {}, &block)
134
+ return to_enum(:sscan_each, key, opts) unless block_given?
135
+ cursor = 0
136
+ loop do
137
+ cursor, keys = sscan(key, cursor, opts)
138
+ keys.each(&block)
139
+ break if cursor == '0'
140
+ end
141
+ end
142
+
102
143
  def sunion(*keys)
103
144
  assert_has_args(keys, 'sunion')
104
- with_sets_at(*keys) {|*sets| sets.reduce(&:+).to_a}
145
+ with_sets_at(*keys) { |*sets| sets.reduce(&:+).to_a }
105
146
  end
106
147
 
107
148
  def sunionstore(destination, *keys)
@@ -113,32 +154,34 @@ class MockRedis
113
154
  end
114
155
 
115
156
  private
157
+
116
158
  def with_set_at(key, &blk)
117
- with_thing_at(key, :assert_sety, proc {Set.new}, &blk)
159
+ with_thing_at(key, :assert_sety, proc { Set.new }, &blk)
118
160
  end
119
161
 
120
162
  def with_sets_at(*keys, &blk)
163
+ keys = keys.flatten
121
164
  if keys.length == 1
122
165
  with_set_at(keys.first, &blk)
123
166
  else
124
167
  with_set_at(keys.first) do |set|
125
168
  with_sets_at(*(keys[1..-1])) do |*sets|
126
- blk.call(*([set] + sets))
169
+ yield(*([set] + sets))
127
170
  end
128
171
  end
129
172
  end
130
173
  end
131
174
 
132
175
  def sety?(key)
133
- data[key].nil? || data[key].kind_of?(Set)
176
+ data[key].nil? || data[key].is_a?(Set)
134
177
  end
135
178
 
136
179
  def assert_sety(key)
137
180
  unless sety?(key)
138
181
  # Not the most helpful error, but it's what redis-rb barfs up
139
- raise Redis::CommandError, "ERR Operation against a key holding the wrong kind of value"
182
+ raise Redis::CommandError,
183
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
140
184
  end
141
185
  end
142
-
143
186
  end
144
187
  end
@@ -0,0 +1,81 @@
1
+ require 'mock_redis/assertions'
2
+
3
+ class MockRedis
4
+ module SortMethod
5
+ include Assertions
6
+
7
+ def sort(key, options = {})
8
+ return [] if key.nil?
9
+
10
+ enumerable = data[key]
11
+
12
+ return [] if enumerable.nil?
13
+
14
+ by = options[:by]
15
+ limit = options[:limit] || []
16
+ store = options[:store]
17
+ get_patterns = Array(options[:get])
18
+ order = options[:order] || 'ASC'
19
+ direction = order.split.first
20
+
21
+ projected = project(enumerable, by, get_patterns)
22
+ sorted = sort_by(projected, direction)
23
+ sliced = slice(sorted, limit)
24
+
25
+ store ? rpush(store, sliced) : sliced
26
+ end
27
+
28
+ private
29
+
30
+ ASCENDING_SORT = proc { |a, b| a.first <=> b.first }
31
+ DESCENDING_SORT = proc { |a, b| b.first <=> a.first }
32
+
33
+ def project(enumerable, by, get_patterns)
34
+ enumerable.map do |*elements|
35
+ element = elements.last
36
+ weight = by ? lookup_from_pattern(by, element) : element
37
+ value = element
38
+
39
+ unless get_patterns.empty?
40
+ value = get_patterns.map do |pattern|
41
+ pattern == '#' ? element : lookup_from_pattern(pattern, element)
42
+ end
43
+ value = value.first if value.length == 1
44
+ end
45
+
46
+ [weight, value]
47
+ end
48
+ end
49
+
50
+ def sort_by(projected, direction)
51
+ sorter =
52
+ case direction.upcase
53
+ when 'DESC'
54
+ DESCENDING_SORT
55
+ when 'ASC', 'ALPHA'
56
+ ASCENDING_SORT
57
+ else
58
+ raise "Invalid direction '#{direction}'"
59
+ end
60
+
61
+ projected.sort(&sorter).map(&:last)
62
+ end
63
+
64
+ def slice(sorted, limit)
65
+ skip = limit.first || 0
66
+ take = limit.last || sorted.length
67
+
68
+ sorted[skip...(skip + take)] || sorted
69
+ end
70
+
71
+ def lookup_from_pattern(pattern, element)
72
+ key = pattern.sub('*', element)
73
+
74
+ if (hash_parts = key.split('->')).length > 1
75
+ hget hash_parts.first, hash_parts.last
76
+ else
77
+ get key
78
+ end
79
+ end
80
+ end
81
+ 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..-1].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
75
+ members.each { |m| yield m }
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