diff --git a/src/sybil/integration/pytest.py b/src/sybil/integration/pytest.py index 648802d..6651c47 100644 --- a/src/sybil/integration/pytest.py +++ b/src/sybil/integration/pytest.py @@ -80,9 +80,12 @@ def runtest(self) -> None: def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: traceback = excinfo.traceback tb = traceback.cut(path=example_module_path) - tb_entry = tb[1] - if getattr(tb_entry, '_rawentry', None) is not None: - traceback = Traceback(tb_entry._rawentry) + # ``tb`` has a single entry when the example frame is the deepest in + # the traceback, leaving no frame below it to re-root at. + if len(tb) > 1: + tb_entry = tb[1] + if getattr(tb_entry, '_rawentry', None) is not None: + traceback = Traceback(tb_entry._rawentry) return traceback def repr_failure( diff --git a/tests/test_functional.py b/tests/test_functional.py index 94a7fbe..4f4dc25 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1,11 +1,15 @@ import sys +from inspect import getsourcefile +from os.path import abspath from pathlib import Path import pytest -from pytest import CaptureFixture +from pytest import CaptureFixture, ExceptionInfo from testfixtures import Replacer, compare, not_there -from sybil import Sybil +from sybil import Sybil, example as example_module +from sybil.document import Document +from sybil.integration.pytest import SybilItem from sybil.parsers.rest import PythonCodeBlockParser, DocTestParser from sybil.python import import_cleanup from .helpers import ( @@ -131,6 +135,32 @@ def test_pytest(capsys: CaptureFixture[str]): assert results.total == 10 +def test_traceback_filter_single_entry_cut() -> None: + # A failure whose deepest frame is the example frame cuts to a single + # traceback entry; filtering it must not raise IndexError. + source = getsourcefile(example_module) + assert source is not None + example_path = abspath(source) + + document = Document(".. code-block:: python\n\n raise ValueError('boom')\n", "x.rst") + for region in PythonCodeBlockParser()(document): + document.add(region) + + with pytest.raises(ValueError) as excinfo: + next(iter(document.examples())).evaluate() + + # Truncate so the example frame is the deepest, collapsing the cut to one. + raw = excinfo.tb + while raw is not None and raw.tb_frame.f_code.co_filename != example_path: + raw = raw.tb_next + assert raw is not None + raw.tb_next = None + + truncated = ExceptionInfo.from_exc_info((excinfo.type, excinfo.value, raw)) + result = SybilItem.__new__(SybilItem)._traceback_filter(truncated) + assert len(result) == 1 + + def test_pytest_output_immune_to_color_env(capsys: CaptureFixture[str]): with Replacer() as replace: replace.in_environ('PY_COLORS', not_there)