summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/unittest.rst65
-rw-r--r--Doc/whatsnew/3.8.rst9
-rw-r--r--Lib/unittest/__init__.py7
-rw-r--r--Lib/unittest/case.py46
-rw-r--r--Lib/unittest/suite.py60
-rw-r--r--Lib/unittest/test/test_runner.py610
-rw-r--r--Misc/NEWS.d/next/Library/2018-09-11-10-51-16.bpo-24412.i-F_E5.rst4
7 files changed, 783 insertions, 18 deletions
diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index 1153459..c401908 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 74bdba3..91e0d5b 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 c55d563..5ff1bf3 100644
--- a/Lib/unittest/__init__.py
+++ b/Lib/unittest/__init__.py
@@ -48,7 +48,8 @@ __all__ = ['TestResult', 'TestCase', 'TestSuite',
'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 @@ __all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases'])
__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 2579c30..a157ae8 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -84,6 +84,30 @@ class _Outcome(object):
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 @@ class TestCase(object):
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 @@ class TestCase(object):
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 353d4a1..41993f9 100644
--- a/Lib/unittest/suite.py
+++ b/Lib/unittest/suite.py
@@ -166,10 +166,18 @@ class TestSuite(BaseTestSuite):
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 @@ class TestSuite(BaseTestSuite):
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 @@ class TestSuite(BaseTestSuite):
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 @@ class TestSuite(BaseTestSuite):
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 3c40056..6f89f77 100644
--- a/Lib/unittest/test/test_runner.py
+++ b/Lib/unittest/test/test_runner.py
@@ -11,8 +11,41 @@ from unittest.test.support import (LoggingResult,
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 @@ class TestCleanUp(unittest.TestCase):
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 @@ class TestCleanUp(unittest.TestCase):
((_, (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 @@ class TestCleanUp(unittest.TestCase):
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 0000000..862500d
--- /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.