Repository: HypothesisWorks/hypothesis

Issue:
Rare float overflow bug in complex_numbers strategy
[Hypothesis 3.86.0](https://hypothesis.readthedocs.io/en/latest/changes.html#v3-86-0) made `floats(min_value=inf, allow_infinity=False)` an error, because the arguments are contradictory and thus impossible to satisfy.

Many test runs later, it turns out that this condition can sometimes be triggered by the following test:

https://github.com/HypothesisWorks/hypothesis/blob/3c23c07ff3ca8401b36bd847e8fbd6a2096a09b6/hypothesis-python/tests/cover/test_complex_numbers.py#L96-L98

Step one is obviously to write a test which can *quickly* trigger this bug (e.g. by only passing very large magnitude bounds), but by inspection I'm pretty sure it's somewhere in the following lines:

https://github.com/HypothesisWorks/hypothesis/blob/3c23c07ff3ca8401b36bd847e8fbd6a2096a09b6/hypothesis-python/src/hypothesis/_strategies.py#L2011-L2021


Discussion:
Three times today :sob: 

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/_strategies.py b/hypothesis-python/src/hypothesis/_strategies.py
--- a/hypothesis-python/src/hypothesis/_strategies.py
+++ b/hypothesis-python/src/hypothesis/_strategies.py
@@ -1878,13 +1878,13 @@ def constrained_complex(draw):
         # this and the max_magnitude
         if max_magnitude is None:
             zi = draw(floats(**allow_kw))
-            rmax = float("inf")
+            rmax = None
         else:
             zi = draw(floats(-max_magnitude, max_magnitude, **allow_kw))
             rmax = cathetus(max_magnitude, zi)
         # Draw the real part from the allowed range given the imaginary part
         if min_magnitude is None or math.fabs(zi) >= min_magnitude:
-            zr = draw(floats(-rmax, rmax, **allow_kw))
+            zr = draw(floats(None if rmax is None else -rmax, rmax, **allow_kw))
         else:
             zr = draw(floats(cathetus(min_magnitude, zi), rmax, **allow_kw))
         # Order of conditions carefully tuned so that for a given pair of
diff --git a/hypothesis-python/src/hypothesis/internal/cathetus.py b/hypothesis-python/src/hypothesis/internal/cathetus.py
--- a/hypothesis-python/src/hypothesis/internal/cathetus.py
+++ b/hypothesis-python/src/hypothesis/internal/cathetus.py
@@ -55,13 +55,17 @@ def cathetus(h, a):
     if h < a:
         return float(u"nan")
 
+    # Thanks to floating-point precision issues when performing multiple
+    # operations on extremely large or small values, we may rarely calculate
+    # a side length that is longer than the hypotenuse.  This is clearly an
+    # error, so we clip to the hypotenuse as the best available estimate.
     if h > sqrt(float_info.max):
         if h > float_info.max / 2:
-            return sqrt(h - a) * sqrt(h / 2 + a / 2) * sqrt(2)
+            b = sqrt(h - a) * sqrt(h / 2 + a / 2) * sqrt(2)
         else:
-            return sqrt(h - a) * sqrt(h + a)
-
-    if h < sqrt(float_info.min):
-        return sqrt(h - a) * sqrt(h + a)
-
-    return sqrt((h - a) * (h + a))
+            b = sqrt(h - a) * sqrt(h + a)
+    elif h < sqrt(float_info.min):
+        b = sqrt(h - a) * sqrt(h + a)
+    else:
+        b = sqrt((h - a) * (h + a))
+    return min(b, h)


Test patch:
diff --git a/hypothesis-python/tests/cover/test_cathetus.py b/hypothesis-python/tests/cover/test_cathetus.py
--- a/hypothesis-python/tests/cover/test_cathetus.py
+++ b/hypothesis-python/tests/cover/test_cathetus.py
@@ -23,7 +23,9 @@
 
 import pytest
 
+from hypothesis import assume, given
 from hypothesis.internal.cathetus import cathetus
+from hypothesis.strategies import floats
 
 
 def test_cathetus_subnormal_underflow():
@@ -99,6 +101,17 @@ def test_cathetus_signs(h, a, b):
     assert abs(cathetus(h, a) - b) <= abs(b) * float_info.epsilon
 
 
+@given(
+    h=floats(0) | floats(min_value=1e308, allow_infinity=False),
+    a=floats(0, allow_infinity=False)
+    | floats(min_value=0, max_value=1e250, allow_infinity=False),
+)
+def test_cathetus_always_leq_hypot(h, a):
+    assume(h >= a)
+    b = cathetus(h, a)
+    assert 0 <= b <= h
+
+
 @pytest.mark.parametrize(
     "a,b,h",
     [