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,102 @@
1
+ require 'mock_redis/assertions'
2
+ require 'mock_redis/utility_methods'
3
+ require 'mock_redis/stream'
4
+
5
+ # TODO: Implement the following commands
6
+ #
7
+ # * xgroup
8
+ # * xreadgroup
9
+ # * xack
10
+ # * xpending
11
+ # * xclaim
12
+ # * xinfo
13
+ # * xtrim
14
+ # * xdel
15
+ #
16
+ # TODO: Complete support for
17
+ #
18
+ # * xtrim
19
+ # - `approximate: true` argument is currently ignored
20
+ # * xadd
21
+ # - `approximate: true` argument (for capped streams) is currently ignored
22
+ #
23
+ # For details of these commands see
24
+ # * https://redis.io/topics/streams-intro
25
+ # * https://redis.io/commands#stream
26
+
27
+ class MockRedis
28
+ module StreamMethods
29
+ include Assertions
30
+ include UtilityMethods
31
+
32
+ def xadd(key, entry, opts = {})
33
+ id = opts[:id] || '*'
34
+ with_stream_at(key) do |stream|
35
+ stream.add id, entry
36
+ stream.trim opts[:maxlen] if opts[:maxlen]
37
+ return stream.last_id
38
+ end
39
+ end
40
+
41
+ def xtrim(key, count)
42
+ with_stream_at(key) do |stream|
43
+ stream.trim count
44
+ end
45
+ end
46
+
47
+ def xlen(key)
48
+ with_stream_at(key) do |stream|
49
+ return stream.count
50
+ end
51
+ end
52
+
53
+ def xrange(key, first = '-', last = '+', count: nil)
54
+ args = [first, last, false]
55
+ args += ['COUNT', count] if count
56
+ with_stream_at(key) do |stream|
57
+ return stream.range(*args)
58
+ end
59
+ end
60
+
61
+ def xrevrange(key, last = '+', first = '-', count: nil)
62
+ args = [first, last, true]
63
+ args += ['COUNT', count] if count
64
+ with_stream_at(key) do |stream|
65
+ return stream.range(*args)
66
+ end
67
+ end
68
+
69
+ def xread(keys, ids, count: nil, block: nil)
70
+ args = []
71
+ args += ['COUNT', count] if count
72
+ args += ['BLOCK', block.to_i] if block
73
+ result = {}
74
+ keys = keys.is_a?(Array) ? keys : [keys]
75
+ ids = ids.is_a?(Array) ? ids : [ids]
76
+ keys.each_with_index do |key, index|
77
+ with_stream_at(key) do |stream|
78
+ data = stream.read(ids[index], *args)
79
+ result[key] = data unless data.empty?
80
+ end
81
+ end
82
+ result
83
+ end
84
+
85
+ private
86
+
87
+ def with_stream_at(key, &blk)
88
+ with_thing_at(key, :assert_streamy, proc { Stream.new }, &blk)
89
+ end
90
+
91
+ def streamy?(key)
92
+ data[key].nil? || data[key].is_a?(Stream)
93
+ end
94
+
95
+ def assert_streamy(key)
96
+ unless streamy?(key)
97
+ raise Redis::CommandError,
98
+ 'WRONGTYPE Operation against a key holding the wrong kind of value'
99
+ end
100
+ end
101
+ end
102
+ end
@@ -3,14 +3,79 @@ require 'mock_redis/assertions'
3
3
  class MockRedis
4
4
  module StringMethods
5
5
  include Assertions
6
+ include UtilityMethods
6
7
 
7
8
  def append(key, value)
8
9
  assert_stringy(key)
9
- data[key] ||= ""
10
+ data[key] ||= ''
10
11
  data[key] << value
11
12
  data[key].length
12
13
  end
13
14
 
