8000 bpo-24959: fix unittest.assertRaises bug where traceback entries are … · python-docs-tr/cpython@88b7d86 · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 88b7d86

Browse files
authored
bpo-24959: fix unittest.assertRaises bug where traceback entries are dropped from chained exceptions (pythonGH-23688)
1 parent da80d6b commit 88b7d86

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,18 +173,10 @@ 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(
186178
exctype, value, tb,
187-
limit=length, capture_locals=self.tb_locals, compact=True)
179+
capture_locals=self.tb_locals, compact=True)
188180
msgLines = list(tb_e.format())
189181

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

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

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

207-
def _count_relevant_tb_levels(self, tb):
208-
length = 0
224+
def _remove_unittest_tb_frames(self, tb):
225+
'''Truncates usercode tb at the first unittest frame.
226+
227+
If the first frame of the traceback is in user code,
228+
the prefix up to the first unittest frame is returned.
229+
If the first frame is already in the unittest module,
230+
the traceback is not modified.
231+
'''
232+
prev = None
209233
while tb and not self._is_relevant_tb_level(tb):
210-
length += 1
234+
prev = tb
211235
tb = tb.tb_next
212-
return length
236+
if prev is not None:
237+
prev.tb_next = None
213238

214239
def __repr__(self):
215240
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
@@ -220,6 +220,61 @@ def test_1(self):
220220
self.assertIs(test_case, test)
221221
self.assertIsInstance(formatted_exc, str)
222222

223+
def test_addFailure_filter_traceback_frames(self):
224+
class Foo(unittest.TestCase):
225+
def test_1(self):
226+
pass
227+
228+
test = Foo('test_1')
229+
def get_exc_info():
230+
try:
231+
test.fail("foo")
232+
except:
233+
return sys.exc_info()
234+
235+
exc_info_tuple = get_exc_info()
236+
237+
full_exc = traceback.format_exception(*exc_info_tuple)
238+
239+
result = unittest.TestResult()
240+
result.startTest(test)
241+
result.addFailure(test, exc_info_tuple)
242+
result.stopTest(test)
243+
244+
formatted_exc = result.failures[0][1]
245+
dropped = [l for l in full_exc if l not in formatted_exc]
246+
self.assertEqual(len(dropped), 1)
247+
self.assertIn("raise self.failureException(msg)", dropped[0])
248+
249+
def test_addFailure_filter_traceback_frames_context(self):
250+
class Foo(unittest.TestCase):
251+
def test_1(self):
252+
pass
253+
254+
test = Foo('test_1')
255+
def get_exc_info():
256+
try:
257+
try:
258+
test.fail("foo")
259+
except:
260+
raise ValueError(42)
261+
except:
262+
return sys.exc_info()
263+
264+
exc_info_tuple = get_exc_info()
265+
266+
full_exc = traceback.format_exception(*exc_info_tuple)
267+
268+
result = unittest.TestResult()
269+
result.startTest(test)
270+
result.addFailure(test, exc_info_tuple)
271+
result.stopTest(test)
272+
273+
formatted_exc = result.failures[0][1]
274+
dropped = [l for l in full_exc if l not in formatted_exc]
275+
self.assertEqual(len(dropped), 1)
276+
self.assertIn("raise self.failureException(msg)", dropped[0])
277+
223278
# "addError(test, err)"
224279
# ...
225280
# "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)
0