- app.py 各接口 Form 默认值与说明、hairline/service.py _REDRAW_PROMPT、 hairline/redraw.py _DEFAULT_PROMPT、face_analysis/hairline_grow.py 兜底值 - add_hair.json / add_hair2.json / 0716add-hair-api.json / 0716add-hair.json / hair_repaint.json 工作流节点60内置文本 - static 测试页、local_test、benchmark 脚本、接口文档同步 注意:新提示词首次请求会因 CLIP 缓存失效变慢一次,之后恢复
110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
"""gateway.reqlog 单测:multipart 入参解析(标量 + 图片存盘转 URL)+ 出参摘要。
|
||
|
||
不依赖网络 / GPU,纯函数级。
|
||
"""
|
||
|
||
import base64
|
||
import json
|
||
|
||
from gateway.reqlog import extract_form_params, summarize_for_log
|
||
|
||
# 1x1 透明 PNG(合法 magic byte)
|
||
_PNG = (
|
||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01"
|
||
b"\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
|
||
)
|
||
|
||
|
||
def _multipart(fields, files, boundary=b"----testbnd"):
|
||
"""手搓 multipart/form-data body。fields: {name:str}; files: {name:(fname,ctype,bytes)}."""
|
||
parts = []
|
||
for name, val in fields.items():
|
||
parts.append(b"--" + boundary + b"\r\n")
|
||
parts.append(b'Content-Disposition: form-data; name="' + name.encode() + b'"\r\n\r\n')
|
||
parts.append(val.encode("utf-8") + b"\r\n")
|
||
for name, (fname, ctype, data) in files.items():
|
||
parts.append(b"--" + boundary + b"\r\n")
|
||
parts.append(b'Content-Disposition: form-data; name="' + name.encode() + b'"; filename="'
|
||
+ fname.encode() + b'"\r\n')
|
||
parts.append(b"Content-Type: " + ctype.encode() + b"\r\n\r\n")
|
||
parts.append(data + b"\r\n")
|
||
parts.append(b"--" + boundary + b"--\r\n")
|
||
return b"".join(parts)
|
||
|
||
|
||
def test_extract_scalars_and_images(tmp_path):
|
||
b64 = "data:image/png;base64," + base64.b64encode(_PNG).decode()
|
||
body = _multipart(
|
||
fields={"gender": "female", "hair_style": "1,3", "use_mask": "True",
|
||
"prompt": "填充遮罩区域的头发,人物增加一点磨皮", "image_base64": b64},
|
||
files={"image_file": ("x.png", "image/png", _PNG)},
|
||
)
|
||
ct = "multipart/form-data; boundary=----testbnd"
|
||
|
||
res = extract_form_params(ct, body, str(tmp_path), "https://test.local")
|
||
|
||
# 标量原样
|
||
assert res["gender"] == "female"
|
||
assert res["hair_style"] == "1,3"
|
||
assert res["use_mask"] == "True"
|
||
|
||
# 图片字段是元信息 dict(不是 base64 文本)
|
||
assert res["image_file"]["_image"] is True
|
||
assert res["image_file"]["source"] == "file"
|
||
assert res["image_file"]["filename"] == "x.png"
|
||
assert res["image_file"]["url"] is not None
|
||
assert res["image_base64"]["_image"] is True
|
||
assert res["image_base64"]["source"] == "base64"
|
||
assert res["image_base64"]["url"] is not None
|
||
|
||
# 图片已落盘
|
||
saved = [p.name for p in tmp_path.iterdir()]
|
||
assert any(n.startswith("in_") and n.endswith(".png") for n in saved)
|
||
# URL 指向正确路径
|
||
assert res["image_file"]["url"].startswith("https://test.local/static/annotations/in_")
|
||
|
||
# 关键:日志里绝不包含 base64 文本
|
||
blob = json.dumps(res, ensure_ascii=False)
|
||
assert b64.split(",", 1)[1][:40] not in blob
|
||
|
||
|
||
def test_extract_non_multipart(tmp_path):
|
||
res = extract_form_params("application/json", b'{"a":1}', str(tmp_path), "https://t.local")
|
||
assert res == {"_content_type": "application/json", "_body_bytes": 7}
|
||
|
||
|
||
def test_extract_empty_content_type(tmp_path):
|
||
res = extract_form_params("", b"hello", str(tmp_path), "https://t.local")
|
||
assert res["_body_bytes"] == 5
|
||
|
||
|
||
def test_summarize_truncates_long_values():
|
||
big = {
|
||
"landmarks": [[i, i] for i in range(500)],
|
||
"name": "x" * 1000,
|
||
"deep": {"a": {"b": {"c": {"d": {"e": {"f": {"g": 1}}}}}}},
|
||
}
|
||
sm = summarize_for_log(big)
|
||
# 长数组被截断 + 标注剩余数
|
||
assert isinstance(sm["landmarks"], list)
|
||
assert sm["landmarks"][-1].startswith("…(+")
|
||
assert len(sm["landmarks"]) == 6 # 5 项 + 1 个标注
|
||
# 长字符串被截断
|
||
assert "…(+" in sm["name"]
|
||
# 总体积受控
|
||
assert len(json.dumps(sm, ensure_ascii=False).encode()) <= 8192
|
||
|
||
|
||
def test_summarize_keeps_small_intact():
|
||
obj = {"code": 0, "data": {"hairline_images": [{"order": 1, "image_middle_url": "https://x/y.png"}]}}
|
||
assert summarize_for_log(obj) == obj
|
||
|
||
|
||
def test_summarize_byte_cap_stub():
|
||
# 构造大量小字段触发字节上限
|
||
obj = {f"k{i}": {f"j{j}": "abcdefghij" for j in range(50)} for i in range(50)}
|
||
sm = summarize_for_log(obj, max_bytes=2048)
|
||
assert isinstance(sm, dict) and sm.get("_truncated") is True
|
||
assert "size_bytes" in sm
|