odoo_17.0.1/odoo/tests/case.py

280 lines
10 KiB
Python
Raw Normal View History

"""Test case implementation"""
import contextlib
import inspect
import logging
import sys
from pathlib import PurePath
from unittest import SkipTest
from unittest import TestCase as _TestCase
_logger = logging.getLogger(__name__)
__unittest = True
_subtest_msg_sentinel = object()
class _Outcome(object):
def __init__(self, test, result):
self.result = result
self.success = True
self.test = test
@contextlib.contextmanager
def testPartExecutor(self, test_case, isTest=False):
try:
yield
except KeyboardInterrupt:
raise
except SkipTest as e:
self.success = False
self.result.addSkip(test_case, str(e))
except: # pylint: disable=bare-except
exc_info = sys.exc_info()
self.success = False
if exc_info is not None:
exception_type, exception, tb = exc_info
tb = self._complete_traceback(tb)
exc_info = (exception_type, exception, tb)
self.test._addError(self.result, test_case, exc_info)
# explicitly break a reference cycle:
# exc_info -> frame -> exc_info
exc_info = None
def _complete_traceback(self, initial_tb):
Traceback = type(initial_tb)
# make the set of frames in the traceback
tb_frames = set()
tb = initial_tb
while tb:
tb_frames.add(tb.tb_frame)
tb = tb.tb_next
tb = initial_tb
# find the common frame by searching the last frame of the current_stack present in the traceback.
current_frame = inspect.currentframe()
common_frame = None
while current_frame:
if current_frame in tb_frames:
common_frame = current_frame # we want to find the last frame in common
current_frame = current_frame.f_back
if not common_frame: # not really useful but safer
_logger.warning('No common frame found with current stack, displaying full stack')
tb = initial_tb
else:
# remove the tb_frames until the common_frame is reached (keep the current_frame tb since the line is more accurate)
while tb and tb.tb_frame != common_frame:
tb = tb.tb_next
# add all current frame elements under the common_frame to tb
current_frame = common_frame.f_back
while current_frame:
tb = Traceback(tb, current_frame, current_frame.f_lasti, current_frame.f_lineno)
current_frame = current_frame.f_back
# remove traceback root part (odoo_bin, main, loading, ...), as
# everything under the testCase is not useful. Using '_callTestMethod',
# '_callSetUp', '_callTearDown', '_callCleanup' instead of the test
# method since the error does not comme especially from the test method.
while tb:
code = tb.tb_frame.f_code
if PurePath(code.co_filename).name == 'case.py' and code.co_name in ('_callTestMethod', '_callSetUp', '_callTearDown', '_callCleanup'):
return tb.tb_next
tb = tb.tb_next
_logger.warning('No root frame found, displaying full stacks')
return initial_tb # this shouldn't be reached
class TestCase(_TestCase):
_class_cleanups = [] # needed, backport for versions < 3.8
__unittest_skip__ = False
__unittest_skip_why__ = ''
_moduleSetUpFailed = False
# pylint: disable=super-init-not-called
def __init__(self, methodName='runTest'):
"""Create an instance of the class that will use the named test
method when executed. Raises a ValueError if the instance does
not have a method with the specified name.
"""
self._testMethodName = methodName
self._outcome = None
if methodName != 'runTest' and not hasattr(self, methodName):
# we allow instantiation with no explicit method name
# but not an *incorrect* or missing method name
raise ValueError("no such test method in %s: %s" %
(self.__class__, methodName))
self._cleanups = []
self._subtest = None
# Map types to custom assertEqual functions that will compare
# instances of said type in more detail to generate a more useful
# error message.
self._type_equality_funcs = {}
self.addTypeEqualityFunc(dict, 'assertDictEqual')
self.addTypeEqualityFunc(list, 'assertListEqual')
self.addTypeEqualityFunc(tuple, 'assertTupleEqual')
self.addTypeEqualityFunc(set, 'assertSetEqual')
self.addTypeEqualityFunc(frozenset, 'assertSetEqual')
self.addTypeEqualityFunc(str, 'assertMultiLineEqual')
def addCleanup(self, function, *args, **kwargs):
"""Add a function, with arguments, to be called when the test is
completed. Functions added are called on a LIFO basis and are
called after tearDown on test failure or success.
Cleanup items are called even if setUp fails (unlike tearDown)."""
self._cleanups.append((function, args, kwargs))
@classmethod
def addClassCleanup(cls, function, *args, **kwargs):
"""Same as addCleanup, except the cleanup items are called even if
setUpClass fails (unlike tearDownClass)."""
cls._class_cleanups.append((function, args, kwargs))
def shortDescription(self):
return None
@contextlib.contextmanager
def subTest(self, msg=_subtest_msg_sentinel, **params):
"""Return a context manager that will return the enclosed block
of code in a subtest identified by the optional message and
keyword parameters. A failure in the subtest marks the test
case as failed but resumes execution at the end of the enclosed
block, allowing further test code to be executed.
"""
parent = self._subtest
if parent:
params = {**params, **{k: v for k, v in parent.params.items() if k not in params}}
self._subtest = _SubTest(self, msg, params)
try:
with self._outcome.testPartExecutor(self._subtest, isTest=True):
yield
finally:
self._subtest = parent
def _addError(self, result, test, exc_info):
"""
This method is similar to feed_errors_to_result in python<=3.10
but only manage one error at a time
This is also inspired from python 3.11 _addError but still manages
subtests errors as in python 3.7-3.10 for minimal changes.
The method remains on the test to easily override it in test_test_suite
"""
if isinstance(test, _SubTest):
result.addSubTest(test.test_case, test, exc_info)
elif exc_info is not None:
if issubclass(exc_info[0], self.failureException):
result.addFailure(test, exc_info)
else:
result.addError(test, exc_info)
def _callSetUp(self):
self.setUp()
def _callTestMethod(self, method):
method()
def _callTearDown(self):
self.tearDown()
def _callCleanup(self, function, *args, **kwargs):
function(*args, **kwargs)
def run(self, result):
result.startTest(self)
testMethod = getattr(self, self._testMethodName)
skip = False
skip_why = ''
try:
skip = self.__class__.__unittest_skip__ or testMethod.__unittest_skip__
skip_why = self.__class__.__unittest_skip_why__ or testMethod.__unittest_skip_why__ or ''
except AttributeError: # testMethod may not have a __unittest_skip__ or __unittest_skip_why__
pass
if skip:
result.addSkip(self, skip_why)
result.stopTest(self)
return
outcome = _Outcome(self, result)
try:
self._outcome = outcome
with outcome.testPartExecutor(self):
self._callSetUp()
if outcome.success:
with outcome.testPartExecutor(self, isTest=True):
self._callTestMethod(testMethod)
with outcome.testPartExecutor(self):
self._callTearDown()
self.doCleanups()
if outcome.success:
result.addSuccess(self)
return result
finally:
result.stopTest(self)
# clear the outcome, no more needed
self._outcome = None
def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after
tearDown."""
while self._cleanups:
function, args, kwargs = self._cleanups.pop()
with self._outcome.testPartExecutor(self):
self._callCleanup(function, *args, **kwargs)
@classmethod
def doClassCleanups(cls):
"""Execute all class cleanup functions. Normally called for you after
tearDownClass."""
cls.tearDown_exceptions = []
while cls._class_cleanups:
function, args, kwargs = cls._class_cleanups.pop()
try:
function(*args, **kwargs)
except Exception:
cls.tearDown_exceptions.append(sys.exc_info())
class _SubTest(TestCase):
def __init__(self, test_case, message, params):
super().__init__()
self._message = message
self.test_case = test_case
self.params = params
self.failureException = test_case.failureException
def runTest(self):
raise NotImplementedError("subtests cannot be run directly")
def _subDescription(self):
parts = []
if self._message is not _subtest_msg_sentinel:
parts.append("[{}]".format(self._message))
if self.params:
params_desc = ', '.join(
"{}={!r}".format(k, v)
for (k, v) in self.params.items())
parts.append("({})".format(params_desc))
return " ".join(parts) or '(<subtest>)'
def id(self):
return "{} {}".format(self.test_case.id(), self._subDescription())
def __str__(self):
return "{} {}".format(self.test_case, self._subDescription())