summaryrefslogtreecommitdiff
path: root/odoo/tests/runner.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/tests/runner.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/tests/runner.py')
-rw-r--r--odoo/tests/runner.py116
1 files changed, 116 insertions, 0 deletions
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