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 字体仍两级
This commit is contained in:
1 parent
6603e6686c
commit
3fd4726198
5 files changed
+366
-54
No files matched your search
@@ -0,0 +1,198 @@
|
||||
#!/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())
|
||||
Reference in new issue
Block a user