Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from io import StringIO 

2from pprint import pprint 

3from typing import Any 

4from typing import Dict 

5from typing import Iterable 

6from typing import Iterator 

7from typing import List 

8from typing import Optional 

9from typing import Tuple 

10from typing import TypeVar 

11from typing import Union 

12 

13import attr 

14import py 

15 

16from _pytest._code.code import ExceptionChainRepr 

17from _pytest._code.code import ExceptionInfo 

18from _pytest._code.code import ReprEntry 

19from _pytest._code.code import ReprEntryNative 

20from _pytest._code.code import ReprExceptionInfo 

21from _pytest._code.code import ReprFileLocation 

22from _pytest._code.code import ReprFuncArgs 

23from _pytest._code.code import ReprLocals 

24from _pytest._code.code import ReprTraceback 

25from _pytest._code.code import TerminalRepr 

26from _pytest._io import TerminalWriter 

27from _pytest.compat import TYPE_CHECKING 

28from _pytest.config import Config 

29from _pytest.nodes import Collector 

30from _pytest.nodes import Item 

31from _pytest.outcomes import skip 

32from _pytest.pathlib import Path 

33 

34if TYPE_CHECKING: 

35 from typing import NoReturn 

36 from typing_extensions import Type 

37 from typing_extensions import Literal 

38 

39 from _pytest.runner import CallInfo 

40 

41 

42def getworkerinfoline(node): 

43 try: 

44 return node._workerinfocache 

45 except AttributeError: 

46 d = node.workerinfo 

47 ver = "%s.%s.%s" % d["version_info"][:3] 

48 node._workerinfocache = s = "[{}] {} -- Python {} {}".format( 

49 d["id"], d["sysplatform"], ver, d["executable"] 

50 ) 

51 return s 

52 

53 

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

55 

56 

57class BaseReport: 

58 when = None # type: Optional[str] 

59 location = None # type: Optional[Tuple[str, Optional[int], str]] 

60 # TODO: Improve this Any. 

61 longrepr = None # type: Optional[Any] 

62 sections = [] # type: List[Tuple[str, str]] 

63 nodeid = None # type: str 

64 

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

66 self.__dict__.update(kw) 

67 

68 if TYPE_CHECKING: 

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

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

71 raise NotImplementedError() 

72 

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

74 if hasattr(self, "node"): 

75 out.line(getworkerinfoline(self.node)) 

76 

77 longrepr = self.longrepr 

78 if longrepr is None: 

79 return 

80 

81 if hasattr(longrepr, "toterminal"): 

82 longrepr.toterminal(out) 

83 else: 

84 try: 

85 s = str(longrepr) 

86 except UnicodeEncodeError: 

87 s = "<unprintable longrepr>" 

88 out.line(s) 

89 

90 def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]: 

91 for name, content in self.sections: 

92 if name.startswith(prefix): 

93 yield prefix, content 

94 

95 @property 

96 def longreprtext(self) -> str: 

97 """ 

98 Read-only property that returns the full string representation 

99 of ``longrepr``. 

100 

101 .. versionadded:: 3.0 

102 """ 

103 file = StringIO() 

104 tw = TerminalWriter(file) 

105 tw.hasmarkup = False 

106 self.toterminal(tw) 

107 exc = file.getvalue() 

108 return exc.strip() 

109 

110 @property 

111 def caplog(self) -> str: 

112 """Return captured log lines, if log capturing is enabled 

113 

114 .. versionadded:: 3.5 

115 """ 

116 return "\n".join( 

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

118 ) 

119 

120 @property 

121 def capstdout(self) -> str: 

122 """Return captured text from stdout, if capturing is enabled 

123 

124 .. versionadded:: 3.0 

125 """ 

126 return "".join( 

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

128 ) 

129 

130 @property 

131 def capstderr(self) -> str: 

132 """Return captured text from stderr, if capturing is enabled 

133 

134 .. versionadded:: 3.0 

135 """ 

136 return "".join( 

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

138 ) 

139 

140 passed = property(lambda x: x.outcome == "passed") 

141 failed = property(lambda x: x.outcome == "failed") 

142 skipped = property(lambda x: x.outcome == "skipped") 

143 

144 @property 

145 def fspath(self) -> str: 

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

147 

148 @property 

149 def count_towards_summary(self) -> bool: 

