Files
2026-10-04 13:50:33 +08:00

340 lines
13 KiB
C

/* test_json.c - JSON 解析器/序列化器 单元测试 */
#include "json.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) { \
failures++; \
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
printf(__VA_ARGS__); \
printf("\n"); \
} \
} while (0)
static void test_basic(void) {
puts("test_basic");
char err[256];
JsonValue *v =
json_parse("{\"a\": 1, \"b\": [true, false, null], \"c\": \"hi\"}", err,
sizeof err);
CHECK(v != NULL, "解析失败: %s", err);
if (!v)
return;
CHECK(json_get_number(v, "a", -1) == 1, "a 应为 1");
CHECK(json_array_size(json_object_get(v, "b")) == 3, "b 应有 3 个元素");
CHECK(json_bool_value(json_array_get(json_object_get(v, "b"), 0), 0) == 1,
"b[0] 应为 true");
CHECK(json_array_get(json_object_get(v, "b"), 2)->type == JSON_NULL,
"b[2] 应为 null");
CHECK(strcmp(json_get_string(v, "c"), "hi") == 0, "c 应为 hi");
char *dump = json_dump(v);
JsonValue *v2 = json_parse(dump, err, sizeof err);
CHECK(v2 != NULL, "往返解析失败: %s", err);
char *dump2 = json_dump(v2);
CHECK(strcmp(dump, dump2) == 0, "往返不一致:\n %s\n %s", dump, dump2);
free(dump);
free(dump2);
json_free(v);
json_free(v2);
}
static void test_escapes(void) {
puts("test_escapes");
char err[256];
JsonValue *v = json_parse(
"\"a\\nb\\t\\\"c\\\\d\\u4e2d\\u6587\\uD83D\\uDE00\"", err, sizeof err);
CHECK(v != NULL, "解析失败: %s", err);
if (!v)
return;
const char *s = json_string_value(v);
CHECK(strcmp(s, "a\nb\t\"c\\d中文😀") == 0, "转义解码错误: [%s]", s);
char *q = json_quote_string(s);
JsonValue *v2 = json_parse(q, err, sizeof err);
CHECK(v2 != NULL, "转义往返失败: %s / %s", q, err);
CHECK(v2 && strcmp(json_string_value(v2), s) == 0, "转义往返内容不一致");
free(q);
json_free(v);
json_free(v2);
}
static void test_numbers(void) {
puts("test_numbers");
char err[256];
JsonValue *v =
json_parse("[0, -1, 3.5, 1e3, -2.5e-2, 123456789012]", err, sizeof err);
CHECK(v != NULL, "解析失败: %s", err);
if (!v)
return;
CHECK(json_number_value(json_array_get(v, 0), -1) == 0, "[0]");
CHECK(json_number_value(json_array_get(v, 1), 0) == -1, "[1]");
CHECK(json_number_value(json_array_get(v, 2), 0) == 3.5, "[2]");
CHECK(json_number_value(json_array_get(v, 3), 0) == 1000, "[3]");
CHECK(json_number_value(json_array_get(v, 4), 0) == -0.025, "[4]");
CHECK(json_number_value(json_array_get(v, 5), 0) == 123456789012.0, "[5]");
json_free(v);
/* 严格按 RFC 8259 扫描数字: 这些以前全都被接受 —— strtod 会把 "1e" 读成 1、
* "5." 读成 5, 于是一个手写错的数字会被静默当成另一个数用下去 (配置文件、
* 存档都是手写的, 用户只会在很久之后从一个不相干的现象里发现它). */
const char *bad_nums[] = {
"01", "-01", "5.", ".5", "+1", "1e", "1e+", "1e-", "1.2e", "-", "1_000",
};
for (size_t i = 0; i < sizeof bad_nums / sizeof bad_nums[0]; i++) {
JsonValue *b = json_parse(bad_nums[i], err, sizeof err);
CHECK(b == NULL, "非法数字应被拒绝: [%s]", bad_nums[i]);
json_free(b);
}
/* 边界上合法的写法不能被误伤 */
const char *ok_nums[] = {
"0",
"-0",
"0.0",
"10",
"-1.5",
"1e3",
"1E3",
"1e+3",
"1e-3",
"0.1e10",
"123456789012345678901234567890",
"1e308",
};
for (size_t i = 0; i < sizeof ok_nums / sizeof ok_nums[0]; i++) {
JsonValue *g = json_parse(ok_nums[i], err, sizeof err);
CHECK(g != NULL && g->type == JSON_NUMBER, "合法数字被拒: [%s] (%s)",
ok_nums[i], err);
json_free(g);
}
}
static void test_invalid(void) {
puts("test_invalid");
char err[256];
const char *bad[] = {
"{", "}", "[1,]", "{\"a\":}", "\"unterminated",
"tru", "{\"a\" 1}", "[1 2]", "01abc", "",
" ", "{\"a\":1}}", "[1,2] extra",
};
for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) {
JsonValue *v = json_parse(bad[i], err, sizeof err);
CHECK(v == NULL, "应当解析失败但成功了: [%s]", bad[i]);
json_free(v);
}
/* 空字符串是合法 JSON 字符串 */
JsonValue *e = json_parse("\"\"", err, sizeof err);
CHECK(e != NULL && strcmp(json_string_value(e), "") == 0,
"空字符串应为合法");
json_free(e);
}
static void test_builders(void) {
puts("test_builders");
JsonValue *o = json_new_object();
json_object_set_string(o, "model", "gpt-x");
json_object_set_number(o, "n", 3);
json_object_set_bool(o, "ok", 1);
JsonValue *arr = json_new_array();
json_array_push(arr, json_new_string("a"));
json_array_push(arr, json_new_string("b"));
json_object_set(o, "items", arr);
json_object_set_string(o, "model", "gpt-y"); /* 覆盖 */
char *s = json_dump(o);
CHECK(strcmp(s, "{\"model\":\"gpt-y\",\"n\":3,\"ok\":true,\"items\":[\"a\","
"\"b\"]}") == 0,
"构造结果不对: %s", s);
free(s);
/* clone 独立性 */
JsonValue *c = json_clone(o);
json_object_set_string(o, "model", "changed");
CHECK(strcmp(json_get_string(c, "model"), "gpt-y") == 0,
"clone 应相互独立");
json_object_remove(o, "model");
CHECK(json_object_get(o, "model") == NULL, "remove 应删除键");
json_free(o);
json_free(c);
}
static void test_nested_depth(void) {
puts("test_nested_depth");
char err[256];
size_t n = 300;
char *deep = xmalloc(n * 2 + 1);
for (size_t i = 0; i < n; i++)
deep[i] = '[';
for (size_t i = 0; i < n; i++)
deep[n + i] = ']';
deep[n * 2] = '\0';
JsonValue *v = json_parse(deep, err, sizeof err);
CHECK(v == NULL, "超深嵌套应被拒绝");
free(deep);
JsonValue *ok = json_parse("[[[1]]]", err, sizeof err);
CHECK(ok != NULL, "正常嵌套应成功");
json_free(ok);
}
/* 报错必须能定位: 只有"语法错了"而不知道错在哪, 等于没有报错 */
static void test_error_location(void) {
puts("test_error_location");
char err[256];
JsonValue *v;
/* 尾随逗号: 行号必须指向真正出问题的那一行 */
v = json_parse("{\n \"a\": 1,\n}\n", err, sizeof err);
CHECK(v == NULL, "尾随逗号应报错");
CHECK(strstr(err, "第 3 行") != NULL, "应报出行号: %s", err);
CHECK(strstr(err, "期望对象的键") != NULL, "应说明期望什么: %s", err);
CHECK(strstr(err, "}") != NULL, "应回显出错处原文: %s", err);
json_free(v);
/* 未闭合的数组: 位置应指向输入结束处 */
v = json_parse("[1, 2\n", err, sizeof err);
CHECK(v == NULL, "未闭合数组应报错");
CHECK(strstr(err, "第 2 行") != NULL, "未闭合应指向结尾: %s", err);
CHECK(strstr(err, "结束") != NULL, "应说明输入在此结束: %s", err);
json_free(v);
/* 缺冒号 */
v = json_parse("{\"a\" 1}", err, sizeof err);
CHECK(v == NULL, "缺冒号应报错");
CHECK(strstr(err, "第 1 行") && strstr(err, "':'") != NULL, "缺冒号: %s",
err);
json_free(v);
/* 尾部多余内容 */
v = json_parse("{\"a\": 1} extra", err, sizeof err);
CHECK(v == NULL, "尾部多余内容应报错");
CHECK(strstr(err, "多余") != NULL, "应说明是多余内容: %s", err);
json_free(v);
/* 非法字符要被回显出来 */
v = json_parse("{\n \"a\": @\n}", err, sizeof err);
CHECK(v == NULL, "非法字符应报错");
CHECK(strstr(err, "@") != NULL, "应回显非法字符: %s", err);
CHECK(strstr(err, "第 2 行") != NULL, "行号应为第 2 行: %s", err);
json_free(v);
/* 控制字符不能被原样吐到终端上 */
v = json_parse("{\"a\": \x01\x02}", err, sizeof err);
CHECK(v == NULL, "控制字符应报错");
CHECK(strchr(err, '\x01') == NULL, "错误信息里不应含原始控制字符");
json_free(v);
/* 合法输入不受影响 */
v = json_parse("{\"a\": [1, {\"b\": null}]}", err, sizeof err);
CHECK(v != NULL, "合法输入不应报错: %s", err);
json_free(v);
}
/* 序列化出去的东西必须是**合法 UTF-8**.
*
* 服务端会因为请求体里有非法 UTF-8 直接回
* 400 ... invalid unicode code point
* 而那段坏数据一旦进了对话历史, 之后每次请求都会失败 —— 除非丢掉整轮对话.
* 坏字节的来源很日常: 某条命令在非 UTF-8 locale 下打出来的路径、被截断的多
* 字节字符、二进制垃圾. 所以这条不能只靠"在入口清理", 必须在序列化器这层兜住:
* 出口是唯一能保证"发出去的永远合法"的地方.
*
* 这个校验函数故意用 utf8_seq_len 自己走一遍, 而不是信任 dump 的实现 ——
* 断言的是**产物**的性质, 不是实现的自述. */
static int utf8_all_valid(const char *s) {
size_t n = strlen(s), i = 0;
while (i < n) {
size_t adv = utf8_seq_len(s + i, n - i);
if (adv == 0)
return 0;
i += adv;
}
return 1;
}
static void test_dump_utf8_safety(void) {
puts("test_dump_utf8_safety");
/* utf8_seq_len: 合法序列返回长度 */
CHECK(utf8_seq_len("\xe4\xb8\xad", 3) == 3, "合法 CJK 应是 3 字节");
CHECK(utf8_seq_len("A", 1) == 1, "ASCII 应是 1 字节");
CHECK(utf8_seq_len("\xf0\x9f\x98\x80", 4) == 4, "emoji 应是 4 字节");
CHECK(utf8_seq_len("\xc2\x80", 2) == 2, "U+0080 的 2 字节编码应合法");
/* 非法的一律返回 0 */
CHECK(utf8_seq_len("\x80", 1) == 0, "孤立续字节应非法");
CHECK(utf8_seq_len("\xe4\xb8", 2) == 0, "被截断的序列应非法");
CHECK(utf8_seq_len("\xc0\xaf", 2) == 0, "过长编码应非法");
CHECK(utf8_seq_len("\xed\xa0\x80", 3) == 0, "UTF-16 代理区应非法");
CHECK(utf8_seq_len("\xf5\x80\x80\x80", 4) == 0, "超出 U+10FFFF 应非法");
CHECK(utf8_seq_len("\xe4\x41\x42", 3) == 0, "续字节格式不对应非法");
/* 各种坏字节经 json_dump 之后, 整份输出必须是合法 UTF-8 */
static const char *bad[] = {
"a\x80"
"b", /* 孤立续字节 */
"a\xe4\xb8"
"b", /* 截断的 3 字节序列 */
"a\xc0\xaf"
"b", /* 过长编码 (历史上被用来夹带 '/') */
"a\xed\xa0\x80"
"b", /* UTF-16 代理区 */
"a\xf5\x80\x80\x80"
"b", /* 超出 U+10FFFF */
"\xff\xfe", /* 二进制垃圾 */
"\xe4\xb8\xad\x80\xe6", /* 合法中文里夹一个坏字节 */
};
for (size_t k = 0; k < sizeof bad / sizeof bad[0]; k++) {
JsonValue *o = json_new_object();
json_object_set_string(o, "content", bad[k]);
char *d = json_dump(o);
CHECK(utf8_all_valid(d), "第 %zu 个坏输入序列化后仍含非法 UTF-8: %s", k,
d);
/* 而且我们的解析器 (和服务端一样严格)必须收得下 */
char err[256];
JsonValue *back = json_parse(d, err, sizeof err);
CHECK(back != NULL, "第 %zu 个输出无法解析回来: %s", k, err);
if (back) {
const char *got = json_get_string(back, "content");
CHECK(got && strstr(got, "\xef\xbf\xbd") != NULL,
"第 %zu 个输出里应出现 U+FFFD 替换字符", k);
json_free(back);
}
free(d);
json_free(o);
}
/* 合法的多字节内容必须**原样保留** —— 别把好数据一起误伤 */
JsonValue *o = json_new_object();
json_object_set_string(o, "content", "中文 😀 tail");
char *d = json_dump(o);
CHECK(strstr(d, "中文") != NULL, "合法中文不该被改动: %s", d);
CHECK(strstr(d, "\xf0\x9f\x98\x80") != NULL, "合法 emoji 不该被改动: %s",
d);
CHECK(strstr(d, "\xef\xbf\xbd") == NULL, "合法输入不该出现替换字符: %s", d);
free(d);
json_free(o);
}
int main(void) {
test_basic();
test_escapes();
test_numbers();
test_invalid();
test_builders();
test_nested_depth();
test_error_location();
test_dump_utf8_safety();
printf("\n%d 项检查, %d 项失败\n", checks, failures);
return failures ? 1 : 0;
}