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
225 lines
8.5 KiB
C
225 lines
8.5 KiB
C
/* test_library.c — 图库 logic 层宿主测试(纯 C,无 SDK/硬件)。
|
|
*
|
|
* 覆盖:
|
|
* T1 自然排序:数字段数值序(page2<page10)、前导零、数字/字母混合、大小写;
|
|
* T2 扩展名过滤:png/jpg/jpeg/bmp 大小写不敏感,gif/txt 不支持;
|
|
* T3 目录扫描(M13 单层语义):只列当前层直接子目录(自然升序),不递归探测子树;
|
|
* 系统目录/空目录/无图目录都列出;隐藏项/普通文件/符号链接不列;
|
|
* 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 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("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");
|
|
}
|
|
|
|
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);
|
|
|
|
snprintf(p, sizeof p, "%s/root.png", root); touch_file(p); /* 普通文件不成为行 */
|
|
|
|
mr_library lib;
|
|
memset(&lib, 0, sizeof lib);
|
|
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
|
CHECK(n == 7, "scan count=%d (want 7: all direct subdirs)", n);
|
|
CHECK(lib.count == n, "count matches");
|
|
CHECK(lib.truncated == 0, "not truncated");
|
|
if (lib.count == 7) {
|
|
static const char *want[7] = {
|
|
"Empty", "MeowAPPs", "Nested", "Series 2", "Series 02", "Series 3", "Series 10"
|
|
};
|
|
for (int i = 0; i < 7; i++)
|
|
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
|
|
"sort[%d]=%s got %s", i, want[i], lib.items[i].name);
|
|
}
|
|
|
|
/* 访问放开:系统目录/空目录/无图目录都列出;隐藏项/文件不列。 */
|
|
CHECK(find_entry(&lib, "MeowAPPs") != NULL, "system dir listed");
|
|
CHECK(find_entry(&lib, "Empty") != NULL, "empty dir listed");
|
|
CHECK(find_entry(&lib, "Series 3") != NULL, "no-image dir listed");
|
|
CHECK(find_entry(&lib, "Nested") != NULL, "intermediate dir listed");
|
|
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
|
|
CHECK(find_entry(&lib, "root.png") == NULL, "loose image file excluded");
|
|
|
|
/* 首帧不做懒判定:所有行待探测(副标题占位)。 */
|
|
for (int i = 0; i < lib.count; i++) {
|
|
CHECK(lib.items[i].pages == -1, "%s pages pending", lib.items[i].name);
|
|
CHECK(lib.items[i].has_images == -1, "%s book state pending", lib.items[i].name);
|
|
CHECK(lib.items[i].probe_state == MR_LIB_PENDING, "%s probe pending", lib.items[i].name);
|
|
CHECK(strcmp(lib.items[i].subtitle, "…") == 0, "%s subtitle placeholder", lib.items[i].name);
|
|
}
|
|
|
|
/* max_depth 参数只为兼容旧调用保留:不再递归,结果一致。 */
|
|
mr_library lib1;
|
|
memset(&lib1, 0, sizeof lib1);
|
|
CHECK(mr_library_scan(&lib1, root, 1) == 7, "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);
|
|
|
|
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;
|
|
}
|