fix(sim): UI 回归改为自洽运行——修 netcfg_save 假失败

现象:check_content_pages.py 只有 netcfg_save 一页 FAIL(保存行值列
显示 ERR 而非 OK),其余 10 页像素与两处往返池水位全过。

真因不是代码回归:模拟器按**当前工作目录**读写 sim_config.ini,而
--cfgtest 的 E-01 场景 2 会伪造一份 4000 B 纯注释"雷"文件、并断言
"拒写后原文件不动"(size == 4000)——连正常通过路径也不删除它。
实测仓库根那份是 2026-10-11 08:37 留下的。之后每次运行都会加载它
→ 外科式保存要在 4000 B 注释后补写受管段 → 输出必超
CFG_FILE_MAX_LEN → 引擎按设计拒写 → 保存行显示 ERR,与"工厂默认
态"的黄金参考对不上。引擎行为是对的,错在自检留雷 + 回归不自洽。

修三处:
- simulator/main.c:run_cfgtest 拆成 scenarios + 外壳,无论成败一律
  cfg_store_remove 配置路径与临时路径,让配置回到"无文件 = 工厂默认"
- check_menu_rows.py / check_content_pages.py:以空临时目录当工作
  目录跑 lvgl_sim(cwd=tmp),调用目录里的残留档不再影响结果
- check_content_pages.py:作废"删 sim_netcfg.bin 残留"的旧兼容清理
  (旧档迁移 2026-10-10 已删),改为只提示、不删别人的档

验证:--cfgtest PASS 且跑完不再留下 sim_config.ini;
check_menu_rows.py PASS(8 状态 + 池基线 5,368 B);
check_content_pages.py "PASS: 11 内容页 + 往返池水位全部通过",
往返 5,448 B。**未刷任何黄金参考**——参考本来就是对的。顺带删掉
本地两份 4000 B 残留(仓库根与 build/Debug,均 gitignore 产物)。

未跑 @build:本次只改 simulator/ 侧代码,固件工程无涉及;模拟器
CMake 构建 0 错 0 警,cppcheck 钩子覆盖 simulator/main.c。

