Files
DemonGatanjieu-Anomalous_Mo…/tests/test_scan_retry.py
T
DemonGatanjieuandClaude Opus 5.5 7b09909e5b test: publish the plugin's tests, one command to run them
tests/ (Python backend tests, .mjs frontend rule tests) is no longer
ignored. tools/run_tests.mjs runs the structure check, every
tests/*.mjs and the Python tests with $PYTHON, the portable
python_embeded or python. docs/guides/testing.md says how to run and
write them; README links it. Developer notes no longer point at 18 test
files that were removed earlier (AGENTS.md table, backend, frontend and
update-guide notes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:47:35 +08:00

102 lines
4.6 KiB
Python

import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
PLUGIN_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PLUGIN_DIR.parents[1]))
sys.path.insert(0, str(PLUGIN_DIR))
import scraper
from api import scan_summary, scanner
from model_identity import computed_file_identity
DIGEST = "a" * 64
def write_info(base, data):
Path(base + ".info").write_text(json.dumps(data), encoding="utf-8")
class ScanRetryTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
root = Path(self.temp.name)
(root / "sub").mkdir()
self.unmatched = root / "sub" / "unmatched.safetensors"
self.matched = root / "matched.safetensors"
self.new = root / "new.safetensors"
for path in (self.unmatched, self.matched, self.new):
path.write_bytes(b"model bytes")
write_info(str(self.unmatched)[:-12], {
"id": -1, "modelId": -1, "name": "unmatched", "files": [{"hashes": {"SHA256": DIGEST}}],
"anomalous_file_identity": computed_file_identity(str(self.unmatched), DIGEST),
})
write_info(str(self.matched)[:-12], {"id": 5, "modelId": 2, "name": "v1"})
Path(str(self.matched)[:-12] + ".preview.png").write_bytes(b"png")
def tearDown(self):
self.temp.cleanup()
def run_scraper(self, *flags):
fetched = []
found = {"id": 7, "modelId": 3, "name": "v2", "model": {"name": "Found"}, "images": []}
argv = ["scraper.py", self.temp.name, *flags]
with mock.patch.object(sys, "argv", argv), \
mock.patch.object(scraper, "fetch_civitai_info", side_effect=lambda h: fetched.append(h) or dict(found)), \
mock.patch.object(scraper, "calculate_sha256", side_effect=AssertionError("hash recomputed")), \
mock.patch.object(scraper.urllib.request, "urlopen", side_effect=OSError("offline test")):
scraper.main()
return fetched
def test_retry_refetches_only_unmatched_models_with_the_saved_hash(self):
self.assertEqual(scraper.count_scan_files(self.temp.name, [], True), 1)
fetched = self.run_scraper("--retry-unmatched", "--virtual-rename")
self.assertEqual(fetched, [DIGEST])
info = json.loads(Path(str(self.unmatched)[:-12] + ".info").read_text(encoding="utf-8"))
self.assertEqual(info["id"], 7)
self.assertEqual(json.loads(Path(str(self.matched)[:-12] + ".info").read_text(encoding="utf-8"))["id"], 5)
self.assertFalse(Path(str(self.new)[:-12] + ".info").exists())
def test_changed_file_is_hashed_again(self):
self.unmatched.write_bytes(b"other bytes, other size")
with mock.patch.object(scraper, "write_report") as report:
self.run_scraper("--retry-unmatched")
failed = [call.kwargs for call in report.call_args_list if call.kwargs.get("status") == "failed"]
self.assertEqual([entry["error"] for entry in failed], ["hash recomputed"])
def test_summary_counts_each_kind_once(self):
with mock.patch.object(scan_summary, "get_active_scan_paths", return_value=[self.temp.name, self.temp.name]):
summary = scan_summary._summarize()
counts = {key: summary[key] for key in ("total", "matched", "unmatched", "new")}
self.assertEqual(counts, {"total": 3, "matched": 1, "unmatched": 1, "new": 1})
def test_summary_lists_model_files_in_other_formats_outside_hidden_folders(self):
root = Path(self.temp.name)
(root / "voice").mkdir()
(root / "voice" / "speaker.pth").write_bytes(b"x")
(root / ".staging").mkdir()
(root / ".staging" / "half.ckpt").write_bytes(b"x")
(root / "notes.txt").write_bytes(b"x")
with mock.patch.object(scan_summary, "get_active_scan_paths", return_value=[self.temp.name]):
summary = scan_summary._summarize()
self.assertEqual(summary["total"], 3)
self.assertEqual(summary["skipped"], 1)
self.assertEqual(summary["skipped_models"][0]["rel"], "voice/speaker.pth")
def test_both_routes_map_request_fields_to_the_same_switches(self):
data = {"offline_only": True, "virtual_rename": True, "retry_unmatched": True, "physical_rename": True}
self.assertEqual(
scanner._scraper_flags(data, False, False),
["--offline-only", "--virtual-rename", "--retry-unmatched"],
)
self.assertIn("--skip-rename", scanner._scraper_flags({}, True, True))
self.assertIn("--physical-rename", scanner._scraper_flags({"physical_rename": True}, False, True))
if __name__ == "__main__":
unittest.main()