Files
Just-DataInspector/Tools/README.md
T
DeepSeek V4.1 Flash 5c48bf922e review(follow-ups): 落地上轮建议并给出实测结论(#2/#4 回退、#3 不重写)
一、Tools 完善
* OffscreenChecks `edgestats`:改为自动挑"最有起伏"的信号。旧实现固定取
  `signals[0]`,而 MF4 的第一条信号是常量(6000)⇒ 三个阈值的断言退化成
  "0 == 0" 的空转。现在扫描前 200 条挑 `max-min` 最大者,SCATTER_TEST 上
  三个阈值分别得到 (20,19)/(20,19)/(20,19),与独立参考实现逐位相同。
* UnitTests 新增 `stdio_mingw_vs_ucrt_formats`:把 MinGW ANSI stdio 与系统
  UCRT 的 printf 在**本程序实际用到的全部格式**(`%.*f` / `%.6g` / `%g` /
  `%.4e` / `%.2f %ls` / `%llu` / `%lld` / `%02X` / `%.2f%%`)上做逐字符比对,
  13.6 万次调用 **0 处差异**(覆盖 1e-40~1e40)。它是"把 swprintf 让给 UCRT"
  这条体积优化的验收标准。
* Tools/README.md:同步新增用例与断言数量(219 项 / 18 用例)。

二、第 33 轮报告 §10 后续建议的结论(详细数字见 Progress 第 34 轮)

1. **`EdgeScanFor` 状态查找哈希化:实测后回退。**
   实现方式为 `deque` + `unordered_multimap<keyHash, EdgeScanState*>`(桶内仍
   逐字段比对,碰撞安全;新建/淘汰/定向作废/清空四处同步索引),功能与像素
   验证全绿。但 198 个状态的整轮查询只从 0.097 降到 0.073 us/query(一轮省
   ≈4.8 us),而产物体积 +5,632 B。⇒ 回退。
   同时修正报告的量级:原估"288 信号 ≈0.3ms/帧",实测 ≈20us/帧。

2. **`EdgePanelLayoutFor` 表驱动化:不做。** 实测 1.9~2.5 us/次(每帧约 14 次
   ≈30us),不是热点;第 33 轮已消除"按种类分派"的重复,剩余 957 行是几何算式
   本身,无法表驱动。本轮改为交付"让这类重写可验证"的手段:
   `-run panels -mode record/compare` 逐面板比对几何 + 像素哈希。

3. **`wcstod` 自写替身:实测 0 字节收益。** 只加
   `-D__USE_MINGW_ANSI_STDIO=0` 全量重编后产物仍是 788,480 B、`mingw_wpformat.o`
   仍被链接。原因是预编译 libstdc++ 自己引用 `__mingw_swprintf`:只要还调用
   `std::to_wstring` / `std::to_string`(全仓约 80 处,若干处实参是浮点),
   gdtoa 必然被链入。⇒ 回退,并在 Makefile 留下说明防止重复尝试。

三、文档
* `Documents/Code_Review_Plan.md` 新增 §六(第 34 轮节点与验收标准)。
* `Documents/Code_Review_Progress.md` 新增第 34 轮逐节点记录(含全部实测数字)。
* `Documents/Code_Review_Report.md` 新增 §11(结论表 + 两处量级修正 + 更新的后续建议)。
* `Documents/agent_mem.md` 新增第 34 轮规则:Tools 约定、两个构建陷阱、
  两条"别再重复做"的实验结论。

验证:
* `make release`(删 .o 全量)零 error / 零 warning,产物 **788,480 B**(与改动前一致)。
* `make -C Tools` 零 error / 零 warning。
* `UnitTests.exe` → checks=219 failed=0 cases=18。
* `OffscreenChecks.exe -all` → 5/5 检查 PASS,checks=71 failed=0。
2026-11-04 11:52:10 +08:00

240 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 轮核心缓存的正确性 |
| `stdio_mingw_vs_ucrt_formats` | MinGW ANSI stdio 与系统 UCRT 的 printf 在**本程序用到的全部格式**上逐字符比对(13.6 万次调用) | 评估“把 `swprintf` 交给 UCRT”是否可行(当前 0 差异);将来真做迁移就是验收标准 |
### 3.2 验收标准
* **零失败**(`result=PASS`)才算通过;
* 断言数量只增不减(当前 **219** 项 / 18 个用例):删断言必须说明理由;
* 任何"为了让它过"而放宽的断言,必须在代码注释里写清依据。
---
## 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` 即可重建。