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
@@ -0,0 +1,240 @@
1
+ class MockRedis
2
+ module GeospatialMethods
3
+ LNG_RANGE = (-180..180).freeze
4
+ LAT_RANGE = (-85.05112878..85.05112878).freeze
5
+ STEP = 26
6
+ UNITS = {
7
+ m: 1,
8
+ km: 1000,
9
+ ft: 0.3048,
10
+ mi: 1609.34
11
+ }.freeze
12
+ D_R = Math::PI / 180.0
13
+ EARTH_RADIUS_IN_METERS = 6_372_797.560856
14
+
15
+ ruby2_keywords def geoadd(key, *args)
16
+ points = parse_points(args)
17
+
18
+ scored_points = points.map do |point|
19
+ score = geohash_encode(point[:lng], point[:lat])[:bits]
20
+ [score.to_s, point[:key]]
21
+ end
22
+
23
+ zadd(key, scored_points)
24
+ end
25
+
26
+ def geodist(key, member1, member2, unit = 'm')
27
+ to_meter = parse_unit(unit)
28
+
29
+ return nil if zcard(key).zero?
30
+
31
+ score1 = zscore(key, member1)
32
+ score2 = zscore(key, member2)
33
+ return nil if score1.nil? || score2.nil?
34
+ hash1 = { bits: score1.to_i, step: STEP }
35
+ hash2 = { bits: score2.to_i, step: STEP }
36
+
37
+ lng1, lat1 = geohash_decode(hash1)
38
+ lng2, lat2 = geohash_decode(hash2)
39
+
40
+ distance = geohash_distance(lng1, lat1, lng2, lat2) / to_meter
41
+ format('%<distance>.4f', distance: distance)
42
+ end
43
+
44
+ def geohash(key, members)
45
+ lng_range = (-180..180)
46
+ lat_range = (-90..90)
47
+ geoalphabet = '0123456789bcdefghjkmnpqrstuvwxyz'
48
+
49
+ Array(members).map do |member|
50
+ score = zscore(key, member)
51
+ next nil unless score
52
+ score = score.to_i
53
+ hash = { bits: score, step: STEP }
54
+ lng, lat = geohash_decode(hash)
55
+ bits = geohash_encode(lng, lat, lng_range, lat_range)[:bits]
56
+ hash = ''
57
+ 11.times do |i|
58
+ shift = (52 - ((i + 1) * 5))
59
+ idx = shift > 0 ? (bits >> shift) & 0x1f : 0
60
+ hash << geoalphabet[idx]
61
+ end
62
+ hash
63
+ end
64
+ end
65
+
66
+ def geopos(key, members)
67
+ Array(members).map do |member|
68
+ score = zscore(key, member)
69
+ next nil unless score
70
+ hash = { bits: score.to_i, step: STEP }
71
+ lng, lat = geohash_decode(hash)
72
+ lng = format_decoded_coord(lng)
73
+ lat = format_decoded_coord(lat)
74
+ [lng, lat]
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def parse_points(args)
81
+ points = args.each_slice(3).to_a
82
+
83
+ if points.last.size != 3
84
+ raise Redis::CommandError,
85
+ "ERR wrong number of arguments for 'geoadd' command"
86
+ end
87
+
88
+ points.map do |point|
89
+ parse_point(point)
90
+ end
91
+ end
92
+
93
+ def parse_point(point)
94
+ lng = Float(point[0])
95
+ lat = Float(point[1])
96
+
97
+ unless LNG_RANGE.include?(lng) && LAT_RANGE.include?(lat)
98
+ lng = format('%<long>.6f', long: lng)
99
+ lat = format('%<lat>.6f', lat: lat)
100
+ raise Redis::CommandError,
101
+ "ERR invalid longitude,latitude pair #{lng},#{lat}"
102
+ end
103
+
104
+ { key: point[2], lng: lng, lat: lat }
105
+ rescue ArgumentError
106
+ raise Redis::CommandError, 'ERR value is not a valid float'
107
+ end
108
+
109
+ # Returns ZSET score for passed coordinates
110
+ def geohash_encode(lng, lat, lng_range = LNG_RANGE, lat_range = LAT_RANGE, step = STEP)
111
+ lat_offset = (lat - lat_range.min) / (lat_range.max - lat_range.min)
112
+ lng_offset = (lng - lng_range.min) / (lng_range.max - lng_range.min)
113
+
114
+ lat_offset *= (1 << step)
115
+ lng_offset *= (1 << step)
116
+
117
+ bits = interleave(lat_offset.to_i, lng_offset.to_i)
118
+
119
+ { bits: bits, step: step }
120
+ end
121
+
122
+ def interleave(x, y)
123
+ b = [0x5555555555555555, 0x3333333333333333, 0x0F0F0F0F0F0F0F0F,
124
+ 0x00FF00FF00FF00FF, 0x0000FFFF0000FFFF]
125
+ s = [1, 2, 4, 8, 16]
126
+
127
+ x = (x | (x << s[4])) & b[4]
128
+ y = (y | (y << s[4])) & b[4]
129
+
130
+ x = (x | (x << s[3])) & b[3]
131
+ y = (y | (y << s[3])) & b[3]
132
+
133
+ x = (x | (x << s[2])) & b[2]
134
+ y = (y | (y << s[2])) & b[2]
135
+
136
+ x = (x | (x << s[1])) & b[1]
137
+ y = (y | (y << s[1])) & b[1]
138
+
139
+ x = (x | (x << s[0])) & b[0]
140
+ y = (y | (y << s[0])) & b[0]
141
+
142
+ x | (y << 1)
143
+ end
144
+
145
+ # Decodes ZSET score to coordinates pair
146
+ def geohash_decode(hash, lng_range = LNG_RANGE, lat_range = LAT_RANGE)
147
+ area = calculate_approximate_area(hash, lng_range, lat_range)
148
+
149
+ lng = (area[:lng_min] + area[:lng_max]) / 2
150
+ lat = (area[:lat_min] + area[:lat_max]) / 2
151
+
152
+ [lng, lat]
153
+ end
154
+
155
+ def calculate_approximate_area(hash, lng_range, lat_range)
156
+ bits = hash[:bits]
157
+ step = hash[:step]
158
+ hash_sep = deinterleave(bits)
159
+
160
+ lat_scale = lat_range.max - lat_range.min
161
+ lng_scale = lng_range.max - lng_range.min
162
+
163
+ ilato = hash_sep & 0xFFFFFFFF # cast int64 to int32 to get lat part of deinterleaved hash
164
+ ilngo = hash_sep >> 32 # shift over to get lng part of hash
165
+
166
+ {
167
+ lat_min: lat_range.min + (ilato * 1.0 / (1 << step)) * lat_scale,
168
+ lat_max: lat_range.min + ((ilato + 1) * 1.0 / (1 << step)) * lat_scale,
169
+ lng_min: lng_range.min + (ilngo * 1.0 / (1 << step)) * lng_scale,
170
+ lng_max: lng_range.min + ((ilngo + 1) * 1.0 / (1 << step)) * lng_scale
171
+ }
172
+ end
173
+
174
+ def deinterleave(bits)
175
+ b = [0x5555555555555555, 0x3333333333333333, 0x0F0F0F0F0F0F0F0F,
176
+ 0x00FF00FF00FF00FF, 0x0000FFFF0000FFFF, 0x00000000FFFFFFFF]
177
+ s = [0, 1, 2, 4, 8, 16]
178
+
179
+ x = bits
180
+ y = bits >> 1
181
+
182
+ x = (x | (x >> s[0])) & b[0]
183
+ y = (y | (y >> s[0])) & b[0]
184
+
185
+ x = (x | (x >> s[1])) & b[1]
186
+ y = (y | (y >> s[1])) & b[1]
187
+
188
+ x = (x | (x >> s[2])) & b[2]
189
+ y = (y | (y >> s[2])) & b[2]
190
+
191
+ x = (x | (x >> s[3])) & b[3]
192
+ y = (y | (y >> s[3])) & b[3]
193
+
194
+ x = (x | (x >> s[4])) & b[4]
195
+ y = (y | (y >> s[4])) & b[4]
196
+
197
+ x = (x | (x >> s[5])) & b[5]
198
+ y = (y | (y >> s[5])) & b[5]
199
+
200
+ x | (y << 32)
201
+ end
202
+
203
+ def format_decoded_coord(coord)
204
+ coord = format('%<coord>.17f', coord: coord)
205
+ l = 1
206
+ l += 1 while coord[-l] == '0'
207
+ coord = coord[0..-l]
208
+ coord[-1] == '.' ? coord[0..-2] : coord
209
+ end
210
+
211
+ def parse_unit(unit)
212
+ unit = unit.to_sym
213
+ return UNITS[unit] if UNITS[unit]
214
+
215
+ raise Redis::CommandError,
216
+ 'ERR unsupported unit provided. please use m, km, ft, mi'
217
+ end
218
+
219
+ def geohash_distance(lng1d, lat1d, lng2d, lat2d)
220
+ lat1r = deg_rad(lat1d)
221
+ lng1r = deg_rad(lng1d)
222
+ lat2r = deg_rad(lat2d)
223
+ lng2r = deg_rad(lng2d)
224
+
225
+ u = Math.sin((lat2r - lat1r) / 2)
226
+ v = Math.sin((lng2r - lng1r) / 2)
227
+
228
+ 2.0 * EARTH_RADIUS_IN_METERS *
229
+ Math.asin(Math.sqrt(u * u + Math.cos(lat1r) * Math.cos(lat2r) * v * v))
230
+ end
231
+
232
+ def deg_rad(ang)
233
+ ang * D_R
234
+ end
235
+
236
+ def rad_deg(ang)
237
+ ang / D_R
238
+ end
239
+ end
240
+ end
@@ -6,41 +6,61 @@ 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
- hash.delete_if { |k,v| field.include?(k) }
14
- orig_size - hash.size
15
- else
16
- 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"
17
16
  end
