1793 lines
59 KiB
Python
1793 lines
59 KiB
Python
# mypy: allow-untyped-defs
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import io
|
|
import operator
|
|
import queue
|
|
import re
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from typing import TYPE_CHECKING
|
|
|
|
import _pytest._code
|
|
import pytest
|
|
from _pytest._code.code import ExceptionChainRepr
|
|
from _pytest._code.code import ExceptionInfo
|
|
from _pytest._code.code import FormattedExcinfo
|
|
from _pytest._io import TerminalWriter
|
|
from _pytest.monkeypatch import MonkeyPatch
|
|
from _pytest.pathlib import bestrelpath
|
|
from _pytest.pathlib import import_path
|
|
from _pytest.pytester import LineMatcher
|
|
from _pytest.pytester import Pytester
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from _pytest._code.code import _TracebackStyle
|
|
|
|
if sys.version_info[:2] < (3, 11):
|
|
from exceptiongroup import ExceptionGroup
|
|
|
|
|
|
@pytest.fixture
|
|
def limited_recursion_depth():
|
|
before = sys.getrecursionlimit()
|
|
sys.setrecursionlimit(150)
|
|
yield
|
|
sys.setrecursionlimit(before)
|
|
|
|
|
|
def test_excinfo_simple() -> None:
|
|
try:
|
|
raise ValueError
|
|
except ValueError:
|
|
info = _pytest._code.ExceptionInfo.from_current()
|
|
assert info.type == ValueError
|
|
|
|
|
|
def test_excinfo_from_exc_info_simple() -> None:
|
|
try:
|
|
raise ValueError
|
|
except ValueError as e:
|
|
assert e.__traceback__ is not None
|
|
info = _pytest._code.ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
|
assert info.type == ValueError
|
|
|
|
|
|
def test_excinfo_from_exception_simple() -> None:
|
|
try:
|
|
raise ValueError
|
|
except ValueError as e:
|
|
assert e.__traceback__ is not None
|
|
info = _pytest._code.ExceptionInfo.from_exception(e)
|
|
assert info.type == ValueError
|
|
|
|
|
|
def test_excinfo_from_exception_missing_traceback_assertion() -> None:
|
|
with pytest.raises(AssertionError, match=r"must have.*__traceback__"):
|
|
_pytest._code.ExceptionInfo.from_exception(ValueError())
|
|
|
|
|
|
def test_excinfo_getstatement():
|
|
def g():
|
|
raise ValueError
|
|
|
|
def f():
|
|
g()
|
|
|
|
try:
|
|
f()
|
|
except ValueError:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
linenumbers = [
|
|
f.__code__.co_firstlineno - 1 + 4,
|
|
f.__code__.co_firstlineno - 1 + 1,
|
|
g.__code__.co_firstlineno - 1 + 1,
|
|
]
|
|
values = list(excinfo.traceback)
|
|
foundlinenumbers = [x.lineno for x in values]
|
|
assert foundlinenumbers == linenumbers
|
|
# for x in info:
|
|
# print "%s:%d %s" %(x.path.relto(root), x.lineno, x.statement)
|
|
# xxx
|
|
|
|
|
|
# testchain for getentries test below
|
|
|
|
|
|
def f():
|
|
#
|
|
raise ValueError
|
|
#
|
|
|
|
|
|
def g():
|
|
#
|
|
__tracebackhide__ = True
|
|
f()
|
|
#
|
|
|
|
|
|
def h():
|
|
#
|
|
g()
|
|
#
|
|
|
|
|
|
class TestTraceback_f_g_h:
|
|
def setup_method(self, method):
|
|
try:
|
|
h()
|
|
except ValueError:
|
|
self.excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
|
|
def test_traceback_entries(self):
|
|
tb = self.excinfo.traceback
|
|
entries = list(tb)
|
|
assert len(tb) == 4 # maybe fragile test
|
|
assert len(entries) == 4 # maybe fragile test
|
|
names = ["f", "g", "h"]
|
|
for entry in entries:
|
|
try:
|
|
names.remove(entry.frame.code.name)
|
|
except ValueError:
|
|
pass
|
|
assert not names
|
|
|
|
def test_traceback_entry_getsource(self):
|
|
tb = self.excinfo.traceback
|
|
s = str(tb[-1].getsource())
|
|
assert s.startswith("def f():")
|
|
assert s.endswith("raise ValueError")
|
|
|
|
def test_traceback_entry_getsource_in_construct(self):
|
|
def xyz():
|
|
try:
|
|
raise ValueError
|
|
except somenoname: # type: ignore[name-defined] # noqa: F821
|
|
pass # pragma: no cover
|
|
|
|
try:
|
|
xyz()
|
|
except NameError:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
else:
|
|
assert False, "did not raise NameError"
|
|
|
|
tb = excinfo.traceback
|
|
source = tb[-1].getsource()
|
|
assert source is not None
|
|
assert source.deindent().lines == [
|
|
"def xyz():",
|
|
" try:",
|
|
" raise ValueError",
|
|
" except somenoname: # type: ignore[name-defined] # noqa: F821",
|
|
]
|
|
|
|
def test_traceback_cut(self) -> None:
|
|
co = _pytest._code.Code.from_function(f)
|
|
path, firstlineno = co.path, co.firstlineno
|
|
assert isinstance(path, Path)
|
|
traceback = self.excinfo.traceback
|
|
newtraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
|
assert len(newtraceback) == 1
|
|
newtraceback = traceback.cut(path=path, lineno=firstlineno + 2)
|
|
assert len(newtraceback) == 1
|
|
|
|
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
|
|
p = pytester.makepyfile("def f(): raise ValueError")
|
|
with pytest.raises(ValueError) as excinfo:
|
|
import_path(p, root=pytester.path).f() # type: ignore[attr-defined]
|
|
basedir = Path(pytest.__file__).parent
|
|
newtraceback = excinfo.traceback.cut(excludepath=basedir)
|
|
for x in newtraceback:
|
|
assert isinstance(x.path, Path)
|
|
assert basedir not in x.path.parents
|
|
assert newtraceback[-1].frame.code.path == p
|
|
|
|
def test_traceback_filter(self):
|
|
traceback = self.excinfo.traceback
|
|
ntraceback = traceback.filter(self.excinfo)
|
|
assert len(ntraceback) == len(traceback) - 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"tracebackhide, matching",
|
|
[
|
|
(lambda info: True, True),
|
|
(lambda info: False, False),
|
|
(operator.methodcaller("errisinstance", ValueError), True),
|
|
(operator.methodcaller("errisinstance", IndexError), False),
|
|
],
|
|
)
|
|
def test_traceback_filter_selective(self, tracebackhide, matching):
|
|
def f():
|
|
#
|
|
raise ValueError
|
|
#
|
|
|
|
def g():
|
|
#
|
|
__tracebackhide__ = tracebackhide
|
|
f()
|
|
#
|
|
|
|
def h():
|
|
#
|
|
g()
|
|
#
|
|
|
|
excinfo = pytest.raises(ValueError, h)
|
|
traceback = excinfo.traceback
|
|
ntraceback = traceback.filter(excinfo)
|
|
print(f"old: {traceback!r}")
|
|
print(f"new: {ntraceback!r}")
|
|
|
|
if matching:
|
|
assert len(ntraceback) == len(traceback) - 2
|
|
else:
|
|
# -1 because of the __tracebackhide__ in pytest.raises
|
|
assert len(ntraceback) == len(traceback) - 1
|
|
|
|
def test_traceback_recursion_index(self):
|
|
def f(n):
|
|
if n < 10:
|
|
n += 1
|
|
f(n)
|
|
|
|
excinfo = pytest.raises(RuntimeError, f, 8)
|
|
traceback = excinfo.traceback
|
|
recindex = traceback.recursionindex()
|
|
assert recindex == 3
|
|
|
|
def test_traceback_only_specific_recursion_errors(self, monkeypatch):
|
|
def f(n):
|
|
if n == 0:
|
|
raise RuntimeError("hello")
|
|
f(n - 1)
|
|
|
|
excinfo = pytest.raises(RuntimeError, f, 25)
|
|
monkeypatch.delattr(excinfo.traceback.__class__, "recursionindex")
|
|
repr = excinfo.getrepr()
|
|
assert "RuntimeError: hello" in str(repr.reprcrash)
|
|
|
|
def test_traceback_no_recursion_index(self) -> None:
|
|
def do_stuff() -> None:
|
|
raise RuntimeError
|
|
|
|
def reraise_me() -> None:
|
|
import sys
|
|
|
|
exc, val, tb = sys.exc_info()
|
|
assert val is not None
|
|
raise val.with_traceback(tb)
|
|
|
|
def f(n: int) -> None:
|
|
try:
|
|
do_stuff()
|
|
except BaseException:
|
|
reraise_me()
|
|
|
|
excinfo = pytest.raises(RuntimeError, f, 8)
|
|
assert excinfo is not None
|
|
traceback = excinfo.traceback
|
|
recindex = traceback.recursionindex()
|
|
assert recindex is None
|
|
|
|
def test_traceback_messy_recursion(self):
|
|
# XXX: simplified locally testable version
|
|
decorator = pytest.importorskip("decorator").decorator
|
|
|
|
def log(f, *k, **kw):
|
|
print(f"{k} {kw}")
|
|
f(*k, **kw)
|
|
|
|
log = decorator(log)
|
|
|
|
def fail():
|
|
raise ValueError("")
|
|
|
|
fail = log(log(fail))
|
|
|
|
excinfo = pytest.raises(ValueError, fail)
|
|
assert excinfo.traceback.recursionindex() is None
|
|
|
|
def test_getreprcrash(self):
|
|
def i():
|
|
__tracebackhide__ = True
|
|
raise ValueError
|
|
|
|
def h():
|
|
i()
|
|
|
|
def g():
|
|
__tracebackhide__ = True
|
|
h()
|
|
|
|
def f():
|
|
g()
|
|
|
|
excinfo = pytest.raises(ValueError, f)
|
|
reprcrash = excinfo._getreprcrash()
|
|
assert reprcrash is not None
|
|
co = _pytest._code.Code.from_function(h)
|
|
assert reprcrash.path == str(co.path)
|
|
assert reprcrash.lineno == co.firstlineno + 1 + 1
|
|
|
|
def test_getreprcrash_empty(self):
|
|
def g():
|
|
__tracebackhide__ = True
|
|
raise ValueError
|
|
|
|
def f():
|
|
__tracebackhide__ = True
|
|
g()
|
|
|
|
excinfo = pytest.raises(ValueError, f)
|
|
assert excinfo._getreprcrash() is None
|
|
|
|
|
|
def test_excinfo_exconly():
|
|
excinfo = pytest.raises(ValueError, h)
|
|
assert excinfo.exconly().startswith("ValueError")
|
|
with pytest.raises(ValueError) as excinfo:
|
|
raise ValueError("hello\nworld")
|
|
msg = excinfo.exconly(tryshort=True)
|
|
assert msg.startswith("ValueError")
|
|
assert msg.endswith("world")
|
|
|
|
|
|
def test_excinfo_repr_str() -> None:
|
|
excinfo1 = pytest.raises(ValueError, h)
|
|
assert repr(excinfo1) == "<ExceptionInfo ValueError() tblen=4>"
|
|
assert str(excinfo1) == "<ExceptionInfo ValueError() tblen=4>"
|
|
|
|
class CustomException(Exception):
|
|
def __repr__(self):
|
|
return "custom_repr"
|
|
|
|
def raises() -> None:
|
|
raise CustomException()
|
|
|
|
excinfo2 = pytest.raises(CustomException, raises)
|
|
assert repr(excinfo2) == "<ExceptionInfo custom_repr tblen=2>"
|
|
assert str(excinfo2) == "<ExceptionInfo custom_repr tblen=2>"
|
|
|
|
|
|
def test_excinfo_for_later() -> None:
|
|
e = ExceptionInfo[BaseException].for_later()
|
|
assert "for raises" in repr(e)
|
|
assert "for raises" in str(e)
|
|
|
|
|
|
def test_excinfo_errisinstance():
|
|
excinfo = pytest.raises(ValueError, h)
|
|
assert excinfo.errisinstance(ValueError)
|
|
|
|
|
|
def test_excinfo_no_sourcecode():
|
|
try:
|
|
exec("raise ValueError()")
|
|
except ValueError:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
s = str(excinfo.traceback[-1])
|
|
assert s == " File '<string>':1 in <module>\n ???\n"
|
|
|
|
|
|
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
|
|
# XXX: simplified locally testable version
|
|
tmp_path.joinpath("test.txt").write_text("{{ h()}}:", encoding="utf-8")
|
|
|
|
jinja2 = pytest.importorskip("jinja2")
|
|
loader = jinja2.FileSystemLoader(str(tmp_path))
|
|
env = jinja2.Environment(loader=loader)
|
|
template = env.get_template("test.txt")
|
|
excinfo = pytest.raises(ValueError, template.render, h=h)
|
|
for item in excinfo.traceback:
|
|
print(item) # XXX: for some reason jinja.Template.render is printed in full
|
|
item.source # shouldn't fail
|
|
if isinstance(item.path, Path) and item.path.name == "test.txt":
|
|
assert str(item.source) == "{{ h()}}:"
|
|
|
|
|
|
def test_entrysource_Queue_example():
|
|
try:
|
|
queue.Queue().get(timeout=0.001)
|
|
except queue.Empty:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
entry = excinfo.traceback[-1]
|
|
source = entry.getsource()
|
|
assert source is not None
|
|
s = str(source).strip()
|
|
assert s.startswith("def get")
|
|
|
|
|
|
def test_codepath_Queue_example() -> None:
|
|
try:
|
|
queue.Queue().get(timeout=0.001)
|
|
except queue.Empty:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
entry = excinfo.traceback[-1]
|
|
path = entry.path
|
|
assert isinstance(path, Path)
|
|
assert path.name.lower() == "queue.py"
|
|
assert path.exists()
|
|
|
|
|
|
def test_match_succeeds():
|
|
with pytest.raises(ZeroDivisionError) as excinfo:
|
|
0 // 0
|
|
excinfo.match(r".*zero.*")
|
|
|
|
|
|
def test_match_raises_error(pytester: Pytester) -> None:
|
|
pytester.makepyfile(
|
|
"""
|
|
import pytest
|
|
def test_division_zero():
|
|
with pytest.raises(ZeroDivisionError) as excinfo:
|
|
0 / 0
|
|
excinfo.match(r'[123]+')
|
|
"""
|
|
)
|
|
result = pytester.runpytest("--tb=short")
|
|
assert result.ret != 0
|
|
|
|
match = [
|
|
r"E .* AssertionError: Regex pattern did not match.",
|
|
r"E .* Regex: '\[123\]\+'",
|
|
r"E .* Input: 'division by zero'",
|
|
]
|
|
result.stdout.re_match_lines(match)
|
|
result.stdout.no_fnmatch_line("*__tracebackhide__ = True*")
|
|
|
|
result = pytester.runpytest("--fulltrace")
|
|
assert result.ret != 0
|
|
result.stdout.re_match_lines([r".*__tracebackhide__ = True.*", *match])
|
|
|
|
|
|
class TestGroupContains:
|
|
def test_contains_exception_type(self) -> None:
|
|
exc_group = ExceptionGroup("", [RuntimeError()])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(RuntimeError)
|
|
|
|
def test_doesnt_contain_exception_type(self) -> None:
|
|
exc_group = ExceptionGroup("", [ValueError()])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert not exc_info.group_contains(RuntimeError)
|
|
|
|
def test_contains_exception_match(self) -> None:
|
|
exc_group = ExceptionGroup("", [RuntimeError("exception message")])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(RuntimeError, match=r"^exception message$")
|
|
|
|
def test_doesnt_contain_exception_match(self) -> None:
|
|
exc_group = ExceptionGroup("", [RuntimeError("message that will not match")])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert not exc_info.group_contains(RuntimeError, match=r"^exception message$")
|
|
|
|
def test_contains_exception_type_unlimited_depth(self) -> None:
|
|
exc_group = ExceptionGroup("", [ExceptionGroup("", [RuntimeError()])])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(RuntimeError)
|
|
|
|
def test_contains_exception_type_at_depth_1(self) -> None:
|
|
exc_group = ExceptionGroup("", [RuntimeError()])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(RuntimeError, depth=1)
|
|
|
|
def test_doesnt_contain_exception_type_past_depth(self) -> None:
|
|
exc_group = ExceptionGroup("", [ExceptionGroup("", [RuntimeError()])])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert not exc_info.group_contains(RuntimeError, depth=1)
|
|
|
|
def test_contains_exception_type_specific_depth(self) -> None:
|
|
exc_group = ExceptionGroup("", [ExceptionGroup("", [RuntimeError()])])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(RuntimeError, depth=2)
|
|
|
|
def test_contains_exception_match_unlimited_depth(self) -> None:
|
|
exc_group = ExceptionGroup(
|
|
"", [ExceptionGroup("", [RuntimeError("exception message")])]
|
|
)
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(RuntimeError, match=r"^exception message$")
|
|
|
|
def test_contains_exception_match_at_depth_1(self) -> None:
|
|
exc_group = ExceptionGroup("", [RuntimeError("exception message")])
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(
|
|
RuntimeError, match=r"^exception message$", depth=1
|
|
)
|
|
|
|
def test_doesnt_contain_exception_match_past_depth(self) -> None:
|
|
exc_group = ExceptionGroup(
|
|
"", [ExceptionGroup("", [RuntimeError("exception message")])]
|
|
)
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert not exc_info.group_contains(
|
|
RuntimeError, match=r"^exception message$", depth=1
|
|
)
|
|
|
|
def test_contains_exception_match_specific_depth(self) -> None:
|
|
exc_group = ExceptionGroup(
|
|
"", [ExceptionGroup("", [RuntimeError("exception message")])]
|
|
)
|
|
with pytest.raises(ExceptionGroup) as exc_info:
|
|
raise exc_group
|
|
assert exc_info.group_contains(
|
|
RuntimeError, match=r"^exception message$", depth=2
|
|
)
|
|
|
|
|
|
class TestFormattedExcinfo:
|
|
@pytest.fixture
|
|
def importasmod(self, tmp_path: Path, _sys_snapshot):
|
|
def importasmod(source):
|
|
source = textwrap.dedent(source)
|
|
modpath = tmp_path.joinpath("mod.py")
|
|
tmp_path.joinpath("__init__.py").touch()
|
|
modpath.write_text(source, encoding="utf-8")
|
|
importlib.invalidate_caches()
|
|
return import_path(modpath, root=tmp_path)
|
|
|
|
return importasmod
|
|
|
|
def test_repr_source(self):
|
|
pr = FormattedExcinfo()
|
|
source = _pytest._code.Source(
|
|
"""\
|
|
def f(x):
|
|
pass
|
|
"""
|
|
).strip()
|
|
pr.flow_marker = "|" # type: ignore[misc]
|
|
lines = pr.get_source(source, 0)
|
|
assert len(lines) == 2
|
|
assert lines[0] == "| def f(x):"
|
|
assert lines[1] == " pass"
|
|
|
|
def test_repr_source_out_of_bounds(self):
|
|
pr = FormattedExcinfo()
|
|
source = _pytest._code.Source(
|
|
"""\
|
|
def f(x):
|
|
pass
|
|
"""
|
|
).strip()
|
|
pr.flow_marker = "|" # type: ignore[misc]
|
|
|
|
lines = pr.get_source(source, 100)
|
|
assert len(lines) == 1
|
|
assert lines[0] == "| ???"
|
|
|
|
lines = pr.get_source(source, -100)
|
|
assert len(lines) == 1
|
|
assert lines[0] == "| ???"
|
|
|
|
def test_repr_source_excinfo(self) -> None:
|
|
"""Check if indentation is right."""
|
|
try:
|
|
|
|
def f():
|
|
1 / 0
|
|
|
|
f()
|
|
|
|
except BaseException:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
else:
|
|
assert False, "did not raise"
|
|
|
|
pr = FormattedExcinfo()
|
|
source = pr._getentrysource(excinfo.traceback[-1])
|
|
assert source is not None
|
|
lines = pr.get_source(source, 1, excinfo)
|
|
for line in lines:
|
|
print(line)
|
|
assert lines == [
|
|
" def f():",
|
|
"> 1 / 0",
|
|
"E ZeroDivisionError: division by zero",
|
|
]
|
|
|
|
def test_repr_source_not_existing(self):
|
|
pr = FormattedExcinfo()
|
|
co = compile("raise ValueError()", "", "exec")
|
|
try:
|
|
exec(co)
|
|
except ValueError:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
repr = pr.repr_excinfo(excinfo)
|
|
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
|
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
|
|
|
|
def test_repr_many_line_source_not_existing(self):
|
|
pr = FormattedExcinfo()
|
|
co = compile(
|
|
"""
|
|
a = 1
|
|
raise ValueError()
|
|
""",
|
|
"",
|
|
"exec",
|
|
)
|
|
try:
|
|
exec(co)
|
|
except ValueError:
|
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
|
repr = pr.repr_excinfo(excinfo)
|
|
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
|
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
|
|
|
|
def test_repr_source_failing_fullsource(self, monkeypatch) -> None:
|
|
pr = FormattedExcinfo()
|
|
|
|
try:
|
|
1 / 0
|
|
except ZeroDivisionError:
|
|
excinfo = ExceptionInfo.from_current()
|
|
|
|
with monkeypatch.context() as m:
|
|
m.setattr(_pytest._code.Code, "fullsource", property(lambda self: None))
|
|
repr = pr.repr_excinfo(excinfo)
|
|
|
|
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
|
|
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
|
|
|
|
def test_repr_local(self) -> None:
|
|
p = FormattedExcinfo(showlocals=True)
|
|
loc = {"y": 5, "z": 7, "x": 3, "@x": 2, "__builtins__": {}}
|
|
reprlocals = p.repr_locals(loc)
|
|
assert reprlocals is not None
|
|
assert reprlocals.lines
|
|
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
|
|
assert reprlocals.lines[1] == "x = 3"
|
|
assert reprlocals.lines[2] == "y = 5"
|
|
assert reprlocals.lines[3] == "z = 7"
|
|
|
|
def test_repr_local_with_error(self) -> None:
|
|
class ObjWithErrorInRepr:
|
|
def __repr__(self):
|
|
raise NotImplementedError
|
|
|
|
p = FormattedExcinfo(showlocals=True, truncate_locals=False)
|
|
loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
|
|
reprlocals = p.repr_locals(loc)
|
|
assert reprlocals is not None
|
|
assert reprlocals.lines
|
|
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
|
|
assert "[NotImplementedError() raised in repr()]" in reprlocals.lines[1]
|
|
|
|
def test_repr_local_with_exception_in_class_property(self) -> None:
|
|
class ExceptionWithBrokenClass(Exception):
|
|
# Type ignored because it's bypassed intentionally.
|
|
@property # type: ignore
|
|
def __class__(self):
|
|
raise TypeError("boom!")
|
|
|
|
class ObjWithErrorInRepr:
|
|
def __repr__(self):
|
|
raise ExceptionWithBrokenClass()
|
|
|
|
p = FormattedExcinfo(showlocals=True, truncate_locals=False)
|
|
loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
|
|
reprlocals = p.repr_locals(loc)
|
|
assert reprlocals is not None
|
|
assert reprlocals.lines
|
|
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
|
|
assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1]
|
|
|
|
def test_repr_local_truncated(self) -> None:
|
|
loc = {"l": [i for i in range(10)]}
|
|
p = FormattedExcinfo(showlocals=True)
|
|
truncated_reprlocals = p.repr_locals(loc)
|
|
assert truncated_reprlocals is not None
|
|
assert truncated_reprlocals.lines
|
|
assert truncated_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, ...]"
|
|
|
|
q = FormattedExcinfo(showlocals=True, truncate_locals=False)
|
|
full_reprlocals = q.repr_locals(loc)
|
|
assert full_reprlocals is not None
|
|
assert full_reprlocals.lines
|
|
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
|
|
|
def test_repr_tracebackentry_lines(self, importasmod) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def func1():
|
|
raise ValueError("hello\\nworld")
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.func1)
|
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
|
p = FormattedExcinfo()
|
|
reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
|
|
|
|
# test as intermittent entry
|
|
lines = reprtb.lines
|
|
assert lines[0] == " def func1():"
|
|
assert lines[1] == '> raise ValueError("hello\\nworld")'
|
|
|
|
# test as last entry
|
|
p = FormattedExcinfo(showlocals=True)
|
|
repr_entry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo)
|
|
lines = repr_entry.lines
|
|
assert lines[0] == " def func1():"
|
|
assert lines[1] == '> raise ValueError("hello\\nworld")'
|
|
assert lines[2] == "E ValueError: hello"
|
|
assert lines[3] == "E world"
|
|
assert not lines[4:]
|
|
|
|
loc = repr_entry.reprfileloc
|
|
assert loc is not None
|
|
assert loc.path == mod.__file__
|
|
assert loc.lineno == 3
|
|
# assert loc.message == "ValueError: hello"
|
|
|
|
def test_repr_tracebackentry_lines2(self, importasmod, tw_mock) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def func1(m, x, y, z):
|
|
raise ValueError("hello\\nworld")
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
|
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
|
entry = excinfo.traceback[-1]
|
|
p = FormattedExcinfo(funcargs=True)
|
|
reprfuncargs = p.repr_args(entry)
|
|
assert reprfuncargs is not None
|
|
assert reprfuncargs.args[0] == ("m", repr("m" * 90))
|
|
assert reprfuncargs.args[1] == ("x", "5")
|
|
assert reprfuncargs.args[2] == ("y", "13")
|
|
assert reprfuncargs.args[3] == ("z", repr("z" * 120))
|
|
|
|
p = FormattedExcinfo(funcargs=True)
|
|
repr_entry = p.repr_traceback_entry(entry)
|
|
assert repr_entry.reprfuncargs is not None
|
|
assert repr_entry.reprfuncargs.args == reprfuncargs.args
|
|
repr_entry.toterminal(tw_mock)
|
|
assert tw_mock.lines[0] == "m = " + repr("m" * 90)
|
|
assert tw_mock.lines[1] == "x = 5, y = 13"
|
|
assert tw_mock.lines[2] == "z = " + repr("z" * 120)
|
|
|
|
def test_repr_tracebackentry_lines_var_kw_args(self, importasmod, tw_mock) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def func1(x, *y, **z):
|
|
raise ValueError("hello\\nworld")
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
|
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
|
entry = excinfo.traceback[-1]
|
|
p = FormattedExcinfo(funcargs=True)
|
|
reprfuncargs = p.repr_args(entry)
|
|
assert reprfuncargs is not None
|
|
assert reprfuncargs.args[0] == ("x", repr("a"))
|
|
assert reprfuncargs.args[1] == ("y", repr(("b",)))
|
|
assert reprfuncargs.args[2] == ("z", repr({"c": "d"}))
|
|
|
|
p = FormattedExcinfo(funcargs=True)
|
|
repr_entry = p.repr_traceback_entry(entry)
|
|
assert repr_entry.reprfuncargs
|
|
assert repr_entry.reprfuncargs.args == reprfuncargs.args
|
|
repr_entry.toterminal(tw_mock)
|
|
assert tw_mock.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}"
|
|
|
|
def test_repr_tracebackentry_short(self, importasmod) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def func1():
|
|
raise ValueError("hello")
|
|
def entry():
|
|
func1()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
p = FormattedExcinfo(style="short")
|
|
reprtb = p.repr_traceback_entry(excinfo.traceback[-2])
|
|
lines = reprtb.lines
|
|
basename = Path(mod.__file__).name
|
|
assert lines[0] == " func1()"
|
|
assert reprtb.reprfileloc is not None
|
|
assert basename in str(reprtb.reprfileloc.path)
|
|
assert reprtb.reprfileloc.lineno == 5
|
|
|
|
# test last entry
|
|
p = FormattedExcinfo(style="short")
|
|
reprtb = p.repr_traceback_entry(excinfo.traceback[-1], excinfo)
|
|
lines = reprtb.lines
|
|
assert lines[0] == ' raise ValueError("hello")'
|
|
assert lines[1] == "E ValueError: hello"
|
|
assert reprtb.reprfileloc is not None
|
|
assert basename in str(reprtb.reprfileloc.path)
|
|
assert reprtb.reprfileloc.lineno == 3
|
|
|
|
def test_repr_tracebackentry_no(self, importasmod):
|
|
mod = importasmod(
|
|
"""
|
|
def func1():
|
|
raise ValueError("hello")
|
|
def entry():
|
|
func1()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
p = FormattedExcinfo(style="no")
|
|
p.repr_traceback_entry(excinfo.traceback[-2])
|
|
|
|
p = FormattedExcinfo(style="no")
|
|
reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo)
|
|
lines = reprentry.lines
|
|
assert lines[0] == "E ValueError: hello"
|
|
assert not lines[1:]
|
|
|
|
def test_repr_traceback_tbfilter(self, importasmod):
|
|
mod = importasmod(
|
|
"""
|
|
def f(x):
|
|
raise ValueError(x)
|
|
def entry():
|
|
f(0)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
p = FormattedExcinfo(tbfilter=True)
|
|
reprtb = p.repr_traceback(excinfo)
|
|
assert len(reprtb.reprentries) == 2
|
|
p = FormattedExcinfo(tbfilter=False)
|
|
reprtb = p.repr_traceback(excinfo)
|
|
assert len(reprtb.reprentries) == 3
|
|
|
|
def test_traceback_short_no_source(
|
|
self,
|
|
importasmod,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def func1():
|
|
raise ValueError("hello")
|
|
def entry():
|
|
func1()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
from _pytest._code.code import Code
|
|
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(Code, "path", "bogus")
|
|
p = FormattedExcinfo(style="short")
|
|
reprtb = p.repr_traceback_entry(excinfo.traceback[-2])
|
|
lines = reprtb.lines
|
|
last_p = FormattedExcinfo(style="short")
|
|
last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo)
|
|
last_lines = last_reprtb.lines
|
|
assert lines[0] == " func1()"
|
|
|
|
assert last_lines[0] == ' raise ValueError("hello")'
|
|
assert last_lines[1] == "E ValueError: hello"
|
|
|
|
def test_repr_traceback_and_excinfo(self, importasmod) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def f(x):
|
|
raise ValueError(x)
|
|
def entry():
|
|
f(0)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
|
|
styles: tuple[_TracebackStyle, ...] = ("long", "short")
|
|
for style in styles:
|
|
p = FormattedExcinfo(style=style)
|
|
reprtb = p.repr_traceback(excinfo)
|
|
assert len(reprtb.reprentries) == 2
|
|
assert reprtb.style == style
|
|
assert not reprtb.extraline
|
|
repr = p.repr_excinfo(excinfo)
|
|
assert repr.reprtraceback
|
|
assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries)
|
|
|
|
assert repr.chain[0][0]
|
|
assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries)
|
|
assert repr.reprcrash is not None
|
|
assert repr.reprcrash.path.endswith("mod.py")
|
|
assert repr.reprcrash.message == "ValueError: 0"
|
|
|
|
def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def f(x):
|
|
raise ValueError(x)
|
|
def entry():
|
|
f(0)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
|
|
p = FormattedExcinfo(abspath=False)
|
|
|
|
raised = 0
|
|
|
|
orig_path_cwd = Path.cwd
|
|
|
|
def raiseos():
|
|
nonlocal raised
|
|
upframe = sys._getframe().f_back
|
|
assert upframe is not None
|
|
if upframe.f_code.co_name == "_makepath":
|
|
# Only raise with expected calls, but not via e.g. inspect for
|
|
# py38-windows.
|
|
raised += 1
|
|
raise OSError(2, "custom_oserror")
|
|
return orig_path_cwd()
|
|
|
|
monkeypatch.setattr(Path, "cwd", raiseos)
|
|
assert p._makepath(Path(__file__)) == __file__
|
|
assert raised == 1
|
|
repr_tb = p.repr_traceback(excinfo)
|
|
|
|
matcher = LineMatcher(str(repr_tb).splitlines())
|
|
matcher.fnmatch_lines(
|
|
[
|
|
"def entry():",
|
|
"> f(0)",
|
|
"",
|
|
f"{mod.__file__}:5: ",
|
|
"_ _ *",
|
|
"",
|
|
" def f(x):",
|
|
"> raise ValueError(x)",
|
|
"E ValueError: 0",
|
|
"",
|
|
f"{mod.__file__}:3: ValueError",
|
|
]
|
|
)
|
|
assert raised == 3
|
|
|
|
def test_repr_excinfo_addouterr(self, importasmod, tw_mock):
|
|
mod = importasmod(
|
|
"""
|
|
def entry():
|
|
raise ValueError()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
repr = excinfo.getrepr()
|
|
repr.addsection("title", "content")
|
|
repr.toterminal(tw_mock)
|
|
assert tw_mock.lines[-1] == "content"
|
|
assert tw_mock.lines[-2] == ("-", "title")
|
|
|
|
def test_repr_excinfo_reprcrash(self, importasmod) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def entry():
|
|
raise ValueError()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
repr = excinfo.getrepr()
|
|
assert repr.reprcrash is not None
|
|
assert repr.reprcrash.path.endswith("mod.py")
|
|
assert repr.reprcrash.lineno == 3
|
|
assert repr.reprcrash.message == "ValueError"
|
|
assert str(repr.reprcrash).endswith("mod.py:3: ValueError")
|
|
|
|
def test_repr_traceback_recursion(self, importasmod):
|
|
mod = importasmod(
|
|
"""
|
|
def rec2(x):
|
|
return rec1(x+1)
|
|
def rec1(x):
|
|
return rec2(x-1)
|
|
def entry():
|
|
rec1(42)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(RuntimeError, mod.entry)
|
|
|
|
for style in ("short", "long", "no"):
|
|
p = FormattedExcinfo(style="short")
|
|
reprtb = p.repr_traceback(excinfo)
|
|
assert reprtb.extraline == "!!! Recursion detected (same locals & position)"
|
|
assert str(reprtb)
|
|
|
|
def test_reprexcinfo_getrepr(self, importasmod) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def f(x):
|
|
raise ValueError(x)
|
|
def entry():
|
|
f(0)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.entry)
|
|
|
|
styles: tuple[_TracebackStyle, ...] = ("short", "long", "no")
|
|
for style in styles:
|
|
for showlocals in (True, False):
|
|
repr = excinfo.getrepr(style=style, showlocals=showlocals)
|
|
assert repr.reprtraceback.style == style
|
|
|
|
assert isinstance(repr, ExceptionChainRepr)
|
|
for r in repr.chain:
|
|
assert r[0].style == style
|
|
|
|
def test_reprexcinfo_unicode(self):
|
|
from _pytest._code.code import TerminalRepr
|
|
|
|
class MyRepr(TerminalRepr):
|
|
def toterminal(self, tw: TerminalWriter) -> None:
|
|
tw.line("я")
|
|
|
|
x = str(MyRepr())
|
|
assert x == "я"
|
|
|
|
def test_toterminal_long(self, importasmod, tw_mock):
|
|
mod = importasmod(
|
|
"""
|
|
def g(x):
|
|
raise ValueError(x)
|
|
def f():
|
|
g(3)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.f)
|
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
|
repr = excinfo.getrepr()
|
|
repr.toterminal(tw_mock)
|
|
assert tw_mock.lines[0] == ""
|
|
tw_mock.lines.pop(0)
|
|
assert tw_mock.lines[0] == " def f():"
|
|
assert tw_mock.lines[1] == "> g(3)"
|
|
assert tw_mock.lines[2] == ""
|
|
line = tw_mock.get_write_msg(3)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[4] == (":5: ")
|
|
assert tw_mock.lines[5] == ("_ ", None)
|
|
assert tw_mock.lines[6] == ""
|
|
assert tw_mock.lines[7] == " def g(x):"
|
|
assert tw_mock.lines[8] == "> raise ValueError(x)"
|
|
assert tw_mock.lines[9] == "E ValueError: 3"
|
|
assert tw_mock.lines[10] == ""
|
|
line = tw_mock.get_write_msg(11)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[12] == ":3: ValueError"
|
|
|
|
def test_toterminal_long_missing_source(
|
|
self, importasmod, tmp_path: Path, tw_mock
|
|
) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def g(x):
|
|
raise ValueError(x)
|
|
def f():
|
|
g(3)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.f)
|
|
tmp_path.joinpath("mod.py").unlink()
|
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
|
repr = excinfo.getrepr()
|
|
repr.toterminal(tw_mock)
|
|
assert tw_mock.lines[0] == ""
|
|
tw_mock.lines.pop(0)
|
|
assert tw_mock.lines[0] == "> ???"
|
|
assert tw_mock.lines[1] == ""
|
|
line = tw_mock.get_write_msg(2)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[3] == ":5: "
|
|
assert tw_mock.lines[4] == ("_ ", None)
|
|
assert tw_mock.lines[5] == ""
|
|
assert tw_mock.lines[6] == "> ???"
|
|
assert tw_mock.lines[7] == "E ValueError: 3"
|
|
assert tw_mock.lines[8] == ""
|
|
line = tw_mock.get_write_msg(9)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[10] == ":3: ValueError"
|
|
|
|
def test_toterminal_long_incomplete_source(
|
|
self, importasmod, tmp_path: Path, tw_mock
|
|
) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def g(x):
|
|
raise ValueError(x)
|
|
def f():
|
|
g(3)
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.f)
|
|
tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8")
|
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
|
repr = excinfo.getrepr()
|
|
repr.toterminal(tw_mock)
|
|
assert tw_mock.lines[0] == ""
|
|
tw_mock.lines.pop(0)
|
|
assert tw_mock.lines[0] == "> ???"
|
|
assert tw_mock.lines[1] == ""
|
|
line = tw_mock.get_write_msg(2)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[3] == ":5: "
|
|
assert tw_mock.lines[4] == ("_ ", None)
|
|
assert tw_mock.lines[5] == ""
|
|
assert tw_mock.lines[6] == "> ???"
|
|
assert tw_mock.lines[7] == "E ValueError: 3"
|
|
assert tw_mock.lines[8] == ""
|
|
line = tw_mock.get_write_msg(9)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[10] == ":3: ValueError"
|
|
|
|
def test_toterminal_long_filenames(
|
|
self, importasmod, tw_mock, monkeypatch: MonkeyPatch
|
|
) -> None:
|
|
mod = importasmod(
|
|
"""
|
|
def f():
|
|
raise ValueError()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.f)
|
|
path = Path(mod.__file__)
|
|
monkeypatch.chdir(path.parent)
|
|
repr = excinfo.getrepr(abspath=False)
|
|
repr.toterminal(tw_mock)
|
|
x = bestrelpath(Path.cwd(), path)
|
|
if len(x) < len(str(path)):
|
|
msg = tw_mock.get_write_msg(-2)
|
|
assert msg == "mod.py"
|
|
assert tw_mock.lines[-1] == ":3: ValueError"
|
|
|
|
repr = excinfo.getrepr(abspath=True)
|
|
repr.toterminal(tw_mock)
|
|
msg = tw_mock.get_write_msg(-2)
|
|
assert msg == str(path)
|
|
line = tw_mock.lines[-1]
|
|
assert line == ":3: ValueError"
|
|
|
|
@pytest.mark.parametrize(
|
|
"reproptions",
|
|
[
|
|
pytest.param(
|
|
{
|
|
"style": style,
|
|
"showlocals": showlocals,
|
|
"funcargs": funcargs,
|
|
"tbfilter": tbfilter,
|
|
},
|
|
id="style={},showlocals={},funcargs={},tbfilter={}".format(
|
|
style, showlocals, funcargs, tbfilter
|
|
),
|
|
)
|
|
for style in ["long", "short", "line", "no", "native", "value", "auto"]
|
|
for showlocals in (True, False)
|
|
for tbfilter in (True, False)
|
|
for funcargs in (True, False)
|
|
],
|
|
)
|
|
def test_format_excinfo(self, reproptions: dict[str, Any]) -> None:
|
|
def bar():
|
|
assert False, "some error"
|
|
|
|
def foo():
|
|
bar()
|
|
|
|
# using inline functions as opposed to importasmod so we get source code lines
|
|
# in the tracebacks (otherwise getinspect doesn't find the source code).
|
|
with pytest.raises(AssertionError) as excinfo:
|
|
foo()
|
|
file = io.StringIO()
|
|
tw = TerminalWriter(file=file)
|
|
repr = excinfo.getrepr(**reproptions)
|
|
repr.toterminal(tw)
|
|
assert file.getvalue()
|
|
|
|
def test_traceback_repr_style(self, importasmod, tw_mock):
|
|
mod = importasmod(
|
|
"""
|
|
def f():
|
|
g()
|
|
def g():
|
|
h()
|
|
def h():
|
|
i()
|
|
def i():
|
|
raise ValueError()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ValueError, mod.f)
|
|
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
|
excinfo.traceback = _pytest._code.Traceback(
|
|
entry if i not in (1, 2) else entry.with_repr_style("short")
|
|
for i, entry in enumerate(excinfo.traceback)
|
|
)
|
|
r = excinfo.getrepr(style="long")
|
|
r.toterminal(tw_mock)
|
|
for line in tw_mock.lines:
|
|
print(line)
|
|
assert tw_mock.lines[0] == ""
|
|
assert tw_mock.lines[1] == " def f():"
|
|
assert tw_mock.lines[2] == "> g()"
|
|
assert tw_mock.lines[3] == ""
|
|
msg = tw_mock.get_write_msg(4)
|
|
assert msg.endswith("mod.py")
|
|
assert tw_mock.lines[5] == ":3: "
|
|
assert tw_mock.lines[6] == ("_ ", None)
|
|
tw_mock.get_write_msg(7)
|
|
assert tw_mock.lines[8].endswith("in g")
|
|
assert tw_mock.lines[9] == " h()"
|
|
tw_mock.get_write_msg(10)
|
|
assert tw_mock.lines[11].endswith("in h")
|
|
assert tw_mock.lines[12] == " i()"
|
|
assert tw_mock.lines[13] == ("_ ", None)
|
|
assert tw_mock.lines[14] == ""
|
|
assert tw_mock.lines[15] == " def i():"
|
|
assert tw_mock.lines[16] == "> raise ValueError()"
|
|
assert tw_mock.lines[17] == "E ValueError"
|
|
assert tw_mock.lines[18] == ""
|
|
msg = tw_mock.get_write_msg(19)
|
|
msg.endswith("mod.py")
|
|
assert tw_mock.lines[20] == ":9: ValueError"
|
|
|
|
def test_exc_chain_repr(self, importasmod, tw_mock):
|
|
mod = importasmod(
|
|
"""
|
|
class Err(Exception):
|
|
pass
|
|
def f():
|
|
try:
|
|
g()
|
|
except Exception as e:
|
|
raise Err() from e
|
|
finally:
|
|
h()
|
|
def g():
|
|
raise ValueError()
|
|
|
|
def h():
|
|
raise AttributeError()
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(AttributeError, mod.f)
|
|
r = excinfo.getrepr(style="long")
|
|
r.toterminal(tw_mock)
|
|
for line in tw_mock.lines:
|
|
print(line)
|
|
assert tw_mock.lines[0] == ""
|
|
assert tw_mock.lines[1] == " def f():"
|
|
assert tw_mock.lines[2] == " try:"
|
|
assert tw_mock.lines[3] == "> g()"
|
|
assert tw_mock.lines[4] == ""
|
|
line = tw_mock.get_write_msg(5)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[6] == ":6: "
|
|
assert tw_mock.lines[7] == ("_ ", None)
|
|
assert tw_mock.lines[8] == ""
|
|
assert tw_mock.lines[9] == " def g():"
|
|
assert tw_mock.lines[10] == "> raise ValueError()"
|
|
assert tw_mock.lines[11] == "E ValueError"
|
|
assert tw_mock.lines[12] == ""
|
|
line = tw_mock.get_write_msg(13)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[14] == ":12: ValueError"
|
|
assert tw_mock.lines[15] == ""
|
|
assert (
|
|
tw_mock.lines[16]
|
|
== "The above exception was the direct cause of the following exception:"
|
|
)
|
|
assert tw_mock.lines[17] == ""
|
|
assert tw_mock.lines[18] == " def f():"
|
|
assert tw_mock.lines[19] == " try:"
|
|
assert tw_mock.lines[20] == " g()"
|
|
assert tw_mock.lines[21] == " except Exception as e:"
|
|
assert tw_mock.lines[22] == "> raise Err() from e"
|
|
assert tw_mock.lines[23] == "E test_exc_chain_repr0.mod.Err"
|
|
assert tw_mock.lines[24] == ""
|
|
line = tw_mock.get_write_msg(25)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[26] == ":8: Err"
|
|
assert tw_mock.lines[27] == ""
|
|
assert (
|
|
tw_mock.lines[28]
|
|
== "During handling of the above exception, another exception occurred:"
|
|
)
|
|
assert tw_mock.lines[29] == ""
|
|
assert tw_mock.lines[30] == " def f():"
|
|
assert tw_mock.lines[31] == " try:"
|
|
assert tw_mock.lines[32] == " g()"
|
|
assert tw_mock.lines[33] == " except Exception as e:"
|
|
assert tw_mock.lines[34] == " raise Err() from e"
|
|
assert tw_mock.lines[35] == " finally:"
|
|
assert tw_mock.lines[36] == "> h()"
|
|
assert tw_mock.lines[37] == ""
|
|
line = tw_mock.get_write_msg(38)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[39] == ":10: "
|
|
assert tw_mock.lines[40] == ("_ ", None)
|
|
assert tw_mock.lines[41] == ""
|
|
assert tw_mock.lines[42] == " def h():"
|
|
assert tw_mock.lines[43] == "> raise AttributeError()"
|
|
assert tw_mock.lines[44] == "E AttributeError"
|
|
assert tw_mock.lines[45] == ""
|
|
line = tw_mock.get_write_msg(46)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[47] == ":15: AttributeError"
|
|
|
|
@pytest.mark.parametrize("mode", ["from_none", "explicit_suppress"])
|
|
def test_exc_repr_chain_suppression(self, importasmod, mode, tw_mock):
|
|
"""Check that exc repr does not show chained exceptions in Python 3.
|
|
- When the exception is raised with "from None"
|
|
- Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr().
|
|
"""
|
|
raise_suffix = " from None" if mode == "from_none" else ""
|
|
mod = importasmod(
|
|
"""
|
|
def f():
|
|
try:
|
|
g()
|
|
except Exception:
|
|
raise AttributeError(){raise_suffix}
|
|
def g():
|
|
raise ValueError()
|
|
""".format(
|
|
raise_suffix=raise_suffix
|
|
)
|
|
)
|
|
excinfo = pytest.raises(AttributeError, mod.f)
|
|
r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress")
|
|
r.toterminal(tw_mock)
|
|
for line in tw_mock.lines:
|
|
print(line)
|
|
assert tw_mock.lines[0] == ""
|
|
assert tw_mock.lines[1] == " def f():"
|
|
assert tw_mock.lines[2] == " try:"
|
|
assert tw_mock.lines[3] == " g()"
|
|
assert tw_mock.lines[4] == " except Exception:"
|
|
assert tw_mock.lines[5] == "> raise AttributeError(){}".format(
|
|
raise_suffix
|
|
)
|
|
assert tw_mock.lines[6] == "E AttributeError"
|
|
assert tw_mock.lines[7] == ""
|
|
line = tw_mock.get_write_msg(8)
|
|
assert line.endswith("mod.py")
|
|
assert tw_mock.lines[9] == ":6: AttributeError"
|
|
assert len(tw_mock.lines) == 10
|
|
|
|
@pytest.mark.parametrize(
|
|
"reason, description",
|
|
[
|
|
pytest.param(
|
|
"cause",
|
|
"The above exception was the direct cause of the following exception:",
|
|
id="cause",
|
|
),
|
|
pytest.param(
|
|
"context",
|
|
"During handling of the above exception, another exception occurred:",
|
|
id="context",
|
|
),
|
|
],
|
|
)
|
|
def test_exc_chain_repr_without_traceback(self, importasmod, reason, description):
|
|
"""
|
|
Handle representation of exception chains where one of the exceptions doesn't have a
|
|
real traceback, such as those raised in a subprocess submitted by the multiprocessing
|
|
module (#1984).
|
|
"""
|
|
exc_handling_code = " from e" if reason == "cause" else ""
|
|
mod = importasmod(
|
|
"""
|
|
def f():
|
|
try:
|
|
g()
|
|
except Exception as e:
|
|
raise RuntimeError('runtime problem'){exc_handling_code}
|
|
def g():
|
|
raise ValueError('invalid value')
|
|
""".format(
|
|
exc_handling_code=exc_handling_code
|
|
)
|
|
)
|
|
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
mod.f()
|
|
|
|
# emulate the issue described in #1984
|
|
attr = "__%s__" % reason
|
|
getattr(excinfo.value, attr).__traceback__ = None
|
|
|
|
r = excinfo.getrepr()
|
|
file = io.StringIO()
|
|
tw = TerminalWriter(file=file)
|
|
tw.hasmarkup = False
|
|
r.toterminal(tw)
|
|
|
|
matcher = LineMatcher(file.getvalue().splitlines())
|
|
matcher.fnmatch_lines(
|
|
[
|
|
"ValueError: invalid value",
|
|
description,
|
|
"* except Exception as e:",
|
|
"> * raise RuntimeError('runtime problem')" + exc_handling_code,
|
|
"E *RuntimeError: runtime problem",
|
|
]
|
|
)
|
|
|
|
def test_exc_chain_repr_cycle(self, importasmod, tw_mock):
|
|
mod = importasmod(
|
|
"""
|
|
class Err(Exception):
|
|
pass
|
|
def fail():
|
|
return 0 / 0
|
|
def reraise():
|
|
try:
|
|
fail()
|
|
except ZeroDivisionError as e:
|
|
raise Err() from e
|
|
def unreraise():
|
|
try:
|
|
reraise()
|
|
except Err as e:
|
|
raise e.__cause__
|
|
"""
|
|
)
|
|
excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
|
|
r = excinfo.getrepr(style="short")
|
|
r.toterminal(tw_mock)
|
|
out = "\n".join(line for line in tw_mock.lines if isinstance(line, str))
|
|
expected_out = textwrap.dedent(
|
|
"""\
|
|
:13: in unreraise
|
|
reraise()
|
|
:10: in reraise
|
|
raise Err() from e
|
|
E test_exc_chain_repr_cycle0.mod.Err
|
|
|
|
During handling of the above exception, another exception occurred:
|
|
:15: in unreraise
|
|
raise e.__cause__
|
|
:8: in reraise
|
|
fail()
|
|
:5: in fail
|
|
return 0 / 0
|
|
E ZeroDivisionError: division by zero"""
|
|
)
|
|
assert out == expected_out
|
|
|
|
def test_exec_type_error_filter(self, importasmod):
|
|
"""See #7742"""
|
|
mod = importasmod(
|
|
"""\
|
|
def f():
|
|
exec("a = 1", {}, [])
|
|
"""
|
|
)
|
|
with pytest.raises(TypeError) as excinfo:
|
|
mod.f()
|
|
# previously crashed with `AttributeError: list has no attribute get`
|
|
excinfo.traceback.filter(excinfo)
|
|
|
|
|
|
@pytest.mark.parametrize("style", ["short", "long"])
|
|
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
|
|
def test_repr_traceback_with_unicode(style, encoding):
|
|
if encoding is None:
|
|
msg: str | bytes = "☹"
|
|
else:
|
|
msg = "☹".encode(encoding)
|
|
try:
|
|
raise RuntimeError(msg)
|
|
except RuntimeError:
|
|
e_info = ExceptionInfo.from_current()
|
|
formatter = FormattedExcinfo(style=style)
|
|
repr_traceback = formatter.repr_traceback(e_info)
|
|
assert repr_traceback is not None
|
|
|
|
|
|
def test_cwd_deleted(pytester: Pytester) -> None:
|
|
pytester.makepyfile(
|
|
"""
|
|
import os
|
|
|
|
def test(tmp_path):
|
|
os.chdir(tmp_path)
|
|
tmp_path.unlink()
|
|
assert False
|
|
"""
|
|
)
|
|
result = pytester.runpytest()
|
|
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
|
result.stderr.no_fnmatch_line("*INTERNALERROR*")
|
|
|
|
|
|
def test_regression_nagative_line_index(pytester: Pytester) -> None:
|
|
"""
|
|
With Python 3.10 alphas, there was an INTERNALERROR reported in
|
|
https://github.com/pytest-dev/pytest/pull/8227
|
|
This test ensures it does not regress.
|
|
"""
|
|
pytester.makepyfile(
|
|
"""
|
|
import ast
|
|
import pytest
|
|
|
|
|
|
def test_literal_eval():
|
|
with pytest.raises(ValueError, match="^$"):
|
|
ast.literal_eval("pytest")
|
|
"""
|
|
)
|
|
result = pytester.runpytest()
|
|
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
|
result.stderr.no_fnmatch_line("*INTERNALERROR*")
|
|
|
|
|
|
@pytest.mark.usefixtures("limited_recursion_depth")
|
|
def test_exception_repr_extraction_error_on_recursion():
|
|
"""
|
|
Ensure we can properly detect a recursion error even
|
|
if some locals raise error on comparison (#2459).
|
|
"""
|
|
|
|
class numpy_like:
|
|
def __eq__(self, other):
|
|
if type(other) is numpy_like:
|
|
raise ValueError(
|
|
"The truth value of an array "
|
|
"with more than one element is ambiguous."
|
|
)
|
|
|
|
def a(x):
|
|
return b(numpy_like())
|
|
|
|
def b(x):
|
|
return a(numpy_like())
|
|
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
a(numpy_like())
|
|
|
|
matcher = LineMatcher(str(excinfo.getrepr()).splitlines())
|
|
matcher.fnmatch_lines(
|
|
[
|
|
"!!! Recursion error detected, but an error occurred locating the origin of recursion.",
|
|
"*The following exception happened*",
|
|
"*ValueError: The truth value of an array*",
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("limited_recursion_depth")
|
|
def test_no_recursion_index_on_recursion_error():
|
|
"""
|
|
Ensure that we don't break in case we can't find the recursion index
|
|
during a recursion error (#2486).
|
|
"""
|
|
|
|
class RecursionDepthError:
|
|
def __getattr__(self, attr):
|
|
return getattr(self, "_" + attr)
|
|
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
RecursionDepthError().trigger
|
|
assert "maximum recursion" in str(excinfo.getrepr())
|
|
|
|
|
|
def _exceptiongroup_common(
|
|
pytester: Pytester,
|
|
outer_chain: str,
|
|
inner_chain: str,
|
|
native: bool,
|
|
) -> None:
|
|
pre_raise = "exceptiongroup." if not native else ""
|
|
pre_catch = pre_raise if sys.version_info < (3, 11) else ""
|
|
filestr = f"""
|
|
{"import exceptiongroup" if not native else ""}
|
|
import pytest
|
|
|
|
def f(): raise ValueError("From f()")
|
|
def g(): raise BaseException("From g()")
|
|
|
|
def inner(inner_chain):
|
|
excs = []
|
|
for callback in [f, g]:
|
|
try:
|
|
callback()
|
|
except BaseException as err:
|
|
excs.append(err)
|
|
if excs:
|
|
if inner_chain == "none":
|
|
raise {pre_raise}BaseExceptionGroup("Oops", excs)
|
|
try:
|
|
raise SyntaxError()
|
|
except SyntaxError as e:
|
|
if inner_chain == "from":
|
|
raise {pre_raise}BaseExceptionGroup("Oops", excs) from e
|
|
else:
|
|
raise {pre_raise}BaseExceptionGroup("Oops", excs)
|
|
|
|
def outer(outer_chain, inner_chain):
|
|
try:
|
|
inner(inner_chain)
|
|
except {pre_catch}BaseExceptionGroup as e:
|
|
if outer_chain == "none":
|
|
raise
|
|
if outer_chain == "from":
|
|
raise IndexError() from e
|
|
else:
|
|
raise IndexError()
|
|
|
|
|
|
def test():
|
|
outer("{outer_chain}", "{inner_chain}")
|
|
"""
|
|
pytester.makepyfile(test_excgroup=filestr)
|
|
result = pytester.runpytest()
|
|
match_lines = []
|
|
if inner_chain in ("another", "from"):
|
|
match_lines.append(r"SyntaxError: <no detail available>")
|
|
|
|
match_lines += [
|
|
r" + Exception Group Traceback (most recent call last):",
|
|
rf" \| {pre_catch}BaseExceptionGroup: Oops \(2 sub-exceptions\)",
|
|
r" \| ValueError: From f\(\)",
|
|
r" \| BaseException: From g\(\)",
|
|
r"=* short test summary info =*",
|
|
]
|
|
if outer_chain in ("another", "from"):
|
|
match_lines.append(r"FAILED test_excgroup.py::test - IndexError")
|
|
else:
|
|
match_lines.append(
|
|
rf"FAILED test_excgroup.py::test - {pre_catch}BaseExceptionGroup: Oops \(2.*"
|
|
)
|
|
result.stdout.re_match_lines(match_lines)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.version_info < (3, 11), reason="Native ExceptionGroup not implemented"
|
|
)
|
|
@pytest.mark.parametrize("outer_chain", ["none", "from", "another"])
|
|
@pytest.mark.parametrize("inner_chain", ["none", "from", "another"])
|
|
def test_native_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
|
|
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=True)
|
|
|
|
|
|
@pytest.mark.parametrize("outer_chain", ["none", "from", "another"])
|
|
@pytest.mark.parametrize("inner_chain", ["none", "from", "another"])
|
|
def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
|
|
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
|
pytest.importorskip("exceptiongroup")
|
|
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
|
|
|
|
|
@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
|
|
def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
|
|
"""Regression test for #10903."""
|
|
pytester.makepyfile(
|
|
"""
|
|
def test():
|
|
__tracebackhide__ = True
|
|
1 / 0
|
|
"""
|
|
)
|
|
result = pytester.runpytest("--tb", tbstyle)
|
|
assert result.ret == 1
|
|
if tbstyle != "line":
|
|
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
|
|
if tbstyle not in ("line", "native"):
|
|
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
|
|
|
|
|
|
def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None:
|
|
"""Hidden entries of chained exceptions are not shown (#1904)."""
|
|
p = pytester.makepyfile(
|
|
"""
|
|
def g1():
|
|
__tracebackhide__ = True
|
|
str.does_not_exist
|
|
|
|
def f3():
|
|
__tracebackhide__ = True
|
|
1 / 0
|
|
|
|
def f2():
|
|
try:
|
|
f3()
|
|
except Exception:
|
|
g1()
|
|
|
|
def f1():
|
|
__tracebackhide__ = True
|
|
f2()
|
|
|
|
def test():
|
|
f1()
|
|
"""
|
|
)
|
|
result = pytester.runpytest(str(p), "--tb=short")
|
|
assert result.ret == 1
|
|
result.stdout.fnmatch_lines(
|
|
[
|
|
"*.py:11: in f2",
|
|
" f3()",
|
|
"E ZeroDivisionError: division by zero",
|
|
"",
|
|
"During handling of the above exception, another exception occurred:",
|
|
"*.py:20: in test",
|
|
" f1()",
|
|
"*.py:13: in f2",
|
|
" g1()",
|
|
"E AttributeError:*'does_not_exist'",
|
|
],
|
|
consecutive=True,
|
|
)
|
|
|
|
|
|
def add_note(err: BaseException, msg: str) -> None:
|
|
"""Adds a note to an exception inplace."""
|
|
if sys.version_info < (3, 11):
|
|
err.__notes__ = getattr(err, "__notes__", []) + [msg] # type: ignore[attr-defined]
|
|
else:
|
|
err.add_note(msg)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"error,notes,match",
|
|
[
|
|
(Exception("test"), [], "test"),
|
|
(AssertionError("foo"), ["bar"], "bar"),
|
|
(AssertionError("foo"), ["bar", "baz"], "bar"),
|
|
(AssertionError("foo"), ["bar", "baz"], "baz"),
|
|
(ValueError("foo"), ["bar", "baz"], re.compile(r"bar\nbaz", re.MULTILINE)),
|
|
(ValueError("foo"), ["bar", "baz"], re.compile(r"BAZ", re.IGNORECASE)),
|
|
],
|
|
)
|
|
def test_check_error_notes_success(
|
|
error: Exception, notes: list[str], match: str
|
|
) -> None:
|
|
for note in notes:
|
|
add_note(error, note)
|
|
|
|
with pytest.raises(Exception, match=match):
|
|
raise error
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"error, notes, match",
|
|
[
|
|
(Exception("test"), [], "foo"),
|
|
(AssertionError("foo"), ["bar"], "baz"),
|
|
(AssertionError("foo"), ["bar"], "foo\nbaz"),
|
|
],
|
|
)
|
|
def test_check_error_notes_failure(
|
|
error: Exception, notes: list[str], match: str
|
|
) -> None:
|
|
for note in notes:
|
|
add_note(error, note)
|
|
|
|
with pytest.raises(AssertionError):
|
|
with pytest.raises(type(error), match=match):
|
|
raise error
|