/* bench/cover_chain_sim.c — 封面链离线回归(0 真实请求,REPORT §18)。 * * 驱动真实 app/logic/cover.c(worker 线程/ncm_cache LRU/cover_current.jpg 落盘), * HTTP 端点为本地合成 stub(按 URL 生成可区分内容,可注入失败次数模拟慢/坏网络), * meow_img 为假句柄(嵌内容哈希,不真解码 JPEG——测的是链路逻辑不是解码器)。 * * 用例: * T1 首次请求 → 封面出现(内容哈希对得上 URL); * T2 定居后 worker **不忙等**:cover_changed() 在 1.2s 内不得再置位 * (§18 前:成功后不清目标 → 每圈解码+换入+g_gen++ 死循环,CPU 占满且播放页每帧 * 整盘重绘——本用例在旧代码上必 FAIL); * T3 切歌换 URL → 封面换入新内容 + cover_current.jpg 同步更新(launcher 媒体卡片读它); * T4 重复请求同一 URL → 去重(0 新增 HTTP,0 换入); * T5 网络失败 ×2 后退避重试成功(不无声死等); * T6 连续失败 ≥8 次放弃(目标清除,不永久占 worker); * T7 空/NULL URL 忽略;cover_cache_reset 清空。 * * 编译(宿主,ncm_app 目录下;weak cover_put 兼容 §18 前的 cover.c 做对照): * gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -Ivendor/jsmn -c bench/cover_chain_sim.c -o /tmp/cs.o * gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -Ivendor/jsmn -c app/logic/cover.c -o /tmp/cs_cover.o * gcc -O2 -ffunction-sections -DJSMN_HEADER -Icore -c core/ncm_cache.c -o /tmp/cs_cache.o * gcc -O2 -ffunction-sections -DJSMN_HEADER -Icore -c core/ncm_store.c -o /tmp/cs_store.o * gcc -O2 -Wl,--gc-sections -o cover_chain_sim /tmp/cs.o /tmp/cs_cover.o /tmp/cs_cache.o \ * /tmp/cs_store.o -lpthread * 运行:./cover_chain_sim (0 网络;PASS/FAIL 退出码) */ #include "../app/ncm_app_internal.h" #include #include #include #include #include #include ncm_app_state_t g_st; volatile int g_app_quit = 0; int g_host_mock = 0; /* ---- 公共 stub ---- */ 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; } static int g_log_warns; static char g_last_log[512]; /* 最近一条日志文本(webp/403 分诊日志断言用) */ void meow_log(meow_log_level lvl, const char *fmt, ...) { if (lvl >= MEOW_LOG_WARN) g_log_warns++; va_list ap; va_start(ap, fmt); vsnprintf(g_last_log, sizeof g_last_log, fmt, ap); va_end(ap); } /* ---- 假 meow_img:嵌内容 FNV 哈希(链路验证用,不真解码) ---- */ struct meow_img { unsigned long hash; int len; int freed; }; static unsigned long fnv1a(const unsigned char *p, size_t n) { unsigned long h = 1469598103934665603UL; for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 1099511628211UL; } return h; } meow_img *meow_img_load_mem(const void *buf, int len) { if (!buf || len <= 0) return NULL; meow_img *im = (meow_img *)malloc(sizeof *im); if (!im) return NULL; im->hash = fnv1a((const unsigned char *)buf, (size_t)len); im->len = len; im->freed = 0; return im; } /* §30 起 cover.c 用 meow_img_load_file(从 ncm_cache 写者临时文件解码):读文件内容哈希。 * 与 url_hash() 的期望值对齐(同内容同哈希)。 */ meow_img *meow_img_load_file(const char *path) { FILE *f = fopen(path, "rb"); if (!f) return NULL; unsigned char buf[4096]; size_t n = fread(buf, 1, sizeof buf, f); fclose(f); if (!n) return NULL; meow_img *im = (meow_img *)malloc(sizeof *im); if (!im) return NULL; im->hash = fnv1a(buf, n); im->len = (int)n; im->freed = 0; return im; } /* meow_img_downscale 桩:不缩放(返回 NULL)→ cover_cap 保留原图(测链逻辑,非缩放器)。 */ meow_img *meow_img_downscale(meow_img *im, int maxdim) { (void)im; (void)maxdim; return NULL; } void meow_img_free(meow_img *im) { if (!im) return; im->freed = 1; free(im); } /* §18 前的 cover.c 无 cover_put:weak 空实现让同一 sim 可对照旧代码(新代码强符号覆盖)。 */ __attribute__((weak)) void cover_put(void) { } /* ---- HTTP stub:内容 = djb2(url) 填充 4KB(每 URL 唯一);可注入失败/延迟/webp/403 ---- */ static int g_http_calls; static int g_fail_next; /* 剩余注入失败次数 */ static int g_delay_ms; /* 模拟慢网络 */ static int g_webp_next; /* 下一次下载返回 WebP 魔数体(200,测「200 解码失败」三分型③) */ static int g_status_next; /* 下一次下载强制返回该 HTTP status(测 403 防盗链;0=200) */ static unsigned long djb2(const char *s) { unsigned long h = 5381; while (*s) h = ((h << 5) + h) + (unsigned char)*s++; return h; } int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp) { g_http_calls++; if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; } if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000); size_t n = 4096 < buf_sz ? 4096 : buf_sz; unsigned long h = djb2(req->url); for (size_t i = 0; i < n; i++) buf[i] = (char)((h >> ((i % 8) * 8)) ^ (unsigned char)i); memset(resp, 0, sizeof *resp); resp->status = 200; resp->body = buf; resp->body_len = n; resp->content_length = n; return NCM_OK; } /* 流式下载桩:cover.c 现走 ncm_http_download(§30 改流式写文件,非旧 ncm_http_request)。 * 与 ncm_http_request 同语义:内容 = djb2(req->url) 填充 4KB,经写回调投递(落入 ncm_cache * 写者临时文件);可注入失败/延迟。 */ int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx, size_t max_bytes, ncm_http_resp_t *resp) { (void)max_bytes; g_http_calls++; if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; } if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000); unsigned char buf[4096]; size_t n = sizeof buf; unsigned long h = djb2(req->url); for (size_t i = 0; i < n; i++) buf[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i); /* webp 注入:前 12 字节置 RIFF....WEBP 魔数(stb 不支持 → cover.c 判 webp=1 不解码) */ if (g_webp_next) { g_webp_next = 0; buf[0]='R';buf[1]='I';buf[2]='F';buf[3]='F'; buf[4]=0;buf[5]=0;buf[6]=0;buf[7]=0; buf[8]='W';buf[9]='E';buf[10]='B';buf[11]='P'; } memset(resp, 0, sizeof *resp); resp->status = g_status_next ? g_status_next : 200; g_status_next = 0; resp->body = (const char *)buf; resp->body_len = n; resp->content_length = n; if (cb && cb(ctx, buf, n) != 0) return NCM_ERR_IO; return NCM_OK; } /* URL 期望内容哈希(与 stub 生成式一致)。cover.c 现对封面 URL 追加 ?param= * 缩略后缀(ncm_app_internal.h,REPORT §NEW 任务2)——实测请求的是参数版 URL,故期望值须按 * 同规则构造,否则「断言 raw URL」与「实测 param URL」哈希错位。已含 param= 不叠。 */ static unsigned long url_hash(const char *url) { char n[512]; snprintf(n, sizeof n, "%s", url); #if NCM_COVER_USE_PARAM if (!strstr(n, "param=")) { size_t l = strlen(n), add = strlen("?param=") + strlen(NCM_COVER_PARAM); if (l + add + 1 < sizeof n) { strcat(n, strchr(n, '?') ? "¶m=" : "?param="); strcat(n, NCM_COVER_PARAM); } } #endif unsigned char tmp[4096]; unsigned long h = djb2(n); for (size_t i = 0; i < sizeof tmp; i++) tmp[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i); return fnv1a(tmp, sizeof tmp); } 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) /* 轮询等待封面内容变成 want_hash(≤ ms)。返回 1=达成。 */ static int wait_cover_hash(unsigned long want, int ms) { for (int t = 0; t < ms; t += 20) { meow_img *im = cover_get(); unsigned long h = im ? im->hash : 0; cover_put(); if (h == want) return 1; usleep(20 * 1000); } return 0; } static unsigned long cur_cover_hash(void) { meow_img *im = cover_get(); unsigned long h = im ? im->hash : 0; cover_put(); return h; } /* 读 cover_current.jpg 并哈希(验证 launcher 媒体卡片读的文件同步更新)。 */ static unsigned long curjpg_hash(void) { char path[600]; snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir); FILE *f = fopen(path, "rb"); if (!f) return 0; unsigned char b[4096]; size_t n = fread(b, 1, sizeof b, f); fclose(f); return n ? fnv1a(b, n) : 0; } #define URL_A "http://p1.music.126.net/albumA/xxx.jpg" #define URL_B "http://p1.music.126.net/albumB/yyy.jpg" #define URL_C "http://p1.music.126.net/albumC/zzz.jpg" #define URL_D "http://p1.music.126.net/albumD/www.jpg" #define URL_E "http://p1.music.126.net/albumE/vvv.jpg" /* webp 注入用(未缓存) */ #define URL_F "http://p1.music.126.net/albumF/uuu.jpg" /* 403 注入用(未缓存) */ int main(void) { if (system("rm -rf /tmp/ncm_cover_sim") != 0) { printf("FAIL rm /tmp cache\n"); return 1; } memset(&g_st, 0, sizeof g_st); snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_cover_sim"); ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir); printf("== T1: first request -> cover appears ==\n"); cover_request(URL_A); CHECK(wait_cover_hash(url_hash(URL_A), 3000), "cover A shown (hash match)"); CHECK(g_http_calls == 1, "http calls=%d (expect 1)", g_http_calls); CHECK(curjpg_hash() == url_hash(URL_A), "cover_current.jpg == A"); printf("== T2: settle -> worker idle (no busy loop) ==\n"); cover_ack(); int churn = 0; for (int t = 0; t < 1200; t += 10) { if (cover_changed()) churn++; usleep(10 * 1000); } CHECK(churn == 0, "cover_changed churn in 1.2s = %d (expect 0; §18 前=忙等死循环)", churn); CHECK(g_http_calls == 1, "http calls=%d (still 1)", g_http_calls); printf("== T3: switch song -> cover switches + cover_current.jpg updated ==\n"); cover_request(URL_B); CHECK(wait_cover_hash(url_hash(URL_B), 3000), "cover switched to B"); CHECK(curjpg_hash() == url_hash(URL_B), "cover_current.jpg == B"); CHECK(g_http_calls == 2, "http calls=%d (expect 2)", g_http_calls); cover_ack(); usleep(400 * 1000); CHECK(!cover_changed(), "no churn after switch"); printf("== T4: re-request same url -> dedup ==\n"); cover_request(URL_B); usleep(500 * 1000); CHECK(g_http_calls == 2, "http calls=%d (still 2, dedup)", g_http_calls); printf("== T5: flaky network x2 -> backoff retry succeeds ==\n"); g_fail_next = 2; cover_request(URL_C); CHECK(wait_cover_hash(url_hash(URL_C), 8000), "cover C after retries"); CHECK(g_http_calls >= 5, "http calls=%d (>=5: 2 fails + 1 ok for C)", g_http_calls); CHECK(g_log_warns >= 2, "warn logs=%d (failures logged, 不无声)", g_log_warns); printf("== T6: permanent failure -> give up after %d tries ==\n", 8); g_fail_next = 100; int before = g_http_calls; cover_request(URL_D); usleep(25 * 1000 * 1000 / 10); /* ~2.5s 不够 8 次退避(0.5+1+2+4*5=23.5s),分段等 */ for (int t = 0; t < 30 && g_http_calls - before < 8; t++) usleep(1000 * 1000); int tries = g_http_calls - before; CHECK(tries == 8, "give up after tries=%d (expect 8)", tries); CHECK(cur_cover_hash() == url_hash(URL_C), "old cover C retained after give-up"); usleep(600 * 1000); CHECK(g_http_calls - before == 8, "no further retries after give-up (calls=%d)", g_http_calls - before); printf("== T8: webp body (200) -> unsupported, clear target, no blind retry ==\n"); g_fail_next = 0; int b8 = g_http_calls; g_webp_next = 1; cover_request(URL_E); usleep(800 * 1000); /* 等 worker 处理 webp 体 → 判 webp → 清目标 → 转怠速 */ CHECK(g_http_calls == b8 + 1, "webp: single download=%d (no retry)", g_http_calls - b8); CHECK(cur_cover_hash() == url_hash(URL_C), "webp: 旧封面 C 保留(兜底,不误换)"); CHECK(strstr(g_last_log, "webp unsupported") != NULL, "log: 'webp unsupported' seen"); printf("== T9: HTTP 403 anti-hotlink -> single download, no retry, distinct log ==\n"); int b9 = g_http_calls; g_status_next = 403; cover_request(URL_F); usleep(800 * 1000); CHECK(g_http_calls == b9 + 1, "403: single download=%d (no retry)", g_http_calls - b9); CHECK(cur_cover_hash() == url_hash(URL_C), "403: 旧封面 C 保留(兜底)"); CHECK(strstr(g_last_log, "403") != NULL, "log: '403' seen"); g_status_next = 0; printf("== T7: null/empty + reset ==\n"); int b7 = g_http_calls; cover_request(NULL); cover_request(""); CHECK(g_http_calls == b7, "null/empty ignored (calls=%d)", g_http_calls); cover_cache_reset(); CHECK(cur_cover_hash() == 0, "reset -> cover cleared"); g_fail_next = 0; printf(g_fail ? "== COVER CHAIN SIM: FAIL ==\n" : "== COVER CHAIN SIM: ALL PASS ==\n"); return g_fail; }