[Python-checkins] bpo-24412: Adds cleanUps for setUpClass and setUpModule. (GH-9190)

Lisa Roach webhook-mailer at python.org
Thu Nov 8 21:34:39 EST 2018


https://github.com/python/cpython/commit/0f221d09cad46bee38d1b7a7822772df66c53028
commit: 0f221d09cad46bee38d1b7a7822772df66c53028
branch: master
author: Lisa Roach <lisaroach14 at gmail.com>
committer: GitHub <noreply at github.com>
date: 2018-11-08T18:34:33-08:00
summary:

bpo-24412: Adds cleanUps for setUpClass and setUpModule. (GH-9190)

files:
A Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst
M Doc/library/unittest.rst
M Doc/whatsnew/3.8.rst
M Lib/unittest/__init__.py
M Lib/unittest/case.py
M Lib/unittest/suite.py
M Lib/unittest/test/test_runner.py

diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index 1153459029ce..c4019088c79f 100644
--- a/Doc/library/unittest.rst
+++ b/Doc/library/unittest.rst
@@ -1448,6 +1448,39 @@ Test cases
 
       .. versionadded:: 3.1
 
+   .. 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
+      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
+
+
+   .. classmethod:: 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
+
+
+
+
+
 
 .. class:: FunctionTestCase(testFunc, setUp=None, tearDown=None, description=None)
 
@@ -2268,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 74bdba3375c9..91e0d5bb7b33 100644
--- a/Doc/whatsnew/3.8.rst
+++ b/Doc/whatsnew/3.8.rst
@@ -233,6 +233,15 @@ unicodedata
   is in a specific normal form. (Contributed by Max Belanger and David Euresti in
   :issue:`32285`).
 
