[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:
@@ -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()
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user