Files
Just-DataInspector/Tools/README.md
T
DeepSeek V4.1 Flash 97e9163314 test(tools): 重建单元测试与离屏验证工具(Tools/ 入库)
背景:第 33 轮 Code Review 的三项后续建议(EdgeScanFor 查找哈希化、
EdgePanelLayoutFor 表驱动化、wcstod 自写替身)都卡在"没有可自证的验证手段"上。
本轮先补齐基础设施,再动被建议的代码。

一、Tools/ 从忽略改为入库(只忽略各工具的 Build/ 与 Temp/ 临时产物)
* `.gitignore`:删除 `Tools/` 整目录忽略,改为 `Tools/**/Build/`;
  截图 / 基线 / 报告一律写 `Temp/tools/`(`Temp/` 早已忽略)。

二、新增 Tools/Makefile(统一构建入口,不碰应用本体)
* 目标:`make` 构建全部、`make UnitTests` / `make OffscreenChecks` 单个、
  `make clean` 清工具产物、`make help`。
* 工具链接应用本体 `../Build/Release/*.o`(排除入口 `Main.o`),
  因此 ABI 开关必须与本体一致:`-fno-exceptions` / `-fno-rtti` /
  链接 `-flto -flto-partition=one`。
* 工具源文件自身**不**编译成 LTO 字节码(否则 GCC 15.2.0 在
  `choose_baseaddr` 处 ICE),只在链接阶段带 LTO 参数。
* `-MMD -MP` + `-include`:改头文件 / 增删源文件自动重编重链。
* 修正一个构建陷阱:`$(foreach)` 生成的工具目标会早于 `all`,
  make 会把第一个显式目标当默认目标 ⇒ 显式 `.DEFAULT_GOAL := all`。

三、Tools/UnitTests:纯数据层单元测试(218 项断言,17 个用例)
* 极简自研框架(`Inc/UnitTestFramework.hpp`):无异常、无 RTTI、
  支持布尔 / 整数 / 近似 / **逐位相等** / 宽字符串断言。
* 覆盖:`TryParseDouble` 与 libc `strtod` 逐位一致性(6 万条随机串)、
  接受性语义、`FormatDecimal*` 永不科学计数法、`NiceStep/BuildTicks`、
  公式语法/语义/归一化、批量求值 `FormulaProgram` 与逐点求值逐位一致、
  `ApplyDrawTransform` 顺序、`TransformValueRange`、
  `ComputeMovingAverageIterated` 阶数等价性、
  `DataSet::FormulaTransformFor` 的值域与分组极值(逐点参考实现比对)。

四、Tools/OffscreenChecks:隐藏窗口离屏验证(5 个检查)
* 窗口 `SW_HIDE`;"鼠标"用 `Application::HandleMouseMove()` 合成注入;
  像素从后备缓冲区读取(不依赖窗口可见性),可保存 PNG 与计算区域 FNV 哈希。
* `smoke`:初始化 / 加载 / 渲染 / 落盘,并断言"同状态两帧逐像素相同"。
* `frames`:连续帧抓取(PNG + 哈希 + 耗时),用于问题分析。
* `panels`:7 个实验性面板全部展开后的几何不变式 + **像素基线 A/B**
  (`-mode record` / `-mode compare`)。
* `edgestats`:边沿统计与工具内**独立参考实现**逐位比对,
  覆盖阈值变化与重复查询稳定性。
* `perf`:整帧中位数、`EdgePanelLayoutFor` 单次耗时、
  198 个统计状态的整轮查询耗时(用于优化前后对比)。

五、文档
* `Tools/README.md`:目录结构、构建/运行方式、每个工具的作用、测试项与
  验收标准、临时产物位置、A/B 基线用法、扩展步骤与已知限制。

实测(本机 GCC 15.2.0 UCRT / 125% DPI):
* `UnitTests.exe` → `checks=218 failed=0`
* `OffscreenChecks -all` → 全部 PASS;`SCATTER_TEST.csv` 整帧 ~5ms;
  `MF4-Record.MF4`(283 信号)198 个统计状态整轮查询 0.097 us/query。
2026-11-04 11:41:08 +08:00

239 lines
12 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 轮核心缓存的正确性 |
### 3.2 验收标准
* **零失败**(`result=PASS`)才算通过;
* 断言数量只增不减(当前 **218** 项):删断言必须说明理由;
* 任何"为了让它过"而放宽的断言,必须在代码注释里写清依据。
---
## 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` 指定。
### 4.1 检查项
| 检查 | 做了什么 | 断言 / 判据 |
| --- | --- | --- |
| `smoke` | 建隐藏窗口 → 加载 → 显示 3 条信号 → 渲染 → 抓帧 → 落盘 `smoke.png` | 客户区/绘图区尺寸合理、绘图区墨迹 > 1000 像素、**同一状态连续两帧逐像素相同**(渲染确定性) |
| `frames` | 沿信号列表逐行移动"鼠标",每帧抓图 + 区域哈希 + 耗时 | 平均帧 < 500ms、帧序列**至少出现一次可见变化**;输出 `frame-XXX.png` 供事后分析 |
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**;`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
| `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 edgestats -run perf` |
| 性能相关 | `-run perf`(前后各跑一次,比 `us/query` 与 `median ms`) |
| 完全不确定影响面 | `-run -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` 即可重建。