文档:UI_自动化测试流程 §五 加排查行 + 变更记录;simulator/README
§3.7 说明删除保证与原因;lvgl-simulator skill 同步;订正审查报告
R-9 那条把该差异误归给 cfg_file 改动的表述。
This commit is contained in:
2026-10-15 10:05:37 +08:00
parent 0cece5a305
commit 172b3f4a20
7 changed files with 57 additions and 15 deletions
+3 -1
View File
@@ -232,7 +232,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化
### 3.7 配置文件自检(--cfgtest)
配置引擎"加载 → 轮询 → 重载 → 保存"的进程内闭环验证(无需窗口 / LVGL,退出码 0=PASS / 非 0=FAIL),在当前工作目录产生 `sim_config.ini`:
配置引擎"加载 → 轮询 → 重载 → 保存"的进程内闭环验证(无需窗口 / LVGL,退出码 0=PASS / 非 0=FAIL);自检期间在当前工作目录**临时**使用 `sim_config.ini`,**结束时无论成败一律删除**(2026-10-15 起,原因见下文 ⚠):
```powershell
.\build\Debug\lvgl_sim.exe --cfgtest
@@ -242,6 +242,8 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化
空目录运行 = 验证出厂模板生成;超限文件(>4096 B)= 判损坏并重建出厂模板;超限文档保存 = 拒绝且不动原文件。窗口模式主循环以与 MCU netTask 相同的 2s 周期调用 `cfg_file_poll()`:外部文件变更沉降后自动重载 `[network]` 段(模拟器无网络栈,变更回调为空操作,仅日志可见)。
⚠ **为什么必须删掉伪造档**:超限文档保存那条断言要求"拒写后原文件不动"(`size == 4000`),所以**连通过路径也会把这份 4000 B 纯注释档留在工作目录**。之后每一次运行都会加载它 → 外科式保存要在 4000 B 注释后补写受管段 → 输出必超 `CFG_FILE_MAX_LEN` → 引擎按设计拒写 → 网口设置页保存行显示 `ERR`,把 UI 像素回归带偏(2026-10-15 实测踩中)。故 `run_cfgtest()` 只是外壳,跑完一律删除;`simulator/tests/` 两套回归另以**空临时目录当工作目录**运行做双保险 → `docs/UI设计/UI_自动化测试流程.md` §五。
---
## 四、已知坑(构建/运行)
+22 -1
View File
@@ -259,7 +259,7 @@ static const uint8_t s_cfgtest_ip[4] = { 10u, 0u, 0u, 1u };
* 说明:验证链 net_cfg_load(只读 INI)→ poll 建基线 → 伪造外部编辑
* → poll 两次(沉降去抖)→ 重载并更新 RAM 副本
*/
static int run_cfgtest(void) {
static int run_cfgtest_scenarios(void) {
int i;
const net_cfg_t *p_cfg;
@@ -380,6 +380,27 @@ static int run_cfgtest(void) {
return 0;
}
/*
* 函数功能:配置自检外壳——保证不在工作目录留下伪造的配置档
* 入口参数:无
* 返回值:run_cfgtest_scenarios 的判定 int
* 说明:E-01 场景 2 会写一个 4000 B 纯注释"雷"文件,并断言"拒写后原文件不动"
* (size == 4000)——**因此连正常通过路径也会把它留在原地**。该文件是
* gitignore 的本地产物,但之后每一次 UI 回归都会加载它:外科手术保存要在
* 4000 B 注释后补写受管段,输出必然超 CFG_FILE_MAX_LEN → 引擎按设计拒写
* → 网口设置页保存行显示 ERR → 黄金参考(工厂默认态)比对失败
* (2026-10-15 实测踩中)。故无论成败一律删除,让配置路径回到
* "无文件 = 工厂默认"这一确定态;回归脚本另以空工作目录运行做双保险
* → docs/UI设计/UI_自动化测试流程.md §三
*/
static int run_cfgtest(void) {
int rc = run_cfgtest_scenarios();
(void)cfg_store_remove(cfg_store_config_path());
(void)cfg_store_remove(cfg_store_tmp_path());
return rc;
}
/*
* 函数功能:日志引擎自检(无窗口、进程内闭环)
* 入口参数:无
+20 -9
View File
@@ -111,8 +111,16 @@ STRIDE = W // 8 # I1:20 字节/行
POOL_AFTER_ROUNDTRIP = 5448
def run_sim(seq, out):
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True)
def run_sim(seq, out, cwd):
"""在指定工作目录里跑一次无头截图。
模拟器按**工作目录**读写 `sim_config.ini`:目录里残留一份配置(尤其
`--cfgtest` E-01 场景 2 伪造的 4000 B 纯注释"雷"文件)会让引擎按设计
拒绝保存 → 网口设置页保存行显示 ERR → 与"工厂默认态"的黄金参考对不上。
因此一律在空临时目录里跑,回到"无配置文件 = 工厂默认"这一确定态。
"""
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True,
cwd=str(cwd))
if r.returncode != 0:
raise RuntimeError("lvgl_sim --keys %r rc=%d: %s"
% (seq, r.returncode, r.stderr.decode(errors="replace")[:200]))
@@ -263,17 +271,20 @@ def main():
tmp = tempfile.mkdtemp(prefix="dtu_ui_pages_")
ok = True
# 清理手工运行残留的配置存储文件(模拟器工作目录),防"工厂默认值"像素参考被污染
# (sim_netcfg.bin 是旧档时代的产物:旧档迁移已于 2026-10-10 删除,此处只做兼容清理)
stale = Path.cwd() / "sim_netcfg.bin"
if stale.exists():
stale.unlink()
# 回归一律在空临时目录里跑(原因见 run_sim 的说明)。调用目录里残留的配置档
# 不再影响像素,只提示一句,便于手工跑 lvgl_sim 的人排查。
# (旧 sim_netcfg.bin 时代的"删除残留"做法已作废:旧档迁移 2026-10-10 删除,
# 且真正会污染结果的是 --cfgtest 留下的 sim_config.ini,删别人的档不合适)
for leftover in ("sim_config.ini", "sim_netcfg.bin"):
if (Path.cwd() / leftover).exists():
print("[提示] 当前目录存在 %s(本脚本不读它;手工运行 lvgl_sim 时留意,"
"--cfgtest 会伪造 4000 B 配置档)" % leftover)
# 1) 跑全部页面 + 文本级断言预检(先于像素比对/刷新:失败即中止,避免误刷参考)
shots = {}
for name, seq in PAGES:
shot = os.path.join(tmp, "shot_%s.bin" % name)
run_sim(seq, shot)
run_sim(seq, shot, tmp)
shots[name] = open(shot, "rb").read()
for name, _seq in PAGES:
@@ -316,7 +327,7 @@ def main():
# 开页-关页往返池水位门禁(info 页 e/s;密码页 ddee e/s 后 ←← 弹框回根框)
for rt_name, rt_seq in (("info", "es"), ("pwd", "ddeeesll")):
out = run_sim(rt_seq, os.path.join(tmp, "shot_rt_%s.bin" % rt_name))
out = run_sim(rt_seq, os.path.join(tmp, "shot_rt_%s.bin" % rt_name), tmp)
m = re.search(r"pool after keys: used=(\d+)", out.decode(errors="replace"))
if not m:
print("FAIL: 往返输出未找到 'pool after keys'(%s %r)" % (rt_name, rt_seq))
+4 -1
View File
@@ -126,7 +126,10 @@ def main():
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)
# 空临时目录当工作目录:模拟器按 cwd 读写 sim_config.ini,
# 残留档(--cfgtest 会伪造 4000 B)会改变页面内容 → 回归必须自洽
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True,
cwd=tmp)
if r.returncode != 0:
print("[%s] 运行失败 rc=%d: %s" % (name, r.returncode,
r.stderr.decode(errors="replace")[:200]))