Close #19880: Fix a reference leak in unittest.TestCase. Explicitly break

reference cycles between frames and the _Outcome instance.
This commit is contained in:
Victor Stinner 2013-12-09 01:52:50 +01:00
parent 28dd6deca8
commit 031bd532c4
3 changed files with 42 additions and 1 deletions

View file

@ -69,6 +69,9 @@ class _Outcome(object):
else: else:
self.success = False self.success = False
self.errors.append((test_case, exc_info)) self.errors.append((test_case, exc_info))
# explicitly break a reference cycle:
# exc_info -> frame -> exc_info
exc_info = None
else: else:
if self.result_supports_subtests and self.success: if self.result_supports_subtests and self.success:
self.errors.append((test_case, None)) self.errors.append((test_case, None))
@ -559,8 +562,8 @@ class TestCase(object):
return return
expecting_failure = getattr(testMethod, expecting_failure = getattr(testMethod,
"__unittest_expecting_failure__", False) "__unittest_expecting_failure__", False)
try:
outcome = _Outcome(result) outcome = _Outcome(result)
try:
self._outcome = outcome self._outcome = outcome
with outcome.testPartExecutor(self): with outcome.testPartExecutor(self):
@ -593,6 +596,15 @@ class TestCase(object):
if stopTestRun is not None: if stopTestRun is not None:
stopTestRun() 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): def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after """Execute all cleanup functions. Normally called for you after
tearDown.""" tearDown."""

View file

@ -1533,6 +1533,32 @@ test case
del case del case
self.assertFalse(wr()) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -21,6 +21,9 @@ Core and Builtins
Library 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 - Issue #17429: platform.linux_distribution() now decodes files from the UTF-8
encoding with the surrogateescape error handler, instead of decoding from the 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 locale encoding in strict mode. It fixes the function on Fedora 19 which is