增加两页UI
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
check_content_pages.py — 三个叶子内容页(装置信息/交流量/直流量)像素黄金参考回归。
|
||||
|
||||
用法:
|
||||
python simulator/tests/check_content_pages.py # 与入库黄金参考逐字节比对
|
||||
python simulator/tests/check_content_pages.py --update # 有意改视觉后刷新黄金参考
|
||||
前置:simulator 已构建(build/Debug/lvgl_sim.exe 存在);纯标准库,任意 python3。
|
||||
|
||||
原理:每页用键序注入打开(info=e / ac=dee / dc=dede),无头截图 I1(160x160)
|
||||
与 ref/shot_<page>.bin 逐字节比对——任何像素变化即 FAIL(含字体、几何、颜色)。
|
||||
另做开页-关页往返池水位门禁:`es` 后 used 须等于 POOL_AFTER_ROUNDTRIP。
|
||||
|
||||
黄金参考更新流程(有意改视觉时):先肉眼看图确认新效果正确,再 --update 刷新,
|
||||
最后 git diff 检查 ref/ 变化符合预期。误报排查见 docs/UI 设计/UI_自动化测试流程.md。
|
||||
|
||||
退出码:0 全 PASS;1 有页不一致或池水位漂移;2 运行/环境错误(exe 缺失、参考缺失)。
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# lvgl_sim.exe 相对本脚本定位(simulator/tests/ -> simulator/build/Debug/)
|
||||
EXE = Path(__file__).resolve().parents[1] / "build" / "Debug" / "lvgl_sim.exe"
|
||||
REF_DIR = Path(__file__).resolve().parent / "ref"
|
||||
|
||||
# (页名, 打开键序):与 ui_menu.c menu_content_page_for() 的节点映射一致
|
||||
PAGES = [
|
||||
("info", "e"), # 装置信息(node 1)
|
||||
("ac", "dee"), # 交流量(node 3,实时数据下第 1 项)
|
||||
("dc", "dede"), # 直流量(node 4,实时数据下第 2 项)
|
||||
]
|
||||
|
||||
W = H = 160
|
||||
STRIDE = W // 8 # I1:20 字节/行
|
||||
|
||||
# 开页-关页往返后的 LVGL 池水位(x64 模拟器)= 菜单基线 + 一次性残留 80 B。
|
||||
# 页面结构变更导致漂移时,先确认新值合理再更新此常量(内存回归门禁)。
|
||||
POOL_AFTER_ROUNDTRIP = 4864
|
||||
|
||||
|
||||
def run_sim(seq, out):
|
||||
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError("lvgl_sim --keys %r rc=%d: %s"
|
||||
% (seq, r.returncode, r.stderr.decode(errors="replace")[:200]))
|
||||
return r.stdout
|
||||
|
||||
|
||||
def ink_runs(data):
|
||||
"""I1 位图按行统计白像素,返回 [(y0,y1,count), ...](count>2 的连续段)。"""
|
||||
px = []
|
||||
for y in range(H):
|
||||
row = data[y * STRIDE:(y + 1) * STRIDE]
|
||||
cnt = sum((b >> (7 - x)) & 1 for b in row for x in range(8))
|
||||
px.append(cnt)
|
||||
runs, start = [], None
|
||||
for y, c in enumerate(px):
|
||||
if c > 2 and start is None:
|
||||
start = y
|
||||
elif c <= 2 and start is not None:
|
||||
runs.append((start, y - 1))
|
||||
start = None
|
||||
if start is not None:
|
||||
runs.append((start, H - 1))
|
||||
return [(a, b, sum(px[a:b + 1])) for a, b in runs]
|
||||
|
||||
|
||||
def diff_count(a, b):
|
||||
n = 0
|
||||
for y in range(H):
|
||||
ra = a[y * STRIDE:(y + 1) * STRIDE]
|
||||
rb = b[y * STRIDE:(y + 1) * STRIDE]
|
||||
for x in range(STRIDE):
|
||||
n += bin(ra[x] ^ rb[x]).count("1")
|
||||
return n
|
||||
|
||||
|
||||
def main():
|
||||
update = "--update" in sys.argv[1:]
|
||||
if not EXE.exists():
|
||||
print("FAIL: 找不到 %s(先构建 simulator)" % EXE)
|
||||
return 2
|
||||
|
||||
tmp = tempfile.mkdtemp(prefix="dtu_ui_pages_")
|
||||
ok = True
|
||||
for name, seq in PAGES:
|
||||
ref = REF_DIR / ("shot_%s.bin" % name)
|
||||
shot = os.path.join(tmp, "shot_%s.bin" % name)
|
||||
run_sim(seq, shot)
|
||||
data = open(shot, "rb").read()
|
||||
|
||||
if update:
|
||||
REF_DIR.mkdir(exist_ok=True)
|
||||
ref.write_bytes(data)
|
||||
print("[%s] 黄金参考已刷新 -> %s(键序 %r)" % (name, ref.name, seq))
|
||||
continue
|
||||
|
||||
if not ref.exists():
|
||||
print("FAIL: 缺黄金参考 %s(首次运行请加 --update 生成并入库)" % ref)
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return 2
|
||||
refdata = ref.read_bytes()
|
||||
if data == refdata:
|
||||
print("[%s] PASS 与黄金参考一致(键序 %r)" % (name, seq))
|
||||
continue
|
||||
|
||||
ok = False
|
||||
n = diff_count(data, refdata)
|
||||
print("[%s] FAIL 与黄金参考不一致:%d px 差异(键序 %r)" % (name, n, seq))
|
||||
print(" 当前截图 ink runs: %s" % ink_runs(data))
|
||||
print(" 黄金参考 ink runs: %s" % ink_runs(refdata))
|
||||
print(" 有意改视觉?确认新效果正确后运行 --update 刷新参考。")
|
||||
|
||||
# 开页-关页往返池水位门禁(info 页:e 打开、s 关闭回菜单)
|
||||
out = run_sim("es", os.path.join(tmp, "shot_rt.bin"))
|
||||
m = re.search(r"pool after keys: used=(\d+)", out.decode(errors="replace"))
|
||||
if not m:
|
||||
print("FAIL: 往返输出未找到 'pool after keys'")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return 2
|
||||
used = int(m.group(1))
|
||||
if update:
|
||||
print("[rt] 开页-关页往返池水位 %d B(--update 不校验,仅提示)" % used)
|
||||
elif used != POOL_AFTER_ROUNDTRIP:
|
||||
ok = False
|
||||
print("[rt] FAIL 往返池水位 %d != 预期 %d(页面结构变更?确认合理后更新常量)"
|
||||
% (used, POOL_AFTER_ROUNDTRIP))
|
||||
else:
|
||||
print("[rt] PASS 开页-关页往返池水位 %d B" % used)
|
||||
|
||||
if update:
|
||||
print("PASS: 黄金参考已刷新(%d 页)" % len(PAGES))
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return 0
|
||||
|
||||
if ok:
|
||||
print("PASS: 3 内容页 + 往返池水位全部通过")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return 0
|
||||
# 失败时保留临时截图便于排查
|
||||
print("(现场截图保留在 %s)" % tmp)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user