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