From 9477e66749bc3f1baa416c009974f1bc592603af Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Sun, 24 Nov 2024 09:32:39 +0800 Subject: [PATCH] Support Redis-rb v5 --- lib/mock_redis.rb | 1 + lib/mock_redis/assertions.rb | 22 +++++- lib/mock_redis/database.rb | 39 ++++----- lib/mock_redis/error.rb | 27 +++++++ lib/mock_redis/geospatial_methods.rb | 16 ++-- lib/mock_redis/hash_methods.rb | 64 +++++++++++---- lib/mock_redis/list_methods.rb | 33 ++++---- lib/mock_redis/set_methods.rb | 30 +++++-- lib/mock_redis/sort_method.rb | 2 +- lib/mock_redis/stream_methods.rb | 9 ++- lib/mock_redis/string_methods.rb | 50 ++++++------ lib/mock_redis/transaction_wrapper.rb | 48 +++++------ lib/mock_redis/zset_methods.rb | 35 ++++---- mock_redis.gemspec | 2 +- spec/cloning_spec.rb | 27 ++++--- spec/commands/blmove_spec.rb | 2 +- spec/commands/blpop_spec.rb | 2 +- spec/commands/brpop_spec.rb | 2 +- spec/commands/brpoplpush_spec.rb | 11 ++- spec/commands/connection_spec.rb | 6 +- spec/commands/expire_spec.rb | 2 +- spec/commands/expireat_spec.rb | 6 +- spec/commands/geoadd_spec.rb | 8 +- spec/commands/geodist_spec.rb | 2 +- spec/commands/hdel_spec.rb | 2 +- spec/commands/hincrby_spec.rb | 2 +- spec/commands/hincrbyfloat_spec.rb | 2 +- spec/commands/hset_spec.rb | 12 +++ spec/commands/incrby_spec.rb | 2 +- spec/commands/incrbyfloat_spec.rb | 2 +- spec/commands/linsert_spec.rb | 2 + spec/commands/lmove_spec.rb | 12 +++ spec/commands/lrem_spec.rb | 2 +- spec/commands/mapped_hmget_spec.rb | 4 +- spec/commands/pexpire_spec.rb | 2 +- spec/commands/pexpireat_spec.rb | 6 +- spec/commands/pipelined_spec.rb | 6 +- spec/commands/psetex_spec.rb | 8 +- spec/commands/renamenx_spec.rb | 2 +- spec/commands/rpop_spec.rb | 5 +- spec/commands/sadd_spec.rb | 6 +- spec/commands/set_spec.rb | 8 +- spec/commands/setbit_spec.rb | 2 +- spec/commands/setex_spec.rb | 4 +- spec/commands/sinter_spec.rb | 6 +- spec/commands/srem_spec.rb | 6 +- spec/commands/sunion_spec.rb | 8 +- spec/commands/xadd_spec.rb | 7 +- spec/commands/xrange_spec.rb | 2 +- spec/commands/xrevrange_spec.rb | 2 +- spec/commands/zadd_spec.rb | 8 +- spec/commands/zrem_spec.rb | 4 +- spec/commands/zremrangebyscore_spec.rb | 4 +- spec/spec_helper.rb | 2 +- spec/support/redis_multiplexer.rb | 1 - .../does_not_cleanup_empty_strings.rb | 2 +- .../only_operates_on_hashes.rb | 2 +- .../shared_examples/only_operates_on_sets.rb | 2 +- .../only_operates_on_strings.rb | 4 +- .../shared_examples/only_operates_on_zsets.rb | 4 +- ...aises_on_invalid_expire_command_options.rb | 2 +- .../shared_examples/sorts_enumerables.rb | 2 +- spec/transactions_spec.rb | 79 ++++++++++++------- 63 files changed, 415 insertions(+), 269 deletions(-) create mode 100644 lib/mock_redis/error.rb diff --git a/lib/mock_redis.rb b/lib/mock_redis.rb index d747d9bf..5d2a44eb 100644 --- a/lib/mock_redis.rb +++ b/lib/mock_redis.rb @@ -1,6 +1,7 @@ require 'set' require 'mock_redis/assertions' +require 'mock_redis/error' require 'mock_redis/database' require 'mock_redis/expire_wrapper' require 'mock_redis/future' diff --git a/lib/mock_redis/assertions.rb b/lib/mock_redis/assertions.rb index dc3db22a..cf09bc63 100644 --- a/lib/mock_redis/assertions.rb +++ b/lib/mock_redis/assertions.rb @@ -1,11 +1,27 @@ +require 'mock_redis/error' + class MockRedis + DUMP_TYPES = RedisClient::RESP3::DUMP_TYPES + module Assertions private def assert_has_args(args, command) - unless args.any? - raise Redis::CommandError, - "ERR wrong number of arguments for '#{command}' command" + if args.empty? + raise Error.command_error( + "ERR wrong number of arguments for '#{command}' command", + self + ) + end + end + + def assert_type(*args) + args.each do |arg| + DUMP_TYPES.fetch(arg.class) do |unexpected_class| + unless DUMP_TYPES.keys.find { |t| t > unexpected_class } + raise TypeError, "Unsupported command argument type: #{unexpected_class}" + end + end end end end diff --git a/lib/mock_redis/database.rb b/lib/mock_redis/database.rb index ef6933bf..5d565a73 100644 --- a/lib/mock_redis/database.rb +++ b/lib/mock_redis/database.rb @@ -95,13 +95,13 @@ def echo(msg) end def expire(key, seconds, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists - assert_valid_integer(seconds) + seconds = Integer(seconds) pexpire(key, seconds.to_i * 1000, nx: nx, xx: xx, lt: lt, gt: gt) end def pexpire(key, ms, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists - assert_valid_integer(ms) + ms = Integer(ms) now, miliseconds = @base.now now_ms = (now * 1000) + miliseconds @@ -109,18 +109,19 @@ def pexpire(key, ms, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metri end def expireat(key, timestamp, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists - assert_valid_integer(timestamp) + timestamp = Integer(timestamp) pexpireat(key, timestamp.to_i * 1000, nx: nx, xx: xx, lt: lt, gt: gt) end def pexpireat(key, timestamp_ms, nx: nil, xx: nil, lt: nil, gt: nil) # rubocop:disable Metrics/ParameterLists - assert_valid_integer(timestamp_ms) + timestamp_ms = Integer(timestamp_ms) if nx && gt || gt && lt || lt && nx || nx && xx - raise Redis::CommandError, <<~TXT.chomp - ERR NX and XX, GT or LT options at the same time are not compatible - TXT + raise Error.command_error( + 'ERR NX and XX, GT or LT options at the same time are not compatible', + self + ) end return false unless exists?(key) @@ -157,7 +158,7 @@ def dump(key) def restore(key, ttl, value, replace: false) if !replace && exists?(key) - raise Redis::CommandError, 'BUSYKEY Target key name already exists.' + raise Error.command_error('BUSYKEY Target key name already exists.', self) end data[key] = Marshal.load(value) # rubocop:disable Security/MarshalLoad if ttl > 0 @@ -211,7 +212,7 @@ def randomkey def rename(key, newkey) unless data.include?(key) - raise Redis::CommandError, 'ERR no such key' + raise Error.command_error('ERR no such key', self) end if key != newkey @@ -227,7 +228,7 @@ def rename(key, newkey) def renamenx(key, newkey) unless data.include?(key) - raise Redis::CommandError, 'ERR no such key' + raise Error.command_error('ERR no such key', self) end if exists?(newkey) @@ -301,19 +302,13 @@ def eval(*args); end private - def assert_valid_integer(integer) - unless looks_like_integer?(integer.to_s) - raise Redis::CommandError, 'ERR value is not an integer or out of range' - end - integer - end - def assert_valid_timeout(timeout) - if !looks_like_integer?(timeout.to_s) - raise Redis::CommandError, 'ERR timeout is not an integer or out of range' - elsif timeout < 0 - raise Redis::CommandError, 'ERR timeout is negative' + timeout = Integer(timeout) + + if timeout < 0 + raise ArgumentError, 'time interval must not be negative' end + timeout end @@ -347,7 +342,7 @@ def has_expiration?(key) end def looks_like_integer?(str) - str =~ /^-?\d+$/ + !!Integer(str) rescue false end def looks_like_float?(str) diff --git a/lib/mock_redis/error.rb b/lib/mock_redis/error.rb new file mode 100644 index 00000000..6b4c7d5d --- /dev/null +++ b/lib/mock_redis/error.rb @@ -0,0 +1,27 @@ +class MockRedis + module Error + module_function + + def build(error_class, message, database) + connection = database.connection + url = "redis://#{connection[:host]}:#{connection[:port]}" + error_class.new("#{message} (#{url})") + end + + def wrong_type_error(database) + build( + Redis::WrongTypeError, + 'WRONGTYPE Operation against a key holding the wrong kind of value', + database + ) + end + + def syntax_error(database) + command_error('ERR syntax error', database) + end + + def command_error(message, database) + build(Redis::CommandError, message, database) + end + end +end diff --git a/lib/mock_redis/geospatial_methods.rb b/lib/mock_redis/geospatial_methods.rb index 8923f3d0..5bcfca12 100644 --- a/lib/mock_redis/geospatial_methods.rb +++ b/lib/mock_redis/geospatial_methods.rb @@ -1,3 +1,5 @@ +require 'mock_redis/error' + class MockRedis module GeospatialMethods LNG_RANGE = (-180..180) @@ -81,8 +83,7 @@ def parse_points(args) points = args.each_slice(3).to_a if points.last.size != 3 - raise Redis::CommandError, - "ERR wrong number of arguments for 'geoadd' command" + raise Error.command_error("ERR wrong number of arguments for 'geoadd' command", self) end points.map do |point| @@ -97,13 +98,15 @@ def parse_point(point) unless LNG_RANGE.include?(lng) && LAT_RANGE.include?(lat) lng = format('%.6f', long: lng) lat = format('%.6f', lat: lat) - raise Redis::CommandError, - "ERR invalid longitude,latitude pair #{lng},#{lat}" + raise Error.command_error( + "ERR invalid longitude,latitude pair #{lng},#{lat}", + self + ) end { key: point[2], lng: lng, lat: lat } rescue ArgumentError - raise Redis::CommandError, 'ERR value is not a valid float' + raise Error.command_error('ERR value is not a valid float', self) end # Returns ZSET score for passed coordinates @@ -212,8 +215,7 @@ def parse_unit(unit) unit = unit.to_sym return UNITS[unit] if UNITS[unit] - raise Redis::CommandError, - 'ERR unsupported unit provided. please use m, km, ft, mi' + raise Error.command_error('ERR unsupported unit provided. please use m, km, ft, mi', self) end def geohash_distance(lng1d, lat1d, lng2d, lat2d) diff --git a/lib/mock_redis/hash_methods.rb b/lib/mock_redis/hash_methods.rb index 530cd2d6..749b69cf 100644 --- a/lib/mock_redis/hash_methods.rb +++ b/lib/mock_redis/hash_methods.rb @@ -1,5 +1,6 @@ require 'mock_redis/assertions' require 'mock_redis/utility_methods' +require 'mock_redis/error' class MockRedis module HashMethods @@ -7,12 +8,16 @@ module HashMethods include UtilityMethods def hdel(key, *fields) + assert_type(key) + with_hash_at(key) do |hash| orig_size = hash.size fields = Array(fields).flatten.map(&:to_s) + assert_type(*fields) + if fields.empty? - raise Redis::CommandError, "ERR wrong number of arguments for 'hdel' command" + raise Error.command_error("ERR wrong number of arguments for 'hdel' command", self) end hash.delete_if { |k, _v| fields.include?(k) } @@ -21,25 +26,31 @@ def hdel(key, *fields) end def hexists(key, field) + assert_type(key, field) + with_hash_at(key) { |h| h.key?(field.to_s) } end def hget(key, field) + assert_type(key, field) + with_hash_at(key) { |h| h[field.to_s] } end def hgetall(key) + assert_type(key) + with_hash_at(key) { |h| h } end def hincrby(key, field, increment) + assert_type(key, field) + with_hash_at(key) do |hash| field = field.to_s + increment = Integer(increment) unless can_incr?(data[key][field]) - raise Redis::CommandError, 'ERR hash value is not an integer' - end - unless looks_like_integer?(increment.to_s) - raise Redis::CommandError, 'ERR value is not an integer or out of range' + raise Error.command_error('ERR hash value is not an integer', self) end new_value = (hash[field] || '0').to_i + increment.to_i @@ -49,14 +60,13 @@ def hincrby(key, field, increment) end def hincrbyfloat(key, field, increment) + assert_type(key, field) + with_hash_at(key) do |hash| field = field.to_s + increment = Float(increment) unless can_incr_float?(data[key][field]) - raise Redis::CommandError, 'ERR hash value is not a float' - end - - unless looks_like_float?(increment.to_s) - raise Redis::CommandError, 'ERR value is not a valid float' + raise Error.command_error('ERR hash value is not a float', self) end new_value = (hash[field] || '0').to_f + increment.to_f @@ -67,21 +77,28 @@ def hincrbyfloat(key, field, increment) end def hkeys(key) + assert_type(key) + with_hash_at(key, &:keys) end def hlen(key) + assert_type(key) + hkeys(key).length end def hmget(key, *fields) fields.flatten! + assert_type(key, *fields) assert_has_args(fields, 'hmget') fields.map { |f| hget(key, f) } end def mapped_hmget(key, *fields) + fields.flatten! + reply = hmget(key, *fields) if reply.is_a?(Array) Hash[fields.zip(reply)] @@ -98,10 +115,15 @@ def hmset(key, *kvpairs) end kvpairs.flatten! + + assert_type(key, *kvpairs) assert_has_args(kvpairs, 'hmset') if kvpairs.length.odd? - raise Redis::CommandError, err_msg || 'ERR wrong number of arguments for \'hmset\' command' + raise Error.command_error( + err_msg || "ERR wrong number of arguments for 'hmset' command", + self + ) end kvpairs.each_slice(2) do |(k, v)| @@ -111,21 +133,27 @@ def hmset(key, *kvpairs) end def mapped_hmset(key, hash) - kvpairs = hash.to_a.flatten + kvpairs = hash.flatten + + assert_type(key, *kvpairs) assert_has_args(kvpairs, 'hmset') if kvpairs.length.odd? - raise Redis::CommandError, "ERR wrong number of arguments for 'hmset' command" + raise Error.command_error("ERR wrong number of arguments for 'hmset' command", self) end hmset(key, *kvpairs) end def hscan(key, cursor, opts = {}) + assert_type(key, cursor) + opts = opts.merge(key: lambda { |x| x[0] }) common_scan(hgetall(key).to_a, cursor, opts) end def hscan_each(key, opts = {}, &block) + assert_type(key) + return to_enum(:hscan_each, key, opts) unless block_given? cursor = 0 loop do @@ -138,11 +166,15 @@ def hscan_each(key, opts = {}, &block) def hset(key, *args) added = 0 args.flatten!(1) + assert_type(key) + with_hash_at(key) do |hash| if args.length == 1 && args[0].is_a?(Hash) args = args[0].to_a.flatten end + assert_type(*args) + args.each_slice(2) do |field, value| added += 1 unless hash.key?(field.to_s) hash[field.to_s] = value.to_s @@ -152,6 +184,7 @@ def hset(key, *args) end def hsetnx(key, field, value) + assert_type(key, field, value) if hget(key, field) false else @@ -161,6 +194,8 @@ def hsetnx(key, field, value) end def hvals(key) + assert_type(key) + with_hash_at(key, &:values) end @@ -176,8 +211,7 @@ def hashy?(key) def assert_hashy(key) unless hashy?(key) - raise Redis::CommandError, - 'WRONGTYPE Operation against a key holding the wrong kind of value' + raise Error.wrong_type_error(self) end end end diff --git a/lib/mock_redis/list_methods.rb b/lib/mock_redis/list_methods.rb index 36342a73..69c338b5 100644 --- a/lib/mock_redis/list_methods.rb +++ b/lib/mock_redis/list_methods.rb @@ -46,9 +46,7 @@ def brpop(*args) end end - def brpoplpush(source, destination, options = {}) - options = { :timeout => options } if options.is_a?(Integer) - timeout = options.is_a?(Hash) && options[:timeout] || 0 + def brpoplpush(source, destination, timeout: 0) assert_valid_timeout(timeout) if llen(source) > 0 @@ -66,7 +64,7 @@ def lindex(key, index) def linsert(key, position, pivot, value) unless %w[before after].include?(position.to_s) - raise Redis::CommandError, 'ERR syntax error' + raise Error.command_error('ERR syntax error', self) end assert_listy(key) @@ -99,9 +97,8 @@ def lmove(source, destination, wherefrom, whereto) wherefrom = wherefrom.to_s.downcase whereto = whereto.to_s.downcase - unless %w[left right].include?(wherefrom) && %w[left right].include?(whereto) - raise Redis::CommandError, 'ERR syntax error' - end + assert_where_field(wherefrom, 'where_source') + assert_where_field(whereto, 'where_destination') value = wherefrom == 'left' ? lpop(source) : rpop(source) (whereto == 'left' ? lpush(destination, value) : rpush(destination, value)) unless value.nil? @@ -127,7 +124,7 @@ def lpush(key, values) def lpushx(key, value) value = [value] unless value.is_a?(Array) if value.empty? - raise Redis::CommandError, "ERR wrong number of arguments for 'lpushx' command" + raise Error.command_error("ERR wrong number of arguments for 'lpushx' command", self) end assert_listy(key) return 0 unless list_at?(key) @@ -140,10 +137,7 @@ def lrange(key, start, stop) end def lrem(key, count, value) - unless looks_like_integer?(count.to_s) - raise Redis::CommandError, 'ERR value is not an integer or out of range' - end - count = count.to_i + count = Integer(count) value = value.to_s with_list_at(key) do |list| @@ -167,12 +161,12 @@ def lset(key, index, value) assert_listy(key) unless list_at?(key) - raise Redis::CommandError, 'ERR no such key' + raise Error.command_error('ERR no such key', self) end index = index.to_i unless (0...llen(key)).cover?(index) - raise Redis::CommandError, 'ERR index out of range' + raise Error.command_error('ERR index out of range', self) end data[key][index] = value.to_s @@ -211,7 +205,7 @@ def rpush(key, values) def rpushx(key, value) value = [value] unless value.is_a?(Array) if value.empty? - raise Redis::CommandError, "ERR wrong number of arguments for 'rpushx' command" + raise Error.command_error("ERR wrong number of arguments for 'rpushx' command", self) end assert_listy(key) return 0 unless list_at?(key) @@ -235,8 +229,13 @@ def listy?(key) def assert_listy(key) unless listy?(key) # Not the most helpful error, but it's what redis-rb barfs up - raise Redis::CommandError, - 'WRONGTYPE Operation against a key holding the wrong kind of value' + raise Error.wrong_type_error(self) + end + end + + def assert_where_field(where, argument_name) + unless %w[left right].include?(where) + raise ArgumentError, "#{argument_name} must be 'LEFT' or 'RIGHT'" end end diff --git a/lib/mock_redis/set_methods.rb b/lib/mock_redis/set_methods.rb index 138a3dbe..c9d936b5 100644 --- a/lib/mock_redis/set_methods.rb +++ b/lib/mock_redis/set_methods.rb @@ -6,9 +6,10 @@ module SetMethods include Assertions include UtilityMethods - def sadd(key, members) + def sadd(key, *members) members_class = members.class - members = Array(members).map(&:to_s) + members = Array(members).flatten.map(&:to_s) + assert_type(key, *members) assert_has_args(members, 'sadd') with_set_at(key) do |s| @@ -21,7 +22,7 @@ def sadd(key, members) if members_class == Array s.size - size_before else - added + added ? 1 : 0 end end end @@ -33,6 +34,7 @@ def sadd?(key, members) end def scard(key) + assert_type(key) with_set_at(key, &:length) end @@ -42,6 +44,7 @@ def sdiff(*keys) end def sdiffstore(destination, *keys) + assert_type(destination, *keys) assert_has_args(keys, 'sdiffstore') with_set_at(destination) do |set| set.replace(sdiff(*keys)) @@ -58,6 +61,7 @@ def sinter(*keys) end def sinterstore(destination, *keys) + assert_type(destination, *keys) assert_has_args(keys, 'sinterstore') with_set_at(destination) do |set| set.replace(sinter(*keys)) @@ -66,16 +70,21 @@ def sinterstore(destination, *keys) end def sismember(key, member) + assert_type(key, member) with_set_at(key) { |s| s.include?(member.to_s) } end def smismember(key, *members) + members.flatten! + + assert_type(key, *members) with_set_at(key) do |set| - members.flatten.map { |m| set.include?(m.to_s) } + members.map { |m| set.include?(m.to_s) } end end def smembers(key) + assert_type(key) with_set_at(key, &:to_a).map(&:dup).reverse end @@ -93,6 +102,7 @@ def smove(src, dest, member) end def spop(key, count = nil) + assert_type(key) with_set_at(key) do |set| if count.nil? member = set.first @@ -112,6 +122,7 @@ def spop(key, count = nil) end def srandmember(key, count = nil) + assert_type(key) members = with_set_at(key, &:to_a) if count if count > 0 @@ -124,7 +135,10 @@ def srandmember(key, count = nil) end end - def srem(key, members) + def srem(key, *members) + members = members.flatten.uniq + assert_type(key, *members) + with_set_at(key) do |s| if members.is_a?(Array) orig_size = s.size @@ -177,6 +191,9 @@ def with_set_at(key, &blk) def with_sets_at(*keys, &blk) keys = keys.flatten + + assert_type(*keys) + if keys.length == 1 with_set_at(keys.first, &blk) else @@ -195,8 +212,7 @@ def sety?(key) def assert_sety(key) unless sety?(key) # Not the most helpful error, but it's what redis-rb barfs up - raise Redis::CommandError, - 'WRONGTYPE Operation against a key holding the wrong kind of value' + raise Error.wrong_type_error(self) end end end diff --git a/lib/mock_redis/sort_method.rb b/lib/mock_redis/sort_method.rb index 54ca490e..53a360af 100644 --- a/lib/mock_redis/sort_method.rb +++ b/lib/mock_redis/sort_method.rb @@ -5,7 +5,7 @@ module SortMethod include Assertions def sort(key, options = {}) - return [] if key.nil? + assert_type(key) enumerable = data[key] diff --git a/lib/mock_redis/stream_methods.rb b/lib/mock_redis/stream_methods.rb index e7d57726..a0d91a36 100644 --- a/lib/mock_redis/stream_methods.rb +++ b/lib/mock_redis/stream_methods.rb @@ -35,6 +35,8 @@ def xadd(key, entry, opts = {}) stream.add id, entry stream.trim opts[:maxlen] if opts[:maxlen] return stream.last_id + rescue Redis::CommandError => e + raise Error.command_error(e.message, self) end end @@ -55,6 +57,8 @@ def xrange(key, first = '-', last = '+', count: nil) args += ['COUNT', count] if count with_stream_at(key) do |stream| return stream.range(*args) + rescue Redis::CommandError => e + raise Error.command_error(e.message, self) end end @@ -63,6 +67,8 @@ def xrevrange(key, last = '+', first = '-', count: nil) args += ['COUNT', count] if count with_stream_at(key) do |stream| return stream.range(*args) + rescue Redis::CommandError => e + raise Error.command_error(e.message, self) end end @@ -94,8 +100,7 @@ def streamy?(key) def assert_streamy(key) unless streamy?(key) - raise Redis::CommandError, - 'WRONGTYPE Operation against a key holding the wrong kind of value' + raise Error.wrong_type_error(self) end end end diff --git a/lib/mock_redis/string_methods.rb b/lib/mock_redis/string_methods.rb index bb43f7f4..cab01322 100644 --- a/lib/mock_redis/string_methods.rb +++ b/lib/mock_redis/string_methods.rb @@ -14,7 +14,7 @@ def append(key, value) def bitfield(*args) if args.length < 4 - raise Redis::CommandError, 'ERR wrong number of arguments for BITFIELD' + raise Error.command_error('ERR wrong number of arguments for BITFIELD', self) end key = args.shift @@ -28,7 +28,7 @@ def bitfield(*args) new_overflow_method = args.shift.to_s.downcase unless %w[wrap sat fail].include? new_overflow_method - raise Redis::CommandError, 'ERR Invalid OVERFLOW type specified' + raise Error.command_error('ERR Invalid OVERFLOW type specified', self) end overflow_method = new_overflow_method @@ -41,9 +41,11 @@ def bitfield(*args) type_size = type[1..].to_i if (type_size > 64 && is_signed) || (type_size >= 64 && !is_signed) - raise Redis::CommandError, + raise Error.command_error( 'ERR Invalid bitfield type. Use something like i16 u8. ' \ - 'Note that u64 is not supported but i64 is.' + 'Note that u64 is not supported but i64 is.', + self + ) end if offset.to_s[0] == '#' @@ -130,12 +132,10 @@ def incr(key) def incrby(key, n) assert_stringy(key) - unless can_incr?(data[key]) - raise Redis::CommandError, 'ERR value is not an integer or out of range' - end + n = Integer(n) - unless looks_like_integer?(n.to_s) - raise Redis::CommandError, 'ERR value is not an integer or out of range' + unless can_incr?(data[key]) + raise Error.command_error('ERR value is not an integer or out of range', self) end new_value = data[key].to_i + n.to_i @@ -146,12 +146,9 @@ def incrby(key, n) def incrbyfloat(key, n) assert_stringy(key) + n = Float(n) unless can_incr_float?(data[key]) - raise Redis::CommandError, 'ERR value is not a valid float' - end - - unless looks_like_float?(n.to_s) - raise Redis::CommandError, 'ERR value is not a valid float' + raise Error.command_error('ERR value is not a valid float', self) end new_value = data[key].to_f + n.to_f @@ -181,7 +178,7 @@ def mset(*kvpairs) kvpairs = kvpairs.first if kvpairs.size == 1 && kvpairs.first.is_a?(Enumerable) if kvpairs.length.odd? - raise Redis::CommandError, 'ERR wrong number of arguments for MSET' + raise Error.command_error('ERR wrong number of arguments for MSET', self) end kvpairs.each_slice(2) do |(k, v)| @@ -237,28 +234,28 @@ def set(key, value, _hash = nil, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil remove_expiration(key) unless keepttl if ex if ex == 0 - raise Redis::CommandError, 'ERR invalid expire time in set' + raise Error.command_error('ERR invalid expire time in set', self) end expire(key, ex) end if px if px == 0 - raise Redis::CommandError, 'ERR invalid expire time in set' + raise Error.command_error('ERR invalid expire time in set', self) end pexpire(key, px) end if exat if exat == 0 - raise Redis::CommandError, 'ERR invalid expire time in set' + raise Error.command_error('ERR invalid expire time in set', self) end expireat(key, exat) end if pxat if pxat == 0 - raise Redis::CommandError, 'ERR invalid expire time in set' + raise Error.command_error('ERR invalid expire time in set', self) end pexpireat(key, pxat) end @@ -347,7 +344,7 @@ def bitcount(key, start = 0, stop = -1) def setex(key, seconds, value) if seconds <= 0 - raise Redis::CommandError, 'ERR invalid expire time in setex' + raise Error.command_error('ERR invalid expire time in setex', self) else set(key, value) expire(key, seconds) @@ -357,7 +354,7 @@ def setex(key, seconds, value) def psetex(key, milliseconds, value) if milliseconds <= 0 - raise Redis::CommandError, 'ERR invalid expire time in psetex' + raise Error.command_error('ERR invalid expire time in psetex', self) else set(key, value) pexpire(key, milliseconds) @@ -377,7 +374,7 @@ def setnx(key, value) def setrange(key, offset, value) assert_stringy(key) value = value.to_s - old_value = (data[key] || '') + old_value = data[key] || '' prefix = zero_pad(old_value[0...offset], offset) data[key] = prefix + value + (old_value[(offset + value.length)..] || '') @@ -395,10 +392,13 @@ def stringy?(key) data[key].nil? || data[key].is_a?(String) end - def assert_stringy(key, - message = 'WRONGTYPE Operation against a key holding the wrong kind of value') + def assert_stringy(key, message = nil) unless stringy?(key) - raise Redis::CommandError, message + if message + raise Error.command_error(message, self) + else + raise Error.wrong_type_error(self) + end end end diff --git a/lib/mock_redis/transaction_wrapper.rb b/lib/mock_redis/transaction_wrapper.rb index 34e4047d..40dde7c6 100644 --- a/lib/mock_redis/transaction_wrapper.rb +++ b/lib/mock_redis/transaction_wrapper.rb @@ -1,4 +1,5 @@ require 'mock_redis/undef_redis_methods' +require 'mock_redis/error' class MockRedis class TransactionWrapper @@ -12,18 +13,12 @@ def initialize(db) @db = db @transaction_futures = [] @multi_stack = [] - @multi_block_given = false end ruby2_keywords def method_missing(method, *args, &block) if in_multi? - future = MockRedis::Future.new([method, *args], block) - @transaction_futures << future - - if @multi_block_given - future - else - 'QUEUED' + MockRedis::Future.new([method, *args], block).tap do |future| + @transaction_futures << future end else @db.expire_keys @@ -40,7 +35,7 @@ def initialize_copy(source) def discard unless in_multi? - raise Redis::CommandError, 'ERR DISCARD without MULTI' + raise Error.command_error('ERR DISCARD without MULTI', self) end pop_multi @@ -50,22 +45,23 @@ def discard def exec unless in_multi? - raise Redis::CommandError, 'ERR EXEC without MULTI' + raise Error.command_error('ERR EXEC without MULTI', self) end + pop_multi return if in_multi? - @multi_block_given = false responses = @transaction_futures.map do |future| result = send(*future.command) future.store_result(result) future.value - rescue StandardError => e - e end - @transaction_futures = [] responses + ensure + # At this point, multi is done, so we can't call discard anymore. + # Therefore, we need to clear the transaction futures manually. + @transaction_futures = [] end def in_multi? @@ -81,20 +77,16 @@ def pop_multi end def multi - if block_given? - push_multi - @multi_block_given = true - begin - yield(self) - exec - rescue StandardError => e - discard - raise e - end - else - raise Redis::CommandError, 'ERR MULTI calls can not be nested' if in_multi? - push_multi - 'OK' + raise Redis::BaseError, "Can't nest multi transaction" if in_multi? + + push_multi + + begin + yield(self) + exec + rescue StandardError => e + discard if in_multi? + raise e end end diff --git a/lib/mock_redis/zset_methods.rb b/lib/mock_redis/zset_methods.rb index 4b8e7afd..cd8c3141 100644 --- a/lib/mock_redis/zset_methods.rb +++ b/lib/mock_redis/zset_methods.rb @@ -12,7 +12,10 @@ def zadd(key, *args) zadd_options = args.pop if args.last.is_a?(Hash) if zadd_options&.include?(:nx) && zadd_options&.include?(:xx) - raise Redis::CommandError, 'ERR XX and NX options at the same time are not compatible' + raise Error.command_error( + 'ERR XX and NX options at the same time are not compatible', + self + ) end if args.size == 1 && args[0].is_a?(Array) @@ -21,7 +24,7 @@ def zadd(key, *args) score, member = args zadd_one_member(key, score, member, zadd_options) else - raise Redis::CommandError, 'ERR wrong number of arguments' + raise ArgumentError, 'wrong number of arguments' end end @@ -57,12 +60,13 @@ def zadd_one_member(key, score, member, zadd_options = {}) private :zadd_one_member def zadd_multiple_members(key, args, zadd_options = {}) - assert_has_args(args, 'zadd') - args = args.each_slice(2).to_a unless args.first.is_a?(Array) with_zset_at(key) do |zset| if zadd_options[:incr] - raise Redis::CommandError, 'ERR INCR option supports a single increment-element pair' + raise Error.command_error( + 'ERR INCR option supports a single increment-element pair', + self + ) elsif zadd_options[:xx] args.each { |score, member| zset.include?(member) && zset.add(score, member.to_s) } 0 @@ -143,7 +147,7 @@ def zrem(key, *args) else args = args.first if args.empty? - raise Redis::CommandError, "ERR wrong number of arguments for 'zrem' command" + retval = 0 else retval = args.map { |member| !!zscore(key, member.to_s) }.count(true) with_zset_at(key) do |z| @@ -257,7 +261,7 @@ def apply_limit(collection, limit) offset, count = limit collection.drop(offset).take(count) else - raise Redis::CommandError, 'ERR syntax error' + raise Error.syntax_error(self) end else collection @@ -277,7 +281,7 @@ def to_response(score_member_pairs, options) def combine_weighted_zsets(keys, options, how) weights = options.fetch(:weights, keys.map { 1 }) if weights.length != keys.length - raise Redis::CommandError, 'ERR syntax error' + raise Error.syntax_error(self) end aggregator = case options.fetch(:aggregate, :sum).to_s.downcase.to_sym @@ -288,7 +292,7 @@ def combine_weighted_zsets(keys, options, how) when :max proc { |a, b| [a, b].compact.max } else - raise Redis::CommandError, 'ERR syntax error' + raise Error.syntax_error(self) end with_zsets_at(*keys, coercible: true) do |*zsets| @@ -342,15 +346,13 @@ def coercible_zsety?(key) def assert_zsety(key) unless zsety?(key) - raise Redis::CommandError, - 'WRONGTYPE Operation against a key holding the wrong kind of value' + raise Error.wrong_type_error(self) end end def assert_coercible_zsety(key) unless coercible_zsety?(key) - raise Redis::CommandError, - 'WRONGTYPE Operation against a key holding the wrong kind of value' + raise Error.wrong_type_error(self) end end @@ -363,9 +365,10 @@ def assert_scorey(value, message = 'ERR value is not a valid float') return if value.to_s =~ /\(?(-|\+)inf/ value = $1 if value.to_s =~ /\((.*)/ - unless looks_like_float?(value) - raise Redis::CommandError, message - end + + assert_type(value) + + raise Error.command_error(message, self) unless looks_like_float?(value) end def assert_range_args(min, max) diff --git a/mock_redis.gemspec b/mock_redis.gemspec index 0629d33a..7a9dd988 100644 --- a/mock_redis.gemspec +++ b/mock_redis.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 3.0' s.add_development_dependency 'rake', '~> 13' - s.add_development_dependency 'redis', '~> 4.8.0' + s.add_development_dependency 'redis', '~> 5' s.add_development_dependency 'rspec', '~> 3.0' s.add_development_dependency 'rspec-its', '~> 1.0' s.add_development_dependency 'timecop', '~> 0.9.1' diff --git a/spec/cloning_spec.rb b/spec/cloning_spec.rb index f4def5af..7d42dfd2 100644 --- a/spec/cloning_spec.rb +++ b/spec/cloning_spec.rb @@ -70,26 +70,27 @@ end context 'transactional info' do - before do - @mock.multi - @mock.incr('foo') - @mock.incrby('foo', 2) - @mock.incrby('foo', 4) - - @clone = @mock.clone + def test_clone + @mock.multi do |r| + r.incr('foo') + r.incrby('foo', 2) + r.incrby('foo', 4) + + yield r.clone + end end it 'makes sure the clone is in a transaction' do - expect do - @clone.exec - end.not_to raise_error + expect { test_clone(&:exec) }.not_to raise_error end it 'deep-copies the queued commands' do - @clone.incrby('foo', 8) - expect(@clone.exec).to eq([1, 3, 7, 15]) + result = test_clone do |clone| + clone.incrby('foo', 8) + expect(clone.exec).to eq([1, 3, 7, 15]) + end - expect(@mock.exec).to eq([1, 3, 7]) + expect(result).to eq([1, 3, 7]) end end end diff --git a/spec/commands/blmove_spec.rb b/spec/commands/blmove_spec.rb index 20f360ae..241b9e14 100644 --- a/spec/commands/blmove_spec.rb +++ b/spec/commands/blmove_spec.rb @@ -26,7 +26,7 @@ it 'raises an error on negative timeout' do expect do @redises.blmove(@list1, @list2, 'left', 'right', :timeout => -1) - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end let(:default_error) { RedisMultiplexer::MismatchedResponse } diff --git a/spec/commands/blpop_spec.rb b/spec/commands/blpop_spec.rb index 0a2dd333..a27fbc11 100644 --- a/spec/commands/blpop_spec.rb +++ b/spec/commands/blpop_spec.rb @@ -31,7 +31,7 @@ it 'raises an error on negative timeout' do expect do @redises.blpop(@list1, @list2, :timeout => -1) - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it_should_behave_like 'a list-only command' diff --git a/spec/commands/brpop_spec.rb b/spec/commands/brpop_spec.rb index 92c8f908..74590c94 100644 --- a/spec/commands/brpop_spec.rb +++ b/spec/commands/brpop_spec.rb @@ -37,7 +37,7 @@ it 'raises an error on negative timeout' do expect do @redises.brpop(@list1, @list2, :timeout => -1) - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it_should_behave_like 'a list-only command' diff --git a/spec/commands/brpoplpush_spec.rb b/spec/commands/brpoplpush_spec.rb index d03248fc..d77345e5 100644 --- a/spec/commands/brpoplpush_spec.rb +++ b/spec/commands/brpoplpush_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' RSpec.describe '#brpoplpush(source, destination, timeout)' do + let(:default_error) { ArgumentError } + before do @list1 = 'mock-redis-test:brpoplpush1' @list2 = 'mock-redis-test:brpoplpush2' @@ -25,7 +27,7 @@ it 'raises an error on negative timeout' do expect do @redises.brpoplpush(@list1, @list2, :timeout => -1) - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it_should_behave_like 'a list-only command' @@ -36,9 +38,10 @@ to be_nil end - it 'ignores positive legacy timeouts and returns nil' do - expect(@redises.mock.brpoplpush('mock-redis-test:not-here', @list1, 1)). - to be_nil + it 'raises error if there is extra argument' do + expect do + @redises.mock.brpoplpush('mock-redis-test:not-here', @list1, 1) + end.to raise_error(ArgumentError) end it 'raises WouldBlock on zero timeout (no blocking in the mock)' do diff --git a/spec/commands/connection_spec.rb b/spec/commands/connection_spec.rb index 352d8c9b..5b3f2cea 100644 --- a/spec/commands/connection_spec.rb +++ b/spec/commands/connection_spec.rb @@ -6,11 +6,11 @@ it 'returns the correct values' do expect(redis.connection).to eq( { - :host => '127.0.0.1', + :host => 'localhost', :port => 6379, :db => 0, - :id => 'redis://127.0.0.1:6379/0', - :location => '127.0.0.1:6379' + :id => 'redis://localhost:6379/0', + :location => 'localhost:6379' } ) end diff --git a/spec/commands/expire_spec.rb b/spec/commands/expire_spec.rb index 312f8e50..4777141e 100644 --- a/spec/commands/expire_spec.rb +++ b/spec/commands/expire_spec.rb @@ -22,7 +22,7 @@ it 'raises an error if seconds is bogus' do expect do @redises.expire(@key, 'a couple minutes or so') - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it 'stringifies key' do diff --git a/spec/commands/expireat_spec.rb b/spec/commands/expireat_spec.rb index 3e3e8fe0..49d00c2a 100644 --- a/spec/commands/expireat_spec.rb +++ b/spec/commands/expireat_spec.rb @@ -19,10 +19,8 @@ expect(@redises.get(@key)).to be_nil end - it "raises an error if you don't give it a Unix timestamp" do - expect do - @redises.expireat(@key, Time.now) # oops, forgot .to_i - end.to raise_error(Redis::CommandError) + it 'returns true when time is a Time object' do + expect(@redises.expireat(@key, Time.now)).to eq true end it 'works with options', redis: '7.0' do diff --git a/spec/commands/geoadd_spec.rb b/spec/commands/geoadd_spec.rb index 10cd7542..1f8878bf 100644 --- a/spec/commands/geoadd_spec.rb +++ b/spec/commands/geoadd_spec.rb @@ -20,9 +20,9 @@ end end - context 'with invalud points' do + context 'with invalid points' do context 'when number of arguments wrong' do - let(:message) { "ERR wrong number of arguments for 'geoadd' command" } + let(:message) { /ERR wrong number of arguments for 'geoadd' command/ } it 'raises Redis::CommandError' do expect { @redises.geoadd(key, 1, 1) } @@ -34,7 +34,7 @@ let(:coords) { [181, 86] } let(:message) do formatted_coords = coords.map { |c| format('%.6f', coords: c) } - "ERR invalid longitude,latitude pair #{formatted_coords.join(',')}" + /ERR invalid longitude,latitude pair #{formatted_coords.join(',')}/ end after { @redises.zrem(key, 'SF') } @@ -47,7 +47,7 @@ context 'when coordinates are not valid floats' do let(:coords) { ['x', 35] } - let(:message) { 'ERR value is not a valid float' } + let(:message) { /ERR value is not a valid float/ } it 'raises Redis::CommandError' do expect { @redises.geoadd key, *coords, 'SF' } diff --git a/spec/commands/geodist_spec.rb b/spec/commands/geodist_spec.rb index 8acfff5e..051df29e 100644 --- a/spec/commands/geodist_spec.rb +++ b/spec/commands/geodist_spec.rb @@ -108,7 +108,7 @@ end context 'with wrong unit' do - let(:message) { 'ERR unsupported unit provided. please use m, km, ft, mi' } + let(:message) { /ERR unsupported unit provided. please use m, km, ft, mi/ } it 'raises an error' do expect { @redises.geodist(key, 'SF', 'LA', 'a') } diff --git a/spec/commands/hdel_spec.rb b/spec/commands/hdel_spec.rb index 6562f183..348911d9 100644 --- a/spec/commands/hdel_spec.rb +++ b/spec/commands/hdel_spec.rb @@ -69,7 +69,7 @@ it 'raises error if an empty array is passed' do expect { @redises.hdel(@key, []) }.to raise_error( Redis::CommandError, - "ERR wrong number of arguments for 'hdel' command" + /ERR wrong number of arguments for 'hdel' command/ ) end diff --git a/spec/commands/hincrby_spec.rb b/spec/commands/hincrby_spec.rb index fad9a327..79b00957 100644 --- a/spec/commands/hincrby_spec.rb +++ b/spec/commands/hincrby_spec.rb @@ -51,7 +51,7 @@ it 'raises an error if the delta does not look like an integer' do expect do @redises.hincrby(@key, @field, 'foo') - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it_should_behave_like 'a hash-only command' diff --git a/spec/commands/hincrbyfloat_spec.rb b/spec/commands/hincrbyfloat_spec.rb index 9cd5b189..d1e3da8f 100644 --- a/spec/commands/hincrbyfloat_spec.rb +++ b/spec/commands/hincrbyfloat_spec.rb @@ -51,7 +51,7 @@ it 'raises an error if the delta does not look like a float' do expect do @redises.hincrbyfloat(@key, @field, 'foobar.baz') - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it_should_behave_like 'a hash-only command' diff --git a/spec/commands/hset_spec.rb b/spec/commands/hset_spec.rb index 1a10b96e..9d42c4d4 100644 --- a/spec/commands/hset_spec.rb +++ b/spec/commands/hset_spec.rb @@ -44,6 +44,18 @@ expect(@redises.hget(@key, 'k2')).to eq('v2') end + it 'raises error when key is nil' do + expect do + @redises.hset(nil, 'abc') + end.to raise_error(TypeError) + end + + it 'raises error when hash key is nil' do + expect do + @redises.hset(@key, nil, 'abc') + end.to raise_error(TypeError) + end + it 'stores multiple arguments correctly' do @redises.hset(@key, 'k1', 'v1', 'k2', 'v2') expect(@redises.hget(@key, 'k1')).to eq('v1') diff --git a/spec/commands/incrby_spec.rb b/spec/commands/incrby_spec.rb index f385ea65..e0dbd460 100644 --- a/spec/commands/incrby_spec.rb +++ b/spec/commands/incrby_spec.rb @@ -37,7 +37,7 @@ it 'raises an error if the delta does not look like an integer' do expect do @redises.incrby(@key, 'foo') - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it_should_behave_like 'a string-only command' diff --git a/spec/commands/incrbyfloat_spec.rb b/spec/commands/incrbyfloat_spec.rb index a12ed1c1..a149e1e7 100644 --- a/spec/commands/incrbyfloat_spec.rb +++ b/spec/commands/incrbyfloat_spec.rb @@ -37,7 +37,7 @@ it 'raises an error if the delta does not look like an float' do expect do @redises.incrbyfloat(@key, 'foobar.baz') - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it_should_behave_like 'a string-only command' diff --git a/spec/commands/linsert_spec.rb b/spec/commands/linsert_spec.rb index 99163ce7..b2d53241 100644 --- a/spec/commands/linsert_spec.rb +++ b/spec/commands/linsert_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' RSpec.describe '#linsert(key, :before|:after, pivot, value)' do + let(:default_error) { Redis::CommandError } + before { @key = 'mock-redis-test:48733' } it 'returns the new size of the list when the pivot is found' do diff --git a/spec/commands/lmove_spec.rb b/spec/commands/lmove_spec.rb index add411b3..1c145e40 100644 --- a/spec/commands/lmove_spec.rb +++ b/spec/commands/lmove_spec.rb @@ -67,6 +67,18 @@ end.to raise_error(Redis::CommandError) end + it 'raises error if wherefrom is not left or right' do + expect do + @redises.lmove(@list1, @list2, 'oops', 'right') + end.to raise_error(ArgumentError, "where_source must be 'LEFT' or 'RIGHT'") + end + + it 'raises error if whereto is not left or right' do + expect do + @redises.lmove(@list1, @list2, 'left', 'oops') + end.to raise_error(ArgumentError, "where_destination must be 'LEFT' or 'RIGHT'") + end + let(:default_error) { RedisMultiplexer::MismatchedResponse } it_should_behave_like 'a list-only command' end diff --git a/spec/commands/lrem_spec.rb b/spec/commands/lrem_spec.rb index 36ba79b5..b4df782c 100644 --- a/spec/commands/lrem_spec.rb +++ b/spec/commands/lrem_spec.rb @@ -68,7 +68,7 @@ it 'raises an error if the value does not look like an integer' do expect do @redises.lrem(@key, 'foo', 'bottles') - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it 'removes empty lists' do diff --git a/spec/commands/mapped_hmget_spec.rb b/spec/commands/mapped_hmget_spec.rb index d4113594..b5a9ac50 100644 --- a/spec/commands/mapped_hmget_spec.rb +++ b/spec/commands/mapped_hmget_spec.rb @@ -15,8 +15,8 @@ to eq({ 'k1' => 'v1', 'mock-redis-test:nonesuch' => nil }) end - it 'treats an array as the first key' do - expect(@redises.mapped_hmget(@key, %w[k1 k2])).to eq({ %w[k1 k2] => 'v1' }) + it 'treats an array as multiple keys' do + expect(@redises.mapped_hmget(@key, %w[k1 k2])).to eq({ 'k1' => 'v1', 'k2' => 'v2' }) end it 'raises an error if given no fields' do diff --git a/spec/commands/pexpire_spec.rb b/spec/commands/pexpire_spec.rb index 44ed96c4..ac84b972 100644 --- a/spec/commands/pexpire_spec.rb +++ b/spec/commands/pexpire_spec.rb @@ -22,7 +22,7 @@ it 'raises an error if ms is bogus' do expect do @redises.pexpire(@key, 'a couple minutes or so') - end.to raise_error(Redis::CommandError) + end.to raise_error(ArgumentError) end it 'works with options', redis: '7.0' do diff --git a/spec/commands/pexpireat_spec.rb b/spec/commands/pexpireat_spec.rb index a96cf1a3..21a1d512 100644 --- a/spec/commands/pexpireat_spec.rb +++ b/spec/commands/pexpireat_spec.rb @@ -24,10 +24,8 @@ def now_ms expect(@redises.get(@key)).to be_nil end - it "raises an error if you don't give it a Unix timestamp" do - expect do - @redises.pexpireat(@key, Time.now) # oops, forgot .to_i - end.to raise_error(Redis::CommandError) + it 'returns true when time object is provided' do + expect(@redises.pexpireat(@key, Time.now)).to eq true end it 'works with options', redis: '7.0' do diff --git a/spec/commands/pipelined_spec.rb b/spec/commands/pipelined_spec.rb index 5e7175ec..38baea87 100644 --- a/spec/commands/pipelined_spec.rb +++ b/spec/commands/pipelined_spec.rb @@ -57,9 +57,9 @@ end it 'returns an array of the array replies' do - results = @redises.pipelined do |_redis| - @redises.lrange(key1, 0, -1) - @redises.lrange(key2, 0, -1) + results = @redises.pipelined do |redis| + redis.lrange(key1, 0, -1) + redis.lrange(key2, 0, -1) end expect(results).to eq([value1, value2]) diff --git a/spec/commands/psetex_spec.rb b/spec/commands/psetex_spec.rb index 724ac997..a015bfa8 100644 --- a/spec/commands/psetex_spec.rb +++ b/spec/commands/psetex_spec.rb @@ -27,18 +27,22 @@ end context 'when expiration time is zero' do + let(:message) { /ERR invalid expire time in psetex/ } + it 'raises Redis::CommandError' do expect do @redises.psetex(@key, 0, 'value') - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in psetex') + end.to raise_error(Redis::CommandError, message) end end context 'when expiration time is negative' do + let(:message) { /ERR invalid expire time in psetex/ } + it 'raises Redis::CommandError' do expect do @redises.psetex(@key, -2, 'value') - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in psetex') + end.to raise_error(Redis::CommandError, message) end end end diff --git a/spec/commands/renamenx_spec.rb b/spec/commands/renamenx_spec.rb index 05faafe6..130a7c3f 100644 --- a/spec/commands/renamenx_spec.rb +++ b/spec/commands/renamenx_spec.rb @@ -25,7 +25,7 @@ it 'raises an error when the source key is nonexistant' do @redises.del(@key) expect do - @redises.rename(@key, @newkey) + @redises.renamenx(@key, @newkey) end.to raise_error(Redis::CommandError) end diff --git a/spec/commands/rpop_spec.rb b/spec/commands/rpop_spec.rb index cf7d0848..5b5f703a 100644 --- a/spec/commands/rpop_spec.rb +++ b/spec/commands/rpop_spec.rb @@ -60,7 +60,10 @@ it_should_behave_like 'a list-only command' do let(:args) { [1] } let(:error) do - [Redis::CommandError, 'WRONGTYPE Operation against a key holding the wrong kind of value'] + [ + Redis::CommandError, + /WRONGTYPE Operation against a key holding the wrong kind of value/ + ] end end end diff --git a/spec/commands/sadd_spec.rb b/spec/commands/sadd_spec.rb index 696d3bb2..9a03c5f8 100644 --- a/spec/commands/sadd_spec.rb +++ b/spec/commands/sadd_spec.rb @@ -5,12 +5,12 @@ context 'sadd' do it 'returns true if the set did not already contain member' do - expect(@redises.sadd(@key, 1)).to eq(true) + expect(@redises.sadd(@key, 1)).to eq(1) end it 'returns false if the set did already contain member' do @redises.sadd(@key, 1) - expect(@redises.sadd(@key, 1)).to eq(false) + expect(@redises.sadd(@key, 1)).to eq(0) end it 'adds member to the set' do @@ -55,7 +55,7 @@ it 'adds an Array as a stringified member' do @redises.sadd(@key, [[1], 2, 3]) - expect(@redises.smembers(@key)).to eq(%w[[1] 2 3]) + expect(@redises.smembers(@key)).to eq(%w[1 2 3]) end it 'raises an error if an empty array is given' do diff --git a/spec/commands/set_spec.rb b/spec/commands/set_spec.rb index 71966890..92fc6a71 100644 --- a/spec/commands/set_spec.rb +++ b/spec/commands/set_spec.rb @@ -25,25 +25,25 @@ it 'raises an error for EX seconds = 0' do expect do @redises.set('mock-redis-test', 1, ex: 0) - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in set') + end.to raise_error(Redis::CommandError, /ERR invalid expire time in set/) end it 'raises an error for PX milliseconds = 0' do expect do @redises.set('mock-redis-test', 1, px: 0) - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in set') + end.to raise_error(Redis::CommandError, /ERR invalid expire time in set/) end it 'raises an error for EXAT seconds = 0' do expect do @redises.set('mock-redis-test', 1, exat: 0) - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in set') + end.to raise_error(Redis::CommandError, /ERR invalid expire time in set/) end it 'raises an error for PXAT seconds = 0' do expect do @redises.set('mock-redis-test', 1, pxat: 0) - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in set') + end.to raise_error(Redis::CommandError, /ERR invalid expire time in set/) end it 'accepts NX' do diff --git a/spec/commands/setbit_spec.rb b/spec/commands/setbit_spec.rb index 2b17b44c..eb6ee1ec 100644 --- a/spec/commands/setbit_spec.rb +++ b/spec/commands/setbit_spec.rb @@ -51,5 +51,5 @@ expect(@redises.getbit(@key, 23)).to eq(0) end - it_should_behave_like 'a string-only command' + it_should_behave_like 'a string-only command', Redis::CommandError end diff --git a/spec/commands/setex_spec.rb b/spec/commands/setex_spec.rb index 22c8a585..a6d10433 100644 --- a/spec/commands/setex_spec.rb +++ b/spec/commands/setex_spec.rb @@ -24,7 +24,7 @@ it 'raises Redis::CommandError' do expect do @redises.setex(@key, 0, 'value') - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in setex') + end.to raise_error(Redis::CommandError, /ERR invalid expire time in setex/) end end @@ -32,7 +32,7 @@ it 'raises Redis::CommandError' do expect do @redises.setex(@key, -2, 'value') - end.to raise_error(Redis::CommandError, 'ERR invalid expire time in setex') + end.to raise_error(Redis::CommandError, /ERR invalid expire time in setex/) end end end diff --git a/spec/commands/sinter_spec.rb b/spec/commands/sinter_spec.rb index 6128fc72..fc3299a0 100644 --- a/spec/commands/sinter_spec.rb +++ b/spec/commands/sinter_spec.rb @@ -15,8 +15,10 @@ expect(@redises.sinter(@evens, @primes)).to eq(['2']) end - it 'treats missing keys as empty sets' do - expect(@redises.sinter(@destination, 'mock-redis-test:nonesuch')).to eq([]) + it 'raises error when key is not set' do + expect do + @redises.sinter(nil, 'mock-redis-test:nonesuch') + end.to raise_error(TypeError) end it 'raises an error if given 0 sets' do diff --git a/spec/commands/srem_spec.rb b/spec/commands/srem_spec.rb index 2c68b978..ec060d6b 100644 --- a/spec/commands/srem_spec.rb +++ b/spec/commands/srem_spec.rb @@ -9,11 +9,11 @@ end it 'returns true if member is in the set' do - expect(@redises.srem(@key, 'bert')).to eq(true) + expect(@redises.srem(@key, 'bert')).to eq(1) end it 'returns false if member is not in the set' do - expect(@redises.srem(@key, 'cookiemonster')).to eq(false) + expect(@redises.srem(@key, 'cookiemonster')).to eq(0) end it 'removes member from the set' do @@ -23,7 +23,7 @@ it 'stringifies member' do @redises.sadd(@key, '1') - expect(@redises.srem(@key, 1)).to eq(true) + expect(@redises.srem(@key, 1)).to eq(1) end it 'cleans up empty sets' do diff --git a/spec/commands/sunion_spec.rb b/spec/commands/sunion_spec.rb index a545058b..288f3a5a 100644 --- a/spec/commands/sunion_spec.rb +++ b/spec/commands/sunion_spec.rb @@ -32,11 +32,11 @@ @redises.set('mock-redis-test:notset', 1) expect do - @redises.sunion(@numbers, 'mock-redis-test:notset') - end.to raise_error(Redis::CommandError) + @redises.sunion(nil, 'mock-redis-test:notset') + end.to raise_error(TypeError) expect do - @redises.sunion('mock-redis-test:notset', @numbers) - end.to raise_error(Redis::CommandError) + @redises.sunion('mock-redis-test:notset', nil) + end.to raise_error(TypeError) end end diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index 0c02b103..a46c3c19 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -47,8 +47,7 @@ expect { @redises.xadd(@key, { key: 'value' }, id: '1234567891233-0') } .to raise_error( Redis::CommandError, - 'ERR The ID specified in XADD is equal or smaller than the target ' \ - 'stream top item' + /ERR The ID specified in XADD is equal or smaller than the target stream top item/ ) end @@ -56,7 +55,7 @@ expect { @redises.xadd('mock-redis-test:unknown-stream', { key: 'value' }, id: '0') } .to raise_error( Redis::CommandError, - 'ERR The ID specified in XADD must be greater than 0-0' + /ERR The ID specified in XADD must be greater than 0-0/ ) end @@ -80,7 +79,7 @@ expect { @redises.xadd(@key, { key: 'value' }, id: 'X') } .to raise_error( Redis::CommandError, - 'ERR Invalid stream ID specified as stream command argument' + /ERR Invalid stream ID specified as stream command argument/ ) end diff --git a/spec/commands/xrange_spec.rb b/spec/commands/xrange_spec.rb index a53f4892..1ca0e4cd 100644 --- a/spec/commands/xrange_spec.rb +++ b/spec/commands/xrange_spec.rb @@ -158,7 +158,7 @@ expect { @redises.xrange(@key, 'X', '+') } .to raise_error( Redis::CommandError, - 'ERR Invalid stream ID specified as stream command argument' + /ERR Invalid stream ID specified as stream command argument/ ) end end diff --git a/spec/commands/xrevrange_spec.rb b/spec/commands/xrevrange_spec.rb index cd4a2f2f..d922e622 100644 --- a/spec/commands/xrevrange_spec.rb +++ b/spec/commands/xrevrange_spec.rb @@ -124,7 +124,7 @@ expect { @redises.xrevrange(@key, 'X', '-') } .to raise_error( Redis::CommandError, - 'ERR Invalid stream ID specified as stream command argument' + /ERR Invalid stream ID specified as stream command argument/ ) end end diff --git a/spec/commands/zadd_spec.rb b/spec/commands/zadd_spec.rb index 0f05ad99..21de2747 100644 --- a/spec/commands/zadd_spec.rb +++ b/spec/commands/zadd_spec.rb @@ -119,9 +119,13 @@ end it 'no longer raises an error if an empty array is given' do + expect(@redises.zadd(@key, [])).to eq(0) + end + + it 'raises error if no argument is set' do expect do - @redises.zadd(@key, []) - end.not_to raise_error(Redis::CommandError) + @redises.zadd(@key) + end.to raise_error(ArgumentError) end it_should_behave_like 'arg 1 is a score' diff --git a/spec/commands/zrem_spec.rb b/spec/commands/zrem_spec.rb index 8d922b32..f6bcf2d9 100644 --- a/spec/commands/zrem_spec.rb +++ b/spec/commands/zrem_spec.rb @@ -41,9 +41,7 @@ end it 'no longer raises an error if member is an empty array' do - expect do - @redises.zrem(@key, []) - end.not_to raise_error(Redis::CommandError) + expect(@redises.zrem(@key, [])).to eq 0 end it_should_behave_like 'a zset-only command' diff --git a/spec/commands/zremrangebyscore_spec.rb b/spec/commands/zremrangebyscore_spec.rb index e69ac421..ac010cfa 100644 --- a/spec/commands/zremrangebyscore_spec.rb +++ b/spec/commands/zremrangebyscore_spec.rb @@ -27,9 +27,9 @@ it_should_behave_like 'a zset-only command' - it 'throws a command error' do + it 'throws a type error' do expect do @redises.zrevrangebyscore(@key, DateTime.now, '-inf') - end.to raise_error(Redis::CommandError) + end.to raise_error(TypeError) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0aa31a75..caba794b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -59,7 +59,7 @@ def args_for_method(method) config.include(TypeCheckingHelper) config.before(:all) do - @redises = RedisMultiplexer.new + @redises = RedisMultiplexer.new(url: 'redis://localhost:6379') end config.before(:each) do diff --git a/spec/support/redis_multiplexer.rb b/spec/support/redis_multiplexer.rb index c5fbbf0b..9fc7e7de 100644 --- a/spec/support/redis_multiplexer.rb +++ b/spec/support/redis_multiplexer.rb @@ -10,7 +10,6 @@ class RedisMultiplexer < BlankSlate def initialize(*a) super() @mock_redis = MockRedis.new(*a) - Redis.exists_returns_integer = true @real_redis = Redis.new(*a) _gsub_clear end diff --git a/spec/support/shared_examples/does_not_cleanup_empty_strings.rb b/spec/support/shared_examples/does_not_cleanup_empty_strings.rb index bac2bbc6..dabff3a9 100644 --- a/spec/support/shared_examples/does_not_cleanup_empty_strings.rb +++ b/spec/support/shared_examples/does_not_cleanup_empty_strings.rb @@ -1,7 +1,7 @@ RSpec.shared_examples_for 'does not remove empty strings on error' do let(:method) { |example| method_from_description(example) } let(:args) { args_for_method(method) } - let(:error) { defined?(default_error) ? default_error : RuntimeError } + let(:error) { defined?(default_error) ? default_error : Redis::WrongTypeError } it 'does not remove empty strings on error' do key = 'mock-redis-test:not-a-string' diff --git a/spec/support/shared_examples/only_operates_on_hashes.rb b/spec/support/shared_examples/only_operates_on_hashes.rb index 117a70d5..c300a674 100644 --- a/spec/support/shared_examples/only_operates_on_hashes.rb +++ b/spec/support/shared_examples/only_operates_on_hashes.rb @@ -8,7 +8,7 @@ @redises.set(key, 1) expect do @redises.send(method, *args) - end.to raise_error(RuntimeError) + end.to raise_error(Redis::WrongTypeError) end it_should_behave_like 'does not remove empty strings on error' diff --git a/spec/support/shared_examples/only_operates_on_sets.rb b/spec/support/shared_examples/only_operates_on_sets.rb index 9d959b3e..bb2eb4d4 100644 --- a/spec/support/shared_examples/only_operates_on_sets.rb +++ b/spec/support/shared_examples/only_operates_on_sets.rb @@ -8,7 +8,7 @@ @redises.set(key, 1) expect do @redises.send(method, *args) - end.to raise_error(RuntimeError) + end.to raise_error(Redis::WrongTypeError) end it_should_behave_like 'does not remove empty strings on error' diff --git a/spec/support/shared_examples/only_operates_on_strings.rb b/spec/support/shared_examples/only_operates_on_strings.rb index d5b907ad..917428c1 100644 --- a/spec/support/shared_examples/only_operates_on_strings.rb +++ b/spec/support/shared_examples/only_operates_on_strings.rb @@ -1,4 +1,4 @@ -RSpec.shared_examples_for 'a string-only command' do +RSpec.shared_examples_for 'a string-only command' do |expected_error = Redis::WrongTypeError| it 'raises an error for non-string values' do |example| key = 'mock-redis-test:string-only-command' @@ -8,6 +8,6 @@ @redises.lpush(key, 1) expect do @redises.send(method, *args) - end.to raise_error(RuntimeError) + end.to raise_error(expected_error) end end diff --git a/spec/support/shared_examples/only_operates_on_zsets.rb b/spec/support/shared_examples/only_operates_on_zsets.rb index a9cbfa7d..5922db1b 100644 --- a/spec/support/shared_examples/only_operates_on_zsets.rb +++ b/spec/support/shared_examples/only_operates_on_zsets.rb @@ -8,7 +8,7 @@ @redises.set(key, 1) expect do @redises.send(method, *args) - end.to raise_error(RuntimeError) + end.to raise_error(Redis::WrongTypeError) end it_should_behave_like 'does not remove empty strings on error' @@ -54,6 +54,6 @@ it 'rejects non-numbers' do @args[@_arg_index] = 'foo' - expect { @redises.send(@method, *@args) }.to raise_error(RuntimeError) + expect { @redises.send(@method, *@args) }.to raise_error(Redis::CommandError) end end diff --git a/spec/support/shared_examples/raises_on_invalid_expire_command_options.rb b/spec/support/shared_examples/raises_on_invalid_expire_command_options.rb index 236b4def..6a79e11c 100644 --- a/spec/support/shared_examples/raises_on_invalid_expire_command_options.rb +++ b/spec/support/shared_examples/raises_on_invalid_expire_command_options.rb @@ -5,7 +5,7 @@ expect { @mock.public_send(command, @key, 1, **options.zip([true, true]).to_h) } .to raise_error( Redis::CommandError, - 'ERR NX and XX, GT or LT options at the same time are not compatible' + /ERR NX and XX, GT or LT options at the same time are not compatible/ ) end end diff --git a/spec/support/shared_examples/sorts_enumerables.rb b/spec/support/shared_examples/sorts_enumerables.rb index 8750620e..7d777638 100644 --- a/spec/support/shared_examples/sorts_enumerables.rb +++ b/spec/support/shared_examples/sorts_enumerables.rb @@ -1,6 +1,6 @@ RSpec.shared_examples_for 'a sortable' do it 'returns empty array on nil' do - expect(@redises.sort(nil)).to eq([]) + expect { @redises.sort(nil) }.to raise_error(TypeError) end context 'ordering' do diff --git a/spec/transactions_spec.rb b/spec/transactions_spec.rb index 8dbfb046..be2cfd94 100644 --- a/spec/transactions_spec.rb +++ b/spec/transactions_spec.rb @@ -6,15 +6,18 @@ end context '#multi' do - it "responds with 'OK'" do - expect(@redises.multi).to eq('OK') + it 'raises error' do + expect { @redises.multi }.to raise_error(LocalJumpError, 'no block given (yield)') end it 'does not permit nesting' do - @redises.multi expect do - @redises.multi - end.to raise_error(Redis::CommandError, 'ERR MULTI calls can not be nested') + @redises.multi do |r1| + r1.multi do |r2| + r2.set('foo', 'bar') + end + end + end.to raise_error(Redis::BaseError, "Can't nest multi transaction") end it 'cleans state of transaction wrapper if exception occurs during transaction' do @@ -52,7 +55,7 @@ @redises.mock.multi do |r| expect do r.multi {} - end.not_to raise_error + end.to raise_error(Redis::BaseError, "Can't nest multi transaction") end end @@ -86,9 +89,17 @@ end context '#discard' do - it "responds with 'OK' after #multi" do - @redises.multi - expect(@redises.discard).to eq 'OK' + it 'runs automatically inside multi block if there is error' do + begin + @redises.multi do |r| + r.set('foo', 'bar') + raise StandardError + end + rescue StandardError + # do nothing + end + + expect(@redises.get('foo')).to eq nil end it "can't be run outside of #multi/#exec" do @@ -100,41 +111,51 @@ context '#exec' do it 'raises an error outside of #multi' do - lambda do - expect(@redises.exec).to raise_error - end + expect { @redises.exec }.to raise_error(Redis::CommandError) end end context 'saving up commands for later' do before(:each) do - @redises.multi @string = 'mock-redis-test:string' @list = 'mock-redis-test:list' end - it "makes commands respond with 'QUEUED'" do - expect(@redises.set(@string, 'string')).to eq 'QUEUED' - expect(@redises.lpush(@list, 'list')).to eq 'QUEUED' + it 'makes commands respond with MockRedis::Future' do + result = [] + + @redises.multi do |r| + result << r.set(@string, 'string') + result << r.lpush(@list, 'list') + end + + expect(result[0].is_a?(Redis::Future)).to eq true + expect(result[1].is_a?(Redis::Future)).to eq true + expect(result[2]).to be_a(MockRedis::Future) + expect(result[3]).to be_a(MockRedis::Future) + + expect(result[2].command).to eq [:set, @string, 'string'] + expect(result[3].command).to eq [:lpush, @list, 'list'] end it "gives you the commands' responses when you call #exec" do - @redises.set(@string, 'string') - @redises.lpush(@list, 'list') - @redises.lpush(@list, 'list') + result = @redises.multi do |r| + r.set(@string, 'string') + r.lpush(@list, 'list') + r.lpush(@list, 'list') + end - expect(@redises.exec).to eq ['OK', 1, 2] + expect(result).to eq ['OK', 1, 2] end - it "does not raise exceptions, but rather puts them in #exec's response" do - @redises.set(@string, 'string') - @redises.lpush(@string, 'oops!') - @redises.lpush(@list, 'list') - - responses = @redises.exec - expect(responses[0]).to eq 'OK' - expect(responses[1]).to be_a(Redis::CommandError) - expect(responses[2]).to eq 1 + it 'raises exceptions if one command fails' do + expect do + @redises.multi do |r| + r.set(@string, 'string') + r.lpush(@string, 'oops!') + r.lpush(@list, 'list') + end + end.to raise_error(Redis::WrongTypeError) end end