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

2 import sys

3 import traceback

4 import types

5 from typing import Any

6 from typing import Callable

7 from typing import Generator

8 from typing import Iterable

9 from typing import List

10 from typing import Optional

11 from typing import Tuple

12 from typing import Type

13 from typing import TYPE_CHECKING

14 from typing import Union

15

16 import _pytest._code

17 import pytest

18 from _pytest.compat import getimfunc

19 from _pytest.compat import is_async_function

20 from _pytest.config import hookimpl

21 from _pytest.fixtures import FixtureRequest

22 from _pytest.nodes import Collector

23 from _pytest.nodes import Item

24 from _pytest.outcomes import exit

25 from _pytest.outcomes import fail

26 from _pytest.outcomes import skip

27 from _pytest.outcomes import xfail

28 from _pytest.python import Class

29 from _pytest.python import Function

30 from _pytest.python import Module

31 from _pytest.runner import CallInfo

32 from _pytest.scope import Scope

33

34 if 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

44 def 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

60 class 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

122 def _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

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

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

376 def 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

405 def 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

415 def _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))