M29 只解决了「回书架不重扫」;首次进书架仍要全量扫 434 目录(真机 3.0–3.4s, 其中 opendir+readdir 占 96%)。本单把扫描结果 + 每个扫描目录的 mtime 存到 MeowData/os.meow.reader/index/shelf.idx,冷启动逐目录 stat 比对 mtime,命中即 完全跳过 readdir;任何异常一律 fail-open 全量重扫。 先量再选(任务 1):宿主 434 目录/595 本/51 rar 虚拟库(贴真机量级) - 434 次目录 stat = 0.48ms/pass(1.1µs/次,不含 readdir); - 读 72KB 索引 = 0.009ms;全量扫描 3.54ms;索引命中 1.29ms(ts_fn=NULL)。 真机按 M28 实测 1079 次 stat=160–170ms(≈150µs/次)外推:434 次 ≈ 65ms, 远低于「可能不可行」的 1s 阈值 → 选「校验全部扫描目录 mtime」(非只 level-1)。 strace 断言:indexonly 载入 0 次 getdents64、恰好 434 次 stat(真的没 readdir)。 生产路径 ≈ 65ms(stat) + <20ms(索引读) + 40–50ms(rdprog×595) + <20ms(build_rows) ≈ 0.13–0.15s(对比 3.0s)。 索引格式 v1(手工小端序列化,不依赖结构体布局): header(magic/version/header_size/root_len) + root + dir 记录×(rel,mtime_s, mtime_ns,books_here) + book 记录×(rel,size,fmt) + trailer(ndirs,nbooks,flags, depth_skipped,crc32,magic)。truncated/depth_skipped 存在 flags/字段里,载入时 如实重建提示条;**不存阅读进度 ts**,载入后每本由 rdprog_load 现读再排序。 失效语义(存每个扫描过的目录,不只 level-1): - level-2 新增 / level-3 深处新增 / 删书 → 该目录 mtime 变 → 重扫; - 一级目录改名 → 父目录 mtime 变 + 旧目录 stat ENOENT → 重扫; - 超深(>6 层)未扫描子树不在索引内,其内部变化不自动失效(本来就不显示), 点「刷新」更新 depth_skipped 计数;mtime 精度受限(exFAT 10ms/FAT32 2s)的 同刻度变更也可能漏判,点「刷新」必可纠正。 M29 语义不变:首次进书架=索引或全扫;回书架=不扫;刷新=删索引+全扫(之后原子 写回新索引)。 安全/健壮(宁可慢不可错): - 写:shelf.idx.tmp 全量写完(尽力 fsync)后 rename;中途被杀只剩 tmp,正式索引 要么旧要么完整新;rename 覆盖失败先删旧再改名;fflush/写错即放弃,绝不影响书架。 - 读:O_NOFOLLOW|O_NONBLOCK + S_ISREG(FIFO/符号链接直接拒绝不阻塞);CRC32 覆盖 全部记录;rel 做绝对路径/“..”/内嵌 NUL/长度校验;版本/根路径/计数/逐目录 mtime 任一不符即重扫;载入崩溃路径全部 goto 清理,不改书架可用性。 - writer_open 失败(卡满/写保护/无 MeowData)→ 扫描不采集目录、照常出书; commit 失败(含只读目录 rename 失败)不留正式索引。 测试:test/shelf_index_smoke 154 pass(含四种变更场景失效、截断/字节翻转/垃圾/ 缺失/FIFO/符号链接/版本不符/根路径不符、ts 现读、truncated/depth_skipped 重建、 只读目录写失败、空 Books、root 缺失、.tmp 残渣),ASan+UBSan 干净; test/shelf_index_bench 模拟 434 目录/595 本/51 rar:scan/load 均 595 本、 truncated=0/depth_skipped=0、索引 72,156B。build_reader.sh 全绿 0 warning (core 6846 / seg 19 / chapter 71 / shelf_group 63 / index 154 / font 14 / loaders PASS)。shelf_bench_ondev 增加 index_load/scan_save 计时与 MEOW_BENCH_IDX=miss(真机首开命中 vs 未命中对比,待有卡复核)。 交付:app.json 0.1.4/code5 → 0.1.5/code6;reader md5 df4cadcf 1,711,924B;mpk md5 fec7b1bb 814,535B;未签名不推卡。
427 lines
18 KiB
C
427 lines
18 KiB
C
/* test/shelf_index_smoke.c — M32 书架索引宿主单测(纯 C,无设备/SDK 依赖)。
|
||
*
|
||
* 覆盖:
|
||
* I1 全量扫描 → 写索引 → 载入:书数/书名/分组/path/fmt/size/ts 全部与扫描一致
|
||
* I2 载入不缓存阅读进度:同一索引换 ts_fn 再载入,ts 立刻反映新值(含真实 rdprog 文件)
|
||
* I3 四种书库变更都必须失效(fail-open 重扫):
|
||
* level-2 新增 / level-3 深处新增 / level-1 目录改名 / 删书
|
||
* 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/types.h>
|
||
#include <unistd.h>
|
||
|
||
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);
|
||
}
|
||
|
||
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);
|
||
|
||
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:四种变更都必须让索引失效。每次变更后:先断言 load=0(不静默用陈旧索引),
|
||
* 再全量重扫 + 写新索引,断言变化可见。 */
|
||
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 直接新增 */
|
||
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,
|
||
"level-2 新增 → 索引失效");
|
||
int n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
|
||
CHECK(find_title(b, n, "新书") != NULL, "重扫后新书可见");
|
||
|
||
/* ② level-3 深处新增(Books/400+.../01、玄幻/01、斗破/) */
|
||
usleep(20000);
|
||
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) == 0,
|
||
"level-3 深处新增 → 索引失效");
|
||
n = scan_and_save(root, b, SHELF_MAX_BOOKS, &rep);
|
||
CHECK(find_title(b, n, "新书2") != 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,
|
||
"重扫后分组键跟随新目录名");
|
||
|
||
/* ④ 删书 */
|
||
usleep(20000);
|
||
snprintf(p, sizeof p, "%s/根下.txt", 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, "根下") == NULL, "重扫后删掉的书消失");
|
||
}
|
||
|
||
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);
|
||
|
||
/* 版本不符:先把版本字段改成 2 */
|
||
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] = { 2, 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); /* 索引落 <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;
|
||
}
|