Index: Lib/unittest.py =================================================================== --- Lib/unittest.py (revision 71118) +++ Lib/unittest.py (working copy) @@ -346,6 +346,7 @@ raise ValueError("no such test method in %s: %s" % \ (self.__class__, methodName)) self._testMethodDoc = testMethod.__doc__ + self._cleanups = [] # Map types to custom assertEqual functions that will compare # instances of said type in more detail to generate a more useful @@ -371,6 +372,14 @@ useful error message when the two arguments are not equal. """ self._type_equality_funcs[typeobj] = _AssertWrapper(function) + + def addCleanup(self, function, *args, **kwargs): + """Add a function, with arguments, to be called when the test is + completed. Functions added are called on a LIFO basis and are + called after tearDown on test failure or success. + + Cleanup items are called even if setUp fails (unlike tearDown).""" + self._cleanups.append((function, args, kwargs)) def setUp(self): "Hook method for setting up the test fixture before exercising it." @@ -432,40 +441,51 @@ result.startTest(self) testMethod = getattr(self, self._testMethodName) try: + success = False try: self.setUp() except SkipTest as e: result.addSkip(self, str(e)) - return except Exception: result.addError(self, sys.exc_info()) - return - - success = False - try: - testMethod() - except self.failureException: - result.addFailure(self, sys.exc_info()) - except _ExpectedFailure as e: - result.addExpectedFailure(self, e.exc_info) - except _UnexpectedSuccess: - result.addUnexpectedSuccess(self) - except SkipTest as e: - result.addSkip(self, str(e)) - except Exception: - result.addError(self, sys.exc_info()) else: - success = True - - try: - self.tearDown() - except Exception: - result.addError(self, sys.exc_info()) - success = False + try: + testMethod() + except self.failureException: + result.addFailure(self, sys.exc_info()) + except _ExpectedFailure as e: + result.addExpectedFailure(self, e.exc_info) + except _UnexpectedSuccess: + result.addUnexpectedSuccess(self) + except SkipTest as e: + result.addSkip(self, str(e)) + except Exception: + result.addError(self, sys.exc_info()) + else: + success = True + + try: + self.tearDown() + except Exception: + result.addError(self, sys.exc_info()) + success = False + + cleanUpSuccess = self._doCleanups(result) + success = success and cleanUpSuccess if success: result.addSuccess(self) finally: result.stopTest(self) + + def _doCleanups(self, result): + ok = True + for function, args, kwargs in reversed(self._cleanups): + try: + function(*args, **kwargs) + except Exception: + ok = False + result.addError(self, sys.exc_info()) + return ok def __call__(self, *args, **kwds): return self.run(*args, **kwds) Index: Lib/test/test_unittest.py =================================================================== --- Lib/test/test_unittest.py (revision 71118) +++ Lib/test/test_unittest.py (working copy) @@ -3016,7 +3016,105 @@ "^unexpectedly None$", "^unexpectedly None : oops$"]) + +class TestCleanUp(TestCase): + + def testCleanUp(self): + class TestableTest(TestCase): + def testNothing(self): + pass + + test = TestableTest('testNothing') + self.assertEqual(test._cleanups, []) + + cleanups = [] + + def cleanup1(*args, **kwargs): + cleanups.append((1, args, kwargs)) + + def cleanup2(*args, **kwargs): + cleanups.append((2, args, kwargs)) + + test.addCleanup(cleanup1, 1, 2, 3, four='hello', five='goodbye') + test.addCleanup(cleanup2) + + self.assertEqual(test._cleanups, + [(cleanup1, (1, 2, 3), dict(four='hello', five='goodbye')), + (cleanup2, (), {})]) + + result = test._doCleanups(None) + self.assertTrue(result) + + self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))]) + + def testCleanUpWithErrors(self): + class TestableTest(TestCase): + def testNothing(self): + pass + + class MockResult(object): + errors = [] + def addError(self, test, exc_info): + self.errors.append((test, exc_info)) + + result = MockResult() + test = TestableTest('testNothing') + + exc1 = Exception('foo') + exc2 = Exception('bar') + def cleanup1(): + raise exc1 + + def cleanup2(): + raise exc2 + + test.addCleanup(cleanup1) + test.addCleanup(cleanup2) + + self.assertFalse(test._doCleanups(result)) + + (test1, (Type1, instance1, _)), (test2, (Type2, instance2, _)) = reversed(MockResult.errors) + self.assertEqual((test1, Type1, instance1), (test, Exception, exc1)) + self.assertEqual((test2, Type2, instance2), (test, Exception, exc2)) + + def testCleanupInRun(self): + blowUp = False + ordering = [] + + class TestableTest(TestCase): + def setUp(self): + ordering.append('setUp') + if blowUp: + raise Exception('foo') + + def testNothing(self): + ordering.append('test') + + def tearDown(self): + ordering.append('tearDown') + + test = TestableTest('testNothing') + + def cleanup(): + ordering.append('cleanup') + test.addCleanup(cleanup) + + def success(some_test): + self.assertEqual(some_test, test) + ordering.append('success') + + result = unittest.TestResult() + result.addSuccess = success + + test.run(result) + self.assertEqual(ordering, ['setUp', 'test', 'tearDown', 'cleanup', 'success']) + + blowUp = True + ordering = [] + test.run(result) + self.assertEqual(ordering, ['setUp', 'cleanup']) + ###################################################################### ## Main ###################################################################### @@ -3024,7 +3122,8 @@ def test_main(): test_support.run_unittest(Test_TestCase, Test_TestLoader, Test_TestSuite, Test_TestResult, Test_FunctionTestCase, - Test_TestSkipping, Test_Assertions, TestLongMessage) + Test_TestSkipping, Test_Assertions, TestLongMessage, + TestCleanUp) if __name__ == "__main__": test_main()