reports.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. import dataclasses
  2. import os
  3. from io import StringIO
  4. from pprint import pprint
  5. from typing import Any
  6. from typing import cast
  7. from typing import Dict
  8. from typing import Iterable
  9. from typing import Iterator
  10. from typing import List
  11. from typing import Mapping
  12. from typing import NoReturn
  13. from typing import Optional
  14. from typing import Tuple
  15. from typing import Type
  16. from typing import TYPE_CHECKING
  17. from typing import TypeVar
  18. from typing import Union
  19. from _pytest._code.code import ExceptionChainRepr
  20. from _pytest._code.code import ExceptionInfo
  21. from _pytest._code.code import ExceptionRepr
  22. from _pytest._code.code import ReprEntry
  23. from _pytest._code.code import ReprEntryNative
  24. from _pytest._code.code import ReprExceptionInfo
  25. from _pytest._code.code import ReprFileLocation
  26. from _pytest._code.code import ReprFuncArgs
  27. from _pytest._code.code import ReprLocals
  28. from _pytest._code.code import ReprTraceback
  29. from _pytest._code.code import TerminalRepr
  30. from _pytest._io import TerminalWriter
  31. from _pytest.compat import final
  32. from _pytest.config import Config
  33. from _pytest.nodes import Collector
  34. from _pytest.nodes import Item
  35. from _pytest.outcomes import skip
  36. if TYPE_CHECKING:
  37. from typing_extensions import Literal
  38. from _pytest.runner import CallInfo
  39. def getworkerinfoline(node):
  40. try:
  41. return node._workerinfocache
  42. except AttributeError:
  43. d = node.workerinfo
  44. ver = "%s.%s.%s" % d["version_info"][:3]
  45. node._workerinfocache = s = "[{}] {} -- Python {} {}".format(
  46. d["id"], d["sysplatform"], ver, d["executable"]
  47. )
  48. return s
  49. _R = TypeVar("_R", bound="BaseReport")
  50. class BaseReport:
  51. when: Optional[str]
  52. location: Optional[Tuple[str, Optional[int], str]]
  53. longrepr: Union[
  54. None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
  55. ]
  56. sections: List[Tuple[str, str]]
  57. nodeid: str
  58. outcome: "Literal['passed', 'failed', 'skipped']"
  59. def __init__(self, **kw: Any) -> None:
  60. self.__dict__.update(kw)
  61. if TYPE_CHECKING:
  62. # Can have arbitrary fields given to __init__().
  63. def __getattr__(self, key: str) -> Any:
  64. ...
  65. def toterminal(self, out: TerminalWriter) -> None:
  66. if hasattr(self, "node"):
  67. worker_info = getworkerinfoline(self.node)
  68. if worker_info:
  69. out.line(worker_info)
  70. longrepr = self.longrepr
  71. if longrepr is None:
  72. return
  73. if hasattr(longrepr, "toterminal"):
  74. longrepr_terminal = cast(TerminalRepr, longrepr)
  75. longrepr_terminal.toterminal(out)
  76. else:
  77. try:
  78. s = str(longrepr)
  79. except UnicodeEncodeError:
  80. s = "<unprintable longrepr>"
  81. out.line(s)
  82. def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]:
  83. for name, content in self.sections:
  84. if name.startswith(prefix):
  85. yield prefix, content
  86. @property
  87. def longreprtext(self) -> str:
  88. """Read-only property that returns the full string representation of
  89. ``longrepr``.
  90. .. versionadded:: 3.0
  91. """
  92. file = StringIO()
  93. tw = TerminalWriter(file)
  94. tw.hasmarkup = False
  95. self.toterminal(tw)
  96. exc = file.getvalue()
  97. return exc.strip()
  98. @property
  99. def caplog(self) -> str:
  100. """Return captured log lines, if log capturing is enabled.
  101. .. versionadded:: 3.5
  102. """
  103. return "\n".join(
  104. content for (prefix, content) in self.get_sections("Captured log")
  105. )
  106. @property
  107. def capstdout(self) -> str:
  108. """Return captured text from stdout, if capturing is enabled.
  109. .. versionadded:: 3.0
  110. """
  111. return "".join(
  112. content for (prefix, content) in self.get_sections("Captured stdout")
  113. )
  114. @property
  115. def capstderr(self) -> str:
  116. """Return captured text from stderr, if capturing is enabled.
  117. .. versionadded:: 3.0
  118. """
  119. return "".join(
  120. content for (prefix, content) in self.get_sections("Captured stderr")
  121. )
  122. @property
  123. def passed(self) -> bool:
  124. """Whether the outcome is passed."""
  125. return self.outcome == "passed"
  126. @property
  127. def failed(self) -> bool:
  128. """Whether the outcome is failed."""
  129. return self.outcome == "failed"
  130. @property
  131. def skipped(self) -> bool:
  132. """Whether the outcome is skipped."""
  133. return self.outcome == "skipped"
  134. @property
  135. def fspath(self) -> str:
  136. """The path portion of the reported node, as a string."""
  137. return self.nodeid.split("::")[0]
  138. @property
  139. def count_towards_summary(self) -> bool:
  140. """**Experimental** Whether this report should be counted towards the
  141. totals shown at the end of the test session: "1 passed, 1 failure, etc".
  142. .. note::
  143. This function is considered **experimental**, so beware that it is subject to changes
  144. even in patch releases.
  145. """
  146. return True
  147. @property
  148. def head_line(self) -> Optional[str]:
  149. """**Experimental** The head line shown with longrepr output for this
  150. report, more commonly during traceback representation during
  151. failures::
  152. ________ Test.foo ________
  153. In the example above, the head_line is "Test.foo".
  154. .. note::
  155. This function is considered **experimental**, so beware that it is subject to changes
  156. even in patch releases.
  157. """
  158. if self.location is not None:
  159. fspath, lineno, domain = self.location
  160. return domain
  161. return None
  162. def _get_verbose_word(self, config: Config):
  163. _category, _short, verbose = config.hook.pytest_report_teststatus(
  164. report=self, config=config
  165. )
  166. return verbose
  167. def _to_json(self) -> Dict[str, Any]:
  168. """Return the contents of this report as a dict of builtin entries,
  169. suitable for serialization.
  170. This was originally the serialize_report() function from xdist (ca03269).
  171. Experimental method.
  172. """
  173. return _report_to_json(self)
  174. @classmethod
  175. def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R:
  176. """Create either a TestReport or CollectReport, depending on the calling class.
  177. It is the callers responsibility to know which class to pass here.
  178. This was originally the serialize_report() function from xdist (ca03269).
  179. Experimental method.
  180. """
  181. kwargs = _report_kwargs_from_json(reportdict)
  182. return cls(**kwargs)
  183. def _report_unserialization_failure(
  184. type_name: str, report_class: Type[BaseReport], reportdict
  185. ) -> NoReturn:
  186. url = "https://github.com/pytest-dev/pytest/issues"
  187. stream = StringIO()
  188. pprint("-" * 100, stream=stream)
  189. pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
  190. pprint("report_name: %s" % report_class, stream=stream)
  191. pprint(reportdict, stream=stream)
  192. pprint("Please report this bug at %s" % url, stream=stream)
  193. pprint("-" * 100, stream=stream)
  194. raise RuntimeError(stream.getvalue())
  195. @final
  196. class TestReport(BaseReport):
  197. """Basic test report object (also used for setup and teardown calls if
  198. they fail).
  199. Reports can contain arbitrary extra attributes.
  200. """
  201. __test__ = False
  202. def __init__(
  203. self,
  204. nodeid: str,
  205. location: Tuple[str, Optional[int], str],
  206. keywords: Mapping[str, Any],
  207. outcome: "Literal['passed', 'failed', 'skipped']",
  208. longrepr: Union[
  209. None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
  210. ],
  211. when: "Literal['setup', 'call', 'teardown']",
  212. sections: Iterable[Tuple[str, str]] = (),
  213. duration: float = 0,
  214. start: float = 0,
  215. stop: float = 0,
  216. user_properties: Optional[Iterable[Tuple[str, object]]] = None,
  217. **extra,
  218. ) -> None:
  219. #: Normalized collection nodeid.
  220. self.nodeid = nodeid
  221. #: A (filesystempath, lineno, domaininfo) tuple indicating the
  222. #: actual location of a test item - it might be different from the
  223. #: collected one e.g. if a method is inherited from a different module.
  224. #: The filesystempath may be relative to ``config.rootdir``.
  225. #: The line number is 0-based.
  226. self.location: Tuple[str, Optional[int], str] = location
  227. #: A name -> value dictionary containing all keywords and
  228. #: markers associated with a test invocation.
  229. self.keywords: Mapping[str, Any] = keywords
  230. #: Test outcome, always one of "passed", "failed", "skipped".
  231. self.outcome = outcome
  232. #: None or a failure representation.
  233. self.longrepr = longrepr
  234. #: One of 'setup', 'call', 'teardown' to indicate runtest phase.
  235. self.when = when
  236. #: User properties is a list of tuples (name, value) that holds user
  237. #: defined properties of the test.
  238. self.user_properties = list(user_properties or [])
  239. #: Tuples of str ``(heading, content)`` with extra information
  240. #: for the test report. Used by pytest to add text captured
  241. #: from ``stdout``, ``stderr``, and intercepted logging events. May
  242. #: be used by other plugins to add arbitrary information to reports.
  243. self.sections = list(sections)
  244. #: Time it took to run just the test.
  245. self.duration: float = duration
  246. #: The system time when the call started, in seconds since the epoch.
  247. self.start: float = start
  248. #: The system time when the call ended, in seconds since the epoch.
  249. self.stop: float = stop
  250. self.__dict__.update(extra)
  251. def __repr__(self) -> str:
  252. return "<{} {!r} when={!r} outcome={!r}>".format(
  253. self.__class__.__name__, self.nodeid, self.when, self.outcome
  254. )
  255. @classmethod
  256. def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
  257. """Create and fill a TestReport with standard item and call info.
  258. :param item: The item.
  259. :param call: The call info.
  260. """
  261. when = call.when
  262. # Remove "collect" from the Literal type -- only for collection calls.
  263. assert when != "collect"
  264. duration = call.duration
  265. start = call.start
  266. stop = call.stop
  267. keywords = {x: 1 for x in item.keywords}
  268. excinfo = call.excinfo
  269. sections = []
  270. if not call.excinfo:
  271. outcome: Literal["passed", "failed", "skipped"] = "passed"
  272. longrepr: Union[
  273. None,
  274. ExceptionInfo[BaseException],
  275. Tuple[str, int, str],
  276. str,
  277. TerminalRepr,
  278. ] = None
  279. else:
  280. if not isinstance(excinfo, ExceptionInfo):
  281. outcome = "failed"
  282. longrepr = excinfo
  283. elif isinstance(excinfo.value, skip.Exception):
  284. outcome = "skipped"
  285. r = excinfo._getreprcrash()
  286. assert (
  287. r is not None
  288. ), "There should always be a traceback entry for skipping a test."
  289. if excinfo.value._use_item_location:
  290. path, line = item.reportinfo()[:2]
  291. assert line is not None
  292. longrepr = os.fspath(path), line + 1, r.message
  293. else:
  294. longrepr = (str(r.path), r.lineno, r.message)
  295. else:
  296. outcome = "failed"
  297. if call.when == "call":
  298. longrepr = item.repr_failure(excinfo)
  299. else: # exception in setup or teardown
  300. longrepr = item._repr_failure_py(
  301. excinfo, style=item.config.getoption("tbstyle", "auto")
  302. )
  303. for rwhen, key, content in item._report_sections:
  304. sections.append((f"Captured {key} {rwhen}", content))
  305. return cls(
  306. item.nodeid,
  307. item.location,
  308. keywords,
  309. outcome,
  310. longrepr,
  311. when,
  312. sections,
  313. duration,
  314. start,
  315. stop,
  316. user_properties=item.user_properties,
  317. )
  318. @final
  319. class CollectReport(BaseReport):
  320. """Collection report object.
  321. Reports can contain arbitrary extra attributes.
  322. """
  323. when = "collect"
  324. def __init__(
  325. self,
  326. nodeid: str,
  327. outcome: "Literal['passed', 'failed', 'skipped']",
  328. longrepr: Union[
  329. None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
  330. ],
  331. result: Optional[List[Union[Item, Collector]]],
  332. sections: Iterable[Tuple[str, str]] = (),
  333. **extra,
  334. ) -> None:
  335. #: Normalized collection nodeid.
  336. self.nodeid = nodeid
  337. #: Test outcome, always one of "passed", "failed", "skipped".
  338. self.outcome = outcome
  339. #: None or a failure representation.
  340. self.longrepr = longrepr
  341. #: The collected items and collection nodes.
  342. self.result = result or []
  343. #: Tuples of str ``(heading, content)`` with extra information
  344. #: for the test report. Used by pytest to add text captured
  345. #: from ``stdout``, ``stderr``, and intercepted logging events. May
  346. #: be used by other plugins to add arbitrary information to reports.
  347. self.sections = list(sections)
  348. self.__dict__.update(extra)
  349. @property
  350. def location( # type:ignore[override]
  351. self,
  352. ) -> Optional[Tuple[str, Optional[int], str]]:
  353. return (self.fspath, None, self.fspath)
  354. def __repr__(self) -> str:
  355. return "<CollectReport {!r} lenresult={} outcome={!r}>".format(
  356. self.nodeid, len(self.result), self.outcome
  357. )
  358. class CollectErrorRepr(TerminalRepr):
  359. def __init__(self, msg: str) -> None:
  360. self.longrepr = msg
  361. def toterminal(self, out: TerminalWriter) -> None:
  362. out.line(self.longrepr, red=True)
  363. def pytest_report_to_serializable(
  364. report: Union[CollectReport, TestReport]
  365. ) -> Optional[Dict[str, Any]]:
  366. if isinstance(report, (TestReport, CollectReport)):
  367. data = report._to_json()
  368. data["$report_type"] = report.__class__.__name__
  369. return data
  370. # TODO: Check if this is actually reachable.
  371. return None # type: ignore[unreachable]
  372. def pytest_report_from_serializable(
  373. data: Dict[str, Any],
  374. ) -> Optional[Union[CollectReport, TestReport]]:
  375. if "$report_type" in data:
  376. if data["$report_type"] == "TestReport":
  377. return TestReport._from_json(data)
  378. elif data["$report_type"] == "CollectReport":
  379. return CollectReport._from_json(data)
  380. assert False, "Unknown report_type unserialize data: {}".format(
  381. data["$report_type"]
  382. )
  383. return None
  384. def _report_to_json(report: BaseReport) -> Dict[str, Any]:
  385. """Return the contents of this report as a dict of builtin entries,
  386. suitable for serialization.
  387. This was originally the serialize_report() function from xdist (ca03269).
  388. """
  389. def serialize_repr_entry(
  390. entry: Union[ReprEntry, ReprEntryNative]
  391. ) -> Dict[str, Any]:
  392. data = dataclasses.asdict(entry)
  393. for key, value in data.items():
  394. if hasattr(value, "__dict__"):
  395. data[key] = dataclasses.asdict(value)
  396. entry_data = {"type": type(entry).__name__, "data": data}
  397. return entry_data
  398. def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
  399. result = dataclasses.asdict(reprtraceback)
  400. result["reprentries"] = [
  401. serialize_repr_entry(x) for x in reprtraceback.reprentries
  402. ]
  403. return result
  404. def serialize_repr_crash(
  405. reprcrash: Optional[ReprFileLocation],
  406. ) -> Optional[Dict[str, Any]]:
  407. if reprcrash is not None:
  408. return dataclasses.asdict(reprcrash)
  409. else:
  410. return None
  411. def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
  412. assert rep.longrepr is not None
  413. # TODO: Investigate whether the duck typing is really necessary here.
  414. longrepr = cast(ExceptionRepr, rep.longrepr)
  415. result: Dict[str, Any] = {
  416. "reprcrash": serialize_repr_crash(longrepr.reprcrash),
  417. "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
  418. "sections": longrepr.sections,
  419. }
  420. if isinstance(longrepr, ExceptionChainRepr):
  421. result["chain"] = []
  422. for repr_traceback, repr_crash, description in longrepr.chain:
  423. result["chain"].append(
  424. (
  425. serialize_repr_traceback(repr_traceback),
  426. serialize_repr_crash(repr_crash),
  427. description,
  428. )
  429. )
  430. else:
  431. result["chain"] = None
  432. return result
  433. d = report.__dict__.copy()
  434. if hasattr(report.longrepr, "toterminal"):
  435. if hasattr(report.longrepr, "reprtraceback") and hasattr(
  436. report.longrepr, "reprcrash"
  437. ):
  438. d["longrepr"] = serialize_exception_longrepr(report)
  439. else:
  440. d["longrepr"] = str(report.longrepr)
  441. else:
  442. d["longrepr"] = report.longrepr
  443. for name in d:
  444. if isinstance(d[name], os.PathLike):
  445. d[name] = os.fspath(d[name])
  446. elif name == "result":
  447. d[name] = None # for now
  448. return d
  449. def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
  450. """Return **kwargs that can be used to construct a TestReport or
  451. CollectReport instance.
  452. This was originally the serialize_report() function from xdist (ca03269).
  453. """
  454. def deserialize_repr_entry(entry_data):
  455. data = entry_data["data"]
  456. entry_type = entry_data["type"]
  457. if entry_type == "ReprEntry":
  458. reprfuncargs = None
  459. reprfileloc = None
  460. reprlocals = None
  461. if data["reprfuncargs"]:
  462. reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
  463. if data["reprfileloc"]:
  464. reprfileloc = ReprFileLocation(**data["reprfileloc"])
  465. if data["reprlocals"]:
  466. reprlocals = ReprLocals(data["reprlocals"]["lines"])
  467. reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry(
  468. lines=data["lines"],
  469. reprfuncargs=reprfuncargs,
  470. reprlocals=reprlocals,
  471. reprfileloc=reprfileloc,
  472. style=data["style"],
  473. )
  474. elif entry_type == "ReprEntryNative":
  475. reprentry = ReprEntryNative(data["lines"])
  476. else:
  477. _report_unserialization_failure(entry_type, TestReport, reportdict)
  478. return reprentry
  479. def deserialize_repr_traceback(repr_traceback_dict):
  480. repr_traceback_dict["reprentries"] = [
  481. deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"]
  482. ]
  483. return ReprTraceback(**repr_traceback_dict)
  484. def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
  485. if repr_crash_dict is not None:
  486. return ReprFileLocation(**repr_crash_dict)
  487. else:
  488. return None
  489. if (
  490. reportdict["longrepr"]
  491. and "reprcrash" in reportdict["longrepr"]
  492. and "reprtraceback" in reportdict["longrepr"]
  493. ):
  494. reprtraceback = deserialize_repr_traceback(
  495. reportdict["longrepr"]["reprtraceback"]
  496. )
  497. reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"])
  498. if reportdict["longrepr"]["chain"]:
  499. chain = []
  500. for repr_traceback_data, repr_crash_data, description in reportdict[
  501. "longrepr"
  502. ]["chain"]:
  503. chain.append(
  504. (
  505. deserialize_repr_traceback(repr_traceback_data),
  506. deserialize_repr_crash(repr_crash_data),
  507. description,
  508. )
  509. )
  510. exception_info: Union[
  511. ExceptionChainRepr, ReprExceptionInfo
  512. ] = ExceptionChainRepr(chain)
  513. else:
  514. exception_info = ReprExceptionInfo(
  515. reprtraceback=reprtraceback,
  516. reprcrash=reprcrash,
  517. )
  518. for section in reportdict["longrepr"]["sections"]:
  519. exception_info.addsection(*section)
  520. reportdict["longrepr"] = exception_info
  521. return reportdict