Coverage for adhoc-cicd-odoo-odoo / odoo / tests / case.py: 52%
190 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:15 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:15 +0000
1"""Test case implementation"""
3import contextlib
4import inspect
5import logging
6import sys
7from pathlib import PurePath
8from unittest import SkipTest
9from unittest import TestCase as _TestCase
11_logger = logging.getLogger(__name__)
14__unittest = True
16_subtest_msg_sentinel = object()
19class _Outcome(object):
20 def __init__(self, test, result):
21 self.result = result
22 self.success = True
23 self.test = test
25 @contextlib.contextmanager
26 def testPartExecutor(self, test_case, isTest=False):
27 try:
28 yield
29 except KeyboardInterrupt:
30 raise
31 except SkipTest as e:
32 self.success = False
33 self.result.addSkip(test_case, str(e))
34 except: # pylint: disable=bare-except
35 exc_info = sys.exc_info()
36 self.success = False
38 if exc_info is not None:
39 exception_type, exception, tb = exc_info
40 tb = self._complete_traceback(tb)
41 exc_info = (exception_type, exception, tb)
42 self.test._addError(self.result, test_case, exc_info)
44 # explicitly break a reference cycle:
45 # exc_info -> frame -> exc_info
46 exc_info = None
48 def _complete_traceback(self, initial_tb):
49 Traceback = type(initial_tb)
51 # make the set of frames in the traceback
52 tb_frames = set()
53 tb = initial_tb
54 while tb:
55 tb_frames.add(tb.tb_frame)
56 tb = tb.tb_next
57 tb = initial_tb
59 # find the common frame by searching the last frame of the current_stack present in the traceback.
60 current_frame = inspect.currentframe()
61 common_frame = None
62 while current_frame:
63 if current_frame in tb_frames:
64 common_frame = current_frame # we want to find the last frame in common
65 current_frame = current_frame.f_back
67 if not common_frame: # not really useful but safer
68 _logger.warning('No common frame found with current stack, displaying full stack')
69 tb = initial_tb
70 else:
71 # remove the tb_frames until the common_frame is reached (keep the current_frame tb since the line is more accurate)
72 while tb and tb.tb_frame != common_frame:
73 tb = tb.tb_next
75 # add all current frame elements under the common_frame to tb
76 current_frame = common_frame.f_back
77 while current_frame:
78 tb = Traceback(tb, current_frame, current_frame.f_lasti, current_frame.f_lineno)
79 current_frame = current_frame.f_back
81 # remove traceback root part (odoo_bin, main, loading, ...), as
82 # everything under the testCase is not useful. Using '_callTestMethod',
83 # '_callSetUp', '_callTearDown', '_callCleanup' instead of the test
84 # method since the error does not comme especially from the test method.
85 while tb:
86 code = tb.tb_frame.f_code
87 if PurePath(code.co_filename).name == 'case.py' and code.co_name in ('_callTestMethod', '_callSetUp', '_callTearDown', '_callCleanup'):
88 return tb.tb_next
89 tb = tb.tb_next
91 _logger.warning('No root frame found, displaying full stacks')
92 return initial_tb # this shouldn't be reached
95class TestCase(_TestCase):
96 _class_cleanups = [] # needed, backport for versions < 3.8
97 __unittest_skip__ = False
98 __unittest_skip_why__ = ''
99 _moduleSetUpFailed = False
101 # pylint: disable=super-init-not-called
102 def __init__(self, methodName='runTest'):
103 """Create an instance of the class that will use the named test
104 method when executed. Raises a ValueError if the instance does
105 not have a method with the specified name.
106 """
107 self._testMethodName = methodName
108 self._outcome = None
109 if methodName != 'runTest' and not hasattr(self, methodName): 109 ↛ 112line 109 didn't jump to line 112 because the condition on line 109 was never true
110 # we allow instantiation with no explicit method name
111 # but not an *incorrect* or missing method name
112 raise ValueError("no such test method in %s: %s" %
113 (self.__class__, methodName))
114 self._cleanups = []
115 self._subtest = None
117 # Map types to custom assertEqual functions that will compare
118 # instances of said type in more detail to generate a more useful
119 # error message.
120 self._type_equality_funcs = {}
121 self.addTypeEqualityFunc(dict, 'assertDictEqual')
122 self.addTypeEqualityFunc(list, 'assertListEqual')
123 self.addTypeEqualityFunc(tuple, 'assertTupleEqual')
124 self.addTypeEqualityFunc(set, 'assertSetEqual')
125 self.addTypeEqualityFunc(frozenset, 'assertSetEqual')
126 self.addTypeEqualityFunc(str, 'assertMultiLineEqual')
128 def addCleanup(self, function, *args, **kwargs):
129 """Add a function, with arguments, to be called when the test is
130 completed. Functions added are called on a LIFO basis and are
131 called after tearDown on test failure or success.
133 Cleanup items are called even if setUp fails (unlike tearDown)."""
134 self._cleanups.append((function, args, kwargs))
136 @classmethod
137 def addClassCleanup(cls, function, *args, **kwargs):
138 """Same as addCleanup, except the cleanup items are called even if
139 setUpClass fails (unlike tearDownClass)."""
140 cls._class_cleanups.append((function, args, kwargs))
142 def shortDescription(self):
143 return None
145 @contextlib.contextmanager
146 def subTest(self, msg=_subtest_msg_sentinel, **params):
147 """Return a context manager that will return the enclosed block
148 of code in a subtest identified by the optional message and
149 keyword parameters. A failure in the subtest marks the test
150 case as failed but resumes execution at the end of the enclosed
151 block, allowing further test code to be executed.
152 """
153 parent = self._subtest
154 if parent: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true
155 params = {**params, **{k: v for k, v in parent.params.items() if k not in params}}
156 self._subtest = _SubTest(self, msg, params)
157 try:
158 with self._outcome.testPartExecutor(self._subtest, isTest=True):
159 yield
160 finally:
161 self._subtest = parent
163 def _addError(self, result, test, exc_info):
164 """
165 This method is similar to feed_errors_to_result in python<=3.10
166 but only manage one error at a time
167 This is also inspired from python 3.11 _addError but still manages
168 subtests errors as in python 3.7-3.10 for minimal changes.
169 The method remains on the test to easily override it in test_test_suite
171 """
172 if isinstance(test, _SubTest):
173 result.addSubTest(test.test_case, test, exc_info)
174 elif exc_info is not None:
175 if issubclass(exc_info[0], self.failureException):
176 result.addFailure(test, exc_info)
177 else:
178 result.addError(test, exc_info)
180 def _callSetUp(self):
181 self.setUp()
183 def _callTestMethod(self, method):
184 method()
186 def _callTearDown(self):
187 self.tearDown()
189 def _callCleanup(self, function, *args, **kwargs):
190 function(*args, **kwargs)
192 def run(self, result):
193 result.startTest(self)
195 testMethod = getattr(self, self._testMethodName)
197 skip = False
198 skip_why = ''
199 try:
200 skip = self.__class__.__unittest_skip__ or testMethod.__unittest_skip__
201 skip_why = self.__class__.__unittest_skip_why__ or testMethod.__unittest_skip_why__ or ''
202 except AttributeError: # testMethod may not have a __unittest_skip__ or __unittest_skip_why__
203 pass
204 if skip: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 result.addSkip(self, skip_why)
206 result.stopTest(self)
207 return
209 outcome = _Outcome(self, result)
210 try:
211 self._outcome = outcome
212 with outcome.testPartExecutor(self):
213 self._callSetUp()
214 if outcome.success: 214 ↛ 220line 214 didn't jump to line 220 because the condition on line 214 was always true
215 with outcome.testPartExecutor(self, isTest=True):
216 self._callTestMethod(testMethod)
217 with outcome.testPartExecutor(self):
218 self._callTearDown()
220 self.doCleanups()
221 if outcome.success: 221 ↛ 223line 221 didn't jump to line 223 because the condition on line 221 was always true
222 result.addSuccess(self)
223 return result
224 finally:
225 result.stopTest(self)
227 # clear the outcome, no more needed
228 self._outcome = None
230 def doCleanups(self):
231 """Execute all cleanup functions. Normally called for you after
232 tearDown."""
234 while self._cleanups:
235 function, args, kwargs = self._cleanups.pop()
236 with self._outcome.testPartExecutor(self):
237 self._callCleanup(function, *args, **kwargs)
239 @classmethod
240 def doClassCleanups(cls):
241 """Execute all class cleanup functions. Normally called for you after
242 tearDownClass."""
243 cls.tearDown_exceptions = []
244 while cls._class_cleanups:
245 function, args, kwargs = cls._class_cleanups.pop()
246 try:
247 function(*args, **kwargs)
248 except Exception:
249 cls.tearDown_exceptions.append(sys.exc_info())
251 @property
252 def canonical_tag(self):
253 module = self.__module__
254 for prefix in ('odoo.addons.', 'odoo.upgrade.'):
255 if module.startswith(prefix):
256 module = module[len(prefix):]
258 module = module.replace('.', '/')
259 return f'/{module}.py:{self.__class__.__name__}.{self._testMethodName}'
261 def get_log_metadata(self):
262 metadata = {
263 'canonical_tag': self.canonical_tag,
264 }
265 return metadata
268class _SubTest(TestCase):
270 def __init__(self, test_case, message, params):
271 super().__init__()
272 self._message = message
273 self.test_case = test_case
274 self.params = params
275 self.failureException = test_case.failureException
277 def runTest(self):
278 raise NotImplementedError("subtests cannot be run directly")
280 def _subDescription(self):
281 parts = []
282 if self._message is not _subtest_msg_sentinel:
283 parts.append("[{}]".format(self._message))
284 if self.params:
285 params_desc = ', '.join(
286 "{}={!r}".format(k, v)
287 for (k, v) in self.params.items())
288 parts.append("({})".format(params_desc))
289 return " ".join(parts) or '(<subtest>)'
291 def id(self):
292 return "{} {}".format(self.test_case.id(), self._subDescription())
294 def __str__(self):
295 return "{} {}".format(self.test_case, self._subDescription())