style: 应用 clang-format
This commit is contained in:
1 parent
016a45bc6e
commit
d1612f1d31
65 files changed
+7288
-5983
No files matched your search
+122
-72
@@ -1,9 +1,9 @@
|
||||
/* fuzz.c - 对抗性模糊测试: JSON 解析器 + Markdown 渲染器
|
||||
*
|
||||
* 这两个模块是"解析不可信输入"的地方 (服务端响应、模型输出),
|
||||
* 也是最可能出崩溃/越界/泄漏的地方. 所以用随机输入去砸它们.
|
||||
* 这两个模块是"解析不可信输入"的地方 (服务端响应、模型输出),
|
||||
* 也是最可能出崩溃/越界/泄漏的地方. 所以用随机输入去砸它们.
|
||||
*
|
||||
* 检查的不变量:
|
||||
* 检查的不变量:
|
||||
* JSON
|
||||
* 1. 任何输入都不能崩溃、不能泄漏 (靠 ASan/UBSan/LeakSanitizer)
|
||||
* 2. 解析成功 => dump 再 parse 再 dump, 两次 dump 必须一致 (幂等)
|
||||
@@ -19,15 +19,14 @@
|
||||
#include "markdown.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static uint64_t RNG;
|
||||
|
||||
static uint32_t rnd(void)
|
||||
{
|
||||
static uint32_t rnd(void) {
|
||||
RNG ^= RNG << 13;
|
||||
RNG ^= RNG >> 7;
|
||||
RNG ^= RNG << 17;
|
||||
@@ -41,13 +40,24 @@ static uint32_t rnd_below(uint32_t n) { return n ? rnd() % n : 0; }
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static const char *STR_POOL[] = {
|
||||
"", "a", "hello", "中文", "😀", "line\nbreak", "tab\there", "quote\"x",
|
||||
"back\\slash", "ctrl\x01\x1f", "surrogate\u00e9", "aaaaaaaaaaaaaaaaaaaa",
|
||||
"/", "\x7f", "\xc3\xa9\xe4\xb8\xad",
|
||||
"",
|
||||
"a",
|
||||
"hello",
|
||||
"中文",
|
||||
"😀",
|
||||
"line\nbreak",
|
||||
"tab\there",
|
||||
"quote\"x",
|
||||
"back\\slash",
|
||||
"ctrl\x01\x1f",
|
||||
"surrogate\u00e9",
|
||||
"aaaaaaaaaaaaaaaaaaaa",
|
||||
"/",
|
||||
"\x7f",
|
||||
"\xc3\xa9\xe4\xb8\xad",
|
||||
};
|
||||
|
||||
static void gen_json_string(StrBuf *sb)
|
||||
{
|
||||
static void gen_json_string(StrBuf *sb) {
|
||||
const char *s = STR_POOL[rnd_below(sizeof STR_POOL / sizeof STR_POOL[0])];
|
||||
char *q = json_quote_string(s);
|
||||
sb_puts(sb, q);
|
||||
@@ -55,27 +65,37 @@ static void gen_json_string(StrBuf *sb)
|
||||
}
|
||||
|
||||
/* 递归生成合法 JSON */
|
||||
static void gen_json(StrBuf *sb, int depth)
|
||||
{
|
||||
static void gen_json(StrBuf *sb, int depth) {
|
||||
uint32_t kind = rnd_below(depth > 3 ? 4 : 6);
|
||||
switch (kind) {
|
||||
case 0: sb_puts(sb, "null"); break;
|
||||
case 1: sb_puts(sb, rnd() & 1 ? "true" : "false"); break;
|
||||
case 0:
|
||||
sb_puts(sb, "null");
|
||||
break;
|
||||
case 1:
|
||||
sb_puts(sb, rnd() & 1 ? "true" : "false");
|
||||
break;
|
||||
case 2: {
|
||||
char buf[64];
|
||||
int mode = (int)rnd_below(3);
|
||||
if (mode == 0) snprintf(buf, sizeof buf, "%d", (int)rnd() - 1000);
|
||||
else if (mode == 1) snprintf(buf, sizeof buf, "%.6g", (double)rnd() / 7.0);
|
||||
else snprintf(buf, sizeof buf, "%de%d", (int)rnd_below(1000), (int)rnd_below(20) - 10);
|
||||
if (mode == 0)
|
||||
snprintf(buf, sizeof buf, "%d", (int)rnd() - 1000);
|
||||
else if (mode == 1)
|
||||
snprintf(buf, sizeof buf, "%.6g", (double)rnd() / 7.0);
|
||||
else
|
||||
snprintf(buf, sizeof buf, "%de%d", (int)rnd_below(1000),
|
||||
(int)rnd_below(20) - 10);
|
||||
sb_puts(sb, buf);
|
||||
break;
|
||||
}
|
||||
case 3: gen_json_string(sb); break;
|
||||
case 3:
|
||||
gen_json_string(sb);
|
||||
break;
|
||||
case 4: {
|
||||
sb_putc(sb, '[');
|
||||
uint32_t n = rnd_below(5);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
if (i) sb_putc(sb, ',');
|
||||
if (i)
|
||||
sb_putc(sb, ',');
|
||||
gen_json(sb, depth + 1);
|
||||
}
|
||||
sb_putc(sb, ']');
|
||||
@@ -85,7 +105,8 @@ static void gen_json(StrBuf *sb, int depth)
|
||||
sb_putc(sb, '{');
|
||||
uint32_t n = rnd_below(5);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
if (i) sb_putc(sb, ',');
|
||||
if (i)
|
||||
sb_putc(sb, ',');
|
||||
gen_json_string(sb);
|
||||
sb_putc(sb, ':');
|
||||
gen_json(sb, depth + 1);
|
||||
@@ -97,8 +118,7 @@ static void gen_json(StrBuf *sb, int depth)
|
||||
}
|
||||
|
||||
/* 返回 1 表示这一轮失败 */
|
||||
static int fuzz_json_once(char *err, size_t errlen)
|
||||
{
|
||||
static int fuzz_json_once(char *err, size_t errlen) {
|
||||
StrBuf sb;
|
||||
sb_init(&sb);
|
||||
|
||||
@@ -107,36 +127,43 @@ static int fuzz_json_once(char *err, size_t errlen)
|
||||
/* 变异模式: 先造合法 JSON, 再随机破坏 */
|
||||
gen_json(&sb, 0);
|
||||
size_t n = sb.len;
|
||||
if (n == 0) { sb_free(&sb); return 0; }
|
||||
/* 每次突变都必须基于"当前"长度. 早期版本用了突变前记下的旧长度,
|
||||
* 截断之后再按下标写就会覆盖掉结尾的 '\0', 后续 strlen 冲出分配区,
|
||||
* 让 ASan 在 json_parse 里报出假阳性 —— 模糊测试器自己不可信比没有更糟. */
|
||||
if (n == 0) {
|
||||
sb_free(&sb);
|
||||
return 0;
|
||||
}
|
||||
/* 每次突变都必须基于"当前"长度. 早期版本用了突变前记下的旧长度,
|
||||
* 截断之后再按下标写就会覆盖掉结尾的 '\0', 后续 strlen 冲出分配区,
|
||||
* 让 ASan 在 json_parse 里报出假阳性 —— 模糊测试器自己不可信比没有更糟.
|
||||
*/
|
||||
uint32_t muts = 1 + rnd_below(4);
|
||||
for (uint32_t i = 0; i < muts && sb.len > 0; i++) {
|
||||
uint32_t op = rnd_below(3);
|
||||
if (op == 0) { /* 翻一个字节 (下标恒 < len) */
|
||||
if (op == 0) { /* 翻一个字节 (下标恒 < len) */
|
||||
sb.data[rnd_below((uint32_t)sb.len)] = (char)(rnd() & 0xFF);
|
||||
} else if (op == 1) { /* 截断 */
|
||||
} else if (op == 1) { /* 截断 */
|
||||
sb.len = rnd_below((uint32_t)sb.len);
|
||||
} else { /* 插入一个字节 */
|
||||
} else { /* 插入一个字节 */
|
||||
if (sb.len < 4096) {
|
||||
size_t pos = rnd_below((uint32_t)sb.len + 1);
|
||||
sb_reserve(&sb, 1);
|
||||
memmove(sb.data + pos + 1, sb.data + pos, sb.len - pos + 1);
|
||||
sb.data[pos] = (char)"{}[]\",:0123456789.eE+-truefalsn"[rnd_below(30)];
|
||||
sb.data[pos] =
|
||||
(char)"{}[]\",:0123456789.eE+-truefalsn"[rnd_below(30)];
|
||||
sb.len++;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 无论怎么突变, 结尾符必须是好的 */
|
||||
if (sb.data) sb.data[sb.len] = '\0';
|
||||
if (sb.data)
|
||||
sb.data[sb.len] = '\0';
|
||||
n = sb.len;
|
||||
} else {
|
||||
/* 纯随机字节 */
|
||||
uint32_t n = rnd_below(256);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
/* 偏向 JSON 元字符, 提高触发率 */
|
||||
static const char meta[] = "{}[]\",:0123456789.eE+-truefalsn \t\n\\/u";
|
||||
static const char meta[] =
|
||||
"{}[]\",:0123456789.eE+-truefalsn \t\n\\/u";
|
||||
char c = (rnd() & 3) ? meta[rnd_below(sizeof meta - 1)]
|
||||
: (char)(rnd() & 0xFF);
|
||||
sb_putc(&sb, c);
|
||||
@@ -175,14 +202,19 @@ static int fuzz_json_once(char *err, size_t errlen)
|
||||
}
|
||||
d2 = json_dump(v2);
|
||||
if (strcmp(d1, d2) != 0) {
|
||||
snprintf(err, errlen, "dump 不幂等: \n 输入 %.200s\n 一次 %.200s\n 二次 %.200s",
|
||||
input, d1, d2);
|
||||
free(d1); free(d2);
|
||||
json_free(v); json_free(v2);
|
||||
snprintf(
|
||||
err, errlen,
|
||||
"dump 不幂等: \n 输入 %.200s\n 一次 %.200s\n 二次 %.200s",
|
||||
input, d1, d2);
|
||||
free(d1);
|
||||
free(d2);
|
||||
json_free(v);
|
||||
json_free(v2);
|
||||
sb_free(&sb);
|
||||
return 1;
|
||||
}
|
||||
free(d1); free(d2);
|
||||
free(d1);
|
||||
free(d2);
|
||||
json_free(v2);
|
||||
json_free(v);
|
||||
}
|
||||
@@ -197,12 +229,12 @@ static int fuzz_json_once(char *err, size_t errlen)
|
||||
#define MD_RENDER_LIMIT (1u << 20)
|
||||
|
||||
/* 渲染到内存; chunk==0 表示一次性 */
|
||||
static char *md_render(const char *src, size_t len, size_t chunk)
|
||||
{
|
||||
static char *md_render(const char *src, size_t len, size_t chunk) {
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *f = open_memstream(&buf, &size);
|
||||
if (!f) return NULL;
|
||||
if (!f)
|
||||
return NULL;
|
||||
|
||||
MdRenderer *r = md_new(1);
|
||||
md_set_out(r, f);
|
||||
@@ -222,16 +254,14 @@ static char *md_render(const char *src, size_t len, size_t chunk)
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int markdown_metachar(int c)
|
||||
{
|
||||
static int markdown_metachar(int c) {
|
||||
/* 这些字符会触发渲染器改写输出; 其余字符必须原样通过 */
|
||||
return c == '*' || c == '_' || c == '`' || c == '[' || c == ']' ||
|
||||
c == '#' || c == '>' || c == '-' || c == '+' || c == '|' ||
|
||||
c == '\n' || c == '\r';
|
||||
}
|
||||
|
||||
static int fuzz_md_once(char *err, size_t errlen)
|
||||
{
|
||||
static int fuzz_md_once(char *err, size_t errlen) {
|
||||
StrBuf sb;
|
||||
sb_init(&sb);
|
||||
|
||||
@@ -240,7 +270,7 @@ static int fuzz_md_once(char *err, size_t errlen)
|
||||
size_t alen = sizeof alpha - 1;
|
||||
|
||||
uint32_t n = rnd_below(400);
|
||||
int pure = (int)rnd_below(4) == 0; /* 1/4 概率生成"无元字符"的干净文本 */
|
||||
int pure = (int)rnd_below(4) == 0; /* 1/4 概率生成"无元字符"的干净文本 */
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
char c;
|
||||
if (pure) {
|
||||
@@ -249,41 +279,57 @@ static int fuzz_md_once(char *err, size_t errlen)
|
||||
} while (markdown_metachar((unsigned char)c) || c == '\0');
|
||||
} else {
|
||||
uint32_t mode = rnd_below(10);
|
||||
if (mode < 7) c = alpha[rnd_below((uint32_t)alen)];
|
||||
else if (mode < 9) c = (char)(0x20 + rnd_below(0x5F));
|
||||
else c = (char)(rnd() & 0xFF); /* 偶尔来点非法 UTF-8 */
|
||||
if (mode < 7)
|
||||
c = alpha[rnd_below((uint32_t)alen)];
|
||||
else if (mode < 9)
|
||||
c = (char)(0x20 + rnd_below(0x5F));
|
||||
else
|
||||
c = (char)(rnd() & 0xFF); /* 偶尔来点非法 UTF-8 */
|
||||
}
|
||||
if (c == '\0') c = 'x';
|
||||
if (c == '\0')
|
||||
c = 'x';
|
||||
sb_putc(&sb, c);
|
||||
}
|
||||
|
||||
const char *src = sb.data ? sb.data : "";
|
||||
size_t len = sb.len;
|
||||
if (len == 0) { sb_free(&sb); return 0; }
|
||||
if (len == 0) {
|
||||
sb_free(&sb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *whole = md_render(src, len, 0);
|
||||
if (!whole) { sb_free(&sb); return 0; }
|
||||
if (!whole) {
|
||||
sb_free(&sb);
|
||||
return 0;
|
||||
}
|
||||
if (strlen(whole) > MD_RENDER_LIMIT * 4) {
|
||||
snprintf(err, errlen, "输出异常膨胀: 输入 %zu 字节 => %zu 字节",
|
||||
len, strlen(whole));
|
||||
free(whole); sb_free(&sb);
|
||||
snprintf(err, errlen, "输出异常膨胀: 输入 %zu 字节 => %zu 字节", len,
|
||||
strlen(whole));
|
||||
free(whole);
|
||||
sb_free(&sb);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 分片不变性 */
|
||||
size_t chunks[4];
|
||||
for (int k = 0; k < 4; k++) chunks[k] = 1 + rnd_below(8);
|
||||
for (int k = 0; k < 4; k++)
|
||||
chunks[k] = 1 + rnd_below(8);
|
||||
for (int k = 0; k < 4; k++) {
|
||||
char *part = md_render(src, len, chunks[k]);
|
||||
if (!part) continue;
|
||||
if (!part)
|
||||
continue;
|
||||
if (strcmp(whole, part) != 0) {
|
||||
snprintf(err, errlen,
|
||||
"分片不变性被破坏 (%zu 字节/片)",
|
||||
chunks[k]);
|
||||
snprintf(err, errlen, "分片不变性被破坏 (%zu 字节/片)", chunks[k]);
|
||||
/* 落盘, 方便用 build/reduce 做最小化 */
|
||||
FILE *fp = fopen("build/md_fail.bin", "wb");
|
||||
if (fp) { fwrite(src, 1, len, fp); fclose(fp); }
|
||||
free(part); free(whole); sb_free(&sb);
|
||||
if (fp) {
|
||||
fwrite(src, 1, len, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
free(part);
|
||||
free(whole);
|
||||
sb_free(&sb);
|
||||
return 1;
|
||||
}
|
||||
free(part);
|
||||
@@ -292,9 +338,11 @@ static int fuzz_md_once(char *err, size_t errlen)
|
||||
/* 无元字符 => 必须逐字节原样 */
|
||||
if (pure) {
|
||||
if (strcmp(whole, src) != 0) {
|
||||
snprintf(err, errlen, "纯净文本被改写了: \n 输入 %.300s\n 输出 %.300s",
|
||||
src, whole);
|
||||
free(whole); sb_free(&sb);
|
||||
snprintf(err, errlen,
|
||||
"纯净文本被改写了: \n 输入 %.300s\n 输出 %.300s", src,
|
||||
whole);
|
||||
free(whole);
|
||||
sb_free(&sb);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -306,23 +354,24 @@ static int fuzz_md_once(char *err, size_t errlen)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int main(int argc, char **argv) {
|
||||
long iters = (argc > 1) ? strtol(argv[1], NULL, 10) : 3000;
|
||||
RNG = (argc > 2) ? strtoull(argv[2], NULL, 10) : 0x9E3779B97F4A7C15ULL;
|
||||
if (!RNG) RNG = 1;
|
||||
if (!RNG)
|
||||
RNG = 1;
|
||||
|
||||
char err[512];
|
||||
long json_fail = 0, md_fail = 0;
|
||||
|
||||
printf("模糊测试: JSON %ld 轮 + Markdown %ld 轮 (种子 %llu)\n",
|
||||
iters, iters, (unsigned long long)RNG);
|
||||
printf("模糊测试: JSON %ld 轮 + Markdown %ld 轮 (种子 %llu)\n", iters,
|
||||
iters, (unsigned long long)RNG);
|
||||
|
||||
for (long i = 0; i < iters; i++) {
|
||||
err[0] = '\0';
|
||||
if (fuzz_json_once(err, sizeof err)) {
|
||||
printf(" ✗ JSON 第 %ld 轮失败: %s\n", i, err);
|
||||
if (++json_fail >= 3) break;
|
||||
if (++json_fail >= 3)
|
||||
break;
|
||||
}
|
||||
}
|
||||
printf(" JSON %ld 轮, %ld 次失败\n", iters, json_fail);
|
||||
@@ -331,7 +380,8 @@ int main(int argc, char **argv)
|
||||
err[0] = '\0';
|
||||
if (fuzz_md_once(err, sizeof err)) {
|
||||
printf(" ✗ Markdown 第 %ld 轮失败: %s\n", i, err);
|
||||
if (++md_fail >= 3) break;
|
||||
if (++md_fail >= 3)
|
||||
break;
|
||||
}
|
||||
}
|
||||
printf(" Markdown %ld 轮, %ld 次失败\n", iters, md_fail);
|
||||
|
||||
+47
-25
@@ -1,6 +1,6 @@
|
||||
/* reduce.c - 把 fuzz 找到的失败输入最小化到最短复现样例
|
||||
*
|
||||
* 判定 (oracle): 同一段输入, "一次性喂" 与 "逐字节喂" 的渲染结果不一致.
|
||||
* 判定 (oracle): 同一段输入, "一次性喂" 与 "逐字节喂" 的渲染结果不一致.
|
||||
* 用法: build/reduce build/md_fail.bin
|
||||
*/
|
||||
#include "markdown.h"
|
||||
@@ -11,8 +11,7 @@
|
||||
|
||||
/* 注意: md_feed 只接受 NUL 结尾的字符串, 而最小化过程中的缓冲区
|
||||
* 是"长度受限"的, 所以必须先做一份带结尾符的副本. */
|
||||
static char *render(const char *src, size_t len, size_t chunk)
|
||||
{
|
||||
static char *render(const char *src, size_t len, size_t chunk) {
|
||||
char *text = xmalloc(len + 1);
|
||||
memcpy(text, src, len);
|
||||
text[len] = '\0';
|
||||
@@ -20,7 +19,10 @@ static char *render(const char *src, size_t len, size_t chunk)
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *f = open_memstream(&buf, &size);
|
||||
if (!f) { free(text); return NULL; }
|
||||
if (!f) {
|
||||
free(text);
|
||||
return NULL;
|
||||
}
|
||||
MdRenderer *r = md_new(1);
|
||||
md_set_out(r, f);
|
||||
if (chunk == 0) {
|
||||
@@ -41,43 +43,60 @@ static char *render(const char *src, size_t len, size_t chunk)
|
||||
}
|
||||
|
||||
/* 只要"一次性"与"逐字节"不一致就算失败 */
|
||||
static int fails(const char *src, size_t len)
|
||||
{
|
||||
if (len == 0) return 0;
|
||||
static int fails(const char *src, size_t len) {
|
||||
if (len == 0)
|
||||
return 0;
|
||||
/* md_feed 以 NUL 结尾, 输入里不能有 NUL */
|
||||
for (size_t i = 0; i < len; i++) if (src[i] == '\0') return 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
if (src[i] == '\0')
|
||||
return 0;
|
||||
char *a = render(src, len, 0);
|
||||
char *b = render(src, len, 1);
|
||||
int bad = (a && b && strcmp(a, b) != 0);
|
||||
if (bad) {
|
||||
/* 确认真的不是"空输出 vs 空输出"这种假阳性 */
|
||||
if (strcmp(a, b) == 0) bad = 0;
|
||||
if (strcmp(a, b) == 0)
|
||||
bad = 0;
|
||||
}
|
||||
free(a); free(b);
|
||||
free(a);
|
||||
free(b);
|
||||
return bad;
|
||||
}
|
||||
|
||||
static void show(const char *label, const char *buf, size_t len)
|
||||
{
|
||||
static void show(const char *label, const char *buf, size_t len) {
|
||||
printf("%s (%zu 字节): ", label, len);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)buf[i];
|
||||
if (c == '\n') printf("\\n");
|
||||
else if (c == '\t') printf("\\t");
|
||||
else if (c == 0x1b) printf("<ESC>");
|
||||
else if (c >= 0x20 && c < 0x7f) putchar(c);
|
||||
else printf("\\x%02x", c);
|
||||
if (c == '\n')
|
||||
printf("\\n");
|
||||
else if (c == '\t')
|
||||
printf("\\t");
|
||||
else if (c == 0x1b)
|
||||
printf("<ESC>");
|
||||
else if (c >= 0x20 && c < 0x7f)
|
||||
putchar(c);
|
||||
else
|
||||
printf("\\x%02x", c);
|
||||
}
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) { fputs("用法: reduce <失败输入文件>\n", stderr); return 2; }
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
fputs("用法: reduce <失败输入文件>\n", stderr);
|
||||
return 2;
|
||||
}
|
||||
size_t len = 0;
|
||||
char *buf = read_entire_file(argv[1], &len);
|
||||
if (!buf) { perror("读取失败"); return 2; }
|
||||
if (!fails(buf, len)) { puts("该输入当前不复现失败"); free(buf); return 1; }
|
||||
if (!buf) {
|
||||
perror("读取失败");
|
||||
return 2;
|
||||
}
|
||||
if (!fails(buf, len)) {
|
||||
puts("该输入当前不复现失败");
|
||||
free(buf);
|
||||
return 1;
|
||||
}
|
||||
|
||||
puts("== 原始失败输入 ==");
|
||||
show(" ", buf, len);
|
||||
@@ -85,7 +104,8 @@ int main(int argc, char **argv)
|
||||
char *a = render(buf, len, 0), *b = render(buf, len, 1);
|
||||
show(" 一次性", a, strlen(a));
|
||||
show(" 逐字节", b, strlen(b));
|
||||
free(a); free(b);
|
||||
free(a);
|
||||
free(b);
|
||||
}
|
||||
|
||||
/* 贪心块删除 */
|
||||
@@ -105,7 +125,8 @@ int main(int argc, char **argv)
|
||||
}
|
||||
free(save);
|
||||
}
|
||||
if (gran == 1) break;
|
||||
if (gran == 1)
|
||||
break;
|
||||
gran /= 2;
|
||||
}
|
||||
|
||||
@@ -115,7 +136,8 @@ int main(int argc, char **argv)
|
||||
char *a = render(buf, len, 0), *b = render(buf, len, 1);
|
||||
show(" 一次性", a, strlen(a));
|
||||
show(" 逐字节", b, strlen(b));
|
||||
free(a); free(b);
|
||||
free(a);
|
||||
free(b);
|
||||
}
|
||||
free(buf);
|
||||
return 0;
|
||||
|
||||
+64
-66
@@ -10,23 +10,23 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static size_t count_role(const Agent *a, const char *role)
|
||||
{
|
||||
static size_t count_role(const Agent *a, const char *role) {
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i < json_array_size(a->messages); i++) {
|
||||
const char *r = json_get_string(json_array_get(a->messages, i), "role");
|
||||
if (r && strcmp(r, role) == 0) n++;
|
||||
if (r && strcmp(r, role) == 0)
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
@@ -35,8 +35,7 @@ static size_t count_role(const Agent *a, const char *role)
|
||||
/* 上下文超限的自动恢复 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void add_msg(Agent *a, const char *role, const char *content)
|
||||
{
|
||||
static void add_msg(Agent *a, const char *role, const char *content) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", role);
|
||||
json_object_set_string(m, "content", content);
|
||||
@@ -44,8 +43,8 @@ static void add_msg(Agent *a, const char *role, const char *content)
|
||||
}
|
||||
|
||||
/* 造一条 assistant 带 tool_calls 的消息 */
|
||||
static void add_assistant_tool_call(Agent *a, const char *id, const char *name)
|
||||
{
|
||||
static void add_assistant_tool_call(Agent *a, const char *id,
|
||||
const char *name) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", "assistant");
|
||||
json_object_set_null(m, "content");
|
||||
@@ -62,8 +61,7 @@ static void add_assistant_tool_call(Agent *a, const char *id, const char *name)
|
||||
json_array_push(a->messages, m);
|
||||
}
|
||||
|
||||
static void add_tool_result(Agent *a, const char *id, const char *content)
|
||||
{
|
||||
static void add_tool_result(Agent *a, const char *id, const char *content) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", "tool");
|
||||
json_object_set_string(m, "tool_call_id", id);
|
||||
@@ -72,20 +70,17 @@ static void add_tool_result(Agent *a, const char *id, const char *content)
|
||||
}
|
||||
|
||||
/* 序列化历史, 便于断言 */
|
||||
static char *dump_history(const Agent *a)
|
||||
{
|
||||
return json_dump(a->messages);
|
||||
}
|
||||
static char *dump_history(const Agent *a) { return json_dump(a->messages); }
|
||||
|
||||
/* 配对不变量: 任何 assistant(tool_calls) 后面必须**紧跟**同 id 的 tool 消息.
|
||||
/* 配对不变量: 任何 assistant(tool_calls) 后面必须**紧跟**同 id 的 tool 消息.
|
||||
* 服务端就是这么要求的, 违反了下一轮直接 400 —— 所以每次动过历史之后都要查. */
|
||||
static void check_pairing(Agent *a)
|
||||
{
|
||||
static void check_pairing(Agent *a) {
|
||||
size_t n = json_array_size(a->messages);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
JsonValue *m = json_array_get(a->messages, i);
|
||||
JsonValue *tcs = json_object_get(m, "tool_calls");
|
||||
if (!tcs) continue;
|
||||
if (!tcs)
|
||||
continue;
|
||||
const char *id = json_get_string(json_array_get(tcs, 0), "id");
|
||||
CHECK(i + 1 < n, "tool_calls 之后必须有消息");
|
||||
if (i + 1 < n) {
|
||||
@@ -97,15 +92,14 @@ static void check_pairing(Agent *a)
|
||||
}
|
||||
}
|
||||
|
||||
/* 一轮一轮摘下去 (/back 走的就是这条路), 历史在每一步都必须仍然自洽.
|
||||
/* 一轮一轮摘下去 (/back 走的就是这条路), 历史在每一步都必须仍然自洽.
|
||||
*
|
||||
* 这里原来测的是"上下文超限自动裁剪" (agent_drop_oldest_round)—— 那套机制
|
||||
* 已经**整个删掉**: 它的判据只是错误文案里的关键词, 却会不可逆地删掉最老的
|
||||
* 几轮对话. 现在超限就是如实报错、历史一个字节都不动, 怎么处理由用户决定
|
||||
* (/back 摘一轮、/reset 重来). 但"摘掉一整轮之后 tool 配对不能散"这条
|
||||
* 不变量与由谁触发无关, 所以留着继续测. */
|
||||
static void test_take_rounds_keeps_pairing(void)
|
||||
{
|
||||
static void test_take_rounds_keeps_pairing(void) {
|
||||
puts("逐轮摘掉之后历史仍然自洽");
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
@@ -122,10 +116,11 @@ static void test_take_rounds_keeps_pairing(void)
|
||||
int rounds = 0;
|
||||
for (;;) {
|
||||
JsonValue *r = agent_take_last_round(&a);
|
||||
if (!r) break;
|
||||
if (!r)
|
||||
break;
|
||||
json_free(r);
|
||||
rounds++;
|
||||
check_pairing(&a); /* 每摘一轮都查一遍配对 */
|
||||
check_pairing(&a); /* 每摘一轮都查一遍配对 */
|
||||
}
|
||||
CHECK(rounds == 4, "共应摘掉 4 轮, 实际 %d", rounds);
|
||||
CHECK(agent_history_len(&a) == 1, "最后应只剩 system, 实际 %zu",
|
||||
@@ -133,8 +128,7 @@ static void test_take_rounds_keeps_pairing(void)
|
||||
agent_free(&a);
|
||||
}
|
||||
|
||||
static void test_take_last_round(void)
|
||||
{
|
||||
static void test_take_last_round(void) {
|
||||
puts("摘掉最近一轮 (/back 用)");
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
@@ -146,7 +140,8 @@ static void test_take_last_round(void)
|
||||
add_msg(&a, "user", "U2");
|
||||
add_msg(&a, "assistant", "A2");
|
||||
|
||||
CHECK(agent_history_len(&a) == 7, "初始 7 条, 实际 %zu", agent_history_len(&a));
|
||||
CHECK(agent_history_len(&a) == 7, "初始 7 条, 实际 %zu",
|
||||
agent_history_len(&a));
|
||||
|
||||
JsonValue *r = agent_take_last_round(&a);
|
||||
CHECK(r != NULL, "应摘到一轮");
|
||||
@@ -192,8 +187,7 @@ static void test_take_last_round(void)
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* 造一条 assistant, 带 count 个工具调用 (id 依次是 c0, c1, ...) */
|
||||
static JsonValue *mk_assistant(size_t count)
|
||||
{
|
||||
static JsonValue *mk_assistant(size_t count) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", "assistant");
|
||||
JsonValue *tcs = json_new_array();
|
||||
@@ -213,8 +207,7 @@ static JsonValue *mk_assistant(size_t count)
|
||||
return m;
|
||||
}
|
||||
|
||||
static JsonValue *mk_tool(const char *id)
|
||||
{
|
||||
static JsonValue *mk_tool(const char *id) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", "tool");
|
||||
json_object_set_string(m, "tool_call_id", id);
|
||||
@@ -222,8 +215,7 @@ static JsonValue *mk_tool(const char *id)
|
||||
return m;
|
||||
}
|
||||
|
||||
static JsonValue *mk_plain(const char *role, const char *text)
|
||||
{
|
||||
static JsonValue *mk_plain(const char *role, const char *text) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", role);
|
||||
json_object_set_string(m, "content", text);
|
||||
@@ -231,35 +223,37 @@ static JsonValue *mk_plain(const char *role, const char *text)
|
||||
}
|
||||
|
||||
/* 数一数某个 assistant 之后紧跟的 tool 消息覆盖了几个 id */
|
||||
static const char *nth_role(const JsonValue *msgs, size_t i)
|
||||
{
|
||||
static const char *nth_role(const JsonValue *msgs, size_t i) {
|
||||
return json_get_string(json_array_get((JsonValue *)msgs, i), "role");
|
||||
}
|
||||
|
||||
static int pairing_ok(const JsonValue *msgs)
|
||||
{
|
||||
static int pairing_ok(const JsonValue *msgs) {
|
||||
size_t n = json_array_size((JsonValue *)msgs);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
JsonValue *m = json_array_get((JsonValue *)msgs, i);
|
||||
const char *r = json_get_string(m, "role");
|
||||
if (!r || strcmp(r, "assistant") != 0) continue;
|
||||
if (!r || strcmp(r, "assistant") != 0)
|
||||
continue;
|
||||
JsonValue *tcs = json_object_get(m, "tool_calls");
|
||||
if (!tcs || json_array_size(tcs) == 0) continue;
|
||||
if (!tcs || json_array_size(tcs) == 0)
|
||||
continue;
|
||||
for (size_t k = 0; k < json_array_size(tcs); k++) {
|
||||
const char *id = json_get_string(json_array_get(tcs, k), "id");
|
||||
size_t j = i + 1;
|
||||
int found = 0;
|
||||
for (; j < n && strcmp(nth_role(msgs, j), "tool") == 0; j++)
|
||||
if (strcmp(json_get_string(json_array_get((JsonValue *)msgs, j),
|
||||
"tool_call_id"), id) == 0) found = 1;
|
||||
if (!found) return 0;
|
||||
"tool_call_id"),
|
||||
id) == 0)
|
||||
found = 1;
|
||||
if (!found)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void test_repair_pairing(void)
|
||||
{
|
||||
static void test_repair_pairing(void) {
|
||||
int fixed = 0;
|
||||
|
||||
puts("结构体检: 本来没问题就不该动它");
|
||||
@@ -290,18 +284,19 @@ static void test_repair_pairing(void)
|
||||
if (r) {
|
||||
CHECK(pairing_ok(r), "修完之后配对必须成立");
|
||||
/* 两处: 给 c1 补一条占位 (它没被回应), 再把错位的那条 c1 结果丢掉
|
||||
* (它被 user 消息隔开, 已经不在 tool 段里了 —— 留着就是孤儿).
|
||||
* (它被 user 消息隔开, 已经不在 tool 段里了 —— 留着就是孤儿).
|
||||
* 丢掉真实结果听着可惜, 但结构坏了只能二选一: 要么丢它, 要么留一段
|
||||
* 服务端必然拒绝的历史. 模型会从占位消息里知道该重跑. */
|
||||
CHECK(fixed == 2, "应补 1 处、丢 1 处孤儿, 实际 %d", fixed);
|
||||
CHECK(json_array_size(r) == 5,
|
||||
"补一条、丢一条之后仍是 5 条, 实际 %zu", json_array_size(r));
|
||||
CHECK(json_array_size(r) == 5, "补一条、丢一条之后仍是 5 条, 实际 %zu",
|
||||
json_array_size(r));
|
||||
/* 占位消息必须紧跟在 assistant 的 tool 段里, 而不是被塞到最后 */
|
||||
CHECK(strcmp(nth_role(r, 2), "tool") == 0 &&
|
||||
strcmp(nth_role(r, 3), "tool") == 0,
|
||||
strcmp(nth_role(r, 3), "tool") == 0,
|
||||
"两条 tool 消息应紧跟在 assistant 之后");
|
||||
const char *c = json_get_string(json_array_get(r, 3), "content");
|
||||
CHECK(c && strstr(c, "没有执行") == NULL && strstr(c, "没有得到结果") != NULL,
|
||||
CHECK(c && strstr(c, "没有执行") == NULL &&
|
||||
strstr(c, "没有得到结果") != NULL,
|
||||
"占位消息要说明它没拿到结果, 实际 %s", c ? c : "(null)");
|
||||
json_free(r);
|
||||
}
|
||||
@@ -313,7 +308,8 @@ static void test_repair_pairing(void)
|
||||
json_array_push(tail, mk_assistant(1));
|
||||
fixed = 0;
|
||||
r = agent_repair_tool_pairing(tail, &fixed);
|
||||
CHECK(r && pairing_ok(r) && fixed == 1, "结尾缺的也要补上 (fixed=%d)", fixed);
|
||||
CHECK(r && pairing_ok(r) && fixed == 1, "结尾缺的也要补上 (fixed=%d)",
|
||||
fixed);
|
||||
json_free(r);
|
||||
json_free(tail);
|
||||
|
||||
@@ -329,7 +325,8 @@ static void test_repair_pairing(void)
|
||||
|
||||
puts("空输入与边界");
|
||||
fixed = 7;
|
||||
CHECK(agent_repair_tool_pairing(NULL, &fixed) == NULL && fixed == 0, "NULL 输入");
|
||||
CHECK(agent_repair_tool_pairing(NULL, &fixed) == NULL && fixed == 0,
|
||||
"NULL 输入");
|
||||
JsonValue *empty = json_new_array();
|
||||
CHECK(agent_repair_tool_pairing(empty, &fixed) == NULL, "空历史没什么可修");
|
||||
json_free(empty);
|
||||
@@ -346,8 +343,7 @@ static void test_repair_pairing(void)
|
||||
/* 面向用户的历史口径: 不含 system */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_conversation_len(void)
|
||||
{
|
||||
static void test_conversation_len(void) {
|
||||
puts("历史条数: system 是配置, 不该算进对话");
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
@@ -382,8 +378,7 @@ static void test_conversation_len(void)
|
||||
agent_free(&b);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
CHECK(a.messages && a.messages->type == JSON_ARRAY, "messages 应为数组");
|
||||
@@ -397,11 +392,13 @@ int main(void)
|
||||
|
||||
/* 关键不变量: 重复设置 system 应替换而不是追加 */
|
||||
agent_add_system(&a, "S2");
|
||||
CHECK(count_role(&a, "system") == 1, "重复设置 system 不应叠加, 实际 %zu 条",
|
||||
count_role(&a, "system"));
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 0), "content"), "S2") == 0,
|
||||
CHECK(count_role(&a, "system") == 1,
|
||||
"重复设置 system 不应叠加, 实际 %zu 条", count_role(&a, "system"));
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 0), "content"),
|
||||
"S2") == 0,
|
||||
"新 system 应在第 0 条");
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 1), "content"), "U1") == 0,
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 1), "content"),
|
||||
"U1") == 0,
|
||||
"原有对话应保留");
|
||||
|
||||
agent_add_user(&a, "U2");
|
||||
@@ -411,7 +408,8 @@ int main(void)
|
||||
CHECK(agent_history_len(&a) == 1, "reset 后只保留 system, 实际 %zu",
|
||||
agent_history_len(&a));
|
||||
CHECK(count_role(&a, "system") == 1, "reset 后 system 仍在");
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 0), "content"), "S2") == 0,
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 0), "content"),
|
||||
"S2") == 0,
|
||||
"reset 保留的是最新 system");
|
||||
|
||||
/* 历史里没有 system 时 reset 应得到空历史 */
|
||||
|
||||
+73
-46
@@ -5,49 +5,53 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static int is_alnum_lower(const char *s, size_t n)
|
||||
{
|
||||
static int is_alnum_lower(const char *s, size_t n) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (!((s[i] >= 'a' && s[i] <= 'z') || (s[i] >= '0' && s[i] <= '9')))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
char tmpl[] = "/tmp/ligent_arch_XXXXXX";
|
||||
char *dir = mkdtemp(tmpl);
|
||||
if (!dir) { perror("mkdtemp"); return 2; }
|
||||
if (!dir) {
|
||||
perror("mkdtemp");
|
||||
return 2;
|
||||
}
|
||||
char err[256];
|
||||
|
||||
puts("根名: 8 位小写字母数字");
|
||||
char *n1 = NULL;
|
||||
CHECK(archive_next_name(dir, NULL, NULL, 0, &n1, err, sizeof err) == 0, "生成失败: %s", err);
|
||||
CHECK(archive_next_name(dir, NULL, NULL, 0, &n1, err, sizeof err) == 0,
|
||||
"生成失败: %s", err);
|
||||
CHECK(n1 && strlen(n1) == ARCHIVE_ROOT_LEN, "根名应为 %d 位, 实际 %s",
|
||||
ARCHIVE_ROOT_LEN, n1 ? n1 : "(null)");
|
||||
CHECK(n1 && is_alnum_lower(n1, strlen(n1)), "根名字符集应为 [a-z0-9]: %s", n1);
|
||||
CHECK(n1 && is_alnum_lower(n1, strlen(n1)), "根名字符集应为 [a-z0-9]: %s",
|
||||
n1);
|
||||
free(n1);
|
||||
|
||||
/* 空字符串与 NULL 等价: 都表示"没有来源" */
|
||||
char *n2 = NULL;
|
||||
CHECK(archive_next_name(dir, "", NULL, 0, &n2, err, sizeof err) == 0, "空 lineage 应可用");
|
||||
CHECK(archive_next_name(dir, "", NULL, 0, &n2, err, sizeof err) == 0,
|
||||
"空 lineage 应可用");
|
||||
CHECK(n2 && strlen(n2) == ARCHIVE_ROOT_LEN, "空 lineage 也应给出根名");
|
||||
free(n2);
|
||||
|
||||
@@ -57,7 +61,8 @@ int main(void)
|
||||
"分支生成失败: %s", err);
|
||||
CHECK(b && strncmp(b, "a1b2c3d4_", 9) == 0, "应以来源加下划线开头: %s", b);
|
||||
CHECK(b && strlen(b) == 8 + 1 + ARCHIVE_BRANCH_LEN, "分支长度不对: %s", b);
|
||||
CHECK(b && is_alnum_lower(b + 9, ARCHIVE_BRANCH_LEN), "后缀字符集不对: %s", b);
|
||||
CHECK(b && is_alnum_lower(b + 9, ARCHIVE_BRANCH_LEN), "后缀字符集不对: %s",
|
||||
b);
|
||||
free(b);
|
||||
|
||||
/* 多次生成应当不同 (随机性) */
|
||||
@@ -65,17 +70,20 @@ int main(void)
|
||||
archive_next_name(dir, "a1b2c3d4", NULL, 0, &x1, err, sizeof err);
|
||||
archive_next_name(dir, "a1b2c3d4", NULL, 0, &x2, err, sizeof err);
|
||||
CHECK(x1 && x2 && strcmp(x1, x2) != 0, "两次生成不应相同: %s / %s", x1, x2);
|
||||
free(x1); free(x2);
|
||||
free(x1);
|
||||
free(x2);
|
||||
|
||||
puts("长度上限: 接不下就重新起根");
|
||||
/* 边界: 来源长度 + 1 + 3 + ".json" <= 240 */
|
||||
size_t maxlineage = ARCHIVE_NAME_MAX - 1 - ARCHIVE_BRANCH_LEN - strlen(".json");
|
||||
size_t maxlineage =
|
||||
ARCHIVE_NAME_MAX - 1 - ARCHIVE_BRANCH_LEN - strlen(".json");
|
||||
char *ok = xmalloc(maxlineage + 1);
|
||||
memset(ok, 'a', maxlineage);
|
||||
ok[maxlineage] = '\0';
|
||||
|
||||
char *edge = NULL;
|
||||
CHECK(archive_next_name(dir, ok, NULL, 0, &edge, err, sizeof err) == 0, "边界生成失败");
|
||||
CHECK(archive_next_name(dir, ok, NULL, 0, &edge, err, sizeof err) == 0,
|
||||
"边界生成失败");
|
||||
CHECK(edge && strncmp(edge, "aaa", 3) == 0, "刚好装得下时应继续接分支");
|
||||
CHECK(edge && strlen(edge) + strlen(".json") <= ARCHIVE_NAME_MAX,
|
||||
"完整文件名不应超上限: %zu", edge ? strlen(edge) + 5 : 0);
|
||||
@@ -86,35 +94,40 @@ int main(void)
|
||||
memset(too, 'a', maxlineage + 1);
|
||||
too[maxlineage + 1] = '\0';
|
||||
char *fresh = NULL;
|
||||
CHECK(archive_next_name(dir, too, NULL, 0, &fresh, err, sizeof err) == 0, "超限生成失败");
|
||||
CHECK(archive_next_name(dir, too, NULL, 0, &fresh, err, sizeof err) == 0,
|
||||
"超限生成失败");
|
||||
CHECK(fresh && strlen(fresh) == ARCHIVE_ROOT_LEN,
|
||||
"超限时应退回 %d 位根名, 实际 %s (%zu 位)",
|
||||
ARCHIVE_ROOT_LEN, fresh ? fresh : "(null)", fresh ? strlen(fresh) : 0);
|
||||
"超限时应退回 %d 位根名, 实际 %s (%zu 位)", ARCHIVE_ROOT_LEN,
|
||||
fresh ? fresh : "(null)", fresh ? strlen(fresh) : 0);
|
||||
free(fresh);
|
||||
puts("前缀: 临时存档");
|
||||
char *tp = NULL;
|
||||
CHECK(archive_next_name(dir, "a1b2c3d4", "tmp_", 0, &tp, err, sizeof err) == 0,
|
||||
CHECK(archive_next_name(dir, "a1b2c3d4", "tmp_", 0, &tp, err, sizeof err) ==
|
||||
0,
|
||||
"带前缀生成失败");
|
||||
CHECK(tp && strncmp(tp, "tmp_a1b2c3d4_", 13) == 0,
|
||||
"前缀应在最前面、其后才是分支名: %s", tp ? tp : "(null)");
|
||||
free(tp);
|
||||
|
||||
char *tr = NULL;
|
||||
CHECK(archive_next_name(dir, NULL, "tmp_", 0, &tr, err, sizeof err) == 0, "带前缀建根失败");
|
||||
CHECK(archive_next_name(dir, NULL, "tmp_", 0, &tr, err, sizeof err) == 0,
|
||||
"带前缀建根失败");
|
||||
CHECK(tr && strncmp(tr, "tmp_", 4) == 0 &&
|
||||
strlen(tr) == 4 + ARCHIVE_ROOT_LEN,
|
||||
strlen(tr) == 4 + ARCHIVE_ROOT_LEN,
|
||||
"新根也应带前缀: %s", tr ? tr : "(null)");
|
||||
free(tr);
|
||||
|
||||
/* 前缀要算进长度上限: 原本刚好装得下的, 加了 tmp_ 就该退回新根 */
|
||||
char *edgep = NULL;
|
||||
CHECK(archive_next_name(dir, ok, "tmp_", 0, &edgep, err, sizeof err) == 0, "边界失败");
|
||||
CHECK(archive_next_name(dir, ok, "tmp_", 0, &edgep, err, sizeof err) == 0,
|
||||
"边界失败");
|
||||
CHECK(edgep && strlen(edgep) == 4 + ARCHIVE_ROOT_LEN,
|
||||
"加了前缀装不下时应退回根名, 实际 %s (%zu 位)",
|
||||
edgep ? edgep : "(null)", edgep ? strlen(edgep) : 0);
|
||||
free(edgep);
|
||||
|
||||
free(ok); free(too);
|
||||
free(ok);
|
||||
free(too);
|
||||
|
||||
puts("导出与载入往返");
|
||||
JsonValue *msgs = json_new_array();
|
||||
@@ -126,30 +139,38 @@ int main(void)
|
||||
char *path = NULL, *name = NULL;
|
||||
time_t t = 1700000000;
|
||||
CHECK(archive_export(msgs, dir, "parentname", NULL, 0, t, t + 5,
|
||||
"test-model", "http://example/v1",
|
||||
&path, &name, err, sizeof err) == 0,
|
||||
"test-model", "http://example/v1", &path, &name, err,
|
||||
sizeof err) == 0,
|
||||
"导出失败: %s", err);
|
||||
CHECK(name && strncmp(name, "parentname_", 11) == 0,
|
||||
"导出名应挂在来源下: %s", name ? name : "(null)");
|
||||
CHECK(path && access(path, R_OK) == 0, "文件应存在: %s", path ? path : "");
|
||||
|
||||
char *created = NULL, *saved = NULL, *model = NULL, *iname = NULL, *iparent = NULL;
|
||||
JsonValue *back = archive_load(path, &created, &saved, &model, &iname, &iparent,
|
||||
err, sizeof err);
|
||||
char *created = NULL, *saved = NULL, *model = NULL, *iname = NULL,
|
||||
*iparent = NULL;
|
||||
JsonValue *back = archive_load(path, &created, &saved, &model, &iname,
|
||||
&iparent, err, sizeof err);
|
||||
CHECK(back != NULL, "载入失败: %s", err);
|
||||
CHECK(back && json_array_size(back) == 1, "消息条数应为 1");
|
||||
CHECK(iname && name && strcmp(iname, name) == 0, "内容里的 name 应与文件名一致");
|
||||
CHECK(iname && name && strcmp(iname, name) == 0,
|
||||
"内容里的 name 应与文件名一致");
|
||||
CHECK(iparent && strcmp(iparent, "parentname") == 0,
|
||||
"内容里的 parent 应记下来源: %s", iparent ? iparent : "(null)");
|
||||
CHECK(model && strcmp(model, "test-model") == 0, "model 应往返");
|
||||
CHECK(created && strstr(created, "2023-") != NULL,
|
||||
"created 应可读: %s", created ? created : "(null)");
|
||||
/* saved 是"这份存档写出时的时间", 与 created (这段对话的开始时间)是两回事 */
|
||||
CHECK(saved && strstr(saved, "2023-") != NULL,
|
||||
"saved 应可读: %s", saved ? saved : "(null)");
|
||||
CHECK(created && strstr(created, "2023-") != NULL, "created 应可读: %s",
|
||||
created ? created : "(null)");
|
||||
/* saved 是"这份存档写出时的时间", 与 created (这段对话的开始时间)是两回事
|
||||
*/
|
||||
CHECK(saved && strstr(saved, "2023-") != NULL, "saved 应可读: %s",
|
||||
saved ? saved : "(null)");
|
||||
|
||||
json_free(back); json_free(msgs);
|
||||
free(created); free(saved); free(model); free(iname); free(iparent);
|
||||
json_free(back);
|
||||
json_free(msgs);
|
||||
free(created);
|
||||
free(saved);
|
||||
free(model);
|
||||
free(iname);
|
||||
free(iparent);
|
||||
|
||||
puts("列举");
|
||||
ArchiveEntry *list = NULL;
|
||||
@@ -165,13 +186,19 @@ int main(void)
|
||||
/* 非 ligent 存档 (没有 format 标记)应被忽略 */
|
||||
char *jd = xasprintf("%s/junk.json", dir);
|
||||
FILE *j2 = fopen(jd, "w");
|
||||
if (j2) { fputs("{\"hello\":1}", j2); fclose(j2); }
|
||||
if (j2) {
|
||||
fputs("{\"hello\":1}", j2);
|
||||
fclose(j2);
|
||||
}
|
||||
n = archive_list(dir, &list);
|
||||
CHECK(n == 1, "非 ligent 存档应被忽略, 实际 %zu", n);
|
||||
archive_list_free(list, n);
|
||||
unlink(jd); free(jd);
|
||||
unlink(jd);
|
||||
free(jd);
|
||||
|
||||
unlink(path); free(path); free(name);
|
||||
unlink(path);
|
||||
free(path);
|
||||
free(name);
|
||||
rmdir(dir);
|
||||
printf("\n%d 项检查, %d 项失败\n", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
|
||||
+56
-35
@@ -1,7 +1,7 @@
|
||||
/* test_attach.c - /include 的暂存、glob 展开与体积/类型判断
|
||||
*
|
||||
* 交互式挑选 (attach_pick)要真终端才能测, 由 e2e 第 55 段覆盖;
|
||||
* 这里测的是不需要人操作的那一半.
|
||||
* 交互式挑选 (attach_pick)要真终端才能测, 由 e2e 第 55 段覆盖;
|
||||
* 这里测的是不需要人操作的那一半.
|
||||
*/
|
||||
#include "attach.h"
|
||||
#include "image.h"
|
||||
@@ -17,57 +17,69 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void write_file(const char *path, const void *data, size_t n)
|
||||
{
|
||||
static void write_file(const char *path, const void *data, size_t n) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { perror(path); exit(2); }
|
||||
if (n && fwrite(data, 1, n, f) != n) { perror("fwrite"); exit(2); }
|
||||
if (!f) {
|
||||
perror(path);
|
||||
exit(2);
|
||||
}
|
||||
if (n && fwrite(data, 1, n, f) != n) {
|
||||
perror("fwrite");
|
||||
exit(2);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static const unsigned char PNG[] = {
|
||||
0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 'p', 'a', 'y', 'l', 'o', 'a', 'd'
|
||||
};
|
||||
static const unsigned char PNG[] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
'p', 'a', 'y', 'l', 'o', 'a', 'd'};
|
||||
|
||||
/* 数一数暂存里某类块有几个 */
|
||||
static size_t count_type(const JsonValue *parts, const char *type)
|
||||
{
|
||||
if (!parts) return 0;
|
||||
static size_t count_type(const JsonValue *parts, const char *type) {
|
||||
if (!parts)
|
||||
return 0;
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i < json_array_size((JsonValue *)parts); i++) {
|
||||
const char *t = json_get_string(json_array_get((JsonValue *)parts, i), "type");
|
||||
if (t && strcmp(t, type) == 0) n++;
|
||||
const char *t =
|
||||
json_get_string(json_array_get((JsonValue *)parts, i), "type");
|
||||
if (t && strcmp(t, type) == 0)
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 记一下回调被问了几次 —— 它只在遇到图片时该被问到.
|
||||
/* 记一下回调被问了几次 —— 它只在遇到图片时该被问到.
|
||||
* 答案和计数放在一起, 以免"计数版只实现了一种答案"这种测试自己的错. */
|
||||
static int ask_count;
|
||||
static int ask_answer;
|
||||
static int counting_ask(void *ud) { (void)ud; ask_count++; return ask_answer; }
|
||||
static int counting_ask(void *ud) {
|
||||
(void)ud;
|
||||
ask_count++;
|
||||
return ask_answer;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
char tmpl[] = "/tmp/ligent_attach_XXXXXX";
|
||||
char *dir = mkdtemp(tmpl);
|
||||
if (!dir) { perror("mkdtemp"); return 2; }
|
||||
if (!dir) {
|
||||
perror("mkdtemp");
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* 备份目录也指到临时目录, 别污染仓库 */
|
||||
image_set_bindir(dir);
|
||||
|
||||
char *f_c = xasprintf("%s/a.c", dir);
|
||||
char *f_c = xasprintf("%s/a.c", dir);
|
||||
char *f_txt = xasprintf("%s/b.txt", dir);
|
||||
char *f_bin = xasprintf("%s/bin.dat", dir);
|
||||
char *f_png = xasprintf("%s/pic.png", dir);
|
||||
@@ -133,24 +145,28 @@ int main(void)
|
||||
CHECK(added == 1, "png 被收下");
|
||||
CHECK(attach_has_image(), "暂存里有图片");
|
||||
parts = attach_take_parts();
|
||||
CHECK(count_type(parts, "ligent_image") == 1, "应当是一个 ligent_image 引用块");
|
||||
CHECK(count_type(parts, "ligent_image") == 1,
|
||||
"应当是一个 ligent_image 引用块");
|
||||
json_free(parts);
|
||||
|
||||
puts("模型不收图时跳过图片 (回调只在遇到图片时被问)");
|
||||
attach_clear();
|
||||
ask_count = 0; ask_answer = 1;
|
||||
ask_count = 0;
|
||||
ask_answer = 1;
|
||||
added = attach_glob(f_txt, counting_ask, NULL, &skipped, err, sizeof err);
|
||||
CHECK(added == 1, "纯文本照带");
|
||||
CHECK(ask_count == 0, "纯文本不该去问'能不能收图' (问了一次就是白探测)");
|
||||
attach_clear();
|
||||
ask_count = 0; ask_answer = 0;
|
||||
ask_count = 0;
|
||||
ask_answer = 0;
|
||||
added = attach_glob(f_png, counting_ask, NULL, &skipped, err, sizeof err);
|
||||
CHECK(added == 0 && skipped == 1, "回调说不行就跳过");
|
||||
CHECK(ask_count == 1, "遇到图片时问了一次, 实际 %d", ask_count);
|
||||
|
||||
puts("匹配不到 / 参数非法");
|
||||
attach_clear();
|
||||
added = attach_glob("/tmp/绝对不存在的模式*.zzz", NULL, NULL, &skipped, err, sizeof err);
|
||||
added = attach_glob("/tmp/绝对不存在的模式*.zzz", NULL, NULL, &skipped, err,
|
||||
sizeof err);
|
||||
CHECK(added == -1 && err[0], "匹配不到返回 -1 并给出原因: %s", err);
|
||||
added = attach_glob(NULL, NULL, NULL, &skipped, err, sizeof err);
|
||||
CHECK(added == -1, "NULL 模式返回 -1");
|
||||
@@ -166,10 +182,15 @@ int main(void)
|
||||
free(pat);
|
||||
attach_clear();
|
||||
|
||||
free(f_c); free(f_txt); free(f_bin); free(f_png); free(f_big);
|
||||
free(f_c);
|
||||
free(f_txt);
|
||||
free(f_bin);
|
||||
free(f_png);
|
||||
free(f_big);
|
||||
image_set_bindir(".");
|
||||
char *cmd = xasprintf("rm -rf %s", tmpl);
|
||||
if (system(cmd) != 0) { }
|
||||
if (system(cmd) != 0) {
|
||||
}
|
||||
free(cmd);
|
||||
|
||||
printf("\n%d 项检查, %d 项失败\n", checks, failures);
|
||||
|
||||
+17
-18
@@ -1,13 +1,13 @@
|
||||
/* test_capability.c - 能力探测的纯逻辑部分
|
||||
*
|
||||
* 探测序列本身 (基线 -> 合并 -> 拆开)要发 HTTP, 属于端到端范畴,
|
||||
* 探测序列本身 (基线 -> 合并 -> 拆开)要发 HTTP, 属于端到端范畴,
|
||||
* 由 e2e 第 51 段对着 mock 覆盖: 正常模型 / 不收图片 / 不收 tools /
|
||||
* 探测失败 (401)/ --no-probe 五种走法都在那儿验.
|
||||
* 探测失败 (401)/ --no-probe 五种走法都在那儿验.
|
||||
*
|
||||
* 这里只测不需要网络的: 名字映射、关闭探测时的语义、缓存查询.
|
||||
* 其中"关闭探测必须给未测而不是不支持"那条是回归测试 —— 那句话看着像废话,
|
||||
* 这里只测不需要网络的: 名字映射、关闭探测时的语义、缓存查询.
|
||||
* 其中"关闭探测必须给未测而不是不支持"那条是回归测试 —— 那句话看着像废话,
|
||||
* 但 CAP_NO 恰好是 0, 而 Agent 是 memset 出来的, 忘了显式初始化就会一声不响地
|
||||
* 把所有工具摘掉. 这个 bug 真的发生过一次.
|
||||
* 把所有工具摘掉. 这个 bug 真的发生过一次.
|
||||
*/
|
||||
#include "capability.h"
|
||||
#include "llm.h"
|
||||
@@ -18,19 +18,18 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
/* CAP_NO 必须是 0 —— Agent 是 memset 出来的, 整个 capability 模块的
|
||||
* 设计都建立在"零值不能是某个真实结论"之上. 这条断言是那个 bug 的护栏. */
|
||||
CHECK(CAP_NO == 0, "CAP_NO 应为 0 (零值即'不支持'是事故的根源)");
|
||||
@@ -45,7 +44,7 @@ int main(void)
|
||||
CHECK(strcmp(cap_name(42), "未测") == 0, "越界值应落到未测");
|
||||
|
||||
puts("关掉探测: 一律未测, 绝不冒充结论");
|
||||
/* 用可变数组而不是字面量: LlmConfig 的这些字段是 char * (真要释放的),
|
||||
/* 用可变数组而不是字面量: LlmConfig 的这些字段是 char * (真要释放的),
|
||||
* 而 -Wwrite-strings 下 "字面量" 是 const char * —— 直接赋值会被警告. */
|
||||
char model[] = "some-model";
|
||||
char base_url[] = "http://127.0.0.1:1/v1";
|
||||
@@ -58,7 +57,7 @@ int main(void)
|
||||
cap_cache_clear();
|
||||
|
||||
Capabilities c;
|
||||
memset(&c, 0xAB, sizeof c); /* 故意填脏, 验证会被完整覆盖 */
|
||||
memset(&c, 0xAB, sizeof c); /* 故意填脏, 验证会被完整覆盖 */
|
||||
int probed = cap_get(&cfg, 0, &c, 0, NULL, 0);
|
||||
CHECK(probed == 0, "被禁用时不该声称探测过");
|
||||
CHECK(c.probed == 0, "probed 应为 0");
|
||||
|
||||
+120
-95
@@ -1,44 +1,42 @@
|
||||
/* test_config.c - 配置默认值 / 环境变量 / 配置文件覆盖 */
|
||||
#include "config.h"
|
||||
#include "util.h"
|
||||
#include "i18n.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void unset_all(void)
|
||||
{
|
||||
static void unset_all(void) {
|
||||
const char *vars[] = {
|
||||
"LIGENT_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY",
|
||||
"LIGENT_BASE_URL", "LIGENT_MODEL", "LIGENT_SYSTEM",
|
||||
"LIGENT_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY",
|
||||
"LIGENT_BASE_URL", "LIGENT_MODEL", "LIGENT_SYSTEM",
|
||||
"LIGENT_TEMPERATURE", "LIGENT_MAX_TOKENS", "LIGENT_TIMEOUT",
|
||||
"LIGENT_MAX_STEPS", "LIGENT_STREAM", "LIGENT_TOOLS",
|
||||
"OPENAI_BASE_URL", "LIGENT_CONFIG",
|
||||
"LIGENT_MAX_STEPS", "LIGENT_STREAM", "LIGENT_TOOLS",
|
||||
"OPENAI_BASE_URL", "LIGENT_CONFIG",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof vars / sizeof vars[0]; i++)
|
||||
unsetenv(vars[i]);
|
||||
}
|
||||
|
||||
static void test_defaults(void)
|
||||
{
|
||||
static void test_defaults(void) {
|
||||
puts("默认值");
|
||||
unset_all();
|
||||
LigentConfig c;
|
||||
@@ -55,8 +53,7 @@ static void test_defaults(void)
|
||||
config_free(&c);
|
||||
}
|
||||
|
||||
static void test_env_key(void)
|
||||
{
|
||||
static void test_env_key(void) {
|
||||
puts("环境变量取 key");
|
||||
unset_all();
|
||||
LigentConfig c;
|
||||
@@ -98,8 +95,7 @@ static void test_env_key(void)
|
||||
config_free(&c);
|
||||
}
|
||||
|
||||
static void test_env_others(void)
|
||||
{
|
||||
static void test_env_others(void) {
|
||||
puts("环境变量其他项");
|
||||
unset_all();
|
||||
LigentConfig c;
|
||||
@@ -132,24 +128,26 @@ static void test_env_others(void)
|
||||
config_free(&c);
|
||||
}
|
||||
|
||||
static void test_thinking_parse(void)
|
||||
{
|
||||
static void test_thinking_parse(void) {
|
||||
puts("思考模式 / 级别解析");
|
||||
int t = 123;
|
||||
CHECK(config_parse_thinking("auto", &t) == 0 && t == THINKING_AUTO, "auto");
|
||||
CHECK(config_parse_thinking("", &t) == 0 && t == THINKING_AUTO, "空串 = auto");
|
||||
CHECK(config_parse_thinking("", &t) == 0 && t == THINKING_AUTO,
|
||||
"空串 = auto");
|
||||
CHECK(config_parse_thinking("on", &t) == 0 && t == THINKING_ON, "on");
|
||||
CHECK(config_parse_thinking("true", &t) == 0 && t == THINKING_ON, "true");
|
||||
CHECK(config_parse_thinking("enabled", &t) == 0 && t == THINKING_ON, "enabled");
|
||||
CHECK(config_parse_thinking("enabled", &t) == 0 && t == THINKING_ON,
|
||||
"enabled");
|
||||
CHECK(config_parse_thinking("off", &t) == 0 && t == THINKING_OFF, "off");
|
||||
CHECK(config_parse_thinking("false", &t) == 0 && t == THINKING_OFF, "false");
|
||||
CHECK(config_parse_thinking("disabled", &t) == 0 && t == THINKING_OFF, "disabled");
|
||||
CHECK(config_parse_thinking("false", &t) == 0 && t == THINKING_OFF,
|
||||
"false");
|
||||
CHECK(config_parse_thinking("disabled", &t) == 0 && t == THINKING_OFF,
|
||||
"disabled");
|
||||
CHECK(config_parse_thinking("maybe", &t) != 0, "无法识别的值应报错");
|
||||
CHECK(config_parse_thinking(NULL, &t) != 0, "NULL 应报错");
|
||||
|
||||
static const char *const good[] = {
|
||||
"minimal", "low", "medium", "high", "xhigh", "max", "ultra", NULL
|
||||
};
|
||||
static const char *const good[] = {"minimal", "low", "medium", "high",
|
||||
"xhigh", "max", "ultra", NULL};
|
||||
for (size_t i = 0; good[i]; i++)
|
||||
CHECK(config_effort_valid(good[i]), "%s 应是合法级别", good[i]);
|
||||
CHECK(!config_effort_valid("huge"), "huge 不是合法级别");
|
||||
@@ -161,8 +159,7 @@ static void test_thinking_parse(void)
|
||||
CHECK(config_thinking_name(THINKING_OFF) != NULL, "THINKING_OFF 有说明");
|
||||
}
|
||||
|
||||
static void test_file_thinking(void)
|
||||
{
|
||||
static void test_file_thinking(void) {
|
||||
puts("配置文件里的 thinking / reasoning_effort");
|
||||
char err[256];
|
||||
LigentConfig c;
|
||||
@@ -170,7 +167,10 @@ static void test_file_thinking(void)
|
||||
/* 布尔写法 */
|
||||
char t1[] = "/tmp/ligent_think_XXXXXX";
|
||||
int fd = mkstemp(t1);
|
||||
if (fd < 0) { CHECK(0, "mkstemp 失败"); return; }
|
||||
if (fd < 0) {
|
||||
CHECK(0, "mkstemp 失败");
|
||||
return;
|
||||
}
|
||||
const char *j1 = "{\"thinking\":false,\"reasoning_effort\":\"medium\"}";
|
||||
ssize_t w = write(fd, j1, strlen(j1));
|
||||
close(fd);
|
||||
@@ -192,7 +192,8 @@ static void test_file_thinking(void)
|
||||
close(fd);
|
||||
CHECK(w == (ssize_t)strlen(j2), "写临时配置失败");
|
||||
config_defaults(&c);
|
||||
CHECK(config_load_file(&c, t2, err, sizeof err) == 0, "加载失败: %s", err);
|
||||
CHECK(config_load_file(&c, t2, err, sizeof err) == 0, "加载失败: %s",
|
||||
err);
|
||||
CHECK(c.thinking == THINKING_ON, "thinking:\"on\" 应为开启");
|
||||
config_free(&c);
|
||||
unlink(t2);
|
||||
@@ -228,8 +229,7 @@ static void test_file_thinking(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void test_file_override(void)
|
||||
{
|
||||
static void test_file_override(void) {
|
||||
puts("配置文件覆盖 + ${VAR} 展开");
|
||||
unset_all();
|
||||
setenv("MY_SECRET", "sk-from-env", 1);
|
||||
@@ -237,16 +237,16 @@ static void test_file_override(void)
|
||||
char tmpl[] = "/tmp/ligent_cfg_XXXXXX";
|
||||
int fd = mkstemp(tmpl);
|
||||
CHECK(fd >= 0, "mkstemp 失败");
|
||||
if (fd < 0) return;
|
||||
const char *json =
|
||||
"{\"base_url\":\"http://localhost:11434/v1\","
|
||||
"\"api_key\":\"${MY_SECRET}\","
|
||||
"\"model\":\"qwen2.5:14b\","
|
||||
"\"temperature\":0.3,"
|
||||
"\"max_tokens\":4096,"
|
||||
"\"max_steps\":9,"
|
||||
"\"stream\":false,"
|
||||
"\"tools\":false}";
|
||||
if (fd < 0)
|
||||
return;
|
||||
const char *json = "{\"base_url\":\"http://localhost:11434/v1\","
|
||||
"\"api_key\":\"${MY_SECRET}\","
|
||||
"\"model\":\"qwen2.5:14b\","
|
||||
"\"temperature\":0.3,"
|
||||
"\"max_tokens\":4096,"
|
||||
"\"max_steps\":9,"
|
||||
"\"stream\":false,"
|
||||
"\"tools\":false}";
|
||||
ssize_t w = write(fd, json, strlen(json));
|
||||
close(fd);
|
||||
CHECK(w == (ssize_t)strlen(json), "写临时配置失败");
|
||||
@@ -256,7 +256,8 @@ static void test_file_override(void)
|
||||
char err[256];
|
||||
int rc = config_load_file(&c, tmpl, err, sizeof err);
|
||||
CHECK(rc == 0, "加载配置失败: %s", err);
|
||||
CHECK(strcmp(c.base_url, "http://localhost:11434/v1") == 0, "base_url 覆盖");
|
||||
CHECK(strcmp(c.base_url, "http://localhost:11434/v1") == 0,
|
||||
"base_url 覆盖");
|
||||
CHECK(strcmp(c.model, "qwen2.5:14b") == 0, "model 覆盖");
|
||||
CHECK(strcmp(c.api_key, "sk-from-env") == 0,
|
||||
"${MY_SECRET} 应被展开, 实际 [%s]", c.api_key);
|
||||
@@ -265,9 +266,10 @@ static void test_file_override(void)
|
||||
CHECK(c.max_steps == 9, "max_steps 覆盖");
|
||||
CHECK(c.stream == 0, "stream 覆盖");
|
||||
CHECK(c.use_tools == 0, "tools 覆盖");
|
||||
CHECK(c.config_path && strcmp(c.config_path, tmpl) == 0, "记录配置文件路径");
|
||||
CHECK(c.config_path && strcmp(c.config_path, tmpl) == 0,
|
||||
"记录配置文件路径");
|
||||
|
||||
/* 坏配置应报错而不是静默通过.
|
||||
/* 坏配置应报错而不是静默通过.
|
||||
*
|
||||
* 解析器的报错跟着界面语言走 (json_parse_ui), 所以下面这段断语先把语言
|
||||
* 钉住, 再顺手把英文那份也验一遍 —— 位置信息两种语言都得有. */
|
||||
@@ -293,7 +295,7 @@ static void test_file_override(void)
|
||||
i18n_set_lang(LIGENT_LANG_EN);
|
||||
config_defaults(&c3);
|
||||
CHECK(config_load_file(&c3, tmpl2, err, sizeof err) != 0 &&
|
||||
strstr(err, "line") != NULL,
|
||||
strstr(err, "line") != NULL,
|
||||
"英文界面下报错也带位置 (且是英文): %s", err);
|
||||
config_free(&c3);
|
||||
i18n_set_lang(LIGENT_LANG_ZH);
|
||||
@@ -306,8 +308,7 @@ static void test_file_override(void)
|
||||
}
|
||||
|
||||
/* 配置只认二进制所在目录: 不做用户级、系统级, 也不做当前目录查找 */
|
||||
static void test_binary_dir_lookup(void)
|
||||
{
|
||||
static void test_binary_dir_lookup(void) {
|
||||
puts("配置位置 = 二进制所在目录");
|
||||
char err[256];
|
||||
|
||||
@@ -322,11 +323,18 @@ static void test_binary_dir_lookup(void)
|
||||
/* 当前目录放一份 ligent.json, 它绝不能被加载 */
|
||||
char tmpl[] = "/tmp/ligent_cwd_XXXXXX";
|
||||
char *tmp = mkdtemp(tmpl);
|
||||
if (!tmp) { free(dir); free(canon); return; }
|
||||
if (!tmp) {
|
||||
free(dir);
|
||||
free(canon);
|
||||
return;
|
||||
}
|
||||
|
||||
char *fake = xasprintf("%s/ligent.json", tmp);
|
||||
FILE *f = fopen(fake, "w");
|
||||
if (f) { fputs("{\"model\":\"FROM-CWD-ONLY\"}", f); fclose(f); }
|
||||
if (f) {
|
||||
fputs("{\"model\":\"FROM-CWD-ONLY\"}", f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
char *saved = getcwd(NULL, 0);
|
||||
int loaded_from_cwd = 0;
|
||||
@@ -338,7 +346,8 @@ static void test_binary_dir_lookup(void)
|
||||
loaded_from_cwd = (c.model && strcmp(c.model, "FROM-CWD-ONLY") == 0);
|
||||
got_dir = c.config_dir ? xstrdup(c.config_dir) : NULL;
|
||||
config_free(&c);
|
||||
if (saved) chdir(saved);
|
||||
if (saved)
|
||||
chdir(saved);
|
||||
}
|
||||
|
||||
CHECK(!loaded_from_cwd, "不应加载当前工作目录下的 ligent.json");
|
||||
@@ -347,7 +356,7 @@ static void test_binary_dir_lookup(void)
|
||||
|
||||
free(got_dir);
|
||||
free(saved);
|
||||
unlink(fake); /* 先删文件再释放路径字符串 */
|
||||
unlink(fake); /* 先删文件再释放路径字符串 */
|
||||
free(fake);
|
||||
rmdir(tmp);
|
||||
free(dir);
|
||||
@@ -355,25 +364,27 @@ static void test_binary_dir_lookup(void)
|
||||
}
|
||||
|
||||
/* 写一个临时配置文件并加载 */
|
||||
static int load_temp_config(LigentConfig *c, const char *json)
|
||||
{
|
||||
static int load_temp_config(LigentConfig *c, const char *json) {
|
||||
char tmpl[] = "/tmp/ligent_prec_XXXXXX";
|
||||
int fd = mkstemp(tmpl);
|
||||
if (fd < 0) return -1;
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
ssize_t w = write(fd, json, strlen(json));
|
||||
close(fd);
|
||||
if (w != (ssize_t)strlen(json)) { unlink(tmpl); return -1; }
|
||||
if (w != (ssize_t)strlen(json)) {
|
||||
unlink(tmpl);
|
||||
return -1;
|
||||
}
|
||||
char err[256];
|
||||
int rc = config_load_file(c, tmpl, err, sizeof err);
|
||||
unlink(tmpl);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 厂商专属的环境变量不能越权覆盖配置文件里写好的密钥.
|
||||
* 这正是用户踩到的坑: 配置写了 OpenRouter 的 key,
|
||||
/* 厂商专属的环境变量不能越权覆盖配置文件里写好的密钥.
|
||||
* 这正是用户踩到的坑: 配置写了 OpenRouter 的 key,
|
||||
* shell 里残留的 DEEPSEEK_API_KEY 把它顶掉, 于是 A 家的密钥发给了 B 家. */
|
||||
static void test_env_key_precedence(void)
|
||||
{
|
||||
static void test_env_key_precedence(void) {
|
||||
puts("api_key 优先级: 厂商环境变量不得越权");
|
||||
unset_all();
|
||||
setenv("DEEPSEEK_API_KEY", "sk-leftover-in-shell", 1);
|
||||
@@ -381,9 +392,9 @@ static void test_env_key_precedence(void)
|
||||
/* 1) 配置文件写了 key => 它就是最终值, 环境变量顶不掉 */
|
||||
LigentConfig c;
|
||||
config_defaults(&c);
|
||||
CHECK(load_temp_config(&c,
|
||||
"{\"base_url\":\"https://openrouter.ai/api/v1\","
|
||||
"\"api_key\":\"sk-or-v1-from-file\"}") == 0, "加载配置失败");
|
||||
CHECK(load_temp_config(&c, "{\"base_url\":\"https://openrouter.ai/api/v1\","
|
||||
"\"api_key\":\"sk-or-v1-from-file\"}") == 0,
|
||||
"加载配置失败");
|
||||
config_apply_env(&c);
|
||||
CHECK(strcmp(c.api_key, "sk-or-v1-from-file") == 0,
|
||||
"配置文件里的 key 不应被残留环境变量顶掉, 实际 %s", c.api_key);
|
||||
@@ -391,7 +402,8 @@ static void test_env_key_precedence(void)
|
||||
|
||||
/* 2) 配置文件没写 key + 端点就是 DeepSeek => 用 DEEPSEEK_API_KEY 兜底 */
|
||||
config_defaults(&c);
|
||||
CHECK(load_temp_config(&c, "{\"base_url\":\"https://api.deepseek.com/v1\"}") == 0,
|
||||
CHECK(load_temp_config(
|
||||
&c, "{\"base_url\":\"https://api.deepseek.com/v1\"}") == 0,
|
||||
"加载配置失败");
|
||||
config_apply_env(&c);
|
||||
CHECK(strcmp(c.api_key, "sk-leftover-in-shell") == 0,
|
||||
@@ -400,7 +412,8 @@ static void test_env_key_precedence(void)
|
||||
|
||||
/* 3) 配置文件没写 key + 端点是别家 => 不猜, 宁可空着让用户看到明确提示 */
|
||||
config_defaults(&c);
|
||||
CHECK(load_temp_config(&c, "{\"base_url\":\"https://openrouter.ai/api/v1\"}") == 0,
|
||||
CHECK(load_temp_config(
|
||||
&c, "{\"base_url\":\"https://openrouter.ai/api/v1\"}") == 0,
|
||||
"加载配置失败");
|
||||
config_apply_env(&c);
|
||||
CHECK(c.api_key[0] == '\0',
|
||||
@@ -410,9 +423,9 @@ static void test_env_key_precedence(void)
|
||||
/* 4) LIGENT_API_KEY 是显式覆盖, 任何时候都最高 */
|
||||
setenv("LIGENT_API_KEY", "sk-explicit", 1);
|
||||
config_defaults(&c);
|
||||
CHECK(load_temp_config(&c,
|
||||
"{\"base_url\":\"https://openrouter.ai/api/v1\","
|
||||
"\"api_key\":\"sk-or-v1-from-file\"}") == 0, "加载配置失败");
|
||||
CHECK(load_temp_config(&c, "{\"base_url\":\"https://openrouter.ai/api/v1\","
|
||||
"\"api_key\":\"sk-or-v1-from-file\"}") == 0,
|
||||
"加载配置失败");
|
||||
config_apply_env(&c);
|
||||
CHECK(strcmp(c.api_key, "sk-explicit") == 0,
|
||||
"LIGENT_API_KEY 应最高优先, 实际 %s", c.api_key);
|
||||
@@ -421,41 +434,48 @@ static void test_env_key_precedence(void)
|
||||
unset_all();
|
||||
}
|
||||
|
||||
/* 软链接: 配置目录要跟着"用户实际调用那个文件"走.
|
||||
* 否则把 ligent 软链到 ~/bin 之后, 配置会被悄悄挪到源文件所在的目录,
|
||||
/* 软链接: 配置目录要跟着"用户实际调用那个文件"走.
|
||||
* 否则把 ligent 软链到 ~/bin 之后, 配置会被悄悄挪到源文件所在的目录,
|
||||
* 用户放在 ~/bin 旁边的 ligent.json 就永远读不到. */
|
||||
static void test_symlink_dir(void)
|
||||
{
|
||||
static void test_symlink_dir(void) {
|
||||
puts("软链接: 配置目录跟着软链接走");
|
||||
|
||||
char self[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", self, sizeof self - 1);
|
||||
if (n <= 0) { puts(" (没有 /proc/self/exe, 跳过)"); return; }
|
||||
if (n <= 0) {
|
||||
puts(" (没有 /proc/self/exe, 跳过)");
|
||||
return;
|
||||
}
|
||||
self[n] = '\0';
|
||||
|
||||
char tmpl[] = "/tmp/ligent_link_XXXXXX";
|
||||
char *dir = mkdtemp(tmpl);
|
||||
if (!dir) { puts(" (mkdtemp 失败, 跳过)"); return; }
|
||||
if (!dir) {
|
||||
puts(" (mkdtemp 失败, 跳过)");
|
||||
return;
|
||||
}
|
||||
|
||||
char *link = xasprintf("%s/ligent", dir);
|
||||
if (symlink(self, link) != 0) {
|
||||
puts(" (symlink 失败, 跳过)");
|
||||
free(link); rmdir(dir);
|
||||
free(link);
|
||||
rmdir(dir);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 核心: 指向软链接时取软链接所在目录, 而不是源文件所在目录 */
|
||||
char *got = config_binary_dir(link);
|
||||
CHECK(got && strcmp(got, dir) == 0,
|
||||
"应取软链接所在目录 %s, 实际 %s", dir, got ? got : "(null)");
|
||||
CHECK(got && strcmp(got, dir) == 0, "应取软链接所在目录 %s, 实际 %s", dir,
|
||||
got ? got : "(null)");
|
||||
free(got);
|
||||
|
||||
/* 路径里的 . 应被规范化, 但最后一段仍不解析 */
|
||||
char *dotted = xasprintf("%s/./ligent", dir);
|
||||
got = config_binary_dir(dotted);
|
||||
CHECK(got && strcmp(got, dir) == 0,
|
||||
"带 . 的路径应规范化, 实际 %s", got ? got : "(null)");
|
||||
free(got); free(dotted);
|
||||
CHECK(got && strcmp(got, dir) == 0, "带 . 的路径应规范化, 实际 %s",
|
||||
got ? got : "(null)");
|
||||
free(got);
|
||||
free(dotted);
|
||||
|
||||
/* argv[0] 只是个名字时经 $PATH 找到的仍应是软链接本身 */
|
||||
char *saved = getenv("PATH") ? xstrdup(getenv("PATH")) : NULL;
|
||||
@@ -465,25 +485,30 @@ static void test_symlink_dir(void)
|
||||
CHECK(got && strcmp(got, dir) == 0,
|
||||
"$PATH 找到软链接时应取它所在目录, 实际 %s", got ? got : "(null)");
|
||||
free(got);
|
||||
if (saved) setenv("PATH", saved, 1); else unsetenv("PATH");
|
||||
free(saved); free(newpath);
|
||||
if (saved)
|
||||
setenv("PATH", saved, 1);
|
||||
else
|
||||
unsetenv("PATH");
|
||||
free(saved);
|
||||
free(newpath);
|
||||
|
||||
/* 直接用真实文件时, 取的还是真实目录 */
|
||||
char *selfdir = xstrdup(self);
|
||||
char *slash = strrchr(selfdir, '/');
|
||||
if (slash) *slash = '\0';
|
||||
if (slash)
|
||||
*slash = '\0';
|
||||
got = config_binary_dir(self);
|
||||
CHECK(got && strcmp(got, selfdir) == 0,
|
||||
"真实文件应取真实目录 %s, 实际 %s", selfdir, got ? got : "(null)");
|
||||
free(got); free(selfdir);
|
||||
CHECK(got && strcmp(got, selfdir) == 0, "真实文件应取真实目录 %s, 实际 %s",
|
||||
selfdir, got ? got : "(null)");
|
||||
free(got);
|
||||
free(selfdir);
|
||||
|
||||
unlink(link);
|
||||
free(link);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
test_defaults();
|
||||
test_env_key();
|
||||
test_env_others();
|
||||
|
||||
+30
-28
@@ -9,33 +9,32 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
puts("重试判定: 只重试服务端明确报错的情况");
|
||||
|
||||
/* 应当重试 */
|
||||
long retryable[] = { 408, 409, 429, 500, 502, 503, 504 };
|
||||
long retryable[] = {408, 409, 429, 500, 502, 503, 504};
|
||||
for (size_t i = 0; i < sizeof retryable / sizeof retryable[0]; i++)
|
||||
CHECK(http_status_retryable(retryable[i]) == 1,
|
||||
"HTTP %ld 应当可重试", retryable[i]);
|
||||
CHECK(http_status_retryable(retryable[i]) == 1, "HTTP %ld 应当可重试",
|
||||
retryable[i]);
|
||||
|
||||
/* 绝不应当重试: 这些是确定性错误, 重试只是浪费时间和钱 */
|
||||
long permanent[] = { 400, 401, 403, 404, 405, 413, 415, 422, 501,
|
||||
200, 201, 204, 301, 0, -1 };
|
||||
long permanent[] = {400, 401, 403, 404, 405, 413, 415, 422,
|
||||
501, 200, 201, 204, 301, 0, -1};
|
||||
for (size_t i = 0; i < sizeof permanent / sizeof permanent[0]; i++)
|
||||
CHECK(http_status_retryable(permanent[i]) == 0,
|
||||
"HTTP %ld 不应重试", permanent[i]);
|
||||
CHECK(http_status_retryable(permanent[i]) == 0, "HTTP %ld 不应重试",
|
||||
permanent[i]);
|
||||
|
||||
/* 默认策略与上限的合理性 */
|
||||
HttpRetry def = HTTP_RETRY_DEFAULT;
|
||||
@@ -53,19 +52,22 @@ int main(void)
|
||||
size_t sz = 0;
|
||||
FILE *f = open_memstream(&buf, &sz);
|
||||
|
||||
/* 这条正是曾经漏出去的: libcurl 的 HTTP/2 调试行走的是 CURLINFO_TEXT,
|
||||
/* 这条正是曾经漏出去的: libcurl 的 HTTP/2 调试行走的是 CURLINFO_TEXT,
|
||||
* 不是 HEADER_OUT, 所以只处理 HEADER_OUT 的做法根本挡不住 */
|
||||
const char *h2 = "== Info: [HTTP/2] [1] [authorization: Bearer sk-0f114e3dac8249f0a36fd8fd5c517360]\n";
|
||||
const char *h2 = "== Info: [HTTP/2] [1] [authorization: Bearer "
|
||||
"sk-0f114e3dac8249f0a36fd8fd5c517360]\n";
|
||||
http_write_redacted(f, h2, strlen(h2), key);
|
||||
|
||||
/* 整块请求头 (HEADER_OUT 是整块传来的, 不是逐行) */
|
||||
const char *hdrs = "POST /v1/chat/completions HTTP/2\r\n"
|
||||
"Authorization: Bearer sk-0f114e3dac8249f0a36fd8fd5c517360\r\n"
|
||||
"Content-Type: application/json\r\n";
|
||||
const char *hdrs =
|
||||
"POST /v1/chat/completions HTTP/2\r\n"
|
||||
"Authorization: Bearer sk-0f114e3dac8249f0a36fd8fd5c517360\r\n"
|
||||
"Content-Type: application/json\r\n";
|
||||
http_write_redacted(f, hdrs, strlen(hdrs), key);
|
||||
|
||||
/* 其它内容不能被改动 */
|
||||
const char *plain = "== Info: TLSv1.3 (IN), TLS handshake, Finished (20):\n";
|
||||
const char *plain =
|
||||
"== Info: TLSv1.3 (IN), TLS handshake, Finished (20):\n";
|
||||
http_write_redacted(f, plain, strlen(plain), key);
|
||||
|
||||
/* 没有密钥时原样输出 */
|
||||
@@ -87,9 +89,9 @@ int main(void)
|
||||
puts("\n空请求体: POST 分支也要扛得住 body == NULL");
|
||||
{
|
||||
/* body 允许为 NULL (http_probe 就是这么调的), 但 POST 分支以前写的是
|
||||
* 无条件的 strlen(body) —— 传 NULL 会在 curl_easy_setopt 期间就段错误.
|
||||
* 无条件的 strlen(body) —— 传 NULL 会在 curl_easy_setopt 期间就段错误.
|
||||
*
|
||||
* 这里故意用一个格式非法的 URL: libcurl 会在真正联网之前就拒绝它,
|
||||
* 这里故意用一个格式非法的 URL: libcurl 会在真正联网之前就拒绝它,
|
||||
* 所以本测试仍然不碰网络. 而 strlen(NULL) 发生在设置 POSTFIELDS 期间、
|
||||
* curl_easy_perform 之前, 也就是说旧代码在这里必崩 —— 这个断言对旧
|
||||
* 代码是有效的回归守卫, 不是走过场. */
|
||||
@@ -99,8 +101,8 @@ int main(void)
|
||||
long status = 0;
|
||||
|
||||
http_global_init();
|
||||
int rc = http_post(":::", "sk-test", NULL, 5, 0, &no_retry,
|
||||
NULL, &status, &attempts, errbuf, sizeof errbuf);
|
||||
int rc = http_post(":::", "sk-test", NULL, 5, 0, &no_retry, NULL,
|
||||
&status, &attempts, errbuf, sizeof errbuf);
|
||||
http_global_cleanup();
|
||||
|
||||
CHECK(rc == -1, "非法 URL 应当返回 -1 (实际返回 %d)", rc);
|
||||
|
||||
+71
-54
@@ -1,10 +1,10 @@
|
||||
/* test_i18n.c - 界面文案表
|
||||
*
|
||||
* 这张表是"每种语言一条". 它最容易出的错不是逻辑, 而是漏: 加了键忘了翻,
|
||||
* 或者译文里少了个占位符 ({path} 写成 {paths})—— 用户看到的就是一句缺字的话.
|
||||
* 所以这里的检查全部是机械的: 遍历表, 逐条比对.
|
||||
* 这张表是"每种语言一条". 它最容易出的错不是逻辑, 而是漏: 加了键忘了翻,
|
||||
* 或者译文里少了个占位符 ({path} 写成 {paths})—— 用户看到的就是一句缺字的话.
|
||||
* 所以这里的检查全部是机械的: 遍历表, 逐条比对.
|
||||
*
|
||||
* 文案本身好不好不在这里管; "英文界面是不是每句都有着落"才是.
|
||||
* 文案本身好不好不在这里管; "英文界面是不是每句都有着落"才是.
|
||||
*/
|
||||
#include "i18n.h"
|
||||
|
||||
@@ -15,27 +15,29 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* 收一份占位符名单 ({name} 里的 name), 按出现顺序放进 out */
|
||||
static int placeholders(const char *s, char (*out)[32], int max)
|
||||
{
|
||||
static int placeholders(const char *s, char (*out)[32], int max) {
|
||||
int n = 0;
|
||||
for (const char *p = s; *p; p++) {
|
||||
if (*p != '{') continue;
|
||||
if (*p != '{')
|
||||
continue;
|
||||
const char *e = strchr(p + 1, '}');
|
||||
if (!e) break;
|
||||
if (!e)
|
||||
break;
|
||||
size_t len = (size_t)(e - p - 1);
|
||||
if (len == 0 || len >= 32 || n >= max) break;
|
||||
if (len == 0 || len >= 32 || n >= max)
|
||||
break;
|
||||
memcpy(out[n], p + 1, len);
|
||||
out[n][len] = '\0';
|
||||
n++;
|
||||
@@ -44,20 +46,27 @@ static int placeholders(const char *s, char (*out)[32], int max)
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 收一份 printf 转换符名单 (%zu、%c、%.20s …), 原样带走标志/宽度/长度.
|
||||
/* 收一份 printf 转换符名单 (%zu、%c、%.20s …), 原样带走标志/宽度/长度.
|
||||
* json.* 那几条是 printf 模板而不是 {name}, 两种语言只要转换符顺序或类型
|
||||
* 对不上, snprintf 就会读错参数 —— 比少字更糟, 得由机器盯住. */
|
||||
static int printf_specs(const char *s, char (*out)[16], int max)
|
||||
{
|
||||
static int printf_specs(const char *s, char (*out)[16], int max) {
|
||||
int n = 0;
|
||||
for (const char *p = s; p && *p; p++) {
|
||||
if (*p != '%' || n >= max) continue;
|
||||
if (*p != '%' || n >= max)
|
||||
continue;
|
||||
const char *q = p + 1;
|
||||
while (*q && strchr("-+ #0", *q)) q++; /* 标志 */
|
||||
while (*q >= '0' && *q <= '9') q++; /* 宽度 */
|
||||
if (*q == '.') { q++; while (*q >= '0' && *q <= '9') q++; }
|
||||
while (*q == 'h' || *q == 'l' || *q == 'L' ||
|
||||
*q == 'z' || *q == 'j' || *q == 't') q++; /* 长度修饰 */
|
||||
while (*q && strchr("-+ #0", *q))
|
||||
q++; /* 标志 */
|
||||
while (*q >= '0' && *q <= '9')
|
||||
q++; /* 宽度 */
|
||||
if (*q == '.') {
|
||||
q++;
|
||||
while (*q >= '0' && *q <= '9')
|
||||
q++;
|
||||
}
|
||||
while (*q == 'h' || *q == 'l' || *q == 'L' || *q == 'z' || *q == 'j' ||
|
||||
*q == 't')
|
||||
q++; /* 长度修饰 */
|
||||
if (strchr("diouxXcspfgeE", *q)) {
|
||||
size_t len = (size_t)(q + 1 - p);
|
||||
if (len < 16) {
|
||||
@@ -66,7 +75,8 @@ static int printf_specs(const char *s, char (*out)[16], int max)
|
||||
n++;
|
||||
}
|
||||
}
|
||||
if (*q) p = q;
|
||||
if (*q)
|
||||
p = q;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
@@ -74,24 +84,22 @@ static int printf_specs(const char *s, char (*out)[16], int max)
|
||||
/* 有没有中日韩字符 (含全角标点: 〔〕 ()). 英文那一列里出现它, 基本就是
|
||||
* "照抄了中文"或者"美化用的框线忘了换" —— 用户看到的是英文句子中间卡着一对
|
||||
* 中文括号. 顺带把"英文栏里混进日文假名"这种复制粘贴也一并挡住. */
|
||||
static int has_cjk(const char *s)
|
||||
{
|
||||
static int has_cjk(const char *s) {
|
||||
for (const unsigned char *p = (const unsigned char *)s; p && *p; p++) {
|
||||
if (p[0] >= 0xE3 && p[0] <= 0xEF && p[1] >= 0x80 && p[2] >= 0x80) {
|
||||
unsigned cp = (unsigned)((p[0] & 0x0Fu) << 12)
|
||||
| (unsigned)((p[1] & 0x3Fu) << 6)
|
||||
| (unsigned)(p[2] & 0x3Fu);
|
||||
if ((cp >= 0x3000 && cp <= 0x30FF) || /* 标点 / 假名 */
|
||||
(cp >= 0x4E00 && cp <= 0x9FFF) || /* 汉字 */
|
||||
(cp >= 0xFF00 && cp <= 0xFFEF)) /* 全角 */
|
||||
unsigned cp = (unsigned)((p[0] & 0x0Fu) << 12) |
|
||||
(unsigned)((p[1] & 0x3Fu) << 6) |
|
||||
(unsigned)(p[2] & 0x3Fu);
|
||||
if ((cp >= 0x3000 && cp <= 0x30FF) || /* 标点 / 假名 */
|
||||
(cp >= 0x4E00 && cp <= 0x9FFF) || /* 汉字 */
|
||||
(cp >= 0xFF00 && cp <= 0xFFEF)) /* 全角 */
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
size_t n = 0;
|
||||
const LigentMsg *tab = i18n_table(&n);
|
||||
CHECK(tab != NULL && n > 0, "文案表不能是空的");
|
||||
@@ -100,7 +108,8 @@ int main(void)
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const LigentMsg *m = &tab[i];
|
||||
CHECK(m->key && *m->key, "第 %zu 条没有 key", i);
|
||||
if (!m->key || !*m->key) continue;
|
||||
if (!m->key || !*m->key)
|
||||
continue;
|
||||
CHECK(m->en && *m->en, "%s 缺英文", m->key);
|
||||
CHECK(m->zh && *m->zh, "%s 缺中文", m->key);
|
||||
CHECK(!has_cjk(m->en ? m->en : ""), "%s 的英文里混进了中日韩字符",
|
||||
@@ -110,32 +119,35 @@ int main(void)
|
||||
* CHECK 的话, check-docs 数出来的"单元项数"就没意义了. */
|
||||
int dup = 0;
|
||||
for (size_t j = i + 1; j < n; j++)
|
||||
if (strcmp(tab[j].key, m->key) == 0) dup = 1;
|
||||
if (strcmp(tab[j].key, m->key) == 0)
|
||||
dup = 1;
|
||||
CHECK(!dup, "key 重复: %s", m->key);
|
||||
|
||||
char pe[16][32], pz[16][32];
|
||||
int ne = placeholders(m->en ? m->en : "", pe, 16);
|
||||
int nz = placeholders(m->zh ? m->zh : "", pz, 16);
|
||||
CHECK(ne == nz, "%s 两种语言的占位符数量不同 (en %d / zh %d)",
|
||||
m->key, ne, nz);
|
||||
CHECK(ne == nz, "%s 两种语言的占位符数量不同 (en %d / zh %d)", m->key,
|
||||
ne, nz);
|
||||
for (int a = 0; a < ne && a < nz; a++) {
|
||||
int found = 0;
|
||||
for (int b = 0; b < nz; b++)
|
||||
if (strcmp(pe[a], pz[b]) == 0) found = 1;
|
||||
if (strcmp(pe[a], pz[b]) == 0)
|
||||
found = 1;
|
||||
CHECK(found, "%s: 英文的 {%s} 在中文里没有对应", m->key, pe[a]);
|
||||
}
|
||||
/* 译文里多出一个原文没有的占位符, 同样会在运行时漏字 */
|
||||
for (int a = 0; a < nz && a < ne; a++) {
|
||||
int found = 0;
|
||||
for (int b = 0; b < ne; b++)
|
||||
if (strcmp(pz[a], pe[b]) == 0) found = 1;
|
||||
if (strcmp(pz[a], pe[b]) == 0)
|
||||
found = 1;
|
||||
CHECK(found, "%s: 中文的 {%s} 在英文里没有对应", m->key, pz[a]);
|
||||
}
|
||||
}
|
||||
|
||||
/* 表里有一部分键是**当 printf 模板用的** (repl.usage.*、repl.model.row、
|
||||
* json.* …): 调用方直接把 t(key) 交给 printf/snprintf. 这些条目的
|
||||
* {name} 检查管不到, 得单独比转换符 —— 少一个 %s 会读到别的参数,
|
||||
* {name} 检查管不到, 得单独比转换符 —— 少一个 %s 会读到别的参数,
|
||||
* 类型写错 (%d 对 %zu)更是直接踩内存. */
|
||||
puts("printf 模板: 两种语言的转换符必须一模一样");
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
@@ -143,12 +155,13 @@ int main(void)
|
||||
char se[16][16], sz[16][16];
|
||||
int ce = printf_specs(m->en ? m->en : "", se, 16);
|
||||
int cz = printf_specs(m->zh ? m->zh : "", sz, 16);
|
||||
if (ce == 0 && cz == 0) continue; /* 不是模板, 跳过 */
|
||||
if (ce == 0 && cz == 0)
|
||||
continue; /* 不是模板, 跳过 */
|
||||
CHECK(ce == cz, "%s: 转换符个数不同 (en %d / zh %d)", m->key, ce, cz);
|
||||
for (int a = 0; a < ce && a < cz; a++)
|
||||
CHECK(strcmp(se[a], sz[a]) == 0,
|
||||
"%s: 第 %d 个转换符不同 (en %s / zh %s)",
|
||||
m->key, a + 1, se[a], sz[a]);
|
||||
"%s: 第 %d 个转换符不同 (en %s / zh %s)", m->key, a + 1,
|
||||
se[a], sz[a]);
|
||||
}
|
||||
|
||||
puts("查表、切换语言、漏键可见");
|
||||
@@ -169,15 +182,17 @@ int main(void)
|
||||
const char *r = tf("cli.err.unknown_option", "option", "--nope", NULL);
|
||||
CHECK(strstr(r, "--nope") != NULL, "值要填进去");
|
||||
CHECK(strstr(r, "{option}") == NULL, "填过的占位符不该留下");
|
||||
CHECK(strstr(tf("cli.err.unknown_option", "wrong", "x", NULL), "{option}") != NULL,
|
||||
CHECK(strstr(tf("cli.err.unknown_option", "wrong", "x", NULL),
|
||||
"{option}") != NULL,
|
||||
"名字对不上时原样保留, 别静默吞掉");
|
||||
i18n_set_lang(LIGENT_LANG_ZH);
|
||||
CHECK(strstr(tf("cli.err.unknown_option", "option", "选项X", NULL), "选项X") != NULL,
|
||||
CHECK(strstr(tf("cli.err.unknown_option", "option", "选项X", NULL),
|
||||
"选项X") != NULL,
|
||||
"中文模板同样能填");
|
||||
/* 同一句里多个占位符, 且顺序与调用顺序相反 */
|
||||
i18n_set_lang(LIGENT_LANG_EN);
|
||||
const char *two = tf("doctor.val.match_bad", "guess", "OpenAI",
|
||||
"owner", "DeepSeek", NULL);
|
||||
const char *two = tf("doctor.val.match_bad", "guess", "OpenAI", "owner",
|
||||
"DeepSeek", NULL);
|
||||
CHECK(strstr(two, "DeepSeek") && strstr(two, "OpenAI"), "两个值都要在");
|
||||
CHECK(strstr(two, "{") == NULL, "不该有没填上的占位符: %s", two);
|
||||
|
||||
@@ -192,7 +207,8 @@ int main(void)
|
||||
CHECK(i18n_parse_lang("") < 0, "空串返回 -1");
|
||||
CHECK(i18n_parse_lang(NULL) < 0, "NULL 返回 -1");
|
||||
|
||||
puts("环境探测: LIGENT_LANG > LC_ALL > LC_MESSAGES > LANG, 认不出/没设 -> 英文");
|
||||
puts("环境探测: LIGENT_LANG > LC_ALL > LC_MESSAGES > LANG, 认不出/没设 -> "
|
||||
"英文");
|
||||
unsetenv("LIGENT_LANG");
|
||||
unsetenv("LC_ALL");
|
||||
unsetenv("LC_MESSAGES");
|
||||
@@ -207,7 +223,8 @@ int main(void)
|
||||
setenv("LIGENT_LANG", "en", 1);
|
||||
CHECK(i18n_lang_from_env() == LIGENT_LANG_EN, "LIGENT_LANG 优先级最高");
|
||||
setenv("LIGENT_LANG", "fr", 1);
|
||||
CHECK(i18n_lang_from_env() == LIGENT_LANG_EN, "LIGENT_LANG 认不出时回退英文");
|
||||
CHECK(i18n_lang_from_env() == LIGENT_LANG_EN,
|
||||
"LIGENT_LANG 认不出时回退英文");
|
||||
unsetenv("LIGENT_LANG");
|
||||
unsetenv("LC_ALL");
|
||||
unsetenv("LC_MESSAGES");
|
||||
|
||||
+76
-60
@@ -5,69 +5,77 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static const char PNG[] = "\x89PNG\r\n\x1a\n rest";
|
||||
static const char PNG[] = "\x89PNG\r\n\x1a\n rest";
|
||||
static const char JPEG[] = "\xff\xd8\xff\xe0 rest";
|
||||
static const char GIF87[] = "GIF87a rest";
|
||||
static const char GIF89[] = "GIF89a rest";
|
||||
static const char WEBP[] = "RIFF\x00\x00\x00\x00WEBPVP8 ";
|
||||
|
||||
static void test_sniff(void)
|
||||
{
|
||||
static void test_sniff(void) {
|
||||
puts("按文件内容认格式 (不看扩展名)");
|
||||
const char *ext = NULL, *mime = NULL;
|
||||
|
||||
CHECK(image_sniff((const unsigned char *)PNG, 14, &ext, &mime) == 0 &&
|
||||
strcmp(ext, "png") == 0 && strcmp(mime, "image/png") == 0, "PNG");
|
||||
strcmp(ext, "png") == 0 && strcmp(mime, "image/png") == 0,
|
||||
"PNG");
|
||||
CHECK(image_sniff((const unsigned char *)JPEG, 7, &ext, &mime) == 0 &&
|
||||
strcmp(ext, "jpg") == 0 && strcmp(mime, "image/jpeg") == 0, "JPEG");
|
||||
strcmp(ext, "jpg") == 0 && strcmp(mime, "image/jpeg") == 0,
|
||||
"JPEG");
|
||||
CHECK(image_sniff((const unsigned char *)GIF87, 10, &ext, &mime) == 0 &&
|
||||
strcmp(ext, "gif") == 0, "GIF87a");
|
||||
strcmp(ext, "gif") == 0,
|
||||
"GIF87a");
|
||||
CHECK(image_sniff((const unsigned char *)GIF89, 10, &ext, &mime) == 0 &&
|
||||
strcmp(ext, "gif") == 0, "GIF89a");
|
||||
strcmp(ext, "gif") == 0,
|
||||
"GIF89a");
|
||||
CHECK(image_sniff((const unsigned char *)WEBP, 16, &ext, &mime) == 0 &&
|
||||
strcmp(ext, "webp") == 0 && strcmp(mime, "image/webp") == 0, "WebP");
|
||||
strcmp(ext, "webp") == 0 && strcmp(mime, "image/webp") == 0,
|
||||
"WebP");
|
||||
|
||||
/* 认不出来要明确失败, 别猜一个格式报上去 */
|
||||
CHECK(image_sniff((const unsigned char *)"not an image", 12, &ext, &mime) != 0,
|
||||
CHECK(image_sniff((const unsigned char *)"not an image", 12, &ext, &mime) !=
|
||||
0,
|
||||
"杂文本不该被认成图片");
|
||||
CHECK(image_sniff((const unsigned char *)"\x89PNG", 4, &ext, &mime) != 0,
|
||||
"截断的 PNG 头不该被认出来");
|
||||
CHECK(image_sniff((const unsigned char *)"RIFFxxxxNOPE", 12, &ext, &mime) != 0,
|
||||
CHECK(image_sniff((const unsigned char *)"RIFFxxxxNOPE", 12, &ext, &mime) !=
|
||||
0,
|
||||
"RIFF 但不是 WEBP 不该被认成 WebP");
|
||||
CHECK(image_sniff(NULL, 0, &ext, &mime) != 0, "NULL 不该崩");
|
||||
}
|
||||
|
||||
static void test_backup(void)
|
||||
{
|
||||
static void test_backup(void) {
|
||||
puts("本地备份: 命名、内容、不重复");
|
||||
char tmpl[] = "/tmp/ligent_img_XXXXXX";
|
||||
char *dir = mkdtemp(tmpl);
|
||||
CHECK(dir != NULL, "建临时目录失败");
|
||||
if (!dir) return;
|
||||
if (!dir)
|
||||
return;
|
||||
|
||||
const unsigned char data[] = "\x89PNG\r\n\x1a\n payload";
|
||||
size_t n = sizeof data - 1;
|
||||
|
||||
char *b1 = image_backup_save(dir, data, n, "png");
|
||||
CHECK(b1 != NULL, "备份应成功");
|
||||
if (!b1) { return; }
|
||||
if (!b1) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 名字形如 <上传时间戳>_<4位随机>.<ext> */
|
||||
CHECK(strlen(b1) > 5, "名字不该太短: %s", b1);
|
||||
@@ -79,34 +87,39 @@ static void test_backup(void)
|
||||
char *p1 = image_backup_path(dir, b1);
|
||||
size_t len = 0;
|
||||
char *back = read_entire_file(p1, &len);
|
||||
CHECK(back && len == n && memcmp(back, data, n) == 0, "备份内容应逐字节一致");
|
||||
free(back); free(p1);
|
||||
CHECK(back && len == n && memcmp(back, data, n) == 0,
|
||||
"备份内容应逐字节一致");
|
||||
free(back);
|
||||
free(p1);
|
||||
|
||||
/* 再存一次应该拿到不同的名字 (随机后缀), 而不是互相覆盖 */
|
||||
char *b2 = image_backup_save(dir, data, n, "png");
|
||||
CHECK(b2 && strcmp(b1, b2) != 0, "两次备份不该同号: %s / %s",
|
||||
b1, b2 ? b2 : "(null)");
|
||||
CHECK(b2 && strcmp(b1, b2) != 0, "两次备份不该同号: %s / %s", b1,
|
||||
b2 ? b2 : "(null)");
|
||||
free(b2);
|
||||
|
||||
free(b1);
|
||||
/* 清场 */
|
||||
char *cmd = xasprintf("rm -rf %s", dir);
|
||||
if (system(cmd) != 0) { /* 清理失败不影响断言 */ }
|
||||
if (system(cmd) != 0) { /* 清理失败不影响断言 */
|
||||
}
|
||||
free(cmd);
|
||||
}
|
||||
|
||||
static void test_base64(void)
|
||||
{
|
||||
static void test_base64(void) {
|
||||
puts("base64: 长度、补位、往返");
|
||||
/* 用几组长度覆盖三种补位情况: 3n、3n+1、3n+2 */
|
||||
struct { const char *in; const char *want; } cases[] = {
|
||||
{ "", "" },
|
||||
{ "f", "Zg==" },
|
||||
{ "fo", "Zm8=" },
|
||||
{ "foo", "Zm9v" },
|
||||
{ "foob", "Zm9vYg==" },
|
||||
{ "fooba", "Zm9vYmE=" },
|
||||
{ "foobar", "Zm9vYmFy" },
|
||||
struct {
|
||||
const char *in;
|
||||
const char *want;
|
||||
} cases[] = {
|
||||
{"", ""},
|
||||
{"f", "Zg=="},
|
||||
{"fo", "Zm8="},
|
||||
{"foo", "Zm9v"},
|
||||
{"foob", "Zm9vYg=="},
|
||||
{"fooba", "Zm9vYmE="},
|
||||
{"foobar", "Zm9vYmFy"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
|
||||
size_t n = strlen(cases[i].in);
|
||||
@@ -119,7 +132,7 @@ static void test_base64(void)
|
||||
free(got);
|
||||
}
|
||||
/* 二进制字节 (0x00 / 0xFF 这类不能被当字符串处理) */
|
||||
const unsigned char bin[] = { 0x00, 0xFF, 0x10, 0x80 };
|
||||
const unsigned char bin[] = {0x00, 0xFF, 0x10, 0x80};
|
||||
char *b = base64_encode(bin, sizeof bin);
|
||||
CHECK(b && strcmp(b, "AP8QgA==") == 0, "二进制应正确编码, 实际 %s",
|
||||
b ? b : "(null)");
|
||||
@@ -127,21 +140,21 @@ static void test_base64(void)
|
||||
}
|
||||
|
||||
/* 造一张内容合法 (靠文件头就能嗅探出来)的假图, 返回字节数 */
|
||||
static size_t fake_png(unsigned char *out, size_t n)
|
||||
{
|
||||
static size_t fake_png(unsigned char *out, size_t n) {
|
||||
static const char magic[] = "\x89PNG\r\n\x1a\n";
|
||||
size_t m = sizeof magic - 1;
|
||||
if (n < m) return 0;
|
||||
if (n < m)
|
||||
return 0;
|
||||
memcpy(out, magic, m);
|
||||
for (size_t i = m; i < n; i++) out[i] = (unsigned char)(i & 0xFF);
|
||||
for (size_t i = m; i < n; i++)
|
||||
out[i] = (unsigned char)(i & 0xFF);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 把一段备份引用塞进一条 user 消息, 模拟 read_image 之后的历史 */
|
||||
static const char *ref_message_type = "ligent_image";
|
||||
|
||||
static JsonValue *history_with_ref(const char *backup)
|
||||
{
|
||||
static JsonValue *history_with_ref(const char *backup) {
|
||||
JsonValue *msgs = json_new_array();
|
||||
|
||||
JsonValue *sys = json_new_object();
|
||||
@@ -162,20 +175,23 @@ static JsonValue *history_with_ref(const char *backup)
|
||||
return msgs;
|
||||
}
|
||||
|
||||
static void test_expand(void)
|
||||
{
|
||||
static void test_expand(void) {
|
||||
puts("请求前展开: 引用 -> image_url + base64");
|
||||
char tmpl[] = "/tmp/ligent_imgx_XXXXXX";
|
||||
char *dir = mkdtemp(tmpl);
|
||||
CHECK(dir != NULL, "建临时目录失败");
|
||||
if (!dir) return;
|
||||
if (!dir)
|
||||
return;
|
||||
image_set_bindir(dir);
|
||||
|
||||
unsigned char png[256];
|
||||
size_t n = fake_png(png, sizeof png);
|
||||
char *backup = image_backup_save(dir, png, n, "png");
|
||||
CHECK(backup != NULL, "备份应成功");
|
||||
if (!backup) { image_set_bindir("."); return; }
|
||||
if (!backup) {
|
||||
image_set_bindir(".");
|
||||
return;
|
||||
}
|
||||
|
||||
/* --- 正常展开 --- */
|
||||
JsonValue *msgs = history_with_ref(backup);
|
||||
@@ -191,14 +207,14 @@ static void test_expand(void)
|
||||
"附件消息应有一个 content 块");
|
||||
JsonValue *wp = json_array_get(wc, 0);
|
||||
CHECK(strcmp(json_get_string(wp, "type"), "image_url") == 0,
|
||||
"发送形态的类型应是 image_url, 实际 %s",
|
||||
json_get_string(wp, "type"));
|
||||
"发送形态的类型应是 image_url, 实际 %s", json_get_string(wp, "type"));
|
||||
JsonValue *iu = json_object_get(wp, "image_url");
|
||||
const char *url = iu ? json_get_string(iu, "url") : NULL;
|
||||
CHECK(url && strncmp(url, "data:image/png;base64,", 22) == 0,
|
||||
"应是 data: URL, 实际 %.40s", url ? url : "(null)");
|
||||
|
||||
/* base64 段应能解回原字节 —— 这里只验长度与首尾, 避免在测试里再写一个解码器 */
|
||||
/* base64 段应能解回原字节 —— 这里只验长度与首尾, 避免在测试里再写一个解码器
|
||||
*/
|
||||
CHECK(url && strlen(url) == 22 + base64_encoded_len(n),
|
||||
"data URL 长度应等于前缀 + base64 长度");
|
||||
|
||||
@@ -274,14 +290,14 @@ static void test_expand(void)
|
||||
free(backup);
|
||||
image_set_bindir(".");
|
||||
char *cmd = xasprintf("rm -rf %s", tmpl);
|
||||
if (system(cmd) != 0) { }
|
||||
if (system(cmd) != 0) {
|
||||
}
|
||||
free(cmd);
|
||||
}
|
||||
|
||||
static void test_pending(void)
|
||||
{
|
||||
static void test_pending(void) {
|
||||
puts("待发附件: 取走即清空");
|
||||
image_pending_take(); /* 先清干净 */
|
||||
image_pending_take(); /* 先清干净 */
|
||||
|
||||
image_pending_add("1700000000_aaaa.png");
|
||||
image_pending_add("1700000001_bbbb.png");
|
||||
@@ -296,7 +312,8 @@ static void test_pending(void)
|
||||
CHECK(json_get_string(p0, "file_id") == NULL,
|
||||
"不该再有 file_id 这种协议字段");
|
||||
CHECK(strcmp(json_get_string(json_array_get(arr, 1), "backup"),
|
||||
"1700000001_bbbb.png") == 0, "第二个备份名");
|
||||
"1700000001_bbbb.png") == 0,
|
||||
"第二个备份名");
|
||||
}
|
||||
json_free(arr);
|
||||
|
||||
@@ -306,8 +323,7 @@ static void test_pending(void)
|
||||
CHECK(image_pending_take() == NULL, "空备份名不该被记下");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
test_sniff();
|
||||
test_backup();
|
||||
test_base64();
|
||||
|
||||
+92
-68
@@ -9,25 +9,26 @@
|
||||
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"); \
|
||||
} \
|
||||
#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)
|
||||
{
|
||||
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);
|
||||
JsonValue *v =
|
||||
json_parse("{\"a\": 1, \"b\": [true, false, null], \"c\": \"hi\"}", err,
|
||||
sizeof err);
|
||||
CHECK(v != NULL, "解析失败: %s", err);
|
||||
if (!v) return;
|
||||
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,
|
||||
@@ -41,18 +42,20 @@ static void test_basic(void)
|
||||
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);
|
||||
free(dump);
|
||||
free(dump2);
|
||||
json_free(v);
|
||||
json_free(v2);
|
||||
}
|
||||
|
||||
static void test_escapes(void)
|
||||
{
|
||||
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);
|
||||
JsonValue *v = json_parse(
|
||||
"\"a\\nb\\t\\\"c\\\\d\\u4e2d\\u6587\\uD83D\\uDE00\"", err, sizeof err);
|
||||
CHECK(v != NULL, "解析失败: %s", err);
|
||||
if (!v) return;
|
||||
if (!v)
|
||||
return;
|
||||
const char *s = json_string_value(v);
|
||||
CHECK(strcmp(s, "a\nb\t\"c\\d中文😀") == 0, "转义解码错误: [%s]", s);
|
||||
|
||||
@@ -61,16 +64,18 @@ static void test_escapes(void)
|
||||
CHECK(v2 != NULL, "转义往返失败: %s / %s", q, err);
|
||||
CHECK(v2 && strcmp(json_string_value(v2), s) == 0, "转义往返内容不一致");
|
||||
free(q);
|
||||
json_free(v); json_free(v2);
|
||||
json_free(v);
|
||||
json_free(v2);
|
||||
}
|
||||
|
||||
static void test_numbers(void)
|
||||
{
|
||||
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);
|
||||
JsonValue *v =
|
||||
json_parse("[0, -1, 3.5, 1e3, -2.5e-2, 123456789012]", err, sizeof err);
|
||||
CHECK(v != NULL, "解析失败: %s", err);
|
||||
if (!v) return;
|
||||
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]");
|
||||
@@ -92,24 +97,34 @@ static void test_numbers(void)
|
||||
}
|
||||
/* 边界上合法的写法不能被误伤 */
|
||||
const char *ok_nums[] = {
|
||||
"0", "-0", "0.0", "10", "-1.5", "1e3", "1E3", "1e+3", "1e-3", "0.1e10",
|
||||
"123456789012345678901234567890", "1e308",
|
||||
"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);
|
||||
CHECK(g != NULL && g->type == JSON_NUMBER, "合法数字被拒: [%s] (%s)",
|
||||
ok_nums[i], err);
|
||||
json_free(g);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_invalid(void)
|
||||
{
|
||||
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",
|
||||
"{", "}", "[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);
|
||||
@@ -118,12 +133,12 @@ static void test_invalid(void)
|
||||
}
|
||||
/* 空字符串是合法 JSON 字符串 */
|
||||
JsonValue *e = json_parse("\"\"", err, sizeof err);
|
||||
CHECK(e != NULL && strcmp(json_string_value(e), "") == 0, "空字符串应为合法");
|
||||
CHECK(e != NULL && strcmp(json_string_value(e), "") == 0,
|
||||
"空字符串应为合法");
|
||||
json_free(e);
|
||||
}
|
||||
|
||||
static void test_builders(void)
|
||||
{
|
||||
static void test_builders(void) {
|
||||
puts("test_builders");
|
||||
JsonValue *o = json_new_object();
|
||||
json_object_set_string(o, "model", "gpt-x");
|
||||
@@ -133,30 +148,34 @@ static void test_builders(void)
|
||||
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"); /* 覆盖 */
|
||||
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,
|
||||
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 应相互独立");
|
||||
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);
|
||||
json_free(o);
|
||||
json_free(c);
|
||||
}
|
||||
|
||||
static void test_nested_depth(void)
|
||||
{
|
||||
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] = ']';
|
||||
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, "超深嵌套应被拒绝");
|
||||
@@ -168,8 +187,7 @@ static void test_nested_depth(void)
|
||||
}
|
||||
|
||||
/* 报错必须能定位: 只有"语法错了"而不知道错在哪, 等于没有报错 */
|
||||
static void test_error_location(void)
|
||||
{
|
||||
static void test_error_location(void) {
|
||||
puts("test_error_location");
|
||||
char err[256];
|
||||
JsonValue *v;
|
||||
@@ -192,7 +210,8 @@ static void test_error_location(void)
|
||||
/* 缺冒号 */
|
||||
v = json_parse("{\"a\" 1}", err, sizeof err);
|
||||
CHECK(v == NULL, "缺冒号应报错");
|
||||
CHECK(strstr(err, "第 1 行") && strstr(err, "':'") != NULL, "缺冒号: %s", err);
|
||||
CHECK(strstr(err, "第 1 行") && strstr(err, "':'") != NULL, "缺冒号: %s",
|
||||
err);
|
||||
json_free(v);
|
||||
|
||||
/* 尾部多余内容 */
|
||||
@@ -220,30 +239,29 @@ static void test_error_location(void)
|
||||
json_free(v);
|
||||
}
|
||||
|
||||
/* 序列化出去的东西必须是**合法 UTF-8**.
|
||||
/* 序列化出去的东西必须是**合法 UTF-8**.
|
||||
*
|
||||
* 服务端会因为请求体里有非法 UTF-8 直接回
|
||||
* 400 ... invalid unicode code point
|
||||
* 而那段坏数据一旦进了对话历史, 之后每次请求都会失败 —— 除非丢掉整轮对话.
|
||||
* 而那段坏数据一旦进了对话历史, 之后每次请求都会失败 —— 除非丢掉整轮对话.
|
||||
* 坏字节的来源很日常: 某条命令在非 UTF-8 locale 下打出来的路径、被截断的多
|
||||
* 字节字符、二进制垃圾. 所以这条不能只靠"在入口清理", 必须在序列化器这层兜住:
|
||||
* 出口是唯一能保证"发出去的永远合法"的地方.
|
||||
* 字节字符、二进制垃圾. 所以这条不能只靠"在入口清理", 必须在序列化器这层兜住:
|
||||
* 出口是唯一能保证"发出去的永远合法"的地方.
|
||||
*
|
||||
* 这个校验函数故意用 utf8_seq_len 自己走一遍, 而不是信任 dump 的实现 ——
|
||||
* 断言的是**产物**的性质, 不是实现的自述. */
|
||||
static int utf8_all_valid(const char *s)
|
||||
{
|
||||
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;
|
||||
if (adv == 0)
|
||||
return 0;
|
||||
i += adv;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void test_dump_utf8_safety(void)
|
||||
{
|
||||
static void test_dump_utf8_safety(void) {
|
||||
puts("test_dump_utf8_safety");
|
||||
|
||||
/* utf8_seq_len: 合法序列返回长度 */
|
||||
@@ -262,19 +280,25 @@ static void test_dump_utf8_safety(void)
|
||||
|
||||
/* 各种坏字节经 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", /* 合法中文里夹一个坏字节 */
|
||||
"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);
|
||||
CHECK(utf8_all_valid(d), "第 %zu 个坏输入序列化后仍含非法 UTF-8: %s", k,
|
||||
d);
|
||||
/* 而且我们的解析器 (和服务端一样严格)必须收得下 */
|
||||
char err[256];
|
||||
JsonValue *back = json_parse(d, err, sizeof err);
|
||||
@@ -294,14 +318,14 @@ static void test_dump_utf8_safety(void)
|
||||
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, "\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)
|
||||
{
|
||||
int main(void) {
|
||||
test_basic();
|
||||
test_escapes();
|
||||
test_numbers();
|
||||
|
||||
+51
-42
@@ -10,41 +10,41 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define BUFEQ(lb, want) \
|
||||
CHECK(strcmp((lb).buf, (want)) == 0 && (lb).len == strlen(want), \
|
||||
#define BUFEQ(lb, want) \
|
||||
CHECK(strcmp((lb).buf, (want)) == 0 && (lb).len == strlen(want), \
|
||||
"内容应为 [%s], 实际 [%s]", want, (lb).buf)
|
||||
|
||||
/* 读一个文本文件的前 n 字节到 buf (用于检查落盘格式) */
|
||||
static void slurp(const char *path, char *buf, size_t buflen)
|
||||
{
|
||||
static void slurp(const char *path, char *buf, size_t buflen) {
|
||||
buf[0] = '\0';
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return;
|
||||
if (!f)
|
||||
return;
|
||||
size_t n = fread(buf, 1, buflen - 1, f);
|
||||
buf[n] = '\0';
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void test_history_file(void)
|
||||
{
|
||||
static void test_history_file(void) {
|
||||
puts("输入历史的持久化");
|
||||
char path[] = "/tmp/ligent_hist_XXXXXX";
|
||||
int fd = mkstemp(path);
|
||||
CHECK(fd >= 0, "mkstemp 失败");
|
||||
if (fd < 0) return;
|
||||
if (fd < 0)
|
||||
return;
|
||||
close(fd);
|
||||
unlink(path); /* 故意从"文件不存在"开始 */
|
||||
unlink(path); /* 故意从"文件不存在"开始 */
|
||||
|
||||
line_history_clear();
|
||||
CHECK(line_history_load(path, 100) == 0, "文件不存在不算错误");
|
||||
@@ -75,7 +75,7 @@ static void test_history_file(void)
|
||||
unlink(path);
|
||||
line_history_add("echo C:\\Users\\me");
|
||||
line_history_append(path, "echo C:\\Users\\me");
|
||||
line_history_add("第一行\n第二行"); /* 反斜杠续行拼出来的多行输入 */
|
||||
line_history_add("第一行\n第二行"); /* 反斜杠续行拼出来的多行输入 */
|
||||
line_history_append(path, "第一行\n第二行");
|
||||
line_history_add("中文历史");
|
||||
line_history_append(path, "中文历史");
|
||||
@@ -84,7 +84,9 @@ static void test_history_file(void)
|
||||
CHECK(strchr(raw, '\n') != NULL && strstr(raw, "\n\n") == NULL,
|
||||
"每条只占一行, 实际 %s", raw);
|
||||
int newlines = 0;
|
||||
for (const char *p = raw; *p; p++) if (*p == '\n') newlines++;
|
||||
for (const char *p = raw; *p; p++)
|
||||
if (*p == '\n')
|
||||
newlines++;
|
||||
CHECK(newlines == 3, "3 条历史应占 3 行, 实际 %d 行", newlines);
|
||||
|
||||
line_history_clear();
|
||||
@@ -92,8 +94,10 @@ static void test_history_file(void)
|
||||
CHECK(line_history_len() == 3, "应读回 3 条, 实际 %zu", line_history_len());
|
||||
CHECK(strcmp(line_history_get(0), "echo C:\\Users\\me") == 0,
|
||||
"反斜杠应原样还原, 实际 [%s]", line_history_get(0));
|
||||
CHECK(line_history_get(1) && strcmp(line_history_get(1), "第一行\n第二行") == 0,
|
||||
"多行条目应整条还原, 实际 [%s]", line_history_get(1) ? line_history_get(1) : "(null)");
|
||||
CHECK(line_history_get(1) &&
|
||||
strcmp(line_history_get(1), "第一行\n第二行") == 0,
|
||||
"多行条目应整条还原, 实际 [%s]",
|
||||
line_history_get(1) ? line_history_get(1) : "(null)");
|
||||
CHECK(strcmp(line_history_get(2), "中文历史") == 0, "中文应原样还原");
|
||||
line_history_clear();
|
||||
|
||||
@@ -107,12 +111,15 @@ static void test_history_file(void)
|
||||
}
|
||||
int lines = 0;
|
||||
slurp(path, raw, sizeof raw);
|
||||
for (const char *p = raw; *p; p++) if (*p == '\n') lines++;
|
||||
for (const char *p = raw; *p; p++)
|
||||
if (*p == '\n')
|
||||
lines++;
|
||||
CHECK(lines == 10, "截短前应有 10 行, 实际 %d", lines);
|
||||
|
||||
line_history_clear();
|
||||
line_history_load(path, 4);
|
||||
CHECK(line_history_len() == 4, "keep=4 时应只剩 4 条, 实际 %zu", line_history_len());
|
||||
CHECK(line_history_len() == 4, "keep=4 时应只剩 4 条, 实际 %zu",
|
||||
line_history_len());
|
||||
CHECK(line_history_get(0) && strcmp(line_history_get(0), "cmd-6") == 0,
|
||||
"应保留最新的 4 条, 第一条应是 cmd-6, 实际 [%s]",
|
||||
line_history_get(0) ? line_history_get(0) : "(null)");
|
||||
@@ -122,20 +129,22 @@ static void test_history_file(void)
|
||||
|
||||
slurp(path, raw, sizeof raw);
|
||||
lines = 0;
|
||||
for (const char *p = raw; *p; p++) if (*p == '\n') lines++;
|
||||
for (const char *p = raw; *p; p++)
|
||||
if (*p == '\n')
|
||||
lines++;
|
||||
CHECK(lines == 4, "文件应被就地截短成 4 行, 实际 %d", lines);
|
||||
|
||||
/* 截短后的文件读回来还是那 4 条 */
|
||||
line_history_clear();
|
||||
line_history_load(path, 100);
|
||||
CHECK(line_history_len() == 4, "截短后应只读到 4 条, 实际 %zu", line_history_len());
|
||||
CHECK(line_history_len() == 4, "截短后应只读到 4 条, 实际 %zu",
|
||||
line_history_len());
|
||||
line_history_clear();
|
||||
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
puts("插入与删除");
|
||||
LineBuf lb;
|
||||
linebuf_init(&lb);
|
||||
@@ -164,13 +173,13 @@ int main(void)
|
||||
linebuf_delete(&lb);
|
||||
BUFEQ(lb, "eXllo");
|
||||
linebuf_end(&lb);
|
||||
linebuf_delete(&lb); /* 末尾 Delete 应无动作 */
|
||||
linebuf_delete(&lb); /* 末尾 Delete 应无动作 */
|
||||
BUFEQ(lb, "eXllo");
|
||||
linebuf_free(&lb);
|
||||
|
||||
puts("UTF-8: 按字符而非字节移动");
|
||||
linebuf_init(&lb);
|
||||
linebuf_set(&lb, "中文abc"); /* 中(3) 文(3) a b c = 9 字节 */
|
||||
linebuf_set(&lb, "中文abc"); /* 中(3) 文(3) a b c = 9 字节 */
|
||||
CHECK(lb.len == 9, "应为 9 字节, 实际 %zu", lb.len);
|
||||
linebuf_left(&lb);
|
||||
CHECK(lb.pos == 8, "左移一格应为 8, 实际 %zu", lb.pos);
|
||||
@@ -202,8 +211,8 @@ int main(void)
|
||||
/* 中间删除多字节字符 */
|
||||
linebuf_set(&lb, "a中b");
|
||||
linebuf_home(&lb);
|
||||
linebuf_right(&lb); /* 光标到 'a' 之后 */
|
||||
linebuf_delete(&lb); /* 删掉 '中' */
|
||||
linebuf_right(&lb); /* 光标到 'a' 之后 */
|
||||
linebuf_delete(&lb); /* 删掉 '中' */
|
||||
BUFEQ(lb, "ab");
|
||||
CHECK(lb.pos == 1, "删除后光标不动, 实际 %zu", lb.pos);
|
||||
linebuf_free(&lb);
|
||||
@@ -217,41 +226,41 @@ int main(void)
|
||||
CHECK(lb.pos == lb.len, "单行时 End 到行尾");
|
||||
|
||||
/* 多行 (粘贴进来的那种): Home/End 只在本行内走 */
|
||||
linebuf_set(&lb, "aaa\nbbb\nccc"); /* 长度 11 */
|
||||
linebuf_set(&lb, "aaa\nbbb\nccc"); /* 长度 11 */
|
||||
CHECK(lb.len == 11, "应为 11 字节, 实际 %zu", lb.len);
|
||||
linebuf_home(&lb);
|
||||
CHECK(lb.pos == 8, "末尾按 Home 应到最后一行的行首 (8), 实际 %zu", lb.pos);
|
||||
linebuf_end(&lb);
|
||||
CHECK(lb.pos == 11, "End 应到本行行尾 (11), 实际 %zu", lb.pos);
|
||||
|
||||
lb.pos = 5; /* 落在 "bbb" 中间 */
|
||||
lb.pos = 5; /* 落在 "bbb" 中间 */
|
||||
linebuf_home(&lb);
|
||||
CHECK(lb.pos == 4, "中间行按 Home 应到该行行首 (4), 实际 %zu", lb.pos);
|
||||
linebuf_end(&lb);
|
||||
CHECK(lb.pos == 7, "中间行按 End 应到该行行尾 (7), 实际 %zu", lb.pos);
|
||||
|
||||
lb.pos = 4; /* 正好在行首 */
|
||||
lb.pos = 4; /* 正好在行首 */
|
||||
linebuf_home(&lb);
|
||||
CHECK(lb.pos == 4, "已在行首时 Home 不应跳到整个缓冲区开头");
|
||||
lb.pos = 7; /* 正好在行尾 (换行符前) */
|
||||
lb.pos = 7; /* 正好在行尾 (换行符前) */
|
||||
linebuf_end(&lb);
|
||||
CHECK(lb.pos == 7, "已在行尾时 End 不应跳过换行符");
|
||||
|
||||
linebuf_set(&lb, "one two three");
|
||||
lb.pos = 7; /* "one two" 之后 */
|
||||
lb.pos = 7; /* "one two" 之后 */
|
||||
linebuf_kill_to_end(&lb);
|
||||
BUFEQ(lb, "one two");
|
||||
|
||||
linebuf_set(&lb, "one two three");
|
||||
lb.pos = 8; /* "one two " 之后 */
|
||||
lb.pos = 8; /* "one two " 之后 */
|
||||
linebuf_kill_to_start(&lb);
|
||||
BUFEQ(lb, "three");
|
||||
CHECK(lb.pos == 0, "删到行首后光标应在行首");
|
||||
|
||||
linebuf_set(&lb, "one two three");
|
||||
linebuf_kill_word(&lb); /* 从末尾回删一个词 */
|
||||
linebuf_kill_word(&lb); /* 从末尾回删一个词 */
|
||||
BUFEQ(lb, "one two ");
|
||||
linebuf_kill_word(&lb); /* 先跳过空格再删词 */
|
||||
linebuf_kill_word(&lb); /* 先跳过空格再删词 */
|
||||
BUFEQ(lb, "one ");
|
||||
linebuf_free(&lb);
|
||||
|
||||
@@ -260,8 +269,8 @@ int main(void)
|
||||
CHECK(line_history_len() == 0, "初始应为空");
|
||||
line_history_add("first");
|
||||
line_history_add("second");
|
||||
line_history_add("second"); /* 与上一条重复, 应忽略 */
|
||||
line_history_add(""); /* 空行不入历史 */
|
||||
line_history_add("second"); /* 与上一条重复, 应忽略 */
|
||||
line_history_add(""); /* 空行不入历史 */
|
||||
CHECK(line_history_len() == 2, "应有 2 条, 实际 %zu", line_history_len());
|
||||
CHECK(strcmp(line_history_get(0), "first") == 0, "第 0 条");
|
||||
CHECK(strcmp(line_history_get(1), "second") == 0, "第 1 条");
|
||||
|
||||
+158
-133
@@ -1,7 +1,7 @@
|
||||
/* test_markdown.c - 流式 Markdown 渲染器测试
|
||||
*
|
||||
* 最关键的不变量: 把同一段文本任意切分后逐片喂入, 输出必须与一次性喂入
|
||||
* 完全一致. SSE 的分片位置是随机的, 这条不成立就会漏出"半个星号".
|
||||
* 完全一致. SSE 的分片位置是随机的, 这条不成立就会漏出"半个星号".
|
||||
*/
|
||||
#include "markdown.h"
|
||||
#include "util.h"
|
||||
@@ -13,27 +13,28 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* 去掉 ANSI 转义, 得到"肉眼可见"的文本 */
|
||||
static char *strip_ansi(const char *s)
|
||||
{
|
||||
static char *strip_ansi(const char *s) {
|
||||
StrBuf out;
|
||||
sb_init(&out);
|
||||
for (const char *p = s; *p; ) {
|
||||
for (const char *p = s; *p;) {
|
||||
if (*p == 0x1b && p[1] == '[') {
|
||||
p += 2;
|
||||
while (*p && !(*p >= '@' && *p <= '~')) p++;
|
||||
if (*p) p++;
|
||||
while (*p && !(*p >= '@' && *p <= '~'))
|
||||
p++;
|
||||
if (*p)
|
||||
p++;
|
||||
continue;
|
||||
}
|
||||
sb_putc(&out, *p++);
|
||||
@@ -41,22 +42,24 @@ static char *strip_ansi(const char *s)
|
||||
return sb_detach(&out);
|
||||
}
|
||||
|
||||
static void show_escaped(const char *s)
|
||||
{
|
||||
static void show_escaped(const char *s) {
|
||||
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
||||
if (*p == 0x1b) printf("<ESC>");
|
||||
else if (*p == '\n') printf("\\n");
|
||||
else putchar(*p);
|
||||
if (*p == 0x1b)
|
||||
printf("<ESC>");
|
||||
else if (*p == '\n')
|
||||
printf("\\n");
|
||||
else
|
||||
putchar(*p);
|
||||
}
|
||||
}
|
||||
|
||||
/* 渲染到内存流; chunk == 0 表示一次性喂入 */
|
||||
static char *do_render(const char *input, int enabled, size_t chunk)
|
||||
{
|
||||
static char *do_render(const char *input, int enabled, size_t chunk) {
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *f = open_memstream(&buf, &size);
|
||||
if (!f) return NULL;
|
||||
if (!f)
|
||||
return NULL;
|
||||
MdRenderer *r = md_new(enabled);
|
||||
md_set_out(r, f);
|
||||
|
||||
@@ -67,7 +70,8 @@ static char *do_render(const char *input, int enabled, size_t chunk)
|
||||
for (size_t i = 0; i < len; i += chunk) {
|
||||
size_t n = len - i < chunk ? len - i : chunk;
|
||||
char tmp[64];
|
||||
if (n >= sizeof tmp) n = sizeof tmp - 1;
|
||||
if (n >= sizeof tmp)
|
||||
n = sizeof tmp - 1;
|
||||
memcpy(tmp, input + i, n);
|
||||
tmp[n] = '\0';
|
||||
md_feed(r, tmp);
|
||||
@@ -79,15 +83,13 @@ static char *do_render(const char *input, int enabled, size_t chunk)
|
||||
return buf;
|
||||
}
|
||||
|
||||
static char *render(const char *in, int enabled)
|
||||
{
|
||||
static char *render(const char *in, int enabled) {
|
||||
return do_render(in, enabled, 0);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_split_invariance(void)
|
||||
{
|
||||
static void test_split_invariance(void) {
|
||||
puts("分片不变性 (一次性 / 3 字节 / 逐字节 必须一致)");
|
||||
static const char *samples[] = {
|
||||
"**粗体**\n",
|
||||
@@ -121,36 +123,43 @@ static void test_split_invariance(void)
|
||||
"```\n# 不是标题\n[x](y)\n```\n",
|
||||
"ls -la | grep foo\n",
|
||||
"a | b\nc | d\n",
|
||||
/* 以下三个是模糊测试找到并最小化出来的历史缺陷,
|
||||
/* 以下三个是模糊测试找到并最小化出来的历史缺陷,
|
||||
* 每个都曾让"一次性喂"与"分片喂"结果不一致, 留作回归见证 */
|
||||
"1|`", /* 表格探测挂起整行 -> 行内代码被漏渲染 */
|
||||
"- |", /* try_block 提前 return 跳过了列表规则 */
|
||||
"s|\n-", /* 表格判断依赖分片边界 */
|
||||
"| a |\n|---|\n", /* 表头+分隔行后紧跟 EOF */
|
||||
"| a |\n|---|", /* 末尾无换行的表格 */
|
||||
"| a | b |\n|---|---|\n| 1 | 2 |\n- 列表\n", /* 表格后紧跟列表 */
|
||||
"| a |\n| b |\n| c |\n", /* 有续行但缺分隔行 */
|
||||
"1|`", /* 表格探测挂起整行 -> 行内代码被漏渲染 */
|
||||
"- |", /* try_block 提前 return 跳过了列表规则 */
|
||||
"s|\n-", /* 表格判断依赖分片边界 */
|
||||
"| a |\n|---|\n", /* 表头+分隔行后紧跟 EOF */
|
||||
"| a |\n|---|", /* 末尾无换行的表格 */
|
||||
"| a | b |\n|---|---|\n| 1 | 2 |\n- 列表\n", /* 表格后紧跟列表 */
|
||||
"| a |\n| b |\n| c |\n", /* 有续行但缺分隔行 */
|
||||
};
|
||||
for (size_t k = 0; k < sizeof samples / sizeof samples[0]; k++) {
|
||||
char *whole = render(samples[k], 1);
|
||||
char *by1 = do_render(samples[k], 1, 1);
|
||||
char *by3 = do_render(samples[k], 1, 3);
|
||||
int ok = whole && by1 && by3 &&
|
||||
strcmp(whole, by1) == 0 && strcmp(whole, by3) == 0;
|
||||
int ok = whole && by1 && by3 && strcmp(whole, by1) == 0 &&
|
||||
strcmp(whole, by3) == 0;
|
||||
checks++;
|
||||
if (!ok) {
|
||||
failures++;
|
||||
printf(" ✗ 分片不一致: [%s]\n", samples[k]);
|
||||
printf(" 一次性: "); show_escaped(whole ? whole : "(null)"); printf("\n");
|
||||
printf(" 逐字节: "); show_escaped(by1 ? by1 : "(null)"); printf("\n");
|
||||
printf(" 3 字节: "); show_escaped(by3 ? by3 : "(null)"); printf("\n");
|
||||
printf(" 一次性: ");
|
||||
show_escaped(whole ? whole : "(null)");
|
||||
printf("\n");
|
||||
printf(" 逐字节: ");
|
||||
show_escaped(by1 ? by1 : "(null)");
|
||||
printf("\n");
|
||||
printf(" 3 字节: ");
|
||||
show_escaped(by3 ? by3 : "(null)");
|
||||
printf("\n");
|
||||
}
|
||||
free(whole); free(by1); free(by3);
|
||||
free(whole);
|
||||
free(by1);
|
||||
free(by3);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_headings(void)
|
||||
{
|
||||
static void test_headings(void) {
|
||||
puts("标题");
|
||||
char *o = render("# 一\n## 二\n### 三\n#### 四\n", 1);
|
||||
CHECK(strstr(o, "\033[1;36m# 一\033[0m") != NULL, "H1 应为粗体青");
|
||||
@@ -160,7 +169,8 @@ static void test_headings(void)
|
||||
char *vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "# 一\n## 二\n### 三\n#### 四\n") == 0,
|
||||
"可见文本应保留 # 标记: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 不是标题的情况必须原样保留 */
|
||||
o = render("#没有空格\n", 1);
|
||||
@@ -176,20 +186,21 @@ static void test_headings(void)
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_bold_italic(void)
|
||||
{
|
||||
static void test_bold_italic(void) {
|
||||
puts("粗体 / 斜体");
|
||||
char *o = render("**粗**\n", 1);
|
||||
CHECK(strstr(o, "\033[1m粗\033[0m") != NULL, "粗体样式: [%s]", o);
|
||||
char *vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "粗\n") == 0, "粗体标记应被吃掉: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
o = render("*斜*\n", 1);
|
||||
CHECK(strstr(o, "\033[3m斜\033[0m") != NULL, "斜体样式: [%s]", o);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "斜\n") == 0, "斜体标记应被吃掉: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 字面星号: 乘法 */
|
||||
o = render("2 * 3 * 4\n", 1);
|
||||
@@ -210,43 +221,46 @@ static void test_bold_italic(void)
|
||||
o = render("**粗 *斜* 粗**\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "粗 斜 粗\n") == 0, "嵌套可见文本: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_code(void)
|
||||
{
|
||||
static void test_code(void) {
|
||||
puts("代码");
|
||||
/* 行内代码: 保留反引号 (便于复制), 内容着色 */
|
||||
char *o = render("用 `ls -la` 看看\n", 1);
|
||||
char *vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "用 `ls -la` 看看\n") == 0,
|
||||
"行内代码应保留反引号: [%s]", vis);
|
||||
CHECK(strcmp(vis, "用 `ls -la` 看看\n") == 0, "行内代码应保留反引号: [%s]",
|
||||
vis);
|
||||
CHECK(strstr(o, "\033[36m") != NULL, "内容应着色: [%s]", o);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 围栏代码块内部不做行内解析 */
|
||||
o = render("```\n**原样**\n```\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "```\n**原样**\n```\n") == 0,
|
||||
"代码块内不应渲染粗体: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 带语言标识 */
|
||||
o = render("```c\nx;\n```\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "```c\nx;\n```\n") == 0,
|
||||
"语言标识与内容应完整保留: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
CHECK(strcmp(vis, "```c\nx;\n```\n") == 0, "语言标识与内容应完整保留: [%s]",
|
||||
vis);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 未闭合的代码块: 不能吞掉后面的内容 */
|
||||
o = render("```\ncode\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "```\ncode\n") == 0, "未闭合代码块: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_blocks(void)
|
||||
{
|
||||
static void test_blocks(void) {
|
||||
puts("列表 / 引用 / 分隔线");
|
||||
/* 列表标记必须保留模型输出的原始字符, 只上色, 不换成 • */
|
||||
char *o = render("- 甲\n- 乙\n", 1);
|
||||
@@ -254,32 +268,38 @@ static void test_blocks(void)
|
||||
CHECK(strcmp(vis, "- 甲\n- 乙\n") == 0, "列表应保留原始 - : [%s]", vis);
|
||||
CHECK(strstr(o, "\033[36m-\033[0m") != NULL, "列表标记 - 应着色: [%s]", o);
|
||||
CHECK(strstr(o, "•") == NULL, "不应出现非 ASCII 圆点: [%s]", o);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
o = render("* 甲\n+ 乙\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "* 甲\n+ 乙\n") == 0, "列表应保留原始 * 和 + : [%s]", vis);
|
||||
CHECK(strcmp(vis, "* 甲\n+ 乙\n") == 0, "列表应保留原始 * 和 + : [%s]",
|
||||
vis);
|
||||
CHECK(strstr(o, "\033[36m*\033[0m") != NULL, "列表标记 * 应着色: [%s]", o);
|
||||
CHECK(strstr(o, "\033[36m+\033[0m") != NULL, "列表标记 + 应着色: [%s]", o);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 表格不受影响: 仍然是框线字符 */
|
||||
o = render("| a |\n|---|\n| 1 |\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strstr(vis, "┌") && strstr(vis, "│") && strstr(vis, "└"),
|
||||
"表格仍用框线字符: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
o = render("> 引用\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "│ 引用\n") == 0, "引用: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
o = render("---\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "---\n") == 0, "分隔线可见文本: [%s]", vis);
|
||||
CHECK(strstr(o, "\033[2m") != NULL, "分隔线应暗色: [%s]", o);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 非分隔线、非列表应原样 */
|
||||
o = render("--x\n", 1);
|
||||
@@ -287,8 +307,7 @@ static void test_blocks(void)
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_passthrough(void)
|
||||
{
|
||||
static void test_passthrough(void) {
|
||||
puts("关闭渲染时逐字节透传");
|
||||
static const char *samples[] = {
|
||||
"# 标题 **粗体** `code`\n",
|
||||
@@ -300,20 +319,19 @@ static void test_passthrough(void)
|
||||
for (size_t k = 0; k < sizeof samples / sizeof samples[0]; k++) {
|
||||
char *whole = render(samples[k], 0);
|
||||
char *by1 = do_render(samples[k], 0, 1);
|
||||
CHECK(strcmp(whole, samples[k]) == 0,
|
||||
"应完全透传: 期望 [%s] 实际 [%s]", samples[k], whole);
|
||||
CHECK(strcmp(whole, samples[k]) == 0, "应完全透传: 期望 [%s] 实际 [%s]",
|
||||
samples[k], whole);
|
||||
CHECK(strcmp(by1, samples[k]) == 0, "分片透传也应一致");
|
||||
free(whole); free(by1);
|
||||
free(whole);
|
||||
free(by1);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_fence_verbatim(void)
|
||||
{
|
||||
static void test_fence_verbatim(void) {
|
||||
puts("围栏代码块: 全部不渲染");
|
||||
/* 纯代码块: 输出里不应出现任何 ANSI 转义 */
|
||||
char *o = render("```c\nint x = 1; // **不是粗体**\n```\n", 1);
|
||||
CHECK(strstr(o, "\033") == NULL,
|
||||
"代码块内不应有任何 ANSI 转义: [%s]", o);
|
||||
CHECK(strstr(o, "\033") == NULL, "代码块内不应有任何 ANSI 转义: [%s]", o);
|
||||
CHECK(strcmp(o, "```c\nint x = 1; // **不是粗体**\n```\n") == 0,
|
||||
"代码块应逐字节原样: [%s]", o);
|
||||
free(o);
|
||||
@@ -322,27 +340,26 @@ static void test_fence_verbatim(void)
|
||||
o = render("```\n# 不是标题\n- 不是列表\n> 不是引用\n[x](y)\n```\n", 1);
|
||||
CHECK(strstr(o, "\033") == NULL, "代码块内不应有转义: [%s]", o);
|
||||
CHECK(strstr(o, "# 不是标题") && strstr(o, "[x](y)") &&
|
||||
strstr(o, "- 不是列表") && strstr(o, "> 不是引用"),
|
||||
strstr(o, "- 不是列表") && strstr(o, "> 不是引用"),
|
||||
"代码块内容应完整: [%s]", o);
|
||||
free(o);
|
||||
|
||||
/* 代码块结束之后, 普通文本要恢复渲染 */
|
||||
o = render("```\nx\n```\n**加粗**\n", 1);
|
||||
CHECK(strstr(o, "\033[1m加粗\033[0m") != NULL,
|
||||
"代码块之后应恢复渲染: [%s]", o);
|
||||
CHECK(strstr(o, "\033[1m加粗\033[0m") != NULL, "代码块之后应恢复渲染: [%s]",
|
||||
o);
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_inline_code_protected(void)
|
||||
{
|
||||
static void test_inline_code_protected(void) {
|
||||
puts("行内代码内容不受行内语法影响");
|
||||
char *o = render("形如 `a**b**c` 的写法\n", 1);
|
||||
CHECK(strstr(o, "\033[1m") == NULL,
|
||||
"行内代码内不应渲染粗体: [%s]", o);
|
||||
CHECK(strstr(o, "\033[1m") == NULL, "行内代码内不应渲染粗体: [%s]", o);
|
||||
char *vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "形如 `a**b**c` 的写法\n") == 0,
|
||||
"行内代码可见文本应原样: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
o = render("`[不是链接](x)` 和 `_不是斜体_`\n", 1);
|
||||
CHECK(strstr(o, "\033[94m") == NULL, "行内代码内不应高亮 []: [%s]", o);
|
||||
@@ -350,17 +367,18 @@ static void test_inline_code_protected(void)
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "`[不是链接](x)` 和 `_不是斜体_`\n") == 0,
|
||||
"可见文本应原样: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_underscore_italic(void)
|
||||
{
|
||||
static void test_underscore_italic(void) {
|
||||
puts("_斜体_ 与词内下划线保护");
|
||||
char *o = render("这是 _斜体_ 文字\n", 1);
|
||||
CHECK(strstr(o, "\033[3m斜体\033[0m") != NULL, "_斜体_ 应渲染: [%s]", o);
|
||||
char *vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "这是 斜体 文字\n") == 0, "可见文本: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 中文紧邻下划线也应生效 (非 ASCII 不算词字符) */
|
||||
o = render("中文_斜体_中文\n", 1);
|
||||
@@ -369,15 +387,14 @@ static void test_underscore_italic(void)
|
||||
|
||||
/* 关键保护: 下划线标识符不能被当成斜体 */
|
||||
o = render("foo_bar_baz 和 __init__ 和 a_b_c\n", 1);
|
||||
CHECK(strstr(o, "\033[3m") == NULL,
|
||||
"下划线标识符不应被渲染为斜体: [%s]", o);
|
||||
CHECK(strstr(o, "\033[3m") == NULL, "下划线标识符不应被渲染为斜体: [%s]",
|
||||
o);
|
||||
CHECK(strcmp(o, "foo_bar_baz 和 __init__ 和 a_b_c\n") == 0,
|
||||
"标识符应逐字节保留: [%s]", o);
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_link_bracket(void)
|
||||
{
|
||||
static void test_link_bracket(void) {
|
||||
puts("链接: [] 下划线+青色高亮, 其余原样");
|
||||
char *o = render("见 [文档](https://example.com/a) 说明\n", 1);
|
||||
/* 这里是下划线 + 青 (SGR 4;36), 不是亮蓝. 整个项目只用 16 色范围内的
|
||||
@@ -387,13 +404,15 @@ static void test_link_bracket(void)
|
||||
char *vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "见 [文档](https://example.com/a) 说明\n") == 0,
|
||||
"可见文本应逐字节保留 (含 URL): [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 图片也是 [...], 同样只高亮方括号部分, 整段保留 */
|
||||
o = render("\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "\n") == 0, "图片应保留原文: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 下标写法不应被当成链接 */
|
||||
o = render("arr[i] = br[j]\n", 1);
|
||||
@@ -402,73 +421,79 @@ static void test_link_bracket(void)
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_tables(void)
|
||||
{
|
||||
static void test_tables(void) {
|
||||
puts("表格");
|
||||
/* ASCII 表格: 逐字节比对完整边框输出 */
|
||||
char *o = render("| a | b |\n|---|---|\n| 1 | 22 |\n", 1);
|
||||
char *vis = strip_ansi(o);
|
||||
const char *want =
|
||||
"┌───┬────┐\n"
|
||||
"│ a │ b │\n"
|
||||
"├───┼────┤\n"
|
||||
"│ 1 │ 22 │\n"
|
||||
"└───┴────┘\n";
|
||||
CHECK(strcmp(vis, want) == 0, "ASCII 表格:\n期望:\n%s\n实际:\n%s", want, vis);
|
||||
const char *want = "┌───┬────┐\n"
|
||||
"│ a │ b │\n"
|
||||
"├───┼────┤\n"
|
||||
"│ 1 │ 22 │\n"
|
||||
"└───┴────┘\n";
|
||||
CHECK(strcmp(vis, want) == 0, "ASCII 表格:\n期望:\n%s\n实际:\n%s", want,
|
||||
vis);
|
||||
CHECK(strstr(o, "\033[1m") != NULL, "表头应加粗: [%s]", o);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 中文列宽按 2 列计算, 边框必须对齐 */
|
||||
o = render("| 名称 | 值 |\n|---|---|\n| 苹果 | 3 |\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
const char *want2 =
|
||||
"┌──────┬────┐\n"
|
||||
"│ 名称 │ 值 │\n"
|
||||
"├──────┼────┤\n"
|
||||
"│ 苹果 │ 3 │\n"
|
||||
"└──────┴────┘\n";
|
||||
CHECK(strcmp(vis, want2) == 0, "中文表格:\n期望:\n%s\n实际:\n%s", want2, vis);
|
||||
free(vis); free(o);
|
||||
const char *want2 = "┌──────┬────┐\n"
|
||||
"│ 名称 │ 值 │\n"
|
||||
"├──────┼────┤\n"
|
||||
"│ 苹果 │ 3 │\n"
|
||||
"└──────┴────┘\n";
|
||||
CHECK(strcmp(vis, want2) == 0, "中文表格:\n期望:\n%s\n实际:\n%s", want2,
|
||||
vis);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 对齐标记 */
|
||||
o = render("| 左 | 中 | 右 |\n|:---|:---:|---:|\n| a | b | c |\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
const char *want3 =
|
||||
"┌────┬────┬────┐\n"
|
||||
"│ 左 │ 中 │ 右 │\n"
|
||||
"├────┼────┼────┤\n"
|
||||
"│ a │ b │ c │\n"
|
||||
"└────┴────┴────┘\n";
|
||||
CHECK(strcmp(vis, want3) == 0, "对齐表格:\n期望:\n%s\n实际:\n%s", want3, vis);
|
||||
free(vis); free(o);
|
||||
const char *want3 = "┌────┬────┬────┐\n"
|
||||
"│ 左 │ 中 │ 右 │\n"
|
||||
"├────┼────┼────┤\n"
|
||||
"│ a │ b │ c │\n"
|
||||
"└────┴────┴────┘\n";
|
||||
CHECK(strcmp(vis, want3) == 0, "对齐表格:\n期望:\n%s\n实际:\n%s", want3,
|
||||
vis);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 顶格必须写 '|'. 不带首竖线的写法按普通文本处理 ——
|
||||
* 这是为保证"输出与分片方式无关"而做的取舍:
|
||||
* 判断必须在行的第 0 个字节就能做出, 否则分片落在竖线之前时,
|
||||
* 这是为保证"输出与分片方式无关"而做的取舍:
|
||||
* 判断必须在行的第 0 个字节就能做出, 否则分片落在竖线之前时,
|
||||
* 行的前半段已被当作普通文本输出, 结果会随分片方式而变. */
|
||||
o = render("a | b\n--- | ---\n1 | 2\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strcmp(vis, "a | b\n--- | ---\n1 | 2\n") == 0,
|
||||
"无首竖线应原样输出: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 首竖线前有缩进同理: 不认 */
|
||||
o = render(" | a |\n |---|\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strstr(vis, "┌") == NULL, "缩进的表格不识别: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 误判防护: 普通文本里出现竖线 + 下一行是短横线, 不能当成表格 */
|
||||
o = render("运行 ls |\n-\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strstr(vis, "┌") == NULL, "竖线+短横线不应误判成表格: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 单列极简表 */
|
||||
o = render("| x |\n|---|\n| 1 |\n", 1);
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strstr(vis, "┌───┐") != NULL, "单列表格: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
|
||||
/* 没有分隔行就不是表格 */
|
||||
o = render("a | b\nc | d\n", 1);
|
||||
@@ -485,17 +510,18 @@ static void test_tables(void)
|
||||
vis = strip_ansi(o);
|
||||
CHECK(strstr(vis, "└───┴───┘\n后续段落 粗体\n") != NULL,
|
||||
"表格后应恢复普通渲染: [%s]", vis);
|
||||
free(vis); free(o);
|
||||
free(vis);
|
||||
free(o);
|
||||
}
|
||||
|
||||
static void test_long_line_bounded(void)
|
||||
{
|
||||
static void test_long_line_bounded(void) {
|
||||
puts("超长待处理缓冲应有界");
|
||||
/* 一个没有换行的超长 fence 信息行: 渲染器不能无限缓存 */
|
||||
StrBuf sb;
|
||||
sb_init(&sb);
|
||||
sb_puts(&sb, "```");
|
||||
for (int i = 0; i < 200000; i++) sb_putc(&sb, 'x');
|
||||
for (int i = 0; i < 200000; i++)
|
||||
sb_putc(&sb, 'x');
|
||||
char *o = render(sb.data, 1);
|
||||
size_t n = strlen(o);
|
||||
CHECK(n > 100000, "应该已经把内容吐出, 而不是无限缓存 (实际 %zu 字节)", n);
|
||||
@@ -503,8 +529,7 @@ static void test_long_line_bounded(void)
|
||||
sb_free(&sb);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
test_split_invariance();
|
||||
test_headings();
|
||||
test_bold_italic();
|
||||
|
||||
+111
-84
@@ -1,8 +1,8 @@
|
||||
/* test_meta.c - meta 历史编辑: 轮次切分、结构安全、各工具的行为 */
|
||||
#include "meta.h"
|
||||
#include "json.h"
|
||||
#include "util.h"
|
||||
#include "agent.h"
|
||||
#include "json.h"
|
||||
#include "meta.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -11,20 +11,19 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* 造一条消息 */
|
||||
static JsonValue *msg(const char *role, const char *content)
|
||||
{
|
||||
static JsonValue *msg(const char *role, const char *content) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", role);
|
||||
json_object_set_string(m, "content", content);
|
||||
@@ -32,8 +31,8 @@ static JsonValue *msg(const char *role, const char *content)
|
||||
}
|
||||
|
||||
/* assistant 带一个 tool_call */
|
||||
static JsonValue *assistant_call(const char *id, const char *name, const char *args)
|
||||
{
|
||||
static JsonValue *assistant_call(const char *id, const char *name,
|
||||
const char *args) {
|
||||
JsonValue *m = json_new_object();
|
||||
json_object_set_string(m, "role", "assistant");
|
||||
json_object_set_string(m, "content", "");
|
||||
@@ -50,7 +49,7 @@ static JsonValue *assistant_call(const char *id, const char *name, const char *a
|
||||
return m;
|
||||
}
|
||||
|
||||
/* 造一段典型历史:
|
||||
/* 造一段典型历史:
|
||||
* #0 system
|
||||
* #1 user A ← 轮 1
|
||||
* #2 assistant(tool_calls)
|
||||
@@ -61,12 +60,12 @@ static JsonValue *assistant_call(const char *id, const char *name, const char *a
|
||||
* #7 user C ← 轮 3
|
||||
* #8 assistant 回答
|
||||
*/
|
||||
static JsonValue *sample(void)
|
||||
{
|
||||
static JsonValue *sample(void) {
|
||||
JsonValue *a = json_new_array();
|
||||
json_array_push(a, msg("system", "你是助手"));
|
||||
json_array_push(a, msg("user", "帮我用 Python 写"));
|
||||
json_array_push(a, assistant_call("call_1", "run_command", "{\"command\":\"ls\"}"));
|
||||
json_array_push(
|
||||
a, assistant_call("call_1", "run_command", "{\"command\":\"ls\"}"));
|
||||
json_array_push(a, msg("tool", "file1\nfile2"));
|
||||
json_array_push(a, msg("assistant", "写好了"));
|
||||
json_array_push(a, msg("user", "改用 C"));
|
||||
@@ -76,13 +75,13 @@ static JsonValue *sample(void)
|
||||
return a;
|
||||
}
|
||||
|
||||
static void test_rounds(void)
|
||||
{
|
||||
static void test_rounds(void) {
|
||||
puts("轮次切分");
|
||||
JsonValue *m = sample();
|
||||
MetaRounds r;
|
||||
CHECK(meta_rounds_build(m, &r) == 1, "应该能切出轮次");
|
||||
CHECK(r.head == 1, "system 之后的 #1 才是第一条可编辑消息, 实际 %zu", r.head);
|
||||
CHECK(r.head == 1, "system 之后的 #1 才是第一条可编辑消息, 实际 %zu",
|
||||
r.head);
|
||||
CHECK(r.count == 3, "应切出 3 轮, 实际 %zu", r.count);
|
||||
CHECK(r.starts[0] == 1, "轮 1 从 #1 开始, 实际 %zu", r.starts[0]);
|
||||
CHECK(r.starts[1] == 5, "轮 2 从 #5 开始, 实际 %zu", r.starts[1]);
|
||||
@@ -105,14 +104,17 @@ static void test_rounds(void)
|
||||
meta_rounds_free(&r);
|
||||
json_free(only);
|
||||
|
||||
/* read_image 补的那条"只有图片附件、没有文字"的 user 消息**不算新的一轮** ——
|
||||
* 判据必须与主循环 (agent_opens_round)一致, 否则 /meta 报的轮号会和主循环的
|
||||
* 口径对不上, 模型照轮号删下去, 删掉的不是它以为的那一轮.
|
||||
* 结构: system / user(text) / assistant(tool_calls) / tool / user(只有图) / assistant */
|
||||
/* read_image 补的那条"只有图片附件、没有文字"的 user 消息**不算新的一轮**
|
||||
* —— 判据必须与主循环 (agent_opens_round)一致, 否则 /meta
|
||||
* 报的轮号会和主循环的 口径对不上, 模型照轮号删下去,
|
||||
* 删掉的不是它以为的那一轮. 结构: system / user(text) /
|
||||
* assistant(tool_calls) / tool / user(只有图) / assistant */
|
||||
JsonValue *withimg = json_new_array();
|
||||
json_array_push(withimg, msg("system", "s"));
|
||||
json_array_push(withimg, msg("user", "看图"));
|
||||
json_array_push(withimg, msg("assistant", "ok")); /* 占位, 模拟带 tool_calls 的轮内消息 */
|
||||
json_array_push(
|
||||
withimg,
|
||||
msg("assistant", "ok")); /* 占位, 模拟带 tool_calls 的轮内消息 */
|
||||
json_array_push(withimg, msg("tool", "结果"));
|
||||
{
|
||||
JsonValue *att = json_new_object();
|
||||
@@ -126,15 +128,15 @@ static void test_rounds(void)
|
||||
}
|
||||
json_array_push(withimg, msg("assistant", "看到图了"));
|
||||
CHECK(meta_rounds_build(withimg, &r) == 1, "带附件的历史也应该能切");
|
||||
CHECK(r.count == 1, "只有附件的那条 user 不该算新的一轮, 实际 %zu 轮", r.count);
|
||||
CHECK(r.count == 1, "只有附件的那条 user 不该算新的一轮, 实际 %zu 轮",
|
||||
r.count);
|
||||
meta_rounds_free(&r);
|
||||
json_free(withimg);
|
||||
|
||||
json_free(m);
|
||||
}
|
||||
|
||||
static void test_deletable(void)
|
||||
{
|
||||
static void test_deletable(void) {
|
||||
puts("结构安全: 哪些消息能单独删");
|
||||
JsonValue *m = sample();
|
||||
CHECK(meta_deletable(m, 0) == 0, "system 不可删");
|
||||
@@ -148,11 +150,13 @@ static void test_deletable(void)
|
||||
|
||||
/* ---- 工具驱动 ---- */
|
||||
|
||||
static char *call(Agent *a, const char *name, const char *json)
|
||||
{
|
||||
static char *call(Agent *a, const char *name, const char *json) {
|
||||
char err[256];
|
||||
JsonValue *args = json_parse(json, err, sizeof err);
|
||||
if (!args) { printf(" (测试自身出错: %s)\n", err); return xstrdup(""); }
|
||||
if (!args) {
|
||||
printf(" (测试自身出错: %s)\n", err);
|
||||
return xstrdup("");
|
||||
}
|
||||
int is_err = 0;
|
||||
char *r = meta_tools_call(name, args, &is_err);
|
||||
json_free(args);
|
||||
@@ -160,37 +164,43 @@ static char *call(Agent *a, const char *name, const char *json)
|
||||
}
|
||||
|
||||
/* 把一段历史挂上, 跑 body, 再摘下来 */
|
||||
#define WITH_TOOLS(a) \
|
||||
do { \
|
||||
CHECK(meta_tools_attach(a) == 0, "attach 应成功"); \
|
||||
CHECK(meta_tools_attach(a) != 0, "重复 attach 应被拒绝"); \
|
||||
#define WITH_TOOLS(a) \
|
||||
do { \
|
||||
CHECK(meta_tools_attach(a) == 0, "attach 应成功"); \
|
||||
CHECK(meta_tools_attach(a) != 0, "重复 attach 应被拒绝"); \
|
||||
} while (0)
|
||||
|
||||
static void test_replace(void)
|
||||
{
|
||||
static void test_replace(void) {
|
||||
puts("replace_message");
|
||||
Agent a; agent_init(&a);
|
||||
json_free(a.messages); a.messages = sample();
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
json_free(a.messages);
|
||||
a.messages = sample();
|
||||
meta_tools_attach(&a);
|
||||
|
||||
int err = 0;
|
||||
char *r = call(&a, "replace_message", "{\"index\":1,\"content\":\"帮我用 C 写\"}");
|
||||
CHECK(err == 0 && r && strstr(r, "已改写") != NULL, "应改写成功: %s", r ? r : "");
|
||||
char *r = call(&a, "replace_message",
|
||||
"{\"index\":1,\"content\":\"帮我用 C 写\"}");
|
||||
CHECK(err == 0 && r && strstr(r, "已改写") != NULL, "应改写成功: %s",
|
||||
r ? r : "");
|
||||
JsonValue *m1 = json_array_get(a.messages, 1);
|
||||
CHECK(strcmp(json_get_string(m1, "content"), "帮我用 C 写") == 0, "内容应被替换");
|
||||
CHECK(strcmp(json_get_string(m1, "content"), "帮我用 C 写") == 0,
|
||||
"内容应被替换");
|
||||
CHECK(strcmp(json_get_string(m1, "role"), "user") == 0, "角色不该变");
|
||||
free(r);
|
||||
|
||||
/* 改 assistant 说的话 —— 这是 meta 存在的理由 */
|
||||
r = call(&a, "replace_message", "{\"index\":4,\"content\":\"改好了\"}");
|
||||
CHECK(r && strstr(r, "已改写") != NULL, "应能改写 assistant 消息");
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 4), "content"), "改好了") == 0,
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 4), "content"),
|
||||
"改好了") == 0,
|
||||
"assistant 内容应被替换");
|
||||
free(r);
|
||||
|
||||
/* tool 结果和 system 都不该被动 */
|
||||
r = call(&a, "replace_message", "{\"index\":3,\"content\":\"x\"}");
|
||||
CHECK(r && strstr(r, "tool 结果") != NULL, "改 tool 结果应被拒绝: %s", r ? r : "");
|
||||
CHECK(r && strstr(r, "tool 结果") != NULL, "改 tool 结果应被拒绝: %s",
|
||||
r ? r : "");
|
||||
free(r);
|
||||
r = call(&a, "replace_message", "{\"index\":0,\"content\":\"x\"}");
|
||||
CHECK(r && strstr(r, "system") != NULL, "改 system 应被拒绝");
|
||||
@@ -205,16 +215,18 @@ static void test_replace(void)
|
||||
agent_free(&a);
|
||||
}
|
||||
|
||||
static void test_delete_safety(void)
|
||||
{
|
||||
static void test_delete_safety(void) {
|
||||
puts("delete_messages 的结构安全");
|
||||
Agent a; agent_init(&a);
|
||||
json_free(a.messages); a.messages = sample();
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
json_free(a.messages);
|
||||
a.messages = sample();
|
||||
meta_tools_attach(&a);
|
||||
|
||||
size_t before = json_array_size(a.messages);
|
||||
char *r = call(&a, "delete_messages", "{\"indices\":[2]}");
|
||||
CHECK(r && strstr(r, "不可拆") != NULL, "带 tool_calls 的 assistant 应被拒绝");
|
||||
CHECK(r && strstr(r, "不可拆") != NULL,
|
||||
"带 tool_calls 的 assistant 应被拒绝");
|
||||
CHECK(json_array_size(a.messages) == before, "被拒绝时不该改动任何东西");
|
||||
free(r);
|
||||
|
||||
@@ -231,8 +243,10 @@ static void test_delete_safety(void)
|
||||
|
||||
/* 合法删除: 删掉第 1 轮里那句 user, 并给上下文提示 */
|
||||
r = call(&a, "delete_messages", "{\"indices\":[1]}");
|
||||
CHECK(r && strstr(r, "已删除 1 条") != NULL, "合法删除应成功: %s", r ? r : "");
|
||||
CHECK(strstr(r, "没有对应的提问") != NULL, "删掉提问后应提示后续回答失去上下文");
|
||||
CHECK(r && strstr(r, "已删除 1 条") != NULL, "合法删除应成功: %s",
|
||||
r ? r : "");
|
||||
CHECK(strstr(r, "没有对应的提问") != NULL,
|
||||
"删掉提问后应提示后续回答失去上下文");
|
||||
CHECK(json_array_size(a.messages) == before - 1, "条数应减一");
|
||||
free(r);
|
||||
|
||||
@@ -245,20 +259,23 @@ static void test_delete_safety(void)
|
||||
agent_free(&a);
|
||||
}
|
||||
|
||||
static void test_delete_rounds(void)
|
||||
{
|
||||
static void test_delete_rounds(void) {
|
||||
puts("delete_rounds");
|
||||
Agent a; agent_init(&a);
|
||||
json_free(a.messages); a.messages = sample();
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
json_free(a.messages);
|
||||
a.messages = sample();
|
||||
meta_tools_attach(&a);
|
||||
|
||||
size_t before = json_array_size(a.messages);
|
||||
char *r = call(&a, "delete_rounds", "{\"from\":2,\"to\":2}");
|
||||
CHECK(r && strstr(r, "已删除轮 2..2") != NULL, "应删掉第 2 轮: %s", r ? r : "");
|
||||
CHECK(r && strstr(r, "已删除轮 2..2") != NULL, "应删掉第 2 轮: %s",
|
||||
r ? r : "");
|
||||
CHECK(json_array_size(a.messages) == before - 2, "第 2 轮是 2 条消息");
|
||||
/* 剩下的结构必须还是自洽的: #2 的 tool_calls 后面仍跟着 tool */
|
||||
CHECK(meta_deletable(a.messages, 2) == 0, "#2 仍应是结构消息");
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 3), "role"), "tool") == 0,
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 3), "role"),
|
||||
"tool") == 0,
|
||||
"tool 结果应仍紧跟它的调用");
|
||||
free(r);
|
||||
|
||||
@@ -276,24 +293,29 @@ static void test_delete_rounds(void)
|
||||
agent_free(&a);
|
||||
}
|
||||
|
||||
static void test_insert_and_undo(void)
|
||||
{
|
||||
static void test_insert_and_undo(void) {
|
||||
puts("insert_message 与 undo");
|
||||
Agent a; agent_init(&a);
|
||||
json_free(a.messages); a.messages = sample();
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
json_free(a.messages);
|
||||
a.messages = sample();
|
||||
meta_tools_attach(&a);
|
||||
|
||||
size_t before = json_array_size(a.messages);
|
||||
char *r = call(&a, "insert_message",
|
||||
"{\"after\":8,\"role\":\"user\",\"content\":\"这个项目必须用 C99\"}");
|
||||
CHECK(r && strstr(r, "插入一条 user") != NULL, "应插入成功: %s", r ? r : "");
|
||||
char *r = call(
|
||||
&a, "insert_message",
|
||||
"{\"after\":8,\"role\":\"user\",\"content\":\"这个项目必须用 C99\"}");
|
||||
CHECK(r && strstr(r, "插入一条 user") != NULL, "应插入成功: %s",
|
||||
r ? r : "");
|
||||
CHECK(json_array_size(a.messages) == before + 1, "条数应加一");
|
||||
CHECK(strcmp(json_get_string(json_array_get(a.messages, 9), "content"),
|
||||
"这个项目必须用 C99") == 0, "应插在 #8 之后");
|
||||
"这个项目必须用 C99") == 0,
|
||||
"应插在 #8 之后");
|
||||
free(r);
|
||||
|
||||
/* tool 角色不能凭空插 —— 没有配对的 tool_calls */
|
||||
r = call(&a, "insert_message", "{\"after\":0,\"role\":\"tool\",\"content\":\"x\"}");
|
||||
r = call(&a, "insert_message",
|
||||
"{\"after\":0,\"role\":\"tool\",\"content\":\"x\"}");
|
||||
CHECK(r && strstr(r, "role 只能是") != NULL, "插 tool 消息应被拒绝");
|
||||
free(r);
|
||||
|
||||
@@ -313,21 +335,24 @@ static void test_insert_and_undo(void)
|
||||
agent_free(&a);
|
||||
}
|
||||
|
||||
static void test_read_tools(void)
|
||||
{
|
||||
static void test_read_tools(void) {
|
||||
puts("list_rounds / read_messages (只读)");
|
||||
Agent a; agent_init(&a);
|
||||
json_free(a.messages); a.messages = sample();
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
json_free(a.messages);
|
||||
a.messages = sample();
|
||||
meta_tools_attach(&a);
|
||||
|
||||
char *r = call(&a, "list_rounds", "{}");
|
||||
CHECK(r && strstr(r, "共 3 轮") != NULL, "应报告 3 轮: %s", r ? r : "");
|
||||
CHECK(r && strstr(r, "轮 1") != NULL && strstr(r, "轮 3") != NULL, "应列出每轮");
|
||||
CHECK(r && strstr(r, "轮 1") != NULL && strstr(r, "轮 3") != NULL,
|
||||
"应列出每轮");
|
||||
CHECK(r && strstr(r, "Python") != NULL, "应带开头预览");
|
||||
free(r);
|
||||
|
||||
r = call(&a, "list_rounds", "{\"from\":2,\"to\":2}");
|
||||
CHECK(r && strstr(r, "轮 2") != NULL && strstr(r, "轮 3") == NULL, "应只列第 2 轮");
|
||||
CHECK(r && strstr(r, "轮 2") != NULL && strstr(r, "轮 3") == NULL,
|
||||
"应只列第 2 轮");
|
||||
free(r);
|
||||
|
||||
r = call(&a, "read_messages", "{\"from\":2,\"to\":3}");
|
||||
@@ -337,9 +362,11 @@ static void test_read_tools(void)
|
||||
free(r);
|
||||
|
||||
CHECK(meta_tools_mutating("list_rounds") == 0, "list_rounds 不是写操作");
|
||||
CHECK(meta_tools_mutating("read_messages") == 0, "read_messages 不是写操作");
|
||||
CHECK(meta_tools_mutating("read_messages") == 0,
|
||||
"read_messages 不是写操作");
|
||||
CHECK(meta_tools_mutating("delete_rounds") == 1, "delete_rounds 是写操作");
|
||||
CHECK(meta_tools_mutating("replace_message") == 1, "replace_message 是写操作");
|
||||
CHECK(meta_tools_mutating("replace_message") == 1,
|
||||
"replace_message 是写操作");
|
||||
CHECK(meta_tools_mutating("不存在的工具") == 0, "未知工具不该被当成写操作");
|
||||
|
||||
r = call(&a, "delete_rounds_but_typo", "{}");
|
||||
@@ -350,16 +377,17 @@ static void test_read_tools(void)
|
||||
agent_free(&a);
|
||||
}
|
||||
|
||||
static void test_reroot_flag(void)
|
||||
{
|
||||
static void test_reroot_flag(void) {
|
||||
puts("重新存根标记");
|
||||
Agent a; agent_init(&a);
|
||||
json_free(a.messages); a.messages = sample();
|
||||
Agent a;
|
||||
agent_init(&a);
|
||||
json_free(a.messages);
|
||||
a.messages = sample();
|
||||
meta_reroot_clear();
|
||||
CHECK(meta_reroot_pending() == 0, "初始不该有标记");
|
||||
|
||||
meta_tools_attach(&a);
|
||||
char *r = call(&a, "list_rounds", "{}"); /* 只读, 不该打标记 */
|
||||
char *r = call(&a, "list_rounds", "{}"); /* 只读, 不该打标记 */
|
||||
free(r);
|
||||
CHECK(meta_reroot_pending() == 0, "只读操作不该打标记");
|
||||
|
||||
@@ -372,8 +400,7 @@ static void test_reroot_flag(void)
|
||||
meta_reroot_clear();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
test_rounds();
|
||||
test_deletable();
|
||||
test_read_tools();
|
||||
|
||||
+25
-29
@@ -1,12 +1,12 @@
|
||||
/* test_models.c - /models 响应体的解析
|
||||
*
|
||||
* 只测纯解析. 拉取 (GET {base_url}/models)要走网络, 由 e2e 第 52 段对着
|
||||
* mock 覆盖, 那里还顺带验了交互式选择本身 (方向键、确认、取消).
|
||||
* mock 覆盖, 那里还顺带验了交互式选择本身 (方向键、确认、取消).
|
||||
*
|
||||
* 为什么要容忍这么多种形状: OpenAI 兼容是个很大的家庭, /models 的返回花样
|
||||
* 比文档里写的多 —— 见过裸数组、见过重复列同一模型、见过元素里没有 id.
|
||||
* 比文档里写的多 —— 见过裸数组、见过重复列同一模型、见过元素里没有 id.
|
||||
* 解析器要么跳过, 要么去重, 但**不能**因此整个失败: 列表里少一个模型远好过
|
||||
* 一个都选不了.
|
||||
* 一个都选不了.
|
||||
*/
|
||||
#include "models.h"
|
||||
|
||||
@@ -16,19 +16,18 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void test_standard(void)
|
||||
{
|
||||
static void test_standard(void) {
|
||||
puts("标准形状: {\"data\":[{\"id\":...}]}");
|
||||
const char *json =
|
||||
"{\"object\":\"list\",\"data\":["
|
||||
@@ -39,20 +38,19 @@ static void test_standard(void)
|
||||
char **l = models_parse(json, &n);
|
||||
CHECK(l && n == 2, "应解析出 2 个, 实际 %zu", n);
|
||||
if (l && n == 2) {
|
||||
CHECK(strcmp(l[0], "alpha") == 0, "顺序应保持服务端给的 (第一个 alpha)");
|
||||
CHECK(strcmp(l[0], "alpha") == 0,
|
||||
"顺序应保持服务端给的 (第一个 alpha)");
|
||||
CHECK(strcmp(l[1], "beta") == 0, "第二个 beta");
|
||||
}
|
||||
models_free(l, n);
|
||||
}
|
||||
|
||||
static void test_dedupe_and_skip(void)
|
||||
{
|
||||
static void test_dedupe_and_skip(void) {
|
||||
puts("重复项要去重, 缺 id 的要跳过");
|
||||
const char *json =
|
||||
"{\"data\":["
|
||||
"{\"id\":\"a\"},{\"id\":\"a\"},{\"object\":\"model\"},"
|
||||
"{\"id\":\"\"},{\"id\":\"b\"},{\"id\":42}"
|
||||
"]}";
|
||||
const char *json = "{\"data\":["
|
||||
"{\"id\":\"a\"},{\"id\":\"a\"},{\"object\":\"model\"},"
|
||||
"{\"id\":\"\"},{\"id\":\"b\"},{\"id\":42}"
|
||||
"]}";
|
||||
size_t n = 0;
|
||||
char **l = models_parse(json, &n);
|
||||
CHECK(l && n == 2, "去重 + 跳过后应剩 2 个, 实际 %zu", n);
|
||||
@@ -63,8 +61,7 @@ static void test_dedupe_and_skip(void)
|
||||
models_free(l, n);
|
||||
}
|
||||
|
||||
static void test_bare_array(void)
|
||||
{
|
||||
static void test_bare_array(void) {
|
||||
puts("兼容裸数组 (字符串 / 对象都认)");
|
||||
size_t n = 0;
|
||||
char **l = models_parse("[\"alpha\",\"beta\"]", &n);
|
||||
@@ -77,8 +74,7 @@ static void test_bare_array(void)
|
||||
models_free(l, n);
|
||||
}
|
||||
|
||||
static void test_bad_input(void)
|
||||
{
|
||||
static void test_bad_input(void) {
|
||||
puts("拿不到模型时返回 NULL, 而不是空数组或崩溃");
|
||||
size_t n = 123;
|
||||
CHECK(models_parse(NULL, &n) == NULL && n == 0, "NULL 输入");
|
||||
@@ -96,7 +92,8 @@ static void test_bad_input(void)
|
||||
CHECK(models_parse("{\"data\":\"nope\"}", &n) == NULL && n == 0,
|
||||
"data 不是数组");
|
||||
n = 123;
|
||||
CHECK(models_parse("{\"data\":[{\"object\":\"model\"}]}", &n) == NULL && n == 0,
|
||||
CHECK(models_parse("{\"data\":[{\"object\":\"model\"}]}", &n) == NULL &&
|
||||
n == 0,
|
||||
"全是坏条目");
|
||||
|
||||
/* out_n 也可为 NULL (调用方只关心数组) */
|
||||
@@ -122,8 +119,7 @@ static void test_bad_input(void)
|
||||
CHECK(1, "释放 NULL 没崩");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
test_standard();
|
||||
test_dedupe_and_skip();
|
||||
test_bare_array();
|
||||
|
||||
+42
-31
@@ -1,12 +1,12 @@
|
||||
/* test_priv.c - 提权模块里那几条"不能搞错"的不变量
|
||||
*
|
||||
* 这个模块摔过一跤: 上一版把认证路径也 setsid + 接管道, 于是 sudo 拿不到
|
||||
* 控制终端, 真机上认证 100% 失败 (而替身 sudo 无条件成功, 给了个假绿).
|
||||
* 所以这里把两条硬不变量钉住:
|
||||
* 控制终端, 真机上认证 100% 失败 (而替身 sudo 无条件成功, 给了个假绿).
|
||||
* 所以这里把两条硬不变量钉住:
|
||||
*
|
||||
* 1. **提权调用里不能有 -n**. 带 -n 就等于"不许提问", 需要密码时直接失败,
|
||||
* 而用户要的恰恰是"提权要有身份验证".
|
||||
* 2. **没有控制终端时能识别出来**. 识别不出来就会起一个等不到输入的进程.
|
||||
* 1. **提权调用里不能有 -n**. 带 -n 就等于"不许提问", 需要密码时直接失败,
|
||||
* 而用户要的恰恰是"提权要有身份验证".
|
||||
* 2. **没有控制终端时能识别出来**. 识别不出来就会起一个等不到输入的进程.
|
||||
*/
|
||||
#include "priv.h"
|
||||
#include "util.h"
|
||||
@@ -20,31 +20,32 @@
|
||||
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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static int argv_has(char *const *a, const char *want)
|
||||
{
|
||||
static int argv_has(char *const *a, const char *want) {
|
||||
for (size_t i = 0; a[i]; i++)
|
||||
if (strcmp(a[i], want) == 0) return 1;
|
||||
if (strcmp(a[i], want) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
puts("包装成 sudo -u <用户> -- <argv>");
|
||||
/* 显式转换, 和 priv.c 里 w[0] = (char *)"sudo" 是同一个理由:
|
||||
/* 显式转换, 和 priv.c 里 w[0] = (char *)"sudo" 是同一个理由:
|
||||
* priv_sudo_argv 的签名要 char *const[] (execvp 的要求), 而这里放进去的
|
||||
* 字面量没有任何人会改写. 把整条链路改成非 const 才是真的丢 const 正确性. */
|
||||
char *plain[] = { (char *)"/bin/sh", (char *)"-c", (char *)"rm -rf /tmp/x", NULL };
|
||||
* 字面量没有任何人会改写. 把整条链路改成非 const 才是真的丢 const 正确性.
|
||||
*/
|
||||
char *plain[] = {(char *)"/bin/sh", (char *)"-c", (char *)"rm -rf /tmp/x",
|
||||
NULL};
|
||||
char **w = priv_sudo_argv("root", plain);
|
||||
CHECK(w != NULL, "应当返回数组");
|
||||
CHECK(strcmp(w[0], "sudo") == 0, "第一个是 sudo, 实际 %s", w[0]);
|
||||
@@ -53,7 +54,7 @@ int main(void)
|
||||
CHECK(strcmp(w[3], "--") == 0,
|
||||
"必须有 -- : 否则命令里的选项会被 sudo 当成自己的");
|
||||
CHECK(strcmp(w[4], "/bin/sh") == 0 && strcmp(w[5], "-c") == 0 &&
|
||||
strcmp(w[6], "rm -rf /tmp/x") == 0,
|
||||
strcmp(w[6], "rm -rf /tmp/x") == 0,
|
||||
"原 argv 原样接在后面, 不被拆开");
|
||||
CHECK(w[7] == NULL, "数组要 NULL 结尾");
|
||||
/* 命门外的那条不变量 */
|
||||
@@ -75,7 +76,8 @@ int main(void)
|
||||
puts("sudo 的失败原话要换成能照做的中文");
|
||||
char err[512];
|
||||
memset(err, 0, sizeof err);
|
||||
priv_explain(1, "sudo: no tty present and no askpass program specified", err, sizeof err);
|
||||
priv_explain(1, "sudo: no tty present and no askpass program specified",
|
||||
err, sizeof err);
|
||||
CHECK(strstr(err, "终端") != NULL, "缺终端要说终端, 实际 %s", err);
|
||||
priv_explain(1, "Sorry, try again.", err, sizeof err);
|
||||
CHECK(strstr(err, "认证") != NULL, "认证失败要说认证, 实际 %s", err);
|
||||
@@ -86,19 +88,27 @@ int main(void)
|
||||
priv_explain(1, "some brand new message", err, sizeof err);
|
||||
CHECK(strstr(err, "some brand new message") != NULL,
|
||||
"认不出来要透传原文, 实际 %s", err);
|
||||
priv_explain(127, "无法执行 sudo: No such file or directory", err, sizeof err);
|
||||
CHECK(strstr(err, "没有 sudo") != NULL, "没有 sudo 时要说清楚, 实际 %s", err);
|
||||
priv_explain(127, "无法执行 sudo: No such file or directory", err,
|
||||
sizeof err);
|
||||
CHECK(strstr(err, "没有 sudo") != NULL, "没有 sudo 时要说清楚, 实际 %s",
|
||||
err);
|
||||
|
||||
puts("没有控制终端时能识别出来");
|
||||
int fds[2];
|
||||
if (pipe(fds) != 0) { perror("pipe"); return 2; }
|
||||
if (pipe(fds) != 0) {
|
||||
perror("pipe");
|
||||
return 2;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { perror("fork"); return 2; }
|
||||
if (pid < 0) {
|
||||
perror("fork");
|
||||
return 2;
|
||||
}
|
||||
if (pid == 0) {
|
||||
close(fds[0]);
|
||||
int rc = -1;
|
||||
if (setsid() >= 0) { /* 摘掉控制终端 */
|
||||
rc = priv_have_tty(); /* 必须是 0 */
|
||||
if (setsid() >= 0) { /* 摘掉控制终端 */
|
||||
rc = priv_have_tty(); /* 必须是 0 */
|
||||
}
|
||||
ssize_t wr = write(fds[1], &rc, sizeof rc);
|
||||
(void)wr;
|
||||
@@ -109,7 +119,8 @@ int main(void)
|
||||
int got = -1;
|
||||
ssize_t rd = read(fds[0], &got, sizeof got);
|
||||
close(fds[0]);
|
||||
while (waitpid(pid, NULL, 0) < 0) { }
|
||||
while (waitpid(pid, NULL, 0) < 0) {
|
||||
}
|
||||
CHECK(rd == (ssize_t)sizeof got && got == 0,
|
||||
"setsid() 之后不该认为有终端 (识别不出来就会卡住)");
|
||||
|
||||
|
||||
+48
-44
@@ -1,45 +1,47 @@
|
||||
/* test_spinner.c - 等待动画的两段式标签
|
||||
*
|
||||
* 动画写的是 stderr, 所以这里把 stderr 挪到一个临时文件, 驱动几轮之后
|
||||
* 再把内容读回来断言. spinner_init(1) 是显式开关, 测试里不依赖 isatty.
|
||||
* 再把内容读回来断言. spinner_init(1) 是显式开关, 测试里不依赖 isatty.
|
||||
*/
|
||||
#include "spinner.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <unistd.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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* 把 stderr 接到一个临时文件上, 返回文件描述符; *saved 收原来的 stderr.
|
||||
/* 把 stderr 接到一个临时文件上, 返回文件描述符; *saved 收原来的 stderr.
|
||||
* 用完必须调用 capture_end 还原, 否则后面的失败信息也会被吞掉. */
|
||||
static int capture_begin(char *path, size_t pathlen, int *saved)
|
||||
{
|
||||
static int capture_begin(char *path, size_t pathlen, int *saved) {
|
||||
snprintf(path, pathlen, "/tmp/ligent_spin_XXXXXX");
|
||||
int fd = mkstemp(path);
|
||||
if (fd < 0) return -1;
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
*saved = dup(STDERR_FILENO);
|
||||
if (*saved < 0) { close(fd); return -1; }
|
||||
if (*saved < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
dup2(fd, STDERR_FILENO);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void capture_end(int fd, int saved, char *buf, size_t buflen)
|
||||
{
|
||||
static void capture_end(int fd, int saved, char *buf, size_t buflen) {
|
||||
fflush(stderr);
|
||||
dup2(saved, STDERR_FILENO);
|
||||
close(saved);
|
||||
@@ -49,14 +51,14 @@ static void capture_end(int fd, int saved, char *buf, size_t buflen)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void test_two_phase(void)
|
||||
{
|
||||
static void test_two_phase(void) {
|
||||
puts("两段式标签: 等待响应 → 思考中");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
if (fd < 0)
|
||||
return;
|
||||
|
||||
spinner_init(1);
|
||||
spinner_start("等待响应");
|
||||
@@ -81,17 +83,18 @@ static void test_two_phase(void)
|
||||
CHECK(a && b && a < b, "先\"等待响应\"后\"思考中\", 顺序反了");
|
||||
CHECK(strstr(buf, "\r\033[K") != NULL, "每帧应先回车清行再打印");
|
||||
/* 停在"思考中"之后就不该再有"等待响应"冒出来 */
|
||||
if (b) CHECK(strstr(b, "等待响应") == NULL, "换标签后不应回退到\"等待响应\"");
|
||||
if (b)
|
||||
CHECK(strstr(b, "等待响应") == NULL, "换标签后不应回退到\"等待响应\"");
|
||||
}
|
||||
|
||||
static void test_idempotent(void)
|
||||
{
|
||||
static void test_idempotent(void) {
|
||||
puts("未开始/未启用时的行为");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
if (fd < 0)
|
||||
return;
|
||||
|
||||
/* 没 start 就 set: 只记标签, 不该打印任何东西, 也不该崩 */
|
||||
spinner_init(1);
|
||||
@@ -121,24 +124,23 @@ static void test_idempotent(void)
|
||||
CHECK(1, "关闭状态下调用不崩溃");
|
||||
}
|
||||
|
||||
static double now_sec(void)
|
||||
{
|
||||
static double now_sec(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
||||
}
|
||||
|
||||
/* spinner_stop 要 pthread_join worker. 如果 worker 是 nanosleep 一整帧,
|
||||
/* spinner_stop 要 pthread_join worker. 如果 worker 是 nanosleep 一整帧,
|
||||
* 这里就得干等最多 250ms —— 而这笔账会加在每一轮模型往返、每一次工具
|
||||
* 来回的用户感知延迟上. */
|
||||
static void test_stop_is_prompt(void)
|
||||
{
|
||||
static void test_stop_is_prompt(void) {
|
||||
puts("stop 不该等满一帧 (250ms)");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
if (fd < 0)
|
||||
return;
|
||||
|
||||
spinner_init(1);
|
||||
double worst = 0.0;
|
||||
@@ -147,31 +149,33 @@ static void test_stop_is_prompt(void)
|
||||
spinner_start("等待响应");
|
||||
spinner_stop();
|
||||
double dt = now_sec() - t0;
|
||||
if (dt > worst) worst = dt;
|
||||
if (dt > worst)
|
||||
worst = dt;
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
capture_end(fd, saved, buf, sizeof buf);
|
||||
unlink(path);
|
||||
CHECK(worst < 0.10, "stop 应立即返回, 最慢一次 %.3fs (帧间隔是 0.25s)", worst);
|
||||
CHECK(worst < 0.10, "stop 应立即返回, 最慢一次 %.3fs (帧间隔是 0.25s)",
|
||||
worst);
|
||||
}
|
||||
|
||||
/* 帧率必须受 250ms 限制. 曾经踩过一次: pthread_cond_timedwait 默认按
|
||||
* CLOCK_REALTIME 解释超时, 而传进去的是 CLOCK_MONOTONIC 的绝对时间
|
||||
* (开机时长永远小于 epoch 时间), 于是超时永远"已经过了" —— worker
|
||||
* 变成疯狂重绘的忙循环, 把 pty 刷屏. */
|
||||
static void test_frame_rate_is_bounded(void)
|
||||
{
|
||||
static void test_frame_rate_is_bounded(void) {
|
||||
puts("帧率受 250ms 限制, 不能狂刷");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
if (fd < 0)
|
||||
return;
|
||||
|
||||
spinner_init(1);
|
||||
spinner_start("等待响应");
|
||||
usleep(600 * 1000); /* 0.6 秒 ≈ 2~3 帧 */
|
||||
usleep(600 * 1000); /* 0.6 秒 ≈ 2~3 帧 */
|
||||
spinner_stop();
|
||||
|
||||
char buf[65536];
|
||||
@@ -179,7 +183,8 @@ static void test_frame_rate_is_bounded(void)
|
||||
unlink(path);
|
||||
|
||||
int frames = 0;
|
||||
for (const char *p = buf; (p = strstr(p, "等待响应")); p++) frames++;
|
||||
for (const char *p = buf; (p = strstr(p, "等待响应")); p++)
|
||||
frames++;
|
||||
CHECK(frames >= 1, "至少该画一帧, 实际 %d", frames);
|
||||
CHECK(frames <= 5, "0.6 秒内不该超过 5 帧, 实际 %d (忙循环?)", frames);
|
||||
}
|
||||
@@ -188,18 +193,18 @@ static void test_frame_rate_is_bounded(void)
|
||||
* 自己 —— 而 spinner_start 曾经先执行 g_label[0] = '\0' 再读那个 label, 于是
|
||||
* 标签被自己清空、退化成默认的"思考中". 症状: 每次 HTTP 重试退避之后, 屏幕上
|
||||
* 的"等待响应"都会变成"思考中", 而这两段等待的含义恰恰是它要区分的. */
|
||||
static void test_notice_keeps_label(void)
|
||||
{
|
||||
static void test_notice_keeps_label(void) {
|
||||
puts("notice 之后标签保持不变");
|
||||
char path[64];
|
||||
int saved = -1;
|
||||
int fd = capture_begin(path, sizeof path, &saved);
|
||||
CHECK(fd >= 0, "无法重定向 stderr");
|
||||
if (fd < 0) return;
|
||||
if (fd < 0)
|
||||
return;
|
||||
|
||||
spinner_init(1);
|
||||
spinner_start("自定义标签A");
|
||||
usleep(300 * 1000); /* 让它画至少一帧 */
|
||||
usleep(300 * 1000); /* 让它画至少一帧 */
|
||||
spinner_notice("一条插进来的提示");
|
||||
usleep(300 * 1000);
|
||||
spinner_stop();
|
||||
@@ -217,8 +222,7 @@ static void test_notice_keeps_label(void)
|
||||
noticed);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
test_two_phase();
|
||||
test_idempotent();
|
||||
test_stop_is_prompt();
|
||||
|
||||
+187
-102
@@ -1,31 +1,30 @@
|
||||
/* test_tools.c - 工具层测试 */
|
||||
#include "tools.h"
|
||||
#include "image.h"
|
||||
#include "json.h"
|
||||
#include "tools.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.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"); \
|
||||
} \
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
checks++; \
|
||||
if (!(cond)) { \
|
||||
failures++; \
|
||||
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static char *call(const char *name, const char *args_json, int *is_error)
|
||||
{
|
||||
static char *call(const char *name, const char *args_json, int *is_error) {
|
||||
char err[256];
|
||||
JsonValue *args = json_parse(args_json, err, sizeof err);
|
||||
if (!args) {
|
||||
@@ -37,12 +36,17 @@ static char *call(const char *name, const char *args_json, int *is_error)
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
char tmpl[] = "/tmp/ligent_test_XXXXXX";
|
||||
char *dir = mkdtemp(tmpl);
|
||||
if (!dir) { perror("mkdtemp"); return 2; }
|
||||
if (chdir(dir) != 0) { perror("chdir"); return 2; }
|
||||
if (!dir) {
|
||||
perror("mkdtemp");
|
||||
return 2;
|
||||
}
|
||||
if (chdir(dir) != 0) {
|
||||
perror("chdir");
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* ---- 工具 schema 合法性 ---- */
|
||||
puts("tools_schema");
|
||||
@@ -62,19 +66,22 @@ int main(void)
|
||||
{
|
||||
int has_image = 0;
|
||||
for (size_t i = 0; i < json_array_size(schema); i++) {
|
||||
JsonValue *fn = json_object_get(json_array_get(schema, i), "function");
|
||||
JsonValue *fn =
|
||||
json_object_get(json_array_get(schema, i), "function");
|
||||
const char *nm = json_get_string(fn, "name");
|
||||
if (nm && strcmp(nm, "read_image") == 0) has_image = 1;
|
||||
if (nm && strcmp(nm, "read_image") == 0)
|
||||
has_image = 1;
|
||||
}
|
||||
CHECK(has_image, "schema 里应有 read_image");
|
||||
}
|
||||
{
|
||||
/* line 是集合型参数: schema 里必须是 array (元素 integer),
|
||||
/* line 是集合型参数: schema 里必须是 array (元素 integer),
|
||||
* 否则模型根本不会想到可以给数组 —— 参数表的 "integer[]" 写法
|
||||
* 只有 schema 展开对了才作数 */
|
||||
JsonValue *props = NULL, *reqd = NULL;
|
||||
for (size_t i = 0; i < json_array_size(schema); i++) {
|
||||
JsonValue *fn = json_object_get(json_array_get(schema, i), "function");
|
||||
JsonValue *fn =
|
||||
json_object_get(json_array_get(schema, i), "function");
|
||||
const char *nm = json_get_string(fn, "name");
|
||||
if (nm && strcmp(nm, "edit_file") == 0) {
|
||||
JsonValue *par = json_object_get(fn, "parameters");
|
||||
@@ -86,18 +93,19 @@ int main(void)
|
||||
int line_required = 0;
|
||||
for (size_t i = 0; i < json_array_size(reqd); i++) {
|
||||
const char *nm = json_string_value(json_array_get(reqd, i));
|
||||
if (nm && strcmp(nm, "line") == 0) line_required = 1;
|
||||
if (nm && strcmp(nm, "line") == 0)
|
||||
line_required = 1;
|
||||
}
|
||||
CHECK(!line_required, "line 可以省略 (= 全文件替换), 不该是 required");
|
||||
|
||||
JsonValue *line = props ? json_object_get(props, "line") : NULL;
|
||||
const char *lt = line ? json_get_string(line, "type") : NULL;
|
||||
CHECK(lt && strcmp(lt, "array") == 0, "edit_file 的 line 应是 array, 实际 %s",
|
||||
lt ? lt : "(缺)");
|
||||
CHECK(lt && strcmp(lt, "array") == 0,
|
||||
"edit_file 的 line 应是 array, 实际 %s", lt ? lt : "(缺)");
|
||||
JsonValue *items = line ? json_object_get(line, "items") : NULL;
|
||||
const char *it = items ? json_get_string(items, "type") : NULL;
|
||||
CHECK(it && strcmp(it, "integer") == 0, "line 的元素应是 integer, 实际 %s",
|
||||
it ? it : "(缺)");
|
||||
CHECK(it && strcmp(it, "integer") == 0,
|
||||
"line 的元素应是 integer, 实际 %s", it ? it : "(缺)");
|
||||
}
|
||||
|
||||
/* 序列化一遍确保可发给服务端 */
|
||||
@@ -137,8 +145,10 @@ int main(void)
|
||||
|
||||
/* ---- write_file / read_file ---- */
|
||||
puts("write_file / read_file");
|
||||
r = call("write_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"content\":\"line1\\nline2\\nline3\\n\"}", &err);
|
||||
r = call(
|
||||
"write_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"content\":\"line1\\nline2\\nline3\\n\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "写入应成功: %s", r);
|
||||
CHECK(access("sub/dir/a.txt", R_OK) == 0, "父目录应被自动创建");
|
||||
free(r);
|
||||
@@ -149,7 +159,8 @@ int main(void)
|
||||
CHECK(strstr(r, "1\t") != NULL, "应带行号: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("read_file", "{\"path\":\"sub/dir/a.txt\",\"offset\":2,\"limit\":1}", &err);
|
||||
r = call("read_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"offset\":2,\"limit\":1}", &err);
|
||||
CHECK(strstr(r, "line2") && !strstr(r, "line1") && !strstr(r, "line3"),
|
||||
"offset/limit 应生效: %s", r);
|
||||
free(r);
|
||||
@@ -162,7 +173,8 @@ int main(void)
|
||||
puts("edit_file");
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":2,\"old_string\":\"line2\","
|
||||
"\"new_string\":\"LINE-TWO\"}", &err);
|
||||
"\"new_string\":\"LINE-TWO\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "按行号替换应成功: %s", r);
|
||||
CHECK(strstr(r, "第 2 行") != NULL, "回执里应带上行号: %s", r);
|
||||
free(r);
|
||||
@@ -173,9 +185,11 @@ int main(void)
|
||||
/* 省略 line 不再是错误: 那是"全文件替换"这条逃生门 (见下面那组用例) */
|
||||
|
||||
/* 行号对不上: 必须报出它**真正在哪一行**, 否则模型只能反复猜 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":3,\"old_string\":\"LINE-TWO\","
|
||||
"\"new_string\":\"x\"}", &err);
|
||||
r = call(
|
||||
"edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":3,\"old_string\":\"LINE-TWO\","
|
||||
"\"new_string\":\"x\"}",
|
||||
&err);
|
||||
CHECK(err == 1, "行号不对应报错: %s", r);
|
||||
CHECK(strstr(r, "第 2 行") != NULL, "应指出它真正的行号: %s", r);
|
||||
free(r);
|
||||
@@ -183,7 +197,8 @@ int main(void)
|
||||
/* 越界行号: 要连文件总行数一起说清楚 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":99,\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
"\"new_string\":\"y\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "只有 3 行") != NULL,
|
||||
"越界行号应报错并给出总行数: %s", r);
|
||||
free(r);
|
||||
@@ -192,16 +207,19 @@ int main(void)
|
||||
* (这正是"全文唯一"最烦人的地方——文件一长, 随便一个短串都不唯一) */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":1,\"old_string\":\"line\","
|
||||
"\"new_string\":\"LINE\"}", &err);
|
||||
"\"new_string\":\"LINE\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "同一行里唯一即可, 别的行的重复不该拦: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 同一行里出现两处 → 仍然拒绝 (行内也要定得下来) */
|
||||
r = call("write_file", "{\"path\":\"dup.txt\",\"content\":\"foo foo\\n\"}", &err);
|
||||
r = call("write_file", "{\"path\":\"dup.txt\",\"content\":\"foo foo\\n\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "写入 dup.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_string\":\"bar\"}",
|
||||
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_"
|
||||
"string\":\"bar\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "第 1 行里有 2 处") != NULL,
|
||||
"同一行里多匹配应拒绝: %s", r);
|
||||
@@ -209,8 +227,10 @@ int main(void)
|
||||
|
||||
/* replace_all 仍然换全文件, 但 line 必须指向其中真实的一处 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_string\":\"bar\","
|
||||
"\"replace_all\":true}", &err);
|
||||
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_"
|
||||
"string\":\"bar\","
|
||||
"\"replace_all\":true}",
|
||||
&err);
|
||||
CHECK(err == 0, "replace_all 应成功: %s", r);
|
||||
free(r);
|
||||
r = call("read_file", "{\"path\":\"dup.txt\"}", &err);
|
||||
@@ -218,8 +238,10 @@ int main(void)
|
||||
"replace_all 应换掉文件里全部匹配: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup.txt\",\"line\":7,\"old_string\":\"bar\",\"new_string\":\"x\","
|
||||
"\"replace_all\":true}", &err);
|
||||
"{\"path\":\"dup.txt\",\"line\":7,\"old_string\":\"bar\",\"new_"
|
||||
"string\":\"x\","
|
||||
"\"replace_all\":true}",
|
||||
&err);
|
||||
CHECK(err == 1, "replace_all 也必须锚定在真实的一行上: %s", r);
|
||||
free(r);
|
||||
|
||||
@@ -228,14 +250,16 @@ int main(void)
|
||||
* "每行都改一次": 真改动的仍是唯一那一处. */
|
||||
puts("edit_file: line 集合");
|
||||
r = call("write_file",
|
||||
"{\"path\":\"set.txt\",\"content\":\"aaa\\nbbb\\nccc\\nddd\\n\"}", &err);
|
||||
"{\"path\":\"set.txt\",\"content\":\"aaa\\nbbb\\nccc\\nddd\\n\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "写入 set.txt: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 数组写法: 起点落在集合里的哪一行都算命中; 回执要带上整个集合 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[1,2],\"old_string\":\"aaa\","
|
||||
"\"new_string\":\"AAA\"}", &err);
|
||||
"\"new_string\":\"AAA\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "集合圈住的替换应成功: %s", r);
|
||||
CHECK(strstr(r, "第 1、2 行") != NULL, "回执里应带上整个集合: %s", r);
|
||||
free(r);
|
||||
@@ -244,40 +268,48 @@ int main(void)
|
||||
free(r);
|
||||
|
||||
/* old_string 跨行时, 把覆盖到的几行都填进集合——不必赌起点是第几行 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[2,3],\"old_string\":\"bbb\\nccc\","
|
||||
"\"new_string\":\"B\\nC\"}", &err);
|
||||
r = call(
|
||||
"edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[2,3],\"old_string\":\"bbb\\nccc\","
|
||||
"\"new_string\":\"B\\nC\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "跨行替换 (集合给两行)应成功: %s", r);
|
||||
free(r);
|
||||
r = call("read_file", "{\"path\":\"set.txt\"}", &err);
|
||||
CHECK(strstr(r, "B") && strstr(r, "C") && !strstr(r, "bbb"), "跨行替换应落盘: %s", r);
|
||||
CHECK(strstr(r, "B") && strstr(r, "C") && !strstr(r, "bbb"),
|
||||
"跨行替换应落盘: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 集合里有多处匹配 → 仍然拒绝 (集合不是"每行改一次"的授权) */
|
||||
r = call("write_file", "{\"path\":\"two.txt\",\"content\":\"foo\\nx\\nfoo\\n\"}", &err);
|
||||
r = call("write_file",
|
||||
"{\"path\":\"two.txt\",\"content\":\"foo\\nx\\nfoo\\n\"}", &err);
|
||||
CHECK(err == 0, "写入 two.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"two.txt\",\"line\":[1,3],\"old_string\":\"foo\","
|
||||
"\"new_string\":\"bar\"}", &err);
|
||||
"\"new_string\":\"bar\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "第 1、3 行里有 2 处") != NULL,
|
||||
"集合里多处匹配应拒绝: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 重复的行号要去重: 不然同一处匹配会被数成两处 */
|
||||
r = call("write_file", "{\"path\":\"dup3.txt\",\"content\":\"foo\\n\"}", &err);
|
||||
r = call("write_file", "{\"path\":\"dup3.txt\",\"content\":\"foo\\n\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "写入 dup3.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup3.txt\",\"line\":[1,1],\"old_string\":\"foo\","
|
||||
"\"new_string\":\"bar\"}", &err);
|
||||
"\"new_string\":\"bar\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "重复行号应去重, 而不是当成两处: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 集合里有一行越界 → 和单个行号时一样, 报出文件总行数 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[1,99],\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
"\"new_string\":\"y\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "只有 4 行") != NULL,
|
||||
"集合里的越界行号应报错并给出总行数: %s", r);
|
||||
free(r);
|
||||
@@ -285,25 +317,30 @@ int main(void)
|
||||
/* 写法本身不对的三种: 空数组、非整数元素、一次给太多 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[],\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
"\"new_string\":\"y\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "空数组") != NULL, "空数组应报错: %s", r);
|
||||
CHECK(strstr(r, "省略") != NULL, "空数组的报错要提示可以整个省略 line: %s", r);
|
||||
CHECK(strstr(r, "省略") != NULL, "空数组的报错要提示可以整个省略 line: %s",
|
||||
r);
|
||||
free(r);
|
||||
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[1,1.5],\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
"\"new_string\":\"y\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "第 2 个") != NULL,
|
||||
"非整数元素应报错并指出是第几个: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- 省略 line = 全文件替换所有匹配 ----
|
||||
* 这是 range 那样的集合参数该有的默认值: 不给范围就是不设范围. */
|
||||
r = call("write_file", "{\"path\":\"all.txt\",\"content\":\"k\\nx\\nk\\n\"}", &err);
|
||||
r = call("write_file",
|
||||
"{\"path\":\"all.txt\",\"content\":\"k\\nx\\nk\\n\"}", &err);
|
||||
CHECK(err == 0, "写入 all.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"all.txt\",\"old_string\":\"k\",\"new_string\":\"K\"}", &err);
|
||||
"{\"path\":\"all.txt\",\"old_string\":\"k\",\"new_string\":\"K\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "省略 line 应做全文件替换: %s", r);
|
||||
CHECK(strstr(r, "全文件替换") != NULL && strstr(r, "2 处") != NULL,
|
||||
"回执要说清是全文件替换、换了几处: %s", r);
|
||||
@@ -312,29 +349,39 @@ int main(void)
|
||||
CHECK(strstr(r, "K") != NULL && !strstr(r, "k"), "两处都该被换掉: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 省略 line 时文件里没有 old_string: 只说整个文件里没有 (没有"哪几行"可说) */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"all.txt\",\"old_string\":\"zzz\",\"new_string\":\"y\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "整个文件里没有") != NULL, "该报整个文件里没有: %s", r);
|
||||
/* 省略 line 时文件里没有 old_string: 只说整个文件里没有 (没有"哪几行"可说)
|
||||
*/
|
||||
r = call(
|
||||
"edit_file",
|
||||
"{\"path\":\"all.txt\",\"old_string\":\"zzz\",\"new_string\":\"y\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "整个文件里没有") != NULL,
|
||||
"该报整个文件里没有: %s", r);
|
||||
free(r);
|
||||
|
||||
/* line 给错了类型 (不是"想全局"): 仍然当场报错, 并告诉它可以省略 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"all.txt\",\"line\":\"1\",\"old_string\":\"K\","
|
||||
"\"new_string\":\"z\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "整数或整数数组") != NULL, "line 类型错应报错: %s", r);
|
||||
"\"new_string\":\"z\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "整数或整数数组") != NULL,
|
||||
"line 类型错应报错: %s", r);
|
||||
CHECK(strstr(r, "整个省略") != NULL, "报错要指出可以省略 line: %s", r);
|
||||
free(r);
|
||||
|
||||
{
|
||||
/* 65 个行号: 超过一次能圈住的范围, 要明说 */
|
||||
char big[600];
|
||||
size_t o = (size_t)snprintf(big, sizeof big, "{\"path\":\"set.txt\",\"line\":[");
|
||||
size_t o = (size_t)snprintf(big, sizeof big,
|
||||
"{\"path\":\"set.txt\",\"line\":[");
|
||||
for (int i = 1; i <= 65; i++)
|
||||
o += (size_t)snprintf(big + o, sizeof big - o, "%s%d", i > 1 ? "," : "", i);
|
||||
snprintf(big + o, sizeof big - o, "],\"old_string\":\"x\",\"new_string\":\"y\"}");
|
||||
o += (size_t)snprintf(big + o, sizeof big - o, "%s%d",
|
||||
i > 1 ? "," : "", i);
|
||||
snprintf(big + o, sizeof big - o,
|
||||
"],\"old_string\":\"x\",\"new_string\":\"y\"}");
|
||||
r = call("edit_file", big, &err);
|
||||
CHECK(err == 1 && strstr(r, "一次最多") != NULL, "行号过多应报错: %s", r);
|
||||
CHECK(err == 1 && strstr(r, "一次最多") != NULL, "行号过多应报错: %s",
|
||||
r);
|
||||
free(r);
|
||||
}
|
||||
|
||||
@@ -348,21 +395,23 @@ int main(void)
|
||||
CHECK(err == 1, "不存在的目录应报错: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 这两个工具都已经删掉: 搜索交给模型自己用 run_command 里的真 grep,
|
||||
/* 这两个工具都已经删掉: 搜索交给模型自己用 run_command 里的真 grep,
|
||||
* 子代理 (spawn_subagent)整个功能也去掉了. 留着断言是为了钉住
|
||||
* "不要再冒出一个半吊子的替代品". */
|
||||
r = call("grep", "{\"pattern\":\"x\",\"path\":\".\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有 grep 工具: %s", r);
|
||||
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有 grep 工具: %s",
|
||||
r);
|
||||
free(r);
|
||||
r = call("spawn_subagent", "{}", &err);
|
||||
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有子代理工具: %s", r);
|
||||
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有子代理工具: %s",
|
||||
r);
|
||||
free(r);
|
||||
|
||||
/* ---- 超大文件不能把内存吃干 ---- */
|
||||
puts("超大文件防护");
|
||||
{
|
||||
/* 稀疏文件, 创建几乎不耗时; 关键是客户端要在读之前就发现它太大.
|
||||
* 开头 16 KB 写成 'x': 这样它是"文本" (探测的前 8 KB 里没有 NUL),
|
||||
/* 稀疏文件, 创建几乎不耗时; 关键是客户端要在读之前就发现它太大.
|
||||
* 开头 16 KB 写成 'x': 这样它是"文本" (探测的前 8 KB 里没有 NUL),
|
||||
* 否则连 read_file 都会先被二进制检测拦掉 —— 这个用例就测不到
|
||||
* "分段读大文件"了. */
|
||||
FILE *big = fopen("huge.txt", "wb");
|
||||
@@ -376,7 +425,8 @@ int main(void)
|
||||
fclose(big);
|
||||
}
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"huge.txt\",\"line\":1,\"old_string\":\"x\",\"new_string\":\"y\"}",
|
||||
"{\"path\":\"huge.txt\",\"line\":1,\"old_string\":\"x\",\"new_"
|
||||
"string\":\"y\"}",
|
||||
&err);
|
||||
CHECK(err == 1, "超大文件的 edit_file 应被拒绝: %.120s", r);
|
||||
CHECK(strstr(r, "上限") != NULL, "应说明是超上限: %.120s", r);
|
||||
@@ -389,12 +439,16 @@ int main(void)
|
||||
unlink("huge.txt");
|
||||
}
|
||||
|
||||
/* ---- 二进制文件: 所有处理文本的工具都必须拒绝 (而且要说明该用什么) ---- */
|
||||
/* ---- 二进制文件: 所有处理文本的工具都必须拒绝 (而且要说明该用什么) ----
|
||||
*/
|
||||
puts("二进制拒绝");
|
||||
{
|
||||
FILE *bf = fopen("bin.dat", "wb");
|
||||
CHECK(bf != NULL, "创建二进制测试文件失败");
|
||||
if (bf) { fwrite("PNG\0\1\2\3", 1, 8, bf); fclose(bf); }
|
||||
if (bf) {
|
||||
fwrite("PNG\0\1\2\3", 1, 8, bf);
|
||||
fclose(bf);
|
||||
}
|
||||
}
|
||||
|
||||
r = call("read_file", "{\"path\":\"bin.dat\"}", &err);
|
||||
@@ -404,9 +458,11 @@ int main(void)
|
||||
free(r);
|
||||
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"bin.dat\",\"line\":1,\"old_string\":\"PNG\",\"new_string\":\"JPG\"}",
|
||||
"{\"path\":\"bin.dat\",\"line\":1,\"old_string\":\"PNG\",\"new_"
|
||||
"string\":\"JPG\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "二进制") != NULL, "edit_file 改二进制应被拒绝: %s", r);
|
||||
CHECK(err == 1 && strstr(r, "二进制") != NULL,
|
||||
"edit_file 改二进制应被拒绝: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("write_file", "{\"path\":\"bin.dat\",\"content\":\"text\"}", &err);
|
||||
@@ -420,30 +476,35 @@ int main(void)
|
||||
char keep[16];
|
||||
memset(keep, 0, sizeof keep);
|
||||
size_t kn = bf ? fread(keep, 1, sizeof keep, bf) : 0;
|
||||
if (bf) fclose(bf);
|
||||
if (bf)
|
||||
fclose(bf);
|
||||
CHECK(kn == 8 && memcmp(keep, "PNG\0\1\2\3", 8) == 0,
|
||||
"被拒绝的读/写不能改动二进制文件的字节");
|
||||
}
|
||||
|
||||
/* 确认提示里要能看出改的是哪一行 */
|
||||
r = call("write_file", "{\"path\":\"confirm.txt\",\"content\":\"aaa\\nbbb\\n\"}", &err);
|
||||
r = call("write_file",
|
||||
"{\"path\":\"confirm.txt\",\"content\":\"aaa\\nbbb\\n\"}", &err);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"confirm.txt\",\"line\":2,\"old_string\":\"bbb\",\"new_string\":\"ccc\"}",
|
||||
"{\"path\":\"confirm.txt\",\"line\":2,\"old_string\":\"bbb\","
|
||||
"\"new_string\":\"ccc\"}",
|
||||
&err);
|
||||
CHECK(err == 0 && strstr(r, "第 2 行") != NULL, "回执带行号: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- 哪些工具会改动磁盘 (决定是否弹确认) ---- */
|
||||
puts("写操作分类");
|
||||
CHECK(tool_mutating("run_command") == 1, "run_command 会执行命令, 必须确认");
|
||||
CHECK(tool_mutating("run_command") == 1,
|
||||
"run_command 会执行命令, 必须确认");
|
||||
CHECK(tool_mutating("write_file") == 1, "write_file 会写盘, 必须确认");
|
||||
CHECK(tool_mutating("edit_file") == 1, "edit_file 会改盘, 必须确认");
|
||||
CHECK(tool_mutating("read_file") == 0, "read_file 只读, 不该打扰");
|
||||
CHECK(tool_mutating("list_dir") == 0, "list_dir 只读, 不该打扰");
|
||||
/* read_image 会在 ligent 自己的目录里留备份 (副作用),
|
||||
/* read_image 会在 ligent 自己的目录里留备份 (副作用),
|
||||
* 但不改用户的任何东西 —— 它是"读", 不该每次都拦一道 */
|
||||
CHECK(tool_mutating("read_image") == 0, "read_image 不改用户的文件, 不该打扰");
|
||||
CHECK(tool_mutating("read_image") == 0,
|
||||
"read_image 不改用户的文件, 不该打扰");
|
||||
CHECK(tool_mutating("没有这个工具") == 0, "未知工具按只读处理");
|
||||
CHECK(tool_mutating(NULL) == 0, "空名字不应崩");
|
||||
|
||||
@@ -458,18 +519,21 @@ int main(void)
|
||||
CHECK(bdir != NULL, "建备份临时目录失败");
|
||||
if (bdir) {
|
||||
image_set_bindir(bdir);
|
||||
image_pending_take(); /* 清干净, 以免上一段残留 */
|
||||
image_pending_take(); /* 清干净, 以免上一段残留 */
|
||||
|
||||
/* 1) 内容是文本、扩展名骗人 */
|
||||
FILE *f = fopen("notanimage.png", "wb");
|
||||
if (f) { fputs("这不是图片", f); fclose(f); }
|
||||
if (f) {
|
||||
fputs("这不是图片", f);
|
||||
fclose(f);
|
||||
}
|
||||
r = call("read_image", "{\"path\":\"notanimage.png\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "不认识") != NULL,
|
||||
"非图片内容应被拒: %s", r);
|
||||
CHECK(strstr(r, "按文件内容判断") != NULL, "应说明是看内容判断的");
|
||||
free(r);
|
||||
|
||||
/* 2) 体积超限: base64 后超过 IMAGE_MAX_B64.
|
||||
/* 2) 体积超限: base64 后超过 IMAGE_MAX_B64.
|
||||
* 4/3 * 原始大小 = 编码后大小, 所以原始要 > 3/4 * 上限. */
|
||||
size_t raw_needed = (size_t)(IMAGE_MAX_B64 / 4 * 3) + 4096;
|
||||
f = fopen("huge.png", "wb");
|
||||
@@ -479,7 +543,8 @@ int main(void)
|
||||
size_t left = raw_needed - 8;
|
||||
while (left > 0) {
|
||||
size_t chunk = left > 65536 ? 65536 : left;
|
||||
if (fwrite(pad, 1, chunk, f) != chunk) break;
|
||||
if (fwrite(pad, 1, chunk, f) != chunk)
|
||||
break;
|
||||
left -= chunk;
|
||||
}
|
||||
free(pad);
|
||||
@@ -493,7 +558,11 @@ int main(void)
|
||||
|
||||
/* 3) 合法的小图: 成功, 并且挂上待发附件 */
|
||||
f = fopen("ok.png", "wb");
|
||||
if (f) { fwrite("\x89PNG\r\n\x1a\n", 1, 8, f); fwrite("payload", 1, 7, f); fclose(f); }
|
||||
if (f) {
|
||||
fwrite("\x89PNG\r\n\x1a\n", 1, 8, f);
|
||||
fwrite("payload", 1, 7, f);
|
||||
fclose(f);
|
||||
}
|
||||
r = call("read_image", "{\"path\":\"ok.png\"}", &err);
|
||||
CHECK(err == 0 && strstr(r, "已读入图片") != NULL,
|
||||
"合法图片应成功: %s", r);
|
||||
@@ -504,14 +573,20 @@ int main(void)
|
||||
CHECK(att && json_array_size(att) == 1, "应挂上 1 个待发附件");
|
||||
if (att && json_array_size(att) == 1)
|
||||
CHECK(strcmp(json_get_string(json_array_get(att, 0), "type"),
|
||||
"ligent_image") == 0, "附件应是内部引用块");
|
||||
"ligent_image") == 0,
|
||||
"附件应是内部引用块");
|
||||
json_free(att);
|
||||
|
||||
/* 只有成功那一张留了备份 —— 两条拒绝路径都不该留下东西 */
|
||||
char *ls = xasprintf("ls %s/ligent.archive/images/*.png | wc -l", bdir);
|
||||
char *ls =
|
||||
xasprintf("ls %s/ligent.archive/images/*.png | wc -l", bdir);
|
||||
FILE *p = popen(ls, "r");
|
||||
int cnt = -1;
|
||||
if (p) { if (fscanf(p, "%d", &cnt) != 1) cnt = -1; pclose(p); }
|
||||
if (p) {
|
||||
if (fscanf(p, "%d", &cnt) != 1)
|
||||
cnt = -1;
|
||||
pclose(p);
|
||||
}
|
||||
free(ls);
|
||||
CHECK(cnt == 1, "只该留 1 份备份 (成功那张), 实际 %d", cnt);
|
||||
|
||||
@@ -522,13 +597,15 @@ int main(void)
|
||||
/* ---- 超长单行: read_file 报出的行号必须与 edit_file 的口径一致 ----
|
||||
*
|
||||
* read_file 以前用 fgets + 固定 64 KB 缓冲, 一行比缓冲长时会被切成"多行"、
|
||||
* 每块各带一个行号. 于是模型按 read_file 看到的行号去 edit_file 改, 要么改错
|
||||
* 地方、要么报越界 —— 而它没有任何办法察觉. 工具描述和 README 都写着
|
||||
* "read_file 打出来的行号可以直接喂给 edit_file 的 line", 这里把它钉住. */
|
||||
* 每块各带一个行号. 于是模型按 read_file 看到的行号去 edit_file 改,
|
||||
* 要么改错 地方、要么报越界 —— 而它没有任何办法察觉. 工具描述和 README
|
||||
* 都写着 "read_file 打出来的行号可以直接喂给 edit_file 的 line",
|
||||
* 这里把它钉住. */
|
||||
puts("超长单行的行号口径");
|
||||
{
|
||||
FILE *lf = fopen("longline.txt", "wb");
|
||||
for (int i = 0; i < 70000; i++) fputc('a', lf); /* 跨过 64 KB 那块缓冲 */
|
||||
for (int i = 0; i < 70000; i++)
|
||||
fputc('a', lf); /* 跨过 64 KB 那块缓冲 */
|
||||
fputc('\n', lf);
|
||||
fputs("second line\nthird line\n", lf);
|
||||
fclose(lf);
|
||||
@@ -537,17 +614,22 @@ int main(void)
|
||||
CHECK(err == 0, "读超长行文件失败: %s", r);
|
||||
CHECK(strstr(r, " 1\ta") != NULL, "第 1 行应以 a 开头:\n%.120s", r);
|
||||
CHECK(strstr(r, " 2\tsecond line") != NULL,
|
||||
"超长行之后的第 2 行应是 second line (它不能被算成好几行): \n%.200s", r);
|
||||
CHECK(strstr(r, " 3\tthird line") != NULL, "第 3 行应是 third line");
|
||||
"超长行之后的第 2 行应是 second line (它不能被算成好几行): "
|
||||
"\n%.200s",
|
||||
r);
|
||||
CHECK(strstr(r, " 3\tthird line") != NULL,
|
||||
"第 3 行应是 third line");
|
||||
CHECK(strstr(r, " 4\t") == NULL, "不应该出现第 4 行");
|
||||
free(r);
|
||||
|
||||
/* 拿 read_file 报出的行号去 edit_file 改, 必须命中同一处 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"longline.txt\",\"line\":3,"
|
||||
"\"old_string\":\"third line\",\"new_string\":\"THIRD LINE\"}", &err);
|
||||
"\"old_string\":\"third line\",\"new_string\":\"THIRD LINE\"}",
|
||||
&err);
|
||||
CHECK(err == 0, "用 read_file 的行号改第 3 行失败: %s", r);
|
||||
CHECK(strstr(r, "第 3 行替换 1 处") != NULL, "回执应说清改的是第 3 行: %s", r);
|
||||
CHECK(strstr(r, "第 3 行替换 1 处") != NULL,
|
||||
"回执应说清改的是第 3 行: %s", r);
|
||||
free(r);
|
||||
char *after = read_entire_file("longline.txt", NULL);
|
||||
CHECK(after && strstr(after, "THIRD LINE") != NULL, "改动应落到文件里");
|
||||
@@ -556,7 +638,8 @@ int main(void)
|
||||
/* 单行本身就超过输出上限: 必须说清"这一行只给了开头", 不能说成
|
||||
* "已截断, 从下一行继续" —— 那会让调用方以为拿到的是完整的行. */
|
||||
FILE *hf = fopen("hugeline.txt", "wb");
|
||||
for (int i = 0; i < 200000; i++) fputc('b', hf);
|
||||
for (int i = 0; i < 200000; i++)
|
||||
fputc('b', hf);
|
||||
fputc('\n', hf);
|
||||
fputs("tail\n", hf);
|
||||
fclose(hf);
|
||||
@@ -571,7 +654,8 @@ int main(void)
|
||||
|
||||
/* ---- 空目录: qsort 的第一个参数会拿到 NULL ---- */
|
||||
puts("list_dir 空目录");
|
||||
if (mkdir("emptydir", 0755) != 0) { /* 可能已存在 */ }
|
||||
if (mkdir("emptydir", 0755) != 0) { /* 可能已存在 */
|
||||
}
|
||||
r = call("list_dir", "{\"path\":\"emptydir\"}", &err);
|
||||
CHECK(err == 0 && strstr(r, "0 个条目") != NULL,
|
||||
"空目录应报 0 个条目, 实际 %s", r);
|
||||
@@ -586,7 +670,8 @@ int main(void)
|
||||
/* ---- 输出截断 ---- */
|
||||
puts("output truncation");
|
||||
r = call("run_command",
|
||||
"{\"command\":\"head -c 300000 /dev/zero | tr '\\\\0' 'a'\"}", &err);
|
||||
"{\"command\":\"head -c 300000 /dev/zero | tr '\\\\0' 'a'\"}",
|
||||
&err);
|
||||
CHECK(strlen(r) <= LIGENT_TOOL_OUTPUT_LIMIT + 200,
|
||||
"输出应被截断, 实际 %zu 字节", strlen(r));
|
||||
CHECK(strstr(r, "截断") != NULL, "应提示已截断");
|
||||
|
||||
Reference in new issue
Block a user