Odoo Test environments requires to modify many parts of the unittest TestCase, Suite and Result. The main initial reason is to **avoid to postpone result at the end of the test suite**, because even if it is convenient to have all errors visible after the tests in some case, odoo logs adds information during the execution that can be useful to debug when a test fail, to have context for an error. (see **OdooTestResult**) We are also fixing the stack trace comming from a unittest and since there is no proper way to hook inside the TestPartExecutor, a dirty hack injects anoter result on the outcome to manage the error and complete the stack trace. This was also a way to avoid to postpone subtest logs at the end of the test case (see _ErrorCatcher) `_feedErrorsToResult` was used to test the test suite behavior since there are many customization and this is quite fragile, especially if unittest changes behavior in other python version. **Python 3.11** introduced python/cpython#664448d8 That, in a way, goes in the same direction of the changed introduced with _ErrorCatcher: immediately feed errors to resut instead of postponing it. But this also removes `_feedErrorsToResult` that was used to test this behaviors, as well as other ones. Since odoo should remain multi-version, this amount of changes on the initial behavior become to complicate to keep cross-version and the (already in our mind for a while) solution to **vendor unittest** will help to simplify most of our test code base. This commit modified the vendored unittest files to simplify them as much as possible to suite our needs. Since the runner is still the unittest one, we need to inherit from unittest.Testcase in order to have the right type. This also means that we still have access to all TestCase methods without overriding them all. This is convenient for assertion methods as an example but the initial idea is to vendor our own version of TestCase to avoid having trouble to adapte our miscommunications to future python versions. A trade-off must be done to chose what should remain in our code base. The idea is to keep logic closely linked to our changes in our code base, mainly around the run method, but also addClassCleanup wich need to be vendored for python 3.7, but assertions methods are independent. Any logic can be moved fom unittest to our vendored version in the future if needed. X-original-commit: 9a5d1ea54be49e4cc8208c33e76a6bbd2414d5d0 Part-of: odoo/odoo#113850
191 lines
6.6 KiB
Python
191 lines
6.6 KiB
Python
"""
|
|
Vendor unittest.TestSuite
|
|
|
|
This is a modified version of python 3.8 unitest.TestSuite
|
|
|
|
Odoo tests customisation combined with the need of a cross version compatibility
|
|
started to make TestSuite and other unitest object more complicated than vendoring
|
|
the part we need for Odoo. This versions is simplified in order
|
|
to minimise the code to maintain
|
|
|
|
- Removes expected failure support
|
|
- Removes module setUp/tearDown support
|
|
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
|
|
from . import case
|
|
from .common import HttpCase
|
|
from .result import stats_logger
|
|
from unittest import util, BaseTestSuite, TestCase
|
|
|
|
__unittest = True
|
|
|
|
|
|
class TestSuite(BaseTestSuite):
|
|
"""A test suite is a composite test consisting of a number of TestCases.
|
|
For use, create an instance of TestSuite, then add test case instances.
|
|
When all tests have been added, the suite can be passed to a test
|
|
runner, such as TextTestRunner. It will run the individual test cases
|
|
in the order in which they were added, aggregating the results. When
|
|
subclassing, do not forget to call the base class constructor.
|
|
"""
|
|
|
|
def run(self, result, debug=False):
|
|
for test in self:
|
|
assert isinstance(test, (TestCase))
|
|
self._tearDownPreviousClass(test, result)
|
|
self._handleClassSetUp(test, result)
|
|
result._previousTestClass = test.__class__
|
|
|
|
if not test.__class__._classSetupFailed:
|
|
test(result)
|
|
|
|
self._tearDownPreviousClass(None, result)
|
|
return result
|
|
|
|
def _handleClassSetUp(self, test, result):
|
|
previousClass = result._previousTestClass
|
|
currentClass = test.__class__
|
|
if currentClass == previousClass:
|
|
return
|
|
if result._moduleSetUpFailed:
|
|
return
|
|
if currentClass.__unittest_skip__:
|
|
return
|
|
|
|
currentClass._classSetupFailed = False
|
|
|
|
try:
|
|
currentClass.setUpClass()
|
|
except Exception as e:
|
|
currentClass._classSetupFailed = True
|
|
className = util.strclass(currentClass)
|
|
self._createClassOrModuleLevelException(result, e,
|
|
'setUpClass',
|
|
className)
|
|
finally:
|
|
if currentClass._classSetupFailed is True:
|
|
currentClass.doClassCleanups()
|
|
if len(currentClass.tearDown_exceptions) > 0:
|
|
for exc in currentClass.tearDown_exceptions:
|
|
self._createClassOrModuleLevelException(
|
|
result, exc[1], 'setUpClass', className,
|
|
info=exc)
|
|
|
|
def _createClassOrModuleLevelException(self, result, exception, method_name,
|
|
parent, info=None):
|
|
errorName = f'{method_name} ({parent})'
|
|
error = _ErrorHolder(errorName)
|
|
if isinstance(exception, case.SkipTest):
|
|
result.addSkip(error, str(exception))
|
|
else:
|
|
if not info:
|
|
result.addError(error, sys.exc_info())
|
|
else:
|
|
result.addError(error, info)
|
|
|
|
def _tearDownPreviousClass(self, test, result):
|
|
previousClass = result._previousTestClass
|
|
currentClass = test.__class__
|
|
if currentClass == previousClass:
|
|
return
|
|
if not previousClass:
|
|
return
|
|
if previousClass._classSetupFailed:
|
|
return
|
|
if previousClass.__unittest_skip__:
|
|
return
|
|
try:
|
|
previousClass.tearDownClass()
|
|
except Exception as e:
|
|
className = util.strclass(previousClass)
|
|
self._createClassOrModuleLevelException(result, e,
|
|
'tearDownClass',
|
|
className)
|
|
finally:
|
|
previousClass.doClassCleanups()
|
|
if len(previousClass.tearDown_exceptions) > 0:
|
|
for exc in previousClass.tearDown_exceptions:
|
|
className = util.strclass(previousClass)
|
|
self._createClassOrModuleLevelException(result, exc[1],
|
|
'tearDownClass',
|
|
className,
|
|
info=exc)
|
|
|
|
|
|
class _ErrorHolder(object):
|
|
"""
|
|
Placeholder for a TestCase inside a result. As far as a TestResult
|
|
is concerned, this looks exactly like a unit test. Used to insert
|
|
arbitrary errors into a test suite run.
|
|
"""
|
|
# Inspired by the ErrorHolder from Twisted:
|
|
# http://twistedmatrix.com/trac/browser/trunk/twisted/trial/runner.py
|
|
|
|
# attribute used by TestResult._exc_info_to_string
|
|
failureException = None
|
|
|
|
def __init__(self, description):
|
|
self.description = description
|
|
|
|
def id(self):
|
|
return self.description
|
|
|
|
def shortDescription(self):
|
|
return None
|
|
|
|
def __repr__(self):
|
|
return "<ErrorHolder description=%r>" % (self.description,)
|
|
|
|
def __str__(self):
|
|
return self.id()
|
|
|
|
def run(self, result):
|
|
# could call result.addError(...) - but this test-like object
|
|
# shouldn't be run anyway
|
|
pass
|
|
|
|
def __call__(self, result):
|
|
return self.run(result)
|
|
|
|
def countTestCases(self):
|
|
return 0
|
|
|
|
|
|
class OdooSuite(TestSuite):
|
|
def _handleClassSetUp(self, test, result):
|
|
previous_test_class = result._previousTestClass
|
|
if not (
|
|
previous_test_class != type(test)
|
|
and hasattr(result, 'stats')
|
|
and stats_logger.isEnabledFor(logging.INFO)
|
|
):
|
|
super()._handleClassSetUp(test, result)
|
|
return
|
|
|
|
test_class = type(test)
|
|
test_id = f'{test_class.__module__}.{test_class.__qualname__}.setUpClass'
|
|
with result.collectStats(test_id):
|
|
super()._handleClassSetUp(test, result)
|
|
|
|
def _tearDownPreviousClass(self, test, result):
|
|
previous_test_class = result._previousTestClass
|
|
if not (
|
|
previous_test_class
|
|
and previous_test_class != type(test)
|
|
and hasattr(result, 'stats')
|
|
and stats_logger.isEnabledFor(logging.INFO)
|
|
):
|
|
super()._tearDownPreviousClass(test, result)
|
|
return
|
|
|
|
test_id = f'{previous_test_class.__module__}.{previous_test_class.__qualname__}.tearDownClass'
|
|
with result.collectStats(test_id):
|
|
super()._tearDownPreviousClass(test, result)
|
|
|
|
def has_http_case(self):
|
|
return self.countTestCases() and any(isinstance(test_case, HttpCase) for test_case in self)
|