105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
"""角色卡解析器单元测试。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import io
|
|
import json
|
|
import struct
|
|
import zipfile
|
|
import zlib
|
|
|
|
from app.modules.imports.card_parser import parse_character_card
|
|
|
|
|
|
def _make_png_text(keyword: str, text: str) -> bytes:
|
|
"""构造含一个 tEXt chunk 的最小 PNG。"""
|
|
|
|
def chunk(ctype: bytes, data: bytes) -> bytes:
|
|
body = ctype + data
|
|
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)
|
|
|
|
payload = keyword.encode("latin-1") + b"\x00" + text.encode("latin-1")
|
|
return b"\x89PNG\r\n\x1a\n" + chunk(b"tEXt", payload) + chunk(b"IEND", b"")
|
|
|
|
|
|
def test_parse_v2_json() -> None:
|
|
card = {"spec": "chara_card_v2", "spec_version": "2.0", "data": {"name": "狂三", "description": "desc", "personality": "腹黑"}}
|
|
parsed = parse_character_card(json.dumps(card).encode("utf-8"))
|
|
assert parsed.name == "狂三"
|
|
assert parsed.personality == "腹黑"
|
|
assert parsed.spec_version == "2.0"
|
|
|
|
|
|
def test_parse_v1_flat_normalized() -> None:
|
|
card = {"name": "V1角色", "description": "d", "personality": "p"}
|
|
parsed = parse_character_card(json.dumps(card).encode("utf-8"))
|
|
assert parsed.name == "V1角色"
|
|
assert parsed.spec_version == "1"
|
|
|
|
|
|
def test_parse_png_chara_chunk() -> None:
|
|
card = {"spec": "chara_card_v2", "spec_version": "2.0", "data": {"name": "PNG卡"}}
|
|
text = base64.b64encode(json.dumps(card).encode("utf-8")).decode("ascii")
|
|
parsed = parse_character_card(_make_png_text("chara", text))
|
|
assert parsed.name == "PNG卡"
|
|
|
|
|
|
def test_parse_png_ccv3_preferred_over_chara() -> None:
|
|
# 同时含 chara(V2) 和 ccv3(V3),优先 ccv3
|
|
card_v3 = {"spec_version": "3.0", "data": {"name": "V3卡", "nickname": "三三"}}
|
|
|
|
def chunk(ctype: bytes, data: bytes) -> bytes:
|
|
body = ctype + data
|
|
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)
|
|
|
|
v3_text = base64.b64encode(json.dumps(card_v3).encode()).decode()
|
|
payload_chara = b"chara\x00" + base64.b64encode(json.dumps({"data": {"name": "旧"}}).encode())
|
|
payload_ccv3 = b"ccv3\x00" + v3_text.encode("latin-1")
|
|
png = b"\x89PNG\r\n\x1a\n" + chunk(b"tEXt", payload_chara) + chunk(b"tEXt", payload_ccv3) + chunk(b"IEND", b"")
|
|
parsed = parse_character_card(png)
|
|
assert parsed.name == "V3卡"
|
|
assert parsed.nickname == "三三"
|
|
|
|
|
|
def test_parse_charx_zip() -> None:
|
|
card = {"spec_version": "3.0", "data": {"name": "CHARX卡"}}
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
zf.writestr("card.json", json.dumps(card))
|
|
parsed = parse_character_card(buf.getvalue())
|
|
assert parsed.name == "CHARX卡"
|
|
|
|
|
|
def test_character_book_preserved() -> None:
|
|
card = {
|
|
"spec": "chara_card_v2",
|
|
"spec_version": "2.0",
|
|
"data": {
|
|
"name": "x",
|
|
"character_book": {"entries": [{"keys": ["a"], "content": "b"}]},
|
|
},
|
|
}
|
|
parsed = parse_character_card(json.dumps(card).encode())
|
|
assert parsed.character_book is not None
|
|
assert parsed.character_book["entries"][0]["keys"] == ["a"]
|
|
|
|
|
|
def test_alternate_greetings_and_tags() -> None:
|
|
card = {"spec_version": "2.0", "data": {"name": "x", "alternate_greetings": ["hi", "yo"], "tags": ["anime"]}}
|
|
parsed = parse_character_card(json.dumps(card).encode())
|
|
assert parsed.alternate_greetings == ["hi", "yo"]
|
|
assert parsed.tags == ["anime"]
|
|
|
|
|
|
def test_png_without_metadata_raises() -> None:
|
|
import pytest
|
|
|
|
def chunk(ctype: bytes, data: bytes) -> bytes:
|
|
body = ctype + data
|
|
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)
|
|
|
|
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IEND", b"")
|
|
with pytest.raises(ValueError):
|
|
parse_character_card(png)
|