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:
1 parent
c5d7a6fb91
commit
97e9163314
14 files changed
+3515
-2
No files matched your search
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* @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__
|
||||
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* @file DataModelTests.cpp
|
||||
* @brief `Utilities/DataModel` 的纯数据层语义回归(不创建窗口、不碰 GUI)。
|
||||
*
|
||||
* 重点用例:
|
||||
* * `dataset.formula_transform_cache` —— 第 32 轮引入的「公式绘制值缓存」
|
||||
* (`DataSet::FormulaTransformFor`)必须与"逐点 `ApplyDrawTransform` 后统计"**逐位一致**,
|
||||
* 并且命中缓存时返回同一指针、样式变化后失效重建。
|
||||
* 这条是"公式信号不再逐帧全扫"的正确性底线。
|
||||
* * `filter.moving_average` —— 多阶等价性(1 阶 == 单次、2 阶 == 链式)。
|
||||
* * `transform.*` —— 变换顺序(公式 → 系数 → 绝对值 → 垂直镜像 → Y 偏移)与区间映射。
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "DataModel.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;
|
||||
}
|
||||
|
||||
/** @brief 构造一条合成信号分组(一条单调时间轴 + 一条信号)。 */
|
||||
jdi::SignalGroup MakeGroup(const std::vector<double>& values, const wchar_t* name)
|
||||
{
|
||||
jdi::SignalGroup group;
|
||||
group.name = L"synthetic";
|
||||
|
||||
jdi::TimeAxis axis;
|
||||
axis.name = L"time";
|
||||
axis.time.resize(values.size());
|
||||
for (std::size_t index = 0; index < values.size(); ++index)
|
||||
{
|
||||
axis.time[index] = static_cast<double>(index) * 0.001;
|
||||
}
|
||||
axis.monotonic = true;
|
||||
axis.minTime = 0.0;
|
||||
axis.maxTime = values.empty() ? 0.0 : axis.time.back();
|
||||
axis.finiteCount = axis.time.size();
|
||||
group.axes.push_back(std::move(axis));
|
||||
|
||||
jdi::SignalData signal;
|
||||
signal.name = name;
|
||||
signal.axisIndex = 0;
|
||||
signal.values = values;
|
||||
group.signals.push_back(std::move(signal));
|
||||
return group;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
JDI_TEST(transform_apply_order)
|
||||
{
|
||||
jdi::SignalStyle style;
|
||||
style.gain = 2.0f;
|
||||
style.absolute = true;
|
||||
style.mirrorVertical = true;
|
||||
style.offsetY = 1.0f;
|
||||
|
||||
// -1.5 → ×2 = -3 → |·| = 3 → 垂直镜像 = -3 → +1 = -2
|
||||
JDI_CHECK_NEAR(jdi::ApplyDrawTransform(style, -1.5), -2.0, 0.0);
|
||||
|
||||
// 公式最先结算:f(t)=x*10,再 ×2、|·|、镜像、+1 ⇒ 2.5 → 25 → 50 → 50 → -50 → -49
|
||||
style.formula = L"f(t)*10";
|
||||
JDI_CHECK_NEAR(jdi::ApplyDrawTransform(style, 2.5), -49.0, 1e-9);
|
||||
|
||||
// NaN 必须保持 NaN(断线语义)
|
||||
JDI_CHECK(std::isnan(jdi::ApplyDrawTransform(style, std::numeric_limits<double>::quiet_NaN())));
|
||||
|
||||
// 水平镜像不改变取值(只改下标),`SourceIndexFor` 负责
|
||||
jdi::SignalStyle mirrored;
|
||||
mirrored.mirrorHorizontal = true;
|
||||
JDI_CHECK_NEAR(jdi::ApplyDrawTransform(mirrored, 7.0), 7.0, 0.0);
|
||||
JDI_CHECK_EQ(jdi::SourceIndexFor(mirrored, 10, 0), 9);
|
||||
JDI_CHECK_EQ(jdi::SourceIndexFor(mirrored, 10, 9), 0);
|
||||
}
|
||||
|
||||
JDI_TEST(transform_value_range_and_order_preserving)
|
||||
{
|
||||
jdi::SignalStyle style;
|
||||
style.gain = 2.0f;
|
||||
style.absolute = true;
|
||||
style.mirrorVertical = true;
|
||||
style.offsetY = 1.0f;
|
||||
|
||||
double low = -2.0;
|
||||
double high = 3.0;
|
||||
jdi::TransformValueRange(style, low, high);
|
||||
// [-2,3] → ×2 = [-4,6] → |·| = [0,6] → 镜像 = [-6,0] → +1 = [-5,1]
|
||||
JDI_CHECK_NEAR(low, -5.0, 0.0);
|
||||
JDI_CHECK_NEAR(high, 1.0, 0.0);
|
||||
|
||||
jdi::SignalStyle plain;
|
||||
JDI_CHECK(jdi::DrawTransformOrderPreserving(plain));
|
||||
plain.gain = -1.0f;
|
||||
JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain));
|
||||
plain.gain = 1.0f;
|
||||
plain.absolute = true;
|
||||
JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain));
|
||||
plain.absolute = false;
|
||||
plain.mirrorVertical = true;
|
||||
JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain));
|
||||
plain.mirrorVertical = false;
|
||||
plain.formula = L"f(t)*f(t)";
|
||||
JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain));
|
||||
plain.formula = L"f(t)";
|
||||
JDI_CHECK(jdi::DrawTransformOrderPreserving(plain));
|
||||
}
|
||||
|
||||
JDI_TEST(filter_moving_average_equivalence)
|
||||
{
|
||||
std::vector<double> values;
|
||||
values.reserve(400);
|
||||
for (int index = 0; index < 400; ++index)
|
||||
{
|
||||
values.push_back(std::sin(static_cast<double>(index) * 0.07) * 100.0 + (index % 7));
|
||||
}
|
||||
values[10] = std::numeric_limits<double>::quiet_NaN();
|
||||
values[200] = std::numeric_limits<double>::quiet_NaN();
|
||||
|
||||
std::vector<float> single;
|
||||
std::vector<float> singleCopy;
|
||||
jdi::ComputeMovingAverage(values, 3, single);
|
||||
|
||||
// 1 阶 == 单次平均(逐位)
|
||||
jdi::ComputeMovingAverageIterated(values, 3, 1, singleCopy);
|
||||
JDI_CHECK_EQ(single.size(), singleCopy.size());
|
||||
bool identical = (single.size() == singleCopy.size());
|
||||
for (std::size_t index = 0; identical && index < single.size(); ++index)
|
||||
{
|
||||
identical = (BitsOf(single[index]) == BitsOf(singleCopy[index]));
|
||||
}
|
||||
JDI_CHECK(identical);
|
||||
|
||||
// 2 阶 == 对 1 阶结果再做一次(逐位)
|
||||
std::vector<double> firstPass(single.begin(), single.end());
|
||||
std::vector<float> chained;
|
||||
jdi::ComputeMovingAverage(firstPass, 3, chained);
|
||||
std::vector<float> iterated;
|
||||
jdi::ComputeMovingAverageIterated(values, 3, 2, iterated);
|
||||
JDI_CHECK_EQ(chained.size(), iterated.size());
|
||||
bool chainIdentical = (chained.size() == iterated.size());
|
||||
for (std::size_t index = 0; chainIdentical && index < chained.size(); ++index)
|
||||
{
|
||||
chainIdentical = (BitsOf(chained[index]) == BitsOf(iterated[index]));
|
||||
}
|
||||
JDI_CHECK(chainIdentical);
|
||||
|
||||
// 常数信号经任意阶数保持不变
|
||||
std::vector<double> constant(500, 7.5);
|
||||
std::vector<float> filtered;
|
||||
jdi::ComputeMovingAverageIterated(constant, 5, 3, filtered);
|
||||
bool constantHeld = (filtered.size() == constant.size());
|
||||
for (std::size_t index = 0; constantHeld && index < filtered.size(); ++index)
|
||||
{
|
||||
constantHeld = (std::fabs(static_cast<double>(filtered[index]) - 7.5) < 1e-5);
|
||||
}
|
||||
JDI_CHECK(constantHeld);
|
||||
}
|
||||
|
||||
JDI_TEST(dataset_group_access)
|
||||
{
|
||||
jdi::DataSet dataSet;
|
||||
int firstIndex = -1;
|
||||
JDI_CHECK(dataSet.AddGroup(MakeGroup({1.0, 2.0, 3.0}, L"a"), firstIndex));
|
||||
JDI_CHECK_EQ(firstIndex, 0);
|
||||
JDI_CHECK_EQ(dataSet.GroupCount(), 1);
|
||||
|
||||
const jdi::SignalGroup* group = dataSet.GroupAt(0);
|
||||
JDI_CHECK(group != nullptr);
|
||||
if (group != nullptr)
|
||||
{
|
||||
JDI_CHECK_EQ(group->signals.size(), 1);
|
||||
JDI_CHECK_WIDE_EQ(group->signals[0].name, std::wstring(L"a"));
|
||||
JDI_CHECK_EQ(group->axes.size(), 1);
|
||||
}
|
||||
|
||||
// 外观属于信号本身:通过 `Style(ref)` 读写
|
||||
const jdi::SignalRef ref{0, 0};
|
||||
jdi::SignalStyle* style = dataSet.Style(ref);
|
||||
JDI_CHECK(style != nullptr);
|
||||
if (style != nullptr)
|
||||
{
|
||||
style->lineWidth = 3.5f;
|
||||
const jdi::SignalStyle* readBack = dataSet.Style(ref);
|
||||
JDI_CHECK(readBack != nullptr && readBack->lineWidth == 3.5f);
|
||||
}
|
||||
JDI_CHECK(dataSet.GroupAt(5) == nullptr);
|
||||
}
|
||||
|
||||
JDI_TEST(dataset_formula_transform_cache)
|
||||
{
|
||||
std::vector<double> values;
|
||||
values.reserve(5000);
|
||||
for (int index = 0; index < 5000; ++index)
|
||||
{
|
||||
values.push_back(std::sin(static_cast<double>(index) * 0.013) * 50.0 + (index % 11) * 0.5);
|
||||
}
|
||||
values[17] = std::numeric_limits<double>::quiet_NaN();
|
||||
values[4999] = std::numeric_limits<double>::quiet_NaN();
|
||||
|
||||
jdi::DataSet dataSet;
|
||||
int groupIndex = -1;
|
||||
JDI_CHECK(dataSet.AddGroup(MakeGroup(values, L"formula"), groupIndex));
|
||||
const jdi::SignalGroup* group = dataSet.GroupAt(0);
|
||||
JDI_CHECK(group != nullptr);
|
||||
if (group == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const jdi::SignalData& signal = group->signals[0];
|
||||
jdi::SignalStyle& style = *dataSet.Style(jdi::SignalRef{0, 0});
|
||||
|
||||
// 恒等 / 空公式 ⇒ 不建立缓存(调用方走 O(1) 保序快路径)
|
||||
JDI_CHECK(dataSet.FormulaTransformFor(signal, style) == nullptr);
|
||||
style.formula = L"f(t)";
|
||||
JDI_CHECK(dataSet.FormulaTransformFor(signal, style) == nullptr);
|
||||
|
||||
// 真公式 ⇒ 建立一次缓存
|
||||
style.formula = L"f(t)*2";
|
||||
style.gain = 1.5f;
|
||||
style.absolute = true;
|
||||
style.mirrorVertical = false;
|
||||
style.offsetY = -3.0f;
|
||||
|
||||
const jdi::DataSet::FormulaTransformCache* entry = dataSet.FormulaTransformFor(signal, style);
|
||||
JDI_CHECK(entry != nullptr);
|
||||
if (entry == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
JDI_CHECK(entry->ready);
|
||||
|
||||
// 参考实现:逐点走 `ApplyDrawTransform`(同一套语义),按缓存给出的 stride 分组
|
||||
double referenceLow = std::numeric_limits<double>::infinity();
|
||||
double referenceHigh = -std::numeric_limits<double>::infinity();
|
||||
std::vector<double> referenceMin(entry->minimums.size(), std::numeric_limits<double>::quiet_NaN());
|
||||
std::vector<double> referenceMax(entry->maximums.size(), std::numeric_limits<double>::quiet_NaN());
|
||||
const std::size_t stride = entry->stride;
|
||||
JDI_CHECK(stride > 0);
|
||||
for (std::size_t index = 0; index < signal.values.size(); ++index)
|
||||
{
|
||||
const double mapped = jdi::ApplyDrawTransform(style, signal.values[index]);
|
||||
if (!std::isfinite(mapped))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t bucket = index / stride;
|
||||
if (bucket >= referenceMin.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (mapped < referenceMin[bucket] || referenceMin[bucket] != referenceMin[bucket])
|
||||
{
|
||||
referenceMin[bucket] = mapped;
|
||||
}
|
||||
if (mapped > referenceMax[bucket] || referenceMax[bucket] != referenceMax[bucket])
|
||||
{
|
||||
referenceMax[bucket] = mapped;
|
||||
}
|
||||
referenceLow = std::min(referenceLow, mapped);
|
||||
referenceHigh = std::max(referenceHigh, mapped);
|
||||
}
|
||||
|
||||
JDI_CHECK_BIT_EQUAL(entry->low, referenceLow);
|
||||
JDI_CHECK_BIT_EQUAL(entry->high, referenceHigh);
|
||||
JDI_CHECK_EQ(entry->minimums.size(), referenceMin.size());
|
||||
|
||||
bool bucketsMatch = (entry->minimums.size() == referenceMin.size());
|
||||
for (std::size_t bucket = 0; bucketsMatch && bucket < referenceMin.size(); ++bucket)
|
||||
{
|
||||
const bool emptyReference = (referenceMin[bucket] != referenceMin[bucket]);
|
||||
const bool emptyEntry = (entry->minimums[bucket] != entry->minimums[bucket]);
|
||||
if (emptyReference != emptyEntry)
|
||||
{
|
||||
bucketsMatch = false;
|
||||
break;
|
||||
}
|
||||
if (emptyReference)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bucketsMatch = BitsOf(entry->minimums[bucket]) == BitsOf(referenceMin[bucket]) &&
|
||||
BitsOf(entry->maximums[bucket]) == BitsOf(referenceMax[bucket]);
|
||||
}
|
||||
JDI_CHECK(bucketsMatch);
|
||||
|
||||
// 命中缓存:同一信号 + 同一样式 ⇒ 返回同一指针(不再重扫)
|
||||
const jdi::DataSet::FormulaTransformCache* again = dataSet.FormulaTransformFor(signal, style);
|
||||
JDI_CHECK(again == entry);
|
||||
|
||||
// 样式变化 ⇒ 失效重建,且新值域按新公式计算
|
||||
style.formula = L"f(t)*3";
|
||||
const jdi::DataSet::FormulaTransformCache* rebuilt = dataSet.FormulaTransformFor(signal, style);
|
||||
JDI_CHECK(rebuilt != nullptr);
|
||||
if (rebuilt != nullptr)
|
||||
{
|
||||
JDI_CHECK(rebuilt != entry);
|
||||
double expectedLow = std::numeric_limits<double>::infinity();
|
||||
double expectedHigh = -std::numeric_limits<double>::infinity();
|
||||
for (const double value : signal.values)
|
||||
{
|
||||
const double mapped = jdi::ApplyDrawTransform(style, value);
|
||||
if (!std::isfinite(mapped))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
expectedLow = std::min(expectedLow, mapped);
|
||||
expectedHigh = std::max(expectedHigh, mapped);
|
||||
}
|
||||
JDI_CHECK_BIT_EQUAL(rebuilt->low, expectedLow);
|
||||
JDI_CHECK_BIT_EQUAL(rebuilt->high, expectedHigh);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @file Main.cpp
|
||||
* @brief 单元测试执行器(控制台程序,无窗口、无 GUI 初始化)。
|
||||
*
|
||||
* 命令行(全部可选):
|
||||
* -list 只列出用例名,不执行
|
||||
* -only <substr> 只执行名字包含 <substr> 的用例
|
||||
* -report <path> 报告文件路径(默认 `../Temp/tools/UnitTests-report.txt`)
|
||||
* -verbose 打印每个用例的开始/结束
|
||||
*
|
||||
* 退出码:0 = 全部通过;1 = 有失败;2 = 参数错误 / 没有任何用例。
|
||||
*
|
||||
* ⚠️ 本工具**不创建窗口、不初始化 GUI、不读用户输入**,可在后台任意次运行。
|
||||
* ⚠️ 报告与截图类临时产物一律写到 `Temp/`(已被 .gitignore 忽略),不要写进源码目录。
|
||||
*/
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
#include "UnitTestFramework.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::FILE* g_report = nullptr;
|
||||
bool g_verbose = false;
|
||||
|
||||
/** @brief 同时输出到标准输出与报告文件。 */
|
||||
void Emit(const char* format, ...)
|
||||
{
|
||||
char buffer[4096];
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
const int length = std::vsnprintf(buffer, sizeof(buffer), format, arguments);
|
||||
va_end(arguments);
|
||||
|
||||
if (length > 0)
|
||||
{
|
||||
std::fputs(buffer, stdout);
|
||||
std::fflush(stdout);
|
||||
if (g_report != nullptr)
|
||||
{
|
||||
std::fputs(buffer, g_report);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 递归创建目录(仅支持 ASCII 路径;失败静默忽略)。 */
|
||||
void EnsureDirectory(const std::wstring& path)
|
||||
{
|
||||
if (path.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (std::size_t index = 1; index <= path.size(); ++index)
|
||||
{
|
||||
if (index == path.size() || path[index] == L'/' || path[index] == L'\\')
|
||||
{
|
||||
const std::wstring prefix = path.substr(0, index);
|
||||
CreateDirectoryW(prefix.c_str(), nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 从 UTF-8 窄字符串转换为宽字符串(只用于路径)。 */
|
||||
std::wstring Widen(const char* text)
|
||||
{
|
||||
const int length = MultiByteToWideChar(CP_ACP, 0, text, -1, nullptr, 0);
|
||||
if (length <= 1)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
std::wstring result(static_cast<std::size_t>(length - 1), L'\0');
|
||||
MultiByteToWideChar(CP_ACP, 0, text, -1, &result[0], length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief 打开报告文件(顺带创建 `Temp/tools`)。 */
|
||||
std::FILE* OpenReport(const char* path)
|
||||
{
|
||||
std::string wide = path; // 仅 ASCII,用于切分目录
|
||||
const std::size_t slash = wide.find_last_of("/\\");
|
||||
if (slash != std::string::npos)
|
||||
{
|
||||
EnsureDirectory(Widen(wide.substr(0, slash).c_str()));
|
||||
}
|
||||
return std::fopen(path, "wb");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const char* only = nullptr;
|
||||
const char* reportPath = "../Temp/tools/UnitTests-report.txt";
|
||||
|
||||
for (int index = 1; index < argc; ++index)
|
||||
{
|
||||
const char* argument = argv[index];
|
||||
if (std::strcmp(argument, "-list") == 0)
|
||||
{
|
||||
for (const jdi::test::TestCase& testCase : jdi::test::Registry())
|
||||
{
|
||||
std::printf("%s\n", testCase.name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (std::strcmp(argument, "-only") == 0 && index + 1 < argc)
|
||||
{
|
||||
only = argv[++index];
|
||||
continue;
|
||||
}
|
||||
if (std::strcmp(argument, "-report") == 0 && index + 1 < argc)
|
||||
{
|
||||
reportPath = argv[++index];
|
||||
continue;
|
||||
}
|
||||
if (std::strcmp(argument, "-verbose") == 0)
|
||||
{
|
||||
g_verbose = true;
|
||||
continue;
|
||||
}
|
||||
std::printf("unknown argument: %s\n", argument);
|
||||
return 2;
|
||||
}
|
||||
|
||||
g_report = OpenReport(reportPath);
|
||||
|
||||
Emit("==============================================================\n");
|
||||
Emit(" Just DataInspector - unit tests\n");
|
||||
Emit(" report: %s\n", reportPath);
|
||||
Emit("==============================================================\n");
|
||||
|
||||
int executed = 0;
|
||||
for (const jdi::test::TestCase& testCase : jdi::test::Registry())
|
||||
{
|
||||
if (only != nullptr && std::strstr(testCase.name, only) == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
++executed;
|
||||
const int failedBefore = jdi::test::FailedChecks();
|
||||
jdi::test::CaseFailed() = false;
|
||||
Emit("[RUN ] %s\n", testCase.name);
|
||||
testCase.function();
|
||||
const int failed = jdi::test::FailedChecks() - failedBefore;
|
||||
if (failed == 0)
|
||||
{
|
||||
if (g_verbose)
|
||||
{
|
||||
Emit("[ OK ] %s\n", testCase.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Emit("--------------------------------------------------------------\n");
|
||||
Emit("SUMMARY checks=%d failed=%d cases=%d result=%s\n", jdi::test::TotalChecks(),
|
||||
jdi::test::FailedChecks(), executed,
|
||||
(jdi::test::FailedChecks() == 0 && executed > 0) ? "PASS" : "FAIL");
|
||||
Emit("--------------------------------------------------------------\n");
|
||||
|
||||
if (g_report != nullptr)
|
||||
{
|
||||
std::fclose(g_report);
|
||||
g_report = nullptr;
|
||||
}
|
||||
|
||||
if (executed == 0)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
return (jdi::test::FailedChecks() == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* @file Utf8UtilTests.cpp
|
||||
* @brief `Utilities/Utf8Util` 的数值解析 / 文本格式化语义回归。
|
||||
*
|
||||
* 重点用例:
|
||||
* * `parse.bit_exact_vs_strtod` —— **逐位**对比 `TryParseDouble` 与 libc `strtod`。
|
||||
* 这是"自写 strtod 替身"类体积优化唯一可信的验收标准(见 Code Review 报告 §10.4):
|
||||
* 常见写法(≤15 位有效数字、|十进制指数| ≤ 22)必须逐位相同;
|
||||
* 17 位以上的长数字允许 ≤1 ULP 偏差(实现里已注释该取舍)。
|
||||
* * `format.*` —— 只断言文档里给出的实例与"绝不出现科学计数法"这类硬约束,
|
||||
* 避免把测试绑死在内部格式细节上。
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "UnitTestFramework.hpp"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief 取 double 的位模式。 */
|
||||
std::uint64_t BitsOf(const double value)
|
||||
{
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
return bits;
|
||||
}
|
||||
|
||||
/** @brief 两个 double 之间相差多少个 ULP(异号时返回一个很大的数)。 */
|
||||
long long UlpDifference(const double left, const double right)
|
||||
{
|
||||
if (left == right)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
const std::uint64_t a = BitsOf(left);
|
||||
const std::uint64_t b = BitsOf(right);
|
||||
if ((a >> 63) != (b >> 63))
|
||||
{
|
||||
return 1000000;
|
||||
}
|
||||
return (a > b) ? static_cast<long long>(a - b) : -static_cast<long long>(b - a);
|
||||
}
|
||||
|
||||
/** @brief 用 `TryParseDouble` 解析整段文本。 */
|
||||
bool Parse(const std::string& text, double& value)
|
||||
{
|
||||
return jdi::TryParseDouble(text.c_str(), text.c_str() + text.size(), value);
|
||||
}
|
||||
|
||||
/** @brief 统计十进制有效数字位数(忽略符号/小数点/前导零/指数部分)。 */
|
||||
int SignificantDigits(const std::string& text)
|
||||
{
|
||||
int digits = 0;
|
||||
bool sawNonZero = false;
|
||||
for (const char character : text)
|
||||
{
|
||||
if (character == 'e' || character == 'E')
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (character < '0' || character > '9')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (character != '0')
|
||||
{
|
||||
sawNonZero = true;
|
||||
}
|
||||
if (sawNonZero)
|
||||
{
|
||||
++digits;
|
||||
}
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
/** @brief 生成 `数字.数字e指数` 形式的随机十进制文本。 */
|
||||
std::string MakeRandomNumber(std::mt19937& rng, const int maxDigits, const bool allowFraction,
|
||||
const bool allowExponent, const int maxExponent)
|
||||
{
|
||||
std::string text;
|
||||
if ((rng() & 1u) != 0u)
|
||||
{
|
||||
text.push_back('-');
|
||||
}
|
||||
const int integerDigits = 1 + static_cast<int>(rng() % static_cast<unsigned>(maxDigits));
|
||||
for (int index = 0; index < integerDigits; ++index)
|
||||
{
|
||||
text.push_back(static_cast<char>('0' + (rng() % 10u)));
|
||||
}
|
||||
if (allowFraction && (rng() % 3u) != 0u)
|
||||
{
|
||||
text.push_back('.');
|
||||
const int fractionDigits = 1 + static_cast<int>(rng() % static_cast<unsigned>(maxDigits));
|
||||
for (int index = 0; index < fractionDigits; ++index)
|
||||
{
|
||||
text.push_back(static_cast<char>('0' + (rng() % 10u)));
|
||||
}
|
||||
}
|
||||
if (allowExponent && (rng() % 2u) == 0u)
|
||||
{
|
||||
text.push_back((rng() & 1u) != 0u ? 'e' : 'E');
|
||||
const bool negativeExponent = (rng() & 1u) != 0u;
|
||||
text.push_back(negativeExponent ? '-' : '+');
|
||||
const int exponent = static_cast<int>(rng() % static_cast<unsigned>(maxExponent + 1));
|
||||
const std::string digits = std::to_string(exponent);
|
||||
text += digits;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
JDI_TEST(parse_fixed_corpus_matches_strtod)
|
||||
{
|
||||
static const char* const corpus[] = {
|
||||
"0", "1", "-1", "+1", "0.1", "-0.1", "1.5",
|
||||
"123.456", "-123.456", "1e10", "1e-10", "1E+5", "1e22", "1e-22",
|
||||
"3.141592653589793", "2.718281828459045", "6.02214076e23", "1.602176634e-19", "0.000001",
|
||||
"1000000", "-0", "0.0", "123456789012345",
|
||||
};
|
||||
|
||||
for (const char* const text : corpus)
|
||||
{
|
||||
double ours = 0.0;
|
||||
const bool accepted = Parse(text, ours);
|
||||
|
||||
char* stop = nullptr;
|
||||
const double reference = std::strtod(text, &stop);
|
||||
const bool referenceAccepted = (stop != text);
|
||||
|
||||
JDI_REQUIRE(text, accepted == referenceAccepted);
|
||||
if (accepted && referenceAccepted)
|
||||
{
|
||||
JDI_CHECK_BIT_EQUAL(ours, reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JDI_TEST(parse_acceptance_semantics)
|
||||
{
|
||||
double value = 0.0;
|
||||
JDI_CHECK(!Parse("", value));
|
||||
JDI_CHECK(!Parse(" ", value));
|
||||
JDI_CHECK(!Parse("abc", value));
|
||||
JDI_CHECK(!Parse("e5", value));
|
||||
|
||||
// 尾部杂字符:与 `strtod` 一致,接受前缀数值(历史语义,不要改)
|
||||
JDI_CHECK(Parse("12abc", value));
|
||||
JDI_CHECK_NEAR(value, 12.0, 0.0);
|
||||
JDI_CHECK(Parse("1,5", value));
|
||||
JDI_CHECK_NEAR(value, 1.0, 0.0);
|
||||
|
||||
// 首尾空白应被忽略
|
||||
JDI_CHECK(Parse(" -2.5\t\r\n", value));
|
||||
JDI_CHECK_NEAR(value, -2.5, 0.0);
|
||||
|
||||
// 十六进制浮点 / inf / nan 走标准库回退路径,语义必须与 strtod 相同
|
||||
static const char* const exotic[] = {"0x1p3", "inf", "-inf", "nan", "infinity", "1e9999", "1e-9999"};
|
||||
for (const char* const text : exotic)
|
||||
{
|
||||
double ours = 0.0;
|
||||
char* stop = nullptr;
|
||||
const double reference = std::strtod(text, &stop);
|
||||
const bool referenceAccepted = (stop != text);
|
||||
JDI_REQUIRE(text, Parse(text, ours) == referenceAccepted);
|
||||
if (referenceAccepted)
|
||||
{
|
||||
JDI_CHECK_BIT_EQUAL(ours, reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JDI_TEST(parse_random_corpus_bit_exact)
|
||||
{
|
||||
std::mt19937 rng(20261104u);
|
||||
int mismatches = 0;
|
||||
int beyondOneUlp = 0;
|
||||
int cases = 0;
|
||||
|
||||
// 常见写法:≤15 位有效数字、|指数| ≤ 22 ⇒ 必须逐位相同。
|
||||
for (int iteration = 0; iteration < 40000; ++iteration)
|
||||
{
|
||||
const std::string text = MakeRandomNumber(rng, 6, true, true, 22);
|
||||
double ours = 0.0;
|
||||
char* stop = nullptr;
|
||||
const double reference = std::strtod(text.c_str(), &stop);
|
||||
if (stop == text.c_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
++cases;
|
||||
if (!Parse(text, ours))
|
||||
{
|
||||
++mismatches;
|
||||
continue;
|
||||
}
|
||||
if (BitsOf(ours) != BitsOf(reference))
|
||||
{
|
||||
++mismatches;
|
||||
if (SignificantDigits(text) <= 15 && std::fabs(reference) > 1e-300 &&
|
||||
std::fabs(reference) < 1e300)
|
||||
{
|
||||
std::printf(" [INFO] bit mismatch on short literal: %s ours=%.17g ref=%.17g\n", text.c_str(),
|
||||
ours, reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JDI_CHECK_EQ(cases, 40000);
|
||||
JDI_CHECK_EQ(mismatches, 0);
|
||||
|
||||
// 长数字(最多 19 位):允许 ≤1 ULP 偏差(实现里有明确注释与容差约定)。
|
||||
for (int iteration = 0; iteration < 20000; ++iteration)
|
||||
{
|
||||
const std::string text = MakeRandomNumber(rng, 19, true, true, 22);
|
||||
double ours = 0.0;
|
||||
char* stop = nullptr;
|
||||
const double reference = std::strtod(text.c_str(), &stop);
|
||||
if (stop == text.c_str() || !Parse(text, ours))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (UlpDifference(ours, reference) > 1)
|
||||
{
|
||||
++beyondOneUlp;
|
||||
}
|
||||
}
|
||||
JDI_CHECK_EQ(beyondOneUlp, 0);
|
||||
}
|
||||
|
||||
JDI_TEST(format_decimal_never_scientific)
|
||||
{
|
||||
JDI_CHECK_WIDE_EQ(jdi::FormatDecimal(1.0e-5), std::wstring(L"0.00001"));
|
||||
JDI_CHECK_WIDE_EQ(jdi::FormatDecimal(0.0), std::wstring(L"0"));
|
||||
JDI_CHECK_WIDE_EQ(jdi::FormatDecimalFixed(700.0, 3), std::wstring(L"700.000"));
|
||||
|
||||
static const double values[] = {1.0e-9, 1.0e-6, 1.0e-5, 0.000123456, 1.0, 1234.5678, 1.0e9};
|
||||
for (const double value : values)
|
||||
{
|
||||
const std::wstring text = jdi::FormatDecimal(value);
|
||||
JDI_REQUIRE("FormatDecimal must not use exponent", text.find(L'e') == std::wstring::npos &&
|
||||
text.find(L'E') == std::wstring::npos);
|
||||
}
|
||||
|
||||
// 文档实例:`FormatValue` = `%.6g`
|
||||
JDI_CHECK_WIDE_EQ(jdi::FormatValue(123.456789), std::wstring(L"123.457"));
|
||||
JDI_CHECK_WIDE_EQ(jdi::FormatValue(1.0), std::wstring(L"1"));
|
||||
}
|
||||
|
||||
JDI_TEST(format_byte_size_and_frequency)
|
||||
{
|
||||
const std::wstring bytes = jdi::FormatByteSize(1024ull * 1024ull);
|
||||
JDI_REQUIRE("FormatByteSize(1MB) contains MB", bytes.find(L"MB") != std::wstring::npos);
|
||||
|
||||
const std::wstring hertz = jdi::FormatFrequency(270.5166);
|
||||
JDI_REQUIRE("FormatFrequency contains unit", hertz.find(L"Hz") != std::wstring::npos);
|
||||
JDI_REQUIRE("FormatFrequency has no exponent", hertz.find(L'e') == std::wstring::npos);
|
||||
|
||||
const std::wstring duration = jdi::FormatDurationCompound(5.06);
|
||||
JDI_REQUIRE("FormatDurationCompound has no exponent", duration.find(L'e') == std::wstring::npos);
|
||||
JDI_REQUIRE("FormatDurationCompound ends with unit", duration.find(L's') != std::wstring::npos);
|
||||
}
|
||||
|
||||
JDI_TEST(text_trim_and_utf8_conversion)
|
||||
{
|
||||
JDI_CHECK_WIDE_EQ(jdi::Trim(L" abc \t"), std::wstring(L"abc"));
|
||||
JDI_CHECK_WIDE_EQ(jdi::Trim(L" \t\r\n "), std::wstring());
|
||||
JDI_CHECK_WIDE_EQ(jdi::ToLowerAscii(L"AbC-129"), std::wstring(L"abc-129"));
|
||||
|
||||
const std::wstring wide = jdi::Utf8ToWide("abc");
|
||||
JDI_CHECK_WIDE_EQ(wide, std::wstring(L"abc"));
|
||||
|
||||
JDI_CHECK(jdi::ContainsInsensitive(L"HelloWorld", L"world"));
|
||||
JDI_CHECK(!jdi::ContainsInsensitive(L"HelloWorld", L"xyz"));
|
||||
}
|
||||
|
||||
JDI_TEST(ticks_nice_step_and_build_ticks)
|
||||
{
|
||||
// `NiceStep` 必须落在 1/2/2.5/5 × 10^n 这类"好读"的格点上
|
||||
static const double ranges[] = {1.0, 10.0, 123.0, 0.0003, 98765.0};
|
||||
for (const double range : ranges)
|
||||
{
|
||||
const double step = jdi::NiceStep(range, 10);
|
||||
JDI_REQUIRE("NiceStep > 0", step > 0.0);
|
||||
const double ratio = step / std::pow(10.0, std::floor(std::log10(step)));
|
||||
const bool nice = std::fabs(ratio - 1.0) < 1e-9 || std::fabs(ratio - 2.0) < 1e-9 ||
|
||||
std::fabs(ratio - 2.5) < 1e-9 || std::fabs(ratio - 5.0) < 1e-9;
|
||||
JDI_REQUIRE("NiceStep is a round number", nice);
|
||||
}
|
||||
|
||||
std::vector<double> ticks;
|
||||
jdi::BuildTicks(0.0, 10.0, 1.0, ticks);
|
||||
JDI_CHECK_EQ(ticks.size(), 11);
|
||||
if (ticks.size() == 11)
|
||||
{
|
||||
JDI_CHECK_NEAR(ticks.front(), 0.0, 0.0);
|
||||
JDI_CHECK_NEAR(ticks.back(), 10.0, 0.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user