Files
Just-DataInspector/Tools/README.md
T
DeepSeek V4.1 Flash 221f9d2410 test(tools): 行数组不变式收紧为"18 个全部等于行数"
背景:`AppendRow()`(提交 85aaed5)已经成为新增面板行的唯一入口,
18 个平行数组由它一次补齐。因此旧的宽松判据("13 个必须等长 +
4 个重采样专属列允许为空")不再需要 —— 任何一列短于行数,
就是"有人绕过 AppendRow 手写 push"的直接证据。

改动:
* `panels` 检查:改为断言 18 个数组长度**全部**等于 `rowRefs.size()`,
  失败时打印全部 18 列的长度便于定位。
* `panelscroll` 检查:同样收紧(大文件 566 行时才会暴露的问题)。
* `Tools/README.md`:同步不变式描述。

文档:
* `Documents/Code_Review_Plan.md` 新增 §八(第 36 轮节点与硬约束)。
* `Documents/Code_Review_Progress.md` 新增第 36 轮(两次重构 + 三个搬移陷阱)。
* `Documents/Code_Review_Report.md` 新增 §13(拆分职责表、为何不做完整合并、
  失败可见性对比、后续建议)。
* `Documents/agent_mem.md` 新增第 36 轮规则。

验证:
* `make release` 全量零 error / 零 warning,产物 **785,408 B**。
* `UnitTests` → checks=260 failed=0;`OffscreenChecks -all` → 6/6 PASS,checks=101 failed=0。
* `panels -mode compare` → 7 个面板像素哈希与几何逐项不变。
* `panelscroll`(MF4 566 行)→ 18 个行数组全部 = 566。
2026-11-04 15:20:07 +08:00

251 lines
13 KiB
Markdown
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.
# `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 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 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**、**18 个行数组长度全部等于行数**;`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
| `panelscroll` | 全部信号合并到同一网格后,逐一滚动 7 种面板的内容区 | 可滚动时:滚到底/滚回顶、`scrollOffset` 夹到 `maxScroll`、滑块到达/返回轨道端点、内容区几何不动;不可滚动时不得出现滚动条。另守住 18 个行数组与行数完全一致 |
| `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 输出(全部在 `Temp/tools/`,已 gitignore)
| 文件 | 内容 |
| --- | --- |
| `offscreen-report.txt` | 完整控制台输出(含 `SUMMARY` 行) |
| `UnitTests-report.txt` | 单元测试报告 |
| `smoke.png` / `panels.png` / `perf.png` | 关键状态截图 |
| `frame-000.png` … | 连续帧(问题分析用) |
| `panels-baseline.txt` | 面板像素基线(`-mode record` 生成) |
---
## 5. 怎么用它(按改动类型选最小集)
| 你改了什么 | 建议跑 |
| --- | --- |
| 任何数据层 / 解析 / 公式 / 变换 | `UnitTests.exe` |
| 绘图、面板、布局、缓存键 | `-run smoke -run panels`(+ 需要时 `-mode compare`) |
| 面板内容滚动 / 面板几何 / 新增面板种类 | `-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` 即可重建。