diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1470,15 +1470,24 @@ Grouping tests Tests grouped by a :class:`TestSuite` are always accessed by iteration. Subclasses can lazily provide tests by overriding :meth:`__iter__`. Note - that this method maybe called several times on a single suite - (for example when counting tests or comparing for equality) - so the tests returned must be the same for repeated iterations. + that this method may be called several times on a single suite (for + example when counting tests or comparing for equality) so the tests + returned by repeated iterations before :meth:`TestSuite.run` must be the + same for each call iteration. After :meth:`TestSuite.run`, callers should + not rely on the tests returned by this method unless the caller uses a + subclass that overrides :meth:`TestSuite._removeTestAtIndex` to preserve + test references. .. versionchanged:: 3.2 In earlier versions the :class:`TestSuite` accessed tests directly rather than through iteration, so overriding :meth:`__iter__` wasn't sufficient for providing tests. + .. versionchanged:: 3.4 + In earlier versions the :class:`TestSuite` held references to each + :class:`TestCase` after :meth:`TestSuite.run`. Subclasses can restore + that behavior by overriding :meth:`TestSuite._removeTestAtIndex`. + In the typical usage of a :class:`TestSuite` object, the :meth:`run` method is invoked by a :class:`TestRunner` rather than by the end-user test harness. diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py --- a/Lib/unittest/suite.py +++ b/Lib/unittest/suite.py @@ -57,12 +57,21 @@ class BaseTestSuite(object): self.addTest(test) def run(self, result): - for test in self: + for index, test in enumerate(self): if result.shouldStop: break test(result) + self._removeTestAtIndex(index) return result + def _removeTestAtIndex(self, index): + """Stop holding a reference to the TestCase at index.""" + try: + self._tests[index] = None + except TypeError: + # support for suite implementations that have overriden self._test + pass + def __call__(self, *args, **kwds): return self.run(*args, **kwds) @@ -87,7 +96,7 @@ class TestSuite(BaseTestSuite): if getattr(result, '_testRunEntered', False) is False: result._testRunEntered = topLevel = True - for test in self: + for index, test in enumerate(self): if result.shouldStop: break @@ -106,6 +115,8 @@ class TestSuite(BaseTestSuite): else: test.debug() + self._removeTestAtIndex(index) + if topLevel: self._tearDownPreviousClass(None, result) self._handleModuleTearDown(result) diff --git a/Lib/unittest/test/test_setups.py b/Lib/unittest/test/test_setups.py --- a/Lib/unittest/test/test_setups.py +++ b/Lib/unittest/test/test_setups.py @@ -494,12 +494,10 @@ class TestSetups(unittest.TestCase): Test.__module__ = 'Module' sys.modules['Module'] = Module - _suite = unittest.defaultTestLoader.loadTestsFromTestCase(Test) - suite = unittest.TestSuite() - suite.addTest(_suite) - messages = ('setUpModule', 'tearDownModule', 'setUpClass', 'tearDownClass', 'test_something') for phase, msg in enumerate(messages): + _suite = unittest.defaultTestLoader.loadTestsFromTestCase(Test) + suite = unittest.TestSuite([_suite]) with self.assertRaisesRegex(Exception, msg): suite.debug() diff --git a/Lib/unittest/test/test_suite.py b/Lib/unittest/test/test_suite.py --- a/Lib/unittest/test/test_suite.py +++ b/Lib/unittest/test/test_suite.py @@ -1,6 +1,8 @@ import unittest +import gc import sys +import weakref from .support import LoggingResult, TestEquality @@ -300,7 +302,46 @@ class Test_TestSuite(unittest.TestCase, # when the bug is fixed this line will not crash suite.run(unittest.TestResult()) + def test_remove_test_at_index(self): + suite = unittest.TestSuite() + suite._tests = [1, 2, 3] + suite._removeTestAtIndex(1) + + self.assertEqual([1, None, 3], suite._tests) + + def test_remove_test_at_index_not_indexable(self): + suite = unittest.TestSuite() + suite._tests = None + + # if _removeAtIndex raises for noniterables this next line will break + suite._removeTestAtIndex(2) + + def assert_garbage_collect_test_after_run(self, TestSuiteClass): + + class Foo(unittest.TestCase): + def test_nothing(self): + pass + + test = Foo('test_nothing') + wref = weakref.ref(test) + + suite = TestSuiteClass([wref()]) + suite.run(unittest.TestResult()) + + del test + + # for the benefit of non-reference counting implementations + gc.collect() + + self.assertEqual(suite._tests, [None]) + self.assertIsNone(wref()) + + def test_garbage_collect_test_after_run_BaseTestSuite(self): + self.assert_garbage_collect_test_after_run(unittest.BaseTestSuite) + + def test_garbage_collect_test_after_run_TestSuite(self): + self.assert_garbage_collect_test_after_run(unittest.TestSuite) def test_basetestsuite(self): class Test(unittest.TestCase): @@ -363,6 +404,5 @@ class Test_TestSuite(unittest.TestCase, self.assertFalse(result._testRunEntered) - if __name__ == '__main__': unittest.main()