✨ feat: 灵台引擎脾神(hj_transform)确定性 Markdown 解析器(WP-15)
- C ABI 新增 hj_transform_version / hj_transform_markdown(契约登记同步);HjBuffer 真实实现(头部+紧随数据) - v1 解析(hunjing-markdown-v1):fenced code/mermaid 提取、独占行 $$ 块级数学、未闭合结构保持源码态、其余归并 markdown block;canonical JSON 键序确定、UTF-8 校验、1MiB/256 block/64KiB 行硬限额 - 引擎内部设施抽取(engine_internal.h:last_error/句柄表/buffer 共享) - doctest 12 个用例(含 1 万次随机鲁棒性冒烟,固定种子可复现);libFuzzer 目标(-DHJ_BUILD_FUZZER,Apple clang 无 fuzzer 运行时,正式跑在 Linux CI) - nm 验证 9 个导出符号全部 hj_*
This commit is contained in:
1 parent
d3c8df79a8
commit
127191c889
10 files changed
+565
-26
No files matched your search
@@ -1,5 +1,4 @@
|
||||
// 灵台引擎核心测试(doctest,ADR-025)。
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "hunjing/lingtai.h"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// doctest 入口:整个测试可执行文件只保留一份实现。
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include <doctest/doctest.h>
|
||||
@@ -0,0 +1,139 @@
|
||||
// 脾神(hj_transform)测试:确定性解析、提取、未闭合源码态、限额与校验。
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "hunjing/lingtai.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct Engine {
|
||||
HjEngine *ptr = nullptr;
|
||||
Engine() { REQUIRE(hj_engine_new(HJ_ABI_VERSION, &ptr) == HJ_OK); }
|
||||
~Engine() { hj_engine_free(ptr); }
|
||||
};
|
||||
|
||||
std::string transform(const Engine &e, std::string_view input, HjStatus *status = nullptr) {
|
||||
HjBuffer *buf = nullptr;
|
||||
HjStatus st = hj_transform_markdown(e.ptr, input.data(), input.size(), &buf);
|
||||
if (status != nullptr) *status = st;
|
||||
if (st != HJ_OK) return {};
|
||||
std::string out(static_cast<const char *>(hj_buffer_data(buf)), hj_buffer_len(buf));
|
||||
hj_buffer_free(buf);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("版本与空输入") {
|
||||
Engine e;
|
||||
CHECK(std::string(hj_transform_version()) == "hunjing-markdown-v1");
|
||||
const std::string out = transform(e, "");
|
||||
CHECK(out == R"jx({"parser_version":"hunjing-markdown-v1","blocks":[]})jx");
|
||||
}
|
||||
|
||||
TEST_CASE("纯文本归并为 markdown block 并裁剪空白") {
|
||||
Engine e;
|
||||
const std::string out = transform(e, "\n你好,魂镜\n\n第二段\n\n");
|
||||
CHECK(out.find(R"jx("type":"markdown")jx") != std::string::npos);
|
||||
CHECK(out.find("你好,魂镜\\n\\n第二段") != std::string::npos);
|
||||
CHECK(out.find(R"jx("flavor":"hunjing-gfm-v1")jx") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("fenced code 与 mermaid 提取") {
|
||||
Engine e;
|
||||
const std::string input = "前置说明\n```python\nprint(1)\n```\n中间\n```mermaid\nflowchart LR\nA-->B\n```\n结尾";
|
||||
const std::string out = transform(e, input);
|
||||
CHECK(out.find(R"jx("type":"code","source":"print(1)","language":"python")jx") != std::string::npos);
|
||||
CHECK(out.find(R"jx("type":"mermaid","source":"flowchart LR\nA-->B")jx") != std::string::npos);
|
||||
CHECK(out.find("前置说明") != std::string::npos);
|
||||
CHECK(out.find("结尾") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("块级数学定界提取") {
|
||||
Engine e;
|
||||
const std::string input = "前文\n$$\nE=mc^2\n$$\n后文";
|
||||
const std::string out = transform(e, input);
|
||||
CHECK(out.find(R"jx("type":"math","notation":"latex","display":"block","source":"E=mc^2")jx") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("未闭合 fenced 保持源码态") {
|
||||
Engine e;
|
||||
const std::string input = "开头\n```js\nunterminated(";
|
||||
const std::string out = transform(e, input);
|
||||
CHECK(out.find(R"jx("type":"markdown")jx") != std::string::npos);
|
||||
CHECK(out.find("```js") != std::string::npos);
|
||||
CHECK(out.find("unterminated(") != std::string::npos);
|
||||
CHECK(out.find(R"jx("type":"code")jx") == std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("未闭合数学定界保持源码态") {
|
||||
Engine e;
|
||||
const std::string out = transform(e, "a\n$$\nunclosed math");
|
||||
CHECK(out.find(R"jx("type":"math")jx") == std::string::npos);
|
||||
CHECK(out.find("$$") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("确定性:同输入两次输出字节一致") {
|
||||
Engine e;
|
||||
const std::string input = "x\n```mermaid\nA-->B\n```\n$$\nz\n$$\n";
|
||||
const std::string a = transform(e, input);
|
||||
const std::string b = transform(e, input);
|
||||
CHECK(a == b);
|
||||
}
|
||||
|
||||
TEST_CASE("非法 UTF-8 拒绝") {
|
||||
Engine e;
|
||||
HjStatus st = HJ_OK;
|
||||
transform(e, std::string_view("ab\xff\xfe", 5), &st);
|
||||
CHECK(st == HJ_ERR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST_CASE("超长输入拒绝") {
|
||||
Engine e;
|
||||
const std::string big((1 << 20) + 1, 'a');
|
||||
HjStatus st = HJ_OK;
|
||||
transform(e, big, &st);
|
||||
CHECK(st == HJ_ERR_LIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
TEST_CASE("参数与句柄校验") {
|
||||
Engine e;
|
||||
HjBuffer *buf = nullptr;
|
||||
CHECK(hj_transform_markdown(e.ptr, "x", 1, nullptr) == HJ_ERR_INVALID_ARGUMENT);
|
||||
CHECK(hj_transform_markdown(nullptr, "x", 1, &buf) == HJ_ERR_HANDLE_INVALID);
|
||||
CHECK(hj_transform_markdown(e.ptr, nullptr, 5, &buf) == HJ_ERR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST_CASE("随机鲁棒性:任意字节输入不崩溃且返回已知状态码") {
|
||||
// 本地等价冒烟:正式 libFuzzer 目标(fuzz/fuzz_transform.cpp)在 Linux CI 运行。
|
||||
Engine e;
|
||||
uint64_t rng = 0x9E3779B97F4A7C15ULL; // 固定种子,可复现
|
||||
auto next = [&rng](uint64_t bound) {
|
||||
rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
|
||||
return static_cast<size_t>(rng % bound);
|
||||
};
|
||||
static const char *kPieces[] = {
|
||||
"text ", "\n", "```", "mermaid\n", "python\n", "$$\n", "`", "$",
|
||||
"hello", "\xE4\xB8\xAD", "\"", "\\", "flowchart LR\nA-->B\n", "\x7f",
|
||||
};
|
||||
for (int iter = 0; iter < 10000; ++iter) {
|
||||
std::string input;
|
||||
if (iter % 2 == 0) {
|
||||
const size_t len = next(512);
|
||||
input.reserve(len);
|
||||
for (size_t k = 0; k < len; ++k) input.push_back(static_cast<char>(next(256)));
|
||||
} else {
|
||||
for (size_t k = 0, m = next(24); k < m; ++k) input += kPieces[next(14)];
|
||||
}
|
||||
HjBuffer *buf = nullptr;
|
||||
const HjStatus st = hj_transform_markdown(e.ptr, input.data(), input.size(), &buf);
|
||||
CHECK((st == HJ_OK || st == HJ_ERR_INVALID_ARGUMENT || st == HJ_ERR_LIMIT_EXCEEDED ||
|
||||
st == HJ_ERR_PARSE_FAILED));
|
||||
if (buf != nullptr) {
|
||||
CHECK(hj_buffer_data(buf) != nullptr);
|
||||
CHECK(hj_buffer_free(buf) == HJ_OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user