Skip to content

Commit 1e91394

Browse files
authored
Merge pull request #4164 from Zac-HD/report-spans
Experimental changes for Crosshair support
2 parents 690520e + 8ebe5b8 commit 1e91394

File tree

4 files changed

+68
-14
lines changed

4 files changed

+68
-14
lines changed

hypothesis-python/RELEASE.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
RELEASE_TYPE: minor
2+
3+
This release adds ``.span_start()`` and ``.span_end()`` methods
4+
to our internal ``PrimitiveProvider`` interface, for use by
5+
:ref:`alternative-backends`.

hypothesis-python/src/hypothesis/internal/conjecture/data.py

+23
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,27 @@ def draw_bytes(
13411341
) -> bytes:
13421342
raise NotImplementedError
13431343

1344+
def span_start(self, label: int, /) -> None: # noqa: B027 # non-abstract noop
1345+
"""Marks the beginning of a semantically meaningful span.
1346+
1347+
Providers can optionally track this data to learn which sub-sequences
1348+
of draws correspond to a higher-level object, recovering the parse tree.
1349+
`label` is an opaque integer, which will be shared by all spans drawn
1350+
from a particular strategy.
1351+
1352+
This method is called from ConjectureData.start_example().
1353+
"""
1354+
1355+
def span_end(self, discard: bool, /) -> None: # noqa: B027 # non-abstract noop
1356+
"""Marks the end of a semantically meaningful span.
1357+
1358+
`discard` is True when the draw was filtered out or otherwise marked as
1359+
unlikely to contribute to the input data as seen by the user's test.
1360+
Note however that side effects can make this determination unsound.
1361+
1362+
This method is called from ConjectureData.stop_example().
1363+
"""
1364+
13441365

