Files
MeowOS-SDK/tuku/test/test_jump_page.c
T
AskaEth ff97f3cee7 feat(tuku): M7 条漫模式与续读——模式一键切换/每册记忆、视口±1 懒解码、滑块跳页、册间衔接
- 条漫:meow_scroll 跟手+fling,按滚动位置懒解码视口±1 页(驻留上限 4 页);
  排版用 mr_book_meta 累加适宽高度(nav.c 纯逻辑,宿主可测)
- 模式:阅读 chrome 图标一键切翻页/条漫;每册模式与 last page 经 meow_config 持久化
  (reader.mode/page.<FNV1a64(册路径)>),重进该册恢复模式与页码
- 续读:离开/翻页/滚动停止节流落盘;书库扫描给含图册副标题追加「· 续读 Pn」
- 册间衔接:末页前进→同父目录下一含图册首页,首页后退→上一册末页;无相邻册轻提示不退出
- 跳页:chrome 内 meow_slider,目标页预解码完成后落位(2.5s 超时降级提示)
- 解码:改走 M12 预算解码 meow_img_load_file_dim;超大 PNG/BMP 先 probe 预判,
  超 40MiB 原始 RGBA 跳过并显示「图片无法显示」,12MiB 以上记 WARN(如实保留残余风险)
- 测试:新增条漫排版/跳页状态/模式续读持久化/册间边界宿主测试,以及真实 SDK 源码树上的
  阅读器集成测试(模式切换/跳页落位/续读/跨册),test/run_webtoon_tests.sh 一键运行
2026-11-04 21:49:33 +08:00

141 lines
5.2 KiB
C