17
+
18
+ hash.delete_if { |k, _v| fields.include?(k) }
19
+ orig_size - hash.size
18
20
  end
19
21
  end
20
22
 
21
23
  def hexists(key, field)
22
- with_hash_at(key) {|h| h.has_key?(field.to_s)}
24
+ with_hash_at(key) { |h| h.key?(field.to_s) }
23
25
  end
24
26
 
25
27
  def hget(key, field)
26
- with_hash_at(key) {|h| h[field.to_s]}
28
+ with_hash_at(key) { |h| h[field.to_s] }
27
29
  end
28
30
 
29
31
  def hgetall(key)
30
- with_hash_at(key) {|h| h}
32
+ with_hash_at(key) { |h| h }
31
33
  end
32
34
 
33
35
  def hincrby(key, field, increment)
34
36
  with_hash_at(key) do |hash|
35
37
  field = field.to_s
36
38
  unless can_incr?(data[key][field])
37
- raise Redis::CommandError, "ERR hash value is not an integer"
39
+ raise Redis::CommandError, 'ERR hash value is not an integer'
38
40
  end
39
41
  unless looks_like_integer?(increment.to_s)
40
- raise Redis::CommandError, "ERR value is not an integer or out of range"
42
+ raise Redis::CommandError, 'ERR value is not an integer or out of range'
41
43
  end
