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.

573 lines
19KB

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