图库: 书库点图改按文件名定位(修 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;交叉构建零警告。
This commit is contained in:
AskaEth committed 2026-11-05 20:26:40 +08:00
1 parent 64336f95d9
commit 4befbfe14c
17 files changed
+523 -112

No files matched your search

+113
View File
@@ -14,6 +14,7 @@
*/
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "logic/reader/page_source.h"
#include <dirent.h>
#include <stdio.h>
@@ -28,12 +29,14 @@ void library_test_set_root(const char *root);
const char *library_test_dir(void);
int library_test_rows(void);
int library_test_src(int idx, meow_list_item *item);
int library_test_open_row(int idx);
/* ui/reader/reader.c 的测试接缝(-DMR_READER_HOST_TEST) */
int mr_reader_ui_mode(void);
int mr_reader_ui_page(void);
int mr_reader_ui_count(void);
int mr_reader_ui_open_err(void);
void mr_reader_open_dir_at_name(const char *dir, const char *name);
static int g_fail;
#define CHECK(cond, ...) do { \
@@ -152,6 +155,22 @@ static int expected_image_index(const char *name)
return n;
}
/* 阅读器目录册用的假解码回调(本测试不经过它们取页)。 */
static meow_img *cap_fake_load(const char *path, void *ud, int maxdim)
{
(void)path; (void)ud; (void)maxdim;
return NULL;
}
static void cap_fake_unload(meow_img *img) { (void)img; }
/* 在阅读器的全量目录册里按文件名取页下标(与阅读器打开时同一份枚举)。 */
static int reader_page_of_name(const mr_dir_book *db, const char *name)
{
for (int i = 0; i < db->count; i++)
if (db->names[i] && strcmp(db->names[i], name) == 0) return i;
return -1;
}
int main(void)
{
const char *base = getenv("TMPDIR");
@@ -184,6 +203,16 @@ int main(void)
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; }
}
/* Big:2100 张图(超过默认 MR_LIB_SCAN_MAX=2000,复现评审 P1-1 截断错位场景)。
* 1x1 BMP(stb 可解)以控制夹具体积/建盘时间。 */
char big[MR_PATH_MAX];
snprintf(big, sizeof big, "%s/Big", root);
if (mkdir_p(big) != 0) { printf("SKIP: mkdir Big failed\n"); return 0; }
for (int i = 1; i <= 2100; i++) {
snprintf(p, sizeof p, "%s/p%04d.bmp", big, i);
if (write_bmp24(p, 1, 1) != 0) { printf("SKIP: big image write failed\n"); return 0; }
}
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
@@ -238,6 +267,90 @@ int main(void)
mr_reader_ui_page(), want2);
meow_view_page_pop();
/* ---- P1-1 回归:单目录 2100 张图(超过默认 cap=2000)不得「点 A 开 B」 ---- */
CHECK(library_test_load(root) == 1, "load root for cap regression");
tap_row("Big");
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "tap Big dir stays in library");
CHECK(strcmp(library_test_dir(), big) == 0, "entered Big (%s)", library_test_dir());
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1,
"Big rows=%d (cap + truncation row)", library_test_rows());
/* 截断提示行仍在,且文案不让人误以为「没有更多图」。 */
meow_list_item trunc_it;
CHECK(library_test_src(library_test_rows() - 1, &trunc_it) == 1 &&
trunc_it.enabled == 0 && trunc_it.title && strcmp(trunc_it.title, "已截断") == 0,
"truncation row present");
CHECK(trunc_it.subtitle && strstr(trunc_it.subtitle, "2000") &&
strstr(trunc_it.subtitle, "阅读器"),
"truncation text keeps images discoverable: %s",
trunc_it.subtitle ? trunc_it.subtitle : "");
/* 阅读器目录册是**全量** 2100 页(书库 cap 只是列表显示上限)。 */
mr_dir_book db;
memset(&db, 0, sizeof db);
CHECK(mr_dir_book_init(&db, big, 720, cap_fake_load, cap_fake_unload, NULL) == 2100,
"reader sees all 2100 pages");
/* 首 3 + 尾 3 + 20 个确定性伪随机行,逐一点开断言「点哪张开哪张」。 */
int picks[26], np = 0;
picks[np++] = 0;
picks[np++] = 1;
picks[np++] = 2;
picks[np++] = MR_LIB_SCAN_MAX - 3;
picks[np++] = MR_LIB_SCAN_MAX - 2;
picks[np++] = MR_LIB_SCAN_MAX - 1;
unsigned seed = 12345u;
for (int i = 0; i < 20; i++) {
seed = seed * 1103515245u + 12345u;
picks[np++] = (int)((seed >> 16) % MR_LIB_SCAN_MAX);
}
int mismatched = 0;
for (int i = 0; i < np; i++) {
int row = picks[i];
meow_list_item it;
memset(&it, 0, sizeof it);
CHECK(library_test_src(row, &it) == 1 && it.title, "pick %d: row source", row);
if (!it.title) continue;
int want_page = reader_page_of_name(&db, it.title);
CHECK(want_page >= 0, "pick %d: %s is a reader page", row, it.title);
CHECK(library_test_open_row(row) == 1, "open row %d", row);
CHECK(meow_ui_activity_active() == ACT_READER, "pick %d opens reader", row);
CHECK(strcmp(mr_reader_dir(), big) == 0, "pick %d reader dir", row);
CHECK(mr_reader_ui_count() == 2100, "pick %d reader pages=%d", row,
mr_reader_ui_count());
if (mr_reader_ui_page() != want_page) mismatched++;
CHECK(mr_reader_ui_page() == want_page,
"pick %d: clicked %s -> page %d (want %d)", row, it.title,
mr_reader_ui_page(), want_page);
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "pick %d back to library", row);
}
CHECK(mismatched == 0, "no click/page mismatch across %d picks", np);
/* 被书库截掉的图(第 2001 张起)在阅读器里仍可打开/翻到。 */
CHECK(reader_page_of_name(&db, "p2099.bmp") == 2098, "p2099 lives at page 2098");
mr_reader_open_dir_at_name(big, "p2099.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "truncated-out image opens reader");
CHECK(mr_reader_ui_count() == 2100, "truncated-out image: all pages present");
CHECK(mr_reader_ui_page() == 2098, "truncated-out image opens at its page (%d)",
mr_reader_ui_page());
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library after truncated image");
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1, "library list unchanged after reads");
mr_dir_book_free(&db);
/* 名字找不到(文件已改名/删除):退回续读/首页,不崩、不串册。 */
mr_reader_open_dir_at_name(pics, "does-not-exist.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "unknown name still opens reader");
CHECK(strcmp(mr_reader_dir(), pics) == 0, "unknown name keeps target dir");
CHECK(mr_reader_ui_count() == 6 && mr_reader_ui_open_err() == 0,
"unknown name falls back safely (pages=%d err=%d)",
mr_reader_ui_count(), mr_reader_ui_open_err());
CHECK(mr_reader_ui_page() >= 0 && mr_reader_ui_page() < 6,
"unknown name fallback page in range (%d)", mr_reader_ui_page());
meow_view_page_pop();
/* 档案册路径(无 zip 夹具时只验证入口不被当成目录):点 txt 不存在行;跳过。 */
meow_view_free(reader);