Skip to content

Commit

Permalink
Do not collect symlinked tests under Windows
Browse files Browse the repository at this point in the history
The check for short paths under Windows via os.path.samefile, introduced in pytest-dev#11936, also found similar tests in symlinked tests in the GH Actions CI.
This checks additionally that one of the files is not a symlink.

Fixes pytest-dev#12039.
  • Loading branch information
mrbean-bremen committed Mar 3, 2024
1 parent c967d50 commit 13204cd
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 3 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ Mike Hoyle (hoylemd)
Mike Lundy
Milan Lesnek
Miro Hrončok
mrbean-bremen
Nathaniel Compton
Nathaniel Waisbrot
Ned Batchelder
Expand Down
1 change: 1 addition & 0 deletions changelog/12039.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed a regression in 8.0.2 where tests have been collected multiple times in the CI under Windows
26 changes: 24 additions & 2 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import functools
import importlib
import os
import stat
from pathlib import Path
import sys
from typing import AbstractSet
Expand Down Expand Up @@ -443,6 +444,20 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No
items[:] = remaining


def _is_junction(path: Path) -> bool:
if sys.version_info >= (3, 12):
return os.path.isjunction(path)

if hasattr(os.stat_result, 'st_reparse_tag'):
try:
st = os.lstat(path)
except (OSError, ValueError, AttributeError):
return False
return bool(st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT)

return False


class FSHookProxy:
def __init__(
self,
Expand Down Expand Up @@ -907,9 +922,16 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
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
# In case the file paths do not match,
# account for short-paths on Windows (#11895).
is_match = os.path.samefile(node.path, matchparts[0])
same_file = os.path.samefile(node.path, matchparts[0])
# we don't want to find links, so we at least
# exclude symlinks to regular directories
is_match = (
same_file and
os.path.islink(node.path) == os.path.islink(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
Expand Down
11 changes: 10 additions & 1 deletion testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1765,7 +1765,7 @@ def test_foo(): assert True

@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."""
"""Reproducer for #11895: short paths not collected on Windows."""
short_path = tempfile.mkdtemp()
if "~" not in short_path: # pragma: no cover
if running_on_ci():
Expand All @@ -1787,3 +1787,12 @@ def test_collect_short_file_windows(pytester: Pytester) -> None:
test_file.write_text("def test(): pass", encoding="UTF-8")
result = pytester.runpytest(short_path)
assert result.parseoutcomes() == {"passed": 1}


def test_collect_symlinks(pytester: Pytester, tmpdir) -> None:
"""Regression test for #12039: Tests collected multiple times under Windows."""
test_file = Path(tmpdir) / "symlink_collection_test.py"
test_file.write_text("def test(): pass", encoding="UTF-8")
result = pytester.runpytest(tmpdir)
# this failed in CI only (GitHub actions)
assert result.parseoutcomes() == {"passed": 1}

0 comments on commit 13204cd

Please sign in to comment.