图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(M18)
用户反馈四件事一起改: - 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路 (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 + .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片= mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。 - 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内 缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期 生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。 2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次 (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。 - 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏 「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。 - 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态 「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。 集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/ 普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比 mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。 reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push, 走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。 回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
This commit is contained in:
1 parent
d07b99056f
commit
64336f95d9
16 files changed
+940
-1927
No files matched your search
+90
-306
@@ -1,24 +1,22 @@
|
||||
/* test_browse_lazy.c — 单层探测 / 懒判定 / mtime 失效缓存 / 行合成(纯 C,无 SDK)。
|
||||
/* test_browse_lazy.c — 浏览行模型 / 零 IO 行合成 / 图片下标一致性(纯 C,无 SDK)。
|
||||
*
|
||||
* 覆盖:
|
||||
* L1 单层探测:直接图片数、自然序首图、空目录标记;不递归(只有子目录含图的目录不算书);
|
||||
* L2 懒判定接线:scan 后行全部 PENDING(占位副标题)→ apply_probe 后页数/首图/副标题/
|
||||
* 书标记;空目录「空目录」;无图目录「无图片」;探测失败「无法读取」且视为非书;
|
||||
* L3 缓存:mtime 失效(同 mtime 命中不读盘;目录变化后重扫并刷新结果);并发调用线程安全;
|
||||
* L3b 评审 P2:探测期间 mtime/size 变化的结果不写缓存(避免「不完整结果绑定新 mtime」);
|
||||
* L4 行合成 O(1):mr_library_row_at 纯内存(IO 计数不变)、截断提示行;
|
||||
* L5 M7 兼容语义:「直接含图兄弟册」+ 自然序册间顺序(M7 mr_book_adjacent 的口径)。
|
||||
* 文件名沿用历史(旧的「懒判定」已随 M18 文件管理器模型删除);本测试现在覆盖:
|
||||
* L1 行合成 O(1):mr_library_row_at 纯内存(IO 计数不变)、截断提示行;
|
||||
* L2 **书库→阅读器下标记号契约**:mr_library_image_index 数出的下标 == 阅读器目录册
|
||||
* (logic/reader/page_source.c: mr_dir_book_init)里同一张图的下标。
|
||||
* 夹具含大小写混排(A.jpg/a.jpg)、数字序号(img2/img10)、大写扩展名(IMG3.JPG)、
|
||||
* 前导零(page01/page1)、夹在图片之间的档案册与普通文件;
|
||||
* L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
* test/test_browse_lazy.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
* test/test_browse_lazy.c logic/library.c logic/reader/page_source.c \
|
||||
* -pthread -o build/host/test_browse_lazy
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader/page_source.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -81,255 +79,47 @@ static void joinp(char *out, int outsz, const char *a, const char *b)
|
||||
snprintf(out, (size_t)outsz, "%s/%s", a, b);
|
||||
}
|
||||
|
||||
/* 把目录 mtime 显式推后 3 秒(避免同秒内变化被缓存误命中)。 */
|
||||
static void bump_mtime(const char *path)
|
||||
static long long now_us(void)
|
||||
{
|
||||
struct timespec ts[2];
|
||||
ts[0].tv_sec = time(NULL) + 3;
|
||||
ts[0].tv_nsec = 0;
|
||||
ts[1] = ts[0];
|
||||
utimensat(AT_FDCWD, path, ts, 0);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL;
|
||||
}
|
||||
|
||||
static const mr_library_entry *find_entry(const mr_library *lib, const char *name)
|
||||
/* 阅读器目录册用的假解码回调(本测试不打开任何页)。 */
|
||||
static meow_img *fake_load(const char *path, void *ud, int maxdim)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i];
|
||||
(void)path; (void)ud; (void)maxdim;
|
||||
return NULL;
|
||||
}
|
||||
static void fake_unload(meow_img *img) { (void)img; }
|
||||
|
||||
static int find_index(const mr_library *lib, const char *name, int *out)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) { *out = i; return 1; }
|
||||
*out = -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- L1 + L2 ---- */
|
||||
static void test_probe_and_apply(const char *root)
|
||||
{
|
||||
char book[MR_PATH_MAX], noimg[MR_PATH_MAX], empty[MR_PATH_MAX], dirs_only[MR_PATH_MAX];
|
||||
char deep[MR_PATH_MAX];
|
||||
joinp(book, sizeof book, root, "book");
|
||||
joinp(noimg, sizeof noimg, root, "noimg");
|
||||
joinp(empty, sizeof empty, root, "empty");
|
||||
joinp(dirs_only, sizeof dirs_only, root, "dirs_only");
|
||||
joinp(deep, sizeof deep, root, "dirs_only/deep");
|
||||
|
||||
mr_library_probe p;
|
||||
CHECK(mr_library_probe_dir(book, &p) == 0, "probe book");
|
||||
CHECK(p.pages == 2, "book pages=%d", p.pages);
|
||||
CHECK(p.empty == 0, "book not empty");
|
||||
CHECK(strstr(p.cover, "page2.png") != NULL, "cover natural-first: %s", p.cover);
|
||||
|
||||
CHECK(mr_library_probe_dir(noimg, &p) == 0 && p.pages == 0 && p.empty == 0,
|
||||
"noimg: 0 pages, not empty");
|
||||
CHECK(mr_library_probe_dir(empty, &p) == 0 && p.pages == 0 && p.empty == 1,
|
||||
"empty dir flagged");
|
||||
CHECK(mr_library_probe_dir(dirs_only, &p) == 0 && p.pages == 0 && p.empty == 0,
|
||||
"dirs_only has no direct images (no recursion)");
|
||||
CHECK(mr_library_probe_dir(deep, &p) == 0 && p.pages == 1, "deep is a book");
|
||||
|
||||
char slash[MR_PATH_MAX];
|
||||
snprintf(slash, sizeof slash, "%s/", book);
|
||||
CHECK(mr_library_probe_dir(slash, &p) == 0 && p.pages == 2, "trailing slash");
|
||||
|
||||
CHECK(mr_library_probe_dir("/no/such/dir", &p) == -1, "missing dir -> -1");
|
||||
CHECK(mr_library_probe_dir(NULL, &p) == -1, "NULL -> -1");
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH) == 5, "scan 5 dirs (got %d)", lib.count);
|
||||
|
||||
const mr_library_entry *e = find_entry(&lib, "book");
|
||||
CHECK(e && e->probe_state == MR_LIB_PENDING && e->pages == -1,
|
||||
"book pending after scan");
|
||||
|
||||
int idx_book, idx_noimg, idx_empty, idx_dirs;
|
||||
CHECK(find_index(&lib, "book", &idx_book), "book index");
|
||||
CHECK(find_index(&lib, "noimg", &idx_noimg), "noimg index");
|
||||
CHECK(find_index(&lib, "empty", &idx_empty), "empty index");
|
||||
CHECK(find_index(&lib, "dirs_only", &idx_dirs), "dirs_only index");
|
||||
if (idx_book < 0 || idx_noimg < 0 || idx_empty < 0 || idx_dirs < 0) {
|
||||
mr_library_free(&lib);
|
||||
return;
|
||||
}
|
||||
|
||||
CHECK(mr_library_probe_dir(lib.items[idx_book].path, &p) == 0, "probe row book");
|
||||
CHECK(mr_library_apply_probe(&lib, idx_book, &p) == 1, "apply book");
|
||||
e = &lib.items[idx_book];
|
||||
CHECK(e->has_images == 1 && e->pages == 2 && e->probe_state == MR_LIB_READY,
|
||||
"book ready");
|
||||
CHECK(strcmp(e->subtitle, "2 pages") == 0, "book subtitle=%s", e->subtitle);
|
||||
CHECK(strstr(e->cover, "page2.png") != NULL, "book row cover=%s", e->cover);
|
||||
|
||||
CHECK(mr_library_probe_dir(lib.items[idx_noimg].path, &p) == 0 &&
|
||||
mr_library_apply_probe(&lib, idx_noimg, &p) == 1, "apply noimg");
|
||||
e = &lib.items[idx_noimg];
|
||||
CHECK(e->has_images == 0 && strcmp(e->subtitle, "无图片") == 0,
|
||||
"noimg subtitle=%s", e->subtitle);
|
||||
|
||||
CHECK(mr_library_probe_dir(lib.items[idx_empty].path, &p) == 0 &&
|
||||
mr_library_apply_probe(&lib, idx_empty, &p) == 1, "apply empty");
|
||||
e = &lib.items[idx_empty];
|
||||
CHECK(e->has_images == 0 && e->empty == 1 && strcmp(e->subtitle, "空目录") == 0,
|
||||
"empty subtitle=%s", e->subtitle);
|
||||
|
||||
CHECK(mr_library_mark_error(&lib, idx_dirs) == 1, "mark error");
|
||||
e = &lib.items[idx_dirs];
|
||||
CHECK(e->probe_state == MR_LIB_FAILED && e->has_images == 0 &&
|
||||
strcmp(e->subtitle, "无法读取") == 0, "failed subtitle=%s", e->subtitle);
|
||||
|
||||
CHECK(mr_library_apply_probe(&lib, 999, &p) == 0, "apply out of range");
|
||||
CHECK(mr_library_mark_error(&lib, -1) == 0, "mark_error out of range");
|
||||
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* 档案册探测回调(P2 用例):可注入「探测期间文件变化」。 */
|
||||
typedef struct arch_probe_ctx {
|
||||
int calls;
|
||||
int bump_times; /* 前 N 次调用里改动文件 mtime(模拟探测期间变化) */
|
||||
} arch_probe_ctx;
|
||||
|
||||
static int arch_probe(const char *path, void *ud)
|
||||
{
|
||||
arch_probe_ctx *c = (arch_probe_ctx *)ud;
|
||||
c->calls++;
|
||||
if (c->calls <= c->bump_times) bump_mtime(path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- L3b:评审 P2——探测期间 mtime 变化不得写入缓存(消除假新鲜) ---- */
|
||||
static void test_cache_race(const char *root)
|
||||
{
|
||||
char arc[MR_PATH_MAX];
|
||||
joinp(arc, sizeof arc, root, "race.cbz");
|
||||
CHECK(touch_file(arc) == 0, "create race.cbz");
|
||||
|
||||
mr_library_probe_cache_clear();
|
||||
mr_library_probe p;
|
||||
|
||||
/* 稳定文件:第二次走缓存,回调只调一次。 */
|
||||
arch_probe_ctx stable = { 0, 0 };
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &stable, &p) == 0 && p.pages == 1,
|
||||
"archive probe");
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &stable, &p) == 0 && p.pages == 1,
|
||||
"archive probe (cached)");
|
||||
CHECK(stable.calls == 1, "stable archive cached (calls=%d)", stable.calls);
|
||||
|
||||
/* 探测期间文件变化:本次结果不得入缓存 → 下一次仍重新探测。 */
|
||||
mr_library_probe_cache_clear();
|
||||
arch_probe_ctx race = { 0, 1 };
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &race, &p) == 0 && p.pages == 1,
|
||||
"racing archive probe");
|
||||
CHECK(race.calls == 1, "racing probe called once (calls=%d)", race.calls);
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &race, &p) == 0 && p.pages == 1,
|
||||
"racing archive probe again");
|
||||
CHECK(race.calls == 2, "changed-during-probe result NOT cached (calls=%d)", race.calls);
|
||||
/* 第二次稳定后落缓存,第三次命中。 */
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &race, &p) == 0 && p.pages == 1,
|
||||
"racing archive probe third");
|
||||
CHECK(race.calls == 2, "stable result cached after third call (calls=%d)", race.calls);
|
||||
mr_library_probe_cache_clear();
|
||||
}
|
||||
|
||||
/* ---- L3:mtime 失效缓存 + 并发 ---- */
|
||||
#define THREADS 6
|
||||
typedef struct th_arg {
|
||||
const char *dir;
|
||||
int want;
|
||||
int *fail;
|
||||
pthread_mutex_t *mu;
|
||||
} th_arg;
|
||||
|
||||
static void *probe_thread(void *arg)
|
||||
{
|
||||
th_arg *a = (th_arg *)arg;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
mr_library_probe p;
|
||||
int ok = (mr_library_probe_cached(a->dir, &p) == 0 && p.pages == a->want);
|
||||
if (!ok) {
|
||||
pthread_mutex_lock(a->mu);
|
||||
(*a->fail)++;
|
||||
pthread_mutex_unlock(a->mu);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_cache(const char *root)
|
||||
{
|
||||
char book[MR_PATH_MAX];
|
||||
joinp(book, sizeof book, root, "book");
|
||||
|
||||
mr_library_probe_cache_clear();
|
||||
mr_library_io_stats st;
|
||||
mr_library_probe p;
|
||||
|
||||
mr_library_test_io_reset();
|
||||
CHECK(mr_library_probe_cached(book, &p) == 0 && p.pages == 2, "cache miss probe");
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 1, "cache miss opens once (got %ld)", st.opendirs);
|
||||
|
||||
mr_library_test_io_reset();
|
||||
CHECK(mr_library_probe_cached(book, &p) == 0 && p.pages == 2, "cache hit");
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 0 && st.lstats == 0, "cache hit does no IO (open=%ld stat=%ld)",
|
||||
st.opendirs, st.lstats);
|
||||
|
||||
char img[MR_PATH_MAX];
|
||||
joinp(img, sizeof img, book, "page30.png");
|
||||
CHECK(touch_file(img) == 0, "add image");
|
||||
bump_mtime(book);
|
||||
mr_library_test_io_reset();
|
||||
CHECK(mr_library_probe_cached(book, &p) == 0 && p.pages == 3,
|
||||
"cache invalidated by mtime (pages=%d)", p.pages);
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 1, "invalidation re-opens (got %ld)", st.opendirs);
|
||||
|
||||
CHECK(mr_library_probe_cached("/no/such/dir", &p) == -1, "missing dir -> -1");
|
||||
|
||||
pthread_t th[THREADS];
|
||||
th_arg args[THREADS];
|
||||
int failures = 0;
|
||||
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
int started = 0;
|
||||
for (int t = 0; t < THREADS; t++) {
|
||||
args[t].dir = book;
|
||||
args[t].want = 3;
|
||||
args[t].fail = &failures;
|
||||
args[t].mu = μ
|
||||
if (pthread_create(&th[t], NULL, probe_thread, &args[t]) == 0) started++;
|
||||
}
|
||||
for (int t = 0; t < started; t++) pthread_join(th[t], NULL);
|
||||
CHECK(started == THREADS, "threads started (%d)", started);
|
||||
CHECK(failures == 0, "concurrent probe failures=%d", failures);
|
||||
pthread_mutex_destroy(&mu);
|
||||
}
|
||||
|
||||
/* ---- L4:行合成 O(1) + 截断提示行 ---- */
|
||||
static void test_rows(const char *root)
|
||||
/* ---- L1 + L3:行合成零 IO / 副标题预生成 ---- */
|
||||
static void test_rows_zero_io(const char *root)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH) > 0, "scan fixture");
|
||||
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, i, &row) == 1, "row %d", i);
|
||||
CHECK(row.title && row.title[0], "row %d title", i);
|
||||
CHECK(row.subtitle && row.subtitle[0] && strcmp(row.subtitle, "…") != 0,
|
||||
"row %d subtitle pre-generated (%s)", i, row.subtitle ? row.subtitle : "");
|
||||
}
|
||||
|
||||
mr_library_io_stats st;
|
||||
mr_library_test_io_reset();
|
||||
struct timespec t0, t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
long long t0 = now_us();
|
||||
int rows = 0;
|
||||
for (int r = 0; r < 2000; r++)
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_row row;
|
||||
if (mr_library_row_at(&lib, i, &row)) rows++;
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
long long us = now_us() - t0;
|
||||
mr_library_test_io_get(&st);
|
||||
long long us = (long long)(t1.tv_sec - t0.tv_sec) * 1000000LL +
|
||||
(t1.tv_nsec - t0.tv_nsec) / 1000LL;
|
||||
CHECK(rows == 2000 * lib.count, "rows synthesized (%d)", rows);
|
||||
CHECK(st.opendirs == 0 && st.readdirs == 0 && st.lstats == 0,
|
||||
"row synth is pure memory (open=%ld rd=%ld stat=%ld)",
|
||||
@@ -343,99 +133,93 @@ static void test_rows(const char *root)
|
||||
"truncation info row");
|
||||
CHECK(strcmp(row.title, "已截断") == 0, "truncation title=%s", row.title);
|
||||
CHECK(strstr(row.subtitle, "2000") != NULL, "truncation subtitle=%s", row.subtitle);
|
||||
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ---- L5:M7 兼容语义(直接含图兄弟册 + 自然序) ---- */
|
||||
static void collect_books(const char *parent, char names[][MR_NAME_MAX], int max, int *out_n)
|
||||
/* ---- L2:书库图片下标 == 阅读器目录册下标 ---- */
|
||||
static void test_image_index_matches_reader(const char *dir)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = 0;
|
||||
if (mr_library_scan(&lib, parent, MR_LIB_MAX_DEPTH) > 0) {
|
||||
for (int i = 0; i < lib.count && n < max; i++) {
|
||||
mr_library_probe p;
|
||||
if (mr_library_probe_cached(lib.items[i].path, &p) == 0 && p.pages > 0) {
|
||||
snprintf(names[n], MR_NAME_MAX, "%s", lib.items[i].name);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
CHECK(mr_library_scan(&lib, dir, MR_LIB_MAX_DEPTH) > 0, "scan mixed dir");
|
||||
|
||||
mr_dir_book db;
|
||||
memset(&db, 0, sizeof db);
|
||||
int pages = mr_dir_book_init(&db, dir, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(pages > 0, "reader dir book pages=%d", pages);
|
||||
|
||||
/* 书库里的图片行按出现顺序,其下标必须依次是 0..n-1(无空洞/无重排)。 */
|
||||
int expect = 0;
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
|
||||
int idx = mr_library_image_index(&lib, i);
|
||||
CHECK(idx == expect, "image row %s index=%d (want %d)",
|
||||
lib.items[i].name, idx, expect);
|
||||
expect++;
|
||||
}
|
||||
CHECK(expect == pages, "library images=%d vs reader pages=%d", expect, pages);
|
||||
if (expect != pages) {
|
||||
mr_library_free(&lib);
|
||||
mr_dir_book_free(&db);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 逐张对齐:书库第 k 张图 == 阅读器目录册第 k 页(下标即起始页)。 */
|
||||
int k = 0;
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
|
||||
char page_path[MR_PATH_MAX];
|
||||
mr_dir_book_path(&db, k, page_path, (int)sizeof page_path);
|
||||
CHECK(strcmp(page_path, lib.items[i].path) == 0,
|
||||
"page[%d]: library=%s reader=%s", k, lib.items[i].path, page_path);
|
||||
k++;
|
||||
}
|
||||
|
||||
/* 档案册/目录不占图片下标。 */
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
if (lib.items[i].kind == MR_LIB_KIND_IMAGE) continue;
|
||||
CHECK(mr_library_image_index(&lib, i) == -1,
|
||||
"non-image %s has no image index", lib.items[i].name);
|
||||
}
|
||||
|
||||
mr_dir_book_free(&db);
|
||||
mr_library_free(&lib);
|
||||
*out_n = n;
|
||||
}
|
||||
|
||||
static int book_index(char names[][MR_NAME_MAX], int n, const char *cur)
|
||||
{
|
||||
for (int i = 0; i < n; i++)
|
||||
if (strcmp(names[i], cur) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void test_m7_semantics(const char *root)
|
||||
{
|
||||
char parent[MR_PATH_MAX];
|
||||
joinp(parent, sizeof parent, root, "series");
|
||||
|
||||
char names[16][MR_NAME_MAX];
|
||||
int n = 0;
|
||||
collect_books(parent, names, 16, &n);
|
||||
CHECK(n == 3, "series books=%d (vol1/vol2/vol10; vol6 is not a book)", n);
|
||||
if (n == 3) {
|
||||
CHECK(strcmp(names[0], "vol1") == 0, "book[0]=%s", names[0]);
|
||||
CHECK(strcmp(names[1], "vol2") == 0, "book[1]=%s", names[1]);
|
||||
CHECK(strcmp(names[2], "vol10") == 0, "book[2]=%s", names[2]);
|
||||
}
|
||||
CHECK(book_index(names, n, "vol6") < 0, "dirs-only vol6 skipped (direct images only)");
|
||||
CHECK(book_index(names, n, ".hidden") < 0, "hidden skipped");
|
||||
CHECK(book_index(names, n, "MeowOS") < 0, "system dir without direct images skipped");
|
||||
|
||||
int i2 = book_index(names, n, "vol2");
|
||||
CHECK(i2 >= 0 && i2 + 1 < n && strcmp(names[i2 + 1], "vol10") == 0,
|
||||
"vol2 next = vol10 (natural order, vol6 skipped)");
|
||||
CHECK(book_index(names, n, "vol10") == n - 1, "vol10 is last (no next)");
|
||||
CHECK(book_index(names, n, "vol1") == 0, "vol1 is first (no prev)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_lazy_XXXXXX";
|
||||
char tmpl[] = "./mr_rows_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
const char *dirs[] = { "book", "noimg", "empty", "dirs_only/deep", "series" };
|
||||
const char *dirs[] = { "book", "mixed", "mixed/sub", "empty" };
|
||||
for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) {
|
||||
joinp(p, sizeof p, root, dirs[i]);
|
||||
CHECK(mkdir_p(p) == 0, "mkdir %s", p);
|
||||
}
|
||||
|
||||
joinp(p, sizeof p, root, "book/page2.png"); CHECK(touch_file(p) == 0, "page2");
|
||||
joinp(p, sizeof p, root, "book/page10.jpg"); CHECK(touch_file(p) == 0, "page10");
|
||||
joinp(p, sizeof p, root, "book/notes.txt"); CHECK(touch_file(p) == 0, "notes");
|
||||
joinp(p, sizeof p, root, "noimg/readme.txt"); CHECK(touch_file(p) == 0, "readme");
|
||||
joinp(p, sizeof p, root, "dirs_only/deep/a.png"); CHECK(touch_file(p) == 0, "deep a");
|
||||
joinp(p, sizeof p, root, "mixed/sub/zz.png");CHECK(touch_file(p) == 0, "nested");
|
||||
|
||||
/* M7 语义夹具:vol1/vol2/vol10 直接含图;vol6 只有子目录含图;.hidden / MeowOS 干扰项 */
|
||||
const char *mk[] = { "series/vol1/001.png", "series/vol2/001.png",
|
||||
"series/vol10/001.png", "series/vol6/sub/001.png",
|
||||
"series/.hidden/001.png", "series/MeowOS/deep/001.png" };
|
||||
for (unsigned i = 0; i < sizeof mk / sizeof mk[0]; i++) {
|
||||
joinp(p, sizeof p, root, mk[i]);
|
||||
/* 先建父目录,再落文件 */
|
||||
char dir[MR_PATH_MAX];
|
||||
snprintf(dir, sizeof dir, "%s", p);
|
||||
char *slash = strrchr(dir, '/');
|
||||
if (slash) { *slash = '\0'; CHECK(mkdir_p(dir) == 0, "mkdir %s", dir); }
|
||||
/* 大小写混排 + 数字序号 + 大写扩展名 + 前导零 + 夹档案/普通文件。 */
|
||||
const char *mixed[] = {
|
||||
"A.jpg", "a.jpg", "img2.png", "IMG3.JPG", "img10.png",
|
||||
"page01.bmp", "page1.jpeg", "cover.cbz", "notes.txt", ".hidden.png"
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof mixed / sizeof mixed[0]; i++) {
|
||||
joinp(p, sizeof p, root, "mixed");
|
||||
size_t dl = strlen(p);
|
||||
snprintf(p + dl, sizeof p - dl, "/%s", mixed[i]);
|
||||
CHECK(touch_file(p) == 0, "image %s", p);
|
||||
}
|
||||
|
||||
test_probe_and_apply(root);
|
||||
test_cache(root);
|
||||
test_cache_race(root);
|
||||
test_rows(root);
|
||||
test_m7_semantics(root);
|
||||
test_rows_zero_io(root);
|
||||
joinp(p, sizeof p, root, "mixed");
|
||||
test_image_index_matches_reader(p);
|
||||
|
||||
rm_rf(root);
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
|
||||
Reference in new issue
Block a user