mock_redis 0.18.0 → 0.19.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.
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