图库: 修系统返回语义与打开册资源路径(M15)

- 返回手势:书库列表排除左缘 24px 系统返回带并清空残留拖拽态;library_go_up
  到 TF 根返回 0(app.c 退出 App),不再上溯系统目录;顶栏返回改为显式
  library_go_parent(访问放开,可越过 TF 根到 /)。lock 页继续自行消费返回。
- 打开册:缩略图源图改用预算解码(meow_img_load_file_dim,峰值数十 MB→~1MB);
  离开书库页停止后台探测/缩略图线程,避免与 open_book 争内存/FD;打开失败按
  errno 记日志,ENOMEM/EMFILE/ENFILE/EAGAIN/EINTR 自动重试一次;修 open_book
  自拷贝 snprintf 的未定义行为。
- 测试:test_browse_ui_host 增加 TF 根退出/顶栏上级/左缘不拖拽断言;新增
  test_open_book_host(封面图+含图子目录的父目录 → reader 打开的完整 UI 链路)。
This commit is contained in:
AskaEth committed 2026-11-05 13:03:20 +08:00
1 parent 5ac227b225
commit 3cbcaaaac1
7 files changed
+377 -13

No files matched your search

+20
View File
@@ -11,6 +11,8 @@
# - test_settings_lock_host(可选):设置页/锁屏与 SDK view/activity/widget 接线的宿主集成
# 验证(owner 归属 + 锁屏 SWIPE 拦截)。需要 SDK **源码树**(不只 meow_ui.a);
# 交付环境只有库+头时自动 SKIP。
# - test_open_book_host(可选):书库点击「封面图 + 含图子目录」的父目录 → 阅读器打开的
# 完整 UI 链路回归(合成夹具,不读真实相册);同样需要 SDK 源码树。
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
# logic/library.c 的探测缓存用 pthread 互斥锁 → 相关编译带 -pthread。
@@ -87,6 +89,24 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
-pthread -lm -lz -o "$OUT"/test_browse_ui_host
"$OUT"/test_browse_ui_host
# 点击父目录(封面图 + 含图子目录)→ 阅读器打开:完整 UI 链路回归。
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST \
-DMR_LIBRARY_UI_TEST_HOOKS -I. -I"$SDK" -I"$SDK/include" \
test/test_open_book_host.c ui/library/library.c ui/reader/reader.c \
logic/library.c logic/reader.c logic/reader/page_source.c \
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
logic/reader/nav.c \
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
-pthread -lm -lz -o "$OUT"/test_open_book_host
"$OUT"/test_open_book_host
else
echo "SKIP test_settings_lock_host/test_browse_ui_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
fi
+26
View File
@@ -39,6 +39,8 @@ int library_test_hint_active(void);
const char *library_test_hint(void);
int library_test_go_root(void);
int library_test_go_up(void);
int library_test_go_parent(void);
int library_test_drag_active(void);
int library_test_has_list(void);
const char *library_test_cache_dir(void);
int library_test_cache_on(void);
@@ -336,6 +338,30 @@ int main(void)
CHECK(library_test_go_up() == 0, "go_up at / returns 0 (app exits)");
CHECK(strcmp(library_test_dir(), "/") == 0, "still at / after exit gesture");
/* ---- U3b:系统返回语义 = TF 根退出 App;顶栏「上级」保留越界浏览 ---- */
CHECK(library_test_load(root) == 1, "load fixture root");
CHECK(library_test_go_up() == 0, "go_up at TF root returns 0 (app exits)");
CHECK(strcmp(library_test_dir(), root) == 0, "still at TF root after exit gesture");
/* 顶栏显式「上级」:访问放开,TF 根 → 其父目录(fixture root 的 parent = data)。 */
CHECK(library_test_go_parent() == 1, "explicit up from TF root consumed");
CHECK(strcmp(library_test_dir(), data) == 0,
"explicit up goes above TF root (%s)", library_test_dir());
CHECK(library_test_load(root) == 1, "back to fixture root");
/* ---- U3c:左缘 24px 系统返回手势带不启动列表拖拽 ---- */
int row_y = list_top + 28;
library_on_touch_down(120, row_y);
CHECK(library_test_drag_active() == 1, "touch inside list starts drag");
library_on_touch_down(5, row_y);
CHECK(library_test_drag_active() == 0, "left-edge touch does not start drag");
library_on_touch_down(23, row_y);
CHECK(library_test_drag_active() == 0, "left-edge (23px) does not start drag");
library_on_touch_down(24, row_y);
CHECK(library_test_drag_active() == 1, "just outside left band starts drag");
library_on_touch_down(120, meow_ui_height() - 4);
CHECK(library_test_drag_active() == 0, "bottom hotzone does not start drag");
/* ---- U4:不可读目录 → 停留 + 提示 ---- */
CHECK(library_test_load(root) == 1, "back to fixture root");
char noread[MR_PATH_MAX];
+241
View File
@@ -0,0 +1,241 @@
/* 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 <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* 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;
}