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

250 lines
9.2 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_cont_adjacent.c — 册间顺序边界 + 书库续读角标宿主测试(无 SDK/硬件)。
*
* 覆盖:
* A1 「直接含图」判定:含图目录=书;只有子目录含图/空目录/仅非图文件=不是书;
* A2 兄弟册排序:同父目录、书库自然序(vol2 < vol10);首/末册无相邻返回 0;
* A3 边界:末页前进→下一册、首页后退→上一册(调用方语义);路径末尾斜杠等价;
* A4 排除:隐藏/系统目录、符号链接、普通文件不参与兄弟册;
* A5 参数:delta 非 ±1、NULL/小缓冲、不存在的目录;
* A6 书库扫描接线:有续读记录的册,行副标题出现「· 续读 Pn」角标。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_cont_adjacent.c logic/library.c logic/reader.c \
* test/settings_reader_stub.c -o test/test_cont_adjacent
* 或 test/run_webtoon_tests.sh。
*/
#include "logic/library.h"
#include "logic/reader.h"
#include "test/settings_reader_stub.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.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 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 touch_file(const char *path)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite("x", 1, 1, f) == 1;
fclose(f);
return ok ? 0 : -1;
}
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);
}
static void joinp(char *out, int outsz, const char *a, const char *b)
{
snprintf(out, (size_t)outsz, "%s/%s", a, b);
}
static void test_has_images(const char *root)
{
char p[MR_PATH_MAX];
CHECK(mr_library_dir_has_images(root) == 1,
"root has a loose image file (scan/adjacency only treats subdirs as books)");
joinp(p, sizeof p, root, "vol1");
CHECK(mr_library_dir_has_images(p) == 1, "vol1 is a book");
joinp(p, sizeof p, root, "dirs_only");
CHECK(mr_library_dir_has_images(p) == 0, "dirs_only is not a book");
joinp(p, sizeof p, root, "empty");
CHECK(mr_library_dir_has_images(p) == 0, "empty is not a book");
joinp(p, sizeof p, root, "txonly");
CHECK(mr_library_dir_has_images(p) == 0, "text-only dir is not a book");
CHECK(mr_library_dir_has_images("/no/such/dir/xyz") == 0, "missing dir");
CHECK(mr_library_dir_has_images(NULL) == 0 && mr_library_dir_has_images("") == 0,
"NULL/empty safe");
}
static void expect_neighbor(const char *dir, int delta, const char *want, const char *tag)
{
char out[MR_PATH_MAX];
int r = mr_book_adjacent(dir, delta, out, (int)sizeof out);
if (!want) {
CHECK(r == 0 && out[0] == '\0', "%s: no neighbor (r=%d out=%s)", tag, r, out);
} else {
CHECK(r == 1 && strcmp(out, want) == 0, "%s: got %s (want %s)", tag, out, want);
}
}
static void test_adjacent(const char *root)
{
char vol1[MR_PATH_MAX], vol2[MR_PATH_MAX], vol10[MR_PATH_MAX];
char series_a[MR_PATH_MAX], series_b[MR_PATH_MAX];
char dirs_only[MR_PATH_MAX], out[MR_PATH_MAX];
joinp(vol1, sizeof vol1, root, "vol1");
joinp(vol2, sizeof vol2, root, "vol2");
joinp(vol10, sizeof vol10, root, "vol10");
joinp(series_a, sizeof series_a, root, "series/a");
joinp(series_b, sizeof series_b, root, "series/b");
joinp(dirs_only, sizeof dirs_only, root, "dirs_only");
/* 自然序:vol1 < vol2 < vol10(vol0 是符号链接,不参与) */
expect_neighbor(vol1, 1, vol2, "vol1 next");
expect_neighbor(vol2, 1, vol10, "vol2 next");
expect_neighbor(vol10, 1, NULL, "vol10 next (end)");
expect_neighbor(vol1, -1, NULL, "vol1 prev (start)");
expect_neighbor(vol2, -1, vol1, "vol2 prev");
expect_neighbor(vol10, -1, vol2, "vol10 prev");
/* 末尾斜杠等价 */
char slash[MR_PATH_MAX];
snprintf(slash, sizeof slash, "%s/", vol2);
expect_neighbor(slash, 1, vol10, "trailing slash next");
expect_neighbor(slash, -1, vol1, "trailing slash prev");
/* 嵌套父目录:series/a <-> series/b */
expect_neighbor(series_a, 1, series_b, "series a next");
expect_neighbor(series_b, -1, series_a, "series b prev");
expect_neighbor(series_b, 1, NULL, "series b next (end)");
/* 不是书的目录:不在兄弟表中 */
expect_neighbor(dirs_only, 1, NULL, "dirs_only not a book");
expect_neighbor(dirs_only, -1, NULL, "dirs_only not a book prev");
/* 路径参数:delta/缓冲/不存在 */
CHECK(mr_book_adjacent(vol1, 0, out, (int)sizeof out) == -1, "delta 0 invalid");
CHECK(mr_book_adjacent(vol1, 2, out, (int)sizeof out) == -1, "delta 2 invalid");
CHECK(mr_book_adjacent(vol1, -2, out, (int)sizeof out) == -1, "delta -2 invalid");
CHECK(mr_book_adjacent(NULL, 1, out, (int)sizeof out) == -1, "NULL dir");
CHECK(mr_book_adjacent(vol1, 1, NULL, 32) == -1, "NULL out");
CHECK(mr_book_adjacent(vol1, 1, out, 4) == 0, "small out -> no fit (0)");
CHECK(mr_book_adjacent("/no/such/book", 1, out, (int)sizeof out) == 0,
"missing parent -> 0");
CHECK(mr_book_adjacent("/", 1, out, (int)sizeof out) == -1, "root path invalid");
}
static void test_library_badge(const char *root)
{
mr_test_cfg_clear();
char vol1[MR_PATH_MAX], vol2[MR_PATH_MAX];
joinp(vol1, sizeof vol1, root, "vol1");
joinp(vol2, sizeof vol2, root, "vol2");
/* vol1 有续读记录(第 5 页),vol2 没有 */
mr_cont_save(vol1, 4, MR_MODE_WEBTOON);
mr_library lib;
memset(&lib, 0, sizeof lib);
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
CHECK(n > 0, "library scan n=%d", n);
int seen1 = 0, seen2 = 0;
for (int i = 0; i < lib.count; i++) {
mr_library_entry *e = &lib.items[i];
if (strcmp(e->name, "vol1") == 0) {
seen1 = 1;
CHECK(strstr(e->subtitle, "续读 P5") != NULL, "vol1 subtitle=%s", e->subtitle);
CHECK(strstr(e->subtitle, "page") != NULL, "vol1 keeps pages text: %s", e->subtitle);
}
if (strcmp(e->name, "vol2") == 0) {
seen2 = 1;
CHECK(strstr(e->subtitle, "续读") == NULL, "vol2 no badge: %s", e->subtitle);
CHECK(strcmp(e->subtitle, "1 page") == 0, "vol2 subtitle=%s", e->subtitle);
}
}
CHECK(seen1 == 1, "vol1 listed");
CHECK(seen2 == 1, "vol2 listed");
mr_library_free(&lib);
/* 记录清除后角标消失 */
mr_cont_save(vol1, 0, MR_MODE_WEBTOON);
memset(&lib, 0, sizeof lib);
CHECK(mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH) > 0, "rescan");
for (int i = 0; i < lib.count; i++)
if (strcmp(lib.items[i].name, "vol1") == 0)
CHECK(strstr(lib.items[i].subtitle, "续读") == NULL, "cleared badge gone");
mr_library_free(&lib);
}
int main(void)
{
char tmpl[] = "./mr_cont_adj_XXXXXX";
char *root = mkdtemp(tmpl);
CHECK(root != NULL, "mkdtemp");
if (!root) return 1;
char p[MR_PATH_MAX];
const char *books[] = { "vol1", "vol2", "vol10", "series/a", "series/b", "dirs_only/inside" };
for (unsigned i = 0; i < sizeof books / sizeof books[0]; i++) {
joinp(p, sizeof p, root, books[i]);
CHECK(mkdir_p(p) == 0, "mkdir %s", p);
char img[MR_PATH_MAX];
joinp(img, sizeof img, p, "001.png");
CHECK(touch_file(img) == 0, "image %s", img);
}
joinp(p, sizeof p, root, "empty");
mkdir_p(p);
joinp(p, sizeof p, root, "txonly");
mkdir_p(p);
joinp(p, sizeof p, root, "txonly/readme.txt");
touch_file(p);
joinp(p, sizeof p, root, ".hidden");
mkdir_p(p);
joinp(p, sizeof p, root, ".hidden/001.png");
touch_file(p);
joinp(p, sizeof p, root, "MeowOS");
mkdir_p(p);
joinp(p, sizeof p, root, "MeowOS/001.png");
touch_file(p);
joinp(p, sizeof p, root, "loose.png");
touch_file(p);
char vol1[MR_PATH_MAX], vol0[MR_PATH_MAX];
joinp(vol1, sizeof vol1, root, "vol1");
joinp(vol0, sizeof vol0, root, "vol0");
if (symlink(vol1, vol0) == 0) { /* 有权限才测;无权限时 symlink 失败也不影响上面的断言 */ }
test_has_images(root);
test_adjacent(root);
test_library_badge(root);
rm_rf(root);
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}