Fix internal error with recursive tracebacks with that frames contain objects that can't be compared

Fix #2459
This commit is contained in:
Bruno Oliveira 2017-06-06 12:41:15 -03:00
parent 57e2ced969
commit 2127a2378a
3 changed files with 68 additions and 7 deletions

View File

@ -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:

1
changelog/2459.bugfix Normal file
View File

@ -0,0 +1 @@
Fix recursion error detection when frames in the traceback contain objects that can't be compared (like ``numpy`` arrays).

View File

@ -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*',
])