Skip to content

Commit

Permalink
dont redistribute over nan, inf, or imprecise floats
Browse files Browse the repository at this point in the history
  • Loading branch information
tybug committed Jan 8, 2025
1 parent 4348fa7 commit 4e3221f
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 6 deletions.
18 changes: 16 additions & 2 deletions hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 collections import defaultdict
from collections.abc import Sequence
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union
Expand Down Expand Up @@ -45,6 +46,7 @@
prefix_selection_order,
random_selection_order,
)
from hypothesis.internal.floats import MAX_PRECISE_INTEGER

if TYPE_CHECKING:
from random import Random
Expand Down Expand Up @@ -1237,17 +1239,29 @@ def redistribute_numeric_pairs(self, chooser):
"""If there is a sum of generated numbers that we need their sum
to exceed some bound, lowering one of them requires raising the
other. This pass enables that."""

# look for a pair of nodes (node1, node2) which are both numeric
# and aren't separated by too many other nodes. We'll decrease node1 and
# increase node2 (note that the other way around doesn't make sense as
# it's strictly worse in the ordering).
def can_choose_node(node):
# don't choose nan, inf, or floats above the threshold where f + 1 > f
# (which is not necessarily true for floats above MAX_PRECISE_INTEGER).
# The motivation for the last condition is to avoid trying weird
# non-shrinks where we raise one node and think we lowered another
# (but didn't).
return node.ir_type in {"integer", "float"} and not (
node.ir_type == "float"
and (math.isnan(node.value) or abs(node.value) > MAX_PRECISE_INTEGER)
)

node1 = chooser.choose(
self.nodes,
lambda node: node.ir_type in {"integer", "float"} and not node.trivial,
lambda node: can_choose_node(node) and not node.trivial,
)
node2 = chooser.choose(
self.nodes,
lambda node: node.ir_type in {"integer", "float"}
lambda node: can_choose_node(node)
# Note that it's fine for node2 to be trivial, because we're going to
# explicitly make it *not* trivial by adding to its value.
and not node.was_forced
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
from hypothesis.internal.conjecture.floats import float_to_lex
from hypothesis.internal.conjecture.shrinking.common import Shrinker
from hypothesis.internal.conjecture.shrinking.integer import Integer

MAX_PRECISE_INTEGER = 2**53
from hypothesis.internal.floats import MAX_PRECISE_INTEGER


class Float(Shrinker):
Expand Down
1 change: 1 addition & 0 deletions hypothesis-python/src/hypothesis/internal/floats.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,5 +219,6 @@ def clamp(lower: float, value: float, upper: float) -> float:

SMALLEST_SUBNORMAL = next_up(0.0)
SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa
MAX_PRECISE_INTEGER = 2**53
assert math.isnan(SIGNALING_NAN)
assert math.copysign(1, SIGNALING_NAN) == 1
41 changes: 39 additions & 2 deletions hypothesis-python/tests/conjecture/test_shrinker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

import pytest

from hypothesis import assume, given, strategies as st
from hypothesis.internal.conjecture.data import ConjectureData
from hypothesis.internal.conjecture.datatree import compute_max_children
from hypothesis.internal.conjecture.engine import ConjectureRunner
from hypothesis.internal.conjecture.shrinker import (
Shrinker,
Expand All @@ -22,7 +24,13 @@
)
from hypothesis.internal.conjecture.utils import Sampler

from tests.conjecture.common import SOME_LABEL, ir, run_to_nodes, shrinking_from
from tests.conjecture.common import (
SOME_LABEL,
ir,
ir_nodes,
run_to_nodes,
shrinking_from,
)


@pytest.mark.parametrize("n", [1, 5, 8, 15])
Expand Down Expand Up @@ -505,7 +513,7 @@ def shrinker(data: ConjectureData):
assert shrinker.choices == (1, 0) + (0,) * n_gap + (1,)


def test_redistribute_pairs_with_forced_node_integer():
def test_redistribute_with_forced_node_integer():
@shrinking_from(ir(15, 10))
def shrinker(data: ConjectureData):
n1 = data.draw_integer(0, 100)
Expand All @@ -518,3 +526,32 @@ def shrinker(data: ConjectureData):
# shrinking. Since the second draw is forced, this isn't possible to shrink
# with just this pass.
assert shrinker.choices == (15, 10)


numeric_nodes = ir_nodes(ir_types=["integer", "float"])


@given(numeric_nodes, numeric_nodes, st.integers() | st.floats(allow_nan=False))
def test_redistribute_numeric_pairs(node1, node2, stop):
assume(node1.value + node2.value > stop)
# avoid exhausting the tree while generating, which causes @shrinking_from's
# runner to raise
assume(
compute_max_children(node1.ir_type, node1.kwargs)
+ compute_max_children(node2.ir_type, node2.kwargs)
> 2
)

@shrinking_from([node1, node2])
def shrinker(data: ConjectureData):
v1 = getattr(data, f"draw_{node1.ir_type}")(**node1.kwargs)
v2 = getattr(data, f"draw_{node2.ir_type}")(**node2.kwargs)
if v1 + v2 > stop:
data.mark_interesting()

shrinker.fixate_shrink_passes(["redistribute_numeric_pairs"])
assert len(shrinker.choices) == 2
# we should always have lowered the first choice and raised the second choice
# - or left the choices the same.
assert shrinker.choices[0] <= node1.value
assert shrinker.choices[1] >= node2.value

0 comments on commit 4e3221f

Please sign in to comment.