diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b0b2701c27a..9ff7ef7c0c7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,11 +27,15 @@ Changes * Change exception raised by ``capture.DontReadFromInput.fileno()`` from ``ValueError`` to ``io.UnsupportedOperation``. Thanks `@vlad-dragos`_ for the PR. +* Fix an internal caching issue which could cause hooks from ``conftest.py`` files in + sub-directories to be called in other directories incorrectly (`#2016`_). + Thanks `@d-b-w`_ for the report and `@nicoddemus`_ for the PR. -* fix `#2013`_: turn RecordedWarning into namedtupe, - to give it a comprehensible repr while preventing unwarranted modification +* fix `#2013`_: turn RecordedWarning into ``namedtuple`` + to give it a comprehensible repr while preventing unwarranted modification. .. _@davidszotten: https://github.com/davidszotten +.. _@d-b-w: https://github.com/d-b-w .. _@fushi: https://github.com/fushi .. _@mattduck: https://github.com/mattduck @@ -39,6 +43,7 @@ Changes .. _#1874: https://github.com/pytest-dev/pytest/pull/1874 .. _#1952: https://github.com/pytest-dev/pytest/pull/1952 .. _#2013: https://github.com/pytest-dev/pytest/issues/2013 +.. _#2016: https://github.com/pytest-dev/pytest/issues/2016 3.0.5.dev0 diff --git a/_pytest/main.py b/_pytest/main.py index 451355ef3e4..fc1a3c20460 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -561,7 +561,20 @@ def pytest_runtest_logreport(self, report): def isinitpath(self, path): return path in self._initialpaths + def _maybe_clear_hook_proxy_cache(self): + """ + Reset the hook proxy cache if the number of conftests has changed + since the last time it was used (#2016). + """ + pm = self.config.pluginmanager + total_conftests = len(pm._conftest_plugins) + last_conftests = getattr(self, 'last_conftests', 0) + if total_conftests != last_conftests: + self._fs2hookproxy.clear() + self.last_conftests = total_conftests + def gethookproxy(self, fspath): + self._maybe_clear_hook_proxy_cache() try: return self._fs2hookproxy[fspath] except KeyError: diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 641c7c31f51..dce5c3618fc 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -481,6 +481,7 @@ def _makefile(self, ext, args, kwargs): ret = None for name, value in items: p = self.tmpdir.join(name).new(ext=ext) + p.dirpath().ensure_dir() source = Source(value) def my_totext(s, encoding="utf-8"): if py.builtin._isbytes(s): diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 377283eb92d..d50d1fa5f13 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -421,3 +421,26 @@ def test_some(): res = testdir.runpytest() assert res.ret == 4 assert 'raise ValueError()' in [line.strip() for line in res.errlines] + + +def test_hook_proxy_cache(testdir): + """Session's gethookproxy() could cache incorrectly conftests (#2016).""" + testdir.makepyfile(**{ + 'root/demo-0/test_foo1.py': "def test1(): pass", + + 'root/demo-a/test_foo2.py': "def test1(): pass", + 'root/demo-a/conftest.py': """ + def pytest_ignore_collect(path, config): + return True + """, + + 'root/demo-b/test_foo3.py': "def test1(): pass", + 'root/demo-c/test_foo4.py': "def test1(): pass", + }) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*test_foo1.py*', + '*test_foo3.py*', + '*test_foo4.py*', + '*3 passed*', + ]) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index e61c84247c3..22f02cb4736 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -4,7 +4,8 @@ import os from _pytest.config import get_config, PytestPluginManager -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import EXIT_NOTESTSCOLLECTED, Session + @pytest.fixture def pytestpm(): @@ -129,6 +130,25 @@ def pytest_plugin_registered(self): finally: undo() + def test_hook_proxy_cache_reset(self, testdir): + """Importing new conftest files should reset the internal hook proxy cache (#2016)""" + config = testdir.parseconfig() + session = Session(config) + testdir.makepyfile(**{ + 'tests/conftest.py': '', + 'tests/subdir/conftest.py': '', + }) + + conftest1 = testdir.tmpdir.join('tests/conftest.py') + conftest2 = testdir.tmpdir.join('tests/subdir/conftest.py') + + config.pluginmanager._importconftest(conftest1) + ihook_a = session.gethookproxy(testdir.tmpdir.join('tests')) + assert ihook_a is not None + config.pluginmanager._importconftest(conftest2) + ihook_b = session.gethookproxy(testdir.tmpdir.join('tests')) + assert ihook_a is not ihook_b + def test_warn_on_deprecated_multicall(self, pytestpm): warnings = []