Skip to content

Commit

Permalink
Merge pull request #3414 from Zac-HD/integer-boundaries
Browse files Browse the repository at this point in the history
Generate integer bounds more often
  • Loading branch information
Zac-HD authored Jul 19, 2022
2 parents 770f898 + af892ce commit 6db8e03
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 13 deletions.
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch makes :func:`~hypothesis.strategies.integers` more likely to
generate boundary values for large two-sided intervals (:issue:`2942`).
1 change: 1 addition & 0 deletions hypothesis-python/scripts/basic-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ if [ "$(python -c 'import platform; print(platform.python_implementation())')" !
pip uninstall -y django pytz

pip install "$(grep 'numpy==' ../requirements/coverage.txt)"
$PYTEST tests/array_api
$PYTEST tests/numpy

pip install "$(grep 'pandas==' ../requirements/coverage.txt)"
Expand Down
23 changes: 14 additions & 9 deletions hypothesis-python/src/hypothesis/internal/conjecture/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,14 @@ def unbounded_integers(data: "ConjectureData") -> int:


def integer_range(
data: "ConjectureData", lower: int, upper: int, center: Optional[int] = None
data: "ConjectureData",
lower: int,
upper: int,
center: Optional[int] = None,
forced: Optional[int] = None,
) -> int:
assert lower <= upper
assert forced is None or lower <= forced <= upper
if lower == upper:
# Write a value even when this is trivial so that when a bound depends
# on other values we don't suddenly disappear when the gap shrinks to
Expand All @@ -83,7 +88,8 @@ def integer_range(
elif center == lower:
above = True
else:
above = not boolean(data)
force_above = None if forced is None else forced < center
above = not data.draw_bits(1, forced=force_above)

if above:
gap = upper - center
Expand All @@ -95,7 +101,7 @@ def integer_range(
bits = gap.bit_length()
probe = gap + 1

if bits > 24 and data.draw_bits(3):
if bits > 24 and data.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.
Expand All @@ -104,7 +110,9 @@ def integer_range(

while probe > gap:
data.start_example(INTEGER_RANGE_DRAW_LABEL)
probe = data.draw_bits(bits)
probe = data.draw_bits(
bits, forced=None if forced is None else abs(forced - center)
)
data.stop_example(discard=probe > gap)

if above:
Expand All @@ -113,7 +121,8 @@ def integer_range(
result = center - probe

assert lower <= result <= upper
return int(result)
assert forced is None or result == forced, (result, forced, center, above)
return result


T = TypeVar("T")
Expand Down Expand Up @@ -161,10 +170,6 @@ def fractional_float(data: "ConjectureData") -> float:
return (int_to_float(FLOAT_PREFIX | data.draw_bits(52)) - 1) / FULL_FLOAT


def boolean(data: "ConjectureData") -> bool:
return bool(data.draw_bits(1))


def biased_coin(
data: "ConjectureData", p: float, *, forced: Optional[bool] = None
) -> bool:
Expand Down
14 changes: 13 additions & 1 deletion hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,19 @@ def do_draw(self, data):
data.stop_example(discard=probe < self.start)
return probe

return d.integer_range(data, self.start, self.end, center=0)
# For bounded integers, make the bounds and near-bounds more likely.
forced = None
if self.end - self.start > 127:
forced = {
122: self.start,
123: self.start,
124: self.end,
125: self.end,
126: self.start + 1,
127: self.end - 1,
}.get(data.draw_bits(7))

return d.integer_range(data, self.start, self.end, center=0, forced=forced)

def filter(self, condition):
if condition is math.isfinite:
Expand Down
3 changes: 1 addition & 2 deletions hypothesis-python/tests/array_api/test_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,7 @@ def test_excluded_min_in_float_arrays(xp, xps, dtype, low, data):
@st.composite
def distinct_int64_integers(draw):
used = draw(st.shared(st.builds(set), key="distinct_int64_integers.used"))
int64_max = 2**63
i = draw(st.integers(1 - int64_max, int64_max).filter(lambda x: x not in used))
i = draw(st.integers(-(2**63), 2**63 - 1).filter(lambda x: x not in used))
used.add(i)
return i

Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/tests/cover/test_filtered_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test_filter_iterations_are_marked_as_discarded():
variable_equal_to_zero = 0 # non-local references disables filter-rewriting
x = st.integers(0, 255).filter(lambda x: x == variable_equal_to_zero)

data = ConjectureData.for_buffer([2, 1, 0])
data = ConjectureData.for_buffer([0, 2, 1, 0])

assert data.draw(x) == 0

Expand Down
17 changes: 17 additions & 0 deletions hypothesis-python/tests/quality/test_integers.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,20 @@ def f(data):
bytes_needed = actual_bits_needed // 8
# 3 extra bytes: two for the sampler, one for the capping value.
assert len(v.buffer) == 3 + bytes_needed


def test_generates_boundary_values_even_when_unlikely():
r = Random()
trillion = 10**12
strat = st.integers(-trillion, trillion)
boundary_vals = {-trillion, -trillion + 1, trillion - 1, trillion}
for _ in range(10_000):
buffer = bytes(r.randrange(0, 255) for _ in range(1000))
val = ConjectureData.for_buffer(buffer).draw(strat)
boundary_vals.discard(val)
if not boundary_vals:
break
else:
raise AssertionError(
f"Expected to see all boundary vals, but still have {boundary_vals}"
)

0 comments on commit 6db8e03

Please sign in to comment.