42
44
 
43
- new_value = (hash[field] || "0").to_i + increment.to_i
45
+ new_value = (hash[field] || '0').to_i + increment.to_i
46
+ hash[field] = new_value.to_s
47
+ new_value
48
+ end
49
+ end
50
+
51
+ def hincrbyfloat(key, field, increment)
52
+ with_hash_at(key) do |hash|
53
+ field = field.to_s
54
+ unless can_incr_float?(data[key][field])
55
+ raise Redis::CommandError, 'ERR hash value is not a float'
56
+ end
57
+
58
+ unless looks_like_float?(increment.to_s)
59
+ raise Redis::CommandError, 'ERR value is not a valid float'
60
+ end
61
+
62
+ new_value = (hash[field] || '0').to_f + increment.to_f
63
+ new_value = new_value.to_i if new_value % 1 == 0
44
64
  hash[field] = new_value.to_s
45
65
  new_value
46
66
  end
@@ -55,22 +75,36 @@ class MockRedis
55
75
  end
56
76
 
57
77
  def hmget(key, *fields)
78
+ fields.flatten!
79
+
58
80
  assert_has_args(fields, 'hmget')
59
- fields.map{|f| hget(key, f)}
81
+ fields.map { |f| hget(key, f) }
60
82
  end
61
83
 
