Skip to content

Commit 9a5d1ea

Browse files
committed
[IMP] tests: refactor unittest classes
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. Part-of: odoo#112294
1 parent 742d165 commit 9a5d1ea

File tree

15 files changed

+662
-2378
lines changed

15 files changed

+662
-2378
lines changed

debian/copyright

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,10 @@ Files: addons/web/static/lib/popper/*
321321
Copyright: 2016 Federico Zivolo
322322
License: MIT
323323

324+
Files: odoo/odoo/tests/case.py odoo/odoo/tests/result.py odoo/odoo/tests/suite.py
325+
Copyright: (c) 1999-2003 Steve Purcell; (c) 2003-2010 Python Software Foundation
326+
License: PSF
327+
324328
Files: addons/web/static/lib/py.js/*
325329
Copyright: 2012
326330
License: DWTFYW

odoo/addons/base/tests/test_mimetypes.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,3 @@ def test_mimetype_get_extension(self):
126126
self.assertEqual(get_extension('filename.not_alnum'), '')
127127
self.assertEqual(get_extension('filename.with space'), '')
128128
self.assertEqual(get_extension('filename.notAnExtension'), '')
129-
130-
131-
132-
if __name__ == '__main__':
133-
unittest.main()

odoo/addons/base/tests/test_test_suite.py

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,29 @@
44
import difflib
55
import logging
66
import re
7+
import sys
78
from contextlib import contextmanager
8-
from unittest import TestCase
9+
from unittest import SkipTest, skip
910
from unittest.mock import patch
1011

11-
from odoo.tests.common import TransactionCase
12+
from odoo.tests.common import BaseCase, TransactionCase
1213
from odoo.tests.common import users, warmup
13-
from odoo.tests.runner import OdooTestResult
14+
from odoo.tests.result import OdooTestResult
15+
from odoo.tests.case import TestCase
1416

1517
_logger = logging.getLogger(__name__)
1618

1719
from odoo.tests import MetaCase
1820

1921

20-
class TestTestSuite(TestCase, metaclass=MetaCase):
22+
if sys.version_info >= (3, 8):
23+
# this is mainly to ensure that simple tests will continue to work even if BaseCase should be used
24+
# this only works if doClassCleanup is available on testCase because of the vendoring of suite.py.
25+
# this test will only work in python 3.8 +
26+
class TestTestSuite(TestCase, metaclass=MetaCase):
2127

22-
def test_test_suite(self):
23-
""" Check that OdooSuite handles unittest.TestCase correctly. """
28+
def test_test_suite(self):
29+
""" Check that OdooSuite handles unittest.TestCase correctly. """
2430

2531

2632
class TestRunnerLoggingCommon(TransactionCase):
@@ -37,7 +43,7 @@ def setUp(self):
3743
self.expected_first_frame_methods = None
3844
return super().setUp()
3945

40-
def _feedErrorsToResult(self, result, errors):
46+
def _addError(self, result, test, exc_info):
4147
# We use this hook to catch the logged error. It is initially called
4248
# post tearDown, and logs the actual errors. Because of our hack
4349
# tests.common._ErrorCatcher, the errors are logged directly. This is
@@ -48,11 +54,10 @@ def _feedErrorsToResult(self, result, errors):
4854
self.test_result = result
4955
# while we are here, let's check that the first frame of the stack
5056
# is always inside the test method
51-
for error in errors:
52-
_, exc_info = error
53-
if exc_info:
54-
tb = exc_info[2]
55-
self._check_first_frame(tb)
57+
58+
if exc_info:
59+
tb = exc_info[2]
60+
self._check_first_frame(tb)
5661

5762
# intercept all ir_logging. We cannot use log catchers or other
5863
# fancy stuff because makeRecord is too low level.
@@ -70,7 +75,7 @@ def handle(logger, record):
7075

7176
fake_result = OdooTestResult()
7277
with patch('logging.Logger.makeRecord', makeRecord), patch('logging.Logger.handle', handle):
73-
super()._feedErrorsToResult(fake_result, errors)
78+
super()._addError(fake_result, test, exc_info)
7479

7580
self._check_log_records(log_records)
7681

@@ -112,9 +117,9 @@ def _assert_log_equal(self, log_record, key, expected):
112117
value = self._clean_message(value)
113118
if value != expected:
114119
if key != 'msg':
115-
self._log_error(f"Key `{key}` => `{value}` is not equal to `{expected}` \n {log_record['str']}")
120+
self._log_error(f"Key `{key}` => `{value}` is not equal to `{expected}` \n {log_record['msg']}")
116121
else:
117-
diff = '\n'.join(difflib.ndiff(value.splitlines(), expected.splitlines()))
122+
diff = '\n'.join(difflib.ndiff(expected.splitlines(), value.splitlines()))
118123
self._log_error(f"Key `{key}` did not matched expected:\n{diff}")
119124

120125
def _log_error(self, message):
@@ -134,6 +139,9 @@ def _clean_message(self, message):
134139

135140
class TestRunnerLogging(TestRunnerLoggingCommon):
136141

142+
def test_has_add_error(self):
143+
self.assertTrue(hasattr(self, '_addError'))
144+
137145
def test_raise(self):
138146
raise Exception('This is an error')
139147

@@ -171,6 +179,8 @@ def make_message(message):
171179
@users('__system__')
172180
@warmup
173181
def test_with_decorators(self):
182+
# note, this test may be broken with a decorator in decorator=5.0.5 since the behaviour changed
183+
# but decoratorx was not introduced yet.
174184
message = (
175185
'''ERROR: Subtest TestRunnerLogging.test_with_decorators (login='__system__')
176186
Traceback (most recent call last):
@@ -448,3 +458,72 @@ def test_raises_teardown(self):
448458
with self.subTest():
449459
raise Exception('This is a second subTest error')
450460
raise Exception('This is a test error')
461+
462+
463+
class TestSubtests(BaseCase):
464+
465+
def test_nested_subtests(self):
466+
with self.subTest(a=1, x=2):
467+
with self.subTest(b=3, x=4):
468+
self.assertEqual(self._subtest._subDescription(), '(b=3, x=4, a=1)')
469+
with self.subTest(b=5, x=6):
470+
self.assertEqual(self._subtest._subDescription(), '(b=5, x=6, a=1)')
471+
472+
473+
class TestClassSetup(BaseCase):
474+
475+
@classmethod
476+
def setUpClass(cls):
477+
raise SkipTest('Skip this class')
478+
479+
def test_method(self):
480+
pass
481+
482+
483+
class TestClassTeardown(BaseCase):
484+
485+
@classmethod
486+
def tearDownClass(cls):
487+
raise SkipTest('Skip this class')
488+
489+
def test_method(self):
490+
pass
491+
492+
493+
class Test01ClassCleanups(BaseCase):
494+
"""
495+
The purpose of this test combined with Test02ClassCleanupsCheck is to check that
496+
class cleanup work. class cleanup where introduced in python3.8 but tests should
497+
remain compatible with python 3.7
498+
"""
499+
executed = False
500+
cleanup = False
501+
502+
@classmethod
503+
def setUpClass(cls):
504+
cls.executed = True
505+
506+
def doCleanup():
507+
cls.cleanup = True
508+
cls.addClassCleanup(doCleanup)
509+
510+
def test_dummy(self):
511+
pass
512+
513+
514+
class Test02ClassCleanupsCheck(BaseCase):
515+
def test_classcleanups(self):
516+
self.assertTrue(Test01ClassCleanups.executed, "This test only makes sence when executed after Test01ClassCleanups")
517+
self.assertTrue(Test01ClassCleanups.cleanup, "TestClassCleanup shoudl have been cleanuped")
518+
519+
520+
@skip
521+
class TestSkipClass(BaseCase):
522+
def test_classcleanups(self):
523+
raise Exception('This should be skipped')
524+
525+
526+
class TestSkipMethof(BaseCase):
527+
@skip
528+
def test_skip_method(self):
529+
raise Exception('This should be skipped')

odoo/addons/base/tests/test_tests_tags.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# -*- coding: utf-8 -*-
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

4-
from odoo.tests.common import TransactionCase, tagged, TagsSelector, BaseCase
4+
from odoo.tests.common import TransactionCase, tagged, BaseCase
5+
from odoo.tests.tag_selector import TagsSelector
56

67

78
@tagged('nodatabase')

odoo/modules/loading.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
326326
if test_results and not test_results.wasSuccessful():
327327
_logger.error(
328328
"Module %s: %d failures, %d errors of %d tests",
329-
module_name, len(test_results.failures), len(test_results.errors),
329+
module_name, test_results.failures_count, test_results.errors_count,
330330
test_results.testsRun
331331
)
332332

odoo/modules/registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def init(self, db_name):
114114
self._sql_constraints = set()
115115
self._init = True
116116
self._database_translated_fields = () # names of translated fields in database
117-
self._assertion_report = odoo.tests.runner.OdooTestResult()
117+
self._assertion_report = odoo.tests.result.OdooTestResult()
118118
self._fields_by_model = None
119119
self._ordinary_tables = None
120120
self._constraint_queue = deque()

odoo/service/server.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import werkzeug.serving
2323
from werkzeug.debug import DebuggedApplication
2424

25+
from ..tests import loader
26+
2527
if os.name == 'posix':
2628
# Unix only for workers
2729
import fcntl
@@ -58,7 +60,6 @@
5860
from odoo.release import nt_service_name
5961
from odoo.tools import config
6062
from odoo.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats
61-
from ..tests import loader, runner
6263

6364
_logger = logging.getLogger(__name__)
6465

@@ -579,7 +580,7 @@ def run(self, preload=None, stop=False):
579580

580581
if stop:
581582
if config['test_enable']:
582-
logger = odoo.tests.runner._logger
583+
logger = odoo.tests.result._logger
583584
with Registry.registries._lock:
584585
for db, registry in Registry.registries.d.items():
585586
report = registry._assertion_report
@@ -1267,7 +1268,8 @@ def _reexec(updated_modules=None):
12671268
os.execve(sys.executable, args, os.environ)
12681269

12691270
def load_test_file_py(registry, test_file):
1270-
from odoo.tests.common import OdooSuite
1271+
# pylint: disable=import-outside-toplevel
1272+
from odoo.tests.suite import OdooSuite
12711273
threading.current_thread().testing = True
12721274
try:
12731275
test_path, _ = os.path.splitext(os.path.abspath(test_file))

odoo/tests/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
"""
2+
Odoo unit testing framework, based on Python unittest.
3+
4+
Some files as case.py, resut.py, suite.py are higly modified versions of unitest
5+
See https://github.com/python/cpython/tree/3.10/Lib/unittest for reference files.
6+
"""
7+
18
from . import common
29
from .common import *
310
from . import test_parse_inline_template

0 commit comments

Comments
 (0)