Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/runner.py: 71%
287 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"""Basic collect and runtest protocol implementations."""
2import bdb
3import os
4import sys
5from typing import Callable
6from typing import cast
7from typing import Dict
8from typing import Generic
9from typing import List
10from typing import Optional
11from typing import Tuple
12from typing import Type
13from typing import TYPE_CHECKING
14from typing import TypeVar
15from typing import Union
17import attr
19from .reports import BaseReport
20from .reports import CollectErrorRepr
21from .reports import CollectReport
22from .reports import TestReport
23from _pytest import timing
24from _pytest._code.code import ExceptionChainRepr
25from _pytest._code.code import ExceptionInfo
26from _pytest._code.code import TerminalRepr
27from _pytest.compat import final
28from _pytest.config.argparsing import Parser
29from _pytest.deprecated import check_ispytest
30from _pytest.nodes import Collector
31from _pytest.nodes import Item
32from _pytest.nodes import Node
33from _pytest.outcomes import Exit
34from _pytest.outcomes import OutcomeException
35from _pytest.outcomes import Skipped
36from _pytest.outcomes import TEST_OUTCOME
38if TYPE_CHECKING:
39 from typing_extensions import Literal
41 from _pytest.main import Session
42 from _pytest.terminal import TerminalReporter
44#
45# pytest plugin hooks.
48def pytest_addoption(parser: Parser) -> None:
49 group = parser.getgroup("terminal reporting", "Reporting", after="general")
50 group.addoption(
51 "--durations",
52 action="store",
53 type=int,
54 default=None,
55 metavar="N",
56 help="Show N slowest setup/test durations (N=0 for all)",
57 )
58 group.addoption(
59 "--durations-min",
60 action="store",
61 type=float,
62 default=0.005,
63 metavar="N",
64 help="Minimal duration in seconds for inclusion in slowest list. "
65 "Default: 0.005.",
66 )
69def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
70 durations = terminalreporter.config.option.durations
71 durations_min = terminalreporter.config.option.durations_min
72 verbose = terminalreporter.config.getvalue("verbose")
73 if durations is None:
74 return
75 tr = terminalreporter
76 dlist = []
77 for replist in tr.stats.values():
78 for rep in replist:
79 if hasattr(rep, "duration"):
80 dlist.append(rep)
81 if not dlist:
82 return
83 dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
84 if not durations:
85 tr.write_sep("=", "slowest durations")
86 else:
87 tr.write_sep("=", "slowest %s durations" % durations)
88 dlist = dlist[:durations]
90 for i, rep in enumerate(dlist):
91 if verbose < 2 and rep.duration < durations_min:
92 tr.write_line("")
93 tr.write_line(
94 "(%s durations < %gs hidden. Use -vv to show these durations.)"
95 % (len(dlist) - i, durations_min)
96 )
97 break
98 tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
101def pytest_sessionstart(session: "Session") -> None:
102 session._setupstate = SetupState()
105def pytest_sessionfinish(session: "Session") -> None:
106 session._setupstate.teardown_exact(None)
109def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
110 ihook = item.ihook
111 ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
112 runtestprotocol(item, nextitem=nextitem)
113 ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
114 return True
117def runtestprotocol(
118 item: Item, log: bool = True, nextitem: Optional[Item] = None
119) -> List[TestReport]:
120 hasrequest = hasattr(item, "_request")
121 if hasrequest and not item._request: # type: ignore[attr-defined]
122 # This only happens if the item is re-run, as is done by
123 # pytest-rerunfailures.
124 item._initrequest() # type: ignore[attr-defined]
125 rep = call_and_report(item, "setup", log)
126 reports = [rep]
127 if rep.passed:
128 if item.config.getoption("setupshow", False):
129 show_test_item(item)
130 if not item.config.getoption("setuponly", False):
131 reports.append(call_and_report(item, "call", log))
132 reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
133 # After all teardown hooks have been called
134 # want funcargs and request info to go away.
135 if hasrequest:
136 item._request = False # type: ignore[attr-defined]
137 item.funcargs = None # type: ignore[attr-defined]
138 return reports
141def show_test_item(item: Item) -> None:
142 """Show test function, parameters and the fixtures of the test item."""
143 tw = item.config.get_terminal_writer()
144 tw.line()
145 tw.write(" " * 8)
146 tw.write(item.nodeid)
147 used_fixtures = sorted(getattr(item, "fixturenames", []))
148 if used_fixtures:
149 tw.write(" (fixtures used: {})".format(", ".join(used_fixtures)))
150 tw.flush()
153def pytest_runtest_setup(item: Item) -> None:
154 _update_current_test_var(item, "setup")
155 item.session._setupstate.setup(item)
158def pytest_runtest_call(item: Item) -> None:
159 _update_current_test_var(item, "call")
160 try:
161 del sys.last_type
162 del sys.last_value
163 del sys.last_traceback
164 except AttributeError:
165 pass
166 try:
167 item.runtest()
168 except Exception as e:
169 # Store trace info to allow postmortem debugging
170 sys.last_type = type(e)
171 sys.last_value = e
172 assert e.__traceback__ is not None
173 # Skip *this* frame
174 sys.last_traceback = e.__traceback__.tb_next
175 raise e
178def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
179 _update_current_test_var(item, "teardown")
180 item.session._setupstate.teardown_exact(nextitem)
181 _update_current_test_var(item, None)
184def _update_current_test_var(
185 item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
186) -> None:
187 """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
189 If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
190 """
191 var_name = "PYTEST_CURRENT_TEST"
192 if when:
193 value = f"{item.nodeid} ({when})"
194 # don't allow null bytes on environment variables (see #2644, #2957)
195 value = value.replace("\x00", "(null)")
196 os.environ[var_name] = value
197 else:
198 os.environ.pop(var_name)
201def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
202 if report.when in ("setup", "teardown"):
203 if report.failed:
204 # category, shortletter, verbose-word
205 return "error", "E", "ERROR"
206 elif report.skipped:
207 return "skipped", "s", "SKIPPED"
208 else:
209 return "", "", ""
210 return None
213#
214# Implementation
217def call_and_report(
218 item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds
219) -> TestReport:
220 call = call_runtest_hook(item, when, **kwds)
221 hook = item.ihook
222 report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
223 if log:
224 hook.pytest_runtest_logreport(report=report)
225 if check_interactive_exception(call, report):
226 hook.pytest_exception_interact(node=item, call=call, report=report)
227 return report
230def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
231 """Check whether the call raised an exception that should be reported as
232 interactive."""
233 if call.excinfo is None:
234 # Didn't raise.
235 return False
236 if hasattr(report, "wasxfail"):
237 # Exception was expected.
238 return False
239 if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)):
240 # Special control flow exception.
241 return False
242 return True
245def call_runtest_hook(
246 item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds
247) -> "CallInfo[None]":
248 if when == "setup":
249 ihook: Callable[..., None] = item.ihook.pytest_runtest_setup
250 elif when == "call":
251 ihook = item.ihook.pytest_runtest_call
252 elif when == "teardown":
253 ihook = item.ihook.pytest_runtest_teardown
254 else:
255 assert False, f"Unhandled runtest hook case: {when}"
256 reraise: Tuple[Type[BaseException], ...] = (Exit,)
257 if not item.config.getoption("usepdb", False):
258 reraise += (KeyboardInterrupt,)
259 return CallInfo.from_call(
260 lambda: ihook(item=item, **kwds), when=when, reraise=reraise
261 )
264TResult = TypeVar("TResult", covariant=True)
267@final
268@attr.s(repr=False, init=False, auto_attribs=True)
269class CallInfo(Generic[TResult]):
270 """Result/Exception info of a function invocation."""
272 _result: Optional[TResult]
273 #: The captured exception of the call, if it raised.
274 excinfo: Optional[ExceptionInfo[BaseException]]
275 #: The system time when the call started, in seconds since the epoch.
276 start: float
277 #: The system time when the call ended, in seconds since the epoch.
278 stop: float
279 #: The call duration, in seconds.
280 duration: float
281 #: The context of invocation: "collect", "setup", "call" or "teardown".
282 when: "Literal['collect', 'setup', 'call', 'teardown']"
284 def __init__(
285 self,
286 result: Optional[TResult],
287 excinfo: Optional[ExceptionInfo[BaseException]],
288 start: float,
289 stop: float,
290 duration: float,
291 when: "Literal['collect', 'setup', 'call', 'teardown']",
292 *,
293 _ispytest: bool = False,
294 ) -> None:
295 check_ispytest(_ispytest)
296 self._result = result
297 self.excinfo = excinfo
298 self.start = start
299 self.stop = stop
300 self.duration = duration
301 self.when = when
303 @property
304 def result(self) -> TResult:
305 """The return value of the call, if it didn't raise.
307 Can only be accessed if excinfo is None.
308 """
309 if self.excinfo is not None:
310 raise AttributeError(f"{self!r} has no valid result")
311 # The cast is safe because an exception wasn't raised, hence
312 # _result has the expected function return type (which may be
313 # None, that's why a cast and not an assert).
314 return cast(TResult, self._result)
316 @classmethod
317 def from_call(
318 cls,
319 func: "Callable[[], TResult]",
320 when: "Literal['collect', 'setup', 'call', 'teardown']",
321 reraise: Optional[
322 Union[Type[BaseException], Tuple[Type[BaseException], ...]]
323 ] = None,
324 ) -> "CallInfo[TResult]":
325 """Call func, wrapping the result in a CallInfo.
327 :param func:
328 The function to call. Called without arguments.
329 :param when:
330 The phase in which the function is called.
331 :param reraise:
332 Exception or exceptions that shall propagate if raised by the
333 function, instead of being wrapped in the CallInfo.
334 """
335 excinfo = None
336 start = timing.time()
337 precise_start = timing.perf_counter()
338 try:
339 result: Optional[TResult] = func()
340 except BaseException:
341 excinfo = ExceptionInfo.from_current()
342 if reraise is not None and isinstance(excinfo.value, reraise):
343 raise
344 result = None
345 # use the perf counter
346 precise_stop = timing.perf_counter()
347 duration = precise_stop - precise_start
348 stop = timing.time()
349 return cls(
350 start=start,
351 stop=stop,
352 duration=duration,
353 when=when,
354 result=result,
355 excinfo=excinfo,
356 _ispytest=True,
357 )
359 def __repr__(self) -> str:
360 if self.excinfo is None:
361 return f"<CallInfo when={self.when!r} result: {self._result!r}>"
362 return f"<CallInfo when={self.when!r} excinfo={self.excinfo!r}>"
365def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
366 return TestReport.from_item_and_call(item, call)
369def pytest_make_collect_report(collector: Collector) -> CollectReport:
370 call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
371 longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
372 if not call.excinfo:
373 outcome: Literal["passed", "skipped", "failed"] = "passed"
374 else:
375 skip_exceptions = [Skipped]
376 unittest = sys.modules.get("unittest")
377 if unittest is not None:
378 # Type ignored because unittest is loaded dynamically.
379 skip_exceptions.append(unittest.SkipTest) # type: ignore
380 if isinstance(call.excinfo.value, tuple(skip_exceptions)):
381 outcome = "skipped"
382 r_ = collector._repr_failure_py(call.excinfo, "line")
383 assert isinstance(r_, ExceptionChainRepr), repr(r_)
384 r = r_.reprcrash
385 assert r
386 longrepr = (str(r.path), r.lineno, r.message)
387 else:
388 outcome = "failed"
389 errorinfo = collector.repr_failure(call.excinfo)
390 if not hasattr(errorinfo, "toterminal"):
391 assert isinstance(errorinfo, str)
392 errorinfo = CollectErrorRepr(errorinfo)
393 longrepr = errorinfo
394 result = call.result if not call.excinfo else None
395 rep = CollectReport(collector.nodeid, outcome, longrepr, result)
396 rep.call = call # type: ignore # see collect_one_node
397 return rep
400class SetupState:
401 """Shared state for setting up/tearing down test items or collectors
402 in a session.
404 Suppose we have a collection tree as follows:
406 <Session session>
407 <Module mod1>
408 <Function item1>
409 <Module mod2>
410 <Function item2>
412 The SetupState maintains a stack. The stack starts out empty:
414 []
416 During the setup phase of item1, setup(item1) is called. What it does
417 is:
419 push session to stack, run session.setup()
420 push mod1 to stack, run mod1.setup()
421 push item1 to stack, run item1.setup()
423 The stack is:
425 [session, mod1, item1]
427 While the stack is in this shape, it is allowed to add finalizers to
428 each of session, mod1, item1 using addfinalizer().
430 During the teardown phase of item1, teardown_exact(item2) is called,
431 where item2 is the next item to item1. What it does is:
433 pop item1 from stack, run its teardowns
434 pop mod1 from stack, run its teardowns
436 mod1 was popped because it ended its purpose with item1. The stack is:
438 [session]
440 During the setup phase of item2, setup(item2) is called. What it does
441 is:
443 push mod2 to stack, run mod2.setup()
444 push item2 to stack, run item2.setup()
446 Stack:
448 [session, mod2, item2]
450 During the teardown phase of item2, teardown_exact(None) is called,
451 because item2 is the last item. What it does is:
453 pop item2 from stack, run its teardowns
454 pop mod2 from stack, run its teardowns
455 pop session from stack, run its teardowns
457 Stack:
459 []
461 The end!
462 """
464 def __init__(self) -> None:
465 # The stack is in the dict insertion order.
466 self.stack: Dict[
467 Node,
468 Tuple[
469 # Node's finalizers.
470 List[Callable[[], object]],
471 # Node's exception, if its setup raised.
472 Optional[Union[OutcomeException, Exception]],
473 ],
474 ] = {}
476 def setup(self, item: Item) -> None:
477 """Setup objects along the collector chain to the item."""
478 needed_collectors = item.listchain()
480 # If a collector fails its setup, fail its entire subtree of items.
481 # The setup is not retried for each item - the same exception is used.
482 for col, (finalizers, exc) in self.stack.items():
483 assert col in needed_collectors, "previous item was not torn down properly"
484 if exc:
485 raise exc
487 for col in needed_collectors[len(self.stack) :]:
488 assert col not in self.stack
489 # Push onto the stack.
490 self.stack[col] = ([col.teardown], None)
491 try:
492 col.setup()
493 except TEST_OUTCOME as exc:
494 self.stack[col] = (self.stack[col][0], exc)
495 raise exc
497 def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
498 """Attach a finalizer to the given node.
500 The node must be currently active in the stack.
501 """
502 assert node and not isinstance(node, tuple)
503 assert callable(finalizer)
504 assert node in self.stack, (node, self.stack)
505 self.stack[node][0].append(finalizer)
507 def teardown_exact(self, nextitem: Optional[Item]) -> None:
508 """Teardown the current stack up until reaching nodes that nextitem
509 also descends from.
511 When nextitem is None (meaning we're at the last item), the entire
512 stack is torn down.
513 """
514 needed_collectors = nextitem and nextitem.listchain() or []
515 exc = None
516 while self.stack:
517 if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
518 break
519 node, (finalizers, _) = self.stack.popitem()
520 while finalizers:
521 fin = finalizers.pop()
522 try:
523 fin()
524 except TEST_OUTCOME as e:
525 # XXX Only first exception will be seen by user,
526 # ideally all should be reported.
527 if exc is None:
528 exc = e
529 if exc:
530 raise exc
531 if nextitem is None:
532 assert not self.stack
535def collect_one_node(collector: Collector) -> CollectReport:
536 ihook = collector.ihook
537 ihook.pytest_collectstart(collector=collector)
538 rep: CollectReport = ihook.pytest_make_collect_report(collector=collector)
539 call = rep.__dict__.pop("call", None)
540 if call and check_interactive_exception(call, rep):
541 ihook.pytest_exception_interact(node=collector, call=call, report=rep)
542 return rep