mock_redis 0.18.0 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d45157a6761c4ee6db463b692fc9576ea1635576702423d8482e4a108662a1bc
4
- data.tar.gz: 1fd788a1b8d7260c68f964af5cad36d83d360ae86616d4b764c1a655b1df953d
3
+ metadata.gz: da2c2693b445e51b487e52f7c9b3986960417686792f9a5e554ca4d215c323e2
4
+ data.tar.gz: 8da2170fcc49e768133a61deb716d72cc18d66291567008d96287b8d2104b15c
5
5
  SHA512:
6
- metadata.gz: fb6a1a256bca8464aff700e90db426ce3e98b454c9bbdb56f44316dba0b8c10dd66afe7938b3f0fb3b22e61cee3c2f96675fc3f5ce1d2efcae24c09c9bd6ba76
7
- data.tar.gz: 6720e42db8d2aafeae70dfeb79ad827b054b10143013510a2f3ac8b209438d70a05aafbf1039aae3f9b85194221cb80461c33700cf0215895ab6989999bcf605
6
+ metadata.gz: 15b8b59cff1120de0befaee6cf08facfb120e239d3fbc1b6b55f40bb60c526f57e5cbb61f1d9f6fd5538067122e49b80e9e782590cd98070a3d49943ec8bb929
7
+ data.tar.gz: 52a09f0896a405137be0075db930bec09898dd96e3d5366a95ef6a189268348f91244c051dbe45db5b4b37023a12f87552868fcaf2a5383316bebe9bdaf44997
@@ -1,3 +1,14 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.2
5
+
6
+ Layout/AlignParameters:
7
+ Enabled: false
8
+
9
+ Layout/DotPosition:
10
+ Enabled: false
11
+
1
12
  Lint/AssignmentInCondition:
2
13
  Enabled: false
3
14
 
@@ -30,16 +41,18 @@ Metrics/ModuleLength:
30
41
  Metrics/PerceivedComplexity:
31
42
  Enabled: false
32
43
 
33
- Style/AlignParameters:
44
+ # This hides the has-a versus is-a relationship indicated by the method name
45
+ Naming/PredicateName:
34
46
  Enabled: false
35
47
 
36
48
  Style/Documentation:
37
49
  Enabled: false
38
50
 
39
- Style/DotPosition:
51
+ Style/DoubleNegation:
40
52
  Enabled: false
41
53
 
42
- Style/DoubleNegation:
54
+ # We have too much code that relies on modifying strings
55
+ Style/FrozenStringLiteralComment:
43
56
  Enabled: false
44
57
 
45
58
  Style/GuardClause:
@@ -55,7 +68,10 @@ Style/Lambda:
55
68
  Enabled: false
56
69
 
57
70
  # TODO: Address these at some point
58
- Style/MethodMissing:
71
+ Style/MethodMissingSuper:
72
+ Enabled: false
73
+
74
+ Style/MissingRespondToMissing:
59
75
  Enabled: false
60
76
 
61
77
  Style/MultilineBlockChain:
@@ -80,10 +96,6 @@ Style/PercentLiteralDelimiters:
80
96
  Style/PerlBackrefs:
81
97
  Enabled: false
82
98
 
83
- # This hides the has-a versus is-a relationship indicated by the method name
84
- Style/PredicateName:
85
- Enabled: false
86
-
87
99
  Style/RescueModifier:
88
100
  Enabled: false
89
101
 
@@ -99,7 +111,10 @@ Style/SymbolArray:
99
111
  Style/TrailingCommaInArguments:
100
112
  Enabled: false
101
113
 
102
- Style/TrailingCommaInLiteral:
114
+ Style/TrailingCommaInArrayLiteral:
115
+ Enabled: false
116
+
117
+ Style/TrailingCommaInHashLiteral:
103
118
  Enabled: false
104
119
 
105
120
  Style/WhenThen:
@@ -0,0 +1,35 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2018-08-03 11:15:53 -0700 using RuboCop version 0.58.2.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 17
10
+ # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
11
+ # AllowedNames: io, id, to, by, on, in, at, ip
12
+ Naming/UncommunicativeMethodParamName:
13
+ Exclude:
14
+ - 'lib/mock_redis/database.rb'
15
+ - 'lib/mock_redis/expire_wrapper.rb'
16
+ - 'lib/mock_redis/geospatial_methods.rb'
17
+ - 'lib/mock_redis/multi_db_wrapper.rb'
18
+ - 'lib/mock_redis/pipelined_wrapper.rb'
19
+ - 'lib/mock_redis/string_methods.rb'
20
+ - 'lib/mock_redis/transaction_wrapper.rb'
21
+ - 'lib/mock_redis/utility_methods.rb'
22
+ - 'lib/mock_redis/zset_methods.rb'
23
+ - 'spec/support/redis_multiplexer.rb'
24
+
25
+ # Offense count: 2
26
+ # Configuration parameters: EnforcedStyle.
27
+ # SupportedStyles: inline, group
28
+ Style/AccessModifierDeclarations:
29
+ Exclude:
30
+ - 'lib/mock_redis/zset_methods.rb'
31
+
32
+ # Offense count: 1
33
+ Style/DateTime:
34
+ Exclude:
35
+ - 'spec/commands/zremrangebyscore_spec.rb'
@@ -13,10 +13,10 @@ services:
13
13
  - redis-server
14
14
 
15
15
  rvm:
16
- - 2.0
17
- - 2.1
18
16
  - 2.2
19
- - 2.3.1
17
+ - 2.3.7
18
+ - 2.4.4
19
+ - 2.5.1
20
20
 
21
21
  before_script:
