Files
MeowOS-SDK/tuku/test/test_mode_switch_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

304 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_mode_switch_host.c — 阅读器模式/续读/跳页/册间衔接的宿主集成测试(可选,需 SDK 源码树)。
*
* 只做纯逻辑测试覆盖不到的接缝:真实 SDK view/activity/widget + 真实图片解码 + 真实
* meow_scroll/meow_anim 节拍。用 40x60 BMP 当页图(stb 可解,无外部依赖),在临时目录
* 建两本相邻册,然后模拟用户操作:
* H1 进入:默认翻页模式、页码 0、控件归属 ACT_READER(顶栏在 on_enter 建);
* H2 chrome 图标一键切模式:翻页 ⇄ 条漫,切换立即写 meow_config(每册记忆);
* H3 跳页滑块:展开 → 拖到末页 → 目标页解码完成后落位(不是先白屏再跳);
* H4 离开/重进:恢复上次页 + 模式(续读);
* H5 册间衔接:条漫末页上拉 → 下一册首页;首页下拉 → 上一册末页;首册再无上一册时
* 停在原地(不退出、不崩);
* H6 册间衔接根锁定:浏览根内没有相邻册时,不跨到根外的兄弟册(M18 评审观察项)。
*
* 编译:test/run_webtoon_tests.sh(需 SDK 源码树;只发布 meow_ui.a 时自动 SKIP)。
*/
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "include/meow_ui_anim.h"
#include "meow_ui_internal.h" /* MEOW_EV_TAP / MEOW_EV_SWIPE(宿主测试内部头) */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
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)
/* app.c 的导航上报在测试里不需要网络侧实现。 */
void mr_nav_report(void) {}
static int g_lib_enters;
static void lib_enter(meow_view *v, void *c) { (void)v; (void)c; g_lib_enters++; }
static void lib_leave(meow_view *v, void *c) { (void)v; (void)c; }
/* ---- 临时目录 + 最小 BMP24(stb 可解) ---- */
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);
}
/* 驱动 SDK 动画节拍(宿主没有 meow_ui_run 主循环)。 */
static void pump_ms(int ms)
{
for (int i = 0; i < ms / 10; i++) {
usleep(10 * 1000);
meow_anim_tick();
}
}
/* 模拟 app.c 的点击路由:on_touch_down → on_tap(阅读页 tap 不在 view on_event 里)。 */
static int tap(int x, int y)
{
reader_on_touch_down(x, y);
reader_on_tap(x, y);
return 1;
}
static int swipe(int dir, int dist)
{
return meow_view_dispatch_event(MEOW_EV_SWIPE, dir, dist);
}
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_mode_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], book1[MR_PATH_MAX], book2[MR_PATH_MAX];
snprintf(root, sizeof root, "./mr_mode_books_%ld", (long)getpid());
snprintf(book1, sizeof book1, "%s/vol1", root);
snprintf(book2, sizeof book2, "%s/vol2", root);
mkdir_p(book1);
mkdir_p(book2);
for (int b = 0; b < 2; b++) {
const char *dir = b ? book2 : book1;
for (int i = 0; i < 3; i++) {
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/p%d.bmp", dir, i + 1);
if (write_bmp24(p, 40, 60) != 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 = { .on_enter = lib_enter, .on_leave = lib_leave };
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);
/* 阅读器册间衔接的浏览根:测试夹具根(生产默认 MR_ROOT_DIR)。 */
mr_reader_test_set_root(root);
CHECK(strcmp(mr_reader_test_root(), root) == 0, "reader browse root installed");
/* TopAppBar 动作格(位图):右起每格 48px;动作 0=模式 / 1=跳页 / 2=fit。 */
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;
/* ---- H1 进入:翻页模式、第 1 页 ---- */
mr_reader_open_dir(book1);
CHECK(meow_ui_activity_active() == ACT_READER, "reader active after open");
CHECK(mr_reader_ui_count() == 3, "book pages=%d", mr_reader_ui_count());
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "default mode is paged");
CHECK(mr_reader_ui_page() == 0, "start at page 0");
pump_ms(300);
/* ---- H2 模式切换:chrome 图标一键切,立即持久化 ---- */
CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed");
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon");
CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_WEBTOON,
"webtoon mode persisted per book");
CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed (back)");
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "switched back to paged");
CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_PAGED, "paged mode persisted");
CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed (again)");
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon again for continuity");
/* ---- H3 跳页:滑块展开 → 拖到末页 → 预解码完成后落位 ---- */
CHECK(tap(jump_x, bar_y) == 1, "jump icon tap 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);
CHECK(mr_reader_ui_jump_open() == 0, "slider closed after pick");
CHECK(mr_reader_ui_jump_pending() == 1 || mr_reader_ui_page() == 2,
"jump pending (waiting for target decode)");
pump_ms(600);
CHECK(mr_reader_ui_jump_pending() == 0, "jump landed");
CHECK(mr_reader_ui_page() == 2, "jumped to last page (got %d)", mr_reader_ui_page());
/* ---- H4 续读:离开保存 page+mode,重进恢复 ---- */
CHECK(swipe(MEOW_SWIPE_LEFT, 200) == 0, "reader leaves back-swipe to app.c");
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library");
CHECK(mr_cont_get_page(book1, -1) == 2, "last page saved (got %d)",
mr_cont_get_page(book1, -1));
CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_WEBTOON, "mode saved");
CHECK(g_lib_enters >= 2, "library re-entered");
mr_reader_open_dir(book1);
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "mode restored on reopen");
CHECK(mr_reader_ui_page() == 2, "page restored on reopen (got %d)", mr_reader_ui_page());
pump_ms(300);
/* ---- H5 册间衔接:条漫末页上拉 → 下一册首页 ---- */
reader_on_touch_down(240, 600);
for (int i = 1; i <= 6; i++) {
reader_on_touch_move(240, 600 - i * 40);
usleep(5 * 1000);
}
CHECK(swipe(MEOW_SWIPE_UP, 240) == 1, "webtoon up-swipe consumed");
CHECK(strcmp(mr_reader_dir(), book2) == 0, "advanced to next book (got %s)", mr_reader_dir());
CHECK(mr_reader_ui_page() == 0, "next book starts at page 0");
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "mode carried into fresh book");
pump_ms(300);
/* 首页下拉 → 上一册末页 */
reader_on_touch_down(240, 300);
for (int i = 1; i <= 6; i++) {
reader_on_touch_move(240, 300 + i * 40);
usleep(5 * 1000);
}
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "webtoon down-swipe consumed");
CHECK(strcmp(mr_reader_dir(), book1) == 0, "back to previous book (got %s)", mr_reader_dir());
CHECK(mr_reader_ui_page() == 2, "previous book lands at last page (got %d)",
mr_reader_ui_page());
pump_ms(300);
/* 跳到首页后,再往上一册:没有相邻册,停在原地并提示(不退出/不崩)。 */
CHECK(tap(jump_x, bar_y) == 1, "jump icon tap consumed (to first page)");
CHECK(mr_reader_ui_jump_open() == 1, "slider open again");
reader_on_touch_down(sx + 2, slider_y);
reader_on_tap(sx + 2, slider_y);
pump_ms(500);
CHECK(mr_reader_ui_page() == 0, "jumped to first page (got %d)", mr_reader_ui_page());
reader_on_touch_down(240, 300);
for (int i = 1; i <= 6; i++) {
reader_on_touch_move(240, 300 + i * 40);
usleep(5 * 1000);
}
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "edge down-swipe consumed");
CHECK(strcmp(mr_reader_dir(), book1) == 0, "no previous book: stay put");
CHECK(meow_ui_activity_active() == ACT_READER, "no previous book: still in reader (no exit)");
/* ---- H6 册间衔接根锁定:浏览根=book2 时,兄弟册 book1 在根外,不得翻过去 ---- */
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library for root-lock case");
mr_reader_test_set_root(book2);
CHECK(strcmp(mr_reader_test_root(), book2) == 0, "reader root narrowed to book2");
mr_reader_open_dir_at(book2, 0); /* 显式首页,避开续读页差异 */
CHECK(meow_ui_activity_active() == ACT_READER, "reopened book2");
CHECK(strcmp(mr_reader_dir(), book2) == 0, "at book2");
if (mr_reader_ui_mode() != MR_MODE_WEBTOON) mr_reader_test_toggle_mode();
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon for root-lock case");
CHECK(mr_reader_ui_page() == 0, "root-lock case at first page (got %d)",
mr_reader_ui_page());
/* 首页下拉 → book1 是 book2 的上一册,但在浏览根外:必须原地不动。 */
reader_on_touch_down(240, 300);
for (int i = 1; i <= 6; i++) {
reader_on_touch_move(240, 300 + i * 40);
usleep(5 * 1000);
}
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "root-lock down-swipe consumed");
CHECK(strcmp(mr_reader_dir(), book2) == 0,
"adjacent book outside browse root is refused (dir=%s)", mr_reader_dir());
CHECK(meow_ui_activity_active() == ACT_READER, "root-lock: still in reader");
mr_reader_test_set_root(root); /* 复原夹具根 */
/* ---- 收尾 ---- */
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "popped back to library");
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" : "reader mode/continuity integration checks passed");
return g_fail ? 1 : 0;
}