Skip to content

Commit

Permalink
Merge 9172991 into 64bf1d6
Browse files Browse the repository at this point in the history
  • Loading branch information
brettjnorris committed Apr 30, 2018
2 parents 64bf1d6 + 9172991 commit 8465e2d
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -105,3 +105,4 @@ required. If you're using a different version of Redis, you may see
failures due to error message text being different. If you're running
a really old version of Redis, you'll definitely see failures due to
stuff that doesn't work!

109 changes: 109 additions & 0 deletions lib/mock_redis/string_methods.rb
Expand Up @@ -3,6 +3,7 @@
class MockRedis
module StringMethods
include Assertions
include UtilityMethods

def append(key, value)
assert_stringy(key)
Expand All @@ -11,6 +12,100 @@ def append(key, value)
data[key].length
end

def bitfield(*args)
if args.length < 4
raise Redis::CommandError, 'ERR wrong number of arguments for BITFIELD'
end

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[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 "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

set_value(key, new_val, is_signed, type_size, offset) if new_val
output.push(new_val)
end
end

output
end

def decr(key)
decrby(key, 1)
end
Expand Down Expand Up @@ -285,5 +380,19 @@ def assert_stringy(key,
raise Redis::CommandError, message
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
33 changes: 33 additions & 0 deletions lib/mock_redis/utility_methods.rb
Expand Up @@ -38,5 +38,38 @@ def common_scan(values, cursor, opts = {})

[next_cursor, filtered_values]
end

def twos_complement_encode(n, size)
if n < 0
str = (n + 1).abs.to_s(2)

binary = left_pad(str, size - 1).chars.map { |c| c == "0" ? 1 : 0 }
binary.unshift(1)
else
binary = left_pad(n.abs.to_s(2), size - 1).chars.map(&:to_i)
binary.unshift(0)
end

binary
end

def twos_complement_decode(array)
total = 0

array.each.with_index do |bit, index|
total += 2 ** (array.length - index - 1) if bit == 1
total = -total if index == 0
end

total
end

def left_pad(str, size)
while str.length < size do
str = "0" + str
end

str
end
end
end
162 changes: 162 additions & 0 deletions spec/commands/bitfield_spec.rb
@@ -0,0 +1,162 @@
require 'spec_helper'

describe '#bitfield(*args)' do
before :each do
@key = "mock-redis-test:bitfield"
@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
it "gets a signed 8 bit value" do
@redises.bitfield(@key, :get, "i8", 0).should == [78]
@redises.bitfield(@key, :get, "i8", 8).should == [104]
@redises.bitfield(@key, :get, "i8", 16).should == [-59]
end

it "gets multiple values with multiple command args" do
@redises.bitfield(@key, :get, "i8", 0,
:get, "i8", 8,
:get, "i8", 16).should == [78, 104, -59]
end

it "gets multiple values using positional offsets" do
@redises.bitfield(@key, :get, "i8", "#0",
: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
it "sets the bit values for an 8 bit signed integer" do
@redises.bitfield(@key, :set, "i8", 0, 63).should == [78]
@redises.bitfield(@key, :set, "i8", 8, -1).should == [104]
@redises.bitfield(@key, :set, "i8", 16, 123).should == [-59]

@redises.bitfield(@key, :get, "i8", 0,
:get, "i8", 8,
:get, "i8", 16).should == [63, -1, 123]

end

it "sets multiple values with multiple command args" do
@redises.bitfield(@key, :set, "i8", 0, 63,
:set, "i8", 8, -1,
:set, "i8", 16, 123).should == [78, 104, -59]

@redises.bitfield(@key, :get, "i8", 0,
:get, "i8", 8,
:get, "i8", 16).should == [63, -1, 123]
end
end

context "with an :incrby command" do
it "returns the incremented by value for an 8 bit signed integer" do
@redises.bitfield(@key, :incrby, "i8", 0, 1).should == [79]
@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
it "returns the correct outputs" do
@redises.bitfield(@key, :set, "i8", 0, 38,
:set, "i8", 8, -99,
:incrby, "i8", 16, 1,
:get, "i8", 0).should == [78, 104, -58, 38]
end
end
end

0 comments on commit 8465e2d

Please sign in to comment.