Files
MeowOS-SDK/tuku/test/test_cont_adjacent.c
T
AskaEth 64336f95d9 图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(M18)
用户反馈四件事一起改:
- 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路
  (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 +
  .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片=
  mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。
- 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内
  缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期
  生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。
  2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次
  (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。
- 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏
  「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。
- 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态
  「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。

集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/
普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比
mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。

reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push,
走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。

回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
2026-11-05 20:26:40 +08:00

248 lines
9.3 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 书库行模型(M18):目录+图片+档案册同列、目录优先;副标题扫描期生成,
* 不再有「· 续读 Pn」角标(角标随「目录自动当书」链路删除,续读仍由 reader 恢复)。
*
* 编译(在 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");
}
/* A6 文件管理器行模型:扫描即出最终行(无占位/无后台补位/无续读角标);
* 目录优先;当前层的图片行带图片下标(点图 → reader 起始页)。 */
static void test_library_rows(const char *root)
{
mr_test_cfg_clear();
char vol1[MR_PATH_MAX];
joinp(vol1, sizeof vol1, root, "vol1");
/* vol1 有续读记录(第 5 页)——书库行不应出现角标。 */
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);
if (n > 0)
CHECK(lib.items[0].kind == MR_LIB_KIND_DIR, "dirs first: %s", lib.items[0].name);
int seen1 = 0, seen_img = 0;
for (int i = 0; i < lib.count; i++) {
const mr_library_entry *e = &lib.items[i];
CHECK(e->subtitle[0] && strcmp(e->subtitle, "…") != 0,
"%s subtitle pre-generated: %s", e->name, e->subtitle);
CHECK(strstr(e->subtitle, "续读") == NULL, "%s has no badge: %s",
e->name, e->subtitle);
if (strcmp(e->name, "vol1") == 0) {
seen1 = 1;
CHECK(e->kind == MR_LIB_KIND_DIR, "vol1 is a dir row");
}
if (strcmp(e->name, "loose.png") == 0) {
seen_img = 1;
CHECK(e->kind == MR_LIB_KIND_IMAGE, "loose.png is an image row");
CHECK(mr_library_image_index(&lib, i) == 0, "loose.png is image #0");
}
}
CHECK(seen1 == 1, "vol1 listed");
CHECK(seen_img == 1, "loose image listed");
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_rows(root);
rm_rf(root);
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}