Merge pull request #8219 from bluetech/setupstate-refactor
runner: refactor SetupState
This commit is contained in:
commit
d5df8f99ab
|
@ -372,6 +372,7 @@ def _fill_fixtures_impl(function: "Function") -> None:
|
||||||
fi = fm.getfixtureinfo(function.parent, function.obj, None)
|
fi = fm.getfixtureinfo(function.parent, function.obj, None)
|
||||||
function._fixtureinfo = fi
|
function._fixtureinfo = fi
|
||||||
request = function._request = FixtureRequest(function, _ispytest=True)
|
request = function._request = FixtureRequest(function, _ispytest=True)
|
||||||
|
fm.session._setupstate.prepare(function)
|
||||||
request._fillfixtures()
|
request._fillfixtures()
|
||||||
# Prune out funcargs for jstests.
|
# Prune out funcargs for jstests.
|
||||||
newfuncargs = {}
|
newfuncargs = {}
|
||||||
|
@ -543,8 +544,8 @@ class FixtureRequest:
|
||||||
self._addfinalizer(finalizer, scope=self.scope)
|
self._addfinalizer(finalizer, scope=self.scope)
|
||||||
|
|
||||||
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
||||||
item = self._getscopeitem(scope)
|
node = self._getscopeitem(scope)
|
||||||
item.addfinalizer(finalizer)
|
node.addfinalizer(finalizer)
|
||||||
|
|
||||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||||
"""Apply a marker to a single test function invocation.
|
"""Apply a marker to a single test function invocation.
|
||||||
|
|
|
@ -33,8 +33,10 @@ from _pytest.nodes import Collector
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
from _pytest.nodes import Node
|
from _pytest.nodes import Node
|
||||||
from _pytest.outcomes import Exit
|
from _pytest.outcomes import Exit
|
||||||
|
from _pytest.outcomes import OutcomeException
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
from _pytest.store import StoreKey
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
@ -103,7 +105,7 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||||
|
|
||||||
|
|
||||||
def pytest_sessionfinish(session: "Session") -> None:
|
def pytest_sessionfinish(session: "Session") -> None:
|
||||||
session._setupstate.teardown_all()
|
session._setupstate.teardown_exact(None)
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
|
def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
|
||||||
|
@ -175,7 +177,7 @@ def pytest_runtest_call(item: Item) -> None:
|
||||||
|
|
||||||
def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
|
def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
|
||||||
_update_current_test_var(item, "teardown")
|
_update_current_test_var(item, "teardown")
|
||||||
item.session._setupstate.teardown_exact(item, nextitem)
|
item.session._setupstate.teardown_exact(nextitem)
|
||||||
_update_current_test_var(item, None)
|
_update_current_test_var(item, None)
|
||||||
|
|
||||||
|
|
||||||
|
@ -401,88 +403,132 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||||
|
|
||||||
|
|
||||||
class SetupState:
|
class SetupState:
|
||||||
"""Shared state for setting up/tearing down test items or collectors."""
|
"""Shared state for setting up/tearing down test items or collectors
|
||||||
|
in a session.
|
||||||
|
|
||||||
def __init__(self):
|
Suppose we have a collection tree as follows:
|
||||||
self.stack: List[Node] = []
|
|
||||||
self._finalizers: Dict[Node, List[Callable[[], object]]] = {}
|
|
||||||
|
|
||||||
def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None:
|
<Session session>
|
||||||
"""Attach a finalizer to the given colitem."""
|
<Module mod1>
|
||||||
assert colitem and not isinstance(colitem, tuple)
|
<Function item1>
|
||||||
assert callable(finalizer)
|
<Module mod2>
|
||||||
# assert colitem in self.stack # some unit tests don't setup stack :/
|
<Function item2>
|
||||||
self._finalizers.setdefault(colitem, []).append(finalizer)
|
|
||||||
|
|
||||||
def _pop_and_teardown(self):
|
The SetupState maintains a stack. The stack starts out empty:
|
||||||
colitem = self.stack.pop()
|
|
||||||
self._teardown_with_finalization(colitem)
|
|
||||||
|
|
||||||
def _callfinalizers(self, colitem) -> None:
|
[]
|
||||||
finalizers = self._finalizers.pop(colitem, None)
|
|
||||||
exc = None
|
|
||||||
while finalizers:
|
|
||||||
fin = finalizers.pop()
|
|
||||||
try:
|
|
||||||
fin()
|
|
||||||
except TEST_OUTCOME as e:
|
|
||||||
# XXX Only first exception will be seen by user,
|
|
||||||
# ideally all should be reported.
|
|
||||||
if exc is None:
|
|
||||||
exc = e
|
|
||||||
if exc:
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
def _teardown_with_finalization(self, colitem) -> None:
|
During the setup phase of item1, prepare(item1) is called. What it does
|
||||||
self._callfinalizers(colitem)
|
is:
|
||||||
colitem.teardown()
|
|
||||||
for colitem in self._finalizers:
|
|
||||||
assert colitem in self.stack
|
|
||||||
|
|
||||||
def teardown_all(self) -> None:
|
push session to stack, run session.setup()
|
||||||
while self.stack:
|
push mod1 to stack, run mod1.setup()
|
||||||
self._pop_and_teardown()
|
push item1 to stack, run item1.setup()
|
||||||
for key in list(self._finalizers):
|
|
||||||
self._teardown_with_finalization(key)
|
|
||||||
assert not self._finalizers
|
|
||||||
|
|
||||||
def teardown_exact(self, item, nextitem) -> None:
|
The stack is:
|
||||||
needed_collectors = nextitem and nextitem.listchain() or []
|
|
||||||
self._teardown_towards(needed_collectors)
|
|
||||||
|
|
||||||
def _teardown_towards(self, needed_collectors) -> None:
|
[session, mod1, item1]
|
||||||
exc = None
|
|
||||||
while self.stack:
|
|
||||||
if self.stack == needed_collectors[: len(self.stack)]:
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
self._pop_and_teardown()
|
|
||||||
except TEST_OUTCOME as e:
|
|
||||||
# XXX Only first exception will be seen by user,
|
|
||||||
# ideally all should be reported.
|
|
||||||
if exc is None:
|
|
||||||
exc = e
|
|
||||||
if exc:
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
def prepare(self, colitem) -> None:
|
While the stack is in this shape, it is allowed to add finalizers to
|
||||||
"""Setup objects along the collector chain to the test-method."""
|
each of session, mod1, item1 using addfinalizer().
|
||||||
|
|
||||||
# Check if the last collection node has raised an error.
|
During the teardown phase of item1, teardown_exact(item2) is called,
|
||||||
|
where item2 is the next item to item1. What it does is:
|
||||||
|
|
||||||
|
pop item1 from stack, run its teardowns
|
||||||
|
pop mod1 from stack, run its teardowns
|
||||||
|
|
||||||
|
mod1 was popped because it ended its purpose with item1. The stack is:
|
||||||
|
|
||||||
|
[session]
|
||||||
|
|
||||||
|
During the setup phase of item2, prepare(item2) is called. What it does
|
||||||
|
is:
|
||||||
|
|
||||||
|
push mod2 to stack, run mod2.setup()
|
||||||
|
push item2 to stack, run item2.setup()
|
||||||
|
|
||||||
|
Stack:
|
||||||
|
|
||||||
|
[session, mod2, item2]
|
||||||
|
|
||||||
|
During the teardown phase of item2, teardown_exact(None) is called,
|
||||||
|
because item2 is the last item. What it does is:
|
||||||
|
|
||||||
|
pop item2 from stack, run its teardowns
|
||||||
|
pop mod2 from stack, run its teardowns
|
||||||
|
pop session from stack, run its teardowns
|
||||||
|
|
||||||
|
Stack:
|
||||||
|
|
||||||
|
[]
|
||||||
|
|
||||||
|
The end!
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# Maps node -> the node's finalizers.
|
||||||
|
# The stack is in the dict insertion order.
|
||||||
|
self.stack: Dict[Node, List[Callable[[], object]]] = {}
|
||||||
|
|
||||||
|
_prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]()
|
||||||
|
|
||||||
|
def prepare(self, item: Item) -> None:
|
||||||
|
"""Setup objects along the collector chain to the item."""
|
||||||
|
# If a collector fails its setup, fail its entire subtree of items.
|
||||||
|
# The setup is not retried for each item - the same exception is used.
|
||||||
for col in self.stack:
|
for col in self.stack:
|
||||||
if hasattr(col, "_prepare_exc"):
|
prepare_exc = col._store.get(self._prepare_exc_key, None)
|
||||||
exc = col._prepare_exc # type: ignore[attr-defined]
|
if prepare_exc:
|
||||||
raise exc
|
raise prepare_exc
|
||||||
|
|
||||||
needed_collectors = colitem.listchain()
|
needed_collectors = item.listchain()
|
||||||
for col in needed_collectors[len(self.stack) :]:
|
for col in needed_collectors[len(self.stack) :]:
|
||||||
self.stack.append(col)
|
assert col not in self.stack
|
||||||
|
self.stack[col] = [col.teardown]
|
||||||
try:
|
try:
|
||||||
col.setup()
|
col.setup()
|
||||||
except TEST_OUTCOME as e:
|
except TEST_OUTCOME as e:
|
||||||
col._prepare_exc = e # type: ignore[attr-defined]
|
col._store[self._prepare_exc_key] = e
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
|
||||||
|
"""Attach a finalizer to the given node.
|
||||||
|
|
||||||
|
The node must be currently active in the stack.
|
||||||
|
"""
|
||||||
|
assert node and not isinstance(node, tuple)
|
||||||
|
assert callable(finalizer)
|
||||||
|
assert node in self.stack, (node, self.stack)
|
||||||
|
self.stack[node].append(finalizer)
|
||||||
|
|
||||||
|
def teardown_exact(self, nextitem: Optional[Item]) -> None:
|
||||||
|
"""Teardown the current stack up until reaching nodes that nextitem
|
||||||
|
also descends from.
|
||||||
|
|
||||||
|
When nextitem is None (meaning we're at the last item), the entire
|
||||||
|
stack is torn down.
|
||||||
|
"""
|
||||||
|
needed_collectors = nextitem and nextitem.listchain() or []
|
||||||
|
exc = None
|
||||||
|
while self.stack:
|
||||||
|
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
||||||
|
break
|
||||||
|
node, finalizers = self.stack.popitem()
|
||||||
|
while finalizers:
|
||||||
|
fin = finalizers.pop()
|
||||||
|
try:
|
||||||
|
fin()
|
||||||
|
except TEST_OUTCOME as e:
|
||||||
|
# XXX Only first exception will be seen by user,
|
||||||
|
# ideally all should be reported.
|
||||||
|
if exc is None:
|
||||||
|
exc = e
|
||||||
|
if exc:
|
||||||
|
raise exc
|
||||||
|
if nextitem is None:
|
||||||
|
assert not self.stack
|
||||||
|
|
||||||
|
|
||||||
def collect_one_node(collector: Collector) -> CollectReport:
|
def collect_one_node(collector: Collector) -> CollectReport:
|
||||||
ihook = collector.ihook
|
ihook = collector.ihook
|
||||||
|
|
|
@ -130,7 +130,8 @@ class TestFillFixtures:
|
||||||
pytester.copy_example()
|
pytester.copy_example()
|
||||||
item = pytester.getitem(Path("test_funcarg_basic.py"))
|
item = pytester.getitem(Path("test_funcarg_basic.py"))
|
||||||
assert isinstance(item, Function)
|
assert isinstance(item, Function)
|
||||||
item._request._fillfixtures()
|
# Execute's item's setup, which fills fixtures.
|
||||||
|
item.session._setupstate.prepare(item)
|
||||||
del item.funcargs["request"]
|
del item.funcargs["request"]
|
||||||
assert len(get_public_names(item.funcargs)) == 2
|
assert len(get_public_names(item.funcargs)) == 2
|
||||||
assert item.funcargs["some"] == "test_func"
|
assert item.funcargs["some"] == "test_func"
|
||||||
|
@ -809,18 +810,25 @@ class TestRequestBasic:
|
||||||
item = pytester.getitem(
|
item = pytester.getitem(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
values = [2]
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def something(request): return 1
|
def something(request):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
values = [2]
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def other(request):
|
def other(request):
|
||||||
return values.pop()
|
return values.pop()
|
||||||
|
|
||||||
def test_func(something): pass
|
def test_func(something): pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
assert isinstance(item, Function)
|
assert isinstance(item, Function)
|
||||||
req = item._request
|
req = item._request
|
||||||
|
|
||||||
|
# Execute item's setup.
|
||||||
|
item.session._setupstate.prepare(item)
|
||||||
|
|
||||||
with pytest.raises(pytest.FixtureLookupError):
|
with pytest.raises(pytest.FixtureLookupError):
|
||||||
req.getfixturevalue("notexists")
|
req.getfixturevalue("notexists")
|
||||||
val = req.getfixturevalue("something")
|
val = req.getfixturevalue("something")
|
||||||
|
@ -831,7 +839,6 @@ class TestRequestBasic:
|
||||||
assert val2 == 2
|
assert val2 == 2
|
||||||
val2 = req.getfixturevalue("other") # see about caching
|
val2 = req.getfixturevalue("other") # see about caching
|
||||||
assert val2 == 2
|
assert val2 == 2
|
||||||
item._request._fillfixtures()
|
|
||||||
assert item.funcargs["something"] == 1
|
assert item.funcargs["something"] == 1
|
||||||
assert len(get_public_names(item.funcargs)) == 2
|
assert len(get_public_names(item.funcargs)) == 2
|
||||||
assert "request" in item.funcargs
|
assert "request" in item.funcargs
|
||||||
|
@ -856,7 +863,7 @@ class TestRequestBasic:
|
||||||
teardownlist = parent.obj.teardownlist
|
teardownlist = parent.obj.teardownlist
|
||||||
ss = item.session._setupstate
|
ss = item.session._setupstate
|
||||||
assert not teardownlist
|
assert not teardownlist
|
||||||
ss.teardown_exact(item, None)
|
ss.teardown_exact(None)
|
||||||
print(ss.stack)
|
print(ss.stack)
|
||||||
assert teardownlist == [1]
|
assert teardownlist == [1]
|
||||||
|
|
||||||
|
|
|
@ -22,21 +22,22 @@ from _pytest.pytester import Pytester
|
||||||
|
|
||||||
class TestSetupState:
|
class TestSetupState:
|
||||||
def test_setup(self, pytester: Pytester) -> None:
|
def test_setup(self, pytester: Pytester) -> None:
|
||||||
ss = runner.SetupState()
|
|
||||||
item = pytester.getitem("def test_func(): pass")
|
item = pytester.getitem("def test_func(): pass")
|
||||||
|
ss = item.session._setupstate
|
||||||
values = [1]
|
values = [1]
|
||||||
ss.prepare(item)
|
ss.prepare(item)
|
||||||
ss.addfinalizer(values.pop, colitem=item)
|
ss.addfinalizer(values.pop, item)
|
||||||
assert values
|
assert values
|
||||||
ss._pop_and_teardown()
|
ss.teardown_exact(None)
|
||||||
assert not values
|
assert not values
|
||||||
|
|
||||||
def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None:
|
def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None:
|
||||||
item = pytester.getitem("def test_func(): pass")
|
item = pytester.getitem("def test_func(): pass")
|
||||||
ss = runner.SetupState()
|
ss = item.session._setupstate
|
||||||
ss.teardown_exact(item, None)
|
ss.prepare(item)
|
||||||
ss.teardown_exact(item, None)
|
ss.teardown_exact(None)
|
||||||
ss.teardown_exact(item, None)
|
ss.teardown_exact(None)
|
||||||
|
ss.teardown_exact(None)
|
||||||
|
|
||||||
def test_setup_fails_and_failure_is_cached(self, pytester: Pytester) -> None:
|
def test_setup_fails_and_failure_is_cached(self, pytester: Pytester) -> None:
|
||||||
item = pytester.getitem(
|
item = pytester.getitem(
|
||||||
|
@ -46,9 +47,11 @@ class TestSetupState:
|
||||||
def test_func(): pass
|
def test_func(): pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
ss = runner.SetupState()
|
ss = item.session._setupstate
|
||||||
pytest.raises(ValueError, lambda: ss.prepare(item))
|
with pytest.raises(ValueError):
|
||||||
pytest.raises(ValueError, lambda: ss.prepare(item))
|
ss.prepare(item)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ss.prepare(item)
|
||||||
|
|
||||||
def test_teardown_multiple_one_fails(self, pytester: Pytester) -> None:
|
def test_teardown_multiple_one_fails(self, pytester: Pytester) -> None:
|
||||||
r = []
|
r = []
|
||||||
|
@ -63,12 +66,13 @@ class TestSetupState:
|
||||||
r.append("fin3")
|
r.append("fin3")
|
||||||
|
|
||||||
item = pytester.getitem("def test_func(): pass")
|
item = pytester.getitem("def test_func(): pass")
|
||||||
ss = runner.SetupState()
|
ss = item.session._setupstate
|
||||||
|
ss.prepare(item)
|
||||||
ss.addfinalizer(fin1, item)
|
ss.addfinalizer(fin1, item)
|
||||||
ss.addfinalizer(fin2, item)
|
ss.addfinalizer(fin2, item)
|
||||||
ss.addfinalizer(fin3, item)
|
ss.addfinalizer(fin3, item)
|
||||||
with pytest.raises(Exception) as err:
|
with pytest.raises(Exception) as err:
|
||||||
ss._callfinalizers(item)
|
ss.teardown_exact(None)
|
||||||
assert err.value.args == ("oops",)
|
assert err.value.args == ("oops",)
|
||||||
assert r == ["fin3", "fin1"]
|
assert r == ["fin3", "fin1"]
|
||||||
|
|
||||||
|
@ -82,11 +86,12 @@ class TestSetupState:
|
||||||
raise Exception("oops2")
|
raise Exception("oops2")
|
||||||
|
|
||||||
item = pytester.getitem("def test_func(): pass")
|
item = pytester.getitem("def test_func(): pass")
|
||||||
ss = runner.SetupState()
|
ss = item.session._setupstate
|
||||||
|
ss.prepare(item)
|
||||||
ss.addfinalizer(fin1, item)
|
ss.addfinalizer(fin1, item)
|
||||||
ss.addfinalizer(fin2, item)
|
ss.addfinalizer(fin2, item)
|
||||||
with pytest.raises(Exception) as err:
|
with pytest.raises(Exception) as err:
|
||||||
ss._callfinalizers(item)
|
ss.teardown_exact(None)
|
||||||
assert err.value.args == ("oops2",)
|
assert err.value.args == ("oops2",)
|
||||||
|
|
||||||
def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
|
def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
|
||||||
|
@ -99,13 +104,14 @@ class TestSetupState:
|
||||||
module_teardown.append("fin_module")
|
module_teardown.append("fin_module")
|
||||||
|
|
||||||
item = pytester.getitem("def test_func(): pass")
|
item = pytester.getitem("def test_func(): pass")
|
||||||
ss = runner.SetupState()
|
mod = item.listchain()[-2]
|
||||||
ss.addfinalizer(fin_module, item.listchain()[-2])
|
ss = item.session._setupstate
|
||||||
ss.addfinalizer(fin_func, item)
|
|
||||||
ss.prepare(item)
|
ss.prepare(item)
|
||||||
|
ss.addfinalizer(fin_module, mod)
|
||||||
|
ss.addfinalizer(fin_func, item)
|
||||||
with pytest.raises(Exception, match="oops1"):
|
with pytest.raises(Exception, match="oops1"):
|
||||||
ss.teardown_exact(item, None)
|
ss.teardown_exact(None)
|
||||||
assert module_teardown
|
assert module_teardown == ["fin_module"]
|
||||||
|
|
||||||
|
|
||||||
class BaseFunctionalTests:
|
class BaseFunctionalTests:
|
||||||
|
|
Loading…
Reference in New Issue