Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/unittest.py: 29%

248 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""Discover and run std-library "unittest" style tests.""" 

2import sys 

3import traceback 

4import types 

5from typing import Any 

6from typing import Callable 

7from typing import Generator 

8from typing import Iterable 

9from typing import List 

10from typing import Optional 

11from typing import Tuple 

12from typing import Type 

13from typing import TYPE_CHECKING 

14from typing import Union 

15 

16import _pytest._code 

17import pytest 

18from _pytest.compat import getimfunc 

19from _pytest.compat import is_async_function 

20from _pytest.config import hookimpl 

21from _pytest.fixtures import FixtureRequest 

22from _pytest.nodes import Collector 

23from _pytest.nodes import Item 

24from _pytest.outcomes import exit 

25from _pytest.outcomes import fail 

26from _pytest.outcomes import skip 

27from _pytest.outcomes import xfail 

28from _pytest.python import Class 

29from _pytest.python import Function 

30from _pytest.python import Module 

31from _pytest.runner import CallInfo 

32from _pytest.scope import Scope 

33 

34if TYPE_CHECKING: 

35 import unittest 

36 import twisted.trial.unittest 

37 

38 _SysExcInfoType = Union[ 

39 Tuple[Type[BaseException], BaseException, types.TracebackType], 

40 Tuple[None, None, None], 

41 ] 

42 

43 

44def pytest_pycollect_makeitem( 

45 collector: Union[Module, Class], name: str, obj: object 

46) -> Optional["UnitTestCase"]: 

47 # Has unittest been imported and is obj a subclass of its TestCase? 

48 try: 

49 ut = sys.modules["unittest"] 

50 # Type ignored because `ut` is an opaque module. 

51 if not issubclass(obj, ut.TestCase): # type: ignore 

52 return None 

53 except Exception: 

54 return None 

55 # Yes, so let's collect it. 

56 item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj) 

57 return item 

58 

59 

60class UnitTestCase(Class): 

61 # Marker for fixturemanger.getfixtureinfo() 

62 # to declare that our children do not support funcargs. 

63 nofuncargs = True 

64 

65 def collect(self) -> Iterable[Union[Item, Collector]]: 

66 from unittest import TestLoader 

67 

68 cls = self.obj 

69 if not getattr(cls, "__test__", True): 

70 return 

71 

72 skipped = _is_skipped(cls) 

73 if not skipped: 

74 self._inject_setup_teardown_fixtures(cls) 

75 self._inject_setup_class_fixture() 

76 

77 self.session._fixturemanager.parsefactories(self, unittest=True) 

78 loader = TestLoader() 

79 foundsomething = False 

80 for name in loader.getTestCaseNames(self.obj): 

81 x = getattr(self.obj, name) 

82 if not getattr(x, "__test__", True): 

83 continue 

84 funcobj = getimfunc(x) 

85 yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj) 

86 foundsomething = True 

87 

88 if not foundsomething: 

89 runtest = getattr(self.obj, "runTest", None) 

90 if runtest is not None: 

91 ut = sys.modules.get("twisted.trial.unittest", None) 

92 # Type ignored because `ut` is an opaque module. 

93 if ut is None or runtest != ut.TestCase.runTest: # type: ignore 

94 yield TestCaseFunction.from_parent(self, name="runTest") 

95 

96 def _inject_setup_teardown_fixtures(self, cls: type) -> None: 

97 """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding 

98 teardown functions (#517).""" 

99 class_fixture = _make_xunit_fixture( 

100 cls, 

101 "setUpClass", 

102 "tearDownClass", 

103 "doClassCleanups", 

104 scope=Scope.Class, 

105 pass_self=False, 

106 ) 

107 if class_fixture: 

108 cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] 

109 

110 method_fixture = _make_xunit_fixture( 

111 cls, 

112 "setup_method", 

113 "teardown_method", 

114 None, 

115 scope=Scope.Function, 

116 pass_self=True, 

117 ) 

118 if method_fixture: 

119 cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] 

120 

121 

122def _make_xunit_fixture( 

123 obj: type, 

124 setup_name: str, 

125 teardown_name: str, 

126 cleanup_name: Optional[str], 

127 scope: Scope, 

128 pass_self: bool, 

129): 

130 setup = getattr(obj, setup_name, None) 

131 teardown = getattr(obj, teardown_name, None) 

132 if setup is None and teardown is None: 

133 return None 

134 

135 if cleanup_name: 

136 cleanup = getattr(obj, cleanup_name, lambda *args: None) 

137 else: 

138 

139 def cleanup(*args): 

140 pass 

141 

