增加两页UI
This commit is contained in:
@@ -69,6 +69,7 @@ set(SIM_SOURCES
|
||||
set(UI_SOURCES
|
||||
${UI_DIR}/ui_menu.c
|
||||
${UI_DIR}/ui_menu_view.c
|
||||
${UI_DIR}/ui_content_page.c
|
||||
${UI_DIR}/ui_menu_tab.c
|
||||
${UI_DIR}/ui_font_cn_12.c
|
||||
)
|
||||
|
||||
+30
-5
@@ -16,6 +16,10 @@ simulator/
|
||||
├── sim_port.c / sim_port.h # 模拟器显示/输入端口(SDL 窗口 + 键盘),替代 MCU 端 ui_port.c
|
||||
├── lv_conf.h # 模拟器专用 LVGL 配置(与 MCU 端 Lib/lv_conf.h 相互独立)
|
||||
├── i1_to_png.py # 截图转 PNG:把 --screenshot 的 I1(1bpp) 原始位图转成 PNG(见 §3.5)
|
||||
├── tests/ # 无头回归脚本 + 黄金参考(见 §3.6,流程文档 docs/UI 设计/UI_自动化测试流程.md)
|
||||
│ ├── check_menu_rows.py # 菜单 8 状态像素回归 + 池基线门禁
|
||||
│ ├── check_content_pages.py # 内容页黄金参考回归 + 开页-关页往返池门禁(--update 刷参考)
|
||||
│ └── ref/ # 黄金参考截图 shot_{info,ac,dc}.bin(I1 原始位图,随仓库提交)
|
||||
├── dbg_log.h # 调试日志 stub:DBG_* 映射到 printf(覆盖 MCU 版头文件)
|
||||
├── SDL2/
|
||||
│ └── SDL2-2.32.10/ # SDL2 预编译 SDK(随仓库提供,无需安装)
|
||||
@@ -38,6 +42,7 @@ simulator/
|
||||
| `dbg_log.h` | MCU 版 `dbg_log.h` 依赖 `sys_clock.h`(RTC 时间戳),PC 无法编译。此 stub 把 `DBG_INFO/ERROR/DEBUG` 映射到 `printf`;因 `simulator/` 在 include 路径最前,**优先生效**覆盖 MCU 版 |
|
||||
| `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ 本目录端口代码,链接预编译 SDL2,产出 `lvgl_sim.exe` |
|
||||
| `i1_to_png.py` | 把 `--screenshot` 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 |
|
||||
| `tests/` | 无头回归脚本(菜单 8 状态像素回归、内容页黄金参考回归)+ 入库黄金参考截图。详见 §3.6 |
|
||||
|
||||
> **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` 直接引用 `Drivers/BSP/UI/` 下的原件,
|
||||
> MCU 与 PC 两端永远同步。**不要**在本目录内复制 UI 源码。
|
||||
@@ -170,9 +175,9 @@ cmake --build build --config Debug
|
||||
cd simulator
|
||||
# 1) 离屏渲染首帧,输出 I1(1bpp) 原始位图(已剥离 8 字节调色板,纯像素数据)
|
||||
.\build\Debug\lvgl_sim.exe --screenshot shot.bin
|
||||
# 2) 转 PNG(默认 160x160;--scale 放大便于查看)
|
||||
python i1_to_png.py shot.bin # -> shot.png
|
||||
python i1_to_png.py shot.bin -o 预览.png --scale 6
|
||||
# 2) 转 PNG(默认 160x160;--scale 放大便于查看;python 统一用仓库 .venv,见 AGENTS.md「Python 环境」)
|
||||
..\.venv\Scripts\python.exe i1_to_png.py shot.bin # -> shot.png
|
||||
..\.venv\Scripts\python.exe i1_to_png.py shot.bin -o 预览.png --scale 6
|
||||
```
|
||||
|
||||
**I1 位图格式**(`i1_to_png.py` 据此解析):1 位/像素,每字节 MSB 在左(bit7 = 该行最左像素),`bit=1` 前景 / `bit=0` 背景,每行 `宽/8` 字节、行内无填充。
|
||||
@@ -188,7 +193,27 @@ python i1_to_png.py shot.bin -o 预览.png --scale 6
|
||||
| `--fg / --bg` | 自定义前/背景色 `R,G,B`(默认黑白) |
|
||||
| `--no-auto-palette` | 关闭调色板自动识别(按纯像素解析) |
|
||||
|
||||
> 依赖 Pillow(`pip install pillow`,本仓库已验证)。`shot.bin`/`shot*.png` 均已列入 `.gitignore`,可随时删除重生成。
|
||||
> 依赖 Pillow(已含于仓库 `.venv`,见 `requirements.txt`)。`shot.bin`/`shot*.png` 均已列入 `.gitignore`,可随时删除重生成。
|
||||
|
||||
### 3.6 无头回归测试(tests/)
|
||||
|
||||
UI 改动的自动化门禁脚本(完整流程见 `docs/UI 设计/UI_自动化测试流程.md`)。脚本纯标准库、任意 python3 均可,为环境统一一律用仓库 `.venv`:
|
||||
|
||||
```powershell
|
||||
# 仓库根目录执行(前置:本目录已构建出 build/Debug/lvgl_sim.exe)
|
||||
.venv\Scripts\python.exe simulator/tests/check_menu_rows.py # 菜单 8 状态像素回归 + 池基线门禁
|
||||
.venv\Scripts\python.exe simulator/tests/check_content_pages.py # 内容页黄金参考回归 + 开页-关页往返池门禁
|
||||
```
|
||||
|
||||
| 脚本 | 原理 | 失败输出 |
|
||||
|------|------|----------|
|
||||
| `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;S0 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 |
|
||||
| `check_content_pages.py` | 三内容页(info=`e` / ac=`dee` / dc=`dede`)无头截图与 `tests/ref/shot_*.bin` **逐字节**比对;另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 差异像素数 + 双方 ink runs 对照 |
|
||||
|
||||
- **黄金参考更新**(有意改视觉后):先肉眼确认新效果正确,再
|
||||
`.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 刷新 `ref/`,随代码提交。
|
||||
- **池水位常量漂移**:菜单/页面结构变更导致门禁失败时,确认新值合理后再更新脚本内常量并登记流程文档变更记录;无法解释的漂移 = 内存回归 bug。
|
||||
- 退出码均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等)。
|
||||
|
||||
---
|
||||
|
||||
@@ -208,7 +233,7 @@ python i1_to_png.py shot.bin -o 预览.png --scale 6
|
||||
- `LV_USE_THEME_MONO = 1` 且 `sim_port_init` 中调用了 `lv_theme_mono_init`
|
||||
- 主循环调用了 `lv_refr_now(NULL)`(PARTIAL 模式保证首帧)
|
||||
- 没有重复设置 tick(SDL 驱动内部已 `lv_tick_set_cb`,**不要**再 `SDL_AddTimer` + `lv_tick_inc`)
|
||||
4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 78 字),缺字渲染为空白。
|
||||
4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 97 汉字 + ASCII,共 193 字形),缺字渲染为空白。
|
||||
补字流程见 `docs/UI 设计/UI_中文字体生成与配置.md`(`tools/font_gen/` 两步脚本)。
|
||||
5. **改了 UI 代码但模拟器没变化**:确认改的是 `Drivers/BSP/UI/ui_menu.c`(共享原件)而非本目录副本(本目录无副本),然后重新 `cmake --build`。
|
||||
6. **编码**:UI 源码为 UTF-8(带 BOM),MSVC 已加 `/utf-8`。**不要**用 PowerShell `Set-Content` 等重写含中文的文件(编码铁律,见根目录 `AGENTS.md`)。
|
||||
|
||||
@@ -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())
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
check_menu_rows.py — 自绘菜单 8 状态像素回归(160x160 I1,MSB-first,1=白 0=黑)。
|
||||
|
||||
用法:
|
||||
python simulator/tests/check_menu_rows.py # 在仓库任意目录均可运行
|
||||
前置:simulator 已构建(build/Debug/lvgl_sim.exe 存在);纯标准库,任意 python3。
|
||||
|
||||
状态(键序注入,u=上 d=下 e=Enter s=Esc):
|
||||
S0 "" 根框 7 行 sel=0
|
||||
S1 "d" sel=1
|
||||
S2 "dd" sel=2
|
||||
S3 "ddu" 回退 → 须与 S1 全像素一致
|
||||
S4 "de" 开 L2(实时数据 3 子项),根框不变
|
||||
S5 "ded" L2 sel=1
|
||||
S6 "des" 返回 → 须与 S1 全像素一致(销毁/重建一致性)
|
||||
S7 "ddes" 根框 sel=2、L2 已毁 → 须与 S2 全像素一致
|
||||
断言:选中行白带(白占比>0.50)+ 带内黑字;非选行黑底白字(0.03<占比<0.45);
|
||||
框体 1px 白边框;一致性状态逐字节相同;S0 池水位 = MENU_POOL_BASELINE。
|
||||
|
||||
退出码: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"
|
||||
|
||||
STATES = [
|
||||
("S0", ""),
|
||||
("S1", "d"),
|
||||
("S2", "dd"),
|
||||
("S3", "ddu"),
|
||||
("S4", "de"),
|
||||
("S5", "ded"),
|
||||
("S6", "des"),
|
||||
("S7", "ddes"),
|
||||
]
|
||||
|
||||
W = H = 160
|
||||
STRIDE = W // 8 # I1:20 字节/行
|
||||
ROW_H = 15
|
||||
BAND_MIN = 0.50 # 选中行白带:白像素占比下限
|
||||
NBAND_LO = 0.03 # 非选行:白字占比下限(文本必须存在)
|
||||
NBAND_HI = 0.45 # 非选行:白像素占比上限(背景须为黑)
|
||||
|
||||
# 根框几何(view_init):x=0,y=44,w=66,h=107
|
||||
ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H = 0, 44, 66, 107
|
||||
|
||||
# LVGL 池基线水位(menu build done,x64 模拟器):菜单结构/样式变更导致漂移时,
|
||||
# 先确认新值合理再更新此常量(内存回归门禁,见 docs/UI 设计/UI_自动化测试流程.md)
|
||||
MENU_POOL_BASELINE = 4784
|
||||
|
||||
|
||||
def load(path):
|
||||
data = open(path, "rb").read()
|
||||
if len(data) < STRIDE * H:
|
||||
raise RuntimeError("shot 过短: %s (%d)" % (path, len(data)))
|
||||
px = [[0] * W for _ in range(H)]
|
||||
for y in range(H):
|
||||
row = data[y * STRIDE:(y + 1) * STRIDE]
|
||||
for x in range(W):
|
||||
px[y][x] = (row[x // 8] >> (7 - x % 8)) & 1
|
||||
return px
|
||||
|
||||
|
||||
def frac(px, x1, x2, y1, y2):
|
||||
tot = wh = 0
|
||||
for y in range(y1, y2 + 1):
|
||||
for x in range(x1, x2 + 1):
|
||||
tot += 1
|
||||
wh += px[y][x]
|
||||
return wh / tot if tot else 0.0
|
||||
|
||||
|
||||
def root_row_strip(i):
|
||||
y1 = ROOT_Y0 + 1 + i * ROW_H
|
||||
return (ROOT_X0 + 1, ROOT_X0 + ROOT_W - 2, y1, y1 + ROW_H - 1)
|
||||
|
||||
|
||||
def l2_geom(root_sel, cnt):
|
||||
x0 = ROOT_X0 + ROOT_W - 1 # 65(共享边框列)
|
||||
y0 = ROOT_Y0 + 1 + root_sel * ROW_H # 顶对齐父框选中行行顶
|
||||
w = 42 # 2+36+2 + 2 边框(3 汉字行)
|
||||
h = cnt * ROW_H + 2
|
||||
return x0, y0, w, h
|
||||
|
||||
|
||||
def l2_row_strip(x0, y0, i, w):
|
||||
y1 = y0 + 1 + i * ROW_H
|
||||
return (x0 + 1, x0 + w - 2, y1, y1 + ROW_H - 1)
|
||||
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def check(cond, msg):
|
||||
if not cond:
|
||||
fails.append(msg)
|
||||
|
||||
|
||||
def raw(path):
|
||||
return open(path, "rb").read()
|
||||
|
||||
|
||||
def main():
|
||||
if not EXE.exists():
|
||||
print("FAIL: 找不到 %s(先构建 simulator)" % EXE)
|
||||
return 2
|
||||
|
||||
tmp = tempfile.mkdtemp(prefix="dtu_ui_rows_")
|
||||
paths = {}
|
||||
s0_out = None
|
||||
try:
|
||||
for name, seq in STATES:
|
||||
out = os.path.join(tmp, "rows_%s.bin" % name)
|
||||
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True)
|
||||
if r.returncode != 0:
|
||||
print("[%s] 运行失败 rc=%d: %s" % (name, r.returncode,
|
||||
r.stderr.decode(errors="replace")[:200]))
|
||||
return 2
|
||||
paths[name] = out
|
||||
if name == "S0":
|
||||
s0_out = r.stdout
|
||||
|
||||
# S0(空键序)= 菜单基线:池水位门禁
|
||||
m = re.search(r"pool after keys: used=(\d+)", s0_out.decode(errors="replace"))
|
||||
if not m:
|
||||
print("FAIL: S0 输出未找到 'pool after keys'")
|
||||
return 2
|
||||
used = int(m.group(1))
|
||||
check(used == MENU_POOL_BASELINE,
|
||||
"S0 池水位 %d != 基线 %d(菜单结构/样式变更?确认合理后更新常量)"
|
||||
% (used, MENU_POOL_BASELINE))
|
||||
|
||||
shots = {n: load(p) for n, p in paths.items()}
|
||||
S = shots
|
||||
|
||||
def row_check(px, x1, x2, y1, y2, band, tag):
|
||||
f = frac(px, x1, x2, y1, y2)
|
||||
if band:
|
||||
check(f > BAND_MIN, "%s 选中行白占比 %.3f <= %.2f" % (tag, f, BAND_MIN))
|
||||
# 带内须有黑字(文本落在白带上)
|
||||
check(frac(px, x1, x2, y1, y2) < 0.999, "%s 带内无黑字" % tag)
|
||||
else:
|
||||
check(NBAND_LO < f < NBAND_HI, "%s 非选行白占比 %.3f 不在 (%.2f,%.2f)"
|
||||
% (tag, f, NBAND_LO, NBAND_HI))
|
||||
|
||||
def border_check(px, x0, y0, w, h, tag):
|
||||
for y in range(y0 + 1, y0 + h - 1):
|
||||
check(px[y][x0] == 1 and px[y][x0 + w - 1] == 1,
|
||||
"%s 左右边框 y=%d 非白" % (tag, y))
|
||||
for x in range(x0 + 1, x0 + w - 1):
|
||||
check(px[y0][x] == 1 and px[y0 + h - 1][x] == 1,
|
||||
"%s 上下边框 x=%d 非白" % (tag, x))
|
||||
|
||||
# --- S0:根框 7 行,sel=0 ---
|
||||
p = S["S0"]
|
||||
border_check(p, ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H, "S0根框")
|
||||
for i in range(7):
|
||||
row_check(p, *root_row_strip(i), band=(i == 0), tag="S0行%d" % i)
|
||||
|
||||
# --- S1/S2:sel 移位 ---
|
||||
for name, sel in (("S1", 1), ("S2", 2)):
|
||||
p = S[name]
|
||||
border_check(p, ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H, name + "根框")
|
||||
for i in range(7):
|
||||
row_check(p, *root_row_strip(i), band=(i == sel), tag=name + "行%d" % i)
|
||||
|
||||
# --- S4:L2 开于 sel=1 行右侧(3 子项、无箭头),根框区与 S1 相同 ---
|
||||
p = S["S4"]
|
||||
lx0, ly0, lw, lh = l2_geom(1, 3)
|
||||
border_check(p, lx0, ly0, lw, lh, "S4-L2框")
|
||||
for i in range(3):
|
||||
row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 0), tag="S4-L2行%d" % i)
|
||||
# 根框区(x 0..65)须与 S1 逐像素一致
|
||||
for y in range(H):
|
||||
for x in range(0, ROOT_X0 + ROOT_W):
|
||||
check(S["S1"][y][x] == p[y][x], "S4 根框区(x=%d,y=%d) 与 S1 不一致" % (x, y))
|
||||
# L2 框下方区域须全黑(无残留对象)
|
||||
f = frac(p, lx0 + 1, lx0 + lw - 2, ly0 + lh + 1, min(ly0 + lh + 14, H - 1))
|
||||
check(f == 0.0, "S4 L2 框下方有像素(占比 %.3f)" % f)
|
||||
|
||||
# --- S5:L2 sel=1,根框区仍与 S1 同 ---
|
||||
p = S["S5"]
|
||||
lx0, ly0, lw, lh = l2_geom(1, 3)
|
||||
for i in range(3):
|
||||
row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 1), tag="S5-L2行%d" % i)
|
||||
for y in range(H):
|
||||
for x in range(0, ROOT_X0 + ROOT_W):
|
||||
check(S["S1"][y][x] == p[y][x], "S5 根框区(x=%d,y=%d) 与 S1 不一致" % (x, y))
|
||||
|
||||
# --- 一致性状态:逐字节全像素比对 ---
|
||||
for a, b, tag in (("S3", "S1", "ddu==d"),
|
||||
("S6", "S1", "des==d(往返)"),
|
||||
("S7", "S2", "ddes==dd(往返)")):
|
||||
if raw(paths[a]) == raw(paths[b]):
|
||||
print("[%s] PASS 全像素一致 (%s)" % (a, tag))
|
||||
else:
|
||||
pa, pb = S[a], S[b]
|
||||
diff = sum(1 for y in range(H) for x in range(W) if pa[y][x] != pb[y][x])
|
||||
fails.append("%s 与 %s 不一致(%d px,%s)" % (a, b, diff, tag))
|
||||
|
||||
if fails:
|
||||
print("FAIL: %d 项断言未过" % len(fails))
|
||||
for msg in fails[:40]:
|
||||
print(" -", msg)
|
||||
if len(fails) > 40:
|
||||
print(" ... 另 %d 项" % (len(fails) - 40))
|
||||
return 1
|
||||
print("PASS: 8 状态全部断言通过(%d 图,池基线 %d B)" % (len(paths), MENU_POOL_BASELINE))
|
||||
return 0
|
||||
finally:
|
||||
# 清理临时截图(失败时保留现场便于排查:仅成功时删)
|
||||
if not fails:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user