Skip to content

Commit 13204cd

Browse files
committed
Do not collect symlinked tests under Windows
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.
1 parent c967d50 commit 13204cd

File tree

4 files changed

+36
-3
lines changed

4 files changed

+36
-3
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ Mike Hoyle (hoylemd)
283283
Mike Lundy
284284
Milan Lesnek
285285
Miro Hrončok
286+
mrbean-bremen
286287
Nathaniel Compton
287288
Nathaniel Waisbrot
288289
Ned Batchelder

changelog/12039.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed a regression in 8.0.2 where tests have been collected multiple times in the CI under Windows

src/_pytest/main.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import functools
77
import importlib
88
import os
9+
import stat
910
from pathlib import Path
1011
import sys
1112
from typing import AbstractSet
@@ -443,6 +444,20 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No
443444
items[:] = remaining
444445

445446

447+
def _is_junction(path: Path) -> bool:
448+
if sys.version_info >= (3, 12):
449+
return os.path.isjunction(path)
450+
451+
if hasattr(os.stat_result, 'st_reparse_tag'):
452+
try:
453+
st = os.lstat(path)
454+
except (OSError, ValueError, AttributeError):
455+
return False
456+
return bool(st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT)
457+
458+
return False
459+
460+
446461
class FSHookProxy:
447462
def __init__(
448463
self,
@@ -907,9 +922,16 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
907922
if isinstance(matchparts[0], Path):
908923
is_match = node.path == matchparts[0]
909924
if sys.platform == "win32" and not is_match:
910-
# In case the file paths do not match, fallback to samefile() to
925+
# In case the file paths do not match,
911926
# account for short-paths on Windows (#11895).
912-
is_match = os.path.samefile(node.path, matchparts[0])
927+
same_file = os.path.samefile(node.path, matchparts[0])
928+
# we don't want to find links, so we at least
929+
# exclude symlinks to regular directories
930+
is_match = (
931+
same_file and
932+
os.path.islink(node.path) == os.path.islink(matchparts[0])
933+
)
934+
913935
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
914936
else:
915937
# TODO: Remove parametrized workaround once collection structure contains

testing/test_collection.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -1765,7 +1765,7 @@ def test_foo(): assert True
17651765

17661766
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only")
17671767
def test_collect_short_file_windows(pytester: Pytester) -> None:
1768-
"""Reproducer for #11895: short paths not colleced on Windows."""
1768+
"""Reproducer for #11895: short paths not collected on Windows."""
17691769
short_path = tempfile.mkdtemp()
17701770
if "~" not in short_path: # pragma: no cover
17711771
if running_on_ci():
@@ -1787,3 +1787,12 @@ def test_collect_short_file_windows(pytester: Pytester) -> None:
17871787
test_file.write_text("def test(): pass", encoding="UTF-8")
17881788
result = pytester.runpytest(short_path)
17891789
assert result.parseoutcomes() == {"passed": 1}
1790+
1791+
1792+
def test_collect_symlinks(pytester: Pytester, tmpdir) -> None:
1793+
"""Regression test for #12039: Tests collected multiple times under Windows."""
1794+
test_file = Path(tmpdir) / "symlink_collection_test.py"
1795+
test_file.write_text("def test(): pass", encoding="UTF-8")
1796+
result = pytester.runpytest(tmpdir)
1797+
# this failed in CI only (GitHub actions)
1798+
assert result.parseoutcomes() == {"passed": 1}

0 commit comments

Comments
 (0)