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:
"""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 True
return path not in self._confcutdir.parents
return path_within_confcutdir(path=path, confcutdir=self._confcutdir)
def _try_load_conftest(
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
@ -609,9 +607,6 @@ class PytestPluginManager(PluginManager):
if directory in self._dirpath2confmods:
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 = []
for parent in reversed((directory, *directory.parents)):
if self._is_in_confcutdir(parent):
@ -1908,3 +1903,10 @@ def apply_warning_filters(
for arg in cmdline_filters:
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
import os
from pathlib import Path
import shutil
import sys
import types
from typing import List
from typing import Optional
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import path_within_confcutdir
from _pytest.config import PytestPluginManager
from _pytest.config.exceptions import UsageError
from _pytest.main import Session
@ -404,7 +407,7 @@ class TestPytestPluginManager:
pytestpm.consider_conftest(mod, registration_name="unused")
class TestPytestPluginManagerBootstrapming:
class TestPytestPluginManagerBootstrapping:
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
pytest.raises(
ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
@ -464,3 +467,53 @@ class TestPytestPluginManagerBootstrapming:
assert pytestpm.has_plugin("abc")
assert not pytestpm.is_blocked("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