Skip to content

Commit

Permalink
gh-98458: unittest: bugfix for infinite loop while handling chained e…
Browse files Browse the repository at this point in the history
…xceptions that contain cycles (#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
  • Loading branch information
AlexTate committed Dec 4, 2022
1 parent 1012dc1 commit 72ec518
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 1 deletion.
56 changes: 56 additions & 0 deletions Lib/test/test_unittest/test_result.py
Expand Up @@ -275,6 +275,62 @@ def get_exc_info():
self.assertEqual(len(dropped), 1)
self.assertIn("raise self.failureException(msg)", dropped[0])

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

def get_exc_info():
try:
loop = Exception("Loop")
loop.__cause__ = loop
loop.__context__ = loop
raise loop
except:
return sys.exc_info()

exc_info_tuple = get_exc_info()

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

formatted_exc = result.failures[0][1]
self.assertEqual(formatted_exc.count("Exception: Loop\n"), 1)

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

def get_exc_info():
try:
# Create two directionally opposed cycles
# __cause__ in one direction, __context__ in the other
A, B, C = Exception("A"), Exception("B"), Exception("C")
edges = [(C, B), (B, A), (A, C)]
for ex1, ex2 in edges:
ex1.__cause__ = ex2
ex2.__context__ = ex1
raise C
except:
return sys.exc_info()

exc_info_tuple = get_exc_info()

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

formatted_exc = result.failures[0][1]
self.assertEqual(formatted_exc.count("Exception: A\n"), 1)
self.assertEqual(formatted_exc.count("Exception: B\n"), 1)
self.assertEqual(formatted_exc.count("Exception: C\n"), 1)

# "addError(test, err)"
# ...
# "Called when the test case test raises an unexpected exception err
Expand Down
4 changes: 3 additions & 1 deletion Lib/unittest/result.py
Expand Up @@ -196,6 +196,7 @@ def _clean_tracebacks(self, exctype, value, tb, test):
ret = None
first = True
excs = [(exctype, value, tb)]
seen = {id(value)} # Detect loops in chained exceptions.
while excs:
(exctype, value, tb) = excs.pop()
# Skip test runner traceback levels
Expand All @@ -214,8 +215,9 @@ def _clean_tracebacks(self, exctype, value, tb, test):

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

def _is_relevant_tb_level(self, tb):
Expand Down
@@ -0,0 +1 @@
Fix infinite loop in unittest when a self-referencing chained exception is raised

0 comments on commit 72ec518

Please sign in to comment.