mock_redis 0.5.4 → 0.31.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/lint.yml +31 -0
- data/.github/workflows/tests.yml +63 -0
- data/.gitignore +1 -1
- data/.overcommit.yml +21 -0
- data/.rspec +1 -1
- data/.rubocop.yml +148 -0
- data/.rubocop_todo.yml +35 -0
- data/.simplecov +4 -0
- data/CHANGELOG.md +278 -0
- data/Gemfile +9 -5
- data/LICENSE.md +21 -0
- data/README.md +52 -16
- data/Rakefile +0 -8
- data/lib/mock_redis/assertions.rb +0 -1
- data/lib/mock_redis/connection_method.rb +13 -0
- data/lib/mock_redis/database.rb +193 -257
- data/lib/mock_redis/expire_wrapper.rb +2 -2
- data/lib/mock_redis/future.rb +23 -0
- data/lib/mock_redis/geospatial_methods.rb +240 -0
- data/lib/mock_redis/hash_methods.rb +83 -24
- data/lib/mock_redis/indifferent_hash.rb +11 -0
- data/lib/mock_redis/info_method.rb +160 -0
- data/lib/mock_redis/list_methods.rb +34 -19
- data/lib/mock_redis/multi_db_wrapper.rb +8 -7
- data/lib/mock_redis/pipelined_wrapper.rb +42 -16
- data/lib/mock_redis/set_methods.rb +62 -19
- data/lib/mock_redis/sort_method.rb +81 -0
- data/lib/mock_redis/stream/id.rb +58 -0
- data/lib/mock_redis/stream.rb +88 -0
- data/lib/mock_redis/stream_methods.rb +102 -0
- data/lib/mock_redis/string_methods.rb +235 -42
- data/lib/mock_redis/transaction_wrapper.rb +62 -28
- data/lib/mock_redis/utility_methods.rb +62 -11
- data/lib/mock_redis/version.rb +4 -1
- data/lib/mock_redis/zset.rb +24 -29
- data/lib/mock_redis/zset_methods.rb +187 -59
- data/lib/mock_redis.rb +77 -27
- data/mock_redis.gemspec +23 -15
- data/spec/client_spec.rb +29 -0
- data/spec/cloning_spec.rb +17 -18
- data/spec/commands/append_spec.rb +4 -4
- data/spec/commands/auth_spec.rb +1 -1
- data/spec/commands/bgrewriteaof_spec.rb +2 -2
- data/spec/commands/bgsave_spec.rb +2 -2
- data/spec/commands/bitcount_spec.rb +25 -0
- data/spec/commands/bitfield_spec.rb +169 -0
- data/spec/commands/blpop_spec.rb +19 -21
- data/spec/commands/brpop_spec.rb +25 -20
- data/spec/commands/brpoplpush_spec.rb +16 -17
- data/spec/commands/connected_spec.rb +7 -0
- data/spec/commands/connection_spec.rb +15 -0
- data/spec/commands/dbsize_spec.rb +3 -3
- data/spec/commands/decr_spec.rb +8 -8
- data/spec/commands/decrby_spec.rb +8 -8
- data/spec/commands/del_spec.rb +35 -3
- data/spec/commands/disconnect_spec.rb +7 -0
- data/spec/commands/dump_spec.rb +19 -0
- data/spec/commands/echo_spec.rb +4 -4
- data/spec/commands/eval_spec.rb +7 -0
- data/spec/commands/evalsha_spec.rb +10 -0
- data/spec/commands/exists_spec.rb +36 -7
- data/spec/commands/expire_spec.rb +48 -20
- data/spec/commands/expireat_spec.rb +12 -13
- data/spec/commands/flushall_spec.rb +5 -5
- data/spec/commands/flushdb_spec.rb +5 -5
- data/spec/commands/future_spec.rb +30 -0
- data/spec/commands/geoadd_spec.rb +58 -0
- data/spec/commands/geodist_spec.rb +118 -0
- data/spec/commands/geohash_spec.rb +52 -0
- data/spec/commands/geopos_spec.rb +55 -0
- data/spec/commands/get_spec.rb +14 -6
- data/spec/commands/getbit_spec.rb +7 -7
- data/spec/commands/getrange_spec.rb +9 -9
- data/spec/commands/getset_spec.rb +7 -7
- data/spec/commands/hdel_spec.rb +41 -11
- data/spec/commands/hexists_spec.rb +11 -11
- data/spec/commands/hget_spec.rb +7 -7
- data/spec/commands/hgetall_spec.rb +15 -5
- data/spec/commands/hincrby_spec.rb +16 -16
- data/spec/commands/hincrbyfloat_spec.rb +58 -0
- data/spec/commands/hkeys_spec.rb +5 -5
- data/spec/commands/hlen_spec.rb +5 -5
- data/spec/commands/hmget_spec.rb +19 -9
- data/spec/commands/hmset_spec.rb +38 -12
- data/spec/commands/hscan_each_spec.rb +48 -0
- data/spec/commands/hscan_spec.rb +27 -0
- data/spec/commands/hset_spec.rb +26 -12
- data/spec/commands/hsetnx_spec.rb +16 -16
- data/spec/commands/hvals_spec.rb +5 -5
- data/spec/commands/incr_spec.rb +8 -8
- data/spec/commands/incrby_spec.rb +13 -13
- data/spec/commands/incrbyfloat_spec.rb +13 -13
- data/spec/commands/info_spec.rb +54 -5
- data/spec/commands/keys_spec.rb +83 -31
- data/spec/commands/lastsave_spec.rb +2 -2
- data/spec/commands/lindex_spec.rb +20 -10
- data/spec/commands/linsert_spec.rb +14 -14
- data/spec/commands/llen_spec.rb +4 -4
- data/spec/commands/lpop_spec.rb +6 -6
- data/spec/commands/lpush_spec.rb +21 -15
- data/spec/commands/lpushx_spec.rb +24 -11
- data/spec/commands/lrange_spec.rb +24 -8
- data/spec/commands/lrem_spec.rb +16 -16
- data/spec/commands/lset_spec.rb +17 -12
- data/spec/commands/ltrim_spec.rb +17 -7
- data/spec/commands/mapped_hmget_spec.rb +13 -9
- data/spec/commands/mapped_hmset_spec.rb +12 -12
- data/spec/commands/mapped_mget_spec.rb +22 -0
- data/spec/commands/mapped_mset_spec.rb +19 -0
- data/spec/commands/mapped_msetnx_spec.rb +26 -0
- data/spec/commands/mget_spec.rb +48 -17
- data/spec/commands/move_spec.rb +37 -37
- data/spec/commands/mset_spec.rb +20 -6
- data/spec/commands/msetnx_spec.rb +14 -14
- data/spec/commands/persist_spec.rb +15 -16
- data/spec/commands/pexpire_spec.rb +86 -0
- data/spec/commands/pexpireat_spec.rb +48 -0
- data/spec/commands/ping_spec.rb +6 -2
- data/spec/commands/pipelined_spec.rb +98 -7
- data/spec/commands/pttl_spec.rb +41 -0
- data/spec/commands/randomkey_spec.rb +3 -3
- data/spec/commands/rename_spec.rb +16 -12
- data/spec/commands/renamenx_spec.rb +13 -15
- data/spec/commands/restore_spec.rb +47 -0
- data/spec/commands/rpop_spec.rb +6 -6
- data/spec/commands/rpoplpush_spec.rb +13 -8
- data/spec/commands/rpush_spec.rb +21 -15
- data/spec/commands/rpushx_spec.rb +24 -11
- data/spec/commands/sadd_spec.rb +14 -10
- data/spec/commands/scan_each_spec.rb +39 -0
- data/spec/commands/scan_spec.rb +64 -0
- data/spec/commands/scard_spec.rb +3 -3
- data/spec/commands/script_spec.rb +9 -0
- data/spec/commands/sdiff_spec.rb +13 -13
- data/spec/commands/sdiffstore_spec.rb +13 -13
- data/spec/commands/select_spec.rb +13 -5
- data/spec/commands/set_spec.rb +112 -0
- data/spec/commands/setbit_spec.rb +25 -16
- data/spec/commands/setex_spec.rb +20 -4
- data/spec/commands/setnx_spec.rb +6 -6
- data/spec/commands/setrange_spec.rb +12 -12
- data/spec/commands/sinter_spec.rb +11 -13
- data/spec/commands/sinterstore_spec.rb +12 -12
- data/spec/commands/sismember_spec.rb +10 -10
- data/spec/commands/smembers_spec.rb +15 -5
- data/spec/commands/smove_spec.rb +13 -13
- data/spec/commands/sort_list_spec.rb +21 -0
- data/spec/commands/sort_set_spec.rb +21 -0
- data/spec/commands/sort_zset_spec.rb +21 -0
- data/spec/commands/spop_spec.rb +19 -4
- data/spec/commands/srandmember_spec.rb +28 -4
- data/spec/commands/srem_spec.rb +17 -12
- data/spec/commands/sscan_each_spec.rb +48 -0
- data/spec/commands/sscan_spec.rb +39 -0
- data/spec/commands/strlen_spec.rb +4 -5
- data/spec/commands/sunion_spec.rb +13 -11
- data/spec/commands/sunionstore_spec.rb +12 -12
- data/spec/commands/ttl_spec.rb +11 -6
- data/spec/commands/type_spec.rb +1 -1
- data/spec/commands/watch_spec.rb +9 -4
- data/spec/commands/xadd_spec.rb +122 -0
- data/spec/commands/xlen_spec.rb +22 -0
- data/spec/commands/xrange_spec.rb +164 -0
- data/spec/commands/xread_spec.rb +66 -0
- data/spec/commands/xrevrange_spec.rb +130 -0
- data/spec/commands/xtrim_spec.rb +36 -0
- data/spec/commands/zadd_spec.rb +100 -11
- data/spec/commands/zcard_spec.rb +4 -4
- data/spec/commands/zcount_spec.rb +18 -10
- data/spec/commands/zincrby_spec.rb +6 -6
- data/spec/commands/zinterstore_spec.rb +54 -20
- data/spec/commands/zpopmax_spec.rb +60 -0
- data/spec/commands/zpopmin_spec.rb +60 -0
- data/spec/commands/zrange_spec.rb +54 -13
- data/spec/commands/zrangebyscore_spec.rb +42 -27
- data/spec/commands/zrank_spec.rb +4 -4
- data/spec/commands/zrem_spec.rb +18 -12
- data/spec/commands/zremrangebyrank_spec.rb +5 -5
- data/spec/commands/zremrangebyscore_spec.rb +12 -5
- data/spec/commands/zrevrange_spec.rb +35 -10
- data/spec/commands/zrevrangebyscore_spec.rb +26 -15
- data/spec/commands/zrevrank_spec.rb +4 -4
- data/spec/commands/zscan_each_spec.rb +48 -0
- data/spec/commands/zscan_spec.rb +26 -0
- data/spec/commands/zscore_spec.rb +7 -7
- data/spec/commands/zunionstore_spec.rb +54 -21
- data/spec/mock_redis_spec.rb +61 -0
- data/spec/spec_helper.rb +35 -8
- data/spec/support/redis_multiplexer.rb +62 -37
- data/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +14 -0
- data/spec/support/shared_examples/only_operates_on_hashes.rb +5 -3
- data/spec/support/shared_examples/only_operates_on_lists.rb +5 -3
- data/spec/support/shared_examples/only_operates_on_sets.rb +5 -3
- data/spec/support/shared_examples/only_operates_on_strings.rb +4 -4
- data/spec/support/shared_examples/only_operates_on_zsets.rb +18 -16
- data/spec/support/shared_examples/sorts_enumerables.rb +56 -0
- data/spec/transactions_spec.rb +79 -29
- metadata +162 -42
- data/LICENSE +0 -19
- data/spec/commands/hash_operator_spec.rb +0 -21
@@ -32,7 +32,9 @@ class MockRedis
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
def brpoplpush(source, destination,
|
35
|
+
def brpoplpush(source, destination, options = {})
|
36
|
+
options = { :timeout => options } if options.is_a?(Integer)
|
37
|
+
timeout = options.is_a?(Hash) && options[:timeout] || 0
|
36
38
|
assert_valid_timeout(timeout)
|
37
39
|
|
38
40
|
if llen(source) > 0
|
@@ -45,12 +47,12 @@ class MockRedis
|
|
45
47
|
end
|
46
48
|
|
47
49
|
def lindex(key, index)
|
48
|
-
with_list_at(key) {|l| l[index]}
|
50
|
+
with_list_at(key) { |l| l[index.to_i] }
|
49
51
|
end
|
50
52
|
|
51
53
|
def linsert(key, position, pivot, value)
|
52
54
|
unless %w[before after].include?(position.to_s)
|
53
|
-
raise Redis::CommandError,
|
55
|
+
raise Redis::CommandError, 'ERR syntax error'
|
54
56
|
end
|
55
57
|
|
56
58
|
assert_listy(key)
|
@@ -82,23 +84,29 @@ class MockRedis
|
|
82
84
|
|
83
85
|
def lpush(key, values)
|
84
86
|
values = [values] unless values.is_a?(Array)
|
85
|
-
|
87
|
+
assert_has_args(values, 'lpush')
|
88
|
+
with_list_at(key) { |l| values.each { |v| l.unshift(v.to_s) } }
|
86
89
|
llen(key)
|
87
90
|
end
|
88
91
|
|
89
92
|
def lpushx(key, value)
|
93
|
+
value = [value] unless value.is_a?(Array)
|
94
|
+
if value.empty?
|
95
|
+
raise Redis::CommandError, "ERR wrong number of arguments for 'lpushx' command"
|
96
|
+
end
|
90
97
|
assert_listy(key)
|
91
98
|
return 0 unless list_at?(key)
|
92
99
|
lpush(key, value)
|
93
100
|
end
|
94
101
|
|
95
102
|
def lrange(key, start, stop)
|
96
|
-
|
103
|
+
start = start.to_i
|
104
|
+
with_list_at(key) { |l| start < l.size ? l[[start, -l.length].max..stop.to_i] : [] }
|
97
105
|
end
|
98
106
|
|
99
107
|
def lrem(key, count, value)
|
100
108
|
unless looks_like_integer?(count.to_s)
|
101
|
-
raise Redis::CommandError,
|
109
|
+
raise Redis::CommandError, 'ERR value is not an integer or out of range'
|
102
110
|
end
|
103
111
|
count = count.to_i
|
104
112
|
value = value.to_s
|
@@ -116,7 +124,7 @@ class MockRedis
|
|
116
124
|
indices_with_value.reverse.take(-count)
|
117
125
|
end
|
118
126
|
|
119
|
-
indices_to_delete.each {|i| list.delete_at(i)}.length
|
127
|
+
indices_to_delete.each { |i| list.delete_at(i) }.length
|
120
128
|
end
|
121
129
|
end
|
122
130
|
|
@@ -124,11 +132,12 @@ class MockRedis
|
|
124
132
|
assert_listy(key)
|
125
133
|
|
126
134
|
unless list_at?(key)
|
127
|
-
raise Redis::CommandError,
|
135
|
+
raise Redis::CommandError, 'ERR no such key'
|
128
136
|
end
|
129
137
|
|
130
|
-
|
131
|
-
|
138
|
+
index = index.to_i
|
139
|
+
unless (0...llen(key)).cover?(index)
|
140
|
+
raise Redis::CommandError, 'ERR index out of range'
|
132
141
|
end
|
133
142
|
|
134
143
|
data[key][index] = value.to_s
|
@@ -137,56 +146,62 @@ class MockRedis
|
|
137
146
|
|
138
147
|
def ltrim(key, start, stop)
|
139
148
|
with_list_at(key) do |list|
|
140
|
-
list
|
149
|
+
list&.replace(list[[start.to_i, -list.length].max..stop.to_i] || [])
|
141
150
|
'OK'
|
142
151
|
end
|
143
152
|
end
|
144
153
|
|
145
154
|
def rpop(key)
|
146
|
-
with_list_at(key) {|list| list
|
155
|
+
with_list_at(key) { |list| list&.pop }
|
147
156
|
end
|
148
157
|
|
149
158
|
def rpoplpush(source, destination)
|
150
159
|
value = rpop(source)
|
151
|
-
lpush(destination, value)
|
160
|
+
lpush(destination, value) unless value.nil?
|
152
161
|
value
|
153
162
|
end
|
154
163
|
|
155
164
|
def rpush(key, values)
|
156
165
|
values = [values] unless values.is_a?(Array)
|
157
|
-
|
166
|
+
assert_has_args(values, 'rpush')
|
167
|
+
with_list_at(key) { |l| values.each { |v| l.push(v.to_s) } }
|
158
168
|
llen(key)
|
159
169
|
end
|
160
170
|
|
161
171
|
def rpushx(key, value)
|
172
|
+
value = [value] unless value.is_a?(Array)
|
173
|
+
if value.empty?
|
174
|
+
raise Redis::CommandError, "ERR wrong number of arguments for 'rpushx' command"
|
175
|
+
end
|
162
176
|
assert_listy(key)
|
163
177
|
return 0 unless list_at?(key)
|
164
178
|
rpush(key, value)
|
165
179
|
end
|
166
180
|
|
167
181
|
private
|
182
|
+
|
168
183
|
def list_at?(key)
|
169
184
|
data[key] && listy?(key)
|
170
185
|
end
|
171
186
|
|
172
187
|
def with_list_at(key, &blk)
|
173
|
-
with_thing_at(key, :assert_listy, proc {[]}, &blk)
|
188
|
+
with_thing_at(key, :assert_listy, proc { [] }, &blk)
|
174
189
|
end
|
175
190
|
|
176
191
|
def listy?(key)
|
177
|
-
data[key].nil? || data[key].
|
192
|
+
data[key].nil? || data[key].is_a?(Array)
|
178
193
|
end
|
179
194
|
|
180
195
|
def assert_listy(key)
|
181
196
|
unless listy?(key)
|
182
197
|
# Not the most helpful error, but it's what redis-rb barfs up
|
183
|
-
raise Redis::CommandError,
|
198
|
+
raise Redis::CommandError,
|
199
|
+
'WRONGTYPE Operation against a key holding the wrong kind of value'
|
184
200
|
end
|
185
201
|
end
|
186
202
|
|
187
203
|
def first_nonempty_list(keys)
|
188
|
-
keys.find{|k| llen(k) > 0}
|
204
|
+
keys.find { |k| llen(k) > 0 }
|
189
205
|
end
|
190
|
-
|
191
206
|
end
|
192
207
|
end
|
@@ -9,22 +9,22 @@ class MockRedis
|
|
9
9
|
|
10
10
|
@prototype_db = db.clone
|
11
11
|
|
12
|
-
@databases = Hash.new {|h,k| h[k] = @prototype_db.clone}
|
12
|
+
@databases = Hash.new { |h, k| h[k] = @prototype_db.clone }
|
13
13
|
@databases[@db_index] = db
|
14
14
|
end
|
15
15
|
|
16
|
-
def respond_to?(method, include_private=false)
|
16
|
+
def respond_to?(method, include_private = false)
|
17
17
|
super || current_db.respond_to?(method, include_private)
|
18
18
|
end
|
19
19
|
|
20
|
-
def method_missing(method, *args, &block)
|
20
|
+
ruby2_keywords def method_missing(method, *args, &block)
|
21
21
|
current_db.send(method, *args, &block)
|
22
22
|
end
|
23
23
|
|
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,12 +39,12 @@ 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)
|
46
46
|
when 'hash'
|
47
|
-
dest.hmset(key, *
|
47
|
+
dest.hmset(key, *src.hgetall(key).map { |k, v| [k, v] }.flatten)
|
48
48
|
when 'list'
|
49
49
|
while value = src.rpop(key)
|
50
50
|
dest.lpush(key, value)
|
@@ -56,7 +56,7 @@ class MockRedis
|
|
56
56
|
when 'string'
|
57
57
|
dest.set(key, src.get(key))
|
58
58
|
when 'zset'
|
59
|
-
src.zrange(key, 0, -1, :with_scores => true).each do |(m,s)|
|
59
|
+
src.zrange(key, 0, -1, :with_scores => true).each do |(m, s)|
|
60
60
|
dest.zadd(key, s, m)
|
61
61
|
end
|
62
62
|
else
|
@@ -75,6 +75,7 @@ class MockRedis
|
|
75
75
|
end
|
76
76
|
|
77
77
|
private
|
78
|
+
|
78
79
|
def current_db
|
79
80
|
@databases[@db_index]
|
80
81
|
end
|
@@ -2,44 +2,70 @@ class MockRedis
|
|
2
2
|
class PipelinedWrapper
|
3
3
|
include UndefRedisMethods
|
4
4
|
|
5
|
-
def respond_to?(method, include_private=false)
|
5
|
+
def respond_to?(method, include_private = false)
|
6
6
|
super || @db.respond_to?(method)
|
7
7
|
end
|
8
8
|
|
9
9
|
def initialize(db)
|
10
10
|
@db = db
|
11
|
-
@
|
12
|
-
@
|
11
|
+
@pipelined_futures = []
|
12
|
+
@nesting_level = 0
|
13
13
|
end
|
14
14
|
|
15
15
|
def initialize_copy(source)
|
16
16
|
super
|
17
17
|
@db = @db.clone
|
18
|
-
@
|
18
|
+
@pipelined_futures = @pipelined_futures.clone
|
19
19
|
end
|
20
20
|
|
21
|
-
def method_missing(method, *args, &block)
|
22
|
-
if
|
23
|
-
|
24
|
-
|
21
|
+
ruby2_keywords def method_missing(method, *args, &block)
|
22
|
+
if in_pipeline?
|
23
|
+
future = MockRedis::Future.new([method, *args], block)
|
24
|
+
@pipelined_futures << future
|
25
|
+
future
|
25
26
|
else
|
26
27
|
@db.send(method, *args, &block)
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
30
|
-
def pipelined(
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
def pipelined(_options = {})
|
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
|
+
|
43
|
+
responses = @pipelined_futures.flat_map do |future|
|
35
44
|
begin
|
36
|
-
|
37
|
-
|
45
|
+
result = if future.block
|
46
|
+
send(*future.command, &future.block)
|
47
|
+
else
|
48
|
+
send(*future.command)
|
49
|
+
end
|
50
|
+
future.store_result(result)
|
51
|
+
|
52
|
+
if future.block
|
53
|
+
result
|
54
|
+
else
|
55
|
+
[result]
|
56
|
+
end
|
57
|
+
rescue StandardError => e
|
38
58
|
e
|
39
59
|
end
|
40
60
|
end
|
41
|
-
@
|
61
|
+
@pipelined_futures = []
|
42
62
|
responses
|
43
63
|
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def in_pipeline?
|
68
|
+
@nesting_level > 0
|
69
|
+
end
|
44
70
|
end
|
45
71
|
end
|
@@ -7,26 +7,33 @@ class MockRedis
|
|
7
7
|
include UtilityMethods
|
8
8
|
|
9
9
|
def sadd(key, members)
|
10
|
+
members_class = members.class
|
10
11
|
members = [members].flatten.map(&:to_s)
|
12
|
+
assert_has_args(members, 'sadd')
|
11
13
|
|
12
14
|
with_set_at(key) do |s|
|
15
|
+
size_before = s.size
|
13
16
|
if members.size > 1
|
14
|
-
|
15
|
-
members.reverse.each {|m| s << m}
|
17
|
+
members.reverse_each { |m| s << m }
|
16
18
|
s.size - size_before
|
17
19
|
else
|
18
|
-
!!s.add?(members.first)
|
20
|
+
added = !!s.add?(members.first)
|
21
|
+
if members_class == Array
|
22
|
+
s.size - size_before
|
23
|
+
else
|
24
|
+
added
|
25
|
+
end
|
19
26
|
end
|
20
27
|
end
|
21
28
|
end
|
22
29
|
|
23
30
|
def scard(key)
|
24
|
-
with_set_at(key
|
31
|
+
with_set_at(key, &:length)
|
25
32
|
end
|
26
33
|
|
27
34
|
def sdiff(*keys)
|
28
35
|
assert_has_args(keys, 'sdiff')
|
29
|
-
with_sets_at(*keys) {|*sets| sets.reduce(&:-)}.to_a
|
36
|
+
with_sets_at(*keys) { |*sets| sets.reduce(&:-) }.to_a
|
30
37
|
end
|
31
38
|
|
32
39
|
def sdiffstore(destination, *keys)
|
@@ -54,11 +61,11 @@ class MockRedis
|
|
54
61
|
end
|
55
62
|
|
56
63
|
def sismember(key, member)
|
57
|
-
with_set_at(key) {|s| s.include?(member.to_s)}
|
64
|
+
with_set_at(key) { |s| s.include?(member.to_s) }
|
58
65
|
end
|
59
66
|
|
60
67
|
def smembers(key)
|
61
|
-
with_set_at(key, &:to_a).reverse
|
68
|
+
with_set_at(key, &:to_a).map(&:dup).reverse
|
62
69
|
end
|
63
70
|
|
64
71
|
def smove(src, dest, member)
|
@@ -74,23 +81,43 @@ class MockRedis
|
|
74
81
|
end
|
75
82
|
end
|
76
83
|
|
77
|
-
def spop(key)
|
84
|
+
def spop(key, count = nil)
|
78
85
|
with_set_at(key) do |set|
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
82
100
|
end
|
83
101
|
end
|
84
102
|
|
85
|
-
def srandmember(key)
|
103
|
+
def srandmember(key, count = nil)
|
86
104
|
members = with_set_at(key, &:to_a)
|
87
|
-
|
105
|
+
if count
|
106
|
+
if count > 0
|
107
|
+
members.sample(count)
|
108
|
+
else
|
109
|
+
Array.new(count.abs) { members[rand(members.length)] }
|
110
|
+
end
|
111
|
+
else
|
112
|
+
members[rand(members.length)]
|
113
|
+
end
|
88
114
|
end
|
89
115
|
|
90
116
|
def srem(key, members)
|
91
117
|
with_set_at(key) do |s|
|
92
118
|
if members.is_a?(Array)
|
93
119
|
orig_size = s.size
|
120
|
+
members = members.map(&:to_s)
|
94
121
|
s.delete_if { |m| members.include?(m) }
|
95
122
|
orig_size - s.size
|
96
123
|
else
|
@@ -99,9 +126,23 @@ class MockRedis
|
|
99
126
|
end
|
100
127
|
end
|
101
128
|
|
129
|
+
def sscan(key, cursor, opts = {})
|
130
|
+
common_scan(smembers(key), cursor, opts)
|
131
|
+
end
|
132
|
+
|
133
|
+
def sscan_each(key, opts = {}, &block)
|
134
|
+
return to_enum(:sscan_each, key, opts) unless block_given?
|
135
|
+
cursor = 0
|
136
|
+
loop do
|
137
|
+
cursor, keys = sscan(key, cursor, opts)
|
138
|
+
keys.each(&block)
|
139
|
+
break if cursor == '0'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
102
143
|
def sunion(*keys)
|
103
144
|
assert_has_args(keys, 'sunion')
|
104
|
-
with_sets_at(*keys) {|*sets| sets.reduce(&:+).to_a}
|
145
|
+
with_sets_at(*keys) { |*sets| sets.reduce(&:+).to_a }
|
105
146
|
end
|
106
147
|
|
107
148
|
def sunionstore(destination, *keys)
|
@@ -113,32 +154,34 @@ class MockRedis
|
|
113
154
|
end
|
114
155
|
|
115
156
|
private
|
157
|
+
|
116
158
|
def with_set_at(key, &blk)
|
117
|
-
with_thing_at(key, :assert_sety, proc {Set.new}, &blk)
|
159
|
+
with_thing_at(key, :assert_sety, proc { Set.new }, &blk)
|
118
160
|
end
|
119
161
|
|
120
162
|
def with_sets_at(*keys, &blk)
|
163
|
+
keys = keys.flatten
|
121
164
|
if keys.length == 1
|
122
165
|
with_set_at(keys.first, &blk)
|
123
166
|
else
|
124
167
|
with_set_at(keys.first) do |set|
|
125
168
|
with_sets_at(*(keys[1..-1])) do |*sets|
|
126
|
-
|
169
|
+
yield(*([set] + sets))
|
127
170
|
end
|
128
171
|
end
|
129
172
|
end
|
130
173
|
end
|
131
174
|
|
132
175
|
def sety?(key)
|
133
|
-
data[key].nil? || data[key].
|
176
|
+
data[key].nil? || data[key].is_a?(Set)
|
134
177
|
end
|
135
178
|
|
136
179
|
def assert_sety(key)
|
137
180
|
unless sety?(key)
|
138
181
|
# Not the most helpful error, but it's what redis-rb barfs up
|
139
|
-
raise Redis::CommandError,
|
182
|
+
raise Redis::CommandError,
|
183
|
+
'WRONGTYPE Operation against a key holding the wrong kind of value'
|
140
184
|
end
|
141
185
|
end
|
142
|
-
|
143
186
|
end
|
144
187
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'mock_redis/assertions'
|
2
|
+
|
3
|
+
class MockRedis
|
4
|
+
module SortMethod
|
5
|
+
include Assertions
|
6
|
+
|
7
|
+
def sort(key, options = {})
|
8
|
+
return [] if key.nil?
|
9
|
+
|
10
|
+
enumerable = data[key]
|
11
|
+
|
12
|
+
return [] if enumerable.nil?
|
13
|
+
|
14
|
+
by = options[:by]
|
15
|
+
limit = options[:limit] || []
|
16
|
+
store = options[:store]
|
17
|
+
get_patterns = Array(options[:get])
|
18
|
+
order = options[:order] || 'ASC'
|
19
|
+
direction = order.split.first
|
20
|
+
|
21
|
+
projected = project(enumerable, by, get_patterns)
|
22
|
+
sorted = sort_by(projected, direction)
|
23
|
+
sliced = slice(sorted, limit)
|
24
|
+
|
25
|
+
store ? rpush(store, sliced) : sliced
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
ASCENDING_SORT = proc { |a, b| a.first <=> b.first }
|
31
|
+
DESCENDING_SORT = proc { |a, b| b.first <=> a.first }
|
32
|
+
|
33
|
+
def project(enumerable, by, get_patterns)
|
34
|
+
enumerable.map do |*elements|
|
35
|
+
element = elements.last
|
36
|
+
weight = by ? lookup_from_pattern(by, element) : element
|
37
|
+
value = element
|
38
|
+
|
39
|
+
unless get_patterns.empty?
|
40
|
+
value = get_patterns.map do |pattern|
|
41
|
+
pattern == '#' ? element : lookup_from_pattern(pattern, element)
|
42
|
+
end
|
43
|
+
value = value.first if value.length == 1
|
44
|
+
end
|
45
|
+
|
46
|
+
[weight, value]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def sort_by(projected, direction)
|
51
|
+
sorter =
|
52
|
+
case direction.upcase
|
53
|
+
when 'DESC'
|
54
|
+
DESCENDING_SORT
|
55
|
+
when 'ASC', 'ALPHA'
|
56
|
+
ASCENDING_SORT
|
57
|
+
else
|
58
|
+
raise "Invalid direction '#{direction}'"
|
59
|
+
end
|
60
|
+
|
61
|
+
projected.sort(&sorter).map(&:last)
|
62
|
+
end
|
63
|
+
|
64
|
+
def slice(sorted, limit)
|
65
|
+
skip = limit.first || 0
|
66
|
+
take = limit.last || sorted.length
|
67
|
+
|
68
|
+
sorted[skip...(skip + take)] || sorted
|
69
|
+
end
|
70
|
+
|
71
|
+
def lookup_from_pattern(pattern, element)
|
72
|
+
key = pattern.sub('*', element)
|
73
|
+
|
74
|
+
if (hash_parts = key.split('->')).length > 1
|
75
|
+
hget hash_parts.first, hash_parts.last
|
76
|
+
else
|
77
|
+
get key
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class MockRedis
|
2
|
+
class Stream
|
3
|
+
class Id
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
attr_accessor :timestamp, :sequence, :exclusive
|
7
|
+
|
8
|
+
def initialize(id, min: nil, sequence: 0)
|
9
|
+
@exclusive = false
|
10
|
+
case id
|
11
|
+
when '*'
|
12
|
+
@timestamp = (Time.now.to_f * 1000).to_i
|
13
|
+
@sequence = 0
|
14
|
+
if self <= min
|
15
|
+
@timestamp = min.timestamp
|
16
|
+
@sequence = min.sequence + 1
|
17
|
+
end
|
18
|
+
when '-'
|
19
|
+
@timestamp = @sequence = 0
|
20
|
+
when '+'
|
21
|
+
@timestamp = @sequence = Float::INFINITY
|
22
|
+
else
|
23
|
+
if id.is_a? String
|
24
|
+
# See https://redis.io/topics/streams-intro
|
25
|
+
# Ids are a unix timestamp in milliseconds followed by an
|
26
|
+
# optional dash sequence number, e.g. -0. They can also optionally
|
27
|
+
# be prefixed with '(' to change the XRANGE to exclusive.
|
28
|
+
(_, @timestamp, @sequence) = id.match(/^\(?(\d+)-?(\d+)?$/).to_a
|
29
|
+
@exclusive = true if id[0] == '('
|
30
|
+
if @timestamp.nil?
|
31
|
+
raise Redis::CommandError,
|
32
|
+
'ERR Invalid stream ID specified as stream command argument'
|
33
|
+
end
|
34
|
+
@timestamp = @timestamp.to_i
|
35
|
+
else
|
36
|
+
@timestamp = id
|
37
|
+
end
|
38
|
+
@sequence = @sequence.nil? ? sequence : @sequence.to_i
|
39
|
+
if self <= min
|
40
|
+
raise Redis::CommandError,
|
41
|
+
'ERR The ID specified in XADD is equal or smaller than ' \
|
42
|
+
'the target stream top item'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
"#{@timestamp}-#{@sequence}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def <=>(other)
|
52
|
+
return 1 if other.nil?
|
53
|
+
return @sequence <=> other.sequence if @timestamp == other.timestamp
|
54
|
+
@timestamp <=> other.timestamp
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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
|
+
if @last_id.to_s == '0-0'
|
27
|
+
raise Redis::CommandError,
|
28
|
+
'ERR The ID specified in XADD must be greater than 0-0'
|
29
|
+
end
|
30
|
+
members.add [@last_id, Hash[values.map { |k, v| [k.to_s, v.to_s] }]]
|
31
|
+
@last_id.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def trim(count)
|
35
|
+
deleted = @members.size - count
|
36
|
+
if deleted > 0
|
37
|
+
@members = if count == 0
|
38
|
+
Set.new
|
39
|
+
else
|
40
|
+
@members.to_a[-count..-1].to_set
|
41
|
+
end
|
42
|
+
deleted
|
43
|
+
else
|
44
|
+
0
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def range(start, finish, reversed, *opts_in)
|
49
|
+
opts = options opts_in, ['count']
|
50
|
+
start_id = MockRedis::Stream::Id.new(start)
|
51
|
+
finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY)
|
52
|
+
items = if start_id.exclusive
|
53
|
+
members
|
54
|
+
.select { |m| (start_id < m[0]) && (finish_id >= m[0]) }
|
55
|
+
.map { |m| [m[0].to_s, m[1]] }
|
56
|
+
else
|
57
|
+
members
|
58
|
+
.select { |m| (start_id <= m[0]) && (finish_id >= m[0]) }
|
59
|
+
.map { |m| [m[0].to_s, m[1]] }
|
60
|
+
end
|
61
|
+
items.reverse! if reversed
|
62
|
+
return items.first(opts['count'].to_i) if opts.key?('count')
|
63
|
+
items
|
64
|
+
end
|
65
|
+
|
66
|
+
def read(id, *opts_in)
|
67
|
+
opts = options opts_in, %w[count block]
|
68
|
+
stream_id = MockRedis::Stream::Id.new(id)
|
69
|
+
items = members.select { |m| (stream_id < m[0]) }.map { |m| [m[0].to_s, m[1]] }
|
70
|
+
return items.first(opts['count'].to_i) if opts.key?('count')
|
71
|
+
items
|
72
|
+
end
|
73
|
+
|
74
|
+
def each
|
75
|
+
members.each { |m| yield m }
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def options(opts_in, permitted)
|
81
|
+
opts_out = {}
|
82
|
+
raise Redis::CommandError, 'ERR syntax error' unless (opts_in.length % 2).zero?
|
83
|
+
opts_in.each_slice(2).map { |pair| opts_out[pair[0].downcase] = pair[1] }
|
84
|
+
raise Redis::CommandError, 'ERR syntax error' unless (opts_out.keys - permitted).empty?
|
85
|
+
opts_out
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|