reports.py 19 KB

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