Files
Just-DataInspector/Tools/UnitTests/Inc/UnitTestFramework.hpp
T
DeepSeek V4.1 Flash 97e9163314 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。
2026-11-04 11:41:08 +08:00

199 lines
6.6 KiB
C++
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.
/**
* @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__