mock_redis 0.5.4 → 0.31.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 +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
|