summaryrefslogtreecommitdiff
path: root/odoo/modules/module.py
diff options
context:
space:
mode:
Diffstat (limited to 'odoo/modules/module.py')
-rw-r--r--odoo/modules/module.py114
1 files changed, 101 insertions, 13 deletions
diff --git a/odoo/modules/module.py b/odoo/modules/module.py
index 8036bff7dd9..f99c25ea11d 100644
--- a/odoo/modules/module.py
+++ b/odoo/modules/module.py
@@ -465,22 +465,110 @@ def get_test_modules(module):
if name.startswith('test_')]
return result
-# Use a custom stream object to log the test executions.
-class TestStream(object):
- def __init__(self, logger_name='odoo.tests'):
- self.logger = logging.getLogger(logger_name)
- self.r = re.compile(r'^-*$|^ *... *$|^ok$')
- def flush(self):
- pass
- def write(self, s):
- if self.r.match(s):
+
+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.
+ """
+ logger = logging.getLogger((test or self).__module__) # test should be always set
+ 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
+ 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.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 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 not isinstance(test, unittest.TestCase):
+ _logger.warning('%r is not a TestCase' % test)
return
- level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
- self.logger.log(level, s)
+ _, _, 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.
+ Simplified verison of TextTestRunner(
+ """
+
+ def run(self, test):
+ result = OdooTestResult()
+
+ start_time = time.perf_counter()
+ test(result)
+ time_taken = time.perf_counter() - start_time
+
+ logger = logging.getLogger(test.__module__)
+ run = result.testsRun
+ logger.info("Ran %d test%s in %.3fs", run, run != 1 and "s" or "", time_taken)
+ return result
current_test = None
-def run_unit_tests(module_name, dbname, position='at_install'):
+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.
@@ -502,7 +590,7 @@ def run_unit_tests(module_name, dbname, position='at_install'):
t0 = time.time()
t0_sql = odoo.sql_db.sql_counter
_logger.info('%s running tests.', m.__name__)
- result = unittest.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
+ result = OdooTestRunner().run(suite)
if time.time() - t0 > 5:
_logger.log(25, "%s tested in %.2fs, %s queries", m.__name__, time.time() - t0, odoo.sql_db.sql_counter - t0_sql)
if not result.wasSuccessful():