用户反馈四件事一起改: - 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路 (探测/页数/封面/续读角标/懒算 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;交叉构建零警告。
228 lines
8.1 KiB
C
228 lines
8.1 KiB
C
/* 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;
|
||
}
|