Account for changes in fixture dependencies properly#14104
Account for changes in fixture dependencies properly#14104Anton3 wants to merge 27 commits intopytest-dev:mainfrom
Conversation
|
@RonnyPfannschmidt please review 😃 |
RonnyPfannschmidt
left a comment
There was a problem hiding this comment.
I'm currently not able to do a deep review
This has a few details that give me headaches
- it sidesteps setup/teardown minimizing and im not sure if its feasible to integrate in a comprehensible manner
- its unclear to me what happens if the involved fixtures are indirect to a test
- Its unclear to me what happens if someone creates a cycle via the new dependency tracking
Are you wondering if fixing #14095 is an overall positive change? If so, I can remove that part from the PR for now, and it can be investigated later separately. There is also a minor issue with my implementation. When I do request._execute_fixturedef(fixturedef, only_maintain_cache=True)it may I can change it so that the other fixtures are not
What do you mean? Parametrized / non-parametrized case? In the parametrized case, we cannot get the parametrized fixture using
The initial cache handling step uses There is |
|
trigger a rebuild is more correct than not with indirect i mean what happens if a replaced fixture is not a dependency of the directly used fixtures, but rather down in the dependency tree with loops i mean what actually happens when one fixture goads another ones closure into calling getfixturevalue for itself |
|
I've found another issue in my implementation. Suppose we depend on some fixture dynamically. After a recomputation we stop depending on it. Everything is fine, but at some point that fixture gets finalized, and because we did One solution is to add "evaluation epoch" to the finalizers to avoid finalizing a more recent evaluation of the fixture than expected. |
|
teardown should idieally only happen in teardown phases - thats why the teardown_towards heleprs got added we should fail rather than teardown in setup/call phases |
|
I've moved parametrized fixture finalization to teardown phase. In We need to be cautious with what to teardown when. Consider these tests: @pytest.fixture(scope="session")
def foo(request):
return request.param
@pytest.mark.parametrize("foo", [1], indirect=True)
def test_a(foo):
pass
@pytest.mark.parametrize("foo", [1], indirect=True)
def test_b(foo):
pass
def test_c():
pass
@pytest.mark.parametrize("foo", [2], indirect=True)
def test_d(foo):
passFinalizing For each parametrized fixture in the current item we should find the next item that uses the fixture with that parameter and if parameters differ then finalize the fixture in the current item's teardown. This requires some code restructuring, because we now require arbitrary lookahead instead of just Overall, setup-plan did get prettier. A hefty chunk of I need to write more tests, for issues you pointed out and some more:
|
25fab5f to
643afdf
Compare
|
I've reworked More importantly, this disallows
The implementations of Alternatively, we could avoid all of those troubles by dropping support for carrying fixtures along tests that do not depend on them: @pytest.fixture(scope="session")
def foo(request):
return request.param
@pytest.parametrize("foo", [1], indirect=True)
def test_a(foo):
pass
def test_b():
assert 2 + 2 == 4
@pytest.parametrize("foo", [1], indirect=True)
def test_c(foo):
passIn this case, pytest auto-reorders fixtures based on param values (if reordering is not turned off by the user), but in a more complex scenario additional teardown-setup work will surface: @pytest.fixture(scope="session")
def foo():
pass
@pytest.fixture(scope="session")
def bar():
pass
@pytest.fixture(scope="session")
def baz():
pass
@pytest.parametrize(("foo", "bar"), [1, 1], indirect=True)
def test_a(foo, bar):
pass
@pytest.parametrize(("bar, "baz"), [1, 1], indirect=True)
def test_b(bar, baz):
pass
@pytest.parametrize(("baz, "foo"), [1, 1], indirect=True)
def test_c(bar, baz):
passCurrently all those fixtures are carried over. They will not be carried over. And so I come back to the "disgusting" idea of tearing down parametrized fixtures in the test before the parameter changes. So in this test setup, |
|
Done! I should add the following tests (besides the ones mentioned earlier in one, two):
|
|
By the way, this PR also relates to:
|
|
@RonnyPfannschmidt what do you think is the lesser evil in this case? import pytest
@pytest.fixture(scope="session")
def foo(request):
return getattr(request, "param", None)
@pytest.mark.parametrize("foo", [1], indirect=True)
def test_a(foo):
assert foo == 1
def test_b(request):
hmmm(request)
@pytest.mark.parametrize("foo", [1], indirect=True)
def test_c(foo):
assert foo == 1
* Assume that parametrized tests autoreordering is turned off, or that the user forced a custom order, or that there are multiple parameters, and autoreordering had to pick an argname to not be consecutive, and our fixture was picked. UPD: I will go with option 1 for now, because in most cases extra setup-teardown won't happen due to autoreordering, and worst-case scenario is tests slowdown, while option 2 risks breaking some tests in-the-wild (I'm almost certain that I'll find out at my worksite that the new version of pytest broke some of the millions of pytest tests.) |
|
teardown when the next item doesnt use is worse than failure at first glance the failure is a acceptable first step as it turns a silent error into a visible one, but the real issue is that teardown_towards should tear it down in the teardown of test_b as test_c has indeed different parameters |
There is no issue there:
The issue is only in |
|
But I got the idea, you think failing explicitly is better than the current state of teardown in setup phase. Will go on with this approach. |
|
the goal should be to eventually e able to do it in teardown_towards but bascially if a fixture has to be removed in the setup phase something went wrong i do fear that i am missing some edge cases here where the teardown is correct but my current understanding is that the teardown ought to happen in the teardown phase of the prior test item |
|
@RonnyPfannschmidt ready for review |
|
@RonnyPfannschmidt fixed + replied Why removal handles are needed:
In #4871 they also suggest that ideally we should avoid fixture finalizers and instead plan fixture setup-teardown at collection stage. This is impossible, because, as explained here, custom implementations of An alternative implementation without finalizers would be the following:
Still, I can't see a non-ad-hoc way of avoiding the pile-up of finalizers of parametrized fixtures in nodes. Removal handles as currently in the PR are the most straightforward solution that I see. A solution for nodes without handles would be to remember the node for which we have a finalizer currently registered and not readd it on fixture recomputation. The drawback would be incorrect finalizer ordering: @pytest.fixture(scope="module")
def foo():
...
@pytest.fixture(scope="module")
def bar():
...
@pytest.mark.parametrize("foo", [1], indirect=True)
def test1(foo, bar):
...
@pytest.mark.parametrize("foo", [2], indirect=True)
def test2(bar, foo):
...The order of fixture teardown on But the instance of AFAICS the only way the correct order can be achieved is by removing the finalizers of |
|
ok, i need to reiterate the handles idea a bit more fir my own understaniding i think the fact that its currently a global variable is part of my confusion ideally the complete details on caching/recreation and current fixture closure turn into the responsibility of the setup state but that is sooo out of scope for this change i do believe having the handles as one variable may create possibly neglectable issues wit in process pytest runs for testing pytest/pytest plugins - off hand it looks to me as if we could leave handles in place by accident if the pytest under test gets really broken - i suspect that this could be resolved by having a weakref.WeakKeyDictionary which would automatically clean up |
|
Alternatively, there is the following compromise:
What do you think? Edit: After thinking for a while, I still believe handles are a cleaner solution, because this approach
|
|
I don't think weakref can be used with handles. If we drop a handle, that does not mean that we no longer want the finalizer to run, unless we want to break the current finalizers API for users. What dropping a handle currently means in the PR is that we don't want the ability to remove the finalizer. Edit: actually, a handle could keep a weakref to If handles are kept past their intended scope by mistake, then finalizers are eventually invoked and cleared, and handles are left owning a bunch of |
|
@RonnyPfannschmidt I've moved fixture finalizers management to |
|
Thanks for moving it into setupstate That removes my biggest concern I did another skim and now it looks solid at first glance to me As this necessary is something big ill have to allocate some time for a deeper read Thank you |
RonnyPfannschmidt
left a comment
There was a problem hiding this comment.
this looks correct to me now
I’m tempted to suggest figuring how to make the finalizer handles a bit less opaque - to ease debugging the data (should be a followup)
i'd like a second pair of eyes on it and i think i have to run a local test
based on the current code and integration in setupstate i'm confident this is good to go
my due diligence needs some more work
thanks for getting this started and powering through the follow-ups
this was hard and important work and I’m grateful someone actually went down that rabbit hole -
|
@RonnyPfannschmidt I've reworked the |
|
Please someone else review this PR! |
|
@nicoddemus @bluetech ping on this one - i'll also try to take another looks tis week but im stretched very thin amt |
|
I've skimmed through fixture-related issues updated in the last 2 years:
|
|
@nicoddemus @bluetech Could you please take a look at the PR? |
RonnyPfannschmidt
left a comment
There was a problem hiding this comment.
i did another round of walk-trough and approve this - i'd still like another pair of critical eyes as this one is a important enhancement that introduces a new mechanism
|
@The-Compiler Could you perhaps take a look at the PR? |
|
@nicoddemus @bluetech gentle ping? |
|
Sorry I won't be able to review this properly. @RonnyPfannschmidt if you have looked at it thoroughly and are confident with the result, feel free to go ahead and merge it. |
|
I will try to review this. If you can split the changes to smaller atomic commits (if possible) that would help a lot. Also, if you can update the pull request summary to explain what is the problem being fixed here that would also help. Currently it lists the code changes, which don't explain the rationale, and links to issues, which I'm sure explain the problems but are a bit hard to sift through. So a clear description would help a lot. |
|
@bluetech I'm afraid the main change in I've written a detailed description. Walking through the tests and linked issues again was actually useful, I've found that #2043 is actually irrelevant to this PR. For now, please try reading through the PR as-is and let me know if anything is unclear. One more thing I could try is adding comments throughout the PR, describing what problems are being solved. This may or may not prove to be necessary. |
|
@Anton3 I understand if it can't be broken down. Thanks for adding the description. I will try to take a look this weekend. |
Move fixture teardown to
SetupStateA fixture's teardown should first perform teardown of the dependent fixtures - this is the only known way that maintains the correct teardown order in all cases. Before this PR, this code resided in
FixtureDef. The disadvantage is that the responsibilities and implementation ofSetupState(including exception handling) was spread across the codebase. This PR moves the execution of fixture teardown intoSetupState, which facilitates code reuse and enforces Single-Responsibility Principle.A more important issue was that fixture teardown for parametrized fixtures was performed during the setup of the next test. This lead to incorrect attribution of fixture teardown errors, time spent and logs to the next test. This PR makes sure to precompute
paramchanges for parametrized fixtures in advance usingnextitemand always runs fixture teardown at test teardown stage.Relevant tests:
test_teardown_stale_fixtures_single_exception- shows correct attribution of teardown errorstest_teardown_stale_fixtures_multiple_exceptionstest_parametrized_fixture_carries_over_unaware_itemtest_fixture_rebuilt_when_param_appearstest_fixture_not_rebuilt_when_not_requestedtest_cache_mismatch_error_on_sudden_getfixturevalue- as was insisted in the review comments, if we discover at setup or call stage that a fixture needs to be recomputed, we prefer to error out insteadtest_cache_key_mismatch_error_on_unexpected_param_changetest_fixture_teardown_when_not_requested_but_param_changestest_fixture_teardown_with_reordered_teststest_parametrized_fixtures_teardown_in_reverse_setup_ordertest_parametrized_fixture_teardown_order_with_mixed_scopestest_override_fixture_with_new_parametrized_fixture- this is a peculiar test that works by sheer luck; we should track fixture overrides more precisely in the futureCloses: #9287
Add
FinalizerHandleto avoid accumulating stale finalizersTo reiterate, adding teardown of the dependent fixture as a "finalizer" to the dependency-fixture is the only known way of achieving the fully correct teardown order. But this meant that stale finalizers from
function-scoped fixtures accumulated at higher-scoped fixtures andNodes. This manifested as a "memory leak" in very large test suites.Besides performance concerns, with
getfixturevaluedependency tracking added, a stale finalizer could result in tearing down a fixture randomly.To solve those issues, this PR introduces
FinalizerHandlethat allows to remove stale finalizers, and plucks all remaining self-finalizers at fixture's teardown.Relevant tests:
test_stale_finalizer_not_invokedCloses: #4871
Execute
pytest_fixture_post_finalizeronce per fixture teardownThis is implemented by turning
pytest_fixture_post_finalizercall into one of fixture finalizers inSetupState.Also, exception handling is improved: if
pytest_fixture_post_finalizerraises an exception, the fixture is still reset correctly.Relevant tests:
test_fixture_post_finalizer_called_oncetest_fixture_post_finalizer_hook_exceptionCloses: #5848
Closes: #14114
Account for changes in
getfixturevaluedependenciesPreviously, for a given fixture, only dependencies from
argnameswere checked to see if they need to be recomputed, recomputing the current fixture. Meanwhile, if a fixture depends on a parametrized fixture viagetfixturevalue, then the current fixture's value could erroneously be carried over, even though the dependency was recomputed due to its parameter change.The fix involves tracking fixture dependencies (
_own_fixture_defs) inFixtureRequest, then adding finalizers to all those fixtures inFixtureDef.execute.This precise dependency information was also used in
setuponly.py.Relevant tests:
test_getfixturevalue_parametrized_dependency_trackedtest_request_stealing_then_getfixturevalue_on_parametrizedCloses: #14103