[IMP] tests: partially backport classCleanups from CPython 3.8

This commit partially backports bpo-24412, which allows the definition of
class cleanups (addClassCleanup) and module cleanups (omitted),
similar to instance cleanups (addCleanup).

This is useful for tests that override unittest's setUpClass and
could crash during its execution: If this happens, it is possible that a
bunch of crap is left in the database or even worse, the cursor becomes
completely fucked; Thanks to the addClassCleanup, we can undo the damage
done by the setUpClass.

Another benefit is that it is called unconditionally after tearDownClass
is called, so it can also be called as a replacement and/or safer
tearDownClass.
This commit is contained in:
Adrian Torres
2019-11-06 14:07:04 +00:00
parent bb716ce6ac
commit ec587297eb
3 changed files with 123 additions and 3 deletions
+3 -2
View File
@@ -492,7 +492,8 @@ def run_unit_tests(module_name, position='at_install'):
:rtype: bool
"""
global current_test
from odoo.tests.common import TagsSelector # Avoid import loop
# avoid dependency hell
from odoo.tests.common import TagsSelector, OdooSuite
current_test = module_name
mods = get_test_modules(module_name)
threading.currentThread().testing = True
@@ -501,7 +502,7 @@ def run_unit_tests(module_name, position='at_install'):
r = True
for m in mods:
tests = unwrap_suite(unittest.TestLoader().loadTestsFromModule(m))
suite = unittest.TestSuite(t for t in tests if position_tag.check(t) and config_tags.check(t))
suite = OdooSuite(t for t in tests if position_tag.check(t) and config_tags.check(t))
if suite.countTestCases():
t0 = time.time()
+2 -1
View File
@@ -20,6 +20,7 @@ import unittest
import psutil
import werkzeug.serving
from werkzeug.debug import DebuggedApplication
from odoo.tests.common import OdooSuite
if os.name == 'posix':
# Unix only for workers
@@ -1130,7 +1131,7 @@ def load_test_file_py(registry, test_file):
for mod_mod in get_test_modules(mod):
mod_path, _ = os.path.splitext(getattr(mod_mod, '__file__', ''))
if test_path == mod_path:
suite = unittest.TestSuite()
suite = OdooSuite()
for t in unittest.TestLoader().loadTestsFromModule(mod_mod):
suite.addTest(t)
_logger.log(logging.INFO, 'running tests %s.', mod_mod.__name__)
+118
View File
@@ -19,6 +19,7 @@ import requests
import shutil
import signal
import subprocess
import sys
import tempfile
import threading
import time
@@ -128,9 +129,126 @@ def new_test_user(env, login='', groups='base.group_user', context=None, **kwarg
# ------------------------------------------------------------
# Main classes
# ------------------------------------------------------------
class OdooSuite(unittest.suite.TestSuite):
if sys.version_info < (3, 8):
# Partial backport of bpo-24412, merged in CPython 3.8
def _handleClassSetup(self, test, result):
previousClass = getattr(result, '_previousTestClass', None)
currentClass = test.__class__
if currentClass == previousClass:
return
if result._moduleSetUpFailed:
return
if getattr(currentClass, "__unittest_skip__", False):
return
try:
currentClass._classSetupFailed = False
except TypeError:
# test may actually be a function
# so its class will be a builtin-type
pass
setUpClass = getattr(currentClass, 'setUpClass', None)
if setUpClass is not None:
unittest.suite._call_if_exists(result, '_setupStdout')
try:
setUpClass()
except Exception as e:
if isinstance(result, unittest.suite._DebugResult):
raise
currentClass._classSetupFailed = True
className = unittest.util.strclass(currentClass)
self._createClassOrModuleLevelException(result, e,
'setUpClass',
className)
finally:
unittest.suite._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 _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 = unittest.suite._ErrorHolder(errorName)
addSkip = getattr(result, 'addSkip', None)
if addSkip is not None and isinstance(exception, unittest.case.SkipTest):
addSkip(error, str(exception))
else:
if not info:
result.addError(error, sys.exc_info())
else:
result.addError(error, info)
def _tearDownPreviousClass(self, test, result):
previousClass = getattr(result, '_previousTestClass', None)
currentClass = test.__class__
if currentClass == previousClass:
return
if getattr(previousClass, '_classSetupFailed', False):
return
if getattr(result, '_moduleSetUpFailed', False):
return
if getattr(previousClass, "__unittest_skip__", False):
return
tearDownClass = getattr(previousClass, 'tearDownClass', None)
if tearDownClass is not None:
unittest.suite._call_if_exists(result, '_setupStdout')
try:
tearDownClass()
except Exception as e:
if isinstance(result, unittest.suite._DebugResult):
raise
className = unittest.util.strclass(previousClass)
self._createClassOrModuleLevelException(result, e,
'tearDownClass',
className)
finally:
unittest.suite._call_if_exists(result, '_restoreStdout')
previousClass.doClassCleanups()
if len(previousClass.tearDown_exceptions) > 0:
for exc in previousClass.tearDown_exceptions:
className = unittest.util.strclass(previousClass)
self._createClassOrModuleLevelException(result, exc[1],
'tearDownClass',
className,
info=exc)
class TreeCase(unittest.TestCase):
if sys.version_info < (3, 8):
# Partial backport of bpo-24412, merged in CPython 3.8
_class_cleanups = []
@classmethod
def addClassCleanup(cls, function, *args, **kwargs):
"""Same as addCleanup, except the cleanup items are called even if
setUpClass fails (unlike tearDownClass). Backport of bpo-24412."""
cls._class_cleanups.append((function, args, kwargs))
@classmethod
def doClassCleanups(cls):
"""Execute all class cleanup functions. Normally called for you after tearDownClass.
Backport of bpo-24412."""
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 __init__(self, methodName='runTest'):
super(TreeCase, self).__init__(methodName)
self.addTypeEqualityFunc(etree._Element, self.assertTreesEqual)