62
84
  def mapped_hmget(key, *fields)
63
85
  reply = hmget(key, *fields)
64
- Hash[*fields.zip(reply).flatten]
86
+ if reply.is_a?(Array)
87
+ Hash[fields.zip(reply)]
88
+ else
89
+ reply
90
+ end
65
91
  end
66
92
 
67
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..-1]
97
+ key = key[0]
98
+ end
99
+
100
+ kvpairs.flatten!
68
101
  assert_has_args(kvpairs, 'hmset')
102
+
69
103
  if kvpairs.length.odd?
70
- raise Redis::CommandError, "ERR wrong number of arguments for HMSET"
104
+ raise Redis::CommandError, err_msg || 'ERR wrong number of arguments for \'hmset\' command'
71
105
  end
72
106
 
73
- kvpairs.each_slice(2) do |(k,v)|
107
+ kvpairs.each_slice(2) do |(k, v)|
74
108
  hset(key, k, v)
75
109
  end
76
110
  'OK'
@@ -86,9 +120,34 @@ class MockRedis
86
120
  hmset(key, *kvpairs)
87
121
  end
88
122
 
89
- def hset(key, field, value)
90
- with_hash_at(key) {|h| h[field.to_s] = value.to_s}
91
- true
123
+ def hscan(key, cursor, opts = {})
124
+ opts = opts.merge(key: lambda { |x| x[0] })
125
+ common_scan(hgetall(key).to_a, cursor, opts)
126
+ end
127
+
128
+ def hscan_each(key, opts = {}, &block)
129
+ return to_enum(:hscan_each, key, opts) unless block_given?
130
+ cursor = 0
131
+ loop do
132
+ cursor, values = hscan(key, cursor, opts)
133
+ values.each(&block)
134
+ break if cursor == '0'
135
+ end
136
+ end
137
+
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
92
151
  end
93
152
 
94
153
  def hsetnx(key, field, value)
@@ -107,18 +166,18 @@ class MockRedis
107
166
  private
108
167
 
109
168
  def with_hash_at(key, &blk)
110
- with_thing_at(key, :assert_hashy, proc {{}}, &blk)
169
+ with_thing_at(key.to_s, :assert_hashy, proc { {} }, &blk)
111
170
  end
112
171
 
113
172
  def hashy?(key)
114
- data[key].nil? || data[key].kind_of?(Hash)
173
+ data[key].nil? || data[key].is_a?(Hash)
115
174
  end
116
175
 
117
176
  def assert_hashy(key)
118
177
  unless hashy?(key)
119
- raise Redis::CommandError, "ERR Operation against a key holding the wrong kind of value"
178
+ raise Redis::CommandError,
179
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
120
180
  end
121
181
  end
122
-
123
182
  end
124
183
  end
