Files
MeowOS-SDK/tuku/test/test_reader_many_pages.c
T
AskaEth 11e09af7a4 图库: 修 M27 评审 P1 门控旁路 + 三个 P2(r1)
P1-1:判定为 backend 的页不再无条件放行 + 后端中途失败不再静默回退 stb。
- meow_img_load_file_step_ex 增加 allow_stb_fallback 参数:0 = 只试后端,
  失败返回 NULL(坏图占位),绝不回落 stb。阅读器按 dec.path 传入。
- 旧行为实测:截断 12MP JPEG 18,403kB、3000×3000 24bpp 头-only BMP 36,056kB、
  非法位深 PNG 16,409kB(stb 都"成功"出图)。修复后:
  截断 JPEG 275kB→NULL、头-only BMP 908kB→NULL、非法位深 PNG 门控占位(0 分配)。

P2-1:PNG 支持矩阵补 png_depth_ok 同式(color 2/4/6 仅 8/16;color 3 无 16;
color 0 允许 1/2/4/8/16),不再出现「矩阵说支持、后端拒绝」。
P2-2:后端命中页也保留两道安全阀:MemAvailable<6MB 占位、估算 >12MB 硬上限占位。
P2-3:B8 指标改为「窗口稳定后只 reset 一次」,量整个 30 翻窗口内相对稳态基线的
最大存活增量(淘汰不再掩盖峰值),并打印稳态/驻留/增量。
P2-4:page_source.h 的估算精度口径限定为「后端命中/典型页」。

回归:
- B7c 新增截断 12MP JPEG / 24bpp 头-only BMP / 非法位深 PNG,断言 backend-only
  NULL 且峰值 ≤3MB、或 stb 门控占位;B7 典型页与 B7b 回退矩阵保持全绿。
- test_reader_many_pages 恢复 24bpp 头-only 夹具(1bpp 保留):24bpp 判 failed、
  1bpp 判 skipped;断言 failed=2 / skipped=1。
- run_tests.sh / run_webtoon_tests.sh EXIT=0;sdk/test_meow_img_stream ALL PASS;
  交叉构建(SDK + tuku)零警告。
2026-11-06 11:46:16 +08:00

332 lines
13 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_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 <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#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;
}