From 700ec6d639e182aba14e24f0cea1f7d2313f8401 Mon Sep 17 00:00:00 2001 From: Brett Norris Date: Fri, 27 Apr 2018 14:10:03 -0400 Subject: [PATCH 1/2] Add support for bitfield overflow command --- lib/mock_redis/string_methods.rb | 99 +++++++++++++++++++++++--------- spec/commands/bitfield_spec.rb | 90 ++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 29 deletions(-) diff --git a/lib/mock_redis/string_methods.rb b/lib/mock_redis/string_methods.rb index 61c8311f..ce5b421b 100644 --- a/lib/mock_redis/string_methods.rb +++ b/lib/mock_redis/string_methods.rb @@ -19,48 +19,82 @@ def bitfield(*args) key = args.shift output = [] + overflow_method = "wrap" while args.length > 0 do command = args.shift.to_s - type, offset = args.shift(2) - is_signed, type_size = type.slice!(0) == "i", type.to_i + unless command == "overflow" + type, offset = args.shift(2) - if offset.to_s[0] == "#" - offset = offset[1..-1].to_i * type_size - end + is_signed, type_size = type.slice(0) == "i", type[1..-1].to_i - bits = [] + if offset.to_s[0] == "#" + offset = offset[1..-1].to_i * type_size + end - type_size.times do |i| - bits.push(getbit(key, offset + i)) - end + bits = [] - if is_signed - val = twos_complement_decode(bits) - else - val = bits.join("").to_i(2) - end + type_size.times do |i| + bits.push(getbit(key, offset + i)) + end - output.push(val) unless command == "incrby" + if is_signed + val = twos_complement_decode(bits) + else + val = bits.join("").to_i(2) + end + end case command - when "incrby", "set" - new_val = args.shift.to_i - new_val += val if command == "incrby" + when "get" + output.push(val) + when "overflow" + new_overflow_method = args.shift.to_s.downcase - if is_signed - val_array = twos_complement_encode(new_val, type_size) - else - str = left_pad(new_val.to_i.abs.to_s(2), type_size) - val_array = str.split('').map(&:to_i) + unless ["wrap", "sat", "fail"].include? new_overflow_method + raise Redis::CommandError, 'ERR Invalid OVERFLOW type specified' end - val_array.each_with_index do |bit, i| - setbit(key, offset + i, bit) + overflow_method = new_overflow_method + when "incrby" + new_val = val + args.shift.to_i + + max = is_signed ? (2 ** (type_size - 1)) - 1 : (2 ** type_size) - 1 + min = is_signed ? (-2 ** (type_size - 1)) : 0 + size = 2 ** type_size + + unless (min..max).include?(new_val) + case overflow_method + when "fail" + new_val = nil + when "sat" + new_val = new_val > max ? max : min + when "wrap" + if is_signed + if new_val > max + remainder = new_val - (max + 1) + new_val = min + remainder.abs + else + remainder = new_val - (min - 1) + new_val = max - remainder.abs + end + else + if new_val > max + new_val = new_val % size + else + new_val = size - new_val.abs + end + end + end end - output.push(new_val) if command == "incrby" + set_value(key, new_val, is_signed, type_size, offset) if new_val + output.push(new_val) + when "set" + output.push(val) + + set_value(key, args.shift.to_i, is_signed, type_size, offset) end end @@ -342,5 +376,18 @@ def assert_stringy(key, end end + def set_value(key, value, is_signed, type_size, offset) + if is_signed + val_array = twos_complement_encode(value, type_size) + else + str = left_pad(value.to_i.abs.to_s(2), type_size) + val_array = str.split('').map(&:to_i) + end + + val_array.each_with_index do |bit, i| + setbit(key, offset + i, bit) + end + end + end end diff --git a/spec/commands/bitfield_spec.rb b/spec/commands/bitfield_spec.rb index f7631bc9..4ef22546 100644 --- a/spec/commands/bitfield_spec.rb +++ b/spec/commands/bitfield_spec.rb @@ -1,10 +1,15 @@ require 'spec_helper' describe '#bitfield(*args)' do - before do + before :each do @key = "mock-redis-test:bitfield" - @str = [78, 104, -59].pack("C*") - @redises.set(@key, @str) + @redises.set(@key, "") + + @redises.bitfield(@key, :set, "i8", 0, 78) + @redises.bitfield(@key, :set, "i8", 8, 104) + @redises.bitfield(@key, :set, "i8", 16, -59) + @redises.bitfield(@key, :set, "u8", 24, 78) + @redises.bitfield(@key, :set, "u8", 32, 84) end context "with a :get command" do @@ -56,6 +61,85 @@ @redises.bitfield(@key, :incrby, "i8", 8, -1).should == [103] @redises.bitfield(@key, :incrby, "i8", 16, 5).should == [-54] end + + context "with an overflow of wrap (default)" do + context "for a signed integer" do + it "wraps the overflow to the minimum and increments from there" do + @redises.bitfield(@key, :get, "i8", 24).should == [78] + @redises.bitfield(@key, :overflow, :wrap, + :incrby, "i8", 0, 200).should == [22] + end + + it "wraps the underflow to the maximum value and decrements from there" do + @redises.bitfield(@key, :overflow, :wrap, + :incrby, "i8", 16, -200).should == [-3] + end + end + + context "for an unsigned integer" do + it "wraps the overflow back to zero and increments from there" do + @redises.bitfield(@key, :get, "u8", 24).should == [78] + @redises.bitfield(@key, :overflow, :wrap, + :incrby, "u8", 24, 233).should == [55] + end + + it "wraps the underflow to the maximum value and decrements from there" do + @redises.bitfield(@key, :get, "u8", 32).should == [84] + @redises.bitfield(@key, :overflow, :wrap, + :incrby, "u8", 32, -233).should == [107] + end + end + end + + context "with an overflow of sat" do + it "sets the overflowed value to the maximum" do + @redises.bitfield(@key, :overflow, :sat, + :incrby, "i8", 0, 256).should == [127] + end + + it "sets the underflowed value to the minimum" do + @redises.bitfield(@key, :overflow, :sat, + :incrby, "i8", 16, -256).should == [-128] + end + end + + context "with an overflow of fail" do + it "raises a redis error on an out of range value" do + @redises.bitfield(@key, :overflow, :fail, + :incrby, "i8", 0, 256).should == [nil] + + @redises.bitfield(@key, :overflow, :fail, + :incrby, "i8", 16, -256).should == [nil] + end + + it "retains the original value after a failed increment" do + @redises.bitfield(@key, :get, "i8", 0).should == [78] + @redises.bitfield(@key, :overflow, :fail, + :incrby, "i8", 0, 256).should == [nil] + @redises.bitfield(@key, :get, "i8", 0).should == [78] + end + end + + context "with multiple overflow commands in one transaction" do + it "handles the overflow values correctly" do + @redises.bitfield(@key, :overflow, :sat, + :incrby, "i8", 0, 256, + :incrby, "i8", 8, -256, + :overflow, :wrap, + :incrby, "i8", 0, 200, + :incrby, "i8", 16, -200, + :overflow, :fail, + :incrby, "i8", 0, 256, + :incrby, "i8", 16, -256).should == [127, -128, 71, -3, nil, nil] + end + end + + context "with an unsupported overflow value" do + it "raises an error" do + expect { @redises.bitfield(@key, :overflow, :foo, + :incrby, "i8", 0, 256) }.to raise_error(Redis::CommandError) + end + end end context "with a mixed set of commands" do From 84305ce7ff930f0460fcfe5b0021f7316e73ba08 Mon Sep 17 00:00:00 2001 From: Brett Norris Date: Mon, 30 Apr 2018 12:27:43 -0400 Subject: [PATCH 2/2] Add bitfield type size validation --- lib/mock_redis/string_methods.rb | 57 +++++++++++++++++--------------- spec/commands/bitfield_spec.rb | 9 +++++ 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/lib/mock_redis/string_methods.rb b/lib/mock_redis/string_methods.rb index ce5b421b..c8134314 100644 --- a/lib/mock_redis/string_methods.rb +++ b/lib/mock_redis/string_methods.rb @@ -24,39 +24,48 @@ def bitfield(*args) while args.length > 0 do command = args.shift.to_s - unless command == "overflow" - type, offset = args.shift(2) - - is_signed, type_size = type.slice(0) == "i", type[1..-1].to_i + if command == "overflow" + new_overflow_method = args.shift.to_s.downcase - if offset.to_s[0] == "#" - offset = offset[1..-1].to_i * type_size + unless ["wrap", "sat", "fail"].include? new_overflow_method + raise Redis::CommandError, 'ERR Invalid OVERFLOW type specified' end - bits = [] + overflow_method = new_overflow_method + next + end - type_size.times do |i| - bits.push(getbit(key, offset + i)) - end + type, offset = args.shift(2) - if is_signed - val = twos_complement_decode(bits) - else - val = bits.join("").to_i(2) - end + is_signed, type_size = type.slice(0) == "i", type[1..-1].to_i + + if (type_size > 64 && is_signed) || (type_size >= 64 && !is_signed) + raise Redis::CommandError, "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is." + end + + if offset.to_s[0] == "#" + offset = offset[1..-1].to_i * type_size + end + + bits = [] + + type_size.times do |i| + bits.push(getbit(key, offset + i)) + end + + if is_signed + val = twos_complement_decode(bits) + else + val = bits.join("").to_i(2) end case command when "get" output.push(val) - when "overflow" - new_overflow_method = args.shift.to_s.downcase - - unless ["wrap", "sat", "fail"].include? new_overflow_method - raise Redis::CommandError, 'ERR Invalid OVERFLOW type specified' - end + when "set" + output.push(val) - overflow_method = new_overflow_method + set_value(key, args.shift.to_i, is_signed, type_size, offset) when "incrby" new_val = val + args.shift.to_i @@ -91,10 +100,6 @@ def bitfield(*args) set_value(key, new_val, is_signed, type_size, offset) if new_val output.push(new_val) - when "set" - output.push(val) - - set_value(key, args.shift.to_i, is_signed, type_size, offset) end end diff --git a/spec/commands/bitfield_spec.rb b/spec/commands/bitfield_spec.rb index 4ef22546..e33dda29 100644 --- a/spec/commands/bitfield_spec.rb +++ b/spec/commands/bitfield_spec.rb @@ -30,6 +30,15 @@ :get, "i8", "#1", :get, "i8", "#2").should == [78, 104, -59] end + + it "shows an error with an invalid type" do + expect { @redises.bitfield(@key, :get, "u64", 0) }.to raise_error + expect { @redises.bitfield(@key, :get, "i128", 0) }.to raise_error(Redis::CommandError) + end + + it "returns a value with an i64 type" do + expect { @redises.bitfield(@key, :get, "i64", 0) }.to_not raise_error(Redis::CommandError) + end end context "with a :set command" do