+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`.)
+
 venv
 ----
 
diff --git a/Lib/unittest/__init__.py b/Lib/unittest/__init__.py
index c55d563e0c38..5ff1bf37b169 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 2579c30474b5..a157ae8a14bc 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -84,6 +84,30 @@ 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."""
+    exceptions = []
+    while _module_cleanups:
+        function, args, kwargs = _module_cleanups.pop()
+        try:
+            function(*args, **kwargs)
+        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.
@@ -390,6 +414,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 +471,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, except 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
@@ -651,9 +683,21 @@ 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):
+        """Execute all class cleanup functions. Normally called for you after
+        tearDownClass."""
+        cls.tearDown_exceptions = []
+        while cls._class_cleanups:
+            function, args, kwargs = cls._class_cleanups.pop()
+            try:
+                function(*args, **kwargs)
+            except Exception as exc:
+                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 353d4a17b963..41993f9cf69a 100644
--- a/Lib/unittest/suite.py
+++ b/Lib/unittest/suite.py
@@ -166,10 +166,18 @@ def _handleClassSetUp(self, test, result):
                     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')
+                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
@@ -199,21 +207,37 @@ def _handleModuleFixture(self, test, result):
             try:
                 setUpModule()
             except Exception as e:
+                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 _addClassOrModuleLevelException(self, result, exception, 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,
+                                        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)
@@ -235,10 +259,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')
+                try:
+                    case.doModuleCleanups()
+                except Exception as e:
+                    self._createClassOrModuleLevelException(result, e,
+                                                            'tearDownModule',
+                                                            previousModule)
 
     def _tearDownPreviousClass(self, test, result):
         previousClass = getattr(result, '_previousTestClass', None)
@@ -261,10 +292,19 @@ 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()
+                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 3c4005671f73..6f89f77ff778 100644
--- a/Lib/unittest/test/test_runner.py
+++ b/Lib/unittest/test/test_runner.py
@@ -11,8 +11,41 @@
                                    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)
+
+
+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):
             def testNothing(self):
@@ -47,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
@@ -63,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):
@@ -135,6 +168,575 @@ 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()
+        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, ordering)
+                if blowUp:
+                    raise Exception()
+            def testNothing(self):
+                ordering.append('test')
+            @classmethod
+            def tearDownClass(cls):
+                ordering.append('tearDownClass')
+
+        runTests(TestableTest)
+        self.assertEqual(ordering, ['setUpClass', 'cleanup_good'])
+
+        ordering = []
+        blowUp = False
+        runTests(TestableTest)
+        self.assertEqual(ordering,
+                         ['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
+
+    def test_debug_executes_classCleanUp(self):
+        ordering = []
+
+        class TestableTest(unittest.TestCase):
+            @classmethod
+            def setUpClass(cls):
+                ordering.append('setUpClass')
+                cls.addClassCleanup(cleanup, ordering)
+            def testNothing(self):
+                ordering.append('test')
+            @classmethod
+            def tearDownClass(cls):
+                ordering.append('tearDownClass')
+
+        suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+        suite.debug()
+        self.assertEqual(ordering,
+                         ['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
+
+    def test_doClassCleanups_with_errors_addClassCleanUp(self):
+        class TestableTest(unittest.TestCase):
+            def testNothing(self):
+                pass
+
+        def cleanup1():
+            raise Exception('cleanup1')
+
+        def cleanup2():
+            raise Exception('cleanup2')
+
+        TestableTest.addClassCleanup(cleanup1)
+        TestableTest.addClassCleanup(cleanup2)
+        with self.assertRaises(Exception) as e:
+            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')
+
+        result = runTests(TestableTest)
+        self.assertEqual(result.errors[0][1].splitlines()[-1],
+                         'Exception: CleanUpExc')
+        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):
+                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', '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: 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):
+        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_doModuleCleanup_with_errors_in_addModuleCleanup(self):
+        module_cleanups = []
+
+        def module_cleanup_good(*args, **kwargs):
+            module_cleanups.append((3, args, kwargs))
+
+        def module_cleanup_bad(*args, **kwargs):
+            raise Exception('CleanUpExc')
+
+        class Module(object):
+            unittest.addModuleCleanup(module_cleanup_good, 1, 2, 3,
+                                      four='hello', five='goodbye')
+            unittest.addModuleCleanup(module_cleanup_bad)
+        self.assertEqual(unittest.case._module_cleanups,
+                         [(module_cleanup_good, (1, 2, 3),
+                           dict(four='hello', five='goodbye')),
+                          (module_cleanup_bad, (), {})])
+        with self.assertRaises(Exception) as e:
+            unittest.case.doModuleCleanups()
+        self.assertEqual(str(e.exception), 'CleanUpExc')
+        self.assertEqual(unittest.case._module_cleanups, [])
+
+    def test_run_module_cleanUp(self):
+        blowUp = True
+        ordering = []
+        class Module(object):
+            @staticmethod
+            def setUpModule():
+                ordering.append('setUpModule')
+                unittest.addModuleCleanup(cleanup, ordering)
+                if blowUp:
+                    raise Exception('setUpModule Exc')
+            @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')
+
+        TestableTest.__module__ = 'Module'
+        sys.modules['Module'] = Module
+        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_good'])
+        self.assertEqual(unittest.case._module_cleanups, [])
+
+    def test_run_multiple_module_cleanUp(self):
+        blowUp = True
+        blowUp2 = False
+        ordering = []
+        class Module1(object):
+            @staticmethod
+            def setUpModule():
+                ordering.append('setUpModule')
+                unittest.addModuleCleanup(cleanup, ordering)
+                if blowUp:
+                    raise Exception()
+            @staticmethod
+            def tearDownModule():
+                ordering.append('tearDownModule')
+
+        class Module2(object):
+            @staticmethod
+            def setUpModule():
+                ordering.append('setUpModule2')
+                unittest.addModuleCleanup(cleanup, ordering)
+                if blowUp2:
+                    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')
+
+        TestableTest.__module__ = 'Module1'
+        sys.modules['Module1'] = Module1
+        TestableTest2.__module__ = 'Module2'
+        sys.modules['Module2'] = Module2
+        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_good', 'setUpModule2',
+                          'setUpClass2', 'test2', 'tearDownClass2',
+                          '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, ordering)
+            @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')
+
+        TestableTest.__module__ = 'Module'
+        sys.modules['Module'] = Module
+        suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+        suite.debug()
+        self.assertEqual(ordering,
+                         ['setUpModule', 'setUpClass', 'test', 'tearDownClass',
+                          'tearDownModule', 'cleanup_good'])
+        self.assertEqual(unittest.case._module_cleanups, [])
+
+    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():
+            ordering.append('cleanup1')
+
+        def cleanup2():
+            ordering.append('cleanup2')
+
+        def cleanup3():
+            ordering.append('cleanup3')
+
+        class Module(object):
+            @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'])
+
+
 class Test_TextTestRunner(unittest.TestCase):
     """Tests for TextTestRunner."""
 
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 000000000000..862500dd19fb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst
@@ -0,0 +1,4 @@
+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.



More information about the Python-checkins mailing list