# 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/.
import math
import sys
from collections.abc import Collection, Iterable, Sequence
from typing import Any, Optional
from hypothesis import (
HealthCheck,
assume,
note,
settings as Settings,
strategies as st,
)
from hypothesis.errors import BackendCannotProceed
from hypothesis.internal.compat import batched
from hypothesis.internal.conjecture.choice import (
ChoiceTypeT,
choice_permitted,
)
from hypothesis.internal.conjecture.data import ConjectureData
from hypothesis.internal.conjecture.providers import (
COLLECTION_DEFAULT_MAX_SIZE,
PrimitiveProvider,
)
from hypothesis.internal.floats import SMALLEST_SUBNORMAL, sign_aware_lte
from hypothesis.internal.intervalsets import IntervalSet
from hypothesis.stateful import RuleBasedStateMachine, initialize, precondition, rule
from hypothesis.strategies import DrawFn, SearchStrategy
from hypothesis.strategies._internal.strings import OneCharStringStrategy, TextStrategy
def build_intervals(intervals: list[int]) -> list[tuple[int, int]]:
if len(intervals) % 2:
intervals = intervals[:-1]
intervals.sort()
return list(batched(intervals, 2, strict=True))
def interval_lists(
*, min_codepoint: int = 0, max_codepoint: int = sys.maxunicode, min_size: int = 0
) -> SearchStrategy[Iterable[Sequence[int]]]:
return (
st.lists(
st.integers(min_codepoint, max_codepoint),
unique=True,
min_size=min_size * 2,
)
.map(sorted)
.map(build_intervals)
)
def intervals(
*, min_codepoint: int = 0, max_codepoint: int = sys.maxunicode, min_size: int = 0
) -> SearchStrategy[IntervalSet]:
return st.builds(
IntervalSet,
interval_lists(
min_codepoint=min_codepoint, max_codepoint=max_codepoint, min_size=min_size
),
)
@st.composite
def integer_weights(
draw: DrawFn, min_value: Optional[int] = None, max_value: Optional[int] = None
) -> dict[int, float]:
# Sampler doesn't play well with super small floats, so exclude them
weights = draw(
st.dictionaries(
st.integers(min_value=min_value, max_value=max_value),
st.floats(0.001, 1),
min_size=1,
max_size=255,
)
)
# invalid to have a weighting that disallows all possibilities
assume(sum(weights.values()) != 0)
# re-normalize probabilities to sum to some arbitrary target < 1
target = draw(st.floats(0.001, 0.999))
factor = target / sum(weights.values())
weights = {k: v * factor for k, v in weights.items()}
# float rounding error can cause this to fail.
assume(0.001 <= sum(weights.values()) <= 0.999)
return weights
@st.composite
def integer_constraints(
draw,
*,
use_min_value=None,
use_max_value=None,
use_shrink_towards=None,
use_weights=None,
use_forced=False,
):
min_value = None
max_value = None
shrink_towards = 0
weights = None
if use_min_value is None:
use_min_value = draw(st.booleans())
if use_max_value is None:
use_max_value = draw(st.booleans())
use_shrink_towards = draw(st.booleans())
if use_weights is None:
use_weights = (
draw(st.booleans()) if (use_min_value and use_max_value) else False
)
# Invariants:
# (1) min_value <= forced <= max_value
# (2) sum(weights.values()) < 1
# (3) len(weights) <= 255
if use_shrink_towards:
shrink_towards = draw(st.integers())
forced = draw(st.integers()) if use_forced else None
if use_weights:
assert use_max_value
assert use_min_value
min_value = draw(st.integers(max_value=forced))
min_val = max(min_value, forced) if forced is not None else min_value
max_value = draw(st.integers(min_value=min_val))
weights = draw(integer_weights(min_value, max_value))
else:
if use_min_value:
min_value = draw(st.integers(max_value=forced))
if use_max_value:
min_vals = []
if min_value is not None:
min_vals.append(min_value)
if forced is not None:
min_vals.append(forced)
min_val = max(min_vals) if min_vals else None
max_value = draw(st.integers(min_value=min_val))
if forced is not None:
assume((forced - shrink_towards).bit_length() < 128)
return {
"min_value": min_value,
"max_value": max_value,
"shrink_towards": shrink_towards,
"weights": weights,
"forced": forced,
}
@st.composite
def _collection_constraints(
draw: DrawFn,
*,
forced: Optional[Any],
use_min_size: Optional[bool] = None,
use_max_size: Optional[bool] = None,
) -> dict[str, int]:
min_size = 0
max_size = COLLECTION_DEFAULT_MAX_SIZE
# collections are quite expensive in entropy. cap to avoid overruns.
cap = 50
if use_min_size is None:
use_min_size = draw(st.booleans())
if use_max_size is None:
use_max_size = draw(st.booleans())
if use_min_size:
min_size = draw(
st.integers(0, min(len(forced), cap) if forced is not None else cap)
)
if use_max_size:
max_size = draw(
st.integers(
min_value=min_size if forced is None else max(min_size, len(forced))
)
)
if forced is None:
# cap to some reasonable max size to avoid overruns.
max_size = min(max_size, min_size + 100)
return {"min_size": min_size, "max_size": max_size}
@st.composite
def string_constraints(
draw: DrawFn,
*,
use_min_size: Optional[bool] = None,
use_max_size: Optional[bool] = None,
use_forced: bool = False,
) -> Any:
interval_set = draw(intervals())
forced = (
draw(TextStrategy(OneCharStringStrategy(interval_set))) if use_forced else None
)
constraints = draw(
_collection_constraints(
forced=forced, use_min_size=use_min_size, use_max_size=use_max_size
)
)
# if the intervalset is empty, then the min size must be zero, because the
# only valid value is the empty string.
if len(interval_set) == 0:
constraints["min_size"] = 0
return {"intervals": interval_set, "forced": forced, **constraints}
@st.composite
def bytes_constraints(
draw: DrawFn,
*,
use_min_size: Optional[bool] = None,
use_max_size: Optional[bool] = None,
use_forced: bool = False,
) -> Any:
forced = draw(st.binary()) if use_forced else None
constraints = draw(
_collection_constraints(
forced=forced, use_min_size=use_min_size, use_max_size=use_max_size
)
)
return {"forced": forced, **constraints}
@st.composite
def float_constraints(
draw,
*,
use_min_value=None,
use_max_value=None,
use_forced=False,
):
if use_min_value is None:
use_min_value = draw(st.booleans())
if use_max_value is None:
use_max_value = draw(st.booleans())
forced = draw(st.floats()) if use_forced else None
pivot = forced if (use_forced and not math.isnan(forced)) else None
min_value = -math.inf
max_value = math.inf
smallest_nonzero_magnitude = SMALLEST_SUBNORMAL
allow_nan = True if (use_forced and math.isnan(forced)) else draw(st.booleans())
if use_min_value:
min_value = draw(st.floats(max_value=pivot, allow_nan=False))
if use_max_value:
if pivot is None:
min_val = min_value
else:
min_val = pivot if sign_aware_lte(min_value, pivot) else min_value
max_value = draw(st.floats(min_value=min_val, allow_nan=False))
largest_magnitude = max(abs(min_value), abs(max_value))
# can't force something smaller than our smallest magnitude.
if pivot is not None and pivot != 0.0:
largest_magnitude = min(largest_magnitude, pivot)
# avoid drawing from an empty range
if largest_magnitude > 0:
smallest_nonzero_magnitude = draw(
st.floats(
min_value=0,
# smallest_nonzero_magnitude breaks internal clamper invariants if
# it is allowed to be larger than the magnitude of {min, max}_value.
#
# Let's also be reasonable here; smallest_nonzero_magnitude is used
# for subnormals, so we will never provide a number above 1 in practice.
max_value=min(largest_magnitude, 1.0),
exclude_min=True,
)
)
assert sign_aware_lte(min_value, max_value)
return {
"min_value": min_value,
"max_value": max_value,
"forced": forced,
"allow_nan": allow_nan,
"smallest_nonzero_magnitude": smallest_nonzero_magnitude,
}
@st.composite
def boolean_constraints(draw: DrawFn, *, use_forced: bool = False) -> Any:
forced = draw(st.booleans()) if use_forced else None
# avoid invalid forced combinations
p = draw(st.floats(0, 1, exclude_min=forced is True, exclude_max=forced is False))
return {"p": p, "forced": forced}
def constraints_strategy(choice_type, strategy_constraints=None, *, use_forced=False):
strategy = {
"boolean": boolean_constraints,
"integer": integer_constraints,
"float": float_constraints,
"bytes": bytes_constraints,
"string": string_constraints,
}[choice_type]
if strategy_constraints is None:
strategy_constraints = {}
return strategy(**strategy_constraints.get(choice_type, {}), use_forced=use_forced)
def choice_types_constraints(strategy_constraints=None, *, use_forced=False):
options: list[ChoiceTypeT] = ["boolean", "integer", "float", "bytes", "string"]
return st.one_of(
st.tuples(
st.just(name),
constraints_strategy(name, strategy_constraints, use_forced=use_forced),
)
for name in options
)