summaryrefslogtreecommitdiff
path: root/odoo/modules/module.py
diff options
context:
space:
mode:
authorXavier Morel <xmo@odoo.com>2020-07-27 12:09:50 +0000
committerXavier Morel <xmo@odoo.com>2020-08-19 07:28:44 +0000
commitec8a64a85c7ce751bbd4fc3b5a2a8224a84c81d0 (patch)
tree0bc5a7b4668688c2ea7d007137ee2705fce19d82 /odoo/modules/module.py
parente46a8bc7d95501569fd95c499b84a7027a3f2831 (diff)
[REF] core: move testing-related functions to odoo/tests submodules
Attempts to clean up odoo/module and odoo/service a tad, they still invoke testing-related utilities but are more logical in what they *contain*.
Diffstat (limited to 'odoo/modules/module.py')
-rw-r--r--odoo/modules/module.py223
1 files changed, 0 insertions, 223 deletions
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