Files
MeowOS-SDK/tuku/test/test_browse_lazy.c
T
AskaEth 4befbfe14c 图库: 书库点图改按文件名定位(修 P1 截断错位)+ 册间衔接根锁定(M18 评审 r1)
P1-1(会静默开错图):取消「书库从列表推导图片下标」。
- 新增 mr_reader_open_dir_at_name(dir, name):阅读器打开目录册后,在**自己刚枚举出的
  全量目录册**(mr_dir_book,与翻页同一份枚举)里按 basename 定位起始页(先精确,
  再大小写不敏感兜底;找不到退回续读/首页)。书库点击只传文件名。
- 删除 mr_library_image_index(不再有跨枚举的隐式契约)。
- 截断提示文案改为「仅显示前 2000 项(图片可在阅读器中翻到)」。
- 修复前(cb02722 探针,2100 张 4 位名夹具):rows=2000/truncated=1/
  library_images=2000/reader_pages=2100/**mismatched=590**(首行 p1412 → 打开 p1411);
  评审夹具为 1995/2000。修复后:2000 个可见图片行按名定位 **0 mismatch**,
  2100 页全量可翻;26 次真实 UI 点开(首尾各 3 + 20 个确定性伪随机)逐张命中。

连带处理(评审 r1):
1. 删掉 reader_page_enter 的 c->view 死写(reopen 判定用 activity active/depth);
   顺手删掉 library_page_create 里同款死写。
2. 阅读器册间衔接根锁定:新增 mr_path_within_root + mr_book_adjacent_rooted,
   book_switch 用浏览根(默认 MR_ROOT_DIR,测试可注入)钳制相邻册;
   根外的兄弟册按「没有相邻册」处理。test_cont_adjacent A7 验证「明文 _ex 会越根、
   rooted 拒绝、根内翻册不受影响」;test_mode_switch_host H6 验证条漫末页不越根。
3. test_reader_perf 不再依赖顶栏 tap 命中时序:模式切换改走测试钩子
   mr_reader_test_toggle_mode()(与顶栏动作同一个 mode_set),负载下稳定。
4. page_cache 注释去掉「缩略图缓存/缩略图线程」过时描述(只改文字)。

回归:run_tests.sh(library 81 / nav 74 / lazy 57 / perf 25 / … / open_book 含 2100 用例)
与 run_webtoon_tests.sh(含 A7/H6)全 PASS;交叉构建零警告。
2026-11-05 20:26:40 +08:00

295 lines
12 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 **书库行与阅读器目录册同源**:书库图片行的名称序列 == 阅读器目录册
* (logic/reader/page_source.c: mr_dir_book_init)的页名序列(同过滤器/同比较器)。
* 点图现在由阅读器按文件名自己定位下标,书库不再推导下标;本用例守住「两边枚举一致」。
* 夹具含大小写混排(A.jpg/a.jpg)、数字序号(img2/img10)、大写扩展名(IMG3.JPG)、
* 前导零(page01/page1)、夹在图片之间的档案册与普通文件;
* L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化);
* L4 截断安全(评审 P1-1):2100 张图(默认 cap=2000)时,2000 个可见图片行按文件名
* 都能定位到同一文件(0 mismatch),被截掉的图仍在阅读器页表里可翻到。
*
* 编译(在 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_library_rows_match_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);
int images = 0;
for (int i = 0; i < lib.count; i++)
if (lib.items[i].kind == MR_LIB_KIND_IMAGE) images++;
CHECK(images == pages, "library images=%d vs reader pages=%d", images, pages);
/* 逐张对齐:书库第 k 张图 == 阅读器目录册第 k 页(同过滤器/同自然序)。
* 点图时阅读器按文件名重新定位下标,所以「名字集合 + 顺序」一致就是契约。 */
int k = 0;
for (int i = 0; i < lib.count && k < pages; 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;
int found = 0;
for (int j = 0; j < pages; j++)
if (db.names[j] && strcmp(db.names[j], lib.items[i].name) == 0) found = 1;
CHECK(found == 0, "non-image %s is not a reader page", lib.items[i].name);
}
mr_dir_book_free(&db);
mr_library_free(&lib);
}
/* ---- L4:2100 张图(默认 cap)——列表截断但按名定位 2000/2000 不错位 ---- */
static void test_cap_name_lookup(const char *root)
{
char big[MR_PATH_MAX], p[MR_PATH_MAX];
joinp(big, sizeof big, root, "big");
CHECK(mkdir_p(big) == 0, "mkdir big");
for (int i = 1; i <= 2100; i++) {
snprintf(p, sizeof p, "%s/p%04d.jpg", big, i);
if (touch_file(p) != 0) { CHECK(0, "big image %s", p); return; }
}
mr_library lib;
memset(&lib, 0, sizeof lib);
int n = mr_library_scan(&lib, big, MR_LIB_MAX_DEPTH);
CHECK(n == MR_LIB_SCAN_MAX, "big dir lists cap rows (%d)", n);
CHECK(lib.truncated == 1 && lib.count == MR_LIB_SCAN_MAX, "big dir truncated at cap");
mr_dir_book db;
memset(&db, 0, sizeof db);
CHECK(mr_dir_book_init(&db, big, 720, fake_load, fake_unload, NULL) == 2100,
"reader enumerates all 2100 pages");
/* 书库不再推导下标;点击传文件名。这里验证 2000 个可见图片行按名定位
* 都能命中**同一个文件**(0 mismatch),被截掉的 100 张也仍在页表里。 */
int checked = 0, mismatch = 0;
for (int i = 0; i < lib.count; i++) {
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
checked++;
int idx = -1;
for (int j = 0; j < db.count; j++)
if (db.names[j] && strcmp(db.names[j], lib.items[i].name) == 0) { idx = j; break; }
char page[MR_PATH_MAX];
mr_dir_book_path(&db, idx, page, (int)sizeof page);
if (idx < 0 || strcmp(page, lib.items[i].path) != 0) mismatch++;
}
CHECK(checked == MR_LIB_SCAN_MAX, "checked %d library image rows", checked);
CHECK(mismatch == 0, "name lookup mismatch=%d (want 0)", mismatch);
/* 被截掉的图(列表外的那 100 张)在阅读器页表里仍存在(可翻到)。
* 截断发生在 readdir 顺序(非自然序),不假定是哪 100 张,按集合差验证。 */
int missing = 0, missing_idx = -1;
char missing_name[MR_NAME_MAX] = "";
for (int j = 0; j < db.count; j++) {
int in_list = 0;
for (int i = 0; i < lib.count; i++)
if (strcmp(lib.items[i].name, db.names[j]) == 0) { in_list = 1; break; }
if (!in_list) {
if (missing == 0) {
missing_idx = j;
snprintf(missing_name, sizeof missing_name, "%s", db.names[j]);
}
missing++;
}
}
CHECK(missing == 2100 - MR_LIB_SCAN_MAX, "truncated-out images=%d (want 100)", missing);
char want_path[MR_PATH_MAX], page[MR_PATH_MAX];
snprintf(want_path, sizeof want_path, "%s/%s", big, missing_name);
mr_dir_book_path(&db, missing_idx, page, (int)sizeof page);
CHECK(missing_idx >= 0 && strcmp(page, want_path) == 0,
"truncated-out %s is still reader page %d", missing_name, missing_idx);
mr_library_row row;
CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1 &&
strstr(row.subtitle, "2000") && strstr(row.subtitle, "阅读器"),
"truncation row keeps images discoverable: %s",
row.subtitle ? row.subtitle : "");
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_library_rows_match_reader(p);
test_cap_name_lookup(root);
rm_rf(root);
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}