From e6756cb2d854da257c511f026624dba14d9dfc51 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 11 Sep 2018 14:51:31 -0700 Subject: [PATCH 1/4] Adds cleanUps for setUpClass and setUpModule. --- Doc/library/unittest.rst | 60 ++++++ Lib/unittest/__init__.py | 7 +- Lib/unittest/case.py | 30 +++ Lib/unittest/suite.py | 4 + Lib/unittest/test/test_runner.py | 306 ++++++++++++++++++++++++++++++- 5 files changed, 403 insertions(+), 4 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index adea431ed48b09..1d882ecab265fb 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1448,6 +1448,66 @@ Test cases .. versionadded:: 3.1 + .. method:: addClassCleanup(function, *args, **kwargs) + + Add a function to be called after :meth:`tearDownClass` to cleanup + resources used during the test class. Functions will be called in reverse + order to the order they are added (:abbr:`LIFO (last-in, first-out)`). + They are called with any arguments and keyword arguments passed into + :meth:`addClassCleanup` when they are added. + + If :meth:`setUpClass` fails, meaning that :meth:`tearDownClass` is not + called, then any cleanup functions added will still be called. + + .. versionadded:: 3.8 + + + .. method:: doClassCleanups() + + This method is called unconditionally after :meth:`tearDownClass`, or + after :meth:`setUpClass` if :meth:`setUpClass` raises an exception. + + It is responsible for calling all the cleanup functions added by + :meth:`addCleanupClass`. If you need cleanup functions to be called + *prior* to :meth:`tearDownClass` then you can call + :meth:`doCleanupsClass` yourself. + + :meth:`doCleanupsClass` pops methods off the stack of cleanup + functions one at a time, so it can be called at any time. + + .. versionadded:: 3.8 + + .. method:: addModuleCleanup(function, *args, **kwargs) + + Add a function to be called after :meth:`tearDownModule` to cleanup + resources used during the test class. Functions will be called in reverse + order to the order they are added (:abbr:`LIFO (last-in, first-out)`). + They are called with any arguments and keyword arguments passed into + :meth:`addModuleCleanup` when they are added. + + If :meth:`setUpModule` fails, meaning that :meth:`tearDownModule` is not + called, then any cleanup functions added will still be called. + + .. versionadded:: 3.8 + + + .. method:: doModuleCleanups() + + This method is called unconditionally after :meth:`tearDownModule`, or + after :meth:`setUpModule` if :meth:`setUpModule` raises an exception. + + It is responsible for calling all the cleanup functions added by + :meth:`addCleanupModule`. If you need cleanup functions to be called + *prior* to :meth:`tearDownModule` then you can call + :meth:`doCleanupsModule` yourself. + + :meth:`doCleanupsModule` pops methods off the stack of cleanup + functions one at a time, so it can be called at any time. + + .. versionadded:: 3.8 + + + .. class:: FunctionTestCase(testFunc, setUp=None, tearDown=None, description=None) diff --git a/Lib/unittest/__init__.py b/Lib/unittest/__init__.py index c55d563e0c38eb..5ff1bf37b16965 100644 --- a/Lib/unittest/__init__.py +++ b/Lib/unittest/__init__.py @@ -48,7 +48,8 @@ def testMultiply(self): 'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main', 'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless', 'expectedFailure', 'TextTestResult', 'installHandler', - 'registerResult', 'removeResult', 'removeHandler'] + 'registerResult', 'removeResult', 'removeHandler', + 'addModuleCleanup'] # Expose obsolete functions for backwards compatibility __all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases']) @@ -56,8 +57,8 @@ def testMultiply(self): __unittest = True from .result import TestResult -from .case import (TestCase, FunctionTestCase, SkipTest, skip, skipIf, - skipUnless, expectedFailure) +from .case import (addModuleCleanup, TestCase, FunctionTestCase, SkipTest, skip, + skipIf, skipUnless, expectedFailure) from .suite import BaseTestSuite, TestSuite from .loader import (TestLoader, defaultTestLoader, makeSuite, getTestCaseNames, findTestCases) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 1faa6b641f2d75..725f245115bb5d 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -84,6 +84,20 @@ def testPartExecutor(self, test_case, isTest=False): def _id(obj): return obj + +_module_cleanups = [] +def addModuleCleanup(function, *args, **kwargs): + """Same as addCleanup, except the cleanup items are called even if + setUpModule fails (unlike tearDownModule).""" + _module_cleanups.append((function, args, kwargs)) + +def doModuleCleanups(): + """Execute all module cleanup functions. Normally called for you after + tearDownModule.""" + while _module_cleanups: + function, args, kwargs = _module_cleanups.pop() + function(*args, **kwargs) + def skip(reason): """ Unconditionally skip a test. @@ -390,6 +404,8 @@ class TestCase(object): _classSetupFailed = False + _class_cleanups = [] + def __init__(self, methodName='runTest'): """Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does @@ -445,6 +461,12 @@ def addCleanup(self, function, *args, **kwargs): Cleanup items are called even if setUp fails (unlike tearDown).""" self._cleanups.append((function, args, kwargs)) + @classmethod + def addClassCleanup(cls, function, *args, **kwargs): + """Same as addCleanup, excet the cleanup items are called even if + setUpClass fails (unlike tearDownClass).""" + cls._class_cleanups.append((function, args, kwargs)) + def setUp(self): "Hook method for setting up the test fixture before exercising it." pass @@ -654,6 +676,14 @@ def doCleanups(self): # even though we no longer us it internally return outcome.success + @classmethod + def doClassCleanups(cls, test): + """Execute all class cleanup functions. Normally called for you after + tearDownClass.""" + while cls._class_cleanups: + function, args, kwargs = cls._class_cleanups.pop() + function(*args, **kwargs) + def __call__(self, *args, **kwds): return self.run(*args, **kwds) diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py index 353d4a17b96389..58411cb2415b6b 100644 --- a/Lib/unittest/suite.py +++ b/Lib/unittest/suite.py @@ -162,6 +162,7 @@ def _handleClassSetUp(self, test, result): try: setUpClass() except Exception as e: + currentClass.doClassCleanups(test) if isinstance(result, _DebugResult): raise currentClass._classSetupFailed = True @@ -199,6 +200,7 @@ def _handleModuleFixture(self, test, result): try: setUpModule() except Exception as e: + case.doModuleCleanups() if isinstance(result, _DebugResult): raise result._moduleSetUpFailed = True @@ -239,6 +241,7 @@ def _handleModuleTearDown(self, result): self._addClassOrModuleLevelException(result, e, errorName) finally: _call_if_exists(result, '_restoreStdout') + case.doModuleCleanups() def _tearDownPreviousClass(self, test, result): previousClass = getattr(result, '_previousTestClass', None) @@ -265,6 +268,7 @@ def _tearDownPreviousClass(self, test, result): self._addClassOrModuleLevelException(result, e, errorName) finally: _call_if_exists(result, '_restoreStdout') + previousClass.doClassCleanups(test) class _ErrorHolder(object): diff --git a/Lib/unittest/test/test_runner.py b/Lib/unittest/test/test_runner.py index 3c4005671f7339..8879a69ee6edb7 100644 --- a/Lib/unittest/test/test_runner.py +++ b/Lib/unittest/test/test_runner.py @@ -11,8 +11,33 @@ ResultWithNoStartTestRunStopTestRun) -class TestCleanUp(unittest.TestCase): +def resultFactory(*_): + return unittest.TestResult() + + +def getRunner(): + return unittest.TextTestRunner(resultclass=resultFactory, + stream=io.StringIO()) + + +def runTests(*cases): + suite = unittest.TestSuite() + for case in cases: + tests = unittest.defaultTestLoader.loadTestsFromTestCase(case) + suite.addTests(tests) + + runner = getRunner() + + # creating a nested suite exposes some potential bugs + realSuite = unittest.TestSuite() + realSuite.addTest(suite) + # adding empty suites to the end exposes potential bugs + suite.addTest(unittest.TestSuite()) + realSuite.addTest(unittest.TestSuite()) + return runner.run(realSuite) + +class TestCleanUp(unittest.TestCase): def testCleanUp(self): class TestableTest(unittest.TestCase): def testNothing(self): @@ -39,6 +64,7 @@ def cleanup2(*args, **kwargs): self.assertTrue(test.doCleanups()) self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))]) + def testCleanUpWithErrors(self): class TestableTest(unittest.TestCase): def testNothing(self): @@ -135,6 +161,284 @@ def cleanup2(): self.assertEqual(ordering, ['setUp', 'test', 'tearDown', 'cleanup1', 'cleanup2']) +class TestClassCleanup(unittest.TestCase): + def test_addClassCleanUp(self): + class TestableTest(unittest.TestCase): + def testNothing(self): + pass + test = TestableTest('testNothing') + self.assertEqual(test._class_cleanups, []) + class_cleanups = [] + + def class_cleanup1(*args, **kwargs): + class_cleanups.append((3, args, kwargs)) + + def class_cleanup2(*args, **kwargs): + class_cleanups.append((4, args, kwargs)) + + TestableTest.addClassCleanup(class_cleanup1, 1, 2, 3, + four='hello', five='goodbye') + TestableTest.addClassCleanup(class_cleanup2) + + self.assertEqual(test._class_cleanups, + [(class_cleanup1, (1, 2, 3), + dict(four='hello', five='goodbye')), + (class_cleanup2, (), {})]) + + TestableTest.doClassCleanups(test) + self.assertEqual(class_cleanups, [(4, (), {}), (3, (1, 2, 3), + dict(four='hello', five='goodbye'))]) + + def test_run_class_cleanUp(self): + ordering = [] + blowUp = True + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + cls.addClassCleanup(cleanup) + if blowUp: + raise Exception() + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + def cleanup(): + ordering.append('cleanup') + + runTests(TestableTest) + self.assertEqual(ordering, ['setUpClass', 'cleanup']) + + ordering = [] + blowUp = False + runTests(TestableTest) + self.assertEqual(ordering, + ['setUpClass', 'test', 'tearDownClass', 'cleanup']) + + def test_debug_class_executes_cleanUp(self): + ordering = [] + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + cls.addClassCleanup(cleanup) + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + def cleanup(): + ordering.append('cleanup') + + suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest) + suite.debug() + self.assertEqual(ordering, + ['setUpClass', 'test', 'tearDownClass', 'cleanup']) + + def test_class_CleanUp_with_errors(self): + class TestableTest(unittest.TestCase): + def testNothing(self): + pass + + test = TestableTest('testNothing') + + def cleanup1(): + raise Exception('cleanup1') + + TestableTest.addClassCleanup(cleanup1) + with self.assertRaises(Exception) as e: + TestableTest.doClassCleanups(test) + self.assertEquals(e, 'cleanup1') + + +class TestModuleCleanUp(unittest.TestCase): + def test_addModuleCleanUp(self): + module_cleanups = [] + + def module_cleanup1(*args, **kwargs): + module_cleanups.append((3, args, kwargs)) + + def module_cleanup2(*args, **kwargs): + module_cleanups.append((4, args, kwargs)) + + class Module(object): + unittest.addModuleCleanup(module_cleanup1, 1, 2, 3, + four='hello', five='goodbye') + unittest.addModuleCleanup(module_cleanup2) + + self.assertEqual(unittest.case._module_cleanups, + [(module_cleanup1, (1, 2, 3), + dict(four='hello', five='goodbye')), + (module_cleanup2, (), {})]) + + unittest.case.doModuleCleanups() + self.assertEqual(module_cleanups, [(4, (), {}), (3, (1, 2, 3), + dict(four='hello', five='goodbye'))]) + self.assertEqual(unittest.case._module_cleanups, []) + + def test_run_module_cleanUp(self): + ordering = [] + blowUp = True + + class Module(object): + @staticmethod + def setUpModule(): + ordering.append('setUpModule') + unittest.addModuleCleanup(cleanup) + if blowUp: + raise Exception() + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule') + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + def cleanup(): + ordering.append('cleanup') + + TestableTest.__module__ = 'Module' + sys.modules['Module'] = Module + runTests(TestableTest) + self.assertEqual(ordering, ['setUpModule', 'cleanup']) + + ordering = [] + blowUp = False + runTests(TestableTest) + self.assertEqual(ordering, + ['setUpModule', 'setUpClass', 'test', 'tearDownClass', + 'tearDownModule', 'cleanup']) + self.assertEqual(unittest.case._module_cleanups, []) + + def test_run_multiple_module_cleanUp(self): + ordering = [] + blowUp = True + + class Module1(object): + @staticmethod + def setUpModule(): + ordering.append('setUpModule') + unittest.addModuleCleanup(cleanup) + if blowUp: + raise Exception() + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule') + + class Module2(object): + @staticmethod + def setUpModule(): + ordering.append('setUpModule2') + unittest.addModuleCleanup(cleanup2) + if blowUp: + raise Exception() + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule2') + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + class TestableTest2(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass2') + def testNothing(self): + ordering.append('test2') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass2') + + def cleanup(): + ordering.append('cleanup') + + def cleanup2(): + ordering.append('cleanup2') + + TestableTest.__module__ = 'Module1' + sys.modules['Module1'] = Module1 + TestableTest2.__module__ = 'Module2' + sys.modules['Module2'] = Module2 + runTests(TestableTest) + runTests(TestableTest2) + self.assertEqual(ordering, ['setUpModule', 'cleanup', + 'setUpModule2', 'cleanup2']) + + ordering = [] + blowUp = False + runTests(TestableTest, TestableTest2) + self.assertEqual(ordering, + ['setUpModule', 'setUpClass', 'test', 'tearDownClass', + 'tearDownModule', 'cleanup', 'setUpModule2', + 'setUpClass2', 'test2', 'tearDownClass2', + 'tearDownModule2', 'cleanup2']) + self.assertEqual(unittest.case._module_cleanups, []) + + def test_debug_module_executes_cleanUp(self): + ordering = [] + + class Module(object): + @staticmethod + def setUpModule(): + ordering.append('setUpModule') + unittest.addModuleCleanup(cleanup) + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule') + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + def cleanup(): + ordering.append('cleanup') + + TestableTest.__module__ = 'Module' + sys.modules['Module'] = Module + suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest) + suite.debug() + self.assertEqual(ordering, + ['setUpModule', 'setUpClass', 'test', 'tearDownClass', + 'tearDownModule', 'cleanup']) + self.assertEqual(unittest.case._module_cleanups, []) + + def test_module_CleanUp_with_errors(self): + def cleanup1(): + raise Exception('cleanup1') + + class Module(object): + unittest.addModuleCleanup(cleanup1) + + with self.assertRaises(Exception) as e: + unittest.case.doModuleCleanups() + self.assertEquals(e, 'cleanup1') + self.assertEqual(unittest.case._module_cleanups, []) + class Test_TextTestRunner(unittest.TestCase): """Tests for TextTestRunner.""" From 5332a2c7ca784759036b95366a02bca6eb59acc8 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 11 Sep 2018 15:07:24 -0700 Subject: [PATCH 2/4] Adds the NEWS entry. --- .../next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst diff --git a/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst b/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst new file mode 100644 index 00000000000000..e14eb0ba6b1c86 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst @@ -0,0 +1,2 @@ +Add ``addModuleCleanup()`` and ``addClassCleanup()`` to unittest to support +cleanups for setUpClass and setUpModule. Patch by Lisa Roach. From 7939f145d1d9048f9b4a2390208bcf3274ca084b Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 12 Sep 2018 14:08:00 -0700 Subject: [PATCH 3/4] Adds exception handling for add*CleanUps. --- Lib/unittest/case.py | 24 +- Lib/unittest/suite.py | 55 +++- Lib/unittest/test/test_runner.py | 414 ++++++++++++++++++++++++++----- 3 files changed, 412 insertions(+), 81 deletions(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 725f245115bb5d..8ce5b245b68f5c 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -86,6 +86,7 @@ def _id(obj): _module_cleanups = [] + def addModuleCleanup(function, *args, **kwargs): """Same as addCleanup, except the cleanup items are called even if setUpModule fails (unlike tearDownModule).""" @@ -94,9 +95,15 @@ def addModuleCleanup(function, *args, **kwargs): def doModuleCleanups(): """Execute all module cleanup functions. Normally called for you after tearDownModule.""" + exceptions = [] while _module_cleanups: function, args, kwargs = _module_cleanups.pop() - function(*args, **kwargs) + try: + function(*args, **kwargs) + except Exception as exc: + exceptions.append(exc) + if exceptions: + raise exceptions[0] def skip(reason): """ @@ -403,7 +410,6 @@ class TestCase(object): # Attribute used by TestSuite for classSetUp _classSetupFailed = False - _class_cleanups = [] def __init__(self, methodName='runTest'): @@ -463,7 +469,7 @@ def addCleanup(self, function, *args, **kwargs): @classmethod def addClassCleanup(cls, function, *args, **kwargs): - """Same as addCleanup, excet the cleanup items are called even if + """Same as addCleanup, except the cleanup items are called even if setUpClass fails (unlike tearDownClass).""" cls._class_cleanups.append((function, args, kwargs)) @@ -673,16 +679,22 @@ def doCleanups(self): function(*args, **kwargs) # return this for backwards compatibility - # even though we no longer us it internally + # even though we no longer use it internally return outcome.success @classmethod - def doClassCleanups(cls, test): + def doClassCleanups(cls): """Execute all class cleanup functions. Normally called for you after tearDownClass.""" + exceptions = [] while cls._class_cleanups: function, args, kwargs = cls._class_cleanups.pop() - function(*args, **kwargs) + try: + function(*args, **kwargs) + except Exception as exc: + exceptions.append(exc) + if exceptions: + raise exceptions[0] def __call__(self, *args, **kwds): return self.run(*args, **kwds) diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py index 58411cb2415b6b..33a5e018732e31 100644 --- a/Lib/unittest/suite.py +++ b/Lib/unittest/suite.py @@ -162,13 +162,20 @@ def _handleClassSetUp(self, test, result): try: setUpClass() except Exception as e: - currentClass.doClassCleanups(test) + try: + currentClass.doClassCleanups() + except Exception as exc: + className = util.strclass(currentClass) + self._createClassOrModuleLevelException(result, exc, + 'setUpClass', + className) if isinstance(result, _DebugResult): raise currentClass._classSetupFailed = True className = util.strclass(currentClass) - errorName = 'setUpClass (%s)' % className - self._addClassOrModuleLevelException(result, e, errorName) + self._createClassOrModuleLevelException(result, e, + 'setUpClass', + className) finally: _call_if_exists(result, '_restoreStdout') @@ -200,15 +207,25 @@ def _handleModuleFixture(self, test, result): try: setUpModule() except Exception as e: - case.doModuleCleanups() + try: + case.doModuleCleanups() + except Exception as exc: + self._createClassOrModuleLevelException(result, exc, + 'setUpModule', + currentModule) if isinstance(result, _DebugResult): raise result._moduleSetUpFailed = True - errorName = 'setUpModule (%s)' % currentModule - self._addClassOrModuleLevelException(result, e, errorName) + self._createClassOrModuleLevelException(result, e, + 'setUpModule', + currentModule) finally: _call_if_exists(result, '_restoreStdout') + def _createClassOrModuleLevelException(self, result, exc, parent, name): + errorName = '{} ({})'.format(parent, name) + self._addClassOrModuleLevelException(result, exc, errorName) + def _addClassOrModuleLevelException(self, result, exception, errorName): error = _ErrorHolder(errorName) addSkip = getattr(result, 'addSkip', None) @@ -237,11 +254,17 @@ def _handleModuleTearDown(self, result): except Exception as e: if isinstance(result, _DebugResult): raise - errorName = 'tearDownModule (%s)' % previousModule - self._addClassOrModuleLevelException(result, e, errorName) + self._createClassOrModuleLevelException(result, e, + 'tearDownModule', + previousModule) finally: _call_if_exists(result, '_restoreStdout') - case.doModuleCleanups() + try: + case.doModuleCleanups() + except Exception as e: + self._createClassOrModuleLevelException(result, e, + 'tearDownModule', + previousModule) def _tearDownPreviousClass(self, test, result): previousClass = getattr(result, '_previousTestClass', None) @@ -264,12 +287,18 @@ def _tearDownPreviousClass(self, test, result): if isinstance(result, _DebugResult): raise className = util.strclass(previousClass) - errorName = 'tearDownClass (%s)' % className - self._addClassOrModuleLevelException(result, e, errorName) + self._createClassOrModuleLevelException(result, e, + 'tearDownClass', + className) finally: _call_if_exists(result, '_restoreStdout') - previousClass.doClassCleanups(test) - + try: + previousClass.doClassCleanups() + except Exception as e: + className = util.strclass(previousClass) + self._createClassOrModuleLevelException(result, e, + 'tearDownClass', + className) class _ErrorHolder(object): """ diff --git a/Lib/unittest/test/test_runner.py b/Lib/unittest/test/test_runner.py index 8879a69ee6edb7..909f45d9eb4d75 100644 --- a/Lib/unittest/test/test_runner.py +++ b/Lib/unittest/test/test_runner.py @@ -37,6 +37,14 @@ def runTests(*cases): return runner.run(realSuite) +def cleanup(ordering, blowUp=False): + if not blowUp: + ordering.append('cleanup_good') + else: + ordering.append('cleanup_exc') + raise Exception('CleanUpExc') + + class TestCleanUp(unittest.TestCase): def testCleanUp(self): class TestableTest(unittest.TestCase): @@ -64,7 +72,6 @@ def cleanup2(*args, **kwargs): self.assertTrue(test.doCleanups()) self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))]) - def testCleanUpWithErrors(self): class TestableTest(unittest.TestCase): def testNothing(self): @@ -73,10 +80,10 @@ def testNothing(self): test = TestableTest('testNothing') outcome = test._outcome = _Outcome() - exc1 = Exception('foo') + CleanUpExc = Exception('foo') exc2 = Exception('bar') def cleanup1(): - raise exc1 + raise CleanUpExc def cleanup2(): raise exc2 @@ -89,7 +96,7 @@ def cleanup2(): ((_, (Type1, instance1, _)), (_, (Type2, instance2, _))) = reversed(outcome.errors) - self.assertEqual((Type1, instance1), (Exception, exc1)) + self.assertEqual((Type1, instance1), (Exception, CleanUpExc)) self.assertEqual((Type2, instance2), (Exception, exc2)) def testCleanupInRun(self): @@ -185,7 +192,7 @@ def class_cleanup2(*args, **kwargs): dict(four='hello', five='goodbye')), (class_cleanup2, (), {})]) - TestableTest.doClassCleanups(test) + TestableTest.doClassCleanups() self.assertEqual(class_cleanups, [(4, (), {}), (3, (1, 2, 3), dict(four='hello', five='goodbye'))]) @@ -197,7 +204,7 @@ class TestableTest(unittest.TestCase): @classmethod def setUpClass(cls): ordering.append('setUpClass') - cls.addClassCleanup(cleanup) + cls.addClassCleanup(cleanup, ordering) if blowUp: raise Exception() def testNothing(self): @@ -206,58 +213,141 @@ def testNothing(self): def tearDownClass(cls): ordering.append('tearDownClass') - def cleanup(): - ordering.append('cleanup') - runTests(TestableTest) - self.assertEqual(ordering, ['setUpClass', 'cleanup']) + self.assertEqual(ordering, ['setUpClass', 'cleanup_good']) ordering = [] blowUp = False runTests(TestableTest) self.assertEqual(ordering, - ['setUpClass', 'test', 'tearDownClass', 'cleanup']) + ['setUpClass', 'test', 'tearDownClass', 'cleanup_good']) - def test_debug_class_executes_cleanUp(self): + def test_debug_executes_classCleanUp(self): ordering = [] class TestableTest(unittest.TestCase): @classmethod def setUpClass(cls): ordering.append('setUpClass') - cls.addClassCleanup(cleanup) + cls.addClassCleanup(cleanup, ordering) def testNothing(self): ordering.append('test') @classmethod def tearDownClass(cls): ordering.append('tearDownClass') - def cleanup(): - ordering.append('cleanup') - suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest) suite.debug() self.assertEqual(ordering, - ['setUpClass', 'test', 'tearDownClass', 'cleanup']) + ['setUpClass', 'test', 'tearDownClass', 'cleanup_good']) - def test_class_CleanUp_with_errors(self): + def test_doClassCleanups_with_errors_addClassCleanUp(self): class TestableTest(unittest.TestCase): def testNothing(self): pass - test = TestableTest('testNothing') - def cleanup1(): raise Exception('cleanup1') + def cleanup2(): + raise Exception('cleanup2') + TestableTest.addClassCleanup(cleanup1) + TestableTest.addClassCleanup(cleanup2) with self.assertRaises(Exception) as e: - TestableTest.doClassCleanups(test) + TestableTest.doClassCleanups() self.assertEquals(e, 'cleanup1') + def test_with_errors_addCleanUp(self): + ordering = [] + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + cls.addClassCleanup(cleanup, ordering) + def setUp(self): + ordering.append('setUp') + self.addCleanup(cleanup, ordering, blowUp=True) + def testNothing(self): + pass + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + # Update this + with self.assertRaises(Exception) as e: + runTests(TestableTest) + self.assertEqual(e, 'cleanup2') + # Test cleanUpClass items still get called. + self.assertEqual(ordering, + ['setUpClass', 'setUp', 'cleanup_exc', + 'tearDownClass', 'cleanup_good']) + + def test_run_with_errors_addClassCleanUp(self): + ordering = [] + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + cls.addClassCleanup(cleanup, ordering, blowUp=True) + def setUp(self): + ordering.append('setUp') + self.addCleanup(cleanup, ordering) + def testNothing(self): + pass + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + with self.assertRaises(Exception) as e: + runTests(TestableTest) + self.assertEqual(e, 'cleanup2') + self.assertEqual(ordering, + ['setUpClass', 'setUp', 'cleanup_good', + 'tearDownClass', 'cleanup_exc']) + + def test_with_errors_in_addClassCleanup_and_setUps(self): + ordering = [] + class_blow_up = False + method_blow_up = False + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + cls.addClassCleanup(cleanup, ordering, blowUp=True) + if class_blow_up: + raise Exception('ClassExc') + def setUp(self): + ordering.append('setUp') + if method_blow_up: + raise Exception('MethodExc') + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(ordering, + ['setUpClass', 'setUp', 'test', + 'tearDownClass', 'cleanup_exc']) + ordering = [] + class_blow_up = True + method_blow_up = False + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(result.errors[1][1].splitlines()[-1], + 'Exception: ClassExc') + self.assertEqual(ordering, + ['setUpClass', 'cleanup_exc']) + class TestModuleCleanUp(unittest.TestCase): - def test_addModuleCleanUp(self): + def test_add_and_do_ModuleCleanup(self): module_cleanups = [] def module_cleanup1(*args, **kwargs): @@ -281,17 +371,40 @@ class Module(object): dict(four='hello', five='goodbye'))]) self.assertEqual(unittest.case._module_cleanups, []) + def test_doModuleCleanup_with_errors_in_addModuleCleanup(self): + module_cleanups = [] + + def module_cleanup1(*args, **kwargs): + module_cleanups.append((3, args, kwargs)) + + def module_cleanup2(*args, **kwargs): + raise Exception('CleanUpExc') + + class Module(object): + unittest.addModuleCleanup(module_cleanup1, 1, 2, 3, + four='hello', five='goodbye') + unittest.addModuleCleanup(module_cleanup2) + + self.assertEqual(unittest.case._module_cleanups, + [(module_cleanup1, (1, 2, 3), + dict(four='hello', five='goodbye')), + (module_cleanup2, (), {})]) + with self.assertRaises(Exception) as e: + unittest.case.doModuleCleanups() + self.assertEqual(e, 'CleanUpExc') + + self.assertEqual(unittest.case._module_cleanups, []) + def test_run_module_cleanUp(self): - ordering = [] blowUp = True - + ordering = [] class Module(object): @staticmethod def setUpModule(): ordering.append('setUpModule') - unittest.addModuleCleanup(cleanup) + unittest.addModuleCleanup(cleanup, ordering) if blowUp: - raise Exception() + raise Exception('setUpModule Exc') @staticmethod def tearDownModule(): ordering.append('tearDownModule') @@ -306,31 +419,30 @@ def testNothing(self): def tearDownClass(cls): ordering.append('tearDownClass') - def cleanup(): - ordering.append('cleanup') - TestableTest.__module__ = 'Module' sys.modules['Module'] = Module - runTests(TestableTest) - self.assertEqual(ordering, ['setUpModule', 'cleanup']) + result = runTests(TestableTest) + self.assertEqual(ordering, ['setUpModule', 'cleanup_good']) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: setUpModule Exc') ordering = [] blowUp = False runTests(TestableTest) self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test', 'tearDownClass', - 'tearDownModule', 'cleanup']) + 'tearDownModule', 'cleanup_good']) self.assertEqual(unittest.case._module_cleanups, []) def test_run_multiple_module_cleanUp(self): - ordering = [] blowUp = True - + blowUp2 = False + ordering = [] class Module1(object): @staticmethod def setUpModule(): ordering.append('setUpModule') - unittest.addModuleCleanup(cleanup) + unittest.addModuleCleanup(cleanup, ordering) if blowUp: raise Exception() @staticmethod @@ -341,8 +453,8 @@ class Module2(object): @staticmethod def setUpModule(): ordering.append('setUpModule2') - unittest.addModuleCleanup(cleanup2) - if blowUp: + unittest.addModuleCleanup(cleanup, ordering) + if blowUp2: raise Exception() @staticmethod def tearDownModule(): @@ -368,39 +480,42 @@ def testNothing(self): def tearDownClass(cls): ordering.append('tearDownClass2') - def cleanup(): - ordering.append('cleanup') - - def cleanup2(): - ordering.append('cleanup2') - TestableTest.__module__ = 'Module1' sys.modules['Module1'] = Module1 TestableTest2.__module__ = 'Module2' sys.modules['Module2'] = Module2 - runTests(TestableTest) - runTests(TestableTest2) - self.assertEqual(ordering, ['setUpModule', 'cleanup', - 'setUpModule2', 'cleanup2']) + runTests(TestableTest, TestableTest2) + self.assertEqual(ordering, ['setUpModule', 'cleanup_good', + 'setUpModule2', 'setUpClass2', 'test2', + 'tearDownClass2', 'tearDownModule2', + 'cleanup_good']) + ordering = [] + blowUp = False + blowUp2 = True + runTests(TestableTest, TestableTest2) + self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test', + 'tearDownClass', 'tearDownModule', + 'cleanup_good', 'setUpModule2', + 'cleanup_good']) ordering = [] blowUp = False + blowUp2 = False runTests(TestableTest, TestableTest2) self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test', 'tearDownClass', - 'tearDownModule', 'cleanup', 'setUpModule2', + 'tearDownModule', 'cleanup_good', 'setUpModule2', 'setUpClass2', 'test2', 'tearDownClass2', - 'tearDownModule2', 'cleanup2']) + 'tearDownModule2', 'cleanup_good']) self.assertEqual(unittest.case._module_cleanups, []) def test_debug_module_executes_cleanUp(self): ordering = [] - class Module(object): @staticmethod def setUpModule(): ordering.append('setUpModule') - unittest.addModuleCleanup(cleanup) + unittest.addModuleCleanup(cleanup, ordering) @staticmethod def tearDownModule(): ordering.append('tearDownModule') @@ -415,29 +530,204 @@ def testNothing(self): def tearDownClass(cls): ordering.append('tearDownClass') - def cleanup(): - ordering.append('cleanup') - TestableTest.__module__ = 'Module' sys.modules['Module'] = Module suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest) suite.debug() self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test', 'tearDownClass', - 'tearDownModule', 'cleanup']) + 'tearDownModule', 'cleanup_good']) self.assertEqual(unittest.case._module_cleanups, []) - def test_module_CleanUp_with_errors(self): + def test_with_errors_in_addClassCleanup(self): + ordering = [] + + class Module(object): + @staticmethod + def setUpModule(): + ordering.append('setUpModule') + unittest.addModuleCleanup(cleanup, ordering) + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule') + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + cls.addClassCleanup(cleanup, ordering, blowUp=True) + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + TestableTest.__module__ = 'Module' + sys.modules['Module'] = Module + + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(ordering, + ['setUpModule', 'setUpClass', 'test', 'tearDownClass', + 'cleanup_exc', 'tearDownModule', 'cleanup_good']) + + def test_with_errors_in_addCleanup(self): + ordering = [] + class Module(object): + @staticmethod + def setUpModule(): + ordering.append('setUpModule') + unittest.addModuleCleanup(cleanup, ordering) + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule') + + class TestableTest(unittest.TestCase): + def setUp(self): + ordering.append('setUp') + self.addCleanup(cleanup, ordering, blowUp=True) + def testNothing(self): + ordering.append('test') + def tearDown(self): + ordering.append('tearDown') + + TestableTest.__module__ = 'Module' + sys.modules['Module'] = Module + + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(ordering, + ['setUpModule', 'setUp', 'test', 'tearDown', + 'cleanup_exc', 'tearDownModule', 'cleanup_good']) + + def test_with_errors_in_addModuleCleanup_and_setUps(self): + ordering = [] + module_blow_up = False + class_blow_up = False + method_blow_up = False + class Module(object): + @staticmethod + def setUpModule(): + ordering.append('setUpModule') + unittest.addModuleCleanup(cleanup, ordering, blowUp=True) + if module_blow_up: + raise Exception('ModuleExc') + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule') + + class TestableTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + ordering.append('setUpClass') + if class_blow_up: + raise Exception('ClassExc') + def setUp(self): + ordering.append('setUp') + if method_blow_up: + raise Exception('MethodExc') + def testNothing(self): + ordering.append('test') + @classmethod + def tearDownClass(cls): + ordering.append('tearDownClass') + + TestableTest.__module__ = 'Module' + sys.modules['Module'] = Module + + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(ordering, + ['setUpModule', 'setUpClass', 'setUp', 'test', + 'tearDownClass', 'tearDownModule', + 'cleanup_exc']) + + ordering = [] + module_blow_up = True + class_blow_up = False + method_blow_up = False + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(result.errors[1][1].splitlines()[-1], + 'Exception: ModuleExc') + self.assertEqual(ordering, ['setUpModule', 'cleanup_exc']) + + ordering = [] + module_blow_up = False + class_blow_up = True + method_blow_up = False + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: ClassExc') + self.assertEqual(result.errors[1][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(ordering, ['setUpModule', 'setUpClass', + 'tearDownModule', 'cleanup_exc']) + + ordering = [] + module_blow_up = False + class_blow_up = False + method_blow_up = True + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: MethodExc') + self.assertEqual(result.errors[1][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'setUp', + 'tearDownClass', 'tearDownModule', + 'cleanup_exc']) + + def test_module_cleanUp_with_multiple_classes(self): + ordering =[] def cleanup1(): - raise Exception('cleanup1') + ordering.append('cleanup1') + + def cleanup2(): + ordering.append('cleanup2') + + def cleanup3(): + ordering.append('cleanup3') class Module(object): - unittest.addModuleCleanup(cleanup1) + @staticmethod + def setUpModule(): + ordering.append('setUpModule') + unittest.addModuleCleanup(cleanup1) + @staticmethod + def tearDownModule(): + ordering.append('tearDownModule') + + class TestableTest(unittest.TestCase): + def setUp(self): + ordering.append('setUp') + self.addCleanup(cleanup2) + def testNothing(self): + ordering.append('test') + def tearDown(self): + ordering.append('tearDown') + + class OtherTestableTest(unittest.TestCase): + def setUp(self): + ordering.append('setUp2') + self.addCleanup(cleanup3) + def testNothing(self): + ordering.append('test2') + def tearDown(self): + ordering.append('tearDown2') + + TestableTest.__module__ = 'Module' + OtherTestableTest.__module__ = 'Module' + sys.modules['Module'] = Module + runTests(TestableTest, OtherTestableTest) + self.assertEqual(ordering, + ['setUpModule', 'setUp', 'test', 'tearDown', + 'cleanup2', 'setUp2', 'test2', 'tearDown2', + 'cleanup3', 'tearDownModule', 'cleanup1']) - with self.assertRaises(Exception) as e: - unittest.case.doModuleCleanups() - self.assertEquals(e, 'cleanup1') - self.assertEqual(unittest.case._module_cleanups, []) class Test_TextTestRunner(unittest.TestCase): """Tests for TextTestRunner.""" From 162b6b23ad1e010aafddb3499826651c4c53ed41 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Sun, 30 Sep 2018 22:19:31 -0700 Subject: [PATCH 4/4] Updates class method error handling and docs. --- Doc/library/unittest.rst | 63 ++++++++++--------- Doc/whatsnew/3.8.rst | 9 +++ Lib/unittest/case.py | 12 ++-- Lib/unittest/suite.py | 45 +++++++------ Lib/unittest/test/test_runner.py | 50 ++++++++------- .../2018-09-11-10-51-16.bpo-24412.i-F_E5.rst | 6 +- 6 files changed, 109 insertions(+), 76 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 1d882ecab265fb..2117ef1435fd0c 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1448,7 +1448,7 @@ Test cases .. versionadded:: 3.1 - .. method:: addClassCleanup(function, *args, **kwargs) + .. classmethod:: addClassCleanup(function, *args, **kwargs) Add a function to be called after :meth:`tearDownClass` to cleanup resources used during the test class. Functions will be called in reverse @@ -1462,7 +1462,7 @@ Test cases .. versionadded:: 3.8 - .. method:: doClassCleanups() + .. classmethod:: doClassCleanups() This method is called unconditionally after :meth:`tearDownClass`, or after :meth:`setUpClass` if :meth:`setUpClass` raises an exception. @@ -1477,34 +1477,7 @@ Test cases .. versionadded:: 3.8 - .. method:: addModuleCleanup(function, *args, **kwargs) - Add a function to be called after :meth:`tearDownModule` to cleanup - resources used during the test class. Functions will be called in reverse - order to the order they are added (:abbr:`LIFO (last-in, first-out)`). - They are called with any arguments and keyword arguments passed into - :meth:`addModuleCleanup` when they are added. - - If :meth:`setUpModule` fails, meaning that :meth:`tearDownModule` is not - called, then any cleanup functions added will still be called. - - .. versionadded:: 3.8 - - - .. method:: doModuleCleanups() - - This method is called unconditionally after :meth:`tearDownModule`, or - after :meth:`setUpModule` if :meth:`setUpModule` raises an exception. - - It is responsible for calling all the cleanup functions added by - :meth:`addCleanupModule`. If you need cleanup functions to be called - *prior* to :meth:`tearDownModule` then you can call - :meth:`doCleanupsModule` yourself. - - :meth:`doCleanupsModule` pops methods off the stack of cleanup - functions one at a time, so it can be called at any time. - - .. versionadded:: 3.8 @@ -2328,6 +2301,38 @@ module will be run and the ``tearDownModule`` will not be run. If the exception :exc:`SkipTest` exception then the module will be reported as having been skipped instead of as an error. +To add cleanup code that must be run even in the case of an exception, use +``addModuleCleanup``: + + +.. function:: addModuleCleanup(function, *args, **kwargs) + + Add a function to be called after :func:`tearDownModule` to cleanup + resources used during the test class. Functions will be called in reverse + order to the order they are added (:abbr:`LIFO (last-in, first-out)`). + They are called with any arguments and keyword arguments passed into + :meth:`addModuleCleanup` when they are added. + + If :meth:`setUpModule` fails, meaning that :func:`tearDownModule` is not + called, then any cleanup functions added will still be called. + + .. versionadded:: 3.8 + + +.. function:: doModuleCleanups() + + This function is called unconditionally after :func:`tearDownModule`, or + after :func:`setUpModule` if :func:`setUpModule` raises an exception. + + It is responsible for calling all the cleanup functions added by + :func:`addCleanupModule`. If you need cleanup functions to be called + *prior* to :func:`tearDownModule` then you can call + :func:`doModuleCleanups` yourself. + + :func:`doModuleCleanups` pops methods off the stack of cleanup + functions one at a time, so it can be called at any time. + + .. versionadded:: 3.8 Signal Handling --------------- diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 259dcf65f2e048..182885bc4a6b26 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -104,6 +104,15 @@ New Modules Improved Modules ================ +unittest +-------- +* Added :func:`~unittest.addModuleCleanup()` and + :meth:`~unittest.TestCase.addClassCleanup()` to unittest to support + cleanups for :func:`~unittest.setUpModule()` and + :meth:`~unittest.TestCase.setUpClass()`. + (Contributed by Lisa Roach in :issue:`24412`.) + + Optimizations ============= diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 8ce5b245b68f5c..d4281826ba9bb6 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -86,12 +86,12 @@ def _id(obj): _module_cleanups = [] - def addModuleCleanup(function, *args, **kwargs): """Same as addCleanup, except the cleanup items are called even if setUpModule fails (unlike tearDownModule).""" _module_cleanups.append((function, args, kwargs)) + def doModuleCleanups(): """Execute all module cleanup functions. Normally called for you after tearDownModule.""" @@ -103,8 +103,11 @@ def doModuleCleanups(): except Exception as exc: exceptions.append(exc) if exceptions: + # Swallows all but first exception. If a multi-exception handler + # gets written we should use that here instead. raise exceptions[0] + def skip(reason): """ Unconditionally skip a test. @@ -410,6 +413,7 @@ class TestCase(object): # Attribute used by TestSuite for classSetUp _classSetupFailed = False + _class_cleanups = [] def __init__(self, methodName='runTest'): @@ -686,15 +690,13 @@ def doCleanups(self): def doClassCleanups(cls): """Execute all class cleanup functions. Normally called for you after tearDownClass.""" - exceptions = [] + cls.tearDown_exceptions = [] while cls._class_cleanups: function, args, kwargs = cls._class_cleanups.pop() try: function(*args, **kwargs) except Exception as exc: - exceptions.append(exc) - if exceptions: - raise exceptions[0] + cls.tearDown_exceptions.append(sys.exc_info()) def __call__(self, *args, **kwds): return self.run(*args, **kwds) diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py index 33a5e018732e31..41993f9cf69afc 100644 --- a/Lib/unittest/suite.py +++ b/Lib/unittest/suite.py @@ -162,13 +162,6 @@ def _handleClassSetUp(self, test, result): try: setUpClass() except Exception as e: - try: - currentClass.doClassCleanups() - except Exception as exc: - className = util.strclass(currentClass) - self._createClassOrModuleLevelException(result, exc, - 'setUpClass', - className) if isinstance(result, _DebugResult): raise currentClass._classSetupFailed = True @@ -178,6 +171,13 @@ def _handleClassSetUp(self, test, result): className) finally: _call_if_exists(result, '_restoreStdout') + if currentClass._classSetupFailed is True: + currentClass.doClassCleanups() + if len(currentClass.tearDown_exceptions) > 0: + for exc in currentClass.tearDown_exceptions: + self._createClassOrModuleLevelException( + result, exc[1], 'setUpClass', className, + info=exc) def _get_previous_module(self, result): previousModule = None @@ -222,17 +222,22 @@ def _handleModuleFixture(self, test, result): finally: _call_if_exists(result, '_restoreStdout') - def _createClassOrModuleLevelException(self, result, exc, parent, name): - errorName = '{} ({})'.format(parent, name) - self._addClassOrModuleLevelException(result, exc, errorName) + def _createClassOrModuleLevelException(self, result, exc, method_name, + parent, info=None): + errorName = f'{method_name} ({parent})' + self._addClassOrModuleLevelException(result, exc, errorName, info) - def _addClassOrModuleLevelException(self, result, exception, errorName): + def _addClassOrModuleLevelException(self, result, exception, errorName, + info=None): error = _ErrorHolder(errorName) addSkip = getattr(result, 'addSkip', None) if addSkip is not None and isinstance(exception, case.SkipTest): addSkip(error, str(exception)) else: - result.addError(error, sys.exc_info()) + if not info: + result.addError(error, sys.exc_info()) + else: + result.addError(error, info) def _handleModuleTearDown(self, result): previousModule = self._get_previous_module(result) @@ -292,13 +297,15 @@ def _tearDownPreviousClass(self, test, result): className) finally: _call_if_exists(result, '_restoreStdout') - try: - previousClass.doClassCleanups() - except Exception as e: - className = util.strclass(previousClass) - self._createClassOrModuleLevelException(result, e, - 'tearDownClass', - className) + previousClass.doClassCleanups() + if len(previousClass.tearDown_exceptions) > 0: + for exc in previousClass.tearDown_exceptions: + className = util.strclass(previousClass) + self._createClassOrModuleLevelException(result, exc[1], + 'tearDownClass', + className, + info=exc) + class _ErrorHolder(object): """ diff --git a/Lib/unittest/test/test_runner.py b/Lib/unittest/test/test_runner.py index 909f45d9eb4d75..6f89f77ff77824 100644 --- a/Lib/unittest/test/test_runner.py +++ b/Lib/unittest/test/test_runner.py @@ -274,11 +274,9 @@ def testNothing(self): def tearDownClass(cls): ordering.append('tearDownClass') - # Update this - with self.assertRaises(Exception) as e: - runTests(TestableTest) - self.assertEqual(e, 'cleanup2') - # Test cleanUpClass items still get called. + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') self.assertEqual(ordering, ['setUpClass', 'setUp', 'cleanup_exc', 'tearDownClass', 'cleanup_good']) @@ -294,16 +292,16 @@ def setUp(self): ordering.append('setUp') self.addCleanup(cleanup, ordering) def testNothing(self): - pass + ordering.append('test') @classmethod def tearDownClass(cls): ordering.append('tearDownClass') - with self.assertRaises(Exception) as e: - runTests(TestableTest) - self.assertEqual(e, 'cleanup2') + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: CleanUpExc') self.assertEqual(ordering, - ['setUpClass', 'setUp', 'cleanup_good', + ['setUpClass', 'setUp', 'test', 'cleanup_good', 'tearDownClass', 'cleanup_exc']) def test_with_errors_in_addClassCleanup_and_setUps(self): @@ -339,12 +337,24 @@ def tearDownClass(cls): method_blow_up = False result = runTests(TestableTest) self.assertEqual(result.errors[0][1].splitlines()[-1], - 'Exception: CleanUpExc') - self.assertEqual(result.errors[1][1].splitlines()[-1], 'Exception: ClassExc') + self.assertEqual(result.errors[1][1].splitlines()[-1], + 'Exception: CleanUpExc') self.assertEqual(ordering, ['setUpClass', 'cleanup_exc']) + ordering = [] + class_blow_up = False + method_blow_up = True + result = runTests(TestableTest) + self.assertEqual(result.errors[0][1].splitlines()[-1], + 'Exception: MethodExc') + self.assertEqual(result.errors[1][1].splitlines()[-1], + 'Exception: CleanUpExc') + self.assertEqual(ordering, + ['setUpClass', 'setUp', 'tearDownClass', + 'cleanup_exc']) + class TestModuleCleanUp(unittest.TestCase): def test_add_and_do_ModuleCleanup(self): @@ -374,25 +384,23 @@ class Module(object): def test_doModuleCleanup_with_errors_in_addModuleCleanup(self): module_cleanups = [] - def module_cleanup1(*args, **kwargs): + def module_cleanup_good(*args, **kwargs): module_cleanups.append((3, args, kwargs)) - def module_cleanup2(*args, **kwargs): + def module_cleanup_bad(*args, **kwargs): raise Exception('CleanUpExc') class Module(object): - unittest.addModuleCleanup(module_cleanup1, 1, 2, 3, + unittest.addModuleCleanup(module_cleanup_good, 1, 2, 3, four='hello', five='goodbye') - unittest.addModuleCleanup(module_cleanup2) - + unittest.addModuleCleanup(module_cleanup_bad) self.assertEqual(unittest.case._module_cleanups, - [(module_cleanup1, (1, 2, 3), + [(module_cleanup_good, (1, 2, 3), dict(four='hello', five='goodbye')), - (module_cleanup2, (), {})]) + (module_cleanup_bad, (), {})]) with self.assertRaises(Exception) as e: unittest.case.doModuleCleanups() - self.assertEqual(e, 'CleanUpExc') - + self.assertEqual(str(e.exception), 'CleanUpExc') self.assertEqual(unittest.case._module_cleanups, []) def test_run_module_cleanUp(self): diff --git a/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst b/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst index e14eb0ba6b1c86..862500dd19fba3 100644 --- a/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst +++ b/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst @@ -1,2 +1,4 @@ -Add ``addModuleCleanup()`` and ``addClassCleanup()`` to unittest to support -cleanups for setUpClass and setUpModule. Patch by Lisa Roach. +Add :func:`~unittest.addModuleCleanup()` and +:meth:`~unittest.TestCase.addClassCleanup()` to unittest to support +cleanups for :func:`~unittest.setUpModule()` and +:meth:`~unittest.TestCase.setUpClass()`. Patch by Lisa Roach.