Files
ligent/tests/test_image.c
T
2026-10-03 14:13:21 +08:00

319 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_image.c - 图片格式嗅探、本地备份、base64、请求前展开 */
#include "image.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, ...) \
do { \
checks++; \
if (!(cond)) { \
failures++; \
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
printf(__VA_ARGS__); \
printf("\n"); \
} \
} while (0)
static 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)
{
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");
CHECK(image_sniff((const unsigned char *)JPEG, 7, &ext, &mime) == 0 &&
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");
CHECK(image_sniff((const unsigned char *)GIF89, 10, &ext, &mime) == 0 &&
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");
/* 认不出来要明确失败,别猜一个格式报上去 */
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,
"RIFF 但不是 WEBP 不该被认成 WebP");
CHECK(image_sniff(NULL, 0, &ext, &mime) != 0, "NULL 不该崩");
}
static void test_backup(void)
{
puts("本地备份:命名、内容、不重复");
char tmpl[] = "/tmp/ligent_img_XXXXXX";
char *dir = mkdtemp(tmpl);
CHECK(dir != NULL, "建临时目录失败");
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; }
/* 名字形如 <上传时间戳>_<4位随机>.<ext> */
CHECK(strlen(b1) > 5, "名字不该太短:%s", b1);
char *dot = strrchr(b1, '.');
CHECK(dot && strcmp(dot, ".png") == 0, "扩展名应从嗅探结果来:%s", b1);
CHECK(dot - b1 >= 7, "时间戳 + 下划线 + 4 位随机:%s", b1);
CHECK(b1[dot - b1 - 5] == '_', "下划线位置应在随机后缀前:%s", b1);
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);
/* 再存一次应该拿到不同的名字(随机后缀),而不是互相覆盖 */
char *b2 = image_backup_save(dir, data, n, "png");
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) { /* 清理失败不影响断言 */ }
free(cmd);
}
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" },
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
size_t n = strlen(cases[i].in);
char *got = base64_encode((const unsigned char *)cases[i].in, n);
CHECK(got && strcmp(got, cases[i].want) == 0,
"\"%s\" 应编码成 %s,实际 %s", cases[i].in, cases[i].want,
got ? got : "(null)");
CHECK(base64_encoded_len(n) == strlen(cases[i].want),
"长度函数应与实际一致(%zu)", n);
free(got);
}
/* 二进制字节(0x00 / 0xFF 这类不能被当字符串处理) */
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)");
free(b);
}
/* 造一张内容合法(靠文件头就能嗅探出来)的假图,返回字节数 */
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;
memcpy(out, magic, m);
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)
{
JsonValue *msgs = json_new_array();
JsonValue *sys = json_new_object();
json_object_set_string(sys, "role", "system");
json_object_set_string(sys, "content", "你是助手");
json_array_push(msgs, sys);
JsonValue *part = json_new_object();
json_object_set_string(part, "type", ref_message_type);
json_object_set_string(part, "backup", backup);
JsonValue *content = json_new_array();
json_array_push(content, part);
JsonValue *um = json_new_object();
json_object_set_string(um, "role", "user");
json_object_set(um, "content", content);
json_array_push(msgs, um);
return msgs;
}
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;
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; }
/* --- 正常展开 --- */
JsonValue *msgs = history_with_ref(backup);
int degraded = -1;
JsonValue *wire = image_expand_for_request(msgs, &degraded);
CHECK(wire && json_array_size(wire) == 2, "副本消息条数应与原历史一致");
CHECK(degraded == 0, "正常展开不该有降级");
JsonValue *wu = json_array_get(wire, 1);
JsonValue *wc = json_object_get(wu, "content");
CHECK(wc && wc->type == JSON_ARRAY && json_array_size(wc) == 1,
"附件消息应有一个 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"));
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 段应能解回原字节 —— 这里只验长度与首尾,避免在测试里再写一个解码器 */
CHECK(url && strlen(url) == 22 + base64_encoded_len(n),
"data URL 长度应等于前缀 + base64 长度");
/* 关键不变量:原始历史没被改动,且里面没有图片字节 */
char *orig = json_dump(msgs);
CHECK(strstr(orig, "ligent_image") != NULL, "原历史里应还是引用");
CHECK(strstr(orig, "base64") == NULL, "原历史里不该出现 base64");
CHECK(strstr(orig, backup) != NULL, "原历史里应记着备份名");
free(orig);
json_free(wire);
/* --- 备份没了:就地降级 --- */
char *bpath = image_backup_path(dir, backup);
CHECK(unlink(bpath) == 0, "删掉备份文件");
free(bpath);
degraded = 0;
wire = image_expand_for_request(msgs, &degraded);
CHECK(degraded == 1, "读不到备份应降级 1 处");
JsonValue *wc2 = json_object_get(json_array_get(wire, 1), "content");
JsonValue *wp2 = json_array_get(wc2, 0);
CHECK(strcmp(json_get_string(wp2, "type"), "text") == 0,
"发送形态应降级成 text 块");
const char *dt = json_get_string(wp2, "text");
CHECK(dt && strstr(dt, "无法交给模型") != NULL, "应说明原因,实际 %s",
dt ? dt : "(null)");
json_free(wire);
/* 降级是**就地**的:原历史里的坏引用应该已经被换掉,否则每轮都会重试 */
char *after = json_dump(msgs);
CHECK(strstr(after, "ligent_image") == NULL, "原历史里的坏引用应已被清掉");
CHECK(strstr(after, "ligent_note") != NULL, "应换成内部说明块");
free(after);
/* 再来一次不该重复降级(历史已经干净了) */
degraded = 0;
wire = image_expand_for_request(msgs, &degraded);
CHECK(degraded == 0, "第二次不该再降级");
json_free(wire);
/* image_has_refs:降级之后应为 0,这正是 4xx 兜底的判据 */
CHECK(image_has_refs(msgs) == 0, "降级后历史里不该还有图片引用");
json_free(msgs);
/* --- 4xx 兜底:image_degrade_all --- */
JsonValue *m2 = history_with_ref(backup);
CHECK(image_has_refs(m2) == 1, "有引用时应报 1");
int dn = image_degrade_all(m2);
CHECK(dn == 1, "应降级 1 处,实际 %d", dn);
CHECK(image_has_refs(m2) == 0, "降级后不该还有引用");
JsonValue *c2 = json_object_get(json_array_get(m2, 1), "content");
const char *t2 = json_get_string(json_array_get(c2, 0), "text");
/* 给模型的话必须是能直接照做的中文,而不是服务端的英文报错 */
CHECK(t2 && strstr(t2, "不符合 API 所需的格式标准") != NULL,
"应说明格式不符,实际 %s", t2 ? t2 : "(null)");
CHECK(t2 && strstr(t2, "请规范化图片") != NULL, "应给出下一步动作");
CHECK(t2 && strchr(t2, '\n') == NULL, "说明不该带换行");
json_free(m2);
/* 没有引用时不该动历史 */
JsonValue *plain = json_new_array();
JsonValue *pm = json_new_object();
json_object_set_string(pm, "role", "user");
json_object_set_string(pm, "content", "你好");
json_array_push(plain, pm);
CHECK(image_has_refs(plain) == 0, "纯文本历史不该报有引用");
CHECK(image_degrade_all(plain) == 0, "纯文本历史不该被改");
JsonValue *pw = image_expand_for_request(plain, NULL);
CHECK(pw && json_array_size(pw) == 1, "纯文本历史应能原样复制");
json_free(pw);
json_free(plain);
free(backup);
image_set_bindir(".");
char *cmd = xasprintf("rm -rf %s", tmpl);
if (system(cmd) != 0) { }
free(cmd);
}
static void test_pending(void)
{
puts("待发附件:取走即清空");
image_pending_take(); /* 先清干净 */
image_pending_add("1700000000_aaaa.png");
image_pending_add("1700000001_bbbb.png");
JsonValue *arr = image_pending_take();
CHECK(arr && json_array_size(arr) == 2, "应取到 2 个块");
if (arr && json_array_size(arr) == 2) {
JsonValue *p0 = json_array_get(arr, 0);
CHECK(strcmp(json_get_string(p0, "type"), "ligent_image") == 0,
"类型应是内部引用块,不是协议块");
CHECK(strcmp(json_get_string(p0, "backup"), "1700000000_aaaa.png") == 0,
"第一个备份名");
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, "第二个备份名");
}
json_free(arr);
CHECK(image_pending_take() == NULL, "取走之后应该空了");
image_pending_add(NULL);
image_pending_add("");
CHECK(image_pending_take() == NULL, "空备份名不该被记下");
}
int main(void)
{
test_sniff();
test_backup();
test_base64();
test_expand();
test_pending();
printf("\n%d 项检查,%d 项失败\n", checks, failures);
return failures ? 1 : 0;
}