Fix PytestPluginManager._is_in_confcutdir on Windows

On Windows two paths might be in different drives, so if a path is not one of the parents of the confcutdir, it does not necessarily mean it is inside the confcutdir either.

Use `Path.is_relative_to` instead which directly answers that question for us.

The previous implementation could cause pytest to inspect all the parent paths of a path from one drive if pytest was invoked from another drive.

Moved the implementation to a function because it would be too convoluted to test the `PytestPluginManager._is_in_confcutdir` method directly.
This commit is contained in:
Bruno Oliveira 2024-02-18 08:04:42 -03:00
parent 5f241f388b
commit f829a1ef06
2 changed files with 63 additions and 8 deletions

View File

@ -580,11 +580,9 @@ class PytestPluginManager(PluginManager):
def _is_in_confcutdir(self, path: Path) -> bool: def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether a path is within the confcutdir. """Whether a path is within the confcutdir.
When false, should not load conftest. When false, should not load conftest or recurse into path for collection.
""" """
if self._confcutdir is None: return path_within_confcutdir(path=path, confcutdir=self._confcutdir)
return True
return path not in self._confcutdir.parents
def _try_load_conftest( def _try_load_conftest(
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
@ -609,9 +607,6 @@ class PytestPluginManager(PluginManager):
if directory in self._dirpath2confmods: if directory in self._dirpath2confmods:
return return
# XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir.
clist = [] clist = []
for parent in reversed((directory, *directory.parents)): for parent in reversed((directory, *directory.parents)):
if self._is_in_confcutdir(parent): if self._is_in_confcutdir(parent):
@ -1908,3 +1903,10 @@ def apply_warning_filters(
for arg in cmdline_filters: for arg in cmdline_filters:
warnings.filterwarnings(*parse_warning_filter(arg, escape=True)) warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
def path_within_confcutdir(*, path: Path, confcutdir: Optional[Path]) -> bool:
# Extracted into a function for unit-testing.
if confcutdir is None:
return True
return path.is_relative_to(confcutdir)

View File

@ -1,12 +1,15 @@
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
import os import os
from pathlib import Path
import shutil import shutil
import sys import sys
import types import types
from typing import List from typing import List
from typing import Optional
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import path_within_confcutdir
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
from _pytest.main import Session from _pytest.main import Session
@ -404,7 +407,7 @@ class TestPytestPluginManager:
pytestpm.consider_conftest(mod, registration_name="unused") pytestpm.consider_conftest(mod, registration_name="unused")
class TestPytestPluginManagerBootstrapming: class TestPytestPluginManagerBootstrapping:
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None: def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
pytest.raises( pytest.raises(
ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"]) ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
@ -464,3 +467,53 @@ class TestPytestPluginManagerBootstrapming:
assert pytestpm.has_plugin("abc") assert pytestpm.has_plugin("abc")
assert not pytestpm.is_blocked("abc") assert not pytestpm.is_blocked("abc")
assert not pytestpm.is_blocked("pytest_abc") assert not pytestpm.is_blocked("pytest_abc")
skip_if_win = pytest.mark.skipif(
not sys.platform.startswith("win"), reason="Windows only"
)
@pytest.mark.parametrize(
"path, confcutdir, expected",
[
(Path("/projects/app/tests"), Path("/projects/app"), True),
(Path("/projects/app"), Path("/projects/app"), True),
(Path("/projects"), Path("/projects/app"), False),
(Path("/"), Path("/projects/app"), False),
pytest.param(
Path("e:/projects/app/tests"),
Path("e:/projects/app"),
True,
marks=skip_if_win,
),
pytest.param(
Path("e:/projects/app"),
Path("e:/projects/app"),
True,
marks=skip_if_win,
),
pytest.param(
Path("e:/"),
Path("e:/projects/app"),
False,
marks=skip_if_win,
),
pytest.param(
Path("c:/testing"),
Path("e:/projects/app"),
False,
marks=skip_if_win,
),
pytest.param(
Path("c:/projects/app"),
Path("e:/projects/app"),
False,
marks=skip_if_win,
),
],
)
def test_path_within_confcutdir(
path: Path, confcutdir: Optional[Path], expected: bool
) -> None:
assert path_within_confcutdir(path=path, confcutdir=confcutdir) == expected