diff options
author | Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> | 2022-12-04 20:06:42 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-04 20:06:42 (GMT) |
commit | 9bcc68b045fd06af3896d15d6375fa4d96b706cc (patch) | |
tree | 4bf24b3f4d734d9c50dd3fc29368d8c1b10110de /Lib | |
parent | 7aa87bba056c9c548812a82cefbd122c67c71b88 (diff) | |
download | cpython-9bcc68b045fd06af3896d15d6375fa4d96b706cc.zip cpython-9bcc68b045fd06af3896d15d6375fa4d96b706cc.tar.gz cpython-9bcc68b045fd06af3896d15d6375fa4d96b706cc.tar.bz2 |
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 72ec518203c3f3577a5e888b12f10bb49060e6c2)
Co-authored-by: AlexTate <0xalextate@gmail.com>
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/unittest/result.py | 4 | ||||
-rw-r--r-- | Lib/unittest/test/test_result.py | 56 |
2 files changed, 59 insertions, 1 deletions
diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index 3da7005..5ca4c23 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -196,6 +196,7 @@ class TestResult(object): 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 @@ class TestResult(object): 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/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py index b0cc3d8..9320b0a 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -275,6 +275,62 @@ class Test_TestResult(unittest.TestCase): 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 |