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 一键运行
This commit is contained in:
AskaEth committed 2026-11-04 21:49:33 +08:00
1 parent 8cbb240931
commit ff97f3cee7
14 files changed
+2539 -93

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
/* test_mode_persist.c — 阅读模式 + 每册续读状态持久化宿主测试(无 SDK/硬件)。
*
* 覆盖:
* P1 模式值:valid/name/parse(paged/webtoon,非法值回退);
* P2 册标识键:reader.page/mode.<FNV1a64(规范路径)>,末尾斜杠等价、不同册不同键、
* 键字符集安全([A-Za-z0-9._-]);
* P3 默认值:无记录时 get_mode/get_page 返回调用方给的默认值;
* P4 读写往返 + 册间隔离 + 一次 save 写两键;
* P5 坏值/非法模式忽略(不覆盖已有记录);写失败只 WARN 不崩;
* P6 书库角标:page>0 → "续读 Pn";page==0/无记录 → 无角标。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_mode_persist.c logic/reader.c \
* test/settings_reader_stub.c -o test/test_mode_persist
* 或 test/run_webtoon_tests.sh。
*/
#include "logic/reader.h"
#include "test/settings_reader_stub.h"
#include "include/meow_ui_config.h"
#include <ctype.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)
static void test_mode_values(void)
{
CHECK(mr_mode_valid(MR_MODE_PAGED) == 1, "paged valid");
CHECK(mr_mode_valid(MR_MODE_WEBTOON) == 1, "webtoon valid");
CHECK(mr_mode_valid(-1) == 0 && mr_mode_valid(2) == 0, "invalid modes");
CHECK(strcmp(mr_mode_name(MR_MODE_PAGED), "paged") == 0, "name paged");
CHECK(strcmp(mr_mode_name(MR_MODE_WEBTOON), "webtoon") == 0, "name webtoon");
CHECK(strcmp(mr_mode_name(99), "paged") == 0, "name invalid -> paged");
CHECK(mr_mode_parse("paged") == MR_MODE_PAGED, "parse paged");
CHECK(mr_mode_parse("webtoon") == MR_MODE_WEBTOON, "parse webtoon");
CHECK(mr_mode_parse("WEBTOON") == -1, "parse case sensitive");
CHECK(mr_mode_parse("") == -1 && mr_mode_parse(NULL) == -1, "parse empty/NULL");
}
static void test_keys(void)
{
char a[64], b[64], c[64];
CHECK(mr_cont_key("/sd/book1", "page", a, (int)sizeof a) == 0, "key ok");
CHECK(strncmp(a, "reader.page.", 12) == 0, "key prefix %s", a);
CHECK(strlen(a) == 12 + 16, "key len=%zu", strlen(a));
for (size_t i = 12; i < strlen(a); i++)
CHECK(isxdigit((unsigned char)a[i]) != 0, "key hex char");
CHECK(mr_cont_key("/sd/book1/", "page", b, (int)sizeof b) == 0, "key trailing slash");
CHECK(strcmp(a, b) == 0, "trailing slash normalizes: %s vs %s", a, b);
CHECK(mr_cont_key("/sd/book1", "mode", c, (int)sizeof c) == 0, "mode key");
CHECK(strncmp(c, "reader.mode.", 12) == 0, "mode prefix %s", c);
CHECK(strcmp(a, c) != 0, "field differs");
CHECK(mr_cont_key("/sd/book2", "page", c, (int)sizeof c) == 0, "book2 key");
CHECK(strcmp(a, c) != 0, "different books differ");
/* 参数/缓冲边界 */
CHECK(mr_cont_key(NULL, "page", a, (int)sizeof a) == -1, "NULL dir");
CHECK(mr_cont_key("", "page", a, (int)sizeof a) == -1, "empty dir");
CHECK(mr_cont_key("/a", NULL, a, (int)sizeof a) == -1, "NULL field");
CHECK(mr_cont_key("/a", "page", NULL, 32) == -1, "NULL out");
CHECK(mr_cont_key("/a", "page", a, 8) == -1 && a[0] == '\0', "small out");
}
static void test_defaults_and_roundtrip(void)
{
mr_test_cfg_clear();
CHECK(mr_cont_get_mode("/sd/a", MR_MODE_PAGED) == MR_MODE_PAGED, "default paged");
CHECK(mr_cont_get_mode("/sd/a", MR_MODE_WEBTOON) == MR_MODE_WEBTOON, "default param used");
CHECK(mr_cont_get_page("/sd/a", 0) == 0, "default page 0");
CHECK(mr_cont_get_page("/sd/a", 7) == 7, "default page param");
CHECK(mr_cont_badge("/sd/a", NULL, 0) == 0, "badge NULL safe");
char badge[24];
CHECK(mr_cont_badge("/sd/a", badge, (int)sizeof badge) == 0 && badge[0] == '\0',
"no record -> no badge");
mr_cont_set_mode("/sd/a", MR_MODE_WEBTOON);
CHECK(mr_cont_get_mode("/sd/a", MR_MODE_PAGED) == MR_MODE_WEBTOON, "mode roundtrip");
CHECK(mr_cont_get_mode("/sd/b", MR_MODE_PAGED) == MR_MODE_PAGED, "mode isolated per book");
mr_cont_set_page("/sd/a", 17);
CHECK(mr_cont_get_page("/sd/a", -1) == 17, "page roundtrip");
CHECK(mr_cont_get_page("/sd/b", -1) == -1, "page isolated per book");
CHECK(mr_cont_badge("/sd/a", badge, (int)sizeof badge) == 1, "badge shown");
CHECK(strcmp(badge, "续读 P18") == 0, "badge text %s", badge);
/* 一次 save 两键(模式 + 页码) */
mr_cont_save("/sd/c", 3, MR_MODE_WEBTOON);
CHECK(mr_cont_get_page("/sd/c", -1) == 3, "save page");
CHECK(mr_cont_get_mode("/sd/c", MR_MODE_PAGED) == MR_MODE_WEBTOON, "save mode");
CHECK(mr_cont_badge("/sd/c", badge, (int)sizeof badge) == 1 &&
strcmp(badge, "续读 P4") == 0, "save badge %s", badge);
/* page 0 视为「没有进度」,不出角标 */
mr_cont_save("/sd/d", 0, MR_MODE_PAGED);
CHECK(mr_cont_badge("/sd/d", badge, (int)sizeof badge) == 0 && badge[0] == '\0',
"page 0 -> no badge");
/* NULL/空路径安全 */
mr_cont_set_page(NULL, 5);
mr_cont_set_mode(NULL, MR_MODE_WEBTOON);
mr_cont_save(NULL, 1, 1);
mr_cont_save("", 1, 1);
CHECK(mr_cont_get_page(NULL, 9) == 9 && mr_cont_get_mode(NULL, MR_MODE_PAGED) == MR_MODE_PAGED,
"NULL dir safe");
}
static void test_bad_values(void)
{
mr_test_cfg_clear();
char key[64];
char buf[24];
/* 坏模式值 → 默认;坏页码(负数/非数字/超上限)→ 默认 */
CHECK(mr_cont_key("/sd/x", "mode", key, (int)sizeof key) == 0, "bad key");
mr_test_cfg_put(key, "vertical");
CHECK(mr_cont_get_mode("/sd/x", MR_MODE_PAGED) == MR_MODE_PAGED, "bad mode -> def");
CHECK(mr_cont_key("/sd/x", "page", key, (int)sizeof key) == 0, "bad page key");
mr_test_cfg_put(key, "-3");
CHECK(mr_cont_get_page("/sd/x", 11) == 11, "negative page -> def");
mr_test_cfg_put(key, "abc");
CHECK(mr_cont_get_page("/sd/x", 12) == 12, "non-numeric page -> def");
mr_test_cfg_put(key, "99999999");
CHECK(mr_cont_get_page("/sd/x", 13) == 13, "too-large page -> def");
mr_test_cfg_put(key, "0");
CHECK(mr_cont_get_page("/sd/x", 14) == 0, "zero page readable");
/* 非法模式值不覆盖已有记录 */
mr_cont_set_mode("/sd/y", MR_MODE_WEBTOON);
mr_cont_set_mode("/sd/y", 42);
CHECK(mr_cont_get_mode("/sd/y", MR_MODE_PAGED) == MR_MODE_WEBTOON, "invalid set ignored");
/* 写失败:只告警,内存保持旧值;不崩 */
mr_cont_save("/sd/z", 5, MR_MODE_PAGED);
int warns = mr_test_log_warns();
mr_test_cfg_fail_writes(1);
mr_cont_set_page("/sd/z", 9);
mr_cont_set_mode("/sd/z", MR_MODE_WEBTOON);
mr_cont_save("/sd/z", 10, MR_MODE_WEBTOON);
CHECK(mr_test_log_warns() > warns, "write failure warns (warns=%d)", mr_test_log_warns());
CHECK(mr_cont_get_page("/sd/z", -1) == 5, "failed write keeps old page");
CHECK(mr_cont_get_mode("/sd/z", MR_MODE_PAGED) == MR_MODE_PAGED, "failed write keeps old mode");
mr_test_cfg_fail_writes(0);
/* 写入后读回同一字符串(便于排查:paged/webtoon 明文) */
CHECK(mr_cont_key("/sd/y", "mode", key, (int)sizeof key) == 0, "read mode key");
CHECK(mr_test_cfg_get(key, buf, (int)sizeof buf) == 1 && strcmp(buf, "webtoon") == 0,
"stored mode string %s", buf);
}
int main(void)
{
test_mode_values();
test_keys();
test_defaults_and_roundtrip();
test_bad_values();
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}