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

282 lines
12 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_library.c — 图库 logic 层宿主测试(纯 C,无 SDK/硬件)。
*
* 覆盖(M18 文件管理器模型):
* T1 自然排序:数字段数值序(page2<page10)、前导零、数字/字母混合、大小写确定性;
* T2 扩展名过滤:图片 png/jpg/jpeg/bmp 与档案册 cbz/zip 大小写不敏感;gif/txt 不支持;
* T3 单层扫描:目录 + 受支持图片 + 档案册混排(目录优先、组内自然序);
* 其它普通文件/隐藏项/符号链接不列;副标题扫描期生成一次(无占位/无懒判定);
* 图片行下标(mr_library_image_index)与行类型;不可读路径 -> -1;max_depth 忽略;
* T4 阅读方向状态(logic/reader.c)。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. -I../sdk test/test_library.c logic/library.c logic/reader.c \
* test/settings_reader_stub.c -pthread -o build/host/test_library
* 或直接 test/run_tests.sh。
*/
#include "logic/library.h"
#include "logic/reader.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;
fputs("x", f);
return fclose(f);
}
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 const mr_library_entry *find_entry(const mr_library *lib, const char *name)
{
for (int i = 0; i < lib->count; i++)
if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i];
return NULL;
}
static int find_index(const mr_library *lib, const char *name)
{
for (int i = 0; i < lib->count; i++)
if (strcmp(lib->items[i].name, name) == 0) return i;
return -1;
}
static void test_natural(void)
{
CHECK(mr_natural_cmp("page2", "page10") < 0, "page2 < page10");
CHECK(mr_natural_cmp("page10", "page2") > 0, "page10 > page2");
CHECK(mr_natural_cmp("page2", "page02") < 0, "fewer leading zeros first");
CHECK(mr_natural_cmp("x001", "x1") > 0, "x1 < x001");
CHECK(mr_natural_cmp("Chapter 9", "Chapter 10") < 0, "chapter 9 < 10");
CHECK(mr_natural_cmp("img2a", "img10b") < 0, "mixed img2a < img10b");
CHECK(mr_natural_cmp("a", "a1") < 0, "prefix shorter first");
CHECK(mr_natural_cmp("abc", "abc") == 0, "identical equal");
CHECK(mr_natural_cmp("ABC", "abc") != 0, "case-only tie is deterministic");
CHECK(mr_natural_cmp("a.jpg", "A.jpg") > 0, "case tie falls back to raw bytes");
CHECK(mr_natural_cmp("A2", "a10") < 0, "case-insensitive primary then numeric");
}
static void test_ext(void)
{
CHECK(mr_path_is_image("a.png") == 1, "png");
CHECK(mr_path_is_image("a.JPG") == 1, "JPG uppercase");
CHECK(mr_path_is_image("a.jpeg") == 1, "jpeg");
CHECK(mr_path_is_image("a.BmP") == 1, "BMP mixed case");
CHECK(mr_path_is_image("a.gif") == 0, "gif unsupported");
CHECK(mr_path_is_image("a.txt") == 0, "txt unsupported");
CHECK(mr_path_is_image("png") == 0, "no extension");
CHECK(mr_path_is_image(".png") == 0, "dotfile-ish no stem");
CHECK(mr_path_is_archive("a.cbz") == 1, "cbz");
CHECK(mr_path_is_archive("a.ZIP") == 1, "ZIP uppercase");
CHECK(mr_path_is_archive("a.7z") == 0, "7z unsupported");
CHECK(mr_path_is_archive("cbz") == 0, "no extension archive");
}
static void test_parent(void)
{
char out[MR_PATH_MAX];
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/b", out, sizeof out) == 0 &&
strcmp(out, "/usr/data/mnt/sd_0/a") == 0, "parent nested");
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/", out, sizeof out) == 0 &&
strcmp(out, "/usr/data/mnt/sd_0") == 0, "parent trailing slash");
CHECK(mr_path_parent("/a", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root child");
CHECK(mr_path_parent("/", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root");
}
static void test_scan(void)
{
char tmpl[] = "./mr_test_XXXXXX";
char *root = mkdtemp(tmpl);
CHECK(root != NULL, "mkdtemp");
if (!root) return;
char p[MR_PATH_MAX];
snprintf(p, sizeof p, "%s/Series 10", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/Series 10/page1.png", root); touch_file(p);
snprintf(p, sizeof p, "%s/Series 10/page2.jpg", root); touch_file(p);
snprintf(p, sizeof p, "%s/Series 2", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/Series 2/page10.png", root); touch_file(p);
snprintf(p, sizeof p, "%s/Series 02", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/Series 02/page1.bmp", root); touch_file(p);
snprintf(p, sizeof p, "%s/Series 3", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/Series 3/page1.gif", root); touch_file(p);
snprintf(p, sizeof p, "%s/Empty", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/Nested/Deeper", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/Nested/Deeper/a.png", root); touch_file(p);
snprintf(p, sizeof p, "%s/.hidden", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/.hidden/h.png", root); touch_file(p);
snprintf(p, sizeof p, "%s/MeowAPPs", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/MeowAPPs/m.png", root); touch_file(p);
/* 当前层文件:图片与档案册都成行;txt/隐藏文件不成行。 */
snprintf(p, sizeof p, "%s/root.png", root); touch_file(p);
snprintf(p, sizeof p, "%s/cover.JPG", root); touch_file(p);
snprintf(p, sizeof p, "%s/book.cbz", root); touch_file(p);
snprintf(p, sizeof p, "%s/bundle.zip", root);touch_file(p);
snprintf(p, sizeof p, "%s/notes.txt", root); touch_file(p);
snprintf(p, sizeof p, "%s/.hidden.png", root); touch_file(p);
char link_path[MR_PATH_MAX], link_target[MR_PATH_MAX];
snprintf(link_target, sizeof link_target, "%s/root.png", root);
snprintf(link_path, sizeof link_path, "%s/link.png", root);
symlink(link_target, link_path); /* 有权限才建;不列 */
mr_library lib;
memset(&lib, 0, sizeof lib);
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
CHECK(n == 11, "scan count=%d (want 11: 7 dirs + 4 files)", n);
CHECK(lib.count == n, "count matches");
CHECK(lib.truncated == 0, "not truncated");
if (lib.count == 11) {
static const char *want[11] = {
"Empty", "MeowAPPs", "Nested", "Series 2", "Series 02", "Series 3", "Series 10",
"book.cbz", "bundle.zip", "cover.JPG", "root.png"
};
for (int i = 0; i < 11; i++)
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
"sort[%d]=%s got %s", i, want[i], lib.items[i].name);
for (int i = 0; i < 7; i++)
CHECK(lib.items[i].kind == MR_LIB_KIND_DIR, "dirs first: %s", lib.items[i].name);
}
/* 行类型与副标题(扫描期生成一次,不是占位)。 */
const mr_library_entry *e = find_entry(&lib, "Empty");
CHECK(e && e->kind == MR_LIB_KIND_DIR && strcmp(e->subtitle, "文件夹") == 0,
"dir subtitle=%s", e ? e->subtitle : "");
e = find_entry(&lib, "root.png");
CHECK(e && e->kind == MR_LIB_KIND_IMAGE && e->size == 1 &&
strncmp(e->subtitle, "图片 · ", strlen("图片 · ")) == 0,
"image subtitle=%s size=%lld", e ? e->subtitle : "", e ? e->size : -1);
e = find_entry(&lib, "book.cbz");
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE &&
strncmp(e->subtitle, "档案册 · ", strlen("档案册 · ")) == 0,
"archive subtitle=%s", e ? e->subtitle : "");
/* 排除项:普通文件/隐藏项/隐藏目录/符号链接。 */
CHECK(find_entry(&lib, "notes.txt") == NULL, "plain text file excluded");
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
CHECK(find_entry(&lib, ".hidden.png") == NULL, "hidden file excluded");
CHECK(find_entry(&lib, "link.png") == NULL, "symlink excluded");
CHECK(find_entry(&lib, "a.png") == NULL, "no recursion into subdirs");
CHECK(find_entry(&lib, "Deeper") == NULL, "no recursion into subdirs (deeper)");
/* 图片下标 = 本层图片自然序(书库点图 → 阅读器起始页的契约)。 */
int i_cover = find_index(&lib, "cover.JPG");
int i_root = find_index(&lib, "root.png");
CHECK(mr_library_image_index(&lib, i_cover) == 0, "cover.JPG is image #0");
CHECK(mr_library_image_index(&lib, i_root) == 1, "root.png is image #1");
CHECK(mr_library_image_index(&lib, find_index(&lib, "book.cbz")) == -1,
"archive has no image index");
CHECK(mr_library_image_index(&lib, find_index(&lib, "Empty")) == -1,
"dir has no image index");
CHECK(mr_library_image_index(&lib, -1) == -1 && mr_library_image_index(&lib, 99) == -1,
"out-of-range image index");
/* 行合成:纯内存,返回扫描期生成的字符串。 */
mr_library_row row;
CHECK(mr_library_row_at(&lib, 0, &row) == 1 && row.index == 0 &&
row.kind == MR_LIB_KIND_DIR && row.title && row.subtitle &&
strcmp(row.subtitle, "文件夹") == 0, "row 0 = first dir");
CHECK(mr_library_row_at(&lib, lib.count, &row) == 0, "no extra row when not truncated");
CHECK(mr_library_row_at(&lib, -1, &row) == 0, "row -1 invalid");
/* max_depth 参数只为兼容旧调用保留:不再递归,结果一致。 */
mr_library lib1;
memset(&lib1, 0, sizeof lib1);
CHECK(mr_library_scan(&lib1, root, 1) == 11, "max_depth ignored (single layer)");
mr_library_free(&lib1);
mr_library_free(&lib);
/* 不可读目录 / 不是目录:返回 -1,lib 可安全 free。 */
mr_library bad;
memset(&bad, 0, sizeof bad);
CHECK(mr_library_scan(&bad, "/no/such/dir/tuku", MR_LIB_MAX_DEPTH) == -1, "missing dir -> -1");
CHECK(bad.count == 0, "missing dir empty");
mr_library_free(&bad);
snprintf(p, sizeof p, "%s/root.png", root);
memset(&bad, 0, sizeof bad);
CHECK(mr_library_scan(&bad, p, MR_LIB_MAX_DEPTH) == -1, "file (not dir) -> -1");
CHECK(bad.count == 0, "file scan empty");
mr_library_free(&bad);
rm_rf(root);
}
static void test_direction(void)
{
mr_reader_set_direction(MR_DIR_RTL);
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "default RTL");
mr_reader_set_direction(MR_DIR_LTR);
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "switch LTR");
mr_reader_set_direction(99);
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "invalid ignored");
mr_reader_set_direction(MR_DIR_RTL);
}
int main(void)
{
test_natural();
test_ext();
test_parent();
test_scan();
test_direction();
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}