Skip to content

bpo-24959: fix unittest.assertRaises bug where traceback entries are dropped from chained exceptions #23688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 8, 2022
53 changes: 39 additions & 14 deletions Lib/unittest/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,10 @@ def stop(self):
def _exc_info_to_string(self, err, test):
"""Converts a sys.exc_info()-style tuple of values into a string."""
exctype, value, tb = err
# Skip test runner traceback levels
while tb and self._is_relevant_tb_level(tb):
tb = tb.tb_next

if exctype is test.failureException:
# Skip assert*() traceback levels
length = self._count_relevant_tb_levels(tb)
else:
length = None
tb = self._clean_tracebacks(exctype, value, tb, test)
tb_e = traceback.TracebackException(
exctype, value, tb,
limit=length, capture_locals=self.tb_locals, compact=True)
capture_locals=self.tb_locals, compact=True)
msgLines = list(tb_e.format())

if self.buffer:
Expand All @@ -200,16 +192,49 @@ def _exc_info_to_string(self, err, test):
msgLines.append(STDERR_LINE % error)
return ''.join(msgLines)

def _clean_tracebacks(self, exctype, value, tb, test):
ret = None
first = True
excs = [(exctype, value, tb)]
while excs:
(exctype, value, tb) = excs.pop()
# Skip test runner traceback levels
while tb and self._is_relevant_tb_level(tb):
tb = tb.tb_next

# Skip assert*() traceback levels
if exctype is test.failureException:
self._remove_unittest_tb_frames(tb)

if first:
ret = tb
first = False
else:
value.__traceback__ = tb

if value is not None:
for c in (value.__cause__, value.__context__):
if c is not None:
excs.append((type(c), c, c.__traceback__))
return ret

def _is_relevant_tb_level(self, tb):
return '__unittest' in tb.tb_frame.f_globals

def _count_relevant_tb_levels(self, tb):
length = 0
def _remove_unittest_tb_frames(self, tb):
'''Truncates usercode tb at the first unittest frame.

If the first frame of the traceback is in user code,
the prefix up to the first unittest frame is returned.
If the first frame is already in the unittest module,
the traceback is not modified.
'''
prev = None
while tb and not self._is_relevant_tb_level(tb):
length += 1
prev = tb
tb = tb.tb_next
return length
if prev is not None:
prev.tb_next = None

def __repr__(self):
return ("<%s run=%i errors=%i failures=%i>" %
Expand Down
55 changes: 55 additions & 0 deletions Lib/unittest/test/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,61 @@ def test_1(self):
self.assertIs(test_case, test)
self.assertIsInstance(formatted_exc, str)

def test_addFailure_filter_traceback_frames(self):
class Foo(unittest.TestCase):
def test_1(self):
pass

test = Foo('test_1')
def get_exc_info():
try:
test.fail("foo")
except:
return sys.exc_info()

exc_info_tuple = get_exc_info()

full_exc = traceback.format_exception(*exc_info_tuple)

result = unittest.TestResult()
result.startTest(test)
result.addFailure(test, exc_info_tuple)
result.stopTest(test)

formatted_exc = result.failures[0][1]
dropped = [l for l in full_exc if l not in formatted_exc]
self.assertEqual(len(dropped), 1)
self.assertIn("raise self.failureException(msg)", dropped[0])

def test_addFailure_filter_traceback_frames_context(self):
class Foo(unittest.TestCase):
def test_1(self):
pass

test = Foo('test_1')
def get_exc_info():
try:
try:
test.fail("foo")
except:
raise ValueError(42)
except:
return sys.exc_info()

exc_info_tuple = get_exc_info()

full_exc = traceback.format_exception(*exc_info_tuple)

result = unittest.TestResult()
result.startTest(test)
result.addFailure(test, exc_info_tuple)
result.stopTest(test)

formatted_exc = result.failures[0][1]
dropped = [l for l in full_exc if l not in formatted_exc]
self.assertEqual(len(dropped), 1)
self.assertIn("raise self.failureException(msg)", dropped[0])

# "addError(test, err)"
# ...
# "Called when the test case test raises an unexpected exception err
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug where :mod:`unittest` sometimes drops frames from tracebacks of exceptions raised in tests.