/* test_jump_page.c — 跳页状态机 + 条漫落位映射宿主测试(无 SDK/硬件)。
*
* 覆盖:
* J1 跳页状态:begin 夹取/非法取消、active/target、预解码就绪落位、超时降级落位;
* J2 落位映射:mr_strip_offset_for_page 与 page_at 互逆(首/中/末页,max 夹取);
* J3 边界:空册/越界/重复取消;正常等待不提前落位(不白屏——没有图就不落)。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_jump_page.c logic/reader/nav.c \
* -o test/test_jump_page && ./test/test_jump_page
* 或 test/run_webtoon_tests.sh。
*/
#include "logic/reader/nav.h"
#include <stdio.h>
#include <string.h>
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
#define VW 480
#define VH 720
#define TIMEOUT 2500ULL
static int g_meta_w[8], g_meta_h[8], g_meta_n;
static int meta_cb(void *ctx, int i, int *w, int *h)
{
(void)ctx;
if (i < 0 || i >= g_meta_n) return 0;
*w = g_meta_w[i];
*h = g_meta_h[i];
return 1;
}
static void build_strip(mr_strip *s, int pages)
{
memset(s, 0, sizeof *s);
g_meta_n = pages;
for (int i = 0; i < pages; i++) {
g_meta_w[i] = 800;
g_meta_h[i] = 1200; /* 适宽后每页 720px */
}
CHECK(mr_strip_build(s, pages, VW, VH, meta_cb, NULL) == 0, "build strip pages=%d", pages);
}
static void test_jump_state(void)
{
mr_jump j;
memset(&j, 0, sizeof j);
CHECK(mr_jump_active(&j) == 0, "fresh inactive");
CHECK(mr_jump_target(&j) == -1, "fresh target -1");
/* 正常:开始 → 未就绪继续等 → 就绪落位 */
mr_jump_begin(&j, 4, 10, 100);
CHECK(mr_jump_active(&j) == 1 && mr_jump_target(&j) == 4, "begin active target=4");
int to = -1;
CHECK(mr_jump_poll(&j, 0, 200, TIMEOUT, &to) == 0 && to == 0, "waiting no timeout");
CHECK(mr_jump_active(&j) == 1, "still active while waiting");
CHECK(mr_jump_poll(&j, 1, 300, TIMEOUT, &to) == 1 && to == 0, "ready lands");
CHECK(mr_jump_active(&j) == 0 && mr_jump_target(&j) == -1, "cleared after land");
/* 超时:仍然落位,但标记降级 */
mr_jump_begin(&j, 2, 10, 1000);
CHECK(mr_jump_poll(&j, 0, 1000 + TIMEOUT - 1, TIMEOUT, &to) == 0, "just before timeout waits");
CHECK(mr_jump_poll(&j, 0, 1000 + TIMEOUT, TIMEOUT, &to) == 1 && to == 1, "timeout lands");
CHECK(mr_jump_active(&j) == 0, "timeout clears");
/* 就绪与超时同时成立:优先算正常就绪 */
mr_jump_begin(&j, 1, 10, 0);
CHECK(mr_jump_poll(&j, 1, 999999, TIMEOUT, &to) == 1 && to == 0,
"ready wins over timeout");
/* timeout=0 表示不限时 */
mr_jump_begin(&j, 1, 10, 0);
CHECK(mr_jump_poll(&j, 0, 999999999ULL, 0, &to) == 0 && to == 0, "no timeout when 0");
/* begin 夹取/非法 */
mr_jump_begin(&j, -1, 10, 0);
CHECK(mr_jump_active(&j) == 0, "begin(-1) cancels");
mr_jump_begin(&j, 10, 10, 0);
CHECK(mr_jump_active(&j) == 0, "begin(count) cancels");
mr_jump_begin(&j, 0, 0, 0);
CHECK(mr_jump_active(&j) == 0, "begin empty book cancels");
/* 取消幂等;NULL 安全 */
mr_jump_begin(&j, 3, 10, 5);
mr_jump_cancel(&j);
mr_jump_cancel(&j);
CHECK(mr_jump_active(&j) == 0, "cancel idempotent");
CHECK(mr_jump_poll(NULL, 1, 1, TIMEOUT, &to) == 0, "poll NULL");
mr_jump_cancel(NULL);
mr_jump_begin(NULL, 0, 10, 0);
/* timed_out 可为 NULL */
mr_jump_begin(&j, 5, 10, 0);
CHECK(mr_jump_poll(&j, 0, TIMEOUT + 1, TIMEOUT, NULL) == 1, "poll NULL timed_out");
}
static void test_land_mapping(void)
{
mr_strip s;
build_strip(&s, 5);
CHECK(mr_strip_total(&s) == 3600, "total=%d", mr_strip_total(&s));
CHECK(mr_strip_max_offset(&s) == 2880, "max=%d", mr_strip_max_offset(&s));
int expect_top[5] = { 0, 720, 1440, 2160, 2880 };
for (int i = 0; i < 5; i++) {
int off = mr_strip_offset_for_page(&s, i);
CHECK(off == expect_top[i], "page %d offset %d (want %d)", i, off, expect_top[i]);
CHECK(mr_strip_page_at(&s, off) == i, "page_at(offset(%d)) == %d", i, i);
}
/* 末页顶端恰在 max(整页铺满视口),落位不会越界 */
CHECK(mr_strip_offset_for_page(&s, 4) == mr_strip_max_offset(&s), "last page == max");
/* page+1 的偏移等于 page 的末端(页间无缝) */
CHECK(mr_strip_top(&s, 1) == mr_strip_top(&s, 0) + mr_strip_page_h(&s, 0),
"page tops contiguous");
/* 跳页目标页必须在解码窗里(否则会被淘汰):窗口只给 1 槽时也能覆盖目标 */
int f = -1, l = -1;
CHECK(mr_strip_window(&s, 0, 1, &f, &l) == 1 && f == l, "single-slot window");
mr_strip_free(&s);
/* 空册:没有可落位目标 */
mr_strip e;
memset(&e, 0, sizeof e);
CHECK(mr_strip_offset_for_page(&e, 0) == -1, "empty book no offset");
CHECK(mr_strip_page_at(&e, 0) == -1, "empty book no page");
}
int main(void)
{
test_jump_state();
test_land_mapping();
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}