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