181 lines
6.4 KiB
Python
181 lines
6.4 KiB
Python
# 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 contextlib
|
|
import os
|
|
import sys
|
|
import textwrap
|
|
import traceback
|
|
from inspect import getframeinfo
|
|
from pathlib import Path
|
|
from typing import Dict, NamedTuple, Optional, Type
|
|
|
|
import hypothesis
|
|
from hypothesis.errors import (
|
|
DeadlineExceeded,
|
|
HypothesisException,
|
|
StopTest,
|
|
UnsatisfiedAssumption,
|
|
_Trimmable,
|
|
)
|
|
from hypothesis.internal.compat import BaseExceptionGroup
|
|
from hypothesis.utils.dynamicvariables import DynamicVariable
|
|
|
|
|
|
def belongs_to(package):
|
|
if not hasattr(package, "__file__"): # pragma: no cover
|
|
return lambda filepath: False
|
|
|
|
root = Path(package.__file__).resolve().parent
|
|
cache = {str: {}, bytes: {}}
|
|
|
|
def accept(filepath):
|
|
ftype = type(filepath)
|
|
try:
|
|
return cache[ftype][filepath]
|
|
except KeyError:
|
|
pass
|
|
try:
|
|
Path(filepath).resolve().relative_to(root)
|
|
result = True
|
|
except Exception:
|
|
result = False
|
|
cache[ftype][filepath] = result
|
|
return result
|
|
|
|
accept.__name__ = f"is_{package.__name__}_file"
|
|
return accept
|
|
|
|
|
|
PREVENT_ESCALATION = os.getenv("HYPOTHESIS_DO_NOT_ESCALATE") == "true"
|
|
|
|
FILE_CACHE: Dict[bytes, bool] = {}
|
|
|
|
|
|
is_hypothesis_file = belongs_to(hypothesis)
|
|
|
|
HYPOTHESIS_CONTROL_EXCEPTIONS = (DeadlineExceeded, StopTest, UnsatisfiedAssumption)
|
|
|
|
|
|
def escalate_hypothesis_internal_error():
|
|
if PREVENT_ESCALATION:
|
|
return
|
|
|
|
_, e, tb = sys.exc_info()
|
|
|
|
if getattr(e, "hypothesis_internal_never_escalate", False):
|
|
return
|
|
|
|
filepath = None if tb is None else traceback.extract_tb(tb)[-1][0]
|
|
if is_hypothesis_file(filepath) and not isinstance(
|
|
e, (HypothesisException, *HYPOTHESIS_CONTROL_EXCEPTIONS)
|
|
):
|
|
raise
|
|
|
|
|
|
def get_trimmed_traceback(exception=None):
|
|
"""Return the current traceback, minus any frames added by Hypothesis."""
|
|
if exception is None:
|
|
_, exception, tb = sys.exc_info()
|
|
else:
|
|
tb = exception.__traceback__
|
|
# Avoid trimming the traceback if we're in verbose mode, or the error
|
|
# was raised inside Hypothesis
|
|
if (
|
|
tb is None
|
|
or hypothesis.settings.default.verbosity >= hypothesis.Verbosity.debug
|
|
or is_hypothesis_file(traceback.extract_tb(tb)[-1][0])
|
|
and not isinstance(exception, _Trimmable)
|
|
):
|
|
return tb
|
|
while tb.tb_next is not None and (
|
|
# If the frame is from one of our files, it's been added by Hypothesis.
|
|
is_hypothesis_file(getframeinfo(tb.tb_frame).filename)
|
|
# But our `@proxies` decorator overrides the source location,
|
|
# so we check for an attribute it injects into the frame too.
|
|
or tb.tb_frame.f_globals.get("__hypothesistracebackhide__") is True
|
|
):
|
|
tb = tb.tb_next
|
|
return tb
|
|
|
|
|
|
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.
|
|
exc_type: Type[BaseException]
|
|
filename: Optional[str]
|
|
lineno: Optional[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
|
|
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 (),
|
|
)
|
|
|
|
|
|
current_pytest_item = DynamicVariable(None)
|
|
|
|
|
|
def _get_exceptioninfo():
|
|
# ExceptionInfo was moved to the top-level namespace in Pytest 7.0
|
|
if "pytest" in sys.modules:
|
|
with contextlib.suppress(Exception):
|
|
# From Pytest 7, __init__ warns on direct calls.
|
|
return sys.modules["pytest"].ExceptionInfo.from_exc_info
|
|
if "_pytest._code" in sys.modules: # old versions only
|
|
with contextlib.suppress(Exception):
|
|
return sys.modules["_pytest._code"].ExceptionInfo
|
|
return None # pragma: no cover # coverage tests always use pytest
|
|
|
|
|
|
def format_exception(err, tb):
|
|
# Try using Pytest to match the currently configured traceback style
|
|
ExceptionInfo = _get_exceptioninfo()
|
|
if current_pytest_item.value is not None and ExceptionInfo is not None:
|
|
item = current_pytest_item.value
|
|
return str(item.repr_failure(ExceptionInfo((type(err), err, tb)))) + "\n"
|
|
|
|
# Or use better_exceptions, if that's installed and enabled
|
|
if "better_exceptions" in sys.modules:
|
|
better_exceptions = sys.modules["better_exceptions"]
|
|
if sys.excepthook is better_exceptions.excepthook:
|
|
return "".join(better_exceptions.format_exception(type(err), err, tb))
|
|
|
|
# If all else fails, use the standard-library formatting tools
|
|
return "".join(traceback.format_exception(type(err), err, tb))
|