/* test_archive.c - 存档命名规则与读写往返 (不需要网络) */ #include "archive.h" #include "util.h" #include #include #include #include #include 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; }