150 """ 

151 **Experimental** 

152 

153 ``True`` if this report should be counted towards the totals shown at the end of the 

154 test session: "1 passed, 1 failure, etc". 

155 

156 .. note:: 

157 

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

159 even in patch releases. 

160 """ 

161 return True 

162 

163 @property 

164 def head_line(self) -> Optional[str]: 

165 """ 

166 **Experimental** 

167 

168 Returns the head line shown with longrepr output for this report, more commonly during 

169 traceback representation during failures:: 

170 

171 ________ Test.foo ________ 

172 

173 

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

175 

176 .. note:: 

177 

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

179 even in patch releases. 

180 """ 

181 if self.location is not None: 

182 fspath, lineno, domain = self.location 

183 return domain 

184 return None 

185 

186 def _get_verbose_word(self, config: Config): 

187 _category, _short, verbose = config.hook.pytest_report_teststatus( 

188 report=self, config=config 

189 ) 

190 return verbose 

191 

192 def _to_json(self) -> Dict[str, Any]: 

193 """ 

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

195 

196 Returns the contents of this report as a dict of builtin entries, suitable for 

197 serialization. 

198 

199 Experimental method. 

200 """ 

201 return _report_to_json(self) 

202 

203 @classmethod 

204 def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R: 

205 """ 

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

207 

208 Factory method that returns either a TestReport or CollectReport, depending on the calling 

209 class. It's the callers responsibility to know which class to pass here. 

210 

211 Experimental method. 

212 """ 

213 kwargs = _report_kwargs_from_json(reportdict) 

214 return cls(**kwargs) 

215 

216 

217def _report_unserialization_failure( 

218 type_name: str, report_class: "Type[BaseReport]", reportdict 

219) -> "NoReturn": 

220 url = "https://github.com/pytest-dev/pytest/issues" 

221 stream = StringIO() 

222 pprint("-" * 100, stream=stream) 

223 pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream) 

224 pprint("report_name: %s" % report_class, stream=stream) 

225 pprint(reportdict, stream=stream) 

226 pprint("Please report this bug at %s" % url, stream=stream) 

227 pprint("-" * 100, stream=stream) 

228 raise RuntimeError(stream.getvalue()) 

229 

230 

231class TestReport(BaseReport): 

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

233 they fail). 

234 """ 

235 

236 __test__ = False 

237 

238 def __init__( 

239 self, 

240 nodeid: str, 

241 location: Tuple[str, Optional[int], str], 

242 keywords, 

243 outcome: "Literal['passed', 'failed', 'skipped']", 

244 longrepr, 

245 when: "Literal['setup', 'call', 'teardown']", 

246 sections: Iterable[Tuple[str, str]] = (), 

247 duration: float = 0, 

248 user_properties: Optional[Iterable[Tuple[str, object]]] = None, 

249 **extra 

250 ) -> None: 

251 #: normalized collection node id 

252 self.nodeid = nodeid 

253 

254 #: a (filesystempath, lineno, domaininfo) tuple indicating the 

255 #: actual location of a test item - it might be different from the 

256 #: collected one e.g. if a method is inherited from a different module. 

257 self.location = location # type: Tuple[str, Optional[int], str] 

258 

259 #: a name -> value dictionary containing all keywords and 

260 #: markers associated with a test invocation. 

261 self.keywords = keywords 

262 

263 #: test outcome, always one of "passed", "failed", "skipped". 

264 self.outcome = outcome 

265 

266 #: None or a failure representation. 

267 self.longrepr = longrepr 

268 

269 #: one of 'setup', 'call', 'teardown' to indicate runtest phase. 

270 self.when = when 

271 

272 #: user properties is a list of tuples (name, value) that holds user 

273 #: defined properties of the test 

274 self.user_properties = list(user_properties or []) 

275 

276 #: list of pairs ``(str, str)`` of extra information which needs to 

277 #: marshallable. Used by pytest to add captured text 

278 #: from ``stdout`` and ``stderr``, but may be used by other plugins 

279 #: to add arbitrary information to reports. 

280 self.sections = list(sections) 

281 

282 #: time it took to run just the test 

283 self.duration = duration 

284 

285 self.__dict__.update(extra) 

286 

287 def __repr__(self) -> str: 

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

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

290 ) 

291 

292 @classmethod 

293 def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": 

294 """ 

295 Factory method to create and fill a TestReport with standard item and call info. 

