From 4ab94cf4e192be533ac5af279eea4610ec10150b Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 11:20:58 +0100 Subject: [PATCH 01/28] First step in implementing streams --- lib/mock_redis/database.rb | 2 ++ lib/mock_redis/streams_methods.rb | 7 +++++++ spec/commands/xadd_spec.rb | 9 +++++++++ 3 files changed, 18 insertions(+) create mode 100644 lib/mock_redis/streams_methods.rb create mode 100644 spec/commands/xadd_spec.rb diff --git a/lib/mock_redis/database.rb b/lib/mock_redis/database.rb index 2314f792..ed5de10e 100644 --- a/lib/mock_redis/database.rb +++ b/lib/mock_redis/database.rb @@ -9,6 +9,7 @@ require 'mock_redis/indifferent_hash' require 'mock_redis/info_method' require 'mock_redis/utility_methods' +require 'mock_redis/streams_methods' class MockRedis class Database @@ -20,6 +21,7 @@ class Database include SortMethod include InfoMethod include UtilityMethods + include StreamsMethods attr_reader :data, :expire_times diff --git a/lib/mock_redis/streams_methods.rb b/lib/mock_redis/streams_methods.rb new file mode 100644 index 00000000..b0d557c2 --- /dev/null +++ b/lib/mock_redis/streams_methods.rb @@ -0,0 +1,7 @@ +class MockRedis + module StreamsMethods + def xadd(key, id, *args) + return "#{Time.now.to_i}-0" if id == '*' + end + end +end diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb new file mode 100644 index 00000000..d2d2bff3 --- /dev/null +++ b/spec/commands/xadd_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe '#xadd(key, id, [field, value, ...])' do + before { @key = 'mock-redis-test:zadd' } + + it "returns an id based on the timestamp" do + expect(@redises.xadd(@key, '*', 'key', 'value')).to match /\d+-0/ + end +end From ed5530d27daf06a1115e0d5b424a922601af6b3a Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 11:21:54 +0100 Subject: [PATCH 02/28] Correct key name --- spec/commands/xadd_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index d2d2bff3..0d028f69 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe '#xadd(key, id, [field, value, ...])' do - before { @key = 'mock-redis-test:zadd' } + before { @key = 'mock-redis-test:xadd' } it "returns an id based on the timestamp" do expect(@redises.xadd(@key, '*', 'key', 'value')).to match /\d+-0/ From 3e51c71c1846e8d70709aa9aed3bbff00ad2a56e Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 11:25:27 +0100 Subject: [PATCH 03/28] Id in time requires milliseconds --- lib/mock_redis/streams_methods.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mock_redis/streams_methods.rb b/lib/mock_redis/streams_methods.rb index b0d557c2..51df3945 100644 --- a/lib/mock_redis/streams_methods.rb +++ b/lib/mock_redis/streams_methods.rb @@ -1,7 +1,9 @@ +require 'date' + class MockRedis module StreamsMethods def xadd(key, id, *args) - return "#{Time.now.to_i}-0" if id == '*' + return "#{DateTime.now.strftime('%Q')}-0" if id == '*' end end end From 075808d8be4a58281dde085fda3f465d25bf0b15 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 16:41:34 +0100 Subject: [PATCH 04/28] Create Streams class --- lib/mock_redis/streams.rb | 43 +++++++++++++++++++++++++++++++ lib/mock_redis/streams_methods.rb | 28 +++++++++++++++++++- spec/commands/xadd_spec.rb | 17 +++++++++++- 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 lib/mock_redis/streams.rb diff --git a/lib/mock_redis/streams.rb b/lib/mock_redis/streams.rb new file mode 100644 index 00000000..85e8e1a5 --- /dev/null +++ b/lib/mock_redis/streams.rb @@ -0,0 +1,43 @@ +require 'forwardable' +require 'set' + +class MockRedis + class Streams + include Enumerable + extend Forwardable + + attr_accessor :members + + def_delegators :members, :empty? + + def initialize + @members = Set.new + @last_timestamp = 0 + @last_i = 0 + end + + def last_id + "#{@last_timestamp}-#{@last_i}" + end + + def add id + t, i = if id == '*' + [ DateTime.now.strftime('%Q').to_i, 0 ] + else + id.split('-').map(&:to_i) + end + i = 0 if i.nil? + if t <= @last_timestamp && i <= @last_i + raise Redis::CommandError, + 'ERR The ID specified in XADD is equal or smaller than the ' \ + 'target stream top item' + end + @last_timestamp = t + @last_i = i + end + + def each + members.each { |m| yield m } + end + end +end diff --git a/lib/mock_redis/streams_methods.rb b/lib/mock_redis/streams_methods.rb index 51df3945..7806d922 100644 --- a/lib/mock_redis/streams_methods.rb +++ b/lib/mock_redis/streams_methods.rb @@ -1,9 +1,35 @@ require 'date' +require 'mock_redis/assertions' +require 'mock_redis/utility_methods' +require 'mock_redis/streams' class MockRedis module StreamsMethods + include Assertions + include UtilityMethods + def xadd(key, id, *args) - return "#{DateTime.now.strftime('%Q')}-0" if id == '*' + with_streams_at(key) do |stream| + stream.add id + return stream.last_id + end + end + + private + + def with_streams_at(key, &blk) + with_thing_at(key, :assert_streamsy, proc { Streams.new }, &blk) + end + + def streamsy?(key) + data[key].nil? || data[key].is_a?(Streams) + end + + def assert_streamsy(key) + unless streamsy?(key) + raise Redis::CommandError, + 'WRONGTYPE Operation against a key holding the wrong kind of value' + end end end end diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index 0d028f69..8f4f4d7f 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -3,7 +3,22 @@ describe '#xadd(key, id, [field, value, ...])' do before { @key = 'mock-redis-test:xadd' } - it "returns an id based on the timestamp" do + it 'returns an id based on the timestamp' do expect(@redises.xadd(@key, '*', 'key', 'value')).to match /\d+-0/ end + + it 'sets the id if it is given' do + expect(@redises.xadd(@key, '1234567891234-2', 'key', 'value')) + .to eq '1234567891234-2' + end + + it 'sets an id based on the timestamp if the given id is before the last' do + @redises.xadd(@key, '1234567891234-0', 'key', 'value') + expect { @redises.xadd(@key, '1234567891233-0', 'key', 'value') } + .to raise_error( + Redis::CommandError, + 'ERR The ID specified in XADD is equal or smaller than the target ' \ + 'stream top item' + ) + end end From d7715c6a5a22bc465b6acee183fea877a34f20ee Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 17:13:34 +0100 Subject: [PATCH 05/28] Ensure data persists by adding a member --- lib/mock_redis/streams.rb | 2 ++ lib/mock_redis/streams_methods.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mock_redis/streams.rb b/lib/mock_redis/streams.rb index 85e8e1a5..7351489c 100644 --- a/lib/mock_redis/streams.rb +++ b/lib/mock_redis/streams.rb @@ -1,5 +1,6 @@ require 'forwardable' require 'set' +require 'date' class MockRedis class Streams @@ -34,6 +35,7 @@ def add id end @last_timestamp = t @last_i = i + members.add last_id end def each diff --git a/lib/mock_redis/streams_methods.rb b/lib/mock_redis/streams_methods.rb index 7806d922..6bb5a155 100644 --- a/lib/mock_redis/streams_methods.rb +++ b/lib/mock_redis/streams_methods.rb @@ -1,4 +1,3 @@ -require 'date' require 'mock_redis/assertions' require 'mock_redis/utility_methods' require 'mock_redis/streams' @@ -9,6 +8,7 @@ module StreamsMethods include UtilityMethods def xadd(key, id, *args) + return_id = nil with_streams_at(key) do |stream| stream.add id return stream.last_id From 026c6f4512f86b8b3d9602d564fa4d5edc292c3b Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 17:32:55 +0100 Subject: [PATCH 06/28] Sequence numbers --- lib/mock_redis/streams.rb | 25 +++++++++++++++---------- spec/commands/xadd_spec.rb | 14 +++++++++++++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/mock_redis/streams.rb b/lib/mock_redis/streams.rb index 7351489c..ae3c2f85 100644 --- a/lib/mock_redis/streams.rb +++ b/lib/mock_redis/streams.rb @@ -22,16 +22,21 @@ def last_id end def add id - t, i = if id == '*' - [ DateTime.now.strftime('%Q').to_i, 0 ] - else - id.split('-').map(&:to_i) - end - i = 0 if i.nil? - if t <= @last_timestamp && i <= @last_i - raise Redis::CommandError, - 'ERR The ID specified in XADD is equal or smaller than the ' \ - 'target stream top item' + t = i = 0 + if id == '*' + t = DateTime.now.strftime('%Q').to_i + if t < @last_timestamp + t = @last_timestamp + i = @last_i + 1 + end + else + t, i = id.split('-').map(&:to_i) + i = 0 if i.nil? + if t <= @last_timestamp && i <= @last_i + raise Redis::CommandError, + 'ERR The ID specified in XADD is equal or smaller than the ' \ + 'target stream top item' + end end @last_timestamp = t @last_i = i diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index 8f4f4d7f..21d53700 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -4,7 +4,8 @@ before { @key = 'mock-redis-test:xadd' } it 'returns an id based on the timestamp' do - expect(@redises.xadd(@key, '*', 'key', 'value')).to match /\d+-0/ + t = Time.now.to_i + expect(@redises.xadd(@key, '*', 'key', 'value')).to match /#{t}\d{3}-0/ end it 'sets the id if it is given' do @@ -21,4 +22,15 @@ 'stream top item' ) end + + it 'caters for the current time being before the last time' do + t = DateTime.now.strftime('%Q').to_i + 2000 + @redises.xadd(@key, "#{t}-0", 'key', 'value') + expect(@redises.xadd(@key, '*', 'key', 'value')).to match /#{t}-1/ + end + + it 'appends a sequence number if it is missing' do + expect(@redises.xadd(@key, '1234567891234', 'key', 'value')) + .to eq '1234567891234-0' + end end From e068c8194fd55904a14ee65c7055f93cdc322129 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 17:55:34 +0100 Subject: [PATCH 07/28] Sequence number with multiple items at the same time --- lib/mock_redis/streams.rb | 2 +- spec/commands/xadd_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/mock_redis/streams.rb b/lib/mock_redis/streams.rb index ae3c2f85..04516f11 100644 --- a/lib/mock_redis/streams.rb +++ b/lib/mock_redis/streams.rb @@ -25,7 +25,7 @@ def add id t = i = 0 if id == '*' t = DateTime.now.strftime('%Q').to_i - if t < @last_timestamp + if t <= @last_timestamp t = @last_timestamp i = @last_i + 1 end diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index 21d53700..6351f773 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -8,6 +8,11 @@ expect(@redises.xadd(@key, '*', 'key', 'value')).to match /#{t}\d{3}-0/ end + it 'increments the sequence number with the same timestamp' do + @redises.xadd(@key, '*', 'key', 'value') + expect(@redises.xadd(@key, '*', 'key', 'value')).to match /\d+-1/ + end + it 'sets the id if it is given' do expect(@redises.xadd(@key, '1234567891234-2', 'key', 'value')) .to eq '1234567891234-2' From 79dbfe7c25d193f73c08f03a0cb37bc15719ea41 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 17:55:59 +0100 Subject: [PATCH 08/28] xlen --- lib/mock_redis/streams_methods.rb | 7 ++++++- spec/commands/xlen_spec.rb | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 spec/commands/xlen_spec.rb diff --git a/lib/mock_redis/streams_methods.rb b/lib/mock_redis/streams_methods.rb index 6bb5a155..7d7a86fb 100644 --- a/lib/mock_redis/streams_methods.rb +++ b/lib/mock_redis/streams_methods.rb @@ -8,13 +8,18 @@ module StreamsMethods include UtilityMethods def xadd(key, id, *args) - return_id = nil with_streams_at(key) do |stream| stream.add id return stream.last_id end end + def xlen(key) + with_streams_at(key) do |stream| + return stream.count + end + end + private def with_streams_at(key, &blk) diff --git a/spec/commands/xlen_spec.rb b/spec/commands/xlen_spec.rb new file mode 100644 index 00000000..809f8bc9 --- /dev/null +++ b/spec/commands/xlen_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe '#xlen(key)' do + before { @key = 'mock-redis-test:xadd' } + + it 'returns the number of items in the stream' do + expect(@redises.xlen(@key)).to eq 0 + @redises.xadd(@key, '*', 'key', 'value') + expect(@redises.xlen(@key)).to eq 1 + 3.times { @redises.xadd(@key, '*', 'key', 'value') } + expect(@redises.xlen(@key)).to eq 4 + end +end From bb66bb5efc45fc8533e3e71d2ea956513e3a30f5 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 20:32:28 +0100 Subject: [PATCH 09/28] Add ranges --- lib/mock_redis/streams.rb | 12 +++++- lib/mock_redis/streams_methods.rb | 8 +++- spec/commands/xlen_spec.rb | 2 +- spec/commands/xrange_spec.rb | 71 +++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 spec/commands/xrange_spec.rb diff --git a/lib/mock_redis/streams.rb b/lib/mock_redis/streams.rb index 04516f11..c5ffe180 100644 --- a/lib/mock_redis/streams.rb +++ b/lib/mock_redis/streams.rb @@ -21,7 +21,7 @@ def last_id "#{@last_timestamp}-#{@last_i}" end - def add id + def add id, values t = i = 0 if id == '*' t = DateTime.now.strftime('%Q').to_i @@ -40,7 +40,15 @@ def add id end @last_timestamp = t @last_i = i - members.add last_id + members.add [ last_id, values ] + end + + def range start, finish + members + .select { |m| + (start == '-' || start.to_i <= m[0].split('-')[0].to_i) && + (finish == '+' || finish.to_i >= m[0].split('-')[0].to_i) + }.to_a end def each diff --git a/lib/mock_redis/streams_methods.rb b/lib/mock_redis/streams_methods.rb index 7d7a86fb..a1f98fd0 100644 --- a/lib/mock_redis/streams_methods.rb +++ b/lib/mock_redis/streams_methods.rb @@ -9,7 +9,7 @@ module StreamsMethods def xadd(key, id, *args) with_streams_at(key) do |stream| - stream.add id + stream.add id, args return stream.last_id end end @@ -20,6 +20,12 @@ def xlen(key) end end + def xrange(key, start, finish) + with_streams_at(key) do |stream| + return stream.range(start, finish) + end + end + private def with_streams_at(key, &blk) diff --git a/spec/commands/xlen_spec.rb b/spec/commands/xlen_spec.rb index 809f8bc9..5125dafb 100644 --- a/spec/commands/xlen_spec.rb +++ b/spec/commands/xlen_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe '#xlen(key)' do - before { @key = 'mock-redis-test:xadd' } + before { @key = 'mock-redis-test:xlen' } it 'returns the number of items in the stream' do expect(@redises.xlen(@key)).to eq 0 diff --git a/spec/commands/xrange_spec.rb b/spec/commands/xrange_spec.rb new file mode 100644 index 00000000..27df079d --- /dev/null +++ b/spec/commands/xrange_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe '#xrange(key, start, end)' do + before { @key = 'mock-redis-test:xrange' } + + it 'finds a single entry with a full range' do + @redises.xadd(@key, '1234567891234-0', 'key', 'value') + expect(@redises.xrange(@key, '-', '+')) + .to eq [ [ '1234567891234-0', [ 'key', 'value' ] ] ] + + end + + context 'six items on the list' do + before :each do + @redises.xadd(@key, '1234567891234-0', 'key1', 'value1') + @redises.xadd(@key, '1234567891245-0', 'key2', 'value2') + @redises.xadd(@key, '1234567891245-1', 'key3', 'value3') + @redises.xadd(@key, '1234567891278-0', 'key4', 'value4') + @redises.xadd(@key, '1234567891278-1', 'key5', 'value5') + @redises.xadd(@key, '1234567891299-0', 'key6', 'value6') + end + + it 'returns entries in sequential order' do + expect(@redises.xrange(@key, '-', '+')).to eq( + [ + [ '1234567891234-0', [ 'key1', 'value1' ] ], + [ '1234567891245-0', [ 'key2', 'value2' ] ], + [ '1234567891245-1', [ 'key3', 'value3' ] ], + [ '1234567891278-0', [ 'key4', 'value4' ] ], + [ '1234567891278-1', [ 'key5', 'value5' ] ], + [ '1234567891299-0', [ 'key6', 'value6' ] ] + ] + ) + end + + it 'returns entries with a lower limit' do + expect(@redises.xrange(@key, '1234567891239-0', '+')).to eq( + [ + [ '1234567891245-0', [ 'key2', 'value2' ] ], + [ '1234567891245-1', [ 'key3', 'value3' ] ], + [ '1234567891278-0', [ 'key4', 'value4' ] ], + [ '1234567891278-1', [ 'key5', 'value5' ] ], + [ '1234567891299-0', [ 'key6', 'value6' ] ] + ] + ) + end + + it 'returns entries with an upper limit' do + expect(@redises.xrange(@key, '-', '1234567891285-0')).to eq( + [ + [ '1234567891234-0', [ 'key1', 'value1' ] ], + [ '1234567891245-0', [ 'key2', 'value2' ] ], + [ '1234567891245-1', [ 'key3', 'value3' ] ], + [ '1234567891278-0', [ 'key4', 'value4' ] ], + [ '1234567891278-1', [ 'key5', 'value5' ] ] + ] + ) + end + + it 'returns entries with both a lower and an upper limit' do + expect(@redises.xrange(@key, '1234567891239-0', '1234567891285-0')).to eq( + [ + [ '1234567891245-0', [ 'key2', 'value2' ] ], + [ '1234567891245-1', [ 'key3', 'value3' ] ], + [ '1234567891278-0', [ 'key4', 'value4' ] ], + [ '1234567891278-1', [ 'key5', 'value5' ] ] + ] + ) + end + end +end From 991cf028956e9043b1c8a7a77da8590cdd77630c Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 20:35:54 +0100 Subject: [PATCH 10/28] streams -> stream --- lib/mock_redis/database.rb | 2 +- lib/mock_redis/{streams.rb => stream.rb} | 0 lib/mock_redis/{streams_methods.rb => stream_methods.rb} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/mock_redis/{streams.rb => stream.rb} (100%) rename lib/mock_redis/{streams_methods.rb => stream_methods.rb} (97%) diff --git a/lib/mock_redis/database.rb b/lib/mock_redis/database.rb index ed5de10e..e15a513f 100644 --- a/lib/mock_redis/database.rb +++ b/lib/mock_redis/database.rb @@ -9,7 +9,7 @@ require 'mock_redis/indifferent_hash' require 'mock_redis/info_method' require 'mock_redis/utility_methods' -require 'mock_redis/streams_methods' +require 'mock_redis/stream_methods' class MockRedis class Database diff --git a/lib/mock_redis/streams.rb b/lib/mock_redis/stream.rb similarity index 100% rename from lib/mock_redis/streams.rb rename to lib/mock_redis/stream.rb diff --git a/lib/mock_redis/streams_methods.rb b/lib/mock_redis/stream_methods.rb similarity index 97% rename from lib/mock_redis/streams_methods.rb rename to lib/mock_redis/stream_methods.rb index a1f98fd0..6f87d27b 100644 --- a/lib/mock_redis/streams_methods.rb +++ b/lib/mock_redis/stream_methods.rb @@ -1,6 +1,6 @@ require 'mock_redis/assertions' require 'mock_redis/utility_methods' -require 'mock_redis/streams' +require 'mock_redis/stream' class MockRedis module StreamsMethods From 22e2717f756ea8fdb956f1dc46e09d07aa77a4d1 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 21:50:03 +0100 Subject: [PATCH 11/28] Extract Ids to new class --- lib/mock_redis/stream.rb | 35 +++++++++--------------------- lib/mock_redis/stream/id.rb | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 lib/mock_redis/stream/id.rb diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index c5ffe180..c25d8698 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -1,6 +1,7 @@ require 'forwardable' require 'set' require 'date' +require 'mock_redis/stream/id' class MockRedis class Streams @@ -13,42 +14,26 @@ class Streams def initialize @members = Set.new - @last_timestamp = 0 - @last_i = 0 + @last_id = nil end def last_id - "#{@last_timestamp}-#{@last_i}" + @last_id.to_s end def add id, values - t = i = 0 - if id == '*' - t = DateTime.now.strftime('%Q').to_i - if t <= @last_timestamp - t = @last_timestamp - i = @last_i + 1 - end - else - t, i = id.split('-').map(&:to_i) - i = 0 if i.nil? - if t <= @last_timestamp && i <= @last_i - raise Redis::CommandError, - 'ERR The ID specified in XADD is equal or smaller than the ' \ - 'target stream top item' - end - end - @last_timestamp = t - @last_i = i - members.add [ last_id, values ] + @last_id = MockRedis::Stream::Id.new(id, min: @last_id) + members.add [ @last_id, values ] + @last_id.to_s end def range start, finish + start_id = MockRedis::Stream::Id.new(start) + finish_id = MockRedis::Stream::Id.new(finish) members .select { |m| - (start == '-' || start.to_i <= m[0].split('-')[0].to_i) && - (finish == '+' || finish.to_i >= m[0].split('-')[0].to_i) - }.to_a + (start_id <= m[0]) && (finish_id >= m[0]) + }.map { |m| [m[0].to_s, m[1]] } end def each diff --git a/lib/mock_redis/stream/id.rb b/lib/mock_redis/stream/id.rb new file mode 100644 index 00000000..d2ab29fa --- /dev/null +++ b/lib/mock_redis/stream/id.rb @@ -0,0 +1,43 @@ +class MockRedis + class Stream + class Id + include Comparable + + attr_accessor :timestamp, :sequence + + def initialize id, min: nil + case id + when '*' + @timestamp = DateTime.now.strftime('%Q').to_i + @sequence = 0 + if self <= min + @timestamp = min.timestamp + @sequence = min.sequence + 1 + end + when '-' + @timestamp = @sequenct = 0 + when '+' + @timestamp = @sequenct = Float::INFINITY + else + @timestamp, @sequence = id.split('-').map(&:to_i) + @sequence = 0 if @sequence.nil? + if self <= min + raise Redis::CommandError, + 'ERR The ID specified in XADD is equal or smaller than ' \ + 'the target stream top item' + end + end + end + + def to_s + "#{@timestamp}-#{@sequence}" + end + + def <=> other + return 1 if other.nil? + return (@sequence <=> other.sequence) if @timestamp == other.timestamp + @timestamp <=> other.timestamp + end + end + end +end From d554f7d8e42546b00f80b50f4b733cc5d66e7368 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 22:02:05 +0100 Subject: [PATCH 12/28] Rubocop --- lib/mock_redis/stream.rb | 10 ++++----- lib/mock_redis/stream/id.rb | 6 ++--- spec/commands/xadd_spec.rb | 6 ++--- spec/commands/xrange_spec.rb | 43 ++++++++++++++++++------------------ 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index c25d8698..c892ac4c 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -21,19 +21,19 @@ def last_id @last_id.to_s end - def add id, values + def add(id, values) @last_id = MockRedis::Stream::Id.new(id, min: @last_id) - members.add [ @last_id, values ] + members.add [@last_id, values] @last_id.to_s end - def range start, finish + def range(start, finish) start_id = MockRedis::Stream::Id.new(start) finish_id = MockRedis::Stream::Id.new(finish) members - .select { |m| + .select do |m| (start_id <= m[0]) && (finish_id >= m[0]) - }.map { |m| [m[0].to_s, m[1]] } + end.map { |m| [m[0].to_s, m[1]] } end def each diff --git a/lib/mock_redis/stream/id.rb b/lib/mock_redis/stream/id.rb index d2ab29fa..043e375d 100644 --- a/lib/mock_redis/stream/id.rb +++ b/lib/mock_redis/stream/id.rb @@ -5,7 +5,7 @@ class Id attr_accessor :timestamp, :sequence - def initialize id, min: nil + def initialize(id, min: nil) case id when '*' @timestamp = DateTime.now.strftime('%Q').to_i @@ -33,9 +33,9 @@ def to_s "#{@timestamp}-#{@sequence}" end - def <=> other + def <=>(other) return 1 if other.nil? - return (@sequence <=> other.sequence) if @timestamp == other.timestamp + return @sequence <=> other.sequence if @timestamp == other.timestamp @timestamp <=> other.timestamp end end diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index 6351f773..633f5925 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -5,12 +5,12 @@ it 'returns an id based on the timestamp' do t = Time.now.to_i - expect(@redises.xadd(@key, '*', 'key', 'value')).to match /#{t}\d{3}-0/ + expect(@redises.xadd(@key, '*', 'key', 'value')).to match(/#{t}\d{3}-0/) end it 'increments the sequence number with the same timestamp' do @redises.xadd(@key, '*', 'key', 'value') - expect(@redises.xadd(@key, '*', 'key', 'value')).to match /\d+-1/ + expect(@redises.xadd(@key, '*', 'key', 'value')).to match(/\d+-1/) end it 'sets the id if it is given' do @@ -31,7 +31,7 @@ it 'caters for the current time being before the last time' do t = DateTime.now.strftime('%Q').to_i + 2000 @redises.xadd(@key, "#{t}-0", 'key', 'value') - expect(@redises.xadd(@key, '*', 'key', 'value')).to match /#{t}-1/ + expect(@redises.xadd(@key, '*', 'key', 'value')).to match(/#{t}-1/) end it 'appends a sequence number if it is missing' do diff --git a/spec/commands/xrange_spec.rb b/spec/commands/xrange_spec.rb index 27df079d..ae1cd404 100644 --- a/spec/commands/xrange_spec.rb +++ b/spec/commands/xrange_spec.rb @@ -6,8 +6,7 @@ it 'finds a single entry with a full range' do @redises.xadd(@key, '1234567891234-0', 'key', 'value') expect(@redises.xrange(@key, '-', '+')) - .to eq [ [ '1234567891234-0', [ 'key', 'value' ] ] ] - + .to eq [['1234567891234-0', %w[key value]]] end context 'six items on the list' do @@ -23,12 +22,12 @@ it 'returns entries in sequential order' do expect(@redises.xrange(@key, '-', '+')).to eq( [ - [ '1234567891234-0', [ 'key1', 'value1' ] ], - [ '1234567891245-0', [ 'key2', 'value2' ] ], - [ '1234567891245-1', [ 'key3', 'value3' ] ], - [ '1234567891278-0', [ 'key4', 'value4' ] ], - [ '1234567891278-1', [ 'key5', 'value5' ] ], - [ '1234567891299-0', [ 'key6', 'value6' ] ] + ['1234567891234-0', %w[key1 value1]], + ['1234567891245-0', %w[key2 value2]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891278-1', %w[key5 value5]], + ['1234567891299-0', %w[key6 value6]] ] ) end @@ -36,11 +35,11 @@ it 'returns entries with a lower limit' do expect(@redises.xrange(@key, '1234567891239-0', '+')).to eq( [ - [ '1234567891245-0', [ 'key2', 'value2' ] ], - [ '1234567891245-1', [ 'key3', 'value3' ] ], - [ '1234567891278-0', [ 'key4', 'value4' ] ], - [ '1234567891278-1', [ 'key5', 'value5' ] ], - [ '1234567891299-0', [ 'key6', 'value6' ] ] + ['1234567891245-0', %w[key2 value2]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891278-1', %w[key5 value5]], + ['1234567891299-0', %w[key6 value6]] ] ) end @@ -48,11 +47,11 @@ it 'returns entries with an upper limit' do expect(@redises.xrange(@key, '-', '1234567891285-0')).to eq( [ - [ '1234567891234-0', [ 'key1', 'value1' ] ], - [ '1234567891245-0', [ 'key2', 'value2' ] ], - [ '1234567891245-1', [ 'key3', 'value3' ] ], - [ '1234567891278-0', [ 'key4', 'value4' ] ], - [ '1234567891278-1', [ 'key5', 'value5' ] ] + ['1234567891234-0', %w[key1 value1]], + ['1234567891245-0', %w[key2 value2]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891278-1', %w[key5 value5]] ] ) end @@ -60,10 +59,10 @@ it 'returns entries with both a lower and an upper limit' do expect(@redises.xrange(@key, '1234567891239-0', '1234567891285-0')).to eq( [ - [ '1234567891245-0', [ 'key2', 'value2' ] ], - [ '1234567891245-1', [ 'key3', 'value3' ] ], - [ '1234567891278-0', [ 'key4', 'value4' ] ], - [ '1234567891278-1', [ 'key5', 'value5' ] ] + ['1234567891245-0', %w[key2 value2]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891278-1', %w[key5 value5]] ] ) end From 4dc0795ebf53c34769a4801202f3ff30b51d95d1 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 22:07:55 +0100 Subject: [PATCH 13/28] streams -> stream (really this time) --- lib/mock_redis/database.rb | 2 +- lib/mock_redis/stream.rb | 2 +- lib/mock_redis/stream_methods.rb | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/mock_redis/database.rb b/lib/mock_redis/database.rb index e15a513f..f2d7240e 100644 --- a/lib/mock_redis/database.rb +++ b/lib/mock_redis/database.rb @@ -21,7 +21,7 @@ class Database include SortMethod include InfoMethod include UtilityMethods - include StreamsMethods + include StreamMethods attr_reader :data, :expire_times diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index c892ac4c..d441dc27 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -4,7 +4,7 @@ require 'mock_redis/stream/id' class MockRedis - class Streams + class Stream include Enumerable extend Forwardable diff --git a/lib/mock_redis/stream_methods.rb b/lib/mock_redis/stream_methods.rb index 6f87d27b..f817d740 100644 --- a/lib/mock_redis/stream_methods.rb +++ b/lib/mock_redis/stream_methods.rb @@ -3,41 +3,41 @@ require 'mock_redis/stream' class MockRedis - module StreamsMethods + module StreamMethods include Assertions include UtilityMethods def xadd(key, id, *args) - with_streams_at(key) do |stream| + with_stream_at(key) do |stream| stream.add id, args return stream.last_id end end def xlen(key) - with_streams_at(key) do |stream| + with_stream_at(key) do |stream| return stream.count end end def xrange(key, start, finish) - with_streams_at(key) do |stream| + with_stream_at(key) do |stream| return stream.range(start, finish) end end private - def with_streams_at(key, &blk) - with_thing_at(key, :assert_streamsy, proc { Streams.new }, &blk) + def with_stream_at(key, &blk) + with_thing_at(key, :assert_streamy, proc { Stream.new }, &blk) end - def streamsy?(key) - data[key].nil? || data[key].is_a?(Streams) + def streamy?(key) + data[key].nil? || data[key].is_a?(Stream) end - def assert_streamsy(key) - unless streamsy?(key) + def assert_streamy(key) + unless streamy?(key) raise Redis::CommandError, 'WRONGTYPE Operation against a key holding the wrong kind of value' end From 393aa66e45e64c2bb9b28ec8e51d7ad29cd8fc23 Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 23:31:27 +0100 Subject: [PATCH 14/28] More xrange --- lib/mock_redis/stream.rb | 2 +- lib/mock_redis/stream/id.rb | 8 ++++---- spec/commands/xrange_spec.rb | 37 ++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index d441dc27..89ed30c1 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -29,7 +29,7 @@ def add(id, values) def range(start, finish) start_id = MockRedis::Stream::Id.new(start) - finish_id = MockRedis::Stream::Id.new(finish) + finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY) members .select do |m| (start_id <= m[0]) && (finish_id >= m[0]) diff --git a/lib/mock_redis/stream/id.rb b/lib/mock_redis/stream/id.rb index 043e375d..cc65424d 100644 --- a/lib/mock_redis/stream/id.rb +++ b/lib/mock_redis/stream/id.rb @@ -5,7 +5,7 @@ class Id attr_accessor :timestamp, :sequence - def initialize(id, min: nil) + def initialize(id, min: nil, sequence: 0) case id when '*' @timestamp = DateTime.now.strftime('%Q').to_i @@ -15,12 +15,12 @@ def initialize(id, min: nil) @sequence = min.sequence + 1 end when '-' - @timestamp = @sequenct = 0 + @timestamp = @sequence = 0 when '+' - @timestamp = @sequenct = Float::INFINITY + @timestamp = @sequence = Float::INFINITY else @timestamp, @sequence = id.split('-').map(&:to_i) - @sequence = 0 if @sequence.nil? + @sequence = sequence if @sequence.nil? if self <= min raise Redis::CommandError, 'ERR The ID specified in XADD is equal or smaller than ' \ diff --git a/spec/commands/xrange_spec.rb b/spec/commands/xrange_spec.rb index ae1cd404..10889dd1 100644 --- a/spec/commands/xrange_spec.rb +++ b/spec/commands/xrange_spec.rb @@ -3,6 +3,10 @@ describe '#xrange(key, start, end)' do before { @key = 'mock-redis-test:xrange' } + it 'finds an empty range' do + expect(@redises.xrange(@key, '-', '+')).to eq [] + end + it 'finds a single entry with a full range' do @redises.xadd(@key, '1234567891234-0', 'key', 'value') expect(@redises.xrange(@key, '-', '+')) @@ -66,5 +70,38 @@ ] ) end + + it 'finds the list with sequence numbers' do + expect(@redises.xrange(@key, '1234567891245-1', '1234567891278-0')).to eq( + [ + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]] + ] + ) + end + + it 'finds the list with lower bound without sequence numbers' do + expect(@redises.xrange(@key, '1234567891245', '+')).to eq( + [ + ['1234567891245-0', %w[key2 value2]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891278-1', %w[key5 value5]], + ['1234567891299-0', %w[key6 value6]] + ] + ) + end + + it 'finds the list with upper bound without sequence numbers' do + expect(@redises.xrange(@key, '-', '1234567891278')).to eq( + [ + ['1234567891234-0', %w[key1 value1]], + ['1234567891245-0', %w[key2 value2]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891278-1', %w[key5 value5]] + ] + ) + end end end From 22b4fe8b03660b38ad22ab7bc220658f83fc716d Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 9 Jun 2018 23:37:54 +0100 Subject: [PATCH 15/28] A little more readable --- lib/mock_redis/stream.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index 89ed30c1..cea8facd 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -31,9 +31,8 @@ def range(start, finish) start_id = MockRedis::Stream::Id.new(start) finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY) members - .select do |m| - (start_id <= m[0]) && (finish_id >= m[0]) - end.map { |m| [m[0].to_s, m[1]] } + .select { |m| (start_id <= m[0]) && (finish_id >= m[0]) } + .map { |m| [m[0].to_s, m[1]] } end def each From 897e63384b12352db914a1ead5184b80dd056968 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 11 Jun 2018 10:15:52 +0100 Subject: [PATCH 16/28] Ensure messages are strings rather than symbols --- lib/mock_redis/stream.rb | 2 +- spec/commands/xadd_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index cea8facd..7a83efe8 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -23,7 +23,7 @@ def last_id def add(id, values) @last_id = MockRedis::Stream::Id.new(id, min: @last_id) - members.add [@last_id, values] + members.add [@last_id, values.map(&:to_s)] @last_id.to_s end diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index 633f5925..5bb99980 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -8,6 +8,12 @@ expect(@redises.xadd(@key, '*', 'key', 'value')).to match(/#{t}\d{3}-0/) end + it 'adds data with symbols' do + @redises.xadd(@key, '*', :symbol_key, :symbol_value) + expect(@redises.xrange(@key, '-', '+').last[1]) + .to eq(%w[symbol_key symbol_value]) + end + it 'increments the sequence number with the same timestamp' do @redises.xadd(@key, '*', 'key', 'value') expect(@redises.xadd(@key, '*', 'key', 'value')).to match(/\d+-1/) From 04832d868ea5b204e695c008a0f2ae82bbaeb592 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 11 Jun 2018 14:10:48 +0100 Subject: [PATCH 17/28] COUNT option for xrange --- lib/mock_redis/stream.rb | 12 ++++++++---- lib/mock_redis/stream_methods.rb | 4 ++-- spec/commands/xrange_spec.rb | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index 7a83efe8..999a3bc7 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -27,12 +27,16 @@ def add(id, values) @last_id.to_s end - def range(start, finish) + def range(start, finish, *options) + opts = {} + options.each_slice(2).map { |pair| opts[pair[0].downcase] = pair[1].to_i } start_id = MockRedis::Stream::Id.new(start) finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY) - members - .select { |m| (start_id <= m[0]) && (finish_id >= m[0]) } - .map { |m| [m[0].to_s, m[1]] } + items = members + .select { |m| (start_id <= m[0]) && (finish_id >= m[0]) } + .map { |m| [m[0].to_s, m[1]] } + return items.first(opts['count'].to_i) if opts.key?('count') + items end def each diff --git a/lib/mock_redis/stream_methods.rb b/lib/mock_redis/stream_methods.rb index f817d740..895970bc 100644 --- a/lib/mock_redis/stream_methods.rb +++ b/lib/mock_redis/stream_methods.rb @@ -20,9 +20,9 @@ def xlen(key) end end - def xrange(key, start, finish) + def xrange(key, start, finish, *options) with_stream_at(key) do |stream| - return stream.range(start, finish) + return stream.range(start, finish, *options) end end diff --git a/spec/commands/xrange_spec.rb b/spec/commands/xrange_spec.rb index 10889dd1..18e89e14 100644 --- a/spec/commands/xrange_spec.rb +++ b/spec/commands/xrange_spec.rb @@ -103,5 +103,20 @@ ] ) end + + it 'returns a limited number of items' do + expect(@redises.xrange(@key, '-', '+', 'COUNT', '2')).to eq( + [ + ['1234567891234-0', %w[key1 value1]], + ['1234567891245-0', %w[key2 value2]] + ] + ) + expect(@redises.xrange(@key, '-', '+', 'count', '2')).to eq( + [ + ['1234567891234-0', %w[key1 value1]], + ['1234567891245-0', %w[key2 value2]] + ] + ) + end end end From c58f28562981a06cacd855edf0ef84805008c269 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Tue, 12 Jun 2018 08:55:53 +0100 Subject: [PATCH 18/28] List commands that are not yet supported --- lib/mock_redis/stream.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index 999a3bc7..bad35758 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -3,6 +3,21 @@ require 'date' require 'mock_redis/stream/id' +# TODO: +# +# * xrange (see https://github.com/antirez/redis/issues/5006) +# * xread +# * xgroup +# * xreadgroup +# * xack +# * xpending +# * xclaim +# * xinfo +# * xtrim +# * xdel +# +# For details of these commands see https://redis.io/topics/streams-intro + class MockRedis class Stream include Enumerable From e3bb1ce0b540bbdebec13ebd04cd54d1a3e9a53c Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Tue, 12 Jun 2018 23:03:34 +0100 Subject: [PATCH 19/28] Correctly report some errors in xadd --- lib/mock_redis/stream.rb | 2 +- lib/mock_redis/stream/id.rb | 12 +++++++++++- lib/mock_redis/stream_methods.rb | 10 +++++++++- spec/commands/xadd_spec.rb | 29 +++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index bad35758..026b950c 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -3,7 +3,7 @@ require 'date' require 'mock_redis/stream/id' -# TODO: +# TODO: Implement the following commands # # * xrange (see https://github.com/antirez/redis/issues/5006) # * xread diff --git a/lib/mock_redis/stream/id.rb b/lib/mock_redis/stream/id.rb index cc65424d..78249767 100644 --- a/lib/mock_redis/stream/id.rb +++ b/lib/mock_redis/stream/id.rb @@ -19,7 +19,17 @@ def initialize(id, min: nil, sequence: 0) when '+' @timestamp = @sequence = Float::INFINITY else - @timestamp, @sequence = id.split('-').map(&:to_i) + if id.is_a? String + (_, @timestamp, @sequence) = id.match(/^(\d+)-?(\d+)?$/) + .to_a + .map(&:to_i) + if @timestamp.nil? + raise Redis::CommandError, + 'ERR Invalid stream ID specified as stream command argument' + end + else + @timestamp = id + end @sequence = sequence if @sequence.nil? if self <= min raise Redis::CommandError, diff --git a/lib/mock_redis/stream_methods.rb b/lib/mock_redis/stream_methods.rb index 895970bc..23a3e36a 100644 --- a/lib/mock_redis/stream_methods.rb +++ b/lib/mock_redis/stream_methods.rb @@ -7,7 +7,15 @@ module StreamMethods include Assertions include UtilityMethods - def xadd(key, id, *args) + def xadd(key = nil, id = nil, *args) + if args.count == 0 + raise Redis::CommandError, + "ERR wrong number of arguments for 'xadd' command" + end + if args.count.odd? + raise Redis::CommandError, + 'ERR wrong number of arguments for XADD' + end with_stream_at(key) do |stream| stream.add id, args return stream.last_id diff --git a/spec/commands/xadd_spec.rb b/spec/commands/xadd_spec.rb index 5bb99980..0022b961 100644 --- a/spec/commands/xadd_spec.rb +++ b/spec/commands/xadd_spec.rb @@ -24,6 +24,11 @@ .to eq '1234567891234-2' end + it 'accepts is as an integer' do + expect(@redises.xadd(@key, 1_234_567_891_234, 'key', 'value')) + .to eq '1234567891234-0' + end + it 'sets an id based on the timestamp if the given id is before the last' do @redises.xadd(@key, '1234567891234-0', 'key', 'value') expect { @redises.xadd(@key, '1234567891233-0', 'key', 'value') } @@ -44,4 +49,28 @@ expect(@redises.xadd(@key, '1234567891234', 'key', 'value')) .to eq '1234567891234-0' end + + it 'raises wrong number of arguments error with missing values' do + expect { @redises.xadd(@key, '*') } + .to raise_error( + Redis::CommandError, + "ERR wrong number of arguments for 'xadd' command" + ) + end + + it 'raises wrong number of arguments error with odd number of values' do + expect { @redises.xadd(@key, '*', 'key', 'value', 'key') } + .to raise_error( + Redis::CommandError, + 'ERR wrong number of arguments for XADD' + ) + end + + it 'raises an invalid stream id error' do + expect { @redises.xadd(@key, 'X', 'key', 'value') } + .to raise_error( + Redis::CommandError, + 'ERR Invalid stream ID specified as stream command argument' + ) + end end From ea10696f70128853d4ce53a7def793289a4460bd Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 23 Jun 2018 21:57:46 +0100 Subject: [PATCH 20/28] Add xrevrange command --- lib/mock_redis/stream.rb | 6 +- lib/mock_redis/stream/id.rb | 4 +- lib/mock_redis/stream_methods.rb | 8 +- spec/commands/xrange_spec.rb | 11 +++ spec/commands/xrevrange_spec.rb | 123 +++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 spec/commands/xrevrange_spec.rb diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index 026b950c..763a5e6c 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -5,7 +5,8 @@ # TODO: Implement the following commands # -# * xrange (see https://github.com/antirez/redis/issues/5006) +# * xrange +# * xrevrange (see https://github.com/antirez/redis/issues/5006) # * xread # * xgroup # * xreadgroup @@ -42,7 +43,7 @@ def add(id, values) @last_id.to_s end - def range(start, finish, *options) + def range(start, finish, reversed, *options) opts = {} options.each_slice(2).map { |pair| opts[pair[0].downcase] = pair[1].to_i } start_id = MockRedis::Stream::Id.new(start) @@ -50,6 +51,7 @@ def range(start, finish, *options) items = members .select { |m| (start_id <= m[0]) && (finish_id >= m[0]) } .map { |m| [m[0].to_s, m[1]] } + items.reverse! if reversed return items.first(opts['count'].to_i) if opts.key?('count') items end diff --git a/lib/mock_redis/stream/id.rb b/lib/mock_redis/stream/id.rb index 78249767..06da39ca 100644 --- a/lib/mock_redis/stream/id.rb +++ b/lib/mock_redis/stream/id.rb @@ -22,7 +22,7 @@ def initialize(id, min: nil, sequence: 0) if id.is_a? String (_, @timestamp, @sequence) = id.match(/^(\d+)-?(\d+)?$/) .to_a - .map(&:to_i) + @timestamp = @timestamp.to_i if @timestamp.nil? raise Redis::CommandError, 'ERR Invalid stream ID specified as stream command argument' @@ -30,7 +30,7 @@ def initialize(id, min: nil, sequence: 0) else @timestamp = id end - @sequence = sequence if @sequence.nil? + @sequence = @sequence.nil? ? sequence : @sequence.to_i if self <= min raise Redis::CommandError, 'ERR The ID specified in XADD is equal or smaller than ' \ diff --git a/lib/mock_redis/stream_methods.rb b/lib/mock_redis/stream_methods.rb index 23a3e36a..bda4523c 100644 --- a/lib/mock_redis/stream_methods.rb +++ b/lib/mock_redis/stream_methods.rb @@ -30,7 +30,13 @@ def xlen(key) def xrange(key, start, finish, *options) with_stream_at(key) do |stream| - return stream.range(start, finish, *options) + return stream.range(start, finish, false, *options) + end + end + + def xrevrange(key, finish, start, *options) + with_stream_at(key) do |stream| + return stream.range(start, finish, true, *options) end end diff --git a/spec/commands/xrange_spec.rb b/spec/commands/xrange_spec.rb index 18e89e14..3a702e42 100644 --- a/spec/commands/xrange_spec.rb +++ b/spec/commands/xrange_spec.rb @@ -104,6 +104,17 @@ ) end + it 'accepts limits as integers' do + expect(@redises.xrange(@key, 1_234_567_891_245, 1_234_567_891_278)).to eq( + [ + ['1234567891245-0', %w[key2 value2]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891278-1', %w[key5 value5]] + ] + ) + end + it 'returns a limited number of items' do expect(@redises.xrange(@key, '-', '+', 'COUNT', '2')).to eq( [ diff --git a/spec/commands/xrevrange_spec.rb b/spec/commands/xrevrange_spec.rb new file mode 100644 index 00000000..b0288c09 --- /dev/null +++ b/spec/commands/xrevrange_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +describe '#xrevrange(key, start, end)' do + before { @key = 'mock-redis-test:xrevrange' } + + it 'finds an empty range' do + expect(@redises.xrevrange(@key, '-', '+')).to eq [] + end + + it 'finds a single entry with a full range' do + @redises.xadd(@key, '1234567891234-0', 'key', 'value') + expect(@redises.xrevrange(@key, '+', '-')) + .to eq [['1234567891234-0', %w[key value]]] + end + + context 'six items on the list' do + before :each do + @redises.xadd(@key, '1234567891234-0', 'key1', 'value1') + @redises.xadd(@key, '1234567891245-0', 'key2', 'value2') + @redises.xadd(@key, '1234567891245-1', 'key3', 'value3') + @redises.xadd(@key, '1234567891278-0', 'key4', 'value4') + @redises.xadd(@key, '1234567891278-1', 'key5', 'value5') + @redises.xadd(@key, '1234567891299-0', 'key6', 'value6') + end + + it 'returns entries in sequential order' do + expect(@redises.xrevrange(@key, '+', '-')).to eq( + [ + ['1234567891299-0', %w[key6 value6]], + ['1234567891278-1', %w[key5 value5]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891245-0', %w[key2 value2]], + ['1234567891234-0', %w[key1 value1]], + ] + ) + end + + it 'returns entries with a lower limit' do + expect(@redises.xrevrange(@key, '+', '1234567891239-0')).to eq( + [ + ['1234567891299-0', %w[key6 value6]], + ['1234567891278-1', %w[key5 value5]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891245-0', %w[key2 value2]], + ] + ) + end + + it 'returns entries with an upper limit' do + require 'pry' + expect(@redises.xrevrange(@key, '1234567891285-0', '-')).to eq( + [ + ['1234567891278-1', %w[key5 value5]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891245-0', %w[key2 value2]], + ['1234567891234-0', %w[key1 value1]], + ] + ) + end + + it 'returns entries with both a lower and an upper limit' do + expect(@redises.xrevrange(@key, '1234567891285-0', '1234567891239-0')).to eq( + [ + ['1234567891278-1', %w[key5 value5]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891245-0', %w[key2 value2]], + ] + ) + end + + it 'finds the list with sequence numbers' do + expect(@redises.xrevrange(@key, '1234567891278-0', '1234567891245-1')).to eq( + [ + ['1234567891278-0', %w[key4 value4]], + ['1234567891245-1', %w[key3 value3]], + ] + ) + end + + it 'finds the list with lower bound without sequence numbers' do + expect(@redises.xrevrange(@key, '+', '1234567891245')).to eq( + [ + ['1234567891299-0', %w[key6 value6]], + ['1234567891278-1', %w[key5 value5]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891245-0', %w[key2 value2]], + ] + ) + end + + it 'finds the list with upper bound without sequence numbers' do + expect(@redises.xrevrange(@key, '1234567891278', '-')).to eq( + [ + ['1234567891278-1', %w[key5 value5]], + ['1234567891278-0', %w[key4 value4]], + ['1234567891245-1', %w[key3 value3]], + ['1234567891245-0', %w[key2 value2]], + ['1234567891234-0', %w[key1 value1]], + ] + ) + end + + it 'returns a limited number of items' do + expect(@redises.xrevrange(@key, '+', '-', 'COUNT', '2')).to eq( + [ + ['1234567891299-0', %w[key6 value6]], + ['1234567891278-1', %w[key5 value5]], + ] + ) + expect(@redises.xrevrange(@key, '+', '-', 'count', '2')).to eq( + [ + ['1234567891299-0', %w[key6 value6]], + ['1234567891278-1', %w[key5 value5]], + ] + ) + end + end +end From 420556a74fa3a8befdc7804160abcfaa5061608b Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 23 Jun 2018 22:07:22 +0100 Subject: [PATCH 21/28] Raise error correctly --- lib/mock_redis/stream.rb | 2 -- lib/mock_redis/stream/id.rb | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index 763a5e6c..e957c671 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -5,8 +5,6 @@ # TODO: Implement the following commands # -# * xrange -# * xrevrange (see https://github.com/antirez/redis/issues/5006) # * xread # * xgroup # * xreadgroup diff --git a/lib/mock_redis/stream/id.rb b/lib/mock_redis/stream/id.rb index 06da39ca..d06cb3d6 100644 --- a/lib/mock_redis/stream/id.rb +++ b/lib/mock_redis/stream/id.rb @@ -22,11 +22,11 @@ def initialize(id, min: nil, sequence: 0) if id.is_a? String (_, @timestamp, @sequence) = id.match(/^(\d+)-?(\d+)?$/) .to_a - @timestamp = @timestamp.to_i if @timestamp.nil? raise Redis::CommandError, 'ERR Invalid stream ID specified as stream command argument' end + @timestamp = @timestamp.to_i else @timestamp = id end From 525d593f5645c0af9cfc6f326f69c69ecae3d99a Mon Sep 17 00:00:00 2001 From: Joseph Haig Date: Sat, 23 Jun 2018 23:25:58 +0100 Subject: [PATCH 22/28] Deal with errors --- lib/mock_redis/stream.rb | 18 +++++++++++++++--- lib/mock_redis/stream_methods.rb | 18 +++++++++++++++--- spec/commands/xlen_spec.rb | 16 ++++++++++++++++ spec/commands/xrange_spec.rb | 32 ++++++++++++++++++++++++++++++++ spec/commands/xrevrange_spec.rb | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 6 deletions(-) diff --git a/lib/mock_redis/stream.rb b/lib/mock_redis/stream.rb index e957c671..6c0c0323 100644 --- a/lib/mock_redis/stream.rb +++ b/lib/mock_redis/stream.rb @@ -41,9 +41,11 @@ def add(id, values) @last_id.to_s end - def range(start, finish, reversed, *options) - opts = {} - options.each_slice(2).map { |pair| opts[pair[0].downcase] = pair[1].to_i } + def range(start, finish, reversed, *opts_in) + opts = options opts_in, ['count'] + unless opts['count'].nil? || /^\d*$/.match(opts['count']) + raise Redis::CommandError, 'ERR value is not an integer or out of range' + end start_id = MockRedis::Stream::Id.new(start) finish_id = MockRedis::Stream::Id.new(finish, sequence: Float::INFINITY) items = members @@ -57,5 +59,15 @@ def range(start, finish, reversed, *options) def each members.each { |m| yield m } end + + private + + def options(opts_in, permitted) + opts_out = {} + raise Redis::CommandError, 'ERR syntax error' unless (opts_in.length % 2).zero? + opts_in.each_slice(2).map { |pair| opts_out[pair[0].downcase] = pair[1] } + raise Redis::CommandError, 'ERR syntax error' unless (opts_out.keys - permitted).empty? + opts_out + end end end diff --git a/lib/mock_redis/stream_methods.rb b/lib/mock_redis/stream_methods.rb index bda4523c..e8efcbe3 100644 --- a/lib/mock_redis/stream_methods.rb +++ b/lib/mock_redis/stream_methods.rb @@ -22,19 +22,31 @@ def xadd(key = nil, id = nil, *args) end end - def xlen(key) + def xlen(key = nil, *args) + if key.nil? || args.count > 0 + raise Redis::CommandError, + "ERR wrong number of arguments for 'xlen' command" + end with_stream_at(key) do |stream| return stream.count end end - def xrange(key, start, finish, *options) + def xrange(key = nil, start = nil, finish = nil, *options) + if finish.nil? + raise Redis::CommandError, + "ERR wrong number of arguments for 'xrange' command" + end with_stream_at(key) do |stream| return stream.range(start, finish, false, *options) end end - def xrevrange(key, finish, start, *options) + def xrevrange(key = nil, finish = nil, start = nil, *options) + if start.nil? + raise Redis::CommandError, + "ERR wrong number of arguments for 'xrevrange' command" + end with_stream_at(key) do |stream| return stream.range(start, finish, true, *options) end diff --git a/spec/commands/xlen_spec.rb b/spec/commands/xlen_spec.rb index 5125dafb..491039ae 100644 --- a/spec/commands/xlen_spec.rb +++ b/spec/commands/xlen_spec.rb @@ -10,4 +10,20 @@ 3.times { @redises.xadd(@key, '*', 'key', 'value') } expect(@redises.xlen(@key)).to eq 4 end + + it 'raises wrong number of arguments error with missing key' do + expect { @redises.xlen } + .to raise_error( + Redis::CommandError, + "ERR wrong number of arguments for 'xlen' command" + ) + end + + it 'raises wrong number of arguments error with extra arguments' do + expect { @redises.xlen(@key, 'xyz') } + .to raise_error( + Redis::CommandError, + "ERR wrong number of arguments for 'xlen' command" + ) + end end diff --git a/spec/commands/xrange_spec.rb b/spec/commands/xrange_spec.rb index 3a702e42..8b5020c6 100644 --- a/spec/commands/xrange_spec.rb +++ b/spec/commands/xrange_spec.rb @@ -130,4 +130,36 @@ ) end end + + it 'raises wrong number of arguments error' do + expect { @redises.xrange(@key, '-') } + .to raise_error( + Redis::CommandError, + "ERR wrong number of arguments for 'xrange' command" + ) + end + + it 'raises syntax error with missing count number' do + expect { @redises.xrange(@key, '-', '+', 'count') } + .to raise_error( + Redis::CommandError, + 'ERR syntax error' + ) + end + + it 'raises not an integer error with bad count argument' do + expect { @redises.xrange(@key, '-', '+', 'count', 'X') } + .to raise_error( + Redis::CommandError, + 'ERR value is not an integer or out of range' + ) + end + + it 'raises an invalid stream id error' do + expect { @redises.xrange(@key, 'X', '+') } + .to raise_error( + Redis::CommandError, + '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 b0288c09..f842d45d 100644 --- a/spec/commands/xrevrange_spec.rb +++ b/spec/commands/xrevrange_spec.rb @@ -120,4 +120,36 @@ ) end end + + it 'raises wrong number of arguments error' do + expect { @redises.xrevrange(@key, '+') } + .to raise_error( + Redis::CommandError, + "ERR wrong number of arguments for 'xrevrange' command" + ) + end + + it 'raises syntax error with missing count number' do + expect { @redises.xrevrange(@key, '+', '-', 'count') } + .to raise_error( + Redis::CommandError, + 'ERR syntax error' + ) + end + + it 'raises not an integer error with bad count argument' do + expect { @redises.xrevrange(@key, '+', '-', 'count', 'X') } + .to raise_error( + Redis::CommandError, + 'ERR value is not an integer or out of range' + ) + end + + it 'raises an invalid stream id error' do + expect { @redises.xrevrange(@key, 'X', '-') } + .to raise_error( + Redis::CommandError, + 'ERR Invalid stream ID specified as stream command argument' + ) + end end From 805d8b8be2725aa69af54089ea425aecc82a62f1 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 25 Jun 2018 14:40:09 +0100 Subject: [PATCH 23/28] Run Travis tests with Redis 5 server --- .travis.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6bd7f0b4..dd2e4c0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,14 @@ sudo: false cache: bundler -addons: - apt: - packages: - - redis-server +# Using RC 3 of Redis 5 (see below) +#addons: +# apt: +# packages: +# - redis-server -services: - - redis-server +#services: +# - redis-server rvm: - 2.0 @@ -22,6 +23,12 @@ before_script: - git config --local user.email "travis@travis.ci" - git config --local user.name "Travis CI" +# Install RC3 of Redis 5 +install: + - wget https://github.com/antirez/redis/archive/5.0-rc3.tar.gz -O /tmp/redis-5.0-rc3.tar.gz + - tar -xvf /tmp/redis-5.0-rc3.tar.gz + - pushd redis-5.0-rc3 && make && ./src/redis-server --daemonize yes && popd + script: - redis-cli --version - bundle exec rspec From e79b515aee38d90014bc50aa18945479512ef122 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 25 Jun 2018 14:47:48 +0100 Subject: [PATCH 24/28] Use redis-cli from installed Redis --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dd2e4c0c..7dcb26b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,9 @@ rvm: - 2.1 - 2.2 - 2.3.1 + - 2.3.7 + - 2.4.4 + - 2.5.1 before_script: - git config --local user.email "travis@travis.ci" @@ -27,10 +30,11 @@ before_script: install: - wget https://github.com/antirez/redis/archive/5.0-rc3.tar.gz -O /tmp/redis-5.0-rc3.tar.gz - tar -xvf /tmp/redis-5.0-rc3.tar.gz - - pushd redis-5.0-rc3 && make && ./src/redis-server --daemonize yes && popd + - pushd redis-5.0-rc3 && make && popd + - redis-5.0-rc3/src/redis-server --daemonize yes script: - - redis-cli --version + - redis-5.0-rc3/src/redis-cli --version - bundle exec rspec - bundle exec overcommit --sign - bundle exec overcommit --run From f490790f3127989e71f74ca84248c38386fcdcb0 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 25 Jun 2018 14:56:58 +0100 Subject: [PATCH 25/28] Extract install of Redis 5 into a separate script --- .travis.yml | 7 ++----- start-redis-five.sh | 7 +++++++ 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100755 start-redis-five.sh diff --git a/.travis.yml b/.travis.yml index 7dcb26b1..3d6a4197 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,11 +27,8 @@ before_script: - git config --local user.name "Travis CI" # Install RC3 of Redis 5 -install: - - wget https://github.com/antirez/redis/archive/5.0-rc3.tar.gz -O /tmp/redis-5.0-rc3.tar.gz - - tar -xvf /tmp/redis-5.0-rc3.tar.gz - - pushd redis-5.0-rc3 && make && popd - - redis-5.0-rc3/src/redis-server --daemonize yes +before_install: + - ./start-redis-five.sh script: - redis-5.0-rc3/src/redis-cli --version diff --git a/start-redis-five.sh b/start-redis-five.sh new file mode 100755 index 00000000..6c23f6a0 --- /dev/null +++ b/start-redis-five.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -ex + +wget https://github.com/antirez/redis/archive/5.0-rc3.tar.gz -O /tmp/redis-5.0-rc3.tar.gz +tar -xvf /tmp/redis-5.0-rc3.tar.gz +cd redis-5.0-rc3 && make +./src/redis-server --daemonize yes From eb5314babc9ffa58d5577a7bb66d66a5e60d8cae Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 25 Jun 2018 15:02:22 +0100 Subject: [PATCH 26/28] Remove pry (oops) --- spec/commands/xrevrange_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/commands/xrevrange_spec.rb b/spec/commands/xrevrange_spec.rb index f842d45d..eb23f0c8 100644 --- a/spec/commands/xrevrange_spec.rb +++ b/spec/commands/xrevrange_spec.rb @@ -49,7 +49,6 @@ end it 'returns entries with an upper limit' do - require 'pry' expect(@redises.xrevrange(@key, '1234567891285-0', '-')).to eq( [ ['1234567891278-1', %w[key5 value5]], From 2ca57b2abaef1ebde1b9fde6bc8677e01b372721 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 25 Jun 2018 15:07:25 +0100 Subject: [PATCH 27/28] Travis didn't like this having execute permissions --- start-redis-five.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 start-redis-five.sh diff --git a/start-redis-five.sh b/start-redis-five.sh old mode 100755 new mode 100644 From 3a0c84665720a89b544ece391ad806ffadba2226 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 25 Jun 2018 15:09:35 +0100 Subject: [PATCH 28/28] Run the redis 5 install without requiring execute permissions --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3d6a4197..f7660c11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ before_script: # Install RC3 of Redis 5 before_install: - - ./start-redis-five.sh + - sh start-redis-five.sh script: - redis-5.0-rc3/src/redis-cli --version