/* test_reader_core.c — 阅读器纯逻辑宿主测试(无 SDK/硬件)。 * * 覆盖: * R1 页源契约:空册 / 不可读目录 / 越界 open / 重复 open-close / 首末页 / 自然序 / * 隐藏文件与子目录排除 / 销毁后失效; * R2 契约对两种消费方式通用:meta() 给条漫连续滚动排高度,open() 给左右翻页取图; * R3 图片头探测:PNG/JPEG/BMP 原始尺寸(不解码); * R4 方向语义:tap 分区 / 拖动增量(RTL 默认与 LTR)/ clamp / 提交阈值; * R5 fit 三档 / 缩放 / 平移夹取 / 进场方向 / 档位循环; * R6 阅读方向状态(M3 API)。 * * 编译(在 tuku/ 下): * gcc -Wall -Wextra -O1 -I. test/test_reader_core.c logic/library.c logic/reader.c \ * logic/reader/page_source.c logic/reader/nav.c -o test/test_reader_core * 或直接 test/run_tests.sh。 */ #include "logic/library.h" #include "logic/reader.h" #include "logic/reader/nav.h" #include "logic/reader/page_source.h" #include #include #include #include #include #include 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) /* ============================================================ */ /* 假解码器:meow_img* 用堆上假句柄代替,记录 open/close 次数 */ /* ============================================================ */ typedef struct fake_img { int id; } fake_img; static int g_loads, g_frees; static int g_last_maxdim; static char g_last_path[MR_PATH_MAX]; static meow_img *fake_load(const char *path, void *ud, int maxdim) { (void)ud; g_loads++; g_last_maxdim = maxdim; snprintf(g_last_path, sizeof g_last_path, "%s", path ? path : ""); fake_img *f = (fake_img *)malloc(sizeof *f); if (!f) return NULL; f->id = g_loads; return (meow_img *)f; } static void fake_unload(meow_img *img) { g_frees++; free(img); } /* ============================================================ */ /* 临时目录工具 */ /* ============================================================ */ 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 write_bytes(const char *path, const unsigned char *buf, int len) { FILE *f = fopen(path, "wb"); if (!f) return -1; int ok = fwrite(buf, 1, (size_t)len, f) == (size_t)len; if (fclose(f) != 0) ok = 0; return ok ? 0 : -1; } static int touch_file(const char *path) { return write_bytes(path, (const unsigned char *)"x", 1); } 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 write_png(const char *path, int w, int h) { unsigned char b[24] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 0, 0, 0, 13, 'I', 'H', 'D', 'R', (unsigned char)(w >> 24), (unsigned char)(w >> 16), (unsigned char)(w >> 8), (unsigned char)w, (unsigned char)(h >> 24), (unsigned char)(h >> 16), (unsigned char)(h >> 8), (unsigned char)h, }; return write_bytes(path, b, (int)sizeof b); } static int write_jpeg(const char *path, int w, int h) { unsigned char b[16] = { 0xFF, 0xD8, /* SOI */ 0xFF, 0xC0, 0x00, 0x11, /* SOF0, len=17 */ 0x08, /* precision */ (unsigned char)(h >> 8), (unsigned char)h, (unsigned char)(w >> 8), (unsigned char)w, 0x03, 0x01, 0x11, 0x00, /* 3 分量(长度不足也无妨:只读到 w/h) */ }; return write_bytes(path, b, (int)sizeof b); } static int write_bmp(const char *path, int w, int h) { unsigned char b[26] = { 0 }; b[0] = 'B'; b[1] = 'M'; b[18] = (unsigned char)w; b[19] = (unsigned char)(w >> 8); b[20] = (unsigned char)(w >> 16); b[21] = (unsigned char)(w >> 24); b[22] = (unsigned char)h; b[23] = (unsigned char)(h >> 8); b[24] = (unsigned char)(h >> 16); b[25] = (unsigned char)(h >> 24); return write_bytes(path, b, (int)sizeof b); } /* ============================================================ */ /* R1/R2 页源契约 */ /* ============================================================ */ static void test_contract(void) { char tmpl[] = "./mr_reader_XXXXXX"; char *root = mkdtemp(tmpl); CHECK(root != NULL, "mkdtemp"); if (!root) return; char p[MR_PATH_MAX]; snprintf(p, sizeof p, "%s/page10.png", root); write_png(p, 600, 900); snprintf(p, sizeof p, "%s/page2.jpg", root); write_jpeg(p, 1200, 1800); snprintf(p, sizeof p, "%s/page1.bmp", root); write_bmp(p, 800, 1200); snprintf(p, sizeof p, "%s/notes.txt", root); touch_file(p); snprintf(p, sizeof p, "%s/.hidden.png", root); touch_file(p); snprintf(p, sizeof p, "%s/sub.png", root); mkdir_p(p); /* 目录不参与成册 */ snprintf(p, sizeof p, "%s/sub/inside.png", root); touch_file(p); mr_dir_book db; mr_book book; int n = mr_dir_book_init(&db, root, 640, fake_load, fake_unload, NULL); CHECK(n == 3, "init count=%d (want 3)", n); CHECK(db.count == 3, "db.count=%d", db.count); mr_dir_book_bind(&book, &db); CHECK(mr_book_valid(&book) == 1, "book valid"); CHECK(mr_book_count(&book) == 3, "book count=3"); CHECK(mr_book_count(NULL) == 0, "NULL count safe"); CHECK(mr_book_valid(NULL) == 0, "NULL invalid"); CHECK(mr_book_open(NULL, 0) == NULL, "NULL open safe"); mr_book_close(NULL, NULL); /* 全程 NULL 安全,不崩 */ mr_book_destroy(NULL); /* 自然序 + 首末页 */ char path[MR_PATH_MAX]; mr_dir_book_path(&db, 0, path, sizeof path); CHECK(strstr(path, "page1.bmp") != NULL, "page 0 = page1 got %s", path); mr_dir_book_path(&db, 1, path, sizeof path); CHECK(strstr(path, "page2.jpg") != NULL, "page 1 = page2 got %s", path); mr_dir_book_path(&db, 2, path, sizeof path); CHECK(strstr(path, "page10.png") != NULL, "page 2 = page10 got %s", path); /* 越界:open/meta/path 全部安全 */ CHECK(mr_book_open(&book, -1) == NULL, "open(-1) NULL"); CHECK(mr_book_open(&book, 3) == NULL, "open(count) NULL"); CHECK(mr_book_open(&book, 9999) == NULL, "open(9999) NULL"); int mw = -1, mh = -1; CHECK(mr_book_meta(&book, -1, &mw, &mh) == 0 && mw == 0 && mh == 0, "meta(-1) unknown"); CHECK(mr_book_meta(&book, 3, &mw, &mh) == 0, "meta(count) unknown"); mr_dir_book_path(&db, 9, path, sizeof path); CHECK(path[0] == '\0', "path out of range empty"); /* 首末页 open + 重复 open-close(每次 open 的句柄独立,各 close 一次) */ int loads0 = g_loads, frees0 = g_frees; meow_img *a = mr_book_open(&book, 0); CHECK(a != NULL, "open(0) ok"); CHECK(strstr(g_last_path, "page1.bmp") != NULL, "open(0) path %s", g_last_path); CHECK(g_last_maxdim == 640, "open passes maxdim=%d", g_last_maxdim); meow_img *a2 = mr_book_open(&book, 0); CHECK(a2 != NULL && a2 != a, "re-open(0) gives independent handle"); CHECK(g_loads == loads0 + 2, "two loads"); mr_book_close(&book, a); mr_book_close(&book, a2); CHECK(g_frees == frees0 + 2, "frees=%d", g_frees - frees0); /* 再开再关一轮:契约允许同一页反复 open/close */ a = mr_book_open(&book, 0); CHECK(a != NULL && g_loads == loads0 + 3, "re-open after close"); mr_book_close(&book, a); CHECK(g_frees == frees0 + 3, "re-close frees"); meow_img *last = mr_book_open(&book, 2); CHECK(last != NULL, "open(last) ok"); CHECK(strstr(g_last_path, "page10.png") != NULL, "open(last) path %s", g_last_path); mr_book_close(&book, last); /* meta:PNG/JPEG/BMP 头解析 */ CHECK(mr_book_meta(&book, 0, &mw, &mh) == 1 && mw == 800 && mh == 1200, "meta bmp=%dx%d", mw, mh); CHECK(mr_book_meta(&book, 1, &mw, &mh) == 1 && mw == 1200 && mh == 1800, "meta jpeg=%dx%d", mw, mh); CHECK(mr_book_meta(&book, 2, &mw, &mh) == 1 && mw == 600 && mh == 900, "meta png=%dx%d", mw, mh); /* R2:契约对条漫通用——用 meta 累加高度,按滚动位置定位页(M7 可直接复用)。 */ long y = 0, heights[3]; for (int i = 0; i < 3; i++) { int sw = 0, sh = 0; mr_book_meta(&book, i, &sw, &sh); int fw = 0, fh = 0; mr_nav_fit_size(MR_FIT_WIDTH, sw, sh, 480, 720, &fw, &fh); heights[i] = fh; y += fh; } CHECK(y == 720 + 720 + 720, "column height=%ld", y); CHECK(heights[0] == 720 && heights[1] == 720, "meta-derived fit heights"); /* 销毁:provider 失效,二次 destroy 幂等 */ mr_book_destroy(&book); CHECK(mr_book_valid(&book) == 0, "destroyed invalid"); CHECK(mr_book_open(&book, 0) == NULL, "open after destroy NULL"); mr_book_destroy(&book); mr_dir_book_free(&db); rm_rf(root); } static void test_empty_and_bad(void) { char tmpl[] = "./mr_reader_empty_XXXXXX"; char *root = mkdtemp(tmpl); CHECK(root != NULL, "mkdtemp empty"); /* 空目录:可读但 0 页(≠ 错误) */ mr_dir_book db; int n = mr_dir_book_init(&db, root, 720, fake_load, fake_unload, NULL); CHECK(n == 0, "empty dir count=0 got %d", n); CHECK(db.count == 0, "empty db count"); mr_book book; mr_dir_book_bind(&book, &db); CHECK(mr_book_count(&book) == 0, "empty book count 0"); CHECK(mr_book_open(&book, 0) == NULL, "empty open NULL"); CHECK(mr_book_meta(&book, 0, NULL, NULL) == 0, "empty meta NULL-safe"); mr_dir_book_free(&db); /* 目录不存在:-1,且 db 可安全 free */ mr_dir_book bad; CHECK(mr_dir_book_init(&bad, "/no/such/dir/tuku-reader", 720, fake_load, fake_unload, NULL) == -1, "missing dir -> -1"); CHECK(bad.count == 0 && bad.names == NULL, "missing dir cleared"); mr_dir_book_free(&bad); /* 参数非法 */ CHECK(mr_dir_book_init(NULL, root, 720, fake_load, fake_unload, NULL) == -1, "NULL db"); CHECK(mr_dir_book_init(&bad, root, 720, NULL, fake_unload, NULL) == -1, "NULL load"); CHECK(mr_dir_book_init(&bad, root, 720, fake_load, NULL, NULL) == -1, "NULL unload"); CHECK(mr_dir_book_init(&bad, "", 720, fake_load, fake_unload, NULL) == -1, "empty dirname"); rm_rf(root); } /* ============================================================ */ /* R4 方向语义 */ /* ============================================================ */ static void test_direction_nav(void) { const int W = 480; CHECK(mr_nav_tap_zone(0, W) == MR_ZONE_LEFT, "zone 0"); CHECK(mr_nav_tap_zone(159, W) == MR_ZONE_LEFT, "zone 159"); CHECK(mr_nav_tap_zone(160, W) == MR_ZONE_MIDDLE, "zone 160"); CHECK(mr_nav_tap_zone(319, W) == MR_ZONE_MIDDLE, "zone 319"); CHECK(mr_nav_tap_zone(320, W) == MR_ZONE_RIGHT, "zone 320"); CHECK(mr_nav_tap_zone(479, W) == MR_ZONE_RIGHT, "zone 479"); CHECK(mr_nav_tap_zone(10, 0) == MR_ZONE_MIDDLE, "zone bad width"); /* RTL(默认):屏左=下一页,屏右=上一页 */ CHECK(mr_nav_tap_delta(1, MR_ZONE_LEFT) == 1, "RTL left=next"); CHECK(mr_nav_tap_delta(1, MR_ZONE_RIGHT) == -1, "RTL right=prev"); CHECK(mr_nav_tap_delta(1, MR_ZONE_MIDDLE) == 0, "RTL middle=chrome"); /* LTR:屏左=上一页,屏右=下一页 */ CHECK(mr_nav_tap_delta(0, MR_ZONE_LEFT) == -1, "LTR left=prev"); CHECK(mr_nav_tap_delta(0, MR_ZONE_RIGHT) == 1, "LTR right=next"); CHECK(mr_nav_tap_delta(0, MR_ZONE_MIDDLE) == 0, "LTR middle=chrome"); /* 拖动:向右(dx>0)露出左侧那页 */ CHECK(mr_nav_swipe_delta(1, 120) == 1, "RTL drag right=next"); CHECK(mr_nav_swipe_delta(1, -120) == -1, "RTL drag left=prev"); CHECK(mr_nav_swipe_delta(0, 120) == -1, "LTR drag right=prev"); CHECK(mr_nav_swipe_delta(0, -120) == 1, "LTR drag left=next"); CHECK(mr_nav_swipe_delta(1, 0) == 0, "drag 0 no-op"); CHECK(mr_nav_clamp_page(-5, 10) == 0, "clamp low"); CHECK(mr_nav_clamp_page(20, 10) == 9, "clamp high"); CHECK(mr_nav_clamp_page(3, 10) == 3, "clamp inside"); CHECK(mr_nav_clamp_page(0, 0) == -1, "clamp empty"); CHECK(mr_nav_commit_dist(480) == 120, "commit dist 480 -> 120"); CHECK(mr_nav_commit_dist(200) == 60, "commit dist 200 -> 60"); CHECK(mr_nav_commit_dist(2000) == 160, "commit dist 2000 -> 160"); CHECK(mr_nav_commit(119, 480) == 0, "119 not committed"); CHECK(mr_nav_commit(120, 480) == 1, "120 committed"); CHECK(mr_nav_commit(-120, 480) == 1, "-120 committed"); /* 进场方向:RTL 下一页从左侧进,LTR 下一页从右侧进 */ CHECK(mr_nav_slide_from(1, 1, W) == -W, "RTL next enters left"); CHECK(mr_nav_slide_from(1, -1, W) == W, "RTL prev enters right"); CHECK(mr_nav_slide_from(0, 1, W) == W, "LTR next enters right"); CHECK(mr_nav_slide_from(0, -1, W) == -W, "LTR prev enters left"); CHECK(mr_nav_slide_from(1, 0, W) == 0, "slide 0"); } /* ============================================================ */ /* R5 fit / 缩放 / 平移 */ /* ============================================================ */ static void test_fit_zoom_pan(void) { int w = 0, h = 0; /* 竖图 1000×1500,视口 480×720 */ CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 1000, 1500, 480, 720, &w, &h) == 0 && w == 480 && h == 720, "fit width %dx%d", w, h); CHECK(mr_nav_fit_size(MR_FIT_HEIGHT, 1000, 1500, 480, 720, &w, &h) == 0 && w == 480 && h == 720, "fit height %dx%d", w, h); CHECK(mr_nav_fit_size(MR_FIT_ORIGINAL, 1000, 1500, 480, 720, &w, &h) == 0 && w == 1000 && h == 1500, "fit original %dx%d", w, h); /* 横图 1500×1000:适宽缩到 480×320,适高放大到 1080×720(可左右平移) */ CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 1500, 1000, 480, 720, &w, &h) == 0 && w == 480 && h == 320, "landscape fit width %dx%d", w, h); CHECK(mr_nav_fit_size(MR_FIT_HEIGHT, 1500, 1000, 480, 720, &w, &h) == 0 && w == 1080 && h == 720, "landscape fit height %dx%d", w, h); CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 0, 100, 480, 720, &w, &h) == -1, "fit bad src"); CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 100, 100, 0, 720, &w, &h) == -1, "fit bad viewport"); CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 100, 100, 480, 720, NULL, &h) == -1, "fit NULL out"); CHECK(mr_nav_zoom_size(480, 720, 200, &w, &h) == 0 && w == 960 && h == 1440, "zoom 200 %dx%d", w, h); CHECK(mr_nav_zoom_size(480, 720, 150, &w, &h) == 0 && w == 720 && h == 1080, "zoom 150 %dx%d", w, h); CHECK(mr_nav_zoom_size(0, 720, 200, &w, &h) == -1, "zoom bad size"); int lx = -1, ly = -1; mr_nav_pan_limit(960, 1440, 480, 720, &lx, &ly); CHECK(lx == 240 && ly == 360, "pan limit %d,%d", lx, ly); mr_nav_pan_limit(480, 720, 480, 720, &lx, &ly); CHECK(lx == 0 && ly == 0, "no overflow no pan"); int px = 9999, py = -9999; mr_nav_pan_clamp(960, 1440, 480, 720, &px, &py); CHECK(px == 240 && py == -360, "pan clamp %d,%d", px, py); px = 10; py = 20; mr_nav_pan_clamp(480, 720, 480, 720, &px, &py); CHECK(px == 0 && py == 0, "pan clamp no overflow"); CHECK(mr_nav_fit_next(MR_FIT_WIDTH) == MR_FIT_HEIGHT, "fit cycle w->h"); CHECK(mr_nav_fit_next(MR_FIT_HEIGHT) == MR_FIT_ORIGINAL, "fit cycle h->o"); CHECK(mr_nav_fit_next(MR_FIT_ORIGINAL) == MR_FIT_WIDTH, "fit cycle o->w"); CHECK(mr_nav_fit_next(99) == MR_FIT_WIDTH, "fit cycle bad"); } /* ============================================================ */ /* R3 图片头探测 */ /* ============================================================ */ static void test_probe(void) { char tmpl[] = "./mr_reader_probe_XXXXXX"; char *root = mkdtemp(tmpl); CHECK(root != NULL, "mkdtemp probe"); if (!root) return; char p[MR_PATH_MAX]; int w = 0, h = 0; snprintf(p, sizeof p, "%s/a.png", root); write_png(p, 1234, 5678); CHECK(mr_probe_size(p, &w, &h) == 1 && w == 1234 && h == 5678, "probe png %dx%d", w, h); snprintf(p, sizeof p, "%s/a.jpg", root); write_jpeg(p, 1600, 2400); CHECK(mr_probe_size(p, &w, &h) == 1 && w == 1600 && h == 2400, "probe jpeg %dx%d", w, h); snprintf(p, sizeof p, "%s/a.bmp", root); write_bmp(p, 640, 480); CHECK(mr_probe_size(p, &w, &h) == 1 && w == 640 && h == 480, "probe bmp %dx%d", w, h); snprintf(p, sizeof p, "%s/notes.txt", root); touch_file(p); CHECK(mr_probe_size(p, &w, &h) == 0 && w == 0 && h == 0, "probe unknown"); CHECK(mr_probe_size("/no/such/file.png", &w, &h) == 0, "probe missing"); CHECK(mr_probe_size(NULL, &w, &h) == 0, "probe NULL"); rm_rf(root); } /* ============================================================ */ /* R6 阅读方向状态 */ /* ============================================================ */ static void test_direction_state(void) { mr_reader_set_direction(MR_DIR_RTL); CHECK(mr_reader_get_direction() == MR_DIR_RTL, "default RTL"); mr_reader_set_direction(MR_DIR_LTR); CHECK(mr_reader_get_direction() == MR_DIR_LTR, "switch LTR"); mr_reader_set_direction(42); CHECK(mr_reader_get_direction() == MR_DIR_LTR, "invalid ignored"); mr_reader_set_direction(MR_DIR_RTL); } int main(void) { test_contract(); test_empty_and_bad(); test_probe(); test_direction_nav(); test_fit_zoom_pan(); test_direction_state(); printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail); return g_fail ? 1 : 0; }