reports.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  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 Mapping
  11. from typing import Optional
  12. from typing import Tuple
  13. from typing import Type
  14. from typing import TYPE_CHECKING
  15. from typing import TypeVar
  16. from typing import Union
  17. import attr
  18. from _pytest._code.code import ExceptionChainRepr
  19. from _pytest._code.code import ExceptionInfo
  20. from _pytest._code.code import ExceptionRepr
  21. from _pytest._code.code import ReprEntry
  22. from _pytest._code.code import ReprEntryNative
  23. from _pytest._code.code import ReprExceptionInfo
  24. from _pytest._code.code import ReprFileLocation
  25. from _pytest._code.code import ReprFuncArgs
  26. from _pytest._code.code import ReprLocals
  27. from _pytest._code.code import ReprTraceback
  28. from _pytest._code.code import TerminalRepr
  29. from _pytest._io import TerminalWriter
  30. from _pytest.compat import final
  31. from _pytest.config import Config
  32. from _pytest.nodes import Collector
  33. from _pytest.nodes import Item
  34. from _pytest.outcomes import skip
  35. if TYPE_CHECKING:
  36. from typing import NoReturn
  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. user_properties: Optional[Iterable[Tuple[str, object]]] = None,
  215. **extra,
  216. ) -> None:
  217. #: Normalized collection nodeid.
  218. self.nodeid = nodeid
  219. #: A (filesystempath, lineno, domaininfo) tuple indicating the
  220. #: actual location of a test item - it might be different from the
  221. #: collected one e.g. if a method is inherited from a different module.
  222. self.location: Tuple[str, Optional[int], str] = location
  223. #: A name -> value dictionary containing all keywords and
  224. #: markers associated with a test invocation.
  225. self.keywords = keywords
  226. #: Test outcome, always one of "passed", "failed", "skipped".
  227. self.outcome = outcome
  228. #: None or a failure representation.
  229. self.longrepr = longrepr
  230. #: One of 'setup', 'call', 'teardown' to indicate runtest phase.
  231. self.when = when
  232. #: User properties is a list of tuples (name, value) that holds user
  233. #: defined properties of the test.
  234. self.user_properties = list(user_properties or [])
  235. #: Tuples of str ``(heading, content)`` with extra information
  236. #: for the test report. Used by pytest to add text captured
  237. #: from ``stdout``, ``stderr``, and intercepted logging events. May
  238. #: be used by other plugins to add arbitrary information to reports.
  239. self.sections = list(sections)
  240. #: Time it took to run just the test.
  241. self.duration = duration
  242. self.__dict__.update(extra)
  243. def __repr__(self) -> str:
  244. return "<{} {!r} when={!r} outcome={!r}>".format(
  245. self.__class__.__name__, self.nodeid, self.when, self.outcome
  246. )
  247. @classmethod
  248. def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
  249. """Create and fill a TestReport with standard item and call info."""
  250. when = call.when
  251. # Remove "collect" from the Literal type -- only for collection calls.
  252. assert when != "collect"
  253. duration = call.duration
  254. keywords = {x: 1 for x in item.keywords}
  255. excinfo = call.excinfo
  256. sections = []
  257. if not call.excinfo:
  258. outcome: Literal["passed", "failed", "skipped"] = "passed"
  259. longrepr: Union[
  260. None,
  261. ExceptionInfo[BaseException],
  262. Tuple[str, int, str],
  263. str,
  264. TerminalRepr,
  265. ] = None
  266. else:
  267. if not isinstance(excinfo, ExceptionInfo):
  268. outcome = "failed"
  269. longrepr = excinfo
  270. elif isinstance(excinfo.value, skip.Exception):
  271. outcome = "skipped"
  272. r = excinfo._getreprcrash()
  273. if excinfo.value._use_item_location:
  274. path, line = item.reportinfo()[:2]
  275. assert line is not None
  276. longrepr = os.fspath(path), line + 1, r.message
  277. else:
  278. longrepr = (str(r.path), r.lineno, r.message)
  279. else:
  280. outcome = "failed"
  281. if call.when == "call":
  282. longrepr = item.repr_failure(excinfo)
  283. else: # exception in setup or teardown
  284. longrepr = item._repr_failure_py(
  285. excinfo, style=item.config.getoption("tbstyle", "auto")
  286. )
  287. for rwhen, key, content in item._report_sections:
  288. sections.append((f"Captured {key} {rwhen}", content))
  289. return cls(
  290. item.nodeid,
  291. item.location,
  292. keywords,
  293. outcome,
  294. longrepr,
  295. when,
  296. sections,
  297. duration,
  298. user_properties=item.user_properties,
  299. )
  300. @final
  301. class CollectReport(BaseReport):
  302. """Collection report object.
  303. Reports can contain arbitrary extra attributes.
  304. """
  305. when = "collect"
  306. def __init__(
  307. self,
  308. nodeid: str,
  309. outcome: "Literal['passed', 'failed', 'skipped']",
  310. longrepr: Union[
  311. None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
  312. ],
  313. result: Optional[List[Union[Item, Collector]]],
  314. sections: Iterable[Tuple[str, str]] = (),
  315. **extra,
  316. ) -> None:
  317. #: Normalized collection nodeid.
  318. self.nodeid = nodeid
  319. #: Test outcome, always one of "passed", "failed", "skipped".
  320. self.outcome = outcome
  321. #: None or a failure representation.
  322. self.longrepr = longrepr
  323. #: The collected items and collection nodes.
  324. self.result = result or []
  325. #: Tuples of str ``(heading, content)`` with extra information
  326. #: for the test report. Used by pytest to add text captured
  327. #: from ``stdout``, ``stderr``, and intercepted logging events. May
  328. #: be used by other plugins to add arbitrary information to reports.
  329. self.sections = list(sections)
  330. self.__dict__.update(extra)
  331. @property
  332. def location(self):
  333. return (self.fspath, None, self.fspath)
  334. def __repr__(self) -> str:
  335. return "<CollectReport {!r} lenresult={} outcome={!r}>".format(
  336. self.nodeid, len(self.result), self.outcome
  337. )
  338. class CollectErrorRepr(TerminalRepr):
  339. def __init__(self, msg: str) -> None:
  340. self.longrepr = msg
  341. def toterminal(self, out: TerminalWriter) -> None:
  342. out.line(self.longrepr, red=True)
  343. def pytest_report_to_serializable(
  344. report: Union[CollectReport, TestReport]
  345. ) -> Optional[Dict[str, Any]]:
  346. if isinstance(report, (TestReport, CollectReport)):
  347. data = report._to_json()
  348. data["$report_type"] = report.__class__.__name__
  349. return data
  350. # TODO: Check if this is actually reachable.
  351. return None # type: ignore[unreachable]
  352. def pytest_report_from_serializable(
  353. data: Dict[str, Any],
  354. ) -> Optional[Union[CollectReport, TestReport]]:
  355. if "$report_type" in data:
  356. if data["$report_type"] == "TestReport":
  357. return TestReport._from_json(data)
  358. elif data["$report_type"] == "CollectReport":
  359. return CollectReport._from_json(data)
  360. assert False, "Unknown report_type unserialize data: {}".format(
  361. data["$report_type"]
  362. )
  363. return None
  364. def _report_to_json(report: BaseReport) -> Dict[str, Any]:
  365. """Return the contents of this report as a dict of builtin entries,
  366. suitable for serialization.
  367. This was originally the serialize_report() function from xdist (ca03269).
  368. """
  369. def serialize_repr_entry(
  370. entry: Union[ReprEntry, ReprEntryNative]
  371. ) -> Dict[str, Any]:
  372. data = attr.asdict(entry) # type:ignore[arg-type]
  373. for key, value in data.items():
  374. if hasattr(value, "__dict__"):
  375. data[key] = attr.asdict(value)
  376. entry_data = {"type": type(entry).__name__, "data": data}
  377. return entry_data
  378. def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
  379. result = attr.asdict(reprtraceback) # type:ignore[arg-type]
  380. result["reprentries"] = [
  381. serialize_repr_entry(x) for x in reprtraceback.reprentries
  382. ]
  383. return result
  384. def serialize_repr_crash(
  385. reprcrash: Optional[ReprFileLocation],
  386. ) -> Optional[Dict[str, Any]]:
  387. if reprcrash is not None:
  388. return attr.asdict(reprcrash) # type:ignore[arg-type]
  389. else:
  390. return None
  391. def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
  392. assert rep.longrepr is not None
  393. # TODO: Investigate whether the duck typing is really necessary here.
  394. longrepr = cast(ExceptionRepr, rep.longrepr)
  395. result: Dict[str, Any] = {
  396. "reprcrash": serialize_repr_crash(longrepr.reprcrash),
  397. "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
  398. "sections": longrepr.sections,
  399. }
  400. if isinstance(longrepr, ExceptionChainRepr):
  401. result["chain"] = []
  402. for repr_traceback, repr_crash, description in longrepr.chain:
  403. result["chain"].append(
  404. (
  405. serialize_repr_traceback(repr_traceback),
  406. serialize_repr_crash(repr_crash),
  407. description,
  408. )
  409. )
  410. else:
  411. result["chain"] = None
  412. return result
  413. d = report.__dict__.copy()
  414. if hasattr(report.longrepr, "toterminal"):
  415. if hasattr(report.longrepr, "reprtraceback") and hasattr(
  416. report.longrepr, "reprcrash"
  417. ):
  418. d["longrepr"] = serialize_exception_longrepr(report)
  419. else:
  420. d["longrepr"] = str(report.longrepr)
  421. else:
  422. d["longrepr"] = report.longrepr
  423. for name in d:
  424. if isinstance(d[name], os.PathLike):
  425. d[name] = os.fspath(d[name])
  426. elif name == "result":
  427. d[name] = None # for now
  428. return d
  429. def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
  430. """Return **kwargs that can be used to construct a TestReport or
  431. CollectReport instance.
  432. This was originally the serialize_report() function from xdist (ca03269).
  433. """
  434. def deserialize_repr_entry(entry_data):
  435. data = entry_data["data"]
  436. entry_type = entry_data["type"]
  437. if entry_type == "ReprEntry":
  438. reprfuncargs = None
  439. reprfileloc = None
  440. reprlocals = None
  441. if data["reprfuncargs"]:
  442. reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
  443. if data["reprfileloc"]:
  444. reprfileloc = ReprFileLocation(**data["reprfileloc"])
  445. if data["reprlocals"]:
  446. reprlocals = ReprLocals(data["reprlocals"]["lines"])
  447. reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry(
  448. lines=data["lines"],
  449. reprfuncargs=reprfuncargs,
  450. reprlocals=reprlocals,
  451. reprfileloc=reprfileloc,
  452. style=data["style"],
  453. )
  454. elif entry_type == "ReprEntryNative":
  455. reprentry = ReprEntryNative(data["lines"])
  456. else:
  457. _report_unserialization_failure(entry_type, TestReport, reportdict)
  458. return reprentry
  459. def deserialize_repr_traceback(repr_traceback_dict):
  460. repr_traceback_dict["reprentries"] = [
  461. deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"]
  462. ]
  463. return ReprTraceback(**repr_traceback_dict)
  464. def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
  465. if repr_crash_dict is not None:
  466. return ReprFileLocation(**repr_crash_dict)
  467. else:
  468. return None
  469. if (
  470. reportdict["longrepr"]
  471. and "reprcrash" in reportdict["longrepr"]
  472. and "reprtraceback" in reportdict["longrepr"]
  473. ):
  474. reprtraceback = deserialize_repr_traceback(
  475. reportdict["longrepr"]["reprtraceback"]
  476. )
  477. reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"])
  478. if reportdict["longrepr"]["chain"]:
  479. chain = []
  480. for repr_traceback_data, repr_crash_data, description in reportdict[
  481. "longrepr"
  482. ]["chain"]:
  483. chain.append(
  484. (
  485. deserialize_repr_traceback(repr_traceback_data),
  486. deserialize_repr_crash(repr_crash_data),
  487. description,
  488. )
  489. )
  490. exception_info: Union[
  491. ExceptionChainRepr, ReprExceptionInfo
  492. ] = ExceptionChainRepr(chain)
  493. else:
  494. exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
  495. for section in reportdict["longrepr"]["sections"]:
  496. exception_info.addsection(*section)
  497. reportdict["longrepr"] = exception_info
  498. return reportdict