296 """ 

297 when = call.when 

298 # Remove "collect" from the Literal type -- only for collection calls. 

299 assert when != "collect" 

300 duration = call.duration 

301 keywords = {x: 1 for x in item.keywords} 

302 excinfo = call.excinfo 

303 sections = [] 

304 if not call.excinfo: 

305 outcome = "passed" # type: Literal["passed", "failed", "skipped"] 

306 # TODO: Improve this Any. 

307 longrepr = None # type: Optional[Any] 

308 else: 

309 if not isinstance(excinfo, ExceptionInfo): 

310 outcome = "failed" 

311 longrepr = excinfo 

312 elif isinstance(excinfo.value, skip.Exception): 

313 outcome = "skipped" 

314 r = excinfo._getreprcrash() 

315 longrepr = (str(r.path), r.lineno, r.message) 

316 else: 

317 outcome = "failed" 

318 if call.when == "call": 

319 longrepr = item.repr_failure(excinfo) 

320 else: # exception in setup or teardown 

321 longrepr = item._repr_failure_py( 

322 excinfo, style=item.config.getoption("tbstyle", "auto") 

323 ) 

324 for rwhen, key, content in item._report_sections: 

325 sections.append(("Captured {} {}".format(key, rwhen), content)) 

326 return cls( 

327 item.nodeid, 

328 item.location, 

329 keywords, 

330 outcome, 

331 longrepr, 

332 when, 

333 sections, 

334 duration, 

335 user_properties=item.user_properties, 

336 ) 

337 

338 

339class CollectReport(BaseReport): 

340 """Collection report object.""" 

341 

342 when = "collect" 

343 

344 def __init__( 

345 self, 

346 nodeid: str, 

347 outcome: "Literal['passed', 'skipped', 'failed']", 

348 longrepr, 

349 result: Optional[List[Union[Item, Collector]]], 

350 sections: Iterable[Tuple[str, str]] = (), 

351 **extra 

352 ) -> None: 

353 #: normalized collection node id 

354 self.nodeid = nodeid 

355 

356 #: test outcome, always one of "passed", "failed", "skipped". 

357 self.outcome = outcome 

358 

359 #: None or a failure representation. 

360 self.longrepr = longrepr 

361 

362 #: The collected items and collection nodes. 

363 self.result = result or [] 

364 

365 #: list of pairs ``(str, str)`` of extra information which needs to 

366 #: marshallable. Used by pytest to add captured text 

367 #: from ``stdout`` and ``stderr``, but may be used by other plugins 

368 #: to add arbitrary information to reports. 

369 self.sections = list(sections) 

370 

371 self.__dict__.update(extra) 

372 

373 @property 

374 def location(self): 

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

376 

377 def __repr__(self) -> str: 

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

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

380 ) 

381 

382 

383class CollectErrorRepr(TerminalRepr): 

384 def __init__(self, msg) -> None: 

385 self.longrepr = msg 

386 

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

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

389 

390 

391def pytest_report_to_serializable( 

392 report: Union[CollectReport, TestReport] 

393) -> Optional[Dict[str, Any]]: 

394 if isinstance(report, (TestReport, CollectReport)): 

395 data = report._to_json() 

396 data["$report_type"] = report.__class__.__name__ 

397 return data 

398 return None 

399 

400 

401def pytest_report_from_serializable( 

402 data: Dict[str, Any], 

403) -> Optional[Union[CollectReport, TestReport]]: 

404 if "$report_type" in data: 

405 if data["$report_type"] == "TestReport": 

406 return TestReport._from_json(data) 

407 elif data["$report_type"] == "CollectReport": 

408 return CollectReport._from_json(data) 

409 assert False, "Unknown report_type unserialize data: {}".format( 

410 data["$report_type"] 

411 ) 

412 return None 

413 

414 

415def _report_to_json(report: BaseReport) -> Dict[str, Any]: 

416 """ 

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

418 

419 Returns the contents of this report as a dict of builtin entries, suitable for 

420 serialization. 

