Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/runner.py: 71%

287 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""Basic collect and runtest protocol implementations.""" 

2import bdb 

3import os 

4import sys 

5from typing import Callable 

6from typing import cast 

7from typing import Dict 

8from typing import Generic 

9from typing import List 

10from typing import Optional 

11from typing import Tuple 

12from typing import Type 

13from typing import TYPE_CHECKING 

14from typing import TypeVar 

15from typing import Union 

16 

17import attr 

18 

19from .reports import BaseReport 

20from .reports import CollectErrorRepr 

21from .reports import CollectReport 

22from .reports import TestReport 

23from _pytest import timing 

24from _pytest._code.code import ExceptionChainRepr 

25from _pytest._code.code import ExceptionInfo 

26from _pytest._code.code import TerminalRepr 

27from _pytest.compat import final 

28from _pytest.config.argparsing import Parser 

29from _pytest.deprecated import check_ispytest 

30from _pytest.nodes import Collector 

31from _pytest.nodes import Item 

32from _pytest.nodes import Node 

33from _pytest.outcomes import Exit 

34from _pytest.outcomes import OutcomeException 

35from _pytest.outcomes import Skipped 

36from _pytest.outcomes import TEST_OUTCOME 

37 

38if TYPE_CHECKING: 

39 from typing_extensions import Literal 

40 

41 from _pytest.main import Session 

42 from _pytest.terminal import TerminalReporter 

43 

44# 

45# pytest plugin hooks. 

46 

47 

48def pytest_addoption(parser: Parser) -> None: 

49 group = parser.getgroup("terminal reporting", "Reporting", after="general") 

50 group.addoption( 

51 "--durations", 

52 action="store", 

53 type=int, 

54 default=None, 

55 metavar="N", 

56 help="Show N slowest setup/test durations (N=0 for all)", 

57 ) 

58 group.addoption( 

59 "--durations-min", 

60 action="store", 

61 type=float, 

62 default=0.005, 

63 metavar="N", 

64 help="Minimal duration in seconds for inclusion in slowest list. " 

65 "Default: 0.005.", 

66 ) 

67 

68 

69def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: 

70 durations = terminalreporter.config.option.durations 

71 durations_min = terminalreporter.config.option.durations_min 

72 verbose = terminalreporter.config.getvalue("verbose") 

73 if durations is None: 

74 return 

75 tr = terminalreporter 

76 dlist = [] 

77 for replist in tr.stats.values(): 

78 for rep in replist: 

79 if hasattr(rep, "duration"): 

80 dlist.append(rep) 

81 if not dlist: 

82 return 

83 dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return] 

84 if not durations: 

85 tr.write_sep("=", "slowest durations") 

86 else: 

87 tr.write_sep("=", "slowest %s durations" % durations) 

88 dlist = dlist[:durations] 

89 

90 for i, rep in enumerate(dlist): 

91 if verbose < 2 and rep.duration < durations_min: 

92 tr.write_line("") 

93 tr.write_line( 

94 "(%s durations < %gs hidden. Use -vv to show these durations.)" 

95 % (len(dlist) - i, durations_min) 

96 ) 

97 break 

98 tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}") 

99 

100 

101def pytest_sessionstart(session: "Session") -> None: 

102 session._setupstate = SetupState() 

103 

104 

105def pytest_sessionfinish(session: "Session") -> None: 

106 session._setupstate.teardown_exact(None) 

107 

108 

109def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: 

110 ihook = item.ihook 

111 ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) 

112 runtestprotocol(item, nextitem=nextitem) 

113 ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) 

114 return True 

115 

116 

117def runtestprotocol( 

118 item: Item, log: bool = True, nextitem: Optional[Item] = None 

119) -> List[TestReport]: 

120 hasrequest = hasattr(item, "_request") 

121 if hasrequest and not item._request: # type: ignore[attr-defined] 

122 # This only happens if the item is re-run, as is done by 

123 # pytest-rerunfailures. 

124 item._initrequest() # type: ignore[attr-defined] 

125 rep = call_and_report(item, "setup", log) 

126 reports = [rep] 

127 if rep.passed: 

128 if item.config.getoption("setupshow", False): 

129 show_test_item(item) 

130 if not item.config.getoption("setuponly", False): 

131 reports.append(call_and_report(item, "call", log)) 

132 reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) 

133 # After all teardown hooks have been called 

134 # want funcargs and request info to go away. 

135 if hasrequest: 

136 item._request = False # type: ignore[attr-defined] 

137 item.funcargs = None # type: ignore[attr-defined] 

138 return reports 

139 

140 

141def show_test_item(item: Item) -> None: 

142 """Show test function, parameters and the fixtures of the test item.""" 

143 tw = item.config.get_terminal_writer() 

144 tw.line() 

145 tw.write(" " * 8) 

146 tw.write(item.nodeid) 

147 used_fixtures = sorted(getattr(item, "fixturenames", [])) 

148 if used_fixtures: 

149 tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) 

150 tw.flush() 

151 

152 

153def pytest_runtest_setup(item: Item) -> None: 

154 _update_current_test_var(item, "setup") 

155 item.session._setupstate.setup(item) 

156 

157 

158def pytest_runtest_call(item: Item) -> None: 

159 _update_current_test_var(item, "call") 

160 try: 

161 del sys.last_type 

162 del sys.last_value 

163 del sys.last_traceback 

164 except AttributeError: 

165 pass 

166 try: 

167 item.runtest() 

168 except Exception as e: 

169 # Store trace info to allow postmortem debugging 

170 sys.last_type = type(e) 

171 sys.last_value = e 

172 assert e.__traceback__ is not None 

173 # Skip *this* frame 

174 sys.last_traceback = e.__traceback__.tb_next 

175 raise e 

176 

177 

178def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: 

179 _update_current_test_var(item, "teardown") 

180 item.session._setupstate.teardown_exact(nextitem) 

181 _update_current_test_var(item, None) 

182 

183 

184def _update_current_test_var( 

185 item: Item, when: Optional["Literal['setup', 'call', 'teardown']"] 

186) -> None: 

187 """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. 

