diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 8d0dd5a7091..851395ba280 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -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() diff --git a/odoo/service/server.py b/odoo/service/server.py index aceedc42d60..9ca6212e0ce 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -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__) diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 6035cf21d10..fcf02041518 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -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)