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。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 11:41:08 +08:00
1 parent c5d7a6fb91
commit 97e9163314
14 files changed
+3515 -2

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
# =============================================================================
# Tools/Makefile —— 验证工具(单元测试 / 离屏验证)的统一构建入口
# =============================================================================
#
# 职责边界(很重要,别混):
# * 根目录 `Makefile` **只负责应用本体**(`Build/Release`、`Build/Debug`)。
# 它按目录通配收集源文件,`Tools/` 不在其 `CXX_SOURCES_PATHS` 里,
# 因此**新增工具不需要改根 Makefile**,也不会把工具代码编进应用本体。
# * 本文件负责 `Tools/` 下所有工具的编译 / 链接 / 清理。
# 工具是"白盒验证":直接链接应用本体已构建好的目标文件
# (`../Build/Release/*.o`,排除入口 `Main.o`),因此**必须先构建本体**。
#
# 用法(在 `Tools/` 目录下执行,或 `make -C Tools ...`):
# make 构建全部工具
# make UnitTests 只构建单元测试
# make OffscreenChecks 只构建离屏验证工具
# make clean 删除所有工具的构建目录(**不动**应用本体)
# make help 打印帮助
#
# 目录约定:
# Tools/<Tool>/Inc 仅头文件
# Tools/<Tool>/Src 仅实现(每个 `.cpp` 都会编译)
# Tools/<Tool>/Build 产物(与 Inc/Src 同级;已被 .gitignore 忽略)
#
# ⚠️ 工具目标文件必须与应用本体使用**同一套 ABI 相关开关**,否则链接会失败:
# `-fno-exceptions`(本体 `NO_EXCEPTIONS = 1`)
# `-fno-rtti` (本体 `NO_RTTI = 1`,本仓已无 dynamic_cast)
# `-flto -flto-partition=one`(本体 `LTO = 1`,目标文件是 LTO 字节码)
# =============================================================================
# ---- 工具清单(新增工具只需在这里追加,并保证目录名 = 目标名)--------------
TOOLS := UnitTests OffscreenChecks
# ⚠️ 必须显式指定默认目标:下面用 `$(foreach)` 生成的工具目标会先于 `all` 定义,
# 而 make 会把"第一个出现的显式目标"当作默认目标 ⇒ 直接 `make` 只会建第一个工具。
.DEFAULT_GOAL := all
# 工具内构建目录名(与 Inc / Src 同级)
BUILD_DIR_NAME := Build
# ---- 工具链 ----------------------------------------------------------------
C_COMPILER := gcc
CXX_COMPILER := g++
C_STD := c17
CXX_STD := c++17
INPUT_CHAR_ENCODING := UTF-8
OUTPUT_CHAR_ENCODING := GBK
OUTPUT_WCHAR_ENCODING := UTF-16LE
# 工具自身优化等级:验证工具追求"跑得快",不参与体积红线
TOOL_OPT := -O2
TOOL_DEBUG_INFO := -g
# ---- 与被测应用本体保持一致的 ABI 开关-------------------------------------
NO_EXCEPTIONS := 1
NO_RTTI := 1
LTO := 1
# ---- 应用本体 --------------------------------------------------------------
APP_BUILD_DIR := ../Build/Release
# 排除入口 `Main.o`(它定义 `WinMain`,与工具的 `main` 不冲突但不需要)
APP_OBJECTS := $(filter-out $(APP_BUILD_DIR)/Main.o,$(wildcard $(APP_BUILD_DIR)/*.o))
# ---- 头文件目录(与应用本体一致)------------------------------------------
PROJECT_INCLUDES := \
../Utilities/Inc \
../Resources/Inc \
../Components/Main/Inc \
../Components/Shared/Inc \
../Components/Application/Inc \
../Components/TabBar/Inc \
../Components/Toolbar/Inc \
../Components/SignalPanel/Inc \
../Components/PlotView/Inc
# ---- 链接库(与应用本体一致)----------------------------------------------
LINK_LIBS := \
-lgdi32 \
-lgdiplus \
-lmsimg32 \
-lcomdlg32 \
-ld3d11 \
-ldwmapi \
-ld3dcompiler
# ---- 编译 / 链接选项 -------------------------------------------------------
# ⚠️ 这里用 `=`(递归展开)而不是 `:=`:`-MF"$(@:%.o=%.d)"` 里的 `$@`
# 必须在**展开 recipe 时**才求值(那时才确定是哪一个目标文件)。
BASE_CXXFLAGS = \
-D_UNICODE -DUNICODE \
$(addprefix -I,$(PROJECT_INCLUDES)) \
-std=$(CXX_STD) \
-finput-charset=$(INPUT_CHAR_ENCODING) \
-fexec-charset=$(OUTPUT_CHAR_ENCODING) \
-fwide-exec-charset=$(OUTPUT_WCHAR_ENCODING) \
-static \
-Wall \
$(TOOL_OPT) \
$(TOOL_DEBUG_INFO) \
-MMD -MP -MF"$(@:%.o=%.d)"
ifeq ($(strip $(NO_EXCEPTIONS)),1)
BASE_CXXFLAGS += -fno-exceptions
endif
ifeq ($(strip $(NO_RTTI)),1)
BASE_CXXFLAGS += -fno-rtti
endif
BASE_LDFLAGS =
ifeq ($(strip $(LTO)),1)
# 本体目标文件是 LTO 字节码 ⇒ **链接**必须带 `-flto -flto-partition=one`。
# ⚠️ 工具自身源文件**不要**加 `-flto`:把工具代码也交给 LTRANS 会在
# `choose_baseaddr`(i386.cc:7447)处 ICE(GCC 15.2.0 实测),
# 而工具体量很小,根本不需要跨单元内联。
BASE_LDFLAGS += -flto -flto-partition=one
endif
# ---- 每个工具的源文件 / 目标文件 / 产物 ------------------------------------
$(foreach tool,$(TOOLS),$(eval $(tool)_SRC := $(wildcard $(tool)/Src/*.cpp)))
$(foreach tool,$(TOOLS),$(eval $(tool)_OBJ := $(patsubst $(tool)/Src/%.cpp,$(tool)/$(BUILD_DIR_NAME)/%.o,$($(tool)_SRC))))
$(foreach tool,$(TOOLS),$(eval $(tool)_EXE := $(tool)/$(BUILD_DIR_NAME)/$(tool).exe))
# 本体的目标文件缺失时给出明确提示(而不是让链接器报一堆 undefined reference)
ifeq ($(strip $(APP_OBJECTS)),)
$(warning ********************************************************************)
$(warning 未找到应用本体目标文件:$(APP_BUILD_DIR)/*.o)
$(warning 请先在项目根目录构建本体,例如:make release -j8)
$(warning ********************************************************************)
endif
# ---- 每个工具的规则(模板)-------------------------------------------------
.SUFFIXES:
define TOOL_RULES
.PHONY: $(1)
$(1): $$($(1)_EXE)
$$($(1)_EXE): $$($(1)_OBJ) $(APP_OBJECTS) Makefile
@echo ====== [$(1)] All File Compiled. Now Linking... ======
$(CXX_COMPILER) $$($(1)_OBJ) $(APP_OBJECTS) -static $(TOOL_DEBUG_INFO) -o $$@ $(LINK_LIBS) $(BASE_LDFLAGS) -mconsole
@echo ====== [$(1)] Link Finished: $$@ ======
$$($(1)_OBJ): $(1)/$(BUILD_DIR_NAME)/%.o: $(1)/Src/%.cpp Makefile | $(1)/$(BUILD_DIR_NAME)
@echo ====== [$(1)] C++ Source File "$$<" Compiling... ======
$(CXX_COMPILER) -c $$(BASE_CXXFLAGS) -I$(1)/Inc $$< -o $$@
$(1)/$(BUILD_DIR_NAME):
@powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path '$$@' | Out-Null"
endef
$(foreach tool,$(TOOLS),$(eval $(call TOOL_RULES,$(tool))))
# ---- 聚合目标 --------------------------------------------------------------
.PHONY: all clean help unit offscreen
all: $(TOOLS)
@echo ====== [Tools] All Tools Built ======
# 便利别名
unit: UnitTests
offscreen: OffscreenChecks
# 各工具的构建目录(空格分隔 → PowerShell 里 `-split ' '` 成数组)
TOOL_BUILD_DIRS := $(strip $(foreach tool,$(TOOLS),$(tool)/$(BUILD_DIR_NAME)))
clean:
@echo ====== [Tools] Cleaning Build Directories... ======
@powershell -NoProfile -Command "'$(TOOL_BUILD_DIRS)' -split ' ' | ForEach-Object { if (Test-Path $$_) { Remove-Item -Recurse -Force $$_ } }"
@echo ====== [Tools] Clean Accomplished ======
help:
@echo Tools/Makefile —— 验证工具构建入口
@echo.
@echo make 构建全部工具:$(TOOLS)
@echo make UnitTests 只构建单元测试(Tools/UnitTests)
@echo make OffscreenChecks 只构建离屏验证工具(Tools/OffscreenChecks)
@echo make clean 删除各工具自己的 Build/ 目录
@echo make help 显示本帮助
@echo.
@echo 依赖:应用本体已构建(../Build/Release/*.o)
# ---- 头文件依赖(与根 Makefile 同一套 `-MMD -MP` 机制)--------------------
# ⚠️ 删掉/重命名头文件后若出现"莫名为 undefined reference",先 `make clean`。
-include $(foreach tool,$(TOOLS),$($(tool)_OBJ:.o=.d))