/* test_mode_persist.c — 阅读模式 + 每册续读状态持久化宿主测试(无 SDK/硬件)。 * * 覆盖: * P1 模式值:valid/name/parse(paged/webtoon,非法值回退); * P2 册标识键:reader.page/mode.,末尾斜杠等价、不同册不同键、 * 键字符集安全([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 #include #include 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; }