fakeredis 0.3.2 → 0.3.3
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.
- data/README.md +16 -2
- data/fakeredis.gemspec +2 -3
- data/lib/fake_redis.rb +1 -0
- data/lib/fakeredis/expiring_hash.rb +56 -0
- data/lib/fakeredis/sorted_set_argument_handler.rb +74 -0
- data/lib/fakeredis/sorted_set_store.rb +80 -0
- data/lib/fakeredis/version.rb +1 -1
- data/lib/fakeredis/zset.rb +4 -0
- data/lib/redis/connection/memory.rb +82 -114
- data/spec/compatibility_spec.rb +2 -2
- data/spec/connection_spec.rb +13 -3
- data/spec/hashes_spec.rb +44 -33
- data/spec/keys_spec.rb +40 -27
- data/spec/lists_spec.rb +42 -27
- data/spec/server_spec.rb +39 -2
- data/spec/sets_spec.rb +22 -16
- data/spec/sorted_sets_spec.rb +229 -58
- data/spec/spec_helper.rb +4 -0
- data/spec/spec_helper_live_redis.rb +14 -0
- data/spec/strings_spec.rb +92 -45
- metadata +14 -7
    
        data/README.md
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            # FakeRedis [](http://travis-ci.org/guilleiguaran/fakeredis)
         | 
| 2 2 | 
             
            This a fake implementation of redis-rb for machines without Redis or test environments
         | 
| 3 3 |  | 
| 4 4 |  | 
| @@ -13,6 +13,18 @@ Add it to your Gemfile: | |
| 13 13 | 
             
                gem "fakeredis"
         | 
| 14 14 |  | 
| 15 15 |  | 
| 16 | 
            +
            ## Versions
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            FakeRedis currently supports redis-rb v3.0.0 or later, if you are using
         | 
| 19 | 
            +
            redis-rb v2.2.x install the version 0.3.x:
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                gem install fakeredis -v "~> 0.3.0"
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            or use the branch 0-3-x on your Gemfile:
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                gem "fakeredis", :git => "git://github.com/guilleiguaran/fakeredis.git", :branch => "0-3-x"
         | 
| 26 | 
            +
             | 
| 27 | 
            +
             | 
| 16 28 | 
             
            ## Usage
         | 
| 17 29 |  | 
| 18 30 | 
             
            You can use FakeRedis without any changes:
         | 
| @@ -38,7 +50,7 @@ Require this either in your Gemfile or in RSpec's support scripts. So either: | |
| 38 50 | 
             
                group :test do
         | 
| 39 51 | 
             
                  gem "rspec"
         | 
| 40 52 | 
             
                  gem "fakeredis", :require => "fakeredis/rspec"
         | 
| 41 | 
            -
                end | 
| 53 | 
            +
                end
         | 
| 42 54 |  | 
| 43 55 | 
             
            Or:
         | 