188 

189 If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment. 

190 """ 

191 var_name = "PYTEST_CURRENT_TEST" 

192 if when: 

193 value = f"{item.nodeid} ({when})" 

194 # don't allow null bytes on environment variables (see #2644, #2957) 

195 value = value.replace("\x00", "(null)") 

196 os.environ[var_name] = value 

197 else: 

198 os.environ.pop(var_name) 

199 

200 

201def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: 

202 if report.when in ("setup", "teardown"): 

203 if report.failed: 

204 # category, shortletter, verbose-word 

205 return "error", "E", "ERROR" 

206 elif report.skipped: 

207 return "skipped", "s", "SKIPPED" 

208 else: 

209 return "", "", "" 

210 return None 

211 

212 

213# 

214# Implementation 

215 

216 

217def call_and_report( 

218 item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds 

219) -> TestReport: 

220 call = call_runtest_hook(item, when, **kwds) 

221 hook = item.ihook 

222 report: TestReport = hook.pytest_runtest_makereport(item=item, call=call) 

223 if log: 

224 hook.pytest_runtest_logreport(report=report) 

225 if check_interactive_exception(call, report): 

226 hook.pytest_exception_interact(node=item, call=call, report=report) 

227 return report 

228 

229 

230def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool: 

231 """Check whether the call raised an exception that should be reported as 

232 interactive.""" 

233 if call.excinfo is None: 

234 # Didn't raise. 

235 return False 

236 if hasattr(report, "wasxfail"): 

237 # Exception was expected. 

238 return False 

239 if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)): 

240 # Special control flow exception. 

241 return False 

242 return True 

243 

244 

245def call_runtest_hook( 

246 item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds 

247) -> "CallInfo[None]": 

248 if when == "setup": 

249 ihook: Callable[..., None] = item.ihook.pytest_runtest_setup 

250 elif when == "call": 

251 ihook = item.ihook.pytest_runtest_call 

252 elif when == "teardown": 

253 ihook = item.ihook.pytest_runtest_teardown 

254 else: 

255 assert False, f"Unhandled runtest hook case: {when}" 

256 reraise: Tuple[Type[BaseException], ...] = (Exit,) 

257 if not item.config.getoption("usepdb", False): 

258 reraise += (KeyboardInterrupt,) 

259 return CallInfo.from_call( 

260 lambda: ihook(item=item, **kwds), when=when, reraise=reraise 

261 ) 

262 

263 

264TResult = TypeVar("TResult", covariant=True) 

265 

266 

267@final 

268@attr.s(repr=False, init=False, auto_attribs=True) 

269class CallInfo(Generic[TResult]): 

270 """Result/Exception info of a function invocation.""" 

271 

272 _result: Optional[TResult] 

273 #: The captured exception of the call, if it raised. 

274 excinfo: Optional[ExceptionInfo[BaseException]] 

275 #: The system time when the call started, in seconds since the epoch. 

276 start: float 

277 #: The system time when the call ended, in seconds since the epoch. 

278 stop: float 

279 #: The call duration, in seconds. 

280 duration: float 

281 #: The context of invocation: "collect", "setup", "call" or "teardown". 

282 when: "Literal['collect', 'setup', 'call', 'teardown']" 

283 

284 def __init__( 

285 self, 

286 result: Optional[TResult], 

287 excinfo: Optional[ExceptionInfo[BaseException]], 

288 start: float, 

289 stop: float, 

290 duration: float, 

291 when: "Literal['collect', 'setup', 'call', 'teardown']", 

292 *, 

293 _ispytest: bool = False, 

294 ) -> None: 

295 check_ispytest(_ispytest) 

296 self._result = result 

297 self.excinfo = excinfo 

298 self.start = start 

299 self.stop = stop 

300 self.duration = duration 

301 self.when = when 

302 

303 @property 

304 def result(self) -> TResult: 

305 """The return value of the call, if it didn't raise. 

