Skip to content

Commit f3ea249

Browse files
authored
bpo-24959: fix unittest.assertRaises bug where traceback entries are dropped from chained exceptions (GH-23688) (GH-31776)
(cherry picked from commit 88b7d86)
1 parent 20e88f7 commit f3ea249

File tree

3 files changed

+95
-14
lines changed

3 files changed

+95
-14
lines changed

Lib/unittest/result.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,9 @@ def stop(self):
173173
def _exc_info_to_string(self, err, test):
174174
"""Converts a sys.exc_info()-style tuple of values into a string."""
175175
exctype, value, tb = err
176-
# Skip test runner traceback levels
177-
while tb and self._is_relevant_tb_level(tb):
178-
tb = tb.tb_next
179-
180-
if exctype is test.failureException:
181-
# Skip assert*() traceback levels
182-
length = self._count_relevant_tb_levels(tb)
183-
else:
184-
length = None
176+
tb = self._clean_tracebacks(exctype, value, tb, test)
185177
tb_e = traceback.TracebackException(
186-
exctype, value, tb, limit=length, capture_locals=self.tb_locals)
178+
exctype, value, tb, capture_locals=self.tb_locals)
187179
msgLines = list(tb_e.format())
188180

189181
if self.buffer:
@@ -199,16 +191,49 @@ def _exc_info_to_string(self, err, test):
199191
msgLines.append(STDERR_LINE % error)
200192
return ''.join(msgLines)
201193

194+
def _clean_tracebacks(self, exctype, value, tb, test):
195+
ret = None
196+
first = True
197+
excs = [(exctype, value, tb)]
198+
while excs:
199+
(exctype, value, tb) = excs.pop()
200+
# Skip test runner traceback levels
201+
while tb and self._is_relevant_tb_level(tb):
202+
tb = tb.tb_next
203+
204+
# Skip assert*() traceback levels
205+
if exctype is test.failureException:
206+
self._remove_unittest_tb_frames(tb)
207+
208+
if first:
209+
ret = tb
210+
first = False
211+
else:
212+
value.__traceback__ = tb
213+
214+
if value is not None:
215+
for c in (value.__cause__, value.__context__):
216+
if c is not None:
217+
excs.append((type(c), c, c.__traceback__))
218+
return ret
202219

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

206-
def _count_relevant_tb_levels(self, tb):
207-
length = 0
223+
def _remove_unittest_tb_frames(self, tb):
224+
'''Truncates usercode tb at the first unittest frame.
225+
226+
If the first frame of the traceback is in user code,
227+
the prefix up to the first unittest frame is returned.
228+
If the first frame is already in the unittest module,
229+
the traceback is not modified.
230+
'''
231+
prev = None
208232
while tb and not self._is_relevant_tb_level(tb):
209-
length += 1
233+
prev = tb
210234
tb = tb.tb_next
211-
return length
235+
if prev is not None:
236+
prev.tb_next = None
212237

213238
def __repr__(self):
214239
return ("<%s run=%i errors=%i failures=%i>" %

Lib/unittest/test/test_result.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,61 @@ def test_1(self):
221221
self.assertIs(test_case, test)
222222
self.assertIsInstance(formatted_exc, str)
223223

224+
def test_addFailure_filter_traceback_frames(self):
225+
class Foo(unittest.TestCase):
226+
def test_1(self):
227+
pass
228+
229+
test = Foo('test_1')
230+
def get_exc_info():
231+
try:
232+
test.fail("foo")
233+
except:
234+
return sys.exc_info()
235+
236+
exc_info_tuple = get_exc_info()
237+
238+
full_exc = traceback.format_exception(*exc_info_tuple)
239+
240+
result = unittest.TestResult()
241+
result.startTest(test)
242+
result.addFailure(test, exc_info_tuple)
243+
result.stopTest(test)
244+
245+
formatted_exc = result.failures[0][1]
246+
dropped = [l for l in full_exc if l not in formatted_exc]
247+
self.assertEqual(len(dropped), 1)
248+
self.assertIn("raise self.failureException(msg)", dropped[0])
249+
250+
def test_addFailure_filter_traceback_frames_context(self):
251+
class Foo(unittest.TestCase):
252+
def test_1(self):
253+
pass
254+
255+
test = Foo('test_1')
256+
def get_exc_info():
257+
try:
258+
try:
259+
test.fail("foo")
260+
except:
261+
raise ValueError(42)
262+
except:
263+
return sys.exc_info()
264+
265+
exc_info_tuple = get_exc_info()
266+
267+
full_exc = traceback.format_exception(*exc_info_tuple)
268+
269+
result = unittest.TestResult()
270+
result.startTest(test)
271+
result.addFailure(test, exc_info_tuple)
272+
result.stopTest(test)
273+
274+
formatted_exc = result.failures[0][1]
275+
dropped = [l for l in full_exc if l not in formatted_exc]
276+
self.assertEqual(len(dropped), 1)
277+
self.assertIn("raise self.failureException(msg)", dropped[0])
278+
224279
# "addError(test, err)"
225280
# ...
226281
# "Called when the test case test raises an unexpected exception err
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix bug where :mod:`unittest` sometimes drops frames from tracebacks of exceptions raised in tests.

0 commit comments

Comments
 (0)