From: Victor Stinner Date: Mon, 9 Dec 2013 00:52:50 +0000 (+0100) Subject: Close #19880: Fix a reference leak in unittest.TestCase. Explicitly break X-Git-Tag: v3.4.0b2~287 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=031bd532c48cf20a9cbf438bdae75dde49e36c51;p=thirdparty%2FPython%2Fcpython.git Close #19880: Fix a reference leak in unittest.TestCase. Explicitly break reference cycles between frames and the _Outcome instance. --- diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 7ed932fafd13..87fb02bd2ef0 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -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.""" diff --git a/Lib/unittest/test/test_case.py b/Lib/unittest/test/test_case.py index 4b931793356a..658d23d132c9 100644 --- a/Lib/unittest/test/test_case.py +++ b/Lib/unittest/test/test_case.py @@ -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() diff --git a/Misc/NEWS b/Misc/NEWS index 351106f1e944..2cefcb65b248 100644 --- 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