1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11import datetime
12import math
13import time
14import types
15import weakref
16from collections import defaultdict
17from collections.abc import Callable, Generator, Hashable, Iterable, Iterator, Sequence
18from contextlib import contextmanager
19from dataclasses import dataclass, field
20from enum import IntEnum
21from functools import cached_property
22from random import Random
23from typing import (
24 TYPE_CHECKING,
25 Any,
26 Literal,
27 NoReturn,
28 TypeAlias,
29 TypeVar,
30 cast,
31 overload,
32)
33
34from hypothesis.errors import (
35 CannotInvert,
36 CannotProceedScopeT,
37 ChoiceTooLarge,
38 FlakyStrategyDefinition,
39 Frozen,
40 InvalidArgument,
41 StopTest,
42)
43from hypothesis.internal.cache import LRUCache
44from hypothesis.internal.compat import add_note
45from hypothesis.internal.conjecture.choice import (
46 BooleanConstraints,
47 BytesConstraints,
48 ChoiceConstraintsT,
49 ChoiceNode,
50 ChoiceT,
51 ChoiceTemplate,
52 ChoiceTypeT,
53 FloatConstraints,
54 IntegerConstraints,
55 StringConstraints,
56 ValueHole,
57 choice_constraints_key,
58 choice_from_index,
59 choice_permitted,
60 choice_size,
61)
62from hypothesis.internal.conjecture.junkdrawer import IntList, gc_cumulative_time
63from hypothesis.internal.conjecture.providers import (
64 COLLECTION_DEFAULT_MAX_SIZE,
65 HypothesisProvider,
66 PrimitiveProvider,
67)
68from hypothesis.internal.conjecture.utils import calc_label_from_name
69from hypothesis.internal.escalation import InterestingOrigin
70from hypothesis.internal.floats import (
71 SMALLEST_SUBNORMAL,
72 float_to_int,
73 int_to_float,
74 sign_aware_lte,
75)
76from hypothesis.internal.intervalsets import IntervalSet
77from hypothesis.internal.observability import PredicateCounts
78from hypothesis.internal.reflection import function_location
79from hypothesis.reporting import debug_report
80from hypothesis.utils.conventions import UniqueIdentifier, not_set
81from hypothesis.utils.deprecation import note_deprecation
82from hypothesis.utils.threading import ThreadLocal
83from hypothesis.vendor.pretty import ArgLabelsT
84
85if TYPE_CHECKING:
86 from hypothesis.strategies import SearchStrategy
87 from hypothesis.strategies._internal.core import DataObject
88 from hypothesis.strategies._internal.random import RandomState
89 from hypothesis.strategies._internal.strategies import Ex
90
91
92def __getattr__(name: str) -> Any:
93 if name == "AVAILABLE_PROVIDERS":
94 from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS
95
96 note_deprecation(
97 "hypothesis.internal.conjecture.data.AVAILABLE_PROVIDERS has been moved to "
98 "hypothesis.internal.conjecture.providers.AVAILABLE_PROVIDERS.",
99 since="2025-01-25",
100 has_codemod=False,
101 stacklevel=1,
102 )
103 return AVAILABLE_PROVIDERS
104
105 raise AttributeError(
106 f"Module 'hypothesis.internal.conjecture.data' has no attribute {name}"
107 )
108
109
110T = TypeVar("T")
111TargetObservations = dict[str, int | float]
112# index, choice_type, constraints, forced value
113MisalignedAt: TypeAlias = tuple[int, ChoiceTypeT, ChoiceConstraintsT, ChoiceT | None]
114
115TOP_LABEL = calc_label_from_name("top")
116UNIQUE_COLLECTION_LABEL = calc_label_from_name(
117 "a collection with a uniqueness constraint"
118)
119MAX_DEPTH = 100
120
121threadlocal = ThreadLocal(global_test_counter=int)
122
123
124class Status(IntEnum):
125 OVERRUN = 0
126 INVALID = 1
127 VALID = 2
128 INTERESTING = 3
129
130 def __repr__(self) -> str:
131 return f"Status.{self.name}"
132
133
134@dataclass(slots=True, frozen=True)
135class StructuralCoverageTag:
136 label: int
137
138
139STRUCTURAL_COVERAGE_CACHE: dict[int, StructuralCoverageTag] = {}
140
141
142def structural_coverage(label: int) -> StructuralCoverageTag:
143 try:
144 return STRUCTURAL_COVERAGE_CACHE[label]
145 except KeyError:
146 return STRUCTURAL_COVERAGE_CACHE.setdefault(label, StructuralCoverageTag(label))
147
148
149# This cache can be quite hot and so we prefer LRUCache over LRUReusedCache for
150# performance. We lose scan resistance, but that's probably fine here.
151POOLED_CONSTRAINTS_CACHE: LRUCache[tuple[Any, ...], ChoiceConstraintsT] = LRUCache(4096)
152
153# The exact types whose values record_value_for_span holds directly: small
154# immutable stdlib types, none of which support weak references. Values of
155# any other type are held via weakref where possible, so that recording never
156# extends an object's lifetime. A tuple rather than a set so that ``in``
157# works even if a value's class has an unhashable metaclass. Exact type
158# checks also exclude symbolic backends.
159_RECORDABLE_VALUE_TYPES: tuple[type, ...] = (
160 int,
161 bool,
162 str,
163 bytes,
164 float,
165 types.NoneType,
166 datetime.date,
167 datetime.time,
168 datetime.datetime,
169 datetime.timedelta,
170)
171
172no_recorded_value = UniqueIdentifier("no_recorded_value")
173
174
175class Span:
176 """A span tracks the hierarchical structure of choices within a single test run.
177
178 Spans are created to mark regions of the choice sequence that are
179 logically related to each other. For instance, Hypothesis tracks:
180 - A single top-level span for the entire choice sequence
181 - A span for the choices made by each strategy
182 - Some strategies define additional spans within their choices. For instance,
183 st.lists() tracks the "should add another element" choice and the "add
184 another element" choices as separate spans.
185
186 Spans provide useful information to the shrinker, mutator, targeted PBT,
187 and other subsystems of Hypothesis.
188
189 Rather than store each ``Span`` as a rich object, it is actually
190 just an index into the ``Spans`` class defined below. This has two
191 purposes: Firstly, for most properties of spans we will never need
192 to allocate storage at all, because most properties are not used on
193 most spans. Secondly, by storing the spans as compact lists
194 of integers, we save a considerable amount of space compared to
195 Python's normal object size.
196
197 This does have the downside that it increases the amount of allocation
198 we do, and slows things down as a result, in some usage patterns because
199 we repeatedly allocate the same Span or int objects, but it will
200 often dramatically reduce our memory usage, so is worth it.
201 """
202
203 __slots__ = ("index", "owner")
204
205 def __init__(self, owner: "Spans", index: int) -> None:
206 self.owner = owner
207 self.index = index
208
209 def __eq__(self, other: object) -> bool:
210 if self is other:
211 return True
212 if not isinstance(other, Span):
213 return NotImplemented
214 return (self.owner is other.owner) and (self.index == other.index)
215
216 def __ne__(self, other: object) -> bool:
217 if self is other:
218 return False
219 if not isinstance(other, Span):
220 return NotImplemented
221 return (self.owner is not other.owner) or (self.index != other.index)
222
223 def __repr__(self) -> str:
224 return f"spans[{self.index}]"
225
226 @property
227 def label(self) -> int:
228 """A label is an opaque value that associates each span with its
229 approximate origin, such as a particular strategy class or a particular
230 kind of draw."""
231 return self.owner.labels[self.owner.label_indices[self.index]]
232
233 @property
234 def parent(self) -> int | None:
235 """The index of the span that this one is nested directly within."""
236 if self.index == 0:
237 return None
238 return self.owner.parentage[self.index]
239
240 @property
241 def start(self) -> int:
242 return self.owner.starts[self.index]
243
244 @property
245 def end(self) -> int:
246 return self.owner.ends[self.index]
247
248 @property
249 def depth(self) -> int:
250 """
251 Depth of this span in the span tree. The top-level span has a depth of 0.
252 """
253 return self.owner.depths[self.index]
254
255 @property
256 def discarded(self) -> bool:
257 """True if this is span's ``stop_span`` call had ``discard`` set to
258 ``True``. This means we believe that the shrinker should be able to delete
259 this span completely, without affecting the value produced by its enclosing
260 strategy. Typically set when a rejection sampler decides to reject a
261 generated value and try again."""
262 return self.index in self.owner.discarded
263
264 @property
265 def choice_count(self) -> int:
266 """The number of choices in this span."""
267 return self.end - self.start
268
269 @property
270 def children(self) -> "list[Span]":
271 """The list of all spans with this as a parent, in increasing index
272 order."""
273 return [self.owner[i] for i in self.owner.children[self.index]]
274
275 @property
276 def recorded_value(self) -> Any:
277 """The value produced by the strategy draw corresponding to this span,
278 or the ``no_recorded_value`` sentinel if no value was recorded, or if
279 a weakly-referenced value has since been collected."""
280 value = self.owner.span_values.get(self.index, no_recorded_value)
281 if isinstance(value, weakref.ReferenceType):
282 referent = value()
283 return no_recorded_value if referent is None else referent
284 return value
285
286
287class SpanProperty:
288 """There are many properties of spans that we calculate by
289 essentially rerunning the test case multiple times based on the
290 calls which we record in SpanProperty.
291
292 This class defines a visitor, subclasses of which can be used
293 to calculate these properties.
294 """
295
296 def __init__(self, spans: "Spans"):
297 self.span_stack: list[int] = []
298 self.spans = spans
299 self.span_count = 0
300 self.choice_count = 0
301
302 def run(self) -> Any:
303 """Rerun the test case with this visitor and return the
304 results of ``self.finish()``."""
305 for record in self.spans.trail:
306 if record == TrailType.STOP_SPAN_DISCARD:
307 self.__pop(discarded=True)
308 elif record == TrailType.STOP_SPAN_NO_DISCARD:
309 self.__pop(discarded=False)
310 elif record == TrailType.CHOICE:
311 self.choice_count += 1
312 else:
313 # everything after TrailType.CHOICE is the label of a span start.
314 self.__push(record - TrailType.CHOICE - 1)
315
316 return self.finish()
317
318 def __push(self, label_index: int) -> None:
319 i = self.span_count
320 assert i < len(self.spans)
321 self.start_span(i, label_index=label_index)
322 self.span_count += 1
323 self.span_stack.append(i)
324
325 def __pop(self, *, discarded: bool) -> None:
326 i = self.span_stack.pop()
327 self.stop_span(i, discarded=discarded)
328
329 def start_span(self, i: int, label_index: int) -> None:
330 """Called at the start of each span, with ``i`` the
331 index of the span and ``label_index`` the index of
332 its label in ``self.spans.labels``."""
333
334 def stop_span(self, i: int, *, discarded: bool) -> None:
335 """Called at the end of each span, with ``i`` the
336 index of the span and ``discarded`` being ``True`` if ``stop_span``
337 was called with ``discard=True``."""
338
339 def finish(self) -> Any:
340 raise NotImplementedError
341
342
343class TrailType(IntEnum):
344 STOP_SPAN_DISCARD = 1
345 STOP_SPAN_NO_DISCARD = 2
346 CHOICE = 3
347 # every trail element larger than TrailType.CHOICE is the label of a span
348 # start, offset by its index. So the first span label is stored as 4, the
349 # second as 5, etc, regardless of its actual integer label.
350
351
352class SpanRecord:
353 """Records the series of ``start_span``, ``stop_span``, and
354 ``draw_bits`` calls so that these may be stored in ``Spans`` and
355 replayed when we need to know about the structure of individual
356 ``Span`` objects.
357
358 Note that there is significant similarity between this class and
359 ``DataObserver``, and the plan is to eventually unify them, but
360 they currently have slightly different functions and implementations.
361 """
362
363 def __init__(self) -> None:
364 self.labels: list[int] = []
365 self.__index_of_labels: dict[int, int] | None = {}
366 self.trail = IntList()
367 self.nodes: list[ChoiceNode] = []
368 # The number of spans started so far, which is also the index that the
369 # next span to start will get. Spans are indexed in start order.
370 self.span_count = 0
371 self.span_values: dict[int, Any] = {}
372
373 def freeze(self) -> None:
374 self.__index_of_labels = None
375
376 def record_choice(self) -> None:
377 self.trail.append(TrailType.CHOICE)
378
379 def start_span(self, label: int) -> None:
380 assert self.__index_of_labels is not None
381 try:
382 i = self.__index_of_labels[label]
383 except KeyError:
384 i = self.__index_of_labels.setdefault(label, len(self.labels))
385 self.labels.append(label)
386 self.trail.append(TrailType.CHOICE + 1 + i)
387 self.span_count += 1
388
389 def stop_span(self, *, discard: bool) -> None:
390 if discard:
391 self.trail.append(TrailType.STOP_SPAN_DISCARD)
392 else:
393 self.trail.append(TrailType.STOP_SPAN_NO_DISCARD)
394
395 def record_value_for_span(self, span_index: int, value: Any) -> None:
396 # Record ``value`` against the span at ``span_index``. Called by
397 # ConjectureData.draw with the value each strategy's do_draw returned.
398 # Values which can be neither held directly nor weakly referenced
399 # (e.g. stdlib containers) are not recorded, and so cannot widen.
400 if type(value) in _RECORDABLE_VALUE_TYPES:
401 self.span_values[span_index] = value
402 else:
403 try:
404 self.span_values[span_index] = weakref.ref(value)
405 except TypeError:
406 pass
407
408
409class _starts_and_ends(SpanProperty):
410 def __init__(self, spans: "Spans") -> None:
411 super().__init__(spans)
412 self.starts = IntList.of_length(len(self.spans))
413 self.ends = IntList.of_length(len(self.spans))
414
415 def start_span(self, i: int, label_index: int) -> None:
416 self.starts[i] = self.choice_count
417
418 def stop_span(self, i: int, *, discarded: bool) -> None:
419 self.ends[i] = self.choice_count
420
421 def finish(self) -> tuple[IntList, IntList]:
422 return (self.starts, self.ends)
423
424
425class _discarded(SpanProperty):
426 def __init__(self, spans: "Spans") -> None:
427 super().__init__(spans)
428 self.result: set[int] = set()
429
430 def finish(self) -> frozenset[int]:
431 return frozenset(self.result)
432
433 def stop_span(self, i: int, *, discarded: bool) -> None:
434 if discarded:
435 self.result.add(i)
436
437
438class _parentage(SpanProperty):
439 def __init__(self, spans: "Spans") -> None:
440 super().__init__(spans)
441 self.result = IntList.of_length(len(self.spans))
442
443 def stop_span(self, i: int, *, discarded: bool) -> None:
444 if i > 0:
445 self.result[i] = self.span_stack[-1]
446
447 def finish(self) -> IntList:
448 return self.result
449
450
451class _depths(SpanProperty):
452 def __init__(self, spans: "Spans") -> None:
453 super().__init__(spans)
454 self.result = IntList.of_length(len(self.spans))
455
456 def start_span(self, i: int, label_index: int) -> None:
457 self.result[i] = len(self.span_stack)
458
459 def finish(self) -> IntList:
460 return self.result
461
462
463class _label_indices(SpanProperty):
464 def __init__(self, spans: "Spans") -> None:
465 super().__init__(spans)
466 self.result = IntList.of_length(len(self.spans))
467
468 def start_span(self, i: int, label_index: int) -> None:
469 self.result[i] = label_index
470
471 def finish(self) -> IntList:
472 return self.result
473
474
475class _mutator_groups(SpanProperty):
476 def __init__(self, spans: "Spans") -> None:
477 super().__init__(spans)
478 self.groups: dict[int, set[tuple[int, int]]] = defaultdict(set)
479 self._unique_stack: list[int] = []
480
481 def start_span(self, i: int, label_index: int) -> None:
482 # TODO should we discard start == end cases? occurs for eg st.data()
483 # which is conditionally or never drawn from. arguably swapping
484 # nodes with the empty list is a useful mutation enabled by start == end?
485 #
486 # Skip spans inside a unique collection: duplicating one element of a
487 # unique collection over another is certain to fail the uniqueness
488 # filter and be discarded. The collection's own span is still added,
489 # so duplicating a collection as a whole remains possible.
490 if not self._unique_stack:
491 key = (self.spans[i].start, self.spans[i].end)
492 self.groups[label_index].add(key)
493 if self.spans.labels[label_index] == UNIQUE_COLLECTION_LABEL:
494 self._unique_stack.append(i)
495
496 def stop_span(self, i: int, *, discarded: bool) -> None:
497 if self._unique_stack and self._unique_stack[-1] == i:
498 self._unique_stack.pop()
499
500 def finish(self) -> Iterable[set[tuple[int, int]]]:
501 # Discard groups with only one span, since the mutator can't
502 # do anything useful with them.
503 return [g for g in self.groups.values() if len(g) >= 2]
504
505
506class Spans:
507 """A lazy collection of ``Span`` objects, derived from
508 the record of recorded behaviour in ``SpanRecord``.
509
510 Behaves logically as if it were a list of ``Span`` objects,
511 but actually mostly exists as a compact store of information
512 for them to reference into. All properties on here are best
513 understood as the backing storage for ``Span`` and are
514 described there.
515 """
516
517 def __init__(self, record: SpanRecord) -> None:
518 self.trail = record.trail
519 self.labels = record.labels
520 self.span_values = record.span_values
521 self.__length = self.trail.count(
522 TrailType.STOP_SPAN_DISCARD
523 ) + record.trail.count(TrailType.STOP_SPAN_NO_DISCARD)
524 self.__children: list[Sequence[int]] | None = None
525
526 @cached_property
527 def starts_and_ends(self) -> tuple[IntList, IntList]:
528 return _starts_and_ends(self).run()
529
530 @property
531 def starts(self) -> IntList:
532 return self.starts_and_ends[0]
533
534 @property
535 def ends(self) -> IntList:
536 return self.starts_and_ends[1]
537
538 @cached_property
539 def discarded(self) -> frozenset[int]:
540 return _discarded(self).run()
541
542 @cached_property
543 def parentage(self) -> IntList:
544 return _parentage(self).run()
545
546 @cached_property
547 def depths(self) -> IntList:
548 return _depths(self).run()
549
550 @cached_property
551 def label_indices(self) -> IntList:
552 return _label_indices(self).run()
553
554 @cached_property
555 def mutator_groups(self) -> list[set[tuple[int, int]]]:
556 return _mutator_groups(self).run()
557
558 @property
559 def children(self) -> list[Sequence[int]]:
560 if self.__children is None:
561 children = [IntList() for _ in range(len(self))]
562 for i, p in enumerate(self.parentage):
563 if i > 0:
564 children[p].append(i)
565 # Replace empty children lists with a tuple to reduce
566 # memory usage.
567 for i, c in enumerate(children):
568 if not c:
569 children[i] = () # type: ignore
570 self.__children = children # type: ignore
571 return self.__children # type: ignore
572
573 def __len__(self) -> int:
574 return self.__length
575
576 def __getitem__(self, i: int) -> Span:
577 n = self.__length
578 if i < -n or i >= n:
579 raise IndexError(f"Index {i} out of range [-{n}, {n})")
580 if i < 0:
581 i += n
582 return Span(self, i)
583
584 # not strictly necessary as we have len/getitem, but required for mypy.
585 # https://github.com/python/mypy/issues/9737
586 def __iter__(self) -> Iterator[Span]:
587 for i in range(len(self)):
588 yield self[i]
589
590
591class _Overrun:
592 status: Status = Status.OVERRUN
593
594 def __repr__(self) -> str:
595 return "Overrun"
596
597
598Overrun = _Overrun()
599
600
601class DataObserver:
602 """Observer class for recording the behaviour of a
603 ConjectureData object, primarily used for tracking
604 the behaviour in the tree cache."""
605
606 def conclude_test(
607 self,
608 status: Status,
609 interesting_origin: InterestingOrigin | None,
610 ) -> None:
611 """Called when ``conclude_test`` is called on the
612 observed ``ConjectureData``, with the same arguments.
613
614 Note that this is called after ``freeze`` has completed.
615 """
616
617 def kill_branch(self) -> None:
618 """Mark this part of the tree as not worth re-exploring."""
619
620 def draw_integer(
621 self, value: int, *, constraints: IntegerConstraints, was_forced: bool
622 ) -> None:
623 pass
624
625 def draw_float(
626 self, value: float, *, constraints: FloatConstraints, was_forced: bool
627 ) -> None:
628 pass
629
630 def draw_string(
631 self, value: str, *, constraints: StringConstraints, was_forced: bool
632 ) -> None:
633 pass
634
635 def draw_bytes(
636 self, value: bytes, *, constraints: BytesConstraints, was_forced: bool
637 ) -> None:
638 pass
639
640 def draw_boolean(
641 self, value: bool, *, constraints: BooleanConstraints, was_forced: bool
642 ) -> None:
643 pass
644
645
646@dataclass(slots=True, frozen=True)
647class ConjectureResult:
648 """Result class storing the parts of ConjectureData that we
649 will care about after the original ConjectureData has outlived its
650 usefulness."""
651
652 status: Status
653 interesting_origin: InterestingOrigin | None
654 nodes: tuple[ChoiceNode, ...] = field(repr=False, compare=False)
655 length: int
656 notes: list[str]
657 expected_exception: BaseException | None
658 expected_traceback: str | None
659 has_discards: bool
660 target_observations: TargetObservations
661 tags: frozenset[StructuralCoverageTag]
662 spans: Spans = field(repr=False, compare=False)
663 arg_spans: set[int] = field(repr=False)
664 # Comments for the explain phase, keyed by span index. The ``None`` key
665 # holds the whole-test comment about varying all commented parts together.
666 span_comments: dict[int | None, str] = field(repr=False)
667 misaligned_at: MisalignedAt | None = field(repr=False)
668 cannot_proceed_scope: CannotProceedScopeT | None = field(repr=False)
669
670 def as_result(self) -> "ConjectureResult":
671 return self
672
673 @property
674 def choices(self) -> tuple[ChoiceT, ...]:
675 return tuple(node.value for node in self.nodes)
676
677
678class ConjectureData:
679 @classmethod
680 def for_choices(
681 cls,
682 choices: Sequence[ChoiceTemplate | ValueHole | ChoiceT],
683 *,
684 observer: DataObserver | None = None,
685 provider: PrimitiveProvider | type[PrimitiveProvider] = HypothesisProvider,
686 random: Random | None = None,
687 ) -> "ConjectureData":
688 from hypothesis.internal.conjecture.engine import choice_count
689
690 return cls(
691 max_choices=choice_count(choices),
692 random=random,
693 prefix=choices,
694 observer=observer,
695 provider=provider,
696 )
697
698 def __init__(
699 self,
700 *,
701 random: Random | None,
702 observer: DataObserver | None = None,
703 provider: PrimitiveProvider | type[PrimitiveProvider] = HypothesisProvider,
704 prefix: Sequence[ChoiceTemplate | ValueHole | ChoiceT] | None = None,
705 max_choices: int | None = None,
706 provider_kw: dict[str, Any] | None = None,
707 ) -> None:
708 from hypothesis.internal.conjecture.engine import BUFFER_SIZE
709
710 if observer is None:
711 observer = DataObserver()
712 if provider_kw is None:
713 provider_kw = {}
714 elif not isinstance(provider, type):
715 raise InvalidArgument(
716 f"Expected {provider=} to be a class since {provider_kw=} was "
717 "passed, but got an instance instead."
718 )
719
720 assert isinstance(observer, DataObserver)
721 self.observer = observer
722 self.max_choices = max_choices
723 self.max_length = BUFFER_SIZE
724 self.overdraw = 0
725 self._random = random
726
727 self.length: int = 0
728 self.index: int = 0
729 self.notes: list[str] = []
730 self.status: Status = Status.VALID
731 self.frozen: bool = False
732 self.testcounter: int = threadlocal.global_test_counter
733 threadlocal.global_test_counter += 1
734 self.start_time = time.perf_counter()
735 self.gc_start_time = gc_cumulative_time()
736 self.events: dict[str, str | int | float] = {}
737 self.interesting_origin: InterestingOrigin | None = None
738 self.draw_times: dict[str, float] = {}
739 self._stateful_run_times: dict[str, float] = defaultdict(float)
740 self.max_depth: int = 0
741 self.has_discards: bool = False
742 # While positive, we are drawing an element of a unique collection, so
743 # a "simplest" ChoiceTemplate enumerates progressively larger choices
744 # (via the shared counter) instead of repeating guaranteed-duplicates.
745 self.unique_element_depth: int = 0
746 self.simplest_index: int = 0
747
748 self.provider: PrimitiveProvider = (
749 provider(self, **provider_kw) if isinstance(provider, type) else provider
750 )
751 assert isinstance(self.provider, PrimitiveProvider)
752
753 self.__result: ConjectureResult | None = None
754
755 # Observations used for targeted search. They'll be aggregated in
756 # ConjectureRunner.generate_new_test_cases and fed to TargetSelector.
757 self.target_observations: TargetObservations = {}
758
759 # Tags which indicate something about which part of the search space
760 # this example is in. These are used to guide generation.
761 self.tags: set[StructuralCoverageTag] = set()
762 self.labels_for_structure_stack: list[set[int]] = []
763
764 # Normally unpopulated but we need this in the niche case
765 # that self.as_result() is Overrun but we still want the
766 # examples for reporting purposes.
767 self.__spans: Spans | None = None
768
769 # We want the top level span to have depth 0, so we start at -1.
770 self.depth: int = -1
771 self.__span_record = SpanRecord()
772
773 # Span indices for discrete reportable parts that which-parts-matter can
774 # try varying, to report if the minimal test case always fails anyway.
775 self.arg_spans: set[int] = set()
776 self.span_comments: dict[int | None, str] = {}
777 self._observability_args: dict[str, Any] = {}
778 self._observability_predicates: defaultdict[str, PredicateCounts] = defaultdict(
779 PredicateCounts
780 )
781 self.invalid_location: str | None = None
782 # (predicate, location) of the most recent filter rejection
783 self._last_rejected_filter: tuple[Callable[[Any], Any], str | None] | None = (
784 None
785 )
786
787 self._sampled_from_all_strategies_elements_message: (
788 tuple[str, object] | None
789 ) = None
790 self._shared_strategy_draws: dict[Hashable, tuple[Any, SearchStrategy]] = {}
791 self._shared_data_strategy: DataObject | None = None
792 self._stateful_repr_parts: list[Any] | None = None
793 self.states_for_ids: dict[int, RandomState] | None = None
794 self.seeds_to_states: dict[Any, RandomState] | None = None
795 self.hypothesis_runner: Any = not_set
796
797 self.expected_exception: BaseException | None = None
798 self.expected_traceback: str | None = None
799
800 self.prefix = prefix
801 self._inverting = False
802 # Appending to a tuple is O(n), so we accumulate nodes in a list and
803 # expose them as a (cached) tuple.
804 self._nodes: list[ChoiceNode] = []
805 self._nodes_tuple: tuple[ChoiceNode, ...] = ()
806 self.misaligned_at: MisalignedAt | None = None
807 self.cannot_proceed_scope: CannotProceedScopeT | None = None
808 self.start_span(TOP_LABEL)
809
810 def __repr__(self) -> str:
811 return (
812 f"ConjectureData({self.status.name}, {len(self.nodes)} "
813 f"choices{', frozen' if self.frozen else ''})"
814 )
815
816 @property
817 def nodes(self) -> tuple[ChoiceNode, ...]:
818 if len(self._nodes_tuple) != len(self._nodes):
819 self._nodes_tuple = tuple(self._nodes)
820 return self._nodes_tuple
821
822 @property
823 def choices(self) -> tuple[ChoiceT, ...]:
824 return tuple(node.value for node in self._nodes)
825
826 # draw_* functions might be called in one of two contexts: either "above" or
827 # "below" the choice sequence. For instance, draw_string calls draw_boolean
828 # from ``many`` when calculating the number of characters to return. We do
829 # not want these choices to get written to the choice sequence, because they
830 # are not true choices themselves.
831 #
832 # `observe` formalizes this. The choice will only be written to the choice
833 # sequence if observe is True.
834
835 @overload
836 def _draw(
837 self,
838 choice_type: Literal["integer"],
839 constraints: IntegerConstraints,
840 *,
841 observe: bool,
842 forced: int | None,
843 ) -> int: ...
844
845 @overload
846 def _draw(
847 self,
848 choice_type: Literal["float"],
849 constraints: FloatConstraints,
850 *,
851 observe: bool,
852 forced: float | None,
853 ) -> float: ...
854
855 @overload
856 def _draw(
857 self,
858 choice_type: Literal["string"],
859 constraints: StringConstraints,
860 *,
861 observe: bool,
862 forced: str | None,
863 ) -> str: ...
864
865 @overload
866 def _draw(
867 self,
868 choice_type: Literal["bytes"],
869 constraints: BytesConstraints,
870 *,
871 observe: bool,
872 forced: bytes | None,
873 ) -> bytes: ...
874
875 @overload
876 def _draw(
877 self,
878 choice_type: Literal["boolean"],
879 constraints: BooleanConstraints,
880 *,
881 observe: bool,
882 forced: bool | None,
883 ) -> bool: ...
884
885 def _draw(
886 self,
887 choice_type: ChoiceTypeT,
888 constraints: ChoiceConstraintsT,
889 *,
890 observe: bool,
891 forced: ChoiceT | None,
892 ) -> ChoiceT:
893 if self._inverting:
894 # A strategy tried to draw while re-encoding a ValueHole - e.g. a
895 # filter predicate which draws, like stateful's rule filters.
896 # Inversions must be pure, so treat this as unencodable.
897 raise CannotInvert("cannot draw during _invert")
898 # this is somewhat redundant with the length > max_length check at the
899 # end of the function, but avoids trying to use a null self.random when
900 # drawing past the node of a ConjectureData.for_choices data.
901 if self.length == self.max_length:
902 debug_report(f"overrun because hit {self.max_length=}")
903 self.mark_overrun()
904 if len(self._nodes) == self.max_choices:
905 debug_report(f"overrun because hit {self.max_choices=}")
906 self.mark_overrun()
907
908 if observe and self.prefix is not None and self.index < len(self.prefix):
909 value = self._pop_choice(choice_type, constraints, forced=forced)
910 if forced is not None:
911 value = forced
912 elif forced is None:
913 value = getattr(self.provider, f"draw_{choice_type}")(**constraints)
914 else:
915 value = forced
916
917 # nan values generated via int_to_float break list membership:
918 #
919 # >>> n = 18444492273895866368
920 # >>> assert math.isnan(int_to_float(n))
921 # >>> assert int_to_float(n) not in [int_to_float(n)]
922 #
923 # because int_to_float nans are not equal in the sense of either
924 # `a == b` or `a is b`.
925 #
926 # This can lead to flaky errors when collections require unique
927 # floats. What was happening is that in some places we provided math.nan
928 # provide math.nan, and in others we provided
929 # int_to_float(float_to_int(math.nan)), and which one gets used
930 # was not deterministic across test iterations.
931 #
932 # To fix this, *never* provide a nan value which is equal (via `is`) to
933 # another provided nan value. This sacrifices some test power; we should
934 # bring that back (ABOVE the choice sequence layer) in the future.
935 #
936 # See https://github.com/HypothesisWorks/hypothesis/issues/3926.
937 if choice_type == "float":
938 assert isinstance(value, float)
939 if math.isnan(value):
940 value = int_to_float(float_to_int(value))
941
942 if observe:
943 was_forced = forced is not None
944 getattr(self.observer, f"draw_{choice_type}")(
945 value, constraints=constraints, was_forced=was_forced
946 )
947 size = 0 if self.provider.avoid_realization else choice_size(value)
948 if self.length + size > self.max_length:
949 debug_report(
950 f"overrun because {self.length=} + {size=} > {self.max_length=}"
951 )
952 self.mark_overrun()
953
954 node = ChoiceNode(
955 type=choice_type,
956 value=value,
957 constraints=constraints,
958 was_forced=was_forced,
959 index=len(self._nodes),
960 )
961 self.__span_record.record_choice()
962 self._nodes.append(node)
963 self.length += size
964
965 return value
966
967 def draw_integer(
968 self,
969 min_value: int | None = None,
970 max_value: int | None = None,
971 *,
972 weights: dict[int, float] | None = None,
973 shrink_towards: int = 0,
974 forced: int | None = None,
975 observe: bool = True,
976 ) -> int:
977 # Validate arguments
978 if weights is not None:
979 assert min_value is not None
980 assert max_value is not None
981 assert len(weights) <= 255 # arbitrary practical limit
982 # We can and should eventually support total weights. But this
983 # complicates shrinking as we can no longer assume we can force
984 # a value to the unmapped probability mass if that mass might be 0.
985 assert sum(weights.values()) < 1
986 # similarly, things get simpler if we assume every value is possible.
987 # we'll want to drop this restriction eventually.
988 assert all(w != 0 for w in weights.values())
989
990 if forced is not None and min_value is not None:
991 assert min_value <= forced
992 if forced is not None and max_value is not None:
993 assert forced <= max_value
994
995 constraints: IntegerConstraints = self._pooled_constraints(
996 "integer",
997 {
998 "min_value": min_value,
999 "max_value": max_value,
1000 "weights": weights,
1001 "shrink_towards": shrink_towards,
1002 },
1003 )
1004 return self._draw("integer", constraints, observe=observe, forced=forced)
1005
1006 def draw_float(
1007 self,
1008 min_value: float = -math.inf,
1009 max_value: float = math.inf,
1010 *,
1011 allow_nan: bool = True,
1012 smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL,
1013 # TODO: consider supporting these float widths at the choice sequence
1014 # level in the future.
1015 # width: Literal[16, 32, 64] = 64,
1016 forced: float | None = None,
1017 observe: bool = True,
1018 ) -> float:
1019 assert smallest_nonzero_magnitude > 0
1020 assert not math.isnan(min_value)
1021 assert not math.isnan(max_value)
1022
1023 if smallest_nonzero_magnitude == 0.0: # pragma: no cover
1024 raise FloatingPointError(
1025 "Got allow_subnormal=True, but we can't represent subnormal floats "
1026 "right now, in violation of the IEEE-754 floating-point "
1027 "specification. This is usually because something was compiled with "
1028 "-ffast-math or a similar option, which sets global processor state. "
1029 "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
1030 "writeup - and good luck!"
1031 )
1032
1033 if forced is not None:
1034 assert allow_nan or not math.isnan(forced)
1035 assert math.isnan(forced) or (
1036 sign_aware_lte(min_value, forced) and sign_aware_lte(forced, max_value)
1037 )
1038
1039 constraints: FloatConstraints = self._pooled_constraints(
1040 "float",
1041 {
1042 "min_value": min_value,
1043 "max_value": max_value,
1044 "allow_nan": allow_nan,
1045 "smallest_nonzero_magnitude": smallest_nonzero_magnitude,
1046 },
1047 )
1048 return self._draw("float", constraints, observe=observe, forced=forced)
1049
1050 def draw_string(
1051 self,
1052 intervals: IntervalSet,
1053 *,
1054 min_size: int = 0,
1055 max_size: int = COLLECTION_DEFAULT_MAX_SIZE,
1056 forced: str | None = None,
1057 observe: bool = True,
1058 ) -> str:
1059 assert forced is None or min_size <= len(forced) <= max_size
1060 assert min_size >= 0
1061 if len(intervals) == 0:
1062 assert min_size == 0
1063
1064 constraints: StringConstraints = self._pooled_constraints(
1065 "string",
1066 {
1067 "intervals": intervals,
1068 "min_size": min_size,
1069 "max_size": max_size,
1070 },
1071 )
1072 return self._draw("string", constraints, observe=observe, forced=forced)
1073
1074 def draw_bytes(
1075 self,
1076 min_size: int = 0,
1077 max_size: int = COLLECTION_DEFAULT_MAX_SIZE,
1078 *,
1079 forced: bytes | None = None,
1080 observe: bool = True,
1081 ) -> bytes:
1082 assert forced is None or min_size <= len(forced) <= max_size
1083 assert min_size >= 0
1084
1085 constraints: BytesConstraints = self._pooled_constraints(
1086 "bytes", {"min_size": min_size, "max_size": max_size}
1087 )
1088 return self._draw("bytes", constraints, observe=observe, forced=forced)
1089
1090 def draw_boolean(
1091 self,
1092 p: float = 0.5,
1093 *,
1094 forced: bool | None = None,
1095 observe: bool = True,
1096 ) -> bool:
1097 assert (forced is not True) or p > 0
1098 assert (forced is not False) or p < 1
1099
1100 constraints: BooleanConstraints = self._pooled_constraints("boolean", {"p": p})
1101 return self._draw("boolean", constraints, observe=observe, forced=forced)
1102
1103 @overload
1104 def _pooled_constraints(
1105 self, choice_type: Literal["integer"], constraints: IntegerConstraints
1106 ) -> IntegerConstraints: ...
1107
1108 @overload
1109 def _pooled_constraints(
1110 self, choice_type: Literal["float"], constraints: FloatConstraints
1111 ) -> FloatConstraints: ...
1112
1113 @overload
1114 def _pooled_constraints(
1115 self, choice_type: Literal["string"], constraints: StringConstraints
1116 ) -> StringConstraints: ...
1117
1118 @overload
1119 def _pooled_constraints(
1120 self, choice_type: Literal["bytes"], constraints: BytesConstraints
1121 ) -> BytesConstraints: ...
1122
1123 @overload
1124 def _pooled_constraints(
1125 self, choice_type: Literal["boolean"], constraints: BooleanConstraints
1126 ) -> BooleanConstraints: ...
1127
1128 def _pooled_constraints(
1129 self, choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT
1130 ) -> ChoiceConstraintsT:
1131 """Memoize common dictionary objects to reduce memory pressure."""
1132 # caching runs afoul of nondeterminism checks
1133 if self.provider.avoid_realization:
1134 return constraints
1135
1136 key = (choice_type, *choice_constraints_key(choice_type, constraints))
1137 try:
1138 return POOLED_CONSTRAINTS_CACHE[key]
1139 except KeyError:
1140 POOLED_CONSTRAINTS_CACHE[key] = constraints
1141 return constraints
1142
1143 def _pop_choice(
1144 self,
1145 choice_type: ChoiceTypeT,
1146 constraints: ChoiceConstraintsT,
1147 *,
1148 forced: ChoiceT | None,
1149 ) -> ChoiceT:
1150 assert self.prefix is not None
1151 # checked in _draw
1152 assert self.index < len(self.prefix)
1153
1154 value = self.prefix[self.index]
1155 if isinstance(value, ChoiceTemplate):
1156 node: ChoiceTemplate = value
1157 if node.count is not None:
1158 assert node.count >= 0
1159 # node templates have to be at the end for now, since it's not immediately
1160 # apparent how to handle overruning a node template while generating a single
1161 # node if the alternative is not "the entire data is an overrun".
1162 assert self.index == len(self.prefix) - 1
1163 if node.type == "simplest":
1164 index = 0
1165 if forced is None and self.unique_element_depth:
1166 # imported here to break an import cycle
1167 from hypothesis.internal.conjecture.datatree import (
1168 compute_max_children,
1169 )
1170
1171 index = self.simplest_index % compute_max_children(
1172 choice_type, constraints
1173 )
1174 self.simplest_index += 1
1175 try:
1176 choice = choice_from_index(index, choice_type, constraints)
1177 except ChoiceTooLarge:
1178 self.mark_overrun()
1179 else:
1180 raise NotImplementedError
1181
1182 if node.count is not None:
1183 node.count -= 1
1184 if node.count < 0:
1185 self.mark_overrun()
1186 return choice
1187
1188 if isinstance(value, ValueHole):
1189 # A hole that no strategy claimed: either its value could not be
1190 # inverted, or it fell out of alignment with strategy draw
1191 # boundaries. Treat it as a misalignment.
1192 if self.misaligned_at is None:
1193 self.misaligned_at = (self.index, choice_type, constraints, forced)
1194 try:
1195 choice = choice_from_index(0, choice_type, constraints)
1196 except ChoiceTooLarge:
1197 self.mark_overrun()
1198 self.index += 1
1199 return choice
1200
1201 choice = value
1202 node_choice_type = {
1203 str: "string",
1204 float: "float",
1205 int: "integer",
1206 bool: "boolean",
1207 bytes: "bytes",
1208 }[type(choice)]
1209 # If we're trying to:
1210 # * draw a different choice type at the same location
1211 # * draw the same choice type with a different constraints, which does not permit
1212 # the current value
1213 #
1214 # then we call this a misalignment, because the choice sequence has
1215 # changed from what we expected at some point. An easy misalignment is
1216 #
1217 # one_of(integers(0, 100), integers(101, 200))
1218 #
1219 # where the choice sequence [0, 100] has constraints {min_value: 0, max_value: 100}
1220 # at index 1, but [0, 101] has constraints {min_value: 101, max_value: 200} at
1221 # index 1 (which does not permit any of the values 0-100).
1222 #
1223 # When the choice sequence becomes misaligned, we generate a new value of the
1224 # type and constraints the strategy expects.
1225 if node_choice_type != choice_type or not choice_permitted(choice, constraints):
1226 # only track first misalignment for now.
1227 if self.misaligned_at is None:
1228 self.misaligned_at = (self.index, choice_type, constraints, forced)
1229 try:
1230 # Fill in any misalignments with index 0 choices. An alternative to
1231 # this is using the index of the misaligned choice instead
1232 # of index 0, which may be useful for maintaining
1233 # "similarly-complex choices" in the shrinker. This requires
1234 # attaching an index to every choice in ConjectureData.for_choices,
1235 # which we don't always have (e.g. when reading from db).
1236 #
1237 # If we really wanted this in the future we could make this complexity
1238 # optional, use it if present, and default to index 0 otherwise.
1239 # This complicates our internal api and so I'd like to avoid it
1240 # if possible.
1241 #
1242 # Additionally, I don't think slips which require
1243 # slipping to high-complexity values are common. Though arguably
1244 # we may want to expand a bit beyond *just* the simplest choice.
1245 # (we could for example consider sampling choices from index 0-10).
1246 choice = choice_from_index(0, choice_type, constraints)
1247 except ChoiceTooLarge:
1248 # should really never happen with a 0-index choice, but let's be safe.
1249 self.mark_overrun()
1250
1251 self.index += 1
1252 return choice
1253
1254 def as_result(self) -> ConjectureResult | _Overrun:
1255 """Convert the result of running this test into
1256 either an Overrun object or a ConjectureResult."""
1257
1258 assert self.frozen
1259 if self.status == Status.OVERRUN:
1260 return Overrun
1261 if self.__result is None:
1262 self.__result = ConjectureResult(
1263 status=self.status,
1264 interesting_origin=self.interesting_origin,
1265 spans=self.spans,
1266 nodes=self.nodes,
1267 length=self.length,
1268 notes=self.notes,
1269 expected_traceback=self.expected_traceback,
1270 expected_exception=self.expected_exception,
1271 has_discards=self.has_discards,
1272 target_observations=self.target_observations,
1273 tags=frozenset(self.tags),
1274 arg_spans=self.arg_spans,
1275 span_comments=self.span_comments,
1276 misaligned_at=self.misaligned_at,
1277 cannot_proceed_scope=self.cannot_proceed_scope,
1278 )
1279 assert self.__result is not None
1280 return self.__result
1281
1282 def __assert_not_frozen(self, name: str) -> None:
1283 if self.frozen:
1284 raise Frozen(f"Cannot call {name} on frozen ConjectureData")
1285
1286 def note(self, value: str) -> None:
1287 self.__assert_not_frozen("note")
1288 self.notes.append(value)
1289
1290 def draw(
1291 self,
1292 strategy: "SearchStrategy[Ex]",
1293 label: int | None = None,
1294 observe_as: str | None = None,
1295 ) -> "Ex":
1296 from hypothesis.internal.observability import observability_enabled
1297 from hypothesis.strategies._internal.lazy import unwrap_strategies
1298 from hypothesis.strategies._internal.utils import to_jsonable
1299
1300 at_top_level = self.depth == 0
1301 start_time = None
1302 gc_start_time = None
1303 if at_top_level:
1304 # We start this timer early, because accessing attributes on a LazyStrategy
1305 # can be almost arbitrarily slow. In cases like characters() and text()
1306 # where we cache something expensive, this led to Flaky deadline errors!
1307 # See https://github.com/HypothesisWorks/hypothesis/issues/2108
1308 start_time = time.perf_counter()
1309 gc_start_time = gc_cumulative_time()
1310
1311 strategy.validate()
1312
1313 if strategy.is_empty:
1314 self.mark_invalid(f"empty strategy {strategy!r}")
1315
1316 if self.depth >= MAX_DEPTH:
1317 self.mark_invalid("max depth exceeded")
1318
1319 # Jump directly to the unwrapped strategy for the label and for do_draw.
1320 # This avoids adding an extra span to all lazy strategies.
1321 unwrapped = unwrap_strategies(strategy)
1322 if label is None:
1323 label = unwrapped.label
1324 assert isinstance(label, int)
1325
1326 # If the next prefix element is a ValueHole, we are the strategy being
1327 # asked to re-encode its value: replace the hole with our inversion of
1328 # it, and let do_draw consume those choices (under our own constraints)
1329 # as usual. If we can't invert it, leave the hole for _pop_choice to
1330 # treat as a misalignment.
1331 if (
1332 self.prefix is not None
1333 and self.index < len(self.prefix)
1334 and isinstance(hole := self.prefix[self.index], ValueHole)
1335 ):
1336 self._inverting = True
1337 try:
1338 inverted = unwrapped._invert(hole.value)
1339 except Exception:
1340 # Usually CannotInvert, but _invert may execute arbitrary user code, eg
1341 # if a .filter is involved.
1342 pass
1343 else:
1344 self.prefix = (
1345 tuple(self.prefix[: self.index])
1346 + inverted
1347 + tuple(self.prefix[self.index + 1 :])
1348 )
1349 finally:
1350 self._inverting = False
1351
1352 span_index = self.__span_record.span_count
1353 self.start_span(label=label)
1354 try:
1355 if not at_top_level:
1356 try:
1357 v = unwrapped.do_draw(self)
1358 self.__span_record.record_value_for_span(span_index, v)
1359 return v
1360 except FlakyStrategyDefinition as err:
1361 # Record the strategy stack as the error unwinds, so that an
1362 # inconsistent-generation failure is explained in terms of the
1363 # strategies being drawn from, not just the choice sequence.
1364 # The top-level draw adds its own "while generating ..." note.
1365 add_note(err, f"while drawing from {strategy!r}")
1366 raise
1367 assert start_time is not None
1368 assert gc_start_time is not None
1369 key = observe_as or f"generate:unlabeled_{len(self.draw_times)}"
1370 try:
1371 try:
1372 v = unwrapped.do_draw(self)
1373 finally:
1374 # Subtract the time spent in GC to avoid overcounting, as it is
1375 # accounted for at the overall example level.
1376 in_gctime = gc_cumulative_time() - gc_start_time
1377 self.draw_times[key] = time.perf_counter() - start_time - in_gctime
1378 except Exception as err:
1379 add_note(
1380 err,
1381 f"while generating {key.removeprefix('generate:')!r} from {strategy!r}",
1382 )
1383 raise
1384 if observability_enabled():
1385 avoid = self.provider.avoid_realization
1386 self._observability_args[key] = to_jsonable(v, avoid_realization=avoid)
1387 self.__span_record.record_value_for_span(span_index, v)
1388 return v
1389 finally:
1390 self.stop_span()
1391
1392 @property
1393 def next_span_index(self) -> int:
1394 """The index that the next span to start will get. Spans are indexed
1395 in start order, so this also counts the spans started so far."""
1396 return self.__span_record.span_count
1397
1398 @contextmanager
1399 def track_arg_span(self) -> Generator[int]:
1400 # Record the span opened by the draw inside this block in ``arg_spans``,
1401 # for the shrinker's explain phase to vary and comment on.
1402 #
1403 # Yields the span's index, which we know in advance even though Span
1404 # objects are only materialized after the test case is completed. (If the
1405 # draw raises instead, we skip recording, along with the rest of the test
1406 # case.)
1407 span_index = self.next_span_index
1408 yield span_index
1409 self.arg_spans.add(span_index)
1410
1411 @contextmanager
1412 def track_arg_label(self, label: str) -> Generator[ArgLabelsT]:
1413 arg_labels: ArgLabelsT = {}
1414
1415 with self.track_arg_span() as span_index:
1416 yield arg_labels
1417
1418 # Mutate the arg_labels dict so that the pretty-printer knows where to
1419 # place the which-parts-matter comments later.
1420 arg_labels[label] = span_index
1421
1422 def start_span(self, label: int) -> None:
1423 self.provider.span_start(label)
1424 self.__assert_not_frozen("start_span")
1425 self.depth += 1
1426 # Logically it would make sense for this to just be
1427 # ``self.depth = max(self.depth, self.max_depth)``, which is what it used to
1428 # be until we ran the code under tracemalloc and found a rather significant
1429 # chunk of allocation was happening here. This was presumably due to varargs
1430 # or the like, but we didn't investigate further given that it was easy
1431 # to fix with this check.
1432 if self.depth > self.max_depth:
1433 self.max_depth = self.depth
1434 self.__span_record.start_span(label)
1435 self.labels_for_structure_stack.append({label})
1436
1437 def stop_span(self, *, discard: bool = False) -> None:
1438 self.provider.span_end(discard)
1439 if self.frozen:
1440 return
1441 if discard:
1442 self.has_discards = True
1443 self.depth -= 1
1444 assert self.depth >= -1
1445 self.__span_record.stop_span(discard=discard)
1446
1447 labels_for_structure = self.labels_for_structure_stack.pop()
1448
1449 if not discard:
1450 if self.labels_for_structure_stack:
1451 self.labels_for_structure_stack[-1].update(labels_for_structure)
1452 else:
1453 self.tags.update([structural_coverage(l) for l in labels_for_structure])
1454
1455 if discard:
1456 # Once we've discarded a span, every test case starting with
1457 # this prefix contains discards. We prune the tree at that point so
1458 # as to avoid future test cases bothering with this region, on the
1459 # assumption that some span that you could have used instead
1460 # there would *not* trigger the discard. This greatly speeds up
1461 # test case generation in some cases, because it allows us to
1462 # ignore large swathes of the search space that are effectively
1463 # redundant.
1464 #
1465 # A scenario that can cause us problems but which we deliberately
1466 # have decided not to support is that if there are side effects
1467 # during data generation then you may end up with a scenario where
1468 # every good test case generates a discard because the discarded
1469 # section sets up important things for later. This is not terribly
1470 # likely and all that you see in this case is some degradation in
1471 # quality of testing, so we don't worry about it.
1472 #
1473 # Note that killing the branch does *not* mean we will never
1474 # explore below this point, and in particular we may do so during
1475 # shrinking. Any explicit request for a data object that starts
1476 # with the branch here will work just fine, but novel prefix
1477 # generation will avoid it, and we can use it to detect when we
1478 # have explored the entire tree (up to redundancy).
1479
1480 self.observer.kill_branch()
1481
1482 @property
1483 def spans(self) -> Spans:
1484 assert self.frozen
1485 if self.__spans is None:
1486 self.__spans = Spans(record=self.__span_record)
1487 return self.__spans
1488
1489 def freeze(self) -> None:
1490 if self.frozen:
1491 return
1492 self.finish_time = time.perf_counter()
1493 self.gc_finish_time = gc_cumulative_time()
1494
1495 # Always finish by closing all remaining spans so that we have a valid tree.
1496 while self.depth >= 0:
1497 self.stop_span()
1498
1499 self.__span_record.freeze()
1500 self.frozen = True
1501 self.observer.conclude_test(self.status, self.interesting_origin)
1502
1503 def choice(
1504 self,
1505 values: Sequence[T],
1506 *,
1507 forced: T | None = None,
1508 observe: bool = True,
1509 ) -> T:
1510 forced_i = None if forced is None else values.index(forced)
1511 i = self.draw_integer(
1512 0,
1513 len(values) - 1,
1514 forced=forced_i,
1515 observe=observe,
1516 )
1517 return values[i]
1518
1519 def conclude_test(
1520 self,
1521 status: Status,
1522 interesting_origin: InterestingOrigin | None = None,
1523 ) -> NoReturn:
1524 assert (interesting_origin is None) or (status == Status.INTERESTING)
1525 self.__assert_not_frozen("conclude_test")
1526 self.interesting_origin = interesting_origin
1527 self.status = status
1528 self.freeze()
1529 raise StopTest(self.testcounter)
1530
1531 def mark_interesting(self, interesting_origin: InterestingOrigin) -> NoReturn:
1532 self.conclude_test(Status.INTERESTING, interesting_origin)
1533
1534 def mark_invalid(
1535 self, why: str | None = None, *, location: str | None = None
1536 ) -> NoReturn:
1537 if why is not None:
1538 self.events["gave up because"] = why
1539 self.invalid_location = location
1540 self.conclude_test(Status.INVALID)
1541
1542 def mark_overrun(self) -> NoReturn:
1543 self.conclude_test(Status.OVERRUN)
1544
1545 def last_rejected_filter_location(self) -> str | None:
1546 """The location of the most recently rejected filter."""
1547 if self._last_rejected_filter is None:
1548 return None
1549 condition, location = self._last_rejected_filter
1550 # fall back to where the predicate was defined if no location is known
1551 return location or function_location(condition)
1552
1553
1554def draw_choice(
1555 choice_type: ChoiceTypeT, constraints: ChoiceConstraintsT, *, random: Random
1556) -> ChoiceT:
1557 cd = ConjectureData(random=random)
1558 return cast(ChoiceT, getattr(cd.provider, f"draw_{choice_type}")(**constraints))