/* test/shelf_index_smoke.c — M32 书架索引宿主单测(纯 C,无设备/SDK 依赖)。 * * 覆盖: * I1 全量扫描 → 写索引 → 载入:书数/书名/分组/path/fmt/size/ts 全部与扫描一致 * I2 载入不缓存阅读进度:同一索引换 ts_fn 再载入,ts 立刻反映新值(含真实 rdprog 文件) * I3 v2 校验范围 = Books 根 + 一级目录: * 自动失效:一级目录直接增/删书、新建 level-2 目录、一级目录改名、根下增删书; * 必须刷新:level-3 深处新增、已有 level-2 目录内新增(索引仍命中,先看到旧列表) * I4 坏档 fail-open:截断 / 中间字节翻转 / 垃圾内容 / 文件缺失 / 索引路径是目录 / * 版本不符 / Books 根路径不符 —— 一律返回 0,不崩 * I5 truncated / depth_skipped 如实重建(索引里存了这两个值) * I6 写盘失败不影响书架:索引目录不可写时 writer_open 返回 NULL、扫描照常出书; * commit 失败不留下半截索引 * I7 空 Books / Books 不存在:空索引可载入;root_missing 不写索引 * I8 载入路径不做 readdir(由 build 验证步骤用 strace 另行断言) */ #include "shelf_model.h" #include "shelf_index.h" #include "progress.h" #include #include #include #include #include #include static int g_pass = 0, g_fail = 0; #define CHECK(cond, msg) do { \ if (cond) g_pass++; \ else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } \ } while (0) static char g_cfgdir[400]; const char *meow_config_dir(void) { return g_cfgdir; } static void mkdirs(const char *path) { char t[400]; snprintf(t, sizeof t, "%s", path); for (char *p = t + 1; *p; p++) { if (*p == '/') { *p = 0; mkdir(t, 0755); *p = '/'; } } mkdir(t, 0755); } static void write_file(const char *path, const char *body) { FILE *f = fopen(path, "wb"); if (!f) { printf("无法创建 %s\n", path); exit(2); } fputs(body, f); fclose(f); } /* 进度按文件名给(I2:换 ts_fn 必须立即生效,证明索引没缓存 ts)。 */ static uint64_t g_ts_boost; static uint64_t ts_of(const char *path) { const char *b = strrchr(path, '/'); b = b ? b + 1 : path; if (strcmp(b, "1.txt") == 0) return 100 + g_ts_boost; if (strcmp(b, "2.epub") == 0) return 50; return 0; } /* 真实进度文件口径(I2 第二段)。 */ static uint64_t prog_ts_of(const char *path) { rdprog p; return (rdprog_load(path, &p) == 1) ? p.ts : 0; } static const shelf_book *find_path(const shelf_book *books, int n, const char *path) { for (int i = 0; i < n; i++) if (strcmp(books[i].path, path) == 0) return &books[i]; return NULL; } static const shelf_book *find_title(const shelf_book *books, int n, const char *title) { for (int i = 0; i < n; i++) if (strcmp(books[i].title, title) == 0) return &books[i]; return NULL; } static int idx_path(const char *root, char *out, int outsz) { return shelf_index_path(root, out, outsz); } /* 读索引 trailer 的 ndirs(v2 应 = 1 + 一级目录数)。 */ static uint32_t idx_trailer_ndirs(const char *root) { char ip[500]; if (idx_path(root, ip, sizeof ip) != 0) return 0; FILE *f = fopen(ip, "rb"); if (!f) return 0; unsigned char tr[24]; if (fseek(f, -(long)sizeof tr, SEEK_END) != 0 || fread(tr, 1, sizeof tr, f) != sizeof tr) { fclose(f); return 0; } fclose(f); return (uint32_t)tr[0] | ((uint32_t)tr[1] << 8) | ((uint32_t)tr[2] << 16) | ((uint32_t)tr[3] << 24); } static int scan_and_save(const char *root, shelf_book *books, int cap, shelf_scan_report *rep) { shelf_idx_writer *w = shelf_index_writer_open(root); CHECK(w != NULL, "writer_open 成功"); int n = shelf_scan_books_ex(root, books, cap, ts_of, rep, w ? shelf_index_writer_dir : NULL, w); if (w) CHECK(shelf_index_writer_commit(w, books, n, rep) == 1, "writer_commit 成功"); return n; } /* 建一棵贴合真机布局的树(含 level-3 深处、超深目录、不支持格式、空目录)。 */ static void make_tree(const char *root) { char p[400]; mkdirs(root); snprintf(p, sizeof p, "%s/轻小说", root); mkdirs(p); snprintf(p, sizeof p, "%s/玄幻", root); mkdirs(p); snprintf(p, sizeof p, "%s/B/sub", root); mkdirs(p); snprintf(p, sizeof p, "%s/D/E/F", root); mkdirs(p); snprintf(p, sizeof p, "%s/G/H/I/J/K/L/M", root); mkdirs(p); snprintf(p, sizeof p, "%s/400+本高质量完本合集/01、玄幻/01、斗破", root); mkdirs(p); snprintf(p, sizeof p, "%s/空目录", root); mkdirs(p); snprintf(p, sizeof p, "%s/轻小说/1.txt", root); write_file(p, "a"); snprintf(p, sizeof p, "%s/轻小说/2.epub", root); write_file(p, "bb"); snprintf(p, sizeof p, "%s/轻小说/notes.md", root); write_file(p, "not a book"); snprintf(p, sizeof p, "%s/玄幻/3.txt", root); write_file(p, "ccc"); snprintf(p, sizeof p, "%s/根下.txt", root); write_file(p, "root book"); snprintf(p, sizeof p, "%s/400+本高质量完本合集/01、玄幻/01、斗破/《斗破》.txt", root); write_file(p, "doupo"); snprintf(p, sizeof p, "%s/B/sub/5.mobi", root); write_file(p, "mobi"); snprintf(p, sizeof p, "%s/D/E/F/6.txt", root); write_file(p, "deep3"); snprintf(p, sizeof p, "%s/G/H/I/J/K/L/M/7.txt", root); write_file(p, "too deep"); } static void test_roundtrip(const char *root) { static shelf_book scanb[SHELF_MAX_BOOKS], loadb[SHELF_MAX_BOOKS]; shelf_scan_report r1, r2; int n = scan_and_save(root, scanb, SHELF_MAX_BOOKS, &r1); CHECK(n == 7, "扫描到 7 本(7.txt 超深不计)"); CHECK(r1.depth_skipped == 1 && r1.truncated == 0, "深度超限计数=1"); char ip[500]; CHECK(idx_path(root, ip, sizeof ip) == 0, "索引路径可得"); struct stat st; CHECK(stat(ip, &st) == 0 && st.st_size > 0, "索引文件已落盘"); printf("index: %s (%lld bytes)\n", ip, (long long)st.st_size); /* v2:只落根 + 一级目录(本树一级 = 轻小说/玄幻/B/D/G/400+合集/空目录 → 7,加根 = 8)。 * 这个数直接决定载入时的 stat 次数(真机冷缓存 4 次≈20ms,434 次=3.15s)。 */ CHECK(idx_trailer_ndirs(root) == 8, "v2 目录记录 = Books 根 + 7 个一级目录"); int n2 = shelf_index_load(root, loadb, SHELF_MAX_BOOKS, ts_of, &r2); CHECK(n2 == 1, "索引命中"); CHECK(r2.nbooks == n && r2.truncated == r1.truncated && r2.depth_skipped == r1.depth_skipped && r2.root_missing == 0, "报告重建一致"); for (int i = 0; i < n; i++) { const shelf_book *b = find_path(loadb, r2.nbooks, scanb[i].path); CHECK(b != NULL, "载入含同一 path"); if (!b) continue; CHECK(strcmp(b->title, scanb[i].title) == 0, "title 一致"); CHECK(strcmp(b->group, scanb[i].group) == 0, "group 一致"); CHECK(strcmp(b->top, scanb[i].top) == 0, "top 一致"); CHECK(strcmp(b->sub, scanb[i].sub) == 0, "sub 一致"); CHECK(strcmp(b->sub_folder, scanb[i].sub_folder) == 0, "sub_folder 一致"); CHECK(b->fmt == scanb[i].fmt && b->size == scanb[i].size, "fmt/size 一致"); CHECK(b->ts == scanb[i].ts, "ts 一致"); } /* 排序:最近阅读优先 */ CHECK(r2.nbooks >= 2 && strcmp(loadb[0].title, "1") == 0, "载入后按 ts 排序(1.txt 100 在最前)"); } /* I2:索引不缓存 ts —— 同一份索引,换 ts_fn 载入必须立即反映新值。 */ static void test_ts_not_cached(const char *root) { static shelf_book b[SHELF_MAX_BOOKS]; shelf_scan_report rep; g_ts_boost = 0; CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) == 7, "重新扫描写索引"); g_ts_boost = 900; static shelf_book lb[SHELF_MAX_BOOKS]; CHECK(shelf_index_load(root, lb, SHELF_MAX_BOOKS, ts_of, &rep) == 1, "索引命中"); const shelf_book *x = find_title(lb, rep.nbooks, "1"); CHECK(x && x->ts == 1000, "载入后 ts 用当前 ts_fn(100+900),不是索引里的旧值"); CHECK(x && strstr(x->sub, "读") != NULL, "副题随新 ts 重建(· 读)"); g_ts_boost = 0; /* 真实进度文件:写 rdprog 后同一索引载入,ts/副题立即更新。 */ char path[400]; snprintf(path, sizeof path, "%s/根下.txt", root); rdprog p; memset(&p, 0, sizeof p); p.valid = 1; p.seg = 2; p.ch = 9; p.fs = 20; p.ls = 130; p.enc = -1; p.ts = 777777; CHECK(rdprog_save(path, &p) == 0, "写入测试进度"); CHECK(shelf_index_load(root, lb, SHELF_MAX_BOOKS, prog_ts_of, &rep) == 1, "索引命中(真实进度)"); const shelf_book *y = find_title(lb, rep.nbooks, "根下"); CHECK(y && y->ts == 777777, "ts 来自 rdprog_load 现读"); /* 删掉进度文件(注意 rdprog_save 的参数是书路径,进度文件另按 hash 落盘)。 */ char pdir[400], phash[24], ppath[440]; if (rdprog_dir(pdir, sizeof pdir) == 0) { rdprog_hash(path, phash, sizeof phash); snprintf(ppath, sizeof ppath, "%s/%s", pdir, phash); unlink(ppath); } } /* I3(v2 = 只校验 Books 根 + 一级目录): * 自动失效(load 返回 0 → 全扫): * ① 一级目录里直接新增书(一级目录 mtime 变) * ② 一级目录里新建子目录(一级目录 mtime 变) * ③ 一级目录改名(Books 根 mtime 变 + 旧目录 stat 失败) * ④ Books 根下删书(根 mtime 变) * ⑤ 一级目录里直接删书(一级目录 mtime 变) * 必须点「刷新」(load 仍命中,先看到旧列表): * ⑥ level-3 深处新增(只改更深目录的 mtime) * ⑦ 已有 level-2 目录里新增(只改 level-2 的 mtime) * * 注意:宿主 ext4/f2fs 的 mtime 有纳秒精度,usleep(20000) 足以拉开刻度;**真机 exFAT 的 * 目录 mtime 只有 1 秒精度**(实测 tm_nsec=0),同秒内二次改动不会变——本测试无法模拟 * 那个真机窗口(见 shelf_index.h 的 mtime 精度说明)。 */ static void test_invalidation(const char *root) { static shelf_book b[SHELF_MAX_BOOKS]; shelf_scan_report rep; char p[400], q[400]; CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "基线扫描写索引"); /* ① 一级目录里直接新增(level-2 相对 Books 是直接子项) */ usleep(20000); snprintf(p, sizeof p, "%s/轻小说/新书.txt", root); write_file(p, "new"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "一级目录直接新增 → 索引失效"); int n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep); CHECK(find_title(b, n, "新书") != NULL, "重扫后新书可见"); /* ② 一级目录里新建 level-2 子目录(一级目录 mtime 变)→ 自动失效 */ usleep(20000); snprintf(p, sizeof p, "%s/轻小说/新子目录", root); mkdirs(p); snprintf(p, sizeof p, "%s/轻小说/新子目录/深书.txt", root); write_file(p, "deep"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "新建 level-2 目录 → 索引失效"); n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep); CHECK(find_title(b, n, "深书") != NULL, "重扫后新子目录里的书可见"); /* ③ 一级目录改名 */ usleep(20000); snprintf(p, sizeof p, "%s/玄幻", root); snprintf(q, sizeof q, "%s/玄幻改", root); CHECK(rename(p, q) == 0, "改名一级目录"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "一级目录改名 → 索引失效"); n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep); const shelf_book *m = find_title(b, n, "3"); CHECK(m && strcmp(m->top, "玄幻改") == 0 && strcmp(m->group, "玄幻改") == 0, "重扫后分组键跟随新目录名"); /* ④ Books 根下删书(根 mtime 变) */ usleep(20000); snprintf(p, sizeof p, "%s/根下.txt", root); CHECK(unlink(p) == 0, "删除 Books 根下的一本书"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "根下删书 → 索引失效"); n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep); CHECK(find_title(b, n, "根下") == NULL, "重扫后删掉的书消失"); /* ⑤ 一级目录里直接删书(一级目录 mtime 变) */ usleep(20000); snprintf(p, sizeof p, "%s/轻小说/2.epub", root); CHECK(unlink(p) == 0, "删除一级目录里直接放的书"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "一级目录直接删书 → 索引失效"); n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep); CHECK(find_title(b, n, "2") == NULL, "重扫后该书消失"); /* ⑥ level-3 深处新增:**不自动失效**(level-1 与根 mtime 都没变)→ 需刷新 */ snprintf(p, sizeof p, "%s/400+本高质量完本合集/01、玄幻/01、斗破/新书2.txt", root); write_file(p, "new2"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 1, "level-3 深处新增 → 索引仍命中(预期:需刷新)"); CHECK(find_title(b, rep.nbooks, "新书2") == NULL, "命中时看不到深处新书(陈旧直到刷新)"); shelf_index_remove(root); /* 顶栏「刷新」= 删索引 + 全扫 */ n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep); CHECK(find_title(b, n, "新书2") != NULL, "刷新后深处新书可见"); /* ⑦ 已有 level-2 目录里新增:**不自动失效** → 需刷新 */ snprintf(p, sizeof p, "%s/B/sub/新书3.txt", root); write_file(p, "new3"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 1, "level-2 目录内新增 → 索引仍命中(预期:需刷新)"); CHECK(find_title(b, rep.nbooks, "新书3") == NULL, "命中时看不到 level-2 新书(陈旧直到刷新)"); shelf_index_remove(root); n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep); CHECK(find_title(b, n, "新书3") != NULL, "刷新后 level-2 新书可见"); } static void test_bad_index(const char *root) { static shelf_book b[SHELF_MAX_BOOKS]; shelf_scan_report rep; char ip[500]; CHECK(idx_path(root, ip, sizeof ip) == 0, "索引路径可得"); CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "基线扫描写索引"); /* 截断:去掉 tail 10 字节(trailer 没了) */ struct stat st; CHECK(stat(ip, &st) == 0, "索引存在"); CHECK(truncate(ip, st.st_size - 10) == 0, "截断索引"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "截断 → 拒绝"); /* 中间字节翻转:CRC 必须抓到 */ CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "重写索引"); FILE *f = fopen(ip, "r+b"); CHECK(f != NULL, "打开索引改写"); if (f) { CHECK(fseek(f, st.st_size / 2, SEEK_SET) == 0, "定位中部"); int c = fgetc(f); CHECK(c != EOF, "读中部字节"); CHECK(fseek(f, st.st_size / 2, SEEK_SET) == 0, "回定位"); fputc(c ^ 0x01, f); fclose(f); } CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "字节翻转 → 拒绝"); /* 垃圾内容 */ write_file(ip, "this is not a shelf index at all, just garbage garbage garbage"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "垃圾内容 → 拒绝"); /* 文件缺失 */ unlink(ip); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "文件缺失 → 拒绝"); /* FIFO:O_NONBLOCK 必须直接拒绝(不能挂死在 open 上) */ CHECK(mkfifo(ip, 0644) == 0, "建 FIFO 假索引"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "FIFO → 拒绝且不阻塞"); unlink(ip); /* 符号链接:O_NOFOLLOW 只认普通文件,不跟随 */ CHECK(symlink("/etc/hostname", ip) == 0, "建符号链接假索引"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "符号链接 → 拒绝"); unlink(ip); /* 索引路径是目录 */ mkdir(ip, 0755); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "索引是目录 → 拒绝"); rmdir(ip); /* 扫描中途被杀留下的 .tmp 残渣:绝不能影响正式索引的载入 */ CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "重写索引"); char tmpip[520]; snprintf(tmpip, sizeof tmpip, "%s.tmp", ip); write_file(tmpip, "half written index leftover"); CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 1, ".tmp 残渣不影响载入"); unlink(tmpip); /* 版本不符:先把版本字段改成 3 */ CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "重写索引"); f = fopen(ip, "r+b"); CHECK(f != NULL, "打开索引改版本"); if (f) { unsigned char v[4] = { 3, 0, 0, 0 }; CHECK(fseek(f, 8, SEEK_SET) == 0, "定位版本字段"); fwrite(v, 1, sizeof v, f); fclose(f); } CHECK(shelf_index_load(root, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "版本不符 → 拒绝"); /* Books 根路径不符(同长度不同路径) */ CHECK(scan_and_save(root, b, SHELF_MAX_BOOKS, &rep) > 0, "重写索引"); char root2[400]; snprintf(root2, sizeof root2, "%s", root); root2[strlen(root2) - 1] = 'z'; /* Books → Bookz */ CHECK(shelf_index_load(root2, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "根路径不符 → 拒绝"); } /* I5:cap 截断 + 超深计数都要从索引如实重建。 */ static void test_report_rebuild(const char *root) { static shelf_book b[SHELF_MAX_BOOKS]; shelf_scan_report r1; int n = scan_and_save(root, b, 3, &r1); CHECK(n == 3 && r1.truncated == 1, "cap=3 扫描截断标记"); CHECK(shelf_index_load(root, b, 3, ts_of, &r1) == 1, "截断索引仍可命中"); CHECK(r1.truncated == 1 && r1.nbooks == 3, "截断标记如实重建"); } /* I6:索引不可写 → 扫描照常;commit 失败无残留。 */ static void test_write_failure(const char *root) { static shelf_book b[SHELF_MAX_BOOKS]; shelf_scan_report rep; char ip[500]; CHECK(idx_path(root, ip, sizeof ip) == 0, "索引路径可得"); char idir[500]; snprintf(idir, sizeof idir, "%s", ip); char *slash = strrchr(idir, '/'); if (slash) *slash = 0; mkdirs(idir); if (geteuid() == 0) { printf("(skip 写失败用例:root 不受权限位限制)\n"); return; } CHECK(chmod(idir, 0500) == 0, "索引目录改为只读"); shelf_idx_writer *w = shelf_index_writer_open(root); CHECK(w == NULL, "只读目录 → writer_open 返回 NULL(不崩)"); /* 生产路径:w=NULL → 扫描不采集目录,仍必须出书。 */ int n = shelf_scan_books_ex(root, b, SHELF_MAX_BOOKS, ts_of, &rep, NULL, NULL); CHECK(n > 0 && rep.root_missing == 0, "索引不可写时书架扫描照常"); CHECK(chmod(idir, 0755) == 0, "恢复目录权限"); /* commit 失败:打开 writer(tmp 已建)后再把目录改只读 → rename 失败 → 返回 0 且无残留。 */ unlink(ip); /* 先清掉前面用例留下的正式索引,才能断言「失败后不存在」 */ w = shelf_index_writer_open(root); CHECK(w != NULL, "可写时 writer_open 成功"); if (w) { CHECK(chmod(idir, 0500) == 0, "提交前目录改只读"); int navail = shelf_scan_books_ex(root, b, SHELF_MAX_BOOKS, ts_of, &rep, shelf_index_writer_dir, w); int ok = shelf_index_writer_commit(w, b, navail, &rep); CHECK(ok == 0, "rename 失败 → commit 返回 0"); CHECK(chmod(idir, 0755) == 0, "恢复目录权限"); struct stat st; CHECK(stat(ip, &st) != 0, "commit 失败不留正式索引"); } } /* I7:空 Books 可缓存;root 不存在不写索引。 */ static void test_empty_and_missing(const char *root) { static shelf_book b[SHELF_MAX_BOOKS]; shelf_scan_report rep; char empty[400]; snprintf(empty, sizeof empty, "%s_empty/Books", root); mkdirs(empty); int n = scan_and_save(empty, b, SHELF_MAX_BOOKS, &rep); CHECK(n == 0 && rep.root_missing == 0, "空 Books 扫描 0 本"); CHECK(shelf_index_load(empty, b, SHELF_MAX_BOOKS, ts_of, &rep) == 1, "空索引可命中"); CHECK(rep.nbooks == 0, "空索引 0 本"); char missing[400]; snprintf(missing, sizeof missing, "%s_nope/Books", root); CHECK(shelf_index_load(missing, b, SHELF_MAX_BOOKS, ts_of, &rep) == 0, "根不存在 → 拒绝"); /* writer_open 只依赖 App 数据目录(不 stat Books):根不存在时也能打开, * 但扫描不到任何目录(ndirs=0)→ commit 必须失败且不发布半截索引。 */ shelf_scan_report rm; shelf_idx_writer *w = shelf_index_writer_open(missing); int mn = shelf_scan_books_ex(missing, b, SHELF_MAX_BOOKS, ts_of, &rm, w ? shelf_index_writer_dir : NULL, w); CHECK(mn == 0 && rm.root_missing == 1, "根不存在 → root_missing"); if (w) CHECK(shelf_index_writer_commit(w, b, mn, &rm) == 0, "根不存在 → commit 拒绝空索引"); } int main(void) { const char *base = getenv("TMPDIR"); if (!base || !base[0]) base = "/tmp"; char tmpl[400]; snprintf(tmpl, sizeof tmpl, "%s/m32_index_XXXXXX", base); char *tmp = mkdtemp(tmpl); if (!tmp) { printf("mkdtemp 失败\n"); return 2; } snprintf(g_cfgdir, sizeof g_cfgdir, "%s/data", tmp); /* 索引落 /index */ mkdirs(g_cfgdir); char root[400]; snprintf(root, sizeof root, "%s/Books", tmp); make_tree(root); test_roundtrip(root); test_ts_not_cached(root); test_invalidation(root); test_bad_index(root); test_report_rebuild(root); test_write_failure(root); test_empty_and_missing(root); printf("shelf_index_smoke: %d pass, %d fail\n", g_pass, g_fail); char cmd[512]; snprintf(cmd, sizeof cmd, "rm -rf '%s' '%s_empty' '%s_nope'", tmp, root, root); if (system(cmd) != 0) { /* 忽略清理失败 */ } return g_fail ? 1 : 0; }