diff --git a/changelog/11895.bugfix.rst b/changelog/11895.bugfix.rst new file mode 100644 index 00000000000..4211213c1e5 --- /dev/null +++ b/changelog/11895.bugfix.rst @@ -0,0 +1 @@ +Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``). diff --git a/src/_pytest/main.py b/src/_pytest/main.py index f1c05754b2f..fd9dddfa318 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -902,6 +902,10 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`. if isinstance(matchparts[0], Path): is_match = node.path == matchparts[0] + if sys.platform == "win32" and not is_match: + # In case the file paths do not match, fallback to samefile() to + # account for short-paths on Windows (#11895). + is_match = os.path.samefile(node.path, matchparts[0]) # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`. else: # TODO: Remove parametrized workaround once collection structure contains diff --git a/testing/test_collection.py b/testing/test_collection.py index b2780eb73ae..c7923c5efb6 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -3,9 +3,11 @@ import pprint import shutil import sys +import tempfile import textwrap from typing import List +from _pytest.assertion.util import running_on_ci from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.main import _in_venv @@ -1758,3 +1760,29 @@ def test_foo(): assert True assert result.ret == ExitCode.OK assert result.parseoutcomes() == {"passed": 1} + + +@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") +def test_collect_short_file_windows(pytester: Pytester) -> None: + """Reproducer for #11895: short paths not colleced on Windows.""" + short_path = tempfile.mkdtemp() + if "~" not in short_path: # pragma: no cover + if running_on_ci(): + # On CI, we are expecting that under the current GitHub actions configuration, + # tempfile.mkdtemp() is producing short paths, so we want to fail to prevent + # this from silently changing without us noticing. + pytest.fail( + f"tempfile.mkdtemp() failed to produce a short path on CI: {short_path}" + ) + else: + # We want to skip failing this test locally in this situation because + # depending on the local configuration tempfile.mkdtemp() might not produce a short path: + # For example, user might have configured %TEMP% exactly to avoid generating short paths. + pytest.skip( + f"tempfile.mkdtemp() failed to produce a short path: {short_path}, skipping" + ) + + test_file = Path(short_path).joinpath("test_collect_short_file_windows.py") + test_file.write_text("def test(): pass", encoding="UTF-8") + result = pytester.runpytest(short_path) + assert result.parseoutcomes() == {"passed": 1}