#!/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////(tier=common/gbk/full)→ 每 (family,weight,tier) 一套,值 = MeowOS/fonts////font.png,tier=目录名。 (2) 两级向后兼容://font.png → 按旧口径,tier 读 font.json charset。 (3) 同 (family,weight) 下 weight 根有 font.png → 按两级(不另扫 tier 子目录)。 (4) index 落盘:/MeowOS/config/fonts/index 每套一条 6 字段 "family","weight","path","label","tier",glyph_count;三级条目含三级 path + tier 列。 (5) 缺件(漏 font.idx 等)子目录 → 该套跳过。 方法:构造假卡根(两/三级混合 + 缺件 + 非法目录),用 qemu-mipsel 跑真实 meow_launcherd --fontscan ,断言 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): """写一套 /[/]/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())