Files
MeowOS-SDK/tuku/test/test_open_book_host.c
T
AskaEth 64336f95d9 图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(M18)
用户反馈四件事一起改:
- 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路
  (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 +
  .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片=
  mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。
- 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内
  缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期
  生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。
  2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次
  (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。
- 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏
  「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。
- 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态
  「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。

集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/
普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比
mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。

reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push,
走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。

回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
2026-11-05 20:26:40 +08:00

251 lines
9.7 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 源码树)。
*
* M18 文件管理器模型:点目录=进入;点图片=从该图开始读(mr_reader_open_dir_at(目录, 下标));
* 点档案册=打开档案册。本测试用合成夹具(真实 BMP,不读用户相册)走完整链路:
* library 列表 → 点图片行 → mr_reader_open_dir_at → reader on_enter → mr_book_open_auto
* 并断言「点哪张就开哪张」:阅读器当前页 == 该书库行在目录图片自然序里的下标。
*
* 另覆盖:
* - 目录行只进目录,不打开阅读器;
* - 阅读页已在本页时再次 mr_reader_open_dir_at 能换册/换页重进(SDK 忽略重复 push 的坑);
* - 离开阅读器回书库后再次点图(同目录二次打开)。
*
* 编译:见 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);
/* 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 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);
if (idx < 0) { CHECK(0, "row %s not found", name); return; }
int list_top = meow_ui_statusbar_height() + 56;
library_on_tap(120, list_top + idx * 56 + 28);
}
/* 某张图片行在本目录图片自然序里的下标 = 排在前面的「图片 · 」行数。 */
static int expected_image_index(const char *name)
{
int ti = row_index_for(name);
if (ti < 0) return -1;
int n = 0;
for (int i = 0; i < ti; i++) {
meow_list_item it;
if (!library_test_src(i, &it)) continue;
if (it.subtitle && strncmp(it.subtitle, "图片 · ", strlen("图片 · ")) == 0) n++;
}
return n;
}
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], pics[MR_PATH_MAX], other[MR_PATH_MAX], p[MR_PATH_MAX];
snprintf(root, sizeof root, "%s/library", data);
snprintf(pics, sizeof pics, "%s/Pictures", root);
snprintf(other, sizeof other, "%s/Other", root);
if (mkdir_p(pics) != 0 || mkdir_p(other) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
/* Pictures:6 张图(数字序号/大小写/前导零混排)+ 1 个普通文件(不成行)。 */
const char *imgs[] = { "A.bmp", "a.bmp", "img2.bmp", "img10.bmp",
"page3.bmp", "Page03.bmp" };
for (unsigned i = 0; i < sizeof imgs / sizeof imgs[0]; i++) {
snprintf(p, sizeof p, "%s/%s", pics, imgs[i]);
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; }
}
snprintf(p, sizeof p, "%s/notes.txt", pics);
{ FILE *f = fopen(p, "wb"); if (f) { fputs("x", f); fclose(f); } }
/* Other:4 张图,用于「已在本页时换册重进」。 */
for (int i = 0; i < 4; i++) {
snprintf(p, sizeof p, "%s/o%d.bmp", other, i + 1);
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; }
}
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");
/* 点目录 = 进入目录,不打开阅读器。 */
tap_row("Pictures");
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "tap dir stays in library");
CHECK(strcmp(library_test_dir(), pics) == 0, "entered Pictures (%s)", library_test_dir());
CHECK(library_test_rows() == 6, "Pictures rows=%d (6 images, txt excluded)",
library_test_rows());
/* 点 img10.bmp:阅读器从该图开始(count=6, page=它在该目录的自然序下标)。 */
int want = expected_image_index("img10.bmp");
CHECK(want == 3, "img10.bmp natural image index=%d (want 3)", want);
tap_row("img10.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "tap image opens reader");
CHECK(strcmp(mr_reader_dir(), pics) == 0, "reader dir=%s", mr_reader_dir());
CHECK(mr_reader_ui_open_err() == 0, "open has no error");
CHECK(mr_reader_ui_count() == 6, "reader pages=%d (want 6)", mr_reader_ui_count());
CHECK(mr_reader_ui_page() == want, "reader starts at selected image: page=%d (want %d)",
mr_reader_ui_page(), want);
/* 让首帧解码 tick 跑一遍,确认不会因坏夹具进入 open_err。 */
for (int i = 0; i < 20; i++) usleep(10 * 1000);
CHECK(mr_reader_ui_open_err() == 0, "still no open_err after decode tick");
/* 已在本页:直接换册重进(M17 遗留修复,SDK 会忽略重复 push)。 */
mr_reader_open_dir_at(other, 2);
CHECK(meow_ui_activity_active() == ACT_READER, "switch keeps reader active");
CHECK(strcmp(mr_reader_dir(), other) == 0, "switched reader dir=%s", mr_reader_dir());
CHECK(mr_reader_ui_count() == 4, "switched book pages=%d (want 4)", mr_reader_ui_count());
CHECK(mr_reader_ui_page() == 2, "switched book page=%d (want 2)", mr_reader_ui_page());
/* 返回书库 → 再点另一张图(同目录二次打开 + 显式起始页优先于续读)。 */
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library");
CHECK(strcmp(library_test_dir(), pics) == 0, "library still in Pictures");
tap_row("page3.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "reopen reader");
CHECK(mr_reader_ui_open_err() == 0, "reopen no error");
CHECK(mr_reader_ui_count() == 6, "reopen pages=%d", mr_reader_ui_count());
int want2 = expected_image_index("page3.bmp");
CHECK(want2 == 4, "page3.bmp natural image index=%d (want 4)", want2);
CHECK(mr_reader_ui_page() == want2, "reopen starts at page=%d (want %d)",
mr_reader_ui_page(), want2);
meow_view_page_pop();
/* 档案册路径(无 zip 夹具时只验证入口不被当成目录):点 txt 不存在行;跳过。 */
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;
}