Files
MeowOS-SDK/tuku/test/test_open_book_host.c
T
AskaEth 4befbfe14c 图库: 书库点图改按文件名定位(修 P1 截断错位)+ 册间衔接根锁定(M18 评审 r1)
P1-1(会静默开错图):取消「书库从列表推导图片下标」。
- 新增 mr_reader_open_dir_at_name(dir, name):阅读器打开目录册后,在**自己刚枚举出的
  全量目录册**(mr_dir_book,与翻页同一份枚举)里按 basename 定位起始页(先精确,
  再大小写不敏感兜底;找不到退回续读/首页)。书库点击只传文件名。
- 删除 mr_library_image_index(不再有跨枚举的隐式契约)。
- 截断提示文案改为「仅显示前 2000 项(图片可在阅读器中翻到)」。
- 修复前(cb02722 探针,2100 张 4 位名夹具):rows=2000/truncated=1/
  library_images=2000/reader_pages=2100/**mismatched=590**(首行 p1412 → 打开 p1411);
  评审夹具为 1995/2000。修复后:2000 个可见图片行按名定位 **0 mismatch**,
  2100 页全量可翻;26 次真实 UI 点开(首尾各 3 + 20 个确定性伪随机)逐张命中。

连带处理(评审 r1):
1. 删掉 reader_page_enter 的 c->view 死写(reopen 判定用 activity active/depth);
   顺手删掉 library_page_create 里同款死写。
2. 阅读器册间衔接根锁定:新增 mr_path_within_root + mr_book_adjacent_rooted,
   book_switch 用浏览根(默认 MR_ROOT_DIR,测试可注入)钳制相邻册;
   根外的兄弟册按「没有相邻册」处理。test_cont_adjacent A7 验证「明文 _ex 会越根、
   rooted 拒绝、根内翻册不受影响」;test_mode_switch_host H6 验证条漫末页不越根。
3. test_reader_perf 不再依赖顶栏 tap 命中时序:模式切换改走测试钩子
   mr_reader_test_toggle_mode()(与顶栏动作同一个 mode_set),负载下稳定。
4. page_cache 注释去掉「缩略图缓存/缩略图线程」过时描述(只改文字)。

回归:run_tests.sh(library 81 / nav 74 / lazy 57 / perf 25 / … / open_book 含 2100 用例)
与 run_webtoon_tests.sh(含 A7/H6)全 PASS;交叉构建零警告。
2026-11-05 20:26:40 +08:00

364 lines
15 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 "logic/reader/page_source.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_open_row(int idx);
/* 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);
void mr_reader_open_dir_at_name(const char *dir, const char *name);
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;
}
/* 阅读器目录册用的假解码回调(本测试不经过它们取页)。 */
static meow_img *cap_fake_load(const char *path, void *ud, int maxdim)
{
(void)path; (void)ud; (void)maxdim;
return NULL;
}
static void cap_fake_unload(meow_img *img) { (void)img; }
/* 在阅读器的全量目录册里按文件名取页下标(与阅读器打开时同一份枚举)。 */
static int reader_page_of_name(const mr_dir_book *db, const char *name)
{
for (int i = 0; i < db->count; i++)
if (db->names[i] && strcmp(db->names[i], name) == 0) return i;
return -1;
}
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; }
}
/* Big:2100 张图(超过默认 MR_LIB_SCAN_MAX=2000,复现评审 P1-1 截断错位场景)。
* 1x1 BMP(stb 可解)以控制夹具体积/建盘时间。 */
char big[MR_PATH_MAX];
snprintf(big, sizeof big, "%s/Big", root);
if (mkdir_p(big) != 0) { printf("SKIP: mkdir Big failed\n"); return 0; }
for (int i = 1; i <= 2100; i++) {
snprintf(p, sizeof p, "%s/p%04d.bmp", big, i);
if (write_bmp24(p, 1, 1) != 0) { printf("SKIP: big 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();
/* ---- P1-1 回归:单目录 2100 张图(超过默认 cap=2000)不得「点 A 开 B」 ---- */
CHECK(library_test_load(root) == 1, "load root for cap regression");
tap_row("Big");
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "tap Big dir stays in library");
CHECK(strcmp(library_test_dir(), big) == 0, "entered Big (%s)", library_test_dir());
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1,
"Big rows=%d (cap + truncation row)", library_test_rows());
/* 截断提示行仍在,且文案不让人误以为「没有更多图」。 */
meow_list_item trunc_it;
CHECK(library_test_src(library_test_rows() - 1, &trunc_it) == 1 &&
trunc_it.enabled == 0 && trunc_it.title && strcmp(trunc_it.title, "已截断") == 0,
"truncation row present");
CHECK(trunc_it.subtitle && strstr(trunc_it.subtitle, "2000") &&
strstr(trunc_it.subtitle, "阅读器"),
"truncation text keeps images discoverable: %s",
trunc_it.subtitle ? trunc_it.subtitle : "");
/* 阅读器目录册是**全量** 2100 页(书库 cap 只是列表显示上限)。 */
mr_dir_book db;
memset(&db, 0, sizeof db);
CHECK(mr_dir_book_init(&db, big, 720, cap_fake_load, cap_fake_unload, NULL) == 2100,
"reader sees all 2100 pages");
/* 首 3 + 尾 3 + 20 个确定性伪随机行,逐一点开断言「点哪张开哪张」。 */
int picks[26], np = 0;
picks[np++] = 0;
picks[np++] = 1;
picks[np++] = 2;
picks[np++] = MR_LIB_SCAN_MAX - 3;
picks[np++] = MR_LIB_SCAN_MAX - 2;
picks[np++] = MR_LIB_SCAN_MAX - 1;
unsigned seed = 12345u;
for (int i = 0; i < 20; i++) {
seed = seed * 1103515245u + 12345u;
picks[np++] = (int)((seed >> 16) % MR_LIB_SCAN_MAX);
}
int mismatched = 0;
for (int i = 0; i < np; i++) {
int row = picks[i];
meow_list_item it;
memset(&it, 0, sizeof it);
CHECK(library_test_src(row, &it) == 1 && it.title, "pick %d: row source", row);
if (!it.title) continue;
int want_page = reader_page_of_name(&db, it.title);
CHECK(want_page >= 0, "pick %d: %s is a reader page", row, it.title);
CHECK(library_test_open_row(row) == 1, "open row %d", row);
CHECK(meow_ui_activity_active() == ACT_READER, "pick %d opens reader", row);
CHECK(strcmp(mr_reader_dir(), big) == 0, "pick %d reader dir", row);
CHECK(mr_reader_ui_count() == 2100, "pick %d reader pages=%d", row,
mr_reader_ui_count());
if (mr_reader_ui_page() != want_page) mismatched++;
CHECK(mr_reader_ui_page() == want_page,
"pick %d: clicked %s -> page %d (want %d)", row, it.title,
mr_reader_ui_page(), want_page);
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "pick %d back to library", row);
}
CHECK(mismatched == 0, "no click/page mismatch across %d picks", np);
/* 被书库截掉的图(第 2001 张起)在阅读器里仍可打开/翻到。 */
CHECK(reader_page_of_name(&db, "p2099.bmp") == 2098, "p2099 lives at page 2098");
mr_reader_open_dir_at_name(big, "p2099.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "truncated-out image opens reader");
CHECK(mr_reader_ui_count() == 2100, "truncated-out image: all pages present");
CHECK(mr_reader_ui_page() == 2098, "truncated-out image opens at its page (%d)",
mr_reader_ui_page());
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library after truncated image");
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1, "library list unchanged after reads");
mr_dir_book_free(&db);
/* 名字找不到(文件已改名/删除):退回续读/首页,不崩、不串册。 */
mr_reader_open_dir_at_name(pics, "does-not-exist.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "unknown name still opens reader");
CHECK(strcmp(mr_reader_dir(), pics) == 0, "unknown name keeps target dir");
CHECK(mr_reader_ui_count() == 6 && mr_reader_ui_open_err() == 0,
"unknown name falls back safely (pages=%d err=%d)",
mr_reader_ui_count(), mr_reader_ui_open_err());
CHECK(mr_reader_ui_page() >= 0 && mr_reader_ui_page() < 6,
"unknown name fallback page in range (%d)", mr_reader_ui_page());
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;
}