- 条漫: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 一键运行
166 lines
7.4 KiB
C
166 lines
7.4 KiB
C
/* 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;
|
||
}
|