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;交叉构建零警告。
364 lines
15 KiB
C
364 lines
15 KiB
C
/* test_open_book_host.c — 书库点击 → 阅读器打开的宿主集成回归(需 SDK 源码树)。
|
||
*
|
||
* M18 文件管理器模型:点目录=进入;点图片=从该图开始读(mr_reader_open_dir_at(目录, 下标));
|
||
* 点档案册=打开档案册。本测试用合成夹具(真实 BMP,不读用户相册)走完整链路:
|
||
* library 列表 → 点图片行 → mr_reader_open_dir_at → reader on_enter → mr_book_open_auto
|
||
* 并断言「点哪张就开哪张」:阅读器当前页 == 该书库行在目录图片自然序里的下标。
|
||
*
|
||
* 另覆盖:
|
||
* - 目录行只进目录,不打开阅读器;
|
||
* - 阅读页已在本页时再次 mr_reader_open_dir_at 能换册/换页重进(SDK 忽略重复 push 的坑);
|
||
* - 离开阅读器回书库后再次点图(同目录二次打开)。
|
||
*
|
||
* 编译:见 test/run_tests.sh(与 test_mode_switch_host 同口径)。
|
||
*/
|
||
#include "tuku_internal.h"
|
||
#include "ui/reader/reader.h"
|
||
#include "logic/reader/page_source.h"
|
||
|
||
#include <dirent.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
|
||
/* ui/library/library.c 的测试接缝 */
|
||
int library_test_load(const char *dir);
|
||
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 { \
|
||
if (cond) { printf(" PASS\n"); } \
|
||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||
} while (0)
|
||
|
||
void mr_nav_report(void) {}
|
||
|
||
static void put16(unsigned char *p, unsigned v)
|
||
{
|
||
p[0] = (unsigned char)v;
|
||
p[1] = (unsigned char)(v >> 8);
|
||
}
|
||
static void put32(unsigned char *p, unsigned v)
|
||
{
|
||
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
|
||
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
|
||
}
|
||
|
||
static int write_bmp24(const char *path, int w, int h)
|
||
{
|
||
int stride = ((w * 3 + 3) / 4) * 4;
|
||
int imgsz = stride * h;
|
||
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
|
||
fh[0] = 'B'; fh[1] = 'M';
|
||
put32(fh + 2, (unsigned)(54 + imgsz));
|
||
put32(fh + 10, 54);
|
||
put32(ih + 0, 40);
|
||
put32(ih + 4, (unsigned)w);
|
||
put32(ih + 8, (unsigned)h);
|
||
put16(ih + 12, 1);
|
||
put16(ih + 14, 24);
|
||
put32(ih + 20, (unsigned)imgsz);
|
||
|
||
FILE *f = fopen(path, "wb");
|
||
if (!f) return -1;
|
||
int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
|
||
fwrite(ih, 1, sizeof ih, f) == sizeof ih;
|
||
unsigned char *row = (unsigned char *)calloc(1, (size_t)stride);
|
||
if (!row) ok = 0;
|
||
for (int y = 0; ok && y < h; y++)
|
||
if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0;
|
||
free(row);
|
||
if (fclose(f) != 0) ok = 0;
|
||
return ok ? 0 : -1;
|
||
}
|
||
|
||
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 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 int row_index_for(const char *name)
|
||
{
|
||
int rows = library_test_rows();
|
||
for (int i = 0; i < rows; i++) {
|
||
meow_list_item it;
|
||
if (!library_test_src(i, &it)) continue;
|
||
if (it.title && strcmp(it.title, name) == 0) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* 用真实点击坐标点某一行(列表顶端 = 状态栏 + 56px 顶栏,行高 56)。 */
|
||
static void tap_row(const char *name)
|
||
{
|
||
int idx = row_index_for(name);
|
||
if (idx < 0) { CHECK(0, "row %s not found", name); return; }
|
||
int list_top = meow_ui_statusbar_height() + 56;
|
||
library_on_tap(120, list_top + idx * 56 + 28);
|
||
}
|
||
|
||
/* 某张图片行在本目录图片自然序里的下标 = 排在前面的「图片 · 」行数。 */
|
||
static int expected_image_index(const char *name)
|
||
{
|
||
int ti = row_index_for(name);
|
||
if (ti < 0) return -1;
|
||
int n = 0;
|
||
for (int i = 0; i < ti; i++) {
|
||
meow_list_item it;
|
||
if (!library_test_src(i, &it)) continue;
|
||
if (it.subtitle && strncmp(it.subtitle, "图片 · ", strlen("图片 · ")) == 0) n++;
|
||
}
|
||
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");
|
||
if (!base || !base[0]) base = ".";
|
||
char tmpl[512];
|
||
snprintf(tmpl, sizeof tmpl, "%s/mr_open_host_XXXXXX", base);
|
||
char *data = mkdtemp(tmpl);
|
||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||
setenv("MEOW_DATA_DIR", data, 1);
|
||
|
||
char root[MR_PATH_MAX], pics[MR_PATH_MAX], other[MR_PATH_MAX], p[MR_PATH_MAX];
|
||
snprintf(root, sizeof root, "%s/library", data);
|
||
snprintf(pics, sizeof pics, "%s/Pictures", root);
|
||
snprintf(other, sizeof other, "%s/Other", root);
|
||
if (mkdir_p(pics) != 0 || mkdir_p(other) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
|
||
|
||
/* Pictures:6 张图(数字序号/大小写/前导零混排)+ 1 个普通文件(不成行)。 */
|
||
const char *imgs[] = { "A.bmp", "a.bmp", "img2.bmp", "img10.bmp",
|
||
"page3.bmp", "Page03.bmp" };
|
||
for (unsigned i = 0; i < sizeof imgs / sizeof imgs[0]; i++) {
|
||
snprintf(p, sizeof p, "%s/%s", pics, imgs[i]);
|
||
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; }
|
||
}
|
||
snprintf(p, sizeof p, "%s/notes.txt", pics);
|
||
{ FILE *f = fopen(p, "wb"); if (f) { fputs("x", f); fclose(f); } }
|
||
|
||
/* Other:4 张图,用于「已在本页时换册重进」。 */
|
||
for (int i = 0; i < 4; i++) {
|
||
snprintf(p, sizeof p, "%s/o%d.bmp", other, i + 1);
|
||
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; }
|
||
|
||
library_test_set_root(root);
|
||
meow_view *lib = library_view_create();
|
||
meow_view *reader = reader_view_create();
|
||
meow_view_page_push(ACT_LIBRARY);
|
||
|
||
CHECK(library_test_load(root) == 1, "load fixture root");
|
||
|
||
/* 点目录 = 进入目录,不打开阅读器。 */
|
||
tap_row("Pictures");
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "tap dir stays in library");
|
||
CHECK(strcmp(library_test_dir(), pics) == 0, "entered Pictures (%s)", library_test_dir());
|
||
CHECK(library_test_rows() == 6, "Pictures rows=%d (6 images, txt excluded)",
|
||
library_test_rows());
|
||
|
||
/* 点 img10.bmp:阅读器从该图开始(count=6, page=它在该目录的自然序下标)。 */
|
||
int want = expected_image_index("img10.bmp");
|
||
CHECK(want == 3, "img10.bmp natural image index=%d (want 3)", want);
|
||
tap_row("img10.bmp");
|
||
CHECK(meow_ui_activity_active() == ACT_READER, "tap image opens reader");
|
||
CHECK(strcmp(mr_reader_dir(), pics) == 0, "reader dir=%s", mr_reader_dir());
|
||
CHECK(mr_reader_ui_open_err() == 0, "open has no error");
|
||
CHECK(mr_reader_ui_count() == 6, "reader pages=%d (want 6)", mr_reader_ui_count());
|
||
CHECK(mr_reader_ui_page() == want, "reader starts at selected image: page=%d (want %d)",
|
||
mr_reader_ui_page(), want);
|
||
|
||
/* 让首帧解码 tick 跑一遍,确认不会因坏夹具进入 open_err。 */
|
||
for (int i = 0; i < 20; i++) usleep(10 * 1000);
|
||
CHECK(mr_reader_ui_open_err() == 0, "still no open_err after decode tick");
|
||
|
||
/* 已在本页:直接换册重进(M17 遗留修复,SDK 会忽略重复 push)。 */
|
||
mr_reader_open_dir_at(other, 2);
|
||
CHECK(meow_ui_activity_active() == ACT_READER, "switch keeps reader active");
|
||
CHECK(strcmp(mr_reader_dir(), other) == 0, "switched reader dir=%s", mr_reader_dir());
|
||
CHECK(mr_reader_ui_count() == 4, "switched book pages=%d (want 4)", mr_reader_ui_count());
|
||
CHECK(mr_reader_ui_page() == 2, "switched book page=%d (want 2)", mr_reader_ui_page());
|
||
|
||
/* 返回书库 → 再点另一张图(同目录二次打开 + 显式起始页优先于续读)。 */
|
||
meow_view_page_pop();
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library");
|
||
CHECK(strcmp(library_test_dir(), pics) == 0, "library still in Pictures");
|
||
|
||
tap_row("page3.bmp");
|
||
CHECK(meow_ui_activity_active() == ACT_READER, "reopen reader");
|
||
CHECK(mr_reader_ui_open_err() == 0, "reopen no error");
|
||
CHECK(mr_reader_ui_count() == 6, "reopen pages=%d", mr_reader_ui_count());
|
||
int want2 = expected_image_index("page3.bmp");
|
||
CHECK(want2 == 4, "page3.bmp natural image index=%d (want 4)", want2);
|
||
CHECK(mr_reader_ui_page() == want2, "reopen starts at page=%d (want %d)",
|
||
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);
|
||
meow_view_free(lib);
|
||
meow_ui_shutdown();
|
||
rm_rf(data);
|
||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||
g_fail ? "see failures above" : "library→reader open checks passed");
|
||
return g_fail ? 1 : 0;
|
||
}
|