142 @pytest.fixture( 

143 scope=scope.value, 

144 autouse=True, 

145 # Use a unique name to speed up lookup. 

146 name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}", 

147 ) 

148 def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: 

149 if _is_skipped(self): 

150 reason = self.__unittest_skip_why__ 

151 raise pytest.skip.Exception(reason, _use_item_location=True) 

152 if setup is not None: 

153 try: 

154 if pass_self: 

155 setup(self, request.function) 

156 else: 

157 setup() 

158 # unittest does not call the cleanup function for every BaseException, so we 

159 # follow this here. 

160 except Exception: 

161 if pass_self: 

162 cleanup(self) 

163 else: 

164 cleanup() 

165 

166 raise 

167 yield 

168 try: 

169 if teardown is not None: 

170 if pass_self: 

171 teardown(self, request.function) 

172 else: 

173 teardown() 

174 finally: 

175 if pass_self: 

176 cleanup(self) 

177 else: 

178 cleanup() 

179 

180 return fixture 

181 

182 

183class TestCaseFunction(Function): 

184 nofuncargs = True 

185 _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None 

186 _testcase: Optional["unittest.TestCase"] = None 

187 

188 def _getobj(self): 

189 assert self.parent is not None 

190 # Unlike a regular Function in a Class, where `item.obj` returns 

191 # a *bound* method (attached to an instance), TestCaseFunction's 

192 # `obj` returns an *unbound* method (not attached to an instance). 

193 # This inconsistency is probably not desirable, but needs some 

194 # consideration before changing. 

195 return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] 

196 

197 def setup(self) -> None: 

198 # A bound method to be called during teardown() if set (see 'runtest()'). 

199 self._explicit_tearDown: Optional[Callable[[], None]] = None 

200 assert self.parent is not None 

201 self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] 

202 self._obj = getattr(self._testcase, self.name) 

203 if hasattr(self, "_request"): 

204 self._request._fillfixtures() 

205 

206 def teardown(self) -> None: 

207 if self._explicit_tearDown is not None: 

208 self._explicit_tearDown() 

209 self._explicit_tearDown = None 

210 self._testcase = None 

211 self._obj = None 

212 

213 def startTest(self, testcase: "unittest.TestCase") -> None: 

214 pass 

215 

216 def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: 

217 # Unwrap potential exception info (see twisted trial support below). 

218 rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) 

219 try: 

220 excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(rawexcinfo) # type: ignore[arg-type] 

221 # Invoke the attributes to trigger storing the traceback 

222 # trial causes some issue there. 

223 excinfo.value 

224 excinfo.traceback 

225 except TypeError: 

226 try: 

227 try: 

228 values = traceback.format_exception(*rawexcinfo) 

229 values.insert( 

230 0, 

231 "NOTE: Incompatible Exception Representation, " 

232 "displaying natively:\n\n", 

233 ) 

234 fail("".join(values), pytrace=False) 

235 except (fail.Exception, KeyboardInterrupt): 

236 raise 

237 except BaseException: 

238 fail( 

239 "ERROR: Unknown Incompatible Exception " 

240 "representation:\n%r" % (rawexcinfo,), 

241 pytrace=False, 

242 ) 

243 except KeyboardInterrupt: 

244 raise 

245 except fail.Exception: 

246 excinfo = _pytest._code.ExceptionInfo.from_current() 

247 self.__dict__.setdefault("_excinfo", []).append(excinfo) 

248 

249 def addError( 

250 self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" 

251 ) -> None: 

252 try: 

253 if isinstance(rawexcinfo[1], exit.Exception): 

254 exit(rawexcinfo[1].msg) 

255 except TypeError: 

256 pass 

257 self._addexcinfo(rawexcinfo) 

258 

259 def addFailure( 

260 self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" 

261 ) -> None: 

262 self._addexcinfo(rawexcinfo) 

263 

264 def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None: 

265 try: 

266 raise pytest.skip.Exception(reason, _use_item_location=True) 

267 except skip.Exception: 

268 self._addexcinfo(sys.exc_info()) 

269 

270 def addExpectedFailure( 

271 self, 

272 testcase: "unittest.TestCase", 

273 rawexcinfo: "_SysExcInfoType", 

274 reason: str = "", 

275 ) -> None: 

276 try: 

277 xfail(str(reason)) 

278 except xfail.Exception: 

279 self._addexcinfo(sys.exc_info()) 

280 

281 def addUnexpectedSuccess( 

282 self, 

283 testcase: "unittest.TestCase", 

284 reason: Optional["twisted.trial.unittest.Todo"] = None, 

285 ) -> None: 

286 msg = "Unexpected success" 

287 if reason: 

288 msg += f": {reason.reason}" 

