From 8a67dc768ccf5a8adea41af11d975b3a7683a1e1 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 26 Nov 2023 23:12:09 -0800 Subject: [PATCH] refactor interesting_origin --- hypothesis-python/src/hypothesis/core.py | 4 +- .../src/hypothesis/internal/escalation.py | 53 ++++++++++++------- .../tests/cover/test_escalation.py | 20 ++++++- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index 1c4cf9e144..723e3da646 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -80,10 +80,10 @@ from hypothesis.internal.conjecture.shrinker import sort_key from hypothesis.internal.entropy import deterministic_PRNG from hypothesis.internal.escalation import ( + InterestingOrigin, current_pytest_item, escalate_hypothesis_internal_error, format_exception, - get_interesting_origin, get_trimmed_traceback, ) from hypothesis.internal.healthcheck import fail_health_check @@ -970,7 +970,7 @@ def _execute_once_for_engine(self, data): self.failed_normally = True - interesting_origin = get_interesting_origin(e) + interesting_origin = InterestingOrigin.from_exception(e) if trace: # pragma: no cover # Trace collection is explicitly disabled under coverage. self.explain_traces[interesting_origin].add(trace) diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py index 605ea52e97..24015e691e 100644 --- a/hypothesis-python/src/hypothesis/internal/escalation.py +++ b/hypothesis-python/src/hypothesis/internal/escalation.py @@ -11,10 +11,11 @@ import contextlib import os import sys +import textwrap import traceback from inspect import getframeinfo from pathlib import Path -from typing import Dict +from typing import Dict, NamedTuple, Type import hypothesis from hypothesis.errors import ( @@ -105,32 +106,46 @@ def get_trimmed_traceback(exception=None): return tb -def get_interesting_origin(exception): +class InterestingOrigin(NamedTuple): # The `interesting_origin` is how Hypothesis distinguishes between multiple # failures, for reporting and also to replay from the example database (even # if report_multiple_bugs=False). We traditionally use the exception type and # location, but have extracted this logic in order to see through `except ...:` # blocks and understand the __cause__ (`raise x from y`) or __context__ that # first raised an exception as well as PEP-654 exception groups. - tb = get_trimmed_traceback(exception) - if tb is None: + exc_type: Type[BaseException] + filename: str + lineno: int + context: "InterestingOrigin | tuple[()]" + group_elems: "tuple[InterestingOrigin, ...]" + + def __str__(self) -> str: + ctx = "" + if self.context: + ctx = textwrap.indent(f"\ncontext: {self.context}", prefix=" ") + group = "" + if self.group_elems: + chunks = "\n ".join(str(x) for x in self.group_elems) + group = textwrap.indent(f"\nchild exceptions:\n {chunks}", prefix=" ") + return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}" + + @classmethod + def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin": filename, lineno = None, None - else: - filename, lineno, *_ = traceback.extract_tb(tb)[-1] - return ( - type(exception), - filename, - lineno, - # Note that if __cause__ is set it is always equal to __context__, explicitly - # to support introspection when debugging, so we can use that unconditionally. - get_interesting_origin(exception.__context__) if exception.__context__ else (), - # We distinguish exception groups by the inner exceptions, as for __context__ - tuple( - map(get_interesting_origin, exception.exceptions) + if tb := get_trimmed_traceback(exception): + filename, lineno, *_ = traceback.extract_tb(tb)[-1] + return cls( + type(exception), + filename, + lineno, + # Note that if __cause__ is set it is always equal to __context__, explicitly + # to support introspection when debugging, so we can use that unconditionally. + cls.from_exception(exception.__context__) if exception.__context__ else (), + # We distinguish exception groups by the inner exceptions, as for __context__ + tuple(map(cls.from_exception, exception.exceptions)) if isinstance(exception, BaseExceptionGroup) - else [] - ), - ) + else (), + ) current_pytest_item = DynamicVariable(None) diff --git a/hypothesis-python/tests/cover/test_escalation.py b/hypothesis-python/tests/cover/test_escalation.py index 212f462c51..2a176403f3 100644 --- a/hypothesis-python/tests/cover/test_escalation.py +++ b/hypothesis-python/tests/cover/test_escalation.py @@ -78,4 +78,22 @@ def test_errors_attribute_error(): def test_handles_null_traceback(): - esc.get_interesting_origin(Exception()) + esc.InterestingOrigin.from_exception(Exception()) + + +def test_handles_context(): + e = ValueError() + e.__context__ = KeyError() + origin = esc.InterestingOrigin.from_exception(e) + assert "ValueError at " in str(origin) + assert " context: " in str(origin) + assert "KeyError at " in str(origin) + + +def test_handles_groups(): + origin = esc.InterestingOrigin.from_exception( + BaseExceptionGroup("message", [ValueError("msg2")]) + ) + assert "ExceptionGroup at " in str(origin) + assert "child exception" in str(origin) + assert "ValueError at " in str(origin)