/* test_open_book_host.c — 书库点击 → 阅读器打开的宿主集成回归(需 SDK 源码树)。 * * 复现用户报告:「父文件夹有封面图片、子文件夹也有图片,点开父文件夹报无法打开此册」。 * 用合成夹具(真实 BMP 当封面/子图,不读用户相册)走完整链路: * library 列表 → 行点击 → mr_reader_open_dir → reader on_enter → mr_book_open_auto * 并断言父目录以「书」打开(count=1)且没有进入 open_err。 * * 另覆盖:封面图很大时打开仍成功(打开只枚举,不解码);同目录二次打开/离开重开。 * * 编译:见 test/run_tests.sh(与 test_mode_switch_host 同口径)。 */ #include "tuku_internal.h" #include "ui/reader/reader.h" #include #include #include #include #include #include /* ui/library/library.c 的测试接缝 */ int library_test_load(const char *dir); void library_test_set_root(const char *root); const char *library_test_dir(void); int library_test_rows(void); int library_test_src(int idx, meow_list_item *item); int library_test_worker_on(void); /* ui/reader/reader.c 的测试接缝(-DMR_READER_HOST_TEST) */ int mr_reader_ui_mode(void); int mr_reader_ui_page(void); int mr_reader_ui_count(void); int mr_reader_ui_open_err(void); static int g_fail; #define CHECK(cond, ...) do { \ if (cond) { printf(" PASS\n"); } \ else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \ } while (0) void mr_nav_report(void) {} static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); } static void put32(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24); } static int write_bmp24(const char *path, int w, int h) { int stride = ((w * 3 + 3) / 4) * 4; int imgsz = stride * h; unsigned char fh[14] = { 0 }, ih[40] = { 0 }; fh[0] = 'B'; fh[1] = 'M'; put32(fh + 2, (unsigned)(54 + imgsz)); put32(fh + 10, 54); put32(ih + 0, 40); put32(ih + 4, (unsigned)w); put32(ih + 8, (unsigned)h); put16(ih + 12, 1); put16(ih + 14, 24); put32(ih + 20, (unsigned)imgsz); FILE *f = fopen(path, "wb"); if (!f) return -1; int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh && fwrite(ih, 1, sizeof ih, f) == sizeof ih; unsigned char *row = (unsigned char *)calloc(1, (size_t)stride); if (!row) ok = 0; for (int y = 0; ok && y < h; y++) if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0; free(row); if (fclose(f) != 0) ok = 0; return ok ? 0 : -1; } 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 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 wait_ready(int ms) { for (int t = 0; t < ms / 10; t++) { library_on_tick(); int ready = 1; int rows = library_test_rows(); for (int i = 0; i < rows; i++) { meow_list_item it; if (!library_test_src(i, &it)) continue; if (it.subtitle && strcmp(it.subtitle, "…") == 0) { ready = 0; break; } } if (ready) return; usleep(10000); } } static int row_index_for(const char *name) { int rows = library_test_rows(); for (int i = 0; i < rows; i++) { meow_list_item it; if (!library_test_src(i, &it)) continue; if (it.title && strcmp(it.title, name) == 0) return i; } return -1; } /* 用真实点击坐标点某一行(列表顶端 = 状态栏 + 56px 顶栏,行高 56)。 */ static void tap_row(const char *name) { int idx = row_index_for(name); int list_top = meow_ui_statusbar_height() + 56; library_on_tap(120, list_top + idx * 56 + 28); } int main(void) { const char *base = getenv("TMPDIR"); if (!base || !base[0]) base = "."; char tmpl[512]; snprintf(tmpl, sizeof tmpl, "%s/mr_open_host_XXXXXX", base); char *data = mkdtemp(tmpl); if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; } setenv("MEOW_DATA_DIR", data, 1); char root[MR_PATH_MAX], parent[MR_PATH_MAX], sub[MR_PATH_MAX], p[MR_PATH_MAX]; snprintf(root, sizeof root, "%s/library", data); snprintf(parent, sizeof parent, "%s/Pictures", root); if (mkdir_p(parent) != 0) { printf("SKIP: mkdir failed\n"); return 0; } /* 父目录:一张封面图 + 两个含图子目录(用户报告的结构)。 */ snprintf(p, sizeof p, "%s/cover.jpg", parent); /* 扩展名即可;打开只枚举不解码 */ FILE *f = fopen(p, "wb"); if (!f) { printf("SKIP: cover write failed\n"); return 0; } fputs("x", f); fclose(f); snprintf(sub, sizeof sub, "%s/2023", parent); mkdir_p(sub); snprintf(p, sizeof p, "%s/a.bmp", sub); write_bmp24(p, 40, 60); snprintf(sub, sizeof sub, "%s/2024", parent); mkdir_p(sub); snprintf(p, sizeof p, "%s/b.bmp", sub); write_bmp24(p, 40, 60); meow_ui_opts o = { 480, 720 }; if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; } library_test_set_root(root); meow_view *lib = library_view_create(); meow_view *reader = reader_view_create(); meow_view_page_push(ACT_LIBRARY); CHECK(library_test_load(root) == 1, "load fixture root"); wait_ready(3000); int ip = row_index_for("Pictures"); meow_list_item it; CHECK(ip >= 0 && library_test_src(ip, &it), "Pictures row present"); CHECK(it.subtitle && strcmp(it.subtitle, "1 page") == 0, "Pictures probed as book with direct cover (subtitle=%s)", it.subtitle ? it.subtitle : ""); /* 点击父目录 → 进阅读器;cover 是唯一直接子图 → count=1,不能是 open_err。 */ tap_row("Pictures"); CHECK(meow_ui_activity_active() == ACT_READER, "tap opens reader"); CHECK(strcmp(mr_reader_dir(), parent) == 0, "reader dir=%s", mr_reader_dir()); CHECK(mr_reader_ui_open_err() == 0, "parent folder opens without open_err"); CHECK(mr_reader_ui_count() == 1, "parent book pages=%d (want 1)", mr_reader_ui_count()); CHECK(library_test_worker_on() == 0, "library background decode stopped while reader is active"); for (int i = 0; i < 30; i++) { usleep(10 * 1000); } CHECK(mr_reader_ui_open_err() == 0, "still no open_err after decode tick"); /* 返回书库 → 后台线程恢复,再次点开(同目录二次打开 + 续读恢复)。 */ meow_view_page_pop(); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library"); CHECK(library_test_worker_on() == 1, "library background decode restarted on return"); tap_row("Pictures"); CHECK(mr_reader_ui_open_err() == 0, "reopen parent without open_err"); CHECK(mr_reader_ui_count() == 1, "reopen pages=%d", mr_reader_ui_count()); meow_view_page_pop(); /* 封面是大图也不影响打开(打开只枚举文件名)。 */ snprintf(p, sizeof p, "%s/cover.bmp", parent); CHECK(write_bmp24(p, 400, 600) == 0, "write large cover"); CHECK(library_test_load(root) == 1, "reload after cover change"); wait_ready(3000); tap_row("Pictures"); CHECK(mr_reader_ui_open_err() == 0, "open with large cover has no open_err"); CHECK(mr_reader_ui_count() == 2, "cover.jpg + cover.bmp pages=%d (want 2)", mr_reader_ui_count()); meow_view_page_pop(); meow_view_free(reader); meow_view_free(lib); meow_ui_shutdown(); rm_rf(data); printf("%s: %s\n", g_fail ? "FAIL" : "PASS", g_fail ? "see failures above" : "library→reader open checks passed"); return g_fail ? 1 : 0; }