|
33 | 33 | from _pytest.nodes import Item
|
34 | 34 | from _pytest.nodes import Node
|
35 | 35 | from _pytest.outcomes import Exit
|
| 36 | +from _pytest.outcomes import OutcomeException |
36 | 37 | from _pytest.outcomes import Skipped
|
37 | 38 | from _pytest.outcomes import TEST_OUTCOME
|
| 39 | +from _pytest.store import StoreKey |
38 | 40 |
|
39 | 41 | if TYPE_CHECKING:
|
40 | 42 | from typing_extensions import Literal
|
@@ -103,7 +105,7 @@ def pytest_sessionstart(session: "Session") -> None:
|
103 | 105 |
|
104 | 106 |
|
105 | 107 | def pytest_sessionfinish(session: "Session") -> None:
|
106 |
| - session._setupstate.teardown_all() |
| 108 | + session._setupstate.teardown_exact(None) |
107 | 109 |
|
108 | 110 |
|
109 | 111 | def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
|
@@ -175,7 +177,7 @@ def pytest_runtest_call(item: Item) -> None:
|
175 | 177 |
|
176 | 178 | def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
|
177 | 179 | _update_current_test_var(item, "teardown")
|
178 |
| - item.session._setupstate.teardown_exact(item, nextitem) |
| 180 | + item.session._setupstate.teardown_exact(nextitem) |
179 | 181 | _update_current_test_var(item, None)
|
180 | 182 |
|
181 | 183 |
|
@@ -401,88 +403,132 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
401 | 403 |
|
402 | 404 |
|
403 | 405 | class SetupState:
|
404 |
| - """Shared state for setting up/tearing down test items or collectors.""" |
| 406 | + """Shared state for setting up/tearing down test items or collectors |
| 407 | + in a session. |
405 | 408 |
|
406 |
| - def __init__(self): |
407 |
| - self.stack: List[Node] = [] |
408 |
| - self._finalizers: Dict[Node, List[Callable[[], object]]] = {} |
| 409 | + Suppose we have a collection tree as follows: |
409 | 410 |
|
410 |
| - def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: |
411 |
| - """Attach a finalizer to the given colitem.""" |
412 |
| - assert colitem and not isinstance(colitem, tuple) |
413 |
| - assert callable(finalizer) |
414 |
| - # assert colitem in self.stack # some unit tests don't setup stack :/ |
415 |
| - self._finalizers.setdefault(colitem, []).append(finalizer) |
| 411 | + <Session session> |
| 412 | + <Module mod1> |
| 413 | + <Function item1> |
| 414 | + <Module mod2> |
| 415 | + <Function item2> |
416 | 416 |
|
417 |
| - def _pop_and_teardown(self): |
418 |
| - colitem = self.stack.pop() |
419 |
| - self._teardown_with_finalization(colitem) |
| 417 | + The SetupState maintains a stack. The stack starts out empty: |
420 | 418 |
|
421 |
| - def _callfinalizers(self, colitem) -> None: |
422 |
| - finalizers = self._finalizers.pop(colitem, None) |
423 |
| - exc = None |
424 |
| - while finalizers: |
425 |
| - fin = finalizers.pop() |
426 |
| - try: |
427 |
| - fin() |
428 |
| - except TEST_OUTCOME as e: |
429 |
| - # XXX Only first exception will be seen by user, |
430 |
| - # ideally all should be reported. |
431 |
| - if exc is None: |
432 |
| - exc = e |
433 |
| - if exc: |
434 |
| - raise exc |
| 419 | + [] |
435 | 420 |
|
436 |
| - def _teardown_with_finalization(self, colitem) -> None: |
437 |
| - self._callfinalizers(colitem) |
438 |
| - colitem.teardown() |
439 |
| - for colitem in self._finalizers: |
440 |
| - assert colitem in self.stack |
| 421 | + During the setup phase of item1, prepare(item1) is called. What it does |
| 422 | + is: |
441 | 423 |
|
442 |
| - def teardown_all(self) -> None: |
443 |
| - while self.stack: |
444 |
| - self._pop_and_teardown() |
445 |
| - for key in list(self._finalizers): |
446 |
| - self._teardown_with_finalization(key) |
447 |
| - assert not self._finalizers |
| 424 | + push session to stack, run session.setup() |
| 425 | + push mod1 to stack, run mod1.setup() |
| 426 | + push item1 to stack, run item1.setup() |
448 | 427 |
|
449 |
| - def teardown_exact(self, item, nextitem) -> None: |
450 |
| - needed_collectors = nextitem and nextitem.listchain() or [] |
451 |
| - self._teardown_towards(needed_collectors) |
| 428 | + The stack is: |
452 | 429 |
|
453 |
| - def _teardown_towards(self, needed_collectors) -> None: |
454 |
| - exc = None |
455 |
| - while self.stack: |
456 |
| - if self.stack == needed_collectors[: len(self.stack)]: |
457 |
| - break |
458 |
| - try: |
459 |
| - self._pop_and_teardown() |
460 |
| - except TEST_OUTCOME as e: |
461 |
| - # XXX Only first exception will be seen by user, |
462 |
| - # ideally all should be reported. |
463 |
| - if exc is None: |
464 |
| - exc = e |
465 |
| - if exc: |
466 |
| - raise exc |
| 430 | + [session, mod1, item1] |
| 431 | +
|
| 432 | + While the stack is in this shape, it is allowed to add finalizers to |
| 433 | + each of session, mod1, item1 using addfinalizer(). |
| 434 | +
|
| 435 | + During the teardown phase of item1, teardown_exact(item2) is called, |
| 436 | + where item2 is the next item to item1. What it does is: |
| 437 | +
|
| 438 | + pop item1 from stack, run its teardowns |
| 439 | + pop mod1 from stack, run its teardowns |
| 440 | +
|
| 441 | + mod1 was popped because it ended its purpose with item1. The stack is: |
| 442 | +
|
| 443 | + [session] |
| 444 | +
|
| 445 | + During the setup phase of item2, prepare(item2) is called. What it does |
| 446 | + is: |
| 447 | +
|
| 448 | + push mod2 to stack, run mod2.setup() |
| 449 | + push item2 to stack, run item2.setup() |
467 | 450 |
|
468 |
| - def prepare(self, colitem) -> None: |
469 |
| - """Setup objects along the collector chain to the test-method.""" |
| 451 | + Stack: |
470 | 452 |
|
471 |
| - # Check if the last collection node has raised an error. |
| 453 | + [session, mod2, item2] |
| 454 | +
|
| 455 | + During the teardown phase of item2, teardown_exact(None) is called, |
| 456 | + because item2 is the last item. What it does is: |
| 457 | +
|
| 458 | + pop item2 from stack, run its teardowns |
| 459 | + pop mod2 from stack, run its teardowns |
| 460 | + pop session from stack, run its teardowns |
| 461 | +
|
| 462 | + Stack: |
| 463 | +
|
| 464 | + [] |
| 465 | +
|
| 466 | + The end! |
| 467 | + """ |
| 468 | + |
| 469 | + def __init__(self) -> None: |
| 470 | + # Maps node -> the node's finalizers. |
| 471 | + # The stack is in the dict insertion order. |
| 472 | + self.stack: Dict[Node, List[Callable[[], object]]] = {} |
| 473 | + |
| 474 | + _prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]() |
| 475 | + |
| 476 | + def prepare(self, item: Item) -> None: |
| 477 | + """Setup objects along the collector chain to the item.""" |
| 478 | + # If a collector fails its setup, fail its entire subtree of items. |
| 479 | + # The setup is not retried for each item - the same exception is used. |
472 | 480 | for col in self.stack:
|
473 |
| - if hasattr(col, "_prepare_exc"): |
474 |
| - exc = col._prepare_exc # type: ignore[attr-defined] |
475 |
| - raise exc |
| 481 | + prepare_exc = col._store.get(self._prepare_exc_key, None) |
| 482 | + if prepare_exc: |
| 483 | + raise prepare_exc |
476 | 484 |
|
477 |
| - needed_collectors = colitem.listchain() |
| 485 | + needed_collectors = item.listchain() |
478 | 486 | for col in needed_collectors[len(self.stack) :]:
|
479 |
| - self.stack.append(col) |
| 487 | + assert col not in self.stack |
| 488 | + self.stack[col] = [col.teardown] |
480 | 489 | try:
|
481 | 490 | col.setup()
|
482 | 491 | except TEST_OUTCOME as e:
|
483 |
| - col._prepare_exc = e # type: ignore[attr-defined] |
| 492 | + col._store[self._prepare_exc_key] = e |
484 | 493 | raise e
|
485 | 494 |
|
| 495 | + def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: |
| 496 | + """Attach a finalizer to the given node. |
| 497 | +
|
| 498 | + The node must be currently active in the stack. |
| 499 | + """ |
| 500 | + assert node and not isinstance(node, tuple) |
| 501 | + assert callable(finalizer) |
| 502 | + assert node in self.stack, (node, self.stack) |
| 503 | + self.stack[node].append(finalizer) |
| 504 | + |
| 505 | + def teardown_exact(self, nextitem: Optional[Item]) -> None: |
| 506 | + """Teardown the current stack up until reaching nodes that nextitem |
| 507 | + also descends from. |
| 508 | +
|
| 509 | + When nextitem is None (meaning we're at the last item), the entire |
| 510 | + stack is torn down. |
| 511 | + """ |
| 512 | + needed_collectors = nextitem and nextitem.listchain() or [] |
| 513 | + exc = None |
| 514 | + while self.stack: |
| 515 | + if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: |
| 516 | + break |
| 517 | + node, finalizers = self.stack.popitem() |
| 518 | + while finalizers: |
| 519 | + fin = finalizers.pop() |
| 520 | + try: |
| 521 | + fin() |
| 522 | + except TEST_OUTCOME as e: |
| 523 | + # XXX Only first exception will be seen by user, |
| 524 | + # ideally all should be reported. |
| 525 | + if exc is None: |
| 526 | + exc = e |
| 527 | + if exc: |
| 528 | + raise exc |
| 529 | + if nextitem is None: |
| 530 | + assert not self.stack |
| 531 | + |
486 | 532 |
|
487 | 533 | def collect_one_node(collector: Collector) -> CollectReport:
|
488 | 534 | ihook = collector.ihook
|
|
0 commit comments