]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
Close #19880: Fix a reference leak in unittest.TestCase. Explicitly break
authorVictor Stinner <victor.stinner@gmail.com>
Mon, 9 Dec 2013 00:52:50 +0000 (01:52 +0100)
committerVictor Stinner <victor.stinner@gmail.com>
Mon, 9 Dec 2013 00:52:50 +0000 (01:52 +0100)
reference cycles between frames and the _Outcome instance.

Lib/unittest/case.py
Lib/unittest/test/test_case.py
Misc/NEWS

index 7ed932fafd13f255f54f5151c8dce709f2db0110..87fb02bd2ef0074d4b8177b1c6a6d6be64b9cfcf 100644 (file)
@@ -69,6 +69,9 @@ class _Outcome(object):
             else:
                 self.success = False
                 self.errors.append((test_case, exc_info))
+            # explicitly break a reference cycle:
+            # exc_info -> frame -> exc_info
+            exc_info = None
         else:
             if self.result_supports_subtests and self.success:
                 self.errors.append((test_case, None))
@@ -559,8 +562,8 @@ class TestCase(object):
             return
         expecting_failure = getattr(testMethod,
                                     "__unittest_expecting_failure__", False)
+        outcome = _Outcome(result)
         try:
-            outcome = _Outcome(result)
             self._outcome = outcome
 
             with outcome.testPartExecutor(self):
@@ -593,6 +596,15 @@ class TestCase(object):
                 if stopTestRun is not None:
                     stopTestRun()
 
+            # explicitly break reference cycles:
+            # outcome.errors -> frame -> outcome -> outcome.errors
+            # outcome.expectedFailure -> frame -> outcome -> outcome.expectedFailure
+            outcome.errors.clear()
+            outcome.expectedFailure = None
+
+            # clear the outcome, no more needed
+            self._outcome = None
+
     def doCleanups(self):
         """Execute all cleanup functions. Normally called for you after
         tearDown."""
index 4b931793356a64815ea9ecdc83f0fe1b6c54e6dd..658d23d132c9fb150223280ba040aaf9a5f8e2c9 100644 (file)
@@ -1533,6 +1533,32 @@ test case
             del case
             self.assertFalse(wr())
 
+    def test_no_exception_leak(self):
+        # Issue #19880: TestCase.run() should not keep a reference
+        # to the exception
+        class MyException(Exception):
+            ninstance = 0
+
+            def __init__(self):
+                MyException.ninstance += 1
+                Exception.__init__(self)
+
+            def __del__(self):
+                MyException.ninstance -= 1
+
+        class TestCase(unittest.TestCase):
+            def test1(self):
+                raise MyException()
+
+            @unittest.expectedFailure
+            def test2(self):
+                raise MyException()
+
+        for method_name in ('test1', 'test2'):
+            testcase = TestCase(method_name)
+            testcase.run()
+            self.assertEqual(MyException.ninstance, 0)
+
 
 if __name__ == "__main__":
     unittest.main()
index 351106f1e9446cd0a99c65afd2ab3f3f99ac6c03..2cefcb65b248b9ba5cda26ef209613f3052d5626 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -21,6 +21,9 @@ Core and Builtins
 Library
 -------
 
+- Issue #19880: Fix a reference leak in unittest.TestCase. Explicitly break
+  reference cycles between frames and the _Outcome instance.
+
 - Issue #17429: platform.linux_distribution() now decodes files from the UTF-8
   encoding with the surrogateescape error handler, instead of decoding from the
   locale encoding in strict mode. It fixes the function on Fedora 19 which is