From ef15de09c3e82e5146770d125fa6c318a4be264b Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 13:52:30 +0100 Subject: [PATCH 01/15] Add failing test --- .../tests/quality/test_float_shrinking.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/tests/quality/test_float_shrinking.py b/hypothesis-python/tests/quality/test_float_shrinking.py index 415746f9ff..16aebdcfc9 100644 --- a/hypothesis-python/tests/quality/test_float_shrinking.py +++ b/hypothesis-python/tests/quality/test_float_shrinking.py @@ -10,7 +10,7 @@ import pytest -from hypothesis import example, given, strategies as st +from hypothesis import example, given, seed, strategies as st from hypothesis.internal.compat import ceil from tests.common.debug import minimal @@ -45,3 +45,16 @@ def test_shrinks_downwards_to_integers_when_fractional(b): ).filter(lambda x: int(x) != x) ) assert g == b + 0.5 + + +@pytest.mark.parametrize("s", range(10)) +def test_shrinks_to_canonical_nan(s): + # Regression test for #4277. A more reliable and minimal example could probably be found. + @given(st.lists(st.just(0)|st.floats().filter(lambda a: a != a), min_size=2, max_size=2)) + @seed(s) + def sort_is_reversible(l): + assert sorted(l, reverse=True) == list(reversed(sorted(l))) + try: + sort_is_reversible() + except AssertionError as e: + assert "[0, nan]" in e.__notes__[0] From 7882f23ba422c4edc9aa8e30d6fa678885a29757 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 13:23:23 +0100 Subject: [PATCH 02/15] Prefer nan to -nan --- hypothesis-python/src/hypothesis/internal/conjecture/choice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/choice.py b/hypothesis-python/src/hypothesis/internal/conjecture/choice.py index 16db7ca8f2..fe52e1e5fc 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/choice.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/choice.py @@ -417,7 +417,7 @@ def choice_to_index(choice: ChoiceT, kwargs: ChoiceKwargsT) -> int: to_order=intervals.index_from_char_in_shrink_order, ) elif isinstance(choice, float): - sign = int(sign_aware_lte(choice, -0.0)) + sign = int(math.copysign(1.0, choice) < 0) return (sign << 64) | float_to_lex(abs(choice)) else: raise NotImplementedError From 64b3511499290ab20761a550dc8387ef72fcb608 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 13:34:54 +0100 Subject: [PATCH 03/15] Shrink non-standard NaNs to math.nan --- .../internal/conjecture/shrinking/common.py | 43 +++++++++++-------- .../internal/conjecture/shrinking/floats.py | 15 +++---- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py index b0c5ec8694..be8faa77ff 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py @@ -107,39 +107,46 @@ def run(self): self.run_step() self.debug("COMPLETE") - def incorporate(self, value): + def consider(self, value): """Try using ``value`` as a possible candidate improvement. - Return True if it works. + Return True if self.current is canonically equal to self.current after the call. """ value = self.make_immutable(value) + if value == self.current: + # shortcut for the simple (non-canonical) equality case, not required for correctness + return True + self.debug(f"considering {value!r}") + canonical = self.make_canonical(value) + if canonical in self.__seen: + return canonical == self.make_canonical(self.current) + self.__seen.add(canonical) self.check_invariants(value) if not self.left_is_better(value, self.current): - if value != self.current and (value == value): - self.debug(f"Rejected {value!r} as worse than {self.current=}") - return False - if value in self.__seen: + self.debug(f"Rejected {value!r} as no better than {self.current=}") return False - self.__seen.add(value) if self.__predicate(value): self.debug(f"shrinking to {value!r}") self.changes += 1 self.current = value return True - return False + else: + self.debug(f"Rejected {value!r} not satisfying predicate") + return False - def consider(self, value): - """Returns True if make_immutable(value) == self.current after calling - self.incorporate(value).""" - self.debug(f"considering {value}") - value = self.make_immutable(value) - if value == self.current: - return True - return self.incorporate(value) + def make_canonical(self, value): + """Convert immutable value into a canonical and hashable, but not necessarily equal, + representation of itself. + + This representation is used only for tracking already-seen values, not passed to the + shrinker. + + Defaults to just returning the (immutable) input value. + """ + return value def make_immutable(self, value): - """Convert value into an immutable (and hashable) representation of - itself. + """Convert value into an immutable representation of itself. It is these immutable versions that the shrinker will work on. diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py index e2f52a281b..f61c1dab66 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py @@ -14,24 +14,23 @@ from hypothesis.internal.conjecture.floats import float_to_lex from hypothesis.internal.conjecture.shrinking.common import Shrinker from hypothesis.internal.conjecture.shrinking.integer import Integer -from hypothesis.internal.floats import MAX_PRECISE_INTEGER +from hypothesis.internal.floats import MAX_PRECISE_INTEGER, float_to_int class Float(Shrinker): def setup(self): - self.NAN = math.nan self.debugging_enabled = True - def make_immutable(self, f): - f = float(f) + def make_canonical(self, f): if math.isnan(f): - # Always use the same NAN so it works properly in self.seen - f = self.NAN + # Distinguish different NaN bit patterns, while making each equal to itself + # To avoid accidental collision w/ a valid (large) float, convert to str. + return hex(float_to_int(f)) return f def check_invariants(self, value): - # We only handle positive floats because we encode the sign separately - # anyway. + # We only handle positive floats (including NaN) because we encode the sign + # separately anyway. assert not (value < 0) def left_is_better(self, left, right): From 2990e24e1ceb189ca111ea984bbd8b7d9b4bca9d Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 13:54:14 +0100 Subject: [PATCH 04/15] RELEASE.rst --- hypothesis-python/RELEASE.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..9c0a8cac20 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +Improve shrinking of non-standard NaN float values (:issue:`4277`). From 3168a18ca17b5341f5b912c77ea32af6f781cdcb Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 13:58:12 +0100 Subject: [PATCH 05/15] Formatting --- hypothesis-python/tests/quality/test_float_shrinking.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/tests/quality/test_float_shrinking.py b/hypothesis-python/tests/quality/test_float_shrinking.py index 16aebdcfc9..7ee874848b 100644 --- a/hypothesis-python/tests/quality/test_float_shrinking.py +++ b/hypothesis-python/tests/quality/test_float_shrinking.py @@ -50,10 +50,17 @@ def test_shrinks_downwards_to_integers_when_fractional(b): @pytest.mark.parametrize("s", range(10)) def test_shrinks_to_canonical_nan(s): # Regression test for #4277. A more reliable and minimal example could probably be found. - @given(st.lists(st.just(0)|st.floats().filter(lambda a: a != a), min_size=2, max_size=2)) + @given( + st.lists( + st.just(0) | st.floats().filter(lambda a: a != a), min_size=2, max_size=2 + ) + ) @seed(s) def sort_is_reversible(l): + # fmt: off assert sorted(l, reverse=True) == list(reversed(sorted(l))) + # fmt: on + try: sort_is_reversible() except AssertionError as e: From 71fa661802e1a065db631d785204bc56ae4d3ad4 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 15:01:09 +0100 Subject: [PATCH 06/15] lint fix --- hypothesis-python/tests/quality/test_float_shrinking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/tests/quality/test_float_shrinking.py b/hypothesis-python/tests/quality/test_float_shrinking.py index 7ee874848b..ed09d09a3e 100644 --- a/hypothesis-python/tests/quality/test_float_shrinking.py +++ b/hypothesis-python/tests/quality/test_float_shrinking.py @@ -58,7 +58,7 @@ def test_shrinks_to_canonical_nan(s): @seed(s) def sort_is_reversible(l): # fmt: off - assert sorted(l, reverse=True) == list(reversed(sorted(l))) + assert sorted(l, reverse=True) == list(reversed(sorted(l))) # noqa: C413 # fmt: on try: From d9fff46fdd92b596e3f35e5960062c3d12695dcf Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 15:08:35 +0100 Subject: [PATCH 07/15] Fix typo --- .../src/hypothesis/internal/conjecture/shrinking/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py index be8faa77ff..89a2991db9 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py @@ -110,7 +110,8 @@ def run(self): def consider(self, value): """Try using ``value`` as a possible candidate improvement. - Return True if self.current is canonically equal to self.current after the call. + Return True if self.current is canonically equal to value after the call, either because + the value was incorporated as an improvement or because it had that value already. """ value = self.make_immutable(value) if value == self.current: From 1db01534f0de9b86db89c0bf55de6a929020edff Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 15:45:57 +0100 Subject: [PATCH 08/15] Add initial value to the initial seen set --- .../src/hypothesis/internal/conjecture/shrinking/common.py | 2 +- hypothesis-python/tests/cover/test_shrink_budgeting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py index 89a2991db9..28f84329a2 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py @@ -34,7 +34,7 @@ def __init__( self.name = name self.__predicate = predicate - self.__seen = set() + self.__seen = {self.make_canonical(self.current)} self.debugging_enabled = debug @property diff --git a/hypothesis-python/tests/cover/test_shrink_budgeting.py b/hypothesis-python/tests/cover/test_shrink_budgeting.py index 6abdca4132..5664cccaf1 100644 --- a/hypothesis-python/tests/cover/test_shrink_budgeting.py +++ b/hypothesis-python/tests/cover/test_shrink_budgeting.py @@ -20,7 +20,7 @@ [ (Integer, 2**16), (Integer, int(sys.float_info.max)), - (Ordering, [[100] * 10]), + (Ordering, [(100,) * 10]), (Ordering, [i * 100 for i in (range(5))]), (Ordering, [i * 100 for i in reversed(range(5))]), ], From ffe267c6ecca3ff8079c5552084ce2ddb99a095f Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 16:13:00 +0100 Subject: [PATCH 09/15] Make return value of consider() more consistent --- .../src/hypothesis/internal/conjecture/shrinking/common.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py index 28f84329a2..8290ec6737 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py @@ -114,13 +114,12 @@ def consider(self, value): the value was incorporated as an improvement or because it had that value already. """ value = self.make_immutable(value) - if value == self.current: - # shortcut for the simple (non-canonical) equality case, not required for correctness - return True self.debug(f"considering {value!r}") canonical = self.make_canonical(value) + if canonical == self.make_canonical(self.current): + return True if canonical in self.__seen: - return canonical == self.make_canonical(self.current) + return False self.__seen.add(canonical) self.check_invariants(value) if not self.left_is_better(value, self.current): From 60adfc081794ec88d499155da5cfd0091db8f9f0 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 16:49:42 +0100 Subject: [PATCH 10/15] Faster nan canonicalization --- .../hypothesis/internal/conjecture/shrinking/floats.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py index f61c1dab66..2b61ce3903 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py @@ -9,12 +9,13 @@ # obtain one at https://mozilla.org/MPL/2.0/. import math +import struct import sys from hypothesis.internal.conjecture.floats import float_to_lex from hypothesis.internal.conjecture.shrinking.common import Shrinker from hypothesis.internal.conjecture.shrinking.integer import Integer -from hypothesis.internal.floats import MAX_PRECISE_INTEGER, float_to_int +from hypothesis.internal.floats import MAX_PRECISE_INTEGER class Float(Shrinker): @@ -23,9 +24,10 @@ def setup(self): def make_canonical(self, f): if math.isnan(f): - # Distinguish different NaN bit patterns, while making each equal to itself - # To avoid accidental collision w/ a valid (large) float, convert to str. - return hex(float_to_int(f)) + # Distinguish different NaN bit patterns, while making each equal to itself. + # Returning bytes instead of integer (float_to_int) avoids accidental + # equality with valid large floats. + return struct.pack("d", f) return f def check_invariants(self, value): From 7ef350fa82dfc140c348d52d9bedd72bb66c9cd9 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 17:37:36 +0100 Subject: [PATCH 11/15] Fix test --- .../tests/quality/test_float_shrinking.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/hypothesis-python/tests/quality/test_float_shrinking.py b/hypothesis-python/tests/quality/test_float_shrinking.py index ed09d09a3e..cb2c938eac 100644 --- a/hypothesis-python/tests/quality/test_float_shrinking.py +++ b/hypothesis-python/tests/quality/test_float_shrinking.py @@ -8,12 +8,17 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. +import math +import struct + import pytest from hypothesis import example, given, seed, strategies as st from hypothesis.internal.compat import ceil +from hypothesis.internal.conjecture.data import ConjectureData from tests.common.debug import minimal +from tests.conjecture.common import shrinking_from def test_shrinks_to_simple_floats(): @@ -47,21 +52,20 @@ def test_shrinks_downwards_to_integers_when_fractional(b): assert g == b + 0.5 -@pytest.mark.parametrize("s", range(10)) -def test_shrinks_to_canonical_nan(s): - # Regression test for #4277. A more reliable and minimal example could probably be found. - @given( - st.lists( - st.just(0) | st.floats().filter(lambda a: a != a), min_size=2, max_size=2 - ) - ) - @seed(s) - def sort_is_reversible(l): - # fmt: off - assert sorted(l, reverse=True) == list(reversed(sorted(l))) # noqa: C413 - # fmt: on +@pytest.mark.parametrize("nan", + [ + math.nan, + -math.nan, + struct.unpack('d', struct.pack('Q', 0xfff8000000000001))[0] + ] +) +def test_shrinks_to_canonical_nan(nan): + @shrinking_from([nan]) + def shrinker(data: ConjectureData): + value = data.draw_float() + if math.isnan(value): + data.mark_interesting() - try: - sort_is_reversible() - except AssertionError as e: - assert "[0, nan]" in e.__notes__[0] + shrinker.shrink() + assert len(shrinker.choices) == 1 + assert struct.pack("d", shrinker.choices[0]) == struct.pack("d", math.nan) From 8f0c7ea8e2f9650948561cb9fc749ad8cdb52568 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 17:47:37 +0100 Subject: [PATCH 12/15] Fix test --- .../tests/conjecture/test_float_encoding.py | 21 +++++++++++++++ .../tests/quality/test_float_shrinking.py | 26 +------------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_float_encoding.py b/hypothesis-python/tests/conjecture/test_float_encoding.py index 8781ad152b..5f955f2883 100644 --- a/hypothesis-python/tests/conjecture/test_float_encoding.py +++ b/hypothesis-python/tests/conjecture/test_float_encoding.py @@ -8,6 +8,8 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. +import math +import struct import sys import pytest @@ -15,9 +17,12 @@ from hypothesis import HealthCheck, assume, example, given, settings, strategies as st from hypothesis.internal.compat import ceil, extract_bits, floor from hypothesis.internal.conjecture import floats as flt +from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.floats import float_to_int +from tests.conjecture.common import shrinking_from + EXPONENTS = list(range(flt.MAX_EXPONENT + 1)) assert len(EXPONENTS) == 2**11 @@ -200,3 +205,19 @@ def test_reject_out_of_bounds_floats_while_shrinking(): kwargs = {"min_value": 103.0} g = minimal_from(103.1, lambda x: x >= 100, kwargs=kwargs) assert g == 103.0 + + +@pytest.mark.parametrize( + "nan", + [math.nan, -math.nan, struct.unpack('d', struct.pack('Q', 0xfff8000000000001))[0]] +) +def test_shrinks_to_canonical_nan(nan): + @shrinking_from([nan]) + def shrinker(data: ConjectureData): + value = data.draw_float() + if math.isnan(value): + data.mark_interesting() + + shrinker.shrink() + assert len(shrinker.choices) == 1 + assert float_to_int(shrinker.choices[0]) == float_to_int(math.nan) diff --git a/hypothesis-python/tests/quality/test_float_shrinking.py b/hypothesis-python/tests/quality/test_float_shrinking.py index cb2c938eac..415746f9ff 100644 --- a/hypothesis-python/tests/quality/test_float_shrinking.py +++ b/hypothesis-python/tests/quality/test_float_shrinking.py @@ -8,17 +8,12 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -import math -import struct - import pytest -from hypothesis import example, given, seed, strategies as st +from hypothesis import example, given, strategies as st from hypothesis.internal.compat import ceil -from hypothesis.internal.conjecture.data import ConjectureData from tests.common.debug import minimal -from tests.conjecture.common import shrinking_from def test_shrinks_to_simple_floats(): @@ -50,22 +45,3 @@ def test_shrinks_downwards_to_integers_when_fractional(b): ).filter(lambda x: int(x) != x) ) assert g == b + 0.5 - - -@pytest.mark.parametrize("nan", - [ - math.nan, - -math.nan, - struct.unpack('d', struct.pack('Q', 0xfff8000000000001))[0] - ] -) -def test_shrinks_to_canonical_nan(nan): - @shrinking_from([nan]) - def shrinker(data: ConjectureData): - value = data.draw_float() - if math.isnan(value): - data.mark_interesting() - - shrinker.shrink() - assert len(shrinker.choices) == 1 - assert struct.pack("d", shrinker.choices[0]) == struct.pack("d", math.nan) From 296af5b1005572677c9c9065038406a19c89ffe0 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 18:02:00 +0100 Subject: [PATCH 13/15] Simplify test --- .../tests/conjecture/test_float_encoding.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_float_encoding.py b/hypothesis-python/tests/conjecture/test_float_encoding.py index 5f955f2883..5dade6f802 100644 --- a/hypothesis-python/tests/conjecture/test_float_encoding.py +++ b/hypothesis-python/tests/conjecture/test_float_encoding.py @@ -17,12 +17,9 @@ from hypothesis import HealthCheck, assume, example, given, settings, strategies as st from hypothesis.internal.compat import ceil, extract_bits, floor from hypothesis.internal.conjecture import floats as flt -from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.floats import float_to_int -from tests.conjecture.common import shrinking_from - EXPONENTS = list(range(flt.MAX_EXPONENT + 1)) assert len(EXPONENTS) == 2**11 @@ -208,16 +205,8 @@ def test_reject_out_of_bounds_floats_while_shrinking(): @pytest.mark.parametrize( - "nan", - [math.nan, -math.nan, struct.unpack('d', struct.pack('Q', 0xfff8000000000001))[0]] + "nan", [-math.nan, struct.unpack("d", struct.pack("Q", 0xFFF8000000000001))[0]] ) def test_shrinks_to_canonical_nan(nan): - @shrinking_from([nan]) - def shrinker(data: ConjectureData): - value = data.draw_float() - if math.isnan(value): - data.mark_interesting() - - shrinker.shrink() - assert len(shrinker.choices) == 1 - assert float_to_int(shrinker.choices[0]) == float_to_int(math.nan) + shrunk = minimal_from(nan, math.isnan) + assert float_to_int(shrunk) == float_to_int(math.nan) From 11085e7edcf0a48a7dbb913c92eeee83fd5a7682 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 18:15:45 +0100 Subject: [PATCH 14/15] Use constant for SIGNALING_NAN --- hypothesis-python/tests/conjecture/test_float_encoding.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_float_encoding.py b/hypothesis-python/tests/conjecture/test_float_encoding.py index 5dade6f802..30f41d7fce 100644 --- a/hypothesis-python/tests/conjecture/test_float_encoding.py +++ b/hypothesis-python/tests/conjecture/test_float_encoding.py @@ -9,7 +9,6 @@ # obtain one at https://mozilla.org/MPL/2.0/. import math -import struct import sys import pytest @@ -18,7 +17,7 @@ from hypothesis.internal.compat import ceil, extract_bits, floor from hypothesis.internal.conjecture import floats as flt from hypothesis.internal.conjecture.engine import ConjectureRunner -from hypothesis.internal.floats import float_to_int +from hypothesis.internal.floats import SIGNALING_NAN, float_to_int EXPONENTS = list(range(flt.MAX_EXPONENT + 1)) assert len(EXPONENTS) == 2**11 @@ -204,9 +203,7 @@ def test_reject_out_of_bounds_floats_while_shrinking(): assert g == 103.0 -@pytest.mark.parametrize( - "nan", [-math.nan, struct.unpack("d", struct.pack("Q", 0xFFF8000000000001))[0]] -) +@pytest.mark.parametrize("nan", [-math.nan, SIGNALING_NAN, -SIGNALING_NAN]) def test_shrinks_to_canonical_nan(nan): shrunk = minimal_from(nan, math.isnan) assert float_to_int(shrunk) == float_to_int(math.nan) From 14a565611b6e5c5af5eb4cc8b2a795a24e1c2e69 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 27 Feb 2025 22:36:15 +0100 Subject: [PATCH 15/15] Use tuple instead of bytes --- .../hypothesis/internal/conjecture/shrinking/floats.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py index 2b61ce3903..f55d3ddc8a 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/floats.py @@ -9,13 +9,12 @@ # obtain one at https://mozilla.org/MPL/2.0/. import math -import struct import sys from hypothesis.internal.conjecture.floats import float_to_lex from hypothesis.internal.conjecture.shrinking.common import Shrinker from hypothesis.internal.conjecture.shrinking.integer import Integer -from hypothesis.internal.floats import MAX_PRECISE_INTEGER +from hypothesis.internal.floats import MAX_PRECISE_INTEGER, float_to_int class Float(Shrinker): @@ -25,9 +24,8 @@ def setup(self): def make_canonical(self, f): if math.isnan(f): # Distinguish different NaN bit patterns, while making each equal to itself. - # Returning bytes instead of integer (float_to_int) avoids accidental - # equality with valid large floats. - return struct.pack("d", f) + # Wrap in tuple to avoid potential collision with (huge) finite floats. + return ("nan", float_to_int(f)) return f def check_invariants(self, value):