#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ test_font_weight.py — MeowOS 字重结构(M2.6)宿主导出单测。 验证 launcher 侧 font 扫描/解析的两块(MEOWOS_SPEC_REF §5.2 字重结构 + §5.7.1 扫描): (1) 两级扫描:<卡根>/MeowOS/fonts/// 每 (family,weight) 一套,生成 preset(值 = MeowOS/fonts///font.png,fam/wt 单列)。 (2) font.json weight 解析:label = ` (common≈4k)`(如 `MiSans Regular(common≈4k)`); 缺 weight 回退 regular;缺三件套/损坏 → 跳过;旧式一级布局(font.png 直接在 /) → 不产出(迁移后需重扫)。 方法:构造一个假卡根(含合法多 weight family / 缺件 family / 旧式文件 / 顶层 stray), 用 qemu-mipsel 跑真实 meow_launcherd --fontscan ,断言 stdout 的 preset 列表。 用法(在 patch_v2/meow_launcherd 下): 先 build_launcher.sh 生成 meow_launcherd(mipsel 静态 ELF),再: python3 test_font_weight.py 依赖 qemu-mipsel(qemu-user)。退出码 0=全过,非 0=断言失败。 """ 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="mfscan_", dir="/tmp") FONTS = os.path.join(FIX_ROOT, "MeowOS", "fonts") def write_font(fam, wt, name, weight="regular", charset="common"): """写一套 //font.png+font.idx+font.json(合法三件套;png/idx 为占位字节)。""" d = os.path.join(FONTS, fam, wt) 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": fam, "name": name, "weight": weight, "glyph_count": 4110, "charset": charset, "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(): """返回 (ret, stdout)。""" r = subprocess.run(["qemu-mipsel", BIN, "--fontscan", FIX_ROOT], capture_output=True, text=True, timeout=120) return r.returncode, r.stdout 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 # ---- 构造卡根 ---- write_font("misans", "regular", "MiSans", weight="regular") write_font("misans", "bold", "MiSans", weight="bold") write_font("meslo", "regular", "Meslo LG S Mono", weight="regular") write_font("default", "regular", "font", weight="regular") # 缺件:无 font.idx → 应跳过 broken = os.path.join(FONTS, "broken", "regular") 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": "broken", "name": "Broken", "weight": "regular"}, f) # 旧式一级布局:font.png 直接在 / 下(无 weight 级)→ 迁移后应不产出 old = os.path.join(FONTS, "legacy") os.makedirs(old, exist_ok=True) with open(os.path.join(old, "font.png"), "wb") as f: f.write(b"PNG") # 顶层 stray 文件(非目录)→ 应忽略 with open(os.path.join(FONTS, "stray.png"), "wb") as f: f.write(b"PNG") rc, out = run_scan() if rc != 0: print(f"FAIL: qemu --fontscan 返回 {rc}\n{out}"); return 1 if os.path.exists(os.path.join(os.path.dirname(FIX_ROOT), "meow_boot.log")): pass # launcher 日志在 /tmp,忽略 # ---- stdout -> dict ---- presets = {} famwt = {} for line in out.splitlines(): line = line.strip() if line.startswith("fonts="): continue if line and line[0].isdigit(): # " 0 Label -> path (fam=.. wt=..)" 但行首 i 数字后无空格分隔可靠断言,用 split 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) elif line.startswith("fonts="): n = int(line.split("=")[1]) if n != len(presets): print(f"FAIL: fonts={n} 但解析到 {len(presets)} 条\n{out}"); return 1 fails = 0 def check(cond, msg): nonlocal fails if not cond: fails += 1 print(f"FAIL: {msg}") else: print(f"ok : {msg}") p1 = "MeowOS/fonts/misans/regular/font.png" p2 = "MeowOS/fonts/misans/bold/font.png" p3 = "MeowOS/fonts/meslo/regular/font.png" p0 = "MeowOS/fonts/default/regular/font.png" # 两级扫描产出(默认 + 3 套,共 4) check(len(presets) == 4, f"扫描产出 4 套(实际 {len(presets)})") check(p0 in presets and presets[p0] == 0, "索引 0 = default/regular") check(p1 in presets, "misans/regular 进列表") check(p2 in presets, "misans/bold 进列表") check(p3 in presets, "meslo/regular 进列表") check(famwt.get(p1) == ("misans", "regular"), "fam/wt(misans/regular) 系家族+字重") check(famwt.get(p2) == ("misans", "bold"), "fam/wt(misans/bold) 系家族+字重") # 缺件 / 旧式一级 / 顶层文件 → 不产出 check(all("broken" not in k for k in presets), "缺 font.idx 的 broken/regular 被跳过") check(all("legacy" not in k for k in presets), "旧式一级布局 legacy(无 weight 级)被忽略(迁移后需重扫)") check(all("stray" not in k for k in presets), "顶层 stray.png(非目录)被忽略") # weight 解析 → 显示名 `Name Weight(common≈4k)` labels = {os.path.basename(os.path.dirname(os.path.dirname(p))): p for p in presets} # 用 stdout 里的 Label 校验(weight 大写 + 档位标记) out_has = "MiSans Regular(common≈4k)" in out out_has_bold = "MiSans Bold(common≈4k)" in out check(out_has, "label 含 `MiSans Regular(common≈4k)`(weight 解析+大小标记)") check(out_has_bold, "label 含 `MiSans Bold(common≈4k)`(bold 字重解析)") 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())