背景:第 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。
85 lines
3.1 KiB
C++
85 lines
3.1 KiB
C++
/**
|
||
* @file CheckRunner.hpp
|
||
* @brief 离屏检查的注册表与统一断言/输出接口。
|
||
*
|
||
* 每个检查都是一个签名为 `int (const CheckOptions&)` 的函数,
|
||
* 通过 `Expect()` 记录断言;返回值只用于"该检查自己额外发现的失败数"(通常直接返回全局失败数)。
|
||
*/
|
||
|
||
#ifndef __CHECKRUNNER_HPP__
|
||
#define __CHECKRUNNER_HPP__
|
||
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "OffscreenSupport.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
namespace tools
|
||
{
|
||
/** @brief 一次检查运行的参数(来自命令行)。 */
|
||
struct CheckOptions
|
||
{
|
||
std::string dataPath; /**< 数据文件路径(ASCII 或本地代码页)。 */
|
||
std::string outDir; /**< 输出目录(默认 `../Temp/tools`)。 */
|
||
std::string mode; /**< `verify`(默认)/ `record` / `compare`。 */
|
||
int frames = 8; /**< 连续帧数量(仅 `frames` 检查使用)。 */
|
||
bool savePng = true; /**< 是否把抓到的帧落盘(默认开,便于事后分析)。 */
|
||
};
|
||
|
||
/** @brief 一个可运行的检查。 */
|
||
struct CheckDescriptor
|
||
{
|
||
const char* name = ""; /**< 检查名(命令行 `-run <name>` 使用)。 */
|
||
const char* summary = ""; /**< 一句话说明(ASCII,`-list` 打印)。 */
|
||
/** 检查实现:`session` 已 `Start()` 并加载好数据文件;返回该检查的失败断言数。 */
|
||
int (*run)(Session& session, const CheckOptions& options) = nullptr;
|
||
};
|
||
|
||
/** @brief 全部已注册检查(顺序即执行顺序)。 */
|
||
const std::vector<CheckDescriptor>& AvailableChecks();
|
||
|
||
/** @brief 按名字查检查;找不到返回 nullptr。 */
|
||
const CheckDescriptor* FindCheck(const std::string& name);
|
||
|
||
/** @brief 统一输出(同时写标准输出与报告文件,由 `Main.cpp` 提供实现)。 */
|
||
void EmitLine(const char* format, ...);
|
||
|
||
/** @brief 断言总数。 */
|
||
int& CheckTotal();
|
||
|
||
/** @brief 失败断言数。 */
|
||
int& CheckFailed();
|
||
|
||
/** @brief 记录一条断言(失败会打印 `[FAIL]` 并累加失败数)。 */
|
||
void Expect(const bool ok, const std::string& what);
|
||
|
||
/** @brief 记录一条"整数相等"断言。 */
|
||
void ExpectEqual(const long long actual, const long long expected, const std::string& what);
|
||
|
||
/** @brief 记录一条数值近似断言。 */
|
||
void ExpectNear(const double actual, const double expected, const double tolerance,
|
||
const std::string& what);
|
||
|
||
/** @brief 把 `gd::RectF` 转成整像素矩形(向外取整,保证覆盖)。 */
|
||
inline PixelRect ToPixelRect(const gd::RectF& rect)
|
||
{
|
||
PixelRect result;
|
||
result.x = static_cast<int>(std::floor(rect.X));
|
||
result.y = static_cast<int>(std::floor(rect.Y));
|
||
result.width = static_cast<int>(std::ceil(rect.X + rect.Width)) - result.x;
|
||
result.height = static_cast<int>(std::ceil(rect.Y + rect.Height)) - result.y;
|
||
return result;
|
||
}
|
||
|
||
/** @brief 读取一个文本文件(失败返回空串)。 */
|
||
std::string ReadTextFile(const std::string& path);
|
||
|
||
/** @brief 写一个文本文件(自动建目录)。 */
|
||
bool WriteTextFile(const std::string& path, const std::string& content);
|
||
} // namespace tools
|
||
} // namespace jdi
|
||
|
||
#endif // __CHECKRUNNER_HPP__
|