summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--odoo/modules/loading.py8
-rw-r--r--odoo/modules/module.py223
-rw-r--r--odoo/service/server.py10
-rw-r--r--odoo/tests/loader.py122
-rw-r--r--odoo/tests/runner.py116
-rwxr-xr-xodoo/tests/test_module_operations.py6
6 files changed, 250 insertions, 235 deletions
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index 03c04003275..a3adfe8492b 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -16,10 +16,8 @@ import odoo.modules.db
import odoo.modules.graph
import odoo.modules.migration
import odoo.modules.registry
-import odoo.tools as tools
-
-from odoo import api, SUPERUSER_ID
-from odoo.modules.module import adapt_version, initialize_sys_path, load_openerp_module
+from .. import SUPERUSER_ID, api, tools
+from .module import adapt_version, initialize_sys_path, load_openerp_module
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')
@@ -271,7 +269,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
report.record_result(load_test(idref, mode))
# Python tests
env['ir.http']._clear_routing_map() # force routing map to be rebuilt
- report.record_result(odoo.modules.module.run_unit_tests(module_name))
+ report.record_result(odoo.tests.loader.run_unit_tests(module_name))
# tests may have reset the environment
env = api.Environment(cr, SUPERUSER_ID, {})
module = env['ir.module.module'].browse(module_id)
diff --git a/odoo/modules/module.py b/odoo/modules/module.py
index 4859de455ff..6b7065a05ad 100644
--- a/odoo/modules/module.py
+++ b/odoo/modules/module.py
@@ -4,16 +4,11 @@
import ast
import collections.abc
import importlib
-import inspect
-import itertools
import logging
import os
import pkg_resources
import re
import sys
-import time
-import unittest
-import threading
import warnings
from os.path import join as opj
@@ -419,222 +414,4 @@ def adapt_version(version):
version = '%s.%s' % (serie, version)
return version
-def get_test_modules(module):
- """ Return a list of module for the addons potentially containing tests to
- feed unittest.TestLoader.loadTestsFromModule() """
- # Try to import the module
- results = _get_tests_modules('odoo.addons', module)
-
- try:
- importlib.import_module('odoo.upgrade.%s' % module)
- except ImportError:
- pass
- else:
- results += _get_tests_modules('odoo.upgrade', module)
-
- return results
-
-def _get_tests_modules(path, module):
- modpath = '%s.%s' % (path, module)
- try:
- mod = importlib.import_module('.tests', modpath)
- except ImportError as e: # will also catch subclass ModuleNotFoundError of P3.6
- # Hide ImportErrors on `tests` sub-module, but display other exceptions
- if e.name == modpath + '.tests' and e.msg.startswith('No module named'):
- return []
- _logger.exception('Can not `import %s`.', module)
- return []
- except Exception as e:
- _logger.exception('Can not `import %s`.', module)
- return []
- if hasattr(mod, 'fast_suite') or hasattr(mod, 'checks'):
- _logger.warning(
- "Found deprecated fast_suite or checks attribute in test module "
- "%s. These have no effect in or after version 8.0.",
- mod.__name__)
-
- result = [mod_obj for name, mod_obj in inspect.getmembers(mod, inspect.ismodule)
- if name.startswith('test_')]
- return result
-
-
-class OdooTestResult(unittest.result.TestResult):
- """
- This class in inspired from TextTestResult (https://github.com/python/cpython/blob/master/Lib/unittest/runner.py)
- Instead of using a stream, we are using the logger,
- but replacing the "findCaller" in order to give the information we
- have based on the test object that is running.
- """
-
- def log(self, level, msg, *args, test=None, exc_info=None, extra=None, stack_info=False, caller_infos=None):
- """
- ``test`` is the running test case, ``caller_infos`` is
- (fn, lno, func, sinfo) (logger.findCaller format), see logger.log for
- the other parameters.
- """
- test = test or self
- if isinstance(test, unittest.case._SubTest) and test.test_case:
- test = test.test_case
- logger = logging.getLogger(test.__module__)
- try:
- caller_infos = caller_infos or logger.findCaller(stack_info)
- except ValueError:
- caller_infos = "(unknown file)", 0, "(unknown function)", None
- (fn, lno, func, sinfo) = caller_infos
- # using logger.log makes it difficult to spot-replace findCaller in
- # order to provide useful location information (the problematic spot
- # inside the test function), so use lower-level functions instead
- if logger.isEnabledFor(level):
- record = logger.makeRecord(logger.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
- logger.handle(record)
-
- def getDescription(self, test):
- if isinstance(test, unittest.case._SubTest):
- return 'Subtest %s' % test._subDescription()
- if isinstance(test, unittest.TestCase):
- # since we have the module name in the logger, this will avoid to duplicate module info in log line
- # we only apply this for TestCase since we can receive error handler or other special case
- return "%s.%s" % (test.__class__.__qualname__, test._testMethodName)
- return str(test)
-
- def startTest(self, test):
- super().startTest(test)
- self.log(logging.INFO, 'Starting %s ...', self.getDescription(test), test=test)
-
- def addError(self, test, err):
- super().addError(test, err)
- self.logError("ERROR", test, err)
-
- def addFailure(self, test, err):
- super().addFailure(test, err)
- self.logError("FAIL", test, err)
-
- def addSubTest(self, test, subtest, err):
- # since addSubTest is not making a call to addFailure or addError we need to manage it too
- # https://github.com/python/cpython/blob/3.7/Lib/unittest/result.py#L136
- if err is not None:
- if issubclass(err[0], test.failureException):
- flavour = "FAIL"
- else:
- flavour = "ERROR"
- self.logError(flavour, subtest, err)
- super().addSubTest(test, subtest, err)
-
- def addSkip(self, test, reason):
- super().addSkip(test, reason)
- self.log(logging.INFO, 'skipped %s', self.getDescription(test), test=test)
-
- def addUnexpectedSuccess(self, test):
- super().addUnexpectedSuccess(test)
- self.log(logging.ERROR, 'unexpected success for %s', self.getDescription(test), test=test)
-
- def logError(self, flavour, test, error):
- err = self._exc_info_to_string(error, test)
- caller_infos = self.getErrorCallerInfo(error, test)
- self.log(logging.INFO, '=' * 70, test=test, caller_infos=caller_infos) # keep this as info !!!!!!
- self.log(logging.ERROR, "%s: %s\n%s", flavour, self.getDescription(test), err, test=test, caller_infos=caller_infos)
-
- def getErrorCallerInfo(self, error, test):
- """
- :param error: A tuple (exctype, value, tb) as returned by sys.exc_info().
- :param test: A TestCase that created this error.
- :returns: a tuple (fn, lno, func, sinfo) matching the logger findCaller format or None
- """
-
- # only test case should be executed in odoo, this is only a safe guard
- if isinstance(test, unittest.suite._ErrorHolder):
- return
- if not isinstance(test, unittest.TestCase):
- _logger.warning('%r is not a TestCase' % test)
- return
- _, _, error_traceback = error
-
- while error_traceback:
- code = error_traceback.tb_frame.f_code
- if code.co_name == test._testMethodName:
- lineno = error_traceback.tb_lineno
- filename = code.co_filename
- method = test._testMethodName
- infos = (filename, lineno, method, None)
- return infos
- error_traceback = error_traceback.tb_next
-
-
-class OdooTestRunner(object):
- """A test runner class that displays results in in logger using OdooTestResult.
- Simplified verison of TextTestRunner
- """
-
- def run(self, test):
- result = OdooTestResult()
- test(result)
- return result
-
current_test = None
-def run_unit_tests(module_name, position='at_install'):
- """
- :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
- if any of them failed, ``None`` if no tests were ran.
- :rtype: bool | None
- """
- global current_test
- # avoid dependency hell
- from odoo.tests.common import TagsSelector, OdooSuite
- current_test = module_name
- mods = get_test_modules(module_name)
- threading.currentThread().testing = True
- config_tags = TagsSelector(tools.config['test_tags'])
- position_tag = TagsSelector(position)
- ran_tests = failures = False
- for m in mods:
- tests = unwrap_suite(unittest.TestLoader().loadTestsFromModule(m))
- suite = OdooSuite(t for t in tests if position_tag.check(t) and config_tags.check(t))
-
- if suite.countTestCases():
- t0 = time.time()
- t0_sql = odoo.sql_db.sql_counter
- _logger.info('%s running tests.', m.__name__)
- result = OdooTestRunner().run(suite)
- log_level = logging.INFO
- if time.time() - t0 > 5:
- log_level = logging.RUNBOT
- _logger.log(log_level, "%s ran %s tests in %.2fs, %s queries", m.__name__, result.testsRun, time.time() - t0, odoo.sql_db.sql_counter - t0_sql)
- ran_tests = True
- if not result.wasSuccessful():
- failures = True
- _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
-
- current_test = None
- threading.currentThread().testing = False
-
- if failures:
- return False
- if ran_tests:
- return True
- return None
-
-def unwrap_suite(test):
- """
- Attempts to unpack testsuites (holding suites or cases) in order to
- generate a single stream of terminals (either test cases or customized
- test suites). These can then be checked for run/skip attributes
- individually.
-
- An alternative would be to use a variant of @unittest.skipIf with a state
- flag of some sort e.g. @unittest.skipIf(common.runstate != 'at_install'),
- but then things become weird with post_install as tests should *not* run
- by default there
- """
- if isinstance(test, unittest.TestCase):
- yield test
- return
-
- subtests = list(test)
- # custom test suite (no test cases)
- if not len(subtests):
- yield test
- return
-
- for item in itertools.chain.from_iterable(
- unwrap_suite(t) for t in subtests):
- yield item
diff --git a/odoo/service/server.py b/odoo/service/server.py
index 87e9b8ddf42..3f9d7376901 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -53,11 +53,11 @@ except ImportError:
import odoo
from odoo.modules import get_modules
-from odoo.modules.module import run_unit_tests, get_test_modules
from odoo.modules.registry import Registry
from odoo.release import nt_service_name
from odoo.tools import config
from odoo.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats
+from ..tests import loader, runner
_logger = logging.getLogger(__name__)
@@ -1171,14 +1171,14 @@ def load_test_file_py(registry, test_file):
try:
test_path, _ = os.path.splitext(os.path.abspath(test_file))
for mod in [m for m in get_modules() if '/%s/' % m in test_file]:
- for mod_mod in get_test_modules(mod):
+ for mod_mod in loader.get_test_modules(mod):
mod_path, _ = os.path.splitext(getattr(mod_mod, '__file__', ''))
if test_path == mod_path:
- tests = odoo.modules.module.unwrap_suite(
+ tests = loader.unwrap_suite(
unittest.TestLoader().loadTestsFromModule(mod_mod))
suite = OdooSuite(tests)
_logger.log(logging.INFO, 'running tests %s.', mod_mod.__name__)
- result = odoo.modules.module.OdooTestRunner().run(suite)
+ result = runner.OdooTestRunner().run(suite)
success = result.wasSuccessful()
if hasattr(registry._assertion_report,'report_result'):
registry._assertion_report.report_result(success)
@@ -1219,7 +1219,7 @@ def preload_registries(dbnames):
_logger.info("Starting post tests")
with odoo.api.Environment.manage():
for module_name in module_names:
- result = run_unit_tests(module_name, position='post_install')
+ result = loader.run_unit_tests(module_name, position='post_install')
registry._assertion_report.record_result(result)
_logger.info("All post-tested in %.2fs, %s queries",
time.time() - t0, odoo.sql_db.sql_counter - t0_sql)
diff --git a/odoo/tests/loader.py b/odoo/tests/loader.py
new file mode 100644
index 00000000000..c2cc53452fa
--- /dev/null
+++ b/odoo/tests/loader.py
@@ -0,0 +1,122 @@
+import importlib
+import inspect
+import itertools
+import logging
+import threading
+import time
+import unittest
+
+import odoo
+from .. import tools
+from .common import TagsSelector, OdooSuite
+from .runner import OdooTestRunner
+
+# backwards compatibility
+_logger = logging.getLogger('odoo.modules.module')
+
+def get_test_modules(module):
+ """ Return a list of module for the addons potentially containing tests to
+ feed unittest.TestLoader.loadTestsFromModule() """
+ # Try to import the module
+ results = _get_tests_modules('odoo.addons', module)
+
+ try:
+ importlib.import_module('odoo.upgrade.%s' % module)
+ except ImportError:
+ pass
+ else:
+ results += _get_tests_modules('odoo.upgrade', module)
+
+ return results
+
+
+def _get_tests_modules(path, module):
+ modpath = '%s.%s' % (path, module)
+ try:
+ mod = importlib.import_module('.tests', modpath)
+ except ImportError as e: # will also catch subclass ModuleNotFoundError of P3.6
+ # Hide ImportErrors on `tests` sub-module, but display other exceptions
+ if e.name == modpath + '.tests' and e.msg.startswith('No module named'):
+ return []
+ _logger.exception('Can not `import %s`.', module)
+ return []
+ except Exception as e:
+ _logger.exception('Can not `import %s`.', module)
+ return []
+ if hasattr(mod, 'fast_suite') or hasattr(mod, 'checks'):
+ _logger.warning(
+ "Found deprecated fast_suite or checks attribute in test module "
+ "%s. These have no effect in or after version 8.0.",
+ mod.__name__)
+
+ result = [mod_obj for name, mod_obj in inspect.getmembers(mod, inspect.ismodule)
+ if name.startswith('test_')]
+ return result
+
+
+def run_unit_tests(module_name, position='at_install'):
+ """
+ :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
+ if any of them failed, ``None`` if no tests were ran.
+ :rtype: bool | None
+ """
+ from ..modules import module
+ # avoid dependency hell
+ module.current_test = module_name
+ mods = get_test_modules(module_name)
+ threading.currentThread().testing = True
+ config_tags = TagsSelector(tools.config['test_tags'])
+ position_tag = TagsSelector(position)
+ ran_tests = failures = False
+ for m in mods:
+ tests = unwrap_suite(unittest.TestLoader().loadTestsFromModule(m))
+ suite = OdooSuite(t for t in tests if position_tag.check(t) and config_tags.check(t))
+
+ if suite.countTestCases():
+ t0 = time.time()
+ t0_sql = odoo.sql_db.sql_counter
+ _logger.info('%s running tests.', m.__name__)
+ result = OdooTestRunner().run(suite)
+ log_level = logging.INFO
+ if time.time() - t0 > 5:
+ log_level = logging.RUNBOT
+ _logger.log(log_level, "%s ran %s tests in %.2fs, %s queries", m.__name__, result.testsRun, time.time() - t0, odoo.sql_db.sql_counter - t0_sql)
+ ran_tests = True
+ if not result.wasSuccessful():
+ failures = True
+ _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
+
+ module.current_test = None
+ threading.currentThread().testing = False
+
+ if failures:
+ return False
+ if ran_tests:
+ return True
+ return None
+
+
+def unwrap_suite(test):
+ """
+ Attempts to unpack testsuites (holding suites or cases) in order to
+ generate a single stream of terminals (either test cases or customized
+ test suites). These can then be checked for run/skip attributes
+ individually.
+
+ An alternative would be to use a variant of @unittest.skipIf with a state
+ flag of some sort e.g. @unittest.skipIf(common.runstate != 'at_install'),
+ but then things become weird with post_install as tests should *not* run
+ by default there
+ """
+ if isinstance(test, unittest.TestCase):
+ yield test
+ return
+
+ subtests = list(test)
+ # custom test suite (no test cases)
+ if not len(subtests):
+ yield test
+ return
+
+ for item in itertools.chain.from_iterable(unwrap_suite(t) for t in subtests):
+ yield item
diff --git a/odoo/tests/runner.py b/odoo/tests/runner.py
new file mode 100644
index 00000000000..23e285005bd
--- /dev/null
+++ b/odoo/tests/runner.py
@@ -0,0 +1,116 @@
+import logging
+import unittest
+
+# backwards compatibility
+_logger = logging.getLogger('odoo.modules.module')
+class OdooTestResult(unittest.result.TestResult):
+ """
+ This class in inspired from TextTestResult (https://github.com/python/cpython/blob/master/Lib/unittest/runner.py)
+ Instead of using a stream, we are using the logger,
+ but replacing the "findCaller" in order to give the information we
+ have based on the test object that is running.
+ """
+
+ def log(self, level, msg, *args, test=None, exc_info=None, extra=None, stack_info=False, caller_infos=None):
+ """
+ ``test`` is the running test case, ``caller_infos`` is
+ (fn, lno, func, sinfo) (logger.findCaller format), see logger.log for
+ the other parameters.
+ """
+ test = test or self
+ if isinstance(test, unittest.case._SubTest) and test.test_case:
+ test = test.test_case
+ logger = logging.getLogger(test.__module__)
+ try:
+ caller_infos = caller_infos or logger.findCaller(stack_info)
+ except ValueError:
+ caller_infos = "(unknown file)", 0, "(unknown function)", None
+ (fn, lno, func, sinfo) = caller_infos
+ # using logger.log makes it difficult to spot-replace findCaller in
+ # order to provide useful location information (the problematic spot
+ # inside the test function), so use lower-level functions instead
+ if logger.isEnabledFor(level):
+ record = logger.makeRecord(logger.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
+ logger.handle(record)
+
+ def getDescription(self, test):
+ if isinstance(test, unittest.case._SubTest):
+ return 'Subtest %s' % test._subDescription()
+ if isinstance(test, unittest.TestCase):
+ # since we have the module name in the logger, this will avoid to duplicate module info in log line
+ # we only apply this for TestCase since we can receive error handler or other special case
+ return "%s.%s" % (test.__class__.__qualname__, test._testMethodName)
+ return str(test)
+
+ def startTest(self, test):
+ super().startTest(test)
+ self.log(logging.INFO, 'Starting %s ...', self.getDescription(test), test=test)
+
+ def addError(self, test, err):
+ super().addError(test, err)
+ self.logError("ERROR", test, err)
+
+ def addFailure(self, test, err):
+ super().addFailure(test, err)
+ self.logError("FAIL", test, err)
+
+ def addSubTest(self, test, subtest, err):
+ # since addSubTest is not making a call to addFailure or addError we need to manage it too
+ # https://github.com/python/cpython/blob/3.7/Lib/unittest/result.py#L136
+ if err is not None:
+ if issubclass(err[0], test.failureException):
+ flavour = "FAIL"
+ else:
+ flavour = "ERROR"
+ self.logError(flavour, subtest, err)
+ super().addSubTest(test, subtest, err)
+
+ def addSkip(self, test, reason):
+ super().addSkip(test, reason)
+ self.log(logging.INFO, 'skipped %s', self.getDescription(test), test=test)
+
+ def addUnexpectedSuccess(self, test):
+ super().addUnexpectedSuccess(test)
+ self.log(logging.ERROR, 'unexpected success for %s', self.getDescription(test), test=test)
+
+ def logError(self, flavour, test, error):
+ err = self._exc_info_to_string(error, test)
+ caller_infos = self.getErrorCallerInfo(error, test)
+ self.log(logging.INFO, '=' * 70, test=test, caller_infos=caller_infos) # keep this as info !!!!!!
+ self.log(logging.ERROR, "%s: %s\n%s", flavour, self.getDescription(test), err, test=test, caller_infos=caller_infos)
+
+ def getErrorCallerInfo(self, error, test):
+ """
+ :param error: A tuple (exctype, value, tb) as returned by sys.exc_info().
+ :param test: A TestCase that created this error.
+ :returns: a tuple (fn, lno, func, sinfo) matching the logger findCaller format or None
+ """
+
+ # only test case should be executed in odoo, this is only a safe guard
+ if isinstance(test, unittest.suite._ErrorHolder):
+ return
+ if not isinstance(test, unittest.TestCase):
+ _logger.warning('%r is not a TestCase' % test)
+ return
+ _, _, error_traceback = error
+
+ while error_traceback:
+ code = error_traceback.tb_frame.f_code
+ if code.co_name == test._testMethodName:
+ lineno = error_traceback.tb_lineno
+ filename = code.co_filename
+ method = test._testMethodName
+ infos = (filename, lineno, method, None)
+ return infos
+ error_traceback = error_traceback.tb_next
+
+
+class OdooTestRunner(object):
+ """A test runner class that displays results in in logger using OdooTestResult.
+ Simplified verison of TextTestRunner
+ """
+
+ def run(self, test):
+ result = OdooTestResult()
+ test(result)
+ return result
diff --git a/odoo/tests/test_module_operations.py b/odoo/tests/test_module_operations.py
index 1ce9d4e3226..fca6dc274c4 100755
--- a/odoo/tests/test_module_operations.py
+++ b/odoo/tests/test_module_operations.py
@@ -5,14 +5,16 @@ import os
import sys
import time
+
sys.path.append(os.path.abspath(os.path.join(__file__,'../../../')))
import odoo
from odoo.tools import topological_sort, unique
from odoo.netsvc import init_logger
from odoo.tests import standalone_tests
+import odoo.tests.loader
-_logger = logging.getLogger('test_module_operations')
+_logger = logging.getLogger('odoo.tests.test_module_operations')
BLACKLIST = {
'auth_ldap', 'document_ftp', 'base_gengo', 'website_gengo', 'website_instantclick', 'pad',
@@ -119,7 +121,7 @@ def test_scripts(args):
registry = odoo.registry(args.database)
for module_name in registry._init_modules:
# import tests for loaded modules
- odoo.modules.module.get_test_modules(module_name)
+ odoo.tests.loader.get_test_modules(module_name)
# fetch and filter scripts to test
funcs = list(unique(