306 

307 Can only be accessed if excinfo is None. 

308 """ 

309 if self.excinfo is not None: 

310 raise AttributeError(f"{self!r} has no valid result") 

311 # The cast is safe because an exception wasn't raised, hence 

312 # _result has the expected function return type (which may be 

313 # None, that's why a cast and not an assert). 

314 return cast(TResult, self._result) 

315 

316 @classmethod 

317 def from_call( 

318 cls, 

319 func: "Callable[[], TResult]", 

320 when: "Literal['collect', 'setup', 'call', 'teardown']", 

321 reraise: Optional[ 

322 Union[Type[BaseException], Tuple[Type[BaseException], ...]] 

323 ] = None, 

324 ) -> "CallInfo[TResult]": 

325 """Call func, wrapping the result in a CallInfo. 

326 

327 :param func: 

328 The function to call. Called without arguments. 

329 :param when: 

330 The phase in which the function is called. 

331 :param reraise: 

332 Exception or exceptions that shall propagate if raised by the 

333 function, instead of being wrapped in the CallInfo. 

334 """ 

335 excinfo = None 

336 start = timing.time() 

337 precise_start = timing.perf_counter() 

338 try: 

339 result: Optional[TResult] = func() 

340 except BaseException: 

341 excinfo = ExceptionInfo.from_current() 

342 if reraise is not None and isinstance(excinfo.value, reraise): 

343 raise 

344 result = None 

345 # use the perf counter 

346 precise_stop = timing.perf_counter() 

347 duration = precise_stop - precise_start 

348 stop = timing.time() 

349 return cls( 

350 start=start, 

351 stop=stop, 

352 duration=duration, 

353 when=when, 

354 result=result, 

355 excinfo=excinfo, 

356 _ispytest=True, 

357 ) 

358 

359 def __repr__(self) -> str: 

360 if self.excinfo is None: 

361 return f"<CallInfo when={self.when!r} result: {self._result!r}>" 

362 return f"<CallInfo when={self.when!r} excinfo={self.excinfo!r}>" 

363 

364 

365def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: 

366 return TestReport.from_item_and_call(item, call) 

367 

368 

369def pytest_make_collect_report(collector: Collector) -> CollectReport: 

370 call = CallInfo.from_call(lambda: list(collector.collect()), "collect") 

371 longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None 

372 if not call.excinfo: 

373 outcome: Literal["passed", "skipped", "failed"] = "passed" 

374 else: 

375 skip_exceptions = [Skipped] 

376 unittest = sys.modules.get("unittest") 

377 if unittest is not None: 

378 # Type ignored because unittest is loaded dynamically. 

379 skip_exceptions.append(unittest.SkipTest) # type: ignore 

380 if isinstance(call.excinfo.value, tuple(skip_exceptions)): 

381 outcome = "skipped" 

382 r_ = collector._repr_failure_py(call.excinfo, "line") 

383 assert isinstance(r_, ExceptionChainRepr), repr(r_) 

384 r = r_.reprcrash 

385 assert r 

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

387 else: 

388 outcome = "failed" 

389 errorinfo = collector.repr_failure(call.excinfo) 

390 if not hasattr(errorinfo, "toterminal"): 

391 assert isinstance(errorinfo, str) 

392 errorinfo = CollectErrorRepr(errorinfo) 

393 longrepr = errorinfo 

394 result = call.result if not call.excinfo else None 

395 rep = CollectReport(collector.nodeid, outcome, longrepr, result) 

396 rep.call = call # type: ignore # see collect_one_node 

397 return rep 

398 

399 

400class SetupState: 

401 """Shared state for setting up/tearing down test items or collectors 

