unittest.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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. import _pytest._code
  16. import pytest
  17. from _pytest.compat import getimfunc
  18. from _pytest.compat import is_async_function
  19. from _pytest.config import hookimpl
  20. from _pytest.fixtures import FixtureRequest
  21. from _pytest.nodes import Collector
  22. from _pytest.nodes import Item
  23. from _pytest.outcomes import exit
  24. from _pytest.outcomes import fail
  25. from _pytest.outcomes import skip
  26. from _pytest.outcomes import xfail
  27. from _pytest.python import Class
  28. from _pytest.python import Function
  29. from _pytest.python import Module
  30. from _pytest.runner import CallInfo
  31. from _pytest.scope import Scope
  32. if TYPE_CHECKING:
  33. import unittest
  34. import twisted.trial.unittest
  35. _SysExcInfoType = Union[
  36. Tuple[Type[BaseException], BaseException, types.TracebackType],
  37. Tuple[None, None, None],
  38. ]
  39. def pytest_pycollect_makeitem(
  40. collector: Union[Module, Class], name: str, obj: object
  41. ) -> Optional["UnitTestCase"]:
  42. # Has unittest been imported and is obj a subclass of its TestCase?
  43. try:
  44. ut = sys.modules["unittest"]
  45. # Type ignored because `ut` is an opaque module.
  46. if not issubclass(obj, ut.TestCase): # type: ignore
  47. return None
  48. except Exception:
  49. return None
  50. # Yes, so let's collect it.
  51. item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj)
  52. return item
  53. class UnitTestCase(Class):
  54. # Marker for fixturemanger.getfixtureinfo()
  55. # to declare that our children do not support funcargs.
  56. nofuncargs = True
  57. def collect(self) -> Iterable[Union[Item, Collector]]:
  58. from unittest import TestLoader
  59. cls = self.obj
  60. if not getattr(cls, "__test__", True):
  61. return
  62. skipped = _is_skipped(cls)
  63. if not skipped:
  64. self._inject_setup_teardown_fixtures(cls)
  65. self._inject_setup_class_fixture()
  66. self.session._fixturemanager.parsefactories(self, unittest=True)
  67. loader = TestLoader()
  68. foundsomething = False
  69. for name in loader.getTestCaseNames(self.obj):
  70. x = getattr(self.obj, name)
  71. if not getattr(x, "__test__", True):
  72. continue
  73. funcobj = getimfunc(x)
  74. yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj)
  75. foundsomething = True
  76. if not foundsomething:
  77. runtest = getattr(self.obj, "runTest", None)
  78. if runtest is not None:
  79. ut = sys.modules.get("twisted.trial.unittest", None)
  80. # Type ignored because `ut` is an opaque module.
  81. if ut is None or runtest != ut.TestCase.runTest: # type: ignore
  82. yield TestCaseFunction.from_parent(self, name="runTest")
  83. def _inject_setup_teardown_fixtures(self, cls: type) -> None:
  84. """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
  85. teardown functions (#517)."""
  86. class_fixture = _make_xunit_fixture(
  87. cls,
  88. "setUpClass",
  89. "tearDownClass",
  90. "doClassCleanups",
  91. scope=Scope.Class,
  92. pass_self=False,
  93. )
  94. if class_fixture:
  95. cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined]
  96. method_fixture = _make_xunit_fixture(
  97. cls,
  98. "setup_method",
  99. "teardown_method",
  100. None,
  101. scope=Scope.Function,
  102. pass_self=True,
  103. )
  104. if method_fixture:
  105. cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined]
  106. def _make_xunit_fixture(
  107. obj: type,
  108. setup_name: str,
  109. teardown_name: str,
  110. cleanup_name: Optional[str],
  111. scope: Scope,
  112. pass_self: bool,
  113. ):
  114. setup = getattr(obj, setup_name, None)
  115. teardown = getattr(obj, teardown_name, None)
  116. if setup is None and teardown is None:
  117. return None
  118. if cleanup_name:
  119. cleanup = getattr(obj, cleanup_name, lambda *args: None)
  120. else:
  121. def cleanup(*args):
  122. pass
  123. @pytest.fixture(
  124. scope=scope.value,
  125. autouse=True,
  126. # Use a unique name to speed up lookup.
  127. name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}",
  128. )
  129. def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
  130. if _is_skipped(self):
  131. reason = self.__unittest_skip_why__
  132. raise pytest.skip.Exception(reason, _use_item_location=True)
  133. if setup is not None:
  134. try:
  135. if pass_self:
  136. setup(self, request.function)
  137. else:
  138. setup()
  139. # unittest does not call the cleanup function for every BaseException, so we
  140. # follow this here.
  141. except Exception:
  142. if pass_self:
  143. cleanup(self)
  144. else:
  145. cleanup()
  146. raise
  147. yield
  148. try:
  149. if teardown is not None:
  150. if pass_self:
  151. teardown(self, request.function)
  152. else:
  153. teardown()
  154. finally:
  155. if pass_self:
  156. cleanup(self)
  157. else:
  158. cleanup()
  159. return fixture
  160. class TestCaseFunction(Function):
  161. nofuncargs = True
  162. _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
  163. _testcase: Optional["unittest.TestCase"] = None
  164. def _getobj(self):
  165. assert self.parent is not None
  166. # Unlike a regular Function in a Class, where `item.obj` returns
  167. # a *bound* method (attached to an instance), TestCaseFunction's
  168. # `obj` returns an *unbound* method (not attached to an instance).
  169. # This inconsistency is probably not desirable, but needs some
  170. # consideration before changing.
  171. return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
  172. def setup(self) -> None:
  173. # A bound method to be called during teardown() if set (see 'runtest()').
  174. self._explicit_tearDown: Optional[Callable[[], None]] = None
  175. assert self.parent is not None
  176. self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
  177. self._obj = getattr(self._testcase, self.name)
  178. if hasattr(self, "_request"):
  179. self._request._fillfixtures()
  180. def teardown(self) -> None:
  181. if self._explicit_tearDown is not None:
  182. self._explicit_tearDown()
  183. self._explicit_tearDown = None
  184. self._testcase = None
  185. self._obj = None
  186. def startTest(self, testcase: "unittest.TestCase") -> None:
  187. pass
  188. def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
  189. # Unwrap potential exception info (see twisted trial support below).
  190. rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
  191. try:
  192. excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(rawexcinfo) # type: ignore[arg-type]
  193. # Invoke the attributes to trigger storing the traceback
  194. # trial causes some issue there.
  195. excinfo.value
  196. excinfo.traceback
  197. except TypeError:
  198. try:
  199. try:
  200. values = traceback.format_exception(*rawexcinfo)
  201. values.insert(
  202. 0,
  203. "NOTE: Incompatible Exception Representation, "
  204. "displaying natively:\n\n",
  205. )
  206. fail("".join(values), pytrace=False)
  207. except (fail.Exception, KeyboardInterrupt):
  208. raise
  209. except BaseException:
  210. fail(
  211. "ERROR: Unknown Incompatible Exception "
  212. "representation:\n%r" % (rawexcinfo,),
  213. pytrace=False,
  214. )
  215. except KeyboardInterrupt:
  216. raise
  217. except fail.Exception:
  218. excinfo = _pytest._code.ExceptionInfo.from_current()
  219. self.__dict__.setdefault("_excinfo", []).append(excinfo)
  220. def addError(
  221. self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
  222. ) -> None:
  223. try:
  224. if isinstance(rawexcinfo[1], exit.Exception):
  225. exit(rawexcinfo[1].msg)
  226. except TypeError:
  227. pass
  228. self._addexcinfo(rawexcinfo)
  229. def addFailure(
  230. self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
  231. ) -> None:
  232. self._addexcinfo(rawexcinfo)
  233. def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None:
  234. try:
  235. raise pytest.skip.Exception(reason, _use_item_location=True)
  236. except skip.Exception:
  237. self._addexcinfo(sys.exc_info())
  238. def addExpectedFailure(
  239. self,
  240. testcase: "unittest.TestCase",
  241. rawexcinfo: "_SysExcInfoType",
  242. reason: str = "",
  243. ) -> None:
  244. try:
  245. xfail(str(reason))
  246. except xfail.Exception:
  247. self._addexcinfo(sys.exc_info())
  248. def addUnexpectedSuccess(
  249. self,
  250. testcase: "unittest.TestCase",
  251. reason: Optional["twisted.trial.unittest.Todo"] = None,
  252. ) -> None:
  253. msg = "Unexpected success"
  254. if reason:
  255. msg += f": {reason.reason}"
  256. # Preserve unittest behaviour - fail the test. Explicitly not an XPASS.
  257. try:
  258. fail(msg, pytrace=False)
  259. except fail.Exception:
  260. self._addexcinfo(sys.exc_info())
  261. def addSuccess(self, testcase: "unittest.TestCase") -> None:
  262. pass
  263. def stopTest(self, testcase: "unittest.TestCase") -> None:
  264. pass
  265. def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
  266. pass
  267. def runtest(self) -> None:
  268. from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
  269. assert self._testcase is not None
  270. maybe_wrap_pytest_function_for_tracing(self)
  271. # Let the unittest framework handle async functions.
  272. if is_async_function(self.obj):
  273. # Type ignored because self acts as the TestResult, but is not actually one.
  274. self._testcase(result=self) # type: ignore[arg-type]
  275. else:
  276. # When --pdb is given, we want to postpone calling tearDown() otherwise
  277. # when entering the pdb prompt, tearDown() would have probably cleaned up
  278. # instance variables, which makes it difficult to debug.
  279. # Arguably we could always postpone tearDown(), but this changes the moment where the
  280. # TestCase instance interacts with the results object, so better to only do it
  281. # when absolutely needed.
  282. # We need to consider if the test itself is skipped, or the whole class.
  283. assert isinstance(self.parent, UnitTestCase)
  284. skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj)
  285. if self.config.getoption("usepdb") and not skipped:
  286. self._explicit_tearDown = self._testcase.tearDown
  287. setattr(self._testcase, "tearDown", lambda *args: None)
  288. # We need to update the actual bound method with self.obj, because
  289. # wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
  290. setattr(self._testcase, self.name, self.obj)
  291. try:
  292. self._testcase(result=self) # type: ignore[arg-type]
  293. finally:
  294. delattr(self._testcase, self.name)
  295. def _traceback_filter(
  296. self, excinfo: _pytest._code.ExceptionInfo[BaseException]
  297. ) -> _pytest._code.Traceback:
  298. traceback = super()._traceback_filter(excinfo)
  299. ntraceback = traceback.filter(
  300. lambda x: not x.frame.f_globals.get("__unittest"),
  301. )
  302. if not ntraceback:
  303. ntraceback = traceback
  304. return ntraceback
  305. @hookimpl(tryfirst=True)
  306. def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
  307. if isinstance(item, TestCaseFunction):
  308. if item._excinfo:
  309. call.excinfo = item._excinfo.pop(0)
  310. try:
  311. del call.result
  312. except AttributeError:
  313. pass
  314. # Convert unittest.SkipTest to pytest.skip.
  315. # This is actually only needed for nose, which reuses unittest.SkipTest for
  316. # its own nose.SkipTest. For unittest TestCases, SkipTest is already
  317. # handled internally, and doesn't reach here.
  318. unittest = sys.modules.get("unittest")
  319. if (
  320. unittest
  321. and call.excinfo
  322. and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
  323. ):
  324. excinfo = call.excinfo
  325. call2 = CallInfo[None].from_call(
  326. lambda: pytest.skip(str(excinfo.value)), call.when
  327. )
  328. call.excinfo = call2.excinfo
  329. # Twisted trial support.
  330. @hookimpl(hookwrapper=True)
  331. def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
  332. if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
  333. ut: Any = sys.modules["twisted.python.failure"]
  334. Failure__init__ = ut.Failure.__init__
  335. check_testcase_implements_trial_reporter()
  336. def excstore(
  337. self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
  338. ):
  339. if exc_value is None:
  340. self._rawexcinfo = sys.exc_info()
  341. else:
  342. if exc_type is None:
  343. exc_type = type(exc_value)
  344. self._rawexcinfo = (exc_type, exc_value, exc_tb)
  345. try:
  346. Failure__init__(
  347. self, exc_value, exc_type, exc_tb, captureVars=captureVars
  348. )
  349. except TypeError:
  350. Failure__init__(self, exc_value, exc_type, exc_tb)
  351. ut.Failure.__init__ = excstore
  352. yield
  353. ut.Failure.__init__ = Failure__init__
  354. else:
  355. yield
  356. def check_testcase_implements_trial_reporter(done: List[int] = []) -> None:
  357. if done:
  358. return
  359. from zope.interface import classImplements
  360. from twisted.trial.itrial import IReporter
  361. classImplements(TestCaseFunction, IReporter)
  362. done.append(1)
  363. def _is_skipped(obj) -> bool:
  364. """Return True if the given object has been marked with @unittest.skip."""
  365. return bool(getattr(obj, "__unittest_skip__", False))