diff --git a/changelog/3804.bugfix.rst b/changelog/3804.bugfix.rst new file mode 100644 index 000000000..d03afe9b2 --- /dev/null +++ b/changelog/3804.bugfix.rst @@ -0,0 +1 @@ +Fix traceback reporting for exceptions with ``__cause__`` cycles. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 78644db8a..d6c5cd90e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -719,7 +719,9 @@ class FormattedExcinfo(object): repr_chain = [] e = excinfo.value descr = None - while e is not None: + seen = set() + while e is not None and id(e) not in seen: + seen.add(id(e)) if excinfo: reprtraceback = self.repr_traceback(excinfo) reprcrash = excinfo._getreprcrash() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 403063ad6..fbdaeacf7 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function import operator import os import sys +import textwrap import _pytest import py import pytest @@ -1265,6 +1266,50 @@ raise ValueError() ] ) + @pytest.mark.skipif("sys.version_info[0] < 3") + def test_exc_chain_repr_cycle(self, importasmod): + 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") + tw = TWMock() + r.toterminal(tw) + out = "\n".join(line for line in tw.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 + @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])