Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" discovery and running of 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 Union 

13 

14import _pytest._code 

15import pytest 

16from _pytest.compat import getimfunc 

17from _pytest.compat import is_async_function 

18from _pytest.compat import TYPE_CHECKING 

19from _pytest.config import hookimpl 

20from _pytest.fixtures import FixtureRequest 

21from _pytest.nodes import Collector 

22from _pytest.nodes import Item 

23from _pytest.outcomes import exit 

24from _pytest.outcomes import fail 

25from _pytest.outcomes import skip 

26from _pytest.outcomes import xfail 

27from _pytest.python import Class 

28from _pytest.python import Function 

29from _pytest.python import PyCollector 

30from _pytest.runner import CallInfo 

31from _pytest.skipping import skipped_by_mark_key 

32from _pytest.skipping import unexpectedsuccess_key 

33 

34if TYPE_CHECKING: 

35 import unittest 

36 from typing import Type 

37 

38 from _pytest.fixtures import _Scope 

39 

40 _SysExcInfoType = Union[ 

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

42 Tuple[None, None, None], 

43 ] 

44 

45 

46def pytest_pycollect_makeitem( 

47 collector: PyCollector, name: str, obj: object 

48) -> Optional["UnitTestCase"]: 

49 # has unittest been imported and is obj a subclass of its TestCase? 

50 try: 

51 ut = sys.modules["unittest"] 

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

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

54 return None 

55 except Exception: 

56 return None 

57 # yes, so let's collect it 

58 item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase 

59 return item 

60 

61 

62class UnitTestCase(Class): 

63 # marker for fixturemanger.getfixtureinfo() 

64 # to declare that our children do not support funcargs 

65 nofuncargs = True 

66 

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

68 from unittest import TestLoader 

69 

70 cls = self.obj 

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

72 return 

73 

74 skipped = _is_skipped(cls) 

75 if not skipped: 

76 self._inject_setup_teardown_fixtures(cls) 

77 self._inject_setup_class_fixture() 

78 

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

80 loader = TestLoader() 

81 foundsomething = False 

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

83 x = getattr(self.obj, name) 

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

85 continue 

86 funcobj = getimfunc(x) 

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

88 foundsomething = True 

89 

90 if not foundsomething: 

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

92 if runtest is not None: 

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

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

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

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

97 

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

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

100 teardown functions (#517)""" 

101 class_fixture = _make_xunit_fixture( 

102 cls, "setUpClass", "tearDownClass", scope="class", pass_self=False 

103 ) 

104 if class_fixture: 

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

106 

107 method_fixture = _make_xunit_fixture( 

108 cls, "setup_method", "teardown_method", scope="function", pass_self=True 

109 ) 

110 if method_fixture: 

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

112 

113 

114def _make_xunit_fixture( 

115 obj: type, setup_name: str, teardown_name: str, scope: "_Scope", pass_self: bool 

116): 

117 setup = getattr(obj, setup_name, None) 

118 teardown = getattr(obj, teardown_name, None) 

119 if setup is None and teardown is None: 

120 return None 

121 

122 @pytest.fixture(scope=scope, autouse=True) 

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

124 if _is_skipped(self): 

125 reason = self.__unittest_skip_why__ 

126 pytest.skip(reason) 

127 if setup is not None: 

128 if pass_self: 

129 setup(self, request.function) 

130 else: 

131 setup() 

132 yield 

133 if teardown is not None: 

134 if pass_self: 

135 teardown(self, request.function) 

136 else: 

137 teardown() 

138 

139 return fixture 

140 

141 

142class TestCaseFunction(Function): 

143 nofuncargs = True 

144 _excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo]] 

145 _testcase = None # type: Optional[unittest.TestCase] 

146 

147 def setup(self) -> None: 

148 # a bound method to be called during teardown() if set (see 'runtest()') 

149 self._explicit_tearDown = None # type: Optional[Callable[[], None]] 

150 assert self.parent is not None 

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

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

153 if hasattr(self, "_request"): 

154 self._request._fillfixtures() 

155 

156 def teardown(self) -> None: 

157 if self._explicit_tearDown is not None: 

158 self._explicit_tearDown() 

159 self._explicit_tearDown = None 

160 self._testcase = None 

161 self._obj = None 

162 

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

164 pass 

165 

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

167 # unwrap potential exception info (see twisted trial support below) 

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

169 try: 

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

171 # invoke the attributes to trigger storing the traceback 

172 # trial causes some issue there 

173 excinfo.value 

174 excinfo.traceback 

175 except TypeError: 

176 try: 

177 try: 

178 values = traceback.format_exception(*rawexcinfo) 

179 values.insert( 

180 0, 

181 "NOTE: Incompatible Exception Representation, " 

182 "displaying natively:\n\n", 

183 ) 

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

