Thanks to visit codestin.com
Credit goes to github.com

Skip to content

gh-98458: unittest: bugfix for infinite loop while handling chained exceptions that contain cycles #98459

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 8 commits into from
Dec 4, 2022
56 changes: 56 additions & 0 deletions Lib/test/test_unittest/test_result.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix infinite loop in unittest when a self-referencing chained exception is raised