Ctrl K
ComunidadGuías…

tests/test_logo_provider.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import csv
import importlib.util
import io
import json
import tempfile
import unittest
from pathlib import Path

from PIL import Image, ImageDraw

from peach.images import bake_square
from peach.logo_provider import LogoCandidateCache, inspect_logo


ROOT = Path(__file__).resolve().parents[1]


def load_script():
    path = ROOT / "scripts" / "fetch_studio_avatar_candidates.py"
    spec = importlib.util.spec_from_file_location("fetch_studio_avatar_candidates_test", path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def png_bytes(color=(20, 40, 60), size=(400, 400)):
    buffer = io.BytesIO()
    Image.new("RGB", size, color).save(buffer, "PNG")
    return buffer.getvalue()


def pattern_png(kind="vertical", size=(400, 400)):
    image = Image.new("RGB", size, "white")
    draw = ImageDraw.Draw(image)
    if kind == "vertical":
        draw.rectangle((0, 0, size[0] // 2, size[1]), fill="black")
    else:
        draw.rectangle((0, 0, size[0], size[1] // 2), fill="black")
    buffer = io.BytesIO()
    image.save(buffer, "PNG")
    return buffer.getvalue()


class Response:
    def __init__(self, body, status=200):
        self.body = body
        self.status = status
        self.headers = {}


class FakeTransport:
    def __init__(self, images):
        self.images = list(images)
        self.calls = []

    def __call__(self, request, _timeout, _max_bytes):
        self.calls.append(request.url)
        if "unavatar.io" in request.url:
            return Response(json.dumps({"url": "https://pbs.example/logo.png"}).encode())
        return Response(self.images.pop(0))


class TwimgTransport:
    """unavatar 回缩略图地址,只有某一档真的存在——用来验证按档位下退。"""

    def __init__(self, resolved, available):
        self.resolved = resolved
        self.available = dict(available)
        self.calls = []

    def __call__(self, request, _timeout, _max_bytes):
        self.calls.append(request.url)
        if "unavatar.io" in request.url:
            return Response(json.dumps({"url": self.resolved}).encode())
        body = self.available.get(request.url)
        return Response(body) if body else Response(b"", 404)


class LogoProviderTests(unittest.TestCase):
    def test_content_cache_rejects_tampering(self):
        with tempfile.TemporaryDirectory() as tmp:
            cache = LogoCandidateCache(Path(tmp))
            body = png_bytes()
            raster = inspect_logo(body)
            path = cache.store("https://x/logo.png", body, raster)
            self.assertEqual(cache.lookup("https://x/logo.png"), body)
            path.write_bytes(b"tampered")
            self.assertIsNone(cache.lookup("https://x/logo.png"))


class StudioLogoScriptTests(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.root = Path(self.tmp.name)
        self.addCleanup(self.tmp.cleanup)
        self.module = load_script()
        self.source = self.root / "missing.csv"
        self.handles = self.root / "handles.csv"
        self.out = self.root / "studio-logo-candidate-test.csv"
        self.health = self.root / "studio-logo-health-test.csv"
        self.images = self.root / "studio-logos"
        self.cache = self.root / "cache"
        self.installed = self.root / "installed"
        self.installed.mkdir()
        self.source.write_text("studio\nStudio A\n", encoding="utf-8-sig")
        self.handles.write_text("studio,handle\nStudio A,studio_a\n", encoding="utf-8-sig")

    def args(self, *extra):
        return [
            "--input", str(self.source), "--output", str(self.out),
            "--handles", str(self.handles), "--image-dir", str(self.images),
            "--cache-dir", str(self.cache), "--installed-dir", str(self.installed),
            "--health", str(self.health), "--interval", "0", *extra,
        ]

    def rows(self):
        with self.out.open(encoding="utf-8-sig", newline="") as handle:
            return list(csv.DictReader(handle))

    def test_new_candidate_has_cache_provenance_hash_and_health(self):
        transport = FakeTransport([png_bytes()])
        self.assertEqual(self.module.main(self.args(), transport=transport), 0)
        row = self.rows()[0]
        self.assertEqual(row["content_state"], "new")
        self.assertEqual(row["accepted"], "True")
        self.assertEqual(len(row["sha256"]), 64)
        self.assertTrue((self.images / row["saved"]).is_file())
        self.assertTrue((self.cache / "evidence" / row["provenance_key"]).is_file())
        self.assertNotIn(str(self.root), row["saved"])
        with self.health.open(encoding="utf-8-sig", newline="") as handle:
            health = next(csv.DictReader(handle))
        self.assertEqual(health["new"], "1")
        self.assertEqual(health["errors"], "0")

    def test_same_installed_content_is_mechanical_unchanged(self):
        # 装着的那张也过 `bake_square`:目录里的文件都是这么来的,拿没归一的原图
        # 当在位图,比的就不是「同一张图换了尺寸」而是归一本身带来的差别。
        (self.installed / "Studio_A.img").write_bytes(
            bake_square(pattern_png(size=(400, 400))))
        refreshed = pattern_png(size=(200, 200))
        self.assertEqual(self.module.main(self.args(), transport=FakeTransport([refreshed])), 0)
        row = self.rows()[0]
        self.assertEqual(row["content_state"], "unchanged")
        self.assertEqual(row["accepted"], "False")
        self.assertNotEqual(row["sha256"], "")
        self.assertLessEqual(int(row["visual_distance"]), 4)

    def test_japanese_studios_get_their_own_file_names(self):
        """只留 ASCII 时这两家同为 `_____`,同一批里后取的那张盖掉先取的。"""
        from peach.previews import logo_key

        names = [self.module.safe_name(studio) for studio in ("セレブの友", "親父の個撮")]
        self.assertEqual(names, [logo_key("セレブの友"), logo_key("親父の個撮")])
        self.assertNotEqual(names[0], names[1])

    def test_an_installed_japanese_studio_mark_is_recognised(self):
        """已装标识按 `logo_key` 落盘;候选拿另一套名字去比,同一张图也会被当成新图。"""
        self.source.write_text("studio\nセレブの友\n", encoding="utf-8-sig")
        self.handles.write_text("studio,handle\nセレブの友,webhakusui\n", encoding="utf-8-sig")
        (self.installed / "セレブの友.img").write_bytes(
            bake_square(pattern_png(size=(400, 400))))
        refreshed = pattern_png(size=(200, 200))
        self.assertEqual(self.module.main(self.args(), transport=FakeTransport([refreshed])), 0)
        row = self.rows()[0]
        self.assertEqual(row["saved"].rsplit(".", 1)[0], "セレブの友")
        self.assertEqual(row["content_state"], "unchanged")

    def test_upload_original_is_preferred_over_the_resolver_thumbnail(self):
        """unavatar 会给缩小档:セレブの友 拿到的是 200×200,原图其实 242×242。"""
        stem = "https://pbs.twimg.com/profile_images/562462783950700545/abc"
        original = png_bytes(size=(242, 242))
        transport = TwimgTransport(f"{stem}_200x200.jpg", {
            f"{stem}.jpg": original,
            f"{stem}_400x400.jpg": png_bytes(size=(400, 400)),
            f"{stem}_200x200.jpg": png_bytes(size=(200, 200)),
        })
        self.assertEqual(self.module.main(self.args(), transport=transport), 0)
        row = self.rows()[0]
        self.assertEqual(row["resolved_url"], f"{stem}.jpg")
        self.assertEqual((row["width"], row["height"]), ("242", "242"))
        self.assertEqual(transport.calls[1], f"{stem}.jpg")

    def test_missing_original_falls_back_to_the_next_tier(self):
        """原图那一份不是每个账号都还在,缺了就退一档,不是整个厂牌取图失败。"""
        stem = "https://pbs.twimg.com/profile_images/562462783950700545/abc"
        transport = TwimgTransport(f"{stem}_400x400.jpg", {
            f"{stem}_200x200.jpg": png_bytes(size=(200, 200)),
        })
        self.assertEqual(self.module.main(self.args(), transport=transport), 0)
        row = self.rows()[0]
        self.assertEqual(row["resolved_url"], f"{stem}_200x200.jpg")
        self.assertEqual(row["content_state"], "new")
        self.assertEqual(row["accepted"], "True")
        self.assertEqual(transport.calls[1:], [
            f"{stem}.jpg", f"{stem}_400x400.jpg", f"{stem}_200x200.jpg",
        ])

    def test_every_tier_missing_is_an_error_not_a_silent_pass(self):
        stem = "https://pbs.twimg.com/profile_images/562462783950700545/abc"
        transport = TwimgTransport(f"{stem}_200x200.jpg", {})
        self.assertEqual(self.module.main(self.args(), transport=transport), 0)
        row = self.rows()[0]
        self.assertEqual(row["content_state"], "error")
        self.assertEqual(row["saved"], "")
        self.assertEqual(row["accepted"], "False")
        with self.health.open(encoding="utf-8-sig", newline="") as handle:
            health = next(csv.DictReader(handle))
        self.assertEqual(health["errors"], "1")
        self.assertEqual(health["last_error_message"], "HTTP 404")

    def test_refresh_surfaces_changed_upstream_content(self):
        (self.installed / "Studio_A.img").write_bytes(pattern_png("vertical"))
        fresh = pattern_png("horizontal")
        self.assertEqual(
            self.module.main(self.args("--refresh"), transport=FakeTransport([fresh])), 0,
        )
        row = self.rows()[0]
        self.assertEqual(row["content_state"], "changed")
        self.assertEqual(row["accepted"], "True")


if __name__ == "__main__":
    unittest.main()