156 lines
6.8 KiB
Python
156 lines
6.8 KiB
Python
#!/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>/ 每 (family,weight) 一套,生成
|
||
preset(值 = MeowOS/fonts/<family>/<weight>/font.png,fam/wt 单列)。
|
||
(2) font.json weight 解析:label = `<name> <Weight>(common≈4k)`(如 `MiSans Regular(common≈4k)`);
|
||
缺 weight 回退 regular;缺三件套/损坏 → 跳过;旧式一级布局(font.png 直接在 <family>/)
|
||
→ 不产出(迁移后需重扫)。
|
||
|
||
方法:构造一个假卡根(含合法多 weight family / 缺件 family / 旧式文件 / 顶层 stray),
|
||
用 qemu-mipsel 跑真实 meow_launcherd --fontscan <root>,断言 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"):
|
||
"""写一套 <fam>/<wt>/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 直接在 <family>/ 下(无 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())
|