Files
ligent/tests/test_archive.c
T
2026-10-03 14:56:56 +08:00

179 lines
7.8 KiB
C

/* test_archive.c - 存档命名规则与读写往返 (不需要网络) */
#include "archive.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 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)
{
char tmpl[] = "/tmp/ligent_arch_XXXXXX";
char *dir = mkdtemp(tmpl);
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(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);
free(n1);
/* 空字符串与 NULL 等价: 都表示"没有来源" */
char *n2 = NULL;
CHECK(archive_next_name(dir, "", NULL, 0, &n2, err, sizeof err) == 0, "空 lineage 应可用");
CHECK(n2 && strlen(n2) == ARCHIVE_ROOT_LEN, "空 lineage 也应给出根名");
free(n2);
puts("分支名: 来源 + _ + 3 位");
char *b = NULL;
CHECK(archive_next_name(dir, "a1b2c3d4", NULL, 0, &b, err, sizeof err) == 0,
"分支生成失败: %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);
free(b);
/* 多次生成应当不同 (随机性) */
char *x1 = NULL, *x2 = NULL;
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);
puts("长度上限: 接不下就重新起根");
/* 边界: 来源长度 + 1 + 3 + ".json" <= 240 */
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(edge && strncmp(edge, "aaa", 3) == 0, "刚好装得下时应继续接分支");
CHECK(edge && strlen(edge) + strlen(".json") <= ARCHIVE_NAME_MAX,
"完整文件名不应超上限: %zu", edge ? strlen(edge) + 5 : 0);
free(edge);
/* 再多一位就装不下了 */
char *too = xmalloc(maxlineage + 2);
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(fresh && strlen(fresh) == ARCHIVE_ROOT_LEN,
"超限时应退回 %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(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(tr && strncmp(tr, "tmp_", 4) == 0 &&
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(edgep && strlen(edgep) == 4 + ARCHIVE_ROOT_LEN,
"加了前缀装不下时应退回根名, 实际 %s (%zu 位)",
edgep ? edgep : "(null)", edgep ? strlen(edgep) : 0);
free(edgep);
free(ok); free(too);
puts("导出与载入往返");
JsonValue *msgs = json_new_array();
JsonValue *m = json_new_object();
json_object_set_string(m, "role", "user");
json_object_set_string(m, "content", "你好, 暗号是紫水晶");
json_array_push(msgs, m);
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,
"导出失败: %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);
CHECK(back != NULL, "载入失败: %s", err);
CHECK(back && json_array_size(back) == 1, "消息条数应为 1");
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)");
json_free(back); json_free(msgs);
free(created); free(saved); free(model); free(iname); free(iparent);
puts("列举");
ArchiveEntry *list = NULL;
size_t n = archive_list(dir, &list);
CHECK(n == 1, "应有 1 份存档, 实际 %zu", n);
if (n == 1) {
CHECK(strncmp(list[0].name, "parentname_", 11) == 0, "名字应保留");
CHECK(list[0].count == 1, "条数应为 1, 实际 %ld", list[0].count);
CHECK(list[0].created != NULL, "时间应从内容里读出来");
}
archive_list_free(list, n);
/* 非 ligent 存档 (没有 format 标记)应被忽略 */
char *jd = xasprintf("%s/junk.json", dir);
FILE *j2 = fopen(jd, "w");
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(path); free(path); free(name);
rmdir(dir);
printf("\n%d 项检查, %d 项失败\n", checks, failures);
return failures ? 1 : 0;
}