From 3cdcac64e8a9d9112fe5288c0413c0b92eb429f6 Mon Sep 17 00:00:00 2001 From: Denis K Date: Wed, 9 Oct 2024 23:06:13 +0200 Subject: [PATCH] fix:bug in bitpos function for the clear bit mode - fakeredis returns the number of bits instead of -1 if it doesn't find 0. - It returns 0 if the key doesn't exist. --- fakeredis/_commands.py | 8 ++++++++ fakeredis/commands_mixins/bitmap_mixin.py | 24 +++++++++++++++++++---- test/test_mixins/test_bitmap_commands.py | 13 ++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/fakeredis/_commands.py b/fakeredis/_commands.py index aafbf176..46b6b262 100644 --- a/fakeredis/_commands.py +++ b/fakeredis/_commands.py @@ -519,6 +519,14 @@ def delete_keys(*keys: CommandItem) -> int: ans += 1 return ans +def positive_range(start: int, end: int, length: int) -> Tuple[int, int]: + # Redis handles negative slightly differently for zrange + if start < 0: + start = max(0, start + length) + if end < 0: + end += length + end = min(end, length - 1) + return start, end + 1 def fix_range(start: int, end: int, length: int) -> Tuple[int, int]: # Redis handles negative slightly differently for zrange diff --git a/fakeredis/commands_mixins/bitmap_mixin.py b/fakeredis/commands_mixins/bitmap_mixin.py index bed7d973..a49229f8 100644 --- a/fakeredis/commands_mixins/bitmap_mixin.py +++ b/fakeredis/commands_mixins/bitmap_mixin.py @@ -10,6 +10,7 @@ BitValue, fix_range_string, fix_range, + positive_range, CommandItem, ) from fakeredis._helpers import SimpleError, casematch @@ -54,6 +55,13 @@ 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) + + # Redis treats non-existent key as an infinite array of 0 bits. + # If the user is looking for the first clear bit return 0, + # If the user is looking for the first set bit, return -1. + if not key.value: + return -1 if bit == 1 else 0 + start = 0 if len(args) == 0 else Int.decode(args[0]) bit_chr = str(bit) key_value = key.value if key.value else b"" @@ -61,16 +69,24 @@ def bitpos(self, key: CommandItem, bit: int, *args: bytes) -> int: 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] + length = len(value) 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]) + length = len(key_value) + + start, end = positive_range(start, end, length) + + if start > end or start >= length: + return -1 + value = value[start:end] if bit_mode else self._bytes_as_bin_string(key_value[start:end]) result = value.find(bit_chr) if result != -1: result += start if bit_mode else (start * 8) + # 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. + elif bit == 0 and len(args) <= 1: + 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..67d8a576 100644 --- a/test/test_mixins/test_bitmap_commands.py +++ b/test/test_mixins/test_bitmap_commands.py @@ -197,7 +197,20 @@ 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")