图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(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;交叉构建零警告。
This commit is contained in:
1 parent
d07b99056f
commit
64336f95d9
16 files changed
+940
-1927
No files matched your search
+222
-195
@@ -1,33 +1,35 @@
|
||||
/* 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 失败 → 停留原目录 + 顶部提示横幅;
|
||||
* U5 启动根不可读:create 时仍建空列表(不是只有横幅);
|
||||
* U6 缩略图 LRU 跳过解压页缓存(M11 P2-1 回归):cache_prune 只统计/删除缩略图,
|
||||
* mrpage_*(page_cache 的解压页)必须保留且不计入 64 张额度。
|
||||
* 目的(M18 文件管理器模型):
|
||||
* U1 行模型:目录 + 图片 + 档案册混排(目录优先+自然序),行副标题是扫描期生成的
|
||||
* 类型·大小(不再有占位/后台补位),图标/右侧箭头按类型区分;
|
||||
* U2 行合成零 IO:反复取可见行不触发 opendir/readdir/lstat(渲染期无文件访问);
|
||||
* U3 点击语义:点目录=进入(空目录也进);点图片=从该图开始读
|
||||
* (mr_reader_open_dir_at(当前目录, 图片下标));点档案册=打开档案册;
|
||||
* U4 根目录锁定:根不显示顶栏返回、library_go_up 在根返回 0(app.c 退出);
|
||||
* 子目录返回=回根;不存在越过浏览根的导航;
|
||||
* U5 无 SD 卡容错:浏览根不可访问(不存在/不是目录)→ 友好空态「未检测到 SD 卡」,
|
||||
* 不弹错误横幅;顶栏刷新/重扫可重试成功;
|
||||
* U6 子目录不可读:停留原目录 + 顶部提示横幅;
|
||||
* U7 书库不再碰缓存目录:CBZ 解压页缓存 mrpage_* 原样保留,且不写任何 bmp。
|
||||
*
|
||||
* 编译:见 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。
|
||||
* 编译:见 test/run_tests.sh(链 SDK 源码树 + 打桩;只发布 meow_ui.a 的环境自动 SKIP)。
|
||||
* ui/library/library.c 以 -DMR_LIBRARY_UI_TEST_HOOKS 编译出测试接缝;logic/library.c
|
||||
* 以 -DMR_LIBRARY_TEST_HOOKS 编译出 IO 计数。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader/page_cache.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
|
||||
/* ---- ui/library/library.c 的测试接缝(仅 -DMR_LIBRARY_UI_TEST_HOOKS 时定义) ---- */
|
||||
/* 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);
|
||||
@@ -37,14 +39,26 @@ 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_topbar_back(void);
|
||||
const char *library_test_empty_text(void);
|
||||
int library_test_sd_missing(void);
|
||||
int library_test_retry(void);
|
||||
int library_test_go_up(void);
|
||||
int library_test_go_parent(void);
|
||||
int library_test_drag_active(void);
|
||||
int library_test_has_list(void);
|
||||
const char *library_test_cache_dir(void);
|
||||
int library_test_cache_on(void);
|
||||
void library_test_cache_prune(void);
|
||||
|
||||
/* 阅读器入口打桩:只记录被打开的册路径与起始页(本测试不建 reader 页)。 */
|
||||
#define MR_READER_START_RESUME (-1)
|
||||
void mr_reader_open_dir_at(const char *dir, int start_index);
|
||||
static char g_opened[MR_PATH_MAX];
|
||||
static int g_opened_start;
|
||||
static int g_opened_n;
|
||||
void mr_reader_open_dir_at(const char *dir, int start_index)
|
||||
{
|
||||
snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : "");
|
||||
g_opened_start = start_index;
|
||||
g_opened_n++;
|
||||
}
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
@@ -55,15 +69,6 @@ static int g_fail;
|
||||
/* 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];
|
||||
@@ -125,16 +130,15 @@ static int row_index_for(const char *name)
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void set_mtime(const char *path, time_t t)
|
||||
static void tap_row(const char *name)
|
||||
{
|
||||
struct timespec ts[2];
|
||||
ts[0].tv_sec = t;
|
||||
ts[0].tv_nsec = 0;
|
||||
ts[1] = ts[0];
|
||||
utimensat(AT_FDCWD, path, ts, 0);
|
||||
int idx = row_index_for(name);
|
||||
if (idx < 0) { CHECK(0, "row %s not found", name); return; }
|
||||
int list_top = meow_ui_statusbar_height() + 56;
|
||||
library_on_tap(120, list_top + idx * 56 + 28);
|
||||
}
|
||||
|
||||
/* 统计缓存目录里的缩略图(*.bmp 且非 mrpage_*)与解压页(mrpage_*.bmp)。 */
|
||||
/* 统计缓存目录里的 bmp(缩略图/解压页)。 */
|
||||
static int count_bmp(const char *dir, int *pages)
|
||||
{
|
||||
DIR *d = opendir(dir);
|
||||
@@ -144,7 +148,7 @@ static int count_bmp(const char *dir, int *pages)
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
size_t len = strlen(de->d_name);
|
||||
if (len <= 4 || strcmp(de->d_name + len - 4, ".bmp") != 0) continue;
|
||||
if (strncmp(de->d_name, "mrpage_", 7) == 0) pg++;
|
||||
if (mr_cache_is_page_name(de->d_name)) pg++;
|
||||
else thumbs++;
|
||||
}
|
||||
closedir(d);
|
||||
@@ -152,71 +156,6 @@ static int count_bmp(const char *dir, int *pages)
|
||||
return thumbs;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* U6:缩略图 LRU 必须跳过解压页缓存(M11 P2-1 回归;MR_CACHE_MAX_FILES=64)。 */
|
||||
static void test_cache_prune_skips_pages(void)
|
||||
{
|
||||
const char *dir = library_test_cache_dir();
|
||||
CHECK(library_test_cache_on() && dir && dir[0], "thumbnail cache enabled: %s",
|
||||
dir ? dir : "");
|
||||
if (!dir || !dir[0]) return;
|
||||
|
||||
/* 清空之前可能写入的 bmp(本测试独占 MEOW_DATA_DIR)。 */
|
||||
DIR *d = opendir(dir);
|
||||
if (d) {
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
size_t len = strlen(de->d_name);
|
||||
if (len > 4 && strcmp(de->d_name + len - 4, ".bmp") == 0) {
|
||||
char full[MR_PATH_MAX + 96];
|
||||
snprintf(full, sizeof full, "%s/%s", dir, de->d_name);
|
||||
unlink(full);
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
char page[MR_PATH_MAX + 96], thumb[MR_PATH_MAX + 96];
|
||||
snprintf(page, sizeof page, "%s/mrpage_0123456789abcdef.bmp", dir);
|
||||
CHECK(touch_file(page) == 0, "create page cache file");
|
||||
set_mtime(page, time(NULL) - 7200); /* 最旧:若被误删一定先删它 */
|
||||
|
||||
time_t base = time(NULL) - 3600;
|
||||
for (int i = 0; i < 65; i++) { /* 65 张缩略图 > MR_CACHE_MAX_FILES */
|
||||
snprintf(thumb, sizeof thumb, "%s/%016x_%ld_%d.bmp", dir,
|
||||
(unsigned)i, (long)(base + i), i);
|
||||
CHECK(touch_file(thumb) == 0, "create thumbnail %d", i);
|
||||
set_mtime(thumb, base + i);
|
||||
}
|
||||
|
||||
int pages_before = 0;
|
||||
CHECK(count_bmp(dir, &pages_before) == 65, "precondition 65 thumbnails");
|
||||
CHECK(pages_before == 1, "precondition 1 page (got %d)", pages_before);
|
||||
|
||||
library_test_cache_prune();
|
||||
|
||||
int pages_after = 0;
|
||||
int thumbs_after = count_bmp(dir, &pages_after);
|
||||
CHECK(access(page, R_OK) == 0, "mrpage_* survives thumbnail LRU");
|
||||
CHECK(pages_after == 1, "mrpage_* not counted as thumbnail (got %d)", pages_after);
|
||||
CHECK(thumbs_after == 64, "thumbnails trimmed to 64 (got %d)", thumbs_after);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
@@ -227,129 +166,192 @@ int main(void)
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
char root[MR_PATH_MAX];
|
||||
char root[MR_PATH_MAX], p[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" };
|
||||
const char *dirs[] = { "album", "empty", "noimg", "deep/sub", "Pictures", "Pictures/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, "album/001.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "album/002.png"); 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);
|
||||
joinp(p, sizeof p, root, "root.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "book.cbz"); touch_file(p);
|
||||
|
||||
/* Pictures:大小写混排 + 数字序号 + 大写扩展名 + 档案册(图片排序契约夹具)。 */
|
||||
const char *pics[] = { "A.jpg", "a.jpg", "img2.png", "IMG3.JPG", "img10.png",
|
||||
"page01.bmp", "page1.jpeg", "cover.cbz", "notes.txt" };
|
||||
for (unsigned i = 0; i < sizeof pics / sizeof pics[0]; i++) {
|
||||
joinp(p, sizeof p, root, "Pictures");
|
||||
size_t dl = strlen(p);
|
||||
snprintf(p + dl, sizeof p - dl, "/%s", pics[i]);
|
||||
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; }
|
||||
|
||||
/* U5:先指向一个不存在的根,再 create —— 真实走「启动时根目录不可读」分支。 */
|
||||
const char *bad_root = "/no/such/tuku/m13_root";
|
||||
/* ---- U5:浏览根不可访问 —— 先指向不存在的根,再 create ---- */
|
||||
const char *bad_root = "/no/such/tuku/m18_root";
|
||||
library_test_set_root(bad_root);
|
||||
CHECK(strcmp(library_test_root(), bad_root) == 0, "bad root installed");
|
||||
meow_view *lib = library_view_create();
|
||||
CHECK(library_test_hint_active(), "startup unreadable root shows hint: %s",
|
||||
library_test_hint());
|
||||
CHECK(library_test_rows() == 0, "startup empty list rows=%d", library_test_rows());
|
||||
CHECK(library_test_has_list(), "startup built listview for empty list");
|
||||
CHECK(library_test_sd_missing() == 1, "unreadable root -> SD missing state");
|
||||
CHECK(library_test_rows() == 0, "empty list rows=%d", library_test_rows());
|
||||
CHECK(library_test_has_list(), "startup still builds a listview");
|
||||
CHECK(strcmp(library_test_dir(), bad_root) == 0, "startup dir=%s", library_test_dir());
|
||||
CHECK(strstr(library_test_empty_text(), "SD") != NULL,
|
||||
"friendly empty state: %s", library_test_empty_text());
|
||||
CHECK(library_test_hint_active() == 0, "no error banner for missing SD");
|
||||
CHECK(library_test_topbar_back() == 0, "no back arrow at storage root");
|
||||
CHECK(library_test_retry() == 0 && library_test_sd_missing() == 1,
|
||||
"retry while still missing stays in empty state (no crash)");
|
||||
|
||||
/* 根路径不是目录(普通文件)同样走 SD 空态。 */
|
||||
char notadir[MR_PATH_MAX];
|
||||
joinp(notadir, sizeof notadir, data, "notadir");
|
||||
touch_file(notadir);
|
||||
library_test_set_root(notadir);
|
||||
library_test_set_dir(notadir);
|
||||
CHECK(library_test_retry() == 0 && library_test_sd_missing() == 1,
|
||||
"root pointing at a file -> SD missing state");
|
||||
|
||||
/* 插卡/挂载恢复:同一路径变成可读目录后,刷新即恢复列表。 */
|
||||
library_test_set_root(root);
|
||||
CHECK(strcmp(library_test_root(), root) == 0, "test root installed");
|
||||
library_test_set_dir(root);
|
||||
CHECK(library_test_retry() > 0, "retry after storage available loads rows");
|
||||
CHECK(library_test_sd_missing() == 0, "SD missing state cleared");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "now browsing root");
|
||||
CHECK(library_test_topbar_back() == 0, "still no back arrow at root");
|
||||
|
||||
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++) {
|
||||
/* ---- U1:行模型(目录优先 + 自然序 + 类型图标/箭头) ---- */
|
||||
CHECK(library_test_load(root) == 1, "load fixture root");
|
||||
CHECK(library_test_rows() == 7, "7 rows (got %d)", library_test_rows());
|
||||
static const char *want[7] = {
|
||||
"album", "deep", "empty", "noimg", "Pictures", "book.cbz", "root.png"
|
||||
};
|
||||
for (int i = 0; i < 7; 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);
|
||||
CHECK(it.subtitle && it.subtitle[0] && strcmp(it.subtitle, "…") != 0,
|
||||
"row[%d] subtitle pre-generated (%s)", i, it.subtitle ? it.subtitle : "");
|
||||
if (i < 5) {
|
||||
CHECK(it.icon_kind == MEOW_LIST_ICON_FOLDER && it.right == MEOW_LIST_RIGHT_ARROW,
|
||||
"row[%d] is a folder row with enter arrow", i);
|
||||
} else {
|
||||
CHECK(it.icon_kind == MEOW_LIST_ICON_FILE && it.right == MEOW_LIST_RIGHT_NONE,
|
||||
"row[%d] is a file row without child arrow", 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");
|
||||
int ic = row_index_for("book.cbz");
|
||||
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 : "");
|
||||
CHECK(ic >= 0 && library_test_src(ic, &it) &&
|
||||
strncmp(it.subtitle, "档案册 · ", strlen("档案册 · ")) == 0,
|
||||
"archive subtitle=%s", it.subtitle ? it.subtitle : "");
|
||||
int ir = row_index_for("root.png");
|
||||
CHECK(ir >= 0 && library_test_src(ir, &it) &&
|
||||
strncmp(it.subtitle, "图片 · ", strlen("图片 · ")) == 0,
|
||||
"image subtitle=%s", it.subtitle ? it.subtitle : "");
|
||||
|
||||
/* ---- 行点击:书 → reader;空目录 → 进入 ---- */
|
||||
int list_top = meow_ui_statusbar_height() + 56;
|
||||
/* ---- U2:行合成零 IO(渲染/滚动路径不访问文件系统) ---- */
|
||||
mr_library_test_io_reset();
|
||||
for (int pass = 0; pass < 500; pass++)
|
||||
for (int i = 0; i < library_test_rows(); i++) {
|
||||
meow_list_item tmp;
|
||||
library_test_src(i, &tmp);
|
||||
}
|
||||
mr_library_io_stats io;
|
||||
mr_library_test_io_get(&io);
|
||||
CHECK(io.opendirs == 0 && io.readdirs == 0 && io.lstats == 0,
|
||||
"row source does no IO (open=%ld rd=%ld stat=%ld)",
|
||||
io.opendirs, io.readdirs, io.lstats);
|
||||
|
||||
/* ---- U3:点击语义 ---- */
|
||||
char pics_dir[MR_PATH_MAX];
|
||||
joinp(pics_dir, sizeof pics_dir, root, "Pictures");
|
||||
CHECK(library_test_load(pics_dir) == 1, "entered Pictures");
|
||||
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "current dir=%s", library_test_dir());
|
||||
|
||||
/* 图片下标 = 该目录里排在该图之前的图片行数(阅读器目录册同序)。 */
|
||||
const char *target = "img10.png";
|
||||
int ti = row_index_for(target);
|
||||
CHECK(ti >= 0, "target image row present");
|
||||
int expect_idx = 0;
|
||||
for (int i = 0; i < ti; i++) {
|
||||
meow_list_item tmp;
|
||||
if (!library_test_src(i, &tmp)) continue;
|
||||
if (tmp.subtitle && strncmp(tmp.subtitle, "图片 · ", strlen("图片 · ")) == 0)
|
||||
expect_idx++;
|
||||
}
|
||||
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");
|
||||
tap_row(target);
|
||||
CHECK(g_opened_n == 1 && strcmp(g_opened, pics_dir) == 0,
|
||||
"tap image opens reader for current dir (%s)", g_opened);
|
||||
CHECK(g_opened_start == expect_idx, "start index=%d (want %d)", g_opened_start, expect_idx);
|
||||
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "tap image 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());
|
||||
g_opened_n = 0;
|
||||
tap_row("cover.cbz");
|
||||
char archive[MR_PATH_MAX];
|
||||
joinp(archive, sizeof archive, pics_dir, "cover.cbz");
|
||||
CHECK(g_opened_n == 1 && strcmp(g_opened, archive) == 0,
|
||||
"tap archive opens archive book (%s)", g_opened);
|
||||
CHECK(g_opened_start == MR_READER_START_RESUME, "archive uses resume start (%d)",
|
||||
g_opened_start);
|
||||
|
||||
/* 点目录 = 进入;空目录也进(不打开阅读器)。 */
|
||||
char sub_dir[MR_PATH_MAX];
|
||||
joinp(sub_dir, sizeof sub_dir, pics_dir, "sub");
|
||||
g_opened_n = 0;
|
||||
tap_row("sub");
|
||||
CHECK(g_opened_n == 0, "tap dir does not open reader");
|
||||
CHECK(strcmp(library_test_dir(), sub_dir) == 0, "entered empty sub dir (%s)",
|
||||
library_test_dir());
|
||||
CHECK(library_test_rows() == 0, "empty dir shows empty list");
|
||||
CHECK(strcmp(library_test_empty_text(), "空文件夹") == 0,
|
||||
"empty dir state: %s", library_test_empty_text());
|
||||
|
||||
/* ---- 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");
|
||||
/* ---- U4:根目录锁定 ---- */
|
||||
char album_dir[MR_PATH_MAX];
|
||||
joinp(album_dir, sizeof album_dir, root, "album");
|
||||
CHECK(library_test_load(album_dir) == 1, "load album");
|
||||
CHECK(library_test_topbar_back() == 1, "back arrow shown in subdir");
|
||||
CHECK(library_test_go_up() == 1, "go_up from subdir consumed");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "back to 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");
|
||||
/* 根:系统返回 = 退出(返回 0),并保持列表不变。 */
|
||||
CHECK(library_test_go_up() == 0, "go_up at root returns 0 (app exits)");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "still at root after exit gesture");
|
||||
CHECK(library_test_topbar_back() == 0, "no back arrow at root");
|
||||
|
||||
/* 「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");
|
||||
/* 顶栏返回:子目录点击返回箭头 = 回根;根上该区域不响应。 */
|
||||
CHECK(library_test_load(album_dir) == 1, "load album again");
|
||||
library_on_tap(20, meow_ui_statusbar_height() + 28);
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "topbar back returns to root");
|
||||
int root_rows_before = library_test_rows();
|
||||
library_on_tap(20, meow_ui_statusbar_height() + 28);
|
||||
CHECK(strcmp(library_test_dir(), root) == 0 && library_test_rows() == root_rows_before,
|
||||
"topbar back is inert at 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");
|
||||
/* 嵌套:deep/sub 两级返回。 */
|
||||
tap_row("deep");
|
||||
tap_row("sub");
|
||||
char deep_sub[MR_PATH_MAX], deep_dir[MR_PATH_MAX];
|
||||
joinp(deep_dir, sizeof deep_dir, root, "deep");
|
||||
joinp(deep_sub, sizeof deep_sub, root, "deep/sub");
|
||||
CHECK(strcmp(library_test_dir(), deep_sub) == 0, "entered deep/sub (%s)", library_test_dir());
|
||||
CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), deep_dir) == 0, "up to deep");
|
||||
CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), root) == 0, "up to root");
|
||||
|
||||
/* ---- U3b:系统返回语义 = TF 根退出 App;顶栏「上级」保留越界浏览 ---- */
|
||||
CHECK(library_test_load(root) == 1, "load fixture root");
|
||||
CHECK(library_test_go_up() == 0, "go_up at TF root returns 0 (app exits)");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "still at TF root after exit gesture");
|
||||
|
||||
/* 顶栏显式「上级」:访问放开,TF 根 → 其父目录(fixture root 的 parent = data)。 */
|
||||
CHECK(library_test_go_parent() == 1, "explicit up from TF root consumed");
|
||||
CHECK(strcmp(library_test_dir(), data) == 0,
|
||||
"explicit up goes above TF root (%s)", library_test_dir());
|
||||
CHECK(library_test_load(root) == 1, "back to fixture root");
|
||||
|
||||
/* ---- U3c:左缘 24px 系统返回手势带不启动列表拖拽 ---- */
|
||||
/* ---- U3c:左缘 24px / 屏底热区不启动列表拖拽 ---- */
|
||||
int list_top = meow_ui_statusbar_height() + 56;
|
||||
int row_y = list_top + 28;
|
||||
library_on_touch_down(120, row_y);
|
||||
CHECK(library_test_drag_active() == 1, "touch inside list starts drag");
|
||||
@@ -362,11 +364,10 @@ int main(void)
|
||||
library_on_touch_down(120, meow_ui_height() - 4);
|
||||
CHECK(library_test_drag_active() == 0, "bottom hotzone does not start drag");
|
||||
|
||||
/* ---- U4:不可读目录 → 停留 + 提示 ---- */
|
||||
CHECK(library_test_load(root) == 1, "back to fixture root");
|
||||
char noread[MR_PATH_MAX];
|
||||
joinp(noread, sizeof noread, root, "noread");
|
||||
/* ---- U6:不可读子目录 → 停留 + 提示 ---- */
|
||||
if (geteuid() != 0) {
|
||||
char noread[MR_PATH_MAX];
|
||||
joinp(noread, sizeof noread, root, "noread");
|
||||
CHECK(mkdir(noread, 0755) == 0, "mkdir noread");
|
||||
chmod(noread, 0000);
|
||||
int ok = library_test_load(noread);
|
||||
@@ -381,7 +382,33 @@ int main(void)
|
||||
printf(" SKIP unreadable-dir case (running as root)\n");
|
||||
}
|
||||
|
||||
test_cache_prune_skips_pages();
|
||||
/* ---- U7:书库不碰缓存目录(CBZ 解压页缓存原样保留,不写 bmp) ---- */
|
||||
CHECK(mr_cache_init(data) == 0, "page cache init");
|
||||
const char *cdir = mr_cache_dir();
|
||||
CHECK(cdir && cdir[0], "page cache dir: %s", cdir ? cdir : "");
|
||||
if (cdir && cdir[0]) {
|
||||
char page[MR_PATH_MAX + 96];
|
||||
snprintf(page, sizeof page, "%s/mrpage_0123456789abcdef.bmp", cdir);
|
||||
CHECK(touch_file(page) == 0, "create page cache file");
|
||||
int pages_before = 0;
|
||||
count_bmp(cdir, &pages_before);
|
||||
CHECK(pages_before == 1, "precondition 1 page (got %d)", pages_before);
|
||||
|
||||
CHECK(library_test_load(root) == 1, "reload root");
|
||||
for (int i = 0; i < library_test_rows(); i++) {
|
||||
meow_list_item tmp;
|
||||
library_test_src(i, &tmp);
|
||||
}
|
||||
tap_row("album");
|
||||
tap_row("001.png"); /* 打开阅读器入口(打桩) */
|
||||
library_test_go_up();
|
||||
|
||||
int pages_after = 0;
|
||||
int thumbs_after = count_bmp(cdir, &pages_after);
|
||||
CHECK(access(page, R_OK) == 0, "mrpage_* survives book browsing");
|
||||
CHECK(pages_after == 1 && thumbs_after == 0,
|
||||
"library writes no bmp to cache (pages=%d thumbs=%d)", pages_after, thumbs_after);
|
||||
}
|
||||
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
|
||||
Reference in new issue
Block a user