任务1 缓存分目录(用户定案):\n- ncm_cache_init mkdir songs/ 与 covers/ 子目录 + 一次性迁移\n (根混放 song_*/cov*/cover_current.jpg 挪进两子目录,幂等,legacy_migrated 计数)\n- ncm_cache_path 按 id 前缀路由(cov*→covers/,其余→songs/)\n- LRU(evict/total_bytes/7天 .part 回收)只扫 songs/;clear 清 songs/+covers/ 的 song_*\n- cover.c save_cover_raw 落 covers/cover_current.jpg\n\n任务2 陈旧 mp3 缓存修复(真机实锤:hires 档键下存 mp3,播放命中旧档):\n- song_local_file 缓存命中且用户档>EXHIGH(无损+)且实体是 .mp3 = 静默降级产物\n → 迁移重命名到 exhigh 键 song_<id>_exhigh.mp3(选极高仍可用;目标已存在删旧)\n → 本档视为未命中走下载(311 flac 门禁拿真无损/正确降级)\n\n验证:cache_evict_sim case10(目录迁移一次成型)+P1-9;player_flow_sim P13(迁移+miss)\n+P1-12 零回归;stream/window/search/degrade/cover_chain/test_ncm_core(host+qemu) 全 PASS;\nbuild_ncm.sh -Wall -Wextra 0 warning。0 真实请求。新 ncm md5 a90ba8c3 (text1028458/bss1364484)。
394 lines
19 KiB
C
394 lines
19 KiB
C
/* bench/cache_evict_sim.c — 缓存池 LRU 淘汰 / 清空 离线验证(0 网络,仅本地文件系统)。
|
||
*
|
||
* 直接驱动 core 的真实函数 ncm_cache_init / ncm_cache_set_limit_gb / ncm_cache_evict /
|
||
* ncm_cache_total_bytes / ncm_cache_clear / ncm_cache_lookup,验证三期第二批接进生产路径的
|
||
* 淘汰语义(REPORT §15):
|
||
* ① 超上限按 mtime 升序删最旧,直到 <= 上限;
|
||
* ② 未超上限无操作;
|
||
* ③ ncm_cache_lookup 刷新 mtime(LRU:被访问的最近优先存活);
|
||
* ④ ncm_cache_set_limit_gb 越界钳制(1~100GB);
|
||
* ⑤ ncm_cache_clear 删除 song_* 全部(mp3/封面/残留 .tmp)。
|
||
* §NEW 缓存分目录:song_* 落 songs/、封面(song_cov*、cover_current.jpg)落 covers/;
|
||
* LRU/7 天回收只扫 songs/;⑩ 覆盖 ncm_cache_init 一次性把根混放文件迁入两子目录。
|
||
* 文件用 ftruncate 生成稀疏文件(逻辑大小可控,磁盘零消耗)。
|
||
*
|
||
* 编译(宿主,链接 core/ncm_cache.c + 本文件):
|
||
* $CC_HOST -O2 -DJSMN_HEADER -I. -Icore -c bench/cache_evict_sim.c -o /tmp/ce.o
|
||
* $CC_HOST -O2 -DJSMN_HEADER -I. -Icore -c core/ncm_cache.c -o /tmp/ce_cache.o
|
||
* $CC_HOST -O2 -o cache_evict_sim /tmp/ce.o /tmp/ce_cache.o -lpthread
|
||
* 运行:./cache_evict_sim (0 网络;PASS/FAIL 退出码)
|
||
*/
|
||
#include "../core/ncm_cache.h"
|
||
#include "../core/ncm_common.h"
|
||
#include "../core/ncm_store.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <errno.h>
|
||
#include <dirent.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/time.h>
|
||
#include <time.h>
|
||
|
||
/* ---- ncm_cache.c 依赖的符号(核心库其他 TU 提供;此处最小内联/桩,避免链 mbedtls) ---- */
|
||
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||
{
|
||
size_t n = 0;
|
||
if (sz) {
|
||
while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; }
|
||
dst[n] = 0;
|
||
}
|
||
while (src[n]) n++;
|
||
return n;
|
||
}
|
||
int ncm_store_mkdir_p(const char *path)
|
||
{
|
||
if (!path || !*path) return -1;
|
||
char tmp[512];
|
||
size_t len = strlen(path);
|
||
if (len >= sizeof tmp) return -1;
|
||
memcpy(tmp, path, len + 1);
|
||
if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
|
||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||
char ch = tmp[i];
|
||
tmp[i] = '\0';
|
||
if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1;
|
||
tmp[i] = ch;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
int ncm_store_read_cfg_int(const char *dir, const char *key, int def) { (void)dir; (void)key; return def; }
|
||
int64_t ncm_now_ms(void)
|
||
{
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_REALTIME, &ts);
|
||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||
}
|
||
|
||
/* ---- 工具 ---- */
|
||
static char g_dir[512];
|
||
static ncm_cache_t g_c;
|
||
static int g_fail;
|
||
|
||
#define CHECK(cond, fmt, ...) do { \
|
||
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||
} while (0)
|
||
|
||
/* 按真实生产命名建缓存文件:路径经 ncm_cache_path(与 app/play/cover 同约定,ext 带前导点)。
|
||
* 结果文件名 = song_<id>.<ext>(ext 带点时即双点,如 song_a..mp3 —— 与 core 现状一致)。 */
|
||
static void make_cached(const char *id, const char *ext, unsigned long long size, long mtime)
|
||
{
|
||
char p[600];
|
||
if (ncm_cache_path(&g_c, id, ext, p, sizeof p) != 0) { printf(" path err %s\n", id); g_fail = 1; return; }
|
||
int fd = open(p, O_WRONLY | O_CREAT, 0644);
|
||
if (fd < 0) { printf(" open %s failed\n", id); g_fail = 1; return; }
|
||
if (ftruncate(fd, (off_t)size) != 0) printf(" truncate %s err\n", id);
|
||
close(fd);
|
||
struct timespec ts[2];
|
||
ts[0].tv_sec = mtime; ts[0].tv_nsec = 0;
|
||
ts[1].tv_sec = mtime; ts[1].tv_nsec = 0;
|
||
utimensat(AT_FDCWD, p, ts, 0);
|
||
}
|
||
|
||
static int cached_exists(const char *id, const char *ext)
|
||
{
|
||
char p[600];
|
||
if (ncm_cache_path(&g_c, id, ext, p, sizeof p) != 0) return 0;
|
||
struct stat st;
|
||
return stat(p, &st) == 0;
|
||
}
|
||
|
||
static unsigned long long mb(unsigned long long n) { return n * 1024ULL * 1024ULL; }
|
||
|
||
static void sweep_dir(const char *dir)
|
||
{
|
||
DIR *d = opendir(dir);
|
||
if (!d) return;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (strncmp(e->d_name, "song_", 5) != 0) continue;
|
||
char p[900];
|
||
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
|
||
unlink(p);
|
||
}
|
||
closedir(d);
|
||
}
|
||
|
||
/* 分目录后:song_* 落在 songs/(封面条目 song_cov* 在 covers/),清根 + 两个子目录。 */
|
||
static void cleanup(void)
|
||
{
|
||
char sub[640];
|
||
sweep_dir(g_dir);
|
||
snprintf(sub, sizeof sub, "%s/songs", g_dir); sweep_dir(sub);
|
||
snprintf(sub, sizeof sub, "%s/covers", g_dir); sweep_dir(sub);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
snprintf(g_dir, sizeof g_dir, "/tmp/ncm_cache_sim_%d", (int)getpid());
|
||
cleanup();
|
||
ncm_cache_init(&g_c, g_dir);
|
||
ncm_cache_set_limit_gb(&g_c, 1); /* 1GB 最小上限(淘汰只在 total > limit 时触发) */
|
||
|
||
/* ① 超上限删最旧。A=700MB(最旧), B=300MB, C=400MB(最新) → 删 A 后 700MB<=1GB 停。 */
|
||
printf("== case 1: evict oldest over-limit ==\n");
|
||
cleanup();
|
||
make_cached("a", ".mp3", mb(700), 1000);
|
||
make_cached("b", ".mp3", mb(300), 1100);
|
||
make_cached("c", ".mp3", mb(400), 1200);
|
||
CHECK(ncm_cache_total_bytes(&g_c) == mb(1400), "total=%llu (expect 1400MB)",
|
||
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||
ncm_cache_evict(&g_c);
|
||
CHECK(!cached_exists("a", ".mp3"), "oldest A evicted");
|
||
CHECK(cached_exists("b", ".mp3") && cached_exists("c", ".mp3"), "B/C kept");
|
||
CHECK(ncm_cache_total_bytes(&g_c) == mb(700), "total=%llu after evict (expect 700MB)",
|
||
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||
|
||
/* ② 4×500MB 超 1GB:删 2 个最旧到 <=1GB。 */
|
||
printf("== case 2: evict two oldest ==\n");
|
||
cleanup();
|
||
make_cached("a", ".mp3", mb(500), 1000);
|
||
make_cached("b", ".mp3", mb(500), 1100);
|
||
make_cached("c", ".mp3", mb(500), 1200);
|
||
make_cached("d", ".mp3", mb(500), 1300);
|
||
ncm_cache_evict(&g_c);
|
||
CHECK(!cached_exists("a", ".mp3") && !cached_exists("b", ".mp3"), "A/B (two oldest) evicted");
|
||
CHECK(cached_exists("c", ".mp3") && cached_exists("d", ".mp3"), "C/D kept");
|
||
CHECK(ncm_cache_total_bytes(&g_c) == mb(1000), "total=%llu (expect 1000MB)",
|
||
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||
|
||
/* ③ 未超上限:无删除。 */
|
||
printf("== case 3: under limit no-op ==\n");
|
||
cleanup();
|
||
make_cached("a", ".mp3", mb(300), 1000);
|
||
make_cached("b", ".mp3", mb(200), 1100);
|
||
ncm_cache_evict(&g_c);
|
||
CHECK(cached_exists("a", ".mp3") && cached_exists("b", ".mp3"), "all kept under limit");
|
||
|
||
/* ④ lookup 刷新 mtime(LRU):A 被访问 → 变最新,淘汰先删 B/C。 */
|
||
printf("== case 4: lookup refreshes LRU mtime ==\n");
|
||
cleanup();
|
||
make_cached("a", ".mp3", mb(600), 1000);
|
||
make_cached("b", ".mp3", mb(600), 1100);
|
||
make_cached("c", ".mp3", mb(600), 1200);
|
||
char path[600];
|
||
CHECK(ncm_cache_lookup(&g_c, "a", ".mp3", path, sizeof path) == NCM_OK, "lookup A hit");
|
||
ncm_cache_evict(&g_c); /* A mtime 已被刷新为最新 → 删 B、C */
|
||
CHECK(cached_exists("a", ".mp3"), "A (recently accessed) survives");
|
||
CHECK(!cached_exists("b", ".mp3") && !cached_exists("c", ".mp3"), "B/C evicted");
|
||
|
||
/* ⑤ set_limit_gb 钳制。 */
|
||
printf("== case 5: limit clamping ==\n");
|
||
ncm_cache_set_limit_gb(&g_c, 0);
|
||
CHECK(ncm_cache_limit_gb(&g_c) == 1, "limit(0)->%d (expect 1)", ncm_cache_limit_gb(&g_c));
|
||
ncm_cache_set_limit_gb(&g_c, 200);
|
||
CHECK(ncm_cache_limit_gb(&g_c) == 100, "limit(200)->%d (expect 100)", ncm_cache_limit_gb(&g_c));
|
||
|
||
/* ⑥ clear 删 song_* 全部(mp3 + 封面 + 残留 .tmp)。 */
|
||
printf("== case 6: clear removes all (mp3+cover+tmp) ==\n");
|
||
cleanup();
|
||
make_cached("x", ".mp3", mb(10), 1000);
|
||
make_cached("y", ".mp3", mb(10), 1001);
|
||
make_cached("cov123", ".jpg", mb(1), 1002);
|
||
/* 残留 .tmp:取 mp3 生产路径后追加 .tmp(ncm_cache_open 的临时名)。 */
|
||
{
|
||
char tp[600];
|
||
ncm_cache_path(&g_c, "z", ".mp3", tp, sizeof tp);
|
||
strncat(tp, ".tmp", sizeof tp - strlen(tp) - 1);
|
||
int fd = open(tp, O_WRONLY | O_CREAT, 0644);
|
||
if (ftruncate(fd, (off_t)mb(5)) != 0) printf(" truncate tmp err\n");
|
||
close(fd);
|
||
}
|
||
CHECK(ncm_cache_clear(&g_c) == NCM_OK, "clear rc=0");
|
||
CHECK(!cached_exists("x", ".mp3") && !cached_exists("y", ".mp3") &&
|
||
!cached_exists("cov123", ".jpg") && ncm_cache_total_bytes(&g_c) == 0,
|
||
"all song_* (mp3+cover+tmp) removed; total=0");
|
||
CHECK(ncm_cache_total_bytes(&g_c) == 0, "total=%llu after clear (expect 0)",
|
||
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||
|
||
/* ⑦ 阶段3:播放中的 .tmp / 中断残体 .part 不被 LRU 淘汰,且不计入池占用。
|
||
* 「正在下载播放的文件」= .tmp(写穿缓存态);「上次中断的可续传残体」= .part。
|
||
* 二者在 evict/total_bytes 中被跳过——防正在听的歌被缓存上限挤掉(§5.1)。 */
|
||
printf("== case 7: evict skips .tmp/.part (playing file not evicted; stage3 §5.1) ==\n");
|
||
cleanup();
|
||
ncm_cache_set_limit_gb(&g_c, 1); /* 1GB(int 内,避免既有 gb_to_bytes int 溢出 bug 干扰本用例) */
|
||
make_cached("a", ".mp3", mb(600), 1000); /* 600MB 最旧 */
|
||
make_cached("b", ".mp3", mb(600), 1100); /* 600MB */
|
||
/* 在途 .tmp(边下边播写穿缓存态):song_b.mp3.tmp */
|
||
{
|
||
char p[600];
|
||
ncm_cache_path(&g_c, "b", ".mp3", p, sizeof p);
|
||
strncat(p, ".tmp", sizeof p - strlen(p) - 1);
|
||
int fd = open(p, O_WRONLY | O_CREAT, 0644);
|
||
if (ftruncate(fd, (off_t)mb(300)) != 0) printf(" truncate tmp err\n");
|
||
close(fd);
|
||
}
|
||
/* 中断残体 .part(可续传):song_d.mp3.part */
|
||
{
|
||
char p[600];
|
||
ncm_cache_path(&g_c, "d", ".mp3", p, sizeof p);
|
||
strncat(p, ".part", sizeof p - strlen(p) - 1);
|
||
int fd = open(p, O_WRONLY | O_CREAT, 0644);
|
||
if (ftruncate(fd, (off_t)mb(300)) != 0) printf(" truncate part err\n");
|
||
close(fd);
|
||
}
|
||
CHECK(ncm_cache_total_bytes(&g_c) == mb(1200),
|
||
"total excludes .tmp/.part (=%lluMB, expect 1200MB)",
|
||
(unsigned long long)(ncm_cache_total_bytes(&g_c) / (1024*1024)));
|
||
ncm_cache_evict(&g_c);
|
||
CHECK(!cached_exists("a", ".mp3"), "oldest committed a evicted");
|
||
CHECK(cached_exists("b", ".mp3"), "b kept (<=limit)");
|
||
/* .tmp/.part 文件仍在(未被打包进 LRU 淘汰) */
|
||
{
|
||
char tp[600], pp[600];
|
||
ncm_cache_path(&g_c, "b", ".mp3", tp, sizeof tp); strncat(tp, ".tmp", sizeof tp-strlen(tp)-1);
|
||
ncm_cache_path(&g_c, "d", ".mp3", pp, sizeof pp); strncat(pp, ".part", sizeof pp-strlen(pp)-1);
|
||
struct stat st;
|
||
CHECK(stat(tp, &st) == 0, "playing .tmp survives evict");
|
||
CHECK(stat(pp, &st) == 0, "resume .part survives evict");
|
||
}
|
||
CHECK(ncm_cache_total_bytes(&g_c) == mb(600),
|
||
"total after evict excludes partials (=%lluMB, expect 600MB)",
|
||
(unsigned long long)(ncm_cache_total_bytes(&g_c) / (1024*1024)));
|
||
|
||
/* ⑧ 阶段3:部分文件 API 快检——part_size 检测 / open_resume 追加 / abort_part 保留 /
|
||
* abort 删除 的命名契约(供 player_manager 续传链路)。 */
|
||
printf("== case 8: .part resume API contract (stage3 §5.2) ==\n");
|
||
cleanup();
|
||
{
|
||
/* 用 ncm_cache_open_resume 打开上一步遗留 .part 无(已 cleanup)——先造一个真 .part */
|
||
char p[600];
|
||
ncm_cache_path(&g_c, "r", ".mp3", p, sizeof p);
|
||
snprintf(p + strlen(p), sizeof p - strlen(p), ".part");
|
||
int fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { (void)!write(fd, "ABCDEF", 6); close(fd); }
|
||
uint64_t sz = 0;
|
||
CHECK(ncm_cache_part_size(&g_c, "r", ".mp3", &sz) == NCM_OK && sz == 6,
|
||
"part_size reports 6 (got %llu)", (unsigned long long)sz);
|
||
ncm_cache_writer_t *w = NULL;
|
||
CHECK(ncm_cache_open_resume(&g_c, "r", ".mp3", &w) == NCM_OK, "open_resume ok");
|
||
CHECK(ncm_cache_write(w, (const unsigned char *)"GH", 2) == NCM_OK, "resume append write");
|
||
CHECK(ncm_cache_commit(w) == NCM_OK, "resume commit (rename .part->final)");
|
||
/* commit 后 .part 转正:lookup 命中且内容=ABCDEFGH,.part 消失 */
|
||
char path[600];
|
||
CHECK(ncm_cache_lookup(&g_c, "r", ".mp3", path, sizeof path) == NCM_OK, "resume commit lookup hit");
|
||
FILE *f = fopen(path, "rb");
|
||
char buf[16] = {0}; int n = 0;
|
||
if (f) { n = (int)fread(buf, 1, sizeof buf - 1, f); fclose(f); }
|
||
CHECK(n == 8 && memcmp(buf, "ABCDEFGH", 8) == 0, "committed bytes=%.8s (expect ABCDEFGH)", buf);
|
||
CHECK(ncm_cache_part_size(&g_c, "r", ".mp3", &sz) == NCM_ERR_NOENT, ".part gone after commit");
|
||
/* abort_part 保留残体 / abort 删除:造两个 .part 分别走两路 */
|
||
ncm_cache_path(&g_c, "s", ".mp3", p, sizeof p);
|
||
snprintf(p + strlen(p), sizeof p - strlen(p), ".part");
|
||
fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { (void)!write(fd, "1234", 4); close(fd); }
|
||
CHECK(ncm_cache_open_resume(&g_c, "s", ".mp3", &w) == NCM_OK, "s open_resume");
|
||
CHECK(ncm_cache_abort_part(w) == NCM_OK, "s abort_part");
|
||
struct stat st2;
|
||
CHECK(stat(p, &st2) == 0, "abort_part keeps .part (resumable)");
|
||
CHECK(ncm_cache_open_resume(&g_c, "s", ".mp3", &w) == NCM_OK, "s reopen_resume");
|
||
CHECK(ncm_cache_abort(w) == NCM_OK, "s abort (delete)");
|
||
CHECK(stat(p, &st2) != 0, "abort deletes .part");
|
||
/* part_remove 显式删除 */
|
||
fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { (void)!write(fd, "x", 1); close(fd); }
|
||
CHECK(ncm_cache_part_remove(&g_c, "s", ".mp3") == NCM_OK, "part_remove ok");
|
||
CHECK(stat(p, &st2) != 0, "part_remove deleted .part");
|
||
}
|
||
|
||
/* ⑨ §26 孤儿半成品回收:LRU 扫描顺手续删超过 7 天的 .part/.tmp(用户不再点该曲、不续传),
|
||
* 新鲜的在途/刚中断残体保留(写端 mtime 持续更新,不会被误删)。 */
|
||
printf("== case 9: orphan .part/.tmp older than 7d swept by LRU scan (stage3 §26.4) ==\n");
|
||
cleanup();
|
||
{
|
||
char op[600], ot[600], lp[600];
|
||
int64_t now = ncm_now_ms() / 1000;
|
||
int64_t old = now - 8 * 86400; /* 8 天前 */
|
||
struct stat st;
|
||
struct timespec ts[2];
|
||
ts[0].tv_sec = old; ts[0].tv_nsec = 0; ts[1].tv_sec = old; ts[1].tv_nsec = 0;
|
||
|
||
/* 8 天前的 .part:应被 evict 扫描顺手删 */
|
||
ncm_cache_path(&g_c, "orph", ".mp3", op, sizeof op);
|
||
snprintf(op + strlen(op), sizeof op - strlen(op), ".part");
|
||
int fd = open(op, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { (void)!write(fd, "old", 3); close(fd); }
|
||
utimensat(AT_FDCWD, op, ts, 0);
|
||
|
||
/* 8 天前的 .tmp:也应被删 */
|
||
ncm_cache_path(&g_c, "orcht", ".mp3", ot, sizeof ot);
|
||
snprintf(ot + strlen(ot), sizeof ot - strlen(ot), ".tmp");
|
||
fd = open(ot, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { (void)!write(fd, "tm", 2); close(fd); }
|
||
utimensat(AT_FDCWD, ot, ts, 0);
|
||
|
||
/* 新鲜的 .part(现在):应保留(可能正在进行/刚中断,mtime 新) */
|
||
ncm_cache_path(&g_c, "live", ".mp3", lp, sizeof lp);
|
||
snprintf(lp + strlen(lp), sizeof lp - strlen(lp), ".part");
|
||
fd = open(lp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { (void)!write(fd, "live", 4); close(fd); }
|
||
|
||
ncm_cache_evict(&g_c);
|
||
CHECK(stat(op, &st) != 0, "8-day .part swept by evict scan");
|
||
CHECK(stat(ot, &st) != 0, "8-day .tmp swept by evict scan");
|
||
CHECK(stat(lp, &st) == 0, "fresh .part kept (inflight/resumable)");
|
||
}
|
||
|
||
/* ⑩ §NEW 缓存分目录:ncm_cache_init 一次性把根混放的 song_*、cov*、cover_current.jpg 迁入
|
||
* songs//covers/(幂等),迁移后 lookup/total 照常在 songs/ 工作(覆盖场景 1)。 */
|
||
printf("== case 10: init legacy migration songs//covers/ one-shot ==\n");
|
||
cleanup();
|
||
{
|
||
char p[900], sp[900], path[900];
|
||
struct stat st;
|
||
/* 根放旧布局混血文件:歌曲(含双点畸形/在途 .tmp)+ 封面条目 + cover_current */
|
||
snprintf(p, sizeof p, "%s/song_legacy..mp3", g_dir);
|
||
FILE *f = fopen(p, "wb"); if (f) { fputs("OLD1", f); fclose(f); }
|
||
snprintf(p, sizeof p, "%s/song_legacy2.mp3", g_dir);
|
||
f = fopen(p, "wb"); if (f) { fputs("OLD2", f); fclose(f); }
|
||
snprintf(p, sizeof p, "%s/song_legacy3.mp3.tmp", g_dir); /* 在途 .tmp 随迁 */
|
||
f = fopen(p, "wb"); if (f) { fputs("TMP!", f); fclose(f); }
|
||
snprintf(p, sizeof p, "%s/song_covDEAD.jpg", g_dir);
|
||
f = fopen(p, "wb"); if (f) { fputs("COV", f); fclose(f); }
|
||
snprintf(p, sizeof p, "%s/cover_current.jpg", g_dir);
|
||
f = fopen(p, "wb"); if (f) { fputs("CUR", f); fclose(f); }
|
||
|
||
/* 重 init 触发迁移(memset 复位 + mkdir + 迁移);注意 limit 被复位到默认 10GB。 */
|
||
ncm_cache_init(&g_c, g_dir);
|
||
ncm_cache_set_limit_gb(&g_c, 1);
|
||
CHECK(g_c.legacy_migrated == 5, "legacy_migrated=%d (expect 5)", g_c.legacy_migrated);
|
||
|
||
snprintf(sp, sizeof sp, "%s/songs/song_legacy..mp3", g_dir);
|
||
CHECK(stat(sp, &st) == 0, "double-dot song moved into songs/");
|
||
snprintf(sp, sizeof sp, "%s/songs/song_legacy2.mp3", g_dir);
|
||
CHECK(stat(sp, &st) == 0, "song moved into songs/");
|
||
snprintf(sp, sizeof sp, "%s/songs/song_legacy3.mp3.tmp", g_dir);
|
||
CHECK(stat(sp, &st) == 0, "in-flight .tmp moved into songs/");
|
||
snprintf(sp, sizeof sp, "%s/covers/song_covDEAD.jpg", g_dir);
|
||
CHECK(stat(sp, &st) == 0, "cover cache entry moved into covers/");
|
||
snprintf(sp, sizeof sp, "%s/covers/cover_current.jpg", g_dir);
|
||
CHECK(stat(sp, &st) == 0, "cover_current.jpg moved into covers/");
|
||
snprintf(sp, sizeof sp, "%s/song_legacy2.mp3", g_dir);
|
||
CHECK(stat(sp, &st) != 0, "root song_* gone after migration");
|
||
|
||
/* 双点存量在 songs/ 内被 lookup 修成单点(不重下) */
|
||
CHECK(ncm_cache_lookup(&g_c, "legacy", "mp3", path, sizeof path) == NCM_OK,
|
||
"migrated double-dot lookup hit");
|
||
CHECK(strstr(path, "song_legacy.mp3") != NULL && strstr(path, "..") == NULL,
|
||
"double-dot fixed to single-dot (%s)", path);
|
||
/* total 只计 songs/(OLD1 4B + OLD2 4B;.tmp 半成品/封面不在 LRU 池) */
|
||
CHECK(ncm_cache_total_bytes(&g_c) == 8,
|
||
"total counts songs only (=%llu, expect 8)",
|
||
(unsigned long long)ncm_cache_total_bytes(&g_c));
|
||
}
|
||
|
||
cleanup();
|
||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||
return g_fail ? 1 : 0;
|
||
}
|