对应 Code_Review_Report §14.5 第 1 项与第 3 项。
第 1 项:删除 EdgePanelLayout::rowPoints / rowIterations(行数组 18 → 16)
- 写入方只有 AppendRow(推空矩形),读取方只有调试框 AddAll 与 PanelSlotKey 的 mix 循环,
绘制方零(旧式“点数 / 迭代次数”输入框早被滑块版 rowLevelTracks / rowIterTracks 取代)。
- 连带同步:AppendRow、调试框、缓存键、rowgeom 导出、panels/panelscroll 长度不变式、
文档里的列数(18/14 → 16/12)。
等价性由基线证伪,而不是靠人工 grep:
old lines = 983, new lines = 919
only in OLD = 64, only in NEW = 0
only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0
即除被删两列外,其余 919 条键逐字节相同。
第 3 项(前半):提取 PlotView::PaintPanelResampleRows
- PaintPanelListRows 里 `if (resamplePanel) { …; continue; }` 共 132 行整块提取,
签名 (g, layout, rowIndex, laneIndex, ref, item);两个局部 lambda
(paintInfoRow 只读统计行 / paintInputRow 联动输入框)随之迁移。
- maxInterval / minInterval 两个纯拷贝局部量直接用 item.maxInterval / item.minInterval。
- 通用参数行(中心轴统计 + 两行滑块)未提取:需 9 个参数,签名比函数体更难读。
§14.5 第 2 项(完整结构合并)本轮不做,方案已定并写入文档:
- 列视图(PanelColumn<T>)方案否决:拷贝忘重绑 = 静默悬垂,需额外自检兜住;
- 若要做就做“vector<PanelRow> + 具名成员访问(无视图)”,242 处读点(127 处命中判定),
必须单独一轮。它属整洁性收益(漏推一列已被 AppendRow + 长度不变式挡住),不是紧急项。
验证
- make release:零 warning;785,408 → 784,896 B(−512);make debug 同样零 warning
- make -C Tools:零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 127 断言全 PASS
- panels -mode compare:7 面板像素哈希 + 几何逐项不变(resample=2BFC74B3D9B3239C)
- rowgeom -mode compare:919 + 7 键,0 差异(基线已按 16 列重录)
- PlotViewPaint.cpp 5575 → 5565;PlotViewPanelPaint.cpp 1016 → 1020
文档:Plan §十、Progress 第 38 轮、Report §15、agent_mem 第 38 轮、Tools/README §4.4
282 lines
16 KiB
Markdown
282 lines
16 KiB
Markdown
# `Tools/` —— 单元测试与离屏验证工具
|
||
|
||
> 本目录是 **验证基础设施**,不是产品代码:
|
||
> 根目录 `Makefile` 只构建应用本体(`Build/Release`、`Build/Debug`),
|
||
> **不会**编译这里的任何文件;这里的一切由 `Tools/Makefile` 独立构建。
|
||
>
|
||
> 设计目标:让"**改完能不能证明没改坏**"变成一条命令的事,
|
||
> 而不是靠肉眼截图 + 反复手点鼠标。
|
||
|
||
---
|
||
|
||
## 1. 目录结构
|
||
|
||
```text
|
||
Tools/
|
||
├─ Makefile # 统一构建入口(构建 / 清理 / 帮助)
|
||
├─ README.md # 本文件(改动工具后必须同步更新)
|
||
├─ UnitTests/ # 工具一:纯数据层单元测试(不创建窗口)
|
||
│ ├─ Inc/
|
||
│ │ └─ UnitTestFramework.hpp
|
||
│ ├─ Src/
|
||
│ │ ├─ Main.cpp # 测试执行器:-list / -only / -report / -verbose
|
||
│ │ ├─ Utf8UtilTests.cpp # 数值解析与格式化语义
|
||
│ │ ├─ FormulaEvalTests.cpp# 公式语法 / 语义 / 批量求值一致性
|
||
│ │ └─ DataModelTests.cpp # 变换顺序、滑动平均、公式值域缓存
|
||
│ └─ Build/ # 构建产物(.gitignore,不入库)
|
||
└─ OffscreenChecks/ # 工具二:离屏验证(隐藏窗口 + 抓帧)
|
||
├─ Inc/
|
||
│ ├─ OffscreenSupport.hpp# 白盒支撑:抓帧 / 哈希 / 隐藏窗口会话
|
||
│ └─ CheckRunner.hpp # 检查注册表 + 断言 / 输出接口
|
||
├─ Src/
|
||
│ ├─ Main.cpp # 命令行入口
|
||
│ ├─ Session.cpp # 隐藏窗口、加载、逐帧渲染(合成鼠标消息)
|
||
│ ├─ Capture.cpp # 后备缓冲抓帧、FNV 哈希、PNG 落盘
|
||
│ └─ Checks.cpp # 5 个检查的具体实现
|
||
└─ Build/ # 构建产物(.gitignore,不入库)
|
||
```
|
||
|
||
**约定**
|
||
|
||
| 项 | 约定 |
|
||
| --- | --- |
|
||
| 头文件 | 只放 `<Tool>/Inc` |
|
||
| 实现 | 只放 `<Tool>/Src`(该目录下**每个** `.cpp` 都会被编译) |
|
||
| 产物 | `<Tool>/Build`,与 `Inc` / `Src` **同级** |
|
||
| 无 Debug/Release 之分 | 工具一律 `-O2 -g`:要跑得快、要能上调试器 |
|
||
| 临时产物 | PNG / 基线 / 报告一律写 `Temp/tools/`(已 gitignore),**不许**写进 `Tools/` |
|
||
|
||
---
|
||
|
||
## 2. 构建
|
||
|
||
```powershell
|
||
# 前置:应用本体必须先构建(工具链接的是本体的 .o,排除入口 Main.o)
|
||
make release -j8
|
||
|
||
# 构建全部工具
|
||
make -C Tools
|
||
|
||
# 只构建某一个
|
||
make -C Tools UnitTests
|
||
make -C Tools OffscreenChecks
|
||
|
||
# 清理(只删两个工具自己的 Build/,不动本体)
|
||
make -C Tools clean
|
||
|
||
# 看帮助
|
||
make -C Tools help
|
||
```
|
||
|
||
产物:
|
||
|
||
```text
|
||
Tools/UnitTests/Build/UnitTests.exe
|
||
Tools/OffscreenChecks/Build/OffscreenChecks.exe
|
||
```
|
||
|
||
> ⚠️ **必须与本体保持同一套 ABI 开关**:`-fno-exceptions`、`-fno-rtti`、
|
||
> 链接时 `-flto -flto-partition=one`。工具源码自身**不**加 `-flto`
|
||
> (否则 GCC 15.2.0 在 `choose_baseaddr` 处 ICE)。
|
||
>
|
||
> ⚠️ 头文件依赖用 `-MMD -MP` 跟踪:改头文件、增删源文件都会自动重编重链。
|
||
> 若出现"离奇的 undefined reference",先 `make -C Tools clean`。
|
||
|
||
---
|
||
|
||
## 3. 工具一:`UnitTests`(纯逻辑,最快、最常用)
|
||
|
||
**不创建窗口、不初始化 GUI**,因此可以无限次在后台运行。
|
||
|
||
```powershell
|
||
Tools\UnitTests\Build\UnitTests.exe # 全部
|
||
... -only formula # 只跑名字含 formula 的用例
|
||
... -list # 列出用例
|
||
... -report ..\Temp\tools\ut.txt # 指定报告路径
|
||
```
|
||
|
||
输出约定(便于脚本 / Agent 解析):
|
||
|
||
```text
|
||
[RUN ] formula_semantics
|
||
[FAIL] <表达式> (file:line)
|
||
SUMMARY checks=<n> failed=<m> cases=<k> result=<PASS|FAIL>
|
||
```
|
||
|
||
### 3.1 测试项
|
||
|
||
| 用例 | 覆盖内容 | 为什么重要 |
|
||
| --- | --- | --- |
|
||
| `parse_fixed_corpus_matches_strtod` | 常见字面量(含 `1e22`、`-0`、`1e-22`)与 `strtod` **逐位相同** | 数值解析的基准线 |
|
||
| `parse_acceptance_semantics` | 空/空白/垃圾/尾随字符/十六进制浮点/`inf`/`nan` 的接受性 | 与改造前的宽松语义一致 |
|
||
| `parse_random_corpus_bit_exact` | 6 万条随机十进制串;≤15 位有效数字要求**逐位相同**,≤19 位允许 ≤1 ULP | 「自写 strtod 替身」类优化的唯一验收标准 |
|
||
| `format_decimal_never_scientific` | `FormatDecimal*` 永不输出科学计数法;`FormatValue` = `%.6g` | 显示层硬约束 |
|
||
| `format_byte_size_and_frequency` | 单位后缀存在且无指数形式 | 单位格式化回归 |
|
||
| `text_trim_and_utf8_conversion` | `Trim` / `ToLowerAscii` / `Utf8ToWide` / 大小写无关包含 | 文本基础 |
|
||
| `ticks_nice_step_and_build_ticks` | `NiceStep` 落在 1/2/2.5/5×10ⁿ;`BuildTicks` 端点 | 标尺可读性 |
|
||
| `formula_syntax_acceptance` | 合法 / 非法写法(单独 `f`、`2e`、`()`、`*2` …) | 公式输入框的报错边界 |
|
||
| `formula_semantics` | `2^3^2=512`、`-2^2=-4`、`2^-1`、`e`/`pi`、恒等 | 语法语义 |
|
||
| `formula_identity_helpers` | `f(t)` ≡ 空串;`HasFormula()`;`NormalizeFormulaText` | 第 32 轮「恒等公式卡死」的护栏 |
|
||
| `formula_program_matches_pointwise` | 批量求值 vs 逐点求值 **逐位相同**(8 条公式 × 4000 点) | 常量折叠 / 幂次改写不许改数值 |
|
||
| `formula_invalid_falls_back_safely` | 非法公式原样返回采样值 | 写错公式不得让波形消失 |
|
||
| `transform_apply_order` | 公式→系数→绝对值→垂直镜像→Y 偏移;NaN 保持;水平镜像只改下标 | 变换顺序 |
|
||
| `transform_value_range_and_order_preserving` | 区间映射(含跨 0 取绝对值)与保序判定 | Y 轴自适应 |
|
||
| `filter_moving_average_equivalence` | 1 阶 ≡ 单次、2 阶 ≡ 链式、常数信号不变 | 均值滤波多阶 |
|
||
| `dataset_group_access` | `AddGroup` / `GroupAt` / `Style(ref)` 读写 | 数据层入口 |
|
||
| `dataset_formula_transform_cache` | 值域 + 逐分组极值 vs 逐点参考**逐位相同**;命中同指针;样式变化失效 | 第 32 轮核心缓存的正确性 |
|
||
| `stdio_mingw_vs_ucrt_formats` | MinGW ANSI stdio 与系统 UCRT 的 printf 在**本程序用到的全部格式**上逐字符比对(13.6 万次调用) | 评估“把 `swprintf` 交给 UCRT”是否可行(结论:0 字节,libstdc++ 强制开 MinGW 版本) |
|
||
| `integer_replacement_matches_std` | `FormatInteger*` 与 `std::to_wstring` / `std::to_string` 逐字符对照(含 LLONG_MIN/MAX、ULLONG_MAX) | 全仓 78 处整数转换的语义护栏(它们已替代标准库,以去掉 printf 依赖) |
|
||
|
||
### 3.2 验收标准
|
||
|
||
* **零失败**(`result=PASS`)才算通过;
|
||
* 断言数量只增不减(当前 **260** 项 / 19 个用例):删断言必须说明理由;
|
||
* 任何"为了让它过"而放宽的断言,必须在代码注释里写清依据。
|
||
|
||
---
|
||
|
||
## 4. 工具二:`OffscreenChecks`(真实渲染,隐藏窗口)
|
||
|
||
**不显示窗口、不移动真实鼠标、不抢焦点**:窗口 `SW_HIDE` 创建,
|
||
"鼠标"通过 `Application::HandleMouseMove()` 注入合成消息,
|
||
像素从**后备缓冲区**读取(不是窗口 DC)。
|
||
|
||
```powershell
|
||
Tools\OffscreenChecks\Build\OffscreenChecks.exe -list
|
||
... -all # 依次跑全部检查
|
||
... -run smoke # 单个检查
|
||
... -run panels -mode record # 记录面板像素基线
|
||
... -run panels -mode compare # 与基线比对(重构前后 A/B)
|
||
... -run rowgeom -mode record # 记录逐行几何基线(默认数据)
|
||
... -run rowgeom -mode compare # 逐项比对几何(含每行每列矩形)
|
||
... -data "..\TestData\MF4-Record.MF4" -run rowgeom -mode record # 566 行规模基线
|
||
... -run frames -frames 12 # 连续抓 12 帧
|
||
... -data "..\TestData\MF4-Record.MF4" -run perf
|
||
... -out ..\Temp\tools\run1 # 换输出目录(避免覆盖基线)
|
||
... -no-png # 不落盘 PNG(只算哈希/耗时)
|
||
```
|
||
|
||
数据文件默认按顺序探测 `TestData/` 里的
|
||
`SCATTER_TEST.csv` → `[1].CSV` → `BIG_DATA.csv` → `Problem_Data.CSV`;
|
||
`TestData/` 不入库,缺失时请用 `-data` 指定。
|
||
|
||
> 默认路径(数据 / 输出 / 报告)**同时支持在 `Tools/` 下运行和在仓库根目录运行**
|
||
> (两种写法会自动回退),因此下面两种调用等价:
|
||
> ```powershell
|
||
> cd Tools ; .\OffscreenChecks\Build\OffscreenChecks.exe -all
|
||
> .\Tools\OffscreenChecks\Build\OffscreenChecks.exe -all
|
||
> ```
|
||
> 显式 `-data` / `-out` / `-report` 仍以**当前工作目录**为准。
|
||
|
||
### 4.1 检查项
|
||
|
||
| 检查 | 做了什么 | 断言 / 判据 |
|
||
| --- | --- | --- |
|
||
| `smoke` | 建隐藏窗口 → 加载 → 显示 3 条信号 → 渲染 → 抓帧 → 落盘 `smoke.png` | 客户区/绘图区尺寸合理、绘图区墨迹 > 1000 像素、**同一状态连续两帧逐像素相同**(渲染确定性) |
|
||
| `frames` | 沿信号列表逐行移动"鼠标",每帧抓图 + 区域哈希 + 耗时 | 平均帧 < 500ms、帧序列**至少出现一次可见变化**;输出 `frame-XXX.png` 供事后分析 |
|
||
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**、**行数组与行数形状一致**(按排版类别判定,见 §4.4);`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
|
||
| `panelscroll` | 全部信号合并到同一网格后,逐一滚动 7 种面板的内容区 | 可滚动时:滚到底/滚回顶、`scrollOffset` 夹到 `maxScroll`、滑块到达/返回轨道端点、内容区几何不动;不可滚动时不得出现滚动条。另守住行数组与行数形状一致 |
|
||
| `rowgeom` | **逐行几何基线**:7 个面板按「细节 / 规模」两层导出全部矩形并比对 | 细节层(6 信号 1 网格、参数行展开、3 个固定点、折起+展开)导出**每个矩形的每个字段**共 983 条键;规模层(全部信号 1 网格,MF4 = 566 行)导出每面板 FNV-1a 摘要 + 行数。`-mode record` / `-mode compare` 双向比对(MISSING / ADDED / CHANGED) |
|
||
| `edgestats` | 3 个阈值下的边沿计数(普通计数)+ 中心轴计数 | 与 `Checks.cpp` 内的**独立参考实现**(朴素逐点)**逐位相等**;同参数重复查询结果稳定 |
|
||
| `perf` | 整帧中位数、`EdgePanelLayoutFor` 单次耗时、198 个统计状态的整轮查询耗时 | 整帧 < 500ms、面板几何 < 200µs/次、统计查询 < 20µs/次;数字用于前后对比 |
|
||
|
||
### 4.2 像素基线的正确用法(A/B)
|
||
|
||
```powershell
|
||
# 1) 改动前:记录基线(同一台机器、同一 DPI、同一数据文件)
|
||
OffscreenChecks.exe -run panels -mode record -out ..\Temp\tools\panel-before
|
||
|
||
# 2) 改代码 + 重新构建本体与工具
|
||
|
||
# 3) 改动后:用同一输出目录比对
|
||
OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before
|
||
```
|
||
|
||
⚠️ **像素哈希是"本机、本 DPI、本数据文件"的基线**,跨机器/跨缩放不可比。
|
||
它适合"我改了一处,其它像素有没有动"的**本地 A/B**,不适合入库当黄金图。
|
||
|
||
### 4.3 几何基线(`rowgeom`)与像素基线的分工
|
||
|
||
| | 像素基线(`panels`) | 几何基线(`rowgeom`) |
|
||
| --- | --- | --- |
|
||
| 回答的问题 | “用户看到的画面变了吗” | “**哪个面板的哪一行哪一列**变了吗” |
|
||
| 粒度 | 整个面板区域一个哈希 | 每个矩形的每个字段(`mean.rowNameRects[001]=x,y,w,h`) |
|
||
| 定位能力 | 只能告诉你“变了” | 直接给出字段名、行号、新旧值 |
|
||
| 规模覆盖 | 固定场景 | 另加“全部信号 1 网格”(MF4 = 566 行)的摘要哈希 |
|
||
|
||
两者都要跑:像素基线防止“几何没变但画错了”,几何基线防止“画得没错但位置挪了”。
|
||
|
||
**基线是检测器,不是黄金图**:预期内的几何变化要重新 `-mode record`,并在提交信息里写明原因。
|
||
|
||
### 4.4 长度不变式为什幺要“按类别”判定
|
||
|
||
`EdgePanelLayout::rowRefs` 是**重载字段**:
|
||
|
||
* 列表式面板(均值/中心轴/弱峰/重采样/截取):`rowRefs[i]` = 第 i 行的信号;
|
||
* 「自由标记」面板:`rowRefs[i]` = 第 i 个**信号分组**,真正的条目行在 `markerRows`,
|
||
且只填 `rowNameRects` / `rowNameCloses`,其余 12 列**必须为空**。
|
||
|
||
所以判据按 `PanelStyleOf(kind).markerGroups` 分流(见 `Checks.cpp` 里 `panels` 的注释)。
|
||
早先“18 列全等”的写法对 MARK 面板是**错的**,只因既有场景恰好未触发才没暴露。
|
||
|
||
### 4.5 输出(全部在 `Temp/tools/`,已 gitignore)
|
||
|
||
| 文件 | 内容 |
|
||
| --- | --- |
|
||
| `offscreen-report.txt` | 完整控制台输出(含 `SUMMARY` 行) |
|
||
| `UnitTests-report.txt` | 单元测试报告 |
|
||
| `smoke.png` / `panels.png` / `perf.png` | 关键状态截图 |
|
||
| `frame-000.png` … | 连续帧(问题分析用) |
|
||
| `panels-baseline.txt` | 面板像素基线(`-mode record` 生成) |
|
||
| `rowgeom-baseline.txt` | 逐行几何明细基线(`-mode record` 生成,约 1000 条键) |
|
||
| `rowgeom-scale-baseline.txt` | 规模场景的每面板摘要哈希(`-mode record` 生成,7 条键) |
|
||
|
||
---
|
||
|
||
## 5. 怎么用它(按改动类型选最小集)
|
||
|
||
| 你改了什么 | 建议跑 |
|
||
| --- | --- |
|
||
| 任何数据层 / 解析 / 公式 / 变换 | `UnitTests.exe` |
|
||
| 绘图、面板、布局、缓存键 | `-run smoke -run panels`(+ 需要时 `-mode compare`) |
|
||
| **面板几何 / 行距 / 内边距 / 折起规则 / 布局算式** | `-run panels -mode compare` + `-run rowgeom -mode compare`(改动前先 `-mode record`) |
|
||
| 面板内容滚动 / 新增面板种类 | `-run panels -run panelscroll`(大文件加 `-data ..\TestData\MF4-Record.MF4`) |
|
||
| 边沿统计 / 中心轴 / 扫描预算 | `-run edgestats -run perf` |
|
||
| 性能相关 | `-run perf`(前后各跑一次,比 `us/query` 与 `median ms`) |
|
||
| 完全不确定影响面 | `-all`(约几秒到几十秒)+ `UnitTests.exe` |
|
||
|
||
> 用户明确要求:**除非要求"全测一遍",否则只跑针对性检查**。
|
||
|
||
---
|
||
|
||
## 6. 新增一个检查 / 一个工具
|
||
|
||
### 6.1 新增离屏检查
|
||
|
||
1. 在 `OffscreenChecks/Src/Checks.cpp` 里写 `int CheckXxx(Session&, const CheckOptions&)`,
|
||
用 `Expect()` / `ExpectEqual()` 记录断言,末尾 `return CheckFailed();`
|
||
2. 在 `AvailableChecks()` 的列表里追加一行 `{ "xxx", "one-line summary", CheckXxx }`
|
||
3. 在 README 的 §4.1 表格里补一行(**必须**)
|
||
|
||
### 6.2 新增工具
|
||
|
||
1. 建 `Tools/<NewTool>/{Inc,Src}`
|
||
2. 在 `Tools/Makefile` 的 `TOOLS` 里追加 `<NewTool>`(目录名 = 目标名)
|
||
3. 在本 README 的 §1 / §2 / §5 里补说明
|
||
|
||
---
|
||
|
||
## 7. 已知限制与坑
|
||
|
||
* **工具是白盒**:`OffscreenSupport.hpp` 用 `#define private public` 放宽访问级别,
|
||
为了读后备缓冲区 / 直接调面板几何。产品接口不因测试而放宽。
|
||
* **必须先构建本体**:工具链接 `../Build/Release/*.o`,本体没构建会给出明确警告。
|
||
* **像素基线不可跨机复现**:DPI、字体、主题不同则哈希不同。
|
||
* **`frames` 里个别帧可能重复**(鼠标落在筛选框/空白带),因此判据是"至少一次可见变化"。
|
||
* **窗口一律 `SW_HIDE`**:不要为了"看看效果"改成显示窗口,那会占用用户屏幕。
|
||
需要看图请直接打开 `Temp/tools/*.png`。
|
||
* **进程退出用 `std::_Exit()`**:`Application` / `PlotView` 是单例,
|
||
若走正常析构会在 `GdiplusShutdown` 之后访问 GDI+ 对象(历史退出码 `0xC0000005`)。
|
||
* **报告/截图不要提交**:全部落在 `Temp/`(已忽略)。`Tools/*/Build/` 也已忽略,
|
||
其它协作者本地 `make -C Tools` 即可重建。
|