From 3cdcac64e8a9d9112fe5288c0413c0b92eb429f6 Mon Sep 17 00:00:00 2001 From: Denis K Date: Wed, 9 Oct 2024 23:06:13 +0200 Subject: [PATCH 1/4] 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") From 83d9cf4cc213da7a3a65a49f257c919dfc7bdaa3 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Thu, 10 Oct 2024 09:13:11 -0400 Subject: [PATCH 2/4] fix:flake8 --- fakeredis/_commands.py | 8 -------- fakeredis/commands_mixins/bitmap_mixin.py | 7 +++---- test/test_mixins/test_bitmap_commands.py | 1 - 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/fakeredis/_commands.py b/fakeredis/_commands.py index 46b6b262..aafbf176 100644 --- a/fakeredis/_commands.py +++ b/fakeredis/_commands.py @@ -519,14 +519,6 @@ 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 a49229f8..6f6c793c 100644 --- a/fakeredis/commands_mixins/bitmap_mixin.py +++ b/fakeredis/commands_mixins/bitmap_mixin.py @@ -10,7 +10,6 @@ BitValue, fix_range_string, fix_range, - positive_range, CommandItem, ) from fakeredis._helpers import SimpleError, casematch @@ -74,12 +73,12 @@ def bitpos(self, key: CommandItem, bit: int, *args: bytes) -> int: end = len(key_value) if len(args) <= 1 else Int.decode(args[1]) length = len(key_value) - start, end = positive_range(start, end, length) + start, end = fix_range(start, end, length) - if start > end or start >= length: + if start == end == -1: return -1 - value = value[start:end] if bit_mode else self._bytes_as_bin_string(key_value[start:end]) + 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) diff --git a/test/test_mixins/test_bitmap_commands.py b/test/test_mixins/test_bitmap_commands.py index 67d8a576..53f53461 100644 --- a/test/test_mixins/test_bitmap_commands.py +++ b/test/test_mixins/test_bitmap_commands.py @@ -212,7 +212,6 @@ def test_bitpos(r: redis.Redis): assert r.bitpos("nokey:bitpos", 1, 1) == -1 - @pytest.mark.min_server("7") def test_bitops_mode_redis7(r: redis.Redis): key = "key:bitpos" From 6e0eea97b9959c86559f318b1ee1c419b89fe69a Mon Sep 17 00:00:00 2001 From: Daniel M Date: Thu, 10 Oct 2024 09:23:06 -0400 Subject: [PATCH 3/4] fix:flake8 --- fakeredis/commands_mixins/bitmap_mixin.py | 28 +++++++++++------------ 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/fakeredis/commands_mixins/bitmap_mixin.py b/fakeredis/commands_mixins/bitmap_mixin.py index 6f6c793c..5f1bd971 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) @@ -55,37 +55,35 @@ def bitpos(self, key: CommandItem, bit: int, *args: bytes) -> int: 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: + 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]) bit_chr = str(bit) - key_value = key.value if key.value else b"" if bit_mode: - value = self._bytes_as_bin_string(key_value) + value = self._bytes_as_bin_string(key.value) end = len(value) if len(args) <= 1 else Int.decode(args[1]) length = len(value) + start, end = fix_range(start, end, length) + value = value[start:end] else: - end = len(key_value) if len(args) <= 1 else Int.decode(args[1]) - length = len(key_value) - - start, end = fix_range(start, end, length) + end = len(key.value) if len(args) <= 1 else Int.decode(args[1]) + length = len(key.value) + start, end = fix_range(start, end, length) + value = self._bytes_as_bin_string(key.value[start:end]) if start == end == -1: 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 + # 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) From 22f390a95af0c7da98f1c808434023013b016a18 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Thu, 10 Oct 2024 09:29:09 -0400 Subject: [PATCH 4/4] fix:flake8 --- fakeredis/commands_mixins/bitmap_mixin.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/fakeredis/commands_mixins/bitmap_mixin.py b/fakeredis/commands_mixins/bitmap_mixin.py index 5f1bd971..4e62903f 100644 --- a/fakeredis/commands_mixins/bitmap_mixin.py +++ b/fakeredis/commands_mixins/bitmap_mixin.py @@ -60,24 +60,15 @@ def bitpos(self, key: CommandItem, bit: int, *args: bytes) -> int: return -1 if bit == 1 else 0 start = 0 if len(args) == 0 else Int.decode(args[0]) - bit_chr = str(bit) - - if bit_mode: - value = self._bytes_as_bin_string(key.value) - end = len(value) if len(args) <= 1 else Int.decode(args[1]) - length = len(value) - start, end = fix_range(start, end, length) - value = value[start:end] - else: - end = len(key.value) if len(args) <= 1 else Int.decode(args[1]) - length = len(key.value) - start, end = fix_range(start, end, length) - value = self._bytes_as_bin_string(key.value[start:end]) - + 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: