Commit 0f221d09 authored by Lisa Roach's avatar Lisa Roach Committed by GitHub

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

parent 49fa4a9f
...@@ -1448,6 +1448,39 @@ Test cases ...@@ -1448,6 +1448,39 @@ Test cases
.. versionadded:: 3.1 .. 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) .. 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 ...@@ -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 :exc:`SkipTest` exception then the module will be reported as having been skipped
instead of as an error. 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 Signal Handling
--------------- ---------------
......
...@@ -233,6 +233,15 @@ unicodedata ...@@ -233,6 +233,15 @@ unicodedata
is in a specific normal form. (Contributed by Max Belanger and David Euresti in is in a specific normal form. (Contributed by Max Belanger and David Euresti in
:issue:`32285`). :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 venv
---- ----
......
...@@ -48,7 +48,8 @@ __all__ = ['TestResult', 'TestCase', 'TestSuite', ...@@ -48,7 +48,8 @@ __all__ = ['TestResult', 'TestCase', 'TestSuite',
'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main', 'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main',
'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless', 'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless',
'expectedFailure', 'TextTestResult', 'installHandler', 'expectedFailure', 'TextTestResult', 'installHandler',
'registerResult', 'removeResult', 'removeHandler'] 'registerResult', 'removeResult', 'removeHandler',
'addModuleCleanup']
# Expose obsolete functions for backwards compatibility # Expose obsolete functions for backwards compatibility
__all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases']) __all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases'])
...@@ -56,8 +57,8 @@ __all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases']) ...@@ -56,8 +57,8 @@ __all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases'])
__unittest = True __unittest = True
from .result import TestResult from .result import TestResult
from .case import (TestCase, FunctionTestCase, SkipTest, skip, skipIf, from .case import (addModuleCleanup, TestCase, FunctionTestCase, SkipTest, skip,
skipUnless, expectedFailure) skipIf, skipUnless, expectedFailure)
from .suite import BaseTestSuite, TestSuite from .suite import BaseTestSuite, TestSuite
from .loader import (TestLoader, defaultTestLoader, makeSuite, getTestCaseNames, from .loader import (TestLoader, defaultTestLoader, makeSuite, getTestCaseNames,
findTestCases) findTestCases)
......
...@@ -84,6 +84,30 @@ class _Outcome(object): ...@@ -84,6 +84,30 @@ class _Outcome(object):
def _id(obj): def _id(obj):
return 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): def skip(reason):
""" """
Unconditionally skip a test. Unconditionally skip a test.
...@@ -390,6 +414,8 @@ class TestCase(object): ...@@ -390,6 +414,8 @@ class TestCase(object):
_classSetupFailed = False _classSetupFailed = False
_class_cleanups = []
def __init__(self, methodName='runTest'): def __init__(self, methodName='runTest'):
"""Create an instance of the class that will use the named test """Create an instance of the class that will use the named test
method when executed. Raises a ValueError if the instance does method when executed. Raises a ValueError if the instance does
...@@ -445,6 +471,12 @@ class TestCase(object): ...@@ -445,6 +471,12 @@ class TestCase(object):
Cleanup items are called even if setUp fails (unlike tearDown).""" Cleanup items are called even if setUp fails (unlike tearDown)."""
self._cleanups.append((function, args, kwargs)) 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): def setUp(self):
"Hook method for setting up the test fixture before exercising it." "Hook method for setting up the test fixture before exercising it."
pass pass
...@@ -651,9 +683,21 @@ class TestCase(object): ...@@ -651,9 +683,21 @@ class TestCase(object):
function(*args, **kwargs) function(*args, **kwargs)
# return this for backwards compatibility # return this for backwards compatibility
# even though we no longer us it internally # even though we no longer use it internally
return outcome.success 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): def __call__(self, *args, **kwds):
return self.run(*args, **kwds) return self.run(*args, **kwds)
......
...@@ -166,10 +166,18 @@ class TestSuite(BaseTestSuite): ...@@ -166,10 +166,18 @@ class TestSuite(BaseTestSuite):
raise raise
currentClass._classSetupFailed = True currentClass._classSetupFailed = True
className = util.strclass(currentClass) className = util.strclass(currentClass)
errorName = 'setUpClass (%s)' % className self._createClassOrModuleLevelException(result, e,
self._addClassOrModuleLevelException(result, e, errorName) 'setUpClass',
className)
finally: finally:
_call_if_exists(result, '_restoreStdout') _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): def _get_previous_module(self, result):
previousModule = None previousModule = None
...@@ -199,21 +207,37 @@ class TestSuite(BaseTestSuite): ...@@ -199,21 +207,37 @@ class TestSuite(BaseTestSuite):
try: try:
setUpModule() setUpModule()
except Exception as e: except Exception as e:
try:
case.doModuleCleanups()
except Exception as exc:
self._createClassOrModuleLevelException(result, exc,
'setUpModule',
currentModule)
if isinstance(result, _DebugResult): if isinstance(result, _DebugResult):
raise raise
result._moduleSetUpFailed = True result._moduleSetUpFailed = True
errorName = 'setUpModule (%s)' % currentModule self._createClassOrModuleLevelException(result, e,
self._addClassOrModuleLevelException(result, e, errorName) 'setUpModule',
currentModule)
finally: finally:
_call_if_exists(result, '_restoreStdout') _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) error = _ErrorHolder(errorName)
addSkip = getattr(result, 'addSkip', None) addSkip = getattr(result, 'addSkip', None)
if addSkip is not None and isinstance(exception, case.SkipTest): if addSkip is not None and isinstance(exception, case.SkipTest):
addSkip(error, str(exception)) addSkip(error, str(exception))
else: 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): def _handleModuleTearDown(self, result):
previousModule = self._get_previous_module(result) previousModule = self._get_previous_module(result)
...@@ -235,10 +259,17 @@ class TestSuite(BaseTestSuite): ...@@ -235,10 +259,17 @@ class TestSuite(BaseTestSuite):
except Exception as e: except Exception as e:
if isinstance(result, _DebugResult): if isinstance(result, _DebugResult):
raise raise
errorName = 'tearDownModule (%s)' % previousModule self._createClassOrModuleLevelException(result, e,
self._addClassOrModuleLevelException(result, e, errorName) 'tearDownModule',
previousModule)
finally: finally:
_call_if_exists(result, '_restoreStdout') _call_if_exists(result, '_restoreStdout')
try:
case.doModuleCleanups()
except Exception as e:
self._createClassOrModuleLevelException(result, e,
'tearDownModule',
previousModule)
def _tearDownPreviousClass(self, test, result): def _tearDownPreviousClass(self, test, result):
previousClass = getattr(result, '_previousTestClass', None) previousClass = getattr(result, '_previousTestClass', None)
...@@ -261,10 +292,19 @@ class TestSuite(BaseTestSuite): ...@@ -261,10 +292,19 @@ class TestSuite(BaseTestSuite):
if isinstance(result, _DebugResult): if isinstance(result, _DebugResult):
raise raise
className = util.strclass(previousClass) className = util.strclass(previousClass)
errorName = 'tearDownClass (%s)' % className self._createClassOrModuleLevelException(result, e,
self._addClassOrModuleLevelException(result, e, errorName) 'tearDownClass',
className)
finally: finally:
_call_if_exists(result, '_restoreStdout') _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): class _ErrorHolder(object):
......
This diff is collapsed.
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.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment