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

228 lines
8.1 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_lazy.c — 浏览行模型 / 零 IO 行合成 / 图片下标一致性(纯 C,无 SDK)。
*
* 文件名沿用历史(旧的「懒判定」已随 M18 文件管理器模型删除);本测试现在覆盖:
* L1 行合成 O(1):mr_library_row_at 纯内存(IO 计数不变)、截断提示行;
* L2 **书库→阅读器下标记号契约**:mr_library_image_index 数出的下标 == 阅读器目录册
* (logic/reader/page_source.c: mr_dir_book_init)里同一张图的下标。
* 夹具含大小写混排(A.jpg/a.jpg)、数字序号(img2/img10)、大写扩展名(IMG3.JPG)、
* 前导零(page01/page1)、夹在图片之间的档案册与普通文件;
* L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化)。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
* test/test_browse_lazy.c logic/library.c logic/reader/page_source.c \
* -pthread -o build/host/test_browse_lazy
*/
#include "logic/library.h"
#include "logic/reader/page_source.h"
#include <dirent.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);
}
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;
}
/* 阅读器目录册用的假解码回调(本测试不打开任何页)。 */
static meow_img *fake_load(const char *path, void *ud, int maxdim)
{
(void)path; (void)ud; (void)maxdim;
return NULL;
}
static void fake_unload(meow_img *img) { (void)img; }
/* ---- L1 + L3:行合成零 IO / 副标题预生成 ---- */
static void test_rows_zero_io(const char *root)
{
mr_library lib;
memset(&lib, 0, sizeof lib);
CHECK(mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH) > 0, "scan fixture");
for (int i = 0; i < lib.count; i++) {
mr_library_row row;
CHECK(mr_library_row_at(&lib, i, &row) == 1, "row %d", i);
CHECK(row.title && row.title[0], "row %d title", i);
CHECK(row.subtitle && row.subtitle[0] && strcmp(row.subtitle, "…") != 0,
"row %d subtitle pre-generated (%s)", i, row.subtitle ? row.subtitle : "");
}
mr_library_io_stats st;
mr_library_test_io_reset();
long long t0 = now_us();
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++;
}
long long us = now_us() - t0;
mr_library_test_io_get(&st);
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);
}
/* ---- L2:书库图片下标 == 阅读器目录册下标 ---- */
static void test_image_index_matches_reader(const char *dir)
{
mr_library lib;
memset(&lib, 0, sizeof lib);
CHECK(mr_library_scan(&lib, dir, MR_LIB_MAX_DEPTH) > 0, "scan mixed dir");
mr_dir_book db;
memset(&db, 0, sizeof db);
int pages = mr_dir_book_init(&db, dir, 720, fake_load, fake_unload, NULL);
CHECK(pages > 0, "reader dir book pages=%d", pages);
/* 书库里的图片行按出现顺序,其下标必须依次是 0..n-1(无空洞/无重排)。 */
int expect = 0;
for (int i = 0; i < lib.count; i++) {
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
int idx = mr_library_image_index(&lib, i);
CHECK(idx == expect, "image row %s index=%d (want %d)",
lib.items[i].name, idx, expect);
expect++;
}
CHECK(expect == pages, "library images=%d vs reader pages=%d", expect, pages);
if (expect != pages) {
mr_library_free(&lib);
mr_dir_book_free(&db);
return;
}
/* 逐张对齐:书库第 k 张图 == 阅读器目录册第 k 页(下标即起始页)。 */
int k = 0;
for (int i = 0; i < lib.count; i++) {
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
char page_path[MR_PATH_MAX];
mr_dir_book_path(&db, k, page_path, (int)sizeof page_path);
CHECK(strcmp(page_path, lib.items[i].path) == 0,
"page[%d]: library=%s reader=%s", k, lib.items[i].path, page_path);
k++;
}
/* 档案册/目录不占图片下标。 */
for (int i = 0; i < lib.count; i++) {
if (lib.items[i].kind == MR_LIB_KIND_IMAGE) continue;
CHECK(mr_library_image_index(&lib, i) == -1,
"non-image %s has no image index", lib.items[i].name);
}
mr_dir_book_free(&db);
mr_library_free(&lib);
}
int main(void)
{
char tmpl[] = "./mr_rows_XXXXXX";
char *root = mkdtemp(tmpl);
CHECK(root != NULL, "mkdtemp");
if (!root) return 1;
char p[MR_PATH_MAX];
const char *dirs[] = { "book", "mixed", "mixed/sub", "empty" };
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, "mixed/sub/zz.png");CHECK(touch_file(p) == 0, "nested");
/* 大小写混排 + 数字序号 + 大写扩展名 + 前导零 + 夹档案/普通文件。 */
const char *mixed[] = {
"A.jpg", "a.jpg", "img2.png", "IMG3.JPG", "img10.png",
"page01.bmp", "page1.jpeg", "cover.cbz", "notes.txt", ".hidden.png"
};
for (unsigned i = 0; i < sizeof mixed / sizeof mixed[0]; i++) {
joinp(p, sizeof p, root, "mixed");
size_t dl = strlen(p);
snprintf(p + dl, sizeof p - dl, "/%s", mixed[i]);
CHECK(touch_file(p) == 0, "image %s", p);
}
test_rows_zero_io(root);
joinp(p, sizeof p, root, "mixed");
test_image_index_matches_reader(p);
rm_rf(root);
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}