[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*.
This commit is contained in:
Xavier Morel
2020-08-19 07:28:44 +00:00
parent e46a8bc7d9
commit ec8a64a85c
6 changed files with 250 additions and 235 deletions
-223
View File
@@ -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