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

2 import bdb

3 import os

4 import sys

5 from typing import Callable

6 from typing import cast

7 from typing import Dict

8 from typing import Generic

9 from typing import List

10 from typing import Optional

11 from typing import Tuple

12 from typing import Type

13 from typing import TYPE_CHECKING

14 from typing import TypeVar

15 from typing import Union

16

17 import attr

18

19 from .reports import BaseReport

20 from .reports import CollectErrorRepr

21 from .reports import CollectReport

22 from .reports import TestReport

23 from _pytest import timing

24 from _pytest._code.code import ExceptionChainRepr

25 from _pytest._code.code import ExceptionInfo

26 from _pytest._code.code import TerminalRepr

27 from _pytest.compat import final

28 from _pytest.config.argparsing import Parser

29 from _pytest.deprecated import check_ispytest

30 from _pytest.nodes import Collector

31 from _pytest.nodes import Item

32 from _pytest.nodes import Node

33 from _pytest.outcomes import Exit

34 from _pytest.outcomes import OutcomeException

35 from _pytest.outcomes import Skipped

36 from _pytest.outcomes import TEST_OUTCOME

37

38 if 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

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

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

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

102 session._setupstate = SetupState()

103

104

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

106 session._setupstate.teardown_exact(None)

107

108

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

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

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

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

154 _update_current_test_var(item, "setup")

155 item.session._setupstate.setup(item)

156

157

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

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

184 def _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

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

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

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

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

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

265

266

267@final

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

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

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

366 return TestReport.from_item_and_call(item, call)

367

368

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

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

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