Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/unittest.py: 29%
248 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1"""Discover and run 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 Type
13from typing import TYPE_CHECKING
14from typing import Union
16import _pytest._code
17import pytest
18from _pytest.compat import getimfunc
19from _pytest.compat import is_async_function
20from _pytest.config import hookimpl
21from _pytest.fixtures import FixtureRequest
22from _pytest.nodes import Collector
23from _pytest.nodes import Item
24from _pytest.outcomes import exit
25from _pytest.outcomes import fail
26from _pytest.outcomes import skip
27from _pytest.outcomes import xfail
28from _pytest.python import Class
29from _pytest.python import Function
30from _pytest.python import Module
31from _pytest.runner import CallInfo
32from _pytest.scope import Scope
34if TYPE_CHECKING:
35 import unittest
36 import twisted.trial.unittest
38 _SysExcInfoType = Union[
39 Tuple[Type[BaseException], BaseException, types.TracebackType],
40 Tuple[None, None, None],
41 ]
44def 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
60class UnitTestCase(Class):
61 # Marker for fixturemanger.getfixtureinfo()
62 # to declare that our children do not support funcargs.
63 nofuncargs = True
65 def collect(self) -> Iterable[Union[Item, Collector]]:
66 from unittest import TestLoader
68 cls = self.obj
69 if not getattr(cls, "__test__", True):
70 return
72 skipped = _is_skipped(cls)
73 if not skipped:
74 self._inject_setup_teardown_fixtures(cls)
75 self._inject_setup_class_fixture()
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
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")
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]
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]
122def _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
135 if cleanup_name:
136 cleanup = getattr(obj, cleanup_name, lambda *args: None)
137 else:
139 def cleanup(*args):
140 pass
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()
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()
180 return fixture
183class TestCaseFunction(Function):
184 nofuncargs = True
185 _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
186 _testcase: Optional["unittest.TestCase"] = None
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]
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()
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
213 def startTest(self, testcase: "unittest.TestCase") -> None:
214 pass
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)
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)
259 def addFailure(
260 self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
261 ) -> None:
262 self._addexcinfo(rawexcinfo)
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())
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())
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())
295 def addSuccess(self, testcase: "unittest.TestCase") -> None:
296 pass
298 def stopTest(self, testcase: "unittest.TestCase") -> None:
299 pass
301 def runtest(self) -> None:
302 from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
304 assert self._testcase is not None
306 maybe_wrap_pytest_function_for_tracing(self)
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)
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)
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
345@hookimpl(tryfirst=True)
346def 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
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
372# Twisted trial support.
375@hookimpl(hookwrapper=True)
376def 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()
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)
398 ut.Failure.__init__ = excstore
399 yield
400 ut.Failure.__init__ = Failure__init__
401 else:
402 yield
405def 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
411 classImplements(TestCaseFunction, IReporter)
412 done.append(1)
415def _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))