Files
MeowOS-For-R3P2/patch_v2/reader_app/test/shelf_index_smoke.c
T
AskaEth 58f2d9bff4 perf(reader): M32 r2 书架索引改 level-1 校验 — 真机冷缓存首开 3.47s→0.13–0.16s(r1 全目录校验被真机证伪)
r1 用宿主 1.1µs/stat + M28 的**热** stat 150µs 外推「434 次≈65ms」,真机 drop_caches 后
实测是 3.15s(7.26ms/次),与 readdir 的 3.0s 同量级:全目录校验的索引命中冷启动仍要
3.06s,只比全扫快 0.2s。「3.0s→0.15s」在冷启动不成立。按任务 1 自己定的判据
(stat 接近 1s 就退回 level-1)改为只校验 Books 根 + 每个一级目录。

真机冷缓存实测(exFAT /usr/data/mnt/sd_0,434 目录/646 文件/595 本;sync + drop_caches;
探针走 /proc/uptime,UI 走 meow_ui_time_ms):
- 索引命中首开:**127 / 138 / 157ms**(复测 147ms),热 dentry 107–120ms;
- 全量扫描首开:**3428 / 3477ms**(复测 3711ms),热 dentry 不变(readdir 不吃 page cache);
- 探针冷分项:索引载入 20–30ms;载入+rdprog×595 70–120ms;4 个目录 stat 0–30ms;
- 从阅读页返回书架 29–31ms(M29 语义未变)。
→ 冷启动 22–26 倍提升,方案值得做;索引实测 **63,579B**(v2;r1 全目录 97,490B)。

实现(格式 v2,magic MEOWIDX2 / version 2):
- writer 回调过滤 rel 含 '/' 的目录:只落 root + 一级目录的 mtime;书/大小/格式记录仍全量;
  level-2+ 不落盘、载入不 stat。载入仍然 0 次 opendir/readdir(strace:0 getdents64,
  真实库 4 次 stat;宿主模拟库 11 次)。
- 校验记录里出现 level-2+ rel 视为坏档;sum(books_here) 改为「≤ nbooks」合理性上界。
- fail-open/原子写/CRC/安全加固/ts 不缓存全部保留(r1 已过评审的部分未动)。

失效矩阵(报文逐条写清):
自动(根 mtime):Books 根下直接增/删/改名书;一级目录新增/删除/改名。
自动(一级目录 mtime):一级目录里直接增/删/改名书、新建子目录。
必须刷新:level-2 及更深处的任何变化(只改深层目录 mtime)。刷新=删索引+全扫。
真机 exFAT mtime 实测 **1 秒**(tm_nsec=0;同秒连续增删 mtime 不变)。我复测 10 轮
「目录刚改过就扫描、同秒再改」→ 9/10 仍命中(陈旧),与评审 29/30 一致;sleep 跨秒后
全部正常失效。头注释已把「10ms」改为「1 秒」,并写明「不在同一秒刻度的变更靠索引失效,
同秒二次变更需刷新」。

bench 修复(P3-3):MEOW_BENCH_IDX=only —— **不预先 load/scan**,直接进书架量生产首开;
miss —— 先删索引再进书架。两者配合 drop_caches 即冷启动口径。旧行为(先 bench_index/
bench_scan 焐热)保留为默认模式。新增 test/shelf_index_probe_mipsel(write/scan/load/
loadts/statpaths/mtime/size)用于真机分项与 mtime 粒度验证,不进发行包。

测试:shelf_index_smoke 170 pass(新增:level-3/level-2 内变更「索引仍命中且先看到旧
列表 → 刷新后可见」、新建 level-2 目录自动失效、一级目录直接删书自动失效、v2 ndirs=
根+一级数);ASan+UBSan 干净;build_reader.sh 0 warning,core 6846 / seg 19 / chapter 71 /
shelf_group 63 / index 170 / font 14 / loaders PASS。真机 6 场景逐项验证:一级直接增/
新建子目录/一级改名/根下删书/一级直接删书 = 自动失效;level-3 深处新增 = 仍命中(需刷新)。

交付:app.json 0.1.5/code6 → **0.1.6/code7**;reader md5 5c848c99… 1,711,988B;
mpk md5 10d676f5… 814,574B;未签名不 push。设备验证后已清理 /tmp 与 m32probe,
真实 MeowData 的 shelf.idx 已删(app 自己的章节 .idx 未动),Books 434/646 不变,
已装 0.1.4 md5 4036b889… 未变。
2026-11-10 18:49:48 +08:00

488 lines
22 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/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 <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);
}
/* 读索引 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); /* 索引落 <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;
}