mock_redis 0.19.0 → 0.24.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +32 -5
- data/.rubocop_todo.yml +1 -1
- data/.travis.yml +9 -10
- data/CHANGELOG.md +46 -0
- data/Gemfile +2 -2
- data/LICENSE.md +21 -0
- data/README.md +39 -15
- data/lib/mock_redis.rb +0 -5
- data/lib/mock_redis/database.rb +59 -22
- data/lib/mock_redis/future.rb +1 -1
- data/lib/mock_redis/geospatial_methods.rb +14 -22
- data/lib/mock_redis/hash_methods.rb +23 -15
- data/lib/mock_redis/indifferent_hash.rb +0 -8
- data/lib/mock_redis/info_method.rb +2 -2
- data/lib/mock_redis/list_methods.rb +2 -2
- data/lib/mock_redis/multi_db_wrapper.rb +2 -2
- data/lib/mock_redis/pipelined_wrapper.rb +25 -6
- data/lib/mock_redis/set_methods.rb +16 -4
- data/lib/mock_redis/stream.rb +62 -0
- data/lib/mock_redis/stream/id.rb +60 -0
- data/lib/mock_redis/stream_methods.rb +87 -0
- data/lib/mock_redis/string_methods.rb +31 -20
- data/lib/mock_redis/transaction_wrapper.rb +27 -14
- data/lib/mock_redis/utility_methods.rb +6 -3
- data/lib/mock_redis/version.rb +1 -1
- data/lib/mock_redis/zset_methods.rb +54 -11
- data/mock_redis.gemspec +6 -6
- data/spec/client_spec.rb +12 -0
- data/spec/commands/blpop_spec.rb +0 -6
- data/spec/commands/brpop_spec.rb +6 -5
- data/spec/commands/dump_spec.rb +19 -0
- data/spec/commands/exists_spec.rb +34 -5
- data/spec/commands/future_spec.rb +11 -1
- data/spec/commands/geoadd_spec.rb +1 -1
- data/spec/commands/geodist_spec.rb +8 -4
- data/spec/commands/geohash_spec.rb +4 -4
- data/spec/commands/geopos_spec.rb +4 -4
- data/spec/commands/get_spec.rb +1 -0
- data/spec/commands/hdel_spec.rb +18 -2
- data/spec/commands/hmset_spec.rb +26 -0
- data/spec/commands/hset_spec.rb +6 -6
- data/spec/commands/keys_spec.rb +17 -0
- data/spec/commands/mget_spec.rb +34 -15
- data/spec/commands/move_spec.rb +5 -5
- data/spec/commands/mset_spec.rb +14 -0
- data/spec/commands/pipelined_spec.rb +72 -0
- data/spec/commands/restore_spec.rb +47 -0
- data/spec/commands/scan_spec.rb +9 -0
- data/spec/commands/set_spec.rb +12 -2
- data/spec/commands/setbit_spec.rb +1 -0
- data/spec/commands/setex_spec.rb +16 -0
- data/spec/commands/spop_spec.rb +15 -0
- data/spec/commands/srandmember_spec.rb +1 -1
- data/spec/commands/srem_spec.rb +5 -0
- data/spec/commands/watch_spec.rb +8 -3
- data/spec/commands/xadd_spec.rb +104 -0
- data/spec/commands/xlen_spec.rb +20 -0
- data/spec/commands/xrange_spec.rb +141 -0
- data/spec/commands/xrevrange_spec.rb +130 -0
- data/spec/commands/xtrim_spec.rb +30 -0
- data/spec/commands/zinterstore_spec.rb +34 -0
- data/spec/commands/zpopmax_spec.rb +60 -0
- data/spec/commands/zpopmin_spec.rb +60 -0
- data/spec/commands/zrange_spec.rb +1 -1
- data/spec/commands/zrangebyscore_spec.rb +1 -1
- data/spec/commands/zrevrange_spec.rb +1 -1
- data/spec/commands/zrevrangebyscore_spec.rb +1 -1
- data/spec/commands/zunionstore_spec.rb +33 -0
- data/spec/mock_redis_spec.rb +4 -6
- data/spec/spec_helper.rb +6 -2
- data/spec/support/redis_multiplexer.rb +18 -1
- data/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +14 -0
- data/spec/support/shared_examples/only_operates_on_hashes.rb +2 -0
- data/spec/support/shared_examples/only_operates_on_lists.rb +2 -0
- data/spec/support/shared_examples/only_operates_on_sets.rb +2 -0
- data/spec/support/shared_examples/only_operates_on_zsets.rb +2 -0
- data/spec/transactions_spec.rb +30 -26
- metadata +45 -31
- data/LICENSE +0 -19
- data/spec/commands/hash_operator_spec.rb +0 -21
data/lib/mock_redis/future.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class MockRedis
|
2
2
|
module GeospatialMethods
|
3
|
-
LNG_RANGE = (-180..180)
|
4
|
-
LAT_RANGE = (-85.05112878..85.05112878)
|
3
|
+
LNG_RANGE = (-180..180).freeze
|
4
|
+
LAT_RANGE = (-85.05112878..85.05112878).freeze
|
5
5
|
STEP = 26
|
6
6
|
UNITS = {
|
7
7
|
m: 1,
|
@@ -23,21 +23,13 @@ class MockRedis
|
|
23
23
|
zadd(key, scored_points)
|
24
24
|
end
|
25
25
|
|
26
|
-
def geodist(key,
|
27
|
-
|
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
|
26
|
+
def geodist(key, member1, member2, unit = 'm')
|
27
|
+
to_meter = parse_unit(unit)
|
36
28
|
|
37
29
|
return nil if zcard(key).zero?
|
38
30
|
|
39
|
-
score1 = zscore(key,
|
40
|
-
score2 = zscore(key,
|
31
|
+
score1 = zscore(key, member1)
|
32
|
+
score2 = zscore(key, member2)
|
41
33
|
return nil if score1.nil? || score2.nil?
|
42
34
|
hash1 = { bits: score1.to_i, step: STEP }
|
43
35
|
hash2 = { bits: score2.to_i, step: STEP }
|
@@ -46,15 +38,15 @@ class MockRedis
|
|
46
38
|
lng2, lat2 = geohash_decode(hash2)
|
47
39
|
|
48
40
|
distance = geohash_distance(lng1, lat1, lng2, lat2) / to_meter
|
49
|
-
format('
|
41
|
+
format('%<distance>.4f', distance: distance)
|
50
42
|
end
|
51
43
|
|
52
|
-
def geohash(key,
|
44
|
+
def geohash(key, members)
|
53
45
|
lng_range = (-180..180)
|
54
46
|
lat_range = (-90..90)
|
55
47
|
geoalphabet = '0123456789bcdefghjkmnpqrstuvwxyz'
|
56
48
|
|
57
|
-
members.map do |member|
|
49
|
+
Array(members).map do |member|
|
58
50
|
score = zscore(key, member)
|
59
51
|
next nil unless score
|
60
52
|
score = score.to_i
|
@@ -71,8 +63,8 @@ class MockRedis
|
|
71
63
|
end
|
72
64
|
end
|
73
65
|
|
74
|
-
def geopos(key,
|
75
|
-
members.map do |member|
|
66
|
+
def geopos(key, members)
|
67
|
+
Array(members).map do |member|
|
76
68
|
score = zscore(key, member)
|
77
69
|
next nil unless score
|
78
70
|
hash = { bits: score.to_i, step: STEP }
|
@@ -103,8 +95,8 @@ class MockRedis
|
|
103
95
|
lat = Float(point[1])
|
104
96
|
|
105
97
|
unless LNG_RANGE.include?(lng) && LAT_RANGE.include?(lat)
|
106
|
-
lng = format('
|
107
|
-
lat = format('
|
98
|
+
lng = format('%<long>.6f', long: lng)
|
99
|
+
lat = format('%<lat>.6f', lat: lat)
|
108
100
|
raise Redis::CommandError,
|
109
101
|
"ERR invalid longitude,latitude pair #{lng},#{lat}"
|
110
102
|
end
|
@@ -209,7 +201,7 @@ class MockRedis
|
|
209
201
|
end
|
210
202
|
|
211
203
|
def format_decoded_coord(coord)
|
212
|
-
coord = format('
|
204
|
+
coord = format('%<coord>.17f', coord: coord)
|
213
205
|
l = 1
|
214
206
|
l += 1 while coord[-l] == '0'
|
215
207
|
coord = coord[0..-l]
|
@@ -6,16 +6,12 @@ class MockRedis
|
|
6
6
|
include Assertions
|
7
7
|
include UtilityMethods
|
8
8
|
|
9
|
-
def hdel(key,
|
9
|
+
def hdel(key, *fields)
|
10
10
|
with_hash_at(key) do |hash|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
orig_size - hash.size
|
16
|
-
else
|
17
|
-
hash.delete(field.to_s) ? 1 : 0
|
18
|
-
end
|
11
|
+
orig_size = hash.size
|
12
|
+
fields = Array(fields).flatten.map(&:to_s)
|
13
|
+
hash.delete_if { |k, _v| fields.include?(k) }
|
14
|
+
orig_size - hash.size
|
19
15
|
end
|
20
16
|
end
|
21
17
|
|
@@ -88,10 +84,17 @@ class MockRedis
|
|
88
84
|
end
|
89
85
|
|
90
86
|
def hmset(key, *kvpairs)
|
87
|
+
if key.is_a? Array
|
88
|
+
err_msg = 'ERR wrong number of arguments for \'hmset\' command'
|
89
|
+
kvpairs = key[1..-1]
|
90
|
+
key = key[0]
|
91
|
+
end
|
92
|
+
|
91
93
|
kvpairs.flatten!
|
92
94
|
assert_has_args(kvpairs, 'hmset')
|
95
|
+
|
93
96
|
if kvpairs.length.odd?
|
94
|
-
raise Redis::CommandError, 'ERR wrong number of arguments for HMSET'
|
97
|
+
raise Redis::CommandError, err_msg || 'ERR wrong number of arguments for HMSET'
|
95
98
|
end
|
96
99
|
|
97
100
|
kvpairs.each_slice(2) do |(k, v)|
|
@@ -125,10 +128,15 @@ class MockRedis
|
|
125
128
|
end
|
126
129
|
end
|
127
130
|
|
128
|
-
def hset(key,
|
129
|
-
|
130
|
-
with_hash_at(key)
|
131
|
-
|
131
|
+
def hset(key, *args)
|
132
|
+
added = 0
|
133
|
+
with_hash_at(key) do |hash|
|
134
|
+
args.each_slice(2) do |field, value|
|
135
|
+
added += 1 unless hash.key?(field.to_s)
|
136
|
+
hash[field.to_s] = value.to_s
|
137
|
+
end
|
138
|
+
end
|
139
|
+
added
|
132
140
|
end
|
133
141
|
|
134
142
|
def hsetnx(key, field, value)
|
@@ -147,7 +155,7 @@ class MockRedis
|
|
147
155
|
private
|
148
156
|
|
149
157
|
def with_hash_at(key, &blk)
|
150
|
-
with_thing_at(key, :assert_hashy, proc { {} }, &blk)
|
158
|
+
with_thing_at(key.to_s, :assert_hashy, proc { {} }, &blk)
|
151
159
|
end
|
152
160
|
|
153
161
|
def hashy?(key)
|
@@ -83,7 +83,7 @@ class MockRedis
|
|
83
83
|
|
84
84
|
# The Ruby Redis client returns commandstats differently when it's called as
|
85
85
|
# "INFO commandstats".
|
86
|
-
# rubocop:disable
|
86
|
+
# rubocop:disable Layout/LineLength
|
87
87
|
COMMAND_STATS_SOLO_INFO = {
|
88
88
|
'auth' => { 'calls' => '572501', 'usec' => '2353163', 'usec_per_call' => '4.11' },
|
89
89
|
'client' => { 'calls' => '1', 'usec' => '80', 'usec_per_call' => '80.00' },
|
@@ -123,7 +123,7 @@ class MockRedis
|
|
123
123
|
'cmdstat_smembers' => 'calls=58,usec=231,usec_per_call=3.98',
|
124
124
|
'cmdstat_sunionstore' => 'calls=4185027,usec=11762454022,usec_per_call=2810.60',
|
125
125
|
}.freeze
|
126
|
-
# rubocop:enable
|
126
|
+
# rubocop:enable Layout/LineLength
|
127
127
|
|
128
128
|
DEFAULT_INFO = [
|
129
129
|
SERVER_INFO,
|
@@ -146,13 +146,13 @@ class MockRedis
|
|
146
146
|
|
147
147
|
def ltrim(key, start, stop)
|
148
148
|
with_list_at(key) do |list|
|
149
|
-
list
|
149
|
+
list&.replace(list[[start.to_i, -list.length].max..stop.to_i] || [])
|
150
150
|
'OK'
|
151
151
|
end
|
152
152
|
end
|
153
153
|
|
154
154
|
def rpop(key)
|
155
|
-
with_list_at(key) { |list| list
|
155
|
+
with_list_at(key) { |list| list&.pop }
|
156
156
|
end
|
157
157
|
|
158
158
|
def rpoplpush(source, destination)
|
@@ -24,7 +24,7 @@ class MockRedis
|
|
24
24
|
def initialize_copy(source)
|
25
25
|
super
|
26
26
|
@databases = @databases.clone
|
27
|
-
@databases.
|
27
|
+
@databases.each_key do |k|
|
28
28
|
@databases[k] = @databases[k].clone
|
29
29
|
end
|
30
30
|
end
|
@@ -39,7 +39,7 @@ class MockRedis
|
|
39
39
|
src = current_db
|
40
40
|
dest = db(db_index)
|
41
41
|
|
42
|
-
if !src.exists(key) || dest.exists(key)
|
42
|
+
if !src.exists?(key) || dest.exists?(key)
|
43
43
|
false
|
44
44
|
else
|
45
45
|
case current_db.type(key)
|
@@ -9,7 +9,7 @@ class MockRedis
|
|
9
9
|
def initialize(db)
|
10
10
|
@db = db
|
11
11
|
@pipelined_futures = []
|
12
|
-
@
|
12
|
+
@nesting_level = 0
|
13
13
|
end
|
14
14
|
|
15
15
|
def initialize_copy(source)
|
@@ -19,7 +19,7 @@ class MockRedis
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def method_missing(method, *args, &block)
|
22
|
-
if
|
22
|
+
if in_pipeline?
|
23
23
|
future = MockRedis::Future.new([method, *args], block)
|
24
24
|
@pipelined_futures << future
|
25
25
|
future
|
@@ -29,9 +29,17 @@ class MockRedis
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def pipelined(_options = {})
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
begin
|
33
|
+
@nesting_level += 1
|
34
|
+
yield self
|
35
|
+
ensure
|
36
|
+
@nesting_level -= 1
|
37
|
+
end
|
38
|
+
|
39
|
+
if in_pipeline?
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
35
43
|
responses = @pipelined_futures.flat_map do |future|
|
36
44
|
begin
|
37
45
|
result = if future.block
|
@@ -40,7 +48,12 @@ class MockRedis
|
|
40
48
|
send(*future.command)
|
41
49
|
end
|
42
50
|
future.store_result(result)
|
43
|
-
|
51
|
+
|
52
|
+
if future.block
|
53
|
+
result
|
54
|
+
else
|
55
|
+
[result]
|
56
|
+
end
|
44
57
|
rescue StandardError => e
|
45
58
|
e
|
46
59
|
end
|
@@ -48,5 +61,11 @@ class MockRedis
|
|
48
61
|
@pipelined_futures = []
|
49
62
|
responses
|
50
63
|
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def in_pipeline?
|
68
|
+
@nesting_level > 0
|
69
|
+
end
|
51
70
|
end
|
52
71
|
end
|
@@ -81,11 +81,22 @@ class MockRedis
|
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
84
|
-
def spop(key)
|
84
|
+
def spop(key, count = nil)
|
85
85
|
with_set_at(key) do |set|
|
86
|
-
|
87
|
-
|
88
|
-
|
86
|
+
if count.nil?
|
87
|
+
member = set.first
|
88
|
+
set.delete(member)
|
89
|
+
member
|
90
|
+
else
|
91
|
+
members = []
|
92
|
+
count.times do
|
93
|
+
member = set.first
|
94
|
+
break if member.nil?
|
95
|
+
set.delete(member)
|
96
|
+
members << member
|
97
|
+
end
|
98
|
+
members
|
99
|
+
end
|
89
100
|
end
|
90
101
|
end
|
91
102
|
|
@@ -106,6 +117,7 @@ class MockRedis
|
|
106
117
|
with_set_at(key) do |s|
|
107
118
|
if members.is_a?(Array)
|
108
119
|
orig_size = s.size
|
120
|
+
members = members.map(&:to_s)
|
109
121
|
s.delete_if { |m| members.include?(m) }
|
110
122
|
orig_size - s.size
|
111
123
|
else
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'set'
|
3
|
+
require 'date'
|
4
|
+
require 'mock_redis/stream/id'
|
5
|
+
|
6
|
+
class MockRedis
|
7
|
+
class Stream
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_accessor :members
|
12
|
+
|
13
|
+
def_delegators :members, :empty?
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@members = Set.new
|
17
|
+
@last_id = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_id
|
21
|
+
@last_id.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(id, values)
|
25
|
+
@last_id = MockRedis::Stream::Id.new(id, min: @last_id)
|
26
|
+
members.add [@last_id, Hash[values.map { |k, v| [k.to_s, v.to_s] }]]
|
27
|
+
@last_id.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def trim(count)
|
31
|
+
deleted = @members.size - count
|
32
|
+
@members = @members.to_a[-count..-1].to_set
|
33
|
+
deleted
|
34
|
+
end
|
35
|
+
|
36
|
+
def range(start, finish, reversed, *opts_in)
|
37
|
+
opts = options opts_in, ['count']
|
38
|
+
start_id = MockRedis::Stream::Id.new(start)
|
39
|
+
finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY)
|
40
|
+
items = members
|
41
|
+
.select { |m| (start_id <= m[0]) && (finish_id >= m[0]) }
|
42
|
+
.map { |m| [m[0].to_s, m[1]] }
|
43
|
+
items.reverse! if reversed
|
44
|
+
return items.first(opts['count'].to_i) if opts.key?('count')
|
45
|
+
items
|
46
|
+
end
|
47
|
+
|
48
|
+
def each
|
49
|
+
members.each { |m| yield m }
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def options(opts_in, permitted)
|
55
|
+
opts_out = {}
|
56
|
+
raise Redis::CommandError, 'ERR syntax error' unless (opts_in.length % 2).zero?
|
57
|
+
opts_in.each_slice(2).map { |pair| opts_out[pair[0].downcase] = pair[1] }
|
58
|
+
raise Redis::CommandError, 'ERR syntax error' unless (opts_out.keys - permitted).empty?
|
59
|
+
opts_out
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class MockRedis
|
2
|
+
class Stream
|
3
|
+
class Id
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
attr_accessor :timestamp, :sequence
|
7
|
+
|
8
|
+
def initialize(id, min: nil, sequence: 0)
|
9
|
+
case id
|
10
|
+
when '*'
|
11
|
+
@timestamp = (Time.now.to_f * 1000).to_i
|
12
|
+
@sequence = 0
|
13
|
+
if self <= min
|
14
|
+
@timestamp = min.timestamp
|
15
|
+
@sequence = min.sequence + 1
|
16
|
+
end
|
17
|
+
when '-'
|
18
|
+
@timestamp = @sequence = 0
|
19
|
+
when '+'
|
20
|
+
@timestamp = @sequence = Float::INFINITY
|
21
|
+
else
|
22
|
+
if id.is_a? String
|
23
|
+
(_, @timestamp, @sequence) = id.match(/^(\d+)-?(\d+)?$/)
|
24
|
+
.to_a
|
25
|
+
if @timestamp.nil?
|
26
|
+
raise Redis::CommandError,
|
27
|
+
'ERR Invalid stream ID specified as stream command argument'
|
28
|
+
end
|
29
|
+
@timestamp = @timestamp.to_i
|
30
|
+
else
|
31
|
+
@timestamp = id
|
32
|
+
end
|
33
|
+
@sequence = @sequence.nil? ? sequence : @sequence.to_i
|
34
|
+
if @timestamp == 0 && @sequence == 0
|
35
|
+
raise Redis::CommandError,
|
36
|
+
'ERR The ID specified in XADD is equal or smaller than ' \
|
37
|
+
'the target stream top item'
|
38
|
+
# TOOD: Redis version 6.0.4, w redis 4.2.1 generates the following error message:
|
39
|
+
# 'ERR The ID specified in XADD must be greater than 0-0'
|
40
|
+
end
|
41
|
+
if self <= min
|
42
|
+
raise Redis::CommandError,
|
43
|
+
'ERR The ID specified in XADD is equal or smaller than ' \
|
44
|
+
'the target stream top item'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"#{@timestamp}-#{@sequence}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def <=>(other)
|
54
|
+
return 1 if other.nil?
|
55
|
+
return @sequence <=> other.sequence if @timestamp == other.timestamp
|
56
|
+
@timestamp <=> other.timestamp
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|