• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sds / mock_redis / 12041341301

27 Nov 2024 12:23AM UTC coverage: 98.3% (+0.3%) from 98.046%
12041341301

push

github

web-flow
Support Redis-rb v5 (#314)

This is an attempt to fix https://github.com/sds/mock_redis/issues/281.
This PR was also tested using the test suite of our service.

There are a few decisions I made regarding this PR, please let me know
if it's not appropriate:

1. Error from Redis now contains the redis connection URL
- To fix this, I created `MockRedis::Error` module which decorate
`Redis::CommandError` and related errors with the connection URL
- I replace most of the `Redis::CommandError` with the new module
wrapper
2. Some methods now return integer as output instead of boolean and
`exists_returns_integer` is removed
  - I replace the output as is
3. Interfaces of most data structures (Hash, Set, ...) now validate type
to be 4 main primitive types
- `assert_type` is added to validate the arguments. Currently it doesn't
look very clean, so any improvements would be great.
4. `sadd`, `srem` now support multiple arguments
5. Transaction: `multi` now needs to call with a block and
`discard`/`exec` cannot be called outside that block anymore
- I tried to refactor the logic to make the tests pass, but I'm not
entirely confident that it works correctly

147 of 150 new or added lines in 13 files covered. (98.0%)

1 existing line in 1 file now uncovered.

1850 of 1882 relevant lines covered (98.3%)

1615.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.44
/lib/mock_redis/string_methods.rb
1
require 'mock_redis/assertions'
8✔
2

3
class MockRedis
8✔
4
  module StringMethods
8✔
5
    include Assertions
8✔
6
    include UtilityMethods
8✔
7

8
    def append(key, value)
8✔
9
      assert_stringy(key)
16✔
10
      data[key] ||= ''
12✔
11
      data[key] << value
12✔
12
      data[key].length
12✔
13
    end
14

15
    def bitfield(*args)
8✔
16
      if args.length < 4
516✔
NEW
17
        raise Error.command_error('ERR wrong number of arguments for BITFIELD', self)
×
18
      end
19

20
      key = args.shift
516✔
21
      output = []
516✔
22
      overflow_method = 'wrap'
516✔
23

24
      until args.empty?
516✔
25
        command = args.shift.to_s
636✔
26

27
        if command == 'overflow'
636✔
28
          new_overflow_method = args.shift.to_s.downcase
52✔
29

30
          unless %w[wrap sat fail].include? new_overflow_method
52✔
31
            raise Error.command_error('ERR Invalid OVERFLOW type specified', self)
4✔
32
          end
33

34
          overflow_method = new_overflow_method
48✔
35
          next
48✔
36
        end
37

38
        type, offset = args.shift(2)
584✔
39

40
        is_signed = type.slice(0) == 'i'
584✔
41
        type_size = type[1..].to_i
584✔
42

43
        if (type_size > 64 && is_signed) || (type_size >= 64 && !is_signed)
584✔
44
          raise Error.command_error(
8✔
45
            'ERR Invalid bitfield type. Use something like i16 u8. ' \
46
            'Note that u64 is not supported but i64 is.',
47
            self
48
          )
49
        end
50

51
        if offset.to_s[0] == '#'
576✔
52
          offset = offset[1..].to_i * type_size
12✔
53
        end
54

55
        bits = []
576✔
56

57
        type_size.times do |i|
576✔
58
          bits.push(getbit(key, offset + i))
4,832✔
59
        end
60

61
        val = is_signed ? twos_complement_decode(bits) : bits.join('').to_i(2)
576✔
62

63
        case command
576✔
64
        when 'get'
65
          output.push(val)
88✔
66
        when 'set'
67
          output.push(val)
412✔
68

69
          set_bitfield(key, args.shift.to_i, is_signed, type_size, offset)
412✔
70
        when 'incrby'
71
          new_val = incr_bitfield(val, args.shift.to_i, is_signed, type_size, overflow_method)
76✔
72

73
          set_bitfield(key, new_val, is_signed, type_size, offset) if new_val
76✔
74
          output.push(new_val)
76✔
75
        end
76
      end
77

78
      output
504✔
79
    end
80

81
    def decr(key)
8✔
82
      decrby(key, 1)
32✔
83
    end
84

85
    def decrby(key, n)
8✔
86
      incrby(key, -n)
64✔
87
    end
88

89
    def get(key)
8✔
90
      key = key.to_s
936✔
91
      assert_stringy(key)
936✔
92
      data[key]
928✔
93
    end
94

95
    def getbit(key, offset)
8✔
96
      assert_stringy(key)
8,672✔
97

98
      offset = offset.to_i
8,668✔
99
      offset_of_byte = offset / 8
8,668✔
100
      offset_within_byte = offset % 8
8,668✔
101

102
      # String#getbyte would be lovely, but it's not in 1.8.7.
103
      byte = (data[key] || '').each_byte.drop(offset_of_byte).first
8,668✔
104

105
      if byte
8,668✔
106
        (byte & (2**7 >> offset_within_byte)) > 0 ? 1 : 0
5,124✔
107
      else
108
        0
3,544✔
109
      end
110
    end
111

112
    def getdel(key)
8✔
113
      value = get(key)
×
114
      del(key)
×
115
      value
×
116
    end
117

118
    def getrange(key, start, stop)
8✔
119
      assert_stringy(key)
20✔
120
      (data[key] || '')[start..stop]
12✔
121
    end
122

123
    def getset(key, value)
8✔
124
      retval = get(key)
12✔
125
      set(key, value)
12✔
126
      retval
12✔
127
    end
128

129
    def incr(key)
8✔
130
      incrby(key, 1)
56✔
131
    end
132

133
    def incrby(key, n)
8✔
134
      assert_stringy(key)
196✔
135
      n = Integer(n)
180✔
136

137
      unless can_incr?(data[key])
176✔
138
        raise Error.command_error('ERR value is not an integer or out of range', self)
16✔
139
      end
140

141
      new_value = data[key].to_i + n.to_i
160✔
142
      data[key] = new_value.to_s
160✔
143
      # for some reason, redis-rb doesn't return this as a string.
144
      new_value
160✔
145
    end
146

147
    def incrbyfloat(key, n)
8✔
148
      assert_stringy(key)
40✔
149
      n = Float(n)
36✔
150
      unless can_incr_float?(data[key])
32✔
151
        raise Error.command_error('ERR value is not a valid float', self)
4✔
152
      end
153

154
      new_value = data[key].to_f + n.to_f
28✔
155
      data[key] = new_value.to_s
28✔
156
      # for some reason, redis-rb doesn't return this as a string.
157
      new_value
28✔
158
    end
159

160
    def mget(*keys, &blk)
8✔
161
      keys.flatten!
60✔
162

163
      assert_has_args(keys, 'mget')
60✔
164

165
      data = keys.map do |key|
52✔
166
        get(key) if stringy?(key)
116✔
167
      end
168

169
      blk ? blk.call(data) : data
52✔
170
    end
171

172
    def mapped_mget(*keys)
8✔
173
      Hash[keys.zip(mget(*keys))]
8✔
174
    end
175

176
    def mset(*kvpairs)
8✔
177
      assert_has_args(kvpairs, 'mset')
48✔
178
      kvpairs = kvpairs.first if kvpairs.size == 1 && kvpairs.first.is_a?(Enumerable)
44✔
179

180
      if kvpairs.length.odd?
44✔
181
        raise Error.command_error('ERR wrong number of arguments for MSET', self)
12✔
182
      end
183

184
      kvpairs.each_slice(2) do |(k, v)|
32✔
185
        set(k, v)
48✔
186
      end
187

188
      'OK'
32✔
189
    end
190

191
    def mapped_mset(hash)
8✔
192
      mset(*hash.to_a.flatten)
4✔
193
    end
194

195
    def msetnx(*kvpairs)
8✔
196
      assert_has_args(kvpairs, 'msetnx')
32✔
197

198
      if kvpairs.each_slice(2).any? { |(k, _)| exists?(k) }
64✔
199
        false
12✔
200
      else
201
        mset(*kvpairs)
16✔
202
        true
12✔
203
      end
204
    end
205

206
    def mapped_msetnx(hash)
8✔
207
      msetnx(*hash.to_a.flatten)
8✔
208
    end
209

210
    # Parameter list required to ensure the ArgumentError is returned correctly
211
    # rubocop:disable Metrics/ParameterLists
212
    def set(key, value, _hash = nil, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil,
8✔
213
      keepttl: nil, get: nil)
214
      key = key.to_s
3,284✔
215
      retval = self.get(key) if get
3,284✔
216

217
      return_true = false
3,280✔
218
      if nx
3,280✔
219
        if exists?(key)
8✔
220
          return false
4✔
221
        else
222
          return_true = true
4✔
223
        end
224
      end
225
      if xx
3,276✔
226
        if exists?(key)
8✔
227
          return_true = true
4✔
228
        else
229
          return false
4✔
230
        end
231
      end
232
      data[key] = value.to_s
3,272✔
233

234
      remove_expiration(key) unless keepttl
3,272✔
235
      if ex
3,272✔
236
        if ex == 0
24✔
237
          raise Error.command_error('ERR invalid expire time in set', self)
4✔
238
        end
239
        expire(key, ex)
20✔
240
      end
241

242
      if px
3,268✔
243
        if px == 0
8✔
244
          raise Error.command_error('ERR invalid expire time in set', self)
4✔
245
        end
246
        pexpire(key, px)
4✔
247
      end
248

249
      if exat
3,264✔
250
        if exat == 0
16✔
251
          raise Error.command_error('ERR invalid expire time in set', self)
4✔
252
        end
253
        expireat(key, exat)
12✔
254
      end
255

256
      if pxat
3,260✔
257
        if pxat == 0
8✔
258
          raise Error.command_error('ERR invalid expire time in set', self)
4✔
259
        end
260
        pexpireat(key, pxat)
4✔
261
      end
262

263
      if get
3,256✔
264
        retval
8✔
265
      else
266
        return_true ? true : 'OK'
3,248✔
267
      end
268
    end
269
    # rubocop:enable Metrics/ParameterLists
270

271
    def setbit(key, offset, value)
8✔
272
      assert_stringy(key, 'ERR bit is not an integer or out of range')
3,788✔
273
      retval = getbit(key, offset)
3,784✔
274

275
      str = data[key] || ''
3,784✔
276

277
      offset = offset.to_i
3,784✔
278
      offset_of_byte = offset / 8
3,784✔
279
      offset_within_byte = offset % 8
3,784✔
280

281
      if offset_of_byte >= str.bytesize
3,784✔
282
        str = zero_pad(str, offset_of_byte + 1)
396✔
283
      end
284

285
      char_index = byte_index = offset_within_char = 0
3,784✔
286
      str.each_char do |c|
3,784✔
287
        if byte_index < offset_of_byte
10,744✔
288
          char_index += 1
6,960✔
289
          byte_index += c.bytesize
6,960✔
290
        else
291
          offset_within_char = byte_index - offset_of_byte
3,784✔
292
          break
3,784✔
293
        end
294
      end
295

296
      char = str[char_index]
3,784✔
297
      char = char.chr if char.respond_to?(:chr) # ruby 1.8 vs 1.9
3,784✔
298
      char_as_number = char.each_byte.reduce(0) do |a, byte|
3,784✔
299
        (a << 8) + byte
3,784✔
300
      end
301

302
      bitmask_length = (char.bytesize * 8 - offset_within_char * 8 - offset_within_byte - 1)
3,784✔
303
      bitmask = 1 << bitmask_length
3,784✔
304

305
      if value.zero?
3,784✔
306
        bitmask ^= 2**(char.bytesize * 8) - 1
1,932✔
307
        char_as_number &= bitmask
1,932✔
308
      else
309
        char_as_number |= bitmask
1,852✔
310
      end
311

312
      str[char_index] = char_as_number.chr
3,784✔
313

314
      data[key] = str
3,784✔
315
      retval
3,784✔
316
    end
317

318
    def bitcount(key, start = 0, stop = -1)
8✔
319
      assert_stringy(key)
28✔
320

321
      str   = data[key] || ''
24✔
322
      count = 0
24✔
323
      m1    = 0x5555555555555555
24✔
324
      m2    = 0x3333333333333333
24✔
325
      m4    = 0x0f0f0f0f0f0f0f0f
24✔
326
      m8    = 0x00ff00ff00ff00ff
24✔
327
      m16   = 0x0000ffff0000ffff
24✔
328
      m32   = 0x00000000ffffffff
24✔
329

330
      str.bytes.to_a[start..stop].each do |byte|
24✔
331
        # Naive Hamming weight
332
        c = byte
72✔
333
        c = (c & m1) + ((c >> 1) & m1)
72✔
334
        c = (c & m2) + ((c >> 2) & m2)
72✔
335
        c = (c & m4) + ((c >> 4) & m4)
72✔
336
        c = (c & m8) + ((c >> 8) & m8)
72✔
337
        c = (c & m16) + ((c >> 16) & m16)
72✔
338
        c = (c & m32) + ((c >> 32) & m32)
72✔
339
        count += c
72✔
340
      end
341

342
      count
24✔
343
    end
344

345
    def setex(key, seconds, value)
8✔
346
      if seconds <= 0
20✔
347
        raise Error.command_error('ERR invalid expire time in setex', self)
8✔
348
      else
349
        set(key, value)
12✔
350
        expire(key, seconds)
12✔
351
        'OK'
12✔
352
      end
353
    end
354

355
    def psetex(key, milliseconds, value)
8✔
356
      if milliseconds <= 0
24✔
357
        raise Error.command_error('ERR invalid expire time in psetex', self)
8✔
358
      else
359
        set(key, value)
16✔
360
        pexpire(key, milliseconds)
16✔
361
        'OK'
16✔
362
      end
363
    end
364

365
    def setnx(key, value)
8✔
366
      if exists?(key)
16✔
367
        false
8✔
368
      else
369
        set(key, value)
8✔
370
        true
8✔
371
      end
372
    end
373

374
    def setrange(key, offset, value)
8✔
375
      assert_stringy(key)
20✔
376
      value = value.to_s
16✔
377
      old_value = data[key] || ''
16✔
378

379
      prefix = zero_pad(old_value[0...offset], offset)
16✔
380
      data[key] = prefix + value + (old_value[(offset + value.length)..] || '')
16✔
381
      data[key].length
16✔
382
    end
383

384
    def strlen(key)
8✔
385
      assert_stringy(key)
12✔
386
      (data[key] || '').bytesize
8✔
387
    end
388

389
    private
8✔
390

391
    def stringy?(key)
8✔
392
      data[key].nil? || data[key].is_a?(String)
13,928✔
393
    end
394

395
    def assert_stringy(key, message = nil)
8✔
396
      unless stringy?(key)
13,728✔
397
        if message
60✔
398
          raise Error.command_error(message, self)
4✔
399
        else
400
          raise Error.wrong_type_error(self)
56✔
401
        end
402
      end
403
    end
404

405
    def set_bitfield(key, value, is_signed, type_size, offset)
8✔
406
      if is_signed
468✔
407
        val_array = twos_complement_encode(value, type_size)
308✔
408
      else
409
        str = left_pad(value.to_i.abs.to_s(2), type_size)
160✔
410
        val_array = str.split('').map(&:to_i)
160✔
411
      end
412

413
      val_array.each_with_index do |bit, i|
468✔
414
        setbit(key, offset + i, bit)
3,744✔
415
      end
416
    end
417

418
    def incr_bitfield(val, incrby, is_signed, type_size, overflow_method)
8✔
419
      new_val = val + incrby
76✔
420

421
      max = is_signed ? (2**(type_size - 1)) - 1 : (2**type_size) - 1
76✔
422
      min = is_signed ? (-2**(type_size - 1)) : 0
76✔
423
      size = 2**type_size
76✔
424

425
      return new_val if (min..max).cover?(new_val)
76✔
426

427
      case overflow_method
60✔
428
      when 'fail'
429
        new_val = nil
20✔
430
      when 'sat'
431
        new_val = new_val > max ? max : min
16✔
432
      when 'wrap'
433
        if is_signed
24✔
434
          if new_val > max
16✔
435
            remainder = new_val - (max + 1)
8✔
436
            new_val = min + remainder.abs
8✔
437
          else
438
            remainder = new_val - (min - 1)
8✔
439
            new_val = max - remainder.abs
8✔
440
          end
441
        else
442
          new_val = new_val > max ? new_val % size : size - new_val.abs
8✔
443
        end
444
      end
445

446
      new_val
60✔
447
    end
448
  end
449
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc