/* test_reader_many_pages.c — 多页(140 张)文件夹打开/翻页/跳页回归 +「从第 N 张开始」API。 * * 用户反馈:「文件夹里图片过多时(例如 140 个),APP 会直接闪退」;且后续书库要改成文件 * 管理器模式,需要「从选中的那张开始看」。本测试走真实 SDK UI 链路(view/activity/anim/ * 真实解码),断言: * M1 140 页册可打开,页数/模式正确(翻页模式、第 1 页); * M2 mr_reader_open_dir_at(dir, 57):从第 57 页(0 基)开始;越界 9999 → 末页;负数 → 首页; * M3 显式下标优先于续读记录;mr_reader_open_dir 仍走续读记录(旧语义不变); * M4 连续翻页穿过坏图(解码失败)不崩、不卡死,坏页给占位后继续翻; * M5 条漫模式:140 页排版/跳页/滚动可用; * M6 常驻页图有字节上界(<=9MB)——页数增长不会线性吃内存。 * * 编译:test/run_tests.sh(与 test_open_book_host 同口径;只发布 meow_ui.a 时 SKIP)。 */ #include "tuku_internal.h" #include "ui/reader/reader.h" #include "meow_ui_internal.h" #include #include #include #include #include #include #define BOOK_PAGES 140 void mr_nav_report(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) 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, unsigned seed) { 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++) { for (int x = 0; x < w; x++) { row[x * 3 + 0] = (unsigned char)((x + seed) & 0xFF); row[x * 3 + 1] = (unsigned char)((y + seed) & 0xFF); row[x * 3 + 2] = (unsigned char)(seed & 0xFF); } 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 write_garbage(const char *path) { static const unsigned char junk[64] = { 'N', 'O', 'T', 'A', 'N', 'I', 'M', 'G' }; FILE *f = fopen(path, "wb"); if (!f) return -1; int ok = fwrite(junk, 1, sizeof junk, f) == sizeof junk; if (fclose(f) != 0) ok = 0; return ok ? 0 : -1; } /* 合法 BMP24 头(声称 3000x3000,无像素数据):stbi_info 能读到尺寸, * 整幅 RGBA 峰值 36MB 必须在解码前拦下(旧代码的 raw 上限是 40MB,这种页会被放行 → * 真机上瞬时分配即可能被 OOM 杀掉)。 * M27 r1:24bpp(后端支持)与 1bpp(后端拒绝→stb)两种都要覆盖: * - 24bpp 头-only:判定 backend → backend-only 失败 → NULL/坏图占位,峰值必须 ≤3MB; * - 1bpp 头-only:判定 stb → 旧门控 skip=peak,零分配。 * 旧实现把 24bpp 换成 1bpp 恰好绕开了「矩阵接受、解码失败→静默回退 stb」的 P1 旁路。 */ static int write_huge_bmp_header(const char *path, int bpp) { unsigned char b[54] = { 0 }; b[0] = 'B'; b[1] = 'M'; put32(b + 2, 54); /* 文件大小(谎报,只为通过头检查) */ put32(b + 10, 54); /* 像素数据偏移 */ put32(b + 14, 40); /* BITMAPINFOHEADER */ put32(b + 18, 3000); /* width */ put32(b + 22, 3000); /* height */ put16(b + 26, 1); /* planes */ put16(b + 28, (unsigned)bpp); /* 24=后端支持但无像素数据;1=后端拒绝→stb */ FILE *f = fopen(path, "wb"); if (!f) return -1; int ok = fwrite(b, 1, sizeof b, f) == sizeof b; 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 pump_ms(int ms) { for (int i = 0; i < ms / 10; i++) { usleep(10 * 1000); meow_anim_tick(); } } static int tap(int x, int y) { reader_on_touch_down(x, y); reader_on_tap(x, y); pump_ms(300); return 1; } int main(void) { const char *base = getenv("TMPDIR"); if (!base || !base[0]) base = "."; char tmpl[512]; snprintf(tmpl, sizeof tmpl, "%s/mr_many_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], book[MR_PATH_MAX], bad[MR_PATH_MAX]; snprintf(root, sizeof root, "./mr_many_books_%ld", (long)getpid()); snprintf(book, sizeof book, "%s/v140", root); snprintf(bad, sizeof bad, "%s/vbad", root); if (mkdir_p(book) != 0 || mkdir_p(bad) != 0) { printf("SKIP: mkdir failed\n"); return 0; } for (int i = 0; i < BOOK_PAGES; i++) { char p[MR_PATH_MAX]; snprintf(p, sizeof p, "%s/p%03d.bmp", book, i + 1); if (write_bmp24(p, 200, 267, (unsigned)i) != 0) { printf("SKIP: bmp write failed\n"); return 0; } } /* 坏册(5 页):1=有效;2=垃圾字节(永久 failed);3=3000×3000 24bpp 头-only * (判定 backend→后端失败→NULL,永久 failed,峰值必须小);4=3000×3000 1bpp 头-only * (回退 stb→skip=peak,可重试);5=有效。 */ for (int i = 0; i < 5; i++) { char p[MR_PATH_MAX]; snprintf(p, sizeof p, "%s/p%d.bmp", bad, i + 1); if (i == 1) { if (write_garbage(p) != 0) { printf("SKIP: write failed\n"); return 0; } } else if (i == 2) { if (write_huge_bmp_header(p, 24) != 0) { printf("SKIP: write failed\n"); return 0; } } else if (i == 3) { if (write_huge_bmp_header(p, 1) != 0) { printf("SKIP: write failed\n"); return 0; } } else if (write_bmp24(p, 200, 267, (unsigned)i) != 0) { printf("SKIP: bmp 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; } static const meow_view_hooks lh = { 0 }; meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL); meow_view *reader = reader_view_create(); meow_view_page_push(ACT_LIBRARY); int w = meow_ui_width(); int mode_x = w - 10 - 48 * 3 + 24; int jump_x = w - 10 - 48 * 2 + 24; int bar_y = 28; int slider_y = 56 + 4 + 20; int sx = 24, sw = w - 48; /* ---- M1 140 页可打开 ---- */ mr_reader_open_dir_at(book, 0); CHECK(meow_ui_activity_active() == ACT_READER, "reader active"); CHECK(mr_reader_ui_count() == BOOK_PAGES, "pages=%d (want %d)", mr_reader_ui_count(), BOOK_PAGES); CHECK(mr_reader_ui_page() == 0, "start at page 0 (got %d)", mr_reader_ui_page()); pump_ms(600); int draws0 = mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws(); meow_ui_paint_full(); CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > draws0, "draw path executed (paged, 140p book)"); /* ---- M2 从第 N 张开始 / 越界钳制 ---- */ meow_view_page_pop(); mr_reader_open_dir_at(book, 57); CHECK(mr_reader_ui_page() == 57, "open_dir_at(57) -> page %d", mr_reader_ui_page()); pump_ms(400); meow_view_page_pop(); mr_reader_open_dir_at(book, 9999); CHECK(mr_reader_ui_page() == BOOK_PAGES - 1, "open_dir_at(9999) clamps to last (%d)", mr_reader_ui_page()); pump_ms(400); meow_view_page_pop(); mr_reader_open_dir_at(book, -5); CHECK(mr_reader_ui_page() == 0, "open_dir_at(-5) clamps to first (%d)", mr_reader_ui_page()); pump_ms(300); meow_view_page_pop(); /* ---- M3 显式下标优先于续读;open_dir 保持续读语义 ---- */ mr_reader_open_dir_at(book, 77); /* 离开时会把 77 落盘 */ pump_ms(300); meow_view_page_pop(); mr_reader_open_dir_at(book, 12); /* 显式下标赢过续读记录 77 */ CHECK(mr_reader_ui_page() == 12, "explicit start beats resume record (%d)", mr_reader_ui_page()); pump_ms(300); meow_view_page_pop(); mr_reader_open_dir(book); /* 旧入口 = 续读 */ CHECK(mr_reader_ui_page() == 12, "open_dir resumes saved page (%d)", mr_reader_ui_page()); pump_ms(300); /* ---- M4 连续翻页(含异步解码)不崩、页码推进、内存有界 ---- */ int ok_move = 1; for (int i = 0; i < 12; i++) { int before = mr_reader_ui_page(); tap(40, 300); pump_ms(260); meow_ui_paint_full(); /* 绘制路径覆盖(P2-5) */ if (mr_reader_ui_page() == before) ok_move = 0; } CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > draws0, "draw path executed across flips"); CHECK(ok_move, "12 consecutive flips advanced the page"); CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "resident bounded on 140p book (%ldKB)", mr_reader_ui_resident_kb()); /* 坏图册(5 页):翻过坏页不崩、可继续翻。最后一页是有效页,允许不前进。 */ meow_view_page_pop(); mr_reader_open_dir_at(bad, 0); pump_ms(600); int bad_ok = 1; for (int i = 0; i < 5; i++) { int before = mr_reader_ui_page(); tap(40, 300); pump_ms(300); if (mr_reader_ui_page() == before && before < 4) bad_ok = 0; } CHECK(bad_ok, "flip across corrupt/huge pages keeps working (page=%d)", mr_reader_ui_page()); CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "huge pages skipped/rejected without allocating them (%ldKB)", mr_reader_ui_resident_kb()); /* M22/M27 r1:坏图 = 永久占位(failed);超预算的**回退路径**大页 = 预算跳过(skipped)。 * - 垃圾页 + 3000×3000 24bpp 头-only(判定 backend,backend-only 失败→NULL)→ failed=2; * - 3000×3000 1bpp 头-only(判定 stb,超预算)→ skipped=1。 * 这条 24bpp 夹具正是 P1 旁路的复现:旧实现会静默回退 stb 并分配 36MB。 */ CHECK(mr_reader_ui_failed_pages() == 2, "garbage + 24bpp head-only are permanent placeholders (failed=%d)", mr_reader_ui_failed_pages()); CHECK(mr_reader_ui_skipped_pages() == 1, "1bpp oversized page is a retryable budget skip (skipped=%d)", mr_reader_ui_skipped_pages()); CHECK(meow_ui_activity_active() == ACT_READER, "still in reader after corrupt page"); /* ---- M5 条漫:140 页排版 + 跳页落位 ---- */ meow_view_page_pop(); mr_reader_open_dir_at(book, 0); pump_ms(400); CHECK(mr_reader_ui_count() == BOOK_PAGES, "back on the 140p book (%d)", mr_reader_ui_count()); CHECK(tap(mode_x, bar_y) == 1, "mode icon consumed"); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon mode on 140 pages"); pump_ms(500); CHECK(tap(jump_x, bar_y) == 1, "jump icon consumed"); CHECK(mr_reader_ui_jump_open() == 1, "slider open"); reader_on_touch_down(sx + sw, slider_y); reader_on_tap(sx + sw, slider_y); pump_ms(900); CHECK(mr_reader_ui_jump_pending() == 0, "jump landed in webtoon"); { int d0 = mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws(); meow_ui_paint_full(); meow_ui_paint_full(); /* 条漫绘制路径覆盖(P2-5) */ CHECK(mr_reader_ui_blit_draws() + mr_reader_ui_scaled_draws() > d0, "draw path executed (webtoon)"); } CHECK(mr_reader_ui_page() >= BOOK_PAGES - 3, "jumped near last page (got %d)", mr_reader_ui_page()); CHECK(mr_reader_ui_resident_kb() <= 9 * 1024, "resident bounded in webtoon (%ldKB)", mr_reader_ui_resident_kb()); meow_view_page_pop(); meow_view_free(reader); meow_view_free(lib); meow_ui_shutdown(); rm_rf(root); rm_rf(data); printf("%s: %s\n", g_fail ? "FAIL" : "PASS", g_fail ? "see failures above" : "many-pages reader checks passed"); return g_fail ? 1 : 0; }