Files
STM32F4-Base/simulator/tests/check_content_pages.py
T
2026-09-09 13:18:06 +08:00

153 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())