unittest.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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 PyCollector
  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: PyCollector, 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 setup(self) -> None:
  165. # A bound method to be called during teardown() if set (see 'runtest()').
  166. self._explicit_tearDown: Optional[Callable[[], None]] = None
  167. assert self.parent is not None
  168. self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
  169. self._obj = getattr(self._testcase, self.name)
  170. if hasattr(self, "_request"):
  171. self._request._fillfixtures()
  172. def teardown(self) -> None:
  173. if self._explicit_tearDown is not None:
  174. self._explicit_tearDown()
  175. self._explicit_tearDown = None
  176. self._testcase = None
  177. self._obj = None
  178. def startTest(self, testcase: "unittest.TestCase") -> None:
  179. pass
  180. def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
  181. # Unwrap potential exception info (see twisted trial support below).
  182. rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
  183. try:
  184. excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(rawexcinfo) # type: ignore[arg-type]
  185. # Invoke the attributes to trigger storing the traceback
  186. # trial causes some issue there.
  187. excinfo.value
  188. excinfo.traceback
  189. except TypeError:
  190. try:
  191. try:
  192. values = traceback.format_exception(*rawexcinfo)
  193. values.insert(
  194. 0,
  195. "NOTE: Incompatible Exception Representation, "
  196. "displaying natively:\n\n",
  197. )
  198. fail("".join(values), pytrace=False)
  199. except (fail.Exception, KeyboardInterrupt):
  200. raise
  201. except BaseException:
  202. fail(
  203. "ERROR: Unknown Incompatible Exception "
  204. "representation:\n%r" % (rawexcinfo,),
  205. pytrace=False,
  206. )
  207. except KeyboardInterrupt:
  208. raise
  209. except fail.Exception:
  210. excinfo = _pytest._code.ExceptionInfo.from_current()
  211. self.__dict__.setdefault("_excinfo", []).append(excinfo)
  212. def addError(
  213. self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
  214. ) -> None:
  215. try:
  216. if isinstance(rawexcinfo[1], exit.Exception):
  217. exit(rawexcinfo[1].msg)
  218. except TypeError:
  219. pass
  220. self._addexcinfo(rawexcinfo)
  221. def addFailure(
  222. self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
  223. ) -> None:
  224. self._addexcinfo(rawexcinfo)
  225. def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None:
  226. try:
  227. raise pytest.skip.Exception(reason, _use_item_location=True)
  228. except skip.Exception:
  229. self._addexcinfo(sys.exc_info())
  230. def addExpectedFailure(
  231. self,
  232. testcase: "unittest.TestCase",
  233. rawexcinfo: "_SysExcInfoType",
  234. reason: str = "",
  235. ) -> None:
  236. try:
  237. xfail(str(reason))
  238. except xfail.Exception:
  239. self._addexcinfo(sys.exc_info())
  240. def addUnexpectedSuccess(
  241. self,
  242. testcase: "unittest.TestCase",
  243. reason: Optional["twisted.trial.unittest.Todo"] = None,
  244. ) -> None:
  245. msg = "Unexpected success"
  246. if reason:
  247. msg += f": {reason.reason}"
  248. # Preserve unittest behaviour - fail the test. Explicitly not an XPASS.
  249. try:
  250. fail(msg, pytrace=False)
  251. except fail.Exception:
  252. self._addexcinfo(sys.exc_info())
  253. def addSuccess(self, testcase: "unittest.TestCase") -> None:
  254. pass
  255. def stopTest(self, testcase: "unittest.TestCase") -> None:
  256. pass
  257. def runtest(self) -> None:
  258. from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
  259. assert self._testcase is not None
  260. maybe_wrap_pytest_function_for_tracing(self)
  261. # Let the unittest framework handle async functions.
  262. if is_async_function(self.obj):
  263. # Type ignored because self acts as the TestResult, but is not actually one.
  264. self._testcase(result=self) # type: ignore[arg-type]
  265. else:
  266. # When --pdb is given, we want to postpone calling tearDown() otherwise
  267. # when entering the pdb prompt, tearDown() would have probably cleaned up
  268. # instance variables, which makes it difficult to debug.
  269. # Arguably we could always postpone tearDown(), but this changes the moment where the
  270. # TestCase instance interacts with the results object, so better to only do it
  271. # when absolutely needed.
  272. if self.config.getoption("usepdb") and not _is_skipped(self.obj):
  273. self._explicit_tearDown = self._testcase.tearDown
  274. setattr(self._testcase, "tearDown", lambda *args: None)
  275. # We need to update the actual bound method with self.obj, because
  276. # wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
  277. setattr(self._testcase, self.name, self.obj)
  278. try:
  279. self._testcase(result=self) # type: ignore[arg-type]
  280. finally:
  281. delattr(self._testcase, self.name)
  282. def _prunetraceback(
  283. self, excinfo: _pytest._code.ExceptionInfo[BaseException]
  284. ) -> None:
  285. super()._prunetraceback(excinfo)
  286. traceback = excinfo.traceback.filter(
  287. lambda x: not x.frame.f_globals.get("__unittest")
  288. )
  289. if traceback:
  290. excinfo.traceback = traceback
  291. @hookimpl(tryfirst=True)
  292. def 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. # Convert unittest.SkipTest to pytest.skip.
  301. # This is actually only needed for nose, which reuses unittest.SkipTest for
  302. # its own nose.SkipTest. For unittest TestCases, SkipTest is already
  303. # handled internally, and doesn't reach here.
  304. unittest = sys.modules.get("unittest")
  305. if (
  306. unittest
  307. and call.excinfo
  308. and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
  309. ):
  310. excinfo = call.excinfo
  311. call2 = CallInfo[None].from_call(
  312. lambda: pytest.skip(str(excinfo.value)), call.when
  313. )
  314. call.excinfo = call2.excinfo
  315. # Twisted trial support.
  316. @hookimpl(hookwrapper=True)
  317. def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
  318. if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
  319. ut: Any = sys.modules["twisted.python.failure"]
  320. Failure__init__ = ut.Failure.__init__
  321. check_testcase_implements_trial_reporter()
  322. def excstore(
  323. self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
  324. ):
  325. if exc_value is None:
  326. self._rawexcinfo = sys.exc_info()
  327. else:
  328. if exc_type is None:
  329. exc_type = type(exc_value)
  330. self._rawexcinfo = (exc_type, exc_value, exc_tb)
  331. try:
  332. Failure__init__(
  333. self, exc_value, exc_type, exc_tb, captureVars=captureVars
  334. )
  335. except TypeError:
  336. Failure__init__(self, exc_value, exc_type, exc_tb)
  337. ut.Failure.__init__ = excstore
  338. yield
  339. ut.Failure.__init__ = Failure__init__
  340. else:
  341. yield
  342. def check_testcase_implements_trial_reporter(done: List[int] = []) -> None:
  343. if done:
  344. return
  345. from zope.interface import classImplements
  346. from twisted.trial.itrial import IReporter
  347. classImplements(TestCaseFunction, IReporter)
  348. done.append(1)
  349. def _is_skipped(obj) -> bool:
  350. """Return True if the given object has been marked with @unittest.skip."""
  351. return bool(getattr(obj, "__unittest_skip__", False))