Files
MeowOS-SDK/tuku/test/test_open_book_host.c
T
AskaEth 3cbcaaaac1 图库: 修系统返回语义与打开册资源路径(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 链路)。
2026-11-05 13:03:20 +08:00

242 lines
8.5 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_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;
}