185 except (fail.Exception, KeyboardInterrupt): 

186 raise 

187 except BaseException: 

188 fail( 

189 "ERROR: Unknown Incompatible Exception " 

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

191 pytrace=False, 

192 ) 

193 except KeyboardInterrupt: 

194 raise 

195 except fail.Exception: 

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

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

198 

199 def addError( 

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

201 ) -> None: 

202 try: 

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

204 exit(rawexcinfo[1].msg) 

205 except TypeError: 

206 pass 

207 self._addexcinfo(rawexcinfo) 

208 

209 def addFailure( 

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

211 ) -> None: 

212 self._addexcinfo(rawexcinfo) 

213 

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

215 try: 

216 skip(reason) 

217 except skip.Exception: 

218 self._store[skipped_by_mark_key] = True 

219 self._addexcinfo(sys.exc_info()) 

220 

221 def addExpectedFailure( 

222 self, 

223 testcase: "unittest.TestCase", 

224 rawexcinfo: "_SysExcInfoType", 

225 reason: str = "", 

226 ) -> None: 

227 try: 

228 xfail(str(reason)) 

229 except xfail.Exception: 

230 self._addexcinfo(sys.exc_info()) 

231 

232 def addUnexpectedSuccess( 

233 self, testcase: "unittest.TestCase", reason: str = "" 

234 ) -> None: 

235 self._store[unexpectedsuccess_key] = reason 

236 

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

238 pass 

239 

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

241 pass 

242 

243 def _expecting_failure(self, test_method) -> bool: 

244 """Return True if the given unittest method (or the entire class) is marked 

245 with @expectedFailure""" 

246 expecting_failure_method = getattr( 

247 test_method, "__unittest_expecting_failure__", False 

248 ) 

249 expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False) 

250 return bool(expecting_failure_class or expecting_failure_method) 

251 

252 def runtest(self) -> None: 

253 from _pytest.debugging import maybe_wrap_pytest_function_for_tracing 

254 

255 assert self._testcase is not None 

256 

257 maybe_wrap_pytest_function_for_tracing(self) 

258 

259 # let the unittest framework handle async functions 

260 if is_async_function(self.obj): 

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

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

263 else: 

264 # when --pdb is given, we want to postpone calling tearDown() otherwise 

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

266 # instance variables, which makes it difficult to debug 

267 # arguably we could always postpone tearDown(), but this changes the moment where the 

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

269 # when absolutely needed 

270 if self.config.getoption("usepdb") and not _is_skipped(self.obj): 

271 self._explicit_tearDown = self._testcase.tearDown 

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

273 

274 # we need to update the actual bound method with self.obj, because 

275 # wrap_pytest_function_for_tracing replaces self.obj by a wrapper 

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

277 try: 

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

279 finally: 

280 delattr(self._testcase, self.name) 

281 

282 def _prunetraceback(self, excinfo: _pytest._code.ExceptionInfo) -> None: 

283 Function._prunetraceback(self, excinfo) 

284 traceback = excinfo.traceback.filter( 

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

286 ) 

287 if traceback: 

288 excinfo.traceback = traceback 

289 

290 

291@hookimpl(tryfirst=True) 

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

293 if isinstance(item, TestCaseFunction): 

294 if item._excinfo: 

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

296 try: 

297 del call.result 

298 except AttributeError: 

299 pass 

300 

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

302 if ( 

303 unittest 

304 and call.excinfo 

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

306 ): 

307 excinfo = call.excinfo 

308 # let's substitute the excinfo with a pytest.skip one 

309 call2 = CallInfo[None].from_call( 

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

311 ) 

312 call.excinfo = call2.excinfo 

313 

314 

315# twisted trial support 

316 

317 

318@hookimpl(hookwrapper=True) 

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

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

321 ut = sys.modules["twisted.python.failure"] # type: Any 

322 Failure__init__ = ut.Failure.__init__ 

323 check_testcase_implements_trial_reporter() 

324 

325 def excstore( 

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

327 ): 

328 if exc_value is None: 

329 self._rawexcinfo = sys.exc_info() 

330 else: 

331 if exc_type is None: 

332 exc_type = type(exc_value) 

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

334 try: 

335 Failure__init__( 

336 self, exc_value, exc_type, exc_tb, captureVars=captureVars 

337 ) 

338 except TypeError: 

339 Failure__init__(self, exc_value, exc_type, exc_tb) 

340 

341 ut.Failure.__init__ = excstore 

342 yield 

343 ut.Failure.__init__ = Failure__init__ 

344 else: 

345 yield 

346 

347 

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

349 if done: 

350 return 

351 from zope.interface import classImplements 

352 from twisted.trial.itrial import IReporter 

353 

354 classImplements(TestCaseFunction, IReporter) 

355 done.append(1) 

356 

357 

358def _is_skipped(obj) -> bool: 

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

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