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

+147
View File
@@ -0,0 +1,147 @@
/**
* @file FormulaEvalTests.cpp
* @brief `Utilities/FormulaEval` 的语法 / 语义回归。
*
* 重点用例:
* * `program.matches_pointwise` —— 批量求值(`FormulaProgram`)与逐点求值
* (`EvaluateFormula`)必须**逐位相同**。第 32 轮的性能优化(常量折叠、幂次改写、
* `always_inline`)只允许改速度,任何一位差异都是回归。
* * 语法边界:`^` 右结合、`-2^2 = -4`、单独的 `f` 非法、`2e` 非法。
*/
#include <cmath>
#include <random>
#include <string>
#include <vector>
#include "DataModel.hpp"
#include "FormulaEval.hpp"
#include "UnitTestFramework.hpp"
namespace
{
/** @brief 取 double 的位模式。 */
std::uint64_t BitsOf(const double value)
{
std::uint64_t bits = 0;
__builtin_memcpy(&bits, &value, sizeof(bits));
return bits;
}
} // namespace
JDI_TEST(formula_syntax_acceptance)
{
std::wstring error;
// 合法写法
static const wchar_t* const valid[] = {
L"f(t)", L"f(t)*2+1", L"f(t)^2", L"-f(t)", L"e*pi", L"2^-1", L"X", L"x",
L"(f(t)-1)/(f(t)+1)", L"2*pi*f(t)",
};
for (const wchar_t* const text : valid)
{
JDI_REQUIRE("valid formula", jdi::ValidateFormula(text, &error));
}
// 非法写法
static const wchar_t* const invalid[] = {
L"f", L"f(", L"f(t)+", L"f(t)*", L"()", L"2e", L"f(t) ** 2", L"*2",
};
for (const wchar_t* const text : invalid)
{
const bool accepted = jdi::ValidateFormula(text, &error);
if (accepted)
{
std::printf(" [INFO] unexpectedly accepted: \"%ls\"\n", text);
}
JDI_REQUIRE("invalid formula must be rejected", !accepted);
}
// 空串 = 不映射(合法)
JDI_CHECK(jdi::ValidateFormula(L"", &error));
}
JDI_TEST(formula_semantics)
{
// `^` 右结合:2^3^2 = 2^(3^2) = 512
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"2^3^2", 0.0), 512.0, 1e-9);
// 一元负号优先于幂:-2^2 = -(2^2) = -4
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"-2^2", 0.0), -4.0, 1e-12);
// 指数的负号:2^-1 = 0.5
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"2^-1", 0.0), 0.5, 1e-12);
// 常量 e / pi(大小写均可)
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"e", 0.0), 2.718281828459045, 1e-15);
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"pi", 0.0), 3.141592653589793, 1e-15);
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"PI", 0.0), 3.141592653589793, 1e-15);
// 恒等写法
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"f(t)", 3.25), 3.25, 0.0);
// 真实使用过的表达式(第 32 轮用户输入):f(t)*2/1.25+14.2,f(2) = 3.2 + 14.2
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"f(t)*2/1.25+14.2", 2.0), 17.4, 1e-12);
// 幂次改写的等价性(实现会把 x^2/x^3/x^0.5 换成乘法/sqrt)
static const double points[] = {-3.5, -1.0, -0.25, 0.0, 0.5, 1.0, 2.0, 7.25, 1.0e6};
for (const double point : points)
{
JDI_CHECK_BIT_EQUAL(jdi::EvaluateFormula(L"f(t)^2", point), point * point);
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"f(t)^0.5", point < 0.0 ? -point : point),
std::sqrt(point < 0.0 ? -point : point), 0.0);
}
}
JDI_TEST(formula_identity_helpers)
{
// `f(t)` 与空串等价(第 32 轮硬规则:点进输入框再点出去不得写入"恒等公式")
JDI_CHECK(jdi::FormulaIsIdentity(L""));
JDI_CHECK(jdi::FormulaIsIdentity(L"f(t)"));
JDI_CHECK(!jdi::FormulaIsIdentity(L"f(t)*2"));
jdi::SignalStyle plain;
JDI_CHECK(!plain.HasFormula());
plain.formula = L"f(t)";
JDI_CHECK(!plain.HasFormula());
plain.formula = L"f(t)*2";
JDI_CHECK(plain.HasFormula());
// 归一化:`X` / `F(T)` 统一写成 `f(t)`;单独的 `f` **不**归一化(非法写法,交给校验报错)
JDI_CHECK_WIDE_EQ(jdi::NormalizeFormulaText(L"X"), std::wstring(L"f(t)"));
JDI_CHECK_WIDE_EQ(jdi::NormalizeFormulaText(L"F(T)"), std::wstring(L"f(t)"));
JDI_CHECK_WIDE_EQ(jdi::NormalizeFormulaText(L"f"), std::wstring(L"f"));
}
JDI_TEST(formula_program_matches_pointwise)
{
static const wchar_t* const formulas[] = {
L"f(t)*2/1.25+14.2", L"f(t)^2", L"f(t)^3", L"(f(t)-1)/(f(t)+1)",
L"2*pi*f(t)+e", L"-f(t)*0.5", L"f(t)^0.5", L"f(t)-2",
};
std::mt19937 rng(20261105u);
std::uniform_real_distribution<double> distribution(-500.0, 500.0);
for (const wchar_t* const formula : formulas)
{
jdi::FormulaProgram program(formula);
JDI_REQUIRE("program must compile", program.Valid());
for (int iteration = 0; iteration < 4000; ++iteration)
{
const double x = distribution(rng);
const double pointwise = jdi::EvaluateFormula(formula, x);
const double batched = program.Evaluate(x);
if (BitsOf(pointwise) != BitsOf(batched))
{
std::printf(" [INFO] %ls x=%.17g pointwise=%.17g batched=%.17g\n", formula, x, pointwise,
batched);
JDI_CHECK_BIT_EQUAL(batched, pointwise);
break;
}
}
}
}
JDI_TEST(formula_invalid_falls_back_safely)
{
// 非法表达式:求值必须**原样返回采样值**(波形照常显示,不会因为写错公式变空)
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"f(t)@@@@", 1.5), 1.5, 0.0);
JDI_CHECK_NEAR(jdi::EvaluateFormula(L"f(t)@@@@", -2.25), -2.25, 0.0);
}