Files
MeowOS-SDK/tuku/test/test_browse_ui_host.c
T
AskaEth 9f34417c49 图库: 单层浏览 + 懒判定与 M7/M11 整合;探测缓存 P2 修复(M13)
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
2026-11-05 12:23:31 +08:00

270 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}