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

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 

18 

19import attr 

20 

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 

38 

39if TYPE_CHECKING: 

40 from typing_extensions import Literal 

41 

42 from _pytest.runner import CallInfo 

43 

44 

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 

55 

56 

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

58 

59 

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

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 

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

242 

243 

244@final 

245class 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 

370class 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 

419class 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 

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] 

436 

437 

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 

450 

451 

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. 

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 

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. 

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