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

1"""Test case implementation""" 

2 

3import contextlib 

4import inspect 

5import logging 

6import sys 

7from pathlib import PurePath 

8from unittest import SkipTest 

9from unittest import TestCase as _TestCase 

10 

11_logger = logging.getLogger(__name__) 

12 

13 

14__unittest = True 

15 

16_subtest_msg_sentinel = object() 

17 

18 

19class _Outcome(object): 

20 def __init__(self, test, result): 

21 self.result = result 

22 self.success = True 

23 self.test = test 

24 

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 

37 

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) 

43 

44 # explicitly break a reference cycle: 

45 # exc_info -> frame -> exc_info 

46 exc_info = None 

47 

48 def _complete_traceback(self, initial_tb): 

49 Traceback = type(initial_tb) 

50 

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 

58 

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 

66 

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 

74 

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 

80 

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 

90 

91 _logger.warning('No root frame found, displaying full stacks') 

92 return initial_tb # this shouldn't be reached 

93 

94 

95class TestCase(_TestCase): 

96 _class_cleanups = [] # needed, backport for versions < 3.8 

97 __unittest_skip__ = False 

98 __unittest_skip_why__ = '' 

99 _moduleSetUpFailed = False 

100 

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 

116 

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') 

127 

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. 

132 

133 Cleanup items are called even if setUp fails (unlike tearDown).""" 

134 self._cleanups.append((function, args, kwargs)) 

135 

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)) 

141 

142 def shortDescription(self): 

143 return None 

144 

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 

162 

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 

170 

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) 

179 

180 def _callSetUp(self): 

181 self.setUp() 

182 

183 def _callTestMethod(self, method): 

184 method() 

185 

186 def _callTearDown(self): 

187 self.tearDown() 

188 

189 def _callCleanup(self, function, *args, **kwargs): 

190 function(*args, **kwargs) 

191 

192 def run(self, result): 

193 result.startTest(self) 

194 

195 testMethod = getattr(self, self._testMethodName) 

196 

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 

208 

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() 

219 

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) 

226 

227 # clear the outcome, no more needed 

228 self._outcome = None 

229 

230 def doCleanups(self): 

231 """Execute all cleanup functions. Normally called for you after 

232 tearDown.""" 

233 

234 while self._cleanups: 

235 function, args, kwargs = self._cleanups.pop() 

236 with self._outcome.testPartExecutor(self): 

237 self._callCleanup(function, *args, **kwargs) 

238 

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()) 

250 

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):] 

257 

258 module = module.replace('.', '/') 

259 return f'/{module}.py:{self.__class__.__name__}.{self._testMethodName}' 

260 

261 def get_log_metadata(self): 

262 metadata = { 

263 'canonical_tag': self.canonical_tag, 

264 } 

265 return metadata 

266 

267 

268class _SubTest(TestCase): 

269 

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 

276 

277 def runTest(self): 

278 raise NotImplementedError("subtests cannot be run directly") 

279 

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>)' 

290 

291 def id(self): 

292 return "{} {}".format(self.test_case.id(), self._subDescription()) 

293 

294 def __str__(self): 

295 return "{} {}".format(self.test_case, self._subDescription())