Files
MeowOS-SDK/tuku/test/test_library.c
T
AskaEth bed6a95485 feat(tuku): 图库 M3 骨架——书库浏览/主题同步/阅读器与锁屏接缝
- 新增顶层 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 零警告
2026-11-04 19:44:18 +08:00

224 lines
8.4 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}