2026-08-02 17:25:53 +08:00
|
|
|
import hashlib
|
2026-07-10 15:32:48 +08:00
|
|
|
import importlib.util
|
2026-08-02 17:25:53 +08:00
|
|
|
import json
|
2026-07-10 15:32:48 +08:00
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import types
|
|
|
|
|
|
2026-08-02 17:25:53 +08:00
|
|
|
import pytest
|
|
|
|
|
|
2026-07-10 15:32:48 +08:00
|
|
|
|
|
|
|
|
def load_installer():
|
|
|
|
|
script_path = os.path.join(os.path.dirname(__file__), "..", "install_feature.py")
|
|
|
|
|
spec = importlib.util.spec_from_file_location("install_feature_under_test", script_path)
|
|
|
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
|
assert spec.loader is not None
|
|
|
|
|
spec.loader.exec_module(module)
|
|
|
|
|
return module
|
|
|
|
|
|
|
|
|
|
|
2026-07-27 15:37:30 +08:00
|
|
|
def test_main_temporarily_lifts_hugging_face_offline_flags(monkeypatch):
|
|
|
|
|
"""An explicit bundle install stays online while runtime remains fail-closed."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
|
|
|
|
|
monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1")
|
|
|
|
|
observed = {}
|
|
|
|
|
|
|
|
|
|
def fake_install():
|
|
|
|
|
observed["hf"] = os.environ["HF_HUB_OFFLINE"]
|
|
|
|
|
observed["transformers"] = os.environ["TRANSFORMERS_OFFLINE"]
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "_install", fake_install)
|
|
|
|
|
|
|
|
|
|
installer.main()
|
|
|
|
|
|
|
|
|
|
assert observed == {"hf": "0", "transformers": "0"}
|
|
|
|
|
assert os.environ["HF_HUB_OFFLINE"] == "1"
|
|
|
|
|
assert os.environ["TRANSFORMERS_OFFLINE"] == "1"
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 15:32:48 +08:00
|
|
|
def test_download_with_hf_hub_uses_accelerated_client(monkeypatch, tmp_path):
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
downloaded = tmp_path / "hf-cache" / "bundle.tar.gz"
|
|
|
|
|
downloaded.parent.mkdir()
|
|
|
|
|
|
|
|
|
|
calls = {}
|
|
|
|
|
|
|
|
|
|
def fake_hf_hub_download(**kwargs):
|
|
|
|
|
calls.update(kwargs)
|
|
|
|
|
downloaded.write_bytes(b"archive")
|
|
|
|
|
return str(downloaded)
|
|
|
|
|
|
|
|
|
|
fake_module = types.ModuleType("huggingface_hub")
|
|
|
|
|
fake_module.hf_hub_download = fake_hf_hub_download
|
|
|
|
|
monkeypatch.setitem(sys.modules, "huggingface_hub", fake_module)
|
|
|
|
|
|
|
|
|
|
progress = []
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: progress.append((p, s)))
|
|
|
|
|
|
|
|
|
|
dest = tmp_path / "staging" / "object-eraser-colorize-amd64-gpu.tar.gz"
|
|
|
|
|
dest.parent.mkdir()
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
installer.download_with_hf_hub(
|
|
|
|
|
"snapotter/feature-bundles",
|
|
|
|
|
"v2.0.0/object-eraser-colorize-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
100,
|
|
|
|
|
2,
|
|
|
|
|
85,
|
|
|
|
|
)
|
|
|
|
|
is True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert dest.read_bytes() == b"archive"
|
|
|
|
|
assert calls["repo_id"] == "snapotter/feature-bundles"
|
|
|
|
|
assert calls["repo_type"] == "model"
|
|
|
|
|
assert calls["filename"] == "v2.0.0/object-eraser-colorize-amd64-gpu.tar.gz"
|
|
|
|
|
assert any("accelerated" in stage.lower() for _, stage in progress)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_download_with_hf_hub_cleans_cache_when_download_raises(monkeypatch, tmp_path):
|
|
|
|
|
"""A failed accelerated download must not leak its .cache staging tree onto
|
|
|
|
|
disk before the urllib fallback runs."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
staging = tmp_path / "staging"
|
|
|
|
|
staging.mkdir()
|
|
|
|
|
# Simulate a partial hf cache tree left behind by a failed transfer.
|
|
|
|
|
leaked_cache = staging / ".cache" / "huggingface" / "download"
|
|
|
|
|
leaked_cache.mkdir(parents=True)
|
|
|
|
|
(leaked_cache / "blob.incomplete").write_bytes(b"partial")
|
|
|
|
|
leaked_nested = staging / "v2.0.0"
|
|
|
|
|
leaked_nested.mkdir()
|
|
|
|
|
|
|
|
|
|
def fake_hf_hub_download(**_kwargs):
|
|
|
|
|
raise RuntimeError("xet CAS unreachable")
|
|
|
|
|
|
|
|
|
|
fake_module = types.ModuleType("huggingface_hub")
|
|
|
|
|
fake_module.hf_hub_download = fake_hf_hub_download
|
|
|
|
|
monkeypatch.setitem(sys.modules, "huggingface_hub", fake_module)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
dest = staging / "object-eraser-colorize-amd64-gpu.tar.gz"
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
installer.download_with_hf_hub(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/object-eraser-colorize-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
100,
|
|
|
|
|
2,
|
|
|
|
|
85,
|
|
|
|
|
)
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
# Both the .cache tree and the nested archive dir are reclaimed.
|
|
|
|
|
assert not (staging / ".cache").exists()
|
|
|
|
|
assert not (staging / "v2.0.0").exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ensure_hf_hub_noops_when_client_already_importable(monkeypatch, tmp_path):
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
fake_module = types.ModuleType("huggingface_hub")
|
|
|
|
|
monkeypatch.setitem(sys.modules, "huggingface_hub", fake_module)
|
|
|
|
|
|
|
|
|
|
ran = {"pip": False}
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
installer.subprocess, "run", lambda *a, **k: ran.__setitem__("pip", True)
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
installer.ensure_hf_hub(str(tmp_path))
|
|
|
|
|
assert ran["pip"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ensure_hf_hub_self_heals_missing_client(monkeypatch, tmp_path):
|
|
|
|
|
"""On a drifted venv where huggingface_hub is missing, ensure_hf_hub must
|
|
|
|
|
pip-install it into that venv rather than let the caller fall back to the
|
|
|
|
|
slow single-stream urllib downloader silently."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
monkeypatch.delitem(sys.modules, "huggingface_hub", raising=False)
|
|
|
|
|
|
|
|
|
|
# Make the huggingface_hub import fail deterministically so ensure_hf_hub
|
|
|
|
|
# takes its self-heal branch.
|
|
|
|
|
import builtins
|
|
|
|
|
|
|
|
|
|
real_import = builtins.__import__
|
|
|
|
|
|
|
|
|
|
def blocked_import(name, *a, **k):
|
|
|
|
|
if name == "huggingface_hub":
|
|
|
|
|
raise ImportError("No module named 'huggingface_hub'")
|
|
|
|
|
return real_import(name, *a, **k)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(builtins, "__import__", blocked_import)
|
|
|
|
|
|
|
|
|
|
venv = tmp_path / "venv"
|
|
|
|
|
(venv / "bin").mkdir(parents=True)
|
|
|
|
|
(venv / "bin" / "python3").write_text("")
|
|
|
|
|
|
|
|
|
|
pip_calls = []
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
installer.subprocess,
|
|
|
|
|
"run",
|
|
|
|
|
lambda cmd, **k: pip_calls.append(cmd) or types.SimpleNamespace(returncode=0),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
installer.ensure_hf_hub(str(venv))
|
|
|
|
|
|
|
|
|
|
assert len(pip_calls) == 1
|
|
|
|
|
cmd = pip_calls[0]
|
|
|
|
|
assert cmd[0] == str(venv / "bin" / "python3")
|
|
|
|
|
assert "install" in cmd
|
|
|
|
|
spec = next(part for part in cmd if part.startswith("huggingface-hub["))
|
|
|
|
|
assert "hf_xet" in spec
|
|
|
|
|
assert "hf_transfer" in spec
|
2026-08-02 17:25:53 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# -- download_and_verify: checksum-mismatch retry contract (issue #714) --
|
|
|
|
|
|
|
|
|
|
GOOD_BYTES = b"good archive bytes"
|
|
|
|
|
BAD_BYTES = b"corrupt archive bytes"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sha256(data: bytes) -> str:
|
|
|
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _last_error(capsys) -> str:
|
|
|
|
|
"""Parse the JSON error line fail() writes to stderr."""
|
|
|
|
|
err_lines = [line for line in capsys.readouterr().err.strip().splitlines() if line]
|
|
|
|
|
payload = json.loads(err_lines[-1])
|
|
|
|
|
return payload["error"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_download_and_verify_accepts_good_first_download(monkeypatch, tmp_path):
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
calls = {"hf": 0, "resume": 0}
|
|
|
|
|
|
|
|
|
|
def fake_hf(*args, **kwargs):
|
|
|
|
|
calls["hf"] += 1
|
|
|
|
|
with open(args[2], "wb") as f:
|
|
|
|
|
f.write(GOOD_BYTES)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def fake_resume(url, tar_path, *rest):
|
|
|
|
|
calls["resume"] += 1
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", fake_hf)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", fake_resume)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert calls == {"hf": 1, "resume": 0}
|
|
|
|
|
assert dest.read_bytes() == GOOD_BYTES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_checksum_mismatch_retries_with_sequential_downloader(monkeypatch, tmp_path):
|
|
|
|
|
"""A corrupt accelerated download must be retried over the plain sequential
|
|
|
|
|
downloader, not by re-running the transport that just produced bad bytes
|
|
|
|
|
(issue #714: hf_xet parallel writes corrupt on some bind mounts, so a
|
|
|
|
|
force_download re-run fails the same way forever)."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
calls = {"hf": 0, "resume": 0}
|
|
|
|
|
|
|
|
|
|
def fake_hf(*args, **kwargs):
|
|
|
|
|
calls["hf"] += 1
|
|
|
|
|
with open(args[2], "wb") as f:
|
|
|
|
|
f.write(BAD_BYTES)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def fake_resume(url, tar_path, *rest):
|
|
|
|
|
calls["resume"] += 1
|
|
|
|
|
assert not os.path.exists(tar_path), "corrupt file must be gone before retry"
|
|
|
|
|
with open(tar_path, "wb") as f:
|
|
|
|
|
f.write(GOOD_BYTES)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", fake_hf)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", fake_resume)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert calls["hf"] == 1, "accelerated client must not be re-run after a mismatch"
|
|
|
|
|
assert calls["resume"] == 1
|
|
|
|
|
assert dest.read_bytes() == GOOD_BYTES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mismatch_retry_discards_stale_resume_sidecars(monkeypatch, tmp_path):
|
|
|
|
|
"""A mismatch is positive evidence of corruption, so the retry must not
|
|
|
|
|
resume from .partial/.meta sidecars a previously killed run left behind
|
|
|
|
|
(welding stale bytes onto the fresh download would fail the checksum again
|
|
|
|
|
and misdiagnose the cause as the user's storage)."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
(tmp_path / "background-removal-amd64-gpu.tar.gz.partial").write_bytes(b"stale")
|
|
|
|
|
(tmp_path / "background-removal-amd64-gpu.tar.gz.meta").write_text(
|
|
|
|
|
'{"bytesDownloaded": 5}'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def fake_hf(*args, **kwargs):
|
|
|
|
|
with open(args[2], "wb") as f:
|
|
|
|
|
f.write(BAD_BYTES)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def fake_resume(url, tar_path, *rest):
|
|
|
|
|
assert not os.path.exists(tar_path + ".partial"), "stale .partial must be wiped"
|
|
|
|
|
assert not os.path.exists(tar_path + ".meta"), "stale .meta must be wiped"
|
|
|
|
|
with open(tar_path, "wb") as f:
|
|
|
|
|
f.write(GOOD_BYTES)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", fake_hf)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", fake_resume)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert dest.read_bytes() == GOOD_BYTES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hf_unavailable_falls_back_to_sequential_download(monkeypatch, tmp_path):
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
seen = {}
|
|
|
|
|
|
|
|
|
|
def fake_resume(url, tar_path, *rest):
|
|
|
|
|
seen["url"] = url
|
|
|
|
|
with open(tar_path, "wb") as f:
|
|
|
|
|
f.write(GOOD_BYTES)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", lambda *a, **k: False)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", fake_resume)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert seen["url"] == (
|
|
|
|
|
"https://huggingface.co/deepsafe/feature-bundles"
|
|
|
|
|
"/resolve/main/v2.0.0/background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
)
|
|
|
|
|
assert dest.read_bytes() == GOOD_BYTES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_first_download_failure_reports_manual_hint(monkeypatch, tmp_path, capsys):
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
|
|
|
|
|
def fake_resume(url, tar_path, *rest):
|
|
|
|
|
raise RuntimeError("Failed to download after 5 attempts: boom")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", lambda *a, **k: False)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", fake_resume)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
error = _last_error(capsys)
|
|
|
|
|
assert "Failed to download after 5 attempts: boom" in error
|
|
|
|
|
assert "resolve/main/v2.0.0/background-removal-amd64-gpu.tar.gz" in error
|
|
|
|
|
assert "Offline Import" in error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_retry_download_failure_keeps_mismatch_context(monkeypatch, tmp_path, capsys):
|
|
|
|
|
"""When the sequential re-download after a mismatch itself fails, the error
|
|
|
|
|
must still say a checksum mismatch started it (with the digests), so a
|
|
|
|
|
#714-class corruption is distinguishable from a plain flaky download."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
|
|
|
|
|
def fake_hf(*args, **kwargs):
|
|
|
|
|
with open(args[2], "wb") as f:
|
|
|
|
|
f.write(BAD_BYTES)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def fake_resume(url, tar_path, *rest):
|
|
|
|
|
raise RuntimeError("Failed to download after 5 attempts: boom")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", fake_hf)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", fake_resume)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
error = _last_error(capsys)
|
|
|
|
|
assert "Checksum mismatch" in error
|
|
|
|
|
assert _sha256(GOOD_BYTES) in error
|
|
|
|
|
assert _sha256(BAD_BYTES) in error
|
|
|
|
|
assert "Failed to download after 5 attempts: boom" in error
|
|
|
|
|
assert "Offline Import" in error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_verify_read_error_fails_with_json_error(monkeypatch, tmp_path, capsys):
|
|
|
|
|
"""EIO while hashing the multi-GB archive (a live possibility on the flaky
|
|
|
|
|
mounts this retry exists for) must surface as a JSON error frame the Node
|
|
|
|
|
bridge can parse, not a raw traceback."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
|
|
|
|
|
def fake_hf(*args, **kwargs):
|
|
|
|
|
with open(args[2], "wb") as f:
|
|
|
|
|
f.write(GOOD_BYTES)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def broken_hash(path):
|
|
|
|
|
raise OSError(5, "Input/output error")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", fake_hf)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", lambda *a: None)
|
|
|
|
|
monkeypatch.setattr(installer, "file_sha256", broken_hash)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
error = _last_error(capsys)
|
|
|
|
|
assert "Input/output error" in error
|
|
|
|
|
assert "Offline Import" in error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_checksum_mismatch_failure_reports_both_hashes(monkeypatch, tmp_path, capsys):
|
|
|
|
|
"""When both transports produce wrong bytes, the error must state the
|
|
|
|
|
expected AND actual digests and point at the offline-import workaround."""
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
dest = tmp_path / "background-removal-amd64-gpu.tar.gz"
|
|
|
|
|
|
|
|
|
|
def fake_hf(*args, **kwargs):
|
|
|
|
|
with open(args[2], "wb") as f:
|
|
|
|
|
f.write(BAD_BYTES)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def fake_resume(url, tar_path, *rest):
|
|
|
|
|
with open(tar_path, "wb") as f:
|
|
|
|
|
f.write(BAD_BYTES)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_hf_hub", fake_hf)
|
|
|
|
|
monkeypatch.setattr(installer, "download_with_resume", fake_resume)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
|
installer.download_and_verify(
|
|
|
|
|
"deepsafe/feature-bundles",
|
|
|
|
|
"v2.0.0/background-removal-amd64-gpu.tar.gz",
|
|
|
|
|
str(dest),
|
|
|
|
|
_sha256(GOOD_BYTES),
|
|
|
|
|
len(GOOD_BYTES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
error = _last_error(capsys)
|
|
|
|
|
assert _sha256(GOOD_BYTES) in error
|
|
|
|
|
assert _sha256(BAD_BYTES) in error
|
|
|
|
|
assert "Offline Import" in error
|
|
|
|
|
assert not dest.exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_local_bundle_checksum_error_reports_actual_hash(monkeypatch, tmp_path, capsys):
|
|
|
|
|
installer = load_installer()
|
|
|
|
|
archive_entry = {
|
|
|
|
|
"file": "v2.0.0/background-removal.tar.gz",
|
|
|
|
|
"sha256": _sha256(GOOD_BYTES),
|
|
|
|
|
"compressedSize": len(GOOD_BYTES),
|
|
|
|
|
"extractedSize": 0,
|
|
|
|
|
}
|
|
|
|
|
manifest_path = tmp_path / "manifest.json"
|
|
|
|
|
manifest_path.write_text(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"bundles": {
|
|
|
|
|
"background-removal": {
|
|
|
|
|
"archives": {
|
|
|
|
|
"amd64-gpu": archive_entry,
|
|
|
|
|
"arm64-cpu": archive_entry,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
models_dir = tmp_path / "ai" / "models"
|
|
|
|
|
models_dir.mkdir(parents=True)
|
|
|
|
|
local = tmp_path / "local.tar.gz"
|
|
|
|
|
local.write_bytes(BAD_BYTES)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setenv("SNAPOTTER_BUNDLE_LOCAL_PATH", str(local))
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
sys,
|
|
|
|
|
"argv",
|
|
|
|
|
["install_feature.py", "background-removal", str(manifest_path), str(models_dir)],
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(installer, "emit_progress", lambda p, s: None)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
|
installer._install()
|
|
|
|
|
|
|
|
|
|
error = _last_error(capsys)
|
|
|
|
|
assert _sha256(GOOD_BYTES) in error
|
|
|
|
|
assert _sha256(BAD_BYTES) in error
|