15
+ def bitfield(*args)
16
+ if args.length < 4
17
+ raise Redis::CommandError, 'ERR wrong number of arguments for BITFIELD'
18
+ end
19
+
20
+ key = args.shift
21
+ output = []
22
+ overflow_method = 'wrap'
23
+
24
+ until args.empty?
25
+ command = args.shift.to_s
26
+
27
+ if command == 'overflow'
28
+ new_overflow_method = args.shift.to_s.downcase
29
+
30
+ unless %w[wrap sat fail].include? new_overflow_method
31
+ raise Redis::CommandError, 'ERR Invalid OVERFLOW type specified'
32
+ end
33
+
34
+ overflow_method = new_overflow_method
35
+ next
36
+ end
37
+
38
+ type, offset = args.shift(2)
39
+
40
+ is_signed = type.slice(0) == 'i'
41
+ type_size = type[1..-1].to_i
42
+
43
+ if (type_size > 64 && is_signed) || (type_size >= 64 && !is_signed)
44
+ raise Redis::CommandError,
45
+ 'ERR Invalid bitfield type. Use something like i16 u8. ' \
46
+ 'Note that u64 is not supported but i64 is.'
47
+ end
48
+
49
+ if offset.to_s[0] == '#'
50
+ offset = offset[1..-1].to_i * type_size
51
+ end
52
+
53
+ bits = []
54
+
55
+ type_size.times do |i|
56
+ bits.push(getbit(key, offset + i))
57
+ end
58
+
59
+ val = is_signed ? twos_complement_decode(bits) : bits.join('').to_i(2)
60
+
61
+ case command
62
+ when 'get'
63
+ output.push(val)
64
+ when 'set'
65
+ output.push(val)
66
+
67
+ set_bitfield(key, args.shift.to_i, is_signed, type_size, offset)
68
+ when 'incrby'
69
+ new_val = incr_bitfield(val, args.shift.to_i, is_signed, type_size, overflow_method)
70
+
71
+ set_bitfield(key, new_val, is_signed, type_size, offset) if new_val
72
+ output.push(new_val)
73
+ end
74
+ end
75
+
76
+ output
77
+ end
78
+
14
79
  def decr(key)
15
80
  decrby(key, 1)
16
81
  end
@@ -20,22 +85,20 @@ class MockRedis
20
85
  end
21
86
 
22
87
  def get(key)
88
+ key = key.to_s
23
89
  assert_stringy(key)
24
90
  data[key]
25
91
  end
26
92
 
27
- def [](key)
28
- get(key)
29
- end
30
-
31
93
  def getbit(key, offset)
32
94
  assert_stringy(key)
33
95
 
96
+ offset = offset.to_i
34
97
  offset_of_byte = offset / 8
35
98
  offset_within_byte = offset % 8
36
99
 
37
100
  # String#getbyte would be lovely, but it's not in 1.8.7.
38
- byte = (data[key] || "").each_byte.drop(offset_of_byte).first
101
+ byte = (data[key] || '').each_byte.drop(offset_of_byte).first
39
102
 
40
103
  if byte
41
104
  (byte & (2**7 >> offset_within_byte)) > 0 ? 1 : 0
@@ -46,7 +109,7 @@ class MockRedis
46
109
 
47
110
  def getrange(key, start, stop)
48
111
  assert_stringy(key)
49
- (data[key] || "")[start..stop]
112
+ (data[key] || '')[start..stop]
50
113
  end
51
114
 
52
115
  def getset(key, value)
@@ -62,11 +125,11 @@ class MockRedis
62
125
  def incrby(key, n)
63
126
  assert_stringy(key)
64
127
  unless can_incr?(data[key])
65
- raise Redis::CommandError, "ERR value is not an integer or out of range"
128
+ raise Redis::CommandError, 'ERR value is not an integer or out of range'
66
129
  end
67
130
 
68
131
  unless looks_like_integer?(n.to_s)
69
- raise Redis::CommandError, "ERR value is not an integer or out of range"
132
+ raise Redis::CommandError, 'ERR value is not an integer or out of range'
70
133
  end
71
134
 
72
135
  new_value = data[key].to_i + n.to_i
@@ -78,11 +141,11 @@ class MockRedis
78
141
  def incrbyfloat(key, n)
79
142
  assert_stringy(key)
80
143
  unless can_incr_float?(data[key])
81
- raise Redis::CommandError, "ERR value is not a valid float"
144
+ raise Redis::CommandError, 'ERR value is not a valid float'
82
145
  end
83
146
 
84
147
  unless looks_like_float?(n.to_s)
85
- raise Redis::CommandError, "ERR value is not a valid float"
148
+ raise Redis::CommandError, 'ERR value is not a valid float'
86
149
  end
87
150
 
88
151
  new_value = data[key].to_f + n.to_f
@@ -91,31 +154,45 @@ class MockRedis
91
154
  new_value
92
155
  end
93
156
 
94
- def mget(*keys)
157
+ def mget(*keys, &blk)
158
+ keys.flatten!
159
+
95
160
  assert_has_args(keys, 'mget')
96
161
 
97
- keys.map do |key|
162
+ data = keys.map do |key|
98
163
  get(key) if stringy?(key)
99
164
  end