| 44 56 |  | 
| @@ -52,6 +64,8 @@ Or: | |
| 52 64 | 
             
            * [obrie](https://github.com/obrie)
         | 
| 53 65 | 
             
            * [jredville](https://github.com/jredville)
         | 
| 54 66 | 
             
            * [redsquirrel](https://github.com/redsquirrel)
         | 
| 67 | 
            +
            * [dpick](https://github.com/dpick)
         | 
| 68 | 
            +
            * [caius](https://github.com/caius) 
         | 
| 55 69 | 
             
            * [Travis-CI](http://travis-ci.org/) (Travis-CI also uses Fakeredis in its tests!!!)
         | 
| 56 70 |  | 
| 57 71 |  | 
    
        data/fakeredis.gemspec
    CHANGED
    
    | @@ -8,12 +8,11 @@ Gem::Specification.new do |s| | |
| 8 8 | 
             
              s.platform    = Gem::Platform::RUBY
         | 
| 9 9 | 
             
              s.authors     = ["Guillermo Iguaran"]
         | 
| 10 10 | 
             
              s.email       = ["guilleiguaran@gmail.com"]
         | 
| 11 | 
            -
              s.homepage    = "https://github.com/ | 
| 11 | 
            +
              s.homepage    = "https://guilleiguaran.github.com/fakeredis"
         | 
| 12 | 
            +
              s.license     = "MIT"
         | 
| 12 13 | 
             
              s.summary     = %q{Fake (In-memory) driver for redis-rb.}
         | 
| 13 14 | 
             
              s.description = %q{Fake (In-memory) driver for redis-rb. Useful for testing environment and machines without Redis.}
         | 
| 14 15 |  | 
| 15 | 
            -
              s.rubyforge_project = "fakeredis"
         | 
| 16 | 
            -
             | 
| 17 16 | 
             
              s.files         = `git ls-files`.split("\n")
         | 
| 18 17 | 
             
              s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
         | 
| 19 18 | 
             
              s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
         | 
    
        data/lib/fake_redis.rb
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            require "fakeredis"
         | 
| @@ -0,0 +1,56 @@ | |
| 1 | 
            +
            module FakeRedis
         | 
| 2 | 
            +
              # Represents a normal hash with some additional expiration information
         | 
| 3 | 
            +
              # associated with each key
         | 
| 4 | 
            +
              class ExpiringHash < Hash
         | 
| 5 | 
            +
                attr_reader :expires
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(*)
         | 
| 8 | 
            +
                  super
         | 
| 9 | 
            +
                  @expires = {}
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def [](key)
         | 
| 13 | 
            +
                  delete(key) if expired?(key)
         | 
| 14 | 
            +
                  super
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def []=(key, val)
         | 
| 18 | 
            +
                  expire(key)
         | 
| 19 | 
            +
                  super
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def delete(key)
         | 
| 23 | 
            +
                  expire(key)
         | 
| 24 | 
            +
                  super
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def expire(key)
         | 
| 28 | 
            +
                  expires.delete(key)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def expired?(key)
         | 
| 32 | 
            +
                  expires.include?(key) && expires[key] < Time.now
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def key?(key)
         | 
| 36 | 
            +
                  delete(key) if expired?(key)
         | 
| 37 | 
            +
                  super
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def values_at(*keys)
         | 
| 41 | 
            +
                  keys.each {|key| delete(key) if expired?(key)}
         | 
| 42 | 
            +
                  super
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def keys
         | 
| 46 | 
            +
                  super.select do |key|
         | 
| 47 | 
            +
                    if expired?(key)
         | 
| 48 | 
            +
                      delete(key)
         | 
| 49 | 
            +
                      false
         | 
| 50 | 
            +
                    else
         | 
| 51 | 
            +
                      true
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
            end
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            module FakeRedis
         | 
| 2 | 
            +
              # Takes in the variable length array of arguments for a zinterstore/zunionstore method
         | 
| 3 | 
            +
              # and parses them into a few attributes for the method to access.
         | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              # Handles throwing errors for various scenarios (matches redis):
         | 
| 6 | 
            +
              #   * Custom weights specified, but not enough or too many given
         | 
| 7 | 
            +
              #   * Invalid aggregate value given
         | 
| 8 | 
            +
              #   * Multiple aggregate values given
         | 
| 9 | 
            +
              class SortedSetArgumentHandler
         | 
| 10 | 
            +
                # [Symbol] The aggregate method to use for the output values. One of %w(sum min max) expected
         | 
| 11 | 
            +
                attr_reader :aggregate
         | 
| 12 | 
            +
                # [Integer] Number of keys in the argument list
         | 
| 13 | 
            +
                attr_accessor :number_of_keys
         | 
| 14 | 
            +
                # [Array] The actual keys in the argument list
         | 
| 15 | 
            +
                attr_accessor :keys
         | 
| 16 | 
            +
                # [Array] integers for weighting the values of each key - one number per key expected
         | 
| 17 | 
            +
                attr_accessor :weights
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                # Used internally
         | 
| 20 | 
            +
                attr_accessor :type
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # Expects all the argments for the method to be passed as an array
         | 
| 23 | 
            +
                def initialize args
         | 
| 24 | 
            +
                  # Pull out known lengths of data
         | 
| 25 | 
            +
                  self.number_of_keys = args.shift
         | 
| 26 | 
            +
                  self.keys = args.shift(number_of_keys)
         | 
| 27 | 
            +
                  # Handle the variable lengths of data (WEIGHTS/AGGREGATE)
         | 
| 28 | 
            +
                  args.inject(self) {|handler, item| handler.handle(item) }
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  # Defaults for unspecified things
         | 
| 31 | 
            +
                  self.weights ||= Array.new(number_of_keys) { 1 }
         | 
| 32 | 
            +
                  self.aggregate ||= :sum
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  # Validate values
         | 
| 35 | 
            +
                  raise(RuntimeError, "ERR syntax error") unless weights.size == number_of_keys
         | 
| 36 | 
            +
                  raise(RuntimeError, "ERR syntax error") unless [:min, :max, :sum].include?(aggregate)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                # Only allows assigning a value *once* - raises Redis::CommandError if a second is given
         | 
| 40 | 
            +
                def aggregate=(str)
         | 
| 41 | 
            +
                  raise(RuntimeError, "ERR syntax error") if (defined?(@aggregate) && @aggregate)
         | 
| 42 | 
            +
                  @aggregate = str.to_s.downcase.to_sym
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                # Decides how to handle an item, depending on where we are in the arguments
         | 
| 46 | 
            +
                def handle(item)
         | 
| 47 | 
            +
                  case item
         | 
| 48 | 
            +
                  when "WEIGHTS"
         | 
| 49 | 
            +
                    self.type = :weights
         | 
| 50 | 
            +
                    self.weights = []
         | 
| 51 | 
            +
                  when "AGGREGATE"
         | 
| 52 | 
            +
                    self.type = :aggregate
         | 
| 53 | 
            +
                  when nil
         | 
| 54 | 
            +
                    # This should never be called, raise a syntax error if we manage to hit it
         | 
| 55 | 
            +
                    raise(RuntimeError, "ERR syntax error")
         | 
| 56 | 
            +
                  else
         | 
| 57 | 
            +
                    send "handle_#{type}", item
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                  self
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def handle_weights(item)
         | 
| 63 | 
            +
                  self.weights << item
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def handle_aggregate(item)
         | 
| 67 | 
            +
                  self.aggregate = item
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def inject_block
         | 
| 71 | 
            +
                  lambda { |handler, item| handler.handle(item) }
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
            end
         | 
| @@ -0,0 +1,80 @@ | |
| 1 | 
            +
            module FakeRedis
         | 
| 2 | 
            +
              class SortedSetStore
         | 
| 3 | 
            +
                attr_accessor :data, :weights, :aggregate, :keys
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize params, data
         | 
| 6 | 
            +
                  self.data = data
         | 
| 7 | 
            +
                  self.weights = params.weights
         | 
| 8 | 
            +
                  self.aggregate = params.aggregate
         | 
| 9 | 
            +
                  self.keys = params.keys
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def hashes
         | 
| 13 | 
            +
                  @hashes ||= keys.map do |src|
         | 
| 14 | 
            +
                    case data[src]
         | 
| 15 | 
            +
                    when ::Set
         | 
| 16 | 
            +
                      # Every value has a score of 1
         | 
| 17 | 
            +
                      Hash[data[src].map {|k,v| [k, 1]}]
         | 
| 18 | 
            +
                    when Hash
         | 
| 19 | 
            +
                      data[src]
         | 
| 20 | 
            +
                    else
         | 
| 21 | 
            +
                      {}
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                # Apply the weightings to the hashes
         | 
| 27 | 
            +
                def computed_values
         | 
| 28 | 
            +
                  unless defined?(@computed_values) && @computed_values
         | 
| 29 | 
            +
                    # Do nothing if all weights are 1, as n * 1 is n
         | 
| 30 | 
            +
                    @computed_values = hashes if weights.all? {|weight| weight == 1 }
         | 
| 31 | 
            +
                    # Otherwise, multiply the values in each hash by that hash's weighting
         | 
| 32 | 
            +
                    @computed_values ||= hashes.each_with_index.map do |hash, index|
         | 
| 33 | 
            +
                      weight = weights[index]
         | 
| 34 | 
            +
                      Hash[hash.map {|k, v| [k, (v * weight)]}]
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                  @computed_values
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def aggregate_sum out
         | 
| 41 | 
            +
                  selected_keys.each do |key|
         | 
| 42 | 
            +
                    out[key] = computed_values.inject(0) do |n, hash|
         | 
| 43 | 
            +
                      n + (hash[key] || 0)
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def aggregate_min out
         | 
| 49 | 
            +
                  selected_keys.each do |key|
         | 
| 50 | 
            +
                    out[key] = computed_values.map {|h| h[key] }.compact.min
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def aggregate_max out
         | 
| 55 | 
            +
                  selected_keys.each do |key|
         | 
| 56 | 
            +
                    out[key] = computed_values.map {|h| h[key] }.compact.max
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def selected_keys
         | 
| 61 | 
            +
                  raise NotImplemented, "subclass needs to implement #selected_keys"
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def call
         | 
| 65 | 
            +
                  ZSet.new.tap {|out| send("aggregate_#{aggregate}", out) }
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              class SortedSetIntersectStore < SortedSetStore
         | 
| 70 | 
            +
                def selected_keys
         | 
| 71 | 
            +
                  @values ||= hashes.inject([]) { |r, h| r.empty? ? h.keys : (r & h.keys) }
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
              class SortedSetUnionStore < SortedSetStore
         | 
| 76 | 
            +
                def selected_keys
         | 
| 77 | 
            +
                  @values ||= hashes.map(&:keys).flatten.uniq
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
            end
         | 
    
        data/lib/fakeredis/version.rb
    CHANGED
    
    
| @@ -1,69 +1,16 @@ | |
| 1 1 | 
             
            require 'set'
         | 
| 2 2 | 
             
            require 'redis/connection/registry'
         | 
| 3 3 | 
             
            require 'redis/connection/command_helper'
         | 
| 4 | 
            +
            require "fakeredis/expiring_hash"
         | 
| 5 | 
            +
            require "fakeredis/sorted_set_argument_handler"
         | 
| 6 | 
            +
            require "fakeredis/sorted_set_store"
         | 
| 7 | 
            +
            require "fakeredis/zset"
         | 
| 4 8 |  | 
| 5 9 | 
             
            class Redis
         | 
| 6 10 | 
             
              module Connection
         | 
| 7 11 | 
             
                class Memory
         | 
| 8 | 
            -
                  # Represents a normal hash with some additional expiration information
         | 
| 9 | 
            -
                  # associated with each key
         | 
| 10 | 
            -
                  class ExpiringHash < Hash
         | 
| 11 | 
            -
                    attr_reader :expires
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    def initialize(*)
         | 
| 14 | 
            -
                      super
         | 
| 15 | 
            -
                      @expires = {}
         | 
| 16 | 
            -
                    end
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                    def [](key)
         | 
| 19 | 
            -
                      delete(key) if expired?(key)
         | 
| 20 | 
            -
                      super
         | 
| 21 | 
            -
                    end
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                    def []=(key, val)
         | 
| 24 | 
            -
                      expire(key)
         | 
| 25 | 
            -
                      super
         | 
| 26 | 
            -
                    end
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                    def delete(key)
         | 
| 29 | 
            -
                      expire(key)
         | 
| 30 | 
            -
                      super
         | 
| 31 | 
            -
                    end
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                    def expire(key)
         | 
| 34 | 
            -
                      expires.delete(key)
         | 
| 35 | 
            -
                    end
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                    def expired?(key)
         | 
| 38 | 
            -
                      expires.include?(key) && expires[key] < Time.now
         | 
| 39 | 
            -
                    end
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                    def key?(key)
         | 
| 42 | 
            -
                      delete(key) if expired?(key)
         | 
| 43 | 
            -
                      super
         | 
| 44 | 
            -
                    end
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                    def values_at(*keys)
         | 
| 47 | 
            -
                      keys.each {|key| delete(key) if expired?(key)}
         | 
| 48 | 
            -
                      super
         | 
| 49 | 
            -
                    end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                    def keys
         | 
| 52 | 
            -
                      super.select do |key|
         | 
| 53 | 
            -
                        if expired?(key)
         | 
| 54 | 
            -
                          delete(key)
         | 
| 55 | 
            -
                          false
         | 
| 56 | 
            -
                        else
         | 
| 57 | 
            -
                          true
         | 
| 58 | 
            -
                        end
         | 
| 59 | 
            -
                      end
         | 
| 60 | 
            -
                    end
         | 
| 61 | 
            -
                  end
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                  class ZSet < Hash
         | 
| 64 | 
            -
                  end
         | 
| 65 | 
            -
             | 
| 66 12 | 
             
                  include Redis::Connection::CommandHelper
         | 
| 13 | 
            +
                  include FakeRedis
         | 
| 67 14 |  | 
| 68 15 | 
             
                  def initialize
         | 
| 69 16 | 
             
                    @data = ExpiringHash.new
         | 
| @@ -93,8 +40,12 @@ class Redis | |
| 93 40 | 
             
                  end
         | 
| 94 41 |  | 
| 95 42 | 
             
                  def write(command)
         | 
| 96 | 
            -
                     | 
| 97 | 
            -
                     | 
| 43 | 
            +
                    meffod = command.shift
         | 
| 44 | 
            +
                    if respond_to?(meffod)
         | 
| 45 | 
            +
                      reply = send(meffod, *command)
         | 
| 46 | 
            +
                    else
         | 
| 47 | 
            +
                      raise RuntimeError, "ERR unknown command '#{meffod}'"
         | 
| 48 | 
            +
                    end
         | 
| 98 49 |  | 
| 99 50 | 
             
                    if reply == true
         | 
| 100 51 | 
             
                      reply = 1
         | 
| @@ -103,7 +54,7 @@ class Redis | |
| 103 54 | 
             
                    end
         | 
| 104 55 |  | 
| 105 56 | 
             
                    @replies << reply
         | 
| 106 | 
            -
                    @buffer << reply if @buffer &&  | 
| 57 | 
            +
                    @buffer << reply if @buffer && meffod != :multi
         | 
| 107 58 | 
             
                    nil
         | 
| 108 59 | 
             
                  end
         | 
| 109 60 |  | 
| @@ -117,13 +68,14 @@ class Redis | |
| 117 68 | 
             
                  # * brpoplpush
         | 
| 118 69 | 
             
                  # * discard
         | 
| 119 70 | 
             
                  # * move
         | 
| 71 | 
            +
                  # * sort
         | 
| 120 72 | 
             
                  # * subscribe
         | 
| 121 73 | 
             
                  # * psubscribe
         | 
| 122 74 | 
             
                  # * publish
         | 
| 123 | 
            -
             | 
| 124 | 
            -
                  # * zunionstore
         | 
| 75 | 
            +
             | 
| 125 76 | 
             
                  def flushdb
         | 
| 126 77 | 
             
                    @data = ExpiringHash.new
         | 
| 78 | 
            +
                    "OK"
         | 
| 127 79 | 
             
                  end
         | 
| 128 80 |  | 
| 129 81 | 
             
                  def flushall
         | 
| @@ -160,6 +112,7 @@ class Redis | |
| 160 112 | 
             
                  def bgreriteaof ; end
         | 
| 161 113 |  | 
| 162 114 | 
             
                  def get(key)
         | 
| 115 | 
            +
                    data_type_check(key, String)
         | 
| 163 116 | 
             
                    @data[key]
         | 
| 164 117 | 
             
                  end
         | 
| 165 118 |  | 
| @@ -175,13 +128,14 @@ class Redis | |
| 175 128 | 
             
                  alias :substr :getrange
         | 
| 176 129 |  | 
| 177 130 | 
             
                  def getset(key, value)
         | 
| 178 | 
            -
                     | 
| 179 | 
            -
                    @data[key]  | 
| 180 | 
            -
             | 
| 131 | 
            +
                    data_type_check(key, String)
         | 
| 132 | 
            +
                    @data[key].tap do
         | 
| 133 | 
            +
                      set(key, value)
         | 
| 134 | 
            +
                    end
         | 
| 181 135 | 
             
                  end
         | 
| 182 136 |  | 
| 183 137 | 
             
                  def mget(*keys)
         | 
| 184 | 
            -
                    raise  | 
| 138 | 
            +
                    raise RuntimeError, "ERR wrong number of arguments for 'mget' command" if keys.empty?
         | 
| 185 139 | 
             
                    @data.values_at(*keys)
         | 
| 186 140 | 
             
                  end
         | 
| 187 141 |  | 
| @@ -254,7 +208,7 @@ class Redis | |
| 254 208 |  | 
| 255 209 | 
             
                  def lrange(key, startidx, endidx)
         | 
| 256 210 | 
             
                    data_type_check(key, Array)
         | 
| 257 | 
            -
                    @data[key] && @data[key][startidx..endidx]
         | 
| 211 | 
            +
                    @data[key] && @data[key][startidx..endidx] || []
         | 
| 258 212 | 
             
                  end
         | 
| 259 213 |  | 
| 260 214 | 
             
                  def ltrim(key, start, stop)
         | 
| @@ -282,7 +236,7 @@ class Redis | |
| 282 236 | 
             
                  def lset(key, index, value)
         | 
| 283 237 | 
             
                    data_type_check(key, Array)
         | 
| 284 238 | 
             
                    return unless @data[key]
         | 
| 285 | 
            -
                    raise RuntimeError if index >= @data[key].size
         | 
| 239 | 
            +
                    raise RuntimeError, "ERR index out of range" if index >= @data[key].size
         | 
| 286 240 | 
             
                    @data[key][index] = value
         | 
| 287 241 | 
             
                  end
         | 
| 288 242 |  | 
| @@ -307,7 +261,7 @@ class Redis | |
| 307 261 | 
             
                  def rpush(key, value)
         | 
| 308 262 | 
             
                    data_type_check(key, Array)
         | 
| 309 263 | 
             
                    @data[key] ||= []
         | 
| 310 | 
            -
                    @data[key].push(value)
         | 
| 264 | 
            +
                    @data[key].push(value.to_s)
         | 
| 311 265 | 
             
                    @data[key].size
         | 
| 312 266 | 
             
                  end
         | 
| 313 267 |  | 
| @@ -320,7 +274,7 @@ class Redis | |
| 320 274 | 
             
                  def lpush(key, value)
         | 
| 321 275 | 
             
                    data_type_check(key, Array)
         | 
| 322 276 | 
             
                    @data[key] ||= []
         | 
| 323 | 
            -
                    @data[key].unshift(value)
         | 
| 277 | 
            +
                    @data[key].unshift(value.to_s)
         | 
| 324 278 | 
             
                    @data[key].size
         | 
| 325 279 | 
             
                  end
         | 
| 326 280 |  | 
| @@ -338,8 +292,9 @@ class Redis | |
| 338 292 |  | 
| 339 293 | 
             
                  def rpoplpush(key1, key2)
         | 
| 340 294 | 
             
                    data_type_check(key1, Array)
         | 
| 341 | 
            -
                     | 
| 342 | 
            -
             | 
| 295 | 
            +
                    rpop(key1).tap do |elem|
         | 
| 296 | 
            +
                      lpush(key2, elem)
         | 
| 297 | 
            +
                    end
         | 
| 343 298 | 
             
                  end
         | 
| 344 299 |  | 
| 345 300 | 
             
                  def lpop(key)
         | 
| @@ -451,7 +406,7 @@ class Redis | |
| 451 406 | 
             
                    keys.flatten.each do |key|
         | 
| 452 407 | 
             
                      @data.delete(key)
         | 
| 453 408 | 
             
                    end
         | 
| 454 | 
            -
                     | 
| 409 | 
            +
                    old_count - @data.keys.size
         | 
| 455 410 | 
             
                  end
         | 
| 456 411 |  | 
| 457 412 | 
             
                  def setnx(key, value)
         | 
| @@ -523,7 +478,11 @@ class Redis | |
| 523 478 | 
             
                  end
         | 
| 524 479 |  | 
| 525 480 | 
             
                  def hmset(key, *fields)
         | 
| 526 | 
            -
                     | 
| 481 | 
            +
                    # mapped_hmset gives us [[:k1, "v1", :k2, "v2"]] for `fields`. Fix that.
         | 
| 482 | 
            +
                    fields = fields[0] if fields.size == 1 && fields[0].is_a?(Array)
         | 
| 483 | 
            +
                    fields = fields[0] if mapped_param?(fields)
         | 
| 484 | 
            +
                    raise RuntimeError, "ERR wrong number of arguments for HMSET" if fields.size > 2 && fields.size.odd?
         | 
| 485 | 
            +
                    raise RuntimeError, "ERR wrong number of arguments for 'hmset' command" if fields.empty? || fields.size.odd?
         | 
| 527 486 | 
             
                    data_type_check(key, Hash)
         | 
| 528 487 | 
             
                    @data[key] ||= {}
         | 
| 529 488 | 
             
                    fields.each_slice(2) do |field|
         | 
| @@ -532,9 +491,8 @@ class Redis | |
| 532 491 | 
             
                  end
         | 
| 533 492 |  | 
| 534 493 | 
             
                  def hmget(key, *fields)
         | 
| 535 | 
            -
                    raise  | 
| 494 | 
            +
                    raise RuntimeError, "ERR wrong number of arguments for 'hmget' command" if fields.empty?
         | 
| 536 495 | 
             
                    data_type_check(key, Hash)
         | 
| 537 | 
            -
                    values = []
         | 
| 538 496 | 
             
                    fields.map do |field|
         | 
| 539 497 | 
             
                      field = field.to_s
         | 
| 540 498 | 
             
                      if @data[key]
         | 
| @@ -612,6 +570,8 @@ class Redis | |
| 612 570 | 
             
                  end
         | 
| 613 571 |  | 
| 614 572 | 
             
                  def mset(*pairs)
         | 
| 573 | 
            +
                    # Handle pairs for mapped_mset command
         | 
| 574 | 
            +
                    pairs = pairs[0] if mapped_param?(pairs)
         | 
| 615 575 | 
             
                    pairs.each_slice(2) do |pair|
         | 
| 616 576 | 
             
                      @data[pair[0].to_s] = pair[1].to_s
         | 
| 617 577 | 
             
                    end
         | 
| @@ -619,9 +579,11 @@ class Redis | |
| 619 579 | 
             
                  end
         | 
| 620 580 |  | 
| 621 581 | 
             
                  def msetnx(*pairs)
         | 
| 582 | 
            +
                    # Handle pairs for mapped_mset command
         | 
| 583 | 
            +
                    pairs = pairs[0] if mapped_param?(pairs)
         | 
| 622 584 | 
             
                    keys = []
         | 
| 623 585 | 
             
                    pairs.each_with_index{|item, index| keys << item.to_s if index % 2 == 0}
         | 
| 624 | 
            -
                    return if keys.any?{|key| @data.key?(key) }
         | 
| 586 | 
            +
                    return false if keys.any? {|key| @data.key?(key) }
         | 
| 625 587 | 
             
                    mset(*pairs)
         | 
| 626 588 | 
             
                    true
         | 
| 627 589 | 
             
                  end
         | 
| @@ -631,31 +593,27 @@ class Redis | |
| 631 593 | 
             
                  end
         | 
| 632 594 |  | 
| 633 595 | 
             
                  def incr(key)
         | 
| 634 | 
            -
                    @data | 
| 635 | 
            -
                    @data[key] = (@data[key].to_i + 1).to_s
         | 
| 596 | 
            +
                    @data.merge!({ key => (@data[key].to_i + 1).to_s || "1"})
         | 
| 636 597 | 
             
                    @data[key].to_i
         | 
| 637 598 | 
             
                  end
         | 
| 638 599 |  | 
| 639 600 | 
             
                  def incrby(key, by)
         | 
| 640 | 
            -
                    @data | 
| 641 | 
            -
                    @data[key] = (@data[key].to_i + by.to_i).to_s
         | 
| 601 | 
            +
                    @data.merge!({ key => (@data[key].to_i + by.to_i).to_s || by })
         | 
| 642 602 | 
             
                    @data[key].to_i
         | 
| 643 603 | 
             
                  end
         | 
| 644 604 |  | 
| 645 605 | 
             
                  def decr(key)
         | 
| 646 | 
            -
                    @data | 
| 647 | 
            -
                    @data[key] = (@data[key].to_i - 1).to_s
         | 
| 606 | 
            +
                    @data.merge!({ key => (@data[key].to_i - 1).to_s || "-1"})
         | 
| 648 607 | 
             
                    @data[key].to_i
         | 
| 649 608 | 
             
                  end
         | 
| 650 609 |  | 
| 651 610 | 
             
                  def decrby(key, by)
         | 
| 652 | 
            -
                    @data | 
| 653 | 
            -
                    @data[key] = (@data[key].to_i - by.to_i).to_s
         | 
| 611 | 
            +
                    @data.merge!({ key => ((@data[key].to_i - by.to_i) || (by.to_i * -1)).to_s })
         | 
| 654 612 | 
             
                    @data[key].to_i
         | 
| 655 613 | 
             
                  end
         | 
| 656 614 |  | 
| 657 615 | 
             
                  def type(key)
         | 
| 658 | 
            -
                    case  | 
| 616 | 
            +
                    case @data[key]
         | 
| 659 617 | 
             
                      when nil then "none"
         | 
| 660 618 | 
             
                      when String then "string"
         | 
| 661 619 | 
             
                      when Hash then "hash"
         | 
| @@ -694,6 +652,7 @@ class Redis | |
| 694 652 | 
             
                    data_type_check(key, ZSet)
         | 
| 695 653 | 
             
                    @data[key] ||= ZSet.new
         | 
| 696 654 | 
             
                    exists = @data[key].key?(value.to_s)
         | 
| 655 | 
            +
                    score = "inf" if score == "+inf"
         | 
| 697 656 | 
             
                    @data[key][value.to_s] = score
         | 
| 698 657 | 
             
                    !exists
         | 
| 699 658 | 
             
                  end
         | 
| @@ -713,7 +672,8 @@ class Redis | |
| 713 672 |  | 
| 714 673 | 
             
                  def zscore(key, value)
         | 
| 715 674 | 
             
                    data_type_check(key, ZSet)
         | 
| 716 | 
            -
                    @data[key] && @data[key][value.to_s] | 
| 675 | 
            +
                    result = @data[key] && @data[key][value.to_s]
         | 
| 676 | 
            +
                    result.to_s if result
         | 
| 717 677 | 
             
                  end
         | 
| 718 678 |  | 
| 719 679 | 
             
                  def zcount(key, min, max)
         | 
| @@ -726,7 +686,12 @@ class Redis | |
| 726 686 | 
             
                    data_type_check(key, ZSet)
         | 
| 727 687 | 
             
                    @data[key] ||= ZSet.new
         | 
| 728 688 | 
             
                    @data[key][value.to_s] ||= 0
         | 
| 729 | 
            -
                     | 
| 689 | 
            +
                    if %w(+inf -inf).include?(num)
         | 
| 690 | 
            +
                      num = "inf" if num == "+inf"
         | 
| 691 | 
            +
                      @data[key][value.to_s] = num
         | 
| 692 | 
            +
                    elsif ! %w(+inf -inf).include?(@data[key][value.to_s])
         | 
| 693 | 
            +
                      @data[key][value.to_s] += num
         | 
| 694 | 
            +
                    end
         | 
| 730 695 | 
             
                    @data[key][value.to_s].to_s
         | 
| 731 696 | 
             
                  end
         | 
| 732 697 |  | 
| @@ -744,11 +709,17 @@ class Redis | |
| 744 709 | 
             
                    data_type_check(key, ZSet)
         | 
| 745 710 | 
             
                    return [] unless @data[key]
         | 
| 746 711 |  | 
| 747 | 
            -
                    if  | 
| 748 | 
            -
             | 
| 749 | 
            -
             | 
| 750 | 
            -
             | 
| 751 | 
            -
             | 
| 712 | 
            +
                    # Sort by score, or if scores are equal, key alphanum
         | 
| 713 | 
            +
                    results = @data[key].sort do |(k1, v1), (k2, v2)|
         | 
| 714 | 
            +
                      if v1 == v2
         | 
| 715 | 
            +
                        k1 <=> k2
         | 
| 716 | 
            +
                      else
         | 
| 717 | 
            +
                        v1 <=> v2
         | 
| 718 | 
            +
                      end
         | 
| 719 | 
            +
                    end
         | 
| 720 | 
            +
                    # Select just the keys unless we want scores
         | 
| 721 | 
            +
                    results = results.map(&:first) unless with_scores
         | 
| 722 | 
            +
                    results[start..stop].flatten.map(&:to_s)
         | 
| 752 723 | 
             
                  end
         | 
| 753 724 |  | 
| 754 725 | 
             
                  def zrevrange(key, start, stop, with_scores = nil)
         | 
| @@ -805,31 +776,23 @@ class Redis | |
| 805 776 | 
             
                    range.size
         | 
| 806 777 | 
             
                  end
         | 
| 807 778 |  | 
| 808 | 
            -
                  def zinterstore(out,  | 
| 779 | 
            +
                  def zinterstore(out, *args)
         | 
| 809 780 | 
             
                    data_type_check(out, ZSet)
         | 
| 781 | 
            +
                    args_handler = SortedSetArgumentHandler.new(args)
         | 
| 782 | 
            +
                    @data[out] = SortedSetIntersectStore.new(args_handler, @data).call
         | 
| 783 | 
            +
                    @data[out].size
         | 
| 784 | 
            +
                  end
         | 
| 810 785 |  | 
| 811 | 
            -
             | 
| 812 | 
            -
             | 
| 813 | 
            -
             | 
| 814 | 
            -
             | 
| 815 | 
            -
                      when Hash
         | 
| 816 | 
            -
                        @data[src]
         | 
| 817 | 
            -
                      else
         | 
| 818 | 
            -
                        {}
         | 
| 819 | 
            -
                      end
         | 
| 820 | 
            -
                    end
         | 
| 821 | 
            -
             | 
| 822 | 
            -
                    @data[out] = ZSet.new
         | 
| 823 | 
            -
                    values = hashes.inject([]) {|r, h| r.empty? ? h.keys : r & h.keys }
         | 
| 824 | 
            -
                    values.each do |value|
         | 
| 825 | 
            -
                      @data[out][value] = hashes.inject(0) {|n, h| n + h[value].to_i }
         | 
| 826 | 
            -
                    end
         | 
| 827 | 
            -
             | 
| 786 | 
            +
                  def zunionstore(out, *args)
         | 
| 787 | 
            +
                    data_type_check(out, ZSet)
         | 
| 788 | 
            +
                    args_handler = SortedSetArgumentHandler.new(args)
         | 
| 789 | 
            +
                    @data[out] = SortedSetUnionStore.new(args_handler, @data).call
         | 
| 828 790 | 
             
                    @data[out].size
         | 
| 829 791 | 
             
                  end
         | 
| 830 792 |  | 
| 831 793 | 
             
                  def zremrangebyrank(key, start, stop)
         | 
| 832 | 
            -
                    sorted_elements = @data[key].sort { |( | 
| 794 | 
            +
                    sorted_elements = @data[key].sort { |(_, r_a), (_, r_b)| r_a <=> r_b }
         | 
| 795 | 
            +
                    start = sorted_elements.length if start > sorted_elements.length
         | 
| 833 796 | 
             
                    elements_to_delete = sorted_elements[start..stop]
         | 
| 834 797 | 
             
                    elements_to_delete.each { |elem, rank| @data[key].delete(elem) }
         | 
| 835 798 | 
             
                    elements_to_delete.size
         | 
| @@ -847,7 +810,8 @@ class Redis | |
| 847 810 |  | 
| 848 811 | 
             
                    def data_type_check(key, klass)
         | 
| 849 812 | 
             
                      if @data[key] && !@data[key].is_a?(klass)
         | 
| 850 | 
            -
                         | 
| 813 | 
            +
                        warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
         | 
| 814 | 
            +
                        raise RuntimeError.new("ERR Operation against a key holding the wrong kind of value")
         | 
| 851 815 | 
             
                      end
         | 
| 852 816 | 
             
                    end
         | 
| 853 817 |  | 
| @@ -863,6 +827,10 @@ class Redis | |
| 863 827 | 
             
                        [offset, count]
         | 
| 864 828 | 
             
                      end
         | 
| 865 829 | 
             
                    end
         | 
| 830 | 
            +
             | 
| 831 | 
            +
                    def mapped_param? param
         | 
| 832 | 
            +
                      param.size == 1 && param[0].is_a?(Array)
         | 
| 833 | 
            +
                    end
         | 
| 866 834 | 
             
                end
         | 
| 867 835 | 
             
              end
         | 
| 868 836 | 
             
            end
         |