402 in a session. 

403 

404 Suppose we have a collection tree as follows: 

405 

406 <Session session> 

407 <Module mod1> 

408 <Function item1> 

409 <Module mod2> 

410 <Function item2> 

411 

412 The SetupState maintains a stack. The stack starts out empty: 

413 

414 [] 

415 

416 During the setup phase of item1, setup(item1) is called. What it does 

417 is: 

418 

419 push session to stack, run session.setup() 

420 push mod1 to stack, run mod1.setup() 

421 push item1 to stack, run item1.setup() 

422 

423 The stack is: 

424 

425 [session, mod1, item1] 

426 

427 While the stack is in this shape, it is allowed to add finalizers to 

428 each of session, mod1, item1 using addfinalizer(). 

429 

430 During the teardown phase of item1, teardown_exact(item2) is called, 

431 where item2 is the next item to item1. What it does is: 

432 

433 pop item1 from stack, run its teardowns 

434 pop mod1 from stack, run its teardowns 

435 

436 mod1 was popped because it ended its purpose with item1. The stack is: 

437 

438 [session] 

439 

440 During the setup phase of item2, setup(item2) is called. What it does 

441 is: 

442 

443 push mod2 to stack, run mod2.setup() 

444 push item2 to stack, run item2.setup() 

445 

446 Stack: 

447 

448 [session, mod2, item2] 

449 

450 During the teardown phase of item2, teardown_exact(None) is called, 

451 because item2 is the last item. What it does is: 

452 

453 pop item2 from stack, run its teardowns 

454 pop mod2 from stack, run its teardowns 

455 pop session from stack, run its teardowns 

456 

457 Stack: 

458 

459 [] 

460 

461 The end! 

462 """ 

463 

464 def __init__(self) -> None: 

465 # The stack is in the dict insertion order. 

466 self.stack: Dict[ 

467 Node, 

468 Tuple[ 

469 # Node's finalizers. 

470 List[Callable[[], object]], 

471 # Node's exception, if its setup raised. 

472 Optional[Union[OutcomeException, Exception]], 

473 ], 

474 ] = {} 

475 

476 def setup(self, item: Item) -> None: 

477 """Setup objects along the collector chain to the item.""" 

478 needed_collectors = item.listchain() 

479 

480 # If a collector fails its setup, fail its entire subtree of items. 

481 # The setup is not retried for each item - the same exception is used. 

482 for col, (finalizers, exc) in self.stack.items(): 

483 assert col in needed_collectors, "previous item was not torn down properly" 

484 if exc: 

485 raise exc 

486 

487 for col in needed_collectors[len(self.stack) :]: 

488 assert col not in self.stack 

489 # Push onto the stack. 

490 self.stack[col] = ([col.teardown], None) 

491 try: 

492 col.setup() 

493 except TEST_OUTCOME as exc: 

494 self.stack[col] = (self.stack[col][0], exc) 

495 raise exc 

496 

497 def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: 

498 """Attach a finalizer to the given node. 

499 

500 The node must be currently active in the stack. 

501 """ 

502 assert node and not isinstance(node, tuple) 

503 assert callable(finalizer) 

504 assert node in self.stack, (node, self.stack) 

505 self.stack[node][0].append(finalizer) 

506 

507 def teardown_exact(self, nextitem: Optional[Item]) -> None: 

508 """Teardown the current stack up until reaching nodes that nextitem 

509 also descends from. 

510 

511 When nextitem is None (meaning we're at the last item), the entire 

512 stack is torn down. 

513 """ 

514 needed_collectors = nextitem and nextitem.listchain() or [] 

515 exc = None 

516 while self.stack: 

517 if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: 

518 break 

519 node, (finalizers, _) = self.stack.popitem() 

520 while finalizers: 

521 fin = finalizers.pop() 

522 try: 

523 fin() 

524 except TEST_OUTCOME as e: 

525 # XXX Only first exception will be seen by user, 

526 # ideally all should be reported. 

527 if exc is None: 

528 exc = e 

529 if exc: 

530 raise exc 

531 if nextitem is None: 

532 assert not self.stack 

533 

534 

535def collect_one_node(collector: Collector) -> CollectReport: 

536 ihook = collector.ihook 

537 ihook.pytest_collectstart(collector=collector) 

538 rep: CollectReport = ihook.pytest_make_collect_report(collector=collector) 

539 call = rep.__dict__.pop("call", None) 

540 if call and check_interactive_exception(call, rep): 

541 ihook.pytest_exception_interact(node=collector, call=call, report=rep) 

542 return rep