Files
MeowOS-SDK/tuku/test/test_cont_adjacent.c
T
AskaEth 9f34417c49 图库: 单层浏览 + 懒判定与 M7/M11 整合;探测缓存 P2 修复(M13)
rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接:
- 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages
  时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注;
  系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测
- 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle /
  entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入;
  档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算
- 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency
  恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏);
  mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标
- mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU +
  互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存
  (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测)
- UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11);
  行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、
  不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表;
  顶栏回根命中加 g_tb_home_idx>=0 防御
- tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出)
- 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf /
  browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6
  改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书);
  run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests)
- 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS;
  worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
2026-11-05 12:23:31 +08:00

289 lines
11 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 书库扫描接线(M13 两段式):scan 只列单层、副标题占位「…」;
* probe/on_tick 补位后出现页数与「· 续读 Pn」角标。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_cont_adjacent.c logic/library.c logic/reader.c \
* test/settings_reader_stub.c -pthread -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");
}
/* 模拟 UI 后台探测:逐行单层 probe 并补位(page>0 的书行由 format_subtitle 拼续读角标)。 */
static void probe_all_rows(mr_library *lib)
{
for (int i = 0; i < lib->count; i++) {
mr_library_probe p;
if (mr_library_probe_cached(lib->items[i].path, &p) == 0)
mr_library_apply_probe(lib, i, &p);
else
mr_library_mark_error(lib, i);
}
}
/* A6 两段式(M13):scan 只做单层枚举、行副标题是占位「…」;后台 probe/on_tick 补位后
* 才有页数与「· 续读 Pn」角标。 */
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(e->probe_state == MR_LIB_PENDING && e->pages == -1,
"vol1 pending after scan");
CHECK(strcmp(e->subtitle, "…") == 0, "vol1 placeholder subtitle=%s", e->subtitle);
}
if (strcmp(e->name, "vol2") == 0) {
seen2 = 1;
CHECK(e->probe_state == MR_LIB_PENDING && strcmp(e->subtitle, "…") == 0,
"vol2 placeholder subtitle=%s", e->subtitle);
}
}
CHECK(seen1 == 1, "vol1 listed");
CHECK(seen2 == 1, "vol2 listed");
/* 第二段:后台探测补位后,页数与续读角标出现。 */
probe_all_rows(&lib);
seen1 = 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(e->has_images == 1 && e->pages == 1, "vol1 probed pages=%d", e->pages);
CHECK(strstr(e->subtitle, "page") != NULL, "vol1 keeps pages text: %s", e->subtitle);
CHECK(strstr(e->subtitle, "续读 P5") != NULL, "vol1 subtitle=%s", e->subtitle);
}
if (strcmp(e->name, "vol2") == 0) {
seen2 = 1;
CHECK(e->has_images == 1 && e->pages == 1, "vol2 probed pages=%d", e->pages);
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 after probe");
CHECK(seen2 == 1, "vol2 listed after probe");
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");
probe_all_rows(&lib);
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;
}