[Python-checkins] gh-98458: unittest: bugfix for infinite loop while handling chained exceptions that contain cycles (#98459)

gpshead webhook-mailer at python.org
Sun Dec 4 14:38:28 EST 2022


https://github.com/python/cpython/commit/72ec518203c3f3577a5e888b12f10bb49060e6c2
commit: 72ec518203c3f3577a5e888b12f10bb49060e6c2
branch: main
author: AlexTate <0xalextate at gmail.com>
committer: gpshead <greg at krypto.org>
date: 2022-12-04T11:37:55-08:00
summary:

gh-98458: unittest: bugfix for infinite loop while handling chained exceptions 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

files:
A Misc/NEWS.d/next/Library/2022-10-19-18-31-53.gh-issue-98458.vwyq7O.rst
M Lib/test/test_unittest/test_result.py
M Lib/unittest/result.py

diff --git a/Lib/test/test_unittest/test_result.py b/Lib/test/test_unittest/test_result.py
index e71d114751d9..efd9c9023505 100644
--- a/Lib/test/test_unittest/test_result.py
+++ b/Lib/test/test_unittest/test_result.py
@@ -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
diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py
index 3da7005e603f..5ca4c23238b4 100644
--- a/Lib/unittest/result.py
+++ b/Lib/unittest/result.py
@@ -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
@@ -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):
diff --git a/Misc/NEWS.d/next/Library/2022-10-19-18-31-53.gh-issue-98458.vwyq7O.rst b/Misc/NEWS.d/next/Library/2022-10-19-18-31-53.gh-issue-98458.vwyq7O.rst
new file mode 100644
index 000000000000..f74195cc8e7d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-10-19-18-31-53.gh-issue-98458.vwyq7O.rst
@@ -0,0 +1 @@
+Fix infinite loop in unittest when a self-referencing chained exception is raised



More information about the Python-checkins mailing list