mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-27 16:06:11 +00:00
test: probe-and-skip symlink tests where symlink creation is unavailable (#2642)
This commit is contained in:
committed by
safishamsi
parent
243f3e32c1
commit
cdf1f65656
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user