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.

463 lines
15KB

  1. """Basic collect and runtest protocol implementations."""
  2. import bdb
  3. import os
  4. import sys
  5. from typing import Callable
  6. from typing import cast
  7. from typing import Dict
  8. from typing import Generic
  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 TypeVar
  15. from typing import Union
  16. import attr
  17. from .reports import BaseReport
  18. from .reports import CollectErrorRepr
  19. from .reports import CollectReport
  20. from .reports import TestReport
  21. from _pytest import timing
  22. from _pytest._code.code import ExceptionChainRepr
  23. from _pytest._code.code import ExceptionInfo
  24. from _pytest._code.code import TerminalRepr
  25. from _pytest.compat import final
  26. from _pytest.config.argparsing import Parser
  27. from _pytest.nodes import Collector
  28. from _pytest.nodes import Item
  29. from _pytest.nodes import Node
  30. from _pytest.outcomes import Exit
  31. from _pytest.outcomes import Skipped
  32. from _pytest.outcomes import TEST_OUTCOME
  33. if TYPE_CHECKING:
  34. from typing_extensions import Literal
  35. from _pytest.main import Session
  36. from _pytest.terminal import TerminalReporter
  37. #
  38. # pytest plugin hooks.
  39. def pytest_addoption(parser: Parser) -> None:
  40. group = parser.getgroup("terminal reporting", "reporting", after="general")
  41. group.addoption(
  42. "--durations",
  43. action="store",
  44. type=int,
  45. default=None,
  46. metavar="N",
  47. help="show N slowest setup/test durations (N=0 for all).",
  48. )
  49. group.addoption(
  50. "--durations-min",
  51. action="store",
  52. type=float,
  53. default=0.005,
  54. metavar="N",
  55. help="Minimal duration in seconds for inclusion in slowest list. Default 0.005",
  56. )
  57. def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
  58. durations = terminalreporter.config.option.durations
  59. durations_min = terminalreporter.config.option.durations_min
  60. verbose = terminalreporter.config.getvalue("verbose")
  61. if durations is None:
  62. return
  63. tr = terminalreporter
  64. dlist = []
  65. for replist in tr.stats.values():
  66. for rep in replist:
  67. if hasattr(rep, "duration"):
  68. dlist.append(rep)
  69. if not dlist:
  70. return
  71. dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
  72. if not durations:
  73. tr.write_sep("=", "slowest durations")
  74. else:
  75. tr.write_sep("=", "slowest %s durations" % durations)
  76. dlist = dlist[:durations]
  77. for i, rep in enumerate(dlist):
  78. if verbose < 2 and rep.duration < durations_min:
  79. tr.write_line("")
  80. tr.write_line(
  81. "(%s durations < %gs hidden. Use -vv to show these durations.)"
  82. % (len(dlist) - i, durations_min)
  83. )
  84. break
  85. tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
  86. def pytest_sessionstart(session: "Session") -> None:
  87. session._setupstate = SetupState()
  88. def pytest_sessionfinish(session: "Session") -> None:
  89. session._setupstate.teardown_all()
  90. def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
  91. ihook = item.ihook
  92. ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
  93. runtestprotocol(item, nextitem=nextitem)
  94. ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
  95. return True
  96. def runtestprotocol(
  97. item: Item, log: bool = True, nextitem: Optional[Item] = None
  98. ) -> List[TestReport]:
  99. hasrequest = hasattr(item, "_request")
  100. if hasrequest and not item._request: # type: ignore[attr-defined]
  101. item._initrequest() # type: ignore[attr-defined]
  102. rep = call_and_report(item, "setup", log)
  103. reports = [rep]
  104. if rep.passed:
  105. if item.config.getoption("setupshow", False):
  106. show_test_item(item)
  107. if not item.config.getoption("setuponly", False):
  108. reports.append(call_and_report(item, "call", log))
  109. reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
  110. # After all teardown hooks have been called
  111. # want funcargs and request info to go away.
  112. if hasrequest:
  113. item._request = False # type: ignore[attr-defined]
  114. item.funcargs = None # type: ignore[attr-defined]
  115. return reports
  116. def show_test_item(item: Item) -> None:
  117. """Show test function, parameters and the fixtures of the test item."""
  118. tw = item.config.get_terminal_writer()
  119. tw.line()
  120. tw.write(" " * 8)
  121. tw.write(item.nodeid)
  122. used_fixtures = sorted(getattr(item, "fixturenames", []))
  123. if used_fixtures:
  124. tw.write(" (fixtures used: {})".format(", ".join(used_fixtures)))
  125. tw.flush()
  126. def pytest_runtest_setup(item: Item) -> None:
  127. _update_current_test_var(item, "setup")
  128. item.session._setupstate.prepare(item)
  129. def pytest_runtest_call(item: Item) -> None:
  130. _update_current_test_var(item, "call")
  131. try:
  132. del sys.last_type
  133. del sys.last_value
  134. del sys.last_traceback
  135. except AttributeError:
  136. pass
  137. try:
  138. item.runtest()
  139. except Exception as e:
  140. # Store trace info to allow postmortem debugging
  141. sys.last_type = type(e)
  142. sys.last_value = e
  143. assert e.__traceback__ is not None
  144. # Skip *this* frame
  145. sys.last_traceback = e.__traceback__.tb_next
  146. raise e
  147. def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
  148. _update_current_test_var(item, "teardown")
  149. item.session._setupstate.teardown_exact(item, nextitem)
  150. _update_current_test_var(item, None)
  151. def _update_current_test_var(
  152. item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
  153. ) -> None:
  154. """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
  155. If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
  156. """
  157. var_name = "PYTEST_CURRENT_TEST"
  158. if when:
  159. value = f"{item.nodeid} ({when})"
  160. # don't allow null bytes on environment variables (see #2644, #2957)
  161. value = value.replace("\x00", "(null)")
  162. os.environ[var_name] = value
  163. else:
  164. os.environ.pop(var_name)
  165. def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
  166. if report.when in ("setup", "teardown"):
  167. if report.failed:
  168. # category, shortletter, verbose-word
  169. return "error", "E", "ERROR"
  170. elif report.skipped:
  171. return "skipped", "s", "SKIPPED"
  172. else:
  173. return "", "", ""
  174. return None
  175. #
  176. # Implementation
  177. def call_and_report(
  178. item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds
  179. ) -> TestReport:
  180. call = call_runtest_hook(item, when, **kwds)
  181. hook = item.ihook
  182. report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
  183. if log:
  184. hook.pytest_runtest_logreport(report=report)
  185. if check_interactive_exception(call, report):
  186. hook.pytest_exception_interact(node=item, call=call, report=report)
  187. return report
  188. def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
  189. """Check whether the call raised an exception that should be reported as
  190. interactive."""
  191. if call.excinfo is None:
  192. # Didn't raise.
  193. return False
  194. if hasattr(report, "wasxfail"):
  195. # Exception was expected.
  196. return False
  197. if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)):
  198. # Special control flow exception.
  199. return False
  200. return True
  201. def call_runtest_hook(
  202. item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds
  203. ) -> "CallInfo[None]":
  204. if when == "setup":
  205. ihook: Callable[..., None] = item.ihook.pytest_runtest_setup
  206. elif when == "call":
  207. ihook = item.ihook.pytest_runtest_call
  208. elif when == "teardown":
  209. ihook = item.ihook.pytest_runtest_teardown
  210. else:
  211. assert False, f"Unhandled runtest hook case: {when}"
  212. reraise: Tuple[Type[BaseException], ...] = (Exit,)
  213. if not item.config.getoption("usepdb", False):
  214. reraise += (KeyboardInterrupt,)
  215. return CallInfo.from_call(
  216. lambda: ihook(item=item, **kwds), when=when, reraise=reraise
  217. )
  218. TResult = TypeVar("TResult", covariant=True)
  219. @final
  220. @attr.s(repr=False)
  221. class CallInfo(Generic[TResult]):
  222. """Result/Exception info a function invocation.
  223. :param T result:
  224. The return value of the call, if it didn't raise. Can only be
  225. accessed if excinfo is None.
  226. :param Optional[ExceptionInfo] excinfo:
  227. The captured exception of the call, if it raised.
  228. :param float start:
  229. The system time when the call started, in seconds since the epoch.
  230. :param float stop:
  231. The system time when the call ended, in seconds since the epoch.
  232. :param float duration:
  233. The call duration, in seconds.
  234. :param str when:
  235. The context of invocation: "setup", "call", "teardown", ...
  236. """
  237. _result = attr.ib(type="Optional[TResult]")
  238. excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]])
  239. start = attr.ib(type=float)
  240. stop = attr.ib(type=float)
  241. duration = attr.ib(type=float)
  242. when = attr.ib(type="Literal['collect', 'setup', 'call', 'teardown']")
  243. @property
  244. def result(self) -> TResult:
  245. if self.excinfo is not None:
  246. raise AttributeError(f"{self!r} has no valid result")
  247. # The cast is safe because an exception wasn't raised, hence
  248. # _result has the expected function return type (which may be
  249. # None, that's why a cast and not an assert).
  250. return cast(TResult, self._result)
  251. @classmethod
  252. def from_call(
  253. cls,
  254. func: "Callable[[], TResult]",
  255. when: "Literal['collect', 'setup', 'call', 'teardown']",
  256. reraise: Optional[
  257. Union[Type[BaseException], Tuple[Type[BaseException], ...]]
  258. ] = None,
  259. ) -> "CallInfo[TResult]":
  260. excinfo = None
  261. start = timing.time()
  262. precise_start = timing.perf_counter()
  263. try:
  264. result: Optional[TResult] = func()
  265. except BaseException:
  266. excinfo = ExceptionInfo.from_current()
  267. if reraise is not None and isinstance(excinfo.value, reraise):
  268. raise
  269. result = None
  270. # use the perf counter
  271. precise_stop = timing.perf_counter()
  272. duration = precise_stop - precise_start
  273. stop = timing.time()
  274. return cls(
  275. start=start,
  276. stop=stop,
  277. duration=duration,
  278. when=when,
  279. result=result,
  280. excinfo=excinfo,
  281. )
  282. def __repr__(self) -> str:
  283. if self.excinfo is None:
  284. return f"<CallInfo when={self.when!r} result: {self._result!r}>"
  285. return f"<CallInfo when={self.when!r} excinfo={self.excinfo!r}>"
  286. def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
  287. return TestReport.from_item_and_call(item, call)
  288. def pytest_make_collect_report(collector: Collector) -> CollectReport:
  289. call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
  290. longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
  291. if not call.excinfo:
  292. outcome: Literal["passed", "skipped", "failed"] = "passed"
  293. else:
  294. skip_exceptions = [Skipped]
  295. unittest = sys.modules.get("unittest")
  296. if unittest is not None:
  297. # Type ignored because unittest is loaded dynamically.
  298. skip_exceptions.append(unittest.SkipTest) # type: ignore
  299. if isinstance(call.excinfo.value, tuple(skip_exceptions)):
  300. outcome = "skipped"
  301. r_ = collector._repr_failure_py(call.excinfo, "line")
  302. assert isinstance(r_, ExceptionChainRepr), repr(r_)
  303. r = r_.reprcrash
  304. assert r
  305. longrepr = (str(r.path), r.lineno, r.message)
  306. else:
  307. outcome = "failed"
  308. errorinfo = collector.repr_failure(call.excinfo)
  309. if not hasattr(errorinfo, "toterminal"):
  310. assert isinstance(errorinfo, str)
  311. errorinfo = CollectErrorRepr(errorinfo)
  312. longrepr = errorinfo
  313. result = call.result if not call.excinfo else None
  314. rep = CollectReport(collector.nodeid, outcome, longrepr, result)
  315. rep.call = call # type: ignore # see collect_one_node
  316. return rep
  317. class SetupState:
  318. """Shared state for setting up/tearing down test items or collectors."""
  319. def __init__(self):
  320. self.stack: List[Node] = []
  321. self._finalizers: Dict[Node, List[Callable[[], object]]] = {}
  322. def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None:
  323. """Attach a finalizer to the given colitem."""
  324. assert colitem and not isinstance(colitem, tuple)
  325. assert callable(finalizer)
  326. # assert colitem in self.stack # some unit tests don't setup stack :/
  327. self._finalizers.setdefault(colitem, []).append(finalizer)
  328. def _pop_and_teardown(self):
  329. colitem = self.stack.pop()
  330. self._teardown_with_finalization(colitem)
  331. def _callfinalizers(self, colitem) -> None:
  332. finalizers = self._finalizers.pop(colitem, None)
  333. exc = None
  334. while finalizers:
  335. fin = finalizers.pop()
  336. try:
  337. fin()
  338. except TEST_OUTCOME as e:
  339. # XXX Only first exception will be seen by user,
  340. # ideally all should be reported.
  341. if exc is None:
  342. exc = e
  343. if exc:
  344. raise exc
  345. def _teardown_with_finalization(self, colitem) -> None:
  346. self._callfinalizers(colitem)
  347. colitem.teardown()
  348. for colitem in self._finalizers:
  349. assert colitem in self.stack
  350. def teardown_all(self) -> None:
  351. while self.stack:
  352. self._pop_and_teardown()
  353. for key in list(self._finalizers):
  354. self._teardown_with_finalization(key)
  355. assert not self._finalizers
  356. def teardown_exact(self, item, nextitem) -> None:
  357. needed_collectors = nextitem and nextitem.listchain() or []
  358. self._teardown_towards(needed_collectors)
  359. def _teardown_towards(self, needed_collectors) -> None:
  360. exc = None
  361. while self.stack:
  362. if self.stack == needed_collectors[: len(self.stack)]:
  363. break
  364. try:
  365. self._pop_and_teardown()
  366. except TEST_OUTCOME as e:
  367. # XXX Only first exception will be seen by user,
  368. # ideally all should be reported.
  369. if exc is None:
  370. exc = e
  371. if exc:
  372. raise exc
  373. def prepare(self, colitem) -> None:
  374. """Setup objects along the collector chain to the test-method."""
  375. # Check if the last collection node has raised an error.
  376. for col in self.stack:
  377. if hasattr(col, "_prepare_exc"):
  378. exc = col._prepare_exc # type: ignore[attr-defined]
  379. raise exc
  380. needed_collectors = colitem.listchain()
  381. for col in needed_collectors[len(self.stack) :]:
  382. self.stack.append(col)
  383. try:
  384. col.setup()
  385. except TEST_OUTCOME as e:
  386. col._prepare_exc = e # type: ignore[attr-defined]
  387. raise e
  388. def collect_one_node(collector: Collector) -> CollectReport:
  389. ihook = collector.ihook
  390. ihook.pytest_collectstart(collector=collector)
  391. rep: CollectReport = ihook.pytest_make_collect_report(collector=collector)
  392. call = rep.__dict__.pop("call", None)
  393. if call and check_interactive_exception(call, rep):
  394. ihook.pytest_exception_interact(node=collector, call=call, report=rep)
  395. return rep