mock_redis 0.5.4 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
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