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 CHANGED
@@ -1,4 +1,4 @@
1
- # FakeRedis [![Build Status](http://travis-ci.org/guilleiguaran/fakeredis.png)](http://travis-ci.org/guilleiguaran/fakeredis)
1
+ # FakeRedis [![Build Status](https://secure.travis-ci.org/guilleiguaran/fakeredis.png)](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/guilleiguaran/fakeredis"
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
@@ -1,3 +1,3 @@
1
1
  module FakeRedis
2
- VERSION = "0.3.2"
2
+ VERSION = "0.3.3"
3
3
  end
@@ -0,0 +1,4 @@
1
+ module FakeRedis
2
+ class ZSet < Hash
3
+ end
4
+ end
@@ -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
- method = command.shift
97
- reply = send(method, *command)
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 && method != :multi
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
- # * zremrangebyrank
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
- old_value = @data[key]
179
- @data[key] = value
180
- return old_value
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 ArgumentError, "wrong number of arguments for 'mget' command" if keys.empty?
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
- elem = rpop(key1)
342
- lpush(key2, elem)
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
- deleted_count = old_count - @data.keys.size
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
- raise ArgumentError, "wrong number of arguments for 'hmset' command" if fields.empty? || fields.size.odd?
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 ArgumentError, "wrong number of arguments for 'hmget' command" if fields.empty?
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[key] = (@data[key] || "0")
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[key] = (@data[key] || "0")
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[key] = (@data[key] || "0")
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[key] = (@data[key] || "0")
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 value = @data[key]
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].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
- @data[key][value.to_s] += num
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 with_scores
748
- @data[key].sort_by {|_,v| v }
749
- else
750
- @data[key].keys.sort_by {|k| @data[key][k] }
751
- end[start..stop].flatten.map(&:to_s)
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, _, *keys)
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
- hashes = keys.map do |src|
812
- case @data[src]
813
- when ::Set
814
- Hash[@data[src].zip([0] * @data[src].size)]
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 { |(v_a, r_a), (v_b, r_b)| r_a <=> r_b }
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
- fail "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
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