From c09018e76759b1040be8fbb4483deac9b0035b97 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 24 Nov 2023 18:03:07 -0500 Subject: [PATCH 01/47] type IntervalSet more --- hypothesis-python/src/hypothesis/internal/intervalsets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/intervalsets.py b/hypothesis-python/src/hypothesis/internal/intervalsets.py index c5e82f6b22..563e5294ce 100644 --- a/hypothesis-python/src/hypothesis/internal/intervalsets.py +++ b/hypothesis-python/src/hypothesis/internal/intervalsets.py @@ -8,6 +8,7 @@ # 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/. +from typing import Union class IntervalSet: @classmethod @@ -61,17 +62,16 @@ def __getitem__(self, i): assert r <= v return r - def __contains__(self, elem): + def __contains__(self, elem: Union[str, int]): if isinstance(elem, str): elem = ord(elem) - assert isinstance(elem, int) assert 0 <= elem <= 0x10FFFF return any(start <= elem <= end for start, end in self.intervals) def __repr__(self): return f"IntervalSet({self.intervals!r})" - def index(self, value): + def index(self, value: int): for offset, (u, v) in zip(self.offsets, self.intervals): if u == value: return offset @@ -81,7 +81,7 @@ def index(self, value): return offset + (value - u) raise ValueError(f"{value} is not in list") - def index_above(self, value): + def index_above(self, value: int): for offset, (u, v) in zip(self.offsets, self.intervals): if u >= value: return offset From 07115c7ba687f4061dde31e4ea63037e15235470 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 24 Nov 2023 18:03:41 -0500 Subject: [PATCH 02/47] implement forced for `many` --- .../src/hypothesis/internal/conjecture/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index 48a5ec3f27..cd83bced47 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -204,11 +204,15 @@ def __init__( min_size: int, max_size: Union[int, float], average_size: Union[int, float], + *, + forced: Optional[int] = None ) -> None: assert 0 <= min_size <= average_size <= max_size + assert forced is None or min_size <= forced <= max_size self.min_size = min_size self.max_size = max_size self.data = data + self.forced_size = forced self.p_continue = _calc_p_continue(average_size - min_size, max_size - min_size) self.count = 0 self.rejections = 0 @@ -227,15 +231,22 @@ def more(self) -> bool: self.data.start_example(ONE_FROM_MANY_LABEL) if self.min_size == self.max_size: + # if we have to hit an exact size, draw unconditionally until that + # point, and no further. should_continue = self.count < self.min_size else: forced_result = None if self.force_stop: + # if our size is forced, we can't reject in a way that would + # cause us to differ from the forced size. + assert self.forced_size is None or self.count == self.forced_size forced_result = False elif self.count < self.min_size: forced_result = True elif self.count >= self.max_size: forced_result = False + elif self.forced_size is not None: + forced_result = self.count < self.forced_size should_continue = self.data.draw_boolean( self.p_continue, forced=forced_result ) From ed42225543305454e42d7d67366401af0874bb35 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 24 Nov 2023 18:05:06 -0500 Subject: [PATCH 03/47] implement forced for draw_string --- .../hypothesis/internal/conjecture/data.py | 21 ++++++++++++++----- .../src/hypothesis/internal/intervalsets.py | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 7eb5fbe080..07026192bd 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1105,10 +1105,13 @@ def draw_string( *, min_size: int = 0, max_size: Optional[int] = None, + forced: Optional[str] = None ) -> str: if max_size is None: max_size = 10**10 # "arbitrarily large" + assert forced is None or min_size <= len(forced) <= max_size + average_size = min( max(min_size * 2, min_size + 5), 0.5 * (min_size + max_size), @@ -1120,15 +1123,21 @@ def draw_string( min_size=min_size, max_size=max_size, average_size=average_size, + forced=None if forced is None else len(forced) ) while elements.more(): + forced_i: Optional[int] = None + if forced is not None: + c = forced[elements.count - 1] + forced_i = intervals.index_from_char_in_shrink_order(c) + if len(intervals) > 256: - if self.draw_boolean(0.2): - i = self._draw_bounded_integer(256, len(intervals) - 1) + if self.draw_boolean(0.2, forced=None if forced_i is None else forced_i > 256): + i = self._draw_bounded_integer(256, len(intervals) - 1, forced=forced_i) else: - i = self._draw_bounded_integer(0, 255) + i = self._draw_bounded_integer(0, 255, forced=forced_i) else: - i = self._draw_bounded_integer(0, len(intervals) - 1) + i = self._draw_bounded_integer(0, len(intervals) - 1, forced=forced_i) chars.append(intervals.char_in_shrink_order(i)) @@ -1466,9 +1475,11 @@ def draw_string( *, min_size: int = 0, max_size: Optional[int] = None, + forced: Optional[str] = None ) -> str: + assert forced is None or min_size <= len(forced) return self.provider.draw_string( - intervals, min_size=min_size, max_size=max_size + intervals, min_size=min_size, max_size=max_size, forced=forced ) def draw_bytes(self, size: int) -> bytes: diff --git a/hypothesis-python/src/hypothesis/internal/intervalsets.py b/hypothesis-python/src/hypothesis/internal/intervalsets.py index 563e5294ce..2763e3d744 100644 --- a/hypothesis-python/src/hypothesis/internal/intervalsets.py +++ b/hypothesis-python/src/hypothesis/internal/intervalsets.py @@ -254,3 +254,24 @@ def char_in_shrink_order(self, i: int) -> str: assert 0 <= i <= self._idx_of_Z return chr(self[i]) + + def index_from_char_in_shrink_order(self, c: str) -> int: + """ + Inverse of char_in_shrink_order. + """ + assert len(c) == 1 + i = self.index(ord(c)) + + if i <= self._idx_of_Z: + n = self._idx_of_Z - self._idx_of_zero + # Rewrite [zero_point, Z_point] to [0, n]. + if self._idx_of_zero <= i <= n: + i -= self._idx_of_zero + assert 0 <= i <= n + # Rewrite [zero_point, 0] to [n + 1, Z_point]. + else: + i = (self._idx_of_zero - i) + n + assert n + 1 <= i <= self._idx_of_Z + assert 0 <= i <= self._idx_of_Z + + return i From ae522188fa5a6272306f9adabbad169176c8037e Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 24 Nov 2023 18:17:57 -0500 Subject: [PATCH 04/47] add test for forced many --- .../tests/conjecture/test_utils.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/hypothesis-python/tests/conjecture/test_utils.py b/hypothesis-python/tests/conjecture/test_utils.py index 24f3fb3621..fb516cf2dc 100644 --- a/hypothesis-python/tests/conjecture/test_utils.py +++ b/hypothesis-python/tests/conjecture/test_utils.py @@ -318,6 +318,26 @@ def test_many_with_max_size(): assert many.more() assert not many.more() +@settings( + database=None, + suppress_health_check=[HealthCheck.filter_too_much] +) +@given(st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) +def test_forced_many(min_size, max_size, forced): + assume(min_size <= forced <= max_size) + + many = cu.many( + ConjectureData.for_buffer([0] * 500), + min_size=min_size, + average_size=(min_size + max_size) / 2, + max_size=max_size, + forced=forced + ) + for _ in range(forced): + assert many.more() + + assert not many.more() + def test_biased_coin_can_be_forced(): data = ConjectureData.for_buffer([0]) From e91ad95d8cb3207ad80f5c2949f252420a355b52 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 24 Nov 2023 23:14:48 -0500 Subject: [PATCH 05/47] move forcing tests to separate file --- .../tests/conjecture/test_forced.py | 43 +++++++++++++++++++ .../tests/conjecture/test_utils.py | 28 ------------ 2 files changed, 43 insertions(+), 28 deletions(-) create mode 100644 hypothesis-python/tests/conjecture/test_forced.py diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py new file mode 100644 index 0000000000..47d35ec4cf --- /dev/null +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -0,0 +1,43 @@ +# This file is part of Hypothesis, which may be found at +# https://github.com/HypothesisWorks/hypothesis/ +# +# Copyright the Hypothesis Authors. +# Individual contributors are listed in AUTHORS.rst and the git log. +# +# This Source Code Form is subject to the terms of the Mozilla Public License, +# 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/. + +from hypothesis import HealthCheck, assume, given, settings +from hypothesis.strategies import integers +from hypothesis.internal.conjecture.data import ConjectureData +from hypothesis.internal.conjecture import utils as cu + + +@settings( + database=None, + suppress_health_check=[HealthCheck.filter_too_much] +) +@given(integers(0, 100), integers(0, 100), integers(0, 100)) +def test_forced_many(min_size, max_size, forced): + assume(min_size <= forced <= max_size) + + many = cu.many( + ConjectureData.for_buffer([0] * 500), + min_size=min_size, + average_size=(min_size + max_size) / 2, + max_size=max_size, + forced=forced + ) + for _ in range(forced): + assert many.more() + + assert not many.more() + + +def test_biased_coin_can_be_forced(): + data = ConjectureData.for_buffer([0]) + assert data.draw_boolean(0.5, forced=True) + + data = ConjectureData.for_buffer([1]) + assert not data.draw_boolean(0.5, forced=False) diff --git a/hypothesis-python/tests/conjecture/test_utils.py b/hypothesis-python/tests/conjecture/test_utils.py index fb516cf2dc..f12081ce71 100644 --- a/hypothesis-python/tests/conjecture/test_utils.py +++ b/hypothesis-python/tests/conjecture/test_utils.py @@ -318,34 +318,6 @@ def test_many_with_max_size(): assert many.more() assert not many.more() -@settings( - database=None, - suppress_health_check=[HealthCheck.filter_too_much] -) -@given(st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) -def test_forced_many(min_size, max_size, forced): - assume(min_size <= forced <= max_size) - - many = cu.many( - ConjectureData.for_buffer([0] * 500), - min_size=min_size, - average_size=(min_size + max_size) / 2, - max_size=max_size, - forced=forced - ) - for _ in range(forced): - assert many.more() - - assert not many.more() - - -def test_biased_coin_can_be_forced(): - data = ConjectureData.for_buffer([0]) - assert data.draw_boolean(0.5, forced=True) - - data = ConjectureData.for_buffer([1]) - assert not data.draw_boolean(0.5, forced=False) - def test_assert_biased_coin_always_treats_one_as_true(): data = ConjectureData.for_buffer([0, 1]) From 3ce637366cb4d8987d504dd1b1fb3cade0188138 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 24 Nov 2023 23:22:00 -0500 Subject: [PATCH 06/47] linting --- .../src/hypothesis/internal/conjecture/data.py | 6 ++++-- .../src/hypothesis/internal/conjecture/utils.py | 2 +- .../src/hypothesis/internal/intervalsets.py | 1 + hypothesis-python/tests/conjecture/test_forced.py | 11 ++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 07026192bd..ab1b24e873 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1212,7 +1212,9 @@ def _draw_bounded_integer( bits = gap.bit_length() probe = gap + 1 - if bits > 24 and self._cd.draw_bits(3, forced=None if forced is None else 0): + if bits > 24 and self.draw_boolean( + 7 / 8, forced=None if forced is None else False + ): # For large ranges, we combine the uniform random distribution from draw_bits # with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our # choice of unicode characters is uniform but the 32bit distribution is not. @@ -1475,7 +1477,7 @@ def draw_string( *, min_size: int = 0, max_size: Optional[int] = None, - forced: Optional[str] = None + forced: Optional[str] = None, ) -> str: assert forced is None or min_size <= len(forced) return self.provider.draw_string( diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index cd83bced47..6123cf3ac8 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -205,7 +205,7 @@ def __init__( max_size: Union[int, float], average_size: Union[int, float], *, - forced: Optional[int] = None + forced: Optional[int] = None, ) -> None: assert 0 <= min_size <= average_size <= max_size assert forced is None or min_size <= forced <= max_size diff --git a/hypothesis-python/src/hypothesis/internal/intervalsets.py b/hypothesis-python/src/hypothesis/internal/intervalsets.py index 2763e3d744..8456394032 100644 --- a/hypothesis-python/src/hypothesis/internal/intervalsets.py +++ b/hypothesis-python/src/hypothesis/internal/intervalsets.py @@ -10,6 +10,7 @@ from typing import Union + class IntervalSet: @classmethod def from_string(cls, s): diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 47d35ec4cf..e03cd27800 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -9,15 +9,12 @@ # obtain one at https://mozilla.org/MPL/2.0/. from hypothesis import HealthCheck, assume, given, settings -from hypothesis.strategies import integers -from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture import utils as cu +from hypothesis.internal.conjecture.data import ConjectureData +from hypothesis.strategies import integers -@settings( - database=None, - suppress_health_check=[HealthCheck.filter_too_much] -) +@settings(database=None, suppress_health_check=[HealthCheck.filter_too_much]) @given(integers(0, 100), integers(0, 100), integers(0, 100)) def test_forced_many(min_size, max_size, forced): assume(min_size <= forced <= max_size) @@ -27,7 +24,7 @@ def test_forced_many(min_size, max_size, forced): min_size=min_size, average_size=(min_size + max_size) / 2, max_size=max_size, - forced=forced + forced=forced, ) for _ in range(forced): assert many.more() From 79a39d3edf0703cc8a35a4950267da900238ec50 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 24 Nov 2023 23:24:17 -0500 Subject: [PATCH 07/47] implement forcing for Sampler --- .../hypothesis/internal/conjecture/utils.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index 6123cf3ac8..f4c81fbf03 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -81,8 +81,12 @@ def check_sample( return tuple(values) -def choice(data: "ConjectureData", values: Sequence[T]) -> T: - return values[data.draw_integer(0, len(values) - 1)] +def choice( + data: "ConjectureData", values: Sequence[T], *, forced: Optional[T] = None +) -> T: + forced_i = None if forced is None else values.index(forced) + i = data.draw_integer(0, len(values) - 1, forced=forced_i) + return values[i] class Sampler: @@ -171,10 +175,17 @@ def __init__(self, weights: Sequence[float]): self.table.append((base, alternate, alternate_chance)) self.table.sort() - def sample(self, data: "ConjectureData") -> int: + def sample(self, data: "ConjectureData", forced: Optional[int] = None) -> int: data.start_example(SAMPLE_IN_SAMPLER_LABEL) - base, alternate, alternate_chance = choice(data, self.table) - use_alternate = data.draw_boolean(alternate_chance) + forced_choice = ( + None + if forced is None + else next((b, a, a_c) for (b, a, a_c) in self.table if forced in (b, a)) + ) + base, alternate, alternate_chance = choice( + data, self.table, forced=forced_choice + ) + use_alternate = data.draw_boolean(alternate_chance, forced=forced == alternate) data.stop_example() if use_alternate: return alternate From 7e4b1d73438bb620c492af888765a4cd7fac2685 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 12:09:21 -0500 Subject: [PATCH 08/47] linting --- .../src/hypothesis/internal/conjecture/data.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index ab1b24e873..fdabc606e2 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1105,7 +1105,7 @@ def draw_string( *, min_size: int = 0, max_size: Optional[int] = None, - forced: Optional[str] = None + forced: Optional[str] = None, ) -> str: if max_size is None: max_size = 10**10 # "arbitrarily large" @@ -1123,7 +1123,7 @@ def draw_string( min_size=min_size, max_size=max_size, average_size=average_size, - forced=None if forced is None else len(forced) + forced=None if forced is None else len(forced), ) while elements.more(): forced_i: Optional[int] = None @@ -1132,8 +1132,12 @@ def draw_string( forced_i = intervals.index_from_char_in_shrink_order(c) if len(intervals) > 256: - if self.draw_boolean(0.2, forced=None if forced_i is None else forced_i > 256): - i = self._draw_bounded_integer(256, len(intervals) - 1, forced=forced_i) + if self.draw_boolean( + 0.2, forced=None if forced_i is None else forced_i > 256 + ): + i = self._draw_bounded_integer( + 256, len(intervals) - 1, forced=forced_i + ) else: i = self._draw_bounded_integer(0, 255, forced=forced_i) else: From 2c8a41af769e518862630545f434de331bccdc00 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 12:11:23 -0500 Subject: [PATCH 09/47] implement forcing for draw_integer, except weights --- .../hypothesis/internal/conjecture/data.py | 76 ++++++++++++++++--- .../tests/conjecture/test_forced.py | 41 +++++++++- 2 files changed, 104 insertions(+), 13 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index fdabc606e2..f0f9dd4302 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1015,29 +1015,57 @@ def draw_integer( return shrink_towards - (idx - gap) if min_value is None and max_value is None: - return self._draw_unbounded_integer() + return self._draw_unbounded_integer(forced=forced) if min_value is None: assert max_value is not None # make mypy happy if max_value <= shrink_towards: - return max_value - abs(self._draw_unbounded_integer()) + return max_value - abs( + self._draw_unbounded_integer( + # no need to worry about the outer abs here: + # forced <= max_value -> + # forced - max_value <= 0 -> + # -forced + max_value >= 0 + forced=None + if forced is None + else -forced + max_value + ) + ) else: probe = max_value + 1 while max_value < probe: self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = self._draw_unbounded_integer() + shrink_towards + probe = ( + self._draw_unbounded_integer( + forced=None if forced is None else forced - shrink_towards + ) + + shrink_towards + ) self._cd.stop_example(discard=max_value < probe) return probe if max_value is None: assert min_value is not None if min_value >= shrink_towards: - return min_value + abs(self._draw_unbounded_integer()) + return min_value + abs( + self._draw_unbounded_integer( + # no need to worry the outer abs here: + # forced >= min_value -> forced - min_value >= 0 + forced=None + if forced is None + else forced - min_value + ) + ) else: probe = min_value - 1 while probe < min_value: self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = self._draw_unbounded_integer() + shrink_towards + probe = ( + self._draw_unbounded_integer( + forced=None if forced is None else forced - shrink_towards + ) + + shrink_towards + ) self._cd.stop_example(discard=probe < min_value) return probe @@ -1167,14 +1195,37 @@ def _write_float(self, f: float) -> None: self._cd.draw_bits(1, forced=sign) self._cd.draw_bits(64, forced=float_to_lex(abs(f))) - def _draw_unbounded_integer(self) -> int: - size = INT_SIZES[INT_SIZES_SAMPLER.sample(self._cd)] - r = self._cd.draw_bits(size) + def _draw_unbounded_integer(self, *, forced: Optional[int] = None) -> int: + forced_i = None + if forced is not None: + # Using any bucket large enough to contain this integer would be a + # valid way to force it. This is because an n bit integer could have + # been drawn from a bucket of size n, or from any bucket of size + # m > n. + # We'll always choose the smallest eligible bucket here. + + # We need an extra bit to handle forced signed integers. INT_SIZES + # is interpreted as unsigned sizes. + bit_size = forced.bit_length() + 1 + size = min(size for size in INT_SIZES if bit_size <= size) + forced_i = INT_SIZES.index(size) + + size = INT_SIZES[INT_SIZES_SAMPLER.sample(self._cd, forced=forced_i)] + + forced_r = None + if forced is not None: + forced_r = forced + forced_r <<= 1 + if forced < 0: + forced_r = -forced_r + forced_r |= 1 + + r = self._cd.draw_bits(size, forced=forced_r) sign = r & 1 r >>= 1 if sign: r = -r - return int(r) + return r def _draw_bounded_integer( self, @@ -1443,9 +1494,10 @@ def draw_integer( assert max_value is not None assert (max_value - min_value) <= 1024 # arbitrary practical limit - if forced is not None: - assert min_value is not None - assert max_value is not None + if forced is not None and min_value is not None: + assert min_value <= forced + if forced is not None and max_value is not None: + assert forced <= max_value return self.provider.draw_integer( min_value=min_value, diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index e03cd27800..485939e812 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -8,10 +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 pytest + from hypothesis import HealthCheck, assume, given, settings from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData -from hypothesis.strategies import integers +from hypothesis.strategies import integers, none @settings(database=None, suppress_health_check=[HealthCheck.filter_too_much]) @@ -38,3 +40,40 @@ def test_biased_coin_can_be_forced(): data = ConjectureData.for_buffer([1]) assert not data.draw_boolean(0.5, forced=False) + + +@pytest.mark.parametrize( + "min_value_s, max_value_s, shrink_towards_s, forced_s", + [ + (integers(), integers(), integers(), integers()), + (integers(), integers(), none(), integers()), + (integers(), none(), integers(), integers()), + (none(), integers(), integers(), integers()), + (none(), none(), integers(), integers()), + (none(), integers(), none(), integers()), + (integers(), none(), none(), integers()), + (none(), none(), none(), integers()), + ], +) +def test_integers_forced(min_value_s, max_value_s, shrink_towards_s, forced_s): + @given(min_value_s, max_value_s, shrink_towards_s, forced_s) + @settings(database=None) + def inner_test(min_value, max_value, shrink_towards, forced): + if min_value is not None: + assume(min_value <= forced) + if max_value is not None: + assume(forced <= max_value) + # default shrink_towards param + if shrink_towards is None: + shrink_towards = 0 + + data = ConjectureData.for_buffer([0] * 10) + + assert ( + data.draw_integer( + min_value, max_value, shrink_towards=shrink_towards, forced=forced + ) + == forced + ) + + inner_test() From 699d56a3aeb31d174f2707e96c5a7817f294f197 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 12:52:06 -0500 Subject: [PATCH 10/47] add test for string forcing --- .../tests/conjecture/test_forced.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 485939e812..6a77619b22 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -10,14 +10,15 @@ import pytest +import hypothesis.strategies as st from hypothesis import HealthCheck, assume, given, settings from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData -from hypothesis.strategies import integers, none +from hypothesis.strategies._internal.lazy import unwrap_strategies @settings(database=None, suppress_health_check=[HealthCheck.filter_too_much]) -@given(integers(0, 100), integers(0, 100), integers(0, 100)) +@given(st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) def test_forced_many(min_size, max_size, forced): assume(min_size <= forced <= max_size) @@ -45,14 +46,14 @@ def test_biased_coin_can_be_forced(): @pytest.mark.parametrize( "min_value_s, max_value_s, shrink_towards_s, forced_s", [ - (integers(), integers(), integers(), integers()), - (integers(), integers(), none(), integers()), - (integers(), none(), integers(), integers()), - (none(), integers(), integers(), integers()), - (none(), none(), integers(), integers()), - (none(), integers(), none(), integers()), - (integers(), none(), none(), integers()), - (none(), none(), none(), integers()), + (st.integers(), st.integers(), st.integers(), st.integers()), + (st.integers(), st.integers(), st.none(), st.integers()), + (st.integers(), st.none(), st.integers(), st.integers()), + (st.none(), st.integers(), st.integers(), st.integers()), + (st.none(), st.none(), st.integers(), st.integers()), + (st.none(), st.integers(), st.none(), st.integers()), + (st.integers(), st.none(), st.none(), st.integers()), + (st.none(), st.none(), st.none(), st.integers()), ], ) def test_integers_forced(min_value_s, max_value_s, shrink_towards_s, forced_s): @@ -77,3 +78,16 @@ def inner_test(min_value, max_value, shrink_towards, forced): ) inner_test() + + +def test_strings_forced(): + s = st.text() + intervals = unwrap_strategies(s).element_strategy.intervals + + @given(s) + @settings(database=None) + def inner_test(forced): + data = ConjectureData.for_buffer([0] * 200) + assert data.draw_string(intervals=intervals, forced=forced) == forced + + inner_test() From 1090d94dc68aade7b12b133d0bfc7c2b07558e25 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 12:52:09 -0500 Subject: [PATCH 11/47] fix index_from_char_in_shrink_order bug --- hypothesis-python/src/hypothesis/internal/intervalsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/intervalsets.py b/hypothesis-python/src/hypothesis/internal/intervalsets.py index 8456394032..a47beb1761 100644 --- a/hypothesis-python/src/hypothesis/internal/intervalsets.py +++ b/hypothesis-python/src/hypothesis/internal/intervalsets.py @@ -266,12 +266,12 @@ def index_from_char_in_shrink_order(self, c: str) -> int: if i <= self._idx_of_Z: n = self._idx_of_Z - self._idx_of_zero # Rewrite [zero_point, Z_point] to [0, n]. - if self._idx_of_zero <= i <= n: + if self._idx_of_zero <= i <= self._idx_of_Z: i -= self._idx_of_zero assert 0 <= i <= n # Rewrite [zero_point, 0] to [n + 1, Z_point]. else: - i = (self._idx_of_zero - i) + n + i = self._idx_of_zero - i + n assert n + 1 <= i <= self._idx_of_Z assert 0 <= i <= self._idx_of_Z From 95d76cae2e5cc9722c1479242a055ba67e048bab Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 12:55:22 -0500 Subject: [PATCH 12/47] fix off by one error --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index f0f9dd4302..daf68d647c 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1161,7 +1161,7 @@ def draw_string( if len(intervals) > 256: if self.draw_boolean( - 0.2, forced=None if forced_i is None else forced_i > 256 + 0.2, forced=None if forced_i is None else forced_i > 255 ): i = self._draw_bounded_integer( 256, len(intervals) - 1, forced=forced_i From 0c9d07275597385c5cbd6a6b4697daec6ab30324 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 13:59:22 -0500 Subject: [PATCH 13/47] parameterize test by min_size, max_size --- .../tests/conjecture/test_forced.py | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 6a77619b22..ce1e6d0f0f 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -17,7 +17,7 @@ from hypothesis.strategies._internal.lazy import unwrap_strategies -@settings(database=None, suppress_health_check=[HealthCheck.filter_too_much]) +@settings(database=None, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) @given(st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) def test_forced_many(min_size, max_size, forced): assume(min_size <= forced <= max_size) @@ -80,14 +80,38 @@ def inner_test(min_value, max_value, shrink_towards, forced): inner_test() -def test_strings_forced(): - s = st.text() - intervals = unwrap_strategies(s).element_strategy.intervals +@pytest.mark.parametrize( + "min_size_s, max_size_s", + [ + (st.none(), st.none()), + (st.integers(min_value=0), st.none()), + (st.none(), st.integers(min_value=0)), + (st.integers(min_value=0), st.integers(min_value=0)), + ], +) +def test_strings_forced(min_size_s, max_size_s): + forced_s = st.text() + intervals = unwrap_strategies(forced_s).element_strategy.intervals + + @given(min_size_s, max_size_s, forced_s) + @settings( + database=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], + ) + def inner_test(min_size, max_size, forced): + if min_size is None: + min_size = 0 + + assume(min_size <= len(forced)) + if max_size is not None: + assume(len(forced) <= max_size) - @given(s) - @settings(database=None) - def inner_test(forced): data = ConjectureData.for_buffer([0] * 200) - assert data.draw_string(intervals=intervals, forced=forced) == forced + assert ( + data.draw_string( + intervals=intervals, forced=forced, min_size=min_size, max_size=max_size + ) + == forced + ) inner_test() From 2eb8fc8617225201a385b7551379ae2fe76f9229 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 14:17:59 -0500 Subject: [PATCH 14/47] use st.data instead of constructing ConjectureData --- .../tests/conjecture/test_forced.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index ce1e6d0f0f..230e4fbae8 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -17,13 +17,16 @@ from hypothesis.strategies._internal.lazy import unwrap_strategies -@settings(database=None, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow]) -@given(st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) -def test_forced_many(min_size, max_size, forced): +@settings( + database=None, + suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], +) +@given(st.data(), st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) +def test_forced_many(data, min_size, max_size, forced): assume(min_size <= forced <= max_size) many = cu.many( - ConjectureData.for_buffer([0] * 500), + data.conjecture_data, min_size=min_size, average_size=(min_size + max_size) / 2, max_size=max_size, @@ -35,7 +38,7 @@ def test_forced_many(min_size, max_size, forced): assert not many.more() -def test_biased_coin_can_be_forced(): +def test_boolean_forced(): data = ConjectureData.for_buffer([0]) assert data.draw_boolean(0.5, forced=True) @@ -57,9 +60,9 @@ def test_biased_coin_can_be_forced(): ], ) def test_integers_forced(min_value_s, max_value_s, shrink_towards_s, forced_s): - @given(min_value_s, max_value_s, shrink_towards_s, forced_s) + @given(st.data(), min_value_s, max_value_s, shrink_towards_s, forced_s) @settings(database=None) - def inner_test(min_value, max_value, shrink_towards, forced): + def inner_test(data, min_value, max_value, shrink_towards, forced): if min_value is not None: assume(min_value <= forced) if max_value is not None: @@ -68,10 +71,8 @@ def inner_test(min_value, max_value, shrink_towards, forced): if shrink_towards is None: shrink_towards = 0 - data = ConjectureData.for_buffer([0] * 10) - assert ( - data.draw_integer( + data.conjecture_data.draw_integer( min_value, max_value, shrink_towards=shrink_towards, forced=forced ) == forced @@ -93,12 +94,12 @@ def test_strings_forced(min_size_s, max_size_s): forced_s = st.text() intervals = unwrap_strategies(forced_s).element_strategy.intervals - @given(min_size_s, max_size_s, forced_s) + @given(st.data(), min_size_s, max_size_s, forced_s) @settings( database=None, suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], ) - def inner_test(min_size, max_size, forced): + def inner_test(data, min_size, max_size, forced): if min_size is None: min_size = 0 @@ -106,9 +107,8 @@ def inner_test(min_size, max_size, forced): if max_size is not None: assume(len(forced) <= max_size) - data = ConjectureData.for_buffer([0] * 200) assert ( - data.draw_string( + data.conjecture_data.draw_string( intervals=intervals, forced=forced, min_size=min_size, max_size=max_size ) == forced From 44d824cec8d8bc3957cbacca273b08776a3dda32 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 14:49:11 -0500 Subject: [PATCH 15/47] simplify shrink_towards logic by clamping to start --- .../hypothesis/internal/conjecture/data.py | 74 +++++++------------ 1 file changed, 25 insertions(+), 49 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index daf68d647c..6990c41961 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -994,6 +994,11 @@ def draw_integer( shrink_towards: int = 0, forced: Optional[int] = None, ) -> int: + if min_value is not None: + shrink_towards = max(min_value, shrink_towards) + if max_value is not None: + shrink_towards = min(max_value, shrink_towards) + # This is easy to build on top of our existing conjecture utils, # and it's easy to build sampled_from and weighted_coin on this. if weights is not None: @@ -1003,71 +1008,42 @@ def draw_integer( sampler = Sampler(weights) idx = sampler.sample(self._cd) - if shrink_towards <= min_value: - return min_value + idx - elif max_value <= shrink_towards: - return max_value - idx + # For range -2..2, interpret idx = 0..4 as [0, 1, 2, -1, -2] + if idx <= (gap := max_value - shrink_towards): + return shrink_towards + idx else: - # For range -2..2, interpret idx = 0..4 as [0, 1, 2, -1, -2] - if idx <= (gap := max_value - shrink_towards): - return shrink_towards + idx - else: - return shrink_towards - (idx - gap) + return shrink_towards - (idx - gap) if min_value is None and max_value is None: return self._draw_unbounded_integer(forced=forced) if min_value is None: assert max_value is not None # make mypy happy - if max_value <= shrink_towards: - return max_value - abs( + probe = max_value + 1 + while max_value < probe: + self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) + probe = ( self._draw_unbounded_integer( - # no need to worry about the outer abs here: - # forced <= max_value -> - # forced - max_value <= 0 -> - # -forced + max_value >= 0 - forced=None - if forced is None - else -forced + max_value + forced=None if forced is None else forced - shrink_towards ) + + shrink_towards ) - else: - probe = max_value + 1 - while max_value < probe: - self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = ( - self._draw_unbounded_integer( - forced=None if forced is None else forced - shrink_towards - ) - + shrink_towards - ) - self._cd.stop_example(discard=max_value < probe) - return probe + self._cd.stop_example(discard=max_value < probe) + return probe if max_value is None: assert min_value is not None - if min_value >= shrink_towards: - return min_value + abs( + probe = min_value - 1 + while probe < min_value: + self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) + probe = ( self._draw_unbounded_integer( - # no need to worry the outer abs here: - # forced >= min_value -> forced - min_value >= 0 - forced=None - if forced is None - else forced - min_value + forced=None if forced is None else forced - shrink_towards ) + + shrink_towards ) - else: - probe = min_value - 1 - while probe < min_value: - self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = ( - self._draw_unbounded_integer( - forced=None if forced is None else forced - shrink_towards - ) - + shrink_towards - ) - self._cd.stop_example(discard=probe < min_value) - return probe + self._cd.stop_example(discard=probe < min_value) + return probe return self._draw_bounded_integer( min_value, From 3c385d1c8e2904ff75d16e995b7c7e1b76516aee Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 14:58:53 -0500 Subject: [PATCH 16/47] fix bug in Sampler forcing --- hypothesis-python/src/hypothesis/internal/conjecture/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index f4c81fbf03..aff5f85aca 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -185,7 +185,9 @@ def sample(self, data: "ConjectureData", forced: Optional[int] = None) -> int: base, alternate, alternate_chance = choice( data, self.table, forced=forced_choice ) - use_alternate = data.draw_boolean(alternate_chance, forced=forced == alternate) + use_alternate = data.draw_boolean( + alternate_chance, forced=None if forced is None else forced == alternate + ) data.stop_example() if use_alternate: return alternate From 4866f5ecf86124710d16cf4c2305a036a0ff69b2 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 15:33:07 -0500 Subject: [PATCH 17/47] add support for forcing integer weights --- .../src/hypothesis/internal/conjecture/data.py | 16 +++++++++++++--- .../src/hypothesis/internal/conjecture/utils.py | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 6990c41961..2d39705d49 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1006,10 +1006,18 @@ def draw_integer( assert max_value is not None sampler = Sampler(weights) - idx = sampler.sample(self._cd) + gap = max_value - shrink_towards + + forced_idx = None + if forced is not None: + if forced >= shrink_towards: + forced_idx = forced - shrink_towards + else: + forced_idx = shrink_towards + gap - forced + idx = sampler.sample(self._cd, forced=forced_idx) # For range -2..2, interpret idx = 0..4 as [0, 1, 2, -1, -2] - if idx <= (gap := max_value - shrink_towards): + if idx <= gap: return shrink_towards + idx else: return shrink_towards - (idx - gap) @@ -1468,7 +1476,9 @@ def draw_integer( if weights is not None: assert min_value is not None assert max_value is not None - assert (max_value - min_value) <= 1024 # arbitrary practical limit + width = max_value - min_value + 1 + assert width <= 1024 # arbitrary practical limit + assert len(weights) == width if forced is not None and min_value is not None: assert min_value <= forced diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index aff5f85aca..371fc215af 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -190,8 +190,10 @@ def sample(self, data: "ConjectureData", forced: Optional[int] = None) -> int: ) data.stop_example() if use_alternate: + assert forced is None or alternate == forced return alternate else: + assert forced is None or base == forced return base From 8e2878638b934883de10daf4ba5217b02a79e057 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 15:36:59 -0500 Subject: [PATCH 18/47] standardize forced test names --- hypothesis-python/tests/conjecture/test_forced.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 230e4fbae8..cb1fcb6dcb 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -38,7 +38,7 @@ def test_forced_many(data, min_size, max_size, forced): assert not many.more() -def test_boolean_forced(): +def test_forced_boolean(): data = ConjectureData.for_buffer([0]) assert data.draw_boolean(0.5, forced=True) @@ -59,7 +59,7 @@ def test_boolean_forced(): (st.none(), st.none(), st.none(), st.integers()), ], ) -def test_integers_forced(min_value_s, max_value_s, shrink_towards_s, forced_s): +def test_forced_integer(min_value_s, max_value_s, shrink_towards_s, forced_s): @given(st.data(), min_value_s, max_value_s, shrink_towards_s, forced_s) @settings(database=None) def inner_test(data, min_value, max_value, shrink_towards, forced): @@ -90,7 +90,7 @@ def inner_test(data, min_value, max_value, shrink_towards, forced): (st.integers(min_value=0), st.integers(min_value=0)), ], ) -def test_strings_forced(min_size_s, max_size_s): +def test_forced_string(min_size_s, max_size_s): forced_s = st.text() intervals = unwrap_strategies(forced_s).element_strategy.intervals From 16179e3f42d420711d617f57e7fec31d970d2e1e Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 15:50:34 -0500 Subject: [PATCH 19/47] add support for forcing bytes --- .../src/hypothesis/internal/conjecture/data.py | 14 ++++++++++---- hypothesis-python/tests/conjecture/test_forced.py | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 2d39705d49..60f7e2ee82 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1159,8 +1159,13 @@ def draw_string( return "".join(chars) - def draw_bytes(self, size: int) -> bytes: - return self._cd.draw_bits(8 * size).to_bytes(size, "big") + def draw_bytes(self, size: int, *, forced: Optional[bytes] = None) -> bytes: + forced_i = None + if forced is not None: + forced_i = int_from_bytes(forced) + size = len(forced) + + return self._cd.draw_bits(8 * size, forced=forced_i).to_bytes(size, "big") def _draw_float(self, forced_sign_bit: Optional[int] = None) -> float: """ @@ -1526,8 +1531,9 @@ def draw_string( intervals, min_size=min_size, max_size=max_size, forced=forced ) - def draw_bytes(self, size: int) -> bytes: - return self.provider.draw_bytes(size) + def draw_bytes(self, size: int, *, forced: Optional[bytes] = None) -> bytes: + assert forced is None or len(forced) <= size + return self.provider.draw_bytes(size, forced=forced) def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool: return self.provider.draw_boolean(p, forced=forced) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index cb1fcb6dcb..8689cd35db 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -115,3 +115,10 @@ def inner_test(data, min_size, max_size, forced): ) inner_test() + + +@given(st.data(), st.integers(min_value=0), st.binary()) +@settings(database=None) +def test_forced_bytes(data, size, forced): + assume(len(forced) <= size) + assert data.conjecture_data.draw_bytes(size, forced=forced) == forced From ac232eac7574b89b908585c7b946857001c2d625 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 25 Nov 2023 23:59:55 -0500 Subject: [PATCH 20/47] add support for forcing floats --- .../hypothesis/internal/conjecture/data.py | 42 ++++++++++++++++--- .../tests/conjecture/test_forced.py | 6 +++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 60f7e2ee82..2b536ceb40 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1070,7 +1070,8 @@ def draw_float( # TODO: consider supporting these float widths at the IR level in the # future. # width: Literal[16, 32, 64] = 64, - # exclude_min and exclude_max handled higher up + # exclude_min and exclude_max handled higher up, + forced: Optional[float] = None, ) -> float: ( sampler, @@ -1087,10 +1088,18 @@ def draw_float( while True: self._cd.start_example(FLOAT_STRATEGY_DO_DRAW_LABEL) - i = sampler.sample(self._cd) if sampler else 0 + forced_i = None + if forced is not None: + forced_i = ( + nasty_floats.index(forced) + 1 if forced in nasty_floats else 0 + ) + + i = sampler.sample(self._cd, forced=forced_i) if sampler else 0 self._cd.start_example(DRAW_FLOAT_LABEL) if i == 0: - result = self._draw_float(forced_sign_bit=forced_sign_bit) + result = self._draw_float( + forced_sign_bit=forced_sign_bit, forced=forced + ) if math.copysign(1.0, result) == -1: assert neg_clamper is not None clamped = -neg_clamper(-result) @@ -1167,14 +1176,26 @@ def draw_bytes(self, size: int, *, forced: Optional[bytes] = None) -> bytes: return self._cd.draw_bits(8 * size, forced=forced_i).to_bytes(size, "big") - def _draw_float(self, forced_sign_bit: Optional[int] = None) -> float: + def _draw_float( + self, forced_sign_bit: Optional[int] = None, *, forced: Optional[float] = None + ) -> float: """ Helper for draw_float which draws a random 64-bit float. """ + if forced is not None: + forced_sign_bit = int(sign_aware_lte(forced, 0.0)) + self._cd.start_example(DRAW_FLOAT_LABEL) try: is_negative = self._cd.draw_bits(1, forced=forced_sign_bit) - f = lex_to_float(self._cd.draw_bits(64)) + f = lex_to_float( + self._cd.draw_bits( + 64, + forced=None + if forced is None + else float_to_lex(-forced if is_negative else forced), + ) + ) return -f if is_negative else f finally: self._cd.stop_example() @@ -1508,14 +1529,23 @@ def draw_float( # TODO: consider supporting these float widths at the IR level in the # future. # width: Literal[16, 32, 64] = 64, - # exclude_min and exclude_max handled higher up + # exclude_min and exclude_max handled higher up, + forced: Optional[float] = None, ) -> float: assert smallest_nonzero_magnitude > 0 + assert not math.isnan(min_value) + assert not math.isnan(max_value) + + if forced is not None: + assert not math.isnan(forced) + assert min_value <= forced <= max_value + return self.provider.draw_float( min_value=min_value, max_value=max_value, allow_nan=allow_nan, smallest_nonzero_magnitude=smallest_nonzero_magnitude, + forced=forced, ) def draw_string( diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 8689cd35db..e5a5fadccb 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -122,3 +122,9 @@ def inner_test(data, min_size, max_size, forced): def test_forced_bytes(data, size, forced): assume(len(forced) <= size) assert data.conjecture_data.draw_bytes(size, forced=forced) == forced + + +@given(st.data(), st.floats(allow_nan=False)) +@settings(database=None) +def test_forced_floats(data, forced): + assert data.conjecture_data.draw_float(forced=forced) == forced From d36d28591c9125e89b6d68dd7e711c34e18de005 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 00:00:55 -0500 Subject: [PATCH 21/47] add return typing for intervalsets --- hypothesis-python/src/hypothesis/internal/intervalsets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/intervalsets.py b/hypothesis-python/src/hypothesis/internal/intervalsets.py index a47beb1761..4a143c80b8 100644 --- a/hypothesis-python/src/hypothesis/internal/intervalsets.py +++ b/hypothesis-python/src/hypothesis/internal/intervalsets.py @@ -63,7 +63,7 @@ def __getitem__(self, i): assert r <= v return r - def __contains__(self, elem: Union[str, int]): + def __contains__(self, elem: Union[str, int]) -> bool: if isinstance(elem, str): elem = ord(elem) assert 0 <= elem <= 0x10FFFF @@ -72,7 +72,7 @@ def __contains__(self, elem: Union[str, int]): def __repr__(self): return f"IntervalSet({self.intervals!r})" - def index(self, value: int): + def index(self, value: int) -> int: for offset, (u, v) in zip(self.offsets, self.intervals): if u == value: return offset @@ -82,7 +82,7 @@ def index(self, value: int): return offset + (value - u) raise ValueError(f"{value} is not in list") - def index_above(self, value: int): + def index_above(self, value: int) -> int: for offset, (u, v) in zip(self.offsets, self.intervals): if u >= value: return offset From 64edc2ed7822878b68ea92aa99644d72b649a9e1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 01:27:04 -0500 Subject: [PATCH 22/47] require equal size in forced draw_bytes --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 2b536ceb40..78359613d9 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1562,7 +1562,7 @@ def draw_string( ) def draw_bytes(self, size: int, *, forced: Optional[bytes] = None) -> bytes: - assert forced is None or len(forced) <= size + assert forced is None or len(forced) == size return self.provider.draw_bytes(size, forced=forced) def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool: From 0f80e0e74f069cfb382e3cb075770910ba496de8 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 01:28:50 -0500 Subject: [PATCH 23/47] add tests for written forced buffer --- .../tests/conjecture/test_forced.py | 77 +++++++++++++++---- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index e5a5fadccb..e83f3adadf 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.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/. +from random import Random + import pytest import hypothesis.strategies as st @@ -17,16 +19,23 @@ from hypothesis.strategies._internal.lazy import unwrap_strategies +# we'd like to use st.data() here, but that tracks too much global state for us +# to ensure its buffer was only written to by our forced draws. +def fresh_data(): + return ConjectureData(8 * 1024, prefix=b"", random=Random()) + + @settings( database=None, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], ) -@given(st.data(), st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) -def test_forced_many(data, min_size, max_size, forced): +@given(st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) +def test_forced_many(min_size, max_size, forced): assume(min_size <= forced <= max_size) + data = fresh_data() many = cu.many( - data.conjecture_data, + data, min_size=min_size, average_size=(min_size + max_size) / 2, max_size=max_size, @@ -37,6 +46,19 @@ def test_forced_many(data, min_size, max_size, forced): assert not many.more() + # ensure values written to the buffer do in fact generate the forced value + data = ConjectureData.for_buffer(data.buffer) + many = cu.many( + data, + min_size=min_size, + average_size=(min_size + max_size) / 2, + max_size=max_size, + ) + for _ in range(forced): + assert many.more() + + assert not many.more() + def test_forced_boolean(): data = ConjectureData.for_buffer([0]) @@ -60,9 +82,9 @@ def test_forced_boolean(): ], ) def test_forced_integer(min_value_s, max_value_s, shrink_towards_s, forced_s): - @given(st.data(), min_value_s, max_value_s, shrink_towards_s, forced_s) + @given(min_value_s, max_value_s, shrink_towards_s, forced_s) @settings(database=None) - def inner_test(data, min_value, max_value, shrink_towards, forced): + def inner_test(min_value, max_value, shrink_towards, forced): if min_value is not None: assume(min_value <= forced) if max_value is not None: @@ -71,13 +93,20 @@ def inner_test(data, min_value, max_value, shrink_towards, forced): if shrink_towards is None: shrink_towards = 0 + data = fresh_data() assert ( - data.conjecture_data.draw_integer( + data.draw_integer( min_value, max_value, shrink_towards=shrink_towards, forced=forced ) == forced ) + data = ConjectureData.for_buffer(data.buffer) + assert ( + data.draw_integer(min_value, max_value, shrink_towards=shrink_towards) + == forced + ) + inner_test() @@ -94,12 +123,12 @@ def test_forced_string(min_size_s, max_size_s): forced_s = st.text() intervals = unwrap_strategies(forced_s).element_strategy.intervals - @given(st.data(), min_size_s, max_size_s, forced_s) + @given(min_size_s, max_size_s, forced_s) @settings( database=None, suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], ) - def inner_test(data, min_size, max_size, forced): + def inner_test(min_size, max_size, forced): if min_size is None: min_size = 0 @@ -107,24 +136,38 @@ def inner_test(data, min_size, max_size, forced): if max_size is not None: assume(len(forced) <= max_size) + data = fresh_data() assert ( - data.conjecture_data.draw_string( - intervals=intervals, forced=forced, min_size=min_size, max_size=max_size + data.draw_string( + intervals=intervals, min_size=min_size, max_size=max_size, forced=forced ) == forced ) + data = ConjectureData.for_buffer(data.buffer) + assert ( + data.draw_string(intervals=intervals, min_size=min_size, max_size=max_size) + == forced + ) + inner_test() -@given(st.data(), st.integers(min_value=0), st.binary()) +@given(st.binary()) @settings(database=None) -def test_forced_bytes(data, size, forced): - assume(len(forced) <= size) - assert data.conjecture_data.draw_bytes(size, forced=forced) == forced +def test_forced_bytes(forced): + data = fresh_data() + assert data.draw_bytes(len(forced), forced=forced) == forced + data = ConjectureData.for_buffer(data.buffer) + assert data.draw_bytes(len(forced)) == forced -@given(st.data(), st.floats(allow_nan=False)) + +@given(st.floats(allow_nan=False)) @settings(database=None) -def test_forced_floats(data, forced): - assert data.conjecture_data.draw_float(forced=forced) == forced +def test_forced_floats(forced): + data = fresh_data() + assert data.draw_float(forced=forced) == forced + + data = ConjectureData.for_buffer(data.buffer) + assert data.draw_float() == forced From 50bc3f7f3d39167c8cba4dfdc7001888c02a80e7 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 12:26:05 -0500 Subject: [PATCH 24/47] allow forced nans --- .../src/hypothesis/internal/conjecture/data.py | 4 ++-- hypothesis-python/tests/conjecture/test_forced.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 78359613d9..dfdc14303c 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1537,8 +1537,8 @@ def draw_float( assert not math.isnan(max_value) if forced is not None: - assert not math.isnan(forced) - assert min_value <= forced <= max_value + assert allow_nan or not math.isnan(forced) + assert math.isnan(forced) or min_value <= forced <= max_value return self.provider.draw_float( min_value=min_value, diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index e83f3adadf..0ff5e8f82a 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -8,6 +8,7 @@ # 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 from random import Random import pytest @@ -163,11 +164,13 @@ def test_forced_bytes(forced): assert data.draw_bytes(len(forced)) == forced -@given(st.floats(allow_nan=False)) +@given(st.floats()) @settings(database=None) def test_forced_floats(forced): data = fresh_data() - assert data.draw_float(forced=forced) == forced + drawn = data.draw_float(forced=forced) + assert drawn == forced or (math.isnan(drawn) and math.isnan(forced)) data = ConjectureData.for_buffer(data.buffer) - assert data.draw_float() == forced + drawn = data.draw_float() + assert drawn == forced or (math.isnan(drawn) and math.isnan(forced)) From ef5e4499f8f2c0c1a2d9585ce5a56492e3be376f Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 18:50:45 -0500 Subject: [PATCH 25/47] revert draw_boolean change --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index dfdc14303c..f81966b6e4 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1277,9 +1277,7 @@ def _draw_bounded_integer( bits = gap.bit_length() probe = gap + 1 - if bits > 24 and self.draw_boolean( - 7 / 8, forced=None if forced is None else False - ): + if bits > 24 and self._cd.draw_bits(3, forced=None if forced is None else 0): # For large ranges, we combine the uniform random distribution from draw_bits # with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our # choice of unicode characters is uniform but the 32bit distribution is not. From 0bdbedbf9640fc5fde19f5a0895ccd02c4fd3280 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 20:07:40 -0500 Subject: [PATCH 26/47] add more info to asserts --- .../src/hypothesis/internal/conjecture/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index 371fc215af..7fdae2a498 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -190,10 +190,10 @@ def sample(self, data: "ConjectureData", forced: Optional[int] = None) -> int: ) data.stop_example() if use_alternate: - assert forced is None or alternate == forced + assert forced is None or alternate == forced, (forced, alternate) return alternate else: - assert forced is None or base == forced + assert forced is None or base == forced, (forced, base) return base @@ -222,7 +222,7 @@ def __init__( *, forced: Optional[int] = None, ) -> None: - assert 0 <= min_size <= average_size <= max_size + assert 0 <= min_size <= average_size <= max_size, (min_size, average_size, max_size) assert forced is None or min_size <= forced <= max_size self.min_size = min_size self.max_size = max_size From efd477e756202cdf51fa87fa17e86e37d0ba6e26 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 20:13:19 -0500 Subject: [PATCH 27/47] rewrite tests to be more efficient, add weighted integer test --- .../tests/conjecture/test_forced.py | 158 ++++++++++-------- 1 file changed, 89 insertions(+), 69 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 0ff5e8f82a..023704c53d 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -25,14 +25,16 @@ def fresh_data(): return ConjectureData(8 * 1024, prefix=b"", random=Random()) - +@given(st.data()) @settings( database=None, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], ) -@given(st.integers(0, 100), st.integers(0, 100), st.integers(0, 100)) -def test_forced_many(min_size, max_size, forced): - assume(min_size <= forced <= max_size) +def test_forced_many(data): + forced = data.draw(st.integers(0, 100)) + min_size = data.draw(st.integers(0, forced)) + max_size = data.draw(st.integers(forced, 100)) + assert min_size <= forced <= max_size # by construction data = fresh_data() many = cu.many( @@ -70,88 +72,106 @@ def test_forced_boolean(): @pytest.mark.parametrize( - "min_value_s, max_value_s, shrink_towards_s, forced_s", + "use_min_value, use_max_value, use_shrink_towards, use_weights", [ - (st.integers(), st.integers(), st.integers(), st.integers()), - (st.integers(), st.integers(), st.none(), st.integers()), - (st.integers(), st.none(), st.integers(), st.integers()), - (st.none(), st.integers(), st.integers(), st.integers()), - (st.none(), st.none(), st.integers(), st.integers()), - (st.none(), st.integers(), st.none(), st.integers()), - (st.integers(), st.none(), st.none(), st.integers()), - (st.none(), st.none(), st.none(), st.integers()), + (True, True, True, True), + (True, True, True, False), + (True, True, False, True), + (True, True, False, False), + (True, False, True, False), + (False, True, True, False), + (False, False, True, False), + (False, True, False, False), + (True, False, False, False), + (False, False, False, False), ], ) -def test_forced_integer(min_value_s, max_value_s, shrink_towards_s, forced_s): - @given(min_value_s, max_value_s, shrink_towards_s, forced_s) - @settings(database=None) - def inner_test(min_value, max_value, shrink_towards, forced): - if min_value is not None: - assume(min_value <= forced) - if max_value is not None: - assume(forced <= max_value) - # default shrink_towards param - if shrink_towards is None: - shrink_towards = 0 - - data = fresh_data() - assert ( - data.draw_integer( - min_value, max_value, shrink_towards=shrink_towards, forced=forced +@given(st.data()) +@settings(database=None) +def test_forced_integer( + use_min_value, use_max_value, use_shrink_towards, use_weights, data +): + min_value = None + max_value = None + shrink_towards = 0 + weights = None + + forced = data.draw(st.integers()) + if use_min_value: + min_value = data.draw(st.integers(max_value=forced)) + if use_max_value: + max_value = data.draw(st.integers(min_value=forced)) + if use_shrink_towards: + shrink_towards = data.draw(st.integers()) + if use_weights: + assert use_max_value and use_min_value + + width = max_value - min_value + 1 + assume(width <= 1024) + + weights = data.draw( + st.lists( + # weights doesn't play well with super small floats. + st.floats(min_value=0.1, max_value=1), + min_size=width, + max_size=width, ) - == forced ) - data = ConjectureData.for_buffer(data.buffer) - assert ( - data.draw_integer(min_value, max_value, shrink_towards=shrink_towards) - == forced + data = fresh_data() + assert ( + data.draw_integer( + min_value, + max_value, + shrink_towards=shrink_towards, + weights=weights, + forced=forced, ) + == forced + ) - inner_test() + data = ConjectureData.for_buffer(data.buffer) + assert ( + data.draw_integer( + min_value, max_value, shrink_towards=shrink_towards, weights=weights + ) + == forced + ) -@pytest.mark.parametrize( - "min_size_s, max_size_s", - [ - (st.none(), st.none()), - (st.integers(min_value=0), st.none()), - (st.none(), st.integers(min_value=0)), - (st.integers(min_value=0), st.integers(min_value=0)), - ], +@pytest.mark.parametrize("use_min_size", [True, False]) +@pytest.mark.parametrize("use_max_size", [True, False]) +@given(st.data()) +@settings( + database=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], ) -def test_forced_string(min_size_s, max_size_s): +def test_forced_string(use_min_size, use_max_size, data): forced_s = st.text() intervals = unwrap_strategies(forced_s).element_strategy.intervals - @given(min_size_s, max_size_s, forced_s) - @settings( - database=None, - suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], - ) - def inner_test(min_size, max_size, forced): - if min_size is None: - min_size = 0 - - assume(min_size <= len(forced)) - if max_size is not None: - assume(len(forced) <= max_size) - - data = fresh_data() - assert ( - data.draw_string( - intervals=intervals, min_size=min_size, max_size=max_size, forced=forced - ) - == forced - ) + forced = data.draw(forced_s) + min_size = 0 + max_size = None + if use_min_size: + min_size = data.draw(st.integers(0, len(forced))) - data = ConjectureData.for_buffer(data.buffer) - assert ( - data.draw_string(intervals=intervals, min_size=min_size, max_size=max_size) - == forced + if use_max_size: + max_size = data.draw(st.integers(min_value=len(forced))) + + data = fresh_data() + assert ( + data.draw_string( + intervals=intervals, min_size=min_size, max_size=max_size, forced=forced ) + == forced + ) - inner_test() + data = ConjectureData.for_buffer(data.buffer) + assert ( + data.draw_string(intervals=intervals, min_size=min_size, max_size=max_size) + == forced + ) @given(st.binary()) From 98b3d401e5659f231db63dca8a402d8bd3af3d1c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 20:13:51 -0500 Subject: [PATCH 28/47] linting --- hypothesis-python/src/hypothesis/internal/conjecture/utils.py | 2 +- hypothesis-python/tests/conjecture/test_forced.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index 7fdae2a498..4cd17079d4 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -222,7 +222,7 @@ def __init__( *, forced: Optional[int] = None, ) -> None: - assert 0 <= min_size <= average_size <= max_size, (min_size, average_size, max_size) + assert 0 <= min_size <= average_size <= max_size assert forced is None or min_size <= forced <= max_size self.min_size = min_size self.max_size = max_size diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 023704c53d..ab79048e2a 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -25,6 +25,7 @@ def fresh_data(): return ConjectureData(8 * 1024, prefix=b"", random=Random()) + @given(st.data()) @settings( database=None, From 43590b72ed8c83a16183eaa90f1969004a1a3c3b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 26 Nov 2023 21:32:05 -0500 Subject: [PATCH 29/47] split assertion to make ruff happy --- hypothesis-python/tests/conjecture/test_forced.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index ab79048e2a..a08a53e1bd 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -105,7 +105,8 @@ def test_forced_integer( if use_shrink_towards: shrink_towards = data.draw(st.integers()) if use_weights: - assert use_max_value and use_min_value + assert use_max_value + assert use_min_value width = max_value - min_value + 1 assume(width <= 1024) From a64d9b0bc84362870016943eed0c442d07b86e57 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 30 Nov 2023 01:18:43 -0500 Subject: [PATCH 30/47] mark next() as no-branch due to coveragepy weirdness --- hypothesis-python/src/hypothesis/internal/conjecture/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index 4cd17079d4..0712b2d8c8 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -177,7 +177,7 @@ def __init__(self, weights: Sequence[float]): def sample(self, data: "ConjectureData", forced: Optional[int] = None) -> int: data.start_example(SAMPLE_IN_SAMPLER_LABEL) - forced_choice = ( + forced_choice = ( # pragma: no branch # https://github.com/nedbat/coveragepy/issues/1617 None if forced is None else next((b, a, a_c) for (b, a, a_c) in self.table if forced in (b, a)) From 9283da35aab9e370ed37620039639b12f5fd326e Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 1 Dec 2023 01:04:31 -0500 Subject: [PATCH 31/47] dont discard in draw_boolean --- .../src/hypothesis/internal/conjecture/data.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index f81966b6e4..972180d146 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -76,7 +76,6 @@ def wrapper(tp): ONE_BOUND_INTEGERS_LABEL = calc_label_from_name("trying a one-bound int allowing 0") INTEGER_RANGE_DRAW_LABEL = calc_label_from_name("another draw in integer_range()") BIASED_COIN_LABEL = calc_label_from_name("biased_coin()") -BIASED_COIN_INNER_LABEL = calc_label_from_name("inside biased_coin()") TOP_LABEL = calc_label_from_name("top") DRAW_BYTES_LABEL = calc_label_from_name("draw_bytes() in ConjectureData") @@ -937,18 +936,9 @@ def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool else: partial = True - if forced is None: - # We want to get to the point where True is represented by - # 1 and False is represented by 0 as quickly as possible, so - # we use the remove_discarded machinery in the shrinker to - # achieve that by discarding any draws that are > 1 and writing - # a suitable draw into the choice sequence at the end of the - # loop. - self._cd.start_example(BIASED_COIN_INNER_LABEL) - i = self._cd.draw_bits(bits) - self._cd.stop_example(discard=i > 1) - else: - i = self._cd.draw_bits(bits, forced=int(forced)) + i = self._cd.draw_bits( + bits, forced=None if forced is None else int(forced) + ) # We always choose the region that causes us to repeat the loop as # the maximum value, so that shrinking the drawn bits never causes @@ -978,8 +968,6 @@ def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool # becomes i > falsey. result = i > falsey - if i > 1: - self._cd.draw_bits(bits, forced=int(result)) break self._cd.stop_example() return result From 944982016b8b4c90ba516f49153e7e72f1f6e233 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 1 Dec 2023 01:04:37 -0500 Subject: [PATCH 32/47] use draw_boolean insead of draw_bits in bounded_integer --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 972180d146..f1360267bd 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1265,7 +1265,9 @@ def _draw_bounded_integer( bits = gap.bit_length() probe = gap + 1 - if bits > 24 and self._cd.draw_bits(3, forced=None if forced is None else 0): + if bits > 24 and self.draw_boolean( + 7 / 8, forced=None if forced is None else False + ): # For large ranges, we combine the uniform random distribution from draw_bits # with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our # choice of unicode characters is uniform but the 32bit distribution is not. From 724ea85bd1d35f4f9c7b505f0dcaa458834f9c45 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 2 Dec 2023 17:39:19 -0500 Subject: [PATCH 33/47] disallow out of bounds forced combinations --- .../src/hypothesis/internal/conjecture/data.py | 6 ++++++ hypothesis-python/tests/conjecture/test_forced.py | 1 + 2 files changed, 7 insertions(+) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index f1360267bd..ac5d5f3927 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1494,6 +1494,12 @@ def draw_integer( assert width <= 1024 # arbitrary practical limit assert len(weights) == width + if forced is not None: + # We draw `forced=forced - shrink_towards` internally. If that grows + # larger than a 128 bit signed integer, we can't represent it. + # Disallow this combination for now. + # Note that bit_length() = 128 -> signed bit size = 129. + assert (forced - shrink_towards).bit_length() < 128 if forced is not None and min_value is not None: assert min_value <= forced if forced is not None and max_value is not None: diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index a08a53e1bd..d78aac12bb 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -110,6 +110,7 @@ def test_forced_integer( width = max_value - min_value + 1 assume(width <= 1024) + assume((forced - shrink_towards).bit_length() < 128) weights = data.draw( st.lists( From 0aa30eeedc71fe0cada30398785d4ea2f7b4ddd9 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 3 Dec 2023 00:24:59 -0500 Subject: [PATCH 34/47] more correct condition check --- .../src/hypothesis/internal/conjecture/data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index ac5d5f3927..15ab10b22b 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1494,9 +1494,9 @@ def draw_integer( assert width <= 1024 # arbitrary practical limit assert len(weights) == width - if forced is not None: - # We draw `forced=forced - shrink_towards` internally. If that grows - # larger than a 128 bit signed integer, we can't represent it. + if forced is not None and (min_value is None or max_value is None): + # We draw `forced=forced - shrink_towards` here internally. If that + # grows larger than a 128 bit signed integer, we can't represent it. # Disallow this combination for now. # Note that bit_length() = 128 -> signed bit size = 129. assert (forced - shrink_towards).bit_length() < 128 From 2014ccf9ff0083ea780e3885c4035aa3e2aa0dae Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 3 Dec 2023 01:00:08 -0500 Subject: [PATCH 35/47] increase max_examples for dtype bug --- hypothesis-python/tests/numpy/test_from_dtype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/tests/numpy/test_from_dtype.py b/hypothesis-python/tests/numpy/test_from_dtype.py index ee74060c2f..e6f6865e0e 100644 --- a/hypothesis-python/tests/numpy/test_from_dtype.py +++ b/hypothesis-python/tests/numpy/test_from_dtype.py @@ -116,7 +116,7 @@ def cannot_encode(string): except UnicodeEncodeError: return True - find_any(nps.from_dtype(np.dtype("U")), cannot_encode) + find_any(nps.from_dtype(np.dtype("U")), cannot_encode, settings(max_examples=2000)) @given(st.data()) From 7ec7d8b4a01392b53d4a5d5c9870d3ae9f9d0378 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 3 Dec 2023 01:09:30 -0500 Subject: [PATCH 36/47] suppress flaky too_slow health check --- hypothesis-python/tests/conjecture/test_forced.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index d78aac12bb..18cd3e9d19 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -88,7 +88,7 @@ def test_forced_boolean(): ], ) @given(st.data()) -@settings(database=None) +@settings(database=None, suppress_health_check=[HealthCheck.too_slow]) def test_forced_integer( use_min_value, use_max_value, use_shrink_towards, use_weights, data ): From 654dc022a2dbe907e38ac8c7e222014721f48945 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 3 Dec 2023 01:15:19 -0500 Subject: [PATCH 37/47] add release notes --- 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..29b06db7b7 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +This patch refactors some more internals, continuing our work on supporting alternative backends (:issue:`3086`). There is no user-visible change. From 5c9c4a643f0215d2269bb0f47c5420740992662d Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 3 Dec 2023 01:25:36 -0500 Subject: [PATCH 38/47] increase budget again :( --- hypothesis-python/tests/numpy/test_from_dtype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/tests/numpy/test_from_dtype.py b/hypothesis-python/tests/numpy/test_from_dtype.py index e6f6865e0e..cd40a7d6d1 100644 --- a/hypothesis-python/tests/numpy/test_from_dtype.py +++ b/hypothesis-python/tests/numpy/test_from_dtype.py @@ -116,7 +116,7 @@ def cannot_encode(string): except UnicodeEncodeError: return True - find_any(nps.from_dtype(np.dtype("U")), cannot_encode, settings(max_examples=2000)) + find_any(nps.from_dtype(np.dtype("U")), cannot_encode, settings(max_examples=5000)) @given(st.data()) From f74b93f4a9a9bd9cef1fa2e22cbf4568bac3d5fe Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 3 Dec 2023 01:48:30 -0500 Subject: [PATCH 39/47] move assume to correct scope --- hypothesis-python/tests/conjecture/test_forced.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 18cd3e9d19..91e697550c 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -110,7 +110,6 @@ def test_forced_integer( width = max_value - min_value + 1 assume(width <= 1024) - assume((forced - shrink_towards).bit_length() < 128) weights = data.draw( st.lists( @@ -121,6 +120,8 @@ def test_forced_integer( ) ) + assume((forced - shrink_towards).bit_length() < 128) + data = fresh_data() assert ( data.draw_integer( From cc77fb13cf076b0febc5c65e95a7c46da434e269 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 7 Dec 2023 23:33:59 -0500 Subject: [PATCH 40/47] formatting --- .../src/hypothesis/internal/conjecture/data.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 15ab10b22b..0532591f86 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1018,11 +1018,8 @@ def draw_integer( probe = max_value + 1 while max_value < probe: self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = ( - self._draw_unbounded_integer( - forced=None if forced is None else forced - shrink_towards - ) - + shrink_towards + probe = shrink_towards + self._draw_unbounded_integer( + forced=None if forced is None else forced - shrink_towards ) self._cd.stop_example(discard=max_value < probe) return probe @@ -1032,11 +1029,8 @@ def draw_integer( probe = min_value - 1 while probe < min_value: self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = ( - self._draw_unbounded_integer( - forced=None if forced is None else forced - shrink_towards - ) - + shrink_towards + probe = shrink_towards + self._draw_unbounded_integer( + forced=None if forced is None else forced - shrink_towards ) self._cd.stop_example(discard=probe < min_value) return probe From a52bf7e170aa7305106fba1cb65526f40798b610 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 7 Dec 2023 23:54:41 -0500 Subject: [PATCH 41/47] simpler forced logic for floats --- .../src/hypothesis/internal/conjecture/data.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 0532591f86..52cdea6fd8 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1070,12 +1070,12 @@ def draw_float( while True: self._cd.start_example(FLOAT_STRATEGY_DO_DRAW_LABEL) - forced_i = None - if forced is not None: - forced_i = ( - nasty_floats.index(forced) + 1 if forced in nasty_floats else 0 - ) - + # If `forced in nasty_floats`, then `forced` was *probably* + # generated by drawing a nonzero index from the sampler. However, we + # have no obligation to generate it that way when forcing. In particular, + # i == 0 is able to produce all possible floats, and the forcing + # logic is simpler if we assume this choice. + forced_i = None if forced is None else 0 i = sampler.sample(self._cd, forced=forced_i) if sampler else 0 self._cd.start_example(DRAW_FLOAT_LABEL) if i == 0: @@ -1088,7 +1088,7 @@ def draw_float( else: assert pos_clamper is not None clamped = pos_clamper(result) - if clamped != result: + if clamped != result and not math.isnan(result): self._cd.stop_example(discard=True) self._cd.start_example(DRAW_FLOAT_LABEL) self._write_float(clamped) From 9a232f07a73c3a8b3d3ae3f16e4d0deabde46180 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 8 Dec 2023 00:04:30 -0500 Subject: [PATCH 42/47] fix generating nans when not allowed --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 52cdea6fd8..1bd9befdac 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1088,7 +1088,7 @@ def draw_float( else: assert pos_clamper is not None clamped = pos_clamper(result) - if clamped != result and not math.isnan(result): + if clamped != result and not (math.isnan(result) and allow_nan): self._cd.stop_example(discard=True) self._cd.start_example(DRAW_FLOAT_LABEL) self._write_float(clamped) From d9cdfe293fa04fa844f80e0fc4654e0c7f7c5399 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 8 Dec 2023 00:10:36 -0500 Subject: [PATCH 43/47] fix lte sign comparison --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 1bd9befdac..564af14886 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1165,7 +1165,7 @@ def _draw_float( Helper for draw_float which draws a random 64-bit float. """ if forced is not None: - forced_sign_bit = int(sign_aware_lte(forced, 0.0)) + forced_sign_bit = int(sign_aware_lte(forced, -0.0)) self._cd.start_example(DRAW_FLOAT_LABEL) try: From 76c37eb1942ddb09c491ed1d2761aea3a1fa1a14 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 8 Dec 2023 16:28:05 -0500 Subject: [PATCH 44/47] simpler forced float draw --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 564af14886..e08ca676cc 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1172,10 +1172,7 @@ def _draw_float( is_negative = self._cd.draw_bits(1, forced=forced_sign_bit) f = lex_to_float( self._cd.draw_bits( - 64, - forced=None - if forced is None - else float_to_lex(-forced if is_negative else forced), + 64, forced=None if forced is None else float_to_lex(abs(forced)) ) ) return -f if is_negative else f From 738b6c2bc94ddece16698794a2b8c6b937c99e1f Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 8 Dec 2023 23:38:41 -0800 Subject: [PATCH 45/47] Test forced-float edge cases --- .../tests/conjecture/test_forced.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index 91e697550c..d525e355a0 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -10,13 +10,15 @@ import math from random import Random +from hypothesis.internal.floats import SIGNALING_NAN, SMALLEST_SUBNORMAL import pytest import hypothesis.strategies as st -from hypothesis import HealthCheck, assume, given, settings +from hypothesis import HealthCheck, assume, example, given, settings from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData +from hypothesis.internal.conjecture.floats import float_to_lex from hypothesis.strategies._internal.lazy import unwrap_strategies @@ -188,13 +190,29 @@ def test_forced_bytes(forced): assert data.draw_bytes(len(forced)) == forced +@example(0.0) +@example(-0.0) +@example(1.0) +@example(1.2345) +@example(SMALLEST_SUBNORMAL) +@example(-SMALLEST_SUBNORMAL) +@example(100 * SMALLEST_SUBNORMAL) +@example(math.nan) +@example(-math.nan) +@example(SIGNALING_NAN) +@example(-SIGNALING_NAN) +@example(1e999) +@example(-1e999) @given(st.floats()) @settings(database=None) def test_forced_floats(forced): data = fresh_data() drawn = data.draw_float(forced=forced) - assert drawn == forced or (math.isnan(drawn) and math.isnan(forced)) + # Bitwise equality check to handle nan, snan, -nan, +0, -0, etc. + assert math.copysign(1, drawn) == math.copysign(1, forced) + assert float_to_lex(abs(drawn)) == float_to_lex(abs(forced)) data = ConjectureData.for_buffer(data.buffer) drawn = data.draw_float() - assert drawn == forced or (math.isnan(drawn) and math.isnan(forced)) + assert math.copysign(1, drawn) == math.copysign(1, forced) + assert float_to_lex(abs(drawn)) == float_to_lex(abs(forced)) From f52c19867f7a7798d63dfaf1fe63d62baf7072cd Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 9 Dec 2023 17:39:31 -0500 Subject: [PATCH 46/47] formatting --- hypothesis-python/tests/conjecture/test_forced.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index d525e355a0..4ac8c3a11a 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -10,7 +10,6 @@ import math from random import Random -from hypothesis.internal.floats import SIGNALING_NAN, SMALLEST_SUBNORMAL import pytest @@ -19,6 +18,7 @@ from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.conjecture.floats import float_to_lex +from hypothesis.internal.floats import SIGNALING_NAN, SMALLEST_SUBNORMAL from hypothesis.strategies._internal.lazy import unwrap_strategies From 0f0ebcce0a510fc63ce36581a300caab870cf30a Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sat, 9 Dec 2023 17:39:49 -0500 Subject: [PATCH 47/47] correct forced_sign_bit handling for nans --- hypothesis-python/src/hypothesis/internal/conjecture/data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index e08ca676cc..74f7f50af0 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1165,7 +1165,9 @@ def _draw_float( Helper for draw_float which draws a random 64-bit float. """ if forced is not None: - forced_sign_bit = int(sign_aware_lte(forced, -0.0)) + # sign_aware_lte(forced, -0.0) does not correctly handle the + # math.nan case here. + forced_sign_bit = math.copysign(1, forced) == -1 self._cd.start_example(DRAW_FLOAT_LABEL) try: