图库: 单层浏览 + 懒判定与 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
This commit is contained in:
1 parent
c9cff99af3
commit
9f34417c49
12 files changed
+2299
-358
No files matched your search
+50
-12
@@ -1,6 +1,9 @@
|
||||
#!/bin/bash
|
||||
# run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。
|
||||
# - test_library:书库扫描/自然排序/阅读方向(46 项)
|
||||
# - test_library:单层目录扫描/自然排序/父目录/阅读方向
|
||||
# - test_browse_nav:访问放开(系统/空/无图目录可进入)、上溯到 /、不可读目录、截断
|
||||
# - test_browse_lazy:单层探测/懒判定补位/mtime 失效缓存/行合成 O(1)/M7 册间语义
|
||||
# - test_browse_perf:500 目录×8 层嵌套+2000 文件夹具,断言扫描单层且有界
|
||||
# - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/
|
||||
# 坏值/非法值/只读一次/写失败只告警)
|
||||
# - test_pin:启动 PIN 格式/设置/校验/修改/禁用/残缺配置/落盘失败(logic/pin.c 为纯 C,
|
||||
@@ -10,30 +13,48 @@
|
||||
# 交付环境只有库+头时自动 SKIP。
|
||||
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
|
||||
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
|
||||
# logic/library.c 的探测缓存用 pthread 互斥锁 → 相关编译带 -pthread。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
OUT=build/host
|
||||
mkdir -p "$OUT"
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_library.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-o test/test_library
|
||||
./test/test_library
|
||||
-pthread -o "$OUT/test_library"
|
||||
"$OUT"/test_library
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_LIBRARY_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_browse_nav.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_browse_nav"
|
||||
"$OUT"/test_browse_nav
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_LIBRARY_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_browse_lazy.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_browse_lazy"
|
||||
"$OUT"/test_browse_lazy
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_LIBRARY_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_browse_perf.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_browse_perf"
|
||||
"$OUT"/test_browse_perf
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_reader_core.c logic/library.c logic/reader.c \
|
||||
logic/reader/page_source.c logic/reader/nav.c test/settings_reader_stub.c \
|
||||
-o test/test_reader_core
|
||||
./test/test_reader_core
|
||||
-pthread -o "$OUT/test_reader_core"
|
||||
"$OUT"/test_reader_core
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
|
||||
-o test/test_settings_reader_persist
|
||||
./test/test_settings_reader_persist
|
||||
-o "$OUT"/test_settings_reader_persist
|
||||
"$OUT"/test_settings_reader_persist
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_pin.c logic/pin.c \
|
||||
-o test/test_pin
|
||||
./test/test_pin
|
||||
-o "$OUT"/test_pin
|
||||
"$OUT"/test_pin
|
||||
|
||||
if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
@@ -47,10 +68,27 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -o test/test_settings_lock_host
|
||||
./test/test_settings_lock_host
|
||||
-pthread -lm -o "$OUT"/test_settings_lock_host
|
||||
"$OUT"/test_settings_lock_host
|
||||
|
||||
# 书库浏览页集成(src2 懒算补位/行语义/上溯至 / /不可读提示);同样需要 SDK 源码树。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_LIBRARY_UI_TEST_HOOKS \
|
||||
-I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_browse_ui_host.c ui/library/library.c logic/library.c \
|
||||
logic/reader.c logic/reader/page_source.c logic/reader/archive_source.c \
|
||||
logic/reader/page_cache.c logic/zip/zip.c \
|
||||
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_browse_ui_host
|
||||
"$OUT"/test_browse_ui_host
|
||||
else
|
||||
echo "SKIP test_settings_lock_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||
echo "SKIP test_settings_lock_host/test_browse_ui_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||
fi
|
||||
|
||||
# ---- 档案册(M11):zip 容器 + CBZ 书库/阅读器集成 ----
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
/* test_browse_lazy.c — 单层探测 / 懒判定 / mtime 失效缓存 / 行合成(纯 C,无 SDK)。
|
||||
*
|
||||
* 覆盖:
|
||||
* L1 单层探测:直接图片数、自然序首图、空目录标记;不递归(只有子目录含图的目录不算书);
|
||||
* L2 懒判定接线:scan 后行全部 PENDING(占位副标题)→ apply_probe 后页数/首图/副标题/
|
||||
* 书标记;空目录「空目录」;无图目录「无图片」;探测失败「无法读取」且视为非书;
|
||||
* L3 缓存:mtime 失效(同 mtime 命中不读盘;目录变化后重扫并刷新结果);并发调用线程安全;
|
||||
* L3b 评审 P2:探测期间 mtime/size 变化的结果不写缓存(避免「不完整结果绑定新 mtime」);
|
||||
* L4 行合成 O(1):mr_library_row_at 纯内存(IO 计数不变)、截断提示行;
|
||||
* L5 M7 兼容语义:「直接含图兄弟册」+ 自然序册间顺序(M7 mr_book_adjacent 的口径)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
* test/test_browse_lazy.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
* -pthread -o build/host/test_browse_lazy
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.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 void joinp(char *out, int outsz, const char *a, const char *b)
|
||||
{
|
||||
snprintf(out, (size_t)outsz, "%s/%s", a, b);
|
||||
}
|
||||
|
||||
/* 把目录 mtime 显式推后 3 秒(避免同秒内变化被缓存误命中)。 */
|
||||
static void bump_mtime(const char *path)
|
||||
{
|
||||
struct timespec ts[2];
|
||||
ts[0].tv_sec = time(NULL) + 3;
|
||||
ts[0].tv_nsec = 0;
|
||||
ts[1] = ts[0];
|
||||
utimensat(AT_FDCWD, path, ts, 0);
|
||||
}
|
||||
|
||||
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, int *out)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) { *out = i; return 1; }
|
||||
*out = -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- L1 + L2 ---- */
|
||||
static void test_probe_and_apply(const char *root)
|
||||
{
|
||||
char book[MR_PATH_MAX], noimg[MR_PATH_MAX], empty[MR_PATH_MAX], dirs_only[MR_PATH_MAX];
|
||||
char deep[MR_PATH_MAX];
|
||||
joinp(book, sizeof book, root, "book");
|
||||
joinp(noimg, sizeof noimg, root, "noimg");
|
||||
joinp(empty, sizeof empty, root, "empty");
|
||||
joinp(dirs_only, sizeof dirs_only, root, "dirs_only");
|
||||
joinp(deep, sizeof deep, root, "dirs_only/deep");
|
||||
|
||||
mr_library_probe p;
|
||||
CHECK(mr_library_probe_dir(book, &p) == 0, "probe book");
|
||||
CHECK(p.pages == 2, "book pages=%d", p.pages);
|
||||
CHECK(p.empty == 0, "book not empty");
|
||||
CHECK(strstr(p.cover, "page2.png") != NULL, "cover natural-first: %s", p.cover);
|
||||
|
||||
CHECK(mr_library_probe_dir(noimg, &p) == 0 && p.pages == 0 && p.empty == 0,
|
||||
"noimg: 0 pages, not empty");
|
||||
CHECK(mr_library_probe_dir(empty, &p) == 0 && p.pages == 0 && p.empty == 1,
|
||||
"empty dir flagged");
|
||||
CHECK(mr_library_probe_dir(dirs_only, &p) == 0 && p.pages == 0 && p.empty == 0,
|
||||
"dirs_only has no direct images (no recursion)");
|
||||
CHECK(mr_library_probe_dir(deep, &p) == 0 && p.pages == 1, "deep is a book");
|
||||
|
||||
char slash[MR_PATH_MAX];
|
||||
snprintf(slash, sizeof slash, "%s/", book);
|
||||
CHECK(mr_library_probe_dir(slash, &p) == 0 && p.pages == 2, "trailing slash");
|
||||
|
||||
CHECK(mr_library_probe_dir("/no/such/dir", &p) == -1, "missing dir -> -1");
|
||||
CHECK(mr_library_probe_dir(NULL, &p) == -1, "NULL -> -1");
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH) == 5, "scan 5 dirs (got %d)", lib.count);
|
||||
|
||||
const mr_library_entry *e = find_entry(&lib, "book");
|
||||
CHECK(e && e->probe_state == MR_LIB_PENDING && e->pages == -1,
|
||||
"book pending after scan");
|
||||
|
||||
int idx_book, idx_noimg, idx_empty, idx_dirs;
|
||||
CHECK(find_index(&lib, "book", &idx_book), "book index");
|
||||
CHECK(find_index(&lib, "noimg", &idx_noimg), "noimg index");
|
||||
CHECK(find_index(&lib, "empty", &idx_empty), "empty index");
|
||||
CHECK(find_index(&lib, "dirs_only", &idx_dirs), "dirs_only index");
|
||||
if (idx_book < 0 || idx_noimg < 0 || idx_empty < 0 || idx_dirs < 0) {
|
||||
mr_library_free(&lib);
|
||||
return;
|
||||
}
|
||||
|
||||
CHECK(mr_library_probe_dir(lib.items[idx_book].path, &p) == 0, "probe row book");
|
||||
CHECK(mr_library_apply_probe(&lib, idx_book, &p) == 1, "apply book");
|
||||
e = &lib.items[idx_book];
|
||||
CHECK(e->has_images == 1 && e->pages == 2 && e->probe_state == MR_LIB_READY,
|
||||
"book ready");
|
||||
CHECK(strcmp(e->subtitle, "2 pages") == 0, "book subtitle=%s", e->subtitle);
|
||||
CHECK(strstr(e->cover, "page2.png") != NULL, "book row cover=%s", e->cover);
|
||||
|
||||
CHECK(mr_library_probe_dir(lib.items[idx_noimg].path, &p) == 0 &&
|
||||
mr_library_apply_probe(&lib, idx_noimg, &p) == 1, "apply noimg");
|
||||
e = &lib.items[idx_noimg];
|
||||
CHECK(e->has_images == 0 && strcmp(e->subtitle, "无图片") == 0,
|
||||
"noimg subtitle=%s", e->subtitle);
|
||||
|
||||
CHECK(mr_library_probe_dir(lib.items[idx_empty].path, &p) == 0 &&
|
||||
mr_library_apply_probe(&lib, idx_empty, &p) == 1, "apply empty");
|
||||
e = &lib.items[idx_empty];
|
||||
CHECK(e->has_images == 0 && e->empty == 1 && strcmp(e->subtitle, "空目录") == 0,
|
||||
"empty subtitle=%s", e->subtitle);
|
||||
|
||||
CHECK(mr_library_mark_error(&lib, idx_dirs) == 1, "mark error");
|
||||
e = &lib.items[idx_dirs];
|
||||
CHECK(e->probe_state == MR_LIB_FAILED && e->has_images == 0 &&
|
||||
strcmp(e->subtitle, "无法读取") == 0, "failed subtitle=%s", e->subtitle);
|
||||
|
||||
CHECK(mr_library_apply_probe(&lib, 999, &p) == 0, "apply out of range");
|
||||
CHECK(mr_library_mark_error(&lib, -1) == 0, "mark_error out of range");
|
||||
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* 档案册探测回调(P2 用例):可注入「探测期间文件变化」。 */
|
||||
typedef struct arch_probe_ctx {
|
||||
int calls;
|
||||
int bump_times; /* 前 N 次调用里改动文件 mtime(模拟探测期间变化) */
|
||||
} arch_probe_ctx;
|
||||
|
||||
static int arch_probe(const char *path, void *ud)
|
||||
{
|
||||
arch_probe_ctx *c = (arch_probe_ctx *)ud;
|
||||
c->calls++;
|
||||
if (c->calls <= c->bump_times) bump_mtime(path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- L3b:评审 P2——探测期间 mtime 变化不得写入缓存(消除假新鲜) ---- */
|
||||
static void test_cache_race(const char *root)
|
||||
{
|
||||
char arc[MR_PATH_MAX];
|
||||
joinp(arc, sizeof arc, root, "race.cbz");
|
||||
CHECK(touch_file(arc) == 0, "create race.cbz");
|
||||
|
||||
mr_library_probe_cache_clear();
|
||||
mr_library_probe p;
|
||||
|
||||
/* 稳定文件:第二次走缓存,回调只调一次。 */
|
||||
arch_probe_ctx stable = { 0, 0 };
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &stable, &p) == 0 && p.pages == 1,
|
||||
"archive probe");
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &stable, &p) == 0 && p.pages == 1,
|
||||
"archive probe (cached)");
|
||||
CHECK(stable.calls == 1, "stable archive cached (calls=%d)", stable.calls);
|
||||
|
||||
/* 探测期间文件变化:本次结果不得入缓存 → 下一次仍重新探测。 */
|
||||
mr_library_probe_cache_clear();
|
||||
arch_probe_ctx race = { 0, 1 };
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &race, &p) == 0 && p.pages == 1,
|
||||
"racing archive probe");
|
||||
CHECK(race.calls == 1, "racing probe called once (calls=%d)", race.calls);
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &race, &p) == 0 && p.pages == 1,
|
||||
"racing archive probe again");
|
||||
CHECK(race.calls == 2, "changed-during-probe result NOT cached (calls=%d)", race.calls);
|
||||
/* 第二次稳定后落缓存,第三次命中。 */
|
||||
CHECK(mr_library_probe_path(arc, 1, arch_probe, &race, &p) == 0 && p.pages == 1,
|
||||
"racing archive probe third");
|
||||
CHECK(race.calls == 2, "stable result cached after third call (calls=%d)", race.calls);
|
||||
mr_library_probe_cache_clear();
|
||||
}
|
||||
|
||||
/* ---- L3:mtime 失效缓存 + 并发 ---- */
|
||||
#define THREADS 6
|
||||
typedef struct th_arg {
|
||||
const char *dir;
|
||||
int want;
|
||||
int *fail;
|
||||
pthread_mutex_t *mu;
|
||||
} th_arg;
|
||||
|
||||
static void *probe_thread(void *arg)
|
||||
{
|
||||
th_arg *a = (th_arg *)arg;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
mr_library_probe p;
|
||||
int ok = (mr_library_probe_cached(a->dir, &p) == 0 && p.pages == a->want);
|
||||
if (!ok) {
|
||||
pthread_mutex_lock(a->mu);
|
||||
(*a->fail)++;
|
||||
pthread_mutex_unlock(a->mu);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_cache(const char *root)
|
||||
{
|
||||
char book[MR_PATH_MAX];
|
||||
joinp(book, sizeof book, root, "book");
|
||||
|
||||
mr_library_probe_cache_clear();
|
||||
mr_library_io_stats st;
|
||||
mr_library_probe p;
|
||||
|
||||
mr_library_test_io_reset();
|
||||
CHECK(mr_library_probe_cached(book, &p) == 0 && p.pages == 2, "cache miss probe");
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 1, "cache miss opens once (got %ld)", st.opendirs);
|
||||
|
||||
mr_library_test_io_reset();
|
||||
CHECK(mr_library_probe_cached(book, &p) == 0 && p.pages == 2, "cache hit");
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 0 && st.lstats == 0, "cache hit does no IO (open=%ld stat=%ld)",
|
||||
st.opendirs, st.lstats);
|
||||
|
||||
char img[MR_PATH_MAX];
|
||||
joinp(img, sizeof img, book, "page30.png");
|
||||
CHECK(touch_file(img) == 0, "add image");
|
||||
bump_mtime(book);
|
||||
mr_library_test_io_reset();
|
||||
CHECK(mr_library_probe_cached(book, &p) == 0 && p.pages == 3,
|
||||
"cache invalidated by mtime (pages=%d)", p.pages);
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 1, "invalidation re-opens (got %ld)", st.opendirs);
|
||||
|
||||
CHECK(mr_library_probe_cached("/no/such/dir", &p) == -1, "missing dir -> -1");
|
||||
|
||||
pthread_t th[THREADS];
|
||||
th_arg args[THREADS];
|
||||
int failures = 0;
|
||||
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
int started = 0;
|
||||
for (int t = 0; t < THREADS; t++) {
|
||||
args[t].dir = book;
|
||||
args[t].want = 3;
|
||||
args[t].fail = &failures;
|
||||
args[t].mu = μ
|
||||
if (pthread_create(&th[t], NULL, probe_thread, &args[t]) == 0) started++;
|
||||
}
|
||||
for (int t = 0; t < started; t++) pthread_join(th[t], NULL);
|
||||
CHECK(started == THREADS, "threads started (%d)", started);
|
||||
CHECK(failures == 0, "concurrent probe failures=%d", failures);
|
||||
pthread_mutex_destroy(&mu);
|
||||
}
|
||||
|
||||
/* ---- L4:行合成 O(1) + 截断提示行 ---- */
|
||||
static void test_rows(const char *root)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
|
||||
mr_library_io_stats st;
|
||||
mr_library_test_io_reset();
|
||||
struct timespec t0, t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
int rows = 0;
|
||||
for (int r = 0; r < 2000; r++)
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_row row;
|
||||
if (mr_library_row_at(&lib, i, &row)) rows++;
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
mr_library_test_io_get(&st);
|
||||
long long us = (long long)(t1.tv_sec - t0.tv_sec) * 1000000LL +
|
||||
(t1.tv_nsec - t0.tv_nsec) / 1000LL;
|
||||
CHECK(rows == 2000 * lib.count, "rows synthesized (%d)", rows);
|
||||
CHECK(st.opendirs == 0 && st.readdirs == 0 && st.lstats == 0,
|
||||
"row synth is pure memory (open=%ld rd=%ld stat=%ld)",
|
||||
st.opendirs, st.readdirs, st.lstats);
|
||||
CHECK(us < 2000000, "row synth bounded (%lldus)", us);
|
||||
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 0, "no extra row when not truncated");
|
||||
lib.truncated = 1;
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1,
|
||||
"truncation info row");
|
||||
CHECK(strcmp(row.title, "已截断") == 0, "truncation title=%s", row.title);
|
||||
CHECK(strstr(row.subtitle, "2000") != NULL, "truncation subtitle=%s", row.subtitle);
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ---- L5:M7 兼容语义(直接含图兄弟册 + 自然序) ---- */
|
||||
static void collect_books(const char *parent, char names[][MR_NAME_MAX], int max, int *out_n)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = 0;
|
||||
if (mr_library_scan(&lib, parent, MR_LIB_MAX_DEPTH) > 0) {
|
||||
for (int i = 0; i < lib.count && n < max; i++) {
|
||||
mr_library_probe p;
|
||||
if (mr_library_probe_cached(lib.items[i].path, &p) == 0 && p.pages > 0) {
|
||||
snprintf(names[n], MR_NAME_MAX, "%s", lib.items[i].name);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
mr_library_free(&lib);
|
||||
*out_n = n;
|
||||
}
|
||||
|
||||
static int book_index(char names[][MR_NAME_MAX], int n, const char *cur)
|
||||
{
|
||||
for (int i = 0; i < n; i++)
|
||||
if (strcmp(names[i], cur) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void test_m7_semantics(const char *root)
|
||||
{
|
||||
char parent[MR_PATH_MAX];
|
||||
joinp(parent, sizeof parent, root, "series");
|
||||
|
||||
char names[16][MR_NAME_MAX];
|
||||
int n = 0;
|
||||
collect_books(parent, names, 16, &n);
|
||||
CHECK(n == 3, "series books=%d (vol1/vol2/vol10; vol6 is not a book)", n);
|
||||
if (n == 3) {
|
||||
CHECK(strcmp(names[0], "vol1") == 0, "book[0]=%s", names[0]);
|
||||
CHECK(strcmp(names[1], "vol2") == 0, "book[1]=%s", names[1]);
|
||||
CHECK(strcmp(names[2], "vol10") == 0, "book[2]=%s", names[2]);
|
||||
}
|
||||
CHECK(book_index(names, n, "vol6") < 0, "dirs-only vol6 skipped (direct images only)");
|
||||
CHECK(book_index(names, n, ".hidden") < 0, "hidden skipped");
|
||||
CHECK(book_index(names, n, "MeowOS") < 0, "system dir without direct images skipped");
|
||||
|
||||
int i2 = book_index(names, n, "vol2");
|
||||
CHECK(i2 >= 0 && i2 + 1 < n && strcmp(names[i2 + 1], "vol10") == 0,
|
||||
"vol2 next = vol10 (natural order, vol6 skipped)");
|
||||
CHECK(book_index(names, n, "vol10") == n - 1, "vol10 is last (no next)");
|
||||
CHECK(book_index(names, n, "vol1") == 0, "vol1 is first (no prev)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_lazy_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
const char *dirs[] = { "book", "noimg", "empty", "dirs_only/deep", "series" };
|
||||
for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) {
|
||||
joinp(p, sizeof p, root, dirs[i]);
|
||||
CHECK(mkdir_p(p) == 0, "mkdir %s", p);
|
||||
}
|
||||
|
||||
joinp(p, sizeof p, root, "book/page2.png"); CHECK(touch_file(p) == 0, "page2");
|
||||
joinp(p, sizeof p, root, "book/page10.jpg"); CHECK(touch_file(p) == 0, "page10");
|
||||
joinp(p, sizeof p, root, "book/notes.txt"); CHECK(touch_file(p) == 0, "notes");
|
||||
joinp(p, sizeof p, root, "noimg/readme.txt"); CHECK(touch_file(p) == 0, "readme");
|
||||
joinp(p, sizeof p, root, "dirs_only/deep/a.png"); CHECK(touch_file(p) == 0, "deep a");
|
||||
|
||||
/* M7 语义夹具:vol1/vol2/vol10 直接含图;vol6 只有子目录含图;.hidden / MeowOS 干扰项 */
|
||||
const char *mk[] = { "series/vol1/001.png", "series/vol2/001.png",
|
||||
"series/vol10/001.png", "series/vol6/sub/001.png",
|
||||
"series/.hidden/001.png", "series/MeowOS/deep/001.png" };
|
||||
for (unsigned i = 0; i < sizeof mk / sizeof mk[0]; i++) {
|
||||
joinp(p, sizeof p, root, mk[i]);
|
||||
/* 先建父目录,再落文件 */
|
||||
char dir[MR_PATH_MAX];
|
||||
snprintf(dir, sizeof dir, "%s", p);
|
||||
char *slash = strrchr(dir, '/');
|
||||
if (slash) { *slash = '\0'; CHECK(mkdir_p(dir) == 0, "mkdir %s", dir); }
|
||||
CHECK(touch_file(p) == 0, "image %s", p);
|
||||
}
|
||||
|
||||
test_probe_and_apply(root);
|
||||
test_cache(root);
|
||||
test_cache_race(root);
|
||||
test_rows(root);
|
||||
test_m7_semantics(root);
|
||||
|
||||
rm_rf(root);
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/* test_browse_nav.c — 浏览访问放开 / 上溯父目录 / 截断标注(纯 C,无 SDK)。
|
||||
*
|
||||
* 覆盖(M13 用户要求②):
|
||||
* N1 访问放开:系统目录(MeowAPPs/MeowData/MeowOS/lost+found)不再隐藏,
|
||||
* 空目录/无图目录都列出且可进入(scan 返回 0 而不是 -1);
|
||||
* N2 隐藏项/普通文件/符号链接仍不列(防环、防噪声);
|
||||
* N3 不可读目录/不存在/不是目录:scan 返回 -1,调用方据此提示并停留;
|
||||
* N4 上溯父目录:mr_path_parent 可一路走到文件系统 "/"(不再以 TF 卡根为界);
|
||||
* N5 单目录条目上限:达到 MR_LIB_SCAN_MAX 后截断并置 truncated(行合成给出提示行)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
* test/test_browse_nav.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
* -pthread -o build/host/test_browse_nav
|
||||
*/
|
||||
#include "logic/library.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)) { chmod(path, 0700); return unlink(path); }
|
||||
chmod(path, 0700);
|
||||
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 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;
|
||||
}
|
||||
|
||||
/* ---- N1/N2 ---- */
|
||||
static void test_open_access(const char *root)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == 7, "root lists 7 dirs (got %d)", n);
|
||||
CHECK(lib.truncated == 0, "root not truncated");
|
||||
|
||||
static const char *want[7] = {
|
||||
"book", "empty", "lost+found", "MeowAPPs", "MeowData", "MeowOS", "noimg"
|
||||
};
|
||||
for (int i = 0; i < 7 && i < lib.count; 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(find_entry(&lib, want[i]) != NULL, "system/empty/noimg dir listed: %s", want[i]);
|
||||
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
|
||||
CHECK(find_entry(&lib, "linkdir") == NULL, "symlink dir excluded");
|
||||
CHECK(find_entry(&lib, "loose.png") == NULL, "file excluded");
|
||||
|
||||
/* 可进入:每个列出的目录都能被 scan(0 项也算成功,不等于 -1)。 */
|
||||
char p[MR_PATH_MAX];
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library child;
|
||||
memset(&child, 0, sizeof child);
|
||||
CHECK(mr_library_scan(&child, lib.items[i].path, MR_LIB_MAX_DEPTH) >= 0,
|
||||
"enter %s", lib.items[i].name);
|
||||
mr_library_free(&child);
|
||||
}
|
||||
|
||||
joinp(p, sizeof p, root, "MeowOS");
|
||||
mr_library syslib;
|
||||
memset(&syslib, 0, sizeof syslib);
|
||||
CHECK(mr_library_scan(&syslib, p, MR_LIB_MAX_DEPTH) == 1 &&
|
||||
find_entry(&syslib, "sub") != NULL, "system dir browsable");
|
||||
mr_library_free(&syslib);
|
||||
|
||||
joinp(p, sizeof p, root, "empty");
|
||||
mr_library empty;
|
||||
memset(&empty, 0, sizeof empty);
|
||||
CHECK(mr_library_scan(&empty, p, MR_LIB_MAX_DEPTH) == 0, "empty dir enterable (0 items)");
|
||||
mr_library_free(&empty);
|
||||
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ---- N3 ---- */
|
||||
static void test_unreadable(const char *root)
|
||||
{
|
||||
mr_library lib;
|
||||
char p[MR_PATH_MAX];
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, "/no/such/dir/m13", MR_LIB_MAX_DEPTH) == -1, "missing -> -1");
|
||||
CHECK(lib.count == 0 && lib.truncated == 0, "failed scan leaves empty lib");
|
||||
mr_library_free(&lib);
|
||||
|
||||
joinp(p, sizeof p, root, "loose.png");
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, p, MR_LIB_MAX_DEPTH) == -1, "file (not dir) -> -1");
|
||||
mr_library_free(&lib);
|
||||
|
||||
if (geteuid() != 0) { /* root 下 EACCES 不生效,跳过权限用例 */
|
||||
joinp(p, sizeof p, root, "noread");
|
||||
CHECK(mkdir_p(p) == 0, "mkdir noread");
|
||||
CHECK(chmod(p, 0000) == 0, "chmod 000");
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, p, MR_LIB_MAX_DEPTH) == -1, "unreadable -> -1");
|
||||
CHECK(lib.count == 0, "unreadable lib empty");
|
||||
mr_library_free(&lib);
|
||||
chmod(p, 0700);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- N4 ---- */
|
||||
static void test_up_to_root(void)
|
||||
{
|
||||
char cur[MR_PATH_MAX];
|
||||
snprintf(cur, sizeof cur, "/usr/data/mnt/sd_0/a/b/c");
|
||||
int steps = 0;
|
||||
while (strcmp(cur, "/") != 0 && steps < 32) {
|
||||
char up[MR_PATH_MAX];
|
||||
int r = mr_path_parent(cur, up, (int)sizeof up);
|
||||
CHECK(r == 0, "parent of %s", cur);
|
||||
if (r != 0) break;
|
||||
CHECK(strcmp(up, cur) != 0, "parent strictly shorter at %s", cur);
|
||||
memcpy(cur, up, sizeof cur);
|
||||
steps++;
|
||||
}
|
||||
CHECK(strcmp(cur, "/") == 0, "reached filesystem root ('%s')", cur);
|
||||
CHECK(steps == 7, "steps to / = %d (want 7)", steps);
|
||||
|
||||
char up[MR_PATH_MAX];
|
||||
CHECK(mr_path_parent("/", up, (int)sizeof up) == 0 && strcmp(up, "/") == 0,
|
||||
"root parent stays /");
|
||||
CHECK(mr_path_parent("/usr/data/mnt/sd_0", up, (int)sizeof up) == 0 &&
|
||||
strcmp(up, "/usr/data/mnt") == 0,
|
||||
"TF root can go above mount point: %s", up);
|
||||
}
|
||||
|
||||
/* ---- N5 ---- */
|
||||
static void test_truncation(void)
|
||||
{
|
||||
CHECK(MR_LIB_SCAN_MAX == 2000, "default cap is 2000 (got %d)", MR_LIB_SCAN_MAX);
|
||||
|
||||
char tmpl[] = "./mr_nav_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return;
|
||||
|
||||
int total = MR_LIB_SCAN_MAX + 8;
|
||||
char p[MR_PATH_MAX];
|
||||
for (int i = 0; i < total; i++) {
|
||||
snprintf(p, sizeof p, "%s/d%04d", root, i);
|
||||
if (mkdir(p, 0755) != 0) { CHECK(0, "mkdir %s", p); break; }
|
||||
}
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == MR_LIB_SCAN_MAX, "cap applied: n=%d", n);
|
||||
CHECK(lib.truncated == 1, "truncated flag set");
|
||||
CHECK(lib.count == MR_LIB_SCAN_MAX, "count == cap");
|
||||
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1,
|
||||
"truncation row present");
|
||||
CHECK(strcmp(row.title, "已截断") == 0, "truncation row title=%s", row.title);
|
||||
|
||||
mr_library_free(&lib);
|
||||
rm_rf(root);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_nav_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
const char *dirs[] = { "MeowAPPs", "MeowData", "MeowOS/sub", "lost+found",
|
||||
"book", "empty", "noimg", ".hidden" };
|
||||
for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) {
|
||||
joinp(p, sizeof p, root, dirs[i]);
|
||||
CHECK(mkdir_p(p) == 0, "mkdir %s", p);
|
||||
}
|
||||
joinp(p, sizeof p, root, "MeowOS/001.png");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "book/001.png");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "noimg/readme.txt");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "loose.png");
|
||||
touch_file(p);
|
||||
/* 符号链接目录不参与浏览(有权限才建;建不了不影响其余断言)。 */
|
||||
char target[MR_PATH_MAX], link[MR_PATH_MAX];
|
||||
joinp(target, sizeof target, root, "book");
|
||||
joinp(link, sizeof link, root, "linkdir");
|
||||
symlink(target, link);
|
||||
|
||||
test_open_access(root);
|
||||
test_unreadable(root);
|
||||
test_up_to_root();
|
||||
|
||||
rm_rf(root);
|
||||
test_truncation();
|
||||
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/* test_browse_perf.c — 单层扫描性能/有界性验收(纯 C,无 SDK)。
|
||||
*
|
||||
* 夹具:500 个顶层目录 × 8 层嵌套(4000 个子目录)+ 根目录 2000 个文件。
|
||||
* 断言(对应 M13 任务⑤):
|
||||
* P1 首帧扫描只做单层:opendir 次数 == 1(旧实现会递归打开数千个目录);
|
||||
* lstat 次数 == 本层条目数;耗时有界;
|
||||
* P2 后台探测同样只读一层:每个目录一次 opendir,不进入其嵌套子树;
|
||||
* P3 行合成(UI src/bind 的数据面)纯内存、零 IO、O(1);
|
||||
* P4 深目录直接扫描也只打开它自己。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
* test/test_browse_perf.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
* -pthread -o build/host/test_browse_perf
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define PERF_DIRS 500
|
||||
#define PERF_DEPTH 8
|
||||
#define PERF_FILES 2000
|
||||
|
||||
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 long long now_us(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_perf_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
/* 根目录:2000 个文件 + 500 个顶层目录(每个顶层目录内是 8 层嵌套 + 1 张图)。 */
|
||||
for (int f = 0; f < PERF_FILES; f++) {
|
||||
snprintf(p, sizeof p, "%s/f%04d.txt", root, f);
|
||||
if (touch_file(p) != 0) { CHECK(0, "file %s", p); break; }
|
||||
}
|
||||
for (int d = 0; d < PERF_DIRS; d++) {
|
||||
char deep[MR_PATH_MAX];
|
||||
snprintf(deep, sizeof deep, "%s/d%04d", root, d);
|
||||
if (mkdir_p(deep) != 0) { CHECK(0, "mkdir %s", deep); break; }
|
||||
snprintf(p, sizeof p, "%s/001.png", deep); /* 顶层目录自身直接含图(=一本书) */
|
||||
if (touch_file(p) != 0) { CHECK(0, "touched top image"); break; }
|
||||
for (int l = 1; l <= PERF_DEPTH; l++) {
|
||||
char sub[MR_PATH_MAX];
|
||||
snprintf(sub, sizeof sub, "%s/l%d", deep, l);
|
||||
if (mkdir(sub, 0755) != 0) { CHECK(0, "mkdir %s", sub); break; }
|
||||
snprintf(deep, sizeof deep, "%s", sub);
|
||||
}
|
||||
snprintf(p, sizeof p, "%s/001.png", deep);
|
||||
if (touch_file(p) != 0) { CHECK(0, "touched deep image"); break; }
|
||||
}
|
||||
|
||||
/* ---- P1:首帧扫描只做单层 ---- */
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
mr_library_test_io_reset();
|
||||
long long t0 = now_us();
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
long long scan_us = now_us() - t0;
|
||||
mr_library_io_stats st;
|
||||
mr_library_test_io_get(&st);
|
||||
|
||||
CHECK(n == PERF_DIRS, "scan lists %d dirs (got %d)", PERF_DIRS, n);
|
||||
CHECK(lib.truncated == 0, "not truncated");
|
||||
CHECK(st.opendirs == 1, "single-layer scan: opendir=%ld (want 1)", st.opendirs);
|
||||
CHECK(st.lstats == PERF_FILES + PERF_DIRS,
|
||||
"lstat only current level: %ld (want %d)", st.lstats, PERF_FILES + PERF_DIRS);
|
||||
CHECK(scan_us < 1500000LL, "scan bounded: %lldus", scan_us);
|
||||
if (lib.count > 1) {
|
||||
CHECK(strcmp(lib.items[0].name, "d0000") == 0, "first=%s", lib.items[0].name);
|
||||
CHECK(strcmp(lib.items[lib.count - 1].name, "d0499") == 0,
|
||||
"last=%s", lib.items[lib.count - 1].name);
|
||||
}
|
||||
printf(" scan: %lldus, %d dirs, opendir=%ld lstat=%ld\n",
|
||||
scan_us, n, st.opendirs, st.lstats);
|
||||
|
||||
/* ---- P2:后台探测每目录只开一次,不进入嵌套子树 ---- */
|
||||
mr_library_test_io_reset();
|
||||
long long p0 = now_us();
|
||||
int bad = 0;
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_probe probe;
|
||||
if (mr_library_probe_cached(lib.items[i].path, &probe) != 0 || probe.pages != 1)
|
||||
bad++;
|
||||
}
|
||||
long long probe_us = now_us() - p0;
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(bad == 0, "all top dirs have 1 direct image (bad=%d)", bad);
|
||||
CHECK(st.opendirs == PERF_DIRS,
|
||||
"probe stays shallow: opendir=%ld (want %d, not %d)",
|
||||
st.opendirs, PERF_DIRS, PERF_DIRS * (PERF_DEPTH + 1));
|
||||
CHECK(st.lstats == PERF_DIRS,
|
||||
"probe lstats only image candidates: %ld (want %d)", st.lstats, PERF_DIRS);
|
||||
CHECK(probe_us < 5000000LL, "probe bounded: %lldus", probe_us);
|
||||
printf(" probe: %lldus, opendir=%ld lstat=%ld\n", probe_us, st.opendirs, st.lstats);
|
||||
|
||||
/* ---- P3:行合成(bind/src 数据面)纯内存 O(1) ---- */
|
||||
mr_library_test_io_reset();
|
||||
long long r0 = now_us();
|
||||
int rows = 0;
|
||||
for (int r = 0; r < 1000; r++)
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_row row;
|
||||
if (mr_library_row_at(&lib, i, &row)) rows++;
|
||||
}
|
||||
long long row_us = now_us() - r0;
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(rows == 1000 * lib.count, "rows synthesized (%d)", rows);
|
||||
CHECK(st.opendirs == 0 && st.readdirs == 0 && st.lstats == 0,
|
||||
"row synthesis does no IO (open=%ld rd=%ld stat=%ld)",
|
||||
st.opendirs, st.readdirs, st.lstats);
|
||||
CHECK(row_us < 1000000LL, "row synthesis bounded: %lldus", row_us);
|
||||
printf(" rows: %lldus for %d rows, zero IO\n", row_us, rows);
|
||||
|
||||
/* ---- P4:直接扫描深层目录同样只打开它自己 ---- */
|
||||
char deep[MR_PATH_MAX];
|
||||
snprintf(deep, sizeof deep, "%s", lib.items[0].path);
|
||||
for (int l = 1; l <= PERF_DEPTH; l++) {
|
||||
char sub[MR_PATH_MAX];
|
||||
snprintf(sub, sizeof sub, "%s/l%d", deep, l);
|
||||
snprintf(deep, sizeof deep, "%s", sub);
|
||||
}
|
||||
mr_library_test_io_reset();
|
||||
mr_library deep_lib;
|
||||
memset(&deep_lib, 0, sizeof deep_lib);
|
||||
CHECK(mr_library_scan(&deep_lib, deep, MR_LIB_MAX_DEPTH) == 0, "deep leaf has no subdir");
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 1, "deep scan opens once (got %ld)", st.opendirs);
|
||||
CHECK(st.lstats == 1, "deep scan lstats only its image (got %ld)", st.lstats);
|
||||
mr_library_free(&deep_lib);
|
||||
|
||||
mr_library_free(&lib);
|
||||
rm_rf(root);
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/* test_browse_ui_host.c — 书库浏览页宿主集成测试(可选,需 SDK 源码树)。
|
||||
*
|
||||
* 目的(M13):
|
||||
* U1 列表用 src2 模式(每帧重取行):扫描后行副标题是占位「…」,后台探测完成、
|
||||
* on_tick 补位后同一行立即显示页数——验证懒算结果能上屏(固定池 vsource 做不到);
|
||||
* U2 行语义:书行显示 "N pages"(无进入箭头),空目录「空目录」,无图目录「无图片」;
|
||||
* U3 导航:可逐级上溯父目录;文件系统 "/" 处 library_go_up 返回 0(app.c 据此退出);
|
||||
* 「TF卡」回根动作回到书库根;
|
||||
* U4 不可读目录:load 失败 → 停留原目录 + 顶部提示横幅。
|
||||
*
|
||||
* 编译:见 test/run_tests.sh(与 test_settings_lock_host 同口径:链 SDK 源码树 + 打桩;
|
||||
* 只发布 meow_ui.a 的交付环境自动 SKIP)。ui/library/library.c 以
|
||||
* -DMR_LIBRARY_UI_TEST_HOOKS 编译出测试接缝;本测试另打桩 mr_reader_open_dir/mr_nav_report。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "logic/library.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
|
||||
/* ---- ui/library/library.c 的测试接缝(仅 -DMR_LIBRARY_UI_TEST_HOOKS 时定义) ---- */
|
||||
int library_test_load(const char *dir);
|
||||
void library_test_set_root(const char *root);
|
||||
const char *library_test_root(void);
|
||||
const char *library_test_dir(void);
|
||||
void library_test_set_dir(const char *dir);
|
||||
int library_test_rows(void);
|
||||
int library_test_src(int idx, meow_list_item *item);
|
||||
int library_test_hint_active(void);
|
||||
const char *library_test_hint(void);
|
||||
int library_test_go_root(void);
|
||||
int library_test_go_up(void);
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { printf(" PASS\n"); } \
|
||||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
/* app.c 的导航上报在测试里不需要网络侧实现。 */
|
||||
void mr_nav_report(void) {}
|
||||
|
||||
/* reader 打桩:只记录被打开的册路径(本测试不建 reader 页)。 */
|
||||
static char g_opened[MR_PATH_MAX];
|
||||
static int g_opened_n;
|
||||
void mr_reader_open_dir(const char *path)
|
||||
{
|
||||
snprintf(g_opened, sizeof g_opened, "%s", path ? path : "");
|
||||
g_opened_n++;
|
||||
}
|
||||
|
||||
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)) { chmod(path, 0700); return unlink(path); }
|
||||
chmod(path, 0700);
|
||||
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 int row_index_for(const char *name)
|
||||
{
|
||||
int rows = library_test_rows();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
meow_list_item it;
|
||||
if (!library_test_src(i, &it)) continue;
|
||||
if (it.title && strcmp(it.title, name) == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void wait_ready(int ms)
|
||||
{
|
||||
for (int t = 0; t < ms / 10; t++) {
|
||||
library_on_tick();
|
||||
int ready = 1;
|
||||
int rows = library_test_rows();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
meow_list_item it;
|
||||
if (!library_test_src(i, &it)) continue;
|
||||
if (it.subtitle && strcmp(it.subtitle, "…") == 0) { ready = 0; break; }
|
||||
}
|
||||
if (ready) return;
|
||||
usleep(10000);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = ".";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/mr_browse_ui_XXXXXX", base);
|
||||
char *data = mkdtemp(tmpl);
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
char root[MR_PATH_MAX];
|
||||
snprintf(root, sizeof root, "%s/library", data);
|
||||
char p[MR_PATH_MAX];
|
||||
const char *dirs[] = { "book", "empty", "noimg", "deep/sub", "MeowOS/sub" };
|
||||
for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) {
|
||||
joinp(p, sizeof p, root, dirs[i]);
|
||||
if (mkdir_p(p) != 0) { printf("SKIP: mkdir %s failed\n", p); return 0; }
|
||||
}
|
||||
joinp(p, sizeof p, root, "book/page2.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "book/page10.jpg"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "noimg/readme.txt"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "deep/sub/001.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "MeowOS/sub/m.png"); touch_file(p);
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
meow_view *lib = library_view_create();
|
||||
library_test_set_root(root);
|
||||
CHECK(strcmp(library_test_root(), root) == 0, "test root installed");
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "library active");
|
||||
|
||||
/* ---- U1:src2 行 + 懒算补位 ---- */
|
||||
CHECK(library_test_load(root) == 1, "load fixture");
|
||||
CHECK(library_test_rows() == 5, "5 rows (got %d)", library_test_rows());
|
||||
|
||||
static const char *want[5] = { "book", "deep", "empty", "MeowOS", "noimg" };
|
||||
for (int i = 0; i < 5; i++) {
|
||||
meow_list_item it;
|
||||
CHECK(library_test_src(i, &it) == 1, "src row %d", i);
|
||||
CHECK(it.title && strcmp(it.title, want[i]) == 0, "row[%d]=%s (want %s)",
|
||||
i, it.title ? it.title : "(null)", want[i]);
|
||||
CHECK(it.subtitle && strcmp(it.subtitle, "…") == 0,
|
||||
"row[%d] starts with placeholder (got %s)", i, it.subtitle ? it.subtitle : "");
|
||||
CHECK(it.right == MEOW_LIST_RIGHT_ARROW, "row[%d] has enter arrow while pending", i);
|
||||
}
|
||||
|
||||
wait_ready(3000);
|
||||
/* U2:补位后的行语义 */
|
||||
for (int i = 0; i < 5; i++) {
|
||||
meow_list_item it;
|
||||
library_test_src(i, &it);
|
||||
CHECK(it.subtitle && strcmp(it.subtitle, "…") != 0,
|
||||
"row[%d] subtitle filled (got %s)", i, it.subtitle ? it.subtitle : "");
|
||||
}
|
||||
int ib = row_index_for("book");
|
||||
int ie = row_index_for("empty");
|
||||
int in = row_index_for("noimg");
|
||||
int id = row_index_for("deep");
|
||||
meow_list_item it;
|
||||
CHECK(ib >= 0 && library_test_src(ib, &it) && strcmp(it.subtitle, "2 pages") == 0,
|
||||
"book subtitle=%s", it.subtitle ? it.subtitle : "");
|
||||
CHECK(it.right == MEOW_LIST_RIGHT_NONE, "book row has no enter arrow (opens reader)");
|
||||
CHECK(ie >= 0 && library_test_src(ie, &it) && strcmp(it.subtitle, "空目录") == 0,
|
||||
"empty subtitle=%s", it.subtitle ? it.subtitle : "");
|
||||
CHECK(in >= 0 && library_test_src(in, &it) && strcmp(it.subtitle, "无图片") == 0,
|
||||
"noimg subtitle=%s", it.subtitle ? it.subtitle : "");
|
||||
CHECK(id >= 0 && library_test_src(id, &it) && strcmp(it.subtitle, "无图片") == 0,
|
||||
"deep (images only in subdir) is not a book: %s", it.subtitle ? it.subtitle : "");
|
||||
|
||||
/* ---- 行点击:书 → reader;空目录 → 进入 ---- */
|
||||
int list_top = meow_ui_statusbar_height() + 56;
|
||||
g_opened_n = 0;
|
||||
library_on_tap(120, list_top + ib * 56 + 28);
|
||||
CHECK(g_opened_n == 1 && strstr(g_opened, "/book") != NULL,
|
||||
"tap book opens reader (%s)", g_opened);
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "tap book does not navigate");
|
||||
|
||||
library_on_tap(120, list_top + ie * 56 + 28);
|
||||
char empty_path[MR_PATH_MAX];
|
||||
joinp(empty_path, sizeof empty_path, root, "empty");
|
||||
CHECK(strcmp(library_test_dir(), empty_path) == 0,
|
||||
"tap empty dir enters it (%s)", library_test_dir());
|
||||
CHECK(library_test_rows() == 0, "empty dir shows empty list");
|
||||
|
||||
/* ---- U3:导航(逐级上溯 / 到 / 退出 / 回 TF 根) ---- */
|
||||
CHECK(library_test_go_up() == 1, "go_up from empty consumed");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "go_up to fixture root");
|
||||
|
||||
char deep[MR_PATH_MAX];
|
||||
joinp(deep, sizeof deep, root, "deep/sub");
|
||||
CHECK(library_test_load(deep) == 1, "load deep/sub");
|
||||
CHECK(library_test_go_up() == 1, "go_up from deep/sub");
|
||||
joinp(p, sizeof p, root, "deep");
|
||||
CHECK(strcmp(library_test_dir(), p) == 0, "now at deep");
|
||||
|
||||
/* 「TF卡」回根 */
|
||||
CHECK(library_test_load(deep) == 1, "reload deep/sub");
|
||||
CHECK(library_test_go_root() == 1 && strcmp(library_test_dir(), root) == 0,
|
||||
"TF卡 action returns to library root");
|
||||
|
||||
/* 上溯到文件系统 /:go_up 在 / 返回 0(app.c 据此退出)。
|
||||
* 宿主(Termux)读不了 "/":从 /usr 返回时 load("/") 失败,仍应切到 "/" 的空列表
|
||||
* (否则会卡在 /usr 无法退出),下一次返回=退出 App。 */
|
||||
library_test_set_dir("/usr");
|
||||
CHECK(library_test_go_up() == 1, "go_up toward / consumed");
|
||||
CHECK(strcmp(library_test_dir(), "/") == 0, "reached / (unreadable root fallback)");
|
||||
CHECK(library_test_go_up() == 0, "go_up at / returns 0 (app exits)");
|
||||
CHECK(strcmp(library_test_dir(), "/") == 0, "still at / after exit gesture");
|
||||
|
||||
/* ---- U4:不可读目录 → 停留 + 提示 ---- */
|
||||
CHECK(library_test_load(root) == 1, "back to fixture root");
|
||||
char noread[MR_PATH_MAX];
|
||||
joinp(noread, sizeof noread, root, "noread");
|
||||
if (geteuid() != 0) {
|
||||
CHECK(mkdir(noread, 0755) == 0, "mkdir noread");
|
||||
chmod(noread, 0000);
|
||||
int ok = library_test_load(noread);
|
||||
CHECK(ok == 0, "unreadable dir load fails");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "stays in previous dir (%s)",
|
||||
library_test_dir());
|
||||
CHECK(library_test_hint_active(), "hint banner shown: %s", library_test_hint());
|
||||
CHECK(strstr(library_test_hint(), "无法读取") != NULL,
|
||||
"hint text mentions unreadable: %s", library_test_hint());
|
||||
chmod(noread, 0700);
|
||||
} else {
|
||||
printf(" SKIP unreadable-dir case (running as root)\n");
|
||||
}
|
||||
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
rm_rf(data);
|
||||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||
g_fail ? "see failures above" : "browse UI checks passed");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
+70
-26
@@ -15,12 +15,13 @@
|
||||
* T6 坏档容错:非 zip/截断/无图片页/声明 96MiB 的 zip 炸弹 → 打开失败或 0 页,不崩。
|
||||
* T7 统一开册契约:目录册与档案册走同一个 mr_book(count/meta/open/close/destroy),
|
||||
* 越界 open 返回 NULL,close_auto 幂等。
|
||||
* T8 书库列档:.cbz 与文件夹册同列自然序(bookA < box < vol2.cbz < Vol10.cbz),
|
||||
* 行=文件名+页数;坏档/无图档案不列;只含档案册的目录写 "N books";
|
||||
* 不注入探测回调时行为与 M7 完全一致(纯目录书库)。
|
||||
* T8 书库列档(M13 单层 + 懒判定):.cbz 与文件夹册同列自然序(bookA < box < ... <
|
||||
* Vol10.cbz);扫描期只成行、副标题占位,探测补位后才有页数/书标记;
|
||||
* 坏档/无图片页档案仍列出但为非书(访问放开,不再隐藏);
|
||||
* 不注入探测回调时档案文件不成行(M7 语义),目录仍按单层列出。
|
||||
* T9 册间衔接:同父目录自然序混排(bookA → vol2.cbz → Vol10.cbz),
|
||||
* 首/末册返回 0;不注入探测回调时档案册不参与(M7 语义)。
|
||||
* T10 续读角标:档案路径作为册标识,副标题后缀「· 续读 Pn」。
|
||||
* T10 续读角标(M13 两段式):档案路径作为册标识,probe 补位后副标题后缀「· 续读 Pn」。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -Werror -O1 -I. test/test_cbz.c logic/library.c logic/reader.c \
|
||||
@@ -715,19 +716,49 @@ static const mr_library_entry *lib_find(const mr_library *lib, const char *name)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* M13:模拟 UI 后台探测补位(目录单层探测 / 档案册注入页数回调)。 */
|
||||
static void lib_probe_all(mr_library *lib)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++) {
|
||||
mr_library_probe p;
|
||||
if (mr_library_probe_path(lib->items[i].path, lib->items[i].is_archive,
|
||||
archive_pages_probe, NULL, &p) == 0)
|
||||
mr_library_apply_probe(lib, i, &p);
|
||||
else
|
||||
mr_library_mark_error(lib, i);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_library(void)
|
||||
{
|
||||
mr_test_cfg_clear();
|
||||
|
||||
/* M13:扫描只做单层枚举——目录(全部列出,含无图的 box)与档案文件(.cbz/.zip)
|
||||
* 都先成行、副标题占位;书/非书与页数由后台 probe 补位。 */
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan_ex(&lib, g_lib, MR_LIB_MAX_DEPTH, archive_pages_probe, NULL);
|
||||
CHECK(n == 4, "library rows=%d (want bookA, box, vol2.cbz, Vol10.cbz)", n);
|
||||
if (n == 4) {
|
||||
CHECK(strcmp(lib.items[0].name, "bookA") == 0, "row0=%s", lib.items[0].name);
|
||||
CHECK(strcmp(lib.items[1].name, "box") == 0, "row1=%s", lib.items[1].name);
|
||||
CHECK(strcmp(lib.items[2].name, "vol2.cbz") == 0, "row2=%s", lib.items[2].name);
|
||||
CHECK(strcmp(lib.items[3].name, "Vol10.cbz") == 0, "row3=%s", lib.items[3].name);
|
||||
CHECK(n == 8, "library rows=%d (want 2 dirs + 6 archives)", n);
|
||||
static const char *want[8] = {
|
||||
"bookA", "box", "broken.cbz", "emptypages.cbz",
|
||||
"notabook.zip", "truncated.cbz", "vol2.cbz", "Vol10.cbz"
|
||||
};
|
||||
for (int i = 0; i < 8 && i < lib.count; i++) {
|
||||
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
|
||||
"row[%d]=%s (want %s)", i, lib.items[i].name, want[i]);
|
||||
CHECK(lib.items[i].probe_state == MR_LIB_PENDING && lib.items[i].pages == -1,
|
||||
"%s pending after scan", lib.items[i].name);
|
||||
CHECK(strcmp(lib.items[i].subtitle, "…") == 0,
|
||||
"%s placeholder subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
|
||||
}
|
||||
|
||||
/* 后台补位(等价 UI 的 on_tick):目录走单层目录探测,档案走注入的 zip 页数回调。 */
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_probe p;
|
||||
int ok = (mr_library_probe_path(lib.items[i].path, lib.items[i].is_archive,
|
||||
archive_pages_probe, NULL, &p) == 0);
|
||||
CHECK(ok, "%s probed", lib.items[i].name);
|
||||
if (ok) mr_library_apply_probe(&lib, i, &p);
|
||||
}
|
||||
|
||||
const mr_library_entry *e = lib_find(&lib, "bookA");
|
||||
@@ -737,15 +768,17 @@ static void test_library(void)
|
||||
CHECK(e && strstr(e->cover, "001.png") != NULL, "bookA cover=%s",
|
||||
e ? e->cover : "");
|
||||
|
||||
/* box 只含档案册:目录本身不直接含图 → 不是书(M13 不做递归/子表统计)。 */
|
||||
e = lib_find(&lib, "box");
|
||||
CHECK(e && e->has_images == 0 && e->subdirs == 0, "box fields");
|
||||
CHECK(e && strcmp(e->subtitle, "2 books") == 0, "box subtitle=%s",
|
||||
CHECK(e && e->has_images == 0 && e->subdirs == 0 && e->is_archive == 0,
|
||||
"box fields");
|
||||
CHECK(e && strcmp(e->subtitle, "无图片") == 0, "box subtitle=%s",
|
||||
e ? e->subtitle : "");
|
||||
CHECK(e && e->cover[0] == '\0', "box cover should be empty");
|
||||
|
||||
e = lib_find(&lib, "vol2.cbz");
|
||||
CHECK(e && e->pages == 3 && e->has_images == 1 && e->subdirs == 0,
|
||||
"vol2.cbz fields");
|
||||
CHECK(e && e->pages == 3 && e->has_images == 1 && e->subdirs == 0 &&
|
||||
e->is_archive == 1, "vol2.cbz fields");
|
||||
CHECK(e && strcmp(e->subtitle, "3 pages") == 0, "vol2.cbz subtitle=%s",
|
||||
e ? e->subtitle : "");
|
||||
CHECK(e && e->cover[0] == '\0', "archive cover should be empty (UI extracts)");
|
||||
@@ -756,19 +789,24 @@ static void test_library(void)
|
||||
CHECK(e && e->pages == 1 && strcmp(e->subtitle, "1 page") == 0,
|
||||
"Vol10.cbz pages/subtitle");
|
||||
|
||||
CHECK(lib_find(&lib, "notabook.zip") == NULL, "text-only zip listed");
|
||||
CHECK(lib_find(&lib, "emptypages.cbz") == NULL, "no-image cbz listed");
|
||||
CHECK(lib_find(&lib, "broken.cbz") == NULL, "broken cbz listed");
|
||||
CHECK(lib_find(&lib, "truncated.cbz") == NULL, "truncated cbz listed");
|
||||
CHECK(lib_find(&lib, ".hidden.cbz") == NULL, "hidden cbz listed");
|
||||
/* 坏档/无图片页档案:列出但探测为非书(M13 访问放开,不再隐藏)。 */
|
||||
static const char *not_book[] = { "notabook.zip", "emptypages.cbz",
|
||||
"broken.cbz", "truncated.cbz" };
|
||||
for (unsigned i = 0; i < sizeof not_book / sizeof not_book[0]; i++) {
|
||||
e = lib_find(&lib, not_book[i]);
|
||||
CHECK(e && e->is_archive == 1 && e->has_images == 0 && e->pages == 0 &&
|
||||
strcmp(e->subtitle, "无图片") == 0,
|
||||
"%s: not a book (subtitle=%s)", not_book[i], e ? e->subtitle : "(missing)");
|
||||
}
|
||||
CHECK(lib_find(&lib, ".hidden.cbz") == NULL, "hidden cbz not listed");
|
||||
mr_library_free(&lib);
|
||||
|
||||
/* 不注入探测回调:与 M7 完全一致(档案册不参与,只含档案册的目录也不 nav)。 */
|
||||
/* 不注入探测回调:M7 纯目录书库(档案文件不成行,目录仍按 M13 单层列出)。 */
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH) == 1,
|
||||
"legacy scan rows=%d (want 1: bookA)", lib.count);
|
||||
CHECK(lib.count >= 1 && strcmp(lib.items[0].name, "bookA") == 0,
|
||||
"legacy scan row0");
|
||||
CHECK(mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH) == 2,
|
||||
"legacy scan rows=%d (want 2: bookA, box)", lib.count);
|
||||
CHECK(lib.count >= 2 && strcmp(lib.items[0].name, "bookA") == 0 &&
|
||||
strcmp(lib.items[1].name, "box") == 0, "legacy scan rows");
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
@@ -835,11 +873,16 @@ static void test_badge_cover(void)
|
||||
mr_test_cfg_clear();
|
||||
mr_cont_save(vol2, 2, MR_MODE_WEBTOON);
|
||||
|
||||
/* M13 两段式:扫描期只有占位副标题;后台 probe 补位后才出现页数与续读角标。 */
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan_ex(&lib, g_lib, MR_LIB_MAX_DEPTH, archive_pages_probe,
|
||||
NULL) == 4, "rescan for badge");
|
||||
NULL) == 8, "rescan for badge (single-layer rows)");
|
||||
const mr_library_entry *e = lib_find(&lib, "vol2.cbz");
|
||||
CHECK(e && strcmp(e->subtitle, "…") == 0, "vol2 placeholder before probe: %s",
|
||||
e ? e->subtitle : "");
|
||||
lib_probe_all(&lib);
|
||||
e = lib_find(&lib, "vol2.cbz");
|
||||
CHECK(e && strstr(e->subtitle, "续读 P3") != NULL, "badge subtitle=%s",
|
||||
e ? e->subtitle : "");
|
||||
CHECK(e && strstr(e->subtitle, "3 pages") != NULL, "badge keeps page text: %s",
|
||||
@@ -848,10 +891,11 @@ static void test_badge_cover(void)
|
||||
CHECK(e && strstr(e->subtitle, "续读") == NULL, "Vol10 should have no badge");
|
||||
mr_library_free(&lib);
|
||||
|
||||
/* 清除记录后角标消失 */
|
||||
/* 清除记录后角标消失(重扫 + 再探测;探测缓存只存页数,角标按当前配置重算)。 */
|
||||
mr_cont_save(vol2, 0, MR_MODE_WEBTOON);
|
||||
memset(&lib, 0, sizeof lib);
|
||||
mr_library_scan_ex(&lib, g_lib, MR_LIB_MAX_DEPTH, archive_pages_probe, NULL);
|
||||
lib_probe_all(&lib);
|
||||
e = lib_find(&lib, "vol2.cbz");
|
||||
CHECK(e && strstr(e->subtitle, "续读") == NULL, "cleared badge gone");
|
||||
mr_library_free(&lib);
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
* A3 边界:末页前进→下一册、首页后退→上一册(调用方语义);路径末尾斜杠等价;
|
||||
* A4 排除:隐藏/系统目录、符号链接、普通文件不参与兄弟册;
|
||||
* A5 参数:delta 非 ±1、NULL/小缓冲、不存在的目录;
|
||||
* A6 书库扫描接线:有续读记录的册,行副标题出现「· 续读 Pn」角标。
|
||||
* 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 -o test/test_cont_adjacent
|
||||
* test/settings_reader_stub.c -pthread -o test/test_cont_adjacent
|
||||
* 或 test/run_webtoon_tests.sh。
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
@@ -158,6 +159,20 @@ static void test_adjacent(const char *root)
|
||||
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();
|
||||
@@ -173,28 +188,52 @@ static void test_library_badge(const char *root)
|
||||
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);
|
||||
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(strstr(e->subtitle, "续读") == NULL, "vol2 no badge: %s", e->subtitle);
|
||||
CHECK(strcmp(e->subtitle, "1 page") == 0, "vol2 subtitle=%s", e->subtitle);
|
||||
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");
|
||||
|
||||
+32
-31
@@ -3,12 +3,14 @@
|
||||
* 覆盖:
|
||||
* T1 自然排序:数字段数值序(page2<page10)、前导零、数字/字母混合、大小写;
|
||||
* T2 扩展名过滤:png/jpg/jpeg/bmp 大小写不敏感,gif/txt 不支持;
|
||||
* T3 目录扫描:只列出含图目录与可导航中间目录;隐藏/系统目录、空目录、无图目录排除;
|
||||
* pages/subdirs/cover/subtitle 正确;父目录路径推导。
|
||||
* T3 目录扫描(M13 单层语义):只列当前层直接子目录(自然升序),不递归探测子树;
|
||||
* 系统目录/空目录/无图目录都列出;隐藏项/普通文件/符号链接不列;
|
||||
* max_depth 兼容参数被忽略;行初始为「待探测」占位;父目录路径推导;
|
||||
* T4 阅读方向状态(logic/reader.c)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. test/test_library.c logic/library.c logic/reader.c \
|
||||
* -o test/test_library && ./test/test_library
|
||||
* 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"
|
||||
@@ -148,44 +150,43 @@ static void test_scan(void)
|
||||
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); /* 根目录直接图片不成为行 */
|
||||
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 == 4, "scan count=%d (want 4)", n);
|
||||
CHECK(n == 7, "scan count=%d (want 7: all direct subdirs)", n);
|
||||
CHECK(lib.count == n, "count matches");
|
||||
if (lib.count == 4) {
|
||||
CHECK(strcmp(lib.items[0].name, "Nested") == 0, "sort[0]=Nested got %s", lib.items[0].name);
|
||||
CHECK(strcmp(lib.items[1].name, "Series 2") == 0, "sort[1]=Series 2 got %s", lib.items[1].name);
|
||||
CHECK(strcmp(lib.items[2].name, "Series 02") == 0, "sort[2]=Series 02 got %s", lib.items[2].name);
|
||||
CHECK(strcmp(lib.items[3].name, "Series 10") == 0, "sort[3]=Series 10 got %s", lib.items[3].name);
|
||||
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);
|
||||
}
|
||||
|
||||
const mr_library_entry *e = find_entry(&lib, "Series 10");
|
||||
CHECK(e && e->pages == 2 && e->has_images == 1 && e->subdirs == 0, "Series 10 pages=2");
|
||||
CHECK(e && strcmp(e->subtitle, "2 pages") == 0, "Series 10 subtitle '%s'", e ? e->subtitle : "");
|
||||
CHECK(e && strstr(e->cover, "page1.png") != NULL, "Series 10 cover natural-first '%s'",
|
||||
e ? e->cover : "");
|
||||
|
||||
e = find_entry(&lib, "Series 2");
|
||||
CHECK(e && e->pages == 1 && e->has_images == 1, "Series 2 pages=1");
|
||||
|
||||
e = find_entry(&lib, "Nested");
|
||||
CHECK(e && e->pages == 0 && e->has_images == 0 && e->subdirs == 1, "Nested intermediate");
|
||||
CHECK(e && strcmp(e->subtitle, "1 folder") == 0, "Nested subtitle '%s'", e ? e->subtitle : "");
|
||||
CHECK(e && strstr(e->cover, "Deeper/a.png") != NULL, "Nested cover from descendant '%s'",
|
||||
e ? e->cover : "");
|
||||
|
||||
CHECK(find_entry(&lib, "Empty") == NULL, "empty dir excluded");
|
||||
CHECK(find_entry(&lib, "Series 3") == NULL, "gif-only dir excluded");
|
||||
/* 访问放开:系统目录/空目录/无图目录都列出;隐藏项/文件不列。 */
|
||||
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, "MeowAPPs") == NULL, "system dir excluded");
|
||||
CHECK(find_entry(&lib, "root.png") == NULL, "loose image file excluded");
|
||||
|
||||
/* 深度 1:Nested 不再可探测到 Deeper 的图,应被排除。 */
|
||||
/* 首帧不做懒判定:所有行待探测(副标题占位)。 */
|
||||
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) == 3, "max_depth=1 excludes Nested");
|
||||
CHECK(mr_library_scan(&lib1, root, 1) == 7, "max_depth ignored (single layer)");
|
||||
mr_library_free(&lib1);
|
||||
|
||||
mr_library_free(&lib);
|
||||
|
||||
Reference in new issue
Block a user