/* test_browse_lazy.c — 浏览行模型 / 零 IO 行合成 / 图片下标一致性(纯 C,无 SDK)。 * * 文件名沿用历史(旧的「懒判定」已随 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/page_source.c \ * -pthread -o build/host/test_browse_lazy */ #include "logic/library.h" #include "logic/reader/page_source.h" #include #include #include #include #include #include #include static int g_pass, g_fail; #define CHECK(cond, ...) do { \ if (cond) { g_pass++; } \ else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \ } while (0) static int mkdir_p(const char *path) { char tmp[MR_PATH_MAX]; snprintf(tmp, sizeof tmp, "%s", path); for (size_t i = 1; i <= strlen(tmp); i++) { if (tmp[i] == '/' || tmp[i] == '\0') { char ch = tmp[i]; tmp[i] = '\0'; if (mkdir(tmp, 0755) != 0) { struct stat st; if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1; } tmp[i] = ch; } } return 0; } static int touch_file(const char *path) { FILE *f = fopen(path, "wb"); if (!f) return -1; fputs("x", f); return fclose(f); } static int rm_rf(const char *path) { struct stat st; if (lstat(path, &st) != 0) return -1; if (!S_ISDIR(st.st_mode)) return unlink(path); DIR *d = opendir(path); if (!d) return -1; struct dirent *de; while ((de = readdir(d)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; char full[MR_PATH_MAX]; snprintf(full, sizeof full, "%s/%s", path, de->d_name); rm_rf(full); } closedir(d); return rmdir(path); } static void joinp(char *out, int outsz, const char *a, const char *b) { snprintf(out, (size_t)outsz, "%s/%s", a, b); } static long long now_us(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL; } /* 阅读器目录册用的假解码回调(本测试不打开任何页)。 */ static meow_img *fake_load(const char *path, void *ud, int maxdim) { (void)path; (void)ud; (void)maxdim; return NULL; } static void fake_unload(meow_img *img) { (void)img; } /* ---- L1 + L3:行合成零 IO / 副标题预生成 ---- */ static void test_rows_zero_io(const char *root) { mr_library lib; memset(&lib, 0, sizeof lib); 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(); 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++; } long long us = now_us() - t0; mr_library_test_io_get(&st); 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)", st.opendirs, st.readdirs, st.lstats); CHECK(us < 2000000, "row synth bounded (%lldus)", us); mr_library_row row; CHECK(mr_library_row_at(&lib, lib.count, &row) == 0, "no extra row when not truncated"); lib.truncated = 1; CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1, "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); } /* ---- L2:书库图片下标 == 阅读器目录册下标 ---- */ static void test_image_index_matches_reader(const char *dir) { mr_library lib; memset(&lib, 0, sizeof lib); 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); } int main(void) { 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", "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, "mixed/sub/zz.png");CHECK(touch_file(p) == 0, "nested"); /* 大小写混排 + 数字序号 + 大写扩展名 + 前导零 + 夹档案/普通文件。 */ 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_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); return g_fail ? 1 : 0; }