289 # Preserve unittest behaviour - fail the test. Explicitly not an XPASS. 

290 try: 

291 fail(msg, pytrace=False) 

292 except fail.Exception: 

293 self._addexcinfo(sys.exc_info()) 

294 

295 def addSuccess(self, testcase: "unittest.TestCase") -> None: 

296 pass 

297 

298 def stopTest(self, testcase: "unittest.TestCase") -> None: 

299 pass 

300 

301 def runtest(self) -> None: 

302 from _pytest.debugging import maybe_wrap_pytest_function_for_tracing 

303 

304 assert self._testcase is not None 

305 

306 maybe_wrap_pytest_function_for_tracing(self) 

307 

308 # Let the unittest framework handle async functions. 

309 if is_async_function(self.obj): 

310 # Type ignored because self acts as the TestResult, but is not actually one. 

311 self._testcase(result=self) # type: ignore[arg-type] 

312 else: 

313 # When --pdb is given, we want to postpone calling tearDown() otherwise 

314 # when entering the pdb prompt, tearDown() would have probably cleaned up 

315 # instance variables, which makes it difficult to debug. 

316 # Arguably we could always postpone tearDown(), but this changes the moment where the 

317 # TestCase instance interacts with the results object, so better to only do it 

318 # when absolutely needed. 

319 # We need to consider if the test itself is skipped, or the whole class. 

320 assert isinstance(self.parent, UnitTestCase) 

321 skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj) 

322 if self.config.getoption("usepdb") and not skipped: 

323 self._explicit_tearDown = self._testcase.tearDown 

324 setattr(self._testcase, "tearDown", lambda *args: None) 

325 

326 # We need to update the actual bound method with self.obj, because 

327 # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. 

328 setattr(self._testcase, self.name, self.obj) 

329 try: 

330 self._testcase(result=self) # type: ignore[arg-type] 

331 finally: 

332 delattr(self._testcase, self.name) 

333 

334 def _prunetraceback( 

335 self, excinfo: _pytest._code.ExceptionInfo[BaseException] 

336 ) -> None: 

337 super()._prunetraceback(excinfo) 

338 traceback = excinfo.traceback.filter( 

339 lambda x: not x.frame.f_globals.get("__unittest") 

340 ) 

341 if traceback: 

342 excinfo.traceback = traceback 

343 

344 

345@hookimpl(tryfirst=True) 

346def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: 

347 if isinstance(item, TestCaseFunction): 

348 if item._excinfo: 

349 call.excinfo = item._excinfo.pop(0) 

350 try: 

351 del call.result 

352 except AttributeError: 

353 pass 

354 

355 # Convert unittest.SkipTest to pytest.skip. 

356 # This is actually only needed for nose, which reuses unittest.SkipTest for 

357 # its own nose.SkipTest. For unittest TestCases, SkipTest is already 

358 # handled internally, and doesn't reach here. 

359 unittest = sys.modules.get("unittest") 

360 if ( 

361 unittest 

362 and call.excinfo 

363 and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined] 

364 ): 

365 excinfo = call.excinfo 

366 call2 = CallInfo[None].from_call( 

367 lambda: pytest.skip(str(excinfo.value)), call.when 

368 ) 

369 call.excinfo = call2.excinfo 

370 

371 

372# Twisted trial support. 

373 

374 

375@hookimpl(hookwrapper=True) 

376def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: 

377 if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: 

378 ut: Any = sys.modules["twisted.python.failure"] 

379 Failure__init__ = ut.Failure.__init__ 

380 check_testcase_implements_trial_reporter() 

381 

382 def excstore( 

383 self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None 

384 ): 

385 if exc_value is None: 

386 self._rawexcinfo = sys.exc_info() 

387 else: 

388 if exc_type is None: 

389 exc_type = type(exc_value) 

390 self._rawexcinfo = (exc_type, exc_value, exc_tb) 

391 try: 

392 Failure__init__( 

393 self, exc_value, exc_type, exc_tb, captureVars=captureVars 

394 ) 

395 except TypeError: 

396 Failure__init__(self, exc_value, exc_type, exc_tb) 

397 

398 ut.Failure.__init__ = excstore 

399 yield 

400 ut.Failure.__init__ = Failure__init__ 

401 else: 

402 yield 

403 

404 

405def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: 

406 if done: 

407 return 

408 from zope.interface import classImplements 

409 from twisted.trial.itrial import IReporter 

410 

411 classImplements(TestCaseFunction, IReporter) 

412 done.append(1) 

413 

414 

415def _is_skipped(obj) -> bool: 

416 """Return True if the given object has been marked with @unittest.skip.""" 

417 return bool(getattr(obj, "__unittest_skip__", False))