You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

406 lines
14KB

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