diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index f872dba0b..9b3408dc4 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -3,7 +3,7 @@ import sys from inspect import CO_VARARGS, CO_VARKEYWORDS import re from weakref import ref -from _pytest.compat import _PY2, _PY3, PY35 +from _pytest.compat import _PY2, _PY3, PY35, safe_str import py builtin_repr = repr @@ -602,21 +602,48 @@ class FormattedExcinfo(object): traceback = excinfo.traceback if self.tbfilter: traceback = traceback.filter() - recursionindex = None + if is_recursion_error(excinfo): - recursionindex = traceback.recursionindex() + traceback, extraline = self._truncate_recursive_traceback(traceback) + else: + extraline = None + last = traceback[-1] entries = [] - extraline = None for index, entry in enumerate(traceback): einfo = (last == entry) and excinfo or None reprentry = self.repr_traceback_entry(entry, einfo) entries.append(reprentry) - if index == recursionindex: - extraline = "!!! Recursion detected (same locals & position)" - break return ReprTraceback(entries, extraline, style=self.style) + def _truncate_recursive_traceback(self, traceback): + """ + Truncate the given recursive traceback trying to find the starting point + of the recursion. + + The detection is done by going through each traceback entry and finding the + point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``. + + Handle the situation where the recursion process might raise an exception (for example + comparing numpy arrays using equality raises a TypeError), in which case we do our best to + warn the user of the error and show a limited traceback. + """ + try: + recursionindex = traceback.recursionindex() + except Exception as e: + max_frames = 10 + extraline = ( + '!!! Recursion error detected, but an error occurred locating the origin of recursion.\n' + ' The following exception happened when comparing locals in the stack frame:\n' + ' {exc_type}: {exc_msg}\n' + ' Displaying first and last {max_frames} stack frames out of {total}.' + ).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback)) + traceback = traceback[:max_frames] + traceback[-max_frames:] + else: + extraline = "!!! Recursion detected (same locals & position)" + traceback = traceback[:recursionindex + 1] + + return traceback, extraline def repr_excinfo(self, excinfo): if _PY2: diff --git a/changelog/2459.bugfix b/changelog/2459.bugfix new file mode 100644 index 000000000..66cf95624 --- /dev/null +++ b/changelog/2459.bugfix @@ -0,0 +1 @@ +Fix recursion error detection when frames in the traceback contain objects that can't be compared (like ``numpy`` arrays). diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index b7dafdb46..3128beff8 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1140,3 +1140,36 @@ def test_cwd_deleted(testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(['* 1 failed in *']) assert 'INTERNALERROR' not in result.stdout.str() + result.stderr.str() + + +def test_exception_repr_extraction_error_on_recursion(): + """ + Ensure we can properly detect a recursion error even + if some locals raise error on comparision (#2459). + """ + class numpy_like(object): + + 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()) + + try: + a(numpy_like()) + except: + from _pytest._code.code import ExceptionInfo + from _pytest.pytester import LineMatcher + exc_info = ExceptionInfo() + + matcher = LineMatcher(str(exc_info.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*', + ])