9f278b9a5c
* Add target list CLI option * Handle target list comments and encoding errors
250 lines
8.4 KiB
Python
250 lines
8.4 KiB
Python
"""Tests for local-source sizing and ``--mount`` target helpers in interface.utils."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from strix.interface.utils import (
|
|
build_mount_targets_info,
|
|
collect_local_sources,
|
|
dedupe_local_targets,
|
|
directory_size_bytes,
|
|
find_oversized_local_targets,
|
|
read_target_list_file,
|
|
)
|
|
|
|
|
|
def _write_file(path: Path, size: int) -> None:
|
|
path.write_bytes(b"x" * size)
|
|
|
|
|
|
def _local_target(target_path: str, *, mount: bool = False) -> dict[str, Any]:
|
|
details: dict[str, Any] = {"target_path": target_path, "workspace_subdir": "repo"}
|
|
if mount:
|
|
details["mount"] = True
|
|
return {"type": "local_code", "details": details, "original": target_path}
|
|
|
|
|
|
def test_directory_size_empty_dir_is_zero(tmp_path: Path) -> None:
|
|
assert directory_size_bytes(tmp_path) == 0
|
|
|
|
|
|
def test_directory_size_sums_flat_and_nested_files(tmp_path: Path) -> None:
|
|
_write_file(tmp_path / "a.txt", 100)
|
|
nested = tmp_path / "sub" / "deep"
|
|
nested.mkdir(parents=True)
|
|
_write_file(nested / "b.txt", 250)
|
|
assert directory_size_bytes(tmp_path) == 350
|
|
|
|
|
|
def test_directory_size_skips_symlinks(tmp_path: Path) -> None:
|
|
_write_file(tmp_path / "real.txt", 100)
|
|
(tmp_path / "link.txt").symlink_to(tmp_path / "real.txt")
|
|
# The symlink target is counted once via the real file, not doubled.
|
|
assert directory_size_bytes(tmp_path) == 100
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="relies on POSIX permissions")
|
|
def test_directory_size_logs_and_skips_unreadable_subdir(
|
|
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
|
pytest.skip("root bypasses directory permissions")
|
|
_write_file(tmp_path / "top.txt", 100)
|
|
locked = tmp_path / "locked"
|
|
locked.mkdir()
|
|
_write_file(locked / "secret.bin", 9999)
|
|
locked.chmod(0o000)
|
|
try:
|
|
with caplog.at_level(logging.WARNING):
|
|
size = directory_size_bytes(tmp_path)
|
|
finally:
|
|
locked.chmod(0o755)
|
|
# The unreadable subtree is excluded (not silently treated as readable) and
|
|
# the omission is logged rather than vanishing without a trace.
|
|
assert size == 100
|
|
assert any("Could not read" in record.message for record in caplog.records)
|
|
|
|
|
|
def test_find_oversized_returns_nothing_under_limit(tmp_path: Path) -> None:
|
|
_write_file(tmp_path / "a.txt", 100)
|
|
targets = [_local_target(str(tmp_path))]
|
|
assert find_oversized_local_targets(targets, max_bytes=1000) == []
|
|
|
|
|
|
def test_find_oversized_returns_target_over_limit(tmp_path: Path) -> None:
|
|
_write_file(tmp_path / "big.bin", 500)
|
|
targets = [_local_target(str(tmp_path))]
|
|
result = find_oversized_local_targets(targets, max_bytes=100)
|
|
assert result == [(str(tmp_path), 500)]
|
|
|
|
|
|
def test_find_oversized_ignores_mounted_targets(tmp_path: Path) -> None:
|
|
_write_file(tmp_path / "big.bin", 500)
|
|
targets = [_local_target(str(tmp_path), mount=True)]
|
|
assert find_oversized_local_targets(targets, max_bytes=100) == []
|
|
|
|
|
|
def test_find_oversized_ignores_non_local_targets() -> None:
|
|
targets = [{"type": "web_application", "details": {"target_url": "https://x"}}]
|
|
assert find_oversized_local_targets(targets, max_bytes=1) == []
|
|
|
|
|
|
@pytest.mark.parametrize("disabled", [0, -1])
|
|
def test_find_oversized_disabled_for_non_positive_limit(tmp_path: Path, disabled: int) -> None:
|
|
_write_file(tmp_path / "big.bin", 500)
|
|
targets = [_local_target(str(tmp_path))]
|
|
assert find_oversized_local_targets(targets, max_bytes=disabled) == []
|
|
|
|
|
|
def test_collect_local_sources_propagates_mount_flag() -> None:
|
|
copied = _local_target("/copied")
|
|
copied["details"]["workspace_subdir"] = "copied"
|
|
mounted = _local_target("/mounted", mount=True)
|
|
mounted["details"]["workspace_subdir"] = "mounted"
|
|
|
|
sources = collect_local_sources([copied, mounted])
|
|
|
|
by_path = {s["source_path"]: s for s in sources}
|
|
assert by_path["/copied"]["mount"] is False
|
|
assert by_path["/mounted"]["mount"] is True
|
|
|
|
|
|
def test_collect_local_sources_repository_is_never_mounted() -> None:
|
|
repo = {
|
|
"type": "repository",
|
|
"details": {"cloned_repo_path": "/clone", "workspace_subdir": "clone"},
|
|
}
|
|
sources = collect_local_sources([repo])
|
|
assert sources == [{"source_path": "/clone", "workspace_subdir": "clone", "mount": False}]
|
|
|
|
|
|
def test_build_mount_targets_info_for_valid_dir(tmp_path: Path) -> None:
|
|
result = build_mount_targets_info([str(tmp_path)])
|
|
assert len(result) == 1
|
|
entry = result[0]
|
|
assert entry["type"] == "local_code"
|
|
assert entry["details"]["mount"] is True
|
|
assert entry["details"]["target_path"] == str(tmp_path.resolve())
|
|
|
|
|
|
def test_build_mount_targets_info_rejects_missing_path(tmp_path: Path) -> None:
|
|
missing = tmp_path / "does-not-exist"
|
|
with pytest.raises(ValueError, match="not an existing directory"):
|
|
build_mount_targets_info([str(missing)])
|
|
|
|
|
|
def test_build_mount_targets_info_rejects_file(tmp_path: Path) -> None:
|
|
file_path = tmp_path / "a-file.txt"
|
|
_write_file(file_path, 10)
|
|
with pytest.raises(ValueError, match="not an existing directory"):
|
|
build_mount_targets_info([str(file_path)])
|
|
|
|
|
|
@pytest.mark.parametrize("empty", ["", " "])
|
|
def test_build_mount_targets_info_rejects_empty_path(empty: str) -> None:
|
|
# An empty path would otherwise resolve to the current working directory
|
|
# and silently bind-mount it into the sandbox.
|
|
with pytest.raises(ValueError, match="must not be empty"):
|
|
build_mount_targets_info([empty])
|
|
|
|
|
|
def test_read_target_list_file_strips_blank_lines(tmp_path: Path) -> None:
|
|
target_list = tmp_path / "targets.txt"
|
|
target_list.write_text(
|
|
"\n"
|
|
" https://test1.com/ \n"
|
|
"\n"
|
|
"http://test2.com:5789/\n"
|
|
" \n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert read_target_list_file(str(target_list)) == [
|
|
"https://test1.com/",
|
|
"http://test2.com:5789/",
|
|
]
|
|
|
|
|
|
def test_read_target_list_file_ignores_comment_lines(tmp_path: Path) -> None:
|
|
target_list = tmp_path / "targets.txt"
|
|
target_list.write_text(
|
|
"# production targets\n"
|
|
"https://test1.com/\n"
|
|
" # staging targets\n"
|
|
"http://test2.com:5789/\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert read_target_list_file(str(target_list)) == [
|
|
"https://test1.com/",
|
|
"http://test2.com:5789/",
|
|
]
|
|
|
|
|
|
def test_read_target_list_file_rejects_empty_file(tmp_path: Path) -> None:
|
|
target_list = tmp_path / "targets.txt"
|
|
target_list.write_text(" \n# no targets yet\n\n", encoding="utf-8")
|
|
|
|
with pytest.raises(ValueError, match="is empty"):
|
|
read_target_list_file(str(target_list))
|
|
|
|
|
|
def test_read_target_list_file_rejects_missing_path(tmp_path: Path) -> None:
|
|
with pytest.raises(ValueError, match="not an existing file"):
|
|
read_target_list_file(str(tmp_path / "missing.txt"))
|
|
|
|
|
|
def test_read_target_list_file_rejects_non_utf8_file(tmp_path: Path) -> None:
|
|
target_list = tmp_path / "targets.txt"
|
|
target_list.write_bytes(b"https://test1.com/\xff\n")
|
|
|
|
with pytest.raises(ValueError, match="must be valid UTF-8 text"):
|
|
read_target_list_file(str(target_list))
|
|
|
|
|
|
@pytest.mark.parametrize("empty", ["", " "])
|
|
def test_read_target_list_file_rejects_empty_path(empty: str) -> None:
|
|
with pytest.raises(ValueError, match="must not be empty"):
|
|
read_target_list_file(empty)
|
|
|
|
|
|
def test_dedupe_keeps_distinct_targets_in_order() -> None:
|
|
targets = [
|
|
_local_target("/a"),
|
|
{"type": "web_application", "details": {"target_url": "https://x"}},
|
|
_local_target("/b", mount=True),
|
|
]
|
|
assert dedupe_local_targets(targets) == targets
|
|
|
|
|
|
def test_dedupe_mount_supersedes_copied_same_path() -> None:
|
|
copied = _local_target("/repo")
|
|
mounted = _local_target("/repo", mount=True)
|
|
|
|
# Copied first, then mounted: the single surviving entry is the mount.
|
|
result = dedupe_local_targets([copied, mounted])
|
|
assert len(result) == 1
|
|
assert result[0]["details"]["mount"] is True
|
|
|
|
# Order-independent: mounted first, copied second also yields the mount.
|
|
result_rev = dedupe_local_targets([mounted, copied])
|
|
assert len(result_rev) == 1
|
|
assert result_rev[0]["details"]["mount"] is True
|
|
|
|
|
|
def test_dedupe_collapses_duplicate_mounts() -> None:
|
|
result = dedupe_local_targets(
|
|
[_local_target("/repo", mount=True), _local_target("/repo", mount=True)]
|
|
)
|
|
assert len(result) == 1
|