Files
AskaEth de18cef02c feat(ncm): 缓存分目录(songs//covers/)+ 陈旧 mp3 缓存修复
任务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)。
2026-09-02 13:51:36 +08:00

394 lines
19 KiB
C
Raw Permalink 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.
/* 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;
}