@@ -0,0 +1,11 @@
1
+ class MockRedis
2
+ class IndifferentHash < Hash
3
+ def has_key?(key)
4
+ super(key.to_s)
5
+ end
6
+
7
+ def key?(key)
8
+ super(key.to_s)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,160 @@
1
+ class MockRedis
2
+ module InfoMethod
3
+ SERVER_INFO = {
4
+ 'arch_bits' => '64',
5
+ 'gcc_version' => '4.2.1',
6
+ 'lru_clock' => '1036434',
7
+ 'multiplexing_api' => 'kqueue',
8
+ 'os' => 'Darwin 12.2.1 x86_64',
9
+ 'process_id' => '14508',
10
+ 'redis_git_dirty' => '0',
11
+ 'redis_git_sha1' => '00000000',
12
+ 'redis_mode' => 'standalone',
13
+ 'redis_version' => '2.6.10',
14
+ 'run_id' => '1f3f7af2fc11eb68011eb43e154adce125c85e9a',
15
+ 'tcp_port' => '6379',
16
+ 'uptime_in_days' => '0',
17
+ 'uptime_in_seconds' => '84215',
18
+ }.freeze
19
+
20
+ CLIENTS_INFO = {
21
+ 'blocked_clients' => '0',
22
+ 'client_biggest_input_buf' => '0',
23
+ 'client_longest_output_list' => '0',
24
+ 'connected_clients' => '1',
25
+ }.freeze
26
+
27
+ MEMORY_INFO = {
28
+ 'mem_allocator' => 'libc',
29
+ 'mem_fragmentation_ratio' => '2.04',
30
+ 'used_memory' => '931456',
31
+ 'used_memory_human' => '909.62K',
32
+ 'used_memory_lua' => '31744',
33
+ 'used_memory_peak' => '1005424',
34
+ 'used_memory_peak_human' => '981.86K',
35
+ 'used_memory_rss' => '1904640',
36
+ }.freeze
37
+
38
+ PERSISTENCE_INFO = {
39
+ 'aof_current_rewrite_time_sec' => '-1',
40
+ 'aof_enabled' => '0',
41
+ 'aof_last_bgrewrite_status' => 'ok',
42
+ 'aof_last_rewrite_time_sec' => '-1',
43
+ 'aof_rewrite_in_progress' => '0',
44
+ 'aof_rewrite_scheduled' => '0',
45
+ 'loading' => '0',
46
+ 'rdb_bgsave_in_progress' => '0',
47
+ 'rdb_changes_since_last_save' => '0',
48
+ 'rdb_current_bgsave_time_sec' => '-1',
49
+ 'rdb_last_bgsave_status' => 'ok',
50
+ 'rdb_last_bgsave_time_sec' => '-1',
51
+ 'rdb_last_save_time' => '1361423635',
52
+ }.freeze
53
+
54
+ STATS_INFO = {
55
+ 'evicted_keys' => '0',
56
+ 'expired_keys' => '0',
57
+ 'instantaneous_ops_per_sec' => '0',
58
+ 'keyspace_hits' => '62645',
59
+ 'keyspace_misses' => '29757',
60
+ 'latest_fork_usec' => '0',
61
+ 'pubsub_channels' => '0',
62
+ 'pubsub_patterns' => '0',
63
+ 'rejected_connections' => '0',
64
+ 'total_commands_processed' => '196800',
65
+ 'total_connections_received' => '4359',
66
+ }.freeze
67
+
68
+ REPLICATION_INFO = {
69
+ 'role' => 'master',
70
+ 'connected_slaves' => '0',
71
+ }.freeze
72
+
73
+ CPU_INFO = {
74
+ 'used_cpu_sys' => '5.54',
75
+ 'used_cpu_sys_childrens' => '0.00',
76
+ 'used_cpu_user' => '7.65',
77
+ 'used_cpu_user_childrens' => '0.02',
78
+ }.freeze
79
+
80
+ KEYSPACE_INFO = {
81
+ 'db0' => 'keys=8,expires=0',
82
+ }.freeze
83
+
84
+ # The Ruby Redis client returns commandstats differently when it's called as
85
+ # "INFO commandstats".
86
+ # rubocop:disable Layout/LineLength
87
+ COMMAND_STATS_SOLO_INFO = {
88
+ 'auth' => { 'calls' => '572501', 'usec' => '2353163', 'usec_per_call' => '4.11' },
89
+ 'client' => { 'calls' => '1', 'usec' => '80', 'usec_per_call' => '80.00' },
90
+ 'config' => { 'calls' => '4', 'usec' => '71', 'usec_per_call' => '17.75' },
91
+ 'flushall' => { 'calls' => '2097790', 'usec' => '27473940655', 'usec_per_call' => '13096.61' },
92
+ 'flushdb' => { 'calls' => '692247', 'usec' => '2235143397', 'usec_per_call' => '3228.82' },
93
+ 'get' => { 'calls' => '185724', 'usec' => '604479', 'usec_per_call' => '3.25' },
94
+ 'info' => { 'calls' => '965566', 'usec' => '229377331', 'usec_per_call' => '237.56' },
95
+ 'keys' => { 'calls' => '16591', 'usec' => '917676', 'usec_per_call' => '55.31' },
96
+ 'llen' => { 'calls' => '39', 'usec' => '150', 'usec_per_call' => '3.85' },
97
+ 'ping' => { 'calls' => '697509', 'usec' => '3836044', 'usec_per_call' => '5.50' },
98
+ 'rpush' => { 'calls' => '2239810', 'usec' => '15013298', 'usec_per_call' => '6.70' },
99
+ 'sadd' => { 'calls' => '13950129156', 'usec' => '30126628979', 'usec_per_call' => '2.16' },
100
+ 'scard' => { 'calls' => '153929', 'usec' => '726596', 'usec_per_call' => '4.72' },
101
+ 'set' => { 'calls' => '6975081982', 'usec' => '16453671451', 'usec_per_call' => '2.36' },
102
+ 'slowlog' => { 'calls' => '136', 'usec' => '16815', 'usec_per_call' => '123.64' },
103
+ 'smembers' => { 'calls' => '58', 'usec' => '231', 'usec_per_call' => '3.98' },
104
+ 'sunionstore' => { 'calls' => '4185027', 'usec' => '11762454022', 'usec_per_call' => '2810.60' },
105
+ }.freeze
106
+
107
+ COMMAND_STATS_COMBINED_INFO = {
108
+ 'cmdstat_auth' => 'calls=572506,usec=2353182,usec_per_call=4.11',
109
+ 'cmdstat_client' => 'calls=1,usec=80,usec_per_call=80.00',
110
+ 'cmdstat_config' => 'calls=4,usec=71,usec_per_call=17.75',
111
+ 'cmdstat_flushall' => 'calls=2097790,usec=27473940655,usec_per_call=13096.61',
112
+ 'cmdstat_flushdb' => 'calls=692247,usec=2235143397,usec_per_call=3228.82',
113
+ 'cmdstat_get' => 'calls=185724,usec=604479,usec_per_call=3.25',
114
+ 'cmdstat_info' => 'calls=965571,usec=229378327,usec_per_call=237.56',
115
+ 'cmdstat_keys' => 'calls=16591,usec=917676,usec_per_call=55.31',
116
+ 'cmdstat_llen' => 'calls=39,usec=150,usec_per_call=3.85',
117
+ 'cmdstat_ping' => 'calls=697509,usec=3836044,usec_per_call=5.50',
118
+ 'cmdstat_rpush' => 'calls=2239810,usec=15013298,usec_per_call=6.70',
119
+ 'cmdstat_sadd' => 'calls=13950129156,usec=30126628979,usec_per_call=2.16',
120
+ 'cmdstat_scard' => 'calls=153929,usec=726596,usec_per_call=4.72',
121
+ 'cmdstat_set' => 'calls=6975081982,usec=16453671451,usec_per_call=2.36',
122
+ 'cmdstat_slowlog' => 'calls=136,usec=16815,usec_per_call=123.64',
123
+ 'cmdstat_smembers' => 'calls=58,usec=231,usec_per_call=3.98',
124
+ 'cmdstat_sunionstore' => 'calls=4185027,usec=11762454022,usec_per_call=2810.60',
125
+ }.freeze
126
+ # rubocop:enable Layout/LineLength
127
+
128
+ DEFAULT_INFO = [
129
+ SERVER_INFO,
130
+ CLIENTS_INFO,
131
+ MEMORY_INFO,
132
+ PERSISTENCE_INFO,
133
+ STATS_INFO,
134
+ REPLICATION_INFO,
135
+ CPU_INFO,
136
+ KEYSPACE_INFO,
137
+ ].inject({}) { |memo, info| memo.merge(info) }
138
+
139
+ ALL_INFO = [
140
+ DEFAULT_INFO,
141
+ COMMAND_STATS_COMBINED_INFO,
142
+ ].inject({}) { |memo, info| memo.merge(info) }
143
+
144
+ def info(section = :default)
145
+ case section
146
+ when :default; DEFAULT_INFO
147
+ when :all; ALL_INFO
148
+ when :server; SERVER_INFO
149
+ when :clients; CLIENTS_INFO
150
+ when :memory; MEMORY_INFO
151
+ when :persistence; PERSISTENCE_INFO
152
+ when :stats; STATS_INFO
153
+ when :replication; REPLICATION_INFO
154
+ when :cpu; CPU_INFO
155
+ when :keyspace; KEYSPACE_INFO
156
+ when :commandstats; COMMAND_STATS_SOLO_INFO
157
+ end
158
+ end
159
+ end
160
+ end