13451366
class HypothesisProvider(PrimitiveProvider):
13461367
lifetime = "test_case"
@@ -2543,6 +2564,7 @@ def draw(
25432564
self.stop_example()
25442565

25452566
def start_example(self, label: int) -> None:
2567+
self.provider.span_start(label)
25462568
self.__assert_not_frozen("start_example")
25472569
self.depth += 1
25482570
# Logically it would make sense for this to just be
@@ -2557,6 +2579,7 @@ def start_example(self, label: int) -> None:
25572579
self.labels_for_structure_stack.append({label})
25582580

25592581
def stop_example(self, *, discard: bool = False) -> None:
2582+
self.provider.span_end(discard)
25602583
if self.frozen:
25612584
return
25622585
if discard:

hypothesis-python/src/hypothesis/internal/conjecture/engine.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@ def __init__(
284284
self.__failed_realize_count = 0
285285
self._verified_by = None # note unsound verification by alt backends
286286

287+
@property
288+
def using_hypothesis_backend(self):
289+
return (
290+
self.settings.backend == "hypothesis" or self._switch_to_hypothesis_provider
291+
)
292+
287293
def explain_next_call_as(self, explanation: str) -> None:
288294
self.__pending_call_explanation = explanation
289295

@@ -314,7 +320,7 @@ def should_optimise(self) -> bool:
314320
return Phase.target in self.settings.phases
315321

316322
def __tree_is_exhausted(self) -> bool:
317-
return self.tree.is_exhausted and self.settings.backend == "hypothesis"
323+
return self.tree.is_exhausted and self.using_hypothesis_backend
318324

319325
def __stoppable_test_function(self, data: ConjectureData) -> None:
320326
"""Run ``self._test_function``, but convert a ``StopTest`` exception
@@ -475,6 +481,9 @@ def test_function(self, data: ConjectureData) -> None:
475481
and (self.__failed_realize_count / self.call_count) > 0.2
476482
):
477483
self._switch_to_hypothesis_provider = True
484+
# skip the post-test-case tracking; we're pretending this never happened
485+
interrupted = True
486+
return
478487
except BaseException:
479488
self.save_buffer(data.buffer)
480489
raise
@@ -562,7 +571,7 @@ def test_function(self, data: ConjectureData) -> None:
562571
self.valid_examples += 1
563572

564573
if data.status == Status.INTERESTING:
565-
if self.settings.backend != "hypothesis":
574+
if not self.using_hypothesis_backend:
566575
# drive the ir tree through the test function to convert it
567576
# to a buffer
568577
initial_origin = data.interesting_origin
@@ -1034,7 +1043,7 @@ def generate_new_examples(self) -> None:
10341043
# a buffer and uses HypothesisProvider as its backing provider,
10351044
# not whatever is specified by the backend. We can improve this
10361045
# once more things are on the ir.
1037-
if self.settings.backend != "hypothesis":
1046+
if not self.using_hypothesis_backend:
10381047
data = self.new_conjecture_data(prefix=b"", max_length=BUFFER_SIZE)
10391048
with suppress(BackendCannotProceed):
10401049
self.test_function(data)
@@ -1310,7 +1319,7 @@ def new_conjecture_data_ir(
13101319
HypothesisProvider if self._switch_to_hypothesis_provider else self.provider
13111320
)
13121321
observer = observer or self.tree.new_observer()
1313-
if self.settings.backend != "hypothesis":
1322+
if not self.using_hypothesis_backend:
13141323
observer = DataObserver()
13151324

13161325
return ConjectureData.for_ir_tree(
@@ -1331,7 +1340,7 @@ def new_conjecture_data(
13311340
HypothesisProvider if self._switch_to_hypothesis_provider else self.provider
13321341
)
13331342
observer = observer or self.tree.new_observer()
1334-
if self.settings.backend != "hypothesis":
1343+
if not self.using_hypothesis_backend:
13351344
observer = DataObserver()
13361345

13371346
return ConjectureData(
@@ -1499,7 +1508,7 @@ def check_result(
14991508
prefix=buffer, max_length=max_length, observer=observer
15001509
)
15011510

1502-
if self.settings.backend == "hypothesis":
1511+
if self.using_hypothesis_backend:
15031512
try:
15041513
self.tree.simulate_test_function(dummy_data)
15051514
except PreviouslyUnseenBehaviour:

hypothesis-python/tests/conjecture/test_alt_backend.py

+25-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
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 itertools
1112
import math
1213
import sys
1314
from contextlib import contextmanager
@@ -16,14 +17,15 @@
1617

1718
import pytest
1819

19-
from hypothesis import given, settings, strategies as st
20+
from hypothesis import HealthCheck, assume, given, settings, strategies as st
2021
from hypothesis.control import current_build_context
2122
from hypothesis.database import InMemoryExampleDatabase
2223
from hypothesis.errors import (
2324
BackendCannotProceed,
2425
Flaky,
2526
HypothesisException,
2627
InvalidArgument,
28+
Unsatisfiable,
2729
)
2830
from hypothesis.internal.compat import int_to_bytes
2931
from hypothesis.internal.conjecture.data import (
@@ -492,15 +494,13 @@ def test_function(_):
492494
class FallibleProvider(TrivialProvider):
493495
def __init__(self, conjecturedata: "ConjectureData", /) -> None:
494496
super().__init__(conjecturedata)
495-
self.prng = Random(0)
497+
self._it = itertools.cycle([1, 1, 1, "discard_test_case", "other"])
496498

497499
def draw_integer(self, *args, **kwargs):
498-
# This is frequent enough that we'll get coverage of the "give up and go
499-
# back to Hypothesis' standard backend" code path.
500-
if self.prng.getrandbits(1):
501-
scope = self.prng.choice(["discard_test_case", "other"])
502-
raise BackendCannotProceed(scope)
503-
return 1
500+
x = next(self._it)
501+
if isinstance(x, str):
502+
raise BackendCannotProceed(x)
503+
return x
504504

505505

506506
def test_falls_back_to_default_backend():
@@ -517,6 +517,23 @@ def test_function(x):
517517
assert seen_other_ints # must have swapped backends then
518518

519519

520+
def test_can_raise_unsatisfiable_after_falling_back():
521+
with temp_register_backend("fallible", FallibleProvider):
522+
523+
@given(st.integers())
524+
@settings(
525+
backend="fallible",
526+
database=None,
527+
max_examples=100,
528+
suppress_health_check=[HealthCheck.filter_too_much],
529+
)
530+
def test_function(x):
531+
assume(x == "unsatisfiable")
532+
533+
with pytest.raises(Unsatisfiable):
534+
test_function()
535+
536+
520537
class ExhaustibleProvider(TrivialProvider):
521538
scope = "exhausted"
522539

0 commit comments

Comments
 (0)