test: probe-and-skip symlink tests where symlink creation is unavailable (#2642)

This commit is contained in:
rajashidattapy
2026-08-12 20:56:28 +01:00
committed by safishamsi
parent 243f3e32c1
commit cdf1f65656
5 changed files with 56 additions and 15 deletions
+41
View File
@@ -1,11 +1,52 @@
from __future__ import annotations
import tempfile
from pathlib import Path
from typing import Any
import pytest
@pytest.fixture(scope="session")
def _can_symlink() -> bool:
"""Whether this machine can create symlinks at all (#2642).
Probed rather than inferred from ``sys.platform``: Windows *can* create
symlinks from an elevated shell or with Developer Mode enabled, and those
runs should still get the coverage. A plain non-elevated Windows shell
raises ``OSError: [WinError 1314] A required privilege is not held by the
client``, which pytest reports as a FAILURE — 15 of them, drowning out real
defects — when what it means is "unsupported here".
One file symlink is enough to probe: Windows gates file and directory
symlinks behind the same ``SeCreateSymbolicLinkPrivilege`` check.
"""
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "probe-src"
src.write_text("x", encoding="utf-8")
try:
(Path(d) / "probe-link").symlink_to(src)
except (OSError, NotImplementedError):
return False
return True
@pytest.fixture
def requires_symlinks(_can_symlink) -> None:
"""Skip a test that must create symlinks when the platform won't allow it.
Take this as a parameter rather than wrapping each ``symlink_to()`` call in
try/except: the guard then sits in the signature where it is visible, and
an OSError from the code UNDER test is still a real failure instead of
being swallowed into a skip.
"""
if not _can_symlink:
pytest.skip(
"symlink creation unavailable on this machine "
"(Windows requires an elevated shell or Developer Mode)"
)
@pytest.fixture(autouse=True)
def _sandbox_home(tmp_path_factory, monkeypatch):
"""Every test gets a throwaway HOME so installers/uninstallers can never
+1 -1
View File
@@ -99,7 +99,7 @@ def test_write_text_atomic_new_file_respects_umask(tmp_path):
assert (os.stat(p).st_mode & 0o777) == (0o666 & ~umask)
def test_write_text_atomic_writes_through_symlink(tmp_path):
def test_write_text_atomic_writes_through_symlink(requires_symlinks, tmp_path):
# Shared-output setups symlink graph.json to shared storage; the atomic write
# must update the target and keep the link, not replace it with a real file.
target = tmp_path / "real.json"
+8 -8
View File
@@ -308,7 +308,7 @@ def test_git_info_exclude_utf8_bom(tmp_path):
assert any("real.py" in f for f in all_files)
def test_detect_follows_symlinked_directory(tmp_path):
def test_detect_follows_symlinked_directory(requires_symlinks, tmp_path):
real_dir = tmp_path / "real_lib"
real_dir.mkdir()
(real_dir / "util.py").write_text("x = 1")
@@ -322,7 +322,7 @@ def test_detect_follows_symlinked_directory(tmp_path):
assert any("linked_lib" in f for f in result_yes["files"]["code"])
def test_detect_follows_symlinked_file(tmp_path):
def test_detect_follows_symlinked_file(requires_symlinks, tmp_path):
(tmp_path / "real.py").write_text("x = 1")
(tmp_path / "link.py").symlink_to(tmp_path / "real.py")
@@ -484,7 +484,7 @@ def test_nested_ignore_overrides_git_info_exclude_and_root(tmp_path):
assert not any(f.endswith("drop.py") for f in code)
def test_detect_handles_circular_symlinks(tmp_path):
def test_detect_handles_circular_symlinks(requires_symlinks, tmp_path):
sub = tmp_path / "a"
sub.mkdir()
(sub / "main.py").write_text("x = 1")
@@ -494,7 +494,7 @@ def test_detect_handles_circular_symlinks(tmp_path):
assert any("main.py" in f for f in result["files"]["code"])
def test_detect_default_does_not_auto_follow_direct_symlink_child(tmp_path):
def test_detect_default_does_not_auto_follow_direct_symlink_child(requires_symlinks, tmp_path):
"""Symlink directory following is explicit opt-in."""
real_dir = tmp_path / "real_lib"
real_dir.mkdir()
@@ -518,7 +518,7 @@ def test_detect_default_does_not_follow_when_no_symlinks(tmp_path):
assert any("other.py" in f for f in result["files"]["code"])
def test_detect_explicit_false_overrides_auto_detect(tmp_path):
def test_detect_explicit_false_overrides_auto_detect(requires_symlinks, tmp_path):
"""An explicit follow_symlinks=False skips symlinked directories."""
real_dir = tmp_path / "real_lib"
real_dir.mkdir()
@@ -530,7 +530,7 @@ def test_detect_explicit_false_overrides_auto_detect(tmp_path):
assert not any("linked_lib" in f for f in result["files"]["code"])
def test_detect_skips_out_of_root_symlinked_directory_even_when_following(tmp_path):
def test_detect_skips_out_of_root_symlinked_directory_even_when_following(requires_symlinks, tmp_path):
root = tmp_path / "root"
root.mkdir()
outside = tmp_path / "outside"
@@ -544,7 +544,7 @@ def test_detect_skips_out_of_root_symlinked_directory_even_when_following(tmp_pa
assert any("symlink target outside scan root" in item for item in result["skipped_sensitive"])
def test_detect_skips_out_of_root_symlinked_file_by_default(tmp_path):
def test_detect_skips_out_of_root_symlinked_file_by_default(requires_symlinks, tmp_path):
root = tmp_path / "root"
root.mkdir()
outside = tmp_path / "outside"
@@ -558,7 +558,7 @@ def test_detect_skips_out_of_root_symlinked_file_by_default(tmp_path):
assert any("symlink target outside scan root" in item for item in result["skipped_sensitive"])
def test_detect_incremental_propagates_follow_symlinks(tmp_path, monkeypatch):
def test_detect_incremental_propagates_follow_symlinks(requires_symlinks, tmp_path, monkeypatch):
"""detect_incremental must forward follow_symlinks so symlinked sub-trees
appear in incremental scans the same way they appear in full scans."""
monkeypatch.chdir(tmp_path)
+4 -4
View File
@@ -390,7 +390,7 @@ def test_collect_files_skips_hidden():
assert not any(part.startswith(".") for part in f.parts)
def test_collect_files_follows_symlinked_directory(tmp_path):
def test_collect_files_follows_symlinked_directory(requires_symlinks, tmp_path):
real_dir = tmp_path / "real_src"
real_dir.mkdir()
(real_dir / "lib.py").write_text("x = 1")
@@ -403,7 +403,7 @@ def test_collect_files_follows_symlinked_directory(tmp_path):
assert [f.name for f in files_yes].count("lib.py") == 2
def test_collect_files_skips_out_of_root_symlinked_directory(tmp_path):
def test_collect_files_skips_out_of_root_symlinked_directory(requires_symlinks, tmp_path):
root = tmp_path / "root"
root.mkdir()
outside = tmp_path / "outside"
@@ -416,7 +416,7 @@ def test_collect_files_skips_out_of_root_symlinked_directory(tmp_path):
assert not any("linked_secret" in str(f) for f in files)
def test_collect_files_skips_out_of_root_symlinked_file_by_default(tmp_path):
def test_collect_files_skips_out_of_root_symlinked_file_by_default(requires_symlinks, tmp_path):
root = tmp_path / "root"
root.mkdir()
outside = tmp_path / "outside"
@@ -429,7 +429,7 @@ def test_collect_files_skips_out_of_root_symlinked_file_by_default(tmp_path):
assert not any(f.name == "secret_link.py" for f in files)
def test_collect_files_handles_circular_symlinks(tmp_path):
def test_collect_files_handles_circular_symlinks(requires_symlinks, tmp_path):
sub = tmp_path / "pkg"
sub.mkdir()
(sub / "mod.py").write_text("x = 1")
+2 -2
View File
@@ -70,7 +70,7 @@ def test_non_pdf_still_read_as_plain_text(tmp_path):
assert "# hello" in llm._file_to_text(md)
def test_read_files_skips_out_of_root_symlink(tmp_path):
def test_read_files_skips_out_of_root_symlink(requires_symlinks, tmp_path):
root = tmp_path / "root"
root.mkdir()
outside = tmp_path / "outside"
@@ -104,7 +104,7 @@ def test_build_image_refs_sets_rel_media_and_bytes(tmp_path):
assert ref.bedrock_format == "png"
def test_build_image_refs_skips_out_of_root_symlink(tmp_path):
def test_build_image_refs_skips_out_of_root_symlink(requires_symlinks, tmp_path):
root = tmp_path / "root"
root.mkdir()
outside = tmp_path / "outside"