mirror of
https://github.com/python/cpython.git
synced 2025-07-23 03:05:38 +00:00
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 72ec518203
)
Co-authored-by: AlexTate <0xalextate@gmail.com>
This commit is contained in:
parent
7aa87bba05
commit
9bcc68b045
3 changed files with 60 additions and 1 deletions
|
@ -196,6 +196,7 @@ class TestResult(object):
|
||||||
ret = None
|
ret = None
|
||||||
first = True
|
first = True
|
||||||
excs = [(exctype, value, tb)]
|
excs = [(exctype, value, tb)]
|
||||||
|
seen = {id(value)} # Detect loops in chained exceptions.
|
||||||
while excs:
|
while excs:
|
||||||
(exctype, value, tb) = excs.pop()
|
(exctype, value, tb) = excs.pop()
|
||||||
# Skip test runner traceback levels
|
# Skip test runner traceback levels
|
||||||
|
@ -214,8 +215,9 @@ class TestResult(object):
|
||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
for c in (value.__cause__, value.__context__):
|
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__))
|
excs.append((type(c), c, c.__traceback__))
|
||||||
|
seen.add(id(c))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _is_relevant_tb_level(self, tb):
|
def _is_relevant_tb_level(self, tb):
|
||||||
|
|
|
@ -275,6 +275,62 @@ class Test_TestResult(unittest.TestCase):
|
||||||
self.assertEqual(len(dropped), 1)
|
self.assertEqual(len(dropped), 1)
|
||||||
self.assertIn("raise self.failureException(msg)", dropped[0])
|
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)"
|
# "addError(test, err)"
|
||||||
# ...
|
# ...
|
||||||
# "Called when the test case test raises an unexpected exception err
|
# "Called when the test case test raises an unexpected exception err
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fix infinite loop in unittest when a self-referencing chained exception is raised
|
Loading…
Add table
Add a link
Reference in a new issue