odoo_17.0.1/odoo/addons/base/tests/test_test_suite.py

533 lines
18 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import contextlib
import difflib
import logging
import re
import sys
from contextlib import contextmanager
from pathlib import PurePath
from unittest import SkipTest, skip
from unittest.mock import patch
from odoo.tests.case import TestCase
from odoo.tests.common import BaseCase, TransactionCase, users, warmup
from odoo.tests.result import OdooTestResult
_logger = logging.getLogger(__name__)
from odoo.tests import MetaCase
if sys.version_info >= (3, 8):
# this is mainly to ensure that simple tests will continue to work even if BaseCase should be used
# this only works if doClassCleanup is available on testCase because of the vendoring of suite.py.
# this test will only work in python 3.8 +
class TestTestSuite(TestCase, metaclass=MetaCase):
def test_test_suite(self):
""" Check that OdooSuite handles unittest.TestCase correctly. """
class TestRunnerLoggingCommon(TransactionCase):
"""
The purpose of this class is to do some "metatesting": it actually checks
that on error, the runner logged the error with the right file reference.
This is mainly to avoid having errors in test/common.py or test/runner.py`.
This kind of metatesting is tricky; in this case the logs are made outside
of the test method, after the teardown actually.
"""
def setUp(self):
self.expected_logs = None
self.expected_first_frame_methods = None
return super().setUp()
def _addError(self, result, test, exc_info):
# We use this hook to catch the logged error. It is initially called
# post tearDown, and logs the actual errors. Because of our hack
# tests.common._ErrorCatcher, the errors are logged directly. This is
# still useful to test errors raised from tests. We cannot assert what
# was logged after the test inside the test, though. This method can be
# temporary renamed to test the real failure.
try:
self.test_result = result
# while we are here, let's check that the first frame of the stack
# is always inside the test method
if exc_info:
tb = exc_info[2]
self._check_first_frame(tb)
# intercept all ir_logging. We cannot use log catchers or other
# fancy stuff because makeRecord is too low level.
log_records = []
def makeRecord(logger, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None):
log_records.append({
'logger': logger, 'name': name, 'level': level, 'fn': fn, 'lno': lno,
'msg': msg % args, 'exc_info': exc_info, 'func': func, 'extra': extra, 'sinfo': sinfo,
})
def handle(logger, record):
# disable error logging
return
fake_result = OdooTestResult()
with patch('logging.Logger.makeRecord', makeRecord), patch('logging.Logger.handle', handle):
super()._addError(fake_result, test, exc_info)
self._check_log_records(log_records)
except Exception as e:
# we don't expect _feedErrorsToResult() to raise any exception, this
# will make it more robust to future changes and eventual mistakes
_logger.exception(e)
def _check_first_frame(self, tb):
""" Check that the first frame of the given traceback is the expected method name. """
# the list expected_first_frame_methods allow to define a list of first
# expected frame (useful for setup/teardown tests)
if self.expected_first_frame_methods is None:
expected_first_frame_method = self._testMethodName
else:
expected_first_frame_method = self.expected_first_frame_methods.pop(0)
first_frame_method = tb.tb_frame.f_code.co_name
if first_frame_method != expected_first_frame_method:
self._log_error(f"Checking first tb frame: {first_frame_method} is not equal to {expected_first_frame_method}")
def _check_log_records(self, log_records):
""" Check that what was logged is what was expected. """
for log_record in log_records:
self._assert_log_equal(log_record, 'logger', _logger)
self._assert_log_equal(log_record, 'name', 'odoo.addons.base.tests.test_test_suite')
self._assert_log_equal(log_record, 'fn', __file__)
self._assert_log_equal(log_record, 'func', self._testMethodName)
if self.expected_logs is not None:
for log_record in log_records:
level, msg = self.expected_logs.pop(0)
self._assert_log_equal(log_record, 'level', level)
self._assert_log_equal(log_record, 'msg', msg)
def _assert_log_equal(self, log_record, key, expected):
""" Check the content of a log record. """
value = log_record[key]
if key == 'msg':
value = self._clean_message(value)
if value != expected:
if key != 'msg':
self._log_error(f"Key `{key}` => `{value}` is not equal to `{expected}` \n {log_record['msg']}")
else:
diff = '\n'.join(difflib.ndiff(expected.splitlines(), value.splitlines()))
self._log_error(f"Key `{key}` did not matched expected:\n{diff}")
def _log_error(self, message):
""" Log an actual error (about a log in a test that doesn't match expectations) """
# we would just log, but using the test_result will help keeping the tests counters correct
self.test_result.addError(self, (AssertionError, AssertionError(message), None))
def _clean_message(self, message):
root_path = PurePath(__file__).parents[4] # removes /odoo/addons/base/tests/test_test_suite.py
python_path = PurePath(contextlib.__file__).parent # /usr/lib/pythonx.x, C:\\python\\Lib, ...
message = re.sub(r'line \d+', 'line $line', message)
message = re.sub(r'py:\d+', 'py:$line', message)
message = re.sub(r'decorator-gen-\d+', 'decorator-gen-xxx', message)
message = message.replace(f'"{root_path}', '"/root_path/odoo')
message = message.replace(f'"{python_path}', '"/usr/lib/python')
message = message.replace('\\', '/')
return message
class TestRunnerLogging(TestRunnerLoggingCommon):
def test_has_add_error(self):
self.assertTrue(hasattr(self, '_addError'))
def test_raise(self):
raise Exception('This is an error')
def test_raise_subtest(self):
"""
with subtest, we expect to have multiple errors, one per subtest
"""
def make_message(message):
return (
f'''ERROR: Subtest TestRunnerLogging.test_raise_subtest (<subtest>)
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_raise_subtest
raise Exception('{message}')
Exception: {message}
''')
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, make_message('This is an error')),
]
with self.subTest():
raise Exception('This is an error')
self.assertFalse(self.expected_logs, "Error should have been logged immediatly")
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, make_message('This is an error2')),
]
with self.subTest():
raise Exception('This is an error2')
self.assertFalse(self.expected_logs, "Error should have been logged immediatly")
@users('__system__')
@warmup
def test_with_decorators(self):
# note, this test may be broken with a decorator in decorator=5.0.5 since the behaviour changed
# but decoratorx was not introduced yet.
message = (
'''ERROR: Subtest TestRunnerLogging.test_with_decorators (login='__system__')
Traceback (most recent call last):
File "<decorator-gen-xxx>", line $line, in test_with_decorators
File "/root_path/odoo/odoo/tests/common.py", line $line, in _users
func(*args, **kwargs)
File "<decorator-gen-xxx>", line $line, in test_with_decorators
File "/root_path/odoo/odoo/tests/common.py", line $line, in warmup
func(*args, **kwargs)
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_with_decorators
raise Exception('This is an error')
Exception: This is an error
''')
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),
]
raise Exception('This is an error')
def test_traverse_contextmanager(self):
@contextmanager
def assertSomething():
yield
raise Exception('This is an error')
with assertSomething():
pass
def test_subtest_sub_call(self):
def func():
with self.subTest():
raise Exception('This is an error')
func()
def test_call_stack(self):
message = (
'''ERROR: TestRunnerLogging.test_call_stack
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_call_stack
alpha()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in alpha
beta()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in beta
gamma()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in gamma
raise Exception('This is an error')
Exception: This is an error
''')
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),
]
def alpha():
beta()
def beta():
gamma()
def gamma():
raise Exception('This is an error')
alpha()
def test_call_stack_context_manager(self):
message = (
'''ERROR: TestRunnerLogging.test_call_stack_context_manager
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_call_stack_context_manager
alpha()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in alpha
beta()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in beta
gamma()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in gamma
raise Exception('This is an error')
Exception: This is an error
''')
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),
]
def alpha():
beta()
def beta():
with self.with_user('admin'):
gamma()
return 0
def gamma():
raise Exception('This is an error')
alpha()
def test_call_stack_subtest(self):
message = (
'''ERROR: Subtest TestRunnerLogging.test_call_stack_subtest (<subtest>)
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_call_stack_subtest
alpha()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in alpha
beta()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in beta
gamma()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in gamma
raise Exception('This is an error')
Exception: This is an error
''')
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),
]
def alpha():
beta()
def beta():
with self.subTest():
gamma()
def gamma():
raise Exception('This is an error')
alpha()
def test_assertQueryCount(self):
message = (
'''FAIL: Subtest TestRunnerLogging.test_assertQueryCount (<subtest>)
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_assertQueryCount
with self.assertQueryCount(system=0):
File "/usr/lib/python/contextlib.py", line $line, in __exit__
next(self.gen)
File "/root_path/odoo/odoo/tests/common.py", line $line, in assertQueryCount
self.fail(msg % (login, count, expected, funcname, filename, linenum))
AssertionError: Query count more than expected for user __system__: 1 > 0 in test_assertQueryCount at base/tests/test_test_suite.py:$line
''')
if self._python_version < (3, 10, 0):
message = message.replace("with self.assertQueryCount(system=0):", "self.env.cr.execute('SELECT 1')")
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),
]
with self.assertQueryCount(system=0):
self.env.cr.execute('SELECT 1')
@users('__system__')
@warmup
def test_assertQueryCount_with_decorators(self):
with self.assertQueryCount(system=0):
self.env.cr.execute('SELECT 1')
def test_reraise(self):
message = (
'''ERROR: TestRunnerLogging.test_reraise
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_reraise
alpha()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in alpha
beta()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in beta
raise Exception('This is an error')
Exception: This is an error
''')
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),
]
def alpha():
# pylint: disable=try-except-raise
try:
beta()
except Exception:
raise
def beta():
raise Exception('This is an error')
alpha()
def test_handle_error(self):
message = (
'''ERROR: TestRunnerLogging.test_handle_error
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in alpha
beta()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in beta
raise Exception('This is an error')
Exception: This is an error
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in test_handle_error
alpha()
File "/root_path/odoo/odoo/addons/base/tests/test_test_suite.py", line $line, in alpha
raise Exception('This is an error2')
Exception: This is an error2
''')
self.expected_logs = [
(logging.INFO, '=' * 70),
(logging.ERROR, message),
]
def alpha():
try:
beta()
except Exception:
raise Exception('This is an error2')
def beta():
raise Exception('This is an error')
alpha()
class TestRunnerLoggingSetup(TestRunnerLoggingCommon):
def setUp(self):
super().setUp()
self.expected_first_frame_methods = [
'setUp',
'cleanupError2',
'cleanupError',
]
def cleanupError():
raise Exception("This is a cleanup error")
self.addCleanup(cleanupError)
def cleanupError2():
raise Exception("This is a second cleanup error")
self.addCleanup(cleanupError2)
raise Exception('This is a setup error')
def test_raises_setup(self):
_logger.error("This shouldn't be executed")
def tearDown(self):
_logger.error("This shouldn't be executed since setup failed")
class TestRunnerLoggingTeardown(TestRunnerLoggingCommon):
def setUp(self):
super().setUp()
self.expected_first_frame_methods = [
'test_raises_teardown',
'test_raises_teardown',
'test_raises_teardown',
'tearDown',
'cleanupError2',
'cleanupError',
]
def cleanupError():
raise Exception("This is a cleanup error")
self.addCleanup(cleanupError)
def cleanupError2():
raise Exception("This is a second cleanup error")
self.addCleanup(cleanupError2)
def tearDown(self):
raise Exception('This is a tearDown error')
def test_raises_teardown(self):
with self.subTest():
raise Exception('This is a subTest error')
with self.subTest():
raise Exception('This is a second subTest error')
raise Exception('This is a test error')
class TestSubtests(BaseCase):
def test_nested_subtests(self):
with self.subTest(a=1, x=2):
with self.subTest(b=3, x=4):
self.assertEqual(self._subtest._subDescription(), '(b=3, x=4, a=1)')
with self.subTest(b=5, x=6):
self.assertEqual(self._subtest._subDescription(), '(b=5, x=6, a=1)')
class TestClassSetup(BaseCase):
@classmethod
def setUpClass(cls):
raise SkipTest('Skip this class')
def test_method(self):
pass
class TestClassTeardown(BaseCase):
@classmethod
def tearDownClass(cls):
raise SkipTest('Skip this class')
def test_method(self):
pass
class Test01ClassCleanups(BaseCase):
"""
The purpose of this test combined with Test02ClassCleanupsCheck is to check that
class cleanup work. class cleanup where introduced in python3.8 but tests should
remain compatible with python 3.7
"""
executed = False
cleanup = False
@classmethod
def setUpClass(cls):
cls.executed = True
def doCleanup():
cls.cleanup = True
cls.addClassCleanup(doCleanup)
def test_dummy(self):
pass
class Test02ClassCleanupsCheck(BaseCase):
def test_classcleanups(self):
self.assertTrue(Test01ClassCleanups.executed, "This test only makes sence when executed after Test01ClassCleanups")
self.assertTrue(Test01ClassCleanups.cleanup, "TestClassCleanup shoudl have been cleanuped")
@skip
class TestSkipClass(BaseCase):
def test_classcleanups(self):
raise Exception('This should be skipped')
class TestSkipMethof(BaseCase):
@skip
def test_skip_method(self):
raise Exception('This should be skipped')