Repository: HypothesisWorks/hypothesis

Issue:
`st.integers()` never generates examples close to the `max_value`
Suppose I have some function that takes numbers and has a large, but finite max value for which it is valid. Numbers higher than this value cause it to throw an exception. Numbers less than or equal to to this value are valid, but the function might exhibit weird/pathological behavior near to the boundary, which would require careful testing (e.g. numerical instability, or a discontinuity at the boundary, or something). My expectation was that if I use `given(integers(max_value=upper_bound))`, then `hypothesis` would duly generate at least some numbers near that upper bound to see what happens there. But that isn't true:

```python
lower_bound = 15
upper_bound = 123456789

@given(foo=integers(min_value=lower_bound, max_value=upper_bound))
@settings(max_examples=5000)
def test_foo(foo):
    if abs(foo - upper_bound) < 10:
        assert False, "hit near to upper bound"
    
    if abs(foo - lower_bound) < 10:
        print("hit near to lower bound")
        
    assert True
```

This prints out lots of `"hit near to lower bound"`, but it never triggers the `"hit near to upper bound"` exception. If there is some implementation failure near the upper bound, hypothesis would not find it. Is there a way to make it give a bit of extra attention to the upper boundary? For a while I assumed that hypothesis did this already and was surprised that it didn't.

I am using hypothesis version 6.10.0.


Discussion:
This is... harder than it sounds, at least while maintaining performance, though it would probably be useful too.

FWIW you're probably seeing only *exactly* the lower bound, and not anything close to it - we generate minimal parts much more often than chance, but not near-minimal.  (a heuristic which helps us to generate a variety of 'tree depths' without exceeding sensible 'tree sizes')
Related to #2671 and #2907.
There is something I am missing. For things like integers, why isn't the upper bound **itself** tested for explicitly?

Changing the example above, it is fairly easy to see that the upper bound isn't picked as a special case:

```py
from hypothesis import strategies as st, given, settings


lower_bound = 0
upper_bound = 100

@given(foo=st.integers(min_value=lower_bound, max_value=upper_bound))
@settings(max_examples=10)
def test_foo(foo):
    if foo == upper_bound:
        assert False, "hit the upper bound"
```
*Neither* bound is special - only zero, as the minimal example!  (or the bound closest to zero, if zero is disallowed).

The solution would be to expand [`IntegerStrategy.do_draw()`](https://github.com/HypothesisWorks/hypothesis/blob/eed37f681b2baf007034a453193d8e314c3c15f4/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py#L93) to do something fancier in the bounded case - e.g. "if the bounds are not together, pick a number in [0, 127], and map the high options to `(min, min, max, max, min+1, max-1)`; otherwise generate as usual".  See `FloatStrategy.do_draw()` for an example of this style (it's tricky!); note that we don't want to push that down further where it would distort e.g. `characters()`.
> _Neither_ bound is special

If there are bounds, they are _very likely_ to be special! I would even argue that zero is quite uninteresting in a case that is bounded between `-5` and `10`, for example.
Oops, unclear phrasing! I agree that we *should* treat bounds (and off-by-one!) specially; I meant that we currently *don't* do that. 

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
--- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
@@ -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
@@ -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
@@ -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.
@@ -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:
@@ -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")
@@ -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:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
@@ -90,7 +90,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):
         kwargs, pred = get_integer_predicate_bounds(condition)


Test patch:
diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py
--- a/hypothesis-python/tests/array_api/test_arrays.py
+++ b/hypothesis-python/tests/array_api/test_arrays.py
@@ -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
 
diff --git a/hypothesis-python/tests/cover/test_filtered_strategy.py b/hypothesis-python/tests/cover/test_filtered_strategy.py
--- a/hypothesis-python/tests/cover/test_filtered_strategy.py
+++ b/hypothesis-python/tests/cover/test_filtered_strategy.py
@@ -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
 
diff --git a/hypothesis-python/tests/quality/test_integers.py b/hypothesis-python/tests/quality/test_integers.py
--- a/hypothesis-python/tests/quality/test_integers.py
+++ b/hypothesis-python/tests/quality/test_integers.py
@@ -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}"
+        )