165
+
166
+ blk ? blk.call(data) : data
167
+ end
168
+
169
+ def mapped_mget(*keys)
170
+ Hash[keys.zip(mget(*keys))]
100
171
  end
101
172
 
102
173
  def mset(*kvpairs)
103
174
  assert_has_args(kvpairs, 'mset')
175
+ kvpairs = kvpairs.first if kvpairs.size == 1 && kvpairs.first.is_a?(Enumerable)
176
+
104
177
  if kvpairs.length.odd?
105
- raise Redis::CommandError, "ERR wrong number of arguments for MSET"
178
+ raise Redis::CommandError, 'ERR wrong number of arguments for MSET'
106
179
  end
107
180
 
108
- kvpairs.each_slice(2) do |(k,v)|
109
- set(k,v)
181
+ kvpairs.each_slice(2) do |(k, v)|
182
+ set(k, v)
110
183
  end
111
184
 
112
- "OK"
185
+ 'OK'
186
+ end
187
+
188
+ def mapped_mset(hash)
189
+ mset(*hash.to_a.flatten)
113
190
  end
114
191
 
115
192
  def msetnx(*kvpairs)
116
193
  assert_has_args(kvpairs, 'msetnx')
117
194
 
118
- if kvpairs.each_slice(2).any? {|(k,v)| exists(k)}
195
+ if kvpairs.each_slice(2).any? { |(k, _)| exists?(k) }
119
196
  false
120
197
  else
121
198
  mset(*kvpairs)
@@ -123,26 +200,62 @@ class MockRedis
123
200
  end
124
201
  end
125
202
 
126
- def set(key, value)
127
- data[key] = value.to_s
128
- 'OK'
203
+ def mapped_msetnx(hash)
204
+ msetnx(*hash.to_a.flatten)
129
205
  end
130
206
 
131
- def []=(key, value)
132
- set(key, value)
207
+ # Parameer list required to ensure the ArgumentError is returned correctly
208
+ # rubocop:disable Metrics/ParameterLists
209
+ def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil)
210
+ key = key.to_s
211
+ return_true = false
212
+ if nx
213
+ if exists?(key)
214
+ return false
215
+ else
216
+ return_true = true
217
+ end
218
+ end
219
+ if xx
220
+ if exists?(key)
221
+ return_true = true
222
+ else
223
+ return false
224
+ end
225
+ end
226
+ data[key] = value.to_s
227
+
228
+ remove_expiration(key) unless keepttl
229
+ if ex
230
+ if ex == 0
231
+ raise Redis::CommandError, 'ERR invalid expire time in set'
232
+ end
233
+ expire(key, ex)
234
+ end
235
+
236
+ if px
237
+ if px == 0
238
+ raise Redis::CommandError, 'ERR invalid expire time in set'
239
+ end
240
+ pexpire(key, px)
241
+ end
242
+
243
+ return_true ? true : 'OK'
133
244
  end
245
+ # rubocop:enable Metrics/ParameterLists
134
246
 
135
247
  def setbit(key, offset, value)
136
- assert_stringy(key, "ERR bit is not an integer or out of range")
248
+ assert_stringy(key, 'ERR bit is not an integer or out of range')
137
249
  retval = getbit(key, offset)
138
250
 
139
- str = data[key] || ""
251
+ str = data[key] || ''
140
252
 
253
+ offset = offset.to_i
141
254
  offset_of_byte = offset / 8
142
255
  offset_within_byte = offset % 8
143
256
 
144
257
  if offset_of_byte >= str.bytesize
145
- str = zero_pad(str, offset_of_byte+1)
258
+ str = zero_pad(str, offset_of_byte + 1)
146
259
  end
147
260
 
148
261
  char_index = byte_index = offset_within_char = 0
@@ -157,27 +270,66 @@ class MockRedis
157
270
  end
158
271
 
159
272
  char = str[char_index]
160
- char = char.chr if char.respond_to?(:chr) # ruby 1.8 vs 1.9
273
+ char = char.chr if char.respond_to?(:chr) # ruby 1.8 vs 1.9
161
274
  char_as_number = char.each_byte.reduce(0) do |a, byte|
162
275
  (a << 8) + byte
163
276
  end
164
- char_as_number |=
165
- (2**((char.bytesize * 8)-1) >>
166
- (offset_within_char * 8 + offset_within_byte))
277
+
278
+ bitmask_length = (char.bytesize * 8 - offset_within_char * 8 - offset_within_byte - 1)
279
+ bitmask = 1 << bitmask_length
280
+
281
+ if value.zero?
282
+ bitmask ^= 2**(char.bytesize * 8) - 1
283
+ char_as_number &= bitmask
284
+ else
285
+ char_as_number |= bitmask
286
+ end
287
+
167
288
  str[char_index] = char_as_number.chr
168
289
 
169
290
  data[key] = str
170
291
  retval
171
292
  end
172
293
 
294
+ def bitcount(key, start = 0, stop = -1)
295
+ assert_stringy(key)
296
+
297
+ str = data[key] || ''
298
+ count = 0
299
+ m1 = 0x5555555555555555
300
+ m2 = 0x3333333333333333
301
+ m4 = 0x0f0f0f0f0f0f0f0f
302
+ m8 = 0x00ff00ff00ff00ff
303
+ m16 = 0x0000ffff0000ffff
304
+ m32 = 0x00000000ffffffff
305
+
306
+ str.bytes.to_a[start..stop].each do |byte|
307
+ # Naive Hamming weight
308
+ c = byte
309
+ c = (c & m1) + ((c >> 1) & m1)
310
+ c = (c & m2) + ((c >> 2) & m2)
311
+ c = (c & m4) + ((c >> 4) & m4)
312
+ c = (c & m8) + ((c >> 8) & m8)
313
+ c = (c & m16) + ((c >> 16) & m16)
314
+ c = (c & m32) + ((c >> 32) & m32)
315
+ count += c
316
+ end
317
+
318
+ count
319
+ end
320
+
173
321
  def setex(key, seconds, value)
174
- set(key, value)
175
- expire(key, seconds)
176
- 'OK'
322
+ if seconds <= 0
323
+ raise Redis::CommandError, 'ERR invalid expire time in setex'
324
+ else
325
+ set(key, value)
326
+ expire(key, seconds)
327
+ 'OK'
328
+ end
177
329
  end
178
330
 
179
331
  def setnx(key, value)
180
- if exists(key)
332
+ if exists?(key)
181
333
  false
182
334
  else
183
335
  set(key, value)
@@ -188,32 +340,73 @@ class MockRedis
188
340
  def setrange(key, offset, value)
189
341
  assert_stringy(key)
190
342
  value = value.to_s
191
- old_value = (data[key] || "")
343
+ old_value = (data[key] || '')
192
344
 
193
345
  prefix = zero_pad(old_value[0...offset], offset)
194
- data[key] = prefix + value + (old_value[(offset + value.length)..-1] || "")
346
+ data[key] = prefix + value + (old_value[(offset + value.length)..-1] || '')
195
347
  data[key].length
196
348
  end
197
349
 
198
350
  def strlen(key)
199
351
  assert_stringy(key)
200
- (data[key] || "").bytesize
352
+ (data[key] || '').bytesize
201
353
  end
202
354
 
203
-
204
-
205
-
206
355
  private
356
+
207
357
  def stringy?(key)
208
- data[key].nil? || data[key].kind_of?(String)
358
+ data[key].nil? || data[key].is_a?(String)
209
359
  end
210
360
 
211
361
  def assert_stringy(key,
212
- message="ERR Operation against a key holding the wrong kind of value")
362
+ message = 'WRONGTYPE Operation against a key holding the wrong kind of value')
213
363
  unless stringy?(key)
214
364
  raise Redis::CommandError, message
215
365
  end
216
366
  end
217
367
 
368
+ def set_bitfield(key, value, is_signed, type_size, offset)
369
+ if is_signed
370
+ val_array = twos_complement_encode(value, type_size)
371
+ else
372
+ str = left_pad(value.to_i.abs.to_s(2), type_size)
373
+ val_array = str.split('').map(&:to_i)
374
+ end
375
+
376
+ val_array.each_with_index do |bit, i|
377
+ setbit(key, offset + i, bit)
378
+ end
379
+ end
380
+
381
+ def incr_bitfield(val, incrby, is_signed, type_size, overflow_method)
382
+ new_val = val + incrby
383
+
384
+ max = is_signed ? (2**(type_size - 1)) - 1 : (2**type_size) - 1
385
+ min = is_signed ? (-2**(type_size - 1)) : 0
386
+ size = 2**type_size
387
+
388
+ return new_val if (min..max).cover?(new_val)
389
+
390
+ case overflow_method
391
+ when 'fail'
392
+ new_val = nil
393
+ when 'sat'
394
+ new_val = new_val > max ? max : min
395
+ when 'wrap'
396
+ if is_signed
397
+ if new_val > max
398
+ remainder = new_val - (max + 1)
399
+ new_val = min + remainder.abs
400
+ else
401
+ remainder = new_val - (min - 1)
402
+ new_val = max - remainder.abs
403
+ end
404
+ else
405
+ new_val = new_val > max ? new_val % size : size - new_val.abs
406
+ end
407
+ end
408
+
409
+ new_val
410
+ end
218
411
  end
219
412
  end
@@ -4,20 +4,27 @@ class MockRedis
4
4
  class TransactionWrapper
5
5
  include UndefRedisMethods
6
6
 
7
- def respond_to?(method, include_private=false)
7
+ def respond_to?(method, include_private = false)
8
8
  super || @db.respond_to?(method)
9
9
  end
10
10
 
11
11
  def initialize(db)
12
12
  @db = db
13
- @queued_commands = []
14
- @in_multi = false
13
+ @transaction_futures = []
14
+ @multi_stack = []
15
+ @multi_block_given = false
15
16
  end
16
17
 
17
- def method_missing(method, *args, &block)
18
- if @in_multi
19
- @queued_commands << [method, *args]
20
- 'QUEUED'
18
+ ruby2_keywords def method_missing(method, *args, &block)
19
+ if in_multi?
20
+ future = MockRedis::Future.new([method, *args], block)
21
+ @transaction_futures << future
22
+
23
+ if @multi_block_given
24
+ future
25
+ else
26
+ 'QUEUED'
27
+ end
21
28
  else
22
29
  @db.expire_keys
23
30
  @db.send(method, *args, &block)
@@ -27,59 +34,86 @@ class MockRedis
27
34
  def initialize_copy(source)
28
35
  super
29
36
  @db = @db.clone
30
- @queued_commands = @queued_commands.clone
37
+ @transaction_futures = @transaction_futures.clone
38
+ @multi_stack = @multi_stack.clone
31
39
  end
32
40
 
33
41
  def discard
34
- unless @in_multi
35
- raise Redis::CommandError, "ERR DISCARD without MULTI"
42
+ unless in_multi?
43
+ raise Redis::CommandError, 'ERR DISCARD without MULTI'
36
44
  end
37
- @in_multi = false
38
- @queued_commands = []
45
+ pop_multi
46
+
47
+ @transaction_futures = []
39
48
  'OK'
40
49
  end
41
50
 
42
51
  def exec
43
- unless @in_multi
44
- raise Redis::CommandError, "ERR EXEC without MULTI"
52
+ unless in_multi?
53
+ raise Redis::CommandError, 'ERR EXEC without MULTI'
45
54
  end
46
- @in_multi = false
47
- responses = @queued_commands.map do |cmd|
55
+ pop_multi
56
+ return if in_multi?
57
+ @multi_block_given = false
58
+
59
+ responses = @transaction_futures.map do |future|
48
60
  begin
49
- send(*cmd)
50
- rescue => e
61
+ result = send(*future.command)
62
+ future.store_result(result)
63
+ future.value
64
+ rescue StandardError => e
51
65
  e
52
66
  end
53
67
  end
54
- @queued_commands = []
68
+
69
+ @transaction_futures = []
55
70
  responses
56
71
  end
57
72
 
73
+ def in_multi?
74
+ @multi_stack.any?
75
+ end
76
+
77
+ def push_multi
78
+ @multi_stack.push(@multi_stack.size + 1)
79
+ end
80
+
81
+ def pop_multi
82
+ @multi_stack.pop
83
+ end
84
+
58
85
  def multi
59
- if @in_multi
60
- raise Redis::CommandError, "ERR MULTI calls can not be nested"
61
- end
62
- @in_multi = true
63
86
  if block_given?
87
+ push_multi
88
+ @multi_block_given = true
64
89
  begin
65
90
  yield(self)
66
- self.exec
91
+ exec
67
92
  rescue StandardError => e
68
- self.discard
93
+ discard
69
94
  raise e
70
95
  end
71
96
  else
97
+ raise Redis::CommandError, 'ERR MULTI calls can not be nested' if in_multi?
98
+ push_multi
72
99
  'OK'
73
100
  end
74
101
  end
75
102
 
103
+ def pipelined
104
+ yield(self) if block_given?
105
+ end
106
+
76
107
  def unwatch
77
108
  'OK'
78
109
  end
79
110
 
80
- def watch(_)
81
- yield self if block_given?
111
+ def watch(*_)
112
+ if block_given?
113
+ yield self
114
+ else
115
+ 'OK'
116
+ end
82
117
  end
83
-
84
118
  end
85
119
  end