pytest-dev / pluggy

A minimalist production ready plugin system
https://pluggy.readthedocs.io/en/latest/
MIT License
1.24k stars 121 forks source link

pytest won't run root conftest.py's pytest_configure in special circumstances #438

Closed Redoubts closed 7 months ago

Redoubts commented 1 year ago

Wanted to cross post https://github.com/pytest-dev/pytest/issues/11365 here, in case it might be on the pluggy side:

Consider the following project: https://github.com/Redoubts/_pytest_bug_report

There's a root level conftest.py, and 3 plugins (two loaded in the conftest, and a third loaded by plugin 2). All define a pytest_configure in some way.

with platform darwin -- Python 3.11.4, pytest-7.4.0, pluggy-1.0.0, we see

% pytest test1.py
mypackage.plugin2 Configured
mypackage.plugin3 Configured
conftest Configured
mypackage.plugin Configured

Which is somewhat expected, each pytest_configure gets called.

If you update pluggy to 1.2+, you see

% pytest test1.py           
mypackage.plugin2 Configured
mypackage.plugin3 Configured
mypackage.plugin Configured
mypackage.plugin3 Configured
============================================================================================ test session starts =============================================================================================
platform darwin -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0
...

Where the third plugin's pytest_configure is called twice, but the root pytest_configure is never called.

I think this is more specifically due to mypackage/plugin3.py's @pytest.hookimpl wrapper. Maybe it's ill advised to do that, but I found this bizarre scenario when testing package updates in a much larger codebase.

bluetech commented 7 months ago

Thanks for the bug report and reproduction.

This regressed with commit 63b7e908b4b22c30d86cd2cff240b3b7aa6da596 - pluggy 1.1.0.

Problem

In this commit I made an optimization such that the list of hook implementations of a hook (_hookimpls) is no longer copied on every call to the hook. But I neglected to think about the case when _hookimpls is modified by calling the hook itself. This is what happens in your reproduction:

Why is plugin3 called twice? First time, because pytest_configure is an historic hook and so it's called by the register(). Second time, because its registration modifies _hookimpls while it is being iterated and it ends up being included in the call.

Why is the conftest getting skipped? Because it's something that can happen when invalidating an iterator. Example:

l = [0, 1, 2]
for x in reversed(l):
    print(x)
    if x == 1:
        l.insert(0, -1)

This prints 2, 1, -1 and 0 is skipped.

BTW, if I modify pluggy to avoid the reverse, then the conftest isn't skipped. But this is incidental, it could have just as well caused an infinite loop (try removing reversed from the snippet above).

Possible solutions

First solution is to bring back the copy.

Second solution is to disallow doing this in one way or another. However pytest explicitly documents this patten so this may be out of the question.

Third solution is to somehow make this work without the copy.

Redoubts commented 7 months ago

ty for addressing!