421 """ 

422 

423 def serialize_repr_entry( 

424 entry: Union[ReprEntry, ReprEntryNative] 

425 ) -> Dict[str, Any]: 

426 data = attr.asdict(entry) 

427 for key, value in data.items(): 

428 if hasattr(value, "__dict__"): 

429 data[key] = attr.asdict(value) 

430 entry_data = {"type": type(entry).__name__, "data": data} 

431 return entry_data 

432 

433 def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: 

434 result = attr.asdict(reprtraceback) 

435 result["reprentries"] = [ 

436 serialize_repr_entry(x) for x in reprtraceback.reprentries 

437 ] 

438 return result 

439 

440 def serialize_repr_crash( 

441 reprcrash: Optional[ReprFileLocation], 

442 ) -> Optional[Dict[str, Any]]: 

443 if reprcrash is not None: 

444 return attr.asdict(reprcrash) 

445 else: 

446 return None 

447 

448 def serialize_longrepr(rep: BaseReport) -> Dict[str, Any]: 

449 assert rep.longrepr is not None 

450 result = { 

451 "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash), 

452 "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback), 

453 "sections": rep.longrepr.sections, 

454 } # type: Dict[str, Any] 

455 if isinstance(rep.longrepr, ExceptionChainRepr): 

456 result["chain"] = [] 

457 for repr_traceback, repr_crash, description in rep.longrepr.chain: 

458 result["chain"].append( 

459 ( 

460 serialize_repr_traceback(repr_traceback), 

461 serialize_repr_crash(repr_crash), 

462 description, 

463 ) 

464 ) 

465 else: 

466 result["chain"] = None 

467 return result 

468 

469 d = report.__dict__.copy() 

470 if hasattr(report.longrepr, "toterminal"): 

471 if hasattr(report.longrepr, "reprtraceback") and hasattr( 

472 report.longrepr, "reprcrash" 

473 ): 

474 d["longrepr"] = serialize_longrepr(report) 

475 else: 

476 d["longrepr"] = str(report.longrepr) 

477 else: 

478 d["longrepr"] = report.longrepr 

479 for name in d: 

480 if isinstance(d[name], (py.path.local, Path)): 

481 d[name] = str(d[name]) 

482 elif name == "result": 

483 d[name] = None # for now 

484 return d 

485 

486 

487def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: 

488 """ 

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

490 

491 Returns **kwargs that can be used to construct a TestReport or CollectReport instance. 

492 """ 

493 

494 def deserialize_repr_entry(entry_data): 

495 data = entry_data["data"] 

496 entry_type = entry_data["type"] 

497 if entry_type == "ReprEntry": 

498 reprfuncargs = None 

499 reprfileloc = None 

500 reprlocals = None 

501 if data["reprfuncargs"]: 

502 reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) 

503 if data["reprfileloc"]: 

504 reprfileloc = ReprFileLocation(**data["reprfileloc"]) 

505 if data["reprlocals"]: 

506 reprlocals = ReprLocals(data["reprlocals"]["lines"]) 

507 

508 reprentry = ReprEntry( 

509 lines=data["lines"], 

510 reprfuncargs=reprfuncargs, 

511 reprlocals=reprlocals, 

512 reprfileloc=reprfileloc, 

513 style=data["style"], 

514 ) # type: Union[ReprEntry, ReprEntryNative] 

515 elif entry_type == "ReprEntryNative": 

516 reprentry = ReprEntryNative(data["lines"]) 

517 else: 

518 _report_unserialization_failure(entry_type, TestReport, reportdict) 

519 return reprentry 

520 

521 def deserialize_repr_traceback(repr_traceback_dict): 

522 repr_traceback_dict["reprentries"] = [ 

523 deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"] 

524 ] 

525 return ReprTraceback(**repr_traceback_dict) 

526 

527 def deserialize_repr_crash(repr_crash_dict: Optional[dict]): 

528 if repr_crash_dict is not None: 

529 return ReprFileLocation(**repr_crash_dict) 

530 else: 

531 return None 

532 

533 if ( 

534 reportdict["longrepr"] 

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

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

537 ): 

538 

539 reprtraceback = deserialize_repr_traceback( 

540 reportdict["longrepr"]["reprtraceback"] 

541 ) 

542 reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]) 

543 if reportdict["longrepr"]["chain"]: 

544 chain = [] 

545 for repr_traceback_data, repr_crash_data, description in reportdict[ 

546 "longrepr" 

547 ]["chain"]: 

548 chain.append( 

549 ( 

550 deserialize_repr_traceback(repr_traceback_data), 

551 deserialize_repr_crash(repr_crash_data), 

552 description, 

553 ) 

554 ) 

555 exception_info = ExceptionChainRepr( 

556 chain 

557 ) # type: Union[ExceptionChainRepr,ReprExceptionInfo] 

558 else: 

559 exception_info = ReprExceptionInfo(reprtraceback, reprcrash) 

560 

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

562 exception_info.addsection(*section) 

563 reportdict["longrepr"] = exception_info 

564 

565 return reportdict