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 NoReturn

12 from typing import Optional

13 from typing import Tuple

14 from typing import Type

15 from typing import TYPE_CHECKING

16 from typing import TypeVar

17 from typing import Union

18

19 import attr

20

21 from _pytest._code.code import ExceptionChainRepr

22 from _pytest._code.code import ExceptionInfo

23 from _pytest._code.code import ExceptionRepr

24 from _pytest._code.code import ReprEntry

25 from _pytest._code.code import ReprEntryNative

26 from _pytest._code.code import ReprExceptionInfo

27 from _pytest._code.code import ReprFileLocation

28 from _pytest._code.code import ReprFuncArgs

29 from _pytest._code.code import ReprLocals

30 from _pytest._code.code import ReprTraceback

31 from _pytest._code.code import TerminalRepr

32 from _pytest._io import TerminalWriter

33 from _pytest.compat import final

34 from _pytest.config import Config

35 from _pytest.nodes import Collector

36 from _pytest.nodes import Item

37 from _pytest.outcomes import skip

38

39 if TYPE_CHECKING:

40 from typing_extensions import Literal

41

42 from _pytest.runner import CallInfo

43

44

45 def 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

55

56

57_R = TypeVar("_R", bound="BaseReport")

58

59

60 class 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']"

69

70 def __init__(self, **kw: Any) -> None:

71 self.__dict__.update(kw)

72

73 if TYPE_CHECKING:

74 # Can have arbitrary fields given to __init__().

75 def __getattr__(self, key: str) -> Any:

76 ...

77

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)

83

84 longrepr = self.longrepr

85 if longrepr is None:

86 return

87

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)

97

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

102

103 @property

104 def longreprtext(self) -> str:

105 """Read-only property that returns the full string representation of

106 ``longrepr``.

107

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()

116

117 @property

118 def caplog(self) -> str:

119 """Return captured log lines, if log capturing is enabled.

120

121 .. versionadded:: 3.5

122 """

123 return "\n".join(

124 content for (prefix, content) in self.get_sections("Captured log")

125 )

126

127 @property

128 def capstdout(self) -> str:

129 """Return captured text from stdout, if capturing is enabled.

130

131 .. versionadded:: 3.0

132 """

133 return "".join(

134 content for (prefix, content) in self.get_sections("Captured stdout")

135 )

136

137 @property

138 def capstderr(self) -> str:

139 """Return captured text from stderr, if capturing is enabled.

140

141 .. versionadded:: 3.0

142 """

143 return "".join(

144 content for (prefix, content) in self.get_sections("Captured stderr")

145 )

146

147 @property

148 def passed(self) -> bool:

149 """Whether the outcome is passed."""

150 return self.outcome == "passed"

151

152 @property

153 def failed(self) -> bool:

154 """Whether the outcome is failed."""

155 return self.outcome == "failed"

156

157 @property

158 def skipped(self) -> bool:

159 """Whether the outcome is skipped."""

160 return self.outcome == "skipped"

161

162 @property

163 def fspath(self) -> str:

164 """The path portion of the reported node, as a string."""

165 return self.nodeid.split("::")[0]

166

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".

171

172 .. note::

173

174 This function is considered **experimental**, so beware that it is subject to changes

175 even in patch releases.

176 """

177 return True

178

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::

184

185 ________ Test.foo ________

186

187

188 In the example above, the head_line is "Test.foo".

189

190 .. note::

191

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

199

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

205

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.

209

210 This was originally the serialize_report() function from xdist (ca03269).

211

212 Experimental method.

213 """

214 return _report_to_json(self)

215

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.

219

220 It is the callers responsibility to know which class to pass here.

221

222 This was originally the serialize_report() function from xdist (ca03269).

223

224 Experimental method.

225 """

226 kwargs = _report_kwargs_from_json(reportdict)

227 return cls(**kwargs)

228

229

230 def _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())

242

243

244@final

245 class TestReport(BaseReport):

246 """Basic test report object (also used for setup and teardown calls if

247 they fail).

248

249 Reports can contain arbitrary extra attributes.

250 """

251

252 __test__ = False

253

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

271

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

276

277 #: A name -> value dictionary containing all keywords and

278 #: markers associated with a test invocation.

279 self.keywords: Mapping[str, Any] = keywords

280

281 #: Test outcome, always one of "passed", "failed", "skipped".

282 self.outcome = outcome

283

284 #: None or a failure representation.

285 self.longrepr = longrepr

286

287 #: One of 'setup', 'call', 'teardown' to indicate runtest phase.

288 self.when = when

289

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 [])

293

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)

299

300 #: Time it took to run just the test.

301 self.duration: float = duration

302

303 self.__dict__.update(extra)

304

305 def __repr__(self) -> str:

306 return "<{} {!r} when={!r} outcome={!r}>".format(

307 self.__class__.__name__, self.nodeid, self.when, self.outcome

308 )

309

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.

313

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 )

367

368

369@final

370 class CollectReport(BaseReport):

371 """Collection report object.

372

373 Reports can contain arbitrary extra attributes.

374 """

375

376 when = "collect"

377

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

391

392 #: Test outcome, always one of "passed", "failed", "skipped".

393 self.outcome = outcome

394

395 #: None or a failure representation.

396 self.longrepr = longrepr

397

398 #: The collected items and collection nodes.

399 self.result = result or []

400

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)

406

407 self.__dict__.update(extra)

408

409 @property

410 def location(self):

411 return (self.fspath, None, self.fspath)

412

413 def __repr__(self) -> str:

414 return "<CollectReport {!r} lenresult={} outcome={!r}>".format(

415 self.nodeid, len(self.result), self.outcome

416 )

417

418

419 class CollectErrorRepr(TerminalRepr):

420 def __init__(self, msg: str) -> None:

421 self.longrepr = msg

422

423 def toterminal(self, out: TerminalWriter) -> None:

424 out.line(self.longrepr, red=True)

425

426

427 def 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]

436

437

438 def 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

450

451

452 def _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.

455

456 This was originally the serialize_report() function from xdist (ca03269).

457 """

458

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

468

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

475

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

483

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

506

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

523

524

525 def _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.

528

529 This was originally the serialize_report() function from xdist (ca03269).

530 """

531

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"])

545

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

558

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)

564

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

570

571 if (

572 reportdict["longrepr"]

573 and "reprcrash" in reportdict["longrepr"]

574 and "reprtraceback" in reportdict["longrepr"]

575 ):

576

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)

598

599 for section in reportdict["longrepr"]["sections"]:

600 exception_info.addsection(*section)

601 reportdict["longrepr"] = exception_info

602

603 return reportdict