字重结构+family→字重子菜单(agent-111: fonts/<family>/<weight>/, gen_font.py --weight, 两级扫描, s_font_dropdown子菜单) + 全屏左滑滑条修复(agent-112)

This commit is contained in:
MeowOS Dev committed 2026-08-30 13:42:41 +08:00
1 parent e8fa373a2f
commit b75ecce5de
16 files changed
+531 -164

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
#!/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())