Skip to content

Commit 2231eea

Browse files
committed
Merge branch 'master' into crosshair-event
2 parents 831ed06 + 3605694 commit 2231eea

5 files changed

Lines changed: 87 additions & 17 deletions

File tree

hypothesis-python/docs/changelog.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ Hypothesis 6.x
1818

1919
.. include:: ../RELEASE.rst
2020

21+
.. _v6.148.5:
22+
23+
--------------------
24+
6.148.5 - 2025-12-01
25+
--------------------
26+
27+
This patch improves the error message for :class:`~hypothesis.errors.FlakyStrategyDefinition`
28+
when the precondition for a rule is flaky (:issue:`4206`).
29+
2130
.. _v6.148.4:
2231

2332
--------------------

hypothesis-python/src/hypothesis/stateful.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Notably, the set of steps available at any point may depend on the
1616
execution to date.
1717
"""
18+
1819
import collections
1920
import dataclasses
2021
import inspect
@@ -35,7 +36,11 @@
3536
)
3637
from hypothesis.control import _current_build_context, current_build_context
3738
from hypothesis.core import TestFunc, given
38-
from hypothesis.errors import InvalidArgument, InvalidDefinition
39+
from hypothesis.errors import (
40+
FlakyStrategyDefinition,
41+
InvalidArgument,
42+
InvalidDefinition,
43+
)
3944
from hypothesis.internal.compat import add_note, batched
4045
from hypothesis.internal.conjecture.engine import BUFFER_SIZE
4146
from hypothesis.internal.conjecture.junkdrawer import gc_cumulative_time
@@ -95,7 +100,11 @@ def __delete__(self, obj):
95100
raise AttributeError("Cannot delete TestCase")
96101

97102

98-
def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0):
103+
def get_state_machine_test(
104+
state_machine_factory, *, settings=None, _min_steps=0, _flaky_state=None
105+
):
106+
# This function is split out from run_state_machine_as_test so that
107+
# HypoFuzz can get and call the test function directly.
99108
if settings is None:
100109
try:
101110
settings = state_machine_factory.TestCase.settings
@@ -108,6 +117,7 @@ def get_state_machine_test(state_machine_factory, *, settings=None, _min_steps=0
108117
# Because settings can vary via e.g. profiles, settings.stateful_step_count
109118
# overrides this argument and we don't bother cross-validating.
110119
raise InvalidArgument(f"_min_steps={_min_steps} must be non-negative.")
120+
_flaky_state = _flaky_state or {}
111121

112122
@settings
113123
@given(st.data())
@@ -161,6 +171,7 @@ def output(s):
161171

162172
# Choose a rule to run, preferring an initialize rule if there are
163173
# any which have not been run yet.
174+
_flaky_state["selecting_rule"] = True
164175
if machine._initialize_rules_to_run:
165176
init_rules = [
166177
st.tuples(st.just(rule), st.fixed_dictionaries(rule.arguments))
@@ -170,6 +181,7 @@ def output(s):
170181
machine._initialize_rules_to_run.remove(rule)
171182
else:
172183
rule, data = cd.draw(machine._rules_strategy)
184+
_flaky_state["selecting_rule"] = False
173185
draw_label = f"generate:rule:{rule.function.__name__}"
174186
cd.draw_times.setdefault(draw_label, 0.0)
175187
in_gctime = gc_cumulative_time() - start_gc
@@ -250,10 +262,23 @@ def run_state_machine_as_test(state_machine_factory, *, settings=None, _min_step
250262
RuleBasedStateMachine when called with no arguments - it can be a class or a
251263
function. settings will be used to control the execution of the test.
252264
"""
265+
flaky_state = {"selecting_rule": False}
253266
state_machine_test = get_state_machine_test(
254-
state_machine_factory, settings=settings, _min_steps=_min_steps
267+
state_machine_factory,
268+
settings=settings,
269+
_min_steps=_min_steps,
270+
_flaky_state=flaky_state,
255271
)
256-
state_machine_test()
272+
try:
273+
state_machine_test()
274+
except FlakyStrategyDefinition as err:
275+
if flaky_state["selecting_rule"]:
276+
add_note(
277+
err,
278+
"while selecting a rule to run. This is usually caused by "
279+
"a flaky precondition, or a bundle that was unexpectedly empty.",
280+
)
281+
raise
257282

258283

259284
class StateMachineMeta(type):

hypothesis-python/src/hypothesis/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

