Skip to content

Commit

Permalink
Merge pull request #2 from ParkmobileUSA/add_support_for_bitfield_com…
Browse files Browse the repository at this point in the history
…mand

Add support for bitfield overflow command
  • Loading branch information
brettjnorris committed Apr 30, 2018
2 parents fc430cb + 84305ce commit 9172991
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 20 deletions.
86 changes: 69 additions & 17 deletions lib/mock_redis/string_methods.rb
Expand Up @@ -19,12 +19,29 @@ def bitfield(*args)

key = args.shift
output = []
overflow_method = "wrap"

while args.length > 0 do
command = args.shift.to_s

if command == "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

overflow_method = new_overflow_method
next
end

type, offset = args.shift(2)

is_signed, type_size = type.slice!(0) == "i", type.to_i
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
Expand All @@ -42,25 +59,47 @@ def bitfield(*args)
val = bits.join("").to_i(2)
end

output.push(val) unless command == "incrby"

case command
when "incrby", "set"
new_val = args.shift.to_i
new_val += val if command == "incrby"

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)
when "get"
output.push(val)
when "set"
output.push(val)

set_value(key, args.shift.to_i, is_signed, type_size, offset)
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

val_array.each_with_index do |bit, i|
setbit(key, offset + i, bit)
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)
end
end

Expand Down Expand Up @@ -342,5 +381,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
99 changes: 96 additions & 3 deletions 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
Expand All @@ -25,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
Expand Down Expand Up @@ -56,6 +70,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
Expand Down

0 comments on commit 9172991

Please sign in to comment.