More coverage
This commit is contained in:
parent
62b2f2ffe6
commit
fbb06f1177
1 changed files with 458 additions and 31 deletions
|
|
@ -1,10 +1,28 @@
|
||||||
import json
|
import json
|
||||||
import enroll.harvest as harvest
|
import os
|
||||||
|
import pytest
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import enroll.harvest as h
|
import enroll.harvest as harvest
|
||||||
from enroll.platform import PlatformInfo
|
from enroll.platform import PlatformInfo
|
||||||
from enroll.systemd import UnitInfo
|
from enroll.systemd import UnitInfo
|
||||||
|
from enroll.pathfilter import PathFilter
|
||||||
|
from enroll.harvest import (
|
||||||
|
_is_confish,
|
||||||
|
_hint_names,
|
||||||
|
_topdirs_for_package,
|
||||||
|
_iter_matching_files,
|
||||||
|
_parse_apt_signed_by,
|
||||||
|
_capture_link,
|
||||||
|
_capture_file,
|
||||||
|
ManagedFile,
|
||||||
|
ManagedLink,
|
||||||
|
ExcludedFile,
|
||||||
|
IgnorePolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
class AllowAllPolicy:
|
class AllowAllPolicy:
|
||||||
|
|
@ -155,17 +173,17 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
||||||
else:
|
else:
|
||||||
yield (root, [], [])
|
yield (root, [], [])
|
||||||
|
|
||||||
monkeypatch.setattr(h.os.path, "isfile", fake_isfile)
|
monkeypatch.setattr(harvest.os.path, "isfile", fake_isfile)
|
||||||
monkeypatch.setattr(h.os.path, "isdir", fake_isdir)
|
monkeypatch.setattr(harvest.os.path, "isdir", fake_isdir)
|
||||||
monkeypatch.setattr(h.os.path, "islink", fake_islink)
|
monkeypatch.setattr(harvest.os.path, "islink", fake_islink)
|
||||||
monkeypatch.setattr(h.os.path, "exists", fake_exists)
|
monkeypatch.setattr(harvest.os.path, "exists", fake_exists)
|
||||||
monkeypatch.setattr(h.os, "walk", fake_walk)
|
monkeypatch.setattr(harvest.os, "walk", fake_walk)
|
||||||
|
|
||||||
# Avoid real system access
|
# Avoid real system access
|
||||||
monkeypatch.setattr(h, "list_enabled_services", lambda: ["openvpn.service"])
|
monkeypatch.setattr(harvest, "list_enabled_services", lambda: ["openvpn.service"])
|
||||||
monkeypatch.setattr(h, "list_enabled_timers", lambda: [])
|
monkeypatch.setattr(harvest, "list_enabled_timers", lambda: [])
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
h,
|
harvest,
|
||||||
"get_unit_info",
|
"get_unit_info",
|
||||||
lambda unit: UnitInfo(
|
lambda unit: UnitInfo(
|
||||||
name=unit,
|
name=unit,
|
||||||
|
|
@ -200,11 +218,11 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
h, "detect_platform", lambda: PlatformInfo("debian", "dpkg", {})
|
harvest, "detect_platform", lambda: PlatformInfo("debian", "dpkg", {})
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(h, "get_backend", lambda info=None: backend)
|
monkeypatch.setattr(harvest, "get_backend", lambda info=None: backend)
|
||||||
|
|
||||||
monkeypatch.setattr(h, "collect_non_system_users", lambda: [])
|
monkeypatch.setattr(harvest, "collect_non_system_users", lambda: [])
|
||||||
|
|
||||||
def fake_stat_triplet(p: str):
|
def fake_stat_triplet(p: str):
|
||||||
if p == "/usr/local/bin/myscript":
|
if p == "/usr/local/bin/myscript":
|
||||||
|
|
@ -212,7 +230,7 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
||||||
# /usr/local/bin/readme.txt remains non-executable
|
# /usr/local/bin/readme.txt remains non-executable
|
||||||
return ("root", "root", "0644")
|
return ("root", "root", "0644")
|
||||||
|
|
||||||
monkeypatch.setattr(h, "stat_triplet", fake_stat_triplet)
|
monkeypatch.setattr(harvest, "stat_triplet", fake_stat_triplet)
|
||||||
|
|
||||||
# Avoid needing source files on disk by implementing our own bundle copier
|
# Avoid needing source files on disk by implementing our own bundle copier
|
||||||
def fake_copy(bundle_dir: str, role_name: str, abs_path: str, src_rel: str):
|
def fake_copy(bundle_dir: str, role_name: str, abs_path: str, src_rel: str):
|
||||||
|
|
@ -220,9 +238,9 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
dst.write_bytes(files.get(abs_path, b""))
|
dst.write_bytes(files.get(abs_path, b""))
|
||||||
|
|
||||||
monkeypatch.setattr(h, "_copy_into_bundle", fake_copy)
|
monkeypatch.setattr(harvest, "_copy_into_bundle", fake_copy)
|
||||||
|
|
||||||
state_path = h.harvest(str(bundle), policy=AllowAllPolicy())
|
state_path = harvest.harvest(str(bundle), policy=AllowAllPolicy())
|
||||||
st = json.loads(Path(state_path).read_text(encoding="utf-8"))
|
st = json.loads(Path(state_path).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
inv = st["inventory"]["packages"]
|
inv = st["inventory"]["packages"]
|
||||||
|
|
@ -275,21 +293,25 @@ def test_shared_cron_snippet_prefers_matching_role_over_lexicographic(
|
||||||
files = {"/etc/cron.d/ntpsec": b"# cron\n"}
|
files = {"/etc/cron.d/ntpsec": b"# cron\n"}
|
||||||
dirs = {"/etc", "/etc/cron.d"}
|
dirs = {"/etc", "/etc/cron.d"}
|
||||||
|
|
||||||
monkeypatch.setattr(h.os.path, "isfile", lambda p: p in files)
|
monkeypatch.setattr(harvest.os.path, "isfile", lambda p: p in files)
|
||||||
monkeypatch.setattr(h.os.path, "islink", lambda p: False)
|
monkeypatch.setattr(harvest.os.path, "islink", lambda p: False)
|
||||||
monkeypatch.setattr(h.os.path, "isdir", lambda p: p in dirs)
|
monkeypatch.setattr(harvest.os.path, "isdir", lambda p: p in dirs)
|
||||||
monkeypatch.setattr(h.os.path, "exists", lambda p: p in files or p in dirs)
|
monkeypatch.setattr(harvest.os.path, "exists", lambda p: p in files or p in dirs)
|
||||||
monkeypatch.setattr(h.os, "walk", lambda root: [("/etc/cron.d", [], ["ntpsec"])])
|
monkeypatch.setattr(
|
||||||
|
harvest.os, "walk", lambda root: [("/etc/cron.d", [], ["ntpsec"])]
|
||||||
|
)
|
||||||
|
|
||||||
# Only include the cron snippet in the system capture set.
|
# Only include the cron snippet in the system capture set.
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
h, "_iter_system_capture_paths", lambda: [("/etc/cron.d/ntpsec", "system_cron")]
|
harvest,
|
||||||
|
"_iter_system_capture_paths",
|
||||||
|
lambda: [("/etc/cron.d/ntpsec", "system_cron")],
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
h, "list_enabled_services", lambda: ["apparmor.service", "ntpsec.service"]
|
harvest, "list_enabled_services", lambda: ["apparmor.service", "ntpsec.service"]
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(h, "list_enabled_timers", lambda: [])
|
monkeypatch.setattr(harvest, "list_enabled_timers", lambda: [])
|
||||||
|
|
||||||
def fake_unit_info(unit: str) -> UnitInfo:
|
def fake_unit_info(unit: str) -> UnitInfo:
|
||||||
if unit == "apparmor.service":
|
if unit == "apparmor.service":
|
||||||
|
|
@ -316,7 +338,7 @@ def test_shared_cron_snippet_prefers_matching_role_over_lexicographic(
|
||||||
condition_result=None,
|
condition_result=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(h, "get_unit_info", fake_unit_info)
|
monkeypatch.setattr(harvest, "get_unit_info", fake_unit_info)
|
||||||
|
|
||||||
# Make apparmor *also* claim the ntpsec package (simulates overly-broad
|
# Make apparmor *also* claim the ntpsec package (simulates overly-broad
|
||||||
# package inference). The snippet routing should still prefer role 'ntpsec'.
|
# package inference). The snippet routing should still prefer role 'ntpsec'.
|
||||||
|
|
@ -341,21 +363,21 @@ def test_shared_cron_snippet_prefers_matching_role_over_lexicographic(
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
h, "detect_platform", lambda: PlatformInfo("debian", "dpkg", {})
|
harvest, "detect_platform", lambda: PlatformInfo("debian", "dpkg", {})
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(h, "get_backend", lambda info=None: backend)
|
monkeypatch.setattr(harvest, "get_backend", lambda info=None: backend)
|
||||||
|
|
||||||
monkeypatch.setattr(h, "stat_triplet", lambda p: ("root", "root", "0644"))
|
monkeypatch.setattr(harvest, "stat_triplet", lambda p: ("root", "root", "0644"))
|
||||||
monkeypatch.setattr(h, "collect_non_system_users", lambda: [])
|
monkeypatch.setattr(harvest, "collect_non_system_users", lambda: [])
|
||||||
|
|
||||||
def fake_copy(bundle_dir: str, role_name: str, abs_path: str, src_rel: str):
|
def fake_copy(bundle_dir: str, role_name: str, abs_path: str, src_rel: str):
|
||||||
dst = Path(bundle_dir) / "artifacts" / role_name / src_rel
|
dst = Path(bundle_dir) / "artifacts" / role_name / src_rel
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
dst.write_bytes(files[abs_path])
|
dst.write_bytes(files[abs_path])
|
||||||
|
|
||||||
monkeypatch.setattr(h, "_copy_into_bundle", fake_copy)
|
monkeypatch.setattr(harvest, "_copy_into_bundle", fake_copy)
|
||||||
|
|
||||||
state_path = h.harvest(str(bundle), policy=AllowAllPolicy())
|
state_path = harvest.harvest(str(bundle), policy=AllowAllPolicy())
|
||||||
st = json.loads(Path(state_path).read_text(encoding="utf-8"))
|
st = json.loads(Path(state_path).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
# Cron snippet should end up attached to the ntpsec role, not apparmor.
|
# Cron snippet should end up attached to the ntpsec role, not apparmor.
|
||||||
|
|
@ -607,3 +629,408 @@ def test_is_confish_not_config(tmp_path: Path):
|
||||||
def test_is_confish_nonexistent():
|
def test_is_confish_nonexistent():
|
||||||
"""Test _is_confish returns False for nonexistent files."""
|
"""Test _is_confish returns False for nonexistent files."""
|
||||||
assert harvest._is_confish("/nonexistent/file.xyz") is False
|
assert harvest._is_confish("/nonexistent/file.xyz") is False
|
||||||
|
|
||||||
|
|
||||||
|
"""Additional coverage tests for harvest.py"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsConfish:
|
||||||
|
"""Tests for _is_confish function"""
|
||||||
|
|
||||||
|
def test_is_confish_true_extensions(self, tmp_path):
|
||||||
|
"""Test files with config extensions are detected."""
|
||||||
|
for ext in [".conf", ".cfg", ".ini", ".yaml", ".json", ".cnf"]:
|
||||||
|
f = tmp_path / f"test{ext}"
|
||||||
|
f.write_text("test", encoding="utf-8")
|
||||||
|
assert _is_confish(str(f)) is True
|
||||||
|
|
||||||
|
def test_is_confish_false(self, tmp_path):
|
||||||
|
"""Test non-config files are not detected."""
|
||||||
|
for name in ["data.txt", "script.sh"]:
|
||||||
|
f = tmp_path / name
|
||||||
|
f.write_text("test", encoding="utf-8")
|
||||||
|
assert _is_confish(str(f)) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestHintNames:
|
||||||
|
"""Tests for _hint_names function"""
|
||||||
|
|
||||||
|
def test_hint_names_simple(self):
|
||||||
|
"""Test simple hint name extraction."""
|
||||||
|
result = _hint_names("nginx", {"nginx"})
|
||||||
|
assert "nginx" in result
|
||||||
|
|
||||||
|
def test_hint_names_multiple(self):
|
||||||
|
"""Test multiple hint names."""
|
||||||
|
result = _hint_names("nginx", {"apache"})
|
||||||
|
assert "nginx" in result
|
||||||
|
assert "apache" in result
|
||||||
|
|
||||||
|
def test_hint_names_empty(self):
|
||||||
|
"""Test empty hint names."""
|
||||||
|
result = _hint_names("", set())
|
||||||
|
assert result == set()
|
||||||
|
|
||||||
|
def test_hint_names_with_service(self):
|
||||||
|
"""Test hint names with .service suffix."""
|
||||||
|
result = _hint_names("nginx.service", set())
|
||||||
|
assert "nginx" in result
|
||||||
|
|
||||||
|
def test_hint_names_with_template(self):
|
||||||
|
"""Test hint names with template unit."""
|
||||||
|
result = _hint_names("nginx@.service", set())
|
||||||
|
assert "nginx" in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestTopdirsForPackage:
|
||||||
|
"""Tests for _topdirs_for_package function"""
|
||||||
|
|
||||||
|
def test_topdirs_single_level(self):
|
||||||
|
"""Test topdirs with single level paths."""
|
||||||
|
pkg_to_etc = {"nginx": ["/etc/nginx/nginx.conf"]}
|
||||||
|
result = _topdirs_for_package("nginx", pkg_to_etc)
|
||||||
|
assert result == {"nginx"}
|
||||||
|
|
||||||
|
def test_topdirs_multiple_paths(self):
|
||||||
|
"""Test topdirs with multiple paths."""
|
||||||
|
pkg_to_etc = {"nginx": ["/etc/nginx/nginx.conf", "/etc/nginx/sites-enabled"]}
|
||||||
|
result = _topdirs_for_package("nginx", pkg_to_etc)
|
||||||
|
assert result == {"nginx"}
|
||||||
|
|
||||||
|
def test_topdirs_empty(self):
|
||||||
|
"""Test topdirs with empty package."""
|
||||||
|
result = _topdirs_for_package("nonexistent", {})
|
||||||
|
assert result == set()
|
||||||
|
|
||||||
|
|
||||||
|
class TestIterMatchingFiles:
|
||||||
|
"""Tests for _iter_matching_files function"""
|
||||||
|
|
||||||
|
def test_iter_matching_files_glob(self, tmp_path):
|
||||||
|
"""Test glob pattern matching."""
|
||||||
|
(tmp_path / "a.txt").write_text("a", encoding="utf-8")
|
||||||
|
(tmp_path / "b.txt").write_text("b", encoding="utf-8")
|
||||||
|
(tmp_path / "c.py").write_text("c", encoding="utf-8")
|
||||||
|
|
||||||
|
os.chdir(tmp_path)
|
||||||
|
result = _iter_matching_files("*.txt")
|
||||||
|
assert len(result) == 2
|
||||||
|
assert any("a.txt" in p for p in result)
|
||||||
|
assert any("b.txt" in p for p in result)
|
||||||
|
|
||||||
|
def test_iter_matching_files_directory_walk(self, tmp_path):
|
||||||
|
"""Test directory walking."""
|
||||||
|
subdir = tmp_path / "sub"
|
||||||
|
subdir.mkdir()
|
||||||
|
(tmp_path / "a.txt").write_text("a", encoding="utf-8")
|
||||||
|
(subdir / "b.txt").write_text("b", encoding="utf-8")
|
||||||
|
|
||||||
|
os.chdir(tmp_path)
|
||||||
|
result = _iter_matching_files(str(tmp_path))
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_iter_matching_files_cap(self, tmp_path):
|
||||||
|
"""Test file cap limit."""
|
||||||
|
for i in range(100):
|
||||||
|
(tmp_path / f"file{i}.txt").write_text(str(i), encoding="utf-8")
|
||||||
|
|
||||||
|
os.chdir(tmp_path)
|
||||||
|
result = _iter_matching_files("*.txt", cap=10)
|
||||||
|
assert len(result) == 10
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseAptSignedBy:
|
||||||
|
"""Tests for _parse_apt_signed_by function"""
|
||||||
|
|
||||||
|
def test_parse_apt_signed_by_bracket(self, tmp_path):
|
||||||
|
"""Test parsing signed-by from bracket notation."""
|
||||||
|
sources_list = tmp_path / "sources.list"
|
||||||
|
sources_list.write_text(
|
||||||
|
"deb [signed-by=/usr/share/keyrings/nginx.gpg] http://nginx.net stable main\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
result = _parse_apt_signed_by([str(sources_list)])
|
||||||
|
assert "/usr/share/keyrings/nginx.gpg" in result
|
||||||
|
|
||||||
|
def test_parse_apt_signed_by_header(self, tmp_path):
|
||||||
|
"""Test parsing signed-by from header."""
|
||||||
|
sources_file = tmp_path / "sources.list"
|
||||||
|
sources_file.write_text(
|
||||||
|
"Signed-By: /usr/share/keyrings/foo.gpg\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
result = _parse_apt_signed_by([str(sources_file)])
|
||||||
|
assert "/usr/share/keyrings/foo.gpg" in result
|
||||||
|
|
||||||
|
def test_parse_apt_signed_by_multiple(self, tmp_path):
|
||||||
|
"""Test parsing multiple signed-by paths."""
|
||||||
|
sources_file = tmp_path / "sources.list"
|
||||||
|
sources_file.write_text(
|
||||||
|
"Signed-By: /usr/share/keyrings/a.gpg, /usr/share/keyrings/b.gpg\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
result = _parse_apt_signed_by([str(sources_file)])
|
||||||
|
assert "/usr/share/keyrings/a.gpg" in result
|
||||||
|
assert "/usr/share/keyrings/b.gpg" in result
|
||||||
|
|
||||||
|
def test_parse_apt_signed_by_oserror(self, tmp_path):
|
||||||
|
"""Test handling of unreadable files."""
|
||||||
|
result = _parse_apt_signed_by(["/nonexistent/file"])
|
||||||
|
assert result == set()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaptureLink:
|
||||||
|
"""Tests for _capture_link function"""
|
||||||
|
|
||||||
|
def test_capture_link_basic(self, tmp_path):
|
||||||
|
"""Test basic link capture."""
|
||||||
|
target = tmp_path / "target.txt"
|
||||||
|
target.write_text("content", encoding="utf-8")
|
||||||
|
link = tmp_path / "link.txt"
|
||||||
|
link.symlink_to(target)
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value=None)
|
||||||
|
policy.deny_reason_link = None # No special link denial
|
||||||
|
|
||||||
|
managed: list[ManagedLink] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
|
||||||
|
result = _capture_link(
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(link),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=set(),
|
||||||
|
seen_global=set(),
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
assert len(managed) == 1
|
||||||
|
assert managed[0].path == str(link)
|
||||||
|
|
||||||
|
def test_capture_link_deny(self, tmp_path):
|
||||||
|
"""Test link capture with deny policy."""
|
||||||
|
target = tmp_path / "target.txt"
|
||||||
|
target.write_text("content", encoding="utf-8")
|
||||||
|
link = tmp_path / "link.txt"
|
||||||
|
link.symlink_to(target)
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value="policy_deny")
|
||||||
|
|
||||||
|
managed: list[ManagedLink] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
|
||||||
|
result = _capture_link(
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(link),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=set(),
|
||||||
|
seen_global=set(),
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
assert len(excluded) == 1
|
||||||
|
|
||||||
|
def test_capture_link_not_symlink(self, tmp_path):
|
||||||
|
"""Test that regular files are rejected."""
|
||||||
|
f = tmp_path / "file.txt"
|
||||||
|
f.write_text("content", encoding="utf-8")
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value=None)
|
||||||
|
|
||||||
|
managed: list[ManagedLink] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
|
||||||
|
result = _capture_link(
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(f),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=set(),
|
||||||
|
seen_global=set(),
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
assert len(excluded) == 1
|
||||||
|
|
||||||
|
def test_capture_link_seen_role(self, tmp_path):
|
||||||
|
"""Test link capture with seen_role deduplication."""
|
||||||
|
target = tmp_path / "target.txt"
|
||||||
|
target.write_text("content", encoding="utf-8")
|
||||||
|
link = tmp_path / "link.txt"
|
||||||
|
link.symlink_to(target)
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value=None)
|
||||||
|
|
||||||
|
managed: list[ManagedLink] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
seen_role = {str(link)}
|
||||||
|
|
||||||
|
result = _capture_link(
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(link),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=seen_role,
|
||||||
|
seen_global=None,
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
assert len(managed) == 0
|
||||||
|
|
||||||
|
def test_capture_link_seen_global(self, tmp_path):
|
||||||
|
"""Test link capture with seen_global deduplication."""
|
||||||
|
target = tmp_path / "target.txt"
|
||||||
|
target.write_text("content", encoding="utf-8")
|
||||||
|
link = tmp_path / "link.txt"
|
||||||
|
link.symlink_to(target)
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value=None)
|
||||||
|
|
||||||
|
managed: list[ManagedLink] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
seen_global = {str(link)}
|
||||||
|
|
||||||
|
result = _capture_link(
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(link),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=None,
|
||||||
|
seen_global=seen_global,
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
assert len(managed) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaptureFile:
|
||||||
|
"""Tests for _capture_file function"""
|
||||||
|
|
||||||
|
def test_capture_file_basic(self, tmp_path):
|
||||||
|
"""Test basic file capture."""
|
||||||
|
bundle = tmp_path / "bundle"
|
||||||
|
bundle.mkdir()
|
||||||
|
(bundle / "artifacts").mkdir()
|
||||||
|
|
||||||
|
source = tmp_path / "source.txt"
|
||||||
|
source.write_text("content", encoding="utf-8")
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value=None)
|
||||||
|
|
||||||
|
managed: list[ManagedFile] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
|
||||||
|
result = _capture_file(
|
||||||
|
bundle_dir=str(bundle),
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(source),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=set(),
|
||||||
|
seen_global=set(),
|
||||||
|
metadata=None,
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
assert len(managed) == 1
|
||||||
|
|
||||||
|
def test_capture_file_seen_role(self, tmp_path):
|
||||||
|
"""Test file capture with seen_role deduplication."""
|
||||||
|
bundle = tmp_path / "bundle"
|
||||||
|
bundle.mkdir()
|
||||||
|
|
||||||
|
source = tmp_path / "source.txt"
|
||||||
|
source.write_text("content", encoding="utf-8")
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value=None)
|
||||||
|
|
||||||
|
managed: list[ManagedFile] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
seen_role = {str(source)}
|
||||||
|
|
||||||
|
result = _capture_file(
|
||||||
|
bundle_dir=str(bundle),
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(source),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=seen_role,
|
||||||
|
seen_global=None,
|
||||||
|
metadata=None,
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
assert len(managed) == 0
|
||||||
|
|
||||||
|
def test_capture_file_seen_global(self, tmp_path):
|
||||||
|
"""Test file capture with seen_global deduplication."""
|
||||||
|
bundle = tmp_path / "bundle"
|
||||||
|
bundle.mkdir()
|
||||||
|
|
||||||
|
source = tmp_path / "source.txt"
|
||||||
|
source.write_text("content", encoding="utf-8")
|
||||||
|
|
||||||
|
policy = MagicMock(spec=IgnorePolicy)
|
||||||
|
policy.deny_reason_link = None
|
||||||
|
policy.deny_reason = MagicMock(return_value=None)
|
||||||
|
|
||||||
|
managed: list[ManagedFile] = []
|
||||||
|
excluded: list[ExcludedFile] = []
|
||||||
|
path_filter = PathFilter([], [])
|
||||||
|
seen_global = {str(source)}
|
||||||
|
|
||||||
|
result = _capture_file(
|
||||||
|
bundle_dir=str(bundle),
|
||||||
|
role_name="test_role",
|
||||||
|
abs_path=str(source),
|
||||||
|
reason="test",
|
||||||
|
policy=policy,
|
||||||
|
path_filter=path_filter,
|
||||||
|
managed_out=managed,
|
||||||
|
excluded_out=excluded,
|
||||||
|
seen_role=None,
|
||||||
|
seen_global=seen_global,
|
||||||
|
metadata=None,
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
assert len(managed) == 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue