fakeredis 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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