图库: 书库点图改按文件名定位(修 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:
1 parent
64336f95d9
commit
4befbfe14c
17 files changed
+523
-112
No files matched your search
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
# run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。
|
||||
# - test_library:单层目录扫描(目录+图片+档案册)/自然排序/图片下标/父目录/阅读方向
|
||||
# - test_library:单层目录扫描(目录+图片+档案册)/自然排序/父目录/根前缀判据/阅读方向
|
||||
# - test_browse_nav:访问放开(系统/空/无图目录可进入)、图片与档案册成行、截断、不可读
|
||||
# - test_browse_lazy:行合成零 IO/副标题预生成/图片下标 == 阅读器目录册下标(集成陷阱)
|
||||
# - test_browse_lazy:行合成零 IO/副标题预生成/书库行与阅读器目录册同序(按名定位契约)
|
||||
# - test_browse_perf:500 目录×8 层嵌套 + 2000 项大目录,断言单层扫描与行合成成本
|
||||
# - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/
|
||||
# 坏值/非法值/只读一次/写失败只告警)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/* test_browse_lazy.c — 浏览行模型 / 零 IO 行合成 / 图片下标一致性(纯 C,无 SDK)。
|
||||
/* 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)里同一张图的下标。
|
||||
* L2 **书库行与阅读器目录册同源**:书库图片行的名称序列 == 阅读器目录册
|
||||
* (logic/reader/page_source.c: mr_dir_book_init)的页名序列(同过滤器/同比较器)。
|
||||
* 点图现在由阅读器按文件名自己定位下标,书库不再推导下标;本用例守住「两边枚举一致」。
|
||||
* 夹具含大小写混排(A.jpg/a.jpg)、数字序号(img2/img10)、大写扩展名(IMG3.JPG)、
|
||||
* 前导零(page01/page1)、夹在图片之间的档案册与普通文件;
|
||||
* L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化)。
|
||||
* L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化);
|
||||
* L4 截断安全(评审 P1-1):2100 张图(默认 cap=2000)时,2000 个可见图片行按文件名
|
||||
* 都能定位到同一文件(0 mismatch),被截掉的图仍在阅读器页表里可翻到。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
@@ -137,8 +140,8 @@ static void test_rows_zero_io(const char *root)
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ---- L2:书库图片下标 == 阅读器目录册下标 ---- */
|
||||
static void test_image_index_matches_reader(const char *dir)
|
||||
/* ---- L2:书库图片行名称序列 == 阅读器目录册页名序列 ---- */
|
||||
static void test_library_rows_match_reader(const char *dir)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
@@ -149,25 +152,15 @@ static void test_image_index_matches_reader(const char *dir)
|
||||
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;
|
||||
}
|
||||
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 页(下标即起始页)。 */
|
||||
/* 逐张对齐:书库第 k 张图 == 阅读器目录册第 k 页(同过滤器/同自然序)。
|
||||
* 点图时阅读器按文件名重新定位下标,所以「名字集合 + 顺序」一致就是契约。 */
|
||||
int k = 0;
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
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);
|
||||
@@ -176,17 +169,90 @@ static void test_image_index_matches_reader(const char *dir)
|
||||
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);
|
||||
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";
|
||||
@@ -219,7 +285,8 @@ int main(void)
|
||||
|
||||
test_rows_zero_io(root);
|
||||
joinp(p, sizeof p, root, "mixed");
|
||||
test_image_index_matches_reader(p);
|
||||
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);
|
||||
|
||||
@@ -165,8 +165,6 @@ int main(void)
|
||||
CHECK(strcmp(biglib.items[PERF_BIG - 1].name, "img1999.png") == 0,
|
||||
"last=%s", biglib.items[PERF_BIG - 1].name);
|
||||
CHECK(biglib.items[PERF_BIG - 1].kind == MR_LIB_KIND_IMAGE, "last is image");
|
||||
CHECK(mr_library_image_index(&biglib, PERF_BIG - 1) == PERF_BIG - 1,
|
||||
"image index at end");
|
||||
}
|
||||
printf(" big dir scan: %lldus, %d rows, opendir=%ld lstat=%ld\n",
|
||||
big_us, bn, st.opendirs, st.lstats);
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* 类型·大小(不再有占位/后台补位),图标/右侧箭头按类型区分;
|
||||
* U2 行合成零 IO:反复取可见行不触发 opendir/readdir/lstat(渲染期无文件访问);
|
||||
* U3 点击语义:点目录=进入(空目录也进);点图片=从该图开始读
|
||||
* (mr_reader_open_dir_at(当前目录, 图片下标));点档案册=打开档案册;
|
||||
* (mr_reader_open_dir_at_name(当前目录, 文件名);阅读器自己定位下标);
|
||||
* 点档案册=打开档案册;
|
||||
* U4 根目录锁定:根不显示顶栏返回、library_go_up 在根返回 0(app.c 退出);
|
||||
* 子目录返回=回根;不存在越过浏览根的导航;
|
||||
* U5 无 SD 卡容错:浏览根不可访问(不存在/不是目录)→ 友好空态「未检测到 SD 卡」,
|
||||
@@ -47,18 +48,28 @@ int library_test_go_up(void);
|
||||
int library_test_drag_active(void);
|
||||
int library_test_has_list(void);
|
||||
|
||||
/* 阅读器入口打桩:只记录被打开的册路径与起始页(本测试不建 reader 页)。 */
|
||||
/* 阅读器入口打桩:记录被打开的册路径 / 起始页 / 按名定位的文件名(本测试不建 reader 页)。 */
|
||||
#define MR_READER_START_RESUME (-1)
|
||||
void mr_reader_open_dir_at(const char *dir, int start_index);
|
||||
void mr_reader_open_dir_at_name(const char *dir, const char *name);
|
||||
static char g_opened[MR_PATH_MAX];
|
||||
static char g_opened_name[MR_NAME_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_name[0] = '\0';
|
||||
g_opened_start = start_index;
|
||||
g_opened_n++;
|
||||
}
|
||||
void mr_reader_open_dir_at_name(const char *dir, const char *name)
|
||||
{
|
||||
snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : "");
|
||||
snprintf(g_opened_name, sizeof g_opened_name, "%s", name ? name : "");
|
||||
g_opened_start = MR_READER_START_RESUME;
|
||||
g_opened_n++;
|
||||
}
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
@@ -279,22 +290,17 @@ int main(void)
|
||||
CHECK(library_test_load(pics_dir) == 1, "entered Pictures");
|
||||
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "current dir=%s", library_test_dir());
|
||||
|
||||
/* 图片下标 = 该目录里排在该图之前的图片行数(阅读器目录册同序)。 */
|
||||
/* 点图片:把**文件名**交给阅读器(阅读器用自己的全量目录册定位起始页;
|
||||
* 书库列表可能被 MR_LIB_SCAN_MAX 截断,所以不再传下标)。 */
|
||||
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;
|
||||
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(g_opened_name, target) == 0, "reader gets file name (%s)", g_opened_name);
|
||||
CHECK(g_opened_start == MR_READER_START_RESUME, "name-based open does not pass an index");
|
||||
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "tap image does not navigate");
|
||||
|
||||
g_opened_n = 0;
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
* A4 排除:隐藏/系统目录、符号链接、普通文件不参与兄弟册;
|
||||
* A5 参数:delta 非 ±1、NULL/小缓冲、不存在的目录;
|
||||
* A6 书库行模型(M18):目录+图片+档案册同列、目录优先;副标题扫描期生成,
|
||||
* 不再有「· 续读 Pn」角标(角标随「目录自动当书」链路删除,续读仍由 reader 恢复)。
|
||||
* 不再有「· 续读 Pn」角标(角标随「目录自动当书」链路删除,续读仍由 reader 恢复);
|
||||
* A7 册间衔接根锁定(M18 评审 P2-1):相邻册越过浏览根时按「没有相邻册」处理,
|
||||
* 根内正常翻册不受影响(mr_book_adjacent_rooted)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. test/test_cont_adjacent.c logic/library.c logic/reader.c \
|
||||
@@ -160,7 +162,7 @@ static void test_adjacent(const char *root)
|
||||
}
|
||||
|
||||
/* A6 文件管理器行模型:扫描即出最终行(无占位/无后台补位/无续读角标);
|
||||
* 目录优先;当前层的图片行带图片下标(点图 → reader 起始页)。 */
|
||||
* 目录优先;图片/档案册按类型成行。 */
|
||||
static void test_library_rows(const char *root)
|
||||
{
|
||||
mr_test_cfg_clear();
|
||||
@@ -191,7 +193,6 @@ static void test_library_rows(const char *root)
|
||||
if (strcmp(e->name, "loose.png") == 0) {
|
||||
seen_img = 1;
|
||||
CHECK(e->kind == MR_LIB_KIND_IMAGE, "loose.png is an image row");
|
||||
CHECK(mr_library_image_index(&lib, i) == 0, "loose.png is image #0");
|
||||
}
|
||||
}
|
||||
CHECK(seen1 == 1, "vol1 listed");
|
||||
@@ -199,6 +200,80 @@ static void test_library_rows(const char *root)
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* A7 册间衔接的根锁定:相邻册必须在浏览根内,否则按「没有相邻册」处理。
|
||||
* 夹具:base/{aaa_outside, root}/{...};root 自己也是「册」(直接含图),
|
||||
* 明文 _ex 会给出 root 的兄弟 aaa_outside,rooted 必须拒绝(不越 SD 卡根)。 */
|
||||
static void test_rooted_adjacent(void)
|
||||
{
|
||||
char tmpl[] = "./mr_rooted_XXXXXX";
|
||||
char *base = mkdtemp(tmpl);
|
||||
CHECK(base != NULL, "mkdtemp rooted fixture");
|
||||
if (!base) return;
|
||||
|
||||
char root[MR_PATH_MAX], outside[MR_PATH_MAX], in1[MR_PATH_MAX], in2[MR_PATH_MAX];
|
||||
joinp(root, sizeof root, base, "root");
|
||||
joinp(outside, sizeof outside, base, "aaa_outside");
|
||||
joinp(in1, sizeof in1, root, "aaa_inside1");
|
||||
joinp(in2, sizeof in2, root, "aaa_inside2");
|
||||
CHECK(mkdir_p(outside) == 0 && mkdir_p(in1) == 0 && mkdir_p(in2) == 0,
|
||||
"mkdir rooted fixture");
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
joinp(p, sizeof p, root, "000.png"); CHECK(touch_file(p) == 0, "root image");
|
||||
joinp(p, sizeof p, outside, "001.png"); CHECK(touch_file(p) == 0, "outside image");
|
||||
joinp(p, sizeof p, in1, "001.png"); CHECK(touch_file(p) == 0, "inside1 image");
|
||||
joinp(p, sizeof p, in2, "001.png"); CHECK(touch_file(p) == 0, "inside2 image");
|
||||
|
||||
char out[MR_PATH_MAX];
|
||||
|
||||
/* 明文衔接:root 的兄弟 aaa_outside 在浏览根之外,_ex 会返回它。 */
|
||||
memset(out, 0, sizeof out);
|
||||
CHECK(mr_book_adjacent_ex(root, -1, out, (int)sizeof out, NULL, NULL) == 1 &&
|
||||
strcmp(out, outside) == 0,
|
||||
"plain adjacency reaches outside root (%s)", out);
|
||||
|
||||
/* 根锁定版:同样的调用必须拒绝越根。 */
|
||||
memset(out, 0x7f, sizeof out);
|
||||
out[0] = '\0';
|
||||
CHECK(mr_book_adjacent_rooted(root, -1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0 && out[0] == '\0',
|
||||
"rooted adjacency refuses book outside root (%s)", out);
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0 && out[0] == '\0',
|
||||
"rooted adjacency: no next inside root");
|
||||
|
||||
/* 根内正常翻册仍然可用(不能被误锁死)。 */
|
||||
memset(out, 0, sizeof out);
|
||||
CHECK(mr_book_adjacent_rooted(in1, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 1 &&
|
||||
strcmp(out, in2) == 0, "rooted next inside root (%s)", out);
|
||||
CHECK(mr_book_adjacent_rooted(in2, -1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 1 &&
|
||||
strcmp(out, in1) == 0, "rooted prev inside root (%s)", out);
|
||||
CHECK(mr_book_adjacent_rooted(in1, -1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0,
|
||||
"rooted: no book before inside1 (outside is out of root)");
|
||||
CHECK(mr_book_adjacent_rooted(in2, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0,
|
||||
"rooted: no book after inside2");
|
||||
|
||||
/* 当前册本身在根外 → 视为没有相邻册。 */
|
||||
CHECK(mr_book_adjacent_rooted(outside, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0, "current dir outside root");
|
||||
|
||||
/* 参数边界。 */
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, NULL, NULL, NULL,
|
||||
out, (int)sizeof out) == -1, "rooted NULL root");
|
||||
CHECK(mr_book_adjacent_rooted(NULL, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == -1, "rooted NULL dir");
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, root, NULL, NULL, NULL, 32) == -1,
|
||||
"rooted NULL out");
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, "", NULL, NULL,
|
||||
out, (int)sizeof out) == -1, "rooted empty root");
|
||||
|
||||
rm_rf(base);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_cont_adj_XXXXXX";
|
||||
@@ -240,6 +315,7 @@ int main(void)
|
||||
test_has_images(root);
|
||||
test_adjacent(root);
|
||||
test_library_rows(root);
|
||||
test_rooted_adjacent();
|
||||
|
||||
rm_rf(root);
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
|
||||
+22
-19
@@ -5,7 +5,7 @@
|
||||
* T2 扩展名过滤:图片 png/jpg/jpeg/bmp 与档案册 cbz/zip 大小写不敏感;gif/txt 不支持;
|
||||
* T3 单层扫描:目录 + 受支持图片 + 档案册混排(目录优先、组内自然序);
|
||||
* 其它普通文件/隐藏项/符号链接不列;副标题扫描期生成一次(无占位/无懒判定);
|
||||
* 图片行下标(mr_library_image_index)与行类型;不可读路径 -> -1;max_depth 忽略;
|
||||
* 行类型;不可读路径 -> -1;max_depth 忽略;浏览根前缀判据 mr_path_within_root;
|
||||
* T4 阅读方向状态(logic/reader.c)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
@@ -80,13 +80,6 @@ static const mr_library_entry *find_entry(const mr_library *lib, const char *nam
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int find_index(const mr_library *lib, const char *name)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void test_natural(void)
|
||||
{
|
||||
CHECK(mr_natural_cmp("page2", "page10") < 0, "page2 < page10");
|
||||
@@ -214,17 +207,27 @@ static void test_scan(void)
|
||||
CHECK(find_entry(&lib, "a.png") == NULL, "no recursion into subdirs");
|
||||
CHECK(find_entry(&lib, "Deeper") == NULL, "no recursion into subdirs (deeper)");
|
||||
|
||||
/* 图片下标 = 本层图片自然序(书库点图 → 阅读器起始页的契约)。 */
|
||||
int i_cover = find_index(&lib, "cover.JPG");
|
||||
int i_root = find_index(&lib, "root.png");
|
||||
CHECK(mr_library_image_index(&lib, i_cover) == 0, "cover.JPG is image #0");
|
||||
CHECK(mr_library_image_index(&lib, i_root) == 1, "root.png is image #1");
|
||||
CHECK(mr_library_image_index(&lib, find_index(&lib, "book.cbz")) == -1,
|
||||
"archive has no image index");
|
||||
CHECK(mr_library_image_index(&lib, find_index(&lib, "Empty")) == -1,
|
||||
"dir has no image index");
|
||||
CHECK(mr_library_image_index(&lib, -1) == -1 && mr_library_image_index(&lib, 99) == -1,
|
||||
"out-of-range image index");
|
||||
/* 浏览根前缀判据(书库导航/阅读器册间衔接的根锁定;纯路径工具)。 */
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0/a/b", "/usr/data/mnt/sd_0") == 1,
|
||||
"nested path inside root");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0", "/usr/data/mnt/sd_0") == 1,
|
||||
"root itself is inside");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0/", "/usr/data/mnt/sd_0") == 1,
|
||||
"root with trailing slash is inside");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0evil", "/usr/data/mnt/sd_0") == 0,
|
||||
"prefix-lookalike rejected");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_00", "/usr/data/mnt/sd_0") == 0,
|
||||
"prefix-lookalike with digits rejected");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt", "/usr/data/mnt/sd_0") == 0,
|
||||
"parent of root rejected");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0x/a", "/usr/data/mnt/sd_0") == 0,
|
||||
"sibling-prefixed path rejected");
|
||||
CHECK(mr_path_within_root("/a/b", "/a/") == 1, "root trailing slash tolerated");
|
||||
CHECK(mr_path_within_root("/a", "/") == 1, "filesystem root accepts abs path");
|
||||
CHECK(mr_path_within_root("/", "/") == 1, "filesystem root itself");
|
||||
CHECK(mr_path_within_root("rel/path", "/") == 0, "relative path not inside /");
|
||||
CHECK(mr_path_within_root(NULL, "/") == 0 && mr_path_within_root("/a", NULL) == 0 &&
|
||||
mr_path_within_root("/a", "") == 0, "NULL/empty safe");
|
||||
|
||||
/* 行合成:纯内存,返回扫描期生成的字符串。 */
|
||||
mr_library_row row;
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* H3 跳页滑块:展开 → 拖到末页 → 目标页解码完成后落位(不是先白屏再跳);
|
||||
* H4 离开/重进:恢复上次页 + 模式(续读);
|
||||
* H5 册间衔接:条漫末页上拉 → 下一册首页;首页下拉 → 上一册末页;首册再无上一册时
|
||||
* 停在原地(不退出、不崩)。
|
||||
* 停在原地(不退出、不崩);
|
||||
* H6 册间衔接根锁定:浏览根内没有相邻册时,不跨到根外的兄弟册(M18 评审观察项)。
|
||||
*
|
||||
* 编译:test/run_webtoon_tests.sh(需 SDK 源码树;只发布 meow_ui.a 时自动 SKIP)。
|
||||
*/
|
||||
@@ -164,6 +165,10 @@ int main(void)
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
|
||||
/* 阅读器册间衔接的浏览根:测试夹具根(生产默认 MR_ROOT_DIR)。 */
|
||||
mr_reader_test_set_root(root);
|
||||
CHECK(strcmp(mr_reader_test_root(), root) == 0, "reader browse root installed");
|
||||
|
||||
/* TopAppBar 动作格(位图):右起每格 48px;动作 0=模式 / 1=跳页 / 2=fit。 */
|
||||
int w = meow_ui_width();
|
||||
int mode_x = w - 10 - 48 * 3 + 24;
|
||||
@@ -258,6 +263,31 @@ int main(void)
|
||||
CHECK(strcmp(mr_reader_dir(), book1) == 0, "no previous book: stay put");
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "no previous book: still in reader (no exit)");
|
||||
|
||||
/* ---- H6 册间衔接根锁定:浏览根=book2 时,兄弟册 book1 在根外,不得翻过去 ---- */
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library for root-lock case");
|
||||
mr_reader_test_set_root(book2);
|
||||
CHECK(strcmp(mr_reader_test_root(), book2) == 0, "reader root narrowed to book2");
|
||||
mr_reader_open_dir_at(book2, 0); /* 显式首页,避开续读页差异 */
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "reopened book2");
|
||||
CHECK(strcmp(mr_reader_dir(), book2) == 0, "at book2");
|
||||
if (mr_reader_ui_mode() != MR_MODE_WEBTOON) mr_reader_test_toggle_mode();
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon for root-lock case");
|
||||
CHECK(mr_reader_ui_page() == 0, "root-lock case at first page (got %d)",
|
||||
mr_reader_ui_page());
|
||||
|
||||
/* 首页下拉 → book1 是 book2 的上一册,但在浏览根外:必须原地不动。 */
|
||||
reader_on_touch_down(240, 300);
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
reader_on_touch_move(240, 300 + i * 40);
|
||||
usleep(5 * 1000);
|
||||
}
|
||||
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "root-lock down-swipe consumed");
|
||||
CHECK(strcmp(mr_reader_dir(), book2) == 0,
|
||||
"adjacent book outside browse root is refused (dir=%s)", mr_reader_dir());
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "root-lock: still in reader");
|
||||
mr_reader_test_set_root(root); /* 复原夹具根 */
|
||||
|
||||
/* ---- 收尾 ---- */
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "popped back to library");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -146,6 +146,14 @@ static void double_tap(int x, int y)
|
||||
pump_ms(120);
|
||||
}
|
||||
|
||||
/* 直接切模式(绕过顶栏 tap 的命中时序):本文件只关心绘制/解码,不验证 chrome 命中;
|
||||
* 顶栏动作本身的回归在 test_mode_switch_host 覆盖。M18 评审:负载下该用例曾偶发。 */
|
||||
static void mode_toggle(void)
|
||||
{
|
||||
mr_reader_test_toggle_mode();
|
||||
pump_ms(100);
|
||||
}
|
||||
|
||||
static void draw_once(void) { meow_ui_paint_full(); }
|
||||
|
||||
int main(void)
|
||||
@@ -239,7 +247,7 @@ int main(void)
|
||||
"paged drag: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws());
|
||||
|
||||
/* ---- P4 条漫模式:切换后滚动,长宽比 1.6 页也必须零重采样 ---- */
|
||||
tap(mode_x, bar_y);
|
||||
mode_toggle();
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon");
|
||||
pump_ms(800);
|
||||
draw_once();
|
||||
@@ -260,14 +268,14 @@ int main(void)
|
||||
mr_reader_ui_scaled_draws());
|
||||
|
||||
/* ---- P5 「先适高、再切条漫」:strip 恒按屏宽排版,解码必须跟随 ---- */
|
||||
tap(mode_x, bar_y); /* 回翻页 */
|
||||
mode_toggle(); /* 回翻页 */
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "back to paged");
|
||||
pump_ms(500);
|
||||
tap(fit_x, bar_y); /* 适宽 → 适高 */
|
||||
CHECK(mr_reader_ui_fit() == MR_FIT_HEIGHT, "fit-height selected (got %d)",
|
||||
mr_reader_ui_fit());
|
||||
pump_ms(700);
|
||||
tap(mode_x, bar_y); /* 适高档 → 条漫 */
|
||||
mode_toggle(); /* 适高档 → 条漫 */
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon after fit-height");
|
||||
pump_ms(900);
|
||||
draw_once();
|
||||
@@ -290,7 +298,7 @@ int main(void)
|
||||
mr_reader_ui_scaled_draws());
|
||||
|
||||
/* ---- P6 双击放大再缩回(评审 P2-1:缩回后必须按新几何重解)---- */
|
||||
tap(mode_x, bar_y); /* 回翻页 */
|
||||
mode_toggle(); /* 回翻页 */
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "paged for zoom test");
|
||||
pump_ms(600);
|
||||
draw_once();
|
||||
@@ -338,7 +346,7 @@ int main(void)
|
||||
CHECK(mr_reader_ui_blit_draws() > bt && mr_reader_ui_scaled_draws() == st,
|
||||
"800x1280 paged drag: zero resample (blit +%d, scaled +%d)",
|
||||
mr_reader_ui_blit_draws() - bt, mr_reader_ui_scaled_draws() - st);
|
||||
tap(mode_x, bar_y); /* 条漫 */
|
||||
mode_toggle(); /* 条漫 */
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "800x1280 webtoon");
|
||||
pump_ms(800);
|
||||
draw_once();
|
||||
|
||||
Reference in new issue
Block a user