22
22
  - git config --local user.email "travis@travis.ci"
@@ -1,5 +1,12 @@
1
1
  # MockRedis Changelog
2
2
 
3
+ ### 0.19.0
4
+
5
+ * Require Ruby 2.2+
6
+ * Add support for `bitfield` command
7
+ * Add support for `geoadd`, `geopos`, `geohash`, and `geodist` commands
8
+ * Fix multi-nested pipeline not releasing lock issue
9
+
3
10
  ### 0.18.0
4
11
 
5
12
  * Fix `hset` return value to return false when the field exists in the hash
data/Gemfile CHANGED
@@ -4,9 +4,9 @@ source 'http://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  # Run all pre-commit hooks via Overcommit during CI runs
7
- gem 'overcommit', '0.39.0'
7
+ gem 'overcommit', '0.45.0'
8
8
 
9
9
  # Pin tool versions (which are executed by Overcommit) for Travis builds
10
- gem 'rubocop', '0.48.1'
10
+ gem 'rubocop', '0.58.2'
11
11
 
12
12
  gem 'coveralls', require: false
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/mock_redis.svg)](http://badge.fury.io/rb/mock_redis)
4
4
  [![Build Status](https://travis-ci.org/brigade/mock_redis.svg)](https://travis-ci.org/brigade/mock_redis)
5
5
  [![Coverage Status](https://coveralls.io/repos/brigade/mock_redis/badge.svg)](https://coveralls.io/r/brigade/mock_redis)
6
- [![Dependency Status](https://gemnasium.com/brigade/mock_redis.svg)](https://gemnasium.com/brigade/mock_redis)
7
6
 
8
7
  MockRedis provides the same interface as `redis-rb`, but it stores its
9
8
  data in memory instead of talking to a Redis server. It is intended
@@ -93,8 +92,8 @@ please submit a pull request with your (tested!) implementation.
93
92
 
94
93
  ## Compatibility
95
94
 
96
- As of version `0.8.2`, Ruby 2.0.0 and above are supported. For
97
- older versions of Ruby, use `0.8.1` or older.
95
+ As of version `0.19.0`, Ruby 2.2 and above are supported. For
96
+ older versions of Ruby, use `0.18.0` or older.
98
97
 
99
98
  ## Running the Tests
100
99
 
@@ -9,6 +9,7 @@ require 'mock_redis/sort_method'
9
9
  require 'mock_redis/indifferent_hash'
10
10
  require 'mock_redis/info_method'
11
11
  require 'mock_redis/utility_methods'
12
+ require 'mock_redis/geospatial_methods'
12
13
 
13
14
  class MockRedis
14
15
  class Database
@@ -20,6 +21,7 @@ class MockRedis
20
21
  include SortMethod
21
22
  include InfoMethod
22
23
  include UtilityMethods
24
+ include GeospatialMethods
23
25
 
24
26
  attr_reader :data, :expire_times
25
27
 
@@ -2,10 +2,11 @@ class MockRedis
2
2
  class FutureNotReady < RuntimeError; end
3
3
 
4
4
  class Future
5
- attr_reader :command
5
+ attr_reader :command, :block
6
6
 
7
- def initialize(command)
7
+ def initialize(command, block = nil)
8
8
  @command = command
9
+ @block = block
9
10
  @result_set = false
10
11
  end
11
12
 
@@ -0,0 +1,248 @@
1
+ class MockRedis
2
+ module GeospatialMethods
3
+ LNG_RANGE = (-180..180)
4
+ LAT_RANGE = (-85.05112878..85.05112878)
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
+ 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, *args)
27
+ if args.length < 2
28
+ raise Redis::CommandError,
29
+ "ERR wrong number of arguments for 'geodist' command"
30
+ end
31
+
32
+ raise Redis::CommandError, 'ERR syntax error' if args.length > 3
33
+
34
+ to_meter = 1
35
+ to_meter = parse_unit(args[2]) if args.length == 3
36
+
37
+ return nil if zcard(key).zero?
38
+
39
+ score1 = zscore(key, args[0])
40
+ score2 = zscore(key, args[1])
41
+ return nil if score1.nil? || score2.nil?
42
+ hash1 = { bits: score1.to_i, step: STEP }
43
+ hash2 = { bits: score2.to_i, step: STEP }
44
+
45
+ lng1, lat1 = geohash_decode(hash1)
46
+ lng2, lat2 = geohash_decode(hash2)
47
+
48
+ distance = geohash_distance(lng1, lat1, lng2, lat2) / to_meter
49
+ format('%.4f', distance)
50
+ end
51
+
52
+ def geohash(key, *members)
53
+ lng_range = (-180..180)
54
+ lat_range = (-90..90)
55
+ geoalphabet = '0123456789bcdefghjkmnpqrstuvwxyz'
56
+
57
+ members.map do |member|
58
+ score = zscore(key, member)
59
+ next nil unless score
60
+ score = score.to_i
61
+ hash = { bits: score, step: STEP }
62
+ lng, lat = geohash_decode(hash)
63
+ bits = geohash_encode(lng, lat, lng_range, lat_range)[:bits]
64
+ hash = ''
65
+ 11.times do |i|
66
+ shift = (52 - ((i + 1) * 5))
67
+ idx = shift > 0 ? (bits >> shift) & 0x1f : 0
68
+ hash << geoalphabet[idx]
69
+ end
70
+ hash
71
+ end
72
+ end
73
+
74
+ def geopos(key, *members)
75
+ members.map do |member|
76
+ score = zscore(key, member)
77
+ next nil unless score
78
+ hash = { bits: score.to_i, step: STEP }
79
+ lng, lat = geohash_decode(hash)
80
+ lng = format_decoded_coord(lng)
81
+ lat = format_decoded_coord(lat)
82
+ [lng, lat]
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def parse_points(args)
89
+ points = args.each_slice(3).to_a
90
+
91
+ if points.last.size != 3
92
+ raise Redis::CommandError,
93
+ "ERR wrong number of arguments for 'geoadd' command"
94
+ end
95
+
96
+ points.map do |point|
97
+ parse_point(point)
98
+ end
99
+ end
100
+
101
+ def parse_point(point)
102
+ lng = Float(point[0])
103
+ lat = Float(point[1])
104
+
105
+ unless LNG_RANGE.include?(lng) && LAT_RANGE.include?(lat)
106
+ lng = format('%.6f', lng)
107
+ lat = format('%.6f', lat)
108
+ raise Redis::CommandError,
109
+ "ERR invalid longitude,latitude pair #{lng},#{lat}"
110
+ end
111
+
112
+ { key: point[2], lng: lng, lat: lat }
113
+ rescue ArgumentError
114
+ raise Redis::CommandError, 'ERR value is not a valid float'
115
+ end
116
+
117
+ # Returns ZSET score for passed coordinates
118
+ def geohash_encode(lng, lat, lng_range = LNG_RANGE, lat_range = LAT_RANGE, step = STEP)
119
+ lat_offset = (lat - lat_range.min) / (lat_range.max - lat_range.min)
120
+ lng_offset = (lng - lng_range.min) / (lng_range.max - lng_range.min)
121
+
122
+ lat_offset *= (1 << step)
123
+ lng_offset *= (1 << step)
124
+
125
+ bits = interleave(lat_offset.to_i, lng_offset.to_i)
126
+
127
+ { bits: bits, step: step }
128
+ end
129
+
130
+ def interleave(x, y)
131
+ b = [0x5555555555555555, 0x3333333333333333, 0x0F0F0F0F0F0F0F0F,
132
+ 0x00FF00FF00FF00FF, 0x0000FFFF0000FFFF]
133
+ s = [1, 2, 4, 8, 16]
134
+
135
+ x = (x | (x << s[4])) & b[4]
136
+ y = (y | (y << s[4])) & b[4]
137
+
138
+ x = (x | (x << s[3])) & b[3]
139
+ y = (y | (y << s[3])) & b[3]
140
+
141
+ x = (x | (x << s[2])) & b[2]
142
+ y = (y | (y << s[2])) & b[2]
143
+
144
+ x = (x | (x << s[1])) & b[1]
145
+ y = (y | (y << s[1])) & b[1]
146
+
147
+ x = (x | (x << s[0])) & b[0]
148
+ y = (y | (y << s[0])) & b[0]
149
+
150
+ x | (y << 1)
151
+ end
152
+
153
+ # Decodes ZSET score to coordinates pair
154
+ def geohash_decode(hash, lng_range = LNG_RANGE, lat_range = LAT_RANGE)
155
+ area = calculate_approximate_area(hash, lng_range, lat_range)
156
+
157
+ lng = (area[:lng_min] + area[:lng_max]) / 2
158
+ lat = (area[:lat_min] + area[:lat_max]) / 2
159
+
160
+ [lng, lat]
161
+ end
162
+
163
+ def calculate_approximate_area(hash, lng_range, lat_range)
164
+ bits = hash[:bits]
165
+ step = hash[:step]
166
+ hash_sep = deinterleave(bits)
167
+
168
+ lat_scale = lat_range.max - lat_range.min
169
+ lng_scale = lng_range.max - lng_range.min
170
+
171
+ ilato = hash_sep & 0xFFFFFFFF # cast int64 to int32 to get lat part of deinterleaved hash
172
+ ilngo = hash_sep >> 32 # shift over to get lng part of hash
173
+
174
+ {
175
+ lat_min: lat_range.min + (ilato * 1.0 / (1 << step)) * lat_scale,
176
+ lat_max: lat_range.min + ((ilato + 1) * 1.0 / (1 << step)) * lat_scale,
177
+ lng_min: lng_range.min + (ilngo * 1.0 / (1 << step)) * lng_scale,
178
+ lng_max: lng_range.min + ((ilngo + 1) * 1.0 / (1 << step)) * lng_scale
179
+ }
180
+ end
181
+
182
+ def deinterleave(bits)
183
+ b = [0x5555555555555555, 0x3333333333333333, 0x0F0F0F0F0F0F0F0F,
184
+ 0x00FF00FF00FF00FF, 0x0000FFFF0000FFFF, 0x00000000FFFFFFFF]
185
+ s = [0, 1, 2, 4, 8, 16]
186
+
187
+ x = bits
188
+ y = bits >> 1
189
+
190
+ x = (x | (x >> s[0])) & b[0]
191
+ y = (y | (y >> s[0])) & b[0]
192
+
193
+ x = (x | (x >> s[1])) & b[1]
194
+ y = (y | (y >> s[1])) & b[1]
195
+
196
+ x = (x | (x >> s[2])) & b[2]
197
+ y = (y | (y >> s[2])) & b[2]
198
+
199
+ x = (x | (x >> s[3])) & b[3]
200
+ y = (y | (y >> s[3])) & b[3]
201
+
202
+ x = (x | (x >> s[4])) & b[4]
203
+ y = (y | (y >> s[4])) & b[4]
204
+
205
+ x = (x | (x >> s[5])) & b[5]
206
+ y = (y | (y >> s[5])) & b[5]
207
+
208
+ x | (y << 32)
209
+ end
210
+
211
+ def format_decoded_coord(coord)
212
+ coord = format('%.17f', coord)
213
+ l = 1
214
+ l += 1 while coord[-l] == '0'
215
+ coord = coord[0..-l]
216
+ coord[-1] == '.' ? coord[0..-2] : coord
217
+ end
218
+
219
+ def parse_unit(unit)
220
+ unit = unit.to_sym
221
+ return UNITS[unit] if UNITS[unit]
222
+
223
+ raise Redis::CommandError,
224
+ 'ERR unsupported unit provided. please use m, km, ft, mi'
225
+ end
226
+
227
+ def geohash_distance(lng1d, lat1d, lng2d, lat2d)
228
+ lat1r = deg_rad(lat1d)
229
+ lng1r = deg_rad(lng1d)
230
+ lat2r = deg_rad(lat2d)
231
+ lng2r = deg_rad(lng2d)
232
+
233
+ u = Math.sin((lat2r - lat1r) / 2)
234
+ v = Math.sin((lng2r - lng1r) / 2)
235
+
236
+ 2.0 * EARTH_RADIUS_IN_METERS *
237
+ Math.asin(Math.sqrt(u * u + Math.cos(lat1r) * Math.cos(lat2r) * v * v))
238
+ end
239
+
240
+ def deg_rad(ang)
241
+ ang * D_R
242
+ end
243
+
244
+ def rad_deg(ang)
245
+ ang / D_R
246
+ end
247
+ end
248
+ end
@@ -20,7 +20,7 @@ class MockRedis
20
20
 
21
21
  def method_missing(method, *args, &block)
22
22
  if @in_pipeline
23
- future = MockRedis::Future.new([method, *args])
23
+ future = MockRedis::Future.new([method, *args], block)
24
24
  @pipelined_futures << future
25
25
  future
26
26
  else
@@ -32,12 +32,16 @@ class MockRedis
32
32
  @in_pipeline = true
33
33
  yield self
34
34
  @in_pipeline = false
35
- responses = @pipelined_futures.map do |future|
35
+ responses = @pipelined_futures.flat_map do |future|
36
36
  begin
37
- result = send(*future.command)
37
+ result = if future.block
38
+ send(*future.command, &future.block)
39
+ else
40
+ send(*future.command)
41
+ end
38
42
  future.store_result(result)
39
43
  result
40
- rescue => e
44
+ rescue StandardError => e
41
45
  e
42
46
  end
43
47
  end
@@ -3,6 +3,7 @@ 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)
@@ -11,6 +12,70 @@ class MockRedis
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
@@ -285,5 +350,49 @@ class MockRedis
285
350
  raise Redis::CommandError, message
286
351
  end
287
352
  end
353
+
354
+ def set_bitfield(key, value, is_signed, type_size, offset)
355
+ if is_signed
356
+ val_array = twos_complement_encode(value, type_size)
357
+ else
358
+ str = left_pad(value.to_i.abs.to_s(2), type_size)
359
+ val_array = str.split('').map(&:to_i)
360
+ end
361
+
362
+ val_array.each_with_index do |bit, i|
363
+ setbit(key, offset + i, bit)
364
+ end
365
+ end
366
+
367
+ def incr_bitfield(val, incrby, is_signed, type_size, overflow_method)
368
+ new_val = val + incrby
369
+
370
+ max = is_signed ? (2**(type_size - 1)) - 1 : (2**type_size) - 1
371
+ min = is_signed ? (-2**(type_size - 1)) : 0
372
+ size = 2**type_size
373
+
374
+ return new_val if (min..max).cover?(new_val)
375
+
376
+ case overflow_method
377
+ when 'fail'
378
+ new_val = nil
379
+ when 'sat'
380
+ new_val = new_val > max ? max : min
381
+ when 'wrap'
382
+ if is_signed
383
+ if new_val > max
384
+ remainder = new_val - (max + 1)
385
+ new_val = min + remainder.abs
386
+ else
387
+ remainder = new_val - (min - 1)
388
+ new_val = max - remainder.abs
389
+ end
390
+ else
391
+ new_val = new_val > max ? new_val % size : size - new_val.abs
392
+ end
393
+ end
394
+
395
+ new_val
396
+ end
288
397
  end
289
398
  end
@@ -59,7 +59,7 @@ class MockRedis
59
59
  result = send(*future.command)
60
60
  future.store_result(result)
61
61
  result
62
- rescue => e
62
+ rescue StandardError => e
63
63
  e
64
64
  end
65
65
  end
@@ -38,5 +38,36 @@ class MockRedis
38
38
 
39
39
  [next_cursor, filtered_values]
40
40
  end
41
+
42
+ def twos_complement_encode(n, size)
43
+ if n < 0
44
+ str = (n + 1).abs.to_s(2)
45
+
46
+ binary = left_pad(str, size - 1).chars.map { |c| c == '0' ? 1 : 0 }
47
+ binary.unshift(1)
48
+ else
49
+ binary = left_pad(n.abs.to_s(2), size - 1).chars.map(&:to_i)
50
+ binary.unshift(0)
51
+ end
52
+
53
+ binary
54
+ end
55
+
56
+ def twos_complement_decode(array)
57
+ total = 0
58
+
59
+ array.each.with_index do |bit, index|
60
+ total += 2**(array.length - index - 1) if bit == 1
61
+ total = -total if index == 0
62
+ end
63
+
64
+ total
65
+ end
66
+
67
+ def left_pad(str, size)
68
+ str = '0' + str while str.length < size
69
+
70
+ str
71
+ end
41
72
  end
42
73
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  class MockRedis
5
- VERSION = '0.18.0'.freeze
5
+ VERSION = '0.19.0'.freeze
6
6
  end
@@ -1,4 +1,4 @@
1
- $LOAD_PATH << File.expand_path('../lib', __FILE__)
1
+ $LOAD_PATH << File.expand_path('lib', __dir__)
2
2
  require 'mock_redis/version'
3
3
 
4
4
  Gem::Specification.new do |s|
@@ -11,16 +11,18 @@ Gem::Specification.new do |s|
11
11
  s.homepage = 'https://github.com/brigade/mock_redis'
12
12
  s.summary = 'Redis mock that just lives in memory; useful for testing.'
13
13
 
14
- s.description = <<-EOS.strip.gsub(/\s+/, ' ')
14
+ s.description = <<-MSG.strip.gsub(/\s+/, ' ')
15
15
  Instantiate one with `redis = MockRedis.new` and treat it like you would a
16
16
  normal Redis object. It supports all the usual Redis operations.
17
- EOS
17
+ MSG
18
18
 
19
19
  s.files = `git ls-files`.split("\n")
20
20
  s.test_files = `git ls-files -- spec/*`.split("\n")
21
21
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
22
22
  s.require_paths = ['lib']
23
23
 
24
+ s.required_ruby_version = '>= 2.2'
25
+
24
26
  s.add_development_dependency 'rake', '>= 10', '< 12'
25
27
  s.add_development_dependency 'redis', '~> 3.3.0'
26
28
  s.add_development_dependency 'rspec', '~> 3.0'
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+
3
+ describe '#bitfield(*args)' do
4
+ before :each do
5
+ @key = 'mock-redis-test:bitfield'
6
+ @redises.set(@key, '')
7
+
8
+ @redises.bitfield(@key, :set, 'i8', 0, 78)
9
+ @redises.bitfield(@key, :set, 'i8', 8, 104)
10
+ @redises.bitfield(@key, :set, 'i8', 16, -59)
11
+ @redises.bitfield(@key, :set, 'u8', 24, 78)
12
+ @redises.bitfield(@key, :set, 'u8', 32, 84)
13
+ end
14
+
15
+ context 'with a :get command' do
16
+ it 'gets a signed 8 bit value' do
17
+ @redises.bitfield(@key, :get, 'i8', 0).should == [78]
18
+ @redises.bitfield(@key, :get, 'i8', 8).should == [104]
19
+ @redises.bitfield(@key, :get, 'i8', 16).should == [-59]
20
+ end
21
+
22
+ it 'gets multiple values with multiple command args' do
23
+ @redises.bitfield(@key, :get, 'i8', 0,
24
+ :get, 'i8', 8,
25
+ :get, 'i8', 16).should == [78, 104, -59]
26
+ end
27
+
28
+ it 'gets multiple values using positional offsets' do
29
+ @redises.bitfield(@key, :get, 'i8', '#0',
30
+ :get, 'i8', '#1',
31
+ :get, 'i8', '#2').should == [78, 104, -59]
32
+ end
33
+
34
+ it 'shows an error with an invalid type' do
35
+ expect do
36
+ @redises.bitfield(@key, :get, 'u64', 0)
37
+ end.to raise_error(Redis::CommandError)
38
+ expect do
39
+ @redises.bitfield(@key, :get, 'i128', 0)
40
+ end.to raise_error(Redis::CommandError)
41
+ end
42
+
43
+ it 'does not throw an error with i64 type' do
44
+ expect do
45
+ @redises.bitfield(@key, :get, 'i64', 0)
46
+ end.to_not raise_error
47
+ end
48
+ end
49
+
50
+ context 'with a :set command' do
51
+ it 'sets the bit values for an 8 bit signed integer' do
52
+ @redises.bitfield(@key, :set, 'i8', 0, 63).should == [78]
53
+ @redises.bitfield(@key, :set, 'i8', 8, -1).should == [104]
54
+ @redises.bitfield(@key, :set, 'i8', 16, 123).should == [-59]
55
+
56
+ @redises.bitfield(@key, :get, 'i8', 0,
57
+ :get, 'i8', 8,
58
+ :get, 'i8', 16).should == [63, -1, 123]
59
+ end
60
+
61
+ it 'sets multiple values with multiple command args' do
62
+ @redises.bitfield(@key, :set, 'i8', 0, 63,
63
+ :set, 'i8', 8, -1,
64
+ :set, 'i8', 16, 123).should == [78, 104, -59]
65
+
66
+ @redises.bitfield(@key, :get, 'i8', 0,
67
+ :get, 'i8', 8,
68
+ :get, 'i8', 16).should == [63, -1, 123]
69
+ end
70
+ end
71
+
72
+ context 'with an :incrby command' do
73
+ it 'returns the incremented by value for an 8 bit signed integer' do
74
+ @redises.bitfield(@key, :incrby, 'i8', 0, 1).should == [79]
75
+ @redises.bitfield(@key, :incrby, 'i8', 8, -1).should == [103]
76
+ @redises.bitfield(@key, :incrby, 'i8', 16, 5).should == [-54]
77
+ end
78
+
79
+ context 'with an overflow of wrap (default)' do
80
+ context 'for a signed integer' do
81
+ it 'wraps the overflow to the minimum and increments from there' do
82
+ @redises.bitfield(@key, :get, 'i8', 24).should == [78]
83
+ @redises.bitfield(@key, :overflow, :wrap,
84
+ :incrby, 'i8', 0, 200).should == [22]
85
+ end
86
+
87
+ it 'wraps the underflow to the maximum value and decrements from there' do
88
+ @redises.bitfield(@key, :overflow, :wrap,
89
+ :incrby, 'i8', 16, -200).should == [-3]
90
+ end
91
+ end
92
+
93
+ context 'for an unsigned integer' do
94
+ it 'wraps the overflow back to zero and increments from there' do
95
+ @redises.bitfield(@key, :get, 'u8', 24).should == [78]
96
+ @redises.bitfield(@key, :overflow, :wrap,
97
+ :incrby, 'u8', 24, 233).should == [55]
98
+ end
99
+
100
+ it 'wraps the underflow to the maximum value and decrements from there' do
101
+ @redises.bitfield(@key, :get, 'u8', 32).should == [84]
102
+ @redises.bitfield(@key, :overflow, :wrap,
103
+ :incrby, 'u8', 32, -233).should == [107]
104
+ end
105
+ end
106
+ end
107
+
108
+ context 'with an overflow of sat' do
109
+ it 'sets the overflowed value to the maximum' do
110
+ @redises.bitfield(@key, :overflow, :sat,
111
+ :incrby, 'i8', 0, 256).should == [127]
112
+ end
113
+
114
+ it 'sets the underflowed value to the minimum' do
115
+ @redises.bitfield(@key, :overflow, :sat,
116
+ :incrby, 'i8', 16, -256).should == [-128]
117
+ end
118
+ end
119
+
120
+ context 'with an overflow of fail' do
121
+ it 'raises a redis error on an out of range value' do
122
+ @redises.bitfield(@key, :overflow, :fail,
123
+ :incrby, 'i8', 0, 256).should == [nil]
124
+
125
+ @redises.bitfield(@key, :overflow, :fail,
126
+ :incrby, 'i8', 16, -256).should == [nil]
127
+ end
128
+
129
+ it 'retains the original value after a failed increment' do
130
+ @redises.bitfield(@key, :get, 'i8', 0).should == [78]
131
+ @redises.bitfield(@key, :overflow, :fail,
132
+ :incrby, 'i8', 0, 256).should == [nil]
133
+ @redises.bitfield(@key, :get, 'i8', 0).should == [78]
134
+ end
135
+ end
136
+
137
+ context 'with multiple overflow commands in one transaction' do
138
+ it 'handles the overflow values correctly' do
139
+ @redises.bitfield(@key, :overflow, :sat,
140
+ :incrby, 'i8', 0, 256,
141
+ :incrby, 'i8', 8, -256,
142
+ :overflow, :wrap,
143
+ :incrby, 'i8', 0, 200,
144
+ :incrby, 'i8', 16, -200,
145
+ :overflow, :fail,
146
+ :incrby, 'i8', 0, 256,
147
+ :incrby, 'i8', 16, -256).should == [127, -128, 71, -3, nil, nil]
148
+ end
149
+ end
150
+
151
+ context 'with an unsupported overflow value' do
152
+ it 'raises an error' do
153
+ expect do
154
+ @redises.bitfield(@key, :overflow, :foo,
155
+ :incrby, 'i8', 0, 256)
156
+ end.to raise_error(Redis::CommandError)
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'with a mixed set of commands' do
162
+ it 'returns the correct outputs' do
163
+ @redises.bitfield(@key, :set, 'i8', 0, 38,
164
+ :set, 'i8', 8, -99,
165
+ :incrby, 'i8', 16, 1,
166
+ :get, 'i8', 0).should == [78, 104, -58, 38]
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe '#geoadd' do
4
+ let(:key) { 'cities' }
5
+
6
+ context 'with valid points' do
7
+ let(:san_francisco) { [-122.5076404, 37.757815, 'SF'] }
8
+ let(:los_angeles) { [-118.6919259, 34.0207305, 'LA'] }
9
+ let(:expected_result) do
10
+ [['LA', 1.364461589564902e+15], ['SF', 1.367859319053696e+15]]
11
+ end
12
+
13
+ before { @redises.geoadd(key, *san_francisco, *los_angeles) }
14
+
15
+ after { @redises.zrem(key, %w[SF LA]) }
16
+
17
+ it 'adds members to ZSET' do
18
+ cities = @redises.zrange(key, 0, -1, with_scores: true)
19
+ expect(cities).to be == expected_result
20
+ end
21
+ end
22
+
23
+ context 'with invalud points' do
24
+ context 'when number of arguments wrong' do
25
+ let(:message) { "ERR wrong number of arguments for 'geoadd' command" }
26
+
27
+ it 'raises Redis::CommandError' do
28
+ expect { @redises.geoadd(key, 1, 1) }
29
+ .to raise_error(Redis::CommandError, message)
30
+ end
31
+ end
32
+
33
+ context 'when coordinates are not in allowed range' do
34
+ let(:coords) { [181, 86] }
35
+ let(:message) do
36
+ formatted_coords = coords.map { |c| format('%.6f', c) }
37
+ "ERR invalid longitude,latitude pair #{formatted_coords.join(',')}"
38
+ end
39
+
40
+ after { @redises.zrem(key, 'SF') }
41
+
42
+ it 'raises Redis::CommandError' do
43
+ expect { @redises.geoadd(key, *coords, 'SF') }
44
+ .to raise_error(Redis::CommandError, message)
45
+ end
46
+ end
47
+
48
+ context 'when coordinates are not valid floats' do
49
+ let(:coords) { ['x', 35] }
50
+ let(:message) { 'ERR value is not a valid float' }
51
+
52
+ it 'raises Redis::CommandError' do
53
+ expect { @redises.geoadd key, *coords, 'SF' }
54
+ .to raise_error(Redis::CommandError, message)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples 'a distance calculator' do
4
+ it 'returns distance between two points in specified unit' do
5
+ dist = @redises.geodist(key, 'SF', 'LA', unit)
6
+ expect(dist).to be == expected_result
7
+ end
8
+ end
9
+
10
+ describe '#geodist' do
11
+ let(:key) { 'cities' }
12
+
13
+ context 'with existing key' do
14
+ let(:san_francisco) { [-122.5076404, 37.757815, 'SF'] }
15
+ let(:los_angeles) { [-118.6919259, 34.0207305, 'LA'] }
16
+
17
+ before { @redises.geoadd(key, *san_francisco, *los_angeles) }
18
+
19
+ after { @redises.zrem(key, %w[SF LA]) }
20
+
21
+ context 'with existing points only' do
22
+ context 'using m as unit' do
23
+ let(:unit) { 'm' }
24
+ let(:expected_result) { '539327.9659' }
25
+
26
+ it 'returns distance between two points in meters' do
27
+ dist = @redises.geodist(key, 'SF', 'LA')
28
+ expect(dist).to be == expected_result
29
+ end
30
+
31
+ it_behaves_like 'a distance calculator'
32
+ end
33
+
34
+ context 'using km as unit' do
35
+ let(:unit) { 'km' }
36
+ let(:expected_result) { '539.3280' }
37
+
38
+ it_behaves_like 'a distance calculator'
39
+ end
40
+
41
+ context 'using ft as unit' do
42
+ let(:unit) { 'ft' }
43
+ let(:expected_result) { '1769448.7069' }
44
+
45
+ it_behaves_like 'a distance calculator'
46
+ end
47
+
48
+ context 'using mi as unit' do
49
+ let(:unit) { 'mi' }
50
+ let(:expected_result) { '335.1237' }
51
+
52
+ it_behaves_like 'a distance calculator'
53
+ end
54
+
55
+ context 'with non-existing points only' do
56
+ it 'returns nil' do
57
+ dist = @redises.geodist(key, 'FF', 'FA')
58
+ expect(dist).to be_nil
59
+ end
60
+ end
61
+
62
+ context 'with both existing and non-existing points' do
63
+ it 'returns nil' do
64
+ dist = @redises.geodist(key, 'SF', 'FA')
65
+ expect(dist).to be_nil
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ context 'with non-existing key' do
72
+ it 'returns empty string' do
73
+ dist = @redises.geodist(key, 'SF', 'LA')
74
+ expect(dist).to be_nil
75
+ end
76
+ end
77
+
78
+ context 'with wrong number of arguments' do
79
+ let(:list) { [key, 'SF', 'LA', 'm', 'smth'] }
80
+
81
+ context 'with less than 3 arguments' do
82
+ [1, 2].each do |count|
83
+ let(:message) { "ERR wrong number of arguments for 'geodist' command" }
84
+
85
+ context "with #{count} arguments" do
86
+ it 'raises an error' do
87
+ args = list.slice(0, count)
88
+ expect { @redises.geodist(*args) }
89
+ .to raise_error(Redis::CommandError, message)
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ context 'with more than 3 arguments' do
96
+ let(:message) { 'ERR syntax error' }
97
+
98
+ it 'raises an error' do
99
+ args = list.slice(0, 5)
100
+ expect { @redises.geodist(*args) }
101
+ .to raise_error(Redis::CommandError, message)
102
+ end
103
+ end
104
+ end
105
+
106
+ context 'with wrong unit' do
107
+ let(:message) { 'ERR unsupported unit provided. please use m, km, ft, mi' }
108
+
109
+ it 'raises an error' do
110
+ expect { @redises.geodist(key, 'SF', 'LA', 'a') }
111
+ .to raise_error(Redis::CommandError, message)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe '#geohash' do
4
+ let(:key) { 'cities' }
5
+
6
+ context 'with existing key' do
7
+ let(:san_francisco) { [-122.5076404, 37.757815, 'SF'] }
8
+ let(:los_angeles) { [-118.6919259, 34.0207305, 'LA'] }
9
+
10
+ before { @redises.geoadd(key, *san_francisco, *los_angeles) }
11
+
12
+ after { @redises.zrem(key, %w[SF LA]) }
13
+
14
+ context 'with existing points only' do
15
+ let(:expected_result) do
16
+ %w[9q8yu38ejp0 9q59e171je0]
17
+ end
18
+
19
+ it 'returns decoded coordinates pairs for each point' do
20
+ results = @redises.geohash(key, 'SF', 'LA')
21
+ expect(results).to be == expected_result
22
+ end
23
+
24
+ context 'with non-existing points only' do
25
+ it 'returns array filled with nils' do
26
+ results = @redises.geohash(key, 'FF', 'FA')
27
+ expect(results).to be == [nil, nil]
28
+ end
29
+ end
30
+
31
+ context 'with both existing and non-existing points' do
32
+ let(:expected_result) do
33
+ ['9q8yu38ejp0', nil]
34
+ end
35
+
36
+ it 'returns mixture of nil and coordinates pair' do
37
+ results = @redises.geohash(key, 'SF', 'FA')
38
+ expect(results).to be == expected_result
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ context 'with non-existing key' do
45
+ before { @redises.del(key) }
46
+
47
+ it 'returns empty array' do
48
+ results = @redises.geohash(key, 'SF', 'LA')
49
+ expect(results).to be == [nil, nil]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe '#geopos' do
4
+ let(:key) { 'cities' }
5
+
6
+ context 'with existing key' do
7
+ let(:san_francisco) { [-122.5076404, 37.757815, 'SF'] }
8
+ let(:los_angeles) { [-118.6919259, 34.0207305, 'LA'] }
9
+
10
+ before { @redises.geoadd(key, *san_francisco, *los_angeles) }
11
+
12
+ after { @redises.zrem(key, %w[SF LA]) }
13
+
14
+ context 'with existing points only' do
15
+ let(:expected_result) do
16
+ [
17
+ %w[-122.5076410174369812 37.75781598995183685],
18
+ %w[-118.69192510843276978 34.020729570911179]
19
+ ]
20
+ end
21
+
22
+ it 'returns decoded coordinates pairs for each point' do
23
+ coords = @redises.geopos(key, 'SF', 'LA')
24
+ expect(coords).to be == expected_result
25
+ end
26
+
27
+ context 'with non-existing points only' do
28
+ it 'returns array filled with nils' do
29
+ coords = @redises.geopos(key, 'FF', 'FA')
30
+ expect(coords).to be == [nil, nil]
31
+ end
32
+ end
33
+
34
+ context 'with both existing and non-existing points' do
35
+ let(:expected_result) do
36
+ [%w[-122.5076410174369812 37.75781598995183685], nil]
37
+ end
38
+
39
+ it 'returns mixture of nil and coordinates pair' do
40
+ coords = @redises.geopos(key, 'SF', 'FA')
41
+ expect(coords).to be == expected_result
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'with non-existing key' do
48
+ before { @redises.del(key) }
49
+
50
+ it 'returns empty array' do
51
+ coords = @redises.geopos(key, 'SF', 'LA')
52
+ expect(coords).to be == [nil, nil]
53
+ end
54
+ end
55
+ end
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
1
  require 'spec_helper'
3
2
 
4
3
  describe '#setbit(key, offset)' do
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
1
  require 'spec_helper'
3
2
 
4
3
  describe '#strlen(key)' do
@@ -67,6 +67,18 @@ describe 'transactions (multi/exec/discard)' do
67
67
  @redises.get('counter').should == '6'
68
68
  @redises.get('test').should == '1'
69
69
  end
70
+
71
+ it 'allows multi blocks within pipelined blocks' do
72
+ @redises.set('counter', 5)
73
+ @redises.pipelined do |pr|
74
+ pr.multi do |r|
75
+ r.set('test', 1)
76
+ r.incr('counter')
77
+ end
78
+ end
79
+ @redises.get('counter').should == '6'
80
+ @redises.get('test').should == '1'
81
+ end
70
82
  end
71
83
 
72
84
  context '#discard' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mock_redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brigade Engineering
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-03-22 00:00:00.000000000 Z
12
+ date: 2018-08-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -86,6 +86,7 @@ files:
86
86
  - ".overcommit.yml"
87
87
  - ".rspec"
88
88
  - ".rubocop.yml"
89
+ - ".rubocop_todo.yml"
89
90
  - ".simplecov"
90
91
  - ".travis.yml"
91
92
  - CHANGELOG.md
@@ -99,6 +100,7 @@ files:
99
100
  - lib/mock_redis/exceptions.rb
100
101
  - lib/mock_redis/expire_wrapper.rb
101
102
  - lib/mock_redis/future.rb
103
+ - lib/mock_redis/geospatial_methods.rb
102
104
  - lib/mock_redis/hash_methods.rb
103
105
  - lib/mock_redis/indifferent_hash.rb
104
106
  - lib/mock_redis/info_method.rb
@@ -122,6 +124,7 @@ files:
122
124
  - spec/commands/bgrewriteaof_spec.rb
123
125
  - spec/commands/bgsave_spec.rb
124
126
  - spec/commands/bitcount_spec.rb
127
+ - spec/commands/bitfield_spec.rb
125
128
  - spec/commands/blpop_spec.rb
126
129
  - spec/commands/brpop_spec.rb
127
130
  - spec/commands/brpoplpush_spec.rb
@@ -140,6 +143,10 @@ files:
140
143
  - spec/commands/flushall_spec.rb
141
144
  - spec/commands/flushdb_spec.rb
142
145
  - spec/commands/future_spec.rb
146
+ - spec/commands/geoadd_spec.rb
147
+ - spec/commands/geodist_spec.rb
148
+ - spec/commands/geohash_spec.rb
149
+ - spec/commands/geopos_spec.rb
143
150
  - spec/commands/get_spec.rb
144
151
  - spec/commands/getbit_spec.rb
145
152
  - spec/commands/getrange_spec.rb
@@ -273,7 +280,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
273
280
  requirements:
274
281
  - - ">="
275
282
  - !ruby/object:Gem::Version
276
- version: '0'
283
+ version: '2.2'
277
284
  required_rubygems_version: !ruby/object:Gem::Requirement
278
285
  requirements:
279
286
  - - ">="
@@ -293,6 +300,7 @@ test_files:
293
300
  - spec/commands/bgrewriteaof_spec.rb
294
301
  - spec/commands/bgsave_spec.rb
295
302
  - spec/commands/bitcount_spec.rb
303
+ - spec/commands/bitfield_spec.rb
296
304
  - spec/commands/blpop_spec.rb
297
305
  - spec/commands/brpop_spec.rb
298
306
  - spec/commands/brpoplpush_spec.rb
@@ -311,6 +319,10 @@ test_files:
311
319
  - spec/commands/flushall_spec.rb
312
320
  - spec/commands/flushdb_spec.rb
313
321
  - spec/commands/future_spec.rb
322
+ - spec/commands/geoadd_spec.rb
323
+ - spec/commands/geodist_spec.rb
324
+ - spec/commands/geohash_spec.rb
325
+ - spec/commands/geopos_spec.rb
314
326
  - spec/commands/get_spec.rb
315
327
  - spec/commands/getbit_spec.rb
316
328
  - spec/commands/getrange_spec.rb