Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_font_tier.py
T
MeowOS 3fd4726198 feat(launcher): 字体扫描迁移三级目录 (agent-193) — <family>/<weight>/<tier>/ 支持(两级向后兼容); FONT_PRESETS_MAX 17→64; 设置页 family→weight 两级不变,同weight多tier按加载大字库开关选档(关=common优先/开=gbk优先); index 落盘含三级条目
launcher ab8e610f→d3cae88b; test_font_tier/test_font_weight PASS
已知缺口: SDK meow_font.c MEOW_FONT_INDEX_MAX=32 截断(>32套需同步提); S_NET_MAX=8 family 上限; default 字体仍两级
2026-08-31 05:09:42 +08:00

199 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_font_tier.py — MeowOS 字体三级(FONT_SYSTEM 定案)宿主导出单测。
验证 launcher 侧 font 扫描对三级结构的处理(FONT_SYSTEM.md §1/§4.5):
(1) 三级扫描:<卡根>/MeowOS/fonts/<family>/<weight>/<tier>/(tier=common/gbk/full)→ 每
(family,weight,tier) 一套,值 = MeowOS/fonts/<family>/<weight>/<tier>/font.png,tier=目录名。
(2) 两级向后兼容:<family>/<weight>/font.png → 按旧口径,tier 读 font.json charset。
(3) 同 (family,weight) 下 weight 根有 font.png → 按两级(不另扫 tier 子目录)。
(4) index 落盘:<root>/MeowOS/config/fonts/index 每套一条 6 字段
"family","weight","path","label","tier",glyph_count;三级条目含三级 path + tier 列。
(5) 缺件(漏 font.idx 等)子目录 → 该套跳过。
方法:构造假卡根(两/三级混合 + 缺件 + 非法目录),用 qemu-mipsel 跑真实
meow_launcherd --fontscan <root>,断言 stdout preset 列表与 index 文件。
用法(在 patch_v2/meow_launcherd 下):
先 build_launcher.sh 生成 meow_launcherd(mipsel 静态 ELF),再:
python3 test_font_tier.py
依赖 qemu-mipsel(qemu-user)。退出码 0=全过,非 0=断言失败。
"""
import csv
import json
import os
import shutil
import subprocess
import sys
import tempfile
ROOT = os.path.dirname(os.path.abspath(__file__))
BIN = os.path.join(ROOT, "meow_launcherd")
FIX_ROOT = tempfile.mkdtemp(prefix="mftier_", dir="/tmp")
FONTS = os.path.join(FIX_ROOT, "MeowOS", "fonts")
def mk_font(rel, family, name, weight, tier, gcount):
"""写一套 <family>/<weight>[/<tier>]/font.png+font.idx+font.json(合法三件套)。"""
d = os.path.join(FONTS, rel)
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, "font.png"), "wb") as f:
f.write(b"\x89PNG\r\n\x1a\nPLACEHOLDER")
with open(os.path.join(d, "font.idx"), "wb") as f:
f.write(b"MFNI\x02\x00PLACEHOLDER")
meta = {
"family": family, "name": name, "weight": weight, "glyph_count": gcount,
"charset": tier, "tier": tier, "size_list": [20, 28], "bit_depth": 4,
"sha256_png": "0" * 64, "sha256_idx": "0" * 64,
}
with open(os.path.join(d, "font.json"), "w", encoding="utf-8") as f:
json.dump(meta, f, ensure_ascii=False, indent=2)
def run_scan():
r = subprocess.run(["qemu-mipsel", BIN, "--fontscan", FIX_ROOT],
capture_output=True, text=True, timeout=120)
return r.returncode, r.stdout
def read_index():
p = os.path.join(FIX_ROOT, "MeowOS", "config", "fonts", "index")
if not os.path.exists(p):
return []
rows = []
with open(p, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
rows.append(next(csv.reader([line])))
return rows
def main():
if not os.path.exists(BIN):
print(f"FAIL: 未找到 {BIN},请先跑 build_launcher.sh"); return 2
if shutil.which("qemu-mipsel") is None:
print("FAIL: 缺 qemu-mipsel"); return 2
# ---- 构造卡根 ----
mk_font("default/regular", "default", "字体", "regular", "common", 4110) # 两级默认
mk_font("misans/regular", "misans", "MiSans", "regular", "common", 4110) # 两级(旧结构)
# source_han_sans_cn 三级:regular/common + regular/gbk(同 weight 双 tier)
mk_font("source_han_sans_cn/regular/common", "source_han_sans_cn", "思源黑体 CN", "regular", "common", 4110)
mk_font("source_han_sans_cn/regular/gbk", "source_han_sans_cn", "思源黑体 CN", "regular", "gbk", 20000)
mk_font("source_han_sans_cn/regular/full", "source_han_sans_cn", "思源黑体 CN", "regular", "full", 30000)
mk_font("source_han_sans_cn/bold/common", "source_han_sans_cn", "思源黑体 CN", "bold", "common", 4110)
# 同 (family,weight) 下 weight 根有 font.png(两级)+ 同 weight 也带 common/ 子目录 → 应按两级处理
d = os.path.join(FONTS, "dual", "regular")
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, "font.png"), "wb") as f:
f.write(b"\x89PNG\r\n\x1a\nPLACEHOLDER")
with open(os.path.join(d, "font.idx"), "wb") as f:
f.write(b"MFNI\x02\x00PLACEHOLDER")
with open(os.path.join(d, "font.json"), "w", encoding="utf-8") as f:
json.dump({"family": "dual", "name": "Dual", "weight": "regular",
"glyph_count": 1000, "charset": "common"}, f)
os.makedirs(os.path.join(d, "common"), exist_ok=True) # 应被忽略(已在两级分支)
# 缺件三级:source_han_sans_cn/medium 下无 font.idx → 跳过
broken = os.path.join(FONTS, "source_han_sans_cn", "medium", "full")
os.makedirs(broken, exist_ok=True)
with open(os.path.join(broken, "font.png"), "wb") as f:
f.write(b"PNG")
with open(os.path.join(broken, "font.json"), "w") as f:
json.dump({"family": "source_han_sans_cn", "name": "思源黑体 CN", "weight": "medium"}, f)
# 杂散三级子目录(tier 名非常规 common/gbk/full,但三件套齐全)→ 应被跳过(仅认 3 档)
stray = os.path.join(FONTS, "source_han_sans_cn", "regular", "backup")
os.makedirs(stray, exist_ok=True)
for n, b in (("font.png", b"PNG"), ("font.idx", b"IDX"), ("font.json", b'{}')):
with open(os.path.join(stray, n), "wb") as f:
f.write(b)
rc, out = run_scan()
if rc != 0:
print(f"FAIL: qemu --fontscan 返回 {rc}\n{out}"); return 1
presets = {}
famwt = {}
for line in out.splitlines():
line = line.strip()
if line.startswith("fonts="):
continue
if line and line[0].isdigit():
parts = line.split(" -> ")
if len(parts) != 2:
continue
idx = int(parts[0].split()[0])
rest, famwt_part = parts[1].split(" (fam=", 1)
path = rest.strip()
fam = famwt_part.split(" wt=")[0]
wt = famwt_part.split(" wt=")[1].rstrip(")")
presets[path] = idx
famwt[path] = (fam, wt)
fails = 0
def check(cond, msg):
nonlocal fails
if not cond:
fails += 1
print(f"FAIL: {msg}")
else:
print(f"ok : {msg}")
n = int(next((l.split("=")[1] for l in out.splitlines() if l.startswith("fonts=")), "0"))
check(n == len(presets), f"fonts={n} 与预设解析数 {len(presets)} 一致")
# 预期 preset(默认 + dual/misans 两级 + shsc 4 套三级 = 7)
check(len(presets) == 7, f"扫描产出 7 套(实际 {len(presets)})")
check("MeowOS/fonts/default/regular/font.png" in presets, "两级默认在列表")
check("MeowOS/fonts/misans/regular/font.png" in presets, "两级 misans/regular 在列表")
check("MeowOS/fonts/dual/regular/font.png" in presets, "dual/regular(weight 根 font.png)按两级处理")
p_common = "MeowOS/fonts/source_han_sans_cn/regular/common/font.png"
p_gbk = "MeowOS/fonts/source_han_sans_cn/regular/gbk/font.png"
p_full = "MeowOS/fonts/source_han_sans_cn/regular/full/font.png"
p_bold = "MeowOS/fonts/source_han_sans_cn/bold/common/font.png"
check(p_common in presets, "三级 shsc/regular/common 在列表")
check(p_gbk in presets, "三级 shsc/regular/gbk 在列表")
check(p_full in presets, "三级 shsc/regular/full 在列表")
check(p_bold in presets, "三级 shsc/bold/common 在列表")
check(famwt.get(p_gbk) == ("source_han_sans_cn", "regular"), "fam/wt 三级条目系家族+字重")
check(all("medium" not in k for k in presets), "缺 font.idx 的 shsc/medium/full 被跳过")
check(all("/regular/backup/" not in k for k in presets), "杂散 tier 子目录 backup(非常规档名)被跳过")
# 同 weight 下的 common 子目录(dual/regular/common)不应被当作三级产出(已在两级分支)
check(all("/dual/regular/common/" not in k for k in presets), "dual/regular 的 common/ 子目录未产生独立条目")
# ---- index 文件 6 字段 + 三级条目 ----
rows = read_index()
check(len(rows) == len(presets), f"index 条数 {len(rows)} == 扫描数 {len(presets)}")
def find(path):
for r in rows:
if len(r) >= 3 and r[2] == path:
return r
return None
r = find(p_gbk)
check(r is not None, "index 含三级 gbk 条目")
if r:
check(len(r) == 6, f"index 每套 6 字段(实际 {len(r)})")
check(r[0] == "source_han_sans_cn" and r[1] == "regular", "index family/weight 列正确")
check(r[4] == "gbk", f"index tier 列 = gbk(实际 {r[4]})")
check(r[5] == "20000", f"index glyph_count 列 = 20000(实际 {r[5]})")
r = find(p_common)
check(r is not None and r[4] == "common", "index 三级 common 条目 tier=common")
r = find(p_full)
check(r is not None and r[4] == "full", "index 三级 full 条目 tier=full")
r = find("MeowOS/fonts/dual/regular/font.png")
check(r is not None and r[4] == "common", "index 两级 dual 条目 tier=common(读 charset)")
r = find("MeowOS/fonts/default/regular/font.png")
check(r is not None and r[0] == "default", "index 默认行 family=default")
# 三级 label(stdout 含档位标记)
check("思源黑体 CN Regular(gbk≈20k)" in out, "label 含 `思源黑体 CN Regular(gbk≈20k)`")
check("思源黑体 CN Regular(common≈4k)" in out, "label 含 `思源黑体 CN Regular(common≈4k)`")
print(f"\n{'RESULT: PASS' if fails == 0 else 'RESULT: FAIL'} ({fails} failures)")
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main())