Skip to content

Commit 9bcc68b

Browse files
gh-98458: unittest: bugfix for infinite loop while handling chained exceptions that contain cycles (GH-98459)
* Bugfix addressing infinite loop while handling self-referencing chained exception in TestResult._clean_tracebacks() * Bugfix extended to properly handle exception cycles in _clean_tracebacks. The "seen" set follows the approach used in the TracebackException class (thank you @iritkatriel for pointing it out) * adds a test for a single chained exception that holds a self-loop in its __cause__ and __context__ attributes (cherry picked from commit 72ec518) Co-authored-by: AlexTate <0xalextate@gmail.com>
1 parent 7aa87bb commit 9bcc68b

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

Lib/unittest/result.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def _clean_tracebacks(self, exctype, value, tb, test):
196196
ret = None
197197
first = True
198198
excs = [(exctype, value, tb)]
199+
seen = {id(value)} # Detect loops in chained exceptions.
199200
while excs:
200201
(exctype, value, tb) = excs.pop()
201202
# Skip test runner traceback levels
@@ -214,8 +215,9 @@ def _clean_tracebacks(self, exctype, value, tb, test):
214215

215216
if value is not None:
216217
for c in (value.__cause__, value.__context__):
217-
if c is not None:
218+
if c is not None and id(c) not in seen:
218219
excs.append((type(c), c, c.__traceback__))
220+
seen.add(id(c))
219221
return ret
220222

221223
def _is_relevant_tb_level(self, tb):

Lib/unittest/test/test_result.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,62 @@ def get_exc_info():
275275
self.assertEqual(len(dropped), 1)
276276
self.assertIn("raise self.failureException(msg)", dropped[0])
277277

278+
def test_addFailure_filter_traceback_frames_chained_exception_self_loop(self):
279+
class Foo(unittest.TestCase):
280+
def test_1(self):
281+
pass
282+
283+
def get_exc_info():
284+
try:
285+
loop = Exception("Loop")
286+
loop.__cause__ = loop
287+
loop.__context__ = loop
288+
raise loop
289+
except:
290+
return sys.exc_info()
291+
292+
exc_info_tuple = get_exc_info()
293+
294+
test = Foo('test_1')
295+
result = unittest.TestResult()
296+
result.startTest(test)
297+
result.addFailure(test, exc_info_tuple)
298+
result.stopTest(test)
299+
300+
formatted_exc = result.failures[0][1]
301+
self.assertEqual(formatted_exc.count("Exception: Loop\n"), 1)
302+
303+
def test_addFailure_filter_traceback_frames_chained_exception_cycle(self):
304+
class Foo(unittest.TestCase):
305+
def test_1(self):
306+
pass
307+
308+
def get_exc_info():
309+
try:
310+
# Create two directionally opposed cycles
311+
# __cause__ in one direction, __context__ in the other
312+
A, B, C = Exception("A"), Exception("B"), Exception("C")
313+
edges = [(C, B), (B, A), (A, C)]
314+
for ex1, ex2 in edges:
315+
ex1.__cause__ = ex2
316+
ex2.__context__ = ex1
317+
raise C
318+
except:
319+
return sys.exc_info()
320+
321+
exc_info_tuple = get_exc_info()
322+
323+
test = Foo('test_1')
324+
result = unittest.TestResult()
325+
result.startTest(test)
326+
result.addFailure(test, exc_info_tuple)
327+
result.stopTest(test)
328+
329+
formatted_exc = result.failures[0][1]
330+
self.assertEqual(formatted_exc.count("Exception: A\n"), 1)
331+
self.assertEqual(formatted_exc.count("Exception: B\n"), 1)
332+
self.assertEqual(formatted_exc.count("Exception: C\n"), 1)
333+
278334
# "addError(test, err)"
279335
# ...
280336
# "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 infinite loop in unittest when a self-referencing chained exception is raised

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy