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

275 lines
9.4 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_browse_nav.c — 浏览模型 / 进入目录 / 截断(纯 C,无 SDK)。
*
* 覆盖(M18 文件管理器模型):
* N1 单层列表:目录 + 受支持图片 + 档案册(cbz/zip)都成行,目录优先+自然序;
* 系统目录/空目录/无图目录都列出且可进入;隐藏项/普通文件/符号链接不列;
* N2 不可读目录/不存在/不是目录:scan 返回 -1,调用方据此显示空态或提示;
* N3 父目录路径推导:mr_path_parent 是纯路径工具(可算到文件系统 "/";
* 浏览越界的限制在 UI 导航层,见 test_browse_ui_host 的根锁定用例);
* N4 单目录条目上限:达到 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 -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 ---- */
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 == 9, "root lists 9 rows (got %d)", n);
CHECK(lib.truncated == 0, "root not truncated");
static const char *want[9] = {
"book", "empty", "lost+found", "MeowAPPs", "MeowData", "MeowOS", "noimg",
"album.cbz", "loose.png"
};
for (int i = 0; i < 9 && 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, "loose image listed (clickable)");
CHECK(find_entry(&lib, "album.cbz") != NULL, "archive listed");
CHECK(find_entry(&lib, "readme.txt") == NULL, "plain file excluded");
/* 可进入:每个列出的目录都能被 scan(0 项也算成功,不等于 -1)。 */
for (int i = 0; i < lib.count; i++) {
if (lib.items[i].kind != MR_LIB_KIND_DIR) continue;
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);
}
char p[MR_PATH_MAX];
joinp(p, sizeof p, root, "book");
mr_library book;
memset(&book, 0, sizeof book);
CHECK(mr_library_scan(&book, p, MR_LIB_MAX_DEPTH) == 1 &&
find_entry(&book, "001.png") != NULL, "dir with images lists the image");
mr_library_free(&book);
joinp(p, sizeof p, root, "MeowOS");
mr_library syslib;
memset(&syslib, 0, sizeof syslib);
CHECK(mr_library_scan(&syslib, p, MR_LIB_MAX_DEPTH) == 2 &&
find_entry(&syslib, "sub") != NULL &&
find_entry(&syslib, "001.png") != 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);
}
/* ---- N2 ---- */
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/m18", 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);
}
}
/* ---- N3 ---- */
static void test_parent_path(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,
"path helper is root-agnostic: %s", up);
}
/* ---- N4 ---- */
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);
joinp(p, sizeof p, root, "album.cbz");
touch_file(p);
joinp(p, sizeof p, root, "readme.txt");
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_parent_path();
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;
}