- 新增顶层 tuku/ 工程(package dev.tuku.app、二进制 tuku):app.c/internal 头/logic/ui 分层、app.json、build/pack 脚本 - 书库:从 TF 卡根浏览文件夹,含图目录=一本书;logic/library 纯 C 扫描+自然排序(page2<page10) - 流式缩略图:后台线程解码+meow_img_downscale(96),首帧/滚动不阻塞;磁盘缓存 MeowData/<pkg>/cache(mtime 失效+LRU) - 列表 meow_listview 虚拟化 + meow_scroll fling/snap;行=缩略图+目录名+页数 - 主题:全部语义色 + meow_theme_add_listener 即时重绘;明暗/风格族两态可读 - 导航:meow_view page push/pop、TopAppBar ⚙→设置;reader/settings/lock 最小桩 - 接缝:mr_reader_get/set_direction(默认 RTL)、lock_view_should_show()(桩 0) - 宿主 gcc 测试 46 项 PASS;交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
224 lines
8.4 KiB
C
224 lines
8.4 KiB
C
/* test_library.c — 图库 logic 层宿主测试(纯 C,无 SDK/硬件)。
|
||
*
|
||
* 覆盖:
|
||
* T1 自然排序:数字段数值序(page2<page10)、前导零、数字/字母混合、大小写;
|
||
* T2 扩展名过滤:png/jpg/jpeg/bmp 大小写不敏感,gif/txt 不支持;
|
||
* T3 目录扫描:只列出含图目录与可导航中间目录;隐藏/系统目录、空目录、无图目录排除;
|
||
* pages/subdirs/cover/subtitle 正确;父目录路径推导。
|
||
*
|
||
* 编译(在 tuku/ 下):
|
||
* gcc -Wall -Wextra -O1 -I. test/test_library.c logic/library.c logic/reader.c \
|
||
* -o test/test_library && ./test/test_library
|
||
* 或直接 test/run_tests.sh。
|
||
*/
|
||
#include "logic/library.h"
|
||
#include "logic/reader.h"
|
||
|
||
#include <dirent.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.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 const mr_library_entry *find_entry(const mr_library *lib, const char *name)
|
||
{
|
||
for (int i = 0; i < lib->count; i++)
|
||
if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i];
|
||
return NULL;
|
||
}
|
||
|
||
static void test_natural(void)
|
||
{
|
||
CHECK(mr_natural_cmp("page2", "page10") < 0, "page2 < page10");
|
||
CHECK(mr_natural_cmp("page10", "page2") > 0, "page10 > page2");
|
||
CHECK(mr_natural_cmp("page2", "page02") < 0, "fewer leading zeros first");
|
||
CHECK(mr_natural_cmp("x001", "x1") > 0, "x1 < x001");
|
||
CHECK(mr_natural_cmp("Chapter 9", "Chapter 10") < 0, "chapter 9 < 10");
|
||
CHECK(mr_natural_cmp("img2a", "img10b") < 0, "mixed img2a < img10b");
|
||
CHECK(mr_natural_cmp("a", "a1") < 0, "prefix shorter first");
|
||
CHECK(mr_natural_cmp("abc", "abc") == 0, "identical equal");
|
||
CHECK(mr_natural_cmp("ABC", "abc") != 0, "case-only tie is deterministic");
|
||
CHECK(mr_natural_cmp("A2", "a10") < 0, "case-insensitive primary then numeric");
|
||
}
|
||
|
||
static void test_ext(void)
|
||
{
|
||
CHECK(mr_path_is_image("a.png") == 1, "png");
|
||
CHECK(mr_path_is_image("a.JPG") == 1, "JPG uppercase");
|
||
CHECK(mr_path_is_image("a.jpeg") == 1, "jpeg");
|
||
CHECK(mr_path_is_image("a.BmP") == 1, "BMP mixed case");
|
||
CHECK(mr_path_is_image("a.gif") == 0, "gif unsupported");
|
||
CHECK(mr_path_is_image("a.txt") == 0, "txt unsupported");
|
||
CHECK(mr_path_is_image("png") == 0, "no extension");
|
||
CHECK(mr_path_is_image(".png") == 0, "dotfile-ish no stem");
|
||
}
|
||
|
||
static void test_parent(void)
|
||
{
|
||
char out[MR_PATH_MAX];
|
||
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/b", out, sizeof out) == 0 &&
|
||
strcmp(out, "/usr/data/mnt/sd_0/a") == 0, "parent nested");
|
||
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/", out, sizeof out) == 0 &&
|
||
strcmp(out, "/usr/data/mnt/sd_0") == 0, "parent trailing slash");
|
||
CHECK(mr_path_parent("/a", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root child");
|
||
CHECK(mr_path_parent("/", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root");
|
||
}
|
||
|
||
static void test_scan(void)
|
||
{
|
||
char tmpl[] = "./mr_test_XXXXXX";
|
||
char *root = mkdtemp(tmpl);
|
||
CHECK(root != NULL, "mkdtemp");
|
||
if (!root) return;
|
||
|
||
char p[MR_PATH_MAX];
|
||
|
||
snprintf(p, sizeof p, "%s/Series 10", root); mkdir_p(p);
|
||
snprintf(p, sizeof p, "%s/Series 10/page1.png", root); touch_file(p);
|
||
snprintf(p, sizeof p, "%s/Series 10/page2.jpg", root); touch_file(p);
|
||
|
||
snprintf(p, sizeof p, "%s/Series 2", root); mkdir_p(p);
|
||
snprintf(p, sizeof p, "%s/Series 2/page10.png", root); touch_file(p);
|
||
|
||
snprintf(p, sizeof p, "%s/Series 02", root); mkdir_p(p);
|
||
snprintf(p, sizeof p, "%s/Series 02/page1.bmp", root); touch_file(p);
|
||
|
||
snprintf(p, sizeof p, "%s/Series 3", root); mkdir_p(p);
|
||
snprintf(p, sizeof p, "%s/Series 3/page1.gif", root); touch_file(p);
|
||
|
||
snprintf(p, sizeof p, "%s/Empty", root); mkdir_p(p);
|
||
|
||
snprintf(p, sizeof p, "%s/Nested/Deeper", root); mkdir_p(p);
|
||
snprintf(p, sizeof p, "%s/Nested/Deeper/a.png", root); touch_file(p);
|
||
|
||
snprintf(p, sizeof p, "%s/.hidden", root); mkdir_p(p);
|
||
snprintf(p, sizeof p, "%s/.hidden/h.png", root); touch_file(p);
|
||
|
||
snprintf(p, sizeof p, "%s/MeowAPPs", root); mkdir_p(p);
|
||
snprintf(p, sizeof p, "%s/MeowAPPs/m.png", root); touch_file(p);
|
||
|
||
snprintf(p, sizeof p, "%s/root.png", root); touch_file(p); /* 根目录直接图片不成为行 */
|
||
|
||
mr_library lib;
|
||
memset(&lib, 0, sizeof lib);
|
||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||
CHECK(n == 4, "scan count=%d (want 4)", n);
|
||
CHECK(lib.count == n, "count matches");
|
||
if (lib.count == 4) {
|
||
CHECK(strcmp(lib.items[0].name, "Nested") == 0, "sort[0]=Nested got %s", lib.items[0].name);
|
||
CHECK(strcmp(lib.items[1].name, "Series 2") == 0, "sort[1]=Series 2 got %s", lib.items[1].name);
|
||
CHECK(strcmp(lib.items[2].name, "Series 02") == 0, "sort[2]=Series 02 got %s", lib.items[2].name);
|
||
CHECK(strcmp(lib.items[3].name, "Series 10") == 0, "sort[3]=Series 10 got %s", lib.items[3].name);
|
||
}
|
||
|
||
const mr_library_entry *e = find_entry(&lib, "Series 10");
|
||
CHECK(e && e->pages == 2 && e->has_images == 1 && e->subdirs == 0, "Series 10 pages=2");
|
||
CHECK(e && strcmp(e->subtitle, "2 pages") == 0, "Series 10 subtitle '%s'", e ? e->subtitle : "");
|
||
CHECK(e && strstr(e->cover, "page1.png") != NULL, "Series 10 cover natural-first '%s'",
|
||
e ? e->cover : "");
|
||
|
||
e = find_entry(&lib, "Series 2");
|
||
CHECK(e && e->pages == 1 && e->has_images == 1, "Series 2 pages=1");
|
||
|
||
e = find_entry(&lib, "Nested");
|
||
CHECK(e && e->pages == 0 && e->has_images == 0 && e->subdirs == 1, "Nested intermediate");
|
||
CHECK(e && strcmp(e->subtitle, "1 folder") == 0, "Nested subtitle '%s'", e ? e->subtitle : "");
|
||
CHECK(e && strstr(e->cover, "Deeper/a.png") != NULL, "Nested cover from descendant '%s'",
|
||
e ? e->cover : "");
|
||
|
||
CHECK(find_entry(&lib, "Empty") == NULL, "empty dir excluded");
|
||
CHECK(find_entry(&lib, "Series 3") == NULL, "gif-only dir excluded");
|
||
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
|
||
CHECK(find_entry(&lib, "MeowAPPs") == NULL, "system dir excluded");
|
||
|
||
/* 深度 1:Nested 不再可探测到 Deeper 的图,应被排除。 */
|
||
mr_library lib1;
|
||
memset(&lib1, 0, sizeof lib1);
|
||
CHECK(mr_library_scan(&lib1, root, 1) == 3, "max_depth=1 excludes Nested");
|
||
mr_library_free(&lib1);
|
||
|
||
mr_library_free(&lib);
|
||
|
||
/* 不可读目录:返回 -1,lib 可安全 free。 */
|
||
mr_library bad;
|
||
memset(&bad, 0, sizeof bad);
|
||
CHECK(mr_library_scan(&bad, "/no/such/dir/tuku", MR_LIB_MAX_DEPTH) == -1, "missing dir -> -1");
|
||
CHECK(bad.count == 0, "missing dir empty");
|
||
mr_library_free(&bad);
|
||
|
||
rm_rf(root);
|
||
}
|
||
|
||
static void test_direction(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(99);
|
||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "invalid ignored");
|
||
mr_reader_set_direction(MR_DIR_RTL);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
test_natural();
|
||
test_ext();
|
||
test_parent();
|
||
test_scan();
|
||
test_direction();
|
||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||
return g_fail ? 1 : 0;
|
||
}
|