From 858bdd5a4905452a9f3a05d80a3b2a9a2dd36d67 Mon Sep 17 00:00:00 2001 From: Diskein Date: Thu, 10 Oct 2024 15:31:16 +0200 Subject: [PATCH] fix:bug in bitpos function for the clear bit mode (#337) Co-authored-by: Daniel M --- fakeredis/commands_mixins/bitmap_mixin.py | 32 +++++++++++++---------- test/test_mixins/test_bitmap_commands.py | 12 +++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/fakeredis/commands_mixins/bitmap_mixin.py b/fakeredis/commands_mixins/bitmap_mixin.py index bed7d973..4e62903f 100644 --- a/fakeredis/commands_mixins/bitmap_mixin.py +++ b/fakeredis/commands_mixins/bitmap_mixin.py @@ -41,7 +41,7 @@ def __init(self, *args: Any, **kwargs: Any) -> None: def _bytes_as_bin_string(value: bytes) -> str: return "".join([bin(i).lstrip("0b").rjust(8, "0") for i in value]) - @command((Key(bytes), Int), (bytes,)) + @command((Key(bytes), Int), (bytes,), flags=msgs.FLAG_DO_NOT_CREATE) def bitpos(self, key: CommandItem, bit: int, *args: bytes) -> int: if bit != 0 and bit != 1: raise SimpleError(msgs.BIT_ARG_MUST_BE_ZERO_OR_ONE) @@ -54,23 +54,27 @@ def bitpos(self, key: CommandItem, bit: int, *args: bytes) -> int: bit_mode = casematch(args[2], b"bit") if not bit_mode and not casematch(args[2], b"byte"): raise SimpleError(msgs.SYNTAX_ERROR_MSG) - start = 0 if len(args) == 0 else Int.decode(args[0]) - bit_chr = str(bit) - key_value = key.value if key.value else b"" - if bit_mode: - value = self._bytes_as_bin_string(key_value) - end = len(value) if len(args) <= 1 else Int.decode(args[1]) - start, end = fix_range(start, end, len(value)) - value = value[start:end] - else: - end = len(key_value) if len(args) <= 1 else Int.decode(args[1]) - start, end = fix_range(start, end, len(key_value)) - value = self._bytes_as_bin_string(key_value[start:end]) + if key.value is None: + # The first clear bit is at 0, the first set bit is not found (-1). + return -1 if bit == 1 else 0 + + start = 0 if len(args) == 0 else Int.decode(args[0]) + source_value = self._bytes_as_bin_string(key.value) if bit_mode else key.value + end = len(source_value) if len(args) <= 1 else Int.decode(args[1]) + length = len(source_value) + start, end = fix_range(start, end, length) + if start == end == -1: + return -1 + source_value = source_value[start:end] if bit_mode else self._bytes_as_bin_string(source_value[start:end]) - result = value.find(bit_chr) + result = source_value.find(str(bit)) if result != -1: result += start if bit_mode else (start * 8) + elif bit == 0 and len(args) <= 1: + # Redis treats the value as padded with zero bytes to an infinity + # if the user is looking for the first clear bit and no end is set. + result = len(key.value) * 8 return result @command(name="BITCOUNT", fixed=(Key(bytes),), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) diff --git a/test/test_mixins/test_bitmap_commands.py b/test/test_mixins/test_bitmap_commands.py index e0b5457a..53f53461 100644 --- a/test/test_mixins/test_bitmap_commands.py +++ b/test/test_mixins/test_bitmap_commands.py @@ -197,7 +197,19 @@ def test_bitpos(r: redis.Redis): assert r.bitpos(key, 1, 1) == 8 r.set(key, b"\x00\x00\x00") assert r.bitpos(key, 1) == -1 + r.set(key, b"\xff\xff\xff") + assert r.bitpos(key, 1) == 0 + assert r.bitpos(key, 1, 1) == 8 + assert r.bitpos(key, 1, 3) == -1 + assert r.bitpos(key, 0) == 24 + assert r.bitpos(key, 0, 1) == 24 + assert r.bitpos(key, 0, 3) == -1 + assert r.bitpos(key, 0, 0, -1) == -1 r.set(key, b"\xff\xf0\x00") + assert r.bitpos("nokey:bitpos", 0) == 0 + assert r.bitpos("nokey:bitpos", 1) == -1 + assert r.bitpos("nokey:bitpos", 0, 1) == 0 + assert r.bitpos("nokey:bitpos", 1, 1) == -1 @pytest.mark.min_server("7")