rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接: - 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages 时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注; 系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测 - 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle / entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入; 档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算 - 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency 恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏); mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标 - mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU + 互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存 (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测) - UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11); 行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、 不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表; 顶栏回根命中加 g_tb_home_idx>=0 防御 - tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出) - 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf / browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6 改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书); run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests) - 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS; worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
270 lines
11 KiB
C
270 lines
11 KiB
C
/* 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
#include <dirent.h>
|
||
|
||
/* ---- 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;
|
||
}
|