From 8d9b95dcdb131716b72b300abe969d4bf5209bd9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 23 Feb 2024 07:51:15 -0300 Subject: [PATCH] Fix collection of short paths on Windows (#11936) Passing a short path in the command line was causing the matchparts check to fail, because ``Path(short_path) != Path(long_path)``. Using ``os.path.samefile`` as fallback ensures the comparsion works on Windows when comparing short/long paths. Fix #11895 --- changelog/11895.bugfix.rst | 1 + src/_pytest/main.py | 4 ++++ testing/test_collection.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 changelog/11895.bugfix.rst diff --git a/changelog/11895.bugfix.rst b/changelog/11895.bugfix.rst new file mode 100644 index 000000000..4211213c1 --- /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 672ad67b9..639bf26c1 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -906,6 +906,10 @@ class Session(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 de8cacb8f..050740045 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -4,9 +4,11 @@ from pathlib import Path 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 @@ -1759,3 +1761,29 @@ def test_does_not_crash_on_recursive_symlink(pytester: Pytester) -> None: 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}