/* test_browse_ui_host.c — 书库浏览页宿主集成测试(可选,需 SDK 源码树)。 * * 目的(M13): * U1 列表用 src2 模式(每帧重取行):扫描后行副标题是占位「…」,后台探测完成、 * on_tick 补位后同一行立即显示页数——验证懒算结果能上屏(固定池 vsource 做不到); * U2 行语义:书行显示 "N pages"(无进入箭头),空目录「空目录」,无图目录「无图片」; * U3 导航:可逐级上溯父目录;文件系统 "/" 处 library_go_up 返回 0(app.c 据此退出); * 「TF卡」回根动作回到书库根; * U4 不可读目录:load 失败 → 停留原目录 + 顶部提示横幅。 * * 编译:见 test/run_tests.sh(与 test_settings_lock_host 同口径:链 SDK 源码树 + 打桩; * 只发布 meow_ui.a 的交付环境自动 SKIP)。ui/library/library.c 以 * -DMR_LIBRARY_UI_TEST_HOOKS 编译出测试接缝;本测试另打桩 mr_reader_open_dir/mr_nav_report。 */ #include "tuku_internal.h" #include "logic/library.h" #include #include #include #include #include #include /* ---- ui/library/library.c 的测试接缝(仅 -DMR_LIBRARY_UI_TEST_HOOKS 时定义) ---- */ int library_test_load(const char *dir); void library_test_set_root(const char *root); const char *library_test_root(void); const char *library_test_dir(void); void library_test_set_dir(const char *dir); int library_test_rows(void); int library_test_src(int idx, meow_list_item *item); int library_test_hint_active(void); const char *library_test_hint(void); int library_test_go_root(void); int library_test_go_up(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) /* app.c 的导航上报在测试里不需要网络侧实现。 */ void mr_nav_report(void) {} /* reader 打桩:只记录被打开的册路径(本测试不建 reader 页)。 */ static char g_opened[MR_PATH_MAX]; static int g_opened_n; void mr_reader_open_dir(const char *path) { snprintf(g_opened, sizeof g_opened, "%s", path ? path : ""); g_opened_n++; } 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)) { chmod(path, 0700); return unlink(path); } chmod(path, 0700); 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 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; } 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); } } int main(void) { const char *base = getenv("TMPDIR"); if (!base || !base[0]) base = "."; char tmpl[512]; snprintf(tmpl, sizeof tmpl, "%s/mr_browse_ui_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]; snprintf(root, sizeof root, "%s/library", data); char p[MR_PATH_MAX]; const char *dirs[] = { "book", "empty", "noimg", "deep/sub", "MeowOS/sub" }; for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) { joinp(p, sizeof p, root, dirs[i]); if (mkdir_p(p) != 0) { printf("SKIP: mkdir %s failed\n", p); return 0; } } joinp(p, sizeof p, root, "book/page2.png"); touch_file(p); joinp(p, sizeof p, root, "book/page10.jpg"); touch_file(p); joinp(p, sizeof p, root, "noimg/readme.txt"); touch_file(p); joinp(p, sizeof p, root, "deep/sub/001.png"); touch_file(p); joinp(p, sizeof p, root, "MeowOS/sub/m.png"); touch_file(p); meow_ui_opts o = { 480, 720 }; if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; } meow_view *lib = library_view_create(); library_test_set_root(root); CHECK(strcmp(library_test_root(), root) == 0, "test root installed"); meow_view_page_push(ACT_LIBRARY); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "library active"); /* ---- U1:src2 行 + 懒算补位 ---- */ CHECK(library_test_load(root) == 1, "load fixture"); CHECK(library_test_rows() == 5, "5 rows (got %d)", library_test_rows()); static const char *want[5] = { "book", "deep", "empty", "MeowOS", "noimg" }; for (int i = 0; i < 5; i++) { meow_list_item it; CHECK(library_test_src(i, &it) == 1, "src row %d", i); CHECK(it.title && strcmp(it.title, want[i]) == 0, "row[%d]=%s (want %s)", i, it.title ? it.title : "(null)", want[i]); CHECK(it.subtitle && strcmp(it.subtitle, "…") == 0, "row[%d] starts with placeholder (got %s)", i, it.subtitle ? it.subtitle : ""); CHECK(it.right == MEOW_LIST_RIGHT_ARROW, "row[%d] has enter arrow while pending", i); } wait_ready(3000); /* U2:补位后的行语义 */ for (int i = 0; i < 5; i++) { meow_list_item it; library_test_src(i, &it); CHECK(it.subtitle && strcmp(it.subtitle, "…") != 0, "row[%d] subtitle filled (got %s)", i, it.subtitle ? it.subtitle : ""); } int ib = row_index_for("book"); int ie = row_index_for("empty"); int in = row_index_for("noimg"); int id = row_index_for("deep"); meow_list_item it; CHECK(ib >= 0 && library_test_src(ib, &it) && strcmp(it.subtitle, "2 pages") == 0, "book subtitle=%s", it.subtitle ? it.subtitle : ""); CHECK(it.right == MEOW_LIST_RIGHT_NONE, "book row has no enter arrow (opens reader)"); CHECK(ie >= 0 && library_test_src(ie, &it) && strcmp(it.subtitle, "空目录") == 0, "empty subtitle=%s", it.subtitle ? it.subtitle : ""); CHECK(in >= 0 && library_test_src(in, &it) && strcmp(it.subtitle, "无图片") == 0, "noimg subtitle=%s", it.subtitle ? it.subtitle : ""); CHECK(id >= 0 && library_test_src(id, &it) && strcmp(it.subtitle, "无图片") == 0, "deep (images only in subdir) is not a book: %s", it.subtitle ? it.subtitle : ""); /* ---- 行点击:书 → reader;空目录 → 进入 ---- */ int list_top = meow_ui_statusbar_height() + 56; g_opened_n = 0; library_on_tap(120, list_top + ib * 56 + 28); CHECK(g_opened_n == 1 && strstr(g_opened, "/book") != NULL, "tap book opens reader (%s)", g_opened); CHECK(strcmp(library_test_dir(), root) == 0, "tap book does not navigate"); library_on_tap(120, list_top + ie * 56 + 28); char empty_path[MR_PATH_MAX]; joinp(empty_path, sizeof empty_path, root, "empty"); CHECK(strcmp(library_test_dir(), empty_path) == 0, "tap empty dir enters it (%s)", library_test_dir()); CHECK(library_test_rows() == 0, "empty dir shows empty list"); /* ---- U3:导航(逐级上溯 / 到 / 退出 / 回 TF 根) ---- */ CHECK(library_test_go_up() == 1, "go_up from empty consumed"); CHECK(strcmp(library_test_dir(), root) == 0, "go_up to fixture root"); char deep[MR_PATH_MAX]; joinp(deep, sizeof deep, root, "deep/sub"); CHECK(library_test_load(deep) == 1, "load deep/sub"); CHECK(library_test_go_up() == 1, "go_up from deep/sub"); joinp(p, sizeof p, root, "deep"); CHECK(strcmp(library_test_dir(), p) == 0, "now at deep"); /* 「TF卡」回根 */ CHECK(library_test_load(deep) == 1, "reload deep/sub"); CHECK(library_test_go_root() == 1 && strcmp(library_test_dir(), root) == 0, "TF卡 action returns to library root"); /* 上溯到文件系统 /:go_up 在 / 返回 0(app.c 据此退出)。 * 宿主(Termux)读不了 "/":从 /usr 返回时 load("/") 失败,仍应切到 "/" 的空列表 * (否则会卡在 /usr 无法退出),下一次返回=退出 App。 */ library_test_set_dir("/usr"); CHECK(library_test_go_up() == 1, "go_up toward / consumed"); CHECK(strcmp(library_test_dir(), "/") == 0, "reached / (unreadable root fallback)"); CHECK(library_test_go_up() == 0, "go_up at / returns 0 (app exits)"); CHECK(strcmp(library_test_dir(), "/") == 0, "still at / after exit gesture"); /* ---- U4:不可读目录 → 停留 + 提示 ---- */ CHECK(library_test_load(root) == 1, "back to fixture root"); char noread[MR_PATH_MAX]; joinp(noread, sizeof noread, root, "noread"); if (geteuid() != 0) { CHECK(mkdir(noread, 0755) == 0, "mkdir noread"); chmod(noread, 0000); int ok = library_test_load(noread); CHECK(ok == 0, "unreadable dir load fails"); CHECK(strcmp(library_test_dir(), root) == 0, "stays in previous dir (%s)", library_test_dir()); CHECK(library_test_hint_active(), "hint banner shown: %s", library_test_hint()); CHECK(strstr(library_test_hint(), "无法读取") != NULL, "hint text mentions unreadable: %s", library_test_hint()); chmod(noread, 0700); } else { printf(" SKIP unreadable-dir case (running as root)\n"); } meow_view_free(lib); meow_ui_shutdown(); rm_rf(data); printf("%s: %s\n", g_fail ? "FAIL" : "PASS", g_fail ? "see failures above" : "browse UI checks passed"); return g_fail ? 1 : 0; }