背景:第 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。
199 lines
6.6 KiB
C++
199 lines
6.6 KiB
C++
/**
|
||
* @file UnitTestFramework.hpp
|
||
* @brief 极简单元测试框架(无异常、无 RTTI,与项目本体 ABI 一致)。
|
||
*
|
||
* 设计取舍:
|
||
* * **不引入第三方框架**(Catch2 / gtest 会带来几 MB 的静态库与异常依赖);
|
||
* * 项目本体用 `-fno-exceptions` 构建,因此断言失败只能"记录 + 继续",
|
||
* 不允许 `throw`;
|
||
* * 每个 `.cpp` 用 `JDI_TEST(名字)` 注册用例,`Main.cpp` 按注册顺序执行。
|
||
*
|
||
* 输出约定(供 CI / Agent 读取):
|
||
* * 逐用例一行 `[RUN ] <name>`,失败时一行 `[FAIL] <expr> (file:line)`;
|
||
* * 结尾一行 `SUMMARY checks=<n> failed=<m> result=<PASS|FAIL>`。
|
||
* ⚠️ 所有输出保持 **ASCII**:控制台代码页为 GBK,中文只在注释里出现。
|
||
*/
|
||
|
||
#ifndef __UNITTESTFRAMEWORK_HPP__
|
||
#define __UNITTESTFRAMEWORK_HPP__
|
||
|
||
#include <cstdio>
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace jdi
|
||
{
|
||
namespace test
|
||
{
|
||
/** @brief 一个注册在案的测试用例。 */
|
||
struct TestCase
|
||
{
|
||
const char* name = ""; /**< 用例名(打印用,建议 `模块.行为`)。 */
|
||
void (*function)() = nullptr; /**< 用例体。 */
|
||
};
|
||
|
||
/** @brief 全局用例表。 */
|
||
inline std::vector<TestCase>& Registry()
|
||
{
|
||
static std::vector<TestCase> registry;
|
||
return registry;
|
||
}
|
||
|
||
/** @brief 断言总数。 */
|
||
inline int& TotalChecks()
|
||
{
|
||
static int value = 0;
|
||
return value;
|
||
}
|
||
|
||
/** @brief 失败断言数。 */
|
||
inline int& FailedChecks()
|
||
{
|
||
static int value = 0;
|
||
return value;
|
||
}
|
||
|
||
/** @brief 当前用例是否已失败(用于给出汇总提示)。 */
|
||
inline bool& CaseFailed()
|
||
{
|
||
static bool value = false;
|
||
return value;
|
||
}
|
||
|
||
/** @brief 注册用例(由 `JDI_TEST` 宏在静态初始化期调用)。 */
|
||
inline bool Register(const char* name, void (*function)())
|
||
{
|
||
Registry().push_back(TestCase{name, function});
|
||
return true;
|
||
}
|
||
|
||
/** @brief 记录一次布尔断言。 */
|
||
inline void Check(bool ok, const char* expression, const char* file, int line)
|
||
{
|
||
++TotalChecks();
|
||
if (!ok)
|
||
{
|
||
++FailedChecks();
|
||
CaseFailed() = true;
|
||
std::printf(" [FAIL] %s (%s:%d)\n", expression, file, line);
|
||
std::fflush(stdout);
|
||
}
|
||
}
|
||
|
||
/** @brief 记录一次"带说明"的布尔断言(说明只用于失败时定位)。 */
|
||
inline void CheckWithNote(bool ok, const char* note, const char* file, int line)
|
||
{
|
||
++TotalChecks();
|
||
if (!ok)
|
||
{
|
||
++FailedChecks();
|
||
CaseFailed() = true;
|
||
std::printf(" [FAIL] %s (%s:%d)\n", note, file, line);
|
||
std::fflush(stdout);
|
||
}
|
||
}
|
||
|
||
/** @brief 记录一次整数相等断言(失败时打印实际值/期望值)。 */
|
||
inline void CheckEqual(const long long actual, const long long expected, const char* note,
|
||
const char* file, int line)
|
||
{
|
||
++TotalChecks();
|
||
if (actual != expected)
|
||
{
|
||
++FailedChecks();
|
||
CaseFailed() = true;
|
||
std::printf(" [FAIL] %s: actual=%lld expected=%lld (%s:%d)\n", note, actual, expected,
|
||
file, line);
|
||
std::fflush(stdout);
|
||
}
|
||
}
|
||
|
||
/** @brief 记录一次浮点近似断言。 */
|
||
inline void CheckNear(const double actual, const double expected, const double tolerance,
|
||
const char* note, const char* file, int line)
|
||
{
|
||
const bool bothNaN = (actual != actual) && (expected != expected);
|
||
const double delta = (actual > expected) ? (actual - expected) : (expected - actual);
|
||
++TotalChecks();
|
||
if (!bothNaN && !(delta <= tolerance))
|
||
{
|
||
++FailedChecks();
|
||
CaseFailed() = true;
|
||
std::printf(" [FAIL] %s: actual=%.17g expected=%.17g tolerance=%.3g (%s:%d)\n", note,
|
||
actual, expected, tolerance, file, line);
|
||
std::fflush(stdout);
|
||
}
|
||
}
|
||
|
||
/** @brief 记录一次"逐位相同"断言(用于数值语义回归)。 */
|
||
inline void CheckBitEqual(const double actual, const double expected, const char* note,
|
||
const char* file, int line)
|
||
{
|
||
std::uint64_t a = 0;
|
||
std::uint64_t b = 0;
|
||
static_assert(sizeof(a) == sizeof(actual), "double must be 8 bytes");
|
||
__builtin_memcpy(&a, &actual, sizeof(a));
|
||
__builtin_memcpy(&b, &expected, sizeof(b));
|
||
++TotalChecks();
|
||
if (a != b)
|
||
{
|
||
++FailedChecks();
|
||
CaseFailed() = true;
|
||
std::printf(" [FAIL] %s: actual=%.17g (0x%016llX) expected=%.17g (0x%016llX) (%s:%d)\n",
|
||
note, actual, static_cast<unsigned long long>(a), expected,
|
||
static_cast<unsigned long long>(b), file, line);
|
||
std::fflush(stdout);
|
||
}
|
||
}
|
||
|
||
/** @brief 记录一次宽字符串相等断言。 */
|
||
inline void CheckWideEqual(const std::wstring& actual, const std::wstring& expected,
|
||
const char* note, const char* file, int line)
|
||
{
|
||
++TotalChecks();
|
||
if (actual != expected)
|
||
{
|
||
++FailedChecks();
|
||
CaseFailed() = true;
|
||
std::printf(" [FAIL] %s: actual=\"%ls\" expected=\"%ls\" (%s:%d)\n", note, actual.c_str(),
|
||
expected.c_str(), file, line);
|
||
std::fflush(stdout);
|
||
}
|
||
}
|
||
} // namespace test
|
||
} // namespace jdi
|
||
|
||
/** @brief 定义一个测试用例(自动注册)。 */
|
||
#define JDI_TEST(case_name) \
|
||
static void case_name(); \
|
||
[[maybe_unused]] static const bool case_name##_registered_ = \
|
||
::jdi::test::Register(#case_name, &case_name); \
|
||
static void case_name()
|
||
|
||
/** @brief 布尔断言。 */
|
||
#define JDI_CHECK(expression) ::jdi::test::Check((expression), #expression, __FILE__, __LINE__)
|
||
|
||
/** @brief 带说明的布尔断言(说明为裸字符串)。 */
|
||
#define JDI_REQUIRE(note, expression) \
|
||
::jdi::test::CheckWithNote((expression), (note), __FILE__, __LINE__)
|
||
|
||
/** @brief 整数相等断言。 */
|
||
#define JDI_CHECK_EQ(actual, expected) \
|
||
::jdi::test::CheckEqual(static_cast<long long>(actual), static_cast<long long>(expected), \
|
||
#actual " == " #expected, __FILE__, __LINE__)
|
||
|
||
/** @brief 浮点近似断言。 */
|
||
#define JDI_CHECK_NEAR(actual, expected, tolerance) \
|
||
::jdi::test::CheckNear((actual), (expected), (tolerance), #actual " ~ " #expected, __FILE__, __LINE__)
|
||
|
||
/** @brief 浮点逐位相等断言(数值语义回归专用)。 */
|
||
#define JDI_CHECK_BIT_EQUAL(actual, expected) \
|
||
::jdi::test::CheckBitEqual((actual), (expected), #actual " bit== " #expected, __FILE__, __LINE__)
|
||
|
||
/** @brief 宽字符串相等断言。 */
|
||
#define JDI_CHECK_WIDE_EQ(actual, expected) \
|
||
::jdi::test::CheckWideEqual((actual), (expected), #actual " == " #expected, __FILE__, __LINE__)
|
||
|
||
#endif // __UNITTESTFRAMEWORK_HPP__
|