11-
__version_info__ = (6, 148, 4)
11+
__version_info__ = (6, 148, 5)
1212
__version__ = ".".join(map(str, __version_info__))

hypothesis-python/tests/cover/test_composite.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

11-
import sys
1211
import typing
1312

1413
import pytest
@@ -196,21 +195,14 @@ def test_drawfn_cannot_be_instantiated():
196195

197196

198197
def test_warns_on_strategy_annotation():
199-
# TODO: print the stack on Python 3.10 and 3.11 to determine the appropriate
200-
# stack depth to use. Consider adding a debug-print if IN_COVERAGE_TESTS
201-
# and the relevant depth is_hypothesis_file(), for easier future fixing.
202-
#
203-
# Meanwhile, the test is not skipped on 3.10/3.11 as it is still required for
204-
# coverage of the warning-generating branch.
205198
with pytest.warns(HypothesisWarning, match="Return-type annotation") as w:
206199

207200
@st.composite
208201
def my_integers(draw: st.DrawFn) -> st.SearchStrategy[int]:
209202
return draw(st.integers())
210203

211-
if sys.version_info[:2] > (3, 11): # TEMP: see PR #3961
212-
assert len(w.list) == 1
213-
assert w.list[0].filename == __file__ # check stacklevel points to user code
204+
assert len(w.list) == 1
205+
assert w.list[0].filename == __file__ # check stacklevel points to user code
214206

215207

216208
def test_composite_allows_overload_without_draw():

hypothesis-python/tests/cover/test_stateful.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,18 @@
2828
from hypothesis.control import current_build_context
2929
from hypothesis.core import encode_failure
3030
from hypothesis.database import InMemoryExampleDatabase
31-
from hypothesis.errors import DidNotReproduce, Flaky, InvalidArgument, InvalidDefinition
31+
from hypothesis.errors import (
32+
DidNotReproduce,
33+
Flaky,
34+
FlakyStrategyDefinition,
35+
InvalidArgument,
36+
InvalidDefinition,
37+
)
3238
from hypothesis.stateful import (
3339
Bundle,
3440
RuleBasedStateMachine,
3541
consumes,
42+
get_state_machine_test,
3643
initialize,
3744
invariant,
3845
multiple,
@@ -151,6 +158,44 @@ def test_flaky_raises_flaky():
151158
FlakyStateMachine.TestCase().runTest()
152159

153160

161+
class FlakyPreconditionMachine(RuleBasedStateMachine):
162+
@precondition(lambda self: not current_build_context().is_final)
163+
@rule()
164+
def action(self):
165+
raise AssertionError
166+
167+
168+
def test_flaky_precondition_error_message():
169+
with raises(FlakyStrategyDefinition) as exc_info:
170+
FlakyPreconditionMachine.TestCase().runTest()
171+
assert any("flaky precondition" in note for note in exc_info.value.__notes__)
172+
173+
174+
class FlakyDrawInRuleMachine(RuleBasedStateMachine):
175+
# Flakiness inside rule execution (via data().draw()) happens AFTER rule selection,
176+
# so the "flaky precondition" note should NOT be added.
177+
@rule(d=data())
178+
def action(self, d):
179+
if current_build_context().is_final:
180+
d.draw(st.integers(0, 0))
181+
d.draw(st.integers())
182+
raise AssertionError
183+
184+
185+
def test_flaky_draw_in_rule_no_precondition_note():
186+
# When flakiness occurs during rule execution (not rule selection),
187+
# the error message should NOT mention flaky preconditions.
188+
with raises(FlakyStrategyDefinition) as exc_info:
189+
FlakyDrawInRuleMachine.TestCase().runTest()
190+
notes = getattr(exc_info.value, "__notes__", [])
191+
assert not any("flaky precondition" in note for note in notes)
192+
193+
194+
def test_get_state_machine_test_is_importable():
195+
# Regression test: get_state_machine_test is used by HypoFuzz
196+
assert callable(get_state_machine_test)
197+
198+
154199
class FlakyRatchettingMachine(RuleBasedStateMachine):
155200
ratchet = 0
156201

@@ -1310,7 +1355,6 @@ def test_targets_repr(bundle_names, initial, repr_):
13101355
bundles = {name: Bundle(name) for name in bundle_names}
13111356

13121357
class Machine(RuleBasedStateMachine):
1313-
13141358
@initialize(targets=[bundles[name] for name in bundle_names])
13151359
def init(self):
13161360
